系统版本:v0.0.7
最后更新:2026-04-10
内容范围:项目结构、编译构建、头文件、命名空间、基础语法、函数、数组与指针、面向对象、成员函数定义、内存管理、模板、STL、异常处理


一、 项目结构与编译构建

1. 完整 C++ 项目目录结构

一个规范的 C++ 项目通常采用以下目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
MyProject/
├── CMakeLists.txt # 顶层 CMake 配置文件
├── README.md # 项目说明文档
├── LICENSE # 开源许可证
├── .gitignore # Git 忽略规则

├── src/ # 源代码目录
│ ├── main.cpp # 程序入口
│ ├── utils.cpp # 工具函数实现
│ └── core/ # 核心模块(可按功能划分子目录)
│ ├── engine.cpp
│ └── renderer.cpp

├── include/ # 头文件目录(对外公开接口)
│ ├── utils.h
│ └── core/
│ ├── engine.h
│ └── renderer.h

├── tests/ # 测试代码目录
│ ├── CMakeLists.txt # 测试专用的 CMake 配置
│ ├── test_utils.cpp
│ └── test_engine.cpp

├── cmake/ # 自定义 CMake 模块(可选)
│ └── FindSomeLib.cmake

├── third_party/ # 第三方库(可选,也可用 FetchContent)
│ └── nlohmann_json/

├── docs/ # 文档目录(可选)
│ └── api.md

├── scripts/ # 辅助脚本(可选)
│ ├── build.sh
│ └── run_tests.sh

└── build/ # 编译输出目录(由 CMake 生成,应加入 .gitignore)
├── CMakeCache.txt
├── Makefile
└── MyProject # 最终生成的可执行文件

各目录说明:

