文章

C++ 学习笔记

C++基础学习

C++ 学习笔记

C++ 学习笔记

目录


1. 枚举

枚举类是整数的集合(默认为 0, 1, 2…),以该类命名的变量只能从该集合中取值。 经常与switch一起使用。

1
2
3
4
5
6
7
enum class Color {
    Red,    // 0
    Green,  // 1
    Blue    // 2
};

Color c = Color::Red;

单纯的枚举量。

1
2
3
4
enum  example 
{
     Aa, Bb, Cc
}Dd;

2. 静态 (static)

类和结构体外(全局静态)

只在当前 cpp 文件的翻译单元内有效;其他翻译单元无法找到。
建议:少用全局变量,必须用全局变量且不需要让函数和变量跨翻译单元时,记得把函数或变量标记为静态。

external linking

在一个cpp内定义了一个变量,要在另一个cpp文件中使用时。

1
2
3
4
5
6
7
8
9
10
//a.cpp中,不加static,加了后无法找到。
int s_var = 5;

//main.cpp中
extern int s_var;   //注意这里没有了赋值
int main(){
    std::cout << s_var << std::endl;
}

//输出5

类和结构体内

修饰变量时

只能创建一次,创建多个实例并改变该值时,改变的是同一个(可以实现在该类的所有实例之间传递数据)。与命名空间类似。定义过程在编译时完成,需要在类外定义

1
2
3
4
5
6
7
8
9
10
11
class Player {
public:
    static int speed;
};

// 类外定义
int Player::speed;

int main() {
    Player::speed = 10;  // 通过类名访问
}

修饰函数时

只能访问静态成员,与命名空间类似。但无需在类外定义。

注意:与命名空间类似的含义是通过 类名::变量名/函数名 的方式访问,而不应通过创建的实例进行引用。

Player::speed = 10; 是正确的
Player e1; e1.speed = 10; 是错误的,因为静态是属于类的所有实例的,单个实例引用无意义。

重要:静态方法不能访问非静态变量,原因就是静态方法没有类实例。 补充:在类内写的非静态方法都会获取当前类实例作为参数(this指针)

局部静态

仅在局部作用域内生效,但可一直存在,生命周期与程序等同。

3. 构造函数

函数名需与类名相同。

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
class Player {
public:
    // 默认构造函数:在类实例化时进行一些初始化操作
    Player() {
        x = y = speed = 0;
        std::cout << "Default constructor" << std::endl;
    }
    
    // 函数重载,同名不同参
    Player(int _x, int _y) {
        x = _x;
        y = _y;
        speed = 0;
    }
};

// 两种禁止实例化的方法
class Test1 {
private:
    Test1();  // 私有构造函数
};

class Test2 {
public:
    Test2() = delete;  // 使用 delete 关键字
};

4. 继承

把类所有的公共功能放到一个父类中,然后从父类派生出一些类。

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
class Entity {
public:
    float x, y;
    
    void move(float dx, float dy) {
        x += dx;
        y += dy;
    }
};

class Player : public Entity {
public:
    char* name;
    
    void print_name() {
        std::cout << "Player " << name << "\n";
    }
};

// 子类是父类的超集,接受父类参数的地方也可以接受子类
void point(Entity e) {
    e.move(5, 5);
}

void run_game() {
    static Player player;
    player.print_name();
    point(player);  // 可以传入子类
}

5. 析构函数

对象被销毁时调用(生命周期),用于对某些初始化的变量进行处理,例如堆上的数据。

1
2
3
4
5
6
class Player {
public:
    ~Player() {
        std::cout << "Player Destroyed" << std::endl;
    }
};

6. 虚函数

对父类中的函数进行覆写,通过将父类中的函数标记为虚函数virtual(同时将子类中的该函数在括号后添加 override,也可不加,但加上更好,可读性、正确性更好,此时若写错函数名,会因为没有对应的父类虚函数而报错),来使子类中对该函数重新写的函数可以生效,不强制要求覆写

使用场景:通过基类指针/引用调用子类版本。 要点:没有虚函数时,一个接受父类指针为参数的函数,并调用父类中的函数时,将子类指针传入时仍会调用父类中的函数,即使在子类中有覆写的函数;使用虚函数后,才会根据实际对象类型来决定调用哪个版本——这就是动态多态。

重要:必须是指针或引用,值传递时没有效果。

需要额外建立虚函数表,以及一个指向该表的成员指针,并在调用时对该表遍历,会影响性能,但不大。

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
class Entity {
public:
    float x, y;
    
    void move(float dx, float dy) {
        x += dx;
        y += dy;
    }
    
    virtual void print_name() {  // 虚函数
        std::cout << "Entity" << std::endl;
    }
};

class Player : public Entity {
public:
    char* name;
    
    void print_name() override {  // 覆写
        std::cout << "Player " << name << "\n";
    }
};

// 子类是父类的超集,接受父类参数的地方也可以接受子类
void point1(Entity* e) {
    e->print_name();
}

void point2(Entity e) {
    e.print_name();
}

void run_game() {
    Entity* e = new Entity();  // 指针的初始化操作
    e->print_name();
    
    Player* player = new Player();
    player->print_name();
    
    point1(player);  // 会输出 Player,未使用虚函数时会输出 Entity
    
    Player player2;
    point2(player2);  // 无论是否使用虚函数都会输出 Entity
                      // 但当将 point2 中参数改为引用时就和指针效果一样了
}

提示:当只是子类内部自己用,不通过基类接口调用,可以不用 virtual,但这时叫做“重新定义”而非“覆写”此时基类指针永远调用基类版本,即使传入的是子类。


7. 纯虚函数

基类中的纯虚函数(也称接口),只包含未实现的方法,作为模板(此时基类不可实例化),由子类去定义各自的实现方法(子类必须实现纯虚函数后才可实例化)。

用途:可以将基类作为参数放入一个通用的函数中,该函数就可以接受子类去执行各自的实现方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Name {
public:
    virtual std::string getName() = 0;  // 纯虚函数定义方法
};

class A : public Name {
public:
    std::string getName() override {
        return "A";
    }
};

void print_name(Name* a) {
    std::cout << a->getName() << std::endl;
}

void test() {
    A* a = new A();
    print_name(a);
}

8. 可见性

修饰符说明
private类内与友元可见,类外不可见
protected继承体系可见,类外不可见
public可见

通过可见性实现想要的类的访问和调用方式,更好地组织代码。(对类内的某些访问和修改可能会造成问题)

特别地:main是一个全局函数,不属于任何类。对于main函数来说,它只能访问对象的public成员,而不能访问protectedprivate成员。


9. 数组

尽可能在栈上创建数组,因为堆上创建数组时会出现内存间接寻址,即变量名对应内存处存储的是数组的地址,而非数组本身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 无法获得数组大小,需要自己维护
void arr() {
    static constexpr const int a_size = 5;
    int a[a_size];           // 在栈上创建数组,标准 C++要求a_size必须在编译时确定,即constexpr/const修饰。
    int* b = new int[a_size]; // 在堆上创建数组,内存访问违规:访问越界时有可能不会报错,需注意。
    delete[] b;
}

// 注意:如果该函数要返回数组,要么传入要返回的数组的指针或引用,要么使用 new 创建
// 因为栈上创建数组离开作用域自动销毁,堆上则不会

// 标准库数组
#include <array>

void arr2() {
    std::array<int, 5> a;  // 有边界检查,记录数组大小等功能,更安全
    std::cout << a.size() << std::endl;
}

10. 字符串

C++中默认的双引号就是一个字符数组const char,并且末尾会补’\0’ (空终止符), 而cout会输出直到’\0’就终止。

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
const char* name = "cherno"; //c风格字符串,当需要修改时要将其该指针转化为数组。
char* name = "cherno"; //报错,因为C++中默认的双引号就是一个字符数组const char*
char name[3] = {'l','i','u'};//报错,缺少空终止符
char name[3] = {'l','i','u',0};//正确
char name[3] = {'l','i','u','\0'};//正确,因为ascii码'\0'就是null

#include <string>
void string_demo() {
    // 一种字符串附加方式,string 类型可以直接加
    std::string s1 = std::string("Hello") + "World";
    std::cout << s1 << std::endl;
    
    // 另一种字符串附加方式
    s1 += "World";
    
    // 再另一种字符串附加方式,""是const char*,即指针,两个指针不能直接相加
    {//使用using namespace尽量缩小作用域
        using namespace std::string_literals;  // 一些字符串函数
        
        std::string s2 = "Hello"s + "World";  // s 是一个运算符
        std::u16string s3 = u"hello"s + u"World";//不同编码的字符串字面量能直接生成对应的标准字符串类型,避免手工转换。
        std::u32string s4 = U"hello"s + U"World";
    }
    
    const char* name1 = u8"jiankang";        // 一字节,u8 可省,UTF-8
    const wchar_t* name2 = L"jiankang";      // 宽字符,Win 两字节,Linux 四字节
    const char16_t* name3 = u"jiankang";     // 两字节 UTF-16
    const char32_t* name4 = U"jiankang";     // 四字节 UTF-32
    
    // R 表示忽略转义字符(原始字符串)
    const char* example = R"(line1
line2
line3
line4)";
    
    // 与上边同样的功能
    const char* example2 = "liu\n"
                          "line2\n"
                          "line3\n";
}

// 以只读和引用的方式传递字符串,提高效率
void print_arr(const std::string& a) {
    std::cout << a << std::endl;
}

11. Const

参考const详解 好处:

  1. 编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期的常量,没有了存储与读内存的操作,使得它的效率也很高。
  2. const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象宏一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而宏定义的常量在内存中有若干个拷贝。
  3. const修饰的函数可以看作是对同名函数的重载。
  4. const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换时可能会产生意料不到的错误。
  5. 这样可以避免由于无意间修改数据而导致的编程错误。

    11.1 修饰变量

    1
    2
    3
    
    int const age = 39;
    //或
    const int age = 39;
    

    修改方法,实际项目中应避免

1
2
3
4
5
6
7
8
int * p = (int *)&age;
*p = 40;

cout << "age=" << age << endl;
cout << "*p=" << *p << endl;
cout << "&age=" << &age << endl;
cout << "p=" << (void*)p << endl;
cout << "&p=" << &p << endl;
局部const变量

当const修饰的变量为局部变量时,代码可编译运行,但常出现“常量折叠”现象:输出 age 仍为原值(如 39),而 *p 为修改后的值(如 40),尽管两者地址相同。

原因:编译器将 const 局部变量的值直接放入符号表,后续读取 age 时直接从符号表取值,不访问内存;而指针修改的是内存中的实际值,导致不一致。

本质:这是编译器优化导致的,不是真正的“一个地址存两个值”。

全局 const 变量

编译通过,但运行时错误(如写入权限冲突)。

原因:全局 const 变量通常存储在只读数据段(如 .rodata),操作系统/链接器禁止修改其内存。

volatile 关键字

使用 const volatile int age = 39; 可以禁止编译器优化,强制每次读取 age 都从内存中获取。

这样,通过指针修改内存后,再输出 age 就能得到修改后的值(如 40),避免了常量折叠。

注意:部分老旧编译器(如 VC++ 6.0)可能不完全支持 volatile 的该语义,仍然会优化。

强制类型转换的必要性

直接写int* p = &age;会报错:不能将const int*隐式转换为int*

必须使用显式转换:(int*)&ageconst_cast<int*>(&age)。后者是 C++ 风格,更推荐。

11.1 指针相关

阅读规则:const 默认修饰它左边的类型;如果左边没有类型,则修饰右边的类型。 a是一个指针,也是一个地址,该地址指向一处内存存储具体的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int b = 5;

// 指向常量的指针(pointer to const),const修饰int,使具体的数据成为常量。
// 可以改变指针指向,但不能通过指针修改所指的值
const int* a = new int;
// *a = 4;          // 错误:不能修改 const int
std::cout << *a << std::endl;   // 可以读取
a = &b;                         // 可以改变指针指向

// 与 const int* 完全等价,写法不同
int const* c = new int;         // 同样是指向常量的指针

// 常量指针(const pointer),const修饰*,使地址/指针成为常量。
// 不能改变指针指向,但可以通过指针修改所指的值
int* const a2 = new int;
*a2 = 6;
// a2 = &b;         // 错误:不能改变常量指针的指向
std::cout << *a2 << std::endl;

// 指向常量的常量指针(const pointer to const)
// 指针指向和所指的值都不能改变,只能读取
const int* const a3 = new int;
std::cout << *a3 << std::endl;

可以通过强制转换绕过const,但不建议使用。

11.2 类中

在方法后面添加const,保证此方法不修改非 mutable 成员。 若常量实例引用为参数时,只能用 const 修饰的方法,因此如果方法没有修改类或者它们不应该修改类时,必须进行该重载。

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
class Entity {
private:
    int m_x;
    int* m_y;
    mutable int m_score;  // mutable 修饰的变量可以在 const 方法中改变
    
public:
    // 只能在类中使用,保证此方法不修改实际的类
    // 通常用在 get 方法上,也可用于后续扩展
    int get_x() const {
        m_score += m_x;  // 可以修改 mutable 变量
        return m_x;
    }
    
    // 可以定义同名的不同方法
    // 在常量实例引用为参数时会调用 const 方法,否则调用该方法,见void print_x(const Entity& e)
    int get_x() {
        return m_x;
    }
    
    // 返回一个不可修改的指针,指向的内存和指向的内存中的值都不可修改
    const int* const get_y() const {
        return m_y;
    }
};

// 若常量实例引用为参数时,只能用 const 修饰的方法,因此必须进行该重载
void print_x(const Entity& e) {
    // e = nullptr;  // 引用不能重新赋值,相当于在改变这个对象
    std::cout << e.get_x() << std::endl;  // 必须保证所调用的方法是 const 的
}

// 指向常量的指针为调用参数时,能修改指针本身,不可以通过 e 修改指向的对象
// 常量指针(Entity* const e) 不能修改指针本身,可以通过 e 修改指向的对象
void print_y(const Entity* e) {
    Entity another;
    e = &another;                  //e = nullptr;时不能再使用该指针,会解引用空指针
    std::cout << e->get_x() << std::endl;  // 安全,e 指向有效对象
}

12. Mutable

一种用法如上文,和 const 一起用,使变量在 const 方法中可修改(一般用于调试时查看调用次数等)。

另一种用法是在 lambda 表达式中(基本不会用到),如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int x = 10;

// [] 中表示变量捕获方式:
// &x 表示引用,x 表示直接传递值
// = 表示所有变量的值传递,& 对所有变量进行引用传递
auto f = [=]() mutable {
    x++;
    std::cout << x << std::endl;
};//不会改变x值

// 相当于
auto f2 = [=]() {
    int y = x;
    y++;
    std::cout << y << std::endl;
};  // 值传递并不会改变原值

auto f3 = [&x]() {
    x++;//如果时值传递,则会报错。
    std::cout << x << std::endl;
};  // 非值传递时不需要 mutable

13. 成员初始化列表

