C++ part third
C++ part third
织智能指针
RAII与引用计数
- 引用计数这种计数是为了防止内存泄露而产生的。 基本想法是对于动态分配的对象,进行引用计数,每当增加一次对同一个对象的引用,那么引用对象的引用计数就会增加一次, 每删除一次引用,引用计数就会减一,当一个对象的引用计数减为零时,就自动删除指向的堆内存。
- 在传统 C++ 中,『记得』手动释放资源,总不是最佳实践。因为我们很有可能就忘记了去释放资源而导致泄露。 所以通常的做法是对于一个对象而言,我们在构造函数的时候申请空间,而在析构函数(在离开作用域时调用)的时候释放空间, 也就是我们常说的 RAII 资源获取即初始化技术。C++11 引入智能指针的概念,让程序员不再需要关心手动释放内存。使用它们需要包含头文件memory 。
std::unique_ptr
- std::unique_ptr
是一种独占的智能指针,它禁止其他智能指针与其共享同一个对象,从而保证代码的安全。
1
2
3
4
5
6
7std::unique_ptr<int> pointer = std::make_unique<int>(10); // make_unique 从 C++14 引入
std::unique_ptr<int> pointer2 = pointer; // 非法
make_unique 并不复杂,C++11 没有提供 std::make_unique ,可以自行实现:
template<typename T, typename ...Args>
std::unique_ptr<T> make_unique( Args&& ...args ) {
return std::unique_ptr<T>( new T( std::forward<Args>(args)... ) );
} - 既然是独占,换句话说就是不可复制。但是,我们可以利用 std::move
将其转移给其他的 unique_ptr 。
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
struct Foo {
Foo() { std::cout << "Foo::Foo" << std::endl; }
~Foo() { std::cout << "Foo::~Foo" << std::endl; }
void foo() { std::cout << "Foo::foo" << std::endl; }
};
void f(const Foo &) {
std::cout << "f(const Foo&)" << std::endl;
}
int main() {
std::unique_ptr<Foo> p1(std::make_unique<Foo>());
// p1 不空, 输出
if (p1) p1->foo();
{
std::unique_ptr<Foo> p2(std::move(p1));
// p2 不空, 输出
f(*p2);
// p2 不空, 输出
if(p2) p2->foo();
// p1 为空, 无输出
if(p1) p1->foo();
p1 = std::move(p2);
// p2 为空, 无输出
if(p2) p2->foo();
std::cout << "p2 被销毁" << std::endl;
}
// p1 不空, 输出
if (p1) p1->foo();
// Foo 的实例会在离开作用域时被销毁
} - 此外,由于独占, std::unique_ptr 不会有引用计数的开销,因此常常是首选。
std::shared_ptr
- std::shared_ptr 是一种智能指针,它能够记录多少个 shared_ptr 共同指向一个对象,从而消除显式的调用 delete ,当引用计数变为零的时候就会将对象自动删除。
- 但还不够,因为使用 std::shared_ptr 仍然需要使用 new 来调用,这使得代码出现了某种程度上的不对称。
- std::make_shared 就能够用来消除显式的使用 new
,会分配创建传入参数中的对象, 并返回这个对象类型的 std::shared_ptr
指针。
1
2
3
4
5
6
7
8
9
10
11
12
13
void foo(std::shared_ptr<int> i) {
(*i)++;
}
int main() {
// Constructed a std::shared_ptr
auto pointer = std::make_shared<int>(10);
foo(pointer);
std::cout << *pointer << std::endl; // 11
// The shared_ptr will be destructed before leaving the scope
return 0;
} - std::shared_ptr 可以通过 get() 方法来获取原始指针,通过 reset()
来减少一个引用计数, 并通 过 use_count() 来查看一个对象的引用计数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17auto pointer = std::make_shared<int>(10);
auto pointer2 = pointer; // 引用计数+1
auto pointer3 = pointer; // 引用计数+1
int *p = pointer.get(); // 这样不会增加引用计数
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 3
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 3
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 3
pointer2.reset();
std::cout << "reset pointer2:" << std::endl;
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 2
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 0, pointer2 已
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 2
pointer3.reset();
std::cout << "reset pointer3:" << std::endl;
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 1
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 0
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 0, pointer3
std::weak_ptr
- std::shared_ptr 引入了引用成环的问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21struct A;
struct B;
struct A {
std::shared_ptr<B> pointer;
~A() {
std::cout << "A 被销毁" << std::endl;
}
};
struct B {
std::shared_ptr<A> pointer;
~B() {
std::cout << "B 被销毁" << std::endl;
}
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->pointer = b;
b->pointer = a;
// 结果是A和B依然不能被销毁,因为二者间形成了一个环,导致两个shared_ptr引用计数都为1
} - 解决这个问题的办法就是使用弱引用指针 std::weak_ptr , std::weak_ptr 是一种弱引用(相比较而言 std::shared_ptr 就是一种强引用)。弱引用不会引起引用计数增加。
- 对于上面的代码,将 A 或 B 中的任意一个 std::shared_ptr 改为
std::weak_ptr 即可解决问题。
1
2
3
4
5
6
7
8
9
10
11
12
13struct A {
std::shared_ptr<B> pointer;
~A() {
std::cout << "A 被销毁" << std::endl;
}
};
struct B {
std::weak_ptr<A> pointer;
~B() {
std::cout << "B 被销毁" << std::endl;
}
};
// 将B的智能指针改为weak_ptr,这样程序结束时A的引用计数就会变成0而销毁,B的引用计数随之变为0而销毁 - std::weak_ptr 没有 * 运算符和 ->
运算符,所以不能够对资源进行操作,它可以用于检查std::shared_ptr
是否存在,其 expired() 方法能在资源未被释放时,会返回 false ,否则返回
true ;除此之外,它也可以用于获取指向原始对象的 std::shared_ptr 指针,其
lock() 方法在原 始对象未被释放时,返回一个指向原始对象的 std::shared_ptr
指针,进而访问原始对象的资源,否则返回默认构造的 std::shared_ptr
(即未托管任何指针)。
1
2
3
4
5
6
7
8
9
10
11std::weak_ptr<int> b;
{
auto a = std::make_shared<int>(1);
b = a;
if(auto c = b.lock()) { // 输出
std::cout << *c << std::endl;
}
}
if(auto c = b.expired()) { // 输出
std::cout << "b is expired" << std::endl;
}
模板与泛型编程
模板函数与模板类
- 模板函数是一种特殊类型的函数,它可以接受多种类型的参数。这是通过在编译时生成不同版本的函数来实现的。
1
2
3
4template <typename T>
T add(T a, T b) {
return a + b;
} - 在这个例子中,add函数是一个模板函数,它可以接受任何类型的参数(只要该类型支持+运算符)
1
2
3
4
5
6
7
8template <typename T>
class MyArray {
public:
T arr[10];
T get(int index) {
return arr[index];
}
}; - 在这个例子中,MyArray是一个模板类,它有一个类型为T的数组成员。
- 模板类和模板函数一样,都是在编译时生成的。这就像是你有一个食谱,你可以用它来做不同口味的蛋糕。
模板特化:特例处理
- 模板特化(Template Specialization)是模板编程中的一个高级特性,它允许你为某些特定类型或条件提供特殊的实现。
- 何时使用模板特化当你有一个通用模板,但需要为某个特定类型提供不同的实现时,模板特化就派上了用场。
1
2
3
4
5
6
7
8template <>
class MyArray<bool> {
public:
uint8_t arr[2]; // 使用位来存储bool值,以节省空间
bool get(int index) {
return arr[index / 8] & (1 << (index % 8));
}
}; - 在这个例子中,我们为bool类型提供了一个特化的MyArray类,以节省存储空间。
- 模板特化是一种非常强大的工具,但它也是一把双刃剑。正确使用时,它可以大大提高代码的效率和可读性。但如果滥用,可能会导致代码变得复杂和难以维护。
- 编译时常量(Compile-time Constants)
- 通过使用constexpr,你可以确保某些计算在编译时完成,从而提高运行时性能。
1
2
3
4template <typename T>
constexpr T square(T x) {
return x * x;
} - 在这个例子中,square函数是一个constexpr函数,这意味着当你用编译时常量作为参数调用它时,所有的计算都会在编译时完成。
- 模板和constexpr的结合使用可以产生高度优化的代码,这在高性能计算和嵌入式系统编程中是非常有价值的。
泛型编程:一份代码,多重任务
- 泛型编程(Generic
Programming)是一种编程范式,旨在通过一份代码来处理多种类型或多种情况。这种做法的目的是为了提高代码的复用性(Reusability)、可维护性(Maintainability)和类型安全性(Type
Safety)。
1
2
3
4template <typename T>
T add(T a, T b) {
return a + b;
} - 这个简单的
add
函数可以用于整数、浮点数、字符串等,无需为每种类型编写单独的函数。1
2
3
4
5
6
7
8template <typename T>
auto print(T value) {
if constexpr (std::is_integral_v<T>) {
std::cout << "Integral: " << value << std::endl;
} else {
std::cout << "Non-integral: " << value << std::endl;
}
} - 这里,if constexpr允许我们在同一个函数中处理不同的类型,而不需要多个函数重载或特化版本。
- SFINAE:编译时决策
- SFINAE允许你在编译时根据类型的特性来选择合适的函数重载。
1
2
3
4
5
6
7
8
9template <typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
void bar(T value) {
std::cout << "Integral: " << value << std::endl;
}
template <typename T, std::enable_if_t<!std::is_integral_v<T>, int> = 0>
void bar(T
value) {
std::cout << "Non-integral: " << value << std::endl;
} - 这里,std::enable_if_t用于在编译时启用或禁用某个函数重载。
- 类型萃取与SFINAE:完美搭档
- 类型萃取和SFINAE经常一起使用,以实现更复杂的编译时逻辑。
1
2
3
4
5
6
7
8template <typename T>
auto baz(T value) -> std::enable_if_t<std::is_integral_v<T>, T> {
return value * 2;
}
template <typename T>
auto baz(T value) -> std::enable_if_t<!std::is_integral_v<T>, T> {
return value;
} - 这里,我们使用了返回类型后置(Trailing Return Type)和std::enable_if_t,以便在编译时选择合适的函数版本。
- 这样的编程方式就像是你有一个“魔术师”的帽子,你可以从中拉出任何你需要的东西,而不必为每种情况都准备一个不同的帽子。
静态多态:模板与函数重载
- 静态多态则是在编译时(Compile-time)解析,因此没有运行时的性能开销。这是通过模板和函数重载(Function
Overloading)来实现的。
1
2
3
4
5
6
7template <typename T>
void print(T value) {
std::cout << value << std::endl;
}
void print(int value) {
std::cout << "Integer: " << value << std::endl;
}
STL
容器
- 顺序容器(sequence
container):每个元素都有固定的位置,位置取决于插入时间和地点,与元素的值无关
- vector
- deque(double end queue)
- list
- string
- forward_list
- 关联容器(associated
container):元素位置取决于元素的值,和插入顺序无关。
- set/multiset
- map/multimap
- 无序式容器(unordered container
- unordered_map/unordered_multimap
- unordered_set/unorder_multiset
迭代器
- iter.begin() / iter.end()
- ++iter
- ( * iter)
- iter != iter.end()
- new_iter = iter