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
2
3
4
5
6
7
8
9
#include <stdio.h>

/*
块注释
*/
int main(int argc, char *argv[]) {
printf("hello world!\n"); // 行注释
return 0;
}

C99 标准规定主函数执行完成之后,需要显式书写return 0;语句表示程序正常退出,主函数返回类型的声明也需要显式的设置为int

变量、常量、常变量

常量是 Linux C 程序运行时不能改变的量,Linux 当中使用的常量类型有字符型整型浮点型等数据类型。

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main(int argc, char *argv[]) {
'U'; // 字符型常量
'\n'; // 转义字符常量
2019; // 整型常量
3.141592654; // 浮点型常量
"Hello Hank!"; // 字符数组常量

return 0;
}

变量是一个具有名称的存储单元,编译系统会自动为变量名分配对应的内存地址。C 程序中的变量都是数据的补码形式进行存储,程序运行时计算机会通过变量名查找对应的内存单元地址,然后通过该地址操作其中保存的变量值

下面代码当中,声明了一个整型变量date,并将其赋值为2019

1
int date = 2019;

常变量具有变量的基本属性,带有数据类型并且占用存储空间;但与常量类似,在程序运行期间不允许修改其值。C99 规范允许使用const关键字声明一个常量,下面将声明一个常量USER并赋值为Hank通常约定常量名称全部大写)。

1
const int DATE = 2019;

常量一旦声明之后就不能再次进行赋值和修改,否则 GCC 编译器将会提示错误信息:error: assignment of read-only variable,请参考下面的代码:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main(int argc, char *argv[]) {
const int DATE = 2019;
DATE = 2010;
}

// main.c: In function ‘main’:
// main.c:6:8: error: assignment of read-only variable ‘DATE’
// DATE = 2010;
// ^

数据类型

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
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main()
{
printf("int %ld byte\n", sizeof(int)); // int 4 byte
printf("unsigned int %ld byte\n", sizeof(unsigned int)); // unsigned int 4 byte
printf("short %ld byte\n", sizeof(short)); // short 2 byte
printf("unsigned short %ld byte\n", sizeof(unsigned short)); // unsigned short 2 byte
printf("long %ld byte\n", sizeof(long)); // long 8 byte
printf("unsigned long %ld byte\n", sizeof(unsigned long)); // unsigned long 8 byte
printf("long long %ld byte\n", sizeof(long long)); // long long 8 byte
printf("unsigned long long %ld byte\n", sizeof(unsigned long long)); // unsigned long long 8 byte
return 0;
}

如果需要使用printf()输出中文,源代码文件必须以GB2312编码格式保存。

值得提醒的是,基本整型int默认是有符号的数据类型,而无符号类型变量原则上不能存放-3这样的负数;使用printf()输出无符号整型数据的时候,格式字符串需要选择%u进行输出。

1
2
3
4
5
6
7
8
#include <stdio.h>

int main() {
int birthday= -1988;
printf("%d\n", birthday); // -1988
printf("%u\n", birthday); // 4294965308
return 0;
}

字符型 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
2
3
4
5
6
7
#include <stdio.h>

int main() {
printf("char %ld byte\n", sizeof(char)); // char 1 byte
printf("unsigned char %ld byte\n", sizeof(unsigned char)); // unsigned char 1 byte
return 0;
}

有符号字符型数据允许存储的取值范围在-128 ~ 127之间,但字符型的 ASCII 编码不可能为负值,因而实际只会使用到0 ~ 127,即最左侧符号位总是为0。如果将负整数直接赋值给字符型变量,操作虽然合法但并不代表一个有效字符,而仅仅保存了一个负整数值。接下来的图片展示了保存字符型变量'1'时的存储情况,由于字符'1'的 ASCII 码为49,因此存储器中实质保存的是数字49的二进制表达形式。

printf()输出字符型数据时,格式字符串需要选择%c;如果格式字符串选择为%d,则会输入该变量的 ASCII 码表达形式。

1
2
3
4
5
6
7
8
#include <stdio.h>

int main() {
char test= 'h';
printf("%d\n", test); // 104
printf("%c\n", test); // h
return 0;
}

需要注意,关键字char前的signed或者unsigned是否能够缺省由具体的编译器决定,这一点与int等其它数据类型不同。在 GCC 编译器当中,char缺省为有符号类型

1
2
3
4
5
6
7
8
#include <stdio.h>

int main() {
char test = 255; // GCC默认字符型变量为有符号类型
printf("%c \n", test); // �
printf("%d \n", test); // -1
return 0;
}

由于char默认为有符号类型,所以赋值为255超出了有符号字符类型的表示范围,导致后面打印输出为-1。如果这里显式声明字符型test的值为无符号类型unsigned则能够正确的打印数值255

1
2
3
4
5
6
int main() {
unsigned char test = 255; // 显式声明字符型变量为无符号类型
printf("%c \n", test); // �
printf("%d \n", test); // 255
return 0;
}

浮点型 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
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main() {
float pi_f = 3.14;
double pi_b = 3.14;
long double pi_ld = 3.14;

printf("float %ld byte\n", sizeof(pi_f)); // float 4 byte
printf("double int %ld byte\n", sizeof(pi_b)); // double int 8 byte
printf("long double %ld byte\n", sizeof(pi_ld)); // long double 16 byte

return 0;
}

值得注意的是:Linux C 当中对于浮点类型的常量值默认会按照double类型来进行处理。

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main() {
float i = 3.14159;
/* 打印一个变量占用的空间 */
printf("float is %d byte\n", sizeof(i)); // float is 4 byte
/* 打印一个常量占用的空间 */
printf("double is %d byte\n", sizeof(3.14159)); // double is 8 byte

return 0;
}

基本数据类型转换

自动类型转换

当两个不同的基本数据类型进行算术运算时,所得结果的数据类型总是存储占用空间更大的那一个。比如下面例子中,整型的i(占用 4 字节存储空间)与字符类型的c(占用 1 字节存储空间)分别与浮点类型f相加(占用 4 字节存储空间)时,得到的sizeof结果总是 4 个字节。

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main() {
int i = 5;
float f = 3.14;
char c = 'H';

printf("result have %ld byte\n", sizeof(i+f)); // result have 4 byte
printf("result have %ld byte\n", sizeof(c+f)); // result have 4 byte

return 0;
}

强制类型转换

可以利用强制类型转换将数据转换为需要的数据类型,使用格式为(目标数据类型)表达式,例如:(double)1(double)(3+5)都会将整型的结果转换为双精度浮点类型。

1
2
3
4
5
6
7
#include <stdio.h>

int main() {
char i = 'A';
printf("result still have %ld byte\n", sizeof((float)i)); // result still have 4 byte
return 0;
}

布尔类型 bool

C++当中存在专门的bool类型,但是 C89/90 当中没有提供专用的布尔类型,因此通常会在代码中使用基本整型来模拟布尔类型数据。

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#define true 1
#define false 0

typedef int bool;

void main() {
bool test = false;
if (test != true) {
printf("工作情况良好!\n");
}
}

C99 规范里新增了_Bool关键字,用于原生定义布尔类型,该类型占用一个字节存储空间,仅拥有01两个取值。

1
2
3
4
5
6
7
#include <stdio.h>

void main() {
_Bool true = 0;
_Bool false = 1;
printf("_Bool is %ld byte\n", sizeof(_Bool)); // _Bool is 1 byte
}

C99 当中由<stdbool.h>头文件将_Bool关键字重新定义为别名bool10分别被定义为了truefalse,因此引入该头文件后,可以直接使用bool作为声明布尔数据类型的关键字,使用truefalse作为布尔类型的取值。

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <stdbool.h>

int main(void) {
bool truly = true;
bool falsely = false;
printf("true is %d and false is %d.\n", truly, falsely); // true is 1 and false is 0.
return 0;
}

进行数据类型转换的时候,GCC 会将任意非零值自动转换为1,也就是true

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>

int main(void) {
bool year = 1985;
printf("Ops, year equals %d!\n", year); // Ops, year equals 1!
return EXIT_SUCCESS;
}

复数类型 complex

C99 新增了复数类型的关键字_Complex,正如同上面提到的_Bool类型与<stdbool.h>bool的关系一样,包含头文件<complex.h>之后,就可以方便的使用complex来代替_Complex关键字的使用。定义复数类型时,complex需要与浮点数据类型floatdoublelong double)组合起来使用。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <complex.h>

int main() {
printf("float complex %ld byte\n", sizeof(float complex)); // float complex 8 byte
printf("double complex %ld byte\n", sizeof(double complex)); // double complex 16 byte
printf("long double complex %ld byte\n", sizeof(long double complex)); // long double complex 32 byte

return 0;
}

GCC 暂不支持 ISO C99 当中定义的虚数类型_Imaginary

数组 array

数组是一组有序数据的集合,数组中每一个元素都是相同的数据类型,并且保存在一个连续的存储空间。C 语言中使用数组必须先声明其长度,以便于事先开辟一个指定大小的存储空间。例如:下图展示了一个包含有 10 个元素的数组int a[10],该数组每个元素的存储空间只能用于存放整型数据。

Linux C 当中,无论当前 GCC 编译选项是 C89 还是 C11,程序代码当中都可以动态的定义数组长度。例如下面的例子当中,根据控制台输入的数值来分配数组长度,然后再将动态分配后的数组长度打印出来。

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(int argc, char *argv[]) {
int number;
scanf("%d", &number); // 输入数组长度
int array[number];
printf("%ld\n", sizeof(array)/4); // 打印整型数组长度
return 0;
}

但是需要注意,数组长度一旦声明之后,就禁止再次进行修改,否则 GCC 会在编译过程中提示redeclaration of ‘array’ with no linkage错误,请看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main(int argc, char *argv[]) {
int number = 5;
int array[number];

number = 8;
int array[number];

return 0;
}