目录/文件 说明
src/ 存放 .cpp 源文件,按功能模块可划分子目录
include/ 存放 .h / .hpp 头文件,与 src/ 结构对应
tests/ 单元测试和集成测试代码
cmake/ 自定义 CMake 查找脚本(如 FindXXX.cmake
third_party/ 直接嵌入的第三方依赖库
docs/ 项目文档、API 说明等
scripts/ 构建、部署、测试等辅助脚本
build/ CMake 输出目录,不应提交到 Git

最佳实践:

  • src/include/ 分离,保持接口与实现解耦
  • build/ 目录永远不要提交到版本控制
  • 大型项目推荐按模块划分子目录,而非把所有文件堆在 src/

2. 使用 g++ 直接编译

适合单文件或小型项目:

1
2
3
4
5
6
7
8
# 编译单个源文件
g++ main.cpp -o main

# 编译多个源文件
g++ main.cpp utils.cpp -o main

# 开启常用警告和优化
g++ -Wall -Wextra -std=c++17 -O2 main.cpp -o main

3. 使用 CMake 构建

CMake 的作用是跨平台地生成 Makefile 或工程文件,简化 C++ 项目的编译过程。

基本构建流程:

1
2
3
mkdir build && cd build
cmake ..
make

CMakeLists.txt 逐行详解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 1. 指定 CMake 最低版本要求
cmake_minimum_required(VERSION 3.5)

# 2. 设置项目名称,同时自动定义 ${PROJECT_NAME}、${PROJECT_SOURCE_DIR} 等变量
project(HelloWorld)

# 3. 设置编译选项
# ${CMAKE_CXX_FLAGS}:保留已有选项
# -Wall:开启几乎所有编译警告
# -Werror:将警告视为错误(严谨开发习惯)
# -std=c++14:指定 C++14 标准
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Werror -std=c++14")

# 4. 定义源文件目录路径变量
set(source_dir "${PROJECT_SOURCE_DIR}/src/")

# 5. 搜索 src/ 下所有 .cpp 文件,存入 source_files 变量
# 注意:GLOB 方式方便但大型项目中 CMake 官方推荐手动列出源文件,
# 以避免新增文件时 CMake 无法感知变化
file(GLOB source_files "${source_dir}/*.cpp")

# 6. 生成可执行文件
# HelloWorld:可执行程序名(Linux 下同名,Windows 下为 HelloWorld.exe)
# ${source_files}:参与编译的源文件列表
add_executable(HelloWorld ${source_files})

更推荐的现代 CMake 写法(C++17):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
cmake_minimum_required(VERSION 3.14)
project(MyProject LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 头文件搜索路径
include_directories(${PROJECT_SOURCE_DIR}/include)

# 手动列出源文件(CMake 官方推荐)
add_executable(${PROJECT_NAME}
src/main.cpp
src/utils.cpp
)

# 链接第三方库(如有)
# target_link_libraries(${PROJECT_NAME} PRIVATE some_lib)

# 启用测试(可选)
enable_testing()
add_subdirectory(tests)

4. 头文件与 #pragma once

4.1 头文件的作用

头文件(.h / .hpp)是 C++ 中声明与实现分离的核心机制:

文件类型 内容 示例
.h / .hpp 声明:函数原型、类定义、宏、常量 int add(int a, int b);
.cpp 实现:函数的具体逻辑、类的成员函数定义 int add(int a, int b) { return a + b; }

为什么需要头文件?

  • 多个 .cpp 文件需要共享同一组声明时,只需 #include 一次头文件
  • 修改实现(.cpp)时,不影响其他文件的编译
  • 隐藏实现细节,只暴露接口

4.2 #include 的两种形式

1
2
#include <iostream>    // 尖括号:搜索系统标准库路径
#include "myheader.h" // 双引号:优先搜索当前文件所在目录,再搜索系统路径

4.3 头文件重复包含的问题

当多个头文件互相引用时,容易出现重复包含

1
2
3
4
a.h 包含了 common.h
b.h 也包含了 common.h
main.cpp 同时 #include "a.h" 和 #include "b.h"
→ common.h 被包含了两次 → 编译报错:重复定义

4.4 #pragma once 的作用

#pragma once 是编译器指令,确保同一个头文件在一次编译过程中只被包含一次

1
2
3
4
5
6
// utils.h
#pragma once

#include <string>

std::string greet(const std::string& name);

工作原理: 编译器首次遇到 #pragma once 时,会记录该文件的唯一标识(通常是文件路径或 inode)。后续再次 #include 时,直接跳过,不再读取文件内容。

4.5 #pragma once vs #ifndef 守卫

传统的替代方案是使用头文件守卫(Include Guard)

1
2
3
4
5
6
7
8
9
// utils.h
#ifndef UTILS_H
#define UTILS_H

#include <string>

std::string greet(const std::string& name);

#endif // UTILS_H
对比项 #pragma once #ifndef 守卫
标准性 非 C++ 标准(但所有主流编译器都支持) C++ 标准,100% 可移植
写法 一行搞定,简洁 需要三行,宏名需手动维护
编译速度 更快(编译器直接跳过文件) 稍慢(仍需打开文件检查宏)
可靠性 依赖文件系统识别唯一性 绝对可靠
推荐度 现代项目首选 老旧项目或极端跨平台场景

最佳实践: 现代 C++ 项目统一使用 #pragma once,简洁高效。只有在需要支持极冷门编译器时才考虑 #ifndef 守卫。


二、 基础语法

1. 数据类型

类型 说明 典型大小
int 整型 4 字节
long long 长整型 8 字节
float 单精度浮点 4 字节
double 双精度浮点 8 字节
char 字符型 1 字节
bool 布尔型 1 字节
std::string 字符串(需 #include <string> 动态

2. 变量声明与初始化

1
2
3
4
5
6
int a = 10;              // 拷贝初始化
int b(10); // 直接初始化
int c{10}; // 列表初始化(C++11,推荐,防止窄化转换)
auto d = 10; // 自动类型推导(C++11)
const int e = 20; // 常量,不可修改
constexpr int f = 30; // 编译期常量(C++11)

3. 输入输出

1
2
3
4
5
6
7
8
9
#include <iostream>

int main() {
int age;
std::cout << "Enter your age: ";
std::cin >> age;
std::cout << "You are " << age << " years old." << std::endl;
return 0;
}

三、 命名空间

1. 为什么需要命名空间

当项目变大或引入多个第三方库时,很容易出现同名冲突

1
2
3
4
5
6
7
// 库 A 提供了这个函数
void log(const std::string& msg);

// 库 B 也提供了这个函数
void log(const std::string& msg);

// main.cpp 同时引入两个库 → 编译报错:重复定义

命名空间(namespace)就是为了解决这个问题——给标识符加一个作用域前缀,避免名称冲突。

2. 基本用法

1
2
3
4
5
6
7
8
9
10
11
12
// 定义命名空间
namespace math {
double pi = 3.14159;

int add(int a, int b) {
return a + b;
}
}

// 使用:作用域解析运算符 ::
int result = math::add(3, 4);
double val = math::pi;

3. using 声明与指令

1
2
3
4
5
6
7
8
9
10
11
// 方式一:using 声明(推荐,只引入需要的符号)
using math::add;
int r = add(1, 2); // 无需前缀

// 方式二:using 指令(引入整个命名空间,慎用)
using namespace math;
double p = pi; // 可以直接用
int s = add(5, 3); // 可以直接用

// 方式三:不引入,始终使用完整限定名(最安全)
double x = math::pi;

最佳实践:

  • .cpp 文件中可以适度使用 using namespace xxx;
  • 头文件中永远不要写 using namespace,会污染所有包含该头文件的源文件
  • 优先使用 using xxx::symbol; 或完整限定名

4. 嵌套命名空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// C++11 之前
namespace company {
namespace project {
void init();
}
}

// C++11 起(推荐写法)
namespace company::project {
void init();
}

// 使用
company::project::init();

5. 匿名命名空间

匿名命名空间中的符号仅在当前编译单元(.cpp 文件)内可见,等价于 C 语言的 static 全局变量:

1
2
3
4
5
6
// utils.cpp
namespace {
int helper() { // 仅本文件可见,外部无法链接
return 42;
}
}

6. std 命名空间

C++ 标准库的所有内容都在 std 命名空间中:

1
2
3
std::cout << "Hello" << std::endl;  // 标准写法
std::vector<int> v;
std::string s = "world";

#include <iostream> 后写 using namespace std; 是初学者常见做法,但在实际项目中不推荐,因为 std 包含大量符号,极易与自定义名称冲突。


四、 控制流

1. 条件语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (score >= 90) {
std::cout << "A" << std::endl;
} else if (score >= 60) {
std::cout << "B" << std::endl;
} else {
std::cout << "C" << std::endl;
}

// switch-case
switch (choice) {
case 1:
std::cout << "Option 1" << std::endl;
break;
case 2:
std::cout << "Option 2" << std::endl;
break;
default:
std::cout << "Unknown" << std::endl;
}

2. 循环语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// for 循环
for (int i = 0; i < 10; ++i) {
std::cout << i << " ";
}

// 范围 for 循环(C++11,推荐遍历容器)
std::vector<int> nums = {1, 2, 3, 4, 5};
for (const auto& num : nums) {
std::cout << num << " ";
}

// while 循环
int count = 0;
while (count < 5) {
std::cout << count << " ";
++count;
}

// do-while 循环
do {
std::cout << "At least once" << std::endl;
} while (false);

四、 函数

1. 基本定义

1
2
3
4
5
6
7
8
9
// 返回值类型 函数名(参数列表)
int add(int a, int b) {
return a + b;
}

// 无返回值
void printHello() {
std::cout << "Hello!" << std::endl;
}

2. 参数传递方式

1
2
3
4
5
6
7
8
9
10
11
12
13
// 值传递:拷贝实参,不影响原值
void byValue(int x) { x = 100; }

// 引用传递:直接操作原值(推荐用于大对象)
void byReference(int& x) { x = 100; }

// const 引用:只读访问,避免拷贝
void byConstRef(const std::string& str) {
std::cout << str << std::endl;
}

// 指针传递
void byPointer(int* x) { *x = 100; }

3. 默认参数与函数重载

1
2
3
4
5
6
7
8
// 默认参数(从右向左设置)
void greet(const std::string& name, const std::string& greeting = "Hello") {
std::cout << greeting << ", " << name << "!" << std::endl;
}

// 函数重载:同名不同参
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }

4. Lambda 表达式(C++11)

1
2
3
4
5
6
auto sum = [](int a, int b) { return a + b; };
std::cout << sum(3, 4) << std::endl; // 输出 7

// 捕获外部变量
int factor = 10;
auto multiply = [factor](int x) { return x * factor; };

五、 数组与指针

1. 数组

1
2
3
4
5
6
7
// C 风格数组
int arr[5] = {1, 2, 3, 4, 5};

// std::array(C++11,推荐,带大小信息)
#include <array>
std::array<int, 5> arr2 = {1, 2, 3, 4, 5};
std::cout << arr2.size() << std::endl; // 输出 5

2. 指针基础

1
2
3
4
5
6
int value = 42;
int* ptr = &value; // ptr 指向 value 的地址
std::cout << *ptr; // 解引用,输出 42

// 空指针(C++11 推荐用 nullptr)
int* empty = nullptr;

3. 指针与数组

1
2
3
int arr[] = {10, 20, 30};
int* p = arr; // 数组名退化为指向首元素的指针
std::cout << *(p + 1); // 输出 20

七、 面向对象编程

面向对象编程(OOP)是 C++ 的核心范式之一,围绕封装、继承、多态三大特性组织代码。

1. 类与对象

类是对象的蓝图,对象是类的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person {
public:
// 构造函数
Person(const std::string& name, int age)
: name_(name), age_(age) {}

// 成员函数
void introduce() const {
std::cout << "I'm " << name_ << ", " << age_ << " years old." << std::endl;
}

// Getter
const std::string& getName() const { return name_; }
int getAge() const { return age_; }

private:
std::string name_;
int age_;
};

// 使用
Person p("Alice", 25);
p.introduce();

2. 访问控制

修饰符 类内 派生类 类外
public
protected
private

设计原则: 成员变量默认用 private,通过 public 接口暴露行为。这就是封装——隐藏内部实现,只暴露必要的接口。

3. 成员函数的定义方式

成员函数是类中定义的函数,用于操作类的成员变量。C++ 提供了多种定义方式:

3.1 在类内部定义(内联函数)

直接在类声明内部定义函数,称为内联函数。编译器可能会将函数体直接展开到调用点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person {
public:
// 直接在类内部定义
void setName(const std::string& name) {
name_ = name;
}

std::string getName() const {
return name_;
}

// 短函数适合内联
int getAge() const { return age_; }

private:
std::string name_;
int age_;
};

适用场景: 函数体简短(通常几行)、调用频繁的 getter/setter。

3.2 在类外部定义

将函数声明放在类内部,实现放在类外部(通常在 .cpp 文件中):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Person.h(头文件)
class Person {
public:
void setName(const std::string& name);
std::string getName() const;
void introduce() const; // 在类内只写声明

private:
std::string name_;
int age_;
};

// Person.cpp(实现文件)
#include "Person.h"

// 类外定义需要使用作用域解析运算符 ::
void Person::setName(const std::string& name) {
name_ = name;
}

std::string Person::getName() const {
return name_;
}

void Person::introduce() const {
std::cout << "I'm " << name_ << ", " << age_ << " years old." << std::endl;
}

适用场景: 函数体较长、复杂实现、需要隐藏细节。

3.3 常量成员函数(const)

const 修饰的成员函数承诺不会修改对象的任何成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Circle {
public:
Circle(double radius) : radius_(radius) {}

// const 成员函数:只读,不修改成员
double getRadius() const {
return radius_; // 只读访问
}

// 非 const 成员函数:可以修改成员
void setRadius(double r) {
radius_ = r;
}

private:
double radius_;
};

// 使用
const Circle c(5.0);
double r = c.getRadius(); // ✅ const 对象只能调用 const 成员函数
// c.setRadius(10.0); // ❌ 编译错误:const 对象不能调用非 const 函数

关键规则:

  • 只读访问的成员函数应该标记为 const
  • const 对象只能调用 const 成员函数
  • const 对象可以调用两者

3.4 静态成员函数

静态成员函数属于类本身,而非某个对象,不能访问 this 指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Config {
public:
Config() { ++instanceCount_; }

// 静态成员函数:不需要对象即可调用
static Config& getInstance() {
static Config instance; // 单例模式
return instance;
}

static int getInstanceCount() {
return instanceCount_;
}

private:
static int instanceCount_; // 静态成员变量
std::string configPath_;
};

// 类外初始化静态成员
int Config::instanceCount_ = 0;

// 使用:无需创建对象
Config& cfg = Config::getInstance();
std::cout << Config::getInstanceCount() << std::endl;

关键规则:

  • 静态成员函数只能访问静态成员变量
  • 可以通过 类名::函数名() 调用,无需对象

3.5 特殊的成员函数

C++ 为类自动生成几个特殊的成员函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class Buffer {
public:
// 默认构造函数
Buffer() = default;

// 带参构造函数
Buffer(size_t size) : data_(new int[size]), size_(size) {}

// 拷贝构造函数
Buffer(const Buffer& other)
: data_(new int[other.size_]), size_(other.size_) {
std::copy(other.data_, other.data_ + other.size_, data_);
}

// 拷贝赋值运算符
Buffer& operator=(const Buffer& other) {
if (this != &other) {
delete[] data_;
size_ = other.size_;
data_ = new int[size_];
std::copy(other.data_, other.data_ + other.size_, data_);
}
return *this;
}

// 移动构造函数(C++11)
Buffer(Buffer&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
}

// 移动赋值运算符(C++11)
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
}
return *this;
}

// 析构函数
~Buffer() {
delete[] data_;
}

private:
int* data_;
size_t size_;
};

