C/C++ 基础
概述
C 语言是系统编程的基石,C++ 在其基础上引入了面向对象、泛型编程和现代内存管理。理解 C/C++ 对于深入理解计算机系统(操作系统、编译器、嵌入式)至关重要。
1. C 语言核心
1.1 指针
指针是存储内存地址的变量,是 C 的核心概念。
int x = 42;
int *p = &x; // p 存储 x 的地址
printf("%d\n", *p); // 42(解引用)
printf("%p\n", p); // 0x7ffd5e8a3c(地址)
printf("%p\n", &x); // 同上
指针与数组的关系:
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // 数组名退化为指针
printf("%d\n", *p); // 10
printf("%d\n", *(p+2)); // 30
printf("%d\n", p[3]); // 40 (等价于 *(p+3))
多级指针:
int x = 10;
int *p = &x;
int **pp = &p;
printf("%d\n", **pp); // 10
函数指针:
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int (*op)(int, int); // 函数指针声明
op = add;
printf("%d\n", op(3, 4)); // 7
op = sub;
printf("%d\n", op(3, 4)); // -1
1.2 指针运算
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;
p++; // 移动 sizeof(int) = 4 字节
// p 现在指向 arr[1]
ptrdiff_t diff = &arr[4] - &arr[0]; // 4(元素个数差)
1.3 动态内存管理:malloc/free
#include <stdlib.h>
// 分配
int *arr = (int *)malloc(100 * sizeof(int));
if (arr == NULL) {
// 分配失败处理
return -1;
}
// 使用
for (int i = 0; i < 100; i++) {
arr[i] = i * i;
}
// 释放
free(arr);
arr = NULL; // 避免悬垂指针
常见陷阱:
| 问题 | 原因 | 后果 |
|---|---|---|
| 内存泄漏 | malloc 后未 free | 内存持续增长 |
| 悬垂指针 | free 后继续使用指针 | 未定义行为 |
| 双重释放 | 同一地址 free 两次 | 堆损坏/崩溃 |
| 缓冲区溢出 | 越界写入 | 安全漏洞 |
1.4 栈 vs 堆
高地址 ┌──────────────────┐
│ Stack │ ← 局部变量,自动管理
│ (向下增长) │ 函数调用栈帧
├──────────────────┤
│ ↓ │
│ (未使用空间) │
│ ↑ │
├──────────────────┤
│ Heap │ ← malloc/free,手动管理
│ (向上增长) │ 动态分配
├──────────────────┤
│ BSS段 │ ← 未初始化全局变量
├──────────────────┤
│ Data段 │ ← 已初始化全局变量
├──────────────────┤
低地址 │ Text段 │ ← 代码(只读)
└──────────────────┘
| 特性 | 栈 (Stack) | 堆 (Heap) |
|---|---|---|
| 管理方式 | 自动(编译器) | 手动(malloc/free) |
| 分配速度 | 极快(移动栈指针) | 较慢(搜索空闲块) |
| 大小限制 | 较小(通常 1-8MB) | 较大(受限于内存) |
| 生命周期 | 函数返回即释放 | 直到显式 free |
| 碎片 | 无 | 可能产生碎片 |
2. C++ 现代特性
2.1 RAII(Resource Acquisition Is Initialization)
RAII 原则:资源获取即初始化,利用对象生命周期自动管理资源。
class FileHandle {
FILE* file_;
public:
FileHandle(const char* path, const char* mode)
: file_(fopen(path, mode)) {
if (!file_) throw std::runtime_error("Cannot open file");
}
~FileHandle() {
if (file_) fclose(file_); // 析构时自动关闭
}
// 禁止拷贝
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
FILE* get() const { return file_; }
};
void process() {
FileHandle fh("data.txt", "r");
// 使用 fh.get()
} // 离开作用域,自动调用析构函数关闭文件
2.2 智能指针
C++11 引入智能指针,自动管理堆内存。
std::unique_ptr:独占所有权
#include <memory>
auto ptr = std::make_unique<int>(42);
// *ptr == 42
// 所有权转移
auto ptr2 = std::move(ptr);
// ptr 现在为 nullptr
// 自定义删除器
auto file_ptr = std::unique_ptr<FILE, decltype(&fclose)>(
fopen("data.txt", "r"), &fclose
);
std::shared_ptr:共享所有权(引用计数)
auto sp1 = std::make_shared<std::vector<int>>(100);
auto sp2 = sp1; // 引用计数 = 2
sp1.reset(); // 引用计数 = 1
sp2.reset(); // 引用计数 = 0,对象被销毁
std::weak_ptr:解决循环引用
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // 用 weak_ptr 打破循环
};
2.3 模板(Templates)
// 函数模板
template<typename T>
T max_val(T a, T b) {
return (a > b) ? a : b;
}
// 类模板
template<typename T, size_t N>
class Array {
T data_[N];
public:
T& operator[](size_t i) { return data_[i]; }
constexpr size_t size() const { return N; }
};
// 使用
Array<int, 10> arr;
arr[0] = 42;
模板特化:
// 通用版本
template<typename T>
std::string to_string(T val) {
return std::to_string(val);
}
// bool 的特化
template<>
std::string to_string<bool>(bool val) {
return val ? "true" : "false";
}
2.4 STL 容器
| 容器 | 底层实现 | 随机访问 | 插入/删除 | 查找 |
|---|---|---|---|---|
vector |
动态数组 | \(O(1)\) | 尾部 \(O(1)\),中间 \(O(n)\) | \(O(n)\) |
list |
双向链表 | \(O(n)\) | \(O(1)\) | \(O(n)\) |
deque |
分段数组 | \(O(1)\) | 首尾 \(O(1)\) | \(O(n)\) |
map |
红黑树 | - | \(O(\log n)\) | \(O(\log n)\) |
unordered_map |
哈希表 | - | 平均 \(O(1)\) | 平均 \(O(1)\) |
set |
红黑树 | - | \(O(\log n)\) | \(O(\log n)\) |
#include <vector>
#include <map>
#include <unordered_map>
// vector
std::vector<int> v = {1, 2, 3};
v.push_back(4);
v.emplace_back(5); // 原地构造,避免拷贝
// map(有序)
std::map<std::string, int> scores;
scores["Alice"] = 95;
scores["Bob"] = 87;
// unordered_map(哈希,更快)
std::unordered_map<std::string, int> cache;
cache.insert({"key1", 100});
auto it = cache.find("key1");
if (it != cache.end()) {
std::cout << it->second << std::endl;
}
2.5 移动语义(Move Semantics)
C++11 引入右值引用和移动语义,避免不必要的深拷贝。
class Buffer {
int* data_;
size_t size_;
public:
// 构造函数
Buffer(size_t n) : data_(new int[n]), size_(n) {}
// 拷贝构造(深拷贝,开销大)
Buffer(const Buffer& other) : data_(new int[other.size_]), size_(other.size_) {
std::copy(other.data_, other.data_ + size_, data_);
}
// 移动构造(窃取资源,开销小)
Buffer(Buffer&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr; // 原对象置空
other.size_ = 0;
}
~Buffer() { delete[] data_; }
};
Buffer create_buffer() {
Buffer buf(1000000);
return buf; // 触发移动构造(或 NRVO)
}
std::move 的本质:将左值强制转换为右值引用。
std::string s1 = "Hello, World!";
std::string s2 = std::move(s1); // s1 的内容被"窃取"
// s1 现在为空字符串
2.6 Lambda 表达式
// 基本语法: [捕获列表](参数) -> 返回类型 { 函数体 }
auto add = [](int a, int b) -> int { return a + b; };
// 捕获方式
int x = 10;
auto by_value = [x]() { return x; }; // 值捕获
auto by_ref = [&x]() { x++; return x; }; // 引用捕获
auto all_val = [=]() { return x; }; // 全部值捕获
auto all_ref = [&]() { x++; }; // 全部引用捕获
// 与 STL 算法配合
std::vector<int> v = {5, 3, 1, 4, 2};
std::sort(v.begin(), v.end(), [](int a, int b) {
return a > b; // 降序
});
// 泛型 Lambda (C++14)
auto generic_add = [](auto a, auto b) { return a + b; };
3. 内存布局
3.1 C++ 对象内存布局
class Base {
int x; // 4 bytes
virtual void f(); // vtable pointer: 8 bytes
};
// sizeof(Base) = 16 (8 vtable ptr + 4 int + 4 padding)
class Derived : public Base {
int y; // 4 bytes
void f() override;
};
// sizeof(Derived) = 16 (vtable ptr 共享 + 4 + 4)
虚函数表(vtable)机制:
Derived 对象布局:
┌──────────────────┐
│ vptr → vtable │ 8 bytes(指向虚函数表)
├──────────────────┤
│ Base::x │ 4 bytes
├──────────────────┤
│ Derived::y │ 4 bytes
└──────────────────┘
vtable (Derived):
┌──────────────────┐
│ &Derived::f │ → Derived 覆写的版本
└──────────────────┘
3.2 内存对齐
struct Bad {
char a; // 1 byte + 3 padding
int b; // 4 bytes
char c; // 1 byte + 3 padding
}; // sizeof = 12
struct Good {
int b; // 4 bytes
char a; // 1 byte
char c; // 1 byte + 2 padding
}; // sizeof = 8
4. 编译模型
C/C++ 的编译过程分为四个阶段:
源代码 (.c/.cpp)
│
▼ 预处理 (Preprocessing)
展开后的源代码
│ - #include 文件包含
│ - #define 宏替换
│ - #ifdef 条件编译
▼ 编译 (Compilation)
汇编代码 (.s)
│ - 语法分析
│ - 语义分析
│ - 优化
▼ 汇编 (Assembly)
目标文件 (.o/.obj)
│ - 机器码
│ - 符号表
▼ 链接 (Linking)
可执行文件 (a.out/.exe)
│ - 符号解析
│ - 地址重定位
│ - 静态链接 vs 动态链接 (.so/.dll)
# GCC 各阶段命令
gcc -E main.c -o main.i # 预处理
gcc -S main.i -o main.s # 编译
gcc -c main.s -o main.o # 汇编
gcc main.o -o main # 链接
# 一步完成
gcc -O2 main.c -o main
4.1 静态链接 vs 动态链接
| 特性 | 静态链接 (.a/.lib) | 动态链接 (.so/.dll) |
|---|---|---|
| 链接时机 | 编译时 | 运行时 |
| 文件大小 | 较大(包含库代码) | 较小 |
| 部署 | 独立,无依赖 | 需要共享库 |
| 更新 | 重新编译 | 替换共享库 |
| 内存 | 每进程一份 | 多进程共享 |
4.2 头文件与编译单元
// math_utils.h(头文件:声明)
#pragma once // 防止重复包含
int add(int a, int b);
int multiply(int a, int b);
// math_utils.cpp(编译单元:定义)
#include "math_utils.h"
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }
// main.cpp
#include "math_utils.h"
int main() {
return add(3, 4);
}
# 分别编译,再链接
g++ -c math_utils.cpp -o math_utils.o
g++ -c main.cpp -o main.o
g++ math_utils.o main.o -o main
5. 现代 C++ 实践建议
| 原则 | 旧方式 | 现代方式 |
|---|---|---|
| 内存管理 | new/delete |
make_unique/make_shared |
| 数组 | C 数组 | std::vector / std::array |
| 字符串 | char* |
std::string / std::string_view |
| 迭代 | 下标遍历 | 范围 for / 迭代器 |
| 回调 | 函数指针 | std::function / Lambda |
| 常量 | #define |
constexpr / const |
| 线程 | pthread |
std::thread / std::async |
参考资料
- "The C Programming Language" - Kernighan & Ritchie
- "Effective Modern C++" - Scott Meyers
- "C++ Primer" - Stanley B. Lippman
- CppReference: https://en.cppreference.com