注意:在成员初始化列表里需要按成员变量定义的顺序写。这很重要,因为不管你怎么写初始化列表,它都会按照定义类的顺序进行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Entity {
private:
    int m_score;
    std::string m_name;
    
public:
    // 函数的括号后写冒号,并按定义类成员变量的顺序进行初始化
    Entity()
        : m_score(0), m_name("Unknown") {}
    
    Entity(const std::string& name, int score)//重载版本
        : m_score(score), m_name(name) {}
};
int main()
{
    Entity e0;
    Entity e1("lk",50);
    std::cout << e0.GetName() <<e0.GetScore() << std::endl;
    std::cout << e1.GetName() <<e1.GetScore()<<std::endl;
}

14. 三元运算符

尽量不对三元操作符进行嵌套。

1
2
// 待赋值量 = 条件 ? 条件成立取值 : 条件不成立取值
s_Speed = (s_Speed == 1) ? 2 : 3;

15. 创建并初始化 C++ 对象

一般使用栈分配。 但如果创建的对象太大,或是需要显式地控制对象的生存期,那就需要堆上创建堆分配,但要注意显式销毁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
    // 栈上分配空间,栈通常较小
    // 当需要分配大量空间时应在堆上分配
    // 且栈上内存在离开作用域时会被销毁,堆上不会
    Entity entity("Hello World");//栈分配
    std::cout << entity.getName() << std::endl;
}

Entity* entity_ptr;

{
    // 在堆上分配会花费更多时间
    Entity* entity = new Entity("Hello World");//堆分配
    entity_ptr = entity;
}
//离开作用域 entity 指针销毁,但堆对象还在,entity_ptr 仍指向它
delete entity_ptr;

16. NEW

和指针一起使用,new 返回的是地址,new 后跟的是分配内存的大小。
和类一起使用,不仅会分配内存,还会调用构造函数。 都必须和 delete 一起使用

1
2
3
4
5
Player* player = new Player();
Player* players = new Player[50]();

delete player;
delete[] players;

new还支持一种叫placement new的用法,此时,会调用构造函数,并在一个特定的内存地址中初始化Entity,可以通过些new()指定内存地址:

1
2
int* b = new int[50]; 
Entity* entity = new(b) Entity();//在b处初始化Entity

17. 隐式转换和 explicit

隐式转换只能进行一次

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 Entity {
private:
    std::string name;
    int age;
    
public:
    Entity(const std::string& name) : name(name), age(-1) {}
    
    // explicit 禁用隐式转换
    explicit Entity(int age) : name("unknown"), age(age) {}
    
    const std::string& getName() const { return name; }
};

void PrintEntity(const Entity& e) {
    std::cout << e.getName() << std::endl;
}

int main() {
    // 隐式转换,将 "Hello World" 转换成 Entity 类再进行赋值
    Entity entity = "Hello World";
    
    // entity = 22;  // 禁用隐式转换后报错,禁用前不报错
    
    // PrintEntity("Hello World");  // 两次隐式转换:const char* -> string -> Entity
    
    PrintEntity(std::string("helloworld"));
    PrintEntity(Entity("helloworld"));
    
    // PrintEntity(22);  // 禁用隐式转换后报错,禁用前不报错,int -> Entity的一次转换。
}

18. 运算符及其重载

运算符相当于函数。 应该相当少地使用操作符重载,只在他非常有意义的时候使用。 如果该运算符的左操作数(第一个参数)是类对象本身,并且需要访问私有成员,通常放在类内; 如果左操作数是其他类型(或需要支持左右操作数的隐式转换),通常放在类外(常声明为友元)。

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
#include <iostream>

// 在类中重载(运算符前是 Vector 类),重载函数写到类中
struct Vector {
    float x;
    float y;
    
    Vector(float x, float y) : x(x), y(y) {}
    
    // 类内重载,需要对现有流的引用
    Vector add(const Vector &other) const {
        return Vector(x + other.x, y + other.y);
    }
    
    // 运算符重载,使用 + 处理两个 Vector 时调用该函数
    Vector operator+(const Vector &other) const {
        return add(other);
    }
    
    // 也可以如下这样写
    // Vector add(const Vector &other) const {
    //     return operator+(other);
    // }
    // Vector operator+(const Vector &other) const {
    //     return Vector(x + other.x, y + other.y);
    // }
};

// 在 cout 中重载,写在类外
// 在类外写需要对现有流的引用 std::ostream& stream
std::ostream& operator<<(std::ostream &ostream, const Vector &other) {
    ostream << "(" << other.x << ", " << other.y << ")";
    return ostream;  // 返回流
}

int main() {
    Vector v1(1, 2);
    Vector v2(2, 3);
    Vector v3 = v1 + v2;  // 调用 Vector operator+()
    std::cout << v3 << std::endl;  // 调用 operator<<()
}

19. this

通过 this 可以访问成员函数,在成员函数中使用 this 表示指向当前实例的指针。 this在一个const函数中,this是一个const Entity* const,在一个非const函数中,那么它就是一个Entity* const类型的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Entity;

void print_init(Entity* e);

class Entity {
public:
    int x, y;
    
    Entity(int x, int y) {
        this->x = x;  // 使用 this 区分成员变量和参数
        this->y = y;
        print_init(this);  // 使用 this 表示指向当前实例的指针
    }
};

void print_init(Entity* e) {
    std::cout << e->x << " " << e->y << std::endl;
}

在非const函数里通过解引用this,我们就可赋值给Entity&,如果是在const方法中,我们会得到一个const引用。

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
void PrintEntity(const Entity& e);  // 参数是引用,引用和取地址不同

class Entity {
public:
    int x, y;
    Entity(int x, int y) {
        // 1. this 是指针,用 -> 访问成员
        this->x = x;
        this->y = y;

        // 2. 解引用得到对象,绑定到非 const 引用
        Entity& ref = *this;      // 合法,ref 是当前对象的别名
        ref.x = 100;              // 修改原对象

        // 3. 传递给需要引用的函数
        PrintEntity(*this);       // 解引用后传递对象(隐式转为 const 引用)是一种 权限缩小(放弃修改能力),始终安全
    }

    int GetX() const {
        // 在 const 成员中,*this 是 const Entity&
        const Entity& ref = *this;  // 只能绑定到 const 引用
        // ref.x = 200;             // 错误,不能修改
        return ref.x;
    }
};

20. 栈的作用域的应用

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
class Entity {
private:
    std::string name;
    
public:
    Entity(std::string name) : name(name) {
        std::cout << "Entity created" << std::endl;
    }
    
    ~Entity() {
        std::cout << "Entity destroyed" << std::endl;
    }
};

// 应用 1:基本作用域指针,自动销毁堆上内存
class ScopedPtr {
private:
    Entity* m_ptr;
    
public:
    ScopedPtr(Entity* ptr) : m_ptr(ptr) {}
    
    ~ScopedPtr() {
        delete m_ptr;
    }
};

int main() {
    {
        ScopedPtr e = new Entity("Hello World");
    }
    std::cout << "Hello World!\n";
}

21. 智能指针

优先使用unique_ptr,其次考虑shared_ptr。 尽量使用unique_ptr因为它有一个较低的开销,但如果你需要在对象之间共享,不能使用unique_ptr的时候,就使用shared_ptr。

作用域指针unique_ptr的使用

unique_ptr是作用域指针,意味着超出作用域时,它会被销毁然后调用delete。 unique_ptr是唯一的,不可复制,不可分享。 如果复制一个unique_ptr,会有两个指针,两个unique_ptr指向同一个内存块,如果其中一个死了,它会释放那段内存,也就是说,指向同一块内存的第二个unique_ptr指向了已经被释放的内存。 unique_ptr构造函数实际上是explicit的,没有构造函数的隐式转换,需要显式调用构造函数。 最好使用std::unique_ptr entity = std::make_unique(); 因为如果构造函数碰巧抛出异常,不会得到一个没有引用的悬空指针从而造成内存泄露,它会稍微安全一些。

共享指针shared_ptr

引用计数基本上是一种方法,可以跟踪你的指针有多少个引用,一旦引用计数达到零,他就被删除了。 例如:我创建了一个共享指针shared_ptr,我又创建了另一个shared_ptr来复制它,我的引用计数是2,第一个和第二个,共2个。当第一个死的时候,我的引用计数器现在减少1,然后当最后一个shared_ptr死了,我的引用计数回到零,内存就被释放。 shared_ptr需要分配另一块内存,叫做控制块,用来存储引用计数,如果您首先创建一个new Entity,然后将其传递给shared_ptr构造函数,它必须分配,做2次内存分配。先做一次new Entity的分配,然后是shared_ptr的控制内存块的分配。然而如果你用make_shared你能把它们组合起来,这样更有效率。

弱指针weak_ptr

可以和共享指针shared_ptr一起使用。 weak_ptr可以被复制,但是同时不会增加额外的控制块来控制计数,仅仅声明这个指针还活着。 场景:

  1. 双向链表、树、图结构中破解循环引用,防止内存泄漏
  2. 解决“观察者模式”中的悬挂指针问题
  3. 为共享资源实现“缓存”且不阻止销毁
  4. 多线程任务中安全地访问共享对象

    示例

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
#include <memory>
class Entity {
public:
    Entity() {
        std::cout << "Entity created" << std::endl;
    }
    
    ~Entity() {
        std::cout << "Entity destroyed" << std::endl;
    }
    
    void update() {
        std::cout << "Entity updated" << std::endl;
    }
};

int main() {
    // 优先使用 unique_ptr,需要共享时再使用 shared_ptr
    std::shared_ptr<Entity> e0;
    
    {
        // unique_ptr 作用域指针,超出作用域时自动销毁,不能复制 unique_ptr
        std::unique_ptr<Entity> entity = std::make_unique<Entity>();
        // 也可使用std::unique_ptr<Entity> entity(new Entity());但更推荐make_unique方法。
        
        // std::unique_ptr<Entity> entity2 = entity;  // 拷贝构造函数和操作符被删除,故无法复制
        
        // std::unique_ptr<Entity> entity = new Entity();  // 有 explicit,只能显式定义
        
        entity->update();
    }
    
    {
        // shared_ptr 共享指针,使用引用计数,跟踪指针有多少个引用,引用数变 0 时被删除
        std::shared_ptr<Entity> entity = std::make_shared<Entity>();
        // std::shared_ptr<Entity> sharedEntity = sharedEntity(new Entity());//不推荐!
        e0 = entity;
        
        // 弱指针,将共享指针赋给弱指针时不会增加计数
        std::weak_ptr<Entity> weak = entity;
    }
    
    std::cout << "Hello World!\n";
}

22. 复制与拷贝构造函数

默认的拷贝构造函数只能执行浅拷贝(对于指针会出现两个指针指向同一个地址的情况——简单的直接赋值),要自己写拷贝构造函数,将指针指向内存里的值也进行复制(深拷贝)。 而当变量不包含任何指针或引用时,两者没有区别。 当变量包含指针或引用时,浅拷贝会导致两个指针指向同一个内存,一个对象修改,另一个对象的值也被更改了. 当在析构的时候,会发生两次free (double free)同一个内存,造成错误。

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
class String {
private:
    char* m_Buffer;  // 指向字符串缓冲区
    unsigned int m_Size;
    
public:
    String(const char* string) {
        m_Size = strlen(string);
        m_Buffer = new char[m_Size];
        memcpy(m_Buffer, string, m_Size);
    }
    
    // 拷贝构造函数(深拷贝)
    String(const String& other)
        : m_Size(other.m_Size) {
        std::cout << "Copy constructor" << std::endl;
        m_Buffer = new char[m_Size];
        memcpy(m_Buffer, other.m_Buffer, m_Size);
    }
    //不使用拷贝函数,禁止赋值
    // String(const String& other) = delete;
    ~String() {
        // 浅拷贝时同一个内存会被释放两次,导致错误
        delete[] m_Buffer;
    }
    
    // 友元函数,允许访问私有成员
    friend std::ostream& operator<<(std::ostream& os, const String& string);
};

std::ostream& operator<<(std::ostream& os, const String& string) {
    os << string.m_Buffer;
    return os;
}

void Print(const String& string) {
    std::cout << string << std::endl;
}

int main() {
    String s1 = "Hello, World!";
    String s2 = s1;  // 调用拷贝构造函数
    
    Print(s1);
    Print(s2);
    
    std::cout << s1 << std::endl;
    std::cout << s2 << std::endl;
}

23. 箭头操作符

箭头操作符 -> 用于通过指针访问对象的成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Entity {
public:
    void print() const {
        std::cout << "Entity" << std::endl;
    }
};

int main() {
    Entity* ptr = new Entity();
    
    // 两种方式等价
    (*ptr).print();  // 先解引用再调用
    ptr->print();    // 使用箭头操作符更简洁
    
    delete ptr;
}

重载箭头操作符: 一般将箭头运算符定义成了const成员,这是因为与递增和递减运算符不一样,获取一个元素并不会改变类对象的状态。

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
class ScopedPtr {
private:
    Entity* m_ptr;
    
public:
    ScopedPtr(Entity* ptr) : m_ptr(ptr) {}
    
    ~ScopedPtr() {
        delete m_ptr;
    }
    
    // 重载箭头操作符
    Entity* operator->() {
        return m_ptr;
    }
    
    // 重载解引用操作符
    Entity& operator*() {
        return *m_ptr;
    }
};

int main() {
    ScopedPtr entity(new Entity());
    entity->print();  // 调用 operator->()
    (*entity).print(); // 调用 operator*()
}

const版本

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
#include <iostream>
class Entity
{
private:
    int x;
public:
    void Print() const   //添加const
    {
        std::cout << "hello!" << std::endl;
    }
};

class ScopedPtr
{
private:
    Entity* m_Ptr;
public:
    ScopedPtr(Entity* ptr)
        : m_Ptr(ptr)
    {
    }
    ~ScopedPtr()
    {
        delete m_Ptr;
    }
    Entity* operator->()
    {
        return m_Ptr;
    }
    const Entity* operator->() const //添加const
    {
        return m_Ptr;
    }
};

int main()
{
    {
        const ScopedPtr entity = new Entity(); 
        entity->Print();
    }
    std::cin.get();
}

C++的动态数组(std::vector)

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <vector>
std::vector<T> a;//T是一种模板类型,尽量使用对象而非指针
a.push_back(element); // 后面插入
    //定义一个类
    struct Vertex
    {
        float x, y, x;
    }

    std::vector<Vertex> vertices; //定义一个Vertex类型的动态数组
    vertices.push_back({ 1, 2, 3 });//列表初始化(结构体或者类,可以按成员声明的顺序用列表构造)
    vertices.push_back({ 4, 5, 6 });//同:vertices.push_back(Vertex(4, 5, 6)
    vertices.push_back({ 7, 8, 9 });

for(Vertex& v : vertices)  //引用遍历,避免复制浪费。
{
    std::cout << v << std::endl;
}

vertices.erase(vertices.begin()+1) //参数是迭代器类型,清除第二个元素

vertices.clear();//清除数组列表

void Function(const std::vector<T>& vec){};//参数传递时,如果不对数组进行修改,请使用常量引用类型传参。