关键规则:

  • 如果类管理资源(指针、文件句柄等),必须显式定义拷贝构造、拷贝赋值、析构函数
  • C++11 起,还需要考虑移动构造和移动赋值
  • 使用 = default 让编译器生成默认实现
  • 使用 = delete 禁用拷贝或移动

3.6 inline 关键字

可以用 inline 关键字显式请求内联(只是建议,编译器可能忽略):

1
2
3
4
5
6
7
8
9
10
11
12
class Math {
public:
// 类内部定义隐式内联
double add(double a, double b) { return a + b; }

// 类外部定义显式内联(在头文件中)
inline double multiply(double a, double b);
};

inline double Math::multiply(double a, double b) {
return a * b;
}

最佳实践: 现代编译器会自动优化内联,无需过度关注。除非有特殊性能需求,否则让编译器自行决定。

3.7 成员函数重载与默认参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Logger {
public:
// 函数重载:同名不同参
void log(const std::string& msg) {
log(msg, Level::INFO);
}

void log(const std::string& msg, Level level) {
// ... 打印日志
}

// 默认参数
void setLevel(Level level = Level::DEBUG) {
level_ = level;
}

private:
enum class Level { DEBUG, INFO, WARNING, ERROR };
Level level_ = Level::INFO;
};

3.8 const 重载与重写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Data {
public:
// const 版本
const std::string& getName() const { return name_; }

// 非 const 版本(可修改)
std::string& getName() { return name_; }

private:
std::string name_;
};

