C/C++核心知识点详解教程
C/C++核心知识点详解教程
本教程从零开始,系统讲解C/C++语言中的关键概念,适合初学者循序渐进地学习。
目录
- 宏定义与预处理
- volatile关键字详解
- static关键字详解
- const关键字详解
- 指针与const的组合
- new/delete与malloc/free对比
- sizeof运算符
- 结构体与联合体
- 内存布局:栈与堆
- extern "C"的作用
1. 宏定义与预处理
1.1 什么是宏定义?
宏定义是C/C++预处理器的功能,在编译前进行文本替换。
基本语法:
#define 宏名 替换文本
1.2 简单宏示例
#define PI 3.14159
#define MAX_SIZE 100int main() {double radius = 5.0;double area = PI * radius * radius; // 编译时替换为 3.14159return 0;
}
1.3 带参数的宏
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))int result = SQUARE(5); // 展开为 ((5) * (5))
int max_val = MAX(10, 20); // 展开为 ((10) > (20) ? (10) : (20))
注意事项:
- 参数要加括号,避免运算优先级问题
- 整个表达式也要加括号
1.4 #和##操作符
# (字符串化操作符)
#define TO_STRING(x) #xTO_STRING(hello) // 展开为 "hello"
## (连接操作符)
#define CONCAT(a, b) a##bint xy = 10;
int value = CONCAT(x, y); // 展开为 xy,即访问变量xy
1.5 宏的陷阱
#define SQUARE(x) x * xint a = SQUARE(2 + 3); // 展开为 2 + 3 * 2 + 3 = 11 (错误!)
// 正确写法: #define SQUARE(x) ((x) * (x))
2. volatile关键字详解
2.1 什么是volatile?
volatile
告诉编译器:“这个变量可能会意外改变,不要对它进行优化”。
简单理解: 每次使用这个变量时,都要从内存中重新读取,而不是使用缓存值。
2.2 为什么需要volatile?
编译器为了提高效率,可能会把变量缓存到寄存器中:
int flag = 0;// 编译器可能优化为无限循环
while (flag == 0) {// 编译器认为flag不会改变,只读取一次
}
如果其他线程或硬件改变了flag
,主循环可能永远检测不到!
2.3 使用场景
场景1: 硬件寄存器
volatile unsigned int *reg = (unsigned int *)0x40000000;*reg = 0x01; // 写入硬件寄存器
*reg = 0x02; // 再次写入
*reg = 0x04;
*reg = 0x08; // 不加volatile,编译器可能只保留最后一次写入
// 加了volatile,四次写入都会执行
场景2: 中断服务程序
volatile int interrupt_flag = 0;void interrupt_handler() {interrupt_flag = 1; // 中断中修改
}int main() {while (interrupt_flag == 0) {// 等待中断}// 处理中断后的逻辑
}
场景3: 多线程共享变量
volatile bool is_running = true;// 线程1
void thread1() {while (is_running) {// 执行任务}
}// 线程2
void thread2() {is_running = false; // 停止线程1
}
2.4 关键要点
- volatile 不保证原子性,只保证可见性
- 多线程编程应配合互斥锁等同步机制
- volatile主要用于与硬件交互和信号处理
3. static关键字详解
static
在不同位置有不同含义,我们逐一讲解。
3.1 函数内的static变量
特点: 只初始化一次,保持值不变
void counter() {static int count = 0; // 只在第一次调用时初始化count++;printf("调用次数: %d\n", count);
}int main() {counter(); // 输出: 1counter(); // 输出: 2counter(); // 输出: 3return 0;
}
普通变量对比:
void counter_normal() {int count = 0; // 每次调用都重新初始化count++;printf("调用次数: %d\n", count); // 始终输出1
}
3.2 文件作用域的static变量
作用: 限制变量只能在当前文件中使用
// file1.c
static int internal_var = 100; // 只能在file1.c中访问void func() {internal_var++; // 可以访问
}
// file2.c
extern int internal_var; // 错误!无法访问file1.c中的static变量
3.3 static函数
作用: 限制函数只能在当前文件中调用
// utils.c
static void helper_function() {// 内部辅助函数
}void public_function() {helper_function(); // 可以调用
}
// main.c
extern void helper_function(); // 错误!无法访问static函数
3.4 为什么static变量只初始化一次?
static变量存储在静态存储区(非栈区),程序运行期间一直存在:
变量类型 | 存储位置 | 生命周期 |
---|---|---|
普通局部变量 | 栈 | 函数调用期间 |
static局部变量 | 静态区 | 整个程序运行期间 |
全局变量 | 静态区 | 整个程序运行期间 |
4. const关键字详解
4.1 基本用法
const
表示"只读",修饰的内容不能被修改。
const int MAX = 100;
MAX = 200; // 错误!const变量不能修改
4.2 const vs #define
特性 | const | #define |
---|---|---|
类型检查 | 有 | 无 |
作用域 | 遵循作用域规则 | 全局替换 |
调试 | 可调试 | 难以调试 |
内存 | 分配内存 | 不分配(编译时替换) |
示例:
#define PI 3.14
const double PI_CONST = 3.14;double area1 = PI * 5 * 5; // 每次使用都替换
double area2 = PI_CONST * 5 * 5; // 只有一个变量
4.3 const修饰函数参数
防止函数内部意外修改参数:
void print_string(const char *str) {// str[0] = 'A'; // 错误!不能修改printf("%s\n", str); // 可以读取
}
4.4 const修饰函数返回值
const char* get_name() {return "Alice";
}int main() {const char *name = get_name();// name[0] = 'B'; // 错误!返回的字符串不能修改return 0;
}
4.5 const数组
const int arr[5] = {1, 2, 3, 4, 5};
// arr[0] = 10; // 错误!
5. 指针与const的组合
这是最容易混淆的部分,我们用口诀记忆。
5.1 四种组合方式
int value = 10;// 1. 指向常量的指针 (pointer to const)
const int *p1 = &value;
// *p1 = 20; // 错误!不能通过p1修改value
p1 = &other; // 正确!可以改变指针指向// 2. 常量指针 (const pointer)
int * const p2 = &value;
*p2 = 20; // 正确!可以修改value
// p2 = &other; // 错误!不能改变指针指向// 3. 指向常量的常量指针
const int * const p3 = &value;
// *p3 = 20; // 错误!不能修改value
// p3 = &other; // 错误!不能改变指针指向// 4. 普通指针
int *p4 = &value;
*p4 = 20; // 正确!
p4 = &other; // 正确!
5.2 记忆技巧
“const在星号左边,指向的内容不能变;const在星号右边,指针本身不能变”
const int *p; // const在*左边 → 内容不能变
int * const p; // const在*右边 → 指针不能变
const int * const p; // 都不能变
5.3 实际应用场景
// 函数不会修改字符串内容
void print_message(const char *msg) {printf("%s\n", msg);
}// 函数不会改变指针指向(但可能修改内容)
void modify_value(int * const ptr) {*ptr = 100; // 可以修改
}
6. new/delete与malloc/free对比
6.1 基本区别
特性 | new/delete | malloc/free |
---|---|---|
语言 | C++运算符 | C语言函数 |
返回类型 | 具体类型指针 | void* |
大小计算 | 自动 | 手动指定 |
构造/析构 | 自动调用 | 不调用 |
失败处理 | 抛出异常 | 返回NULL |
6.2 使用示例
malloc/free (C语言):
int *p = (int *)malloc(sizeof(int) * 10); // 分配10个int
if (p == NULL) {// 分配失败处理
}
free(p); // 释放内存
new/delete (C++):
int *p = new int[10]; // 自动计算大小
delete[] p; // 释放数组int *single = new int(5); // 分配单个int并初始化为5
delete single;
6.3 对象的构造和析构
class MyClass {
public:MyClass() { cout << "构造函数" << endl; }~MyClass() { cout << "析构函数" << endl; }
};// 使用new - 会调用构造函数和析构函数
MyClass *obj1 = new MyClass();
delete obj1;// 使用malloc - 不会调用构造和析构
MyClass *obj2 = (MyClass *)malloc(sizeof(MyClass));
free(obj2); // 危险!没有调用析构函数
6.4 关键要点
- C++中优先使用
new/delete
new
配对delete
,new[]
配对delete[]
- 不要混用
new
和free
,或malloc
和delete
7. sizeof运算符
7.1 sizeof是什么?
sizeof
是编译时运算符(不是函数!),返回类型或变量的字节大小。
7.2 基本用法
printf("%zu\n", sizeof(int)); // 4 (32/64位系统)
printf("%zu\n", sizeof(char)); // 1
printf("%zu\n", sizeof(double)); // 8int arr[10];
printf("%zu\n", sizeof(arr)); // 40 (10 * 4)
7.3 sizeof vs strlen
char str[] = "hello";sizeof(str); // 6 (包括'\0')
strlen(str); // 5 (不包括'\0')
关键区别:
特性 | sizeof | strlen |
---|---|---|
计算时机 | 编译时 | 运行时 |
作用对象 | 任何类型 | 字符串 |
是否包含’\0’ | 是 | 否 |
7.4 指针的sizeof
int arr[10];
int *p = arr;sizeof(arr); // 40 (数组大小)
sizeof(p); // 8 (64位系统指针大小) 或 4 (32位)
易错点:
void func(int arr[]) {// arr实际是指针!int size = sizeof(arr); // 错误!得到的是指针大小,不是数组大小
}
7.5 结构体的sizeof
struct Example {char a; // 1字节int b; // 4字节char c; // 1字节
};sizeof(struct Example); // 可能是12,不是6!
原因:字节对齐,下一节详细讲解。
8. 结构体与联合体
8.1 结构体 (struct)
特点: 所有成员各自占用独立内存
struct Student {char name[20]; // 20字节int age; // 4字节float score; // 4字节
}; // 总大小 = 28字节(可能因对齐而变化)
8.2 联合体 (union)
特点: 所有成员共享同一块内存
union Data {int i; // 4字节float f; // 4字节char c; // 1字节
}; // 总大小 = 4字节(最大成员的大小)
示例:
union Data d;
d.i = 10;
printf("%d\n", d.i); // 10d.f = 3.14;
printf("%f\n", d.f); // 3.14
printf("%d\n", d.i); // 未定义!被覆盖了
8.3 内存布局对比
struct S {int a;char b;double c;
};union U {int a;char b;double c;
};sizeof(struct S); // 16 (字节对齐)
sizeof(union U); // 8 (最大成员double的大小)
8.4 字节对齐规则
编译器为了提高访问效率,会对结构体成员进行对齐:
规则:
- 每个成员按其类型大小对齐
- 结构体总大小是最大成员的整数倍
struct Example1 {char a; // 偏移0, 占1字节// 填充3字节int b; // 偏移4, 占4字节char c; // 偏移8, 占1字节// 填充3字节
}; // 总大小12字节struct Example2 {char a; // 偏移0char c; // 偏移1// 填充2字节int b; // 偏移4
}; // 总大小8字节 (优化后)
8.5 联合体的应用场景
场景1: 节省内存
struct Packet {int type;union {int int_data;float float_data;char str_data[20];} data; // 根据type决定使用哪个成员
};
场景2: 类型转换
union FloatBits {float f;unsigned int bits;
};union FloatBits fb;
fb.f = 3.14;
printf("0x%X\n", fb.bits); // 查看浮点数的二进制表示
9. 内存布局:栈与堆
9.1 内存分区
C程序的内存分为几个区域:
高地址
+------------------+
| 栈 (Stack) | ← 局部变量、函数参数
+------------------+ ↓ 向下增长
| ↓ |
| |
| ↑ |
+------------------+ ↑ 向上增长
| 堆 (Heap) | ← 动态分配内存
+------------------+
| 全局/静态区 | ← 全局变量、static变量
+------------------+
| 常量区 | ← 字符串常量
+------------------+
| 代码区 | ← 程序代码
+------------------+
低地址
9.2 栈 (Stack)
特点:
- 自动管理,函数结束自动释放
- 容量有限(通常几MB)
- 速度快
- 存储局部变量
void func() {int a = 10; // 栈上分配char str[100]; // 栈上分配
} // 函数结束,自动释放
9.3 堆 (Heap)
特点:
- 手动管理(malloc/free 或 new/delete)
- 容量大(受系统内存限制)
- 速度较慢
- 适合大数据或生命周期不确定的数据
void func() {int *p = (int *)malloc(1000 * sizeof(int)); // 堆上分配// 使用p...free(p); // 必须手动释放!
}
9.4 常见错误
1. 栈溢出
void func() {int huge_array[1000000]; // 太大!可能栈溢出
}
2. 内存泄漏
void func() {int *p = (int *)malloc(100);// 忘记free(p)! → 内存泄漏
}
3. 悬空指针
int* func() {int a = 10;return &a; // 危险!返回栈上变量的地址
} // a已释放,返回的指针无效
9.5 栈的多线程应用
每个线程都有独立的栈:
// 线程1
void thread1() {int local1 = 1; // 线程1的栈
}// 线程2
void thread2() {int local2 = 2; // 线程2的栈
}
10. extern "C"的作用
10.1 问题背景
C++支持函数重载,C语言不支持:
// C++可以这样
void print(int x);
void print(double x);
void print(const char *x);
编译器通过名字修饰(Name Mangling)区分:
print(int)
→_Z5printi
print(double)
→_Z5printd
但C语言编译器不做名字修饰:
print
→print
10.2 extern "C"的作用
告诉C++编译器:“这段代码按C语言方式编译”
// C++文件中
extern "C" {void c_function(); // 按C方式编译
}// 等价于
extern "C" void c_function();
10.3 实际应用场景
场景1: C++调用C库
// math_lib.h (C语言库)
#ifdef __cplusplus
extern "C" {
#endifvoid calculate(int a, int b);#ifdef __cplusplus
}
#endif
场景2: 提供C接口给其他语言
// my_library.cpp
extern "C" {int add(int a, int b) {return a + b; // C接口}
}
10.4 为什么需要条件编译?
#ifdef __cplusplus
extern "C" {
#endif// 函数声明#ifdef __cplusplus
}
#endif
- C编译器不认识
extern "C"
,需要条件编译 __cplusplus
是C++编译器预定义的宏
总结与学习路径
关键知识点回顾
- 预处理: 宏定义在编译前进行文本替换
- volatile: 防止编译器优化,用于硬件和多线程
- static: 控制变量生命周期和作用域
- const: 定义只读数据,提高代码安全性
- 指针+const: 记住口诀"const在星号哪边,哪边就不能变"
- 内存管理: new/delete(C++) vs malloc/free©
- sizeof: 编译时计算大小
- 结构体vs联合体: 独立内存 vs 共享内存
- 栈vs堆: 自动管理 vs 手动管理
- extern “C”: 实现C/C++互操作
学习建议
- 多敲代码: 每个知识点都要亲手实践
- 看编译结果: 使用
gcc -E
查看预处理结果 - 调试验证: 用GDB等工具观察内存布局
- 循序渐进: 先掌握基础,再学习高级特性
- 阅读经典书籍:
- 《C Primer Plus》
- 《C++ Primer》
- 《深入理解计算机系统》
练习建议
// 练习1: 写一个安全的字符串拷贝函数
void safe_strcpy(char *dest, const char *src, size_t size);// 练习2: 实现一个简单的内存池
typedef struct MemPool {char *memory;size_t size;size_t used;
} MemPool;// 练习3: 理解字节对齐
struct AlignTest {char a;int b;short c;
};
// 计算sizeof并画出内存布局