Linux C 标准程序设计语言
最早的 C 编程语言标准由美国国家标准协会(ANSI)在 1989 年首次发布(C89 版本),后于 1990 年由国际标准化组织(ISO)修订后发布(C90 版本)标准,而后经历了 C99、C11 等一系列主要版本的演进,截止目前最新的版本是 2018 年 10 月发布的 C18 版本。笔者当前使用的 Linux C 编译工具是 2018 年 1 月 25 日释出的GCC 7.3.0版本,提供了 C89/C90 、C99、C11 等一系列 ISO 标准特性的支持。
本文将分为《语法规范》与《应用程序》两个姊妹篇,前者侧重于介绍 Linux C 各个数据类型的存储模型,并概括了函数、条件编译、动态内存管理、位运算、指针等嵌入式 C 程序设计的常用概念。后者将涉及 Linux 文件系统 IO、进程间通信、多线程、网络编程等应用程序开发方面的内容。
Hello Wrold
第一步,我们先来尝试编写一个老派的 Hello World
程序,main
函数是 Linux C
语言程序的执行入口,因此也称为主函数。主函数的argc
参数为整型,用于统计程序执行时传递给主函数的命令行参数的个数。而argv
参数是一个字符串数组,数组中每个元素指向一个命令行输入的执行参数。
不同于 Python、JavaScript
这样的脚本语言,函数内部每条语句尾部的分号;
都不能被省略。代码开头的#include
预处理指令用于包含标准
IO
头文件,从而能够在后续主函数中调用printf()
方法。另外值得注意的是,C
语言代码当中存在/*块注释*/
和//行注释
两种注释风格,但是//
风格在
GCC 的 C89/C90
编译选项下会提示错误信息,开发人员可以根据实际情况酌情使用。
1 |
|
C99 标准规定主函数执行完成之后,需要显式书写
return 0;
语句表示程序正常退出,主函数返回类型的声明也需要显式的设置为int
。
变量、常量、常变量
常量是 Linux C 程序运行时不能改变的量,Linux
当中使用的常量类型有字符型
、整型
、浮点型
等数据类型。
1 |
|
变量是一个具有名称的存储单元,编译系统会自动为变量名分配对应的内存地址。C 程序中的变量都是数据的补码形式进行存储,程序运行时计算机会通过变量名查找对应的内存单元地址,然后通过该地址操作其中保存的变量值。
下面代码当中,声明了一个整型变量date
,并将其赋值为2019
。
1 | int date = 2019; |
常变量具有变量的基本属性,带有数据类型并且占用存储空间;但与常量类似,在程序运行期间不允许修改其值。C99
规范允许使用const
关键字声明一个常量,下面将声明一个常量USER
并赋值为Hank
(通常约定常量名称全部大写)。
1 | const int DATE = 2019; |
常量一旦声明之后就不能再次进行赋值和修改,否则 GCC
编译器将会提示错误信息:error: assignment of read-only variable
,请参考下面的代码:
1 |
|
数据类型
C 语言是强类型语言,ANSI C 当中无论定义变量还是常量都需要事先声明数据类型,编译器将会根据数据类型来为变量和常量分配相应存储空间,不同数据类型具有不同的存储长度与存储方式,C99 标准中常用的数据类型见下表:
注意:红色标注的部分表示的是 C99 标准当中新增的特性。
整型 int
整型数据会以整数补码的方式存储,Keil C51
编译器会分配2个字节共16位空间,而 GCC
编译器则会分配4个字节共32位空间。32
位当中最左边的一位是符号位,该位为0
表示正数,为1
则表示负数。接下来的表格展示了整型数据的存储空间以及取值范围:
数据类型 | 字节数 | 取值范围 |
---|---|---|
int
基本整型 |
4 个字节 | -2147483648 ~ 2147483647 ,即\(-2^{31} \Rightarrow (2^{31}-1)\)。 |
unsigned int
无符号基本整型 |
4 个字节 | 0 ~ 4294967295 ,即\(0 \Rightarrow (2^{32}-1)\)。 |
short
短整型 |
2 个字节 | -32768 ~ 32767 ,即\(-2^{15} \Rightarrow (2^{15}-1)\)。 |
unsigned short
无符号短整型 |
2 个字节 | 0 ~ 65535 ,即\(0 \Rightarrow (2^{16}-1)\)。 |
long
长整型 |
8 个字节 | -9223372036854775808 ~ 9223372036854775807 ,即\(-2^{63} \Rightarrow (2^{63}-1)\)。 |
unsigned long
无符号长整型 |
8 个字节 | 0 ~ 18446744073709551615 ,即\(0 \Rightarrow (2^{64}-1)\)。 |
long long
双长整型 |
8 个字节 | -9223372036854775808 ~ 9223372036854775807 ,即\(-2^{63} \Rightarrow (2^{63}-1)\)。 |
unsigned long long
无符号双长整型 |
8 个字节 | 0 ~ 18446744073709551615 ,即\(0 \Rightarrow (2^{64}-1)\)。 |
注意上面表格当中,无符号类型由于需要有 1 位来作为符号位,因此取值范围计算公式的指数部分需要相应的减去
1
位(例如:\(2^{15}\)、\(2^{31}\)、\(-2^{63}\)),而有符号类型计算公式的指数部分则与该数据类型可存储的字节数相匹配。另外,取值范围计算公式\(2^{n}-1\)中出现的减去1
的情况,是由于正整数一侧的取值范围包含了0
(*虽然数学上0
并非正整数_),因而需要将正整数部分的取值范围相应的减掉一。
为了更加清晰的理解整型数据存储空间分配与取值范围的关系,下面的示意图展示了短整型short
的最大取值32767
、无符号短整型unsigned short
的最大取值65535
的存储空间占用情况:
sizeof()
并非一个函数调用,而是标准 C
语言提供的一个单目操作符,通常称为求字节宽度运算符;其作用是以long unsigned int
数据类型返回当前操作数所占用存储空间的字节大小,因此下面例子的printf()
语句中,格式化字符串需要使用%ld
进行接收。
1 |
|
如果需要使用
printf()
输出中文,源代码文件必须以GB2312编码格式保存。
值得提醒的是,基本整型int
默认是有符号的数据类型,而无符号类型变量原则上不能存放-3
这样的负数;使用printf()
输出无符号整型数据的时候,格式字符串需要选择%u
进行输出。
1 |
|
字符型 char
Linux C
当中字符型数据必须以单引号'c'
进行声明,每个字符型变量只能保存
1 个 ASCII 有效字符,这是由于字符类型实际存储的是该字符的 ASCII
编码,因为 ASCII 字符集编码通常表达为一个整型数据,所以 C99
规范当中也将其视为一种整型数据。下面的表格展示了字符型占用的存储空间以及取值范围:
数据类型 | 字节数 | 取值范围 |
---|---|---|
signed char
有符号字符型 |
1 个字节 | -128 ~ 127 ,即\(-2^{7} \Rightarrow (2^{7}-1)\)。 |
unsigned char
无符号字符型 |
1 个字节 | 0 ~ 255 ,即\(0 \Rightarrow (2^{8}-1)\)。 |
1 |
|
有符号字符型数据允许存储的取值范围在-128 ~ 127
之间,但字符型的
ASCII
编码不可能为负值,因而实际只会使用到0 ~ 127
,即最左侧符号位总是为0
。如果将负整数直接赋值给字符型变量,操作虽然合法但并不代表一个有效字符,而仅仅保存了一个负整数值。接下来的图片展示了保存字符型变量'1'
时的存储情况,由于字符'1'
的
ASCII
码为49
,因此存储器中实质保存的是数字49
的二进制表达形式。
printf()
输出字符型数据时,格式字符串需要选择%c
;如果格式字符串选择为%d
,则会输入该变量的
ASCII 码表达形式。
1 |
|
需要注意,关键字char
前的signed
或者unsigned
是否能够缺省由具体的编译器决定,这一点与int
等其它数据类型不同。在
GCC 编译器当中,char
缺省为有符号类型。
1 |
|
由于char
默认为有符号类型,所以赋值为255
超出了有符号字符类型的表示范围,导致后面打印输出为-1
。如果这里显式声明字符型test
的值为无符号类型unsigned
则能够正确的打印数值255
。
1 | int main() { |
浮点型 float
浮点型用来表示具有小数点的实数,并以规范化的二进制数指数形式存放在存储单元。之所以称为浮点型,是由于实数的指数形式有多种,比如对于3.1416
,可以表示为\(3.14159 × 10^0\)、\(0.314159 × 10^1\)、\(0.0314159 ×
10^2\)等形式,小数点的位置可以自由进行浮动。
数据类型 | 字节数 | 有效数字 | 取值范围(绝对值) |
---|---|---|---|
float
单精度浮点型 |
4 个字节 | 6 | \(0\) 以及 \((1.2×10^{-38}) \Rightarrow (3.4×10^{38})\)。 |
double
双精度浮点型 |
8 个字节 | 15 | \(0\) 以及 \((2.3×10^{-308}) \Rightarrow (1.7×10^{308})\)。 |
long double
长双精度浮点型 |
16 个字节 | 19 | \(0\) 以及 \((3.4×10^{-4932}) \Rightarrow (1.1×10^{4932})\)。 |
为了保持存储结构的一致性,必须将实数转换为规范化的指数形式后再保存至存储单元,即小数点前数字为0
,小数点之后第
1
位数字不为0
,对应于前面例子3.1416
的规范化的指数形式是\(0.314159 ×
10^1\),下图展示了其具体的存储结构,注意小数部分.314159
实际是以二进制形式保存在存储单元中的。
C99 和 C11
标准并未明确定义浮点类型的指数和小数部分各占据总存储单元的多少,具体数值由各个编译器指定。同时,各个编译器对于浮点数据类型所占用的存储空间长度也有所不同,例如long double
类型的长度在
GCC 当中被定义为 16 个字节。
1 |
|
值得注意的是:Linux C
当中对于浮点类型的常量值默认会按照double
类型来进行处理。
1 |
|
基本数据类型转换
自动类型转换
当两个不同的基本数据类型进行算术运算时,所得结果的数据类型总是存储占用空间更大的那一个。比如下面例子中,整型的i
(占用
4 字节存储空间)与字符类型的c
(占用 1
字节存储空间)分别与浮点类型f
相加(占用 4
字节存储空间)时,得到的sizeof
结果总是 4 个字节。
1 |
|
强制类型转换
可以利用强制类型转换将数据转换为需要的数据类型,使用格式为(目标数据类型)表达式
,例如:(double)1
、(double)(3+5)
都会将整型的结果转换为双精度浮点类型。
1 |
|
布尔类型 bool
C++当中存在专门的bool
类型,但是 C89/90
当中没有提供专用的布尔类型,因此通常会在代码中使用基本整型来模拟布尔类型数据。
1 |
|
C99
规范里新增了_Bool
关键字,用于原生定义布尔类型,该类型占用一个字节存储空间,仅拥有0
和1
两个取值。
1 |
|
C99
当中由<stdbool.h>
头文件将_Bool
关键字重新定义为别名bool
,1
和0
分别被定义为了true
和false
,因此引入该头文件后,可以直接使用bool
作为声明布尔数据类型的关键字,使用true
和false
作为布尔类型的取值。
1 |
|
进行数据类型转换的时候,GCC
会将任意非零值自动转换为1
,也就是true
。
1 |
|
复数类型 complex
C99
新增了复数类型的关键字_Complex
,正如同上面提到的_Bool
类型与<stdbool.h>
中bool
的关系一样,包含头文件<complex.h>
之后,就可以方便的使用complex
来代替_Complex
关键字的使用。定义复数类型时,complex
需要与浮点数据类型(float
、double
、long double
)组合起来使用。
1 |
|
GCC 暂不支持 ISO C99 当中定义的虚数类型
_Imaginary
。
数组 array
数组是一组有序数据的集合,数组中每一个元素都是相同的数据类型,并且保存在一个连续的存储空间。C
语言中使用数组必须先声明其长度,以便于事先开辟一个指定大小的存储空间。例如:下图展示了一个包含有
10
个元素的数组int a[10]
,该数组每个元素的存储空间只能用于存放整型数据。
Linux C 当中,无论当前 GCC 编译选项是 C89 还是 C11,程序代码当中都可以动态的定义数组长度。例如下面的例子当中,根据控制台输入的数值来分配数组长度,然后再将动态分配后的数组长度打印出来。
1 |
|
但是需要注意,数组长度一旦声明之后,就禁止再次进行修改,否则 GCC
会在编译过程中提示redeclaration of ‘array’ with no linkage
错误,请看下面的代码:
1 | int main(int argc, char *argv[]) { |
如果使用static
关键字将数组定义为静态存储方式,那么数组的长度必须是一个常量,否则
GCC 编译器依然会提示错误信息:
1 | int main(int argc, char *argv[]) { |
初始化
对于一个已经声明了长度的数组,可以选择一次性初始化全部数组元素,也可以选择只初始化部分数组元素,长度之内未进行显式初始化的元素,将会被隐式的初始化为该数组的数据类型所对应的默认值,例如:整型数组缺省元素的默认值将会被置为0
。
1 | int main(int argc, char *argv[]) { |
二维数组结构上类似于线性代数中的矩阵,其声明与初始化方式与上面的一维数组类似,但是出于代码可读性的考虑,推荐使用{}
符号对元素进行分组。
1 | int main(int argc, char *argv[]) { |
字符数组
字符类型数据以字符的 ASCII 编码进行存储,由于 ASCII
编码是整数形式,因此 C99
标准当中,将字符类型视为整型数据的一种。字符数组当中的每个元素存放一个字符,其定义与初始化的方式与普通数组类似。下面的示例代码,定义并初始化了一个名为c[]
的字符数组,并最终将其打印至控制台:
1 |
|
上面例子代码当中,每个字符占据着1
个字节的存储单元,数组c[]
最终形成的存储结构如下图所示:
由于 C 语言当中没有字符串类型,因此字符串被存放在字符类型数组当中进行处理,请看接下来的例子:
1 |
|
造成c1
和c2
的长度各不相同的原因在于,Linux C
当中以双引号"..."直接声明字符串的时候,系统默认会在字符串最后添加一个'\0'
字符作为结束标志。'\0'
的
ASCII
编码十进制形式为0
,二进制形式为0000 0000
,系统当中用来表示一个不可显示的空操作符。
由上图可以看出,字符串"Hank"
等效于字符数组{'H', 'a', 'n', 'k', '\0'}
。使用printf()
函数打印字符串或字符数组时,格式符%c
表示输出一个字符,%s
表示输出的是一个字符串,具体使用可以参数下面的例子:
1 |
|
二维数组
二维数组也称为矩阵(matrix),可以将其形象的理解为一个具有行和列的表格。
1 |
|
定义二维乃至多维数组时,对于没有显式进行赋值的数组元素,Linux C
将默认其值为该数组对应数据类型的缺省值,例如上面代码中,第 3 行第 5
列并未显式进行赋值定义,所以其值默认为整型数据的缺省值0
,该二维数组在内存中的实际存储结构如下:
注意:由于数组每个元素都拥有连续的存储地址,其实际展现出存储结构也应是线性形态。但是上图为了形象体现二维数组矩阵的存储结构,所以将其抽象为了表格形态,但是图中的每一行元素都通过箭头与下一行首尾相连,以体现其真实存储地址的连续性。
结构体 struct
由于数组只能存放相同数据类型的数据,而结构体是一种由不同类型数据组成的组合型数据结构。使用时需要先声明结构体类型,再建立结构体变量。
1 | struct 结构体类型名称 { |
结构体类型定义完成之后,就可以定义该结构体类型的变量,具体定义格式如下所示:
1 | struct 结构体类型名称 结构体变量名称; |
下面的代码定义了一个Date
和Student
结构体类型以及名为hank
的结构体变量:
1 | /* 声明Date结构体类型 */ |
定义结构体变量以后,系统会对其分配内存单元。结构体变量占用的内存长度是各数据类型成员所占用的长度之和,每个成员都拥有自己独立的内存单元。例如对于上面代码中定义的Data
和Student
结构体,声明并定义之后的内存布局如下图所示:
Linux C 当中可以不指定结构体类型的名称,而直接定义一个结构体变量,就像下面代码所展示的那样:
1 | struct { |
结构体变量定义完成之后,就可以开始对结构体成员进行初始化赋值,然后以结构体变量名称.成员名称
的方式访问,接下来看一个从结构体类型定义、结构体变量声明、结构体初始化、引用并输出结构体成员值的完整例子:
1 |
|
无论 GCC
采取哪种标准的编译选项,都可以在初始化结构体变量时,以.结构体成员名称 = 成员初始值
的方式赋值给指定成员,其它未赋值成员的值将会是该成员数据类型所对应的缺省值(整型默认为0
,浮点型默认为0.000000
,字符型默认为\0
,指针类型默认为NULL
),修改一下上面例子代码,只对结构体成员year
进行显式赋值操作,由于month
和day
都是整型数据,因此其默认值为0
。
1 |
|
同类型的结构体变量可以相互赋值,修改一下上面例子程序的代码并执行:
1 |
|
结构体数组与普通数组一样,只是每个元素都是结构体类型的数据,下面的代码定义并初始化了一个结构体数组:
1 | struct Date { |
共用体 union
共用体可以在同一个地址开始的内存单元中存放几种不同的数据类型,正是由于占用内存的起始地址相同,所以共用体某一时刻只能存放一个数据,而不能同时存放多个;下面展示了共用体定义的基本格式,可以看到语法上与结构体非常类似:
1 | union 共用体类型名称 { |
共用体拥有让一个变量同时具备多种数据类型的能力,接下来,我们声明一个称为Variable
的结构体变量,该变量拥有存放字符型、整型、浮点型数据的能力。
1 | union Variable { |
共用体变量所占用的内存长度,等于内存占用最长的那个数据类型的变量长度。因此,上面定义的Variable
结构体变量占用的存储空间为4
个字节,即占用内存空间最大的浮点型数据real
的长度。
共用体也可以不指定共用体类型的名称,而直接定义一个共用体变量;但是需要注意,由于共用体内成员占据的都是同一块存储空间,因此定义共用体变量以后,只能初始化其中的一个成员,比如像下面这样:
1 | union { |
共用体变量定义完成之后,在代码当中并不能直接引用,只能通过共用体变量名称.成员名称
引用或初始化共用体中的成员:
1 |
|
GCC 也支持显式的初始化共用体的指定成员,请接着阅读下面示例代码:
1 | union Variable { |
由于共用体变量中保存的成员总是最后一次赋值的成员,所以每次赋值操作都会覆盖之前保存的成员状态。此外,因为共用体成员之间都共享着一个起始地址,所以共用体变量的地址与其成员的地址都是相同的,这是与结构体非常不同的一点,接下来的示例代码将会非常好的展示这一点:
1 |
|
Linux C 允许相同类型的共用体变量相互进行赋值,也可以定义一个共用体数组,甚至出现在结构体类型的定义当中。
枚举类型 enum
如果变量拥有几种可能的值,那么就可以考虑将该变量定义为枚举类型,枚举类型变量的取值范围仅限于枚举类型定义的范围。枚举类型的声明格式如下:
1 | enum 枚举类型名称 {枚举元素列表} |
根据上面的声明格式,我们将在下面代码中声明一个枚举类型Week
,然后初始化一个枚举变量today
,并将其结果打印出来:
1 |
|
GCC 编译器对于枚举类型元素会按照常量进行处理,因此这些元素也被称为枚举常量,而既然是常量就不能对其直接进行赋值操作,否则编译器将会提示错误信息。
1 |
|
枚举类型每个元素的值默认为一个整型数据,GCC
编译器会按照1
,2
,3
...的序数顺序为枚举类型的每个元素赋值,如果将枚举类型元素赋值为一个非整型数据,那么
GCC 编译器将会提示枚举值不是一个整型常量
的错误信息。
1 |
|
正是由于枚举值是一个整型(int)的常量,因此其占用的存储空间总是整型数据所占用的存储空间,接着看下面的例子:
1 |
|
枚举与其它的复合数据类型一样,可以不用声明枚举类型名称,而直接定义枚举类型变量,因此上面的例子也可以改写为下面这样:
1 |
|
运算符
运算符用于执行程序程序代码当中的运算操作,使用时需要注意优先级、结合性、运算目数方面的事项,Linux C 支持的运算符主要有如下几种类型:
赋值运算符
运算符 | 含义 | 运算目数 | 示例 |
---|---|---|---|
= | 赋值 运算符 | 双目 运算符 | a = 0 |
+= | 复合赋值运算符,等效于a = a + b 。 |
双目运算符 | a += b |
-= | 复合赋值运算符,等效于a = a - b 。 |
双目运算符 | a -= b |
*= | 复合赋值运算符,等效于a = a * b 。 |
双目运算符 | a *= b |
/= | 复合赋值运算符,等效于a = a / b 。 |
双目运算符 | a /= b |
%= | 复合赋值运算符,等效于a = a % b 。 |
双目运算符 | a %= b |
>>= | 复合赋值运算符,等效于a = a >> b 。 |
双目运算符 | a >>= b |
<<= | 复合赋值运算符,等效于a = a << b 。 |
双目运算符 | a <<= b |
&= | 复合赋值运算符,等效于a = a & b 。 |
双目运算符 | a &= b |
^= | 复合赋值运算符,等效于a = a ^ b 。 |
双目运算符 | a ^= b |
│= | 复合赋值运算符,等效于a = a │ b 。 |
双目运算符 | a │= b |
算术运算符
运算符 | 含义 | 运算目数 | 示例 |
---|---|---|---|
+ | 加法 运算符 | 双目 运算符 | a + b |
- | 减法 运算符 | 双目 运算符 | a - b |
* | 乘法 运算符 | 双目 运算符 | a * b |
/ | 除法 运算符 | 双目 运算符 | a / b |
% | 取余 运算符 | 双目 运算符 | a % b |
++ | 自增 运算符 | 单目 运算符 | a++ 或++a |
-- | 自减 运算符 | 单目 运算符 | a-- 或--a |
关系运算符
运算符 | 含义 | 运算目数 | 示例 |
---|---|---|---|
> | 大于 运算符 | 双目 运算符 | a > b |
< | 小于 运算符 | 双目 运算符 | a < b |
== | 等于 运算符 | 双目 运算符 | a == b |
>= | 大于等于 运算符 | 双目 运算符 | a >= b |
<= | 小于等于 运算符 | 双目 运算符 | a <= b |
!= | 不等于 运算符 | 双目 运算符 | a != b |
逻辑运算符
运算符 | 含义 | 运算目数 | 示例 |
---|---|---|---|
&& | 逻辑与 运算符 | 双目 运算符 | a && b |
‖ | 逻辑或 运算符 | 双目 运算符 | a ‖ b |
! | 逻辑非 运算符 | 双目 运算符 | a ! b |
位运算符
运算符 | 含义 | 运算目数 | 示例 |
---|---|---|---|
& | 按位与 运算符 | 双目 运算符 | a & b |
│ | 按位或 运算符 | 双目 运算符 | a │ b |
^ | 接位异或 运算符 | 双目 运算符 | a ^ b |
~ | 按位取反 运算符 | 单目 运算符 | ~a |
<< | 左移 运算符 | 双目 运算符 | a << b |
>> | 右移 运算符 | 双目 运算符 | a >> b |
选择结构
GCC
编译器表达逻辑运算结果时,数值0
等效于false
数值1
等效于true
,包括其它任何非0
值也都会被认为等效于true
,当程序在执行选择结构的判断时,尤为需要注意这一点。
1 |
|
if 结构
如果表达式
结果为true
,那么执行语句
;如果表达式
结果为false
,那么跳过该选择结构并执行后面语句。
1 | if(表达式) { |
如果表达式
结果为true
执行语句1
;如果表达式
结果为false
执行语句2
。
1 | if(表达式) { |
如果表达式1
结果为true
就执行语句1
并跳过该选择结构;如果表达式1
结果为false
就继续判断表达式2
,如果表达式2
结果为true
就执行语句2
并跳过该选择结构,如果表达式2
结果为false
,那么就直接执行语句3
,最后跳出该选择结构并执行后续代码。
1 | if(表达式1) { |
if
语句中的表达式,可以是关系表达式、逻辑表达式、甚至数值表达式,实际开发过程当中注意灵活进行使用;接下来编写一个关于if
结构的完整
Linux C 程序示例代码:
1 |
|
GCC
当中,只要包含了<stdbool.h>
头文件,就可以方便的在逻辑运算和选择结构当中使用bool
类型以及true
、false
关键字,修改一下上面的代码:
1 |
|
switch 结构
下面的伪代码当中,switch
语句上表达式
的结果必须是整型或者字符型,否则
GCC
编译器会提示error: switch quantity not an integer
错误信息。
1 | switch (表达式) { |
同样的,在接下来的代码当中,我们将会编写一个关于switch
语句的完整例子:
1 |
|
循环结构
面向过程的程序开发当中,通常拥有顺序结构、选择结构、循环结构三种基本结构,本节内容将会来介绍相对更为繁琐一些的循环结构。
while 结构
表达式
结果为true
时执行循环体中的语句
,为false
时跳出循环体执行后续其它语句。
1 | while(表达式){ |
1 |
|
do while 结构
首先无条件执行循环体中的语句
,然后检查表达式
内的条件,如果表达式
结果为true
就继续执行循环体,如果表达式
结果为false
就跳出循环体。
1 | do { |
1 |
|
for 结构
首先执行表达式1
,主要用于设置初始条件,只执行一次;
然后执行表达式2
,主要用于判断循环是否继续进行;
最后执行表达式3
,主要用于循环变量进行自增操作。
1 | for (表达式1; 表达式2; 表达式3) { |
1 |
|
上面的while
、do while
、for
三个例子程序,都是在循环索引小于6
的条件之下打印当前执行的索引值,因而执行的结果都是相同的:
1 | 当前执行了第0次 |
continue 关键字
continue
用于跳出当前循环,仅能作用于循环结构。
1 |
|
break 关键字
break
用于终止整个循环,可用于循环结构以及switch
选择结构。
1 |
|
使用函数
一个 Linux C 程序通常由一个主函数和若干的其它函数组成,Linux C
当中的函数遵循先定义后使用的原则,对于 GCC
编译系统提供的库函数,需要先使用#include
指令将头文件包含至源码文件当中,下面伪代码展示了定义一个带参函数的方法:
1 | 返回值数据类型 函数名称( 形式参数类型 形式参数名称, ... ) { |
函数定义完成之后,就可以通过下面的格式进行函数的调用:
1 | 函数名称(实际参数值); |
函数调用后返回结果的数据类型与函数定义中的返回值数据类型一致,如果函数不需要接收返回值和参数,则必须使用void
关键字显式进行定义。
1 | void 函数名称( void ) { |
与其它编程语言不同,Linux C
当中无任何参数的函数function()
是指当前函数的参数个数不确定,而显式定义了void
关键字参数的function(void)
才是指当前函数没有任何参数。
1 | void function(void) {} // 定义void参数表示该函数不接受任何参数。 |
因而上面例子中,函数function(void)
由于显式定义了void
参数,因此
GCC
编译器会报出参数个数过多的错误信息,如果移除函数定义中的void
关键字,function()
就可以正常通过编译。
同样的,当函数没有返回值时,函数定义中除了返回值类型使用void
以外,函数内部也不需要再使用return
语句,下面的例子代码定义了一个返回值类型为void
并且不具有return
语句的函数:
1 | void function(void) { |
由于函数必须遵循先定义后使用原则,当主函数位于其它函数前面时,必须将其它函数的声明语句放置到主函数之前,确保代码顺序执行时主函数能识别后面声明的函数内容。函数声明有时也被称为函数原型,如下两种函数声明方法在 GCC 中都有效:
返回值数据类型 函数名称( 形式参数类型 );
返回值数据类型 函数名称( 形式参数类型 形式参数名称 );
1 |
|
上面的例子当中,使用了字符数组作为函数的参数,Linux C 当中规定使用数组作为函数的实际参数时,函数形式参数得到的将会是该数组首元素的地址,请看下面的例子:
1 |
|
正是由于数组作为函数参数时,传递的是数组的首地址;当在接收数组参数的函数内部,对数组进行修改操作之后,将会改变所有引用该数组名称的数组元素内容,这一点需要特别注意。
1 |
|
综上,使用数组元素作为实际参数时,向形式参数变量传递的是数组元素的值。而用数组名称作为函数实际参数时,向形式参数(数组名、指针变量)传递的是数组首元素的地址。
GCC 当中无论编译规范选择 C89/C90 或者 C99 乃至 C11,主函数返回类型是否为
int
或void
以及函数最后是否有return
语句,编译都能够正常通过并且没有任何警告信息。
变量作用域与函数
函数内定义的变量称为局部变量,函数外定义的变量称为全局变量,习惯上会将全局变量的首字母进行大写处理。
1 |
|
多个函数共享一个全局变量,很容易造成数据操作的结果相互污染,因此使用全局变量时需要格外谨慎。
函数的定义和声明通常都是全局的,但是 Linux C
允许在函数内部再嵌套定义一个函数,类似于局部变量,该函数只能在定义它的那个函数内部有效,因此也可以称为局部函数。下面代码当中,主函数main()
内部定义了一个inner()
并正常调用,
1 |
|
局部函数仅在主函数内可见,当在主函数外进行调用时 GCC 编译器会提示语法错误。
变量存储类型
变量的存储方式分为动态(程序运行期间按需动态的进行分配)和静态(程序运行期间由系统固定分配)两种,现代操作系统当中,用户的内存空间一般会分为程序存储区、静态存储区、动态存储区三块区域。
用户数据主要放置在动态和静态存储区当中,静态存储区主要保存全局变量以及static
声明的静态局部变量,程序执行时为其分配存储空间,执行完毕后释放这些空间。程序运行过程中,全局变量占据固定存储单元,无须动态分配与释放。
动态存储区会在函数开始调用时动态分配存储空间,调用完毕后释放这些存储空间,分配与释放都是伴随函数调用动态进行的。动态存储区存放的数据主要分为以下 3 类:
- 函数的形式参数,调用函数时会为其动态分配存储空间;
- 函数内声明的自动变量,即未使用
static
关键字声明的非静态变量。 - 函数调用时的现场保护和返回地址。
GCC 当中的每个变量与函数都拥有数据类型与存储类型(数据在内存中的存储方式,即静态和动态存储)两个属性, 因此在声明变量与函数时,应当同时声明其数据与存储类型。目前 GCC 支持的存储类型主要包括自动(auto)、静态(static)、寄存器(register)、外部(extern),接下来将会逐一进行介绍。
自动 auto
Linux C
函数中的局部变量默认为自动存储类型,函数中定义的局部变量和形式参数都属于这个类型,如果需要显式进行声明,可以使用auto
关键字:
1 |
|
静态 static
静态局部变量使用static
关键字声明,保存在用户内存的静态存储区当中,其值在函数调用结束后并不会释放存储单元;下一次函数调用时,该静态局部变量依然继续保留原值。因为静态局部变量在程序运行过程中占据固定的存储单元,所以缺省赋值的情况下,GCC
编译器会默认赋予相应数据类型的初值(整形变量缺省为0
,字符型变量缺省为\0
空操作符)。
1 |
|
静态局部变量不能被其它函数引用,而只能被变量声明所在的函数使用。
寄存器 register
传统计算机体系结构里,静态/动态变量都存储在计算机内存单元当中,CPU
通过指令集与内存进行交互。为了提高执行效率,可以将一些存取较为频繁的变量放置到
CPU 寄存器当中,GCC
当中可以使用register
关键字声明这样的变量。
1 |
|
现代计算机体系结构,CPU 已经可以智能的将常用变量调度至寄存器,而毋需手动再进行声明配置。
外部 extern
这里简单总结一下,auto
关键字声明的自动变量保存在用户内存的动态存储区,static
关键字声明的静态局部变量保存在用户内存的静态存储区,register
关键字声明的寄存器变量保存在
CPU 的寄存器区,它们都是局部变量。
接下来将要介绍的extern
关键字,则主要作用于全局变量。一般情况下,全局变量存放在静态存储区的,通常其作用域是从变量定义处至程序文件末尾,在此作用域内的代码都可以引用该全局变量。而通过使用extern
关键字,可以方便的扩展全局变量的作用域。
(1)extern
可以在一个源码文件内部扩展全局变量的作用域:
1 |
|
上面代码当中,extern
关键字扩展了全局变量char A, B;
的作用域到main()
函数。但是事实上,GCC
已经自带了全局变量作用域提升的特性,即使此处不向main()
函数当中添加extern
关键字,依然能正常编译运行,运行结果与添加extern
关键字的情况相同。
(2)extern
也可以将一个源码文件内部全局变量的作用域扩展至其它源文件当中:
首先,按照如下目录结构建立源文件:
1 | ➜ tree |
向method.h
添加如下代码,其中定义了字符类型的全局变量A
和B
:
1 |
|
将上面method.h
源文件当中定义的变量作用域通过extern
关键字扩展至main.c
的主函数当中:
1 | void method(); |
执行gcc main.c utils/method.c
命令将两个源文件编译为一个可执行文件,输出结果为:A的小写仍然是x,B的小写仍然是b!
。
本源文件中的
extern
变量优先级高于外部源文件中的extern
变量优先级。
(3)如果需要限制该全局变量仅在本源码文件中使用,那么可以通过static
关键字将该全局变量声明为静态的,这样其它文件中就无法使用extern
关键字扩展其作用域,现在将method.c
内的全局变量A
、B
声明为static
:
1 |
|
源文件main.c
保持对全局变量A
和B
的声明不变,执行后
GCC 报出以下错误信息:
1 | void method(); |
值得注意的是:局部变量声明
static
存储类型是为了指定存储区域,而全局变量本身就分配在静态存储区,添加static
存储类型只是为了控制其作用域的扩展。
函数存储类型
函数编写的目的是为了让其它函数调用,因而函数本质上是全局的。但是根据函数能否被其它源文件中的代码调用,将函数分为内部函数和外部函数。
内部函数 static
内部函数只能被本源文件当中的代码调用,定义时只需要在函数返回类型之前添加static
关键字,因此也称为静态函数。
1 | static 返回值数据类型 函数名称(参数列表) { |
内部函数在多人协作开发时,可以有效的防止多个源文件中定义的函数名称相互污染。现在将变量存储类型小节的例子程序稍作修改,向utils
目录下method.c
当中的method()
函数添加static
关键字,使其成为一个不能被其它源文件中代码调用的内部函数。
1 |
|
接下来在main.c
源文件中声明method()
并调用该函数,此时执行gcc main.c utils/method.c
编译控制台报出错误信息:
1 | void method(); |
外部函数 extern
外部函数可以方便的被其它源文件中的代码调用,Linuc C
当中函数默认为外部函数,也可以显式的使用extern
关键字进行定义。
1 | extern 返回值数据类型 函数名称(参数列表) { |
现在将前面例子中的method()
函数显式添加上extern
关键字,使其成为一个外部函数。
1 |
|
接下来在main.c
中调用该method()
外部函数,并执行gcc main.c utils/method.c
成功编译。
1 | void method(); |
剖析指针
变量的地址(通过取地址运算符&
获得)称为该变量的指针,存放地址/指针的变量(使用指针运算符*
声明)称为指针变量,其声明格式如下:
1 | 数据类型 *指针变量名称 |
指针变量由 Linux C 基本数据类型派生而来,由于每种数据类型的存放方式与占用空间不同,为了确保指针运算能够正确的得到执行,指针变量声明时必须指定其所属数据类型。
1 |
|
注意:对于一个已经声明并定义完成的指针变量,使用地址运算符
&指针变量名称
只能得到该指针变量的地址,而通过指针运算符*指针变量名称
则可以获取其保存的真实数据。
将指针变量作为函数的参数时,传入的实质上是变量的存储地址,因此函数中对该指针变量进行的任何操作,其结果都会反映至该指针实际指向变量的状态,请仔细理解接下面的示例程序:
1 |
|
指向数组的指针
一个数组包含有若干个元素,每个元素都占据着相应存储单元并拥有各自的地址。指针变量即可以指向基本数据类型变量,也可以指向数组中的元素。Linux C 语言当中,数组名称就代表数组当中首元素的地址,请参考下面的示例代码:
1 |
|
数组指针的运算
当指针指向数组元素时,可以对指针进行加+
减-
运算(包括自增++
自减--
运算),此时指针会基于所属数据类型占用的字节数进行前后移动,进而达到通过移动指针来访问数组各个元素的目的。
1 |
|
通过指针访问数组元素
除了使用传统的下标数组名称[元素位置]
访问数组以外,Linux
C 还支持使用指针以*(数组名+元素位置)
的形式来进行访问。
1 |
|
使用指针方式访问数组元素,通常会获得更优的执行效率。
使用数组名称作为函数参数
使用数组名称作为函数参数时,由于实际向函数传递的是数组首元素地址,因此函数中对于该数组进行的任何修改,同样也将会反映到原始声明的数组上,这一特性与前面提到的向函数传递基本数据类型指针相似。
1 |
|
指向字符串的指针
Linux C
语言没有字符串数据类型,字符串都是以字符数组形式保存。可以通过数组名和下标引用字符串中的字符,或者使用格式字符串%s
和数组名称输出整个字符串,也可以通过字符类型的指针变量引用一个字符串常量,此时指针变量保存的是字符串首个字符的地址,就像下面这样:
1 |
|
虽然可以通过下标[]
方便的修改字符串数组中指定元素的值,但是字符指针变量所指向字符串当中的元素值是不可修改的,否则
GCC
编译器将会提示错误信息:[1] 25465 segmentation fault(core dumped)./ a.out
。
1 |
|
由于字符指针变量实际保存的是字符串首元素的地址,因此可以像字符数组一样,通过与整数进行加减运算来完成字符串截取功能。
1 |
|
可以改进一下上面的代码,让printf()
成为一个可变格式输出的函数,也就是将格式字符串声明并定义为一个字符数组,提升代码可读性。
1 |
|
字符指针与字符数组使用上最显著的区别在于:字符数组当中元素的值是可以进行修改的,而字符指针变量所指向字符串常量中的元素不可被修改,否则
GCC
编译时将会提示错误信息[1] 18548 segmentation fault (core dumped) ./a.out
。
1 |
|
由于字符指针变量指向的字符串常量里的元素不能修改,所以当字符指针作为参数传递至其它函数进行处理时,如果重新将一个字符串常量赋值给该字符指针,那么该字符指针将仅仅被作为一个普通的函数局部变量进行处理。
1 |
|
上面例子当中,method()
函数对传入的字符指针参数pointer
重新进行了赋值,但是主函数中string
指针所指向的字符串常量并未发生改变。这是由于字符指针总是占有
8
个字节存储空间,保存的总是字符串常量在内存中首个元素的地址。因此,当在method()
函数内声明一个字符串常量并赋值给pointer
指针的时候,仅仅是改变了形式参数pointer
所指向的字符串常量首地址值,而并不会影响主函数中字符指针string
所指向的字符串常量地址,因而pointer
仅仅被视为一个普通的局部函数。
函数与指针
GCC 编译器会为 Linux C 函数分配一段存储空间,这段存储空间的起始地址称为函数的指针,用来保存函数指针地址的变量称为函数的指针变量,声明一个函数指针的具体格式如下:
1 | 返回类型 (*函数指针名称)(形式参数...) |
函数指针变量只能指向其声明时所指定类型的函数,Linux C 当中除使用函数名调用函数以外,还可以通过函数指针来进行调用,具体调用格式如下所示:
1 | (*函数指针名称)(实际参数...) |
通过函数指针调用函数时,必须先让声明的函数指针变量指向该函数,来看下面这个函数指针实际应用的例子:
1 |
|
声明函数指针时,可以缺省函数的参数名称,而只需要保留数据类型,即
int (*function)(int integer);
亦可以写作int (*function)(int);
。
与前面小节内容提到的数组指针、指向字符串的字符指针不同,函数的指针变量不能进行算术运算,否则
GCC
编译器将提示[1] 23924 segmentation fault (core dumped) ./a.out
错误。
1 |
|
函数指针作为参数
指向函数的指针可以作为其它函数的参数,即将函数的入口地址传递给其它函数的形式参数,以便于在该函数中方便的调用参数指针所指向的那个函数。这样做的好处在于不需要修改主调函数中的任何代码,就可以通过方便的更换传递给它的函数指针参数,替换相应的功能,具体请参考下面的例子:
1 |
|
由于 Linux 内核源代码当中,大量使用了指向函数的指针作为函数的参数,因而本小节内容是 Linux C 编程开发当中一个较为重要的知识点。
返回指针值的函数
Linux C 函数除了返回整型、字符型、浮点类型等基本数据类型以外,还能够返回指针数据类型,即返回一个存储地址值。一个返回指针值的函数定义格式如下:
1 | 返回类型 *函数名称(形式参数...) { |
接下来,结合前面章节中提到的指向字符串的字符指针、以及指针作为函数参数的内容,编写一个返回字符类型指针的示例程序:
1 |
|
指针数组
数组元素全部为指针类型的数组称为指针数组,即数组中每个元素都存放的是一个内存地址值,指针数组的定义格式为:
1 | 数据类型 *数组名称[数组长度] |
指针数组常用于方便灵活的处理多个字符串,由于字符串本身就是一个字符数组,因此存放有多个字符串的数组本质就是一个二维数组。由于二维数组定义时需要指定列数,每一列包含的元素个数都是相同的,一旦某个字符串的长度过长,就会以该字符串的长度作为每行的长度,从而造成大量内存空间的浪费。
1 |
|
上面代码当中,声明并定义一个字符指针数组,并保存了 3 个字符串常量的首地址。由于指针变量的长度总是为 8 个字节,而且数组本身是连续存储的,因此上面打印的字符串数组内存地址总是按照 8 个字节间隔进行排列。
指针类型数据的指针
指向指针的指针,顾名思义就是指向指针数据类型的指针变量,可以通过如下格式进行声明和定义:
1 | 数据类型 **指针名称; |
1 |
|
前面提到的指针数组的元素不仅可以指向字符串,也可以指向整型或者浮点型等其它数据类型,来看下面的例子:
1 |
|
上面代码由于整型指针数组
addresses
不像上一个例子中的字符串常量那样,可以自动获取首地址值,因此必须通过取地址运算符&
显式的获取地址来构建这个指针数组。
main 函数的指针参数
main()
函数携带的形式参数(int argc, char *argv[])
也称为
Linux C 程序的命令行参数,其中第 1
个参数argc
是单词argument
count的缩写,表示参数的个数;,第 2
个参数argv
是单词argument
vector的缩写,表示参数向量,这是一个字符类型的指针数组,数组中每个元素都指向命令行输入的字符串,即命令 参数1 参数2...
。
1 |
|
打印结果,Linux
控制台下编译为默认的a.out
文件后执行结果如下,注意打印时将运行a.out
文件的命令也作为了输入参数:
1 | ➜ ./a.out hank uinika |
其实,形式参数
argc
和argv
也可以命名为其它名字,但是出于约定成俗的习惯,建议继续沿用这样的命名方式。
void 无类型指针
C99
标准提供了void
类型的指针,用于定义一个基类型为void
的指针变量,即void *变量名
。该指针并不指向任何确定的数据类型,只会在使用其它数据类型的指针变量对其进行赋值时,由
GCC
编译系统自动对其进行类型转换。转换后的指针只能得到其它数据类型的纯地址值,并不能自动指向该类型指针之前所引用的数据,任何尝试打印自动类型转换后void
指针所引用数据的行为,都将会引发
GCC 的编译警告信息,就如同下面代码这样:
1 |
|
简单修改一下上面的代码,只获取自动类型转换后void
指针变量pointer
内保存的地址值,代码就可以正常得到执行。
1 |
|
NULL 空类型指针
GCC
提供的stdio.h
头文件里通过预定义指令#define NULL 0
定义了符号常量NULL
,如果将其赋值给指针变量,那么就认为该指针变量为空指针,保存的变量地址值为00000000
。
1 |
|
注意:指针变量值为
NULL
和未赋值的指针变量属于两类概念,前者保存的地址值为00000000
,后者则可能指向一个无法预料的值,也就是江湖传说中的野指针。
指针运算
指针变量与整型数据进行加减运算,即将该指针变量值与其所指向变量类型占用的存储单元字节数进行加减运算。
1 |
|
将变量的地址赋值给一个指针,注意不能将一个整数赋值给指针变量。
1 |
|
指针变量之间可以相减,得到的差是两个指针变量之间的元素个数。
1 |
|
如果两个指针变量都指向同一个数组的不同元素,则可以进行比较运算,即前面数组元素的指针将会小于后面的数组元素。
1 |
|
归纳总结
- 首先需要明确指针的概念,指针就是变量的内存地址,凡是出现指针的地方都可以使用地址进行代替。变量的指针就是变量的地址,指针变量就是地址变量。
- 区别指针与指针变量,指针是指地址值本身,指针变量是用来存放这个地址的变量。
- 明确指针的指向所表达的概念,对于指针变量而言,保存的是哪个变量的地址,就认为指针变量指向了这个变量。
void
是一种特殊类型的指针,并不指向任何特定的数据类型。如果要保存特定数据类型的数据,那么首先要对指针地址进行类型转换(由 GCC 编译器自动转换或由代码强制进行转换)。- 指向数组的指针变量,其保存的实质是该数组的首元素地址,而指针数组则是指数组元素全部为指针类型的数组,注意区分这两个不同的概念。
- 注意分辨字符数组与字符串指针,字符数组中元素的值可以修改,而字符指针变量所指向字符串常量中的元素不可修改。
- 无类型指针
void
不指向任何确定的数据类型,空指针NULL
保存的地址值为00000000
,未进行初始化赋值的指针,由于指向不确定的存储区域,因此被称为野指针。
动态内存分配
前面章节介绍过,Linux C 当中的全局变量分配在内存中的静态存储区,非静态局部变量(包括形式参数)分配在内存中的动态存储区,这个存储区是一个称为栈(stack)的区域。此外,Linux C 还允许建立一个称为堆(heap)的自由动态存储区,用于存放一些临时数据,这些数据毋须在程序声明部分定义,也毋须在函数结束时释放,而是随时按需去申请开辟指定大小的空间,然后再随时手动进行释放。因为不会在代码中声明和定义相关的变量,所以也就不能通过变量或数组名称引用这些数据,而只能通过指针进行引用。
内存的动态分配是通过 Linux C
提供的头文件stdlib.h
内置的四个标准库函数实现:malloc()
、calloc()
、free()
、realloc()
,C99
标准将它们的返回类型定义为void
类型,即不指向具体的数据类型,只表示指向一个抽象的纯地址。
void *malloc(unsigned int size)
用于在内存的动态存储区中分配一个长度为size
的连续空间,返回值是所分配存储空间的首字节地址。如果函数由于内存空间不足等原因未能执行成功,那么将会返回一个空指针。
1 |
|
void *calloc(unsigned n, unsigned size)
用于在内存的动态存储区分配n
个长度为size
的连续空间,可以用于保存一个动态数组。函数返回值同样指向所分配区域的首地址,执行不成功同样返回空指针。
1 |
|
void free(void *p)
释放void
类型指针变量p
所指向的动态存储空间,以便于
Linux
操作系统进行回收复用。通常情况下,参数p
是malloc()
或calloc()
函数调用后的返回值。
1 |
|
void *realloc(void *p, unsigned int size)
如果已经通过malloc()
或calloc()
函数获取动态存储空间,那么可以使用realloc()
重新进行存储空间size
的分配,执行成功后返回重新分配的地址,如果分配不成功则返回空指针。
1 |
|
realloc()
调用后的返回值必须通过声明void
类型的指针变量进行接收,否则
GCC
编译器将提示警告信息:warning: ignoring return value of ‘realloc’, declared with attribute warn_unused_result [-Wunused-result]
。
typedef 声明
除了直接使用各种原生数据类型,Linux C
还支持typedef
关键字自定义数据类型,具体使用格式如下:
1 | typedef 原生数据类型 自定义数据类型 |
下面的例子当中,我们将通过typedef
对原生的数据类型进行重命名:
1 |
|
预处理指令
GCC 编译器可以在预处理阶段对 Linux C
源代码进行一系列预处理操作,首先会清除代码中的注释,然后对预处理指令(Preprocessing
Directive)进行处理,预处理指令通常以#
开头,指令语句后面没有分号。经过预处理之后的代码将不再包含相关指令。例如:将#include
指令替换为头文件指定的内容,用#define
指令指定的值替换代码中引用的位置。
宏定义 #define #undef
宏定义主要用于减少代码中一些重复出现常量的书写工作量,宏名称习惯上使用大写字母表示,具体定义格式如下:
1 |
接下来,我们编写一个完整的宏定义示例:
1 |
|
通常情况下,#define
指令位于源代码文件开头,有效范围从该指令位置一直覆盖到源文件结束。如果需要,也可以采用另外一个宏定义指令#undef
手动终止该宏定义的作用域,其具体使用格式为:
1 |
修改一下前面的代码,通过添加#undef
指令来控制SENTENCE
的作用域范围:
1 |
|
注意:宏定义并非变量,GCC 也不会为其分配存储空间,仅仅是对源代码字符串进行的简单替换。
上面的宏定义只是定义了一个常量值,如果需要对一个表达式进行宏定义,那么就需要使用到带参数的宏定义,具体定义格式如下:
1 |
接下来一份完整的示例代码继续走起:
1 |
|
文件包含 #include
文件包含指令#include
用于将其它源代码文件包含至指令所在位置,一条#include
指令只能包含一个源文件,包含多个源文件需要多条#include
指令,其具体使用格式如下:
1 |
现在,在测试代码目录里新建一个utils
子文件夹,然后添加一份如下所示的method.c
源码文件:
1 |
|
紧接着在上级目录编写main.c
主函数,并在其中调用method.c
提供的method()
函数。
1 |
|
这种书写在源码头部的文件通常被称为头文件,里边通常用于放置函数定义、结构体定义、宏定义;头文件后缀名例行使用.h
结尾,因此出于规范方面的考量,需要将上面的method.c
后缀名修改为method.h
,下面展示一份完整的示例代码:
1 |
|
接下来,在main.c
主函数当中调用头文件method.h
定义的各种值:
1 |
|
条件编译 #ifdef #ifndef #if
正常情况下,一份源代码文件当中的所有代码都会参予编译,但是某些时刻只希望编译代码文件当中的部分内容,此时就需要使用到条件编译相关的预定义指令。
(1)如果标识符
已经被#define
预处理指令定义,那么
GCC
将会预处理代码段 1
,否则就会预处理代码段 2
。前面有提到:条件编译本质上是一系列宏替换操作,因此预处理后的源文件将只会根据条件保留代码段 1
和代码段 2
中之一。
1 |
|
1 |
|
(2)如果标识符
没有被#define
预处理指令定义,那么
GCC
将会预处理代码段 1
,否则就会预处理代码段 2
。
1 |
|
1 |
|
(3)当表达式
为真(非零值)时就预处理代码段 1
,否则预处理代码段 2
。
1 |
|
1 |
|
位运算
位运算是以二进制为对象的运算,参加位运算的只能为整型或字符型数据,浮点类型数据不能参与位运算。位运算是嵌入式系统开发当中的常用计算操作,下表列出了 Linux C 当中可以使用的位运算。
运算符 | 释义 | 运算符 | 释义 |
---|---|---|---|
& | 按位与,例如:a & b 。 |
~ | 按位取反,例如:~a 。 |
│ | 按位或,例如:a │ b 。 |
<< | 左移,例如:a << b 。 |
^ | 接位异或,例如:a ^ b 。 |
>> | 右移,例如:a >> b 。 |
按位与 &
参予运算的两个数据,按二进制位进行与运算,真值表如下:
表达式 | 结果 | 表达式 | 结果 |
---|---|---|---|
0 & 0 |
0 |
1 & 0 |
0 |
0 & 1 |
0 |
1 & 1 |
1 |
Linux C 程序当中,前缀0b
代表 2
进制数值,前缀0x
代表 16 进制数值,无任何前缀的数值默认为 10
进制数值。由于字符类型占用一个字节(1 个 Byte 等于 8 个
bit)空间,因此后面关于位运算的示例代码当中,一律使用unsigned char
数据类型来声明二进制数据。
1 |
|
(1)清零,即将一个字节单元的八位全部置为二进制0
。操作方法是将操作数a
二进制值当中的1
替换为0
作为操作数b
来参予与运算。
1 |
|
(2)获取二进制的指定位,方法是将操作数a
的二进制值当中,需要获取的位都置为1
,除需要获取位之外的所有位都置为0
,然后让其参予同操作数的与运算。
1 |
|
按位或 |
两个操作数对应的二进制位当中,只要有一个的状态为1
,那么运算的结果就是1
。
表达式 | 结果 | 表达式 | 结果 |
---|---|---|---|
0 | 0 |
0 |
1 | 0 |
1 |
0 | 1 |
1 |
1 | 1 |
1 |
接位或运算通常用来将操作数的指定位置设为1
,方法是将操作数当中需要保持原样的位置设为0
,需要指定为1
的位置保持1
,然后参予同操作数的或运算。
1 |
|
按位异或 ^
如果参予运算的两个二进制操作数的对应位异号,那么结果为1
;如果对应位同号则结果为0
。
表达式 | 结果 | 表达式 | 结果 |
---|---|---|---|
0 ^ 0 |
0 |
1 ^ 0 |
1 |
0 ^ 1 |
1 |
1 ^ 1 |
0 |
(1)翻转指定的位,即1
转换为0
,0
转换为1
。方法是将操作数二进制位当中需要翻转的位置设置为1
,其它位置设置为0
,然后参予与操作数的异或运算。
1 |
|
(2)
交换两个变量的取值,即将操作数a
和b
进行交叉赋值。
1 |
|
按位取反 ~
按位取反运算符~
是一个单目运算符,用来对二进制操作数的每一位进行取反,即1
变为0
然后0
变为1
。
1 |
|
按位取反可以与其它位运算符结合使用,比如通过和按位与运算符&
一起结合使用,可以方便的将操作数的指定位置为0
;
1 |
|
按位左移 <<
将操作数的二进制位左移指定位数之后补0
。左移 1
位相当于该数值乘以2
,左移 n
位则相当于乘以2ᐢ
。按位左移运算比乘法更快,日常开发中需要强调性能的场合,可以考虑将乘以2ᐢ
的幂运算替换为左移n
位,但是特别注意当操作数二进制所有位都被左移出去之后,最终的结果将会等于0
,这并非最终期望的乘法结果。
1 |
|
按位右移 >>
同样的,按位右移操作符>>
是将操作数的二进制位左移指定位数之后补0
。左移
1 位相当于该数值除以2
,左移 n
位则相当于除以2ᐢ
。当试图使用右移运算代替除法计算时,依然需要注意当操作数所有位都被右移出去之后结果为0
的情况。
1 |
|
Linux C 标准程序设计语言