// 使用
Data d;
d.getName() = "Alice"; // 调用非 const 版本
const Data cd;
std::string n = cd.getName(); // 调用 const 版本

4. 构造函数详解

构造函数负责对象的初始化。C++ 提供了多种构造函数形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Student {
public:
// 默认构造函数
Student() : name_("Unknown"), age_(0), gpa_(0.0) {}

// 带参构造函数
Student(const std::string& name, int age)
: name_(name), age_(age), gpa_(0.0) {}

// 完整构造函数
Student(const std::string& name, int age, double gpa)
: name_(name), age_(age), gpa_(gpa) {}

// 委托构造函数(C++11):复用另一个构造函数
Student(const std::string& name) : Student(name, 0) {}

// 拷贝构造函数
Student(const Student& other)
: name_(other.name_), age_(other.age_), gpa_(other.gpa_) {}

// 移动构造函数(C++11):接管资源,避免拷贝
Student(Student&& other) noexcept
: name_(std::move(other.name_)),
age_(other.age_),
gpa_(other.gpa_) {}

private:
std::string name_;
int age_;
double gpa_;
};

关键规则:

  • 成员初始化列表(: name_(name))优于在构造函数体内赋值,因为前者直接构造,后者先默认构造再赋值
  • 如果类管理资源(如动态内存),必须实现拷贝构造拷贝赋值,或显式禁用它们