// main.c: In function ‘main’:
// main.c:8:9: error: redeclaration of ‘array’ with no linkage
// int array[number];
// ^~~~~
// main.c:5:9: note: previous declaration of ‘array’ was here
// int array[number];
// ^~~~~

如果使用static关键字将数组定义为静态存储方式,那么数组的长度必须是一个常量,否则 GCC 编译器依然会提示错误信息:

1
2
3
4
5
6
7
8
int main(int argc, char *argv[]) {
int number = 5;
static int array[number];
return 0;
}

// main.c:6:16: error: storage size of ‘array’ isn’t constant
// static int array[number];

初始化

对于一个已经声明了长度的数组,可以选择一次性初始化全部数组元素,也可以选择只初始化部分数组元素,长度之内未进行显式初始化的元素,将会被隐式的初始化为该数组的数据类型所对应的默认值,例如:整型数组缺省元素的默认值将会被置为0

1
2
3
4
5
6
7
int main(int argc, char *argv[]) {
int b[10] = {1,2,3,4,5,6,7,8,9}; // 初始化指定长度的数组。
int a[] = {1,2,3,4,5,6,7,8,9}; // 缺省数组长度进行初始化,数组长度为实际元素个数。
int c[10] = {1,2,3,4,5}; // 下面数组初始化结果为:{1,2,3,4,5,0,0,0,0,0}。

return 0;
}

二维数组结构上类似于线性代数中的矩阵,其声明与初始化方式与上面的一维数组类似,但是出于代码可读性的考虑,推荐使用{}符号对元素进行分组。

1
2
3
4
int main(int argc, char *argv[]) {
int [2,5] = [{1,2,3,4,5},{1,2,3,4,5}]; // 声明并初始化一个二维数组
return 0;
}

字符数组

字符类型数据以字符的 ASCII 编码进行存储,由于 ASCII 编码是整数形式,因此 C99 标准当中,将字符类型视为整型数据的一种。字符数组当中的每个元素存放一个字符,其定义与初始化的方式与普通数组类似。下面的示例代码,定义并初始化了一个名为c[]的字符数组,并最终将其打印至控制台:

1
2
3
4
5
6
7
#include <stdio.h>

int main() {
char c[] = {'I', ' ', 'a', 'm', ' ', 'H', 'a', 'n', 'k', '!'};
printf("%s\n", c); // I am Hank!
return 0;
}

上面例子代码当中,每个字符占据着1个字节的存储单元,数组c[]最终形成的存储结构如下图所示:

由于 C 语言当中没有字符串类型,因此字符串被存放在字符类型数组当中进行处理,请看接下来的例子:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main() {
char c1[] = {'H', 'a', 'n', 'k'};
printf("c1 is %ld bytes.\n", sizeof(c1)); // c1 is 4 bytes.

char c2[] = "Hank";
printf("c2 is %ld bytes.\n", sizeof(c2)); // c2 is 5 bytes.

return 0;
}

造成c1c2的长度各不相同的原因在于,Linux C 当中以双引号"..."直接声明字符串的时候,系统默认会在字符串最后添加一个'\0'字符作为结束标志'\0'的 ASCII 编码十进制形式为0,二进制形式为0000 0000,系统当中用来表示一个不可显示的空操作符

由上图可以看出,字符串"Hank"等效于字符数组{'H', 'a', 'n', 'k', '\0'}。使用printf()函数打印字符串或字符数组时,格式符%c表示输出一个字符,%s表示输出的是一个字符串,具体使用可以参数下面的例子:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main() {
char c1[] = {'H', 'a', 'n', 'k', '\0'};
printf("字符数组%s的是第1个字符是%c。\n", c1, c1[0]); // 字符数组Hank的是第1个字符是H。

char c2[] = "Hank";
printf("字符串%s的是第1个字符同样是%c。\n", c2, c2[0]); // 字符串Hank的是第1个字符同样是H。

return 0;
}

二维数组

二维数组也称为矩阵matrix),可以将其形象的理解为一个具有的表格。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main(int argc, char *argv[]) {
int matrix[3][5] = {
{1991, 1992, 1993, 1994, 1995},
{2001, 2002, 2003, 2004, 2005},
{2011, 2012}
}; // 声明并定义一个具有3行3列元素的整型二维数组

printf("第2行第1列保存的整数值为:%d\n", matrix[1][0]); // 第2行第1列保存的整数值为:2001
printf("第3行第5列保存的整数值为:%d\n", matrix[2][4]); // 第3行第5列保存的整数值为:0

return 0;
}

定义二维乃至多维数组时,对于没有显式进行赋值的数组元素,Linux C 将默认其值为该数组对应数据类型的缺省值,例如上面代码中,第 3 行第 5 列并未显式进行赋值定义,所以其值默认为整型数据的缺省值0,该二维数组在内存中的实际存储结构如下:

注意:由于数组每个元素都拥有连续的存储地址,其实际展现出存储结构也应是线性形态。但是上图为了形象体现二维数组矩阵的存储结构,所以将其抽象为了表格形态,但是图中的每一行元素都通过箭头与下一行首尾相连,以体现其真实存储地址的连续性。

结构体 struct

由于数组只能存放相同数据类型的数据,而结构体是一种由不同类型数据组成的组合型数据结构。使用时需要先声明结构体类型,再建立结构体变量。

1
2
3
4
struct 结构体类型名称 {
成员数据类型 成员数据名称;
... ...
};

结构体类型定义完成之后,就可以定义该结构体类型的变量,具体定义格式如下所示:

1
struct 结构体类型名称 结构体变量名称;

下面的代码定义了一个DateStudent结构体类型以及名为hank的结构体变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 声明Date结构体类型 */
struct Date {
int year;
int month;
int day;
};

/* 声明Student结构体类型 */
struct Student {
int age;
char name[10];
char address[20];
struct Date birthday; // 声明Date结构体变量
};

struct Student hank; // 声明Student结构体变量

定义结构体变量以后,系统会对其分配内存单元。结构体变量占用的内存长度是各数据类型成员所占用的长度之和,每个成员都拥有自己独立的内存单元。例如对于上面代码中定义的DataStudent结构体,声明并定义之后的内存布局如下图所示:

Linux C 当中可以不指定结构体类型的名称,而直接定义一个结构体变量,就像下面代码所展示的那样:

1
2
3
4
5
6
struct {
int age;
char name[10];
char address[20];
struct Date birthday;
} hank;

结构体变量定义完成之后,就可以开始对结构体成员进行初始化赋值,然后以结构体变量名称.成员名称的方式访问,接下来看一个从结构体类型定义、结构体变量声明、结构体初始化、引用并输出结构体成员值的完整例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main(int argc, char *argv[]) {
struct Date {
int year;
int month;
int day;
};

struct Date date = {2019, 3, 1}; // 初始化结构体变量
printf("%d年%d月%d日\n", date.year, date.month, date.day); // 2019年3月1日

return 0;
}

无论 GCC 采取哪种标准的编译选项,都可以在初始化结构体变量时,以.结构体成员名称 = 成员初始值的方式赋值给指定成员,其它未赋值成员的值将会是该成员数据类型所对应的缺省值(整型默认为0,浮点型默认为0.000000,字符型默认为\0,指针类型默认为NULL),修改一下上面例子代码,只对结构体成员year进行显式赋值操作,由于monthday都是整型数据,因此其默认值为0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int main(int argc, char *argv[]) {
struct Date {
int year;
int month;
int day;
};

struct Date date = {.year = 2019};

printf("%d年%d月%d日\n", date.year, date.month, date.day); // 2019年0月0日

return 0;
}

同类型的结构体变量可以相互赋值,修改一下上面例子程序的代码并执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

int main(int argc, char *argv[]) {
struct Date {
int year;
int month;
int day;
};

struct Date date1 = {.year = 2018, .month = 8, .day = 1};
struct Date date2 = date1; // 将结构体date1的成员值直接赋值给date2

printf("%d年%d月%d日\n", date2.year, date2.month, date2.day); // 2018年8月1日

return 0;
}

结构体数组与普通数组一样,只是每个元素都是结构体类型的数据,下面的代码定义并初始化了一个结构体数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Date {
int year;
int month;
int day;
};

struct Date calendar[6] = {
{2020, 1, 1},
{2020, 1, 2},
{2020, 1, 3},
{.year = 2020, .month = 1, .day = 4}
{.year = 2020, .month = 1, .day = 5}
{.year = 2020, .month = 1, .day = 6}
};

共用体 union

共用体可以在同一个地址开始的内存单元中存放几种不同的数据类型,正是由于占用内存的起始地址相同,所以共用体某一时刻只能存放一个数据,而不能同时存放多个;下面展示了共用体定义的基本格式,可以看到语法上与结构体非常类似:

1
2
3
4
union 共用体类型名称 {
成员数据类型 成员数据名称;
... ...
}

共用体拥有让一个变量同时具备多种数据类型的能力,接下来,我们声明一个称为Variable的结构体变量,该变量拥有存放字符型、整型、浮点型数据的能力。

1
2
3
4
5
6
7
union Variable {
char character; // 有符号字符型,占用1个字节空间。
int integer; // 基本整型,占用2个字节空间。
float real; // 单精度浮点型,占用4个字节空间。
}

Variable.integer = 100; // 将整数100存储在Variable结构体变量。

共用体变量所占用的内存长度,等于内存占用最长的那个数据类型的变量长度。因此,上面定义的Variable结构体变量占用的存储空间为4个字节,即占用内存空间最大的浮点型数据real的长度。

共用体也可以不指定共用体类型的名称,而直接定义一个共用体变量;但是需要注意,由于共用体内成员占据的都是同一块存储空间,因此定义共用体变量以后,只能初始化其中的一个成员,比如像下面这样:

1
2
3
4
5
union {
char character;
int integer;
float real;
} character = {'H'}; // 初始化第1个成员character

