C程序设计
目录
第1章 数的表示与补码
1.1 补码表示(以4位计算机为例)
在计算机中,负数采用补码表示。补码的计算规则为:补码 = 反码 + 1
示例:计算 -3 的补码表示
- 3 的二进制表示为:
0011 - -3 的反码为:
1100(按位取反) - -3 的补码为:
1101(反码 + 1)
补码运算示例:
5 - 3 = 5 + (-3) = 5 + (16 - 3) = 5 + 13
在4位系统中:
– 5 的二进制:0101
– -3 的补码(即13的二进制):1101
– 结果:0101 + 1101 = 10010(溢出后为 0010,即2)
1.2 补码原理
为什么负数要用补码表示?
使用补码可以将减法运算转换为加法运算,简化CPU的硬件设计。补码表示法使得正数和负数的加法运算可以使用同一套加法器电路。
第2章 指针和数组
2.1 C语言数组的本质
重要概念:C语言只有一维数组
多维数组实际上是数组的数组。例如:
int calendar[12][31]; // 数组大小为12,每个元素是一维数组 int[31]
这个声明表示:
– calendar 是一个包含12个元素的数组
– 每个元素是一个包含31个整数的数组
第3章 预处理器
3.1 预处理器指令
| 指令/宏 | 作用 | 示例 |
|---|---|---|
# |
参数添加引号(字符串化) | #define STR(arg) #argSTR(Hello) → "Hello"STR(8) → "8" |
## |
连接前后内容(标记粘贴) | #define XNAME(x) x##nXNAME(8) → 8n |
#error |
编译错误,停止编译 | #error "Error message" |
#pragma comment(lib, "libname") |
静态链接库 | #pragma comment(lib, "user32.lib") |
#pragma pack(n)#pragma pack() |
指定内存字节对齐 | #pragma pack(1) |
__TIME__ |
编译时间 | 字符串字面量 |
__DATE__ |
编译日期 | 字符串字面量 |
__FILE__ |
当前文件名 | 字符串字面量 |
__LINE__ |
当前行号 | 整数常量 |
__func__ |
当前函数名(C11标准) | 字符串字面量 |
__FUNCTION__ |
当前函数名(编译器扩展) | 字符串字面量 |
3.2 内存对齐规则
3.2.1 默认对齐规则
- 第一个成员的首地址为0
- 每个成员的首地址是自身大小的整数倍
- 结构体整体对齐:结构体的大小必须是最大成员大小的整数倍
示例:
struct S3 {
char a[18]; // 首地址为0,由于b是8字节对齐,所以a占用大小为24
double b; // 8字节,首地址必须是8的倍数
char c; // 1字节
int d; // 4字节,c和d总共8个字节,刚好对齐
short e; // 2字节,e占用4个字节(对齐到4字节边界)
}; // 整个结构体需要8字节对齐,总大小为48字节
内存布局分析:
– a[18]: 0-17(18字节),但需要对齐到8字节边界,实际占用0-23(24字节)
– b: 24-31(8字节)
– c: 32(1字节)
– d: 36-39(4字节,c后填充3字节)
– e: 40-41(2字节),后填充2字节到44
– 结构体总大小:48字节(8的倍数)
3.2.2 #pragma pack(n) 指令
每个平台上的编译器都有自己的默认”对齐系数”(对齐模数)。程序员可以通过 #pragma pack(n) 来改变这一系数,其中 n 可以是 1、2、4、8、16。
对齐规则:
-
数据成员对齐规则:结构体(或联合)的数据成员,第一个数据成员放在 offset 为 0 的地方,以后每个数据成员的对齐按照
#pragma pack指定的数值和该数据成员自身长度中,较小的那个进行。 -
整体对齐规则:在数据成员完成各自对齐之后,结构体(或联合)本身也要进行对齐,对齐将按照
#pragma pack指定的数值和结构体(或联合)最大数据成员长度中,较小的那个进行。
示例:
#pragma pack(1) // 设置为1字节对齐
struct S {
char a; // 1字节
int b; // 4字节,但按1字节对齐,所以紧跟在a后面
double c; // 8字节,但按1字节对齐
}; // 总大小:1 + 4 + 8 = 13字节
#pragma pack() // 恢复默认对齐
第4章 基础特性
4.1 自增自减运算符
4.1.1 i++ 和 ++i 的区别
| 特性 | i++(后缀) |
++i(前缀) |
|---|---|---|
| 返回值 | 返回原来的值 | 返回加1后的值 |
| 左值性 | 不能作为左值 | 可以作为左值 |
| 效率 | 需要临时变量 | 更高效 |
C++ 中的实现原理:
// 前缀形式:返回引用,可以作为左值
int& int::operator++() {
*this += 1; // 增加
return *this; // 返回引用
}
// 后缀形式:返回常量值,不能作为左值
const int int::operator++(int) {
int oldValue = *this; // 保存原值
++(*this); // 增加
return oldValue; // 返回原值
}
4.2 字符串函数
4.2.1 strcpy 和 strlen
重要概念:
- C语言字符串以
'\0'(空字符)结尾 strcpy拷贝时包括'\0'strlen计算长度时不包括'\0'
示例:
char str[] = "Hello";
printf("%zu\n", strlen(str)); // 输出:5(不包括'\0')
char dest[10];
strcpy(dest, str); // 拷贝 "Hello\0",共6个字符
4.3 static 关键字
4.3.1 static 变量
static全局变量:只在当前文件内有效,具有内部链接性static局部变量:在函数内有效,但生命周期延长到程序结束,只初始化一次
示例:
static int g_a = 10; // g_a 只在当前文件有效
void func() {
static int count = 0; // 只初始化一次,函数调用间保持值
count++;
printf("%d\n", count);
}
4.3.2 static 函数
static函数:只在当前文件内有效,具有内部链接性
示例:
static int add(int a, int b) { // 只能在当前文件使用
return a + b;
}
4.3.3 static 类成员(C++)
static类成员变量:在类中只是声明,在类外定义时分配存储空间static类成员函数:- 没有
this指针 - 不能访问非静态成员变量
- 可以通过类名直接调用
4.4 运算符优先级
记忆口诀:算移关位逻赋
| 类型 | 运算符 | 结合性 | 优先级 |
|---|---|---|---|
| 前述运算符 | () [] -> . |
自左向右 | 0(最高) |
| 单目运算符 | ! ~ ++ -- - * & sizeof |
自右向左 | 1 |
| 算术运算符 | * / % |
自左向右 | 2 |
| 算术运算符 | + - |
自左向右 | 3 |
| 移位运算符 | << >> |
自左向右 | 4 |
| 关系运算符 | < <= > >= |
自左向右 | 5 |
| 关系运算符 | == != |
自左向右 | 6 |
| 位运算符 | & |
自左向右 | 7 |
| 位运算符 | ^ |
自左向右 | 8 |
| 位运算符 | \| |
自左向右 | 9 |
| 逻辑运算符 | && |
自左向右 | 10 |
| 逻辑运算符 | \|\| |
自左向右 | 11 |
| 条件运算符 | ?: |
自右向左 | 12 |
| 赋值运算符 | = += -= 等 |
自右向左 | 13 |
| 逗号运算符 | , |
自左向右 | 14(最低) |
4.4.1 词法解析(贪心法)
C语言编译器使用”贪心法”进行词法解析:尽可能多地读取字符组成一个符号。
示例:
int a = 5;
int b = 10;
int c = a+++b; // 解析为 a++ + b,结果:a=6, b=10, c=15
4.5 volatile 关键字
作用: 强制编译器每次从内存获取变量的值,防止编译器优化。
使用场景:
– 硬件寄存器
– 多线程共享变量
– 中断服务程序中的变量
示例:
volatile int flag = 0; // 每次访问都从内存读取,不进行优化
4.6 const 关键字
const 用于声明常量,但位置不同,含义不同:
const char* p; // p指向的内容不可修改(指向常量的指针)
char const* p; // 同上,等价写法
char* const p; // p指针本身不可修改(常量指针)
const char* const p; // 指针和内容都不可修改
记忆方法:
– const 在 * 左边:指向的内容是常量
– const 在 * 右边:指针本身是常量
4.7 浮点数比较
由于浮点数在计算机中的表示存在精度问题,不能直接使用 == 进行比较。
浮点数比较宏定义:
#define eps 1e-9 // 定义精度
// 大于等于 (>=)
#define MoreEqu(a, b) (((a) - (b)) > (-eps))
// 大于 (>)
#define More(a, b) (((a) - (b)) > (eps))
// 小于等于 (<=)
#define LessEqu(a, b) (((a) - (b)) < (eps))
// 小于 (<)
#define Less(a, b) (((a) - (b)) < (-eps))
// 等于 (==)
#define Equ(a, b) ((fabs((a) - (b))) < (eps))
使用示例:
double x = 0.1 + 0.2;
if (Equ(x, 0.3)) {
printf("Equal\n");
}
第5章 高级特性
5.1 函数调用约定
__cdecl、__fastcall 与 __stdcall 都是调用约定(Calling convention),它决定以下内容:
- 函数参数的压栈顺序
- 调用者还是被调用者负责把参数弹出栈
- 产生函数修饰名的方法
| 调用方式 | 参数入栈顺序 | 平衡堆栈 | 缺点 | 备注 |
|---|---|---|---|---|
__cdecl |
自右向左入栈 | 调用者清理 | 生成的可执行文件较大 | C和C++默认调用方式 |
__stdcall |
自右向左入栈 | 被调用者清理 | – | Windows API默认调用方式 一般WIN32的函数都是 __stdcall |
__fastcall |
前两个参数通过 ecx 和 edx 寄存器传递,剩下的从右向左 |
被调用者清理 | – | 性能较好 |
__thiscall |
自右向左压栈 | 被调用者清理 | – | this 指针存放于 ecx 寄存器类成员函数默认调用方式 (GCC用堆栈保存this,VC用ecx) |
线程函数调用约定:
– _beginthread 需要 __cdecl 的线程函数地址
– _beginthreadex 和 CreateThread 需要 __stdcall 的线程函数地址
5.2 可变参数列表
特点:
– 可变参数默认使用 __cdecl 调用方式
– 使用 va_list、va_start、va_arg、va_end 宏处理
示例:
#include <stdarg.h>
int sum(int count, ...) {
va_list ap;
va_start(ap, count);
int total = 0;
for (int i = 0; i < count; i++) {
total += va_arg(ap, int);
}
va_end(ap);
return total;
}
5.3 静态链接和动态链接
| 特性 | 静态链接 | 动态链接 |
|---|---|---|
| 本质 | .o 文件的集合,编译时链接 |
运行时链接,装载时进行地址重定位 |
| 优点 | 1. 依赖小 2. 运行速度快 |
1. 空间小 2. 更新方便 |
| 缺点 | 1. 空间浪费 2. 更新困难 |
1. 每次执行都需要链接 2. 性能损失约5% |
5.3.1 VC C/C++ 标准库
编译选项:
– /MT:multithread, static version(多线程静态版本)
– /MD:multithread- and DLL-specific version(多线程动态版本)
| 库类型 | 名称 | 类型 | 链接类型 |
|---|---|---|---|
| C语言运行库 | LIBCMTD.lib |
Debug | 静态链接 |
| C语言运行库 | LIBCMT.lib |
Release | 静态链接 |
| C语言运行库 | MSVCRTD.lib |
Debug | 动态链接 |
| C语言运行库 | MSVCRT.lib |
Release | 动态链接 |
| C++语言运行库 | LIBCPMTD.lib |
Debug | 静态链接 |
| C++语言运行库 | LIBCPMT.lib |
Release | 静态链接 |
| C++语言运行库 | MSVCPRTD.lib |
Debug | 动态链接 |
| C++语言运行库 | MSVCPRT.lib |
Release | 动态链接 |
5.4 32位程序和64位程序
32位和64位程序的区别主要体现在:
– 指针大小:32位为4字节,64位为8字节
– 地址空间:32位为4GB,64位更大
– 寄存器:64位有更多通用寄存器
– 调用约定:可能不同
5.5 大端和小端
字节序(Endianness):
- 小端(Little Endian):低字节存储在低地址(低低高高)
- 大端(Big Endian):高字节存储在低地址(低高低高)
注意: 基本所有现代操作系统都是小端序。