4. 析构函数

析构函数在对象生命周期结束时自动调用,用于清理资源。

1
2
3
4
5
6
7
8
9
10
11
12
class Buffer {
public:
Buffer(size_t size) : data_(new int[size]), size_(size) {}

~Buffer() {
delete[] data_; // 释放动态分配的内存
}

private:
int* data_;
size_t size_;
};

关键规则: 基类有虚函数时,析构函数必须声明为 virtual,否则通过基类指针删除派生类对象时会只调用基类析构函数,导致派生类部分内存泄漏。

5. 静态成员

static 成员属于类本身,而非某个对象实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Counter {
public:
Counter() { ++count_; }
~Counter() { --count_; }

// 静态成员函数:只能访问静态成员
static int getCount() { return count_; }

private:
static int count_; // 静态成员变量(类外定义)
};

// 类外初始化静态成员
int Counter::count_ = 0;

// 使用
Counter a, b, c;
std::cout << Counter::getCount() << std::endl; // 输出 3

6. 友元(friend)

友元允许外部函数或类访问私有成员,打破了封装,应谨慎使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Vector2D {
public:
Vector2D(double x, double y) : x_(x), y_(y) {}

// 友元函数:可以访问私有成员
friend double dot(const Vector2D& a, const Vector2D& b);

// 友元类
friend class VectorSerializer;

private:
double x_, y_;
};

