理解比记忆更重要,代码都直接运行一遍,想明白整个过程,肯定没问题。希望能对期末复习的小朋友们有所帮助。
第1章 C语言概述与开发环境
C语言的特殊地位
C语言在高级语言中拥有独特地位——兼具高级特性和底层控制能力。什么意思呢?
- 高级特性:像其他高级语言一样,C语言有函数、结构体等抽象机制,让你不用关心硬件细节就能写程序
- 底层控制:C语言可以直接访问和管理内存地址(通过指针),可以操作位运算,甚至可以内嵌汇编
一句话总结:C语言站在高级语言和汇编语言之间,既有高级语言的抽象能力,又有接近底层的控制力。
程序开发流程
源代码到可执行文件的完整过程:
| 阶段 | 做什么 | 类比 |
|---|---|---|
| 预处理 | 处理 #include、#define 等指令 |
把参考书整本抄进来 |
| 编译 | 把预处理后的代码翻译成目标代码(.o文件) | 翻译成机器能懂的"半成品" |
| 链接 | 把多个目标代码和库合并成可执行文件 | 把各个零件组装成成品 |
注意:程序开发设计阶段的主要任务是确定实现方案和架构,不是写代码!写代码是"实现阶段"的事。
开发环境
- GCC编译器:推荐初学者使用,原因是标准兼容性和跨平台性(不是"编译最快"或"错误提示好")
- Windows安装:最简便的方式是使用集成MinGW的IDE(如Code::Blocks、Dev-C++)
- 历史背景:上世纪C语言开发主要在命令行环境中进行,不是图形界面
知识速记
- C语言兼具高级特性和底层控制能力
- C语言能直接访问和管理内存地址
- 源代码到可执行文件:预处理-编译-链接
- 设计阶段主要任务:确定实现方案和架构
- GCC推荐理由:标准兼容性和跨平台性
- 上世纪C开发主要在命令行环境,不是图形界面
第2章 基本语法与编码规范
语句结束符
C语言中每条语句必须以分号 ; 结尾。这是硬性规定,忘记分号是初学者最常见的编译错误之一。
int a = 10; // 正确
int b = 20 // 编译错误:缺少分号
中文标点陷阱
代码中使用中文标点符号会导致编译错误和语法冲突!
int a=10; // 错误!"="和";"是中文标点
int a=10; // 正确的英文标点
中文标点和英文标号看起来几乎一样,但计算机区分它们。这是很多同学从中文输入法切回来时容易犯的错误。
代码注释
良好的代码注释应该重点说明代码的目的和逻辑,而不是代码的语法结构。
// 好的注释:说明目的
// 计算圆的面积
area = PI * r * r;
// 坏的注释:重复语法
// 把PI乘以r再乘以r赋值给area
area = PI * r * r;
printf函数的头文件
printf 函数的声明位于 stdio.h 头文件中,不是 string.h!
| 函数 | 所属头文件 |
|---|---|
| printf, scanf, fprintf, fopen | stdio.h |
| strlen, strcpy, strcmp | string.h |
| malloc, free, exit | stdlib.h |
| sin, cos, sqrt | math.h |
知识速记
- 每条语句必须以分号 ; 结尾
- 中文标点导致编译错误和语法冲突
- 良好注释说明目的和逻辑,不是语法结构
- printf声明在stdio.h,不是string.h
第3章 头文件与函数基础
头文件的作用
使用头文件而不是直接声明的主要好处是提高代码组织性和重用性。
// 直接声明:每次都要手写,容易出错
int add(int a, int b);
void print_result(int r);
// 使用头文件:一次定义,处处包含
#include "mylib.h" // 所有声明都在头文件中
头文件的核心价值:
- 组织性:把相关声明集中管理
- 重用性:多个源文件可以共享同一套声明
- 一致性:修改声明只需改头文件,不用逐个文件改
注意:使用头文件的主要好处不是减少编译时间(这是一个常见的误解!)
头文件保护机制
复杂项目中避免头文件重复包含的方法是使用头文件保护机制:
#ifndef MYHEADER_H
#define MYHEADER_H
// 头文件内容
#endif
或者使用 #pragma once(部分编译器支持)。
函数参数传递方式
C语言中函数调用时参数采用按值传递方式。这意味着函数接收的是参数的副本,修改副本不影响原变量。(第11章会详细讲)
递归调用
C语言中实现函数递归调用的方法是函数调用自身:
int factorial(int n) {
if (n <= 1) return 1; // 基准条件
return n * factorial(n-1); // 调用自身
}
知识速记
- 头文件好处:提高代码组织性和重用性
- 头文件好处不是减少编译时间
- stdio.h包含标准输入输出函数声明
- 避免重复包含:使用头文件保护机制
- 函数参数按值传递
- 递归 = 函数调用自身
第4章 数据类型与类型转换
基本数据类型
| 类型 | 用途 | 大小(典型) |
|---|---|---|
| int | 存储整数 | 4字节 |
| float | 单精度浮点数 | 4字节 |
| double | 双精度浮点数 | 8字节 |
| char | 字符(本质是小整数) | 1字节 |
为什么不全用long double?
虽然 long double 精度最高,但占用内存多且计算慢。选择数据类型要在精度和性能之间取舍。
静态类型 vs 动态类型
- C语言是静态类型语言:变量类型在编译时确定,声明时必须指定类型
- Python是动态类型语言:变量类型在运行时确定,不需要提前声明
C语言(静态): int a = 10; // 编译时就知道a是int
Python(动态): a = 10 // 运行时才知道a是int
类型转换
- 隐式类型转换:编译器自动进行的类型转换
int a = 10;
double b = a; // int自动转为double,b = 10.0
2. 强制类型转换:使用括号加目标类型
double pi = 3.14;
int i = (int)pi; // 强制转换,i = 3(小数部分截断,不是四舍五入!)
注意:强制类型转换的语法是 (目标类型),不是什么 convert 关键字,也不是函数调用!
int赋值浮点数的后果
int a = 3.14; // 小数部分被截断,a = 3
不是编译错误,不是运行错误,不是自动转换——是小数部分被截断!
知识速记
- int存储整数,float/double存浮点数,char存字符
- 静态类型语言在编译时确定类型
- Python是动态类型系统
- 隐式转换:编译器自动进行
- 强制转换语法:
(类型),如(int)pi - int赋值浮点数:小数部分被截断(不是四舍五入)
- 不全用long double:占内存多且计算慢
第5章 变量与可移植性
未初始化变量
C语言中未初始化的局部变量包含不确定的垃圾值(不是0,不是空值,不是默认值)!
int a; // a的值是不确定的垃圾值
printf("%d", a); // 可能输出任何值,这是未定义行为
注意:全局变量会自动初始化为0,但局部变量不会!这是重要区别。
变量命名
- 良好变量命名风格的主要好处是提高代码可读性和维护性
- 变量名不能使用C语言关键字,因为关键字有特殊语法含义(如
int、return、while等)
int大小问题
int类型大小在不同平台可能不同,这是因为历史原因和硬件架构差异:
- 16位系统:int通常是2字节
- 32位系统:int通常是4字节
- 64位系统:int通常是4字节(但long可能不同)
可移植代码
编写可移植C语言代码应该使用标准类型并避免平台特性:
- 使用
sizeof而不是硬编码大小 - 使用标准库函数而不是平台特有API
- 注意路径分隔符(Windows用
\,Linux用/)
IDE格式化功能
IDE代码格式化功能的主要作用是自动调整格式保持一致性,不是检查语法,不是优化性能。
知识速记
- 未初始化局部变量:不确定的垃圾值
- 全局变量自动初始化为0,局部变量不会
- 良好命名提高可读性和维护性
- 变量名不能用关键字(关键字有特殊语法含义)
- int大小因平台不同:历史原因和硬件架构差异
- 可移植代码:使用标准类型,避免平台特性
- IDE格式化:自动调整格式保持一致性
第6章 常量定义:#define vs const
这是高频对比知识点,必须牢记两者的区别!
| 特性 | #define | const |
|---|---|---|
| 处理阶段 | 预处理阶段(编译前) | 编译阶段 |
| 类型检查 | 没有类型检查 | 有类型检查 |
| 内存占用 | 不占用内存(文本替换) | 占用内存(真正变量) |
| 调试信息 | 调试时看不到宏名 | 调试时可以看到变量名 |
| 作用域 | 全局(无作用域限制) | 遵循变量作用域规则 |
#define 的工作原理
#define PI 3.14
// 预处理器在编译前把所有PI替换成3.14(纯文本替换)
// 所以PI不是真正的变量,不分配内存
const 的工作原理
const double PI = 3.14;
// PI是一个真正的变量,有类型(double),有内存地址
// 只是编译器禁止修改它的值
什么时候用const?
当需要类型检查和调试信息时,应该使用 const 而不是 #define。因为 const 有类型检查,编译器能帮你发现类型不匹配的错误。
知识速记
-
define和const的主要区别:处理阶段和类型检查不同
-
define不分配内存,const分配内存
- const有类型检查且分配内存
-
define和const没有区别?错!
- 需要类型检查和调试信息时用const
第7章 运算符与表达式
算术运算符
C语言中算术运算符包括加、减、乘、除、取模运算符(+ - * / %)。
取模运算符 % 的特点:
- 求两数相除的余数
- 只适用于整数,浮点数不能使用
- 结果符号与左操作数一致
10 % 3 // = 1(10除以3余1)
-10 % 3 // = -1(符号与左操作数一致)
10.0 % 3 // 编译错误!浮点数不能用%
运算符优先级
运算符优先级决定表达式中各运算符的求值顺序。要改变默认优先级,使用圆括号 ():
int a = 2 + 3 * 4; // = 14(先乘除后加减)
int b = (2 + 3) * 4; // = 20(括号改变优先级)
== 和 = 的区别
初学者容易混淆 == 和 =,因为语法相似但功能完全不同:
if (a == 5) // 比较:a是否等于5?
if (a = 5) // 赋值:把5赋给a,永远为真!
短路求值
逻辑运算符 && 和 || 具有短路求值特性:
&&:如果左边为假,右边不计算||:如果左边为真,右边不计算
if (p != NULL && *p > 0) // 如果p是NULL,不会对*p解引用
if (b != 0 && a/b > 0) // 如果b为0,不会执行除法,避免除零错误
短路求值的主要好处不是简化语法,而是避免除零错误或空指针访问!
复合赋值运算符
+=、-=、*=、/=、%= 等将运算和赋值合并:
a += b; // 等价于 a = a + b
a -= b; // 等价于 a = a - b
a *= b; // 等价于 a = a * b
优点:代码更简洁、减少重复书写变量名的错误。
sizeof运算符
sizeof 运算符的作用是返回数据类型或变量的字节数:
sizeof(int) // 通常返回4
sizeof(char) // 返回1
sizeof(double) // 通常返回8
知识速记
- 算术运算符:+ - * / %
- 取模运算符只适用于整数,浮点数不能用
- 改变优先级:使用圆括号()
- ==是比较,=是赋值(语法相似功能不同)
- 短路求值:避免除零错误或空指针访问
- 短路求值主要好处不是简化语法
- 复合赋值:+=等价于a=a+b
- sizeof返回字节数
第8章 控制流程:条件与循环
if语句
基本语法结构:if(条件) 语句
C语言中没有专门的布尔类型(C99之前),真假用整数表示:
- 0 表示假
- 非0 表示真
if (5) // 真!5是非0
if (0) // 假
if (-1) // 真!-1也是非0
switch语句
相比多个if-else的优势:代码清晰且可能有性能优势(编译器可优化为跳转表)。
switch中忘记写break的后果:会继续执行下一个case(称为"穿透")!
switch (grade) {
case 'A': printf("优秀"); break; // 有break
case 'B': printf("良好"); // 没有break,会继续执行case 'C'
case 'C': printf("中等"); break;
}
// 如果grade='B',输出"良好中等"——两个都打印了!
break vs continue
| 语句 | 作用 |
|---|---|
| break | 跳出当前循环或switch(不是所有循环!) |
| continue | 跳过当前迭代,进入下一次循环判断 |
注意:continue不是跳出循环!continue是跳过本次循环的剩余部分。
三种循环
| 循环 | 特点 | 适用场景 |
|---|---|---|
| for | 初始化+条件+更新三合一 | 已知循环次数 |
| while | 先判断再执行 | 可能一次都不执行 |
| do-while | 先执行再判断 | 至少执行一次 |
for循环的三个部分
for (初始化表达式; 条件判断表达式; 更新表达式) {
// 循环体
}
// 例:for(int i=0; i<10; i++) { ... }
// 初始化:int i=0(执行一次)
// 条件:i<10(每次循环前检查)
// 更新:i++(每次循环后执行)
do-while的适用场景
当至少需要执行一次循环体时,do-while更合适,比如先获取用户输入再判断:
int n;
do {
printf("请输入正整数:");
scanf("%d", &n);
} while (n <= 0); // 至少输入一次
goto语句
goto语句不推荐使用,因为破坏程序结构、降低可读性。
知识速记
- if语法:if(条件)语句结构
- C语言0为假,非0为真
- switch忘记break:继续执行下一个case
- break:跳出当前循环或switch
- continue:跳过当前迭代(不是跳出循环!)
- do-while:至少执行一次
- for三部分:初始化、条件、更新
- goto不推荐:破坏结构、降低可读性
第9章 数组、指针与字符串
数组的核心特点
- 存储相同类型的数据
- 内存中连续存储
- 下标从0开始
- 大小固定(声明后不能改变)
int arr[5]; // 声明包含5个int的数组
int arr[] = {10, 20, 30, 40, 50}; // 初始化
arr[0] // 第1个元素(值为10)
arr[2] // 第3个元素(值为30)
arr[4] // 第5个元素(值为50)
注意:arr[2]是下标为2的元素,即第3个元素(下标从0开始!)
数组越界
访问数组越界可能导致程序崩溃或数据损坏。C语言不自动检查数组边界,原因是为了性能考虑(不是技术限制,不是设计缺陷!)。
int arr[5];
arr[10] = 99; // 越界访问!可能崩溃、可能损坏数据、可能看似正常
指针——C语言的精髓
指针被称为C语言精髓,因为它提供了对内存的直接控制能力。
int a = 10;
int *p = &a; // p存储a的地址
printf("%d", *p); // 输出10(解引用*p就是a)
关键关系:*p 和 a 代表同一个变量!
指针的主要应用场景:
- 动态内存管理(malloc/free)
- 高效数据访问
- 实现引用调用
- 传递大型数据结构
指针初始化
好的编程习惯:声明指针时初始化为NULL
int *p = NULL; // 安全初始化
字符串表示
C语言中字符串是以空字符 \0 结尾的字符数组:
char str[] = "Hello"; // 实际存储:'H','e','l','l','o','\0'
// 5个可见字符,但占6个字节(含'\0')
字符数组 vs 字符指针
| 特性 | char str[] = "Hello" | char *p = "Hello" |
|---|---|---|
| 存储位置 | 栈/数据段 | 字符串常量(只读数据段) |
| 可修改 | 可以修改数组元素 | 不能修改(段错误!) |
| 内存 | 数组自己的空间 | 指针指向常量区 |
char str[] = "Hello";
str[0] = 'h'; // 可以修改
char *p = "Hello";
p[0] = 'h'; // 段错误!字符串常量不可修改
内存管理
C语言内存管理相比高级语言的特点:需要程序员手动管理(malloc分配、free释放)。
知识速记
- 数组:相同类型、连续存储、下标从0、大小固定
- 数组越界:可能崩溃或数据损坏
- C不检查数组边界:为了性能考虑
- 指针是C语言精髓:对内存的直接控制
- *p和a代表同一个变量(p指向a时)
- 字符串以'\0'结尾
- "Hello":5个字符,6个字节
- 字符数组可修改,字符指针指向的常量不可修改
- C语言内存管理:程序员手动管理
- 指针初始化为NULL是好习惯
第10章 结构体、共用体与枚举
结构体(struct)
结构体的主要作用:将不同类型的数据组合成一个整体。
struct Student {
int id; // 学号
char name[51]; // 姓名
float score; // 成绩
};
struct Student stu = {1001, "Zhang"};
printf("ID: %d\n", stu.id); // 使用点运算符访问成员
printf("Name: %s\n", stu.name);
- 访问成员:点运算符
.(结构体变量)或箭头运算符->(结构体指针) - 结构体赋值是值拷贝,修改副本不影响原件
struct Point p1 = {3, 4};
struct Point p2 = p1; // 值拷贝!p2是独立的副本
p2.x = 10; // 修改p2不影响p1
共用体(union)
所有成员共享同一块内存,同时只能使用一个成员:
union Data {
int i;
float f;
char c;
};
// sizeof(union Data) = max(sizeof(int), sizeof(float), sizeof(char))
| 特性 | 结构体 struct | 共用体 union |
|---|---|---|
| 内存 | 成员独立存储 | 成员共享存储 |
| 同时使用 | 可以使用所有成员 | 同时只能用一个 |
| 大小 | 各成员大小之和(含对齐) | 最大成员的大小 |
| 场景 | 存储不同类型的相关数据 | 节省内存 |
枚举(enum)
枚举用于定义一组命名的整数常量,提高代码可读性:
enum Color { RED, GREEN, BLUE };
// RED = 0, GREEN = 1, BLUE = 2(默认从0开始)
如果显式指定某个值,后续值在此基础上递增:
enum Week { MON=1, TUE, WED, THU, FRI };
// MON=1, TUE=2, WED=3, THU=4, FRI=5
枚举 vs #define
| 特性 | enum | #define |
|---|---|---|
| 类型检查 | 有类型检查 | 无 |
| 自动分配值 | 自动递增 | 手动指定 |
| 调试信息 | 可见枚举名 | 只看到数字 |
| 作用域 | 遵循作用域 | 全局污染 |
typedef
使用typedef为结构体类型创建别名,简化声明:
typedef struct { int x; int y; } Point;
Point p1; // 不用写struct Point
知识速记
- 结构体:将不同类型数据组合成一个整体
- 结构体赋值是值拷贝
- 共用体成员共享内存,同时只能用一个
- 枚举默认从0开始,显式赋值后递增
- 枚举优势:有类型检查且自动分配值
- typedef简化结构体声明
- 点运算符访问成员:变量名.成员名
第11章 函数:参数传递与递归
传值调用 vs 传地址调用
这是C语言最重要的概念之一!
传值调用:函数接收参数的副本,修改不影响原变量
void change(int n) {
n = 100; // 修改的是副本
}
int a = 5;
change(a);
printf("%d", a); // 输出5!a没有变!
传地址调用(通过指针实现):函数接收变量的地址,可以修改原变量
void change(int *n) {
*n = 100; // 通过指针修改原变量
}
int a = 5;
change(&a); // 传递a的地址
printf("%d", a); // 输出100!a被修改了!
引用调用的优势
- 可以修改原变量
- 效率更高(不需要复制大量数据)
什么时候用传地址调用?
- 需要在函数内修改传入变量的值
- 传递大型数据结构(如结构体、数组)以避免复制开销
函数返回指针的注意事项
不能返回局部变量的地址!因为局部变量在函数返回后内存就被释放了:
int* bad_func() {
int local = 10;
return &local; // 返回局部变量地址!悬空指针!
}
int* good_func() {
int *p = malloc(sizeof(int)); // 堆上分配
*p = 10;
return p;
}
递归
递归是函数直接或间接调用自身的过程。递归三要素:
- 基准条件(终止条件):防止无限递归
- 递归步骤:每次调用向基准条件靠近
- 深度限制:递归深度不能超过栈空间
int factorial(int n) {
if (n <= 1) return 1; // 基准条件
return n * factorial(n-1); // 递归步骤:向基准条件靠近
}
// factorial(4) = 4 * factorial(3) = 4 * 3 * 2 * 1 = 24
return语句
return语句将计算结果返回给调用者:
int add(int a, int b) {
return a + b; // 返回a+b的值
}
void函数不需要return值(也可以用 return; 提前返回)。
main函数
C程序从main函数开始执行,main是程序的入口点。
知识速记
- C语言函数参数默认按值传递
- 传值调用:修改不影响原变量
- 传地址调用:通过指针修改原变量
- 引用调用优势:可以修改原变量且效率更高
- 返回指针:不能返回局部变量地址
- 递归三要素:基准条件、向基准靠近、深度限制
- factorial(4) = 24
- 程序从main函数开始执行
- void表示不返回值
第12章 算法:排序与迭代
三种基础排序对比
| 排序 | 基本思想 | 时间复杂度 | 稳定性 | 特点 |
|---|---|---|---|---|
| 冒泡排序 | 相邻元素比较交换,大元素逐步移到末尾 | O(n^2) | 稳定 | 适用于数据量小或基本有序 |
| 选择排序 | 每次选最小元素放到已排序部分末尾 | O(n^2) | 不稳定 | 交换次数少 |
| 插入排序 | 将元素插入已排序部分的正确位置 | O(n^2) | 稳定 | 数据基本有序时效率高 |
为什么选择排序不稳定?
选择排序可能改变相等元素的相对位置。例如:[5a, 5b, 3] - 选最小的3和5a交换 - [3, 5b, 5a],两个5的相对顺序变了。
归并排序
- 基本思想:分治法——将数组分割后合并
- 时间复杂度:O(n log n) 对数线性复杂度
- 主要缺点:需要额外的存储空间
- 相比冒泡排序:时间复杂度更优,但空间复杂度更高
递归 vs 迭代
| 特性 | 递归 | 迭代 |
|---|---|---|
| 方式 | 函数调用自身 | 循环重复执行 |
| 优点 | 代码简洁,适合递归结构问题 | 执行效率高,不依赖栈空间 |
| 缺点 | 可能栈溢出,函数调用开销大 | 复杂问题代码不够直观 |
| 场景 | 树遍历、阶乘、斐波那契 | 简单重复操作 |
斐波那契数列
每个数是前两个数之和:1, 1, 2, 3, 5, 8, 13, 21, ...
int a = 1, b = 1;
printf("%d %d ", a, b);
for(int i = 3; i <= 8; i++) {
int next = a + b;
printf("%d ", next);
a = b;
b = next;
}
// 输出:1 1 2 3 5 8 13 21
计算斐波那契数列时C语言版本出现负数不是因为算法错误,而是因为整数溢出!fib(47)以后int就放不下了。
常见编程模式
累加求和:
int sum = 0;
for(int i = 1; i <= n; i++) {
sum = sum + i;
}
// 1+2+3+4+5 = 15
找最大值:
int max = arr[0];
for(int i = 1; i < n; i++) {
if(arr[i] > max) max = arr[i];
}
// 注意:i从1开始,因为max初始化为arr[0]
筛选偶数:
for(int i = 0; i < n; i++) {
if(arr[i] % 2 == 0) {
printf("%d ", arr[i]);
}
}
冒泡排序核心交换:
if(arr[j] > arr[j+1]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp; // 三变量交换
}
知识速记
- 冒泡排序:O(n^2),稳定
- 选择排序:O(n^2),不稳定,交换次数少
- 插入排序:O(n^2),稳定,基本有序时效率高
- 归并排序:O(n log n),需要额外空间
- 选择排序不稳定:可能改变相等元素相对位置
- 斐波那契出现负数:整数溢出,不是算法错误
- 累加和模式、找最大值模式
- 三变量交换
第13章 文件操作
文件操作三步曲
- 打开文件:
fopen() - 读写文件:
fprintf()/fscanf()、fwrite()/fread() - 关闭文件:
fclose()
fopen函数
FILE *fp = fopen("data.txt", "r");
| 模式 | 含义 | 注意 |
|---|---|---|
| "r" | 只读模式 | 文件必须存在 |
| "w" | 写入模式 | 会覆盖原有内容! |
| "a" | 追加模式 | 在文件末尾添加内容 |
检查文件是否成功打开
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
printf("文件打开失败!\n");
return -1;
}
为什么必须调用fclose?
- 刷新缓冲区:将缓冲区中的数据真正写入磁盘
- 释放系统资源:文件描述符是有限资源
忘记关闭文件的后果:
- 数据未完全写入而丢失
- 系统文件描述符耗尽
- 内存泄露
文件路径
C语言中使用字符串表示文件路径:
- 相对路径:相对于当前目录,如
"data.txt" - 绝对路径:从根目录开始,如
"/home/user/data.txt"
注意:不同操作系统使用不同的路径分隔符:Windows用 \,Linux/Mac用 /
写入数据
向文件写入数据使用 fprintf 或 fwrite 函数:
FILE *fp = fopen("out.txt", "w");
fprintf(fp, "Hello World\n"); // 格式化写入
fclose(fp);
知识速记
- 文件操作:fopen - 读写 - fclose
- 检查打开成功:文件指针是否为NULL
- fclose作用:释放资源+确保数据写入
- "r"只读、"w"写入(覆盖)、"a"追加
- 路径用字符串表示
- 跨平台注意路径分隔符
- 写入用fprintf或fwrite
第14章 Python与C混合开发
Python与C的区别
| 特性 | C语言 | Python |
|---|---|---|
| 语法 | 大括号 {} |
缩进 |
| 内存管理 | 手动管理 | 自动管理(GC) |
| 开发效率 | 较低 | 较高(语法简洁+丰富库) |
| 执行效率 | 高(编译后机器码) | 较低(解释执行) |
混合开发的分工
| 任务 | 适合谁 | 原因 |
|---|---|---|
| 业务逻辑、数据处理、快速原型 | Python | 开发效率高 |
| 性能敏感的计算密集型任务 | C | 执行效率高 |
编译动态链接库
将C语言函数编译成.so文件的目的是供其他语言调用。
gcc -shared -fPIC -o mylib.so mylib.c
| 选项 | 含义 |
|---|---|
| -shared | 生成共享库(动态链接库)而非可执行文件 |
| -fPIC | 生成位置无关代码(Position Independent Code),使库可加载到内存任意位置 |
注意:Linux下动态链接库扩展名为 .so,Windows下为 .dll。
Python调用C库
使用ctypes模块:
import ctypes
lib = ctypes.CDLL("./mylib.so") # 加载动态库
lib.add.restype = ctypes.c_int # 设置返回值类型
result = lib.add(3, 5) # 调用C函数
也可以使用 cffi 或编写 C 扩展模块。
什么情况下Python可能比C更快?
当使用高度优化的科学计算库时(如NumPy底层是C实现的),Python可能比手写的C代码更快。
知识速记
- C用大括号,Python用缩进
- C手动管理内存,Python自动管理
- C编译为.so供其他语言调用
- -shared:生成共享库
- -fPIC:位置无关代码
- .so(Linux)、.dll(Windows)
- Python用ctypes加载C库
- lib.add.restype = ctypes.c_int 设置返回类型
- 用优化库时Python可能比C更快
第15章 字符编码与加密
ASCII编码
char本质上是8位整数,可以直接当整数用:
char ch = 'A';
printf("ASCII: %d", ch); // 输出65('A'的ASCII值)
char c = 65; // 等价于 char c = 'A'
数字字符转整数
char ch = '5';
int num = ch - '0'; // num = 5
// '0'=48, '5'=53, 53-48=5
大小写转换
利用ASCII码中大小写字母的固定偏移差:
// 小写转大写
if(ch >= 'a' && ch <= 'z') {
ch = ch - 'a' + 'A';
}
// 大写转小写
if(ch >= 'A' && ch <= 'Z') {
ch = ch - 'A' + 'a';
}
原理:'a'-'z'是97-122,'A'-'Z'是65-90,差值恒为32。
偏移加密(凯撒密码)
偏移加密的基本原理:将字符按固定偏移量移动。
char encrypted = ch + 3; // 每个字符向后移3位
凯撒密码是偏移加密的一种。但偏移加密安全性较低,容易破解(只有26种可能,暴力枚举即可)。
Base64编码
Base64编码的用途是将二进制数据转换为文本格式,不是加密,不是压缩,不是校验!
知识速记
- char本质是小整数,可用%d输出ASCII值
- char c = 65 等价于 c = 'A'
- 数字字符转整数:ch - '0'
- 大小写转换:ch - 'a' + 'A' 或 ch - 'A' + 'a'
- 偏移加密:按固定偏移量移动字符
- 凯撒密码是偏移加密的一种
- 偏移加密安全性较低
- Base64:二进制转文本格式(不是加密)
第16章 安全编程与陷阱
整数除以整数
C语言中两个整数相除,结果仍然是整数,小数部分被截断:
int a = 5, b = 2;
float result = a / b; // result = 2.0,不是2.5!
// 因为5/2先按整数除法得2,再转为float
// 要得到2.5:
float result = 5.0 / 2; // 5.0是浮点数
float result = (float)a / b; // 强制转换
整数溢出
整数溢出可能导致缓冲区溢出和权限提升等安全问题,不只是计算错误!
int big = 2147483647; // int最大值
big = big + 1; // 溢出!可能变成负数
数组越界与缓冲区溢出
- C语言不自动检查数组边界——原因是为了性能考虑
- 字符串处理容易缓冲区溢出——因为没有自动边界检查机制
- 缓冲区溢出攻击的目标是执行恶意代码或获取权限
float转int
浮点数强制转为整数时,小数部分被截断(不是四舍五入):
float f = 3.14;
int i = (int)f; // i = 3(截断,不是四舍五入)
float f = 3.99;
int i = (int)f; // i = 3(仍然截断!不是4)
sizeof运算符
printf("int size: %d bytes", sizeof(int)); // 通常输出4
sizeof返回数据类型或变量占用的字节数。
知识速记
- 整数除整数得整数(5/2=2,不是2.5)
- 要得小数:用5.0/2或(float)5/2
- 整数溢出可导致缓冲区溢出和权限提升
- C不检查数组边界:为了性能
- 缓冲区溢出攻击目标:执行恶意代码或获取权限
- float转int:截断,不是四舍五入
- sizeof返回字节数
终极自查清单
容易混淆的知识点
| 知识点 | 正确理解 | 常见误解 |
|---|---|---|
| C语言能直接访问内存地址 | 是的 | 以为高级语言不能 |
| 计算斐波那契出现负数 | 是整数溢出 | 以为是算法错误 |
| 函数参数传递方式 | 按值传递 | 以为按引用 |
| #define与const占内存 | #define不占,const占 | 以为都占内存 |
| const有类型检查吗 | 有类型检查 | 以为没有 |
| printf在哪个头文件 | stdio.h | 以为是string.h |
| 良好注释应说明什么 | 目的和逻辑 | 以为是语法结构 |
| 头文件主要好处 | 提高组织性和重用性 | 以为是减少编译时间 |
| continue的作用 | 跳过当前迭代 | 以为跳出循环 |
| 短路求值主要好处 | 避免除零/空指针 | 以为是简化语法 |
| 上世纪C开发环境 | 命令行环境 | 以为是图形界面 |
必背代码模板
1. 结构体定义与使用
struct Student {
int id;
char name[51];
};
struct Student stu = {1001, "Zhang"};
printf("ID: %d\n", stu.id);
2. 指针传参交换两个值
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int x = 5, y = 10;
swap(&x, &y); // x变为10,y变为5
3. 数组遍历求和
int sum = 0;
for(int i = 0; i < n; i++) {
sum += arr[i];
}
4. 文件操作
FILE *fp = fopen("data.txt", "r");
if(fp == NULL) { return -1; }
fprintf(fp, "Hello");
fclose(fp);
5. 递归求阶乘
int factorial(int n) {
if(n <= 1) return 1;
return n * factorial(n-1);
}
6. 动态链接库编译与Python调用
gcc -shared -fPIC -o mylib.so mylib.c
import ctypes
lib = ctypes.CDLL("./mylib.so")
lib.add.restype = ctypes.c_int
C语言的核心是指针和内存管理,把这两块搞透,就赢了大半!
复习建议:先过一遍自查清单,把容易混淆的知识点搞清楚;再背代码模板;多动手写代码验证理解。
理解为主,记忆为辅,知其然更要知其所以然!
预览时标签不可点