C++的std::vector使用优化

  1. 当向vector数组中添加新元素时,为了扩充容量,当前的vector的内容会从内存中的旧位置复制到内存中的新位置(产生一次复制),然后删除旧位置的内存。一般每次新分配空间是原空间的2倍,可能会随编译器和c++版本变化。解决办法: vertices.reserve(n) ,直接指定容量大小,避免重复分配产生的复制浪费。
  2. 在push_back() 向容器尾部添加元素时,首先会在栈上创建一个临时元素对象(不在已经分配好内存的vector,即堆上),然后再将这个对象拷贝或者移动到【我们真正想添加元素的容器】中,再销毁临时对象。这其中,就造成了一次复制浪费。 解决办法: emplace_back,直接在容器尾部创建元素,即直接在已经分配好内存的那个容器中直接添加元素,不创建临时对象。
  3. reserve提前申请内存,避免动态申请开销 emplace_back直接在容器尾部创建元素,省略拷贝或移动过程。

示例

下面代码会产生六次复制。

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
#include <iostream>
#include <vector>

struct Vertex
{
    float x, y, z;

    Vertex(float x, float y, float z)
        : x(x), y(y), z(z)
    {
    }

    Vertex(const Vertex& vertex)
        : x(vertex.x), y(vertex.y), z(vertex.z)
    {
        std::cout << "Copied!" << std::endl;
    }
};

int main()
{
    std::vector<Vertex> vertices;
    //第一次push_back在栈上调用一次构造函数,再调用一次拷贝函数将栈上对象拷贝到堆上。
    vertices.push_back(Vertex(1, 2, 3 )); //同vertices.push_back({ 1, 2, 3 });
    //第二次push_back,先在在栈上调用一次构造函数,空间不足,将vertices现有的一个Vertex对象复制到新内存,拷贝一次,再调用一次拷贝函数将栈上对象拷贝到新内存后面。
    vertices.push_back(Vertex(4, 5, 6 ));
    //同上,一次临时对象构造,2次已有对象拷贝和一次临时对象拷贝。
    vertices.push_back(Vertex(7, 8, 9 ));

    std::cin.get();
}

不同方法的拷贝次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int main()
{   
    std::vector<Vertex> vertices;
    //ver 1 : copy 6 times
    vertices.push_back({ 1,2,3 });
    vertices.push_back({ 4,5,6 });
    vertices.push_back({ 7,8,9 });

    //ver 2 : copy 3 times
    vertices.reserve(3);
    vertices.push_back({ 1,2,3 });
    vertices.push_back({ 4,5,6 });
    vertices.push_back({ 7,8,9 });

    //ver 3 : copy 0 times
    vertices.reserve(3);
    vertices.emplace_back(1, 2, 3);
    vertices.emplace_back(4, 5, 6);
    vertices.emplace_back(7, 8, 9);

    std::cin.get();
}

C++中如何处理多返回值

通过函数参数传引用或指针的方式

把函数定义成void,然后通过参数引用传递的形式“返回”两个字符串,这个实际上是修改了目标值,而不是返回值,但某种意义上它确实是返回了两个字符串,而且没有复制操作,技术上可以说是很好的。但这样做会使得函数的形参太多了,可读性降低,有利有弊 。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
void GetUserAge(const std::string& user_name,bool& work_status,int& age)
{
    if (user_name.compare("xiaoli") == 0)
    {
        work_status = true;
        age = 18;
    }
    else
    {
        work_status = false;
        age = -1;
    }
}

int main()
{
    bool work_status = false;
    int age = -1;
    GetUserAge("xiaoli", work_status, age);
    std::cout << "查询结果:" << work_status << "    " << "年龄:" << age << std::endl;
    getchar();
    return 0;
}

通过函数的返回值是一个array(数组)或vector

Array是在栈上创建,更好,而vector会把它的底层储存在堆上,都只适用于相同类型的多种数据的返回。 可以通过模版函数template<size_t M>创建可变长的Array返回值。

示例

固定大小数组的返回:

1
2
3
4
5
6
7
8
9
10
11
12
//设置是array的类型是stirng,大小是2
std::array<std::string, 2> ChangeString() {
    std::string a = "1";
    std::string b = "2";

    std::array<std::string, 2> result;
    result[0] = a;
    result[1] = b;
    return result;

    //也可以return std::array<std::string, 2>(a, b);
}

使用std::pair返回两个返回值

可以返回两个不同类型的数据,只能通过first这样的方法访问,可读性较差。

示例

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
#include <iostream>

std::pair<bool, int> GetUserAge(const std::string& user_name)
{
    std::pair<bool, int> result;

    if (user_name.compare("xiaoli") == 0)
    {
        result = std::make_pair(true, 18);
    }
    else
    {
        result = std::make_pair(false, -1);
    }

    return result;
}

int main()
{
    std::pair<bool, int> result = GetUserAge("xiaolili");
    //也可用std::tie解包。
    std::cout << "查询结果:" << result.first << "   " << "年龄:" << result.second << std::endl;
    getchar();
    return 0;
}

使用std::tuple返回三个或者三个以上返回值

可以将三个或者三个以上的异构成员绑定在一起,

示例

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
#include <iostream>
#include <tuple>

std::tuple<bool, int,int> GetUserAge(const std::string& user_name)
{
    std::tuple<bool, int,int> result;

    if (user_name.compare("xiaoli") == 0)
    {
        result = std::make_tuple(true, 18,0);
    }
    else
    {
        result = std::make_tuple(false, -1,-1);
    }

    return result;
}

int main()
{
    std::tuple<bool, int,int> result = GetUserAge("xiaolili");

    bool work_status;
    int age;
    int user_id;

    std::tie(work_status, age, user_id) = result;//解包元组
    std::cout << "查询结果:" << work_status << "    " << "年龄:" << age <<"   "<<"用户id:"<<user_id <<std::endl;
    getchar();
    return 0;
}

返回一个结构体(推荐)

结构体是在栈上建立的,可读性更佳。

示例

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
#include <iostream>
struct result {
    std::string str;
    int val;
};
result Function () {
    return {"1", 1};//C++新特性,可以直接这样子让函数自动补全成结构体
}
int main() {
    auto temp = Function();
    std::cout << temp.str << ' ' << temp.val << std::endl;
}
--------------------------------------------
#include <iostream>
using namespace std;

struct Result
{
    int add;
    int sub;
};

Result operation(int a, int b)
{
    Result ret;
    ret.add = a + b;
    ret.sub = a - b;
    return ret;
}

int main()
{
    Result res;
    res = operation(5, 3);
    cout << "5+3=" << res.add << endl;
    cout << "5-3=" << res.sub << endl;
}

C++的结构化绑定

函数返回为tuple、pair、struct等时且赋值给另外变量的时候,直接得到成员

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <string>
#include <tuple>

std::tuple<std::string, int> CreatPerson() 
{
    return {"Cherno", 24};
}

int main()
{
    auto[name, age] = CreatPerson(); //直接用name和age来储存返回值
    std::cout << name;
}

C++的模板

让编译器基于DIY的规则去为你写代码。

函数的模板

当实际调用时,模板函数才会基于传递的参数来真的创建。 格式: template<typename T>+具体函数。 typename也可换为

  1. 非类型模板参数(整数、枚举、指针、引用等编译期常量),如int ,size_t 。
  2. auto,自动推导类型
  3. typename...,接收任意数量的模板参数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    #include<iostream>
    template<typename T> void Print(T temp) {
     //把类型改成模板类型的名字如T就可以了
     std::cout << temp;
    }
    //干净简洁
    int main() {
     Print(1);
     Print("hello");
     Print(5.5);
     Print(96);//这里其实是隐式的传递信息给模板,可读性不高
     Print<int>(96);//可以显示的定义模板参数,声明函数接受的形参的类型
     Print<char>(96);//输出的可以是数字,也可以是字符!这样的操纵性强了很多
    }
    

    类的模板

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    //可以传类型,也可以传数字
    //两个模板参数:类型和大小
    template<typename T, int size> class Array {
    private:
     T m_Array[size];
    };
    int main() {
     Array<int, 5> array;
    }
    

模板特例化

分为全特化和偏特化,在库开发、泛型算法、类型萃取中频繁使用。

  1. 为特定类型提供更优算法(如 std::sort 对小数组改用插入排序)。
  2. 禁用某些不安全类型(如 std::unique_ptr 对数组类型的偏特化)。
  3. 通过特化实现 if constexpr 出现前的条件编译。

    注意:优先考虑重载,不要滥用特化。

C++ 堆与栈内存的比较

基本概念