// 友元函数定义(不在类内)
double dot(const Vector2D& a, const Vector2D& b) {
return a.x_ * b.x_ + a.y_ * b.y_; // 可以访问私有成员
}

7. 运算符重载

让自定义类型支持 +-==<< 等运算符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Complex {
public:
Complex(double real, double imag) : real_(real), imag_(imag) {}

// 成员函数形式:重载 +
Complex operator+(const Complex& other) const {
return {real_ + other.real_, imag_ + other.imag_};
}

// 成员函数形式:重载 ==
bool operator==(const Complex& other) const {
return real_ == other.real_ && imag_ == other.imag_;
}

// 友元函数形式:重载 <<(必须是友元,因为左操作数是 ostream)
friend std::ostream& operator<<(std::ostream& os, const Complex& c) {
os << c.real_ << " + " << c.imag_ << "i";
return os;
}

private:
double real_, imag_;
};

// 使用
Complex a(1, 2), b(3, 4);
Complex c = a + b;
std::cout << c << std::endl; // 输出 "4 + 6i"

最佳实践:

  • 对称运算符(+-==)推荐用友元函数实现
  • 赋值类运算符(+=-=)推荐用成员函数实现
  • <<>> 必须是友元函数

8. 继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Animal {
public:
Animal(const std::string& name) : name_(name) {}

virtual void speak() const {
std::cout << "..." << std::endl;
}

virtual ~Animal() = default; // 虚析构函数

protected:
std::string name_;
};

class Dog : public Animal {
public:
Dog(const std::string& name) : Animal(name) {}

void speak() const override {
std::cout << name_ << " says: Woof!" << std::endl;
}
};

class Cat : public Animal {
public:
Cat(const std::string& name) : Animal(name) {}

void speak() const override {
std::cout << name_ << " says: Meow!" << std::endl;
}
};

继承方式对比:

继承方式 public 成员变为 protected 成员变为 使用场景
public public protected 最常见,”是一个”关系
protected protected protected 较少用
private private private “用…实现”关系

9. 多态详解

多态的核心是通过基类指针/引用调用派生类的实现

1
2
3
4
5
6
7
8
9
10
11
// 多态调用
std::vector<std::unique_ptr<Animal>> animals;
animals.push_back(std::make_unique<Dog>("Buddy"));
animals.push_back(std::make_unique<Cat>("Whiskers"));

for (const auto& animal : animals) {
animal->speak(); // 自动调用对应派生类的 speak()
}
// 输出:
// Buddy says: Woof!
// Whiskers says: Meow!

多态的三个必要条件:

  1. 基类有 virtual 函数
  2. 通过基类指针或引用调用
  3. 派生类用 override 重写(C++11 推荐,编译器会检查签名是否匹配)

overridefinal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Base {
public:
virtual void foo() {}
virtual void bar() {}
};

