【编程语言】【C语言】一篇文件构建C语言知识体系
第一章 C语言基础
1.1 C语言概述
1.1.1 C语言的发展历程
C语言的发展历程是一部充满创新与变革的历史,下面为你详细介绍:
- 诞生背景:20世纪70年代初,贝尔实验室的丹尼斯·里奇(Dennis Ritchie)为了开发UNIX操作系统,在B语言的基础上设计了C语言。当时,操作系统的开发需要一种高效、灵活且可移植的编程语言,C语言应运而生。
- 发展阶段
- 1972年:C语言正式诞生,最初用于UNIX系统的开发,展现出了强大的系统编程能力。
- 1978年:布莱恩·柯林汉(Brian Kernighan)和丹尼斯·里奇(Dennis Ritchie)出版了名著《C程序设计语言》,这本书也被称为K&R C,成为了C语言的经典教材,极大地推动了C语言的普及。
- 1989年:美国国家标准协会(ANSI)制定了第一个C语言标准,即ANSI C,后来被国际标准化组织(ISO)采纳,称为ISO C。这个标准统一了C语言的语法和语义,使得C语言在不同的编译器和平台上具有更好的兼容性。
- 1999年:ISO发布了C99标准,对C语言进行了一系列的扩展和改进,如支持变长数组、复数类型等。
- 2011年:ISO发布了C11标准,进一步增强了C语言的功能,如支持多线程、原子操作等。
1.1.2 C语言的特点和应用领域
1. 特点
- 高效性:C语言是一种编译型语言,它生成的机器代码执行效率高,能够充分发挥计算机的硬件性能。许多操作系统、编译器等系统软件都是用C语言编写的。
- 灵活性:C语言提供了丰富的运算符和数据类型,允许程序员直接操作内存,进行底层的编程。同时,C语言的语法简洁,结构清晰,易于学习和掌握。
- 可移植性:C语言的标准具有高度的可移植性,只要遵循标准编写的C程序,就可以在不同的操作系统和硬件平台上编译和运行。
- 功能强大:C语言可以进行位操作、文件操作等底层操作,还可以通过函数库实现各种复杂的功能,如图形处理、网络编程等。
2. 应用领域
- 系统软件:操作系统(如Windows、Linux)、编译器、数据库管理系统等大多是用C语言开发的。因为C语言能够直接访问硬件资源,对系统性能进行优化。
- 嵌入式系统:在智能家居、汽车电子、工业控制等领域,嵌入式系统广泛应用。C语言由于其高效性和可移植性,成为了嵌入式系统开发的首选语言。
- 游戏开发:许多游戏的引擎和底层逻辑都是用C语言编写的,以提高游戏的性能和响应速度。
- 网络编程:C语言可以实现网络协议的底层细节,开发网络服务器、客户端等应用程序。
1.2 第一个C语言程序
1.2.1 程序的基本结构
下面是一个简单的C语言程序示例:
#include <stdio.h>int main() {printf("Hello, World!\n");return 0;
}
让我们来分析一下这个程序的基本结构:
- 预处理指令:
#include <stdio.h>
是一个预处理指令,它告诉编译器在编译之前要包含标准输入输出库(stdio.h)的内容。这个库提供了许多输入输出函数,如printf
和scanf
。 - 主函数:
int main()
是C语言程序的入口点,每个C程序都必须有一个main
函数。int
表示main
函数的返回值类型是整数,通常返回0
表示程序正常结束。 - 函数体:
{}
内的部分是main
函数的函数体,包含了程序要执行的语句。printf("Hello, World!\n");
是一个输出语句,用于在屏幕上输出字符串Hello, World!
。\n
是换行符,表示换行。 - 返回语句:
return 0;
语句用于结束main
函数,并返回一个整数值0
给操作系统。
1.2.2 编译和运行过程
编写好C语言程序后,需要经过编译和运行两个步骤才能看到程序的执行结果。具体过程如下:
- 编辑:使用文本编辑器(如Visual Studio Code、Notepad++等)编写C语言代码,并将其保存为
.c
文件,例如hello.c
。 - 编译:使用编译器(如GCC、Clang等)将
.c
文件编译成可执行文件。在命令行中,可以使用以下命令进行编译:
gcc hello.c -o hello
其中,gcc
是GCC编译器的命令,hello.c
是源文件的名称,-o
选项用于指定输出文件的名称,hello
是生成的可执行文件的名称。
- 运行:编译成功后,会生成一个可执行文件。在命令行中,可以使用以下命令运行该文件:
./hello
如果一切正常,屏幕上会输出 Hello, World!
。
1.3 数据类型
1.3.1 基本数据类型
1.3.1.1 整型
整型用于表示整数,C语言提供了多种整型数据类型,不同的类型在内存中占用的字节数和表示的范围不同。常见的整型数据类型如下:
类型名称 | 占用字节数 | 表示范围 |
---|---|---|
char | 1 | -128 到 127 或 0 到 255 |
short | 2 | -32768 到 32767 |
int | 4 | -2147483648 到 2147483647 |
long | 4 或 8(取决于系统) | -2147483648 到 2147483647 或 -9223372036854775808 到 9223372036854775807 |
long long | 8 | -9223372036854775808 到 9223372036854775807 |
可以使用 sizeof
运算符来查看不同整型数据类型在当前系统中占用的字节数,示例代码如下:
#include <stdio.h>int main() {printf("Size of char: %zu bytes\n", sizeof(char));printf("Size of short: %zu bytes\n", sizeof(short));printf("Size of int: %zu bytes\n", sizeof(int));printf("Size of long: %zu bytes\n", sizeof(long));printf("Size of long long: %zu bytes\n", sizeof(long long));return 0;
}
1.3.1.2 浮点型
浮点型用于表示小数,C语言提供了两种浮点型数据类型:float
和 double
。
float
:单精度浮点型,占用 4 个字节,能够表示大约 6 到 7 位有效数字。double
:双精度浮点型,占用 8 个字节,能够表示大约 15 到 16 位有效数字。
示例代码如下:
#include <stdio.h>int main() {float f = 3.14f;double d = 3.1415926;printf("Float value: %f\n", f);printf("Double value: %lf\n", d);return 0;
}
1.3.1.3 字符型
字符型数据类型 char
用于表示单个字符,在内存中占用 1 个字节。字符型数据通常使用单引号 ' '
来表示,例如 'A'
、'a'
、'0'
等。
示例代码如下:
#include <stdio.h>int main() {char ch = 'A';printf("Character: %c\n", ch);return 0;
}
1.3.2 构造数据类型
1.3.2.1 数组
数组是一组相同类型的数据的集合,这些数据在内存中连续存储。数组的定义方式如下:
数据类型 数组名[数组长度];
例如,定义一个包含 5 个整数的数组:
int arr[5];
可以通过下标来访问数组中的元素,下标从 0 开始。示例代码如下:
#include <stdio.h>int main() {int arr[5] = {1, 2, 3, 4, 5};for (int i = 0; i < 5; i++) {printf("arr[%d] = %d\n", i, arr[i]);}return 0;
}
1.3.2.2 结构体
结构体是一种用户自定义的数据类型,它可以包含不同类型的数据成员。结构体的定义方式如下:
struct 结构体名 {数据类型 成员名1;数据类型 成员名2;// ...
};
例如,定义一个包含姓名、年龄和成绩的学生结构体:
struct Student {char name[20];int age;float score;
};
可以通过结构体变量来访问结构体的成员,示例代码如下:
#include <stdio.h>
#include <string.h>struct Student {char name[20];int age;float score;
};int main() {struct Student s;strcpy(s.name, "John");s.age = 20;s.score = 85.5;printf("Name: %s\n", s.name);printf("Age: %d\n", s.age);printf("Score: %.2f\n", s.score);return 0;
}
1.3.2.3 共用体
共用体也是一种用户自定义的数据类型,它与结构体类似,但共用体的所有成员共享同一块内存空间,因此在同一时间只能使用一个成员。共用体的定义方式如下:
union 共用体名 {数据类型 成员名1;数据类型 成员名2;// ...
};
例如,定义一个包含整数和浮点数的共用体:
union Data {int i;float f;
};
示例代码如下:
#include <stdio.h>union Data {int i;float f;
};int main() {union Data d;d.i = 10;printf("Integer value: %d\n", d.i);d.f = 3.14f;printf("Float value: %f\n", d.f);return 0;
}
1.3.3 指针类型
指针是C语言中一种非常重要的数据类型,它用于存储变量的内存地址。指针的定义方式如下:
数据类型 *指针变量名;
例如,定义一个指向整数的指针:
int num = 10;
int *p = #
其中,&
是取地址运算符,用于获取变量的内存地址。可以通过指针来访问和修改变量的值,示例代码如下:
#include <stdio.h>int main() {int num = 10;int *p = #printf("Value of num: %d\n", num);printf("Address of num: %p\n", &num);printf("Value of p: %p\n", p);printf("Value pointed to by p: %d\n", *p);*p = 20;printf("New value of num: %d\n", num);return 0;
}
1.3.4 空类型
空类型 void
表示没有类型,通常用于以下几种情况:
- 函数返回值:当函数没有返回值时,可以使用
void
作为返回值类型。例如:
void printMessage() {printf("Hello, World!\n");
}
- 函数参数:当函数不接受任何参数时,可以使用
void
作为参数列表。例如:
int getRandomNumber(void) {return rand();
}
- 指针:
void
指针可以指向任何类型的数据,但在使用时需要进行类型转换。例如:
#include <stdio.h>int main() {int num = 10;void *p = #int *ip = (int *)p;printf("Value of num: %d\n", *ip);return 0;
}
1.4 变量和常量
1.4.1 变量的定义和使用
变量是程序中用于存储数据的内存单元,在使用变量之前需要先进行定义。变量的定义方式如下:
数据类型 变量名;
例如,定义一个整数变量:
int num;
可以在定义变量的同时进行初始化,示例代码如下:
#include <stdio.h>int main() {int num = 10;printf("Value of num: %d\n", num);num = 20;printf("New value of num: %d\n", num);return 0;
}
1.4.2 常量的表示方法
常量是在程序运行过程中值不会发生改变的量,C语言中常见的常量表示方法有以下几种:
- 整型常量:直接使用整数表示,如
10
、-20
等。 - 浮点型常量:使用小数表示,如
3.14
、-2.5
等。 - 字符常量:使用单引号
' '
表示,如'A'
、'a'
等。 - 字符串常量:使用双引号
" "
表示,如"Hello, World!"
。 - 符号常量:使用
#define
预处理指令定义,例如:
#include <stdio.h>#define PI 3.1415926int main() {float radius = 5.0;float area = PI * radius * radius;printf("Area of the circle: %.2f\n", area);return 0;
}
1.5 运算符和表达式
1.5.1 算术运算符
算术运算符用于进行基本的数学运算,常见的算术运算符如下:
运算符 | 描述 | 示例 |
---|---|---|
+ | 加法 | a + b |
- | 减法 | a - b |
* | 乘法 | a * b |
/ | 除法 | a / b |
% | 取模(求余数) | a % b |
++ | 自增 | ++a 或 a++ |
-- | 自减 | --a 或 a-- |
示例代码如下:
#include <stdio.h>int main() {int a = 10, b = 3;printf("a + b = %d\n", a + b);printf("a - b = %d\n", a - b);printf("a * b = %d\n", a * b);printf("a / b = %d\n", a / b);printf("a % b = %d\n", a % b);int c = a++;printf("a++ = %d, a = %d\n", c, a);int d = --b;printf("--b = %d, b = %d\n", d, b);return 0;
}
1.5.2 关系运算符
关系运算符用于比较两个值的大小关系,返回值为 0
(假)或 1
(真)。常见的关系运算符如下:
运算符 | 描述 | 示例 |
---|---|---|
== | 等于 | a == b |
!= | 不等于 | a != b |
> | 大于 | a > b |
< | 小于 | a < b |
>= | 大于等于 | a >= b |
<= | 小于等于 | a <= b |
第二章 流程控制
在编程的世界里,流程控制就像是交通信号灯🚥,指挥着代码的执行顺序和方向。它能让程序根据不同的条件做出不同的反应,执行不同的任务。接下来,我们就详细了解一下几种常见的流程控制结构。
2.1 顺序结构
顺序结构是程序中最基本的结构,就像我们按照顺序一步一步地完成任务一样,代码会按照书写的顺序从上到下依次执行。
2.1.1 语句的执行顺序
在顺序结构中,代码就像排队的小朋友👫,一个接着一个执行。例如下面这段 Python 代码:
print("第一步:打开电脑")
print("第二步:启动编辑器")
print("第三步:开始编程")
程序会先执行第一条 print
语句,输出“第一步:打开电脑”,然后执行第二条 print
语句,输出“第二步:启动编辑器”,最后执行第三条 print
语句,输出“第三步:开始编程”。
2.1.2 输入输出函数的使用
输入输出函数是程序与用户交互的重要工具。输入函数可以让用户输入数据,输出函数则可以将程序的结果展示给用户。
- 输入函数:在 Python 中,使用
input()
函数获取用户输入。例如:
name = input("请输入你的名字:")
print(f"你好,{name}!")
运行这段代码时,程序会暂停,等待用户输入名字,然后将用户输入的名字存储在变量 name
中,并输出问候语。
- 输出函数:在 Python 中,使用
print()
函数输出信息。例如:
age = 20
print(f"你的年龄是 {age} 岁。")
这段代码会将变量 age
的值插入到字符串中,并输出完整的信息。
2.2 选择结构
选择结构就像人生的岔路口🚦,程序会根据不同的条件选择不同的执行路径。常见的选择结构有 if
语句和 switch
语句。
2.2.1 if语句
if
语句是最常用的选择结构,它可以根据条件的真假来决定是否执行某段代码。
2.2.1.1 单分支if语句
单分支 if
语句只有一个条件判断,如果条件为真,则执行 if
语句块中的代码;如果条件为假,则跳过该语句块。语法如下:
if 条件:# 条件为真时执行的代码print("条件满足,执行此代码块")
例如:
score = 80
if score >= 60:print("恭喜你,考试及格了!")
2.2.1.2 双分支if-else语句
双分支 if-else
语句有两个分支,当条件为真时,执行 if
语句块中的代码;当条件为假时,执行 else
语句块中的代码。语法如下:
if 条件:# 条件为真时执行的代码print("条件满足,执行此代码块")
else:# 条件为假时执行的代码print("条件不满足,执行此代码块")
例如:
score = 50
if score >= 60:print("恭喜你,考试及格了!")
else:print("很遗憾,考试不及格。")
2.2.1.3 多分支if-else-if语句
多分支 if-else-if
语句可以处理多个条件判断,它会依次检查每个条件,当某个条件为真时,执行对应的语句块,并跳过后面的条件判断。语法如下:
if 条件1:# 条件1为真时执行的代码print("条件1满足,执行此代码块")
elif 条件2:# 条件2为真时执行的代码print("条件2满足,执行此代码块")
else:# 所有条件都不满足时执行的代码print("所有条件都不满足,执行此代码块")
例如:
score = 85
if score >= 90:print("优秀!")
elif score >= 80:print("良好!")
elif score >= 60:print("及格!")
else:print("不及格!")
2.2.2 switch语句
在 Python 中没有 switch
语句,但在其他编程语言(如 Java、C++)中,switch
语句可以根据一个表达式的值来选择执行不同的代码块。语法如下:
switch (表达式) {case 值1:// 表达式的值等于值1时执行的代码System.out.println("值为1");break;case 值2:// 表达式的值等于值2时执行的代码System.out.println("值为2");break;default:// 表达式的值不等于任何一个 case 值时执行的代码System.out.println("值不是1也不是2");
}
2.3 循环结构
循环结构可以让程序重复执行某段代码,就像跑步🏃♂️,一圈又一圈地重复相同的动作。常见的循环结构有 for
循环、while
循环和 do-while
循环。
2.3.1 for循环
for
循环通常用于已知循环次数的情况,它会按照一定的顺序遍历一个序列(如列表、元组、字符串等)。语法如下:
for 变量 in 序列:# 循环体,每次循环执行的代码print(变量)
例如:
fruits = ["苹果", "香蕉", "橙子"]
for fruit in fruits:print(f"我喜欢吃 {fruit}。")
2.3.2 while循环
while
循环会在条件为真时不断地执行循环体中的代码,直到条件为假时停止。语法如下:
while 条件:# 循环体,条件为真时执行的代码print("条件为真,继续循环")
例如:
count = 0
while count < 5:print(f"当前计数:{count}")count = count + 1
2.3.3 do-while循环
在 Python 中没有 do-while
循环,但在其他编程语言(如 Java、C++)中,do-while
循环会先执行一次循环体,然后再检查条件,如果条件为真,则继续循环;如果条件为假,则停止循环。语法如下:
do {// 循环体,至少执行一次System.out.println("执行循环体");
} while (条件);
2.3.4 循环的嵌套
循环的嵌套是指在一个循环体中再嵌套另一个循环,就像俄罗斯套娃一样🎎。例如,下面的代码使用嵌套的 for
循环打印一个乘法口诀表:
for i in range(1, 10):for j in range(1, i + 1):print(f"{j} x {i} = {i * j}", end="\t")print()
2.3.5 break和continue语句
- break语句:
break
语句用于跳出当前所在的循环,就像紧急刹车🚗,让循环立即停止。例如:
for i in range(10):if i == 5:breakprint(i)
- continue语句:
continue
语句用于跳过当前循环的剩余部分,直接进入下一次循环,就像跳过一个障碍物,继续前进🏃♀️。例如:
for i in range(10):if i % 2 == 0:continueprint(i)
通过这些流程控制结构,我们可以编写出更加灵活、复杂的程序,实现各种不同的功能。💪
第三章 函数
3.1 函数的定义和调用
3.1.1 函数的定义格式
函数就像是一个“魔法盒子”🧙♂️,你给它一些输入,它就能按照特定的规则给出输出。在不同的编程语言中,函数的定义格式会有所不同,下面以常见的 Python 和 C 语言为例:
Python 语言
def function_name(parameters):# 函数体,实现具体功能的代码statement(s)return result # 可选,用于返回结果
def
是 Python 中定义函数的关键字。function_name
是你给函数取的名字,要遵循一定的命名规则,就像给你的魔法盒子贴上一个独特的标签🏷️。parameters
是函数的参数,是你要传递给魔法盒子的“原料”,可以有多个参数,用逗号分隔。- 函数体是实现具体功能的代码块,需要缩进。
return
语句用于返回函数的结果,不是必需的。
C 语言
return_type function_name(parameter_list) {// 函数体statement(s);return value; // 如果返回类型为 void,则不需要 return 语句
}
return_type
是函数的返回值类型,比如int
、float
等,如果函数不返回任何值,使用void
。function_name
同样是函数名。parameter_list
是参数列表,每个参数需要指定类型。- 函数体用花括号
{}
括起来。
3.1.2 函数的调用方式
函数定义好后,就可以在其他地方调用它,就像使用魔法盒子一样。
Python 语言
def add_numbers(a, b):return a + bresult = add_numbers(3, 5) # 调用函数,传递参数 3 和 5
print(result) # 输出结果 8
在 Python 中,直接使用函数名,后面跟上括号,括号内传入实际的参数即可调用函数。
C 语言
#include <stdio.h>int add_numbers(int a, int b) {return a + b;
}int main() {int result = add_numbers(3, 5); // 调用函数printf("%d\n", result); // 输出结果 8return 0;
}
在 C 语言中,也是通过函数名和参数列表来调用函数,通常在 main
函数中调用其他函数。
3.1.3 函数的返回值
函数的返回值是函数执行完毕后返回给调用者的结果。返回值的类型在函数定义时就已经确定。
Python 语言
def square(x):return x * xresult = square(4) # 调用函数,返回 16
print(result)
在 Python 中,return
语句可以返回任意类型的值,包括数字、字符串、列表等。
C 语言
#include <stdio.h>int square(int x) {return x * x;
}int main() {int result = square(4); // 调用函数,返回 16printf("%d\n", result);return 0;
}
在 C 语言中,返回值的类型必须与函数定义时的返回类型一致。如果函数返回类型为 void
,则不需要 return
语句,或者使用 return;
表示函数结束。
3.2 函数的参数
3.2.1 形式参数和实际参数
- 形式参数(形参):在函数定义时,括号内声明的参数称为形式参数。它们就像是魔法盒子上标注的需要的“原料”类型和名称,但此时并没有实际的值。
def multiply(a, b): # a 和 b 是形式参数return a * b
int multiply(int a, int b) { // a 和 b 是形式参数return a * b;
}
- 实际参数(实参):在函数调用时,传递给函数的具体值称为实际参数。它们是真正的“原料”,会被传递给函数使用。
result = multiply(3, 5) # 3 和 5 是实际参数
int result = multiply(3, 5); // 3 和 5 是实际参数
3.2.2 参数的传递方式
3.2.2.1 值传递
值传递是指在函数调用时,将实际参数的值复制一份传递给形式参数。函数内部对形式参数的修改不会影响到实际参数。
Python 语言
def change_value(x):x = x + 1return xnum = 5
new_num = change_value(num)
print(num) # 输出 5,num 的值没有改变
print(new_num) # 输出 6
C 语言
#include <stdio.h>void change_value(int x) {x = x + 1;
}int main() {int num = 5;change_value(num);printf("%d\n", num); // 输出 5,num 的值没有改变return 0;
}
在值传递中,就像是你给魔法盒子一份“原料”的复印件,魔法盒子对复印件做任何修改都不会影响到你原来的“原料”。
3.2.2.2 地址传递
地址传递是指在函数调用时,将实际参数的地址传递给形式参数。函数内部可以通过地址直接访问和修改实际参数的值。
C 语言
#include <stdio.h>void change_value(int *x) {*x = *x + 1;
}int main() {int num = 5;change_value(&num);printf("%d\n", num); // 输出 6,num 的值被改变了return 0;
}
在地址传递中,你给魔法盒子的是“原料”的存放地址,魔法盒子可以根据地址找到真正的“原料”并进行修改。
3.3 函数的嵌套调用和递归调用
3.3.1 函数的嵌套调用
函数的嵌套调用是指在一个函数的内部调用另一个函数。就像一个大的魔法盒子里面套着一个小的魔法盒子🎁。
Python 语言
def add(a, b):return a + bdef multiply_and_add(x, y, z):result = add(x, y) * zreturn resultfinal_result = multiply_and_add(2, 3, 4)
print(final_result) # 输出 20
C 语言
#include <stdio.h>int add(int a, int b) {return a + b;
}int multiply_and_add(int x, int y, int z) {int result = add(x, y) * z;return result;
}int main() {int final_result = multiply_and_add(2, 3, 4);printf("%d\n", final_result); // 输出 20return 0;
}
在上面的例子中,multiply_and_add
函数内部调用了 add
函数。
3.3.2 函数的递归调用
函数的递归调用是指函数在其函数体内部调用自身。递归就像是一个“无限循环”的魔法盒子,自己调用自己,但必须有一个终止条件,否则会陷入无限递归。
Python 语言
def factorial(n):if n == 0 or n == 1:return 1else:return n * factorial(n - 1)result = factorial(5)
print(result) # 输出 120
C 语言
#include <stdio.h>int factorial(int n) {if (n == 0 || n == 1) {return 1;} else {return n * factorial(n - 1);}
}int main() {int result = factorial(5);printf("%d\n", result); # 输出 120return 0;
}
在计算阶乘的例子中,factorial
函数不断调用自身,直到 n
为 0 或 1 时停止递归。
3.4 变量的作用域和存储类别
3.4.1 局部变量和全局变量
- 局部变量:在函数内部定义的变量称为局部变量。局部变量的作用域仅限于定义它的函数内部,就像每个魔法盒子都有自己独有的“小仓库”,其他魔法盒子无法直接访问。
Python 语言
def test_function():local_variable = 10 # 局部变量print(local_variable)test_function()
# print(local_variable) # 会报错,因为 local_variable 超出了作用域
C 语言
#include <stdio.h>void test_function() {int local_variable = 10; // 局部变量printf("%d\n", local_variable);
}int main() {test_function();// printf("%d\n", local_variable); // 会报错,因为 local_variable 超出了作用域return 0;
}
- 全局变量:在函数外部定义的变量称为全局变量。全局变量的作用域是整个程序,所有函数都可以访问和修改它,就像一个公共的“大仓库”。
Python 语言
global_variable = 20 # 全局变量def test_function():print(global_variable)test_function()
print(global_variable)
C 语言
#include <stdio.h>int global_variable = 20; // 全局变量void test_function() {printf("%d\n", global_variable);
}int main() {test_function();printf("%d\n", global_variable);return 0;
}
3.4.2 变量的存储类别
3.4.2.1 自动变量
自动变量是最常见的变量类型,在函数内部定义的局部变量默认就是自动变量。自动变量在函数调用时创建,函数执行完毕后销毁。
C 语言
#include <stdio.h>void test_function() {auto int a = 10; // 显式声明为自动变量,auto 关键字可省略printf("%d\n", a);
}int main() {test_function();return 0;
}
3.4.2.2 静态变量
静态变量使用 static
关键字声明。静态变量在程序运行期间只初始化一次,即使函数调用结束,它的值也不会丢失。
C 语言
#include <stdio.h>void test_function() {static int count = 0; // 静态变量count++;printf("%d\n", count);
}int main() {test_function(); // 输出 1test_function(); // 输出 2return 0;
}
3.4.2.3 寄存器变量
寄存器变量使用 register
关键字声明。寄存器变量存储在 CPU 的寄存器中,访问速度比普通变量快。但寄存器的数量有限,编译器可能会忽略 register
声明。
C 语言
#include <stdio.h>void test_function() {register int num = 5; // 寄存器变量printf("%d\n", num);
}int main() {test_function();return 0;
}
3.4.2.4 外部变量
外部变量使用 extern
关键字声明。外部变量用于在一个文件中引用另一个文件中定义的全局变量。
文件 1(file1.c)
#include <stdio.h>int global_variable = 10; // 全局变量void test_function();int main() {test_function();return 0;
}
文件 2(file2.c)
#include <stdio.h>extern int global_variable; // 引用外部变量void test_function() {printf("%d\n", global_variable);
}
在上面的例子中,file2.c
通过 extern
关键字引用了 file1.c
中定义的全局变量 global_variable
。
第四章 数组
在编程的世界里,数组就像是一个神奇的收纳盒🧰,可以把多个数据有序地存放在一起,方便我们统一管理和使用。下面就让我们一起来深入了解不同类型的数组吧。
4.1 一维数组
一维数组可以想象成是一排整齐排列的小格子📦,每个小格子都可以存放一个数据。
4.1.1 一维数组的定义和初始化
1. 定义
在大多数编程语言中,定义一维数组需要指定数组的类型和数组的大小。例如在 C 语言中,定义一个包含 5 个整数的一维数组可以这样写:
int arr[5];
这里 int
表示数组中存储的数据类型是整数,arr
是数组的名称,[5]
表示数组的大小为 5,也就是有 5 个小格子可以存放整数。
2. 初始化
- 全部初始化:可以在定义数组的同时给数组中的每个元素赋值。例如:
int arr[5] = {1, 2, 3, 4, 5};
这样数组 arr
中的元素就依次是 1、2、3、4、5。
- 部分初始化:如果只给部分元素赋值,未赋值的元素会自动初始化为 0。例如:
int arr[5] = {1, 2};
此时数组 arr
中的元素依次是 1、2、0、0、0。
4.1.2 一维数组的引用
要访问一维数组中的某个元素,只需要通过数组的下标就可以了。数组的下标是从 0 开始的,也就是说第一个元素的下标是 0,第二个元素的下标是 1,以此类推。例如:
#include <stdio.h>int main() {int arr[5] = {1, 2, 3, 4, 5};printf("数组中第 3 个元素的值是:%d\n", arr[2]);return 0;
}
在这个例子中,arr[2]
表示访问数组 arr
中第 3 个元素(因为下标从 0 开始),输出结果为 3。
4.1.3 一维数组的应用
一维数组在很多场景中都有广泛的应用,比如计算一组数据的平均值。以下是一个简单的 C 语言示例:
#include <stdio.h>int main() {int arr[5] = {1, 2, 3, 4, 5};int sum = 0;for (int i = 0; i < 5; i++) {sum += arr[i];}float average = (float)sum / 5;printf("这组数据的平均值是:%.2f\n", average);return 0;
}
在这个例子中,我们使用一维数组存储了 5 个整数,然后通过循环计算它们的总和,最后求出平均值并输出。
4.2 二维数组
二维数组可以想象成是一个表格📊,有行和列,就像一个矩阵一样。
4.2.1 二维数组的定义和初始化
1. 定义
在 C 语言中,定义一个二维数组需要指定数组的行数和列数。例如,定义一个 3 行 4 列的二维数组可以这样写:
int arr[3][4];
这里 int
表示数组中存储的数据类型是整数,arr
是数组的名称,[3]
表示数组有 3 行,[4]
表示数组有 4 列。
2. 初始化
- 按行初始化:可以按照行的顺序给二维数组的元素赋值。例如:
int arr[3][4] = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12}
};
这样数组 arr
就被初始化为一个 3 行 4 列的矩阵。
- 连续初始化:也可以将所有元素连续写在一起进行初始化。例如:
int arr[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
这种方式和按行初始化的效果是一样的。
4.2.2 二维数组的引用
要访问二维数组中的某个元素,需要同时指定行下标和列下标。例如:
#include <stdio.h>int main() {int arr[3][4] = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12}};printf("数组中第 2 行第 3 列的元素的值是:%d\n", arr[1][2]);return 0;
}
在这个例子中,arr[1][2]
表示访问数组 arr
中第 2 行第 3 列的元素(因为下标从 0 开始),输出结果为 7。
4.2.3 二维数组的应用
二维数组常用于处理矩阵运算、图像处理等场景。例如,计算一个 3 行 3 列矩阵的对角线元素之和:
#include <stdio.h>int main() {int arr[3][3] = {{1, 2, 3},{4, 5, 6},{7, 8, 9}};int sum = 0;for (int i = 0; i < 3; i++) {sum += arr[i][i];}printf("矩阵对角线元素之和是:%d\n", sum);return 0;
}
在这个例子中,我们使用二维数组存储矩阵的数据,然后通过循环计算对角线元素之和并输出。
4.3 字符数组和字符串
字符数组和字符串在编程中经常用于处理文本信息,它们就像是一个个文字的小仓库📚。
4.3.1 字符数组的定义和初始化
1. 定义
字符数组的定义和普通数组类似,只不过存储的数据类型是字符。例如,定义一个包含 10 个字符的字符数组可以这样写:
char str[10];
这里 char
表示数组中存储的数据类型是字符,str
是数组的名称,[10]
表示数组的大小为 10。
2. 初始化
- 逐个字符初始化:可以逐个给字符数组的元素赋值。例如:
char str[5] = {'H', 'e', 'l', 'l', 'o'};
- 字符串常量初始化:也可以使用字符串常量来初始化字符数组。例如:
char str[6] = "Hello";
需要注意的是,使用字符串常量初始化时,字符串末尾会自动添加一个字符串结束符 '\0'
,所以数组的大小要比字符串的长度多 1。
4.3.2 字符串的输入输出
1. 输入
在 C 语言中,可以使用 scanf
函数或 gets
函数来输入字符串。例如:
#include <stdio.h>int main() {char str[100];printf("请输入一个字符串:");scanf("%s", str);printf("你输入的字符串是:%s\n", str);return 0;
}
这里使用 scanf
函数输入字符串,需要注意的是,scanf
函数遇到空格就会停止读取。如果需要输入包含空格的字符串,可以使用 gets
函数,但 gets
函数存在缓冲区溢出的风险,在实际开发中建议使用 fgets
函数。
2. 输出
可以使用 printf
函数或 puts
函数来输出字符串。例如:
#include <stdio.h>int main() {char str[100] = "Hello, World!";printf("使用 printf 输出:%s\n", str);puts("使用 puts 输出:");puts(str);return 0;
}
puts
函数会自动在字符串末尾添加换行符。
4.3.3 字符串处理函数
C 语言提供了很多字符串处理函数,方便我们对字符串进行各种操作。以下是一些常用的字符串处理函数:
strlen
函数:用于计算字符串的长度(不包括字符串结束符'\0'
)。例如:
#include <stdio.h>
#include <string.h>int main() {char str[100] = "Hello, World!";int len = strlen(str);printf("字符串的长度是:%d\n", len);return 0;
}
strcpy
函数:用于将一个字符串复制到另一个字符串中。例如:
#include <stdio.h>
#include <string.h>int main() {char str1[100] = "Hello";char str2[100];strcpy(str2, str1);printf("复制后的字符串是:%s\n", str2);return 0;
}
strcmp
函数:用于比较两个字符串的大小。如果两个字符串相等,返回 0;如果第一个字符串小于第二个字符串,返回一个负数;如果第一个字符串大于第二个字符串,返回一个正数。例如:
#include <stdio.h>
#include <string.h>int main() {char str1[100] = "Hello";char str2[100] = "World";int result = strcmp(str1, str2);if (result == 0) {printf("两个字符串相等\n");} else if (result < 0) {printf("字符串 %s 小于字符串 %s\n", str1, str2);} else {printf("字符串 %s 大于字符串 %s\n", str1, str2);}return 0;
}
通过这些字符串处理函数,我们可以更方便地对字符串进行操作和处理。🎉
第五章 指针
指针是 C 语言中一个非常重要且强大的概念,它就像是一把钥匙,可以直接访问和操作计算机内存中的数据。下面让我们一起来深入了解指针的相关知识吧😃!
5.1 指针的基本概念
5.1.1 指针变量的定义和初始化
1. 指针变量的定义
指针变量是一种特殊的变量,它存储的是内存地址。在 C 语言中,定义指针变量的一般形式为:
数据类型 *指针变量名;
例如:
int *p; // 定义一个指向整型数据的指针变量 p
这里的 *
是指针声明符,表示 p
是一个指针变量,它可以指向一个整型数据的内存地址。
2. 指针变量的初始化
指针变量在使用之前最好进行初始化,否则它可能会指向一个随机的内存地址,这可能会导致程序出现不可预期的错误。初始化指针变量的方法有两种:
- 指向已存在的变量
#include <stdio.h>int main() {int num = 10;int *p = # // 初始化指针 p 指向变量 num 的地址printf("num 的地址是: %p\n", &num);printf("指针 p 存储的地址是: %p\n", p);return 0;
}
在这个例子中,&
是取地址运算符,&num
表示变量 num
的内存地址,将这个地址赋值给指针 p
,就完成了指针的初始化。
- 初始化为
NULL
如果暂时不知道指针要指向哪里,可以将其初始化为NULL
,NULL
是一个特殊的指针值,表示空指针。
int *p = NULL;
5.1.2 指针的运算
指针可以进行一些特殊的运算,主要包括以下几种:
1. 指针的算术运算
- 指针的加法和减法
指针可以加上或减去一个整数,其结果是一个新的指针,指向的地址会根据指针所指向的数据类型的大小进行相应的偏移。例如:
#include <stdio.h>int main() {int arr[5] = {1, 2, 3, 4, 5};int *p = arr; // 指针 p 指向数组 arr 的首元素printf("p 指向的地址: %p\n", p);p = p + 2; // 指针 p 向后移动 2 个整型元素的位置printf("移动后 p 指向的地址: %p\n", p);return 0;
}
在这个例子中,p + 2
表示指针 p
向后移动了 2 个整型元素的位置,因为每个整型元素占用 4 个字节(在 32 位系统中),所以指针实际移动了 2 * 4 = 8
个字节。
- 指针的自增和自减
指针也可以使用自增(++
)和自减(--
)运算符,例如:
#include <stdio.h>int main() {int arr[5] = {1, 2, 3, 4, 5};int *p = arr; // 指针 p 指向数组 arr 的首元素printf("p 指向的元素: %d\n", *p);p++; // 指针 p 向后移动一个整型元素的位置printf("移动后 p 指向的元素: %d\n", *p);return 0;
}
这里的 p++
表示指针 p
向后移动了一个整型元素的位置。
2. 指针的比较运算
指针可以进行比较运算,比较的是它们所指向的内存地址的大小。例如:
#include <stdio.h>int main() {int arr[5] = {1, 2, 3, 4, 5};int *p1 = &arr[0];int *p2 = &arr[2];if (p1 < p2) {printf("p1 指向的地址小于 p2 指向的地址\n");}return 0;
}
5.1.3 指针和数组的关系
在 C 语言中,指针和数组有着密切的关系,数组名在大多数情况下可以看作是一个指向数组首元素的常量指针。
1. 数组名作为指针
#include <stdio.h>int main() {int arr[5] = {1, 2, 3, 4, 5};int *p = arr; // 数组名 arr 作为指针,指向数组的首元素printf("数组首元素的值: %d\n", *p);return 0;
}
在这个例子中,arr
可以看作是一个指向数组首元素的指针,将其赋值给指针 p
,就可以通过指针 p
来访问数组的元素。
2. 通过指针访问数组元素
可以使用指针的算术运算来访问数组的其他元素,例如:
#include <stdio.h>int main() {int arr[5] = {1, 2, 3, 4, 5};int *p = arr;for (int i = 0; i < 5; i++) {printf("arr[%d] = %d\n", i, *(p + i));}return 0;
}
这里的 *(p + i)
表示访问指针 p
向后偏移 i
个元素位置的元素。
5.2 指针数组和指向指针的指针
5.2.1 指针数组的定义和使用
1. 指针数组的定义
指针数组是一个数组,数组中的每个元素都是一个指针。定义指针数组的一般形式为:
数据类型 *数组名[数组长度];
例如:
int *arr[3]; // 定义一个包含 3 个整型指针的指针数组
2. 指针数组的使用
指针数组可以用来存储多个指针,例如存储多个字符串的首地址:
#include <stdio.h>int main() {char *strs[3] = {"Hello", "World", "C Language"};for (int i = 0; i < 3; i++) {printf("%s\n", strs[i]);}return 0;
}
在这个例子中,strs
是一个指针数组,每个元素都是一个指向字符串首字符的指针。
5.2.2 指向指针的指针的定义和使用
1. 指向指针的指针的定义
指向指针的指针,也称为二级指针,它存储的是一个指针的地址。定义指向指针的指针的一般形式为:
数据类型 **指针变量名;
例如:
int **pp; // 定义一个指向整型指针的指针
2. 指向指针的指针的使用
指向指针的指针可以用来间接访问其他指针所指向的数据,例如:
#include <stdio.h>int main() {int num = 10;int *p = #int **pp = &p;printf("num 的值: %d\n", **pp);return 0;
}
在这个例子中,pp
是一个指向指针 p
的指针,通过 **pp
可以间接访问 num
的值。
5.3 函数指针和指针函数
5.3.1 函数指针的定义和使用
1. 函数指针的定义
函数指针是一种指向函数的指针,它可以存储函数的入口地址。定义函数指针的一般形式为:
返回值类型 (*指针变量名)(参数列表);
例如:
int (*p)(int, int); // 定义一个指向返回值为整型,参数为两个整型的函数的指针
2. 函数指针的使用
函数指针可以用来调用函数,例如:
#include <stdio.h>int add(int a, int b) {return a + b;
}int main() {int (*p)(int, int) = add; // 函数指针 p 指向函数 addint result = p(3, 4); // 通过函数指针调用函数printf("3 + 4 = %d\n", result);return 0;
}
在这个例子中,p
是一个函数指针,它指向函数 add
,通过 p(3, 4)
可以调用函数 add
并得到结果。
5.3.2 指针函数的定义和使用
1. 指针函数的定义
指针函数是一种返回值为指针的函数。定义指针函数的一般形式为:
数据类型 *函数名(参数列表);
例如:
int *getArray() {static int arr[5] = {1, 2, 3, 4, 5};return arr;
}
2. 指针函数的使用
可以调用指针函数并使用返回的指针来访问数据,例如:
#include <stdio.h>int *getArray() {static int arr[5] = {1, 2, 3, 4, 5};return arr;
}int main() {int *p = getArray();for (int i = 0; i < 5; i++) {printf("%d ", *(p + i));}printf("\n");return 0;
}
在这个例子中,getArray
是一个指针函数,它返回一个指向数组的指针,通过这个指针可以访问数组的元素。
5.4 动态内存分配
5.4.1 动态内存分配函数
在 C 语言中,可以使用动态内存分配函数来在程序运行时分配和释放内存。常用的动态内存分配函数有以下几个:
1. malloc
函数
malloc
函数用于分配指定大小的内存块,其原型为:
void *malloc(size_t size);
size
表示要分配的内存块的大小(以字节为单位),函数返回一个指向分配的内存块的指针。如果分配失败,返回 NULL
。例如:
#include <stdio.h>
#include <stdlib.h>int main() {int *p = (int *)malloc(5 * sizeof(int)); // 分配 5 个整型元素的内存空间if (p == NULL) {printf("内存分配失败\n");return 1;}for (int i = 0; i < 5; i++) {p[i] = i + 1;}for (int i = 0; i < 5; i++) {printf("%d ", p[i]);}printf("\n");free(p); // 释放分配的内存return 0;
}
在这个例子中,使用 malloc
函数分配了 5 个整型元素的内存空间,然后通过指针 p
来访问和操作这些内存空间,最后使用 free
函数释放了分配的内存。
2. calloc
函数
calloc
函数用于分配指定数量和大小的内存块,并将这些内存块初始化为 0,其原型为:
void *calloc(size_t num, size_t size);
num
表示要分配的元素数量,size
表示每个元素的大小(以字节为单位)。例如:
#include <stdio.h>
#include <stdlib.h>int main() {int *p = (int *)calloc(5, sizeof(int)); // 分配 5 个整型元素的内存空间并初始化为 0if (p == NULL) {printf("内存分配失败\n");return 1;}for (int i = 0; i < 5; i++) {printf("%d ", p[i]);}printf("\n");free(p); // 释放分配的内存return 0;
}
3. realloc
函数
realloc
函数用于重新分配已经分配的内存块的大小,其原型为:
void *realloc(void *ptr, size_t size);
ptr
是指向已经分配的内存块的指针,size
是要重新分配的内存块的大小。如果 ptr
为 NULL
,则 realloc
函数的作用和 malloc
函数相同。例如:
#include <stdio.h>
#include <stdlib.h>int main() {int *p = (int *)malloc(3 * sizeof(int)); // 分配 3 个整型元素的内存空间if (p == NULL) {printf("内存分配失败\n");return 1;}for (int i = 0; i < 3; i++) {p[i] = i + 1;}p = (int *)realloc(p, 5 * sizeof(int)); // 重新分配 5 个整型元素的内存空间if (p == NULL) {printf("内存分配失败\n");return 1;}for (int i = 3; i < 5; i++) {p[i] = i + 1;}for (int i = 0; i < 5; i++) {printf("%d ", p[i]);}printf("\n");free(p); // 释放分配的内存return 0;
}
5.4.2 内存泄漏和内存溢出
1. 内存泄漏
内存泄漏是指程序在运行过程中分配了内存,但在不再使用这些内存时没有及时释放,导致这些内存无法被其他程序使用。例如:
#include <stdio.h>
#include <stdlib.h>int main() {while (1) {int *p = (int *)malloc(1024); // 不断分配内存// 没有释放内存}return 0;
}
在这个例子中,程序不断地分配内存,但没有释放,随着程序的运行,可用的内存会越来越少,最终可能导致系统崩溃。
2. 内存溢出
内存溢出是指程序在申请内存时,没有足够的内存空间可供分配。例如:
#include <stdio.h>
#include <stdlib.h>int main() {int *p = (int *)malloc(1024 * 1024 * 1024); // 申请 1GB 的内存if (p == NULL) {printf("内存分配失败,可能是内存溢出\n");}return 0;
}
在这个例子中,如果系统没有足够的内存空间可供分配,malloc
函数会返回 NULL
,表示内存分配失败。
为了避免内存泄漏和内存溢出,在使用动态内存分配函数时,一定要记得在不再使用内存时及时使用 free
函数释放内存,并且在分配内存时要合理评估所需的内存大小。💪
第六章 结构体和共用体
6.1 结构体
6.1.1 结构体的定义和初始化
1. 结构体的定义
结构体是一种用户自定义的数据类型,它可以将不同类型的数据组合在一起,形成一个新的数据类型。就像一个“小盒子”,可以把各种不同的“物品”(数据)放在里面🧰。
定义结构体的一般形式如下:
struct 结构体名 {数据类型 成员名1;数据类型 成员名2;// 可以有更多成员
};
例如,定义一个表示学生信息的结构体:
struct Student {char name[20];int age;float score;
};
这里,struct Student
就是我们定义的新数据类型,它包含了三个成员:name
(字符数组,用于存储学生姓名)、age
(整数,用于存储学生年龄)和 score
(浮点数,用于存储学生成绩)。
2. 结构体的初始化
结构体的初始化可以在定义结构体变量时进行,有两种常见的初始化方式:
- 顺序初始化:按照结构体成员的定义顺序依次赋值。
struct Student stu1 = {"Tom", 18, 90.5};
这里,"Tom"
赋值给 name
,18
赋值给 age
,90.5
赋值给 score
。
- 指定成员初始化:可以明确指定要初始化的成员。
struct Student stu2 = {.name = "Jerry", .score = 85.0, .age = 17};
这种方式不要求按照成员定义的顺序赋值,更加灵活。
6.1.2 结构体变量的引用
结构体变量的成员可以通过“点运算符”(.
)来引用。就像打开“小盒子”,取出里面的“物品”一样😃。
例如,对于上面定义的 stu1
结构体变量:
#include <stdio.h>struct Student {char name[20];int age;float score;
};int main() {struct Student stu1 = {"Tom", 18, 90.5};printf("Name: %s\n", stu1.name);printf("Age: %d\n", stu1.age);printf("Score: %.2f\n", stu1.score);return 0;
}
在这个例子中,stu1.name
引用了 stu1
结构体变量的 name
成员,stu1.age
引用了 age
成员,stu1.score
引用了 score
成员。
6.1.3 结构体数组和结构体指针
1. 结构体数组
结构体数组是由多个相同结构体类型的元素组成的数组。就像一排“小盒子”,每个“小盒子”都装着相同类型的“物品”📦。
定义结构体数组的方式如下:
struct Student students[3];
这里定义了一个包含 3 个 struct Student
类型元素的数组。
可以对结构体数组进行初始化:
struct Student students[3] = {{"Tom", 18, 90.5},{"Jerry", 17, 85.0},{"Alice", 19, 92.0}
};
2. 结构体指针
结构体指针是指向结构体变量的指针。通过结构体指针可以间接访问结构体变量的成员。使用“箭头运算符”(->
)来引用结构体指针所指向的结构体变量的成员。
例如:
#include <stdio.h>struct Student {char name[20];int age;float score;
};int main() {struct Student stu = {"Tom", 18, 90.5};struct Student *p = &stu;printf("Name: %s\n", p->name);printf("Age: %d\n", p->age);printf("Score: %.2f\n", p->score);return 0;
}
这里,p
是一个指向 struct Student
类型的指针,它指向了 stu
结构体变量。p->name
等价于 (*p).name
,用于引用 stu
结构体变量的 name
成员。
6.1.4 结构体作为函数参数
结构体可以作为函数的参数传递,有两种传递方式:
1. 值传递
将结构体变量的副本传递给函数,函数内部对参数的修改不会影响到原结构体变量。
#include <stdio.h>struct Student {char name[20];int age;float score;
};void printStudent(struct Student s) {printf("Name: %s\n", s.name);printf("Age: %d\n", s.age);printf("Score: %.2f\n", s.score);
}int main() {struct Student stu = {"Tom", 18, 90.5};printStudent(stu);return 0;
}
2. 指针传递
将结构体变量的地址传递给函数,函数内部可以通过指针直接修改原结构体变量的内容。
#include <stdio.h>struct Student {char name[20];int age;float score;
};void updateScore(struct Student *s, float newScore) {s->score = newScore;
}int main() {struct Student stu = {"Tom", 18, 90.5};updateScore(&stu, 95.0);printf("New Score: %.2f\n", stu.score);return 0;
}
6.2 共用体
6.2.1 共用体的定义和初始化
1. 共用体的定义
共用体也是一种用户自定义的数据类型,它和结构体类似,但不同的是,共用体的所有成员共享同一块内存空间。就像一个“魔术盒子”,同一时间只能放一个“物品”🎩。
定义共用体的一般形式如下:
union 共用体名 {数据类型 成员名1;数据类型 成员名2;// 可以有更多成员
};
例如,定义一个表示不同类型数据的共用体:
union Data {int i;float f;char str[20];
};
2. 共用体的初始化
共用体的初始化只能对第一个成员进行初始化。
union Data data = {10}; // 初始化第一个成员 i
6.2.2 共用体变量的引用
共用体变量的成员同样通过“点运算符”(.
)来引用。但要注意,由于共用体的所有成员共享同一块内存空间,某一时刻只有一个成员是有效的。
例如:
#include <stdio.h>union Data {int i;float f;char str[20];
};int main() {union Data data;data.i = 10;printf("data.i: %d\n", data.i);data.f = 20.5;printf("data.f: %.2f\n", data.f);return 0;
}
在这个例子中,当给 data.f
赋值后,data.i
的值就不再有效了。
6.2.3 共用体和结构体的区别
1. 内存占用
- 结构体:结构体的每个成员都有自己独立的内存空间,结构体的总内存大小是所有成员内存大小之和。
- 共用体:共用体的所有成员共享同一块内存空间,共用体的总内存大小是其最大成员的内存大小。
2. 数据存储
- 结构体:可以同时存储多个不同类型的数据,每个成员都可以独立使用。
- 共用体:同一时间只能存储一个成员的数据,修改一个成员的值会覆盖其他成员的值。
6.3 枚举类型
6.3.1 枚举类型的定义和使用
1. 枚举类型的定义
枚举类型是一种用户自定义的数据类型,它用于定义一组具有离散值的常量。就像一个“选项列表”,可以从中选择一个选项🎛️。
定义枚举类型的一般形式如下:
enum 枚举名 {枚举常量1,枚举常量2,// 可以有更多枚举常量
};
例如,定义一个表示星期的枚举类型:
enum Weekday {MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY
};
2. 枚举类型的使用
可以定义枚举类型的变量,并为其赋值。
#include <stdio.h>enum Weekday {MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY
};int main() {enum Weekday today = WEDNESDAY;if (today == WEDNESDAY) {printf("Today is Wednesday!\n");}return 0;
}
6.3.2 枚举常量的值
枚举常量默认从 0 开始依次递增。例如,在上面的 enum Weekday
中,MONDAY
的值为 0,TUESDAY
的值为 1,以此类推。
也可以为枚举常量指定特定的值:
enum Color {RED = 1,GREEN = 2,BLUE = 4
};
在这个例子中,RED
的值为 1,GREEN
的值为 2,BLUE
的值为 4。如果只指定了部分枚举常量的值,未指定值的枚举常量会在前一个枚举常量的值基础上依次递增。
第七章 文件操作
7.1 文件的基本概念
7.1.1 文件的分类
在计算机世界里,文件就像是一个个装满信息的小盒子,根据存储内容和存储方式的不同,文件可以分为以下几类😃:
1. 文本文件(Text File)
- 存储形式:文本文件以 ASCII 码或 Unicode 码的形式存储数据,简单来说,它里面存的就是我们能看懂的字符。比如我们用记事本写的文章、代码文件等都是文本文件。
- 优点:具有良好的可读性,我们可以直接用文本编辑器打开查看和编辑内容。
- 缺点:占用存储空间相对较大,因为每个字符都需要用相应的编码来表示。
2. 二进制文件(Binary File)
- 存储形式:二进制文件以二进制的形式存储数据,它可以存储任何类型的数据,如图片、音频、视频等。这些数据在文件中是按照特定的格式进行组织的。
- 优点:占用存储空间相对较小,读写速度快,适合存储大量的数据。
- 缺点:可读性差,不能直接用文本编辑器查看内容,需要特定的软件才能打开和处理。
3. 其他分类方式
除了上述两种常见的分类方式,文件还可以根据用途分为系统文件、用户文件等;根据文件的访问权限分为只读文件、读写文件等。
7.1.2 文件的打开和关闭
在对文件进行读写操作之前,我们需要先打开文件;操作完成后,为了释放系统资源,我们需要关闭文件。这就好比我们要进入一个房间拿东西,需要先打开门,拿完东西后再把门关上🚪。
1. 文件的打开
在不同的编程语言中,打开文件的方式可能会有所不同,但一般都需要指定文件名和打开模式。常见的打开模式有:
- 只读模式(
r
):只能读取文件内容,不能修改文件。如果文件不存在,会抛出错误。 - 写入模式(
w
):用于向文件中写入数据。如果文件已经存在,会清空文件内容;如果文件不存在,会创建一个新文件。 - 追加模式(
a
):用于向文件末尾追加数据。如果文件不存在,会创建一个新文件。 - 读写模式(
r+
、w+
、a+
):既可以读取文件内容,也可以写入数据,具体的行为取决于模式的组合。
以下是 Python 中打开文件的示例代码:
# 以只读模式打开文件
file = open('example.txt', 'r')
2. 文件的关闭
文件使用完毕后,必须调用 close()
方法来关闭文件,以释放系统资源。如果不关闭文件,可能会导致数据丢失或文件损坏。
# 关闭文件
file.close()
为了避免忘记关闭文件,Python 还提供了 with
语句,它会自动处理文件的打开和关闭操作:
# 使用 with 语句打开文件
with open('example.txt', 'r') as file:# 读取文件内容content = file.read()print(content)
# 文件会在 with 语句块结束后自动关闭
7.2 文件的读写操作
7.2.1 字符读写函数
字符读写函数用于逐个字符地读写文件内容,就像我们一个一个地数苹果一样🍎。
1. 字符写入函数
在 Python 中,可以使用 write()
方法向文件中写入单个字符:
with open('example.txt', 'w') as file:file.write('H')file.write('e')file.write('l')file.write('l')file.write('o')
2. 字符读取函数
可以使用 read(1)
方法从文件中读取单个字符:
with open('example.txt', 'r') as file:char = file.read(1)while char:print(char, end='')char = file.read(1)
7.2.2 字符串读写函数
字符串读写函数用于一次性读写一段字符串,就像我们一次搬一摞书一样📚。
1. 字符串写入函数
使用 write()
方法可以向文件中写入一个字符串:
with open('example.txt', 'w') as file:file.write('Hello, World!')
2. 字符串读取函数
可以使用 read()
方法读取整个文件内容,或者使用 readline()
方法逐行读取文件内容:
# 读取整个文件内容
with open('example.txt', 'r') as file:content = file.read()print(content)# 逐行读取文件内容
with open('example.txt', 'r') as file:line = file.readline()while line:print(line, end='')line = file.readline()
7.2.3 格式化读写函数
格式化读写函数可以按照指定的格式读写文件内容,就像我们按照特定的模板填写表格一样📄。
1. 格式化写入函数
在 Python 中,可以使用 format()
方法或 f-string 来格式化字符串,并将其写入文件:
name = 'Alice'
age = 20
with open('example.txt', 'w') as file:file.write(f'Name: {name}, Age: {age}')
2. 格式化读取函数
可以使用 split()
方法将读取的字符串按照指定的分隔符分割成多个部分:
with open('example.txt', 'r') as file:content = file.read()parts = content.split(', ')name = parts[0].split(': ')[1]age = int(parts[1].split(': ')[1])print(f'Name: {name}, Age: {age}')
7.2.4 二进制读写函数
二进制读写函数用于读写二进制文件,如图片、音频、视频等。在 Python 中,可以使用 'rb'
和 'wb'
模式来打开二进制文件。
1. 二进制写入函数
# 读取图片文件
with open('image.jpg', 'rb') as source_file:data = source_file.read()# 写入新的图片文件
with open('new_image.jpg', 'wb') as target_file:target_file.write(data)
2. 二进制读取函数
with open('image.jpg', 'rb') as file:data = file.read()print(len(data))
7.3 文件的定位和错误处理
7.3.1 文件的定位函数
文件的定位函数用于在文件中移动文件指针的位置,就像我们在书本中翻到指定的页码一样📖。
1. seek()
函数
seek()
函数用于将文件指针移动到指定的位置,它接受两个参数:偏移量和起始位置。起始位置可以是 0(文件开头)、1(当前位置)或 2(文件末尾)。
with open('example.txt', 'r') as file:# 将文件指针移动到第 3 个字符的位置file.seek(2)char = file.read(1)print(char)
2. tell()
函数
tell()
函数用于返回文件指针的当前位置:
with open('example.txt', 'r') as file:# 获取文件指针的当前位置position = file.tell()print(f'当前位置: {position}')
7.3.2 文件的错误处理函数
在文件操作过程中,可能会出现各种错误,如文件不存在、权限不足等。为了避免程序崩溃,我们需要对这些错误进行处理。
1. try-except
语句
在 Python 中,可以使用 try-except
语句来捕获和处理文件操作过程中可能出现的异常:
try:with open('nonexistent_file.txt', 'r') as file:content = file.read()print(content)
except FileNotFoundError:print('文件不存在!')
2. finally
语句
finally
语句中的代码无论是否发生异常都会执行,通常用于释放资源,如关闭文件:
file = None
try:file = open('example.txt', 'r')content = file.read()print(content)
except FileNotFoundError:print('文件不存在!')
finally:if file:file.close()
通过以上的学习,我们可以掌握文件的基本概念、打开和关闭、读写操作、定位和错误处理等知识,从而更好地进行文件操作💪。
第八章 预处理命令
在 C 语言中,预处理命令是在编译器对源程序进行编译之前,由预处理程序对源程序中的预处理命令进行处理的过程。预处理命令可以帮助我们更方便地编写和管理代码。下面我们来详细了解各种预处理命令。
8.1 宏定义
宏定义是预处理命令中非常重要的一部分,它可以将一个标识符定义为一个字符串,在编译之前,编译器会将源程序中所有该标识符替换为所定义的字符串。宏定义分为无参数宏定义、带参数宏定义和宏定义的嵌套。
8.1.1 无参数宏定义
无参数宏定义的一般形式为:
#define 标识符 字符串
- 示例:
#include <stdio.h>
#define PI 3.14159int main() {double r = 5.0;double area = PI * r * r;printf("圆的面积是: %f\n", area);return 0;
}
- 解释:
- 在这个例子中,
#define PI 3.14159
就是一个无参数宏定义,它将PI
定义为3.14159
。在编译之前,编译器会将源程序中所有的PI
替换为3.14159
。这样,当我们需要修改圆周率的值时,只需要修改宏定义中的值即可,而不需要在代码中逐个修改所有使用到圆周率的地方,提高了代码的可维护性😃。
- 在这个例子中,
8.1.2 带参数宏定义
带参数宏定义的一般形式为:
#define 标识符(参数表) 字符串
- 示例:
#include <stdio.h>
#define MAX(a, b) ((a) > (b)? (a) : (b))int main() {int x = 10, y = 20;int max = MAX(x, y);printf("最大值是: %d\n", max);return 0;
}
- 解释:
#define MAX(a, b) ((a) > (b)? (a) : (b))
是一个带参数的宏定义。这里的MAX
是宏名,(a, b)
是参数表,((a) > (b)? (a) : (b))
是宏体。当在代码中使用MAX(x, y)
时,编译器会将其替换为((x) > (y)? (x) : (y))
。需要注意的是,宏定义中的参数要加括号,以避免在替换时出现运算优先级的问题🤔。
8.1.3 宏定义的嵌套
宏定义的嵌套是指在一个宏定义中可以使用另一个宏定义。
- 示例:
#include <stdio.h>
#define PI 3.14159
#define R 5.0
#define AREA PI * R * Rint main() {printf("圆的面积是: %f\n", AREA);return 0;
}
- 解释:
- 在这个例子中,
AREA
宏定义中使用了PI
和R
宏定义。在编译之前,编译器会先将AREA
中的PI
和R
替换为它们所定义的字符串,然后再进行后续的编译。这样可以使代码更加简洁和易于理解👍。
- 在这个例子中,
8.2 文件包含
文件包含是指在一个源文件中可以将另一个源文件的内容包含进来,这样可以将多个源文件组合成一个更大的程序。文件包含使用 #include
指令。
8.2.1 #include 指令的使用
#include
指令有两种形式:
- 形式一:
#include <文件名>
这种形式用于包含系统提供的头文件,编译器会在系统指定的标准库目录中查找该文件。例如:
#include <stdio.h>
- 形式二:
#include "文件名"
这种形式用于包含用户自己编写的头文件,编译器会先在当前源文件所在的目录中查找该文件,如果找不到,再到系统指定的标准库目录中查找。例如:
#include "myheader.h"
8.2.2 头文件的编写
头文件通常包含函数声明、宏定义、类型定义等内容,其作用是为源文件提供必要的信息。
- 示例:
// myheader.h
#ifndef MYHEADER_H
#define MYHEADER_H#define PI 3.14159
double circle_area(double r);#endif
// mysource.c
#include "myheader.h"
#include <stdio.h>double circle_area(double r) {return PI * r * r;
}int main() {double r = 5.0;double area = circle_area(r);printf("圆的面积是: %f\n", area);return 0;
}
- 解释:
- 在头文件
myheader.h
中,使用了#ifndef
、#define
和#endif
来防止头文件被重复包含。circle_area
函数的声明放在头文件中,而函数的定义放在源文件mysource.c
中。这样,其他源文件如果需要使用circle_area
函数,只需要包含myheader.h
头文件即可,提高了代码的模块化程度👏。
- 在头文件
8.3 条件编译
条件编译可以根据不同的条件,选择性地编译源程序中的不同部分,这样可以使程序在不同的环境下具有不同的功能。
8.3.1 #ifdef 和 #ifndef 指令
#ifdef
指令:- 一般形式为:
#ifdef 标识符程序段 1
#else程序段 2
#endif
- 其作用是,如果标识符已经被
#define
定义过,则编译程序段 1,否则编译程序段 2。 - 示例:
#include <stdio.h>
#define DEBUG#ifdef DEBUG#define PRINT_INFO printf("调试信息: 程序正在运行\n")
#else#define PRINT_INFO
#endifint main() {PRINT_INFO;return 0;
}
-
在这个例子中,由于定义了
DEBUG
宏,所以会编译#ifdef
后面的程序段,即#define PRINT_INFO printf("调试信息: 程序正在运行\n")
,这样在main
函数中调用PRINT_INFO
时就会输出调试信息。 -
#ifndef
指令:- 一般形式为:
#ifndef 标识符程序段 1
#else程序段 2
#endif
- 其作用与
#ifdef
相反,如果标识符没有被#define
定义过,则编译程序段 1,否则编译程序段 2。
8.3.2 #if 和 #else 指令
#if
指令可以根据一个常量表达式的值来决定是否编译某段代码。
- 一般形式:
#if 常量表达式程序段 1
#else程序段 2
#endif
- 示例:
#include <stdio.h>
#define VERSION 2#if VERSION == 1#define MESSAGE "这是版本 1"
#elif VERSION == 2#define MESSAGE "这是版本 2"
#else#define MESSAGE "未知版本"
#endifint main() {printf("%s\n", MESSAGE);return 0;
}
- 解释:
- 在这个例子中,根据
VERSION
的值,选择不同的程序段进行编译。如果VERSION
为 1,则编译#if
后面的程序段;如果VERSION
为 2,则编译#elif
后面的程序段;如果VERSION
既不是 1 也不是 2,则编译#else
后面的程序段。这样可以根据不同的版本号输出不同的信息😎。
- 在这个例子中,根据
第九章 位运算
位运算在计算机科学中是一种非常强大且基础的操作,它直接对二进制位进行操作,能让我们以高效的方式处理数据。接下来让我们一起深入了解位运算的相关知识😎。
9.1 位运算符
9.1.1 按位与运算符(&)
按位与运算符 &
会对两个操作数的对应二进制位进行逐位比较,只有当两个对应位都为 1 时,结果的该位才为 1,否则为 0。
示例:
假设我们有两个十进制数 5 和 3,将它们转换为二进制:
- 5 的二进制表示是
0101
- 3 的二进制表示是
0011
进行按位与运算:
0101
& 0011
------0001
结果的二进制是 0001
,转换为十进制就是 1。
代码示例(Python):
a = 5
b = 3
result = a & b
print(result) # 输出 1
9.1.2 按位或运算符(|)
按位或运算符 |
同样对两个操作数的对应二进制位进行逐位比较,只要两个对应位中有一个为 1,结果的该位就为 1,只有当两个对应位都为 0 时,结果的该位才为 0。
示例:
还是以 5 和 3 为例:
- 5 的二进制表示是
0101
- 3 的二进制表示是
0011
进行按位或运算:
0101
| 0011
------0111
结果的二进制是 0111
,转换为十进制就是 7。
代码示例(Python):
a = 5
b = 3
result = a | b
print(result) # 输出 7
9.1.3 按位异或运算符(^)
按位异或运算符 ^
对两个操作数的对应二进制位进行逐位比较,当两个对应位不同时,结果的该位为 1,相同时结果的该位为 0。
示例:
对于 5 和 3:
- 5 的二进制表示是
0101
- 3 的二进制表示是
0011
进行按位异或运算:
0101
^ 0011
------0110
结果的二进制是 0110
,转换为十进制就是 6。
代码示例(Python):
a = 5
b = 3
result = a ^ b
print(result) # 输出 6
9.1.4 取反运算符(~)
取反运算符 ~
是单目运算符,它会对操作数的每一个二进制位进行取反操作,即 1 变为 0,0 变为 1。
示例:
假设操作数是 5,二进制表示为 0101
,取反后得到 1010
。在计算机中,整数通常以补码形式存储,这里需要注意取反后的结果是补码形式。对于有符号整数,取反后的结果计算方法比较复杂,这里简单说明,在 Python 中:
代码示例(Python):
a = 5
result = ~a
print(result) # 输出 -6
9.1.5 左移运算符(<<)
左移运算符 <<
会将操作数的二进制位向左移动指定的位数,右边空出的位用 0 填充。左移 n 位相当于将操作数乘以 2 的 n 次方。
示例:
将 5 左移 2 位:
- 5 的二进制表示是
0101
左移 2 位后得到010100
,转换为十进制就是 20。
代码示例(Python):
a = 5
result = a << 2
print(result) # 输出 20
9.1.6 右移运算符(>>)
右移运算符 >>
会将操作数的二进制位向右移动指定的位数,左边空出的位根据操作数的符号位填充(正数补 0,负数补 1)。右移 n 位相当于将操作数除以 2 的 n 次方(向下取整)。
示例:
将 5 右移 1 位:
- 5 的二进制表示是
0101
右移 1 位后得到0010
,转换为十进制就是 2。
代码示例(Python):
a = 5
result = a >> 1
print(result) # 输出 2
9.2 位运算的应用
9.2.1 位屏蔽
位屏蔽是指通过按位与运算,将某些位保留,其他位清零。我们可以使用一个掩码(mask)来实现。掩码中需要保留的位为 1,需要清零的位为 0。
示例:
假设我们有一个 8 位的二进制数 11010110
,我们想保留低 4 位,其他位清零。掩码可以设置为 00001111
。
11010110
& 00001111
----------00000110
结果就是保留了低 4 位,其他位清零。
代码示例(Python):
num = 0b11010110
mask = 0b00001111
result = num & mask
print(bin(result)) # 输出 0b110
9.2.2 位清零
位清零是指将操作数的某些位设置为 0。可以通过按位与一个掩码来实现,掩码中需要清零的位为 0,其他位为 1。
示例:
假设我们有一个 8 位的二进制数 11010110
,我们想将第 3 位和第 4 位清零。掩码可以设置为 11100111
。
11010110
& 11100111
----------11000110
结果就是第 3 位和第 4 位被清零。
代码示例(Python):
num = 0b11010110
mask = 0b11100111
result = num & mask
print(bin(result)) # 输出 0b11000110
9.2.3 位设置
位设置是指将操作数的某些位设置为 1。可以通过按位或一个掩码来实现,掩码中需要设置为 1 的位为 1,其他位为 0。
示例:
假设我们有一个 8 位的二进制数 11010110
,我们想将第 2 位设置为 1。掩码可以设置为 00000100
。
11010110
| 00000100
----------11010110
结果就是第 2 位被设置为 1。
代码示例(Python):
num = 0b11010110
mask = 0b00000100
result = num | mask
print(bin(result)) # 输出 0b11010110
9.2.4 位测试
位测试是指检查操作数的某些位是否为 1。可以通过按位与一个掩码来实现,掩码中需要测试的位为 1,其他位为 0。如果结果不为 0,则说明该位为 1,否则为 0。
示例:
假设我们有一个 8 位的二进制数 11010110
,我们想测试第 4 位是否为 1。掩码可以设置为 00010000
。
11010110
& 00010000
----------00010000
结果不为 0,说明第 4 位为 1。
代码示例(Python):
num = 0b11010110
mask = 0b00010000
result = num & mask
if result != 0:print("第 4 位为 1")
else:print("第 4 位为 0")
通过这些位运算的应用,我们可以在很多场景下高效地处理数据,比如在嵌入式系统中进行硬件寄存器的操作等👏。
第十章 高级编程技巧
10.1 内存管理
10.1.1 内存池的实现
1. 什么是内存池
想象一下,你是一个餐厅老板,每次有顾客点菜,你都要现去采购食材,这样效率会很低。而内存池就像是你提前准备好的食材储备库,当程序需要内存时,直接从这个储备库里拿,而不是每次都向操作系统申请。内存池是一种内存分配方式,它预先分配一定数量、大小相等的内存块,程序需要内存时直接从这些预先分配好的内存块中获取,使用完后再归还到内存池中,而不是归还给操作系统。
2. 实现步骤
- 初始化内存池:首先要确定内存池的大小和每个内存块的大小。例如,我们创建一个可以容纳 100 个大小为 1024 字节内存块的内存池。
#define BLOCK_SIZE 1024
#define POOL_SIZE 100typedef struct MemoryBlock {struct MemoryBlock *next;char data[BLOCK_SIZE];
} MemoryBlock;typedef struct MemoryPool {MemoryBlock *freeList;int blockCount;
} MemoryPool;void initMemoryPool(MemoryPool *pool) {pool->blockCount = POOL_SIZE;pool->freeList = NULL;for (int i = 0; i < POOL_SIZE; i++) {MemoryBlock *block = (MemoryBlock *)malloc(sizeof(MemoryBlock));block->next = pool->freeList;pool->freeList = block;}
}
- 分配内存:当程序需要内存时,从内存池的空闲列表中取出一个内存块。
void* allocateMemory(MemoryPool *pool) {if (pool->freeList == NULL) {return NULL; // 内存池为空}MemoryBlock *block = pool->freeList;pool->freeList = block->next;pool->blockCount--;return block->data;
}
- 释放内存:当程序使用完内存后,将内存块归还给内存池。
void freeMemory(MemoryPool *pool, void *ptr) {MemoryBlock *block = (MemoryBlock *)((char *)ptr - offsetof(MemoryBlock, data));block->next = pool->freeList;pool->freeList = block;pool->blockCount++;
}
10.1.2 垃圾回收机制
1. 什么是垃圾回收
在编程中,我们会创建很多对象和变量,当这些对象和变量不再被使用时,它们占用的内存就成了“垃圾”。垃圾回收机制就是自动回收这些不再使用的内存的一种机制,就像清洁工定时清理垃圾一样,保证系统内存的有效利用。
2. 常见的垃圾回收算法
- 标记 - 清除算法:
- 标记阶段:从根对象(如全局变量)开始,遍历所有可达的对象,并标记它们。可以想象成给每个可达的对象贴上一个“使用中”的标签。
- 清除阶段:遍历整个内存空间,将未标记的对象(即不可达的对象)的内存回收。就像把没有标签的垃圾清理掉。
- 标记 - 整理算法:
- 标记阶段:和标记 - 清除算法一样,从根对象开始标记所有可达的对象。
- 整理阶段:将所有标记的对象移动到内存的一端,然后将边界之外的内存回收。这就好比把有用的物品都集中到一个地方,然后把剩下的空间清理出来。
- 复制算法:
- 将内存分为两个相等的区域,每次只使用其中一个区域。
- 当这个区域的内存用完后,将所有存活的对象复制到另一个区域,然后将原来的区域全部清空。就像把一个房间里有用的东西搬到另一个房间,然后把原来的房间打扫干净。
10.2 多线程编程
10.2.1 线程的创建和销毁
1. 线程的创建
线程是程序中的一个执行单元,多线程编程可以让程序同时执行多个任务,提高程序的效率。在 C 语言中,可以使用 POSIX 线程库来创建线程。
#include <pthread.h>
#include <stdio.h>// 线程函数
void* threadFunction(void* arg) {printf("This is a new thread.\n");return NULL;
}int main() {pthread_t thread;// 创建线程int result = pthread_create(&thread, NULL, threadFunction, NULL);if (result != 0) {perror("Thread creation failed");return 1;}// 等待线程结束pthread_join(thread, NULL);printf("Main thread exiting.\n");return 0;
}
在上面的代码中,pthread_create
函数用于创建一个新的线程,它接受四个参数:线程标识符的指针、线程属性、线程函数和传递给线程函数的参数。
2. 线程的销毁
线程的销毁有两种方式:
- 自然结束:线程函数执行完毕后,线程自然结束。
- 强制终止:可以使用
pthread_cancel
函数来强制终止一个线程,但这种方式需要谨慎使用,因为可能会导致资源泄漏等问题。
pthread_cancel(thread);
10.2.2 线程同步和互斥
1. 为什么需要线程同步和互斥
多个线程同时访问共享资源时,可能会出现数据不一致的问题。例如,两个线程同时对一个变量进行加 1 操作,可能会导致最终结果不是预期的。线程同步和互斥就是为了解决这些问题,保证多个线程对共享资源的访问是安全的。
2. 互斥锁
互斥锁是一种最常用的线程同步机制,它就像一把锁,同一时间只允许一个线程访问共享资源。
#include <pthread.h>
#include <stdio.h>pthread_mutex_t mutex;
int sharedVariable = 0;void* increment(void* arg) {// 加锁pthread_mutex_lock(&mutex);sharedVariable++;// 解锁pthread_mutex_unlock(&mutex);return NULL;
}int main() {pthread_t thread1, thread2;// 初始化互斥锁pthread_mutex_init(&mutex, NULL);// 创建线程pthread_create(&thread1, NULL, increment, NULL);pthread_create(&thread2, NULL, increment, NULL);// 等待线程结束pthread_join(thread1, NULL);pthread_join(thread2, NULL);// 销毁互斥锁pthread_mutex_destroy(&mutex);printf("Shared variable: %d\n", sharedVariable);return 0;
}
在上面的代码中,pthread_mutex_lock
函数用于加锁,pthread_mutex_unlock
函数用于解锁,pthread_mutex_init
函数用于初始化互斥锁,pthread_mutex_destroy
函数用于销毁互斥锁。
3. 信号量
信号量是另一种线程同步机制,它可以控制同时访问共享资源的线程数量。例如,一个停车场有 10 个车位,信号量就可以控制最多有 10 辆车进入停车场。
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>sem_t semaphore;
int sharedVariable = 0;void* increment(void* arg) {// 等待信号量sem_wait(&semaphore);sharedVariable++;// 释放信号量sem_post(&semaphore);return NULL;
}int main() {pthread_t thread1, thread2;// 初始化信号量sem_init(&semaphore, 0, 1);// 创建线程pthread_create(&thread1, NULL, increment, NULL);pthread_create(&thread2, NULL, increment, NULL);// 等待线程结束pthread_join(thread1, NULL);pthread_join(thread2, NULL);// 销毁信号量sem_destroy(&semaphore);printf("Shared variable: %d\n", sharedVariable);return 0;
}
在上面的代码中,sem_wait
函数用于等待信号量,sem_post
函数用于释放信号量,sem_init
函数用于初始化信号量,sem_destroy
函数用于销毁信号量。
10.3 网络编程
10.3.1 网络编程基础
1. 网络模型
常见的网络模型有 OSI 七层模型和 TCP/IP 四层模型。
- OSI 七层模型:从下到上分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。它就像一个多层的大楼,每一层都有自己的功能,通过层与层之间的协作来完成网络通信。
- TCP/IP 四层模型:从下到上分别是网络接口层、网络层、传输层和应用层。它是 OSI 七层模型的简化版本,更符合实际的网络应用。
2. 网络协议
网络协议是网络通信的规则和约定,常见的网络协议有 TCP、UDP、HTTP 等。
- TCP(传输控制协议):是一种面向连接的、可靠的传输协议。它就像打电话,在通信之前需要先建立连接,通信过程中保证数据的可靠传输,通信结束后需要断开连接。
- UDP(用户数据报协议):是一种无连接的、不可靠的传输协议。它就像发短信,不需要建立连接,直接发送数据,不保证数据的可靠传输。
- HTTP(超文本传输协议):是一种用于传输超文本的协议,常用于网页浏览。它是基于 TCP 协议的应用层协议。
10.3.2 TCP 和 UDP 编程
1. TCP 编程
TCP 编程的基本步骤如下:
- 服务器端:
- 创建套接字(socket)。
- 绑定地址和端口(bind)。
- 监听连接(listen)。
- 接受连接(accept)。
- 收发数据(send/recv)。
- 关闭套接字(close)。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>#define PORT 8888int main() {int serverSocket, clientSocket;struct sockaddr_in serverAddr, clientAddr;socklen_t clientAddrLen = sizeof(clientAddr);char buffer[1024];// 创建套接字serverSocket = socket(AF_INET, SOCK_STREAM, 0);if (serverSocket == -1) {perror("Socket creation failed");return 1;}// 绑定地址和端口serverAddr.sin_family = AF_INET;serverAddr.sin_addr.s_addr = INADDR_ANY;serverAddr.sin_port = htons(PORT);if (bind(serverSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) == -1) {perror("Bind failed");return 1;}// 监听连接if (listen(serverSocket, 5) == -1) {perror("Listen failed");return 1;}printf("Waiting for connections...\n");// 接受连接clientSocket = accept(serverSocket, (struct sockaddr *)&clientAddr, &clientAddrLen);if (clientSocket == -1) {perror("Accept failed");return 1;}printf("Connection accepted.\n");// 收发数据memset(buffer, 0, sizeof(buffer));recv(clientSocket, buffer, sizeof(buffer), 0);printf("Received: %s\n", buffer);send(clientSocket, "Hello, client!", strlen("Hello, client!"), 0);// 关闭套接字close(clientSocket);close(serverSocket);return 0;
}
- 客户端:
- 创建套接字(socket)。
- 连接服务器(connect)。
- 收发数据(send/recv)。
- 关闭套接字(close)。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>#define PORT 8888
#define SERVER_IP "127.0.0.1"int main() {int clientSocket;struct sockaddr_in serverAddr;char buffer[1024];// 创建套接字clientSocket = socket(AF_INET, SOCK_STREAM, 0);if (clientSocket == -1) {perror("Socket creation failed");return 1;}// 设置服务器地址serverAddr.sin_family = AF_INET;serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);serverAddr.sin_port = htons(PORT);// 连接服务器if (connect(clientSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) == -1) {perror("Connect failed");return 1;}printf("Connected to server.\n");// 收发数据send(clientSocket, "Hello, server!", strlen("Hello, server!"), 0);memset(buffer, 0, sizeof(buffer));recv(clientSocket, buffer, sizeof(buffer), 0);printf("Received: %s\n", buffer);// 关闭套接字close(clientSocket);return 0;
}
2. UDP 编程
UDP 编程的基本步骤如下:
- 服务器端:
- 创建套接字(socket)。
- 绑定地址和端口(bind)。
- 收发数据(recvfrom/sendto)。
- 关闭套接字(close)。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>#define PORT 8888int main() {int serverSocket;struct sockaddr_in serverAddr, clientAddr;socklen_t clientAddrLen = sizeof(clientAddr);char buffer[1024];// 创建套接字serverSocket = socket(AF_INET, SOCK_DGRAM, 0);if (serverSocket == -1) {perror("Socket creation failed");return 1;}// 绑定地址和端口serverAddr.sin_family = AF_INET;serverAddr.sin_addr.s_addr = INADDR_ANY;serverAddr.sin_port = htons(PORT);if (bind(serverSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) == -1) {perror("Bind failed");return 1;}printf("Waiting for data...\n");// 收发数据memset(buffer, 0, sizeof(buffer));recvfrom(serverSocket, buffer, sizeof(buffer), 0, (struct sockaddr *)&clientAddr, &clientAddrLen);printf("Received: %s\n", buffer);sendto(serverSocket, "Hello, client!", strlen("Hello, client!"), 0, (struct sockaddr *)&clientAddr, clientAddrLen);// 关闭套接字close(serverSocket);return 0;
}
- 客户端:
- 创建套接字(socket)。
- 收发数据(sendto/recvfrom)。
- 关闭套接字(close)。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>#define PORT 8888
#define SERVER_IP "127.0.0.1"int main() {int clientSocket;struct sockaddr_in serverAddr;socklen_t serverAddrLen = sizeof(serverAddr);char buffer[1024];// 创建套接字clientSocket = socket(AF_INET, SOCK_DGRAM, 0);if (clientSocket == -1) {perror("Socket creation failed");return 1;}// 设置服务器地址serverAddr.sin_family = AF_INET;serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP);serverAddr.sin_port = htons(PORT);// 收发数据sendto(clientSocket, "Hello, server!", strlen("Hello, server!"), 0, (struct sockaddr *)&serverAddr, serverAddrLen);memset(buffer, 0, sizeof(buffer));recvfrom(clientSocket, buffer, sizeof(buffer), 0, (struct sockaddr *)&serverAddr, &serverAddrLen);printf("Received: %s\n", buffer);// 关闭套接字close(clientSocket);return