C程序设计

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) #arg
STR(Hello)"Hello"
STR(8)"8"
## 连接前后内容(标记粘贴) #define XNAME(x) x##n
XNAME(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 默认对齐规则

  1. 第一个成员的首地址为0
  2. 每个成员的首地址是自身大小的整数倍
  3. 结构体整体对齐:结构体的大小必须是最大成员大小的整数倍

示例:

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。

对齐规则:

  1. 数据成员对齐规则:结构体(或联合)的数据成员,第一个数据成员放在 offset 为 0 的地方,以后每个数据成员的对齐按照 #pragma pack 指定的数值和该数据成员自身长度中,较小的那个进行。

  2. 整体对齐规则:在数据成员完成各自对齐之后,结构体(或联合)本身也要进行对齐,对齐将按照 #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 strcpystrlen

重要概念:

  • 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 前两个参数通过 ecxedx 寄存器传递,剩下的从右向左 被调用者清理 性能较好
__thiscall 自右向左压栈 被调用者清理 this 指针存放于 ecx 寄存器
类成员函数默认调用方式
(GCC用堆栈保存this,VC用ecx)

线程函数调用约定:
_beginthread 需要 __cdecl 的线程函数地址
_beginthreadexCreateThread 需要 __stdcall 的线程函数地址

5.2 可变参数列表

特点:
– 可变参数默认使用 __cdecl 调用方式
– 使用 va_listva_startva_argva_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 获取系统信息

32位Windows进程虚拟内存空间

6.3 64位 Windows 进程虚拟内存空间

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

64位Windows进程虚拟内存空间1

64位Windows进程虚拟内存空间2

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 进行链接:

  1. 相似段合并:通过 LC_SYMTAB 符号表加载命令,生成全局符号表

相似段合并

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

重定位表1

重定位表2

重定位表3

  1. 符号解析:通过全局符号表查找模块外部符号对应的地址,完成符号的解析和重定位

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 外部变量访问流程

动态链接时,外部变量的访问通过以下步骤:

  1. LC_SEGMENT_64 加载 __DATA 段,从 got 节头获取间接符号表索引

GOT节头

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

间接索引表

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

符号表

  1. libPrint.dylib 获取 global_var 的地址,并更新 __DATAgot 节里面的 global_var 地址

6.6.3 外部函数访问流程

调用 dyld_stub_binder 执行以下流程:

  1. LC_SEGMENT_64 加载 __DATA 段,从 __la_symbol_ptr 节头获取间接符号表索引

LA符号指针

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

间接索引表2

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

符号表2

  1. 更新 __DATA_la_symbol_ptr 里面的符号地址

更新符号地址


总结

本文档涵盖了C语言程序设计的核心概念,包括:

  1. 数的表示:补码原理和计算
  2. 数组和指针:C语言数组的本质
  3. 预处理器:宏定义、内存对齐
  4. 基础特性:运算符、关键字、字符串处理
  5. 高级特性:函数调用约定、链接方式、内存管理
  6. 可执行文件格式:内存布局、链接流程

这些知识对于深入理解C语言和系统编程至关重要。

留下评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注

Index