【编程语言】【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 

int main() {
    printf("Hello, World!\n");
    return 0;
}

让我们来分析一下这个程序的基本结构:

  • 预处理指令#include 是一个预处理指令,它告诉编译器在编译之前要包含标准输入输出库(stdio.h)的内容。这个库提供了许多输入输出函数,如 printfscanf
  • 主函数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 

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语言提供了两种浮点型数据类型:floatdouble

  • float:单精度浮点型,占用 4 个字节,能够表示大约 6 到 7 位有效数字。
  • double:双精度浮点型,占用 8 个字节,能够表示大约 15 到 16 位有效数字。

示例代码如下:

#include 

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 

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 

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 
#include 

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 

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 = &num;

其中,& 是取地址运算符,用于获取变量的内存地址。可以通过指针来访问和修改变量的值,示例代码如下:

#include 

int main() {
    int num = 10;
    int *p = &num;
    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 

int main() {
    int num = 10;
    void *p = &num;
    int *ip = (int *)p;
    printf("Value of num: %d\n", *ip);
    return 0;
}

1.4 变量和常量

1.4.1 变量的定义和使用

变量是程序中用于存储数据的内存单元,在使用变量之前需要先进行定义。变量的定义方式如下:

数据类型 变量名;

例如,定义一个整数变量:

int num;

可以在定义变量的同时进行初始化,示例代码如下:

#include 

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 

#define PI 3.1415926

int 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
++ 自增 ++aa++
-- 自减 --aa--

示例代码如下:

#include 

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 (表达式) {
    case1:
        // 表达式的值等于值1时执行的代码
        System.out.println("值为1");
        break;
    case2:
        // 表达式的值等于值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:
        break
    print(i)
  • continue语句continue 语句用于跳过当前循环的剩余部分,直接进入下一次循环,就像跳过一个障碍物,继续前进‍♀️。例如:
for i in range(10):
    if i % 2 == 0:
        continue
    print(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 是函数的返回值类型,比如 intfloat 等,如果函数不返回任何值,使用 void
  • function_name 同样是函数名。
  • parameter_list 是参数列表,每个参数需要指定类型。
  • 函数体用花括号 {} 括起来。

3.1.2 函数的调用方式

函数定义好后,就可以在其他地方调用它,就像使用魔法盒子一样。

Python 语言
def add_numbers(a, b):
    return a + b

result = add_numbers(3, 5)  # 调用函数,传递参数 3 和 5
print(result)  # 输出结果 8

在 Python 中,直接使用函数名,后面跟上括号,括号内传入实际的参数即可调用函数。

C 语言
#include 

int add_numbers(int a, int b) {
    return a + b;
}

int main() {
    int result = add_numbers(3, 5);  // 调用函数
    printf("%d\n", result);  // 输出结果 8
    return 0;
}

在 C 语言中,也是通过函数名和参数列表来调用函数,通常在 main 函数中调用其他函数。

3.1.3 函数的返回值

函数的返回值是函数执行完毕后返回给调用者的结果。返回值的类型在函数定义时就已经确定。

Python 语言
def square(x):
    return x * x

result = square(4)  # 调用函数,返回 16
print(result)

在 Python 中,return 语句可以返回任意类型的值,包括数字、字符串、列表等。

C 语言
#include 

int square(int x) {
    return x * x;
}

int main() {
    int result = square(4);  // 调用函数,返回 16
    printf("%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 + 1
    return x

num = 5
new_num = change_value(num)
print(num)  # 输出 5,num 的值没有改变
print(new_num)  # 输出 6
C 语言
#include 

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 

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 + b

def multiply_and_add(x, y, z):
    result = add(x, y) * z
    return result

final_result = multiply_and_add(2, 3, 4)
print(final_result)  # 输出 20
C 语言
#include 

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);  // 输出 20
    return 0;
}

在上面的例子中,multiply_and_add 函数内部调用了 add 函数。

3.3.2 函数的递归调用

函数的递归调用是指函数在其函数体内部调用自身。递归就像是一个“无限循环”的魔法盒子,自己调用自己,但必须有一个终止条件,否则会陷入无限递归。

Python 语言
def factorial(n):
    if n == 0 or n == 1:
        return 1
    else:
        return n * factorial(n - 1)

result = factorial(5)
print(result)  # 输出 120
C 语言
#include 

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);  # 输出 120
    return 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 

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 

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 

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 

void test_function() {
    static int count = 0;  // 静态变量
    count++;
    printf("%d\n", count);
}

int main() {
    test_function();  // 输出 1
    test_function();  // 输出 2
    return 0;
}
3.4.2.3 寄存器变量

寄存器变量使用 register 关键字声明。寄存器变量存储在 CPU 的寄存器中,访问速度比普通变量快。但寄存器的数量有限,编译器可能会忽略 register 声明。

C 语言
#include 

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 

int global_variable = 10;  // 全局变量

void test_function();

int main() {
    test_function();
    return 0;
}
文件 2(file2.c)
#include 

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 

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 

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 

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 

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 

int main() {
    char str[100];
    printf("请输入一个字符串:");
    scanf("%s", str);
    printf("你输入的字符串是:%s\n", str);
    return 0;
}

这里使用 scanf 函数输入字符串,需要注意的是,scanf 函数遇到空格就会停止读取。如果需要输入包含空格的字符串,可以使用 gets 函数,但 gets 函数存在缓冲区溢出的风险,在实际开发中建议使用 fgets 函数。

2. 输出

可以使用 printf 函数或 puts 函数来输出字符串。例如:

#include 

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 
#include 

int main() {
    char str[100] = "Hello, World!";
    int len = strlen(str);
    printf("字符串的长度是:%d\n", len);
    return 0;
}
  • strcpy 函数:用于将一个字符串复制到另一个字符串中。例如:
#include 
#include 

int main() {
    char str1[100] = "Hello";
    char str2[100];
    strcpy(str2, str1);
    printf("复制后的字符串是:%s\n", str2);
    return 0;
}
  • strcmp 函数:用于比较两个字符串的大小。如果两个字符串相等,返回 0;如果第一个字符串小于第二个字符串,返回一个负数;如果第一个字符串大于第二个字符串,返回一个正数。例如:
#include 
#include 

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 

int main() {
    int num = 10;
    int *p = &num;  // 初始化指针 p 指向变量 num 的地址
    printf("num 的地址是: %p\n", &num);
    printf("指针 p 存储的地址是: %p\n", p);
    return 0;
}

在这个例子中,& 是取地址运算符,&num 表示变量 num 的内存地址,将这个地址赋值给指针 p,就完成了指针的初始化。

  • 初始化为 NULL
    如果暂时不知道指针要指向哪里,可以将其初始化为 NULLNULL 是一个特殊的指针值,表示空指针。
int *p = NULL;
5.1.2 指针的运算

指针可以进行一些特殊的运算,主要包括以下几种:

1. 指针的算术运算
  • 指针的加法和减法
    指针可以加上或减去一个整数,其结果是一个新的指针,指向的地址会根据指针所指向的数据类型的大小进行相应的偏移。例如:
#include 

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 

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 

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 

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 

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 

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 

int main() {
    int num = 10;
    int *p = &num;
    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 

int add(int a, int b) {
    return a + b;
}

int main() {
    int (*p)(int, int) = add;  // 函数指针 p 指向函数 add
    int 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 

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 
#include 

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 
#include 

int main() {
    int *p = (int *)calloc(5, sizeof(int));  // 分配 5 个整型元素的内存空间并初始化为 0
    if (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 是要重新分配的内存块的大小。如果 ptrNULL,则 realloc 函数的作用和 malloc 函数相同。例如:

#include 
#include 

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 
#include 

int main() {
    while (1) {
        int *p = (int *)malloc(1024);  // 不断分配内存
        // 没有释放内存
    }
    return 0;
}

在这个例子中,程序不断地分配内存,但没有释放,随着程序的运行,可用的内存会越来越少,最终可能导致系统崩溃。

2. 内存溢出

内存溢出是指程序在申请内存时,没有足够的内存空间可供分配。例如:

#include 
#include 

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" 赋值给 name18 赋值给 age90.5 赋值给 score

  • 指定成员初始化:可以明确指定要初始化的成员。
struct Student stu2 = {.name = "Jerry", .score = 85.0, .age = 17};

这种方式不要求按照成员定义的顺序赋值,更加灵活。

6.1.2 结构体变量的引用

结构体变量的成员可以通过“点运算符”(.)来引用。就像打开“小盒子”,取出里面的“物品”一样。

例如,对于上面定义的 stu1 结构体变量:

#include 

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 

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 

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 

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 

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 

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 
#define PI 3.14159

int 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 
#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 
#define PI 3.14159
#define R 5.0
#define AREA PI * R * R

int main() {
    printf("圆的面积是: %f\n", AREA);
    return 0;
}
  • 解释
    • 在这个例子中,AREA 宏定义中使用了 PIR 宏定义。在编译之前,编译器会先将 AREA 中的 PIR 替换为它们所定义的字符串,然后再进行后续的编译。这样可以使代码更加简洁和易于理解。

8.2 文件包含

文件包含是指在一个源文件中可以将另一个源文件的内容包含进来,这样可以将多个源文件组合成一个更大的程序。文件包含使用 #include 指令。

8.2.1 #include 指令的使用

#include 指令有两种形式:

  • 形式一
#include <文件名>

这种形式用于包含系统提供的头文件,编译器会在系统指定的标准库目录中查找该文件。例如:

#include 
  • 形式二
#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 

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 
#define DEBUG

#ifdef DEBUG
    #define PRINT_INFO printf("调试信息: 程序正在运行\n")
#else
    #define PRINT_INFO
#endif

int 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 
#define VERSION 2

#if VERSION == 1
    #define MESSAGE "这是版本 1"
#elif VERSION == 2
    #define MESSAGE "这是版本 2"
#else
    #define MESSAGE "未知版本"
#endif

int 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 100

typedef 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 
#include 

// 线程函数
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 
#include 

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 
#include 
#include 

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 
#include 
#include 
#include 
#include 
#include 

#define PORT 8888

int 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 
#include 
#include 
#include 
#include 
#include 

#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 
#include 
#include 
#include 
#include 
#include 

#define PORT 8888

int 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 
#include 
#include 
#include 
#include 
#include 

#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

你可能感兴趣的:(C语言知识点,c语言,开发语言)