C语言复习全攻略-一文吃透所有知识点

理解比记忆更重要,代码都直接运行一遍,想明白整个过程,肯定没问题。希望能对期末复习的小朋友们有所帮助。


第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"  // 所有声明都在头文件中

头文件的核心价值:

  1. 组织性:把相关声明集中管理
  2. 重用性:多个源文件可以共享同一套声明
  3. 一致性:修改声明只需改头文件,不用逐个文件改

注意:使用头文件的主要好处不是减少编译时间(这是一个常见的误解!)

头文件保护机制

复杂项目中避免头文件重复包含的方法是使用头文件保护机制:

#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

类型转换

  1. 隐式类型转换:编译器自动进行的类型转换

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语言关键字,因为关键字有特殊语法含义(如 intreturnwhile 等)

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    // = 110除以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章 数组、指针与字符串

数组的核心特点

  1. 存储相同类型的数据
  2. 内存中连续存储
  3. 下标从0开始
  4. 大小固定(声明后不能改变)
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被修改了!

引用调用的优势

  1. 可以修改原变量
  2. 效率更高(不需要复制大量数据)

什么时候用传地址调用?

  1. 需要在函数内修改传入变量的值
  2. 传递大型数据结构(如结构体、数组)以避免复制开销

函数返回指针的注意事项

不能返回局部变量的地址!因为局部变量在函数返回后内存就被释放了:

int* bad_func() {  
    int local = 10;  
    return &local;  // 返回局部变量地址!悬空指针!  
}  

int* good_func() {  
    int *p = malloc(sizeof(int));  // 堆上分配  
    *p = 10;  
    return p;  
}

递归

递归是函数直接或间接调用自身的过程。递归三要素:

  1. 基准条件(终止条件):防止无限递归
  2. 递归步骤:每次调用向基准条件靠近
  3. 深度限制:递归深度不能超过栈空间
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章 文件操作

文件操作三步曲

  1. 打开文件:fopen()
  2. 读写文件:fprintf()/fscanf()fwrite()/fread()
  3. 关闭文件: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?

  1. 刷新缓冲区:将缓冲区中的数据真正写入磁盘
  2. 释放系统资源:文件描述符是有限资源

忘记关闭文件的后果:

  • 数据未完全写入而丢失
  • 系统文件描述符耗尽
  • 内存泄露

文件路径

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语言的核心是指针和内存管理,把这两块搞透,就赢了大半!

复习建议:先过一遍自查清单,把容易混淆的知识点搞清楚;再背代码模板;多动手写代码验证理解。

理解为主,记忆为辅,知其然更要知其所以然!

预览时标签不可点

分类: C