C语言精讲(视频教程)
概述
- C以高效、灵活、可移植为核心特点,是操作系统、嵌入式系统、驱动程序开发的基石,也是学习计算机底层原理(如内存管理、指针机制)的“入门钥匙”。
- 视频教程:
https://pan.quark.cn/s/e258d116f7f2
一、C语言基础:构建程序的“砖瓦”
C语言程序的基本结构由头文件包含、函数定义、语句块组成,所有程序的入口都是main()
函数。
1.1 基本语法规则
(1)程序结构示例
一个最简单的C语言程序(输出“Hello, C!”):
#include <stdio.h> // 头文件包含:引入标准输入输出库// 主函数:程序入口,int表示返回值类型,argc/argv为命令行参数(可省略)
int main(int argc, char *argv[]) {printf("Hello, C!\n"); // 语句:调用printf函数输出内容,末尾需加“;”return 0; // 主函数返回0,表示程序正常结束
}
- 头文件:以
#include
开头,用于引入库函数的声明(如stdio.h
包含printf
的声明),分为标准库头文件(<xxx.h>
)和自定义头文件("xxx.h"
)。 - 函数:C语言的基本执行单元,格式为
返回值类型 函数名(参数列表) { 语句块 }
,main()
是唯一必须定义的函数。 - 语句:每条执行指令以
;
结尾,printf
是标准库中的输出函数,\n
表示换行符。
(2)数据类型:变量的“身份标识”
C语言的数据类型分为基本类型和构造类型,基本类型是程序的“最小数据单元”:
类型分类 | 具体类型 | 占用内存(32位系统) | 用途说明 |
---|---|---|---|
整数类型 | char | 1字节 | 存储字符(本质是ASCII码)或小整数 |
short | 2字节 | 短整数 | |
int | 4字节 | 常用整数类型 | |
long | 4字节(32位)/8字节(64位) | 长整数 | |
浮点数类型 | float | 4字节 | 单精度浮点数(精度约6-7位) |
double | 8字节 | 双精度浮点数(精度约15-17位) | |
布尔类型 | _Bool (C99新增) | 1字节 | 存储0 (假)或1 (真),需包含<stdbool.h> |
无类型 | void | 无 | 表示“无返回值”或“无类型指针” |
变量定义规则:必须先定义、后使用,格式为类型名 变量名;
,可初始化(如int a = 10;
)。
常量:不可修改的值,分为字面常量(如10
、3.14
、'A'
)和const
修饰的常量(如const int MAX = 100;
)。
(3)运算符:变量的“计算工具”
C语言运算符按功能分为算术、赋值、关系、逻辑、位运算等,需注意优先级(如*
高于+
)和结合性(如赋值运算符“从右到左”)。
运算符类型 | 常用运算符 | 示例 | 优先级(高→低) |
---|---|---|---|
算术运算符 | + - * / % (取余) | 3 + 2 、5 % 2 = 1 | 高 |
赋值运算符 | = += -= *= /= | a += 3 → a = a+3 | 低 |
关系运算符 | > < >= <= == != | a > 5 (返回0或1) | 中 |
逻辑运算符 | && (与)、` | (或)、 !`(非) | |
位运算符 | & (与)、` | (或)、 ^`(异或)、`<<`(左移)、`>>`(右移) | 3 << 1 = 6 (二进制11 →110 ) |
注意:
- 整数除法(如
5 / 2
)结果为整数(2
),需转换为浮点数(如5.0 / 2
)才能得到2.5
; - 位运算仅适用于整数类型,是嵌入式开发中操作硬件寄存器的核心手段。
(4)控制流:程序的“执行路线”
控制流决定程序的执行顺序,分为顺序结构(默认)、分支结构(条件判断)、循环结构(重复执行)。
① 分支结构:if-else
与switch
if-else
:处理二分支或多分支判断int score = 85; if (score >= 90) {printf("优秀\n"); } else if (score >= 80) {printf("良好\n"); } else {printf("合格\n"); }
switch
:处理多分支(基于整数/字符匹配),break
用于跳出分支,default
处理默认情况char grade = 'B'; switch (grade) {case 'A': printf("90-100\n"); break;case 'B': printf("80-89\n"); break;default: printf("其他\n"); }
② 循环结构:for
、while
、do-while
for
:适合已知循环次数的场景,格式为for(初始化; 条件; 更新)
// 输出1-5 for (int i = 1; i <= 5; i++) {printf("%d ", i); }
while
:适合未知循环次数的场景,先判断条件再执行int i = 1; while (i <= 5) {printf("%d ", i);i++; }
do-while
:先执行一次循环体,再判断条件(至少执行一次)int i = 1; do {printf("%d ", i);i++; } while (i <= 5);
③ 跳转语句:break
、continue
、return
break
:跳出当前循环或switch
;continue
:跳过本次循环剩余语句,直接进入下一次循环;return
:结束当前函数,返回指定值(无返回值函数用return;
)。
1.2 数组:同类型数据的“集合”
数组是连续存储的同类型变量集合,通过“数组名+下标”访问元素(下标从0
开始)。
(1)一维数组
- 定义格式:
类型名 数组名[数组长度];
(长度必须是常量) - 初始化:可部分初始化(未初始化元素默认为
0
)int arr1[5] = {1, 2, 3}; // 等价于{1,2,3,0,0} int arr2[] = {1,2,3,4}; // 长度自动推导为4
- 访问元素:
arr[0]
(第一个元素)、arr[4]
(第五个元素),注意避免下标越界(如访问arr[5]
会导致内存越界,程序崩溃)。
(2)二维数组
可理解为“数组的数组”,常用于存储表格类数据,定义格式:类型名 数组名[行长度][列长度];
int matrix[2][3] = {{1,2,3}, // 第一行{4,5,6} // 第二行
};
printf("%d", matrix[1][2]); // 输出6(第二行第三列)
二、C语言核心:指针与函数(难点与重点)
指针和函数是C语言的“灵魂”,指针直接操作内存地址,函数实现代码复用,二者结合是C语言灵活性的核心来源。
2.1 指针:内存地址的“名片”
(1)指针的本质
- 内存地址:计算机中每个字节的唯一编号(如
0x0012FF44
),指针变量就是存储“内存地址”的变量。 - 定义格式:
类型名 *指针名;
,*
表示“指针类型”,类型名
表示指针指向的数据类型。
(2)指针的基本操作
int a = 10; // 定义普通变量a,内存地址假设为0x0012FF44
int *p = &a; // 定义指针p,存储a的地址(&是“取地址符”)printf("%d", a); // 输出10(直接访问a的值)
printf("%p", &a); // 输出0x0012FF44(a的地址,%p是地址格式化符)
printf("%p", p); // 输出0x0012FF44(p存储的地址)
printf("%d", *p); // 输出10(*是“解引用符”,通过p的地址访问a的值)*p = 20; // 通过指针修改a的值
printf("%d", a); // 输出20
(3)指针的核心应用场景
-
函数传参:实现“引用传递”
C语言默认是“值传递”(函数接收参数的副本,修改副本不影响原变量),通过指针可实现“修改原变量”:// 交换两个整数的值 void swap(int *x, int *y) {int temp = *x;*x = *y;*y = temp; }int main() {int a = 1, b = 2;swap(&a, &b); // 传入a和b的地址printf("a=%d, b=%d", a, b); // 输出a=2, b=1return 0; }
-
操作数组:数组名本质是“常量指针”
数组名存储数组首元素的地址(不可修改),因此arr[i]
等价于*(arr + i)
:int arr[] = {1,2,3,4}; int *p = arr; // p指向数组首元素(等价于&arr[0]) printf("%d", *(p+2)); // 输出3(访问arr[2])
-
动态内存分配
结合malloc
/free
函数(需包含<stdlib.h>
),可在程序运行时动态申请内存(数组长度可动态确定):int n = 5; int *p = (int*)malloc(n * sizeof(int)); // 申请5个int的内存(20字节) if (p == NULL) { // 必须检查内存是否申请成功printf("内存申请失败\n");return 1; } for (int i = 0; i < n; i++) {p[i] = i + 1; // 给动态数组赋值 } free(p); // 释放内存,避免内存泄漏 p = NULL; // 避免野指针(指向已释放内存的指针)
2.2 函数:代码复用的“单元”
(1)函数的定义与调用
函数的核心是“输入参数→处理逻辑→输出返回值”,需满足“声明与定义一致”(参数类型、返回值类型匹配)。
- 函数声明:告诉编译器函数的“接口”,通常放在头文件中
// 声明:返回值int,参数为两个int int add(int x, int y);
- 函数定义:实现函数的具体逻辑
// 定义:与声明一致 int add(int x, int y) {return x + y; // 返回两数之和 }
- 函数调用:在
main()
或其他函数中使用int main() {int result = add(3, 5); // 调用add,传入3和5printf("%d", result); // 输出8return 0; }
(2)函数的高级特性
- 函数参数默认值:C语言标准(C89/C99)不支持默认参数,需通过“函数重载”的模拟实现(如定义多个参数个数不同的函数)。
- 函数指针:指向函数的指针,可实现“函数回调”(如排序函数
qsort
中传入比较函数):// 比较函数:用于qsort排序(升序) int compare(const void *a, const void *b) {return *(int*)a - *(int*)b; }int main() {int arr[] = {3,1,2};qsort(arr, 3, sizeof(int), compare); // 传入比较函数的地址for (int i = 0; i < 3; i++) {printf("%d ", arr[i]); // 输出1 2 3}return 0; }
三、C语言进阶:结构体与文件操作
3.1 结构体:自定义复杂数据类型
结构体(struct
)用于将不同类型的数据打包成一个整体,解决“单一类型无法描述复杂对象”的问题(如描述“学生”需要姓名、年龄、成绩等)。
(1)结构体的定义与使用
// 定义结构体类型(struct Student)
struct Student {char name[20]; // 姓名(字符数组)int age; // 年龄float score; // 成绩
};int main() {// 定义结构体变量并初始化struct Student stu1 = {"Zhang San", 20, 90.5};// 访问结构体成员(用“.”)printf("Name: %s\n", stu1.name); // 输出Zhang Sanprintf("Age: %d\n", stu1.age); // 输出20// 结构体指针(用“->”访问成员)struct Student *p = &stu1;printf("Score: %.1f\n", p->score); // 输出90.5return 0;
}
(2)结构体数组
存储多个结构体对象(如多个学生信息):
struct Student class[2] = {{"Zhang San", 20, 90.5},{"Li Si", 19, 88.0}
};
printf("%s", class[1].name); // 输出Li Si
3.2 文件操作:数据持久化
文件操作是将数据存储到磁盘(或从磁盘读取)的核心手段,C语言通过文件指针(FILE*
)操作文件,需包含<stdio.h>
。
(1)文件打开与关闭
- 打开文件:
FILE *fopen(const char *filename, const char *mode);
mode
(打开模式)常用值:"r"
:只读(文件必须存在);"w"
:只写(文件不存在则创建,存在则清空);"a"
:追加(在文件末尾写入,文件不存在则创建);"rb"
/"wb"
:二进制文件的读/写。
- 关闭文件:
int fclose(FILE *stream);
(必须关闭,避免资源泄漏)。
(2)文件读写操作
① 文本文件读写
- 写入:
fprintf(FILE *stream, const char *format, ...);
(类似printf
,但输出到文件) - 读取:
fscanf(FILE *stream, const char *format, ...);
(类似scanf
,但从文件读取)
示例:将学生信息写入文件,再读取并输出:
int main() {struct Student stu = {"Zhang San", 20, 90.5};FILE *fp = fopen("student.txt", "w"); // 打开文件(写模式)if (fp == NULL) { // 检查文件是否成功打开perror("fopen failed"); // 输出错误信息return 1;}// 写入数据到文件fprintf(fp, "%s %d %.1f", stu.name, stu.age, stu.score);fclose(fp); // 关闭文件// 读取文件数据struct Student read_stu;fp = fopen("student.txt", "r"); // 重新打开(读模式)fscanf(fp, "%s %d %.1f", read_stu.name, &read_stu.age, &read_stu.score);printf("%s %d %.1f", read_stu.name, read_stu.age, read_stu.score); // 输出写入的数据fclose(fp);return 0;
}
② 二进制文件读写
适合存储结构体等复杂数据(避免文本格式转换的损耗):
- 写入:
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
- 读取:
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
四、C语言工程实践:规范与避坑
4.1 常见错误与避坑
-
野指针:指针未初始化(指向随机地址)或指向已释放的内存,访问野指针会导致程序崩溃。
避坑:指针定义时初始化(如int *p = NULL;
),释放内存后将指针置为NULL
。 -
内存泄漏:使用
malloc
/calloc
申请的内存未用free
释放,长期运行会耗尽内存。
避坑:申请内存后,确保在所有退出路径(如return
、break
)中调用free
。 -
数组下标越界:访问数组时下标超过“长度-1”,会破坏其他变量的内存,导致程序行为异常。
避坑:用常量定义数组长度(如#define LEN 5
),循环中检查下标范围。 -
函数声明与定义不一致:如声明时参数为
int
,定义时为float
,编译器会报错或产生未定义行为。
避坑:将函数声明放在头文件中,定义与声明严格匹配。
4.2 编码规范
- 命名规范:变量/函数名用“小写+下划线”(如
student_score
),常量用“大写+下划线”(如MAX_SIZE
)。 - 注释规范:关键逻辑用单行注释(
//
),函数功能用多行注释(/* ... */
),说明参数和返回值。 - 头文件保护:自定义头文件中添加“防止重复包含”的宏(避免编译错误):
// student.h #ifndef STUDENT_H // 如果未定义STUDENT_H #define STUDENT_H // 定义STUDENT_Hstruct Student {char name[20];int age;float score; };#endif // 结束条件编译
五、总结
C语言的核心优势在于对内存的直接控制和极高的执行效率,它的难点(指针、内存管理)正是其灵活性的来源。学习C语言不仅是掌握一门编程语言,更是理解计算机底层工作原理(如内存布局、指令执行)的过程。
对于初学者,建议按“基础语法→函数与指针→结构体与文件→工程实践”的顺序学习,通过大量编程练习(如实现排序算法、链表、小型文件管理系统)巩固知识点,同时注重编码规范和错误排查能力的培养。掌握C语言后,再学习C++、Java、Python等语言,会对“内存管理”“面向过程/面向对象”等概念有更深刻的理解。