C++ 基础与高级知识完整总结
适用范围:C++11/14/17/20 基础学习与复习。本文重点解释“语法是什么、有什么作用、什么时候使用、容易出现什么问题”。示例默认使用现代 C++ 写法。
一、C++ 程序的基本结构
一个最简单的 C++ 程序:
#include <iostream>
int main() {
std::cout << "Hello, C++!" << std::endl;
return 0;
}
1. 预处理指令
#include <iostream>
#include 是预处理指令,作用是在编译前把指定头文件的内容引入当前文件。iostream 提供输入输出流对象,如 std::cout、std::cin。
常见头文件:
#include <iostream> // 输入输出
#include <string> // std::string
#include <vector> // std::vector
#include <algorithm> // sort、find、copy 等算法
#include <memory> // 智能指针
#include <fstream> // 文件流
#include <unordered_map>
#include <map>
#include <set>
尖括号通常用于标准库或编译器搜索路径中的头文件;双引号通常用于自己的头文件:
#include "my_class.h"
2. main 函数
main 是程序入口。操作系统启动程序后,会从 main 开始执行。
int main() {
return 0;
}
返回 0 通常表示程序正常结束。现代 C++ 中,main 执行到末尾时可以省略 return 0;,但初学阶段保留更直观。
3. 命名空间
std::cout << "hello";
std 是标准库命名空间,cout 是其中的成员。
也可以写:
using namespace std;
cout << "hello";
但不建议在头文件中使用 using namespace std;,大型项目中容易造成命名冲突。更推荐明确写 std::,或者只引入需要的名称:
using std::cout;
using std::string;
二、注释
注释是给程序员看的内容,不参与程序运行。
1. 单行注释
// 这是单行注释
int age = 18; // 变量说明
2. 多行注释
/*
这是多行注释
可以写较长的说明
*/
3. 注释的作用
- 解释代码意图,而不是重复代码本身。
- 标明函数的参数、返回值和异常条件。
- 记录复杂算法的思路。
- 临时关闭一小段代码进行排查。
- 标记待完成任务,例如 TODO、FIXME。
不推荐:
int age = 18; // 把 age 设为 18
更有价值:
// 未成年用户不允许创建支付订单
if (age < 18) {
return false;
}
注意:不要使用注释掩盖错误代码,也不要让注释长期与实际代码不一致。
三、变量、常量与标识符
1. 变量
变量是内存中的一块有名字的存储空间,值可以在程序运行过程中改变。
int age = 20;
age = 21;
声明和定义:
int score; // 声明并定义,未初始化
int count = 0; // 定义并初始化
int total{100}; // 推荐的列表初始化
局部变量如果没有初始化,值是不确定的:
int x; // 不要直接使用 x,里面可能是随机值
2. 常量
常量一旦初始化,通常不能再修改。
const int MAX_SIZE = 100;
const double PI = 3.1415926;
推荐使用 constexpr 表示编译期常量:
constexpr int BUFFER_SIZE = 1024;
const 的重点是“不能通过该名字修改”;constexpr 强调“值可以在编译期间确定”。
3. 标识符命名规则
标识符可以由字母、数字和下划线组成,但不能以数字开头,也不能使用关键字。
int student_count; // 合法
int 2count; // 非法
int class; // 非法,class 是关键字
常见命名风格:
int studentCount; // 小驼峰
class StudentManager; // 大驼峰
constexpr int MAX_SIZE; // 常量大写
命名要表达含义,避免 a、b、tmp1 充斥整个大型函数。
四、C++ 关键字
关键字是语言保留的特殊单词,不能作为变量名。
常见关键字及作用:
| int、double、char、bool | 基本数据类型 |
| void | 无返回值或无具体类型 |
| const | 常量、只读约束 |
| constexpr | 编译期常量或编译期计算 |
| if、else、switch | 条件分支 |
| for、while、do | 循环 |
| break | 跳出循环或 switch |
| continue | 跳过本轮循环 |
| return | 返回结果并结束函数 |
| class、struct | 定义自定义类型 |
| public、private、protected | 访问权限 |
| new、delete | 动态内存分配和释放,不推荐单独使用 |
| nullptr | 空指针 |
| this | 当前对象指针 |
| virtual | 虚函数,实现运行时多态 |
| override | 明确表示重写基类虚函数 |
| final | 禁止继续继承或重写 |
| static | 静态存储、类静态成员、内部链接等 |
| extern | 声明其他文件定义的变量或函数 |
| friend | 声明友元 |
| template | 模板编程 |
| typename | 表示依赖名称是类型 |
| auto | 自动推导类型 |
| decltype | 获取表达式类型 |
| enum | 枚举类型 |
| namespace | 命名空间 |
| try、catch、throw | 异常处理 |
| using | 类型别名、引入名称 |
| explicit | 禁止隐式转换构造 |
| mutable | 允许在 const 成员函数中修改特定成员 |
| noexcept | 表明函数不抛异常 |
| nullptr | C++11 的空指针字面量 |
五、数据类型
1. 基本数据类型
bool ok = true; // 布尔类型,true 或 false
char ch = 'A'; // 单个字符
int age = 20; // 整数
short s = 10; // 短整数
long l = 100000L; // 长整数
long long big = 1LL; // 更大的整数
float f = 3.14f; // 单精度浮点数
double d = 3.14; // 双精度浮点数
long double ld = 3.14L;
具体占用字节数与平台有关,不能只凭经验写死。需要大小时使用:
#include <iostream>
std::cout << sizeof(int) << '\\n';
2. void
表示没有具体值:
void printMessage() {
std::cout << "message";
}
void* 是无类型指针,可以保存任意对象地址,但使用前必须转换回正确类型,类型不安全,现代 C++ 应尽量少用。
3. 字符串
C 风格字符串:
const char* name = "Tom";
char text[] = "hello";
末尾自动包含 \\\\0 作为结束标志。
现代 C++ 推荐:
#include <string>
std::string name = "Tom";
name += " Smith";
std::cout << name.length();
4. 类型转换
隐式转换:
double d = 3; // int 自动转为 double
显式转换推荐使用 C++ 风格转换:
double result = static_cast<double>(a) / b;
常见转换:
- static_cast:普通、明确、可检查的类型转换。
- const_cast:去除或添加 const,使用不当可能导致未定义行为。
- dynamic_cast:多态体系中的安全向下转型,需要虚函数。
- reinterpret_cast:底层位级别重新解释,风险最高,应非常谨慎。
六、运算符
1. 算术运算符
+ – * / %
/ 在两个整数之间进行整数除法:
5 / 2 // 结果为 2
5.0 / 2 // 结果为 2.5
% 只能用于整数,表示求余数。
2. 关系运算符
== != > < >= <=
不要把 = 和 == 混淆:
x = 5; // 赋值
x == 5; // 比较
3. 逻辑运算符
&& // 与
|| // 或
! // 非
具有短路特性:
if (ptr != nullptr && ptr->value > 0) {
// 只有前半部分为 true,才会访问 ptr->value
}
4. 自增、自减
++i; // 先加再使用
i++; // 先使用再加
—i;
i—;
单独使用时差别不大,但在复杂表达式中容易产生混淆,建议拆开写。
5. 赋值运算符
= += -= *= /= %=
6. 位运算符
& // 按位与
| // 按位或
^ // 按位异或
~ // 按位取反
<< // 左移
>> // 右移
位运算常用于标志位、权限、底层协议和性能敏感代码。不要把 &、| 与 &&、|| 混淆。
7. 三目运算符
int maxValue = a > b ? a : b;
适合简单条件,不要把复杂逻辑全部塞进一行。
8. 作用域运算符与成员访问
std::cout; // 作用域运算符
object.name; // 对象访问成员
ptr->name; // 指针访问成员
ptr->name 等价于 (*ptr).name。
七、流程控制
1. if / else
if (score >= 90) {
std::cout << "优秀";
} else if (score >= 60) {
std::cout << "及格";
} else {
std::cout << "不及格";
}
2. switch
switch (choice) {
case 1:
std::cout << "开始";
break;
case 2:
std::cout << "退出";
break;
default:
std::cout << "未知选项";
}
如果没有 break,会继续执行后面的分支,这种行为叫贯穿。
3. 循环
for (int i = 0; i < 10; ++i) {
std::cout << i << '\\n';
}
int n = 0;
while (n < 10) {
++n;
}
do {
std::cout << "至少执行一次";
} while (false);
范围 for:
std::vector<int> values{1, 2, 3};
for (const int value : values) {
std::cout << value;
}
需要修改元素时使用引用:
for (int& value : values) {
value *= 2;
}
八、数组、指针和引用
1. 数组
数组是一组连续存储、类型相同的元素。
int arr[5] = {1, 2, 3, 4, 5};
std::cout << arr[0];
下标从 0 开始,最后一个元素下标为 size – 1。越界访问不会自动报错,可能造成严重问题。
for (int i = 0; i < 5; ++i) {
std::cout << arr[i];
}
二维数组:
int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};
现代 C++ 中,固定大小数组可以使用:
#include <array>
std::array<int, 5> values{1, 2, 3, 4, 5};
相比原生数组,std::array 支持 .size()、迭代器等标准接口。
2. 指针
指针保存另一个对象的地址:
int value = 10;
int* ptr = &value;
std::cout << *ptr; // 解引用,读取 value
*ptr = 20; // 修改 value
&value 是取地址,*ptr 是解引用。
空指针:
int* ptr = nullptr;
if (ptr != nullptr) {
std::cout << *ptr;
}
不要使用旧式的 NULL 或 0 作为空指针,现代 C++ 使用 nullptr。
3. 指针和数组
数组名在很多表达式中会退化为首元素地址:
int arr[3] = {10, 20, 30};
int* p = arr;
std::cout << *(p + 1); // 20
但数组名不是普通指针,sizeof(arr) 与 sizeof(p) 的含义不同。
4. 引用
引用是已有对象的别名,定义时必须初始化,之后不能改为引用另一个对象。
int value = 10;
int& ref = value;
ref = 20; // value 也变成 20
引用常用于函数参数,避免拷贝:
void print(const std::string& text) {
std::cout << text;
}
- const T&:只读传参,适合较大对象。
- T&:允许函数修改实参。
- T:传值,会产生拷贝或移动。
- T*:可以为空,表达“可能没有对象”。
5. 左值引用与右值引用
左值引用:
int x = 10;
int& ref = x;
右值引用:
int&& rref = 10;
右值引用主要用于移动语义和完美转发,减少临时对象拷贝:
std::string a = "hello";
std::string b = std::move(a);
调用 std::move 后,原对象仍然有效,但内容处于“被移动后”的状态,不应继续假定它保留原内容。
九、内存的四个区域
通常把程序内存抽象为以下区域:
1. 代码区
存放程序的机器指令,也叫文本区。通常具有只读或不可写属性。函数代码、程序执行指令主要位于这里。
2. 全局区 / 静态区
存放全局变量、静态变量、字符串字面量等,程序启动时分配,程序结束时释放。
int globalValue = 10;
static int staticValue = 20;
void function() {
static int count = 0;
++count;
}
局部静态变量只初始化一次,生命周期持续到程序结束。
3. 栈区
栈区存放函数调用相关数据、局部变量、函数参数等。函数结束后,栈上的局部对象通常自动销毁。
void function() {
int local = 10; // 通常位于栈区
}
特点:速度快、自动管理、空间相对有限。不要返回局部变量的地址或引用:
int& badFunction() {
int value = 10;
return value; // 错误:返回了已失效对象的引用
}
4. 堆区
堆区用于动态内存分配,生命周期由程序员或资源管理对象控制。
旧式写法:
int* p = new int(10);
delete p;
p = nullptr;
数组:
int* values = new int[5];
delete[] values;
常见问题:
- 忘记 delete:内存泄漏。
- 重复 delete:未定义行为。
- 释放后继续使用:悬空指针。
- new[] 和 delete 配对错误。
现代 C++ 优先使用 RAII 和智能指针:
#include <memory>
auto p = std::make_unique<int>(10);
auto shared = std::make_shared<int>(20);
一般原则:能用栈对象就不要 new;需要动态生命周期时使用 std::unique_ptr;确实需要共享所有权时才使用 std::shared_ptr。
十、函数
函数用于封装可重复使用的逻辑。
int add(int a, int b) {
return a + b;
}
1. 函数声明与定义
声明告诉编译器函数的名称、参数和返回值:
int add(int a, int b); // 函数声明
定义提供具体实现:
int add(int a, int b) {
return a + b;
}
大型项目通常把声明放在 .h 文件,把实现放在 .cpp 文件。
2. 参数传递
传值:
void change(int x) {
x = 100; // 不影响调用者
}
传引用:
void change(int& x) {
x = 100; // 会修改调用者的变量
}
只读引用:
void print(const std::string& s);
指针传参:
void change(int* p) {
if (p) *p = 100;
}
3. 返回值
int square(int x) {
return x * x;
}
返回引用或指针时,必须确保返回对象的生命周期仍然存在。不能返回局部变量的引用、指针或局部数组地址。
4. 函数重载
同一个作用域中,函数名相同但参数列表不同,称为函数重载。
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
仅返回值不同不能构成重载:
int get();
double get(); // 非法,无法仅靠返回值区分
重载的意义是让相同逻辑使用统一名称,编译器根据参数类型选择合适版本。
5. 默认参数
调用者没有传入参数时,使用默认值:
void showMessage(const std::string& message, int times = 1) {
for (int i = 0; i < times; ++i) {
std::cout << message << '\\n';
}
}
showMessage("hello");
showMessage("hello", 3);
注意:默认参数一般从右向左连续提供,不能出现中间缺省、后面又有非默认参数:
void func(int a, int b = 1, int c = 2); // 正确
// void func(int a = 1, int b, int c); // 错误
默认参数最好只在函数声明中写一次,避免声明和定义不一致。
6. 占位参数
占位参数通常指有参数类型但没有参数名的参数:
void func(int, double) {
std::cout << "参数未使用";
}
它可以表示接口需要接收某些参数,但当前实现不使用这些参数,也可以配合函数重载或特殊接口设计使用。
有些课程也把“占位参数”理解为未命名参数:
void callback(int /*unused*/) {}
不要为了消除警告而随意加入占位参数,应确保函数接口设计确实需要它。
7. 内联函数
inline int square(int x) {
return x * x;
}
inline 主要解决头文件中函数定义的重复定义问题,并向编译器表达“该函数适合内联”的意图。是否真正内联由编译器决定,不要把它当作强制优化指令。
8. 函数指针
函数指针保存函数的地址,可以通过指针调用函数。
int add(int a, int b) {
return a + b;
}
int (*funcPtr)(int, int) = add;
std::cout << funcPtr(2, 3);
函数指针类型的阅读方式:int (*funcPtr)(int, int) 表示 funcPtr 是一个指向函数的指针,该函数接收两个 int,返回 int。
常见用途:回调函数、策略切换、事件处理、C 风格接口。
void calculate(int a, int b, int (*operation)(int, int)) {
std::cout << operation(a, b);
}
现代 C++ 更推荐使用:
#include <functional>
std::function<int(int, int)> operation = add;
如果只是编译期传入一个函数或函数对象,模板通常比 std::function 更高效。
9. Lambda 表达式
auto add = [](int a, int b) {
return a + b;
};
结构:
[capture](parameters) -> return_type {
body
}
捕获:
int x = 10;
int y = 20;
auto byValue = [x, y]() { return x + y; };
auto byReference = [&x]() { x++; };
auto allByValue = [=]() { return x + y; };
auto allByReference = [&]() { return x + y; };
注意引用捕获对象必须在 lambda 使用期间仍然有效,不能捕获即将销毁的局部变量引用并在以后调用。
十一、结构体与枚举
1. struct
结构体用于把多个相关数据组合为一个类型。
struct Student {
std::string name;
int age;
double score;
};
Student stu{"Alice", 20, 92.5};
std::cout << stu.name;
struct 与 class 最大的默认区别:
- struct 默认成员和继承权限是 public。
- class 默认成员和继承权限是 private。
结构体也可以有构造函数、成员函数、继承和多态。
2. enum 与 enum class
enum Color { Red, Green, Blue };
Color color = Red;
更推荐作用域枚举:
enum class Status {
Ready,
Running,
Finished
};
Status status = Status::Ready;
enum class 不会把枚举值直接污染到外部作用域,也不会轻易隐式转换为整数。
十二、类与对象
1. 类和对象
类是对象的设计模板,对象是类创建出来的具体实例。
class Student {
public:
std::string name;
void study() {
std::cout << name << " is studying";
}
};
Student student;
student.name = "Alice";
student.study();
2. 封装
封装是把数据和操作数据的函数放在一起,并限制外部直接访问内部实现。
class BankAccount {
private:
double balance = 0;
public:
void deposit(double amount) {
if (amount > 0) balance += amount;
}
double getBalance() const {
return balance;
}
};
封装的意义:
- 防止外部随意修改非法状态。
- 隐藏实现细节。
- 方便修改内部实现而不影响调用者。
- 通过成员函数集中进行校验。
3. 访问权限
- public:类内、类外、派生类都可以访问。
- private:只有类内成员函数和友元可以访问。
- protected:类内和派生类可以访问,类外通常不能访问。
4. 构造函数
构造函数在对象创建时自动调用,用于初始化成员。
class Person {
private:
std::string name;
int age;
public:
Person(std::string n, int a) : name(std::move(n)), age(a) {}
};
成员初始化列表比在函数体内赋值更直接,也必须用于:
- const 数据成员。
- 引用成员。
- 没有默认构造函数的成员对象。
- 基类构造。
5. 析构函数
对象销毁时自动调用析构函数:
class Resource {
public:
~Resource() {
// 释放资源
}
};
现代 C++ 推荐让成员对象和智能指针自动管理资源,避免手写复杂析构逻辑。
6. this 指针
this 指向当前对象:
class Person {
private:
std::string name;
public:
void setName(const std::string& name) {
this->name = name;
}
};
7. const 成员函数
class Person {
public:
std::string getName() const {
return name;
}
private:
std::string name;
};
const 成员函数承诺不修改对象的普通成员,const 对象只能调用 const 成员函数。
8. 静态成员
静态数据成员属于类,而不是某一个对象:
class Counter {
public:
static int count;
};
int Counter::count = 0;
静态成员函数没有 this 指针,只能直接访问静态成员。
9. 对象特性
面向对象通常强调:
- 状态:对象保存的数据。
- 行为:对象提供的成员函数。
- 身份:每个对象是独立实例。
- 生命周期:构造、使用、销毁。
- 封装:隐藏实现并保护状态。
- 可复制和可移动:对象能否安全拷贝或移动。
10. 继承
继承允许派生类复用基类的成员,并扩展新的功能。
class Animal {
public:
void eat() {
std::cout << "eat";
}
};
class Dog : public Animal {
public:
void bark() {
std::cout << "bark";
}
};
常见继承方式:
- public 继承:表达“is-a”关系,最常用。
- protected 继承:较少使用。
- private 继承:表达实现复用,不代表公开的 is-a 关系。
注意:优先使用组合解决复用问题,继承应表示稳定的“是一种”关系。
11. 多态
多态指同一个接口在不同对象上产生不同表现。
运行时多态需要虚函数:
class Animal {
public:
virtual void speak() const {
std::cout << "animal";
}
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void speak() const override {
std::cout << "wang";
}
};
Animal* animal = new Dog();
animal->speak(); // 调用 Dog::speak
delete animal;
基类析构函数在多态使用时必须是虚析构函数,否则通过基类指针删除派生对象可能只调用基类析构函数。
12. 抽象类与纯虚函数
class Shape {
public:
virtual double area() const = 0;
virtual ~Shape() = default;
};
含有纯虚函数的类是抽象类,不能直接创建对象,只能作为接口被继承。
13. override 与 final
class Dog : public Animal {
public:
void speak() const override;
};
override 可以让编译器检查你确实重写了基类虚函数,推荐始终使用。
class FinalDog final : public Animal {};
final 可以禁止继续继承;用于虚函数时可以禁止继续重写。
14. 友元
友元函数或友元类可以访问类的 private 和 protected 成员。
class Box {
private:
int value = 10;
friend void printBox(const Box& box);
};
void printBox(const Box& box) {
std::cout << box.value;
}
友元打破了封装,应谨慎使用。常见用途是重载输出运算符、实现两个类之间的紧密协作。
15. 运算符重载
class Point {
public:
int x = 0;
int y = 0;
Point operator+(const Point& other) const {
return {x + other.x, y + other.y};
}
};
运算符重载应符合直觉,不能让 + 做删除文件这种完全不符合语义的事情。
16. Rule of 0 / 3 / 5
如果类自己管理资源,通常要考虑:
- 析构函数
- 拷贝构造函数
- 拷贝赋值运算符
- 移动构造函数
- 移动赋值运算符
现代 C++ 的优先原则是 Rule of 0:让 std::string、std::vector、智能指针等成员自动管理资源,从而不需要自己写这些特殊成员函数。
十三、模板
模板允许编写与具体类型无关的通用代码。
1. 函数模板
template <typename T>
T maxValue(T a, T b) {
return a > b ? a : b;
}
std::cout << maxValue(3, 5);
std::cout << maxValue(2.1, 1.5);
typename 和 class 在模板类型参数位置通常等价。
2. 类模板
template <typename T>
class Box {
private:
T value;
public:
explicit Box(T v) : value(std::move(v)) {}
const T& get() const { return value; }
};
使用:
Box<int> intBox(10);
Box<std::string> stringBox("hello");
3. 非类型模板参数
template <typename T, std::size_t N>
class FixedArray {
private:
T data[N];
};
N 不是类型,而是编译期常量参数。
4. 模板特化
当某种类型需要特殊处理时可以特化:
template <typename T>
struct Printer {
static void print(const T& value) {
std::cout << value;
}
};
模板是 C++ 泛型编程的基础,STL 容器和算法大量使用模板。
十四、STL 容器
STL 主要包括容器、迭代器、算法、函数对象和适配器。
1. string
std::string text = "hello";
text += " world";
text.push_back('!');
std::cout << text.size();
常用函数:
text.empty();
text.size();
text.find("world");
text.substr(0, 5);
text.replace(0, 5, "hi");
text.erase(0, 2);
注意 find 找不到时返回 std::string::npos。
2. vector
动态数组,内存连续,支持随机访问。
std::vector<int> values{1, 2, 3};
values.push_back(4);
values.pop_back();
std::cout << values[0];
常用函数:
values.size();
values.empty();
values.clear();
values.front();
values.back();
values.at(0); // 越界时抛异常
values.reserve(100); // 预留容量
size() 是当前元素数量,capacity() 是已分配容量。扩容可能导致原来的迭代器、指针和引用失效。
3. array
固定大小数组:
std::array<int, 3> values{1, 2, 3};
4. deque
双端队列,两端插入和删除效率较高:
std::deque<int> q;
q.push_front(1);
q.push_back(2);
5. list 与 forward_list
链表适合在已知位置进行插入删除,但不支持高效随机访问。
std::list<int> values{1, 2, 3};
values.push_front(0);
多数情况下 vector 的缓存局部性更好,不能因为“插入删除快”就默认使用 list。
6. stack
栈,后进先出:
std::stack<int> st;
st.push(1);
st.push(2);
st.top();
st.pop();
7. queue
队列,先进先出:
std::queue<int> q;
q.push(1);
q.push(2);
q.front();
q.pop();
8. priority_queue
优先队列,默认大顶堆:
std::priority_queue<int> pq;
pq.push(3);
pq.push(10);
std::cout << pq.top(); // 10
小顶堆:
std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap;
9. set 与 multiset
set 保存有序且不重复的元素,multiset 允许重复。
std::set<int> values{3, 1, 2, 2};
// 内容为 1, 2, 3
10. map 与 multimap
map 保存键值对,键有序且不重复:
std::map<std::string, int> scores;
scores["Alice"] = 95;
scores["Bob"] = 88;
使用 operator[] 访问不存在的键时,会自动插入默认值。只查询时可以使用 find 或 at。
11. unordered_map 与 unordered_set
基于哈希表,平均查找复杂度接近 O(1),不保证有序:
std::unordered_map<std::string, int> count;
++count["apple"];
键类型需要支持哈希。
12. pair 与 tuple
std::pair<std::string, int> person{"Alice", 20};
std::cout << person.first;
std::tuple<int, std::string, double> data{1, "test", 3.14};
std::cout << std::get<1>(data);
C++17 结构化绑定:
auto [name, age] = person;
十五、迭代器
迭代器可以理解为指向容器元素的“通用指针”。
std::vector<int> values{1, 2, 3};
for (auto it = values.begin(); it != values.end(); ++it) {
std::cout << *it;
}
begin() 指向第一个元素,end() 指向尾后位置,不能解引用 end()。
常见迭代器:
values.begin();
values.end();
values.cbegin(); // const 迭代器
values.rbegin(); // 反向开始
values.rend();
容器修改后,部分迭代器可能失效,尤其是 vector 扩容、erase 和 insert 操作后要注意。
十六、谓词、函数对象与 lambda
1. 谓词
谓词是返回布尔值的可调用对象,常用于判断条件:
bool isEven(int value) {
return value % 2 == 0;
}
也可以使用 lambda:
auto isEven = [](int value) {
return value % 2 == 0;
};
2. 函数对象
重载了 operator() 的对象叫函数对象:
struct IsGreater {
int limit;
bool operator()(int value) const {
return value > limit;
}
};
函数对象可以保存状态,比普通函数更灵活。
3. std::function
std::function<bool(int)> predicate = isEven;
可以保存普通函数、lambda、函数对象和成员函数包装器,但会有一定类型擦除开销。
十七、STL 基本算法
算法通常位于 <algorithm>,很多算法使用半开区间 [begin, end)。
1. 遍历:for_each
std::for_each(values.begin(), values.end(), [](int value) {
std::cout << value << ' ';
});
C++11 以后,简单遍历通常直接使用范围 for 更易读。
2. 查找:find
auto it = std::find(values.begin(), values.end(), 3);
if (it != values.end()) {
std::cout << "找到了";
}
按条件查找:
auto it = std::find_if(values.begin(), values.end(), [](int x) {
return x > 10;
});
3. 计数:count / count_if
int count = std::count(values.begin(), values.end(), 3);
int evenCount = std::count_if(values.begin(), values.end(), [](int x) {
return x % 2 == 0;
});
4. 排序:sort
std::sort(values.begin(), values.end());
std::sort(values.begin(), values.end(), std::greater<int>());
自定义排序:
std::sort(values.begin(), values.end(), [](int a, int b) {
return a > b;
});
比较函数必须满足严格弱序关系,不能随便返回 a >= b,否则可能出现未定义行为或排序结果异常。
5. 稳定排序:stable_sort
相等元素的相对顺序保持不变,代价通常高于 sort。
6. 拷贝:copy
std::vector<int> target(values.size());
std::copy(values.begin(), values.end(), target.begin());
输出到末尾:
std::vector<int> target;
std::copy(values.begin(), values.end(), std::back_inserter(target));
目标空间不足时直接写入是错误的。
7. 替换:replace / replace_if
std::replace(values.begin(), values.end(), 0, 100);
std::replace_if(values.begin(), values.end(), [](int x) {
return x < 0;
}, 0);
8. 生成:fill / generate
std::fill(values.begin(), values.end(), 0);
int current = 1;
std::generate(values.begin(), values.end(), [¤t]() {
return current++;
});
fill 用固定值填充,generate 使用可调用对象生成每个元素。
9. 删除:remove / remove_if
values.erase(
std::remove_if(values.begin(), values.end(), [](int x) {
return x < 0;
}),
values.end()
);
remove 不会真正改变容器大小,只会把需要保留的元素移动到前面,必须配合 erase,这叫 erase-remove 惯用法。
C++20 可以使用:
std::erase_if(values, [](int x) { return x < 0; });
10. 翻转与反转
std::reverse(values.begin(), values.end());
11. 最大最小
auto maxIt = std::max_element(values.begin(), values.end());
auto minIt = std::min_element(values.begin(), values.end());
12. 二分查找
前提是区间已经有序:
std::sort(values.begin(), values.end());
if (std::binary_search(values.begin(), values.end(), 5)) {
std::cout << "存在";
}
还可以使用:
std::lower_bound(values.begin(), values.end(), 5);
std::upper_bound(values.begin(), values.end(), 5);
13. 数值算法
需要 <numeric>:
int sum = std::accumulate(values.begin(), values.end(), 0);
第三个参数的类型会影响结果类型。计算 long long 总和时不要从 0 开始,否则可能先按 int 累加:
long long sum = std::accumulate(values.begin(), values.end(), 0LL);
十八、异常处理
try {
throw std::runtime_error("something went wrong");
} catch (const std::exception& e) {
std::cerr << e.what();
}
建议:
- 按引用捕获异常,通常使用 const std::exception&。
- 不要用异常处理普通流程控制。
- 析构函数不要抛异常。
- 资源应使用 RAII,保证异常发生时也能自动释放。
十九、智能指针与 RAII
1. RAII
RAII 的核心是:资源绑定对象生命周期。对象构造时获取资源,析构时释放资源。
资源不仅包括内存,也包括文件、锁、网络连接等。
2. unique_ptr
唯一所有权:
auto ptr = std::make_unique<int>(10);
不能复制,只能移动:
auto ptr2 = std::move(ptr);
3. shared_ptr
共享所有权,最后一个 shared_ptr 销毁时对象才释放:
auto p1 = std::make_shared<int>(10);
auto p2 = p1;
不要为了方便到处使用 shared_ptr,它会引入引用计数开销和所有权复杂性。
4. weak_ptr
不拥有对象,常用于打破 shared_ptr 循环引用:
std::weak_ptr<int> weak = p1;
if (auto locked = weak.lock()) {
std::cout << *locked;
}
二十、文件、输入输出与编译
1. 输入输出
int age;
std::cin >> age;
std::cout << age;
std::cerr << "error";
读取一整行:
std::string line;
std::getline(std::cin, line);
混合使用 operator>> 与 getline 时,前面的换行符可能留在输入缓冲区,需要处理。
2. 文件
#include <fstream>
std::ofstream out("data.txt");
out << "hello";
std::ifstream in("data.txt");
std::string line;
while (std::getline(in, line)) {
std::cout << line << '\\n';
}
优先让流对象自动关闭文件,不要手动忘记关闭资源。
3. 编译过程
C++ 程序大致经历:
常见编译命令:
g++ -std=c++17 -Wall -Wextra -pedantic main.cpp -o main
建议始终开启警告。警告不一定是错误,但经常提示潜在错误。
二十一、现代 C++ 常用写法
1. auto
auto value = 10;
auto it = values.begin();
auto 不是动态类型,类型仍然在编译期确定。不要让 auto 隐藏关键类型导致代码难以理解。
2. decltype
decltype(value) another = value;
根据表达式推导类型,常用于模板。
3. nullptr
始终优先使用:
int* p = nullptr;
4. emplace
std::vector<std::pair<int, std::string>> values;
values.emplace_back(1, "hello");
直接在容器内部构造对象,有时可以减少临时对象。
5. 移动语义
std::string source = "large text";
std::string target = std::move(source);
适合转移资源所有权,但不要滥用 std::move,尤其不要对仍需使用的对象盲目移动。
二十二、最常见的错误与注意事项
1. 未初始化变量
int x;
std::cout << x; // 错误风险
声明时就初始化:
int x{};
2. 数组越界
int arr[3];
arr[3] = 10; // 越界
优先使用 std::array、std::vector,并在需要时使用 .at()。
3. 悬空指针和悬空引用
对象已经销毁后,仍保存着它的地址或引用,就会产生悬空问题。
4. 内存泄漏
int* p = new int(10);
// 忘记 delete
使用 std::make_unique 或 std::make_shared。
5. 返回局部变量地址
int* bad() {
int x = 10;
return &x;
}
这是错误的,因为函数结束后 x 已销毁。
6. 误用 sizeof
void print(int arr[]) {
// 这里的 arr 实际上是指针,sizeof(arr) 不是数组总大小
}
数组传参时通常同时传长度,或者使用 std::span、std::vector。
7. 有符号和无符号比较
for (int i = 0; i < values.size(); ++i) {
// 可能出现 signed/unsigned 警告
}
可使用 std::size_t,或直接使用范围 for。
8. 浮点数直接比较
if (a == b) { }
浮点数可能存在精度误差,通常比较误差范围:
std::abs(a – b) < 1e-9
9. 多态基类没有虚析构
如果通过基类指针删除派生类对象,基类析构函数应为 virtual。
10. 拷贝导致性能问题
大对象传参优先考虑:
void process(const LargeObject& object);
11. 头文件重复包含
使用 #pragma once 或 include guard:
#pragma once
12. 宏滥用
宏没有类型检查,容易污染命名空间。常量优先使用 constexpr,函数优先使用 inline 或模板。
13. 不必要的 using namespace
尤其不要在头文件中写:
using namespace std;
14. vector 迭代器失效
push_back 触发扩容后,旧迭代器、指针和引用可能失效;erase 后被删除元素及其后部分迭代器也可能失效。
15. 继承关系不合理
不要为了复用几行代码就继承。若不是稳定的“是一种”关系,优先考虑组合:
class Car {
private:
Engine engine;
};
二十三、推荐的学习顺序
第一阶段:语法基础
第二阶段:面向对象
第三阶段:STL
第四阶段:高级 C++
二十四、一页速查表
// 变量和常量
int x{10};
const int limit{100};
constexpr int bufferSize{1024};
// 引用和指针
int& ref = x;
int* ptr = &x;
int* empty = nullptr;
// 函数
int add(int a, int b = 0) { return a + b; }
// Lambda
auto isPositive = [](int value) { return value > 0; };
// 容器
std::vector<int> values{3, 1, 2};
std::sort(values.begin(), values.end());
// 算法
auto it = std::find_if(values.begin(), values.end(), isPositive);
// 类
class Student {
private:
std::string name;
public:
explicit Student(std::string n) : name(std::move(n)) {}
const std::string& getName() const { return name; }
};
// 智能指针
auto student = std::make_unique<Student>("Alice");
结语
学习 C++ 不要只记语法,要同时理解三个问题:
真正掌握 C++ 的标志,不是能写出很多语法,而是能够正确处理类型、生命周期、所有权、异常、性能和接口设计。初学阶段先写出正确、清晰的代码,再逐步学习模板、移动语义、并发和底层优化。
网硕互联帮助中心





评论前必须登录!
注册