验证方法:
int x = 0x12345678;
char* p = (char*)&x;
// 小端:p[0] = 0x78, p[1] = 0x56, p[2] = 0x34, p[3] = 0x12
// 大端:p[0] = 0x12, p[1] = 0x34, p[2] = 0x56, p[3] = 0x78
5.6 堆和栈的区别
| 特性 | 栈(Stack) | 堆(Heap) |
|---|---|---|
| 申请方式 | 系统自动分配和回收 | 手工申请和释放(malloc/free) |
| 扩展方式 | 高地址 → 低地址 | 低地址 → 高地址 |
| 大小限制 | 通常约2MB(可配置) | 虚拟内存大小 |
| 分配和读写效率 | 高 | 比栈低 |
| 内存碎片 | 无 | 可能有 |
| 生命周期 | 自动管理 | 手动管理 |
第6章 可执行文件格式
6.1 基本内存布局
程序在内存中的典型布局:

典型布局(从高地址到低地址):
– 栈(Stack):局部变量、函数参数
– 堆(Heap):动态分配的内存
– BSS段:未初始化的全局变量和静态变量
– 数据段(Data):已初始化的全局变量和静态变量
– 代码段(Text):程序代码
6.2 32位 Windows 进程虚拟内存空间
32位进程的虚拟地址空间大小为 4GB(2^32),通常分为:
– 用户空间:0x00000000 – 0x7FFFFFFF(2GB)
– 内核空间:0x80000000 – 0xFFFFFFFF(2GB)
验证方法:
#include <windows.h>
SYSTEM_INFO sys_info;
GetSystemInfo(&sys_info);
// 可以通过 sys_info 获取系统信息