共用体变量定义完成之后,在代码当中并不能直接引用,只能通过共用体变量名称.成员名称引用或初始化共用体中的成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main(int argc, char *argv[]) {
union Variable {
char character;
int integer;
float real;
};

union Variable demo;
demo.integer = 2012; // 初始化成员integer
printf("%d\n", demo.integer); // 2012
}

GCC 也支持显式的初始化共用体的指定成员,请接着阅读下面示例代码:

1
2
3
4
5
6
7
union Variable {
char character;
int integer;
float real;
};

union Variable demo = {.integer = 'U'}; // 仅初始化指定成员

由于共用体变量中保存的成员总是最后一次赋值的成员,所以每次赋值操作都会覆盖之前保存的成员状态。此外,因为共用体成员之间都共享着一个起始地址,所以共用体变量的地址与其成员的地址都是相同的,这是与结构体非常不同的一点,接下来的示例代码将会非常好的展示这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main(int argc, char *argv[]) {
union Variable {
char character;
int integer;
float real;
};

union Variable demo;

demo.integer = 2012;
printf("当前共用体装载的是成员integer的值%d地址为%p\n", demo.integer, &demo.integer); // 当前共用体装载的是成员integer的值2012地址为0x7ffd99356564

demo.real = 3.14;
printf("当前共用体装载的是成员real的值%f地址为%p\n", demo.real, &demo.real); // 当前共用体装载的是成员real的值3.140000地址为0x7ffd99356564
}

Linux C 允许相同类型的共用体变量相互进行赋值,也可以定义一个共用体数组,甚至出现在结构体类型的定义当中。

枚举类型 enum

如果变量拥有几种可能的值,那么就可以考虑将该变量定义为枚举类型,枚举类型变量的取值范围仅限于枚举类型定义的范围。枚举类型的声明格式如下:

1
enum 枚举类型名称 {枚举元素列表}

根据上面的声明格式,我们将在下面代码中声明一个枚举类型Week,然后初始化一个枚举变量today,并将其结果打印出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

int main(int argc, char *argv[]) {
enum Week {
Monday = 1,
Tuesday = 2,
Wednesday = 3,
Thursday = 4,
Friday = 5,
Saturday = 6,
Sunday = 7
};

enum Week today = Friday;
printf("今天是星期%d\n", today); // 今天是星期5

return 0;
}

GCC 编译器对于枚举类型元素会按照常量进行处理,因此这些元素也被称为枚举常量,而既然是常量就不能对其直接进行赋值操作,否则编译器将会提示错误信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

int main(int argc, char *argv[]) {
enum Person {
youth = 18,
midlife = 30,
elder = 50
};

midlife = 20; // 不能对枚举常量进行赋值操作

return 0;
}

// main.c: In function ‘main’:
// main.c:12:11: error: lvalue required as left operand of assignment
// midlife = 20;
// ^

枚举类型每个元素的值默认为一个整型数据,GCC 编译器会按照123...的序数顺序为枚举类型的每个元素赋值,如果将枚举类型元素赋值为一个非整型数据,那么 GCC 编译器将会提示枚举值不是一个整型常量的错误信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main(int argc, char *argv[]) {
enum Circular {
pi = 3.14,
};

enum Circular value = pi;
printf("圆周率PI的值为%d\n", pi); // 5

return 0;
}

// main.c: In function ‘main’:
// main.c:5:10: error: enumerator value for ‘pi’ is not an integer constant
// pi = 3.14,
// ^~~~

正是由于枚举值是一个整型(int)的常量,因此其占用的存储空间总是整型数据所占用的存储空间,接着看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

int main(int argc, char *argv[]) {
enum Ordinal {
first = 1,
second = 2,
third = 3
};

enum Ordinal two = second;

// // Ordinal枚举类型变量two的枚举值为2,它占用了4个字节存储空间!
printf("Ordinal枚举类型变量two的枚举值为%d,它占用了%ld个字节存储空间!\n", two, sizeof(two));

return 0;
}

枚举与其它的复合数据类型一样,可以不用声明枚举类型名称,而直接定义枚举类型变量,因此上面的例子也可以改写为下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main(int argc, char *argv[]) {
enum {
first = 1,
second = 2,
third = 3
} two = second;

// Ordinal枚举类型变量two的枚举值为2!
printf("Ordinal枚举类型变量two的枚举值为%d!\n", two);

return 0;
}

运算符

运算符用于执行程序程序代码当中的运算操作,使用时需要注意优先级结合性运算目数方面的事项,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
2
3
4
5
6
7
#include <stdio.h>

int main(int argc, char *argv[]) {
if (3) {
printf("非零数值皆为真!\n"); // 非零数值皆为真!
}
}

if 结构

如果表达式结果为true,那么执行语句;如果表达式结果为false,那么跳过该选择结构并执行后面语句。

1
2
3
if(表达式) {
语句
}

如果表达式结果为true执行语句1;如果表达式结果为false执行语句2

1
2
3
4
5
6
if(表达式) {
语句1
}
else {
语句2
}

如果表达式1结果为true就执行语句1并跳过该选择结构;如果表达式1结果为false就继续判断表达式2,如果表达式2结果为true就执行语句2并跳过该选择结构,如果表达式2结果为false,那么就直接执行语句3,最后跳出该选择结构并执行后续代码。

1
2
3
4
5
6
7
8
9
if(表达式1) {
语句1
}
else if(表达式2) {
语句2
}
else {
语句3
}

if 语句中的表达式,可以是关系表达式、逻辑表达式、甚至数值表达式,实际开发过程当中注意灵活进行使用;接下来编写一个关于if结构的完整 Linux C 程序示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main(int argc, char *argv[]) {
char alphabet = 'U';

if (alphabet == 'Z') {
printf("这是true条件下的语句!\n");
}
else if (alphabet == 'U') {
printf("这是false条件下的语句!\n"); // 执行结果:这是false条件下的语句!
}
else {
printf("都错了!\n");
}

return 0;
}

GCC 当中,只要包含了<stdbool.h>头文件,就可以方便的在逻辑运算和选择结构当中使用bool类型以及truefalse关键字,修改一下上面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdbool.h>

int main(int argc, char *argv[]) {
if (false) {
printf("这是true条件下的语句!\n");
}
else if (true) {
printf("这是false条件下的语句!\n"); // 执行结果:这是false条件下的语句!
}
else {
printf("都错了!\n");
}

return 0;
}

switch 结构

下面的伪代码当中,switch语句上表达式的结果必须是整型或者字符型,否则 GCC 编译器会提示error: switch quantity not an integer错误信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
switch (表达式) {

case 常量1:
语句1
break;

case 常量2:
语句2
break;

default:
break;
}

同样的,在接下来的代码当中,我们将会编写一个关于switch语句的完整例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdbool.h>

int main(int argc, char *argv[]) {
char alphabet = 'U';

switch (alphabet) {
case 'H':
printf("这是字母H!\n");
break;
case 'U':
printf("这是字母U!\n"); // 执行结果:这是字母U!
break;
default:
printf("都错了!\n");
break;
}

return 0;
}

循环结构

面向过程的程序开发当中,通常拥有顺序结构选择结构循环结构三种基本结构,本节内容将会来介绍相对更为繁琐一些的循环结构。

while 结构

表达式结果为true时执行循环体中的语句,为false时跳出循环体执行后续其它语句。

1
2
3
while(表达式){
语句
}
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main(int argc, char *argv[]) {
int index = 0;

while (index < 6) {
printf("当前执行了第%d次\n", index);
index++;
}

return 0;
}

do while 结构

首先无条件执行循环体中的语句,然后检查表达式内的条件,如果表达式结果为true就继续执行循环体,如果表达式结果为false就跳出循环体。

1
2
3
do {
语句
} while (表达式)
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main(int argc, char *argv[]) {
int index = 0;

do {
printf("当前执行了第%d次\n", index);
index++;
} while (index < 6);

return 0;
}

for 结构

首先执行表达式1,主要用于设置初始条件,只执行一次; 然后执行表达式2,主要用于判断循环是否继续进行; 最后执行表达式3,主要用于循环变量进行自增操作。

1
2
3
for (表达式1; 表达式2; 表达式3) {
语句
}
1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main(int argc, char *argv[]) {
int index;

for (index = 0; index < 6; index++) {
printf("当前执行了第%d次\n", index);
}

return 0;
}

上面的whiledo whilefor三个例子程序,都是在循环索引小于6的条件之下打印当前执行的索引值,因而执行的结果都是相同的:

1
2
3
4
5
6
当前执行了第0次
当前执行了第1次
当前执行了第2次
当前执行了第3次
当前执行了第4次
当前执行了第5次

continue 关键字

continue用于跳出当前循环,仅能作用于循环结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

int main(int argc, char *argv[]) {
int index;

for (index = 0; index < 6; index++) {
if (index == 3) {
printf("当前for循环的第%d次就将会被continue\n", index);
continue;
}
}

return 0;
}

// 当前for循环的第3次就将会被continue

break 关键字

break用于终止整个循环,可用于循环结构以及switch选择结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

int main(int argc, char *argv[]) {
int index;

for (index = 0; index < 6; index++) {
if (index == 3) {
break;
}
printf("当前for循环仅执行至第%d次就将被break\n", index);
}

return 0;
}

// 当前for循环仅执行至第0次就将被break
// 当前for循环仅执行至第1次就将被break
// 当前for循环仅执行至第2次就将被break

使用函数

一个 Linux C 程序通常由一个主函数和若干的其它函数组成,Linux C 当中的函数遵循先定义后使用的原则,对于 GCC 编译系统提供的库函数,需要先使用#include指令将头文件包含至源码文件当中,下面伪代码展示了定义一个带参函数的方法:

1
2
3
4
5
6
返回值数据类型 函数名称( 形式参数类型 形式参数名称, ... ) {
... ...
函数体;
... ...
return 返回值;
}