class Derived : public Base {
public:
void foo() override {} // 明确标注重写,编译器检查
void bar() final {} // 禁止进一步重写
};

class MoreDerived : public Derived {
// void bar() override {}; // 编译报错:bar 被 final 修饰
};

10. 纯虚函数与抽象类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Shape {
public:
virtual double area() const = 0; // 纯虚函数
virtual ~Shape() = default;
};

class Circle : public Shape {
public:
Circle(double r) : radius_(r) {}
double area() const override { return 3.14159 * radius_ * radius_; }
private:
double radius_;
};

class Rectangle : public Shape {
public:
Rectangle(double w, double h) : width_(w), height_(h) {}
double area() const override { return width_ * height_; }
private:
double width_, height_;
};

关键规则:

  • 含有纯虚函数的类是抽象类,不能实例化
  • 派生类必须实现所有纯虚函数,否则它也是抽象类
  • 纯虚函数可以有默认实现,但派生类仍需显式声明

11. 组合 vs 继承

继承表达”是一个”(is-a)关系,组合表达”有一个”(has-a)关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 组合示例:Engine 是 Car 的一部分
class Engine {
public:
void start() { std::cout << "Engine started" << std::endl; }
};

class Car {
public:
void drive() {
engine_.start();
std::cout << "Car is driving" << std::endl;
}
private:
Engine engine_; // 组合
};

设计原则:优先使用组合,而非继承。

  • 继承是强耦合,基类改动会影响所有派生类
  • 组合更灵活,可以在运行时替换组件
  • 过度继承会导致”菱形继承”等复杂问题

12. RAII:资源获取即初始化

RAII(Resource Acquisition Is Initialization)是 C++ 最重要的编程范式:用对象的生命周期管理资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 手动管理(容易泄漏)
void bad() {
FILE* f = fopen("data.txt", "r");
// ... 如果中间 throw 异常,f 永远不会被 fclose
fclose(f);
}

// RAII 管理(安全)
class FileGuard {
public:
FileGuard(const char* path, const char* mode)
: file_(fopen(path, mode)) {}
~FileGuard() {
if (file_) fclose(file_);
}
// 禁用拷贝
FileGuard(const FileGuard&) = delete;
FileGuard& operator=(const FileGuard&) = delete;

FILE* get() const { return file_; }

private:
FILE* file_;
};

void good() {
FileGuard f("data.txt", "r");
// 无论正常返回还是抛出异常,析构函数都会执行
}

RAII 的典型应用:

  • std::unique_ptr / std::shared_ptr → 管理动态内存
  • std::lock_guard → 管理互斥锁
  • std::fstream → 管理文件句柄
  • 自定义 Guard 类 → 管理任何需要成对 acquire/release 的资源

13. 面向对象设计原则速查

原则 含义 示例
单一职责 一个类只做一件事 不要把数据库操作和 UI 渲染放在同一个类
开闭原则 对扩展开放,对修改封闭 用虚函数 + 派生类扩展功能,而非修改基类
依赖倒置 依赖抽象,而非具体实现 函数参数用 Shape& 而非 Circle&
接口隔离 接口尽量小而专 不要一个 IAnimal 包含 fly()、swim()、run()
里氏替换 派生类必须能替换基类 Dog 必须能用在所有需要 Animal 的地方

七、 内存管理

1. 栈 vs 堆

特性 栈(Stack) 堆(Heap)
分配方式 自动 手动(new/delete
生命周期 作用域结束自动释放 手动释放
速度 较慢
大小 有限(通常几 MB) 受内存限制

2. 动态内存分配

1
2
3
4
5
6
7
// 单个对象
int* p = new int(42);
delete p;

// 数组
int* arr = new int[10];
delete[] arr;

3. 智能指针(C++11,推荐)

1
2
3
4
5
6
7
8
9
10
11
#include <memory>

// unique_ptr:独占所有权,不可拷贝
std::unique_ptr<int> up = std::make_unique<int>(42);

// shared_ptr:共享所有权,引用计数
std::shared_ptr<int> sp1 = std::make_shared<int>(42);
std::shared_ptr<int> sp2 = sp1; // 引用计数 +1

// weak_ptr:弱引用,不增加计数,用于打破循环引用
std::weak_ptr<int> wp = sp1;

原则: 优先使用智能指针,避免手动 new/delete


八、 模板

1. 函数模板

1
2
3
4
5
6
7
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}

