计算机专业:老师也许会跳过的 C 语言基础知识,赶紧补上一课
对于理工科的大一新生来说,C 语言是个绕不开的坎。由于在进入大学前,许多人都完全没有接触过编程相关的内容,导致对于这门课的接受能力普遍偏低,学起来也非常费劲。这里就总结一些可能在课堂上老师不会详细讲解,但是对于理解 C 语言个人感觉比较重要的一些内容,供大家参考讨论。
首先我们来讨论一些基础的内容。由于学校教学时长是有限的,每节课的时间也比较短,因此在进入具体教学前的绪论环节,并不会花费过多的笔墨。很多时候,甚至只会告诉同学如何安装 IDE、如何新建文件、保存、编译并运行,但不会告诉同学们为什么要这么做,每一步背后到底都干了些什么。要搞清楚这些问题,首先需要知道一些编程语言的基本知识,我们从分类讲起。
编程语言的分类其实有很多种分类方法,首先可以将其分为高级语言与低级语言,而高级语言之中又有着非常多的种类,这些将在下面进行介绍。
首先按照高级低级来区分,可以分为机器语言(汇编语言)和其他。高级语言的分类较多,这边先简单聊一聊低级语言。
机器语言仅由 0 和 1 组成,是计算机硬件能够直接理解的语言。不同的架构——如我们熟知的 ARM,x86,RISC-V 等架构——都有着不同的机器语言。机器语言能够直接操作处理器,其操作码在计算机内都有着对应的电路来完成。
汇编语言是在机器语言的基础上诞生的一种语言,每一条语句都与机器语言中的操作码一一对应,能够直接翻译到机器语言。其存在的意义就是能够方便程序员理解。
上图:汇编语言与机器语言对比
举一个简单的例子:如果想要让两个数进行加法运算,例如计算 2+3 ,那么使用机器语言可能就是 00000011 00000010 00000011 (这里为了方便加入了空格,实际的机器语言中不存在空格,只有 0 和 1 ,且该机器码为个人杜撰,仅作为例子使用,并非某一架构实际所使用的机器码),而用汇编语言写起来则是 ADD 2, 3 这样较为便于理解的方式。当然,汇编语言可不止这么简单,不过由于本文并非主要介绍汇编语言,因此就不进行深入讨论了。
尽管如此,汇编语言依然非常复杂,而且限制颇多,一旦需要写一些复杂功能,或是算法运算,很容易写完后连自己都读不懂。我在学习微机原理时,写过一个课程项目,要求是用汇编为 89C51 单片机写一段摇一摇计数的代码,剔除驱动 LCD 屏的代码,总共不过四五百行,却又写了近三百行注释以便理解。虽然其中有我对于汇编不够熟练的缘故,但其繁复程度可见一斑。
由此可见,显然不太可能用机器语言或是汇编语言来进行复杂代码的编写。这时高级语言就应运而生。下面就简单介绍一下高级语言以及其分类。
接下来介绍通过语言的翻译方式来进行的分类,以三种目前非常流行的语言为例,分别为 C 语言,Python 以及 Java。
由于所有写的代码最终都会变成机器语言才能执行,因此不同的语言最终也会殊途同归,翻译回汇编和机器语言,只是不同类型的语言翻译的方式不同而已。这边首先介绍 C 语言为代表的编译型语言。
编译型语言,顾名思义,就是通过编译将代码翻译到机器语言,再进行执行,因此执行前会首先将代码进行编译,这一步在老师教学的时候,会告诉同学们必须要先点击编译再点击执行,或点击编译并执行,其原因就在这里。编译会调用现成的编译器对代码进行分析,优化,处理,其中的过程在这里由于篇幅原因也不过多赘述了,总之最后会将所写的 .c 代码编译为以 .exe(Windows 下)或 .out (macOS 下)结尾可执行文件。
C 语言编译前
C 语言编译后
这里可以控制编译器生成汇编语言文件,可以看一下两者的差距
上图:C 语言与对应汇编语言
显然 C 语言的版本更容易理解。
编译型语言虽然在会在执行前进行分析优化,运行起来速度也非常快,但对于大型程序来说,编译耗时也会非常长。那么能否不进行编译而直接运行呢?答案显然是可以的,这就是解释型语言,如 Python。
对于解释型语言,将不会使用编译器进行翻译,最终生成机器语言的可执行文件,再进行执行。它会调用解释器,逐行翻译源文件,将每一行实时翻译到机器码并执行。如此一来,就不需要进行编译,执行前的准备时间大大减少。但是由于解释器并不会对代码进行优化,而且每次运行时都需要从头解释一遍,导致执行效率不如编译型语言。
编译型语言还存在另一个问题。根据前文所提到的,每种架构都有独特的机器语言,而编译的过程就是将代码翻译为机器语言的过程,这就导致每次编译生成的文件都只能在特定平台上运行。那能不能做到一次编译,就能在全平台运行呢?显然这也是可以的。这就是混合型语言,如 Java。
这类语言同样需要编译,但是编译后生成的并非机器码,而是字节码。通常这类语言在运行时会再转换成机器码执行,或直接由虚拟机解释执行。由于编译到字节码而非机器码,因而编译得到的执行文件是全平台通用的。
许多同学在学习 C 语言的时候可能会疑惑,数据类型到底有什么用?要理解这个问题,我们先来看看数据是怎么存储的。
在内存中,所有数据都会被以二进制进行存储,即 01001001 等形式。这些数据仅仅只是 0 和 1 而已,所表达的意义都是人为规定的。
通常,第一位会被视为符号位,即 0 位正, 1 为负。然而,如果我希望第二位来表示符号位,也完全是符合规定的,只是所有涉及到运算的代码都要重写罢了。而数据类型就是用来规定每一位所代表的意义。
举个例子,在 32 位系统中,对于 int 类型而言,第一位表示符号,后 31 位表示具体的值。而对于 float 类型而言,尽管第一位也表示符号,但剩下的 31 位与 int 类型所表示的意义就不同了。紧接着的 8 位是指数位,剩下的是尾数,即使用科学计数法表示为 尾数 * (2 ^ 指数) 。这里是 2 的原因是计算机中所有数据都是二进制存储的,而非十进制。
上图:float 类型存储方式
这里用 0 10000010 00010000 00000000 0000000 来展示一下 float 类型的具体计算。理解这段需要会一些简单的进制转换,如果不会建议自学一下。通过二进制计算器,可以很容易得到它对应的十进制数是 1094975488 。
对于 float 类型来说,其指数为 10000010 ,即 129 ,再根据规定减去 127 ,最终得到其指数为 2 。对于尾数而言,由于一定由 1 开头,因此最开始的这位 1 会被省略,因此其尾数实际为 10001000 00000000 00000000 ,即为 1.0001 * (2 ^ 2) ,换算成十进制为 4.25 。
如果希望对这两个类型的数据进行简单的加法运算,而不指定数据类型,汇编中会直接进行对位相加,即对应的每一个 0 或 1 相加,并加上前一位的进位。这样计算会得到 10000010 00010000 00000000 00000000 ,显然不是我们想要的 1094975492.25 。
如果不指定数据类型,计算就会得到错误的答案。由此可见,在内存中无意义的一串二进制数,我们可以通过规定每一位的意义,来得到不一样的结果。
很多同学可能也有这样的疑惑,为什么光一个整数就有 short , int , lang 三种,浮点数也有 float , double 两种,甚至还有与 int 类型对应的 char 类型呢?只需要 int 和 double 不就够用了么?
由于现在的计算机内存普遍充裕,不太会遇到内存空间不足的问题,因此可以直接选用高精度的数据类型进行存储与计算。然而,在多年以前,或是在嵌入式领域,这类存储空间非常紧张的条件下,不同数据类型的差距就显现出来了。
由于在这些条件下,每一个 bit 都显得弥足珍贵,因此程序员会想方设法地优化存储空间的使用,能够用低精度的就不会用高精度。
而浮点数根据上文对存储方式的解释可以看出,精度越高,其所能表示的大小越小,因此在表示较大的,对精度要求高的数据时,就必须使用高精度的数了,反之则可以用低精度的节省空间。由于 float 所能表示的精度实在是非常低,因此建议在学校编写 C 程序时,如无特殊要求,一律使用 double 类型。
而 char 类型则较为特殊,可以与整数类型进行相互转换。在单片机等环境中,由于存储空间有限,因此更倾向于使用 char 这一只消耗一字节的数据类型,而不是 int 等更大的。另外, char 一般用来表示字符,因此如果要表示例如 \’A\’ 这种字符型的数据时,一般用 char 类型。 char 类型在后文有关字符串的部分还会提到。
然而, short 类型不一定就比 int 类型消耗的空间少, long 也不一定就比 int 表示的精度高,一切由编译器决定(只需要遵守 2 <= short <= int <= long 就是符合规定的)。因此如果真的有需要,可以用 char 来降低消耗,而不是使用 short 。
上图:数据类型在不同操作系统下的大小
由此可见,虽然常用的数据类型就这么几个,但是其他的类型也都有其存在的意义,可以不用,但不能没有。数据类型一旦确定,该变量在内存中所分配的大小,以及每一位所代表的意义,也就随之确定下来了。
明白数据类型,接下来就可以定义数组了。一个数组是由一定数量的,相同数据类型的变量组成的一种数据结构,也就是说,一个数组可以由一定数量的其他数组组成,而这些数组也可以由数组组成,形成套娃。
在 C 语言中,数组在定义时必须显式指定其长度与数据类型,而在一些其他语言——如 Java、 Python 中——可以不断扩展数组的长度,但 C 语言中却不能这样做。这又是为什么呢?这需要从如何在内存中生成一个数组说起。
我们在 C 语言中创建数组时,会指定数组的数据类型和长度,而编译器可以根据 数据类型 * sizeof(数据类型) 推算出这一数组具体需要占据多大的内存空间,进而在程序运行到这一步,需要创建数组时,为其在内存中申请符合要求的,连续的一段空间进行数组的生成。但为什么要连续的空间,而不能断断续续呢?
数组在访问时,会首先找到其内存地址。数组在创建时的变量名,实际也是一个指向数组第一项的一个指针(后面会讲到)。随后,根据具体访问哪一项,如第 n 项,就会将这一地址加上 n * sizeof(数据类型) ,就能直接找到这一项的内存地址。因此数组在生成时需要申请连续的内存地址,否则就无法做到这么高效的访问速度。
那么问题来了:为什么别的语言能做到扩展数组长度,通过变量来初始化数组,而不是通过常数来指定呢?
事实上,在最底层的实现中,它们也是会指定一个具体的值来生成数组,其原理与 C 语言完全相同。但是在需要更长的数组时,会申请一段更长的连续内存空间来存放新数组,并将原来的旧数组完全复制一份过去。当然,各种语言会存在一定的优化,申请的空间会比所需的空间略大一些,防止重复不断的复制降低运行效率。
上图:数组动态扩容
由于 C 语言的数组是最原始的数组,语言本身不会自行进行申请新地址,复制旧数组等操作,因此需要在初始化时就指定好长度。
另外一个初学时难以理解的概念就是指针了。 先来看下指针到底是什么。指针是一个存放内存地址的变量,也就是说可以直接访问并操作内存。
上图:指针示意图
图中 a 表示一个整数类型的变量,值为 100,在内存中存放在 0x0010 这一地址中。因此可以定义一个指针 x 指向这一地址。可以理解为 x 中存放了 0x0010 这一地址,访问这一指针就相当于访问这一地址。这就引出了一个问题:既然指针存放的是地址,访问的也是地址,那么为什么还要定义一个类型呢?
原因很简单,因为要取出该地址具体存放的值。前面说过,数据类型决定了该数据所占的大小,以及每一位具体所代表的内容。因此,要取出该地址具体存放的值,必须要知道其数据类型才行。这就是为什么 C 语言中定义指针时要指定数据类型,指明该内存地址存放数据的具体数据类型。
指针与数组的关系也非常紧密。定义数组时取的名称就是指向数组第一个元素的指针,也就是说,要访问数组 a 中的 a[0] ,可以直接访问 *a 。以此类推,可以通过访问 *(a+1), *(a+2)来访问 a[1], a[2] 。这是因为在定义数组时已经指定了数据类型,因此这里的 +1 就不是简单的加法,而是在指针存储的地址的基础上,加上 sizeof(a[0]) (这里的 sizeof 用来获取某一变量在存放时使用的内存大小)。从上图可以看出,每个 int 类型如果占了 4 个字节,那么每次 +1 都会将内存地址 +4 再访问。
需要注意的是,通过这种方式访问数组会有数组越界的问题。也就是说,如果定义了一个长为 n 的数组,但是通过 *(a+n) 来访问第 n+1 位,C 语言并不会有任何的错误提示,只会返回一个存储在该内存地址的,根据定义的数据类型来计算得出的值。很多情况下无法分辨到底是否越界,因此使用这种方式访问需要小心谨慎。
另外,虽然数组名是一个指针,但是是一个常量,因此不能给其赋值。
之前提到, char 类型多用于表示字符。字符串是由字符组成的,其底层是一串由 char 类型的变量组成的数组,因此可以通过 char* 或是 char[] 来生成字符串。赋值时,可以通过数组一个一个字符赋值,也可以通过双引号直接赋值。
在一些其他编程语言中,会专门有一个数据类型 String 来表示字符串,但在 C 语言中并没有。因此对字符串的处理就等价于对字符数组的处理。
在处理字符串时需要注意,数组长度是包含最后的 \\0 的,而 strlen 函数则不会。另外,如果通过数组的方式一个个添加字符,且在最后没有加上 \\0 ,那么则由于数组越界进而使得字符串中的数据出现错误。为了防止出现这一错误,最好直接通过双引号进行赋值。另外,不论字符数组有多长,第一次出现 \\0 就代表着字符串的结束。
由于 char 实际就是一个数字,因此在解决如 大小写转换 之类的问题时,可以通过 +- 32 来解决。这里的 32 来自于 ASCII 码表,每一个数字都对应着一个字符。 码表 可以在网上轻松找到。如果不记得具体的大小,可以通过格式化输出 %d 直接查看对应的数字,如果记不得大小写间差了 32,可以用 \’a\’ – \’A\’ 来临时凑合使用一下。
老师可能不会着重提语法格式,但是实际上良好的格式能够显著提升代码的可读性,方便理解与找错。
首先,根据 C99 标准, main 函数应当定义为 int main(void) {…} 或是 int main(int argc, char *argv[]) {…} 。前一种在现在学习的阶段更为常用,其中的 void 一般是可以省略的。但是,最后的 return 0; 是可以被省略的,如果不写将会默认返回 0。有些老师或者书上可能会写成 void main() {…} ,或是说一定要显示地写出 return 0; 。这些都是错误的。具体标准可以参考 标准文档 。
main 函数定义
上图:main 函数返回值
缩进与换行的使用也是很重要的。 { 与 } 应当独占一行,其中所包裹的内容应当进行一次缩进。另外,尽管 if 语句或是 for 语句等,如果大括号内只包含一条语句,很多老师会去掉大括号,并写在一行内。这并不是一个好习惯,应当照样换行,加上大括号与缩进,方便阅读与之后的修改。
可以使用在线格式化,或是 astyle 等格式化软件来进行代码格式化。
除了以上的这些老师可能会一笔带过的内容外,还有一些我在编程中所学到的一些小 tips:
Warning(敲代码是不可忽略的警告)
希望所有看到这篇文章的、需要学习 C 语言的同学们能够顺利学好这门课,取得一个好成绩。
对于准备成为一名优秀程序员的朋友,如果你想更好的提升你的编程核心能力(内功),让自己成为一个具有真材实料的厉害的程序员,不妨从现在开始!C/C++,永不过时的编程语言~
编程学习书籍分享:
编程学习视频分享:
整理分享(多年学习的源码、项目实战视频、项目笔记,基础入门教程)
欢迎转行和学习编程的伙伴,利用更多的资料学习成长比自己琢磨更快哦!
对于C/C++感兴趣可以关注小编在后台私信我:【编程交流】一起来学习哦!可以领取一些C/C++的项目学习视频资料哦!已经设置好了关键词自动回复,自动领取就好了!
学习C语言的7个步骤,对照一下,看你处在哪个阶段?
C是编译型语言。如果之前使用过编译型语言(如,Pascal或FORTRAN),就会很熟悉组建C程序的几个基本步骤。但是,如果以前使用的是解释型语言(如,BASIC)或面向图形界面语言(如,Visual Basic),或者甚至没接触过任何编程语言,就有必要学习如何编译。别担心,这并不复杂。首先,为了让读者对编程有大概的了解,我们把编写C程序的过程分解成7个步骤(见图1.3)。注意,这是理想状态。在实际的使用过程中,尤其是在较大型的项目中,可能要做一些重复的工作,根据下一个步骤的情况来调整或改进上一个步骤。
图1.3 编程的7个步骤
1 第1步:定义程序的目标
在动手写程序之前,要在脑中有清晰的思路。想要程序去做什么首先自己要明确自己想做什么,思考你的程序需要哪些信息,要进行哪些计算和控制,以及程序应该要报告什么信息。在这一步骤中,不涉及具体的计算机语言,应该用一般术语来描述问题。
2 第2步:设计程序
对程序应该完成什么任务有概念性的认识后,就应该考虑如何用程序来完成它。例如,用户界面应该是怎样的?如何组织程序?目标用户是谁?准备花多长时间来完成这个程序?
除此之外,还要决定在程序(还可能是辅助文件)中如何表示数据,以及用什么方法处理数据。学习C语言之初,遇到的问题都很简单,没什么可选的。但是,随着要处理的情况越来越复杂,需要决策和考虑的方面也越来越多。通常,选择一个合适的方式表示信息可以更容易地设计程序和处理数据。
再次强调,应该用一般术语来描述问题,而不是用具体的代码。但是,你的某些决策可能取决于语言的特性。例如,在数据表示方面,C的程序员就比Pascal的程序员有更多选择。
3 第3步:编写代码
设计好程序后,就可以编写代码来实现它。也就是说,把你设计的程序翻译成C语言。这里是真正需要使用C语言的地方。可以把思路写在纸上,但是最终还是要把代码输入计算机。这个过程的机制取决于编程环境,我们稍后会详细介绍一些常见的环境。一般而言,使用文本编辑器创建源代码文件。该文件中内容就是你翻译的C语言代码。程序清单1.1是一个C源代码的示例。
程序清单1.1 C源代码示例
在这一步骤中,应该给自己编写的程序添加文字注释。最简单的方式是使用C的注释工具在源代码中加入对代码的解释。第2章将详细介绍如何在代码中添加注释。
4 第4步:编译
接下来的这一步是编译源代码。再次提醒读者注意,编译的细节取决于编程的环境,我们稍后马上介绍一些常见的编程环境。现在,先从概念的角度讲解编译发生了什么事情。
前面介绍过,编译器是把源代码转换成可执行代码的程序。可执行代码是用计算机的机器语言表示的代码。这种语言由数字码表示的指令组成。如前所述,不同的计算机使用不同的机器语言方案。C编译器负责把C代码翻译成特定的机器语言。此外,C编译器还将源代码与C库(库中包含大量的标准函数供用户使用,如printf()和scanf())的代码合并成最终的程序(更精确地说,应该是由一个被称为链接器的程序来链接库函数,但是在大多数系统中,编译器运行链接器)。其结果是,生成一个用户可以运行的可执行文件,其中包含着计算机能理解的代码。
编译器还会检查C语言程序是否有效。如果C编译器发现错误,就不生成可执行文件并报错。理解特定编译器报告的错误或警告信息是程序员要掌握的另一项技能。
5 第5步:运行程序
传统上,可执行文件是可运行的程序。在常见环境(包括Windows命令提示符模式、UNIX终端模式和Linux终端模式)中运行程序要输入可执行文件的文件名,而其他环境可能要运行命令(如,在VAX中的VMS[2])或一些其他机制。例如,在Windows和Macintosh提供的集成开发环境(IDE)中,用户可以在IDE中通过选择菜单中的选项或按下特殊键来编辑和执行C程序。最终生成的程序可通过单击或双击文件名或图标直接在操作系统中运行。
6 第6步:测试和调试程序
程序能运行是个好迹象,但有时也可能会出现运行错误。接下来,应该检查程序是否按照你所设计的思路运行。你会发现你的程序中有一些错误,计算机行话叫作bug。查找并修复程序错误的过程叫调试。学习的过程中不可避免会犯错,学习编程也是如此。因此,当你把所学的知识应用于编程时,最好为自己会犯错做好心理准备。随着你越来越老练,你所写的程序中的错误也会越来越不易察觉。
将来犯错的机会很多。你可能会犯基本的设计错误,可能错误地实现了一个好想法,可能忽视了输入检查导致程序瘫痪,可能会把圆括号放错地方,可能误用C语言或打错字,等等。把你将来犯错的地方列出来,这份错误列表应该会很长。
看到这里你可能会有些绝望,但是情况没那么糟。现在的编译器会捕获许多错误,而且自己也可以找到编译器未发现的错误。在学习本书的过程中,我们会给读者提供一些调试的建议。
7 第7步:维护和修改代码
创建完程序后,你发现程序有错,或者想扩展程序的用途,这时就要修改程序。例如,用户输入以Zz开头的姓名时程序出现错误、你想到了一个更好的解决方案、想添加一个更好的新特性,或者要修改程序使其能在不同的计算机系统中运行,等等。如果在编写程序时清楚地做了注释并采用了合理的设计方案,这些事情都很简单。
8 说明
编程并非像描述那样是一个线性的过程。有时,要在不同的步骤之间往复。例如,在写代码时发现之前的设计不切实际,或者想到了一个更好的解决方案,或者等程序运行后,想改变原来的设计思路。对程序做文字注释为今后的修改提供了方便。
许多初学者经常忽略第1步和第2步(定义程序目标和设计程序),直接跳到第3步(编写代码)。刚开始学习时,编写的程序非常简单,完全可以在脑中构思好整个过程。即使写错了,也很容易发现。但是,随着编写的程序越来越庞大、越来越复杂,动脑不动手可不行,而且程序中隐藏的错误也越来越难找。最终,那些跳过前两个步骤的人往往浪费了更多的时间,因为他们写出的程序难看、缺乏条理、让人难以理解。要编写的程序越大越复杂,事先定义和设计程序环节的工作量就越大。
磨刀不误砍柴工,应该养成先规划再动手编写代码的好习惯,用纸和笔记录下程序的目标和设计框架。这样在编写代码的过程中会更加得心应手、条理清晰。
C伴侣推荐
1、C Primer Plus 第6版 中文版
[美] 史蒂芬·普拉达(Stephen Prata) 著,姜佑 译
- 畅销的重量级C语言大百科全书
- C语言从入门到精通的入门经典
- 第五版销售超过250000册
本书详细讲解了C语言的基本概念和编程技巧。
全书共17章。第1章、第2章介绍了C语言编程的预备知识。第3章~第15章详细讲解了C语言的相关知识,包括数据类型、格式化输入/输出、运算符、表达式、语句、循环、字符输入和输出、函数、数组和指针、字符和字符串函数、内存管理、文件输入和输出、结构、位操作等。第16章、第17章介绍C预处理器、C库和高级数据表示。本书以丰富多样的程序为例,讲解C语言的知识要点和注意事项。每章末尾设计了大量复习题和编程练习,帮助读者巩固所学知识和提高实际编程能力。附录给出了各章复习题的参考答案和丰富的参考资料。
2、C Primer Plus 第6版 中文版习题解答
史蒂芬·普拉达(Stephen Prata),曹良亮 编
- 经典畅销图书《C,Primer,Plus》的学习伴侣
- 北京师范大学名师详细剖析所有题目
- 全面提升C编程能力的优选编程练习册
这是经久不衰的C语言畅销经典教程《C Primer Plus(第6版)中文版》的习题精解教程图书。 为了锻炼并提升读者的动手编程能力,《C Primer Plus(第6版)》除了在每章正文中包含了大量短小精悍的实例之外,还在每章末尾提供了颇有深度的综合性的复习题和编程练习题。 本书编者曹良亮博士尝试对这些复习题和编程练习给出了自己的理解和解答。 本书每章开篇先采用思维导图的形式给出了本章涵盖的知识点,然后采用简练的文字梳理了每章涉及的理论内容,最后对复习题和编程练习进行了详细的剖析,最后给出了解题思路和代码实现。 作为《C Primer Plus(第6版)》的理想学习伴侣,本书可以切实提升C语言初学人员的编程技能,掌握C语言编程的精髓,为后期的程序开发之路打下坚实基础。
本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com
文章为作者独立观点不代本网立场,未经允许不得转载。