函数定义完成之后,就可以通过下面的格式进行函数的调用:

1
函数名称(实际参数值);

函数调用后返回结果的数据类型与函数定义中的返回值数据类型一致,如果函数不需要接收返回值参数,则必须使用void关键字显式进行定义。

1
2
3
void 函数名称( void ) {
函数体;
}

与其它编程语言不同,Linux C 当中无任何参数的函数function()是指当前函数的参数个数不确定,而显式定义了void关键字参数的function(void)才是指当前函数没有任何参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void function(void) {} // 定义void参数表示该函数不接受任何参数。

int main(int argc, char *argv[]) {
function("Hank");
return 0;
}

// main.c: In function ‘main’:
// main.c:5:3: error: too many arguments to function ‘function’
// function("Hank");
// ^~~~~~~~
// main.c:1:5: note: declared here
// int function(void) {}
// ^~~~~~~~

因而上面例子中,函数function(void)由于显式定义了void参数,因此 GCC 编译器会报出参数个数过多的错误信息,如果移除函数定义中的void关键字,function()就可以正常通过编译。

同样的,当函数没有返回值时,函数定义中除了返回值类型使用void以外,函数内部也不需要再使用return语句,下面的例子代码定义了一个返回值类型为void并且不具有return语句的函数:

1
2
3
void function(void) {
printf("这是一个没有返回值的函数");
}

由于函数必须遵循先定义后使用原则,当主函数位于其它函数前面时,必须将其它函数的声明语句放置到主函数之前,确保代码顺序执行时主函数能识别后面声明的函数内容。函数声明有时也被称为函数原型,如下两种函数声明方法在 GCC 中都有效:

  • 返回值数据类型 函数名称( 形式参数类型 );
  • 返回值数据类型 函数名称( 形式参数类型 形式参数名称 );
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

void function1(char[]);
void function2(char string[]);

int main(int argc, char *argv[]) {
function1("这是函数1");
function2("这是函数2");
return 0;
}

void function1(char string[]) {
printf("%s\n", string); // 这是函数1
}

void function2(char string[]) {
printf("%s\n", string); // 这是函数2
}

上面的例子当中,使用了字符数组作为函数的参数,Linux C 当中规定使用数组作为函数的实际参数时,函数形式参数得到的将会是该数组首元素的地址,请看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

void function(int[]);

int main(int argc, char *argv[]) {
int array[] = {1, 2, 3, 4, 5};
printf("%p\n", &array[0]); // 0x7ffc0c4f6060
function(array); // 0x7ffc0c4f6060
return 0;
}

void function(int array[]) {
printf("%p\n", array);
}

正是由于数组作为函数参数时,传递的是数组的首地址;当在接收数组参数的函数内部,对数组进行修改操作之后,将会改变所有引用该数组名称的数组元素内容,这一点需要特别注意。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>

void function(int[]);
void traversal(int[]);

int main(int argc, char *argv[]) {
int array[] = {1, 2, 3, 4, 5};
traversal(array); // 1 2 3 4 5
function(array);
traversal(array); // 1 2 3 4 100
return 0;
}

void function(int array[]) {
array[4] = 100;
traversal(array); // 1 2 3 4 100
}

/* 遍历打印数组内容 */
void traversal(int array[]) {
int index;
for (index = 0; index < 5; index++) {
printf("%d\n", array[index]);
}
}

综上,使用数组元素作为实际参数时,向形式参数变量传递的是数组元素的值。而用数组名称作为函数实际参数时,向形式参数(数组名、指针变量)传递的是数组首元素的地址。

GCC 当中无论编译规范选择 C89/C90 或者 C99 乃至 C11,主函数返回类型是否为intvoid以及函数最后是否有return语句,编译都能够正常通过并且没有任何警告信息。

变量作用域与函数

函数内定义的变量称为局部变量,函数外定义的变量称为全局变量,习惯上会将全局变量的首字母进行大写处理。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int Test = 2019;

int main(int argc, char *argv[]) {
int test = 2006;
printf("局部变量test的值为%d\n", test); // 局部变量test的值为2006
printf("全局变量Test的值为%d\n", Test); // 全局变量Test的值为2019
return 0;
}

多个函数共享一个全局变量,很容易造成数据操作的结果相互污染,因此使用全局变量时需要格外谨慎。

函数的定义和声明通常都是全局的,但是 Linux C 允许在函数内部再嵌套定义一个函数,类似于局部变量,该函数只能在定义它的那个函数内部有效,因此也可以称为局部函数。下面代码当中,主函数main()内部定义了一个inner()并正常调用,

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main(int argc, char *argv[]) {
int Test = 2019;

void inner() {
printf("这是一个引用了局部变量%d的嵌套函数!\n", Test); // 这是一个引用了局部变量2019的嵌套函数!
}
inner();

return 0;
}

局部函数仅在主函数内可见,当在主函数外进行调用时 GCC 编译器会提示语法错误。

变量存储类型

变量的存储方式分为动态(程序运行期间按需动态的进行分配)和静态(程序运行期间由系统固定分配)两种,现代操作系统当中,用户的内存空间一般会分为程序存储区静态存储区动态存储区三块区域。

用户数据主要放置在动态和静态存储区当中,静态存储区主要保存全局变量以及static声明的静态局部变量,程序执行时为其分配存储空间,执行完毕后释放这些空间。程序运行过程中,全局变量占据固定存储单元,无须动态分配与释放。

动态存储区会在函数开始调用时动态分配存储空间,调用完毕后释放这些存储空间,分配与释放都是伴随函数调用动态进行的。动态存储区存放的数据主要分为以下 3 类:

  1. 函数的形式参数,调用函数时会为其动态分配存储空间;
  2. 函数内声明的自动变量,即未使用static关键字声明的非静态变量。
  3. 函数调用时的现场保护和返回地址。

GCC 当中的每个变量函数都拥有数据类型与存储类型(数据在内存中的存储方式,即静态和动态存储)两个属性, 因此在声明变量与函数时,应当同时声明其数据与存储类型。目前 GCC 支持的存储类型主要包括自动(auto)、静态(static)、寄存器(register)、外部(extern),接下来将会逐一进行介绍。

自动 auto

Linux C 函数中的局部变量默认为自动存储类型,函数中定义的局部变量和形式参数都属于这个类型,如果需要显式进行声明,可以使用auto关键字:

1
2
3
4
5
6
7
8
#include <stdio.h>

int main(int argc, char *argv[]) {
auto float pi = 3.14;
printf("显式声明了一个自动变量:%f\n", pi); // 显式声明了一个自动变量:3.140000

return 0;
}

静态 static

静态局部变量使用static关键字声明,保存在用户内存的静态存储区当中,其值在函数调用结束后并不会释放存储单元;下一次函数调用时,该静态局部变量依然继续保留原值。因为静态局部变量在程序运行过程中占据固定的存储单元,所以缺省赋值的情况下,GCC 编译器会默认赋予相应数据类型的初值(整形变量缺省为0,字符型变量缺省为\0空操作符)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int method();

int main(int argc, char *argv[]) {
method(); // method()中静态局部变量index的值为:1
method(); // method()中静态局部变量index的值为:2
return 0;
}

int method() {
static int index = 0; // 程序执行过程中,静态局部变量index的状态将会得到保留
printf("method()中静态局部变量index的值为:%d\n", ++index);
}

静态局部变量不能被其它函数引用,而只能被变量声明所在的函数使用

寄存器 register

传统计算机体系结构里,静态/动态变量都存储在计算机内存单元当中,CPU 通过指令集与内存进行交互。为了提高执行效率,可以将一些存取较为频繁的变量放置到 CPU 寄存器当中,GCC 当中可以使用register关键字声明这样的变量。

1
2
3
4
5
6
7
8
#include <stdio.h>

int main(int argc, char *argv[]) {
register int tmall = 1111;
printf("声明了一个寄存器变量:%d\n", tmall); // 声明了一个寄存器变量:1111

return 0;
}

现代计算机体系结构,CPU 已经可以智能的将常用变量调度至寄存器,而毋需手动再进行声明配置。

外部 extern

这里简单总结一下,auto关键字声明的自动变量保存在用户内存的动态存储区static关键字声明的静态局部变量保存在用户内存的静态存储区register关键字声明的寄存器变量保存在 CPU 的寄存器区,它们都是局部变量

接下来将要介绍的extern关键字,则主要作用于全局变量。一般情况下,全局变量存放在静态存储区的,通常其作用域是从变量定义处至程序文件末尾,在此作用域内的代码都可以引用该全局变量。而通过使用extern关键字,可以方便的扩展全局变量的作用域。

(1)extern可以在一个源码文件内部扩展全局变量的作用域:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

void method();

int main(int argc, char *argv[]) {
extern char A, B;
A = 'a';
B = 'b';
method(); // A的小写是a,B的小写是b!
return 0;
}

char A, B;

void method() {
printf("A的小写是%c,B的小写是%c!\n", A, B);
}

上面代码当中,extern关键字扩展了全局变量char A, B;的作用域到main()函数。但是事实上,GCC 已经自带了全局变量作用域提升的特性,即使此处不向main()函数当中添加extern关键字,依然能正常编译运行,运行结果与添加extern关键字的情况相同。

(2)extern也可以将一个源码文件内部全局变量的作用域扩展至其它源文件当中:

首先,按照如下目录结构建立源文件:

1
2
3
4
5
➜  tree
.
├── main.c
└── utils
└── method.h

method.h添加如下代码,其中定义了字符类型的全局变量AB

1
2
3
4
5
6
7
8
#include <stdio.h>

char A = '?';
char B = '?';

void method() {
printf("A的小写仍然是%c,B的小写仍然是%c!\n", A, B);
}

将上面method.h源文件当中定义的变量作用域通过extern关键字扩展至main.c的主函数当中:

1
2
3
4
5
6
7
8
9
10
void method();

int main(int argc, char *argv[]) {
extern char A, B;
A = 'a';
B = 'b';
method(); // A的小写仍然是x,B的小写仍然是b!

return 0;
}

执行gcc main.c utils/method.c命令将两个源文件编译为一个可执行文件,输出结果为:A的小写仍然是x,B的小写仍然是b!

本源文件中的extern变量优先级高于外部源文件中的extern变量优先级。

(3)如果需要限制该全局变量仅在本源码文件中使用,那么可以通过static关键字将该全局变量声明为静态的,这样其它文件中就无法使用extern关键字扩展其作用域,现在将method.c内的全局变量AB声明为static

1
2
3
4
5
6
7
8
#include <stdio.h>

static char A = '?';
static char B = '?';

void method() {
printf("A的小写仍然是%c,B的小写仍然是%c!\n", A, B);
}

源文件main.c保持对全局变量AB的声明不变,执行后 GCC 报出以下错误信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void method();

int main(int argc, char *argv[])
{
extern char A, B;
A = 'a';
B = 'b';
method(); // A的小写仍然是a,B的小写仍然是b!

return 0;
}

// /tmp/cczIQZ43.o:在函数‘main’中:
// main.c:(.text+0x11):对‘A’未定义的引用
// main.c:(.text+0x18):对‘B’未定义的引用
// collect2: error: ld returned 1 exit status

值得注意的是:局部变量声明static存储类型是为了指定存储区域,而全局变量本身就分配在静态存储区,添加static存储类型只是为了控制其作用域的扩展

函数存储类型

函数编写的目的是为了让其它函数调用,因而函数本质上是全局的。但是根据函数能否被其它源文件中的代码调用,将函数分为内部函数外部函数

内部函数 static

内部函数只能被本源文件当中的代码调用,定义时只需要在函数返回类型之前添加static关键字,因此也称为静态函数

1
2
3
static 返回值数据类型 函数名称(参数列表) {
// 代码
}

内部函数在多人协作开发时,可以有效的防止多个源文件中定义的函数名称相互污染。现在将变量存储类型小节的例子程序稍作修改,向utils目录下method.c当中的method()函数添加static关键字,使其成为一个不能被其它源文件中代码调用的内部函数。

1
2
3
4
5
6
#include <stdio.h>

static void method()
{
printf("这是一个内部函数,也称为静态函数!\n");
}

接下来在main.c源文件中声明method()并调用该函数,此时执行gcc main.c utils/method.c编译控制台报出错误信息:

1
2
3
4
5
6
7
8
9
10
void method();

int main(int argc, char *argv[]) {
method();
return 0;
}

// /tmp/ccCcRM70.o:在函数‘main’中:
// main.c:(.text+0x15):对‘method’未定义的引用
// collect2: error: ld returned 1 exit status

外部函数 extern

外部函数可以方便的被其它源文件中的代码调用,Linuc C 当中函数默认为外部函数,也可以显式的使用extern关键字进行定义。

1
2
3
extern 返回值数据类型 函数名称(参数列表) {
// 代码
}

现在将前面例子中的method()函数显式添加上extern关键字,使其成为一个外部函数。

1
2
3
4
5
#include <stdio.h>

extern void method() {
printf("这是一个使用extern关键字声明的外部函数!\n");
}

接下来在main.c中调用该method()外部函数,并执行gcc main.c utils/method.c成功编译。

1
2
3
4
5
6
void method();

int main(int argc, char *argv[]) {
method(); // 这是一个内部函数,也称为静态函数!
return 0;
}

剖析指针

变量的地址(通过取地址运算符&获得)称为该变量的指针,存放地址/指针的变量(使用指针运算符*声明)称为指针变量,其声明格式如下:

1
数据类型 *指针变量名称

指针变量由 Linux C 基本数据类型派生而来,由于每种数据类型的存放方式与占用空间不同,为了确保指针运算能够正确的得到执行,指针变量声明时必须指定其所属数据类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main(int argc, char *argv[]) {
short number = 2019; // 定义一个短整型变量,占据2个字节存储空间
short *pointer = &number; // 声明一个指针类型变量,占据8个字节存储空间

printf("短整型变量number当前的存储地址是:%p\n", &number); // 短整型变量number当前的存储地址是:0x7ffdba5e89fe
printf("指定变量pointer存放的指针地址是:%p\n", pointer); // 指定变量pointer存放的指针地址是:0x7ffdba5e89fe
printf("指定变量pointer自身的存储地址是:%p\n", &pointer); // 指定变量pointer自身的存储地址是:0x7ffdba5e8a00
printf("指定变量pointer指向地址保存的数据是:%d\n", *pointer); // 指定变量pointer指向地址保存的数据是:2019
printf("指针类型总是占据%ld个字节存储空间\n", sizeof(pointer)); // 指针类型总是占据8个字节存储空间

return 0;
}

注意:对于一个已经声明并定义完成的指针变量,使用地址运算符&指针变量名称只能得到该指针变量的地址,而通过指针运算符*指针变量名称则可以获取其保存的真实数据。

将指针变量作为函数的参数时,传入的实质上是变量的存储地址,因此函数中对该指针变量进行的任何操作,其结果都会反映至该指针实际指向变量的状态,请仔细理解接下面的示例程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int method(char *param);