std::cout << max(3, 5) << std::endl; // int
std::cout << max(3.14, 2.72) << std::endl; // double

2. 类模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <typename T>
class Stack {
public:
void push(const T& value) { data_.push_back(value); }
T pop() {
T top = data_.back();
data_.pop_back();
return top;
}
bool empty() const { return data_.empty(); }
private:
std::vector<T> data_;
};

Stack<int> intStack;
intStack.push(10);

九、 STL 标准库

1. 常用容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <vector>
#include <string>
#include <map>
#include <unordered_map>
#include <set>

// vector:动态数组(最常用)
std::vector<int> v = {1, 2, 3};
v.push_back(4);
v.pop_back();

// string:字符串
std::string s = "Hello";
s += " World";
std::cout << s.length() << std::endl;

// map:有序键值对(红黑树)
std::map<std::string, int> ages;
ages["Alice"] = 25;

// unordered_map:哈希表,O(1) 查找
std::unordered_map<std::string, int> fastMap;

// set:有序集合
std::set<int> nums = {3, 1, 2}; // 自动排序为 {1, 2, 3}

2. 常用算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <algorithm>
#include <numeric>

std::vector<int> nums = {3, 1, 4, 1, 5, 9};

// 排序
std::sort(nums.begin(), nums.end());

// 查找
auto it = std::find(nums.begin(), nums.end(), 4);
if (it != nums.end()) {
std::cout << "Found: " << *it << std::endl;
}

// 累加
int sum = std::accumulate(nums.begin(), nums.end(), 0);

// 遍历
std::for_each(nums.begin(), nums.end(), [](int n) {
std::cout << n << " ";
});

十、 异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdexcept>

double divide(double a, double b) {
if (b == 0) {
throw std::invalid_argument("Division by zero");
}
return a / b;
}

int main() {
try {
double result = divide(10, 0);
} catch (const std::invalid_argument& e) {
std::cerr << "Error: " << e.what() << std::endl;
} catch (...) {
std::cerr << "Unknown error" << std::endl;
}
return 0;
}

十一、 常用 C++ 特性速查

C++11

特性 示例
auto 类型推导 auto x = 10;
范围 for for (auto& item : vec)
Lambda [](int x){ return x * 2; }
智能指针 std::make_shared<T>()
nullptr int* p = nullptr;
constexpr constexpr int SIZE = 100;

C++17

特性 示例
结构化绑定 auto [a, b] = std::pair{1, 2};
std::optional std::optional<int> find(int key);
std::variant std::variant<int, std::string> v;
if constexpr if constexpr (std::is_integral_v<T>)
文件系统 std::filesystem::path p("file.txt");

📊 版本迭代记录

版本 日期 更新内容
v0.0.1 2026-04-04 初始版本,包含编译运行、基础语法、控制流、函数、数组与指针、面向对象、内存管理、模板、STL、异常处理、C++11/17 特性速查
v0.0.2 2026-04-04 补充 CMakeLists.txt 逐行详解
v0.0.3 2026-04-04 补充完整 C++ 项目目录结构
v0.0.4 2026-04-04 重构内容逻辑:将项目结构、g++ 编译、CMake 构建整合为统一的”项目结构与编译构建”章节,按”认识结构 → 简单编译 → 构建系统”的顺序组织
v0.0.5 2026-04-07 新增头文件笔记:头文件作用、#include 两种形式、重复包含问题、#pragma once 原理及与 #ifndef 守卫对比
v0.0.6 2026-04-07 新增命名空间完整讲解;大幅扩展面向对象编程:构造函数/析构函数详解、静态成员、友元、运算符重载、多态三要素、override/final、组合vs继承、RAII范式、面向对象设计原则
v0.0.7 2026-04-10 新增成员函数定义完整讲解:类内/类外定义、常量成员函数、静态成员函数、特殊成员函数(构造/析构/拷贝/移动)、inline关键字、函数重载与默认参数、const重载