特性栈(Stack)堆(Heap)
管理方式编译器自动分配和释放程序员手动分配(new/deletemalloc/free
现代 C++ 可用智能指针辅助管理
分配时机函数调用时,为局部变量、函数参数等分配空间运行时通过指令动态申请
生命周期随函数栈帧的创建而创建,随函数返回而自动销毁new 分配开始,直到对应的 delete 释放(或程序结束)
空间大小通常较小(几 MB,可编译期设定)较大(受系统虚拟内存限制,可达数 GB)
分配速度极快:仅移动栈顶指针较慢:需要查找空闲内存块、处理空闲链表、可能涉及系统调用
内存碎片无(后进先出,连续分配)可能产生外部碎片(频繁分配/释放不同大小对象,不连续)
线程安全每个线程有自己的栈,天然线程安全堆是所有线程共享的,多线程下需要同步(锁/原子操作)
存储内容局部变量、函数参数、返回地址、临时对象等动态创建的对象、大块数据、需跨函数生存的数据
溢出风险栈溢出:递归过深或超大局部数组堆溢出:申请超出可用内存(较少见,一般由 OOM 引发)
典型错误缓冲区溢出导致栈损坏内存泄漏(忘记 delete)、悬垂指针(已释放内存仍被访问)

注意事项与最佳实践

  1. 尽量使用栈:如果变量生命周期小且尺寸不大,优先栈分配,避免手动内存管理带来的复杂性和错误。
  2. 堆对象务必释放:使用 delete / delete[],或更推荐 RAII 容器(std::vectorstd::stringstd::unique_ptrstd::shared_ptr)自动管理。
  3. 避免在栈上分配过大对象:例如 char huge[10 * 1024 * 1024]; 极易栈溢出,改用 std::vector<char>
  4. 注意拷贝开销:返回大型栈对象可能涉及拷贝(虽有 RVO/NRVO),但返回堆对象(智能指针)只需移动指针。
  5. 线程安全:多线程程序中对堆的访问需要同步(如使用互斥锁保护共享堆数据);栈是线程私有的,无需加锁。
  6. 内存碎片:长期运行的服务器程序,频繁分配/释放不同大小的堆对象可能导致碎片,可考虑内存池或自定义分配器。

总结

  • :小而快,自动管理,适合生命周期确定的小对象。
  • :大而慢,手动管理(或 RAII),适合大对象或跨函数存活的对象。

理解这两者的本质,有助于写出更安全、高效的 C++ 代码。在实际开发中,应优先使用栈和标准容器(它们内部使用堆)。

C++的宏

宏在预处理阶段执行。 用于将代码中的文本替换为其他东西(纯文本替换)(不一定是简单的替换,是可以自定义调用宏的方式的) 常用于辅助调试,日志系统。

辅助调试

1
2
3
4
5
6
#defind PR_DEBUG 1 //可以在这里切换成0,作为一个开关
#if PR_DEBUG == 1   //如果PR_DEBUG为1
#defind LOG(x) std::cout << x << std::endl  //则执行这个宏
#else   //反之
#defind LOG(x)   //这个宏什么也不定义,即是无意义
#endif    //结束

C++的auto关键字

迭代器场景

1
2
3
4
5
6
7
8
9
10
11
12
std::vector<std::string> strings;
strings.push_back("Apple");
strings.push_back("Orange");
for (std::vector<std::string>::iterator it = strings.begin(); //不使用auto
    it != strings.end(); it++)
{
    std::cout << *it << std::endl;
}
for (auto it = strings.begin(); it != strings.end(); it++) //使用auto
{
    std::cout << *it << std::endl;
}

类型名过长

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
#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>

class Device{};

class DeviceManager
{
private:
    std::unordered_map<std::string, std::vector<Device *>> m_Devices;
public:
    const std::unordered_map<std::string, std::vector<Device *>> &GetDevices() const
    {
        return m_Devices;
    }
};

int main()
{
    DeviceManager dm;
    const std::unordered_map<std::string, std::vector<Device *>> &devices = dm.GetDevices();//不使用auto
    const auto& devices = dm.GetDevices(); //使用auto

    std::cin.get();
}

也可用using或typedef方法

1
2
3
4
using DeviceMap = std::unordered_map<std::string, std::vector<Device*>>;
typedef std::unordered_map<std::string, std::vector<Device*>> DeviceMap;

const DeviceMap& devices = dm.GetDevices();

C++的静态数组std::array

静态的是指不增长的数组,当创建array时就要初始化其大小,不可再改变。 用std::array的好处是可以访问它的大小(通过size()函数),它是一个类,且有越界检查,分配在栈上。 使用模版解决:传入一个标准数组作为参数,但不知道数组的大小的问题。

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
#include <iostream>
#include <array>

template <typename T>
void printarray(const T &data)
{
    for (int i = 0; i < data.size(); i++)
    {
        std::cout << data[i] << std::endl;
    }
}

template <typename T, unsigned long N> // or // template <typename T, size_t N>
void printarray2(const std::array<T, N> &data)
{
    for (int i = 0; i < N; i++)
    {
        std::cout << data[i] << std::endl;
    }
}

int main()
{
    std::array<int, 5> data;
    data[0] = 2;
    data[4] = 1;
    printarray(data);
    printarray2(data);
}

C语言风格的函数指针

无参数的函数指针

1
2
3
4
5
6
7
8
9
10
void Print() {
    std::cout << "hello,world" << std::endl;
}
int main() {
    //void(*function)() = Print; 正常写法,但一般用auto就可以了
    auto function = Print();    //ERROR!,auto无法识别void类型
    auto function = Print;  //OK!,去掉括号就不是在调用这个函数,而是在获取函数指针,得到了这个函数的地址。就像是带了&取地址符号一样"auto function = &Print;""(隐式转换)。
    function();//调用函数
    //这里函数指针其实也用到了解引用(*),这里是发生了隐式的转化,使得代码看起来更加简洁明了!
}

有参数的函数指针

1
2
3
4
5
6
7
8
void Print(int a) {
    std::cout << a << std::endl;
}
int main() {
    auto temp = Print;  //正常应该是 void(*temp)(int) = Print,太过于麻烦,用auto即可
    //也可以用typedef或者using来使用函数指针,见auto章节
    temp(1);    //在用函数指针的时候也传参数进去就可以正常使用了
}

实际使用场景

将一个函数作为另一个函数的形参。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void Print(int val) {
    std::cout << val << std::endl;
}

//下面就将一个函数作为形参传入另一个函数里了
void ForEach(const std::vector<int>& values, void(*function)(int)) {
    for (int temp : values) {
        function(temp); //就可以在当前函数里用其他函数了
    }
}

int main() {
    std::vector<int> valus = { 1, 2, 3, 4, 5 };
    ForEach(values, Print); //这里就是传入了一个函数指针进去!!!!
}

C++的lambda

lambda本质上是一个匿名函数,在过程中生成的,用完即弃的函数。

场景

在我们会设置函数指针指向函数的任何地方,我们都可以将它设置为lambda

使用

[捕获列表](参数列表) { 函数体 }

中括号表示的是捕获,作用是如何传递变量 lambda使用外部(相对)的变量时,就要使用捕获。 [=],则是将所有变量值传递到lambda中 [&],则是将所有变量引用传递到lambda中 [a]是将变量a通过值传递,如果是[&a]就是将变量a引用传递 它可以有0个或者多个捕获

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
#include <iostream>
#include <vector>
#include <functional>
void ForEach(const std::vector<int>& values, void(*function)(int)) {
    for (int temp : values) {
        function(temp);     //正常调用lambda函数
    }
}

int main() {
    std::vector<int> valus = { 1, 2, 3, 4, 5 };
    //函数指针的地方都可以用auto来简化操作,lambda亦是
    //这样子来定义lambda表达式会更加清晰明了
    auto lambda = [](int val){ std::cout << val << std::endl; }
    ForEach(values, lambda);    
}
-------------------------------------------------
//lambda可以使用外部(相对)的变量,而[]就是表示打算如何传递变量
#include <functional>   //要用捕获就必须要用C++新的函数指针!
//新的函数指针的签名有所不同!
void ForEach(const std::vector<int>& values, const std::function<void(int)>& func) {
    for (int temp : values) {
        func(temp);     
    }
}

int main() {
    std::vector<int> valus = { 1, 2, 3, 4, 5 };
    //注意这里的捕获必须要和C++新带的函数指针关联起来!!!
    int a = 5;  //如果lambda需要外部的a向量
    //则在捕获中写入a就好了
    auto lambda = [a](int val){ std::cout << a << std::endl; }
    //auto lambda = [=](int value) mutable { a = 5; std::cout << "Value: " << value << a << std::endl; };值捕获要修改捕获的值时添加mutable,实际是新变量赋值的语法糖

    ForEach(values, lambda);    
}

find_if场景

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

int main() {
    std::vector<int> values = { 1, 5, 3, 4, 2 };
    //下面就用到了lambda作为函数指针构成了find_it的规则
    auto it = std::find_if(values.begin(), values.end(), [](int value) { return value > 3; });  //返回第一个大于3的元素的迭代器 
    std::cout << *it << std::endl;  //将其输出
}

C++的命名空间

  1. C++独有,C没有.
  2. 希望能够在不同的上下文中调用相同的符号,避免命名冲突时使用。
  3. 每个命名空间是一个作用域.
  4. 命名空间可以不连续
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <string>
#include <algorithm>
namespace apple {
    void print(const char *text) {
        std::cout << text << std::endl;
    }
}

namespace orange {
    void print(const char *text) {
        std::string temp = text;
        std::reverse(temp);
        std::cout << temp << std::endl;
    }
}
int main() {
    //using namespace apple::print; //单独引出一个print函数
    //using namespace apple;//引出apple名称空间的所有成员

    apple::print("hello");  //输出正常text
    orange::print("world"); //输出反转的text
}

模板特例化

模板特例化的完整定义(包括函数体或类成员)必须出现在原始模板所属的命名空间中,但允许你先在该命名空间内只放一个声明,然后在命名空间外部写出完整的定义。这样做类似于普通函数在头文件中声明、在源文件中定义。 比如 std::hash(一个类模板) 是定义在 namespace std 中的,那么你的特例化 std::hash 也必须在 std 命名空间内提供定义。否则编译器可能找不到它。Foo是自定义结构体,标准库无法求其哈希值。

1
2
3
4
5
6
7
8
9
10
11
12
//std::hash 是一个模板,它对于大多数类型都有一个默认的计算哈希值的方法
//为 Foo 单独提供一个 特例化版本,告诉编译器:“对于 Foo 这个类型,请用我写的这个 hash,不要用默认的”。
namespace std {
    template <>//特例化版本,不是泛型版本,所有模板参数都已经确定了(这里就是 Foo)
    struct hash<Foo> {//声明一个结构体(类)hash,它的模板参数被指定为 Foo。
    //给出了成员函数 operator() 的实现。
        size_t operator()(const Foo& f) const {
            return hash<string>()(f.str) ^ hash<double>()(f.d);//分别计算 f.str(假设是 string 类型)和 f.d(假设是 double 类型)的哈希值。用按位异或 ^ 把它们组合成一个最终的哈希值。
            //hash<string>() 会创建一个临时的 std::hash<string> 对象,然后 (f.str) 调用它的 operator() 得到字符串的哈希值。
        }
    };
}

1
2
3
4
5
6
7
8
9
10
11
// 第一步:在 std 中声明特例化(只有声明,没有定义)
namespace std {
    template <> struct hash<Foo>;
}

// 第二步:在 std 外部定义特例化(给出完整实现)
template<> struct std::hash<Foo> {
    size_t operator()(const Foo& f) const {
        return hash<string>()(f.str) ^ hash<double>()(f.d);
    }
};

全局命名空间

全局命名空间就是全局作用域。所有写在所有类、函数、其他命名空间之外的变量、函数、类型等,都属于这个隐式的命名空间。

它没有名字,但可以用 :: 前缀显式访问其中的成员,例如 ::member_name 表示“全局作用域里的 member_name”,而不是当前局部或某个命名空间中的同名标识符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

int count = 100;           // 全局变量,属于全局命名空间

void func() {
    int count = 200;       // 局部变量,隐藏了全局的 count
    std::cout << "局部 count: " << count << std::endl;      // 输出 200
    std::cout << "全局 count: " << ::count << std::endl;    // 输出 100
}

int main() {
    func();
    return 0;
}

嵌套的命名空间

1
2
3
4
5
6
7
8
namespace foo {
    namespace bar {
        class Cat { /*...*/ };
    }
}

// 调用方式
foo::bar::Cat

内联命名空间

内联命名空间可以被外层命名空间直接使用。

1
2
3
4
5
6
7
8
// inline必须出现在命名空间第一次出现的地方
inline namespace FifthEd {
    // ...
}
// 后续再打开命名空间的时候可以写inline也可以不写
namespace FifthEd {  // 隐式内联
    // ...
}

常用于库的版本管理。为了同时保留新旧两个版本,且默认使用新版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace foo {
    // 第4版的代码(非内联)
    namespace FourthEd {
        class Cat { /* 旧接口 */ };
    }

    // 第5版的代码(内联)
    inline namespace FifthEd {
        class Cat { /* 新接口 */ };
    }
}

// foo::Cat会调用第5版的代码(内联),而foo::FourthEd::Cat会调用第4版的代码(非内联)。

未命名的命名空间

特性一:静态生命周期(Static Lifetime)

总结
在未命名命名空间中定义的变量具有静态生命周期(static lifetime)。这意味着它们在程序启动时(或第一次使用前)被创建,并一直存活到程序结束。这与全局变量类似,但仅限于当前文件内部。

详细说明

  • 静态生命周期对象的初始化在 main 函数执行之前完成(对于静态初始化和常量初始化),动态初始化也在 main 之前或第一次使用前(如果涉及线程局部等,但普通静态变量在 main 前完成)。
  • 这种变量在内存的静态存储区分配,不会被栈管理。
  • 即使定义在函数内部,也未尝不可,但未命名命名空间通常定义在全局作用域,其中的变量在整个程序运行期间保持状态。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// file1.cpp
#include <iostream>

namespace {
    int counter = 0;      // 静态生命周期,程序开始时初始化为0
    std::string msg = "Hello"; // 同样在main之前构造
}

void increment() {
    ++counter;
}

void printCounter() {
    std::cout << "Counter: " << counter << std::endl;
}

int main() {
    printCounter();   // 输出 Counter: 0
    increment();
    printCounter();   // 输出 Counter: 1
    // 即使 main 返回后,counter 仍存在,但已无法访问(作用域结束)
    return 0;
}

说明:countermain 之前已被创建并初始化为 0,它的值在整个程序执行期间保持不变(除非被修改),程序结束时销毁。


特性二:每个文件有独立的未命名命名空间,实体互不干扰

总结
每个 .cpp 文件(翻译单元)定义自己的未命名命名空间。即使两个文件中定义了同名的变量或函数,它们也代表不同实体,互不影响。
如果未命名命名空间定义在头文件中,则每个包含该头文件的 .cpp 文件都会得到该命名空间的一份独立副本。

详细说明

  • 编译器为每个翻译单元的未命名命名空间生成一个唯一的内部名称(例如 _GLOBAL__N_1)。因此不同文件中的同名实体不会发生链接冲突。
  • 这对于在多个源文件中实现内部全局状态非常有用,例如每个文件需要独立计数器或辅助函数。

示例

file1.cpp

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

namespace {
    int value = 100;
    void show() { std::cout << "file1: value = " << value << std::endl; }
}

void testFile1() {
    show();
}

file2.cpp

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

namespace {
    int value = 200;   // 与 file1 中的 value 无关
    void show() { std::cout << "file2: value = " << value << std::endl; }
}

void testFile2() {
    show();
}

main.cpp

1
2
3
4
5
6
7
8
void testFile1();
void testFile2();

int main() {
    testFile1();  // 输出 file1: value = 100
    testFile2();  // 输出 file2: value = 200
    return 0;
}

说明:两个 value 和两个 show 分别存在于各自的翻译单元,互不冲突。

头文件示例

common.h

1
2
3
namespace {
    int sharedCounter = 0;  // 每个包含此头文件的 .cpp 都有自己的 sharedCounter
}

a.cpp

1
2
3
#include "common.h"
void incA() { ++sharedCounter; }
int getA() { return sharedCounter; }

b.cpp

1
2
3
#include "common.h"
void incB() { ++sharedCounter; }
int getB() { return sharedCounter; }

main.cpp
调用 incA()incB() 后,getA()getB() 返回的值是独立的(例如各自为 1),因为 sharedCountera.cppb.cpp 中是不同变量。


特性三:仅在特定文件内部有效,不跨文件

总结
未命名命名空间中的实体具有内部链接(internal linkage),因此它们只在当前 .cpp 文件内可见,无法在其他文件中通过任何方式访问。这与普通命名空间(外部链接)形成对比。

详细说明

  • 即使你在另一个文件中使用 extern 声明试图引用未命名命名空间的变量,也会导致链接错误(未定义符号),因为该符号只在定义它的文件中存在。
  • 这种特性使得未命名命名空间成为实现“文件私有”封装的最佳工具。

示例

fileA.cpp

1
2
3
namespace {
    int secret = 42;
}

fileB.cpp

1
2
3
4
5
extern int secret;   // 试图引用 fileA 中的 secret
int main() {
    int x = secret;  // 链接错误:secret 未定义
    return 0;
}

编译时会报链接错误,因为 secretfileB.cpp 中不可见。


特性四:作用域与外围作用域相同,需避免与全局名字冲突

总结
未命名命名空间中定义的名字的作用域与包含该命名空间的作用域相同。如果未命名命名空间定义在文件最外层(全局作用域),其中的名字就如同全局名字一样可以直接使用,但它们必须与同一文件中的其他全局名字(包括其他未命名命名空间中的名字)不重复,否则会产生重定义错误。

详细说明

  • 因为未命名命名空间不引入新的作用域层次,它相当于把内部的名字“提升”到外围作用域。
  • 如果外围作用域已经存在同名实体,则发生冲突。
  • 解决方法是:要么避免定义同名全局实体,要么将全局实体放入具名命名空间,或者改用 static 关键字(但 static 已不推荐)。

示例

1
2
3
4
5
int i = 10;           // 全局变量

namespace {
    int i = 20;       // 错误:重复定义 i
}

修正方案:

1
2
3
4
5
6
7
8
9
10
11
// 方案1:只保留一个
int i = 10;
// 或者只保留未命名命名空间中的 i

// 方案2:将全局 i 放入具名命名空间
namespace Global {
    int i = 10;
}
namespace {
    int i = 20;       // 现在与 Global::i 不冲突
}

特性五:取代文件中的静态声明(替代 static

总结
在 C++ 早期,为了实现文件内部可见性(内部链接),程序员使用 static 关键字修饰全局变量或函数。C++ 标准现在推荐使用未命名命名空间代替 static,因为它能更统一地处理所有类型(包括类、结构体等),并且语义更清晰。

详细说明

  • static 全局变量/函数在 C 和旧 C++ 中用于限制作用域为当前文件。
  • 未命名命名空间可以达到相同效果,且允许将整个类型定义(class、struct)以及模板等放入内部链接范围。
  • 从 C++11 起,使用未命名命名空间是官方推荐的做法,而文件静态声明已被弃用(但在实践中仍可能遇到)。

示例对比

旧方式(不推荐)

1
2
3
// file.cpp
static int helper() { return 42; }
static int data = 100;

新方式(推荐)

1
2
3
4
5
// file.cpp
namespace {
    int helper() { return 42; }
    int data = 100;
}

两者效果相同:helperdata 仅在当前文件内可见。

优势:未命名命名空间可以包含整个类的定义,而 static 不能直接修饰类(只能修饰成员函数或变量)。例如:

1
2
3
4
5
6
7
8
namespace {
    class InternalClass {
    public:
        void method() { /* ... */ }
    };
    InternalClass obj;   // 文件内可使用
}
// 其他文件无法看到 InternalClass 或 obj

如果用 static,则无法直接让整个类具有内部链接,只能单独静态化对象。


完整总结表格

特性说明示例要点
静态生命周期变量在程序启动时创建,结束时销毁namespace { int counter; } counter 在 main 之前初始化
每文件独立不同文件的未命名命名空间完全无关两个 .cpp 中各有一个 int value;,互不影响
仅文件内有效内部链接,其他文件不可见在 fileB.cpp 中用 extern 引用 fileA.cpp 中的未命名变量 → 链接错误
作用域同外围名字直接暴露在外围作用域,避免冲突全局有 int i; 则未命名空间不能再有 int i;
替代 static取代文件内静态声明,功能更强static int f();namespace { int f(); }

C++的线程

参考菜鸟教程 - C++多线程 多线程对于加速程序是十分有用的,线程的主要目的就是优化。 线程 (Thread):

  1. 线程是程序执行中的单一顺序控制流,多个线程可以在同一个进程中独立运行。
  2. 线程共享进程的地址空间、文件描述符、堆和全局变量等资源,但每个线程有自己的栈、寄存器和程序计数器。

    并发:多个任务在时间片段内交替执行,表现出同时进行的效果。 并行:多个任务在多个处理器或处理器核上同时执行。 主要组件:

  3. std::thread:用于创建和管理线程。
  4. std::mutex:用于线程之间的互斥,防止多个线程同时访问共享资源。
  5. std::lock_guard 和 std::unique_lock:用于管理锁的获取和释放。
  6. std::condition_variable:用于线程间的条件变量,协调线程间的等待和通知。
  7. std::future 和 std::promise:用于实现线程间的值传递和任务同步。

创建线程

std::thread thread_object(callable, args...); callable:可调用对象,可以是函数指针、函数对象、Lambda 表达式等。 args…:传递给 callable 的参数列表。

线程管理

join() 用于等待线程完成执行。如果不调用 join() 或 detach() 而直接销毁线程对象,会导致程序崩溃。 detach() 将线程与主线程分离,线程在后台独立运行,主线程不再等待它。

线程的传参

分为值传递和引用传递,引用传递需要使用std::ref()

线程同步与互斥

互斥量(Mutex)

互斥量是一种同步原语,用于防止多个线程同时访问共享资源。当一个线程需要访问共享资源时,它首先需要锁定(lock)互斥量。如果互斥量已经被其他线程锁定,那么请求锁定的线程将被阻塞,直到互斥量被解锁(unlock)。

1
2
3
4
std::mutex mtx;
mtx.lock();   // 锁定互斥锁,std::lock_guard<std::mutex> lock(mtx); // 或unique_lock自动锁定和解锁
// 访问共享资源
mtx.unlock(); // 释放互斥锁

锁(Locks)

std::lock_guard:作用域锁,当构造时自动锁定互斥量,当析构时自动解锁。 std::unique_lock:与std::lock_guard类似,但提供了更多的灵活性,例如可以转移所有权和通过unlock手动解锁。

条件变量(Condition Variable)

条件变量用于线程间的协调,允许一个或多个线程等待某个条件的发生。它通常与互斥量一起使用,以实现线程间的同步。

std::condition_variable用于实现线程间的等待和通知机制。

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

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void workerThread() {
    std::unique_lock<std::mutex> lk(mtx);
    cv.wait(lk, []{ return ready; }); // 等待条件
    // 当条件满足时执行工作
}

void mainThread() {
    {
        std::lock_guard<std::mutex> lk(mtx);
        // 准备数据
        ready = true;
    } // 离开作用域时解锁
    cv.notify_one(); // 通知一个等待的线程
}

原子操作(Atomic Operations)

原子操作要么完全执行,要么完全不执行,不会出现中间状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <atomic>
#include <thread>

std::atomic<int> count(0);

void increment() {
    count.fetch_add(1, std::memory_order_relaxed);
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    return count; // 应返回2
}

线程局部存储(Thread Local Storage, TLS)

线程局部存储允许每个线程拥有自己的数据副本。这可以通过thread_local关键字实现,避免了对共享资源的争用。

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

thread_local int threadData = 0;

void threadFunction() {
    threadData = 42; // 每个线程都有自己的threadData副本
    std::cout << "Thread data: " << threadData << std::endl;
}

int main() {
    std::thread t1(threadFunction);
    std::thread t2(threadFunction);
    t1.join();
    t2.join();
    return 0;
}

死锁(Deadlock)和避免策略

死锁发生在多个线程互相等待对方释放资源,但没有一个线程能够继续执行。避免死锁的策略包括:

总是以相同的顺序请求资源。 使用超时来尝试获取资源。 使用死锁检测算法。

线程间通信

std::future 和 std::promise:实现线程间的值传递。

1
2
3
4
5
6
7
8
std::promise<int> p;
std::future<int> f = p.get_future();

std::thread t([&p] {
    p.set_value(10); // 设置值,触发 future
});

int result = f.get(); // 获取值

示例

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <atomic>
#include <chrono>

// 模拟传感器获取距离(单位:cm)
int read_sensor() {
    static int fake_distance = 100;  // 初始较远
    // 模拟距离变化:逐渐靠近障碍物,然后远离
    static bool approaching = true;
    if (approaching) {
        fake_distance -= 5;
        if (fake_distance <= 20) approaching = false;
    } else {
        fake_distance += 10;
        if (fake_distance >= 100) approaching = true;
    }
    return fake_distance;
}

// 共享数据
struct RobotData {
    int distance;                  // 当前距离(cm)
    bool data_ready;               // 是否有新数据待处理
    std::mutex mtx;
    std::condition_variable cv;
};

// 控制指令枚举
enum class Motion {
    FORWARD,
    STOP,
    BACKWARD
};

// 传感器线程:持续读取数据并通知控制线程
void sensor_thread(RobotData& robot_data, std::atomic<bool>& running) {
    while (running) {
        int new_distance = read_sensor();
        {
            std::lock_guard<std::mutex> lock(robot_data.mtx);
            robot_data.distance = new_distance;
            robot_data.data_ready = true;
        }
        robot_data.cv.notify_one();  // 唤醒控制线程
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }
}

// 控制线程:等待新数据,决策并执行运动
void control_thread(RobotData& robot_data, std::atomic<bool>& running) {
    const int SAFE_DISTANCE = 30;   // 安全距离阈值(cm)
    Motion current_motion = Motion::FORWARD;

    while (running) {
        int distance;
        {
            std::unique_lock<std::mutex> lock(robot_data.mtx);
            // 等待数据就绪或被唤醒(防止虚假唤醒)
            robot_data.cv.wait(lock, [&robot_data] { return robot_data.data_ready; });
            distance = robot_data.distance;
            robot_data.data_ready = false;  // 重置标志
        }

        // 决策逻辑
        if (distance < SAFE_DISTANCE) {
            if (current_motion != Motion::BACKWARD) {
                std::cout << "[控制] 障碍物距离 " << distance << "cm < " << SAFE_DISTANCE
                          << "cm,立即后退!" << std::endl;
                current_motion = Motion::BACKWARD;
            }
        } else {
            if (current_motion != Motion::FORWARD) {
                std::cout << "[控制] 距离安全(" << distance << "cm),继续前进。" << std::endl;
                current_motion = Motion::FORWARD;
            }
        }

        // 执行运动(此处仅打印模拟)
        switch (current_motion) {
            case Motion::FORWARD:
                std::cout << "[执行] 前进 --- 当前距离: " << distance << "cm" << std::endl;
                break;
            case Motion::BACKWARD:
                std::cout << "[执行] 后退 --- 当前距离: " << distance << "cm" << std::endl;
                break;
            case Motion::STOP:
                std::cout << "[执行] 停止 --- 当前距离: " << distance << "cm" << std::endl;
                break;
        }
    }
}

int main() {
    std::cout << "=== 机器人超声波避障模拟(多线程)===" << std::endl;
    std::cout << "安全距离阈值: 30cm" << std::endl;
    std::cout << "按 'q' 回车退出程序。" << std::endl;

    RobotData robot_data;
    robot_data.distance = 100;
    robot_data.data_ready = false;

    std::atomic<bool> running(true);

    // 启动线程,std::thread objName (一个函数指针以及其他可选的任何参数)
    std::thread sensor(sensor_thread, std::ref(robot_data), std::ref(running));
    std::thread control(control_thread, std::ref(robot_data), std::ref(running));

    // 主线程等待用户退出指令
    char cmd;
    while (std::cin >> cmd) {
        if (cmd == 'q') {
            running = false;
            break;
        }
    }

    // 唤醒可能阻塞的控制线程,使其能检查 running 标志
    robot_data.cv.notify_all();

    // 等待线程结束,在主线程上等待 工作线程 完成所有的执行之后,再继续执行主线程
    sensor.join();
    control.join();

    std::cout << "程序已安全退出。" << std::endl;
    return 0;
}

线程结束 = 线程函数执行完毕返回。 三个线程在执行,join只是让主线程等待sensor,执行完,此时主线程暂停,而sensor 和 control还是在运行。 join() 顺序互换:对并发执行的子线程本身无影响,主线程只是换了个顺序等待,最终都等待全部结束。在正确的代码中,结果不变。

C++的计时

见可视化基准测试。

C++多维数组

在C++中处理多维数组时,最佳实践的核心目标是:内存连续性、缓存友好性、避免指针混乱、以及在现代C++中优先使用标准库容器

首选方案:一维数组 + 手动索引

对于数值计算或性能敏感的场景,最推荐的做法是用 一维std::vector 存储数据,然后通过函数计算索引。

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

class Matrix {
public:
    Matrix(size_t rows, size_t cols) : data_(rows * cols), rows_(rows), cols_(cols) {}

    double& operator()(size_t i, size_t j) {
        return data_[i * cols_ + j];
    }
    
    const double& operator()(size_t i, size_t j) const {
        return data_[i * cols_ + j];
    }

private:
    std::vector<double> data_;
    size_t rows_, cols_;
};

优点

  • 所有元素在内存中连续,遍历时缓存命中率高
  • 分配/释放简单(只有一次堆分配)
  • 与C API、BLAS/LAPACK等数值库无缝交互(可获取底层指针)

固定大小:使用 std::array 嵌套

若维度在编译期已知且较小,用 std::array<std::array<T, N2>, N1>

1
2
3
4
5
#include <array>
std::array<std::array<int, 3>, 2> grid = {{
    {1, 2, 3},
    {4, 5, 6}
}};

注意:外层array的元素(内层array)是连续存储的,整体内存仍是连续的。

现代C++备选:std::mdspan (C++23)

C++23引入了 std::mdspan,它不拥有数据,但提供多维视图:

1
2
3
4
5
6
#include <mdspan>
#include <vector>

std::vector<double> data(rows * cols);
auto view = std::mdspan(data.data(), rows, cols);
// 使用 view(i, j)

这结合了一维存储的连续性和多维访问的便利性,且零开销。

高维数组:考虑专用库

对于3维以上或需要切片、广播等操作,直接使用:

  • Eigen (线性代数)
  • Boost.MultiArray
  • xtensor (类似NumPy的C++库)

这些库内部采用连续内存 + 索引计算,并提供丰富功能。

性能关键点:遍历顺序

无论选择哪种方式,务必按行优先遍历(C++默认布局):

1
2
3
4
5
6
7
8
9
// 高效:外层循环行,内层循环列
for (size_t i = 0; i < rows; ++i)
    for (size_t j = 0; j < cols; ++j)
        process(mat(i, j));

// 低效:外层循环列,内层循环行(跳步访问)
for (size_t j = 0; j < cols; ++j)
    for (size_t i = 0; i < rows; ++i)
        process(mat(i, j));

总结决策表

场景推荐方案
编译期固定大小std::array 嵌套
运行时大小,性能敏感std::vector + 索引计算
需要便捷语法,C++23可用std::mdspan + 一维存储
3维以上或科学计算Eigen / xtensor
快速原型,维度很小普通 [][N] 静态二维数组(栈上)

最终建议:除非维度很小且固定,否则优先选择一维 std::vector + 索引函数,这是最平衡可维护性和性能的做法。

C++内置的排序函数

sort( vec.begin(), vec.end(), 谓语)

谓语可以设置排序的规则,谓语可以是内置函数,也可以是lambda表达式。 它接受两个元素,返回 bool 值,表示 a 是否应该排在 b 之前。 不保证相同大小的元素是否还是原来的顺序,std::stable_sort可以保证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<iostream>
#include<vector>
#include<algorithm>
#include<functional>

int main()
{
    std::vector<int>  values = {3, 5, 1, 4, 2};
    std::sort(values.begin(), values.end()); //默认是从小到大排序
    std::sort(values.begin(), values.end(),std::greater<int>());//std::greater内置函数,会按照从大到小顺序排列,需要添加头文件functional。
    std::sort(values.begin(), values.end(), [](int a, int b)
    {
            return a < b;
    });//lambda表达式,如果第一个参数小,则第一个参数排在前面,故为升序。
}

C++的类型双关(type punning)

底层编程,较危险,可能导致未定义行为,内存越界,对齐问题,可移植性等问题。

将同一块内存的东西通过不同type的指针给取出来

取地址,换成对应类型的指针,再解引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
int main()
{
    int a = 50;
    double value = *(double*)&a;
    std::cout << value << std::endl;

    std::cin.get();
}
//可以用引用,这样就可以避免拷贝成一个新的变量:(只是演示,这样做很糟糕)
#include <iostream>
int main()
{
    int a = 50;
    double& value = *(double*)&a;
    std::cout << value << std::endl;

    std::cin.get();
}

把一个结构体转换成数组进行操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
struct Entity
{
    int x, y;
};

int main()
{
    Entity e = {5, 8};
    int *position = (int *)&e;
    std::cout << position[0] << ", " << position[1] << std::endl;

    int y = *(int *)((char *)&e + 4);
    std::cout << y << std::endl;
}

C++的联合体( union )

  1. 通常union是匿名使用的,但是匿名union不能含有成员函数
  2. 在可以使用类型双关的时候,使用union时,可读性更强
  3. .union的特点是共用内存 。可以像使用结构体或者类一样使用它们,也可以给它添加静态函数或者普通函数、方法等。然而不能使用虚方法,还有其他一些限制。
  4. 分配的大小是按最大成员的sizeof
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
#include <iostream>
struct Vector2
{
    float x, y;
};

struct Vector4
{
    union // 不写名称,作为匿名使用
    {
        struct //第一个Union成员
        {
            float x, y, z, w;
        };
        struct // 第二个Union成员,与第一个成员共享内存
        {
            Vector2 a, b;//a和x,y的内存共享,b和z,w的内存共享
        };
    };
};

void PrintVector2(const Vector2 &vector)
{
    std::cout << vector.x << ", " << vector.y << std::endl;
}

int main()
{
    Vector4 vector = {1.0f, 2.0f, 3.0f, 4.0f};
    PrintVector2(vector.a);
    PrintVector2(vector.b);
    vector.z = 500;
    std::cout << "-----------------------" << std::endl;
    PrintVector2(vector.a);
    PrintVector2(vector.b);
}
//输出:
1,2
3,4
-----------------------
1,2
500,4

C++的虚析构函数

如果用基类指针来引用派生类对象,那么基类的析构函数必须是 virtual 的,否则 C++ 只会调用基类的析构函数,不会调用派生类的析构函数。

  1. 凡是有虚函数的类,都应声明虚析构函数(因为通常会被继承)。

  2. 即使没有虚函数,如果类会被作为基类使用,也建议声明虚析构函数(防止未来有人通过基类指针删除派生对象)。

  3. 如果类不会被继承(如 final 类),则无需虚析构(避免虚表开销)。

  4. 派生类的析构函数可以省略 virtual,但为了可读性,建议加上 override 关键字:

  5. 构造函数没有“虚”的概念,构造总是“先基类后子类”,但析构函数要添加virtual,才能“先析构子类后析构基类”。

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
#include <iostream>

class Base
{
public:
    Base() { std::cout << "Base Constructor\n"; }
    virtual ~Base() { std::cout << "Base Destructor\n"; }
};

class Derived : public Base
{
public:
    Derived()
    {
        m_Array = new int[5];
        std::cout << "Derived Constructor\n";
    }
    ~Derived() override 
    {
        delete[] m_Array;
        std::cout << "Derived Destructor\n";
    }

private:
    int *m_Array;
};

int main()
{
    Base *base = new Base();
    delete base;
    std::cout << "-----------------" << std::endl;
    Derived *derived = new Derived();
    delete derived;
    std::cout << "-----------------" << std::endl;
    Base *poly = new Derived();
    delete poly; // 基类析构函数中如果不加virtual,则此处会造成内存泄漏
    // Base Constructor
    // Base Destructor
    // -----------------
    // Base Constructor
    // Derived Constructor
    // Derived Destructor
    // Base Destructor
    // -----------------
    // Base Constructor
    // Derived Constructor
    // Derived Destructor //基类析构函数中如果不加virtual,子类的虚构函数不会被调用
    // Base Destructor
}

C++的类型转换

static_cast

static_cast用于进行比较“自然”和低风险的转换,如整型和浮点型、字符型之间的互相转换,不能用于指针类型的强制转换。

1
2
3
4
5
double dPi = 3.1415926;
int num = static_cast<int>(dPi);  //num的值为3
double d = 1.1;
void *p = &d;
double *dp = static_cast<double *>(p);

reinterpret_cast

reinterpret_cast 用于进行各种不同类型的指针之间强制转换。

危险,不推荐。

1
2
int *ip;
char *pc = reinterpret_cast<char *>(ip);

const_cast

const_cast 添加或者移除const性质,常用于函数重载。

绝对不要对原本就是常量的对象使用 const_cast 并修改它——未定义行为,可能导致程序崩溃或数据损坏。 只有原始对象不是常量,但被 const 指针/引用指向时,移除 const 才是安全的。

1
2
3
4
5
6
7
8
9
10
11
12
13
const string &shorterString(const string &s1, const string &s2)
{
    return s1.size() <= s2.size() ? s1 : s2;
}

//上面函数返回的是常量string引用,当需要返回一个非常量string引用时,可以增加下面这个函数
//复用 const 版本的函数逻辑来实现非 const 版本,避免代码重复。
string &shorterString(string &s1, string &s2) //函数重载
{
    auto &r = shorterString(const_cast<const string &>(s1), 
                            const_cast<const string &>(s2));
    return const_cast<string &>(r);
}

dynamic_cast

dynamic_cast 不检查转换安全性,仅运行时检查,如果不能转换,返回NULL。 支持运行时类型识别(run-time type identification,RTTI)。 使用RTTI运算符有潜在风险。 dynamic_cast 主要用于多态类型的安全向下转型(基类 → 派生类)或交叉转型(兄弟类之间)。它的使用前提是基类至少有一个虚函数(即多态的)。当转型失败时:对指针返回 nullptr对引用抛出 std::bad_cast 异常

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#include <iostream>
#include <vector>
#include <memory>

// 基类:包含虚函数,满足多态条件
class Shape {
public:
    virtual ~Shape() = default;
    virtual void draw() const { std::cout << "Draw Shape\n"; }
};

class Circle : public Shape {
public:
    void draw() const override { std::cout << "Draw Circle\n"; }
    void setRadius(double r) { radius = r; }
    double getRadius() const { return radius; }
private:
    double radius = 0.0;
};

class Rectangle : public Shape {
public:
    void draw() const override { std::cout << "Draw Rectangle\n"; }
    void setWidthHeight(double w, double h) { width = w; height = h; }
private:
    double width = 0.0, height = 0.0;
};

// 一个独立类,仅用于演示交叉转型
class Printable {
public:
    virtual ~Printable() = default;
    virtual void print() const { std::cout << "Printable\n"; }
};

// 多重继承:Circle 同时也是 Printable
class AdvCircle : public Circle, public Printable {
public:
    void print() const override { std::cout << "AdvCircle printable\n"; }
};

int main() {
    // 场景1:向下转型(指针版本)
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>());
    shapes.push_back(std::make_unique<Rectangle>());

    for (auto& s : shapes) {
        // 尝试转换为 Circle*
        if (Circle* circle = dynamic_cast<Circle*>(s.get())) {
            circle->setRadius(5.0);
            std::cout << "It's a Circle, radius set to " << circle->getRadius() << '\n';
        }
        // 尝试转换为 Rectangle*
        else if (Rectangle* rect = dynamic_cast<Rectangle*>(s.get())) {
            rect->setWidthHeight(3.0, 4.0);
            std::cout << "It's a Rectangle, dimensions set\n";
        }
        // 通用接口
        s->draw();
    }

    // 场景2:向下转型(引用版本,需要捕获异常)
    Shape& shapeRef = *shapes[0];  // 指向 Circle
    try {
        Circle& circleRef = dynamic_cast<Circle&>(shapeRef);
        circleRef.setRadius(10.0);
        std::cout << "Via reference: radius = " << circleRef.getRadius() << '\n';
    } catch (const std::bad_cast& e) {
        std::cout << "Reference cast failed: " << e.what() << '\n';
    }

    // 场景3:交叉转型(从 Shape* 转换为 Printable*)
    AdvCircle advCircle;
    Shape* shapePtr = &advCircle;  // 指向基类

    // 交叉转型:Shape* -> Printable*,因为 AdvCircle 同时继承自两者
    if (Printable* printable = dynamic_cast<Printable*>(shapePtr)) {
        printable->print();  // 输出 "AdvCircle printable"
    } else {
        std::cout << "Cross cast failed\n";
    }

    // 错误示范:从 Shape* 转型为不相关的类(会失败)
    std::string* strPtr = dynamic_cast<std::string*>(shapePtr);  // 编译错误:std::string 不是多态类型
    // 正确做法:必须两个类都是多态类型,且存在继承/派生关系
    // 若强转不相关多态类型,返回 nullptr
    class AnotherBase { virtual void dummy() {} };
    AnotherBase* another = dynamic_cast<AnotherBase*>(shapePtr); // 允许编译,运行返回 nullptr

    return 0;
}

C++预编译头文件

  1. 为了解决一个项目中同一个头文件被反复编译的问题。使得写代码时不需要一遍又一遍的去#include那些常用的头文件,比如C++标准库,而且能大大提高编译速度
  2. 预编译头文件的使用会隐藏掉这个cpp文件的依赖。比如用了#include ,就清楚的知道这个cpp文件中需要vector的依赖,而如果放到预编译头文件中,就会将该信息隐藏。

示例项目结构

1
2
3
4
5
6
7
my_project/
├── CMakeLists.txt
├── src/
│   ├── pch.h          # 预编译头文件(集中放置常用头)
│   ├── main.cpp
│   ├── foo.cpp
│   └── foo.h

预编译头文件 src/pch.h

1
2
3
4
5
6
7
8
9
10
11
12
// src/pch.h
#pragma once

// 常用标准库头文件
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <map>

// 项目内高频使用的自定义头文件(若有)
// #include "common/utility.h"

普通源文件示例

src/foo.h

1
2
3
// src/foo.h
#pragma once
void foo();

src/foo.cpp

1
2
3
4
5
6
7
8
9
10
11
// src/foo.cpp
#include "pch.h"    // 预编译头必须放在最前面
#include "foo.h"

void foo() {
    std::vector<int> v = {1, 2, 3};
    std::for_each(v.begin(), v.end(), [](int n) {
        std::cout << n << " ";
    });
    std::cout << std::endl;
}

src/main.cpp

1
2
3
4
5
6
7
8
9
10
// src/main.cpp
#include "pch.h"
#include "foo.h"

int main() {
    std::string msg = "Hello, PCH!";
    std::cout << msg << std::endl;
    foo();
    return 0;
}

CMakeLists.txt(使用 target_precompile_headers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
cmake_minimum_required(VERSION 3.16)
project(PCHExample)

# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 添加可执行文件
add_executable(my_app
    src/main.cpp
    src/foo.cpp
    src/pch.h   # 将 pch.h 显式加入项目(便于 IDE 识别)
)

# 将 pch.h 设为预编译头文件
# 注意:pch.h 必须被所有使用它的源文件 include(且通常放在最开头)
target_precompile_headers(my_app PRIVATE src/pch.h)

# 可选:如果 pch.h 位于非标准路径,需要添加包含目录
target_include_directories(my_app PRIVATE src)

构建与验证

1
2
3
4
cd my_project
mkdir build && cd build
cmake ..
cmake --build .

第一次构建时,CMake 会自动生成并编译预编译头文件(通常生成 .gch.pch 文件)。后续修改 foo.cppmain.cpp 但未修改 pch.h 时,预编译头会直接复用,编译速度显著提升。

关键点说明

  • 预编译头必须第一个被 #include:在所有 .cpp 文件中,#include "pch.h" 必须出现在任何其他代码(包括其他 #include)之前,否则可能导致编译错误。
  • CMake 版本target_precompile_headers 需要 CMake 3.16+。对于旧版本,需手动调用编译器命令生成预编译头(较繁琐,不推荐)。
  • PRIVATE vs PUBLIC:示例中使用 PRIVATE,因为预编译头仅对该目标自身有效。如果预编译头被多个目标共享,可使用 PUBLICINTERFACE
  • 包含路径:确保 pch.h 的所在目录在包含路径中(本例通过 target_include_directories 添加)。
  • 编译器支持:MSVC、GCC、Clang 均良好支持预编译头,CMake 会自动处理平台差异。

适用场景总结

  • 大型项目,包含大量稳定、庞大的头文件(标准库、Boost、Qt、Eigen 等)。
  • 频繁全量或增量编译的开发环境(如 CI、日常调试)。
  • 头文件依赖层次深、解析开销大的情况。

C++的基准测试

编写一个计时器对代码测试性能。记住要在release模式去测试。 利用工具: chrome://tracing (chrome浏览器自带的一个工具,将该网址输入即可) 基本原理: cpp的计时器配合自制简易json配置写出类,将时间分析结果写入一个json文件,用chrome://tracing 这个工具进行可视化 。 可视化基准测试。

C++如何处理optional数据(std::optional)

std::optional 是 C++17 引入的模板类,用于表示一个可选值,即值可能存在也可能不存在。它能替代返回指针或特殊值的方式,更安全且可读性更高。

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
#include <iostream>
#include <fstream>
#include <optional>
#include <string>

std::optional<std::string> ReadFileAsString(const std::string& filepath)
{
    std::ifstream stream(filepath);
    if (stream)
    {
        std::string result;
        getline(stream, result);
        stream.close();
        return result;
    }
    return {};
    //如果文本存在的话,它会返回所有文本的字符串。如果不存在或者不能读取;则返回optional {}
}

int main()
{
     std::optional<std::string> data = ReadFileAsString("data.txt");
    //auto data = ReadFileAsString("data.txt"); //可用auto关键字
    if (data)//也可用data.has_value()判断是否有值
    {   
       // std::string& str = *data;
       // std::cout << "File read successfully!" << str<< std::endl;
        std::cout << "File read successfully!" << data.value() << std::endl; //也可用(*opt),value_or()方法,value_or(default)无值时返回默认值
    }
    else
    {
        std::cout << "File could not be opened!" << std::endl;
    }

    std::cin.get();
}
//输出
// File read successfully!"data!"

C++单一变量存放多种类型的数据(std::variant)

指定一个叫std::variant的东西,然后列出它可能的数据类型。

  1. std::variant的大小会比union大一些 。
  2. variant更加类型安全,不会造成未定义行为。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    #include<iostream>
    #include<variant>
    int main()
    {
     std::variant<std::string,int> data; // <>里面的类型不能重复
     data = "ydc";
     // 索引的第一种方式:std::get,但是要与上一次赋值类型相同,不然会报错
     std::cout<<std::get<std::string>(data)<<std::endl;
     // 索引的第二种方式,std::get_if,传入地址,返回为指针
     if (auto value = std::get_if<std::string>(&data))
     {
         std::string& v = *value;
     }
     data = 2;
     std::cout<<std::get<int>(data)<<std::endl;
     data.index();// 返回一个整数,代表data当前存储的数据的类型在<>里的序号,比如返回0代表存的是string, 返回1代表存的是int
     std::cin.get();
    }
    

C++如何存储任意类型的数据(std::any)

对于小类型(small type)来说,any将它们存储为一个严格对齐的Union, 对于大类型,会用void*,动态分配内存 。

尽量不用。只有当类型集合无法在编译期确定,且无法用继承或多态优雅表达时,才考虑 std::any。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <any>
// 这里的new的函数,是为了设置一个断点,通过编译器观察主函数中何处调用了new,什么时候达到大类型,开始用堆分配。
void *operator new(size_t size)
{
    return malloc(size);
}

int main()
{
    std::any data;
    data = 2;
    data = "Cherno";
    data = std::string("Cherno");

    std::string& string = std::any_cast<std::string&>(data); //用any_cast指定转换的类型,如果这个时候any不是想要转换的类型,则会抛出一个类型转换的异常
    // 通过引用减少复制操作,以免影响性能
}

如何让C++运行得更快(std::async)

std::async 是 C++11 标准库中用于简化异步任务的高级工具,它将任务的启动与结果的获取(std::future)封装在一起。相比底层的 std::thread,它更安全、更易用,能自动管理资源并方便地获取返回值或异常

传递引用需要用std::ref / std::cref(常量引用)包裹,注意引用对象的生命周期

✨ 核心特性与启动策略

std::async 的核心在于其启动策略,你可以通过 std::launch 枚举来控制任务的执行方式:

策略 (Launch Policy)行为描述关键点
std::launch::async强制异步执行。保证任务会在一个新的线程中立刻执行。最适合希望任务立即并行运行的场景。
std::launch::deferred延迟(惰性)执行。调用时不创建线程,只有当外部通过 std::future 调用 get()wait() 时,任务才会在调用线程(通常是主线程)上同步执行。适用于任务可能不会被用到的情况,可以节省系统资源。
默认策略std::launch::async \| std::launch::deferred由库和系统自动决定是创建新线程异步执行,还是延迟同步执行。提供了便利性,但也带来了不确定性。关键建议:请务必明确指定你需要的策略,以避免不可预期的行为。

⚙️ 工作原理与实现机制

  1. 核心组件std::async 本质上是对 std::futurestd::promise 和线程管理的高层封装。它创建任务,将结果存储到 std::promise,再通过返回的 std::future 来获取该结果。
  2. 返回值std::async 返回一个 std::future 对象,用于异步接收任务的结果。
  3. 异常处理:如果任务内部抛出异常,该异常不会被忽略,而是会存储在 std::future 中。当调用 .get() 时,这个异常会被重新抛出,让你有机会捕获并处理。
  4. 资源管理std::async 返回的 std::future 对象在析构时,会隐式等待(join)其关联的异步任务完成。这虽方便,但也可能在不注意时造成阻塞。
  5. 实现差异std::async 的底层实现依赖编译器。例如,GCC/Clang 在 std::launch::async 策略下通常直接创建新线程;而 MSVC 可能使用线程池来复用线程,在大量任务时能减少开销,但也可能引起任务排队。

💻 使用示例

基础示例:包含一个完整的可运行示例,演示了如何使用 std::async 执行异步任务并获取结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <future>
#include <chrono>
#include <thread>

// 模拟一个耗时任务
int fetchData() {
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return 42;
}

int main() {
    // 1. 使用 std::launch::async 强制异步执行
    std::future<int> result = std::async(std::launch::async, fetchData);

    std::cout << "等待异步任务完成..." << std::endl;

    // 2. 阻塞主线程,等待并获取结果
    int value = result.get();
    std::cout << "异步任务结果: " << value << std::endl;

    return 0;
}

策略对比

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
#include <iostream>
#include <future>
#include <thread>

int heavyWork(int id) {
    std::cout << "任务 " << id << " 在线程 " << std::this_thread::get_id() << " 中执行" << std::endl;
    return id * id;
}

int main() {
    // 1. 强制异步执行 (async 策略)
    auto fut_async = std::async(std::launch::async, heavyWork, 1);
    std::cout << "async 策略已调用" << std::endl;

    // 2. 延迟执行 (deferred 策略)
    auto fut_deferred = std::async(std::launch::deferred, heavyWork, 2);
    std::cout << "deferred 策略已调用,但任务尚未执行" << std::endl;

    // 3. 默认策略
    auto fut_default = std::async(heavyWork, 3);
    std::cout << "默认策略已调用" << std::endl;

    // 获取结果
    std::cout << "async 任务结果: " << fut_async.get() << std::endl;
    // 直到调用 get() 时,deferred 任务才执行
    std::cout << "deferred 任务结果: " << fut_deferred.get() << std::endl;
    std::cout << "默认策略任务结果: " << fut_default.get() << std::endl;

    return 0;
}

可能的输出顺序:

1
2
3
4
5
6
7
async 策略已调用
deferred 策略已调用,但任务尚未执行
默认策略已调用
async 任务结果: 1
任务 2 在线程 0x7f8b9b... 中执行
deferred 任务结果: 4
默认策略任务结果: 9

传参与异常处理

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
#include <iostream>
#include <future>
#include <stdexcept>

int mightThrow(int a, int b) {
    if (b == 0) throw std::runtime_error("除数不能为零!");
    return a / b;
}

int main() {
    // 传递多个参数
    auto safe_future = std::async(std::launch::async, mightThrow, 10, 2);
    try {
        int result = safe_future.get(); // 正常获取结果
        std::cout << "安全结果: " << result << std::endl;
    } catch (const std::exception& e) {
        std::cout << "捕获到异常: " << e.what() << std::endl;
    }

    // 会抛出异常的异步任务
    auto dangerous_future = std::async(std::launch::async, mightThrow, 10, 0);
    try {
        int result = dangerous_future.get(); // 异常将在 .get() 时重新抛出
        std::cout << "危险结果: " << result << std::endl;
    } catch (const std::exception& e) {
        std::cout << "成功捕获异常: " << e.what() << std::endl;
    }
    return 0;
}

预期输出:

1
2
安全结果: 5
成功捕获异常: 除数不能为零!

⚖️ 与 std::thread 的对比

了解 std::asyncstd::thread 的区别,有助于你更好地选择合适的工具。

特性std::asyncstd::thread
抽象层级高级异步任务抽象底层线程的直接封装
结果获取通过 std::future 自动返回无返回值,需通过共享变量或 std::promise 间接获取
资源管理自动管理线程生命周期必须显式调用 join()detach()
异常处理捕获异常并在 .get() 时重新抛出线程内未捕获的异常会直接导致 std::terminate
执行策略灵活,支持异步、延迟或默认总是立即创建新线程
适用场景短期任务、需要返回结果、追求代码简洁长期后台任务、需要精细控制线程行为(如优先级)

💎 总结与建议

  1. 明确策略务必std::async 明确指定 std::launch::asyncstd::launch::deferred 策略,以避免默认策略的不确定性。
  2. 优势场景:当需要异步执行一个短期任务、希望方便地获取返回值或进行异常处理,并希望避免手动管理线程时,std::async 是更安全、便捷的选择。
  3. 局限与风险
    • 潜在阻塞:注意 std::future 的析构函数可能会隐式等待任务完成,导致意外阻塞。
    • 资源竞争:如果任务需要访问共享资源,仍需使用互斥锁(如 std::mutex)等同步机制。
    • 实现差异:不同编译器的底层实现(如线程池)可能不同,这可能会影响性能和行为,编写可移植代码时需注意。

如何让C++字符串更快 in C++

  1. 能分配在栈上就别分配到堆上
  2. std::string_view
  3. SSO(短字符串优化)、COW(写时复制技术优化)

    优化原因

  4. std::string和它的很多函数都喜欢分配在堆上
  5. 处理字符串时,比如使用substr切割字符串时,这个函数会自己处理完原字符串后创建出一个全新的字符串
  6. 在数据传递中减少拷贝是提高性能的最常用办法。比如用引用类型。
  7. 当函数形参为 const std::string&,实参为 const char(字符串字面值、字符数组退化的指针、或字符串指针)时,编译器会:调用 std::string 的非显式构造函数 std::string(const char),分配内存并拷贝字符数据(直到 ‘\0’)。生成一个临时 std::string 对象,然后将 const std::string& 绑定到该临时对象。函数结束后,临时对象被销毁。

具体优化

通过 string_view

string_view提供一个字符串的视图,即可以通过这个类以各种方法“观测”字符串,但不允许修改字符串。 由于它只读的特性,它并不真正持有这个字符串的拷贝,而是与相对应的字符串共享这一空间。 即——构造时不发生字符串的复制。同时,你也可以自由的移动这个视图,移动视图并不会移动原定的字符串。

  1. string_view通常用于函数参数类型,代替 const string&,可以避免不必要的内存分配。
  2. string_view字面量的后缀是 sv。(string字面量的后缀是 s)。
  3. 视图的生命期不能短于使用它的地方。
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include <iostream>
#include <string>
#include <string_view>

// 自定义 operator new 统计堆内存分配次数
static uint32_t s_AllocCount = 0;
void* operator new(size_t size) 
{
    s_AllocCount++;
    std::cout << "Allocating " << size << " bytes\n";
    return malloc(size);
}

// 选择 PrintName 的参数类型:1 -> std::string_view, 0 -> const std::string&
#define STRING_view 1

#if STRING_view
void PrintName(std::string_view name)
{
    std::cout << name << std::endl;
}
#else
void PrintName(const std::string& name)
{
    std::cout << name << std::endl;
}
#endif

// 选择数据源:0 -> std::string 对象, 1 -> C 风格字符串
#define USE_CSTRING 0

int main()
{
    // 根据宏定义数据源
#if USE_CSTRING
    const char* source = "Yan Chernosafhiahfiuauadvkjnkjasjfnanvanvanjasdfsgs";
#else
    const std::string source = "Yan Chernosafhiahfiuauadvkjnkjasjfnanvanvanjasdfsgs";
#endif

#if STRING_view
    // 使用 std::string_view,避免堆分配
    // 注意:从 std::string 获取 C 指针需要调用 .c_str()
    std::string_view firstName(
#if USE_CSTRING
        source,
#else
        source.c_str(),
#endif
        3);
    std::string_view lastName(
#if USE_CSTRING
        source + 4,
#else
        source.c_str() + 4,
#endif
        9);
#else
    // 使用 std::string,会产生堆分配
    std::string firstName;
    std::string lastName;
#if USE_CSTRING
    // C 风格字符串:先构造临时 std::string 再 substr
    firstName = std::string(source).substr(0, 3);
    lastName  = std::string(source).substr(4, 9);
#else
    firstName = source.substr(0, 3);
    lastName  = source.substr(4, 9);
#endif
#endif

    // 打印原始字符串及子串
    PrintName(source);
    PrintName(firstName);
    PrintName(lastName);

    std::cout << s_AllocCount << " allocations" << std::endl;

    return 0;
}

C++的单例模式

一、核心概念:单例模式的本质

单例模式确保一个类在全局范围内有且仅有一个实例,并提供访问该实例的统一入口。

从 C++ 视角看,单例确实很像一种强化版的命名空间

  • 命名空间:组织全局函数和全局变量,但无法控制“实例化次数”(开发者可以随意复制它,造出第二个、第三个“看似一样”的状态实例。),也不能拥有私有状态构造函数。
  • 单例类:同样组织全局功能,但通过私有构造函数严格限制实例数量,从而可以安全地持有内部状态(例如随机数引擎、渲染上下文),并控制初始化时机。

二、何时使用单例模式(应用场景判断)

场景特征具体例子
资源唯一且需要全局共享渲染器(Renderer)、日志系统(Logger)、音频引擎
需要维护全局状态,且状态需惰性初始化随机数生成器(需要种子初始化)、配置管理器
为避免重复初始化带来的开销或逻辑错误数据库连接池、线程池

关键判断依据:若一个对象在整个程序生命周期内只需要一个实例,且多个模块需要频繁访问它,单例便是合适的候选方案。

三、C++ 单例的两种经典实现对比

单例模式的两种主流 C++ 实现方式,它们的区别在于实例创建时机与存储位置

方法一:静态成员变量(类外定义)

1
2
3
4
5
6
7
class SingleTon {
    static SingleTon m_temp; // 声明
    SingleTon() {} //私有构造函数
public:
    static SingleTon& get() { return m_temp; }
};
SingleTon SingleTon::m_temp; // 在类外(全局作用域)定义
  • 特点:实例在 main 函数执行之前(静态初始化阶段)就已经构造完成。
  • 优点:实现直观,无锁调用。
  • 隐患静态初始化顺序问题。如果另一个全局对象的构造函数试图调用 SingleTon::get(),此时 m_temp 可能尚未构造(未定义行为)。

方法二:局部静态变量(Meyers’ Singleton)

1
2
3
4
5
6
7
8
class Random {
    Random() {} //私有构造函数
public:
    static Random& Get() {
        static Random instance; // 首次调用时构造
        return instance;
    }
};
  • 特点:实例在 Get() 函数第一次被调用时 才构造(惰性初始化)。
  • 优点
    1. 解决了静态初始化顺序问题(在需要时才创建)。
    2. C++11 起线程安全:编译器保证多线程同时首次调用时,static 局部变量仅被初始化一次。
    3. 代码更简洁,无需类外定义。
  • 结论:这是 C++11 及以后标准下最推荐的单例写法

四、关键防御手段:拒绝拷贝与赋值

Random(const Random&) = delete;必须的。如果忘记这点,会出现下面的漏洞:

1
2
3
// 如果没有 delete 拷贝构造
SingleTon& s1 = SingleTon::get();
SingleTon s2 = s1; // 编译通过!构造了第二个实例,破坏单例原则

因此,完整的现代单例类必须显式删除或私有化拷贝构造函数赋值运算符

1
2
3
4
5
6
class Singleton {
public:
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    // ...
};

五、补充:如何优雅地组织接口(像命名空间一样调用)

单例与 namespace 很像,在 Random 例子的最后部分通过静态成员函数包装实现了类似命名空间的调用体验:

1
2
// 用户只需 Random::Float(),不必每次都写 Random::Get().Float()
static float Float() { return Get().IFloat(); }

这是一种很好的封装技巧,既隐藏了单例获取细节,又保持了接口简洁。

六、总结:最佳实践模板

现代 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
class Renderer {
public:
    // 1. 禁止拷贝
    Renderer(const Renderer&) = delete;
    Renderer& operator=(const Renderer&) = delete;

    // 2. 统一访问入口(线程安全,惰性初始化)
    static Renderer& Get() {
        static Renderer instance;
        return instance;
    }

    // 3. 业务接口
    void Submit(const Command& cmd) { /* ... */ }

private:
    // 4. 私有构造与析构(如果需要可以公开析构)
    Renderer() { 
        // 初始化图形API等 
    }
    ~Renderer() { /* 清理 */ }
};

// 使用方式
int main() {
    Renderer::Get().Submit(cmd);
}
  1. 程序启动,进入 main 之前,此时 Renderer 类本身作为类型定义已经存在,但没有任何 Renderer 对象被创建。static Renderer& Get() 函数只是一段代码,尚未执行。
  2. 调用 Renderer::Get(),遇到 static Renderer instance;给instance实例分配内存,调用 Renderer 的私有构造函数,返回 instance 的引用,通过返回的引用调用 Submit(cmd)
  3. 后续再调用Renderer::Get(),会直接返回已创建的静态instance 的引用。

Get() 是 Renderer 类的静态成员函数。静态成员函数虽然不属于任何对象,但它属于类的作用域,拥有访问该类私有成员的权限。

跟踪内存分配的简单方法

重写new和delete操作符函数,并在里面打印分配和释放了多少内存,也可在重载的这两个函数里面设置断点,通过查看调用栈即可知道什么地方分配或者释放了内存。

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
54
55
56
57
// 重写new、free操作符之后就能方便地跟踪内存分配了(加断点)

#include <iostream>
#include <memory>

struct AllocationMetrics
{
    uint32_t TotalAllocated = 0; //总分配内存
    uint32_t TotalFreed = 0; //总释放内存

    uint32_t CurrentUsage() { return TotalAllocated - TotalFreed; } //写一个小函数来输出 当前用了多少内存
};

static AllocationMetrics s_AllocationMetrics; //创建一个全局静态实例

void *operator new(size_t size)
{
    s_AllocationMetrics.TotalAllocated += size; //在每一个new里计算总共分配了多少内存
    // std::cout << "Allocate " << size << " bytes.\n";
    return malloc(size);
}

void operator delete(void *memory, size_t size)
{
    s_AllocationMetrics.TotalFreed += size;
    // std::cout << "Free " << size << " bytes.\n";
    free(memory);
}

struct Object
{
    int x, y, z;
};
//可以用一个函数输出我们的内存使用情况
static void PrintMemoryUsage()
{
    std::cout << "Memory Usage:" << s_AllocationMetrics.CurrentUsage() << " bytes\n";
}

int main()
{
    PrintMemoryUsage();
    {
        std::unique_ptr<Object> obj = std::make_unique<Object>();
        PrintMemoryUsage();
    }

    PrintMemoryUsage();
    Object *obj = new Object;
    PrintMemoryUsage();
    delete obj;
    PrintMemoryUsage();
    std::string string = "Cherno";
    PrintMemoryUsage();

    return 0;
}

详细且准确的分析

使用专用工具,如AddressSanitizer等。 cmake集成

1
2
3
4
5
6
7
8
9
# 1. 添加可执行文件目标
add_executable(my_program main.cpp)

# 2. 定义一个变量来存储编译选项
set(ASAN_FLAGS "-fsanitize=address -fno-omit-frame-pointer -g -O1")

# 3. 为目标添加编译和链接选项
target_compile_options(my_program PRIVATE ${ASAN_FLAGS})
target_link_options(my_program PRIVATE ${ASAN_FLAGS})

C++的左值与右值(lvalue and rvalue)

1. 左值(lvalue)

定义核心
左值是具有确定地址、拥有持久存储空间的表达式。它代表一块可以被标识的内存位置,因此可以出现在赋值运算符的左侧(也可以出现在右侧)。

特征

  • 有名字的变量、函数返回左值引用、解引用指针等都是左值。
  • 生命周期通常与所在作用域绑定(或更长,如全局、静态变量)。
  • 可以取地址(&运算符有效)。

示例

1
2
3
int x = 5;        // x 是左值
int* p = &x;      // &x 合法
int& ref = x;     // 左值引用绑定到左值

2. 左值引用(lvalue reference)

语法T&
绑定规则

  • 非常量左值引用T&只能绑定到左值,不能绑定到右值(临时对象)。
  • 常量左值引用const T&)可以绑定到左值,也可以绑定到右值。当绑定右值时,编译器会创建一个临时对象,并将引用绑定到该临时对象,同时延长临时对象的生命周期至引用作用域结束。

为什么常量引用可以绑定右值?
因为常量引用承诺不修改所引用的对象,因此允许引用临时对象是安全的;若允许非常量引用绑定临时对象,则修改操作将作用于一个即将销毁的值,极易引发未定义行为。

示例

1
2
3
4
5
6
7
8
9
void foo(int& ref) {}          // 仅接受左值
void bar(const int& ref) {}    // 接受左值和右值

int a = 10;
foo(a);           // OK
foo(10);          // 错误:非常量引用不能绑定右值

bar(a);           // OK
bar(10);          // OK,临时 int 被创建,生命周期延长

3. 右值(rvalue)

定义核心
右值是临时的、没有持久存储空间的表达式。它通常不拥有可标识的内存地址(或者说地址不可被用户直接取用),往往出现在赋值运算符的右侧。

特征

  • 字面量(除字符串字面量外)、临时表达式结果、函数返回非引用类型等都是右值。
  • 生命周期短暂,一般仅存在于表达式求值期间。
  • 不能对右值直接取地址(& 操作非法,但可通过右值引用间接操作)。

示例

1
2
3
int x = 5;
int y = x + 3;        // x + 3 是右值(临时结果)
int* p = &(x + 3);    // 错误:不能对右值取地址

4. 右值引用(rvalue reference)

语法T&&
设计目的

  • 识别并接管临时对象的资源(移动语义)。
  • 配合移动构造函数和移动赋值运算符,避免不必要的深拷贝,显著提升性能。

绑定规则

  • 右值引用只能绑定到右值,不能直接绑定到左值。
  • 可以使用 std::move 将左值强制转换为右值引用类型,从而绑定到右值引用。

重要概念

“有名字的右值引用本身是左值”。

右值引用变量一旦被命名,它就成为一个持久的、可寻址的实体,因此它是一个左值。如果希望将它作为右值继续传递,需要使用 std::move

延长生命周期
将一个右值绑定到右值引用(或 const T&)会延长该临时对象的生命周期,直至该引用离开作用域。

示例

1
2
3
4
int&& rref = 10;          // 右值引用绑定右值,临时 int 生命周期延长
int a = 5;
int&& rref2 = a;          // 错误:不能将右值引用绑定到左值
int&& rref3 = std::move(a); // OK,std::move 将 a 转为右值

5. 右值引用的优势:资源窃取与移动语义

场景分析
考虑一个函数需要接受一个大型对象(如 std::stringstd::vector)作为参数:

  • 如果使用 const T&,虽然可以同时接受左值和右值,但只能进行只读访问,无法修改或“窃取”其内部资源。若需要在函数内部复制或存储该字符串,仍必须执行一次深拷贝
  • 如果使用 T&&,则明确表示“我只需要一个临时对象,或调用者主动放弃了所有权”。此时可以安全地将临时对象的内部数据指针“偷”过来(例如通过移动构造函数),从而避免昂贵的深拷贝。

移动语义本质
通过转移堆内存的所有权,将原对象的指针置空,新对象直接接管资源。原临时对象在销毁时不会释放已转移的资源,而新对象无需重新分配内存。

示例

1
2
3
4
5
6
7
8
9
void ProcessString(const std::string& str) {
    // 只能读取 str,若需要保存副本则必须拷贝
    std::string copy = str;   // 深拷贝
}

void ProcessString(std::string&& str) {
    // 明确 str 是临时对象,可窃取其资源
    std::string local = std::move(str); // 移动构造,仅交换指针
}

结合常量引用的局限
const std::string& 虽然兼容右值,但函数内部无法修改它(const 修饰),更无法从中移动资源。因此,对于需要“吸收”参数的场景,重载 T&& 版本是实现优化的关键。

6. 函数参数重载与绑定行为详解

给定四种重载形式:

1
2
3
4
void PrintName(std::string name);              // (1) 按值传递
void PrintName(std::string& name);             // (2) 左值引用
void PrintName(const std::string& name);       // (3) const左值引用
void PrintName(std::string&& name);            // (4) 右值引用

行为分析

调用表达式绑定情况
PrintName(fullName);优先绑定 (2),若无 (2) 则 (1) 和 (3) 均可(产生二义性时需显式指定)。
PrintName(firstName+lastName);优先绑定 (4),若无 (4) 则 (1) 和 (3) 均可(临时对象可构造 std::string 参数或绑定到 const&)。

详细规则

  • 按值传递 (1):实参用于构造形参。左值实参触发拷贝构造,右值实参触发移动构造(若类型支持)或拷贝构造。始终安全,但可能产生拷贝开销。
  • 左值引用 (2):仅接受左值,保证函数内可修改且修改会反映到外部对象。
  • const左值引用 (3):万能引用(除volatile外),可绑定左值或右值。函数内不可修改,常用于只读参数。
  • 右值引用 (4):仅接受右值,明确标识该参数可被“窃取”。调用者要么传递临时对象,要么显式使用 std::move 表示放弃所有权。

重载决议优先级(简化):

  • 对于左值实参:首选 T&,次选 const T&,最后选 T(值传递)。
  • 对于右值实参:首选 T&&,次选 const T&,最后选 T(值传递,但会触发移动构造)。

7. 示例代码深度解析

1
2
3
4
5
int &GetValue()
{ // 返回左值引用
    static int value = 10;
    return value;
}
  • value 是静态局部变量,生命周期贯穿整个程序,返回其引用是安全的。
  • 调用 GetValue() 得到的是左值引用,但这个表达式本身是一个左值,应将其当做value的别名,因此 GetValue() = 5; 合法:相当于 value = 5;
1
void SetValue(int value) {}
  • 按值传递,形参 value 在函数内部是一个左值(有名字的参数变量)。
  • 调用 SetValue(i) 时,i 拷贝给 value;调用 SetValue(10) 时,右值 10 拷贝给 value。这里编译器可能会优化掉拷贝。
1
2
// int& a = 10; // 错误:非常量左值引用不能绑定右值
const int &a = 10; // 正确:常量左值引用绑定右值,临时 int 生命周期延长至 a 作用域结束
1
2
3
void PrintName(std::string &name) { // 非const左值引用
    std::cout << "[lvalue]" << name << std::endl;
}
  • 仅接受左值。PrintName(fullName); 正确,PrintName(firstName + lastName); 错误(右值不能绑定到非const左值引用)。
1
2
3
4
void PrintName(std::string &&name) // 右值引用不能绑定到左值
{
    std::cout << "[rvalue]" << name << std::endl;
}
  • 仅接受右值。PrintName(fullName); 错误,PrintName(firstName + lastName); 正确(左值不能绑定到右值引用)。

总结表

概念可绑定对象典型用途
左值 T&仅左值需要修改传入对象的函数参数
const 左值 const T&左值 + 右值只读参数,兼容临时对象(万能引用)
右值 T&&仅右值(或 std::move 左值)移动语义,资源窃取,优化性能

C++移动语义

1. 什么是移动语义?

移动语义是 C++11 引入的一项重要特性,它允许在对象的生命周期转移过程中窃取资源,而不是深拷贝资源。典型场景包括:

  • 从临时对象(右值)构造新对象
  • 将对象放入容器(如 vector
  • 函数返回大对象

2. 核心语法部件

部件作用
右值引用 T&&绑定到临时对象(右值),标识其资源可被安全窃取
移动构造函数 T(T&&)从右值窃取资源构造新对象
移动赋值运算符 T& operator=(T&&)从右值窃取资源赋值给已存在对象
std::move将左值无条件转换为右值引用,标记“此对象资源可被移动”
noexcept标记移动操作不抛异常,对容器性能至关重要

不使用移动语义(拷贝)

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
54
55
56
#include <iostream>
#include <cstring>

class String {
public:
    String() = default;
    String(const char* string) {  //构造函数
        printf("Created!\n");
        m_Size = strlen(string);
        m_Data = new char[m_Size];
        memcpy(m_Data, string, m_Size);
    }

    String(const String& other) { // 拷贝构造函数
        printf("Copied!\n");
        m_Size = other.m_Size;
        m_Data = new char[m_Size];
        memcpy(m_Data, other.m_Data, m_Size);
    }

    ~String() {
        delete[] m_Data;
    }

    void Print() {
        for (uint32_t i = 0; i < m_Size; ++i)
            printf("%c", m_Data[i]);

        printf("\n");
    }
private:
    char* m_Data;
    uint32_t m_Size;
};

class Entity {
public:
    Entity(const String& name)
        : m_Name(name) {}
    void PrintName() {
        m_Name.Print();
    }
private:
    String m_Name;
};

int main(int argc, const char* argv[]) {
    Entity entity(String("Cherno"));
    entity.PrintName();

    return 0;
}
//输出结果:
Created!
Copied!
Cherno!
  • Created → 临时 String 构造。
  • CopiedEntity 的成员 m_Name 用拷贝构造初始化。
  • 临时对象析构(隐式,输出 Destroyed 未显示)。
  • entity.PrintName() 输出 Cherno
  1. String("Cherno") 创建临时对象 → 堆上分配内存,存储 "Cherno"
  2. Entity 构造函数接受 const String&,触发拷贝构造 → 又分配一块新内存,复制数据。
  3. 临时对象析构 → 释放第一块内存。

代价:两次内存分配 + 一次数据拷贝。

使用移动语义

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include<iostream>
class String
{
public:
    String() = default;
    String(const char* string) //构造函数
    {
        printf("Created\n");
        m_Size = strlen(string);
        m_Data = new char[m_Size];
        memcpy(m_Data, string, m_Size);
    }
    String(const String& other) // 拷贝构造函数
    {
        printf("Copied\n");
        m_Size = other.m_Size;
        m_Data = new char[m_Size];
        memcpy(m_Data, other.m_Data, m_Size);
    }
    String(String&& other) noexcept // 右值引用拷贝,相当于移动,就是把复制一次指针,原来的指针给nullptr
    {
        //让新对象的指针指向指定内存,然后将旧对象的指针移开
        //所以这里做的其实是接管了原来旧的内存,而不是将这片内存复制粘贴!
        printf("Moved\n");
        m_Size = other.m_Size;
        m_Data = other.m_Data;
        //这里便完成了数据的转移,将other里的数据偷走了
        other.m_Size = 0;
        other.m_Data = nullptr;
    }
    ~String()
    {
        printf("Destroyed\n");
        delete m_Data;
    }
    void Print() {
        for (uint32_t i = 0; i < m_Size; ++i)
            printf("%c", m_Data[i]);

        printf("\n");
    }
private:
    uint32_t m_Size;
    char* m_Data;
};
class Entity
{
public:
    Entity(const String& name) : m_Name(name)
    {
    }
    void PrintName() {
        m_Name.Print();
    }

    Entity(String&& name) : m_Name(std::move(name)) // std::move(name)也可以换成(String&&)name
    {
    }
private:
    String m_Name;
};
int main()
{
    Entity entity(String("Cherno"));
    entity.PrintName();
    std::cin.get();
}
//输出:
Created!
Moved!
Destroyed!
Cherno!
Destroyed!
  • Created → 临时对象构造。
  • Moved → 临时对象被移动构造到 m_Name
  • Destroyed → 移动后的临时对象析构(m_Datanullptr,安全)。
  • Cherno → 打印。
  • Destroyedentity 析构,释放移动接管的内存。
  1. 临时对象创建(同上)。
  2. Entity 构造函数接受 String&&,内部使用 std::move 触发移动构造 → 新对象直接接管临时对象的指针,将临时对象的指针置空。
  3. 临时对象析构时 delete 空指针,安全无害。

收益:一次内存分配,无数据拷贝,性能大幅提升。

1. 移动构造的标准写法

1
2
3
4
5
6
7
8
String(String&& other) noexcept
    : m_Size(other.m_Size)
    , m_Data(other.m_Data)
{
    // 将源对象置于“有效但未指定”状态,通常置空指针
    other.m_Data = nullptr;
    other.m_Size = 0;
}

必须注意

  • 移动后源对象必须保持可析构状态(析构时 delete nullptr 安全)。
  • 一般不依赖源对象的其他值,除非文档明确。

2. noexcept 的重要性

笔记提到 noexcept 指定符,这里补充容器优化的关键点

  • std::vector 在扩容时,如果元素的移动构造函数是 noexcept,则会使用移动来转移元素;否则为了异常安全,会退化为拷贝
  • 因此,移动构造和移动赋值应尽量声明为 noexcept

示例:

1
2
std::vector<String> vec;
vec.push_back(String("Hello")); // 若移动构造非 noexcept,扩容时可能触发拷贝

深度理解:有名字的右值引用是左值

1
2
3
4
5
void foo(String&& s)   // s 的类型是右值引用,但 s 本身是左值
{
    String s2 = s;                // 调用拷贝构造(s 是左值)
    String s3 = std::move(s);     // 调用移动构造
}

规则记忆

  • 判断表达式是左值还是右值,不能只看类型
  • 有名字的变量(即使类型是右值引用)在表达式中是左值
  • 如果想要对右值引用变量再次移动,必须使用 std::move

std::move 原理简析

std::move 本质上是一个 static_cast

1
2
3
4
template<typename T>
typename std::remove_reference<T>::type&& move(T&& t) noexcept {
    return static_cast<typename std::remove_reference<T>::type&&>(t);
}

它的唯一作用就是将表达式强制转换为右值引用,从而允许调用移动语义。

六、移动语义的现代实践补充

1. 移动赋值运算符

除移动构造外,赋值时也会用到移动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
String(String&& other) noexcept
    : m_Size(other.m_Size)
    , m_Data(other.m_Data)
{
    // 将源对象置于“有效但未指定”状态,通常置空指针
    other.m_Data = nullptr;
    other.m_Size = 0;
}

String& operator=(String&& other) noexcept {
    if (this != &other) {
        delete[] m_Data;           // 释放自身原有资源

        m_Data = other.m_Data;
        m_Size = other.m_Size;

        other.m_Data = nullptr;
        other.m_Size = 0;
    }
    return *this;
}

2. 完美转发与移动语义结合

在泛型函数中,使用 std::forward 保持参数的左右值属性。

1
2
3
4
5
6
7
//写一个工厂函数 makeEntity,它的工作是接收一个参数,然后原样传给 Entity 的构造函数。
// 我们希望这个函数能够:
// - 如果用户传左值,就调用拷贝构造
// - 如果用户传右值,就调用移动构造
Entity makeEntity(??? arg) {
    return Entity(arg);  // 问题:这里的 arg 永远是左值!
}

在函数内部,形参 arg 始终是一个有名字的变量,也就是左值。它永远只会调用拷贝构造函数,因为 arg 是左值。

3. 移动语义对返回值的优化

C++11 起,函数返回局部变量时,会优先尝试移动,甚至触发拷贝消除(RVO)

1
2
3
4
String createString() {
    String s("hello");
    return s;  // 移动或 RVO,几乎无开销,自动尝试把 return s; 变成 return std::move(s);。
}

七、总结要点

要点描述
移动语义目的避免临时对象深拷贝,提升性能
移动构造函数T(T&&),窃取资源,源对象指针置空
std::move将左值转为右值引用,标识“可移动”
noexcept保证移动操作不抛异常,影响 STL 容器优化
值类别陷阱右值引用类型的有名字变量是左值
最佳实践实现移动构造/赋值时,务必加 noexcept;析构函数妥善处理 nullptr

C++的参数计算顺序

在 C++ 中,函数参数的求值顺序是未指定的(unspecified),甚至在某些情况下会导致未定义行为(undefined behavior)。

示例

1
2
3
4
5
6
7
8
9
10
11
12
void PrintSum(int a, int b) {
    std::cout << a << " + " << b << " = " << a + b << std::endl;
}

int main() {
    int value = 0;
    PrintSum(value++, ++value);  //  未定义行为!
    int a = value++;    // a = 0, value = 1
    int b = ++value;    // b = 2, value = 2
    PrintSum(a, b);     // 输出:0 + 2 = 2(顺序确定)

}

没有任何可靠方法预测结果,value++和++value的执行顺序不确定,甚至可能崩溃,因此必须避免此类写法。

本文由作者按照 CC BY 4.0 进行授权