int main(int argc, char *argv[]) {
int array[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
char symbol = 'H';
char *pointer = &symbol;
method(pointer);
printf("主函数指针变量pointer指向的值依然是%c\n", *pointer); // 主函数指针变量pointer指向的值依然是U
return 0;
}

int method(char *param) {
*param = 'U';
printf("method()函数参数传入的指针变量被修改为%c\n", *param); // method()函数参数传入的指针变量被修改为U
}

指向数组的指针

一个数组包含有若干个元素,每个元素都占据着相应存储单元并拥有各自的地址。指针变量即可以指向基本数据类型变量,也可以指向数组中的元素。Linux C 语言当中,数组名称就代表数组当中首元素的地址,请参考下面的示例代码:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main(int argc, char *argv[]) {
int array[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
printf("%p\n", array); // 0x7fff1d270770
printf("%p\n", &array[0]); // 0x7fff1d270770
if (&array[0] == array) {
printf("数组名称array就代表数组当中首元素array[0]的地址\n");
}
return 0;
}

数组指针的运算

当指针指向数组元素时,可以对指针进行加+-运算(包括自增++自减--运算),此时指针会基于所属数据类型占用的字节数进行前后移动,进而达到通过移动指针来访问数组各个元素的目的。

1
2
3
4
5
6
7
8
#include <stdio.h>

int main(int argc, char *argv[]) {
int array[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int *pointer = &array[3] + 2;
printf("指针pointer当前指向的数组元素是%d\n", *pointer); // 指针pointer当前指向的数组元素是5
return 0;
}

通过指针访问数组元素

除了使用传统的下标数组名称[元素位置]访问数组以外,Linux C 还支持使用指针以*(数组名+元素位置)的形式来进行访问。

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(int argc, char *argv[]) {
int array[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
printf("以下标方式访问数组%d\n", array[5]); // 以下标方式访问数组5
printf("以指针方式访问数组%d\n", *(array + 5)); // 以指针方式访问数组5

return 0;
}

使用指针方式访问数组元素,通常会获得更优的执行效率。

使用数组名称作为函数参数

使用数组名称作为函数参数时,由于实际向函数传递的是数组首元素地址,因此函数中对于该数组进行的任何修改,同样也将会反映到原始声明的数组上,这一特性与前面提到的向函数传递基本数据类型指针相似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

void method(char *param);

int main(int argc, char *argv[])
{
char *string = "I love this land!";
method(string);
printf("主函数中字符类型指针所指向的字符串内容依然为:%s\n", string); // 主函数中字符类型指针所指向的字符串内容依然为:I love this land!

return 0;
}

void method(char *param)
{
char *pointer = param; // GCC当中,此处必须显式将指向字符串的字符指针类型形式参数赋值给一个局部变量。
pointer = "I also love this ocean!";
printf("method()函数将参数传入的字符类型指针变量值修改为:%s\n", pointer); // method()函数将参数传入的字符类型指针变量值修改为:I love this land!
}

指向字符串的指针

Linux C 语言没有字符串数据类型,字符串都是以字符数组形式保存。可以通过数组名和下标引用字符串中的字符,或者使用格式字符串%s和数组名称输出整个字符串,也可以通过字符类型的指针变量引用一个字符串常量,此时指针变量保存的是字符串首个字符的地址,就像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main(int argc, char *argv[]) {
char name1[] = "uinika"; // 声明一个字符数组
printf("通过数组名打印字符串:%s\n", name1); // 通过数组名打印字符串:uinika
printf("通过数组名打印字符串中的一个元素:%c\n", name1[0]); // 通过数组名打印字符串中的一个元素:u

char *name2 = "hank"; // 声明一个字符指针
printf("使用字符指针打印字符串:%s\n", name2); // 使用字符指针打印字符串:hank
printf("使用字符指针打印字符串中的一个元素:%c\n", name2[0]); // 使用字符指针打印字符串中的一个元素:h

return 0;
}

虽然可以通过下标[]方便的修改字符串数组中指定元素的值,但是字符指针变量所指向字符串当中的元素值是不可修改的,否则 GCC 编译器将会提示错误信息:[1] 25465 segmentation fault(core dumped)./ a.out

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(int argc, char *argv[]) {
char *pointer = "uinika";
pointer[5] = 'H'; // [1] 25465 segmentation fault(core dumped)./ a.out
printf("%s\n", pointer);

return 0;
}

由于字符指针变量实际保存的是字符串首元素的地址,因此可以像字符数组一样,通过与整数进行加减运算来完成字符串截取功能。

1
2
3
4
5
6
7
8
#include <stdio.h>

int main(int argc, char *argv[]) {
char *pointer = "uinika";
printf("截取后的字符串结果:%s\n", pointer + 3); // 截取后的字符串结果:ika

return 0;
}

可以改进一下上面的代码,让printf()成为一个可变格式输出的函数,也就是将格式字符串声明并定义为一个字符数组,提升代码可读性。

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(int argc, char *argv[]) {
char *pointer = "uinika";
char format[] = "截取后的字符串结果:%s\n"; // 这是一个格式字符串变量
printf(format, pointer + 3); // 截取后的字符串结果:ika

return 0;
}

字符指针与字符数组使用上最显著的区别在于:字符数组当中元素的值是可以进行修改的,而字符指针变量所指向字符串常量中的元素不可被修改,否则 GCC 编译时将会提示错误信息[1] 18548 segmentation fault (core dumped) ./a.out

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main(int argc, char *argv[]) {
char array[] = "uinika";
array[0] = 'U';
printf("将首字母替换为大写:%s\n", array); // 将首字母替换为大写:Uinika

char *pointer = "uinika";
pointer[0] = 'U';
printf("截取后的字符串结果:%s\n", pointer); // [1] 18548 segmentation fault (core dumped) ./a.out
return 0;
}

由于字符指针变量指向的字符串常量里的元素不能修改,所以当字符指针作为参数传递至其它函数进行处理时,如果重新将一个字符串常量赋值给该字符指针,那么该字符指针将仅仅被作为一个普通的函数局部变量进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

void method(char *pointer);

int main(int argc, char *argv[]) {
char *string = "I love this land!";
method(string);
// 主函数中字符类型指针所指向的字符串内容依然为:I love this land!
printf("主函数中字符类型指针所指向的字符串内容依然为:%s\n", string);
return 0;
}

void method(char *pointer) {
pointer = "I also love this ocean!";
// method()函数将参数传入的字符类型指针变量值修改为:I also love this ocean!
printf("method()函数将参数传入的字符类型指针变量值修改为:%s\n", pointer);
}

上面例子当中,method()函数对传入的字符指针参数pointer重新进行了赋值,但是主函数中string指针所指向的字符串常量并未发生改变。这是由于字符指针总是占有 8 个字节存储空间,保存的总是字符串常量在内存中首个元素的地址。因此,当在method()函数内声明一个字符串常量并赋值给pointer指针的时候,仅仅是改变了形式参数pointer所指向的字符串常量首地址值,而并不会影响主函数中字符指针string所指向的字符串常量地址,因而pointer仅仅被视为一个普通的局部函数。

函数与指针

GCC 编译器会为 Linux C 函数分配一段存储空间,这段存储空间的起始地址称为函数的指针,用来保存函数指针地址的变量称为函数的指针变量,声明一个函数指针的具体格式如下:

1
返回类型 (*函数指针名称)(形式参数...)

函数指针变量只能指向其声明时所指定类型的函数,Linux C 当中除使用函数名调用函数以外,还可以通过函数指针来进行调用,具体调用格式如下所示:

1
(*函数指针名称)(实际参数...)

通过函数指针调用函数时,必须先让声明的函数指针变量指向该函数,来看下面这个函数指针实际应用的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int main(int argc, char *argv[]) {
int method(int integer); // 函数声明
int (*function)(int integer); // 函数指针声明
function = method; // 让函数指针去指向method函数
int value = (*function)(2018); // 通过指针变量调用函数
printf("函数method返回结果:%d\n", value); // 函数method返回结果:2018

return 0;
}

int method(int integer) {
return integer;
}

声明函数指针时,可以缺省函数的参数名称,而只需要保留数据类型,即int (*function)(int integer);亦可以写作int (*function)(int);

与前面小节内容提到的数组指针、指向字符串的字符指针不同,函数的指针变量不能进行算术运算,否则 GCC 编译器将提示[1] 23924 segmentation fault (core dumped) ./a.out错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int main(int argc, char *argv[]) {
int method(int integer); // 函数声明
int (*function)(int); // 定义函数指针
function = method; // 让函数指针去指向method函数
function += 5;
int value = (*function)(2018); // [1] 24115 segmentation fault (core dumped) ./a.out

return 0;
}

int method(int integer) {
return integer;
}

函数指针作为参数

指向函数的指针可以作为其它函数的参数,即将函数的入口地址传递给其它函数的形式参数,以便于在该函数中方便的调用参数指针所指向的那个函数。这样做的好处在于不需要修改主调函数中的任何代码,就可以通过方便的更换传递给它的函数指针参数,替换相应的功能,具体请参考下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>

int main(int argc, char *argv[]) {
/* 声明函数 */
int addition(int a, int b);
int subtraction(int a, int b);
int calculator(int (*counter)(int, int));

/* 定义指向函数的指针 */
int (*add)(int, int) = addition;
int (*sub)(int, int) = subtraction;

/* 调用函数并传入函数指针作为参数 */
int result = calculator(add);
printf("执行加法函数addition的结果:%d\n", result); // 执行加法函数addition的结果:4004

/* 再次调用函数并传入函数指针作为参数 */
result = calculator(sub);
printf("执行减法函数subtraction的结果:%d\n", result); // 执行减法函数subtraction的结果:-34

return 0;
}

/** 加法函数 */
int addition(int a, int b) {
return a + b;
}

/** 减法函数 */
int subtraction(int a, int b) {
return a - b;
}

/** 主调函数 */
int calculator(int (*counter)(int, int)) {
return counter(1985, 2019);
}

由于 Linux 内核源代码当中,大量使用了指向函数的指针作为函数的参数,因而本小节内容是 Linux C 编程开发当中一个较为重要的知识点。

返回指针值的函数

Linux C 函数除了返回整型、字符型、浮点类型等基本数据类型以外,还能够返回指针数据类型,即返回一个存储地址值。一个返回指针值的函数定义格式如下:

1
2
3
返回类型 *函数名称(形式参数...) {
return 指针类型数据;
}

接下来,结合前面章节中提到的指向字符串的字符指针、以及指针作为函数参数的内容,编写一个返回字符类型指针的示例程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

char *function(char *parameter);

int main(int argc, char *argv[]) {
char *string = "hello world";
printf("指向字符串的字符指针变量string的地址为:%p\n", string); // 指向字符串的字符指针变量string的地址为:0x55869dde8778
printf("function()函数返回的字符型指针变量地址:%p\n", function(string)); // function()函数返回的字符型指针变量地址:0x55869dde8778

(string == function(string)) ? printf("它们地址相等!\n") : printf("它们地址不相等!\n"); // 它们地址相等!

return 0;
}

char *function(char *parameter) {
char *result = parameter;
return result;
}

指针数组

数组元素全部为指针类型的数组称为指针数组,即数组中每个元素都存放的是一个内存地址值,指针数组的定义格式为:

1
数据类型 *数组名称[数组长度]

指针数组常用于方便灵活的处理多个字符串,由于字符串本身就是一个字符数组,因此存放有多个字符串的数组本质就是一个二维数组。由于二维数组定义时需要指定列数,每一列包含的元素个数都是相同的,一旦某个字符串的长度过长,就会以该字符串的长度作为每行的长度,从而造成大量内存空间的浪费。

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main(int argc, char *argv[])
char *cities[] = {"guizhou", "chengdu", "chongqing"};
printf("指针数组cities占用的存储空间大小为%ld个字节\n", sizeof(cities));

printf("字符串%s的指针地址为%p\n", cities[0], cities[0]); // 字符串guizhou的指针地址为0x55ff28f727f8
printf("字符串%s的指针地址为%p\n", cities[1], cities[1]); // 字符串chengdu的指针地址为0x55ff28f72800
printf("字符串%s指针地址为%p\n", cities[2], cities[2]); // 字符串chongqing指针地址为0x55ff28f72808

return 0;
}

上面代码当中,声明并定义一个字符指针数组,并保存了 3 个字符串常量的首地址。由于指针变量的长度总是为 8 个字节,而且数组本身是连续存储的,因此上面打印的字符串数组内存地址总是按照 8 个字节间隔进行排列。

指针类型数据的指针

指向指针的指针,顾名思义就是指向指针数据类型的指针变量,可以通过如下格式进行声明和定义:

1
数据类型 **指针名称;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main(int argc, char *argv[]) {
char *cities[] = {"guizhou", "chengdu", "chongqing"};
char **pointer; // 指向指针数据类型的指针变量

int index;
for (index = 0; index < 3; index++) {
pointer = cities + index; // 根据循环索引index逐步移动指针
printf("%s\n", *pointer); // 遍历字符类型指针数组cities当中存储的常量字符串
}

return 0;
}

前面提到的指针数组的元素不仅可以指向字符串,也可以指向整型或者浮点型等其它数据类型,来看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int main(int argc, char *argv[]) {
int numbers[10] = {0, 1, 2, 3, 4, 5}; // 整型数组
int *addresses[10] = {&numbers[0], &numbers[1], &numbers[2], &numbers[3], &numbers[4], &numbers[5]}; // 基于numbers的元素构建整型指针数组addresses
int **pointer, index;
pointer = addresses; // 使pinter指向指针数组address首地址

for (index = 0; index < 6; index++) {
printf("%d\n", **pointer); // 打印pointer指向的指针里所保存指针地址所指向的数据
pointer += 1; // 移动指针到下一个数组元素
}

return 0;
}

上面代码由于整型指针数组addresses不像上一个例子中的字符串常量那样,可以自动获取首地址值,因此必须通过取地址运算符&显式的获取地址来构建这个指针数组。

main 函数的指针参数

main()函数携带的形式参数(int argc, char *argv[])也称为 Linux C 程序的命令行参数,其中第 1 个参数argc是单词argument count的缩写,表示参数的个数;,第 2 个参数argv是单词argument vector的缩写,表示参数向量,这是一个字符类型的指针数组,数组中每个元素都指向命令行输入的字符串,即命令 参数1 参数2...

1
2
3
4
5
6
7
8
#include <stdio.h>

int main(int argc, char *argv[]) {
printf("当前参数的个数为%d\n", argc);
printf("当前命令行传递的参数值为%s、%s、%s\n", argv[0], argv[1], argv[2]);

return 0;
}

打印结果,Linux 控制台下编译为默认的a.out文件后执行结果如下,注意打印时将运行a.out文件的命令也作为了输入参数:

1
2
3
4
➜  ./a.out hank uinika

当前参数的个数为3
当前命令行传递的参数值为./a.out、hank、uinika

其实,形式参数argcargv也可以命名为其它名字,但是出于约定成俗的习惯,建议继续沿用这样的命名方式。

void 无类型指针

C99 标准提供了void类型的指针,用于定义一个基类型为void的指针变量,即void *变量名。该指针并不指向任何确定的数据类型,只会在使用其它数据类型的指针变量对其进行赋值时,由 GCC 编译系统自动对其进行类型转换。转换后的指针只能得到其它数据类型的纯地址值,并不能自动指向该类型指针之前所引用的数据,任何尝试打印自动类型转换后void指针所引用数据的行为,都将会引发 GCC 的编译警告信息,就如同下面代码这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

int main(int argc, char *argv[]) {
int number = 1985;
void *pointer = &number;
printf("%d\n", *pointer);

return 0;
}

/*
main.c: In function ‘main’:
main.c:7:18: warning: dereferencing ‘void *’ pointer
printf("%d\n", *pointer);
^~~~~~~~
main.c:7:18: error: invalid use of void expression
printf("%d\n", *pointer);
^
*/

简单修改一下上面的代码,只获取自动类型转换后void指针变量pointer内保存的地址值,代码就可以正常得到执行。

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(int argc, char *argv[]) {
int number = 1985;
void *pointer = &number;
printf("%p\n", pointer); // 0x7fff3988204c

return 0;
}

NULL 空类型指针

GCC 提供的stdio.h头文件里通过预定义指令#define NULL 0定义了符号常量NULL,如果将其赋值给指针变量,那么就认为该指针变量为空指针,保存的变量地址值为00000000

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main(int argc, char *argv[]) {
int number = 1985;
int *pointer = &number;
pointer = NULL;

printf("%p\n", pointer); // 00000000

return 0;
}

注意:指针变量值为NULL未赋值的指针变量属于两类概念,前者保存的地址值为00000000,后者则可能指向一个无法预料的值,也就是江湖传说中的野指针

指针运算

指针变量与整型数据进行加减运算,即将该指针变量值与其所指向变量类型占用的存储单元字节数进行加减运算。

1
2
3
4
5
6
7
8
#include <stdio.h>

int main(int argc, char *argv[]) {
int universities[2] = {211, 985};
int *goal = &universities[0];
printf("%d\n", *(goal + 1)); // 985
return 0;
}

将变量的地址赋值给一个指针,注意不能将一个整数赋值给指针变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main(int argc, char *argv[]) {
int integer = 985;
int *number = &integer; // 工作正常
int *pointer = 0x7fff3967308c; // 引发GCC编译器警告
// main.c: In function ‘main’:
// main.c:6:18: warning: initialization makes pointer from integer without a cast [-Wint-conversion]
// int *pointer = 0x7fff3967308c; // 引发GCC编译器警告
// ^~~~~~~~~~~~~~

return 0;
}

指针变量之间可以相减,得到的差是两个指针变量之间的元素个数。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main(int argc, char *argv[]) {
int numbers[5] = {1, 2, 3, 4, 5};
int *zero = &numbers[0];
int *four = &numbers[4];
printf("%d\n", *four - *zero); // 4

return 0;
}

如果两个指针变量都指向同一个数组的不同元素,则可以进行比较运算,即前面数组元素的指针将会小于后面的数组元素。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main(int argc, char *argv[]) {
char *cities[3] = {"chengdu", "zunyi", "guiyang"};
char *chengdu = cities[0];
char *guiyang = cities[2];
printf("贵阳比成都更远吗?%s!\n", (*chengdu < *guiyang) ? "是的" : "不是"); // 贵阳比成都更远吗?是的!

return 0;
}

归纳总结

  • 首先需要明确指针的概念,指针就是变量的内存地址,凡是出现指针的地方都可以使用地址进行代替。变量的指针就是变量的地址,指针变量就是地址变量
  • 区别指针指针变量指针是指地址值本身,指针变量是用来存放这个地址的变量。
  • 明确指针的指向所表达的概念,对于指针变量而言,保存的是哪个变量的地址,就认为指针变量指向了这个变量。
  • 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
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
void *address = malloc(16); // 在动态存储区开辟16字节的存储区域,并返回首地址。
printf("%p\n", address); // 0x55c638ba7260

return 0;
}

void *calloc(unsigned n, unsigned size)

用于在内存的动态存储区分配n个长度为size的连续空间,可以用于保存一个动态数组。函数返回值同样指向所分配区域的首地址,执行不成功同样返回空指针。

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
void *address = calloc(4, 16); // 在动态存储区开辟4个16字节的连续存储空间,并返回首地址。
printf("%p\n", address); // 0x55ffbb6b9260

return 0;
}

void free(void *p)

释放void类型指针变量p所指向的动态存储空间,以便于 Linux 操作系统进行回收复用。通常情况下,参数pmalloc()calloc()函数调用后的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
void *address = malloc(16);
void *addresses = calloc(8, 32);

free(address);
free(addresses);

return 0;
}