6.3 64位 Windows 进程虚拟内存空间
特点:
– X64 CPU 仅支持64位虚拟地址中的48位
– 48位地址空间理论上支持 256TB 的虚拟内存空间
– Windows 做了进一步限制,只使用其中的 16TB


6.4 PE 文件格式
PE(Portable Executable)文件格式:
– PE 文件源于 Unix 通用对象文件格式 COFF 发展而来
– IAT(Import Address Table):通过函数名获取函数地址,并填入 IAT 中
– 包含多个节(Section):.text、.data、.rdata 等
6.5 静态链接流程
6.5.1 链接前置条件说明
示例:两个模块的链接
a.c 文件(引用外部符号):
// a.c 文件,也就是a模块
extern int global_var;
void func(int a);
int main() {
int a = 100;
func(a + global_var);
return 0;
}
b.c 文件(提供符号):
// b.c 文件
int global_var = 1;
void func(int a) {
global_var = a;
}
编译和链接命令:
# 编译和汇编:生成 a.o b.o
xcrun -sdk iphoneos clang -c a.c b.c -target arm64-apple-ios12.2
# 链接:a.o 和 b.o 链接成可执行文件 ab
xcrun -sdk iphoneos clang a.o b.o -o ab -target arm64-apple-ios12.2
6.5.2 链接流程
使用静态链接器 ld 进行链接:
- 相似段合并:通过
LC_SYMTAB符号表加载命令,生成全局符号表

