C语言学习笔记
1. C 语言概述
-
1.1 C 语言的历史与发展
- 起源
- C 语言由 Dennis Ritchie 在 1972 年于贝尔实验室开发,目的是为了实现 UNIX 操作系统。
- C 语言是在 B 语言的基础上发展而来的,B 语言是由 Ken Thompson 开发的。
- 发展
- 1978 年,Brian Kernighan 和 Dennis Ritchie 合作出版了《C 程序设计语言》(K&R),这本书成为 C 语言的经典教材,并推广了 C 语言的使用。
- 1983 年,C 语言被国际标准化组织(ISO)采纳,形成了 ISO C 标准。
- 1999 年,发布了 C99 标准,增加了对新的数据类型(如 long long 和 bool)的支持,并引入了变量声明的灵活性。
- 2011 年,发布了 C11 标准,增强了多线程支持、内存管理功能以及对泛型编程的支持。
-
1.2 C 语言的特点与应用
- 特点
- 高效性:C 语言接近于计算机硬件,编写的程序执行效率高。
- 灵活性:提供了低级的内存操作能力,允许直接操作硬件。
- 可移植性:C 语言代码可在不同平台上编译运行,只需少量修改。
- 丰富的库支持:标准库提供了大量的函数,可以用于输入输出、字符串处理、数学运算等。
- 结构化编程:支持函数和模块化编程,使程序结构清晰、易于维护。
- 应用
- 系统软件:操作系统(如 UNIX、Linux、Windows)和编译器的开发。
- 嵌入式系统:广泛应用于嵌入式设备开发,如微控制器和嵌入式操作系统。
- 游戏开发:许多游戏引擎使用 C 语言进行开发,以提高性能。
- 网络编程:在网络协议和服务器软件中广泛应用。
- 科学计算与数值分析:用于开发高性能的计算程序。
-
1.3 C 语言的编译过程
-
1.4 常用 C 语言开发工具与环境搭建
- 开发环境
- 文本编辑器:可使用任何文本编辑器,如 Vim、Nano、Notepad++、VS Code 等。
- 集成开发环境(IDE):如 Code::Blocks、Dev-C++、Eclipse CDT、Visual Studio 等,这些 IDE 提供了代码高亮、调试和编译功能。
- 编译器
- GCC(GNU Compiler Collection):广泛使用的开源编译器,支持多种平台。
- Clang:由 LLVM 项目开发,提供高效的编译速度和丰富的功能。
- MSVC(Microsoft Visual C++):适用于 Windows 开发的专有编译器。
- 调试工具
- GDB(GNU Debugger):开源调试工具,用于调试 C 语言程序。
- Valgrind:内存调试和泄漏检测工具,帮助查找内存管理问题。
- 设置环境
- Linux 系统:可以直接使用终端安装 GCC 和其他工具。
- Windows 系统:可以使用 MinGW 或 Cygwin 安装 GCC,或者直接安装 Visual Studio。
- macOS 系统:可以使用 Homebrew 安装 GCC 或 Clang。
2. C 语言基础
C语言是一种广泛使用的程序设计语言,因其高效性和灵活性而受到欢迎。理解C语言的基本概念是编程的基础,下面将详细介绍C语言的基础知识
3. 控制结构
控制结构是编程语言的基本组成部分,用于控制程序的执行流程。主要包括条件语句和循环语句,这些结构帮助程序根据不同的输入和条件采取不同的执行路径,从而实现复杂的逻辑和功能。
4. 函数
函数是程序设计中的一个基本概念,用于将特定的操作或逻辑封装起来,以便重用和组织代码。在C语言中,函数的使用能够使程序更加模块化、易于理解和维护。
-
4.1 函数的定义与声明 函数声明是在使用函数之前对其名称、返回类型及参数类型的声明。它告诉编译器这个函数的基本信息,但并不提供具体的实现。 函数定义则是对函数的具体实现,包含了函数的主体和逻辑。 函数声明的语法: - return_type function_name(parameter_type1 parameter_name1, parameter_type2 parameter_name2, ...);
复制代码函数定义的语法: - return_type function_name(parameter_type1 parameter_name1, parameter_type2 parameter_name2, ...) {
- // 函数的具体实现
- }
复制代码示例: - #include <stdio.h>
- // 函数声明
- int add(int a, int b);
- // 函数定义
- int add(int a, int b) {
- return a + b;
- }
- int main() {
- int sum = add(5, 10); // 调用函数
- printf("Sum: %d\n", sum);
- return 0;
- }
复制代码其中,函数的声明有多种方式: 在C语言中,函数声明是对函数名称、返回类型和参数类型的说明,目的是让编译器知道函数的基本信息,以便在调用函数之前进行正确的类型检查。函数声明可以有几种不同的形式,以下是详细说明: 4.1.1 基本函数声明 基本的函数声明包括返回类型、函数名称和参数列表。参数列表可以为空,也可以包含一个或多个参数。 语法: - return_type function_name(parameter_type1 parameter_name1, parameter_type2 parameter_name2, ...);
复制代码示例: - int add(int a, int b); // 返回类型为int,函数名为add,接受两个int类型的参数
复制代码
其中:
编译器对C代码是顺序编译的,而且总是从main函数开始。因此,如果自定义函数位于main函数后,则必须在main函数前先声明该函数(即调用之前先声明);但如果自定义函数位于main函数前,则不用再进行函数声明,此时函数定义已包含了函数声明的作用。
其中:
在定义函数时指定的形参,在为出现函数调用时,它们并不占内存中的存储单元,因此称它们是形式参数或虚拟参数,简称形参,表示它们并不是实际存在的数据,所以形参里的变量不能赋值。
4.1.2 不带参数的函数声明 如果函数不接受任何参数,可以使用void关键字表示参数列表为空。这是C语言中表明函数不接收参数的一种方式。 语法: - extern return_type function_name(void);
复制代码其中extern声明关键字可以省略,void关键字也可以省略 示例: - void printMessage(void); // 返回类型为void,表示该函数没有参数
复制代码4.1.3 函数声明中的参数类型 在函数声明中,参数名称可以省略,只保留参数类型。这样做的好处是能够更简洁地声明函数,但编写代码的人必须清楚函数的参数类型。 语法: - return_type function_name(parameter_type1, parameter_type2, ...);
复制代码示例: - float calculateArea(float, float); // 省略参数名称,仅保留参数类型
复制代码4.1.4 函数指针的声明 函数指针可以用来指向具有特定参数和返回类型的函数。声明函数指针时,首先定义返回类型,然后是指针标识符,最后是参数列表。 语法: - return_type (*pointer_name)(parameter_type1, parameter_type2, ...);
复制代码示例: - int (*operation)(int, int); // 声明一个指向返回int类型并接受两个int参数的函数的指针
复制代码4.1.5 带有数组或指针参数的函数声明 如果函数接受数组或指针作为参数,参数可以声明为数组的指针类型。对于数组参数,通常在声明时省略数组的大小。 语法: - return_type function_name(type_name[]); // 数组形式
- // 或
- return_type function_name(type_name*); // 指针形式
复制代码示例: - void processArray(int arr[], int size); // 数组作为参数
- void processPointer(int* ptr, int size); // 指针作为参数
复制代码4.1.6 变长参数的函数声明 C语言允许使用变长参数的函数,这类函数可以接收不定数量的参数。通常使用stdarg.h库来处理这类参数。声明时使用...表示参数的可变性。 语法: - return_type function_name(parameter_type1, ..., parameter_typeN, ...);
复制代码示例: - #include <stdarg.h>
- void myPrintf(const char* format, ...); // 变长参数的函数声明
复制代码4.1.7 函数的类型定义 可以使用typedef定义一个函数类型,以简化函数指针的声明。这样在需要多个地方使用相同类型的函数指针时,可以提高可读性。 示例: - typedef int (*FuncPtr)(int, int); // 定义一个函数指针类型
- FuncPtr add; // 使用定义的类型声明函数指针
复制代码 -
4.2 函数参数传递 C语言中的函数参数可以通过两种方式传递:值传递和引用传递(指针传递)。
-
4.3 递归函数 递归函数是一个直接或间接调用自身的函数。递归通常用于解决具有重复结构的问题,如树遍历和数学计算(例如阶乘和斐波那契数列)。每个递归函数必须有一个基准条件,以防止无限递归。 示例:计算阶乘 - #include <stdio.h>
- int factorial(int n) {
- if (n == 0) { // 基准条件
- return 1;
- }
- return n * factorial(n - 1); // 递归调用
- }
- int main() {
- int num = 5;
- printf("Factorial of %d: %d\n", num, factorial(num)); // 输出 120
- return 0;
- }
复制代码 -
4.4 函数指针 函数指针是指向函数的指针,可以用来实现回调机制、动态函数调用等。通过函数指针,可以将函数作为参数传递,或者在运行时决定要调用哪个函数。 定义函数指针的语法: - return_type (*pointer_name)(parameter_type1, parameter_type2, ...);
复制代码示例: - #include <stdio.h>
- // 定义一个函数
- int add(int a, int b) {
- return a + b;
- }
- int subtract(int a, int b) {
- return a - b;
- }
- // 定义函数指针
- typedef int (*operation)(int, int);
- int main() {
- operation op; // 函数指针
- op = add; // 指向 add 函数
- printf("Add: %d\n", op(5, 10)); // 调用 add
- op = subtract; // 指向 subtract 函数
- printf("Subtract: %d\n", op(10, 5)); // 调用 subtract
- return 0;
- }
复制代码 -
4.5 随机数 在C语言中,生成随机数通常使用rand()函数,该函数定义在stdio.h头文件中。rand()函数每次被调用时都会返回一个伪随机数,范围从0到RAND_MAX,RAND_MAX是一个定义在stdio.h中的常量,通常为32767。 其中:如果想生成特定范围内的随机数,可使用取模运算符%来实现 例:
5. 数组
数组是一种数据结构,可以存储固定数量的相同类型的元素。数组的元素可以通过索引进行访问,索引从零开始。
-
5.1 一维数组 定义和初始化 一维数组是线性的数据结构,元素在内存中是连续存储的。可以通过以下方式定义和初始化一维数组: - type arrayName [ arraySize ];
复制代码其中:
- type 可以是任意有效的 C 数据类型。
- arrayName必须是一个有效的标识符。
- arraySize 必须是一个大于零的整数常量
例: - int myArray[10];// 声明数组 声明一个类型为int,包含10个元素的数组myArray
- myArray[0] = 1;// 定义数组元素(初始化数组元素)
- int myNewArray[10] = { 1, 2, 3, 4, 7, 5, 6, 8, 9, 0 };// 批量初始化数组(定义数组)
复制代码访问数组元素 数组元素可以通过数组名称加索引进行访问。元素的索引是放在方括号内,跟在数组名称的后边。例如: - int salary = myNewArray[9];
复制代码其中:
- 数组名[下标值] 可以访问数组。
- [数组名]下标值 也可以访问数组,此方法不推荐使用,此为C语言语法糖之一,后续指针部分会有讲解。
- #include <stdio.h>
- int main() {
- // 定义并初始化数组
- int arr[5] = {1, 2, 3, 4, 5};
- // 访问数组元素
- for (int i = 0; i < 5; i++) {
- printf("%d ", arr[i]);
- }
- return 0;
- }
复制代码
在C语言中,标识符(Identifier)是用来给变量、函数、数组、结构体等程序元素命名的字符序列。标识符遵循以下规则:
- 首字符:标识符的第一个字符必须是字母(大写或小写)或下划线(_)。
- 后续字符:后续的字符可以是字母、数字或下划线。
- 长度限制:标识符的长度可能会受到编译器的限制,但至少可以有31个字符。
- 大小写敏感:C语言是大小写敏感的,这意味着identifier和Identifier被视为两个不同的标识符。
- 保留字:不能使用C语言的保留字作为标识符,例如int、if、while等。
- 可读性:为了代码的可读性,标识符应该具有描述性,能够让人理解其代表的含义。
例如,myVariable、counter1、_value、temp123都是有效的C语言标识符。而2variable(以数字开头)、my-variable(包含减号)和if(C语言保留字)则不是有效的标识符。
数组的特点
- 固定大小:数组在定义时需要指定大小,无法动态改变。
- 元素类型一致:数组中的所有元素必须是相同类型。
常用操作
- 遍历数组:使用循环结构访问每个元素。
- 求和/平均值:可以通过遍历数组实现。
- int sum = 0;
- for (int i = 0; i < 5; i++) {
- sum += arr[i];
- }
- float average = sum / 5.0;
复制代码 -
5.2 二维数组 定义和初始化 二维数组可以视为数组的数组,常用于表示矩阵。定义和初始化的方式如下: - #include <stdio.h>
- int main() {
- // 定义并初始化二维数组
- int matrix[3][3] = {
- {1, 2, 3},
- {4, 5, 6},
- {7, 8, 9}
- };
- // 访问二维数组元素
- for (int i = 0; i < 3; i++) {
- for (int j = 0; j < 3; j++) {
- printf("%d ", matrix[i][j]);
- }
- printf("\n");
- }
- return 0;
- }
复制代码二维数组的特点
- 行和列:访问方式为 matrix[row][column]。
- 内存布局:在内存中,二维数组是以行优先的方式存储的。
常用操作
- 矩阵加法:可以通过遍历两个矩阵实现相应位置元素的加法。
- 转置矩阵:可以通过交换行和列来实现。
-
5.3 多维数组 定义和初始化 多维数组是指具有两个以上维度的数组,常用于更复杂的数据结构,如三维数组等。以下是一个三维数组的示例: - #include <stdio.h>
- int main() {
- // 定义并初始化三维数组
- int array[2][3][4] = {
- {
- {1, 2, 3, 4},
- {5, 6, 7, 8},
- {9, 10, 11, 12}
- },
- {
- {13, 14, 15, 16},
- {17, 18, 19, 20},
- {21, 22, 23, 24}
- }
- };
- // 访问三维数组元素
- for (int i = 0; i < 2; i++) {
- for (int j = 0; j < 3; j++) {
- for (int k = 0; k < 4; k++) {
- printf("%d ", array[i][j][k]);
- }
- printf("\n");
- }
- printf("\n");
- }
- return 0;
- }
复制代码多维数组的特点
- 维度任意:可以定义任意维度的数组,但需要考虑内存使用情况。
- 访问方式:访问元素时需要指定所有维度的索引。
-
5.4 数组与函数的结合 数组可以作为函数的参数传递。C语言中,数组的实际传递是以指针的形式进行的,因此在函数内对数组的修改会影响原数组。 - void modifyArray(int arr[], int size) {
- for (int i = 0; i < size; i++) {
- arr[i] *= 2; // 将数组中的每个元素乘以2
- }
- }
复制代码在主函数中,可以这样调用: - int main() {
- int myArray[] = {1, 2, 3, 4, 5};
- int size = sizeof(myArray) / sizeof(myArray[0]);
- modifyArray(myArray, size);
- // myArray 现在变成 {2, 4, 6, 8, 10}
- return 0;
- }
复制代码虽然C语言不支持直接返回数组,但可以通过返回指针来实现。通常使用动态内存分配来创建数组。 - int* createArray(int size) {
- int* arr = (int*)malloc(size * sizeof(int));
- for (int i = 0; i < size; i++) {
- arr[i] = i + 1; // 初始化数组
- }
- return arr;
- }
复制代码注意,调用者需要负责手动释放这个动态分配的内存。 - int main() {
- int size = 5;
- int* myArray = createArray(size);
- // 使用 myArray
- free(myArray); // 释放内存
- return 0;
- }
复制代码对于多维数组,可以通过在函数参数中明确维度来传递。 - void printMatrix(int matrix[][3], int rows) {
- for (int i = 0; i < rows; i++) {
- for (int j = 0; j < 3; j++) {
- printf("%d ", matrix[i][j]);
- }
- printf("\n");
- }
- }
复制代码在主函数中,可以这样调用: - int main() {
- int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};
- printMatrix(matrix, 2);
- return 0;
- }
复制代码 -
5.4 获取数组长度 数组长度可以使用 sizeof 运算符来获取数组的长度,例如: - int numbers[] = {1, 2, 3, 4, 5};
- int length = sizeof(numbers) / sizeof(numbers[0]);
复制代码
在 C 语言中,一个数组只能存储一种有效的数据类型,所以同一个数组里面的元素数据类型都相同,求数组大小的常见方法是使用 sizeof 运算符来计算数组的总字节数,然后除以单个数组元素的字节数,从而得到数组的元素个数。这是因为 sizeof 运算符返回的是对象(包括数组)的字节大小,而不是元素的个数。我们需要知道每个元素的大小才能正确计算数组的元素个数。
- sizeof(numbers)
sizeof(numbers) 返回的是整个数组 numbers 占用的字节数。对于数组 numbers,其类型是 int[5],即一个包含 5 个整数的数组。假设 int 类型在你的机器上占用 4 个字节,那么:
- sizeof(numbers) 将返回数组 numbers 的总字节数:
sizeof(numbers) = 5 * sizeof(int) = 5 * 4 = 20 字节。
- sizeof(numbers[0])
numbers[0] 是数组中的第一个元素,也就是 int 类型的一个整数。sizeof(numbers[0]) 返回的是单个数组元素的大小。
- sizeof(numbers[0]) = sizeof(int) = 4 字节。
- 元素个数
length = 20 / 4 = 5
因此,数组 numbers 中包含 5 个元素。
- 注意事项
- 仅适用于静态数组:这种方法只能用于静态数组(即在编译时就已知大小的数组)。对于动态数组(例如通过 malloc 分配的数组),这种方法是不可行的,因为 sizeof 对动态数组返回的是指针的大小,而不是数组的实际大小。对于动态数组,你需要使用额外的变量来跟踪数组的大小。
例:
- int *numbers = malloc(5 * sizeof(int)); // 动态分配的数组
- int length = 5; // 必须手动维护数组的长度
复制代码
- 数组的类型:如果数组是多维数组,例如 int numbers[3][4];,sizeof(numbers) 将返回整个数组的字节数(3 * 4 * sizeof(int))。此时,sizeof(numbers[0]) 返回的是第一维的元素大小(4 * sizeof(int)),所以在这种情况下你需要进行适当的计算,或者直接使用 sizeof(numbers) / sizeof(numbers[0]) 来获取某一维的大小。
6. 字符串处理
字符串是由字符组成的数组,以 '\0'(空字符)结尾。C语言中的字符串处理非常灵活,但也需要注意数组的边界和内存管理。
7. 指针
在学习指针之前,我们需要了解计算机中内存的概念
指针是C语言中的一个重要特性,它允许程序直接访问内存地址,极大地增强了程序的灵活性和效率。
-
7.1 指针的定义与使用 指针是一个变量,其值为另一个变量的地址。可以通过取地址运算符&获取变量的地址,通过解引用运算符*访问指针所指向的值。
- int a = 10; // 定义一个整数变量
- int *p = &a; // 定义一个指向整数的指针,并将其初始化为a的地址
复制代码
- #include <stdio.h>
- int main() {
- int a = 10; // 整数变量
- int *p = &a; // 指针p指向a的地址
- printf("a的值: %d\n", a); // 输出: a的值: 10
- printf("p的值: %p\n", (void*)p); // 输出p的地址
- printf("*p的值: %d\n", *p); // 输出: *p的值: 10
- *p = 20; // 通过指针修改a的值
- printf("修改后的a的值: %d\n", a); // 输出: 修改后的a的值: 20
- return 0;
- }
复制代码其中,指针还能够改变某些常量的值 - #include<stdio.h>
- int main() {
- const int a = 10;
- printf("常量a的值是:%d\n", a);
- int* p = &a;
- *p = 100;
- printf("常量a的值是:%d\n", a);
- printf("指针p的值是:%d\n", *p);// 打印指针 p 指向的值,即 a 的值
- return 0;
- }
复制代码 -
7.2 指针与数组的关系 在C语言中,数组名实际上是一个指向数组第一个元素的指针。因此,数组可以通过指针进行操作。
-
7.2.1 数组与指针 - #include <stdio.h>
- int main() {
- int arr[] = {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;
- }
复制代码 -
7.3 指针与函数 指针可以作为函数参数,使得函数能够直接修改传入的变量。此外,函数也可以返回指针。
-
7.4 指针的高级使用
8. 结构体与联合体
概述:
在C语言中存在一种数据结构:
- 数组:描述一组具有相同类型数据的有序集合,用于大量处理相同类型的数据运算。
但数组只能处理单一数据类型,所以C语言中给出了另一种构造数据类型——结构体
- 8.1 结构体的定义与使用
- 结构体变量的定义和初始化
- 结构体类型和结构体变量的关系:
- 8.2 嵌套结构体
- 8.3 结构体与指针
- 8.4 结构体做函数参数
- 1.结构体普通变量做函数参数
- 2.结构体指针变量做函数参数
- 3.结构体数组名做函数参数
- 4.const修饰结构体指针形参变量
- 8.5 typedef关键字详解
- 8.6 联合体(共用体)的定义与使用
- 8.7 枚举
9. 文件操作
- 9.1 文件的打开与关闭
- 9.2 文件的读写操作
- 9.3 错误处理
10. C 语言标准库
- 10.1 常用头文件介绍
- 10.1.1 :输入输出库
- 10.1.2 :标准库(内存分配、随机数等)
- 10.1.3 :字符串处理函数
- 10.1.4 :数学函数
- 10.2 使用标准库函数的注意事项
11. 进阶主题
- 11.1 动态内存管理
- 11.1.1 malloc()、calloc()、realloc()、free()
- 11.2 数据结构
- 11.2.1 链表
- 11.2.2 栈与队列
- 11.2.3 树
- 11.2.4 图
- 11.3 预处理器指令
- 11.4 多文件项目与头文件
12. 调试与优化
- 12.1 常见错误与调试技巧
- 12.2 性能优化方法
- 12.3 代码规范与注释
13. 实践项目
- 13.1 简单计算器
- 13.2 学生信息管理系统
- 13.3 猜数字游戏
- 13.4 文件系统模拟
- 13.5 数据结构实现(如链表、栈、队列)
14. 拓展学习
- 14.1 C++ 和其他语言的对比
- 14.2 C 语言在嵌入式系统中的应用
- 14.3 C 语言在操作系统中的应用
- 14.4 学习算法与数据结构的结合
15. 资源推荐
- 15.1 书籍推荐
- 15.2 在线学习平台
- 15.3 社区与论坛
|