void *realloc(void *p, unsigned int size)

如果已经通过malloc()calloc()函数获取动态存储空间,那么可以使用realloc()重新进行存储空间size的分配,执行成功后返回重新分配的地址,如果分配不成功则返回空指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
/* 分配一个32字节动态存储空间,并将首地址赋值给void类型指针变量address。 */
void *address = malloc(32);
printf("%p\n", address); // 0x55a5481c7260

/* 重新分配address占用的动态存储空间,并将重新分配后的存储空间首地址赋予void类型指针变量update。*/
void *update = realloc(address, 64);
printf("%p\n", update); // 0x55a5481c76a0

return 0;
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>

typedef int Age; // 声明整型数据为代表年纪的Date

typedef struct {
int year;
int month;
int day;
} Date; // 声明结构体类型为Date

typedef int Array[10]; // 声明整型数组类型为Array

typedef char *Pointer; //声明字符指针类型为String

int main(int argc, char *argv[]) {
Age student = 33;
printf("%d\n", student); // 33

Date birthday = {1985, 10, 1};
printf("%d\n", birthday.year); // 1985

Array ordinal = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
printf("%d\n", ordinal[9]); // 9

char c = 'V';
Pointer input = &c;
printf("%c\n", *input); // V

return 0;
}

预处理指令

GCC 编译器可以在预处理阶段对 Linux C 源代码进行一系列预处理操作,首先会清除代码中的注释,然后对预处理指令(Preprocessing Directive)进行处理,预处理指令通常以#开头,指令语句后面没有分号。经过预处理之后的代码将不再包含相关指令。例如:将#include指令替换为头文件指定的内容,用#define指令指定的值替换代码中引用的位置。

宏定义 #define #undef

宏定义主要用于减少代码中一些重复出现常量的书写工作量,宏名称习惯上使用大写字母表示,具体定义格式如下:

1
#define 宏名称 宏值

接下来,我们编写一个完整的宏定义示例:

1
2
3
4
5
6
7
8
9
#include <stdio.h>

#define SENTENCE "Hello World!"

int main(int argc, char *argv[]) {
char regards[] = SENTENCE;
printf("%s\n", regards); // Hello World!
return 0;
}

通常情况下,#define指令位于源代码文件开头,有效范围从该指令位置一直覆盖到源文件结束。如果需要,也可以采用另外一个宏定义指令#undef手动终止该宏定义的作用域,其具体使用格式为:

1
#undef 待终止作用域范围的宏定义名称

修改一下前面的代码,通过添加#undef指令来控制SENTENCE的作用域范围:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

#define SENTENCE "Hello World!"

int main(int argc, char *argv[]) {
char regards[] = SENTENCE;

#undef SENTENCE

printf("%s\n", regards); // Hello World!
return 0;
}

注意:宏定义并非变量,GCC 也不会为其分配存储空间,仅仅是对源代码字符串进行的简单替换。

上面的宏定义只是定义了一个常量值,如果需要对一个表达式进行宏定义,那么就需要使用到带参数的宏定义,具体定义格式如下:

1
#define 宏定义名称(参数列表) 参数的表达式

接下来一份完整的示例代码继续走起:

1
2
3
4
5
6
7
8
9
#include <stdio.h>

#define ADDITION(x, y) (x + y)

int main(int argc, char *argv[]) {
int result = ADDITION(100, 25);
printf("%d\n", result); // 125
return 0;
}

文件包含 #include

文件包含指令#include用于将其它源代码文件包含至指令所在位置,一条#include指令只能包含一个源文件,包含多个源文件需要多条#include指令,其具体使用格式如下:

1
2
#include <文件名>  // 标准方式,在系统目录中查找依赖文件。
#include "文件名" // 首先在代码所在目录查找,如果未找到再按照标准方式查找。

现在,在测试代码目录里新建一个utils子文件夹,然后添加一份如下所示的method.c源码文件:

1
2
3
4
5
#include <stdio.h>

void method() {
printf("This is a method!\n");
}

紧接着在上级目录编写main.c主函数,并在其中调用method.c提供的method()函数。

1
2
3
4
5
6
#include "./utils/method.c"

int main(int argc, char *argv[]) {
method(); // This is a method!
return 0;
}

这种书写在源码头部的文件通常被称为头文件,里边通常用于放置函数定义、结构体定义、宏定义;头文件后缀名例行使用.h结尾,因此出于规范方面的考量,需要将上面的method.c后缀名修改为method.h,下面展示一份完整的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