- 重定位:
LC_SEGMENT_64加载__TEXT段,获取__TEXT节头重定位表的偏移,从重定位表找出模块引用的外部符号名称、地址和大小



- 符号解析:通过全局符号表查找模块外部符号对应的地址,完成符号的解析和重定位
6.6 动态链接流程
特点:
– 使用动态链接器 dyld
– LC_LOAD_DYLINKER:动态链接器加载命令
– LC_LOAD_DYLIB:动态库加载命令
6.6.1 链接前置条件
print.c 文件(提供符号):
// print.c 文件
#include <stdio.h>
char *global_var = "global_var";
void print(char *str) {
printf("wkk:%s\n", str);
}
main.c 文件(引用外部符号):
// main.c 文件
void print(char *str);
extern char *global_var;
int main() {
print(global_var);
return 0;
}
编译和链接命令:
# 1. 编译 main.c
xcrun -sdk iphoneos clang -c main.c -o main.o -target arm64-apple-ios12.2
# 2. 编译 print.c 成动态库 libPrint.dylib
xcrun -sdk iphoneos clang -fPIC -shared print.c -o libPrint.dylib -target arm64-apple-ios12.2
# 3. 链接 main.o 和 libPrint.dylib 成可执行文件 main
xcrun -sdk iphoneos clang main.o -o main -L . -l Print -target arm64-apple-ios12.2
6.6.2 外部变量访问流程
动态链接时,外部变量的访问通过以下步骤:
LC_SEGMENT_64加载__DATA段,从got节头获取间接符号表索引

LC_DYSYMTAB加载间接索引表,从间接索引表找到global_var外部符号

LC_SYMTAB加载符号表,并找出global_var对应的模块

- 从
libPrint.dylib获取global_var的地址,并更新__DATA、got节里面的global_var地址
6.6.3 外部函数访问流程
调用 dyld_stub_binder 执行以下流程:
LC_SEGMENT_64加载__DATA段,从__la_symbol_ptr节头获取间接符号表索引

LC_DYSYMTAB加载间接索引表,从间接索引表找到_print外部符号

LC_SYMTAB加载符号表,并找出_print对应的模块

- 更新
__DATA、_la_symbol_ptr里面的符号地址

总结
本文档涵盖了C语言程序设计的核心概念,包括:
- 数的表示:补码原理和计算
- 数组和指针:C语言数组的本质
- 预处理器:宏定义、内存对齐
- 基础特性:运算符、关键字、字符串处理
- 高级特性:函数调用约定、链接方式、内存管理
- 可执行文件格式:内存布局、链接流程
这些知识对于深入理解C语言和系统编程至关重要。
