C++
New features
C++11 引入了 {} 初始化表达式
C++11 引入了 range-based for-loop
C++14 的 lambda 允许用 auto 自动推断传入参数类型
C++17 CTAD / compile-time argument deduction / 编译期参数推断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include<iostream> #include<vector> #include<algorithm> using namespace std;
int main(int argc, char *argv[]) { vector v = {4,3,2,1};
int sum; for_each(v.begin(),v.end(),[&](auto vi){ sum += vi; });
cout << sum << endl;
return 0; }
|
C++20 引入区间(ranges),g++ 编译时指定 -std=gnu20
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <vector> #include <iostream> #include <numeric> #include <ranges> #include <cmath>
int main() { std::vector v = {4, 3, 2, 1, 0, -1, -2};
for (auto &&vi: v | std::views::filter([] (auto &&x) { return x >= 0; }) | std::views::transform([] (auto &&x) { return sqrtf(x); }) ) { std::cout << vi << std::endl; }
return 0; }
|
C++20 引入模块(module)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import <vector>; import <iostream>; import <numeric>; import <ranges>; import <cmath>;
int main() { std::vector v = {4, 3, 2, 1, 0, -1, -2};
for (auto &&vi: v | std::views::filter([] (auto &&x) { return x >= 0; }) | std::views::transform([] (auto &&x) { return sqrtf(x); }) ) { std::cout << vi << std::endl; }
return 0; }
|
C++20 允许函数参数为自动推断(auto)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import <vector>; import <iostream>; import <numeric>; import <ranges>; import <cmath>;
void myfunc(auto &&v) { for (auto &&vi: v | std::views::filter([] (auto &&x) { return x >= 0; }) | std::views::transform([] (auto &&x) { return sqrtf(x); }) ) { std::cout << vi << std::endl; } }
int main() { std::vector v = {4, 3, 2, 1, 0, -1, -2}; myfunc(v); return 0; }
|
C++23
引入协程(coroutine)和生成器(generator),注意需要切换到最新的编译器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import <vector>; import <iostream>; import <numeric>; import <ranges>; import <cmath>; import <generator>;
std::generator<int> myfunc(auto &&v) { for (auto &&vi: v | std::views::filter([] (auto &&x) { return x >= 0; }) | std::views::transform([] (auto &&x) { return sqrtf(x); }) ) { co_yield vi; } }
int main() { std::vector v = {4, 3, 2, 1, 0, -1, -2}; for (auto &&vi: myfunc(v)) { std::cout << vi << std::endl; } return 0; }
|
C++20 标准库加入 format 支持
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import <vector>; import <iostream>; import <numeric>; import <ranges>; import <cmath>; import <generator>; import <format>;
std::generator<int> myfunc(auto &&v) { for (auto &&vi: v | std::views::filter([] (auto &&x) { return x >= 0; }) | std::views::transform([] (auto &&x) { return sqrtf(x); }) ) { co_yield vi; } }
int main() { std::vector v = {4, 3, 2, 1, 0, -1, -2}; for (auto &&vi: myfunc(v)) { std::format_to(std::cout, "number is {}\n", vi); } return 0; }
|
C++思想:封装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include <stdlib.h> #include <stdio.h>
int main() { std::vector<int> v(4);
v[0] = 4; v[1] = 3; v[2] = 2; v[3] = 1;
int sum = 0; for (size_t i = 0; i < nv; i++) { sum += v[i]; }
printf("%d\n", sum);
free(v); return 0; }
|
比如要表达一个数组,需要:起始地址指针v,数组大小nv。
因此 C++ 的 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 25 26 27 28
| #include <stdlib.h> #include <stdio.h>
int main() { std::vector<int> v(2);
v[0] = 4; v[1] = 3;
v.resize(4);
v[2] = 2; v[3] = 1;
int sum = 0; for (size_t i = 0; i < nv; i++) { sum += v[i]; }
printf("%d\n", sum);
free(v); return 0; }
|
仅当出现“修改一个成员时,其他也成员要被修改,否则出错”的现象时,才需要getter/setter
封装。
各个成员之间相互正交,比如数学矢量类
Vec3,就没必要去搞封装,只会让程序员变得痛苦,同时还有一定性能损失。特别是当
getter/setter 函数分离了声明和定义,实现在另一个文件时。
C++思想:RAII(Resource
Acquisition Is Initialization)
资源获取视为初始化,反之,资源释放视为销毁
与 Java,Python 等垃圾回收语言不同,C++
的解构函数是显式的,离开作用域自动销毁,毫不含糊(有好处也有坏处,对高性能计算而言利大于弊)
异常安全(exception-safe)
C++ 标准保证当异常发生时,会调用已创建对象的解构函数。
因此 C++ 中没有(也不需要) finally 语句。
如果对时序有要求或对性能有要求就不能依靠
GC。比如 mutex 忘记 unlock 造成死锁等等……
自定义构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #include <iostream> #include <string>
struct Pig { std::string m_name; int m_weight;
Pig() { m_name = "佩奇"; m_weight = 80; } };
int main() { Pig pig;
std::cout << "name: " << pig.m_name << std::endl; std::cout << "weight: " << pig.m_weight << std::endl;
return 0; }
|
为什么需要初始化表达式?
假如类成员为 const 和引用
假如类成员没有无参构造函数
避免重复初始化,更高效
构造函数:单个参数(避免陷阱)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #include <iostream> #include <string>
struct Pig { std::string m_name; int m_weight;
Pig(int weight) : m_name("一只重达" + std::to_string(weight) + "kg的猪") , m_weight(weight) {} };
int main() { Pig pig = 80;
std::cout << "name: " << pig.m_name << std::endl; std::cout << "weight: " << pig.m_weight << 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
| #include <iostream> #include <string>
struct Pig { std::string m_name; int m_weight;
explicit Pig(int weight) : m_name("一只重达" + std::to_string(weight) + "kg的猪") , m_weight(weight) {} };
int main() { Pig pig(80);
std::cout << "name: " << pig.m_name << std::endl; std::cout << "weight: " << pig.m_weight << std::endl;
return 0; }
|
避免 80 被隐式转换为 pig 类,使用 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
| #include <iostream> #include <string>
struct Pig { std::string m_name; int m_weight;
explicit Pig(int weight) : m_name("一只重达" + std::to_string(weight) + "kg的猪") , m_weight(weight) {} };
void show(Pig pig) { std::cout << "name: " << pig.m_name << std::endl; std::cout << "weight: " << pig.m_weight << std::endl; }
int main() { show(Pig(80));
return 0; }
|
比如 std::vector 的构造函数 vector(size_t n) 也是 explicit 的
使用 {} 和 ()
调用构造函数,有什么区别?
int(3.14f) 不会出错,但是 int{3.14f} 会出错,因为 {}
是非强制转换。
Pig(“佩奇”, 3.14f) 不会出错,但是 Pig{“佩奇”, 3.14f}
会出错,原因同上,更安全。
可读性:Pig(1, 2) 则 Pig 有可能是个函数,Pig{1, 2}
看起来更明确。
在 C++ 中:
- 使用 static_cast<int>(3.14f) 而不是 int(3.14f)
- 使用 reinterpret_cast<void >(0xb8000) 而不是 (void
)0xb8000
更加明确用的哪一种类型转换(cast),从而避免一些像是
static_cast(ptr) 的错误。
编译器默认生成的构造函数:无参数
默认生成的构造函数,这些类型不会被初始化为
0:
1.int, float, double 等基础类型
2.void , Object 等指针类型
3.完全由这些类型组成的类
这些类型被称为 POD(plain-old-data).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #include <iostream> #include <string>
struct Pig { std::string m_name; int m_weight; };
void show(Pig pig) { std::cout << "name: " << pig.m_name << std::endl; std::cout << "weight: " << pig.m_weight << std::endl; }
int main() { Pig pig;
show(pig); return 0; }
|
可以手动指定初始化 weight 为0。
通过 {}
语法指定的初始化值,会在编译器自动生成的构造函数里执行。
| struct Pig { std::string m_name; int m_weight{0}; };
|
通过 {}
语法指定的初始化值,不仅会在编译器自动生成的构造函数里执行,也会在用户自定义构造函数里执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #include <iostream> #include <string>
struct Pig { std::string m_name; int m_weight{0};
Pig(std::string name) : m_name(name) {} };
void show(Pig pig) { std::cout << "name: " << pig.m_name << std::endl; std::cout << "weight: " << pig.m_weight << std::endl; }
int main() { Pig pig("佩奇");
show(pig); return 0; }
|
类成员的 {} 中还可以有多个参数,甚至能用 =,当然 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
| #include <iostream> #include <string>
struct Demo { explicit Demo(std::string a, std::string b) { std::cout << "Demo(" << a << ',' << b << ')' << std::endl; } };
struct Pig { std::string m_name{"佩奇"}; int m_weight = 80; Demo m_demo{"Hello", "world"}; };
void show(Pig pig) { std::cout << "name: " << pig.m_name << std::endl; std::cout << "weight: " << pig.m_weight << std::endl; }
int main() { Pig pig;
show(pig); return 0; }
|
另外:
与
| int x{0}; void *p{nullptr};
|
等价,都会零初始化。
std::cout << int{}; 会打印出 0
当一个类(和他的基类)没有定义任何构造函数,这时编译器会自动生成一个参数个数和成员一样的构造函数。
他会将 {}
内的内容,会按顺序赋值给对象的每一个成员。目的是为了方便程序员不必手写冗长的构造函数一个个赋值给成员。
且初始化列表的构造函数只支持通过 {} 或 = {} 来构造,不支持通过 ()
构造 (为了向下兼容 C++98)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #include <iostream> #include <string>
struct Pig { std::string m_name; int m_weight; };
void show(Pig pig) { std::cout << "name: " << pig.m_name << std::endl; std::cout << "weight: " << pig.m_weight << std::endl; }
int main() { Pig pig1 = {"佩奇", 80}; Pig pig2{"佩奇", 80};
show(pig1); return 0; }
|
所以可以使用默认构造函数类,解决函数多返回值(妙用)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| struct { bool hit; Vec3 pos; Vec3 normal; float depth; } intersect(Ray r) { ... return {true, r.origin, r.direction, 233.0f}; }
int main() { Ray r; auto hit = intersect(r); if (hit.hit) { r.origin = hit.pos; r.direction = hit.normal; ... } }
|
和 std::tuple 相比,最大的好处是每个属性都有名字,不容易搞错。
函数的参数,如果是很复杂的类型,你不想把类型名重复写一遍,也可以利用
{} 初始化列表来简化
| void func(std::tuple<int, float, std::string> arg, std::vector<int> arr) { ... }
int main() { func({1, 3.14f, "佩奇"}, {1, 4, 2, 8, 5, 7}); func(std::tuple<int, float, std::string>(1, 3.14f, "佩奇"), std::vector<int>({1, 4, 2, 8, 5, 7})); func(std::tuple(1, 3.14f, "佩奇"), std::vector({1, 4, 2, 8, 5, 7})); }
|
一旦我们定义了自己的构造函数,编译器就不会再生成默认的无参构造函数。
有自定义构造函数时仍想用默认构造函数:= default
| struct Pig { std::string m_name; int m_weight{0};
Pig() = default;
Pig(std::string name, int weight) : m_name(name), m_weight(weight) {} };
|
拷贝构造函数
编译器默认会生成拷贝构造函数:Pig(Pig 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
| #include <iostream> #include <string>
struct Pig { std::string m_name; int m_weight{0}; };
void show(Pig pig) { std::cout << "name: " << pig.m_name << std::endl; std::cout << "weight: " << pig.m_weight << std::endl; }
int main() { Pig pig{"佩奇", 80};
show(pig);
Pig pig2 = pig;
show(pig);
return 0; }
|
拷贝赋值函数
编译器默认还会生成这样一个重载’=’这个运算符的函数:
Pig &operator=(Pig const &other);
| Pig pig = pig2;
Pig pig; pig = pig2;
|
追求性能时推荐用拷贝构造,因为可以避免一次无参构造,拷贝赋值是出于需要临时修改对象的灵活性需要。
如何避免不经意的隐式拷贝
将拷贝构造函数声明为 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
| #include <iostream> #include <string>
struct Pig { std::string m_name; int m_weight{0};
Pig(std::string name, int weight) : m_name(name), m_weight(weight) {}
explicit Pig(Pig const &other) = default; };
void show(Pig pig) { std::cout << "name: " << pig.m_name << std::endl; std::cout << "weight: " << pig.m_weight << std::endl; }
int main() { Pig pig{"佩奇", 80};
show(Pig{pig}); return 0; }
|
自动生成的特殊函数
| struct C { C();
C(C const &c); C(C &&c); C &operator=(C const &c); C &operator=(C &&c);
~C(); };
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| #include <iostream> #include <string>
struct Pig { std::string m_name; int m_weight{0};
Pig(std::string name, int weight) : m_name(name), m_weight(weight) {}
Pig() {}
Pig(Pig const &other) : m_name(other.m_name) , m_weight(other.m_weight) {}
Pig &operator=(Pig const &other) { m_name = other.m_name; m_weight = other.m_weight; return *this; }
Pig(Pig &&other) : m_name(std::move(other.m_name)) , m_weight(std::move(other.m_weight)) {}
Pig &operator=(Pig &&other) { m_name = std::move(other.m_name); m_weight = std::move(other.m_weight); return *this; }
~Pig() {} };
void show(Pig pig) { std::cout << "name: " << pig.m_name << std::endl; std::cout << "weight: " << pig.m_weight << std::endl; }
int main() { Pig pig;
show(pig); return 0; }
|
如果其中一个成员(比如m_name)不支持拷贝构造函数,那么
Pig 类的拷贝构造函数将不会被编译器自动生成。
设计规则 经验
- 如果一个类定义了析构函数,那么您必须同时定义或删除
拷贝构造函数和拷贝赋值函数,否则出错。
- 如果一个类定义或删除了拷贝构造函数,那么您必须同时定义或删除
拷贝赋值函数,否则出错,删除可导致低效。
- 如果一个类定义了移动构造函数,那么您必须同时定义或删除
移动赋值函数,否则出错,删除可导致低效。
- 如果一个类定义了拷贝构造函数或拷贝赋值函数,那么您必须最好同时定义
移动构造函数或
移动赋值函数,否则低效。
例如:
在 = 时,默认是会拷贝的。
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
| #include <cstdlib> #include <iostream> #include <cstring>
struct Vector { size_t m_size; int *m_data;
Vector(size_t n) { m_size = n; m_data = (int *)malloc(n * sizeof(int)); }
~Vector() { free(m_data); }
size_t size() { return m_size; }
void resize(size_t size) { m_size = size; m_data = (int *)realloc(m_data, m_size); }
int &operator[](size_t index) { return m_data[index]; } };
int main() { Vector v1(32);
Vector v2 = v1;
return 0; }
|
| free(): double free detected in tcache 2
|
当前 Vector 的实现造成一个很大的问题:其 m_data
指针是按地址值浅拷贝的,而不深拷贝其指向的数组!
在退出 main 函数作用域的时候,v1.m_data 会被释放两次!更危险的则是 v1
被解构而 v2 仍在被使用的情况。
这就是为什么“如果一个类定义了解构函数,那么您必须同时定义或删除拷贝构造函数和拷贝赋值函数,否则出错。”
最简单的办法是,直接禁止用户拷贝这个类的对象,在 C++11 中可以用 =
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
| struct Vector { size_t m_size; int *m_data;
Vector(size_t n) { m_size = n; m_data = (int *)malloc(n * sizeof(int)); }
~Vector() { free(m_data); }
Vector(Vector const &other) = delete;
size_t size() { return m_size; }
void resize(size_t size) { m_size = size; m_data = (int *)realloc(m_data, m_size); }
int &operator[](size_t index) { return m_data[index]; } };
|
如果需要允许用户拷贝你的 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 25 26 27 28 29 30 31 32
| struct Vector { size_t m_size; int *m_data;
Vector(size_t n) { m_size = n; m_data = (int *)malloc(n * sizeof(int)); }
~Vector() { free(m_data); }
Vector(Vector const &other) { m_size = other.m_size; m_data = (int *)malloc(m_size * sizeof(int)); memcpy(m_data, other.m_data, m_size * sizeof(int)); }
size_t size() { return m_size; }
void resize(size_t size) { m_size = size; m_data = (int *)realloc(m_data, m_size); }
int &operator[](size_t index) { return m_data[index]; } };
|
区分拷贝构造和拷贝赋值
区分两种拷贝可以提高性能。
int x = 1; // 拷贝构造函数 x = 2; // 拷贝赋值函数
拷贝赋值函数 ≈ 解构函数 + 拷贝构造函数。
拷贝构造:直接未初始化的内存上构造 2 拷贝赋值:先销毁现有的
1,再重新构造 2
| ... Vector(Vector const &other) { m_size = other.m_size; m_data = (int *)malloc(m_size * sizeof(int)); memcpy(m_data, other.m_data, m_size * sizeof(int)); }
Vector &operator=(Vector const &other) { this->~Vector(); new (this) Vector(other); return *this; } ...
|
更高效的写法
| Vector &operator=(Vector const &other) { m_size = other.m_size; m_data = (int *)realloc(m_data, m_size * sizeof(int)); memcpy(m_data, other.m_data, m_size * sizeof(int)); return *this; }
|
内存的销毁重新分配可以通过realloc,从而就地利用当前现有的m_data,避免重新分配。
拷贝和移动
时间复杂度:移动是 O(1),拷贝是 O(n)。
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> #include <vector>
void test_copy() { std::vector<int> v1(10); std::vector<int> v2(200);
v1 = v2;
std::cout << "after copy:" << std::endl; std::cout << "v1 length " << v1.size() << std::endl; std::cout << "v2 length " << v2.size() << std::endl; }
void test_move() { std::vector<int> v1(10); std::vector<int> v2(200);
v1 = std::move(v2);
std::cout << "after move:" << std::endl; std::cout << "v1 length " << v1.size() << std::endl; std::cout << "v2 length " << v2.size() << std::endl; }
void test_swap() { std::vector<int> v1(10); std::vector<int> v2(200);
std::swap(v1, v2);
std::cout << "after swap:" << std::endl; std::cout << "v1 length " << v1.size() << std::endl; std::cout << "v2 length " << v2.size() << std::endl; }
int main() { test_copy(); test_move(); test_swap(); return 0; }
|
swap 可能是这样实现的:
| template <class T> void swap(T& t1, T& t2) { T tmp = std::move(t2); t2 = std::move(t1); t1 = std::move(tmp); }
|
swap 在高性能计算中可以用来实现双缓存(ping-pong buffer)。
哪些情况会触发“移动”
这些情况下编译器会调用移动:
| return v2; v1 = std::vector<int>(200); v1 = std::move(v2);
|
这些情况下编译器会调用拷贝:
| return std::as_const(v2) v1 = v2
|
注意,下面两个语句没有任何作用:
这两个函数只是负责转换类型,实际产生移动/拷贝效果的是在类的构造/赋值函数里。
| std::move(v2) std::as_const(v2)
|
移动构造函数
默认移动构造和移动赋值,编译器会自动这样做:
移动构造≈拷贝构造+他解构+他默认构造
移动赋值≈拷贝赋值+他解构+他默认构造
虽然低效,但至少可以保证不出错。
若自定义了移动构造,则: 移动赋值≈解构(当前自身的资源)+
移动构造
| Vector(Vector &&other) { m_size = other.m_size; other.m_size = 0; m_data = other.m_data; other.m_data = nullptr; }
Vector &operator=(Vector &&other) { this->~Vector(); new (this) Vector(std::move(other)); return *this; }
|
如果有移动赋值函数,可以删除拷贝赋值函数。那么,当用户调用:
时,因为拷贝赋值被删除,编译器会尝试:
从而先调用拷贝构造函数(拷贝v1),然后因为 List(v1)
相当于就地构造的对象,从而变成了移动语义,从而进一步调用移动赋值函数。
智能指针
unique_ptr
C++11 引入了 unique_ptr 容器,他的解构函数中会调用 delete p
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| #include <cstdio> #include <cstdlib>
struct C { C() { printf("分配内存!\n"); }
~C() { printf("释放内存!\n"); } };
int main() { C *p = new C;
if (rand() != 0) { printf("出了点小状况……\n"); return 1; }
delete p; return 0; }
|
unique_ptr 则把下面连个操作封装成一个操作
只需要:
= nullptr 时,就释放了 unique_ptr 。
| int main() { std::unique_ptr<C> p = std::make_unique<C>();
if (1 + 1 == 2) { printf("出了点小状况……\n"); return 1; }
return 0; }
|
禁止拷贝
| int main() { std::unique_ptr<C> p = std::make_unique<C>(); func(p); return 0; }
|
因为 unique_ptr 删除了拷贝构造函数。如果要拷贝,有以下方法:
第一种是获取原始指针,你的 func()
实际上并不需要“夺走”资源的占有权(ownership)。
func() 只是调用了 p
的某个成员函数而已,并没有接过掌管对象生命周期的大权。
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 <cstdio> #include <memory>
struct C { C() { printf("分配内存!\n"); }
~C() { printf("释放内存!\n"); }
void do_something() { printf("成员函数!\n"); } };
void func(C *p) { p->do_something(); }
int main() { std::unique_ptr<C> p = std::make_unique<C>(); func(p.get()); return 0; }
|
第二种是移动,你的 func()
需要“夺走”资源的占有权。
func 把指针放到一个全局的列表里,p 的生命周期将会变得和 objlist
一样长。因此需要接过掌管对象生命周期的大权。
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 <cstdio> #include <memory> #include <vector>
struct C { C() { printf("分配内存!\n"); }
~C() { printf("释放内存!\n"); } void do_something() { printf("我的数字是 %d!\n", m_number); } };
std::vector<std::unique_ptr<C>> objlist;
void func(std::unique_ptr<C> p) { objlist.push_back(std::move(p)); }
int main() { std::unique_ptr<C> p = std::make_unique<C>(); printf("移交前:%p\n", p.get()); func(std::move(p)); printf("移交后:%p\n", p.get()); return 0; }
|
| 分配内存! 移交前:0x55ec91b162b0 移交后:(nil) 释放内存!
|
如果,移交控制权后仍希望访问到 p
指向的对象。最简单的办法是,在移交控制权给 func
前,提前通过 p.get() 获取原始指针。
| int main() { std::unique_ptr<C> p = std::make_unique<C>(); func(std::move(p));
p->do_something();
return 0; }
|
| int main() { std::unique_ptr<C> p = std::make_unique<C>();
C *raw_p = p.get(); func(std::move(p));
raw_p->do_something();
return 0; }
|
不过你得保证 raw_p 的存在时间不超过 p
的生命周期,否则会出现危险的空悬指针。
| int main() { std::unique_ptr<C> p = std::make_unique<C>();
C *raw_p = p.get(); func(std::move(p));
raw_p->do_something();
objlist.clear();
raw_p->do_something();
return 0; }
|
| 分配内存! 我的数字是 42! 释放内存! 我的数字是 -803182608!
|
shared_ptr
牺牲效率换来自由度的 shared_ptr
则允许拷贝,他解决重复释放的方式是通过引用计数。
当一个 shared_ptr 初始化时,将计数器设为1。 当一个 shared_ptr
被拷贝时,计数器加1。 当一个 shared_ptr
被解构时,计数器减1。减到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
| #include <cstdio> #include <memory> #include <vector>
struct C { int m_number;
C() { printf("分配内存!\n"); m_number = 42; }
~C() { printf("释放内存!\n"); m_number = -2333333; }
void do_something() { printf("我的数字是 %d!\n", m_number); } };
std::vector<std::shared_ptr<C>> objlist;
void func(std::shared_ptr<C> p) { objlist.push_back(std::move(p)); }
int main() { std::shared_ptr<C> p = std::make_shared<C>();
func(p); func(p);
p->do_something();
objlist.clear();
p->do_something();
return 0; }
|
只要还有存在哪怕一个指针指向该对象,就不会被解构。
可以使用 p.use_count() 来获取当前指针的引用计数。
注意 p.func() 是 shared_ptr 类型本身的成员函数,而
p->func() 是 p 指向对象(也就是
C)的成员函数,不要混淆。
weak_ptr
弱引用的拷贝与解构不影响其引用计数器。
可以通过 lock() 随时产生一个新的 shared_ptr 作为强引用。但不
lock 的时候不影响计数。
如果失效(计数器归零)则 expired() 会返回 false,且 lock() 也会返回
nullptr。
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
| int main() { std::shared_ptr<C> p = std::make_shared<C>();
printf("use count = %ld\n", p.use_count());
std::weak_ptr<C> weak_p = p;
printf("use count = %ld\n", p.use_count());
func(std::move(p));
if (weak_p.expired()) printf("错误:弱引用已失效!"); else weak_p.lock()->do_something();
objlist.clear();
if (weak_p.expired()) printf("错误:弱引用已失效!"); else weak_p.lock()->do_something();
return 0; }
|
| 分配内存 use count = 1 use count = 1 我的数字是 42 释放内存 错误:弱引用已失效!
|
智能指针作为类的成员变量
判断要用哪一种智能指针:
- unique_ptr:当该指针所指对象仅仅属于我时。比如:父窗口中指向子窗口的指针。
- 原始指针:当该对象不属于我,但他释放前我必然被释放时。有一定风险。对象使用指针。比如:子窗口中指向父窗口的指针。
- shared_ptr:当该对象由多个对象共享时,或虽然该对象仅仅属于我,但有使用
weak_ptr 的需要,对象使用shared_ptr。
- weak_ptr:当该对象不属于我,且他释放后我仍可能不被释放时,对象使用weak_ptr。比如:指向窗口中上一次被点击的元素。
- 初学者可以多用 shared_ptr 和 weak_ptr 的组合,更安全。
shared_ptr 需要维护一个 atomic
的引用计数器,效率低,需要额外的一块管理内存,访问实际对象需要二级指针,而且
deleter 使用了类型擦除技术。
全部用
shared_ptr,可能出现循环引用之类的问题,导致内存泄露,依然需要使用不影响计数的原始指针或者
weak_ptr 来避免。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include <memory>
struct C { std::shared_ptr<C> m_child; std::shared_ptr<C> m_parent; };
int main() { auto parent = std::make_shared<C>(); auto child = std::make_shared<C>();
parent->m_child = child; child->m_parent = parent;
parent = nullptr; child = nullptr;
return 0; }
|
如何解决?只需要把其中逻辑上“不具有所属权”的那一个改成 weak_ptr
即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include <memory>
struct C { std::shared_ptr<C> m_child; std::weak_ptr<C> m_parent; };
int main() { auto parent = std::make_shared<C>(); auto child = std::make_shared<C>();
parent->m_child = child; child->m_parent = parent;
parent = nullptr; child = nullptr;
return 0; }
|
改成 weak_ptr, 没有 parent 的所有权。
也可以把 m_parent 变成原始指针。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include <memory>
struct C { std::shared_ptr<C> m_child; C *m_parent; };
int main() { auto parent = std::make_shared<C>(); auto child = std::make_shared<C>();
parent->m_child = child; child->m_parent = parent.get();
parent = nullptr; child = nullptr;
return 0; }
|
假定他释放前我必然被释放,完全可以把 m_child
变成一个标志这“完全所有权”的 unique_ptr。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include <memory>
struct C { std::unique_ptr<C> m_child; C *m_parent; };
int main() { auto parent = std::make_unique<C>(); auto child = std::make_unique<C>();
child->m_parent = parent.get(); parent->m_child = std::move(child);
parent = nullptr;
return 0; }
|
安全类型
以下类型是安全的:
| int id; std::vector<int> arr; std::shared_ptr<Object> child; Object *parent;
|
以下对象是不安全的:
| char *ptr; GLint tex; std::vector<Object *> objs;
|
有不安全类型成员的对象的类的设计需要考虑之前的设计原则问题。
如果你的类所有成员,都是安全的类型,那么五大构造/析构函数都无需声明(或声明为
= default),你的类自动就是安全的。
最好的判断方式是:如果你不需要自定义的解构函数,那么这个类就不需要担心。
因为如果用到了自定义解构函数,往往意味着你的类成员中,包含有不安全的类型。
这样的类型一般无外乎两种情况:
你的类管理着资源。
你的类是数据结构。
管理着资源的类
这个类管理着某种资源,资源往往不能被“复制”。比如一个 OpenGL
的着色器,或是一个 Qt 的窗口。
一般删除拷贝函数,然后统一用智能指针管理。
| struct Shader { GLuint sha; GLuint target{GL_ARRAY_BUFFER};
Shader(GLuint type) { CHECK_GL(sha = g1CreateShader(type)); } ~Shader() { CHECK_GL(g1DeIeteShader(sha)); }
Shader(Shader const &) = delete; Shader &operator=(Shader const &) = delete;
};
|
数据结构类
如果可以,自己定义拷贝和移动函数。
函数参数类型优化
如果是基础类型(比如 int,float)则按值传递: float
squareRoot(float val);
如果是原始指针(比如 int ,Object
)则按值传递: void doSomethingWith(Object
*ptr);
如果是数据容器类型(比如
vector,string)则按常引用传递: int
sumArray(std::vector const &arr);
如果数据容器不大(比如 tuple<int,
int>),则其实可以按值传递: glm::vec3
calculateGravityAt(glm::vec3 pos);
如果是智能指针(比如
shared_ptr),且需要生命周期控制权,则按值传递: void
addObject(std::shared_ptr
如果是智能指针,但不需要生命周期,则通过 .get()
获取原始指针后,按值传递: void
modifyObject(Object *obj);
其他语言对比
Java 和 Python
的业务需求大多是在和资源打交道,从而基本都是刚刚说的要删除拷贝函数的那一类。
Java 和 Python
干脆简化:一切非基础类型的对象都是浅拷贝,引用计数由垃圾回收机制自动管理。
以系统级编程、算法数据结构、高性能计算为主要业务的
C++,才发展出了这些思想,并将拷贝/移动/指针/可变性/多线程等概念作为语言基本元素存在。