云计算百科
云计算领域专业知识百科平台

C++_基础与高级知识完整总结

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(), [&current]() {
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++ 程序大致经历:

  • 预处理:处理 #include、宏等。
  • 编译:把源代码翻译为目标文件。
  • 汇编:生成机器码。
  • 链接:把目标文件和库连接成可执行文件。
  • 运行:操作系统加载程序并执行。
  • 常见编译命令:

    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

  • string、vector、array。
  • deque、list、stack、queue。
  • set、map、unordered_map。
  • 迭代器、算法、lambda 和谓词。
  • 智能指针、异常和 RAII。
  • 第四阶段:高级 C++

  • 模板和模板特化。
  • 移动语义和完美转发。
  • 右值引用。
  • 多线程、锁和原子操作。
  • CMake、单元测试、调试工具。
  • 设计模式和大型项目代码组织。

  • 二十四、一页速查表

    // 变量和常量
    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++ 的标志,不是能写出很多语法,而是能够正确处理类型、生命周期、所有权、异常、性能和接口设计。初学阶段先写出正确、清晰的代码,再逐步学习模板、移动语义、并发和底层优化。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » C++_基础与高级知识完整总结
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!