/* 宏定义 */
#define PI 3.14

/* 结构体定义 */
struct {
int year;
int month;
int day;
} date = {1985, 8, 6};

/* 函数定义 */
void tool() {
printf("Function!\n");
}

接下来,在main.c主函数当中调用头文件method.h定义的各种值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "./utils/method.h"

int main(int argc, char *argv[]) {
float pi = PI;
printf("%f\n", pi); // 3.140000

int year = date.year;
int month = date.month;
int day = date.day;
printf("%d年%d月%d日\n", year, month, day); // 1985年8月6日

tool(); // Function!

return 0;
}

条件编译 #ifdef #ifndef #if

正常情况下,一份源代码文件当中的所有代码都会参予编译,但是某些时刻只希望编译代码文件当中的部分内容,此时就需要使用到条件编译相关的预定义指令。

(1)如果标识符已经被#define预处理指令定义,那么 GCC 将会预处理代码段 1,否则就会预处理代码段 2。前面有提到:条件编译本质上是一系列宏替换操作,因此预处理后的源文件将只会根据条件保留代码段 1代码段 2中之一。

1
2
3
4
5
#ifdef 标识符
代码段 1
#else
代码段 2
#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

#define AGE 18 // 宏定义AGE

#ifdef AGE // 如果宏定义AGE存在,
#define AUTH 0 // 那么宏定义 AUTH = 0 将被预处理并参予编译。
#else
#define AUTH 1
#endif

int main(int argc, char *argv[]) {
int permission = AUTH;
printf("当前编译的宏定义值:%d\n", permission); // 当前编译的宏定义值:0

return 0;
}

(2)如果标识符没有被#define预处理指令定义,那么 GCC 将会预处理代码段 1,否则就会预处理代码段 2

1
2
3
4
5
#ifndef 标识符
代码段
#else
代码段
#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

#define AGE 18 // 宏定义AGE

#ifndef AGE // 如果宏定义AGE不存在,
#define AUTH 0
#else
#define AUTH 1 // 那么宏定义 AUTH = 0 将被预处理并参予编译。
#endif

int main(int argc, char *argv[]) {
int permission = AUTH;
printf("当前编译的宏定义值:%d\n", permission); // 当前编译的宏定义值:1

return 0;
}

(3)当表达式为真(非零值)时就预处理代码段 1,否则预处理代码段 2

1
2
3
4
5
#if 表达式
代码段 1
#else
代码段 2
#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

#define CURRENT Linux // or Windows

int main(int argc, char *argv[]) {
#if CURRENT != Windowns // 如果当前操作系统为Windows
printf("Current OS is Windows\n");
#else
printf("执行结果:Current OS is Linux\n"); // 执行结果:Current OS is Linux
#endif

return 0;
}

位运算

位运算是以二进制为对象的运算,参加位运算的只能为整型或字符型数据,浮点类型数据不能参与位运算。位运算是嵌入式系统开发当中的常用计算操作,下表列出了 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
2
3
4
5
6
7
8
#include <stdio.h>

int main(int argc, char *argv[]) {
unsigned char a = 0b00101011;
printf("输出16进制结果:%#X\n", a); // 输出16进制结果:0X2B

return 0;
}

(1)清零,即将一个字节单元的八位全部置为二进制0。操作方法是将操作数a二进制值当中的1替换为0作为操作数b来参予运算。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main(int argc, char *argv[]) {
unsigned char a = 0b00101011;
unsigned char b = 0b10010100;
/* & 00000000 */
printf("输出16进制结果:%#X\n", a & b); // 输出16进制结果:0

return 0;
}

(2)获取二进制的指定位,方法是将操作数a的二进制值当中,需要获取的位都置为1除需要获取位之外的所有位都置为0,然后让其参予同操作数的运算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main(int argc, char *argv[]) {
/* 获取操作数 a1 的低 4 位 */
unsigned char a1 = 0b00110011;
unsigned char b1 = 0b00001111;
/* & 00000011 */
printf("输出16进制结果:%#X\n", a1 & b1); // 输出16进制结果:0X3

/* 获取操作数 12 的高 4 位 */
unsigned char a2 = 0b00110011;
unsigned char b2 = 0b11110000;
/* & 00110000 */
printf("输出16进制结果:%#X\n", a2 & b2); // 输出16进制结果:0X30

return 0;
}

按位或 |

两个操作数对应的二进制位当中,只要有一个的状态为1,那么运算的结果就是1

表达式 结果 表达式 结果
0 | 0 0 1 | 0 1
0 | 1 1 1 | 1 1

接位或运算通常用来将操作数的指定位置设为1,方法是将操作数当中需要保持原样的位置设为0,需要指定为1的位置保持1,然后参予同操作数的运算。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main(int argc, char *argv[]) {
unsigned char a = 0b10101010;
unsigned char b = 0b00001111;
/* | 10101111 */
printf("输出16进制结果:%#X\n", a & b); // 输出16进制结果:0XA

return 0;
}

按位异或 ^

如果参予运算的两个二进制操作数的对应位异号,那么结果为1;如果对应位同号则结果为0

表达式 结果 表达式 结果
0 ^ 0 0 1 ^ 0 1
0 ^ 1 1 1 ^ 1 0

(1)翻转指定的位,即1转换为00转换为1。方法是将操作数二进制位当中需要翻转的位置设置为1,其它位置设置为0,然后参予与操作数的异或运算。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main(int argc, char *argv[]) {
unsigned char a = 0b11111001;
unsigned char b = 0b00001111;
/* ^ 11110110 */
printf("输出16进制结果:%#X\n", a & b); // 输出16进制结果:0X9

return 0;
}

(2) 交换两个变量的取值,即将操作数ab进行交叉赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main(int argc, char *argv[]) {
int a = 1985;
int b = 2019;

a = a ^ b;
b = b ^ a;
a = a ^ b;

printf("当前a的值为%d,b的值为%d!\n", a, b); // 当前a的值为2019,b的值为1985!

return 0;
}

按位取反 ~

按位取反运算符~是一个单目运算符,用来对二进制操作数的每一位进行取反,即1变为0然后0变为1

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main(int argc, char *argv[]) {
unsigned char a = 0b11111111;
/* ~ 00000000 */
unsigned char b = ~a;
printf("输出16进制结果:%#X\n", b); // 输出16进制结果:0

return 0;
}

按位取反可以与其它位运算符结合使用,比如通过和按位与运算符&一起结合使用,可以方便的将操作数的指定位置为0;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

int main(int argc, char *argv[]) {
unsigned char a = 0b00001111;

unsigned char b = ~1;
/* 1 00000011 */
/* ~ 11111110 */

unsigned char c = a & b;
/* a 00001111 */
/* b 11111110 */
/* c & 00001110 */

printf("输出16进制结果:%#X\n", a & b); // 输出16进制结果:0XE

return 0;
}

按位左移 <<

将操作数的二进制位左移指定位数之后补0左移 1 位相当于该数值乘以2,左移 n 位则相当于乘以2ᐢ。按位左移运算比乘法更快,日常开发中需要强调性能的场合,可以考虑将乘以2ᐢ的幂运算替换为左移n位,但是特别注意当操作数二进制所有位都被左移出去之后,最终的结果将会等于0,这并非最终期望的乘法结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

int main(int argc, char *argv[]) {
unsigned char a = 0b00100000;

unsigned char b = a << 1;
/* << 1 01000000 */
unsigned char c = a << 2;
/* << 2 10000000 */
unsigned char d = a << 3;
/* << 3 00000000 */

printf("待操作10进制值:%d,待操作16进制值:%#X\n", a, a); // 待操作10进制值:32,待操作16进制值:0X20
printf("输出10进制结果:%d,输出16进制结果:%#X\n", b, b); // 输出10进制结果:64,输出16进制结果:0X40
printf("输出10进制结果:%d,输出16进制结果:%#X\n", c, c); // 输出10进制结果:128,输出16进制结果:0X80
printf("输出10进制结果:%d,输出16进制结果:%#X\n", d, d); // 输出10进制结果:0,输出16进制结果:0

return 0;
}

按位右移 >>

同样的,按位右移操作符>>是将操作数的二进制位左移指定位数之后补0左移 1 位相当于该数值除以2,左移 n 位则相当于除以2ᐢ。当试图使用右移运算代替除法计算时,依然需要注意当操作数所有位都被右移出去之后结果为0的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>

int main(int argc, char *argv[]) {
unsigned char a = 0b00010000;

unsigned char b = a >> 1;
/* >> 1 00001000 */
unsigned char c = a >> 2;
/* >> 2 00000100 */
unsigned char d = a >> 3;
/* >> 3 00000010 */
unsigned char e = a >> 4;
/* >> 4 00000001 */
unsigned char f = a >> 5;
/* >> 5 00000000 */

printf("待操作10进制值:%d,待操作16进制值:%#X\n", a, a); // 待操作10进制值:16,待操作16进制值:0X10
printf("输出10进制结果:%d,输出16进制结果:%#X\n", b, b); // 输出10进制结果:8,输出16进制结果:0X8
printf("输出10进制结果:%d,输出16进制结果:%#X\n", c, c); // 输出10进制结果:4,输出16进制结果:0X4
printf("输出10进制结果:%d,输出16进制结果:%#X\n", d, d); // 输出10进制结果:2,输出16进制结果:0X2
printf("输出10进制结果:%d,输出16进制结果:%#X\n", e, e); // 输出10进制结果:1,输出16进制结果:0X1
printf("输出10进制结果:%d,输出16进制结果:%#X\n", f, f); // 输出10进制结果:0,输出16进制结果:0

return 0;
}

Linux C 标准程序设计语言

http://www.uinio.com/C&C++/C/

作者

Hank

发布于

2014-03-16

更新于

2017-05-21

许可协议