1.3.1 C++新特性
1.智能指针
1. 为什么要用智能指针
- 内存泄漏:内存手动释放,使用智能指针可以自动释放
malloc free; new delete - 共享所有权指针的传播和释放,比如多线程使用同一个对象时析构问题
2. 三种智能指针对比
C++ 中提供了三种智能指针:unique_ptr
、shared_ptr
和 weak_ptr
。它们分别用于不同的场景,有不同的内存管理特点。下面是它们的对比:auto_ptrC++11已经弃用
智能指针 | 所有权管理 | 引用计数 | 性能 | 优点 | 缺点 |
---|---|---|---|---|---|
unique_ptr | 独占对象的所有权 | 无 | 高性能 | - 独占所有权,确保没有多个指针指向同一个对象 - 自动释放内存,避免内存泄漏 | - 不能复制,只能移动 - 只能有一个拥有者 |
shared_ptr | 共享对象的所有权 | 有 | 性能较差(因引用计数管理) | - 可以在多个指针间共享所有权 - 引用计数机制,多个 shared_ptr 可以管理同一个对象 | - 引用计数机制开销较大 - 循环引用问题 |
weak_ptr | 解决 shared_ptr 的循环引用问题 | 无 | 性能较好 | - 用于打破 shared_ptr 之间的循环引用- 不增加引用计数,不干扰对象生命周期 | - 不能直接访问对象,需要通过 shared_ptr 转化 |
3. shared_ptr
1. 使用智能指针可以自动释放占用的内存
shared_ptr<Buffer> buf=make_shared<Buffer>("auto free memory");// Buffer对象分配在堆上,但能自动释放
/*
解释:
1. make_shared<Buffer>("auto free memory"):
是 C++ 标准库提供的一个函数,它创建一个 shared_ptr,并通过动态分配内存来创建 Buffer 对象。它传递给 Buffer 类构造函数一个字符串 "auto free memory",用于初始化 Buffer 对象的成员。
该函数内部会先为 Buffer 对象分配内存,然后为 shared_ptr 自身分配内存,最后将 Buffer 对象的地址交给 shared_ptr 来管理。
2. shared_ptr<Buffer> buf:
buf 是一个智能指针,类型为 shared_ptr<Buffer>。它持有 Buffer 对象的所有权,并负责在最后一个 shared_ptr 被销毁时自动释放内存。
shared_ptr 使用引用计数机制来管理内存,当没有 shared_ptr 再引用这个对象时,它会自动销毁并释放 Buffer 对象的内存。
3. 自动释放:
shared_ptr 在对象引用计数为 0 时,会自动释放内存。这意味着当 buf 超出作用域,或者 buf 被重新赋值时,Buffer 对象的内存会被自动释放。
由于 shared_ptr 管理着内存的生命周期,所以你不需要手动调用 delete 来释放 Buffer 对象。
*/
Buffer *buf = new Buffer("auto free memory");// Buffer对象分配在堆上,但需要手动delete释放
/*
1. new Buffer("auto free memory"):
这里使用 new 操作符动态分配了一个 Buffer 对象。Buffer 的构造函数接受一个字符串参数 "auto free memory",并用它初始化 Buffer 对象的成员。
new 操作符会在堆上分配内存,并返回一个指向该内存块的指针。
2. Buffer *buf:
buf 是一个指向 Buffer 对象的原生指针(Buffer*)。
你需要手动管理对象的生命周期。当不再需要该对象时,你必须手动调用 delete 来释放它所占用的内存,否则会导致内存泄漏。
*/
2. 共享所有权指针的传播和释放
进程处理完之后会释放,count共享,析构会释放
shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。再最后一个shared_ptr析构的时候,内存才会被释放
shared_ptr实现包含了两部分,
1. 一个指向堆上创建的对象的裸指针,raw_ptr
2. 一个指向内部隐藏的、共享的管理对象。share_count_object 引用计数
3. 常用函数
/*
1. make_shared:
std::make_shared<Buffer>("Hello, World!") 会创建一个 shared_ptr,并在堆上动态分配 Buffer 对象。这是推荐的创建 shared_ptr 的方式,它避免了 new 的多次内存分配。
2. get:
buf1.get() 返回 shared_ptr 内部管理的原生指针,允许直接操作对象,但要小心对象的生命周期,因为 shared_ptr 的引用计数会管理对象的释放。
3.use_count:
buf1.use_count() 返回当前有多少个 shared_ptr 共享该对象的所有权。在示例中,buf1 和 buf2 共享 Buffer 对象,所以返回的值为 2。
4.reset:
buf1.reset() 会解除 buf1 对原对象的所有权,并使它指向一个新的对象(如果给定了新的对象)。当 reset 被调用后,buf1 不再管理原对象,原对象如果没有其他 shared_ptr 引用,会被销毁。
5.unique:
buf1.unique() 检查当前 shared_ptr 是否唯一指向对象。如果当前 shared_ptr 唯一指向对象(没有其他 shared_ptr 共享所有权),它返回 true;否则返回 false。
6.swap:
buf1.swap(buf3) 交换 buf1 和 buf3 的所有权,即它们各自指向的对象也交换。调用后,buf1 会指向 buf3 原来的对象,buf3 会指向 buf1 原来的对象。
*/
#include <iostream>
#include <memory>
class Buffer {
public:
explicit Buffer(const std::string& msg) : message(msg) {
std::cout << "Buffer created: " << message << std::endl;
}
~Buffer() {
std::cout << "Buffer destroyed." << std::endl;
}
void print() { std::cout << "Message: " << message << std::endl; }
private:
std::string message;
};
int main() {
// 使用 make_shared 创建 shared_ptr,推荐的创建方式
// make_shared 会在内部同时分配 memory for shared_ptr 和对象,避免两次分配
std::shared_ptr<Buffer> buf1 = std::make_shared<Buffer>("Hello, World!");
// 使用 get() 获取 shared_ptr 内部的原生指针,原生指针可以直接操作对象
Buffer* rawPtr = buf1.get(); // 获取原生指针
rawPtr->print(); // 通过原生指针访问成员函数
// 使用 use_count() 获取当前 shared_ptr 的引用计数
std::cout << "use_count: " << buf1.use_count() << std::endl; // 输出 1,因为只有 buf1 指向该对象
// 创建另一个 shared_ptr buf2,并共享 buf1 的所有权
std::shared_ptr<Buffer> buf2 = buf1;
std::cout << "use_count after buf2: " << buf1.use_count() << std::endl; // 输出 2,因为 buf1 和 buf2 都指向同一个对象
// 使用 reset() 来重置智能指针的所有权,这会解除与原对象的关联
// 重置后,buf1 将不再管理原来的对象
buf1.reset(new Buffer("New Buffer"));
// 此时 buf1 指向新对象,buf2 仍然指向原对象
buf1->print(); // 输出 "Message: New Buffer"
std::cout << "use_count after reset: " << buf1.use_count() << std::endl; // 输出 1,因为 buf1 唯一指向新对象
std::cout << "use_count of buf2: " << buf2.use_count() << std::endl; // 输出 1,buf2 仍然指向原对象
// 使用 unique() 判断 shared_ptr 是否唯一指向对象
if (buf1.unique()) {
std::cout << "buf1 is the unique owner of the object." << std::endl;
} else {
std::cout << "buf1 is not the unique owner." << std::endl;
}
// 使用 swap() 交换 buf1 和 buf3 的所有权
std::shared_ptr<Buffer> buf3 = std::make_shared<Buffer>("Buffer 3");
buf1.swap(buf3); // 交换 buf1 和 buf3 的所有权
buf1->print(); // 输出 "Message: Buffer 3"
buf3->print(); // 输出 "Message: New Buffer"
// 最后,buf1、buf2、buf3 超出作用域时,自动销毁相应的对象
return 0;
}
- shared_ptr不能通过“直接将原始这种赋值”来初始化
- std::shared_ptr 管理动态数组时,需要指定自定义删除器,因为 shared_ptr 的默认删除器(即 delete)不能直接用于删除数组。对于数组类型的对象,我们必须使用 delete[] 来正确地释放内存。
std::shared_ptr<int[]> p3(new int[10], [](int* p) { delete[] p; });
/*
std::shared_ptr<int[]> p3:shared_ptr 类型为 int[],即管理一个动态数组。
new int[10]:创建一个包含 10 个 int 元素的动态数组,内存分配在堆上。
[](int* p) { delete[] p; }:这是一个 lambda 表达式,定义了一个自定义删除器,负责调用 delete[] 来释放数组内存
*/
4. 要注意的问题
- 不要用一个原始指针初始化多个 shared_ptr
解释:
std::shared_ptr 是通过引用计数来管理内存的。每当你创建一个新的 shared_ptr 来管理同一个原始指针时,引用计数就会增加。但是,如果多个 shared_ptr 使用相同的原始指针初始化,它们将共享同一引用计数,而这可能导致重复删除(多次调用 delete),从而引发未定义行为。
错误示例:
#include <memory>
int main() {
int* p = new int(10);
std::shared_ptr<int> ptr1(p); // ptr1 使用原始指针 p 初始化
std::shared_ptr<int> ptr2(p); // 错误:ptr2 也使用原始指针 p 初始化,导致 double delete
return 0;
}
在此示例中,ptr1 和 ptr2 都管理同一个原始指针 p,这将导致 delete 被调用两次,从而引发未定义行为。
正确的做法:
始终使用 std::make_shared 或一个新的指针来创建 shared_ptr,以确保引用计数管理是安全的。
#include <memory>
int main() {
auto ptr1 = std::make_shared<int>(10); // 使用 make_shared 正确创建 shared_ptr
auto ptr2 = ptr1; // ptr2 和 ptr1 共享所有权,引用计数增加
return 0;
}
通过 std::make_shared 创建 shared_ptr 可以避免使用原始指针初始化多个 shared_ptr。
- 不要在函数实参中创建 shared_ptr
解释:
在函数实参中创建 shared_ptr 可能会导致内存管理问题,尤其是在函数调用期间,如果不恰当使用,可能导致提前销毁对象或意外共享所有权。此外,使用 shared_ptr 作为函数参数时,通常需要额外的注意来确保内存正确管理。
错误示例:
#include <memory>
void foo(std::shared_ptr<int> ptr) {
// 这里,ptr 是一个局部的 shared_ptr,当函数结束时 ptr 会被销毁
std::cout << *ptr << std::endl; // 正常使用,但是内存会在函数结束时释放
}
int main() {
foo(std::make_shared<int>(10)); // 不推荐这样在函数实参中创建 shared_ptr
return 0;
}
在此示例中,std::make_shared(10) 会在函数调用时创建一个新的 shared_ptr,该指针会在函数结束时销毁,这可能导致不必要的拷贝和对象生命周期问题。
正确的做法:
使用 std::shared_ptr 作为函数参数时,可以传递已有的 shared_ptr,避免在函数内部分配新的对象。
#include <memory>
void foo(std::shared_ptr<int> ptr) {
std::cout << *ptr << std::endl;
}
int main() {
auto ptr = std::make_shared<int>(10); // 在外部创建 shared_ptr
foo(ptr); // 传递已有的 shared_ptr
return 0;
}
通过传递已创建的 shared_ptr 来避免在函数参数中直接创建新的 shared_ptr。
3. 通过 shared_from_this() 返回 this 指针
解释:
shared_from_this() 是 std::enable_shared_from_this 类模板提供的成员函数,允许一个类的实例(如果它是由 shared_ptr 管理的)返回一个指向该对象的 shared_ptr。但要确保在调用 shared_from_this() 时,该对象必须由 shared_ptr 管理,否则会抛出异常。
错误示例:
#include <iostream>
#include <memory>
class MyClass : public std::enable_shared_from_this<MyClass> {
public:
void foo() {
std::shared_ptr<MyClass> ptr = shared_from_this(); // 错误:如果不是由 shared_ptr 管理,抛出异常
std::cout << "Inside foo" << std::endl;
}
};
int main() {
MyClass* rawPtr = new MyClass();
rawPtr->foo(); // 错误:rawPtr 不是由 shared_ptr 管理,无法调用 shared_from_this()
return 0;
}
在此示例中,rawPtr 是一个普通的原始指针,而 shared_from_this() 只能在对象由 shared_ptr 管理时才有效。如果直接使用原始指针,调用 shared_from_this() 将导致程序崩溃。
正确的做法:
确保对象是通过 shared_ptr 管理的,然后再调用 shared_from_this()。
#include <iostream>
#include <memory>
class MyClass : public std::enable_shared_from_this<MyClass> {
public:
void foo() {
std::shared_ptr<MyClass> ptr = shared_from_this(); // 正确:shared_ptr 管理此对象
std::cout << "Inside foo" << std::endl;
}
};
int main() {
std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>(); // 使用 shared_ptr 管理对象
ptr->foo(); // 正确:对象由 shared_ptr 管理,可以调用 shared_from_this()
return 0;
}
通过 std::make_shared 管理对象,调用 shared_from_this() 就是安全的。
- 避免循环引用
解释:
循环引用是指两个或更多的 shared_ptr 互相引用对方,导致它们的引用计数无法降到 0,从而导致内存泄漏。在这种情况下,shared_ptr 永远不会删除这些对象,因为它们的引用计数永远不会为 0。
#include <iostream>
#include <memory>
class A;
class B {
public:
std::shared_ptr<A> a_ptr;
~B() { std::cout << "B destroyed" << std::endl; }
};
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyed" << std::endl; }
};
int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b; // A 指向 B
b->a_ptr = a; // B 指向 A,形成循环引用
// 当 a 和 b 超出作用域时,它们不会被销毁,因为有循环引用
return 0;
}
在这个示例中,a 和 b 相互持有对方的 shared_ptr,导致它们的引用计数永远不会为 0,导致内存泄漏。
正确的做法:
使用 std::weak_ptr 来打破循环引用。weak_ptr 不会增加引用计数,因此它不会导致循环引用问题。
#include <iostream>
#include <memory>
class A;
class B {
public:
std::weak_ptr<A> a_ptr; // 使用 weak_ptr 代替 shared_ptr,避免循环引用
~B() { std::cout << "B destroyed" << std::endl; }
};
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyed" << std::endl; }
};
int main() {
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b; // A 指向 B
b->a_ptr = a; // B 通过 weak_ptr 指向 A,避免循环引用
// 当 a 和 b 超出作用域时,它们会被正确销毁
return 0;
}
在这里,std::weak_ptr 代替了 shared_ptr,解决了循环引用的问题,避免了内存泄漏。
std::weak_ptr
- 不增加引用计数:weak_ptr 不会增加对象的引用计数,且 weak_ptr 无法直接访问管理的对象,它的主要作用是作为观察者。
- 避免循环引用:当 shared_ptr 之间形成循环引用时,weak_ptr 可以用来打破这种循环,因为它不会影响引用计数。
- 过期检查:weak_ptr 可以通过 lock() 方法获取一个 shared_ptr,该方法会检查对象是否已被销毁。如果对象已被销毁,lock() 会返回一个空的 shared_ptr。
4. unique_ptr
- unique_ptr 不能被赋值给另一个 unique_ptr
解释:
std::unique_ptr 是 独占型 智能指针,它不支持复制操作。也就是说,不能将一个 unique_ptr 直接赋值给另一个 unique_ptr,因为这样会导致两个 unique_ptr 同时拥有对同一资源的控制,违背了 独占所有权 的原则。unique_ptr 只能通过 移动语义(std::move)来转移所有权。
错误示例:
#include <iostream>
#include <memory>
int main() {
std::unique_ptr<int> ptr1 = std::make_unique<int>(10);
std::unique_ptr<int> ptr2 = ptr1; // 错误:无法将 unique_ptr 复制到另一个 unique_ptr
return 0;
}
编译器会报错,提示无法复制 unique_ptr。
正确做法:使用 std::move 转移所有权
#include <iostream>
#include <memory>
int main() {
std::unique_ptr<int> ptr1 = std::make_unique<int>(10);
std::unique_ptr<int> ptr2 = std::move(ptr1); // 正确:通过 std::move 转移所有权
if (!ptr1) {
std::cout << "ptr1 is now null" << std::endl;
}
std::cout << *ptr2 << std::endl; // 输出 10
return 0;
}
在这里,std::move 将 ptr1 的所有权转移给了 ptr2,之后 ptr1 变为 nullptr,但 ptr2 继续管理该对象。
- unique_ptr 可以指向一个数组
解释:
std::unique_ptr 不仅可以管理单个对象,还可以管理动态数组。与管理单个对象类似,unique_ptr 会在其析构时自动调用 delete[] 来释放数组内存。如果你使用 unique_ptr 管理动态数组,必须使用 unique_ptr<T[]> 来正确管理内存。
示例:
#include <iostream>
#include <memory>
int main() {
std::unique_ptr<int[]> arr = std::make_unique<int[]>(5); // 使用 unique_ptr 管理数组
// 初始化数组元素
for (int i = 0; i < 5; ++i) {
arr[i] = i * 10;
}
// 输出数组元素
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
// 当 arr 超出作用域时,内存会自动释放
return 0;
}
在这个例子中,arr 是一个 unique_ptr<int[]>,它管理一个包含 5 个 int 元素的动态数组。当 arr 超出作用域时,它会自动释放内存。
注意:
当 unique_ptr 管理数组时,必须使用 std::make_unique<int[]>() 来创建数组,不能使用 std::make_unique(),因为后者是用于单个对象的。
- unique_ptr 需要指定删除器的类型
解释:
std::unique_ptr 默认使用 delete 作为删除器来销毁对象。如果你使用 unique_ptr 管理一个数组,默认的删除器 delete 会导致错误,因为数组必须使用 delete[] 来删除。如果你需要处理其他类型的资源或自定义删除逻辑,可以指定一个 自定义删除器。
示例:
默认删除器:
#include <iostream>
#include <memory>
int main() {
std::unique_ptr<int> ptr = std::make_unique<int>(10); // 使用默认删除器
std::cout << *ptr << std::endl; // 输出 10
return 0;
}
在这个例子中,ptr 使用默认的 delete 删除器来销毁对象。
自定义删除器:
如果你需要在释放资源时执行其他操作(例如,记录日志、释放特定类型的资源等),你可以使用自定义删除器。自定义删除器通常是一个函数指针、函数对象或 lambda 表达式。
#include <iostream>
#include <memory>
struct MyDeleter {
void operator()(int* ptr) const {
std::cout << "Custom delete for " << *ptr << std::endl;
delete ptr; // 自定义删除操作
}
};
int main() {
// 使用自定义删除器
std::unique_ptr<int, MyDeleter> ptr(new int(10), MyDeleter());
std::cout << *ptr << std::endl; // 输出 10
return 0;
}
在这个例子中,我们定义了一个 MyDeleter 结构体,它提供了一个自定义的删除操作。unique_ptr 在析构时会调用这个删除器。
自定义删除器用于数组:
#include <iostream>
#include <memory>
int main() {
// 使用自定义删除器管理数组
std::unique_ptr<int[], std::function<void(int*)>> arr(
new int[5], [](int* p) {
std::cout << "Custom delete for array\n";
delete[] p;
});
// 初始化数组元素
for (int i = 0; i < 5; ++i) {
arr[i] = i * 10;
}
// 输出数组元素
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
// 自动调用自定义删除器释放内存
return 0;
}
在这个示例中,我们使用了 std::function 类型的自定义删除器来管理数组的释放。该删除器会在 unique_ptr 析构时调用,正确地释放数组内存。
5. weak_ptr弱引用的智能指针
- share_ptr虽然已经很好用了,但是有一点share_ptr智能指针还是有内存泄露的情况,当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的shared_ptr, weak_ptr只是提供了对管理对象的一个访问手段。
1. 基本用法
- use_count()
作用:
返回与 weak_ptr 关联的 shared_ptr 的引用计数。
示例:
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> sp = std::make_shared<int>(10); // 创建 shared_ptr
std::weak_ptr<int> wp = sp; // 关联 weak_ptr
std::cout << "use_count: " << wp.use_count() << std::endl; // 输出 1
{
std::shared_ptr<int> sp2 = wp.lock(); // 提升 weak_ptr 到 shared_ptr
std::cout << "use_count after lock: " << wp.use_count() << std::endl; // 输出 2
}
std::cout << "use_count after sp2 out of scope: " << wp.use_count() << std::endl; // 输出 1
return 0;
}
解释:
wp.use_count() 返回当前与 weak_ptr 关联的 shared_ptr 的数量。
在 sp2 提升时,shared_ptr 的引用计数增加到 2;sp2 离开作用域后,引用计数降回 1。
2. expired()
作用:
判断 weak_ptr 关联的对象是否已经被释放。如果对象已释放,则返回 true。
示例:
#include <iostream>
#include <memory>
int main() {
std::weak_ptr<int> wp;
{
std::shared_ptr<int> sp = std::make_shared<int>(10); // 创建 shared_ptr
wp = sp; // 关联 weak_ptr
std::cout << "Is expired? " << std::boolalpha << wp.expired() << std::endl; // 输出 false
}
// sp 超出作用域,对象被释放
std::cout << "Is expired? " << std::boolalpha << wp.expired() << std::endl; // 输出 true
return 0;
}
解释:
expired() 返回 true,表示 weak_ptr 所关联的 shared_ptr 已经释放,对象不再存在。
3. lock()
作用:
尝试从 weak_ptr 获取一个 shared_ptr。如果对象仍然存在,lock() 返回一个指向该对象的 shared_ptr;否则返回一个空的 shared_ptr。
示例:
#include <iostream>
#include <memory>
int main() {
std::weak_ptr<int> wp;
{
std::shared_ptr<int> sp = std::make_shared<int>(20); // 创建 shared_ptr
wp = sp; // 关联 weak_ptr
// 使用 lock 提升 weak_ptr
if (auto sp2 = wp.lock()) {
std::cout << "Object is alive, value: " << *sp2 << std::endl; // 输出 20
} else {
std::cout << "Object is expired" << std::endl;
}
}
// sp 超出作用域,对象被释放
if (auto sp2 = wp.lock()) {
std::cout << "Object is alive, value: " << *sp2 << std::endl;
} else {
std::cout << "Object is expired" << std::endl; // 输出 "Object is expired"
}
return 0;
}
解释:
lock() 返回一个有效的 shared_ptr,前提是对象仍然存在。
当 shared_ptr 管理的对象被销毁后,lock() 返回一个空的 shared_ptr。
2.weak_ptr返回this指针
-
weak_ptr 返回 this 指针与 shared_ptr 的区别
在 C++ 中,weak_ptr 和 shared_ptr 都是智能指针,用于管理动态分配的内存。它们的使用场景和特性有所不同,尤其是在类成员函数中,涉及到 this 指针时,二者的行为和用途也有所区别。 -
shared_ptr 和 weak_ptr 的区别
shared_ptr:
shared_ptr 是 共享所有权 的智能指针,多个 shared_ptr 可以指向同一个对象,并且引用计数会增加,直到最后一个 shared_ptr 被销毁时,才会释放对象的内存。
当你返回一个 shared_ptr 时,它会增加对象的引用计数,确保对象在引用存在时不会被销毁。
weak_ptr:
weak_ptr 是 弱引用,它不会增加引用计数,只是观察 shared_ptr 所管理的对象。weak_ptr 不会阻止对象的销毁,因此可以避免循环引用问题。
当你返回 weak_ptr 时,不会影响对象的生命周期。如果需要访问对象,需要使用 lock() 方法提升为 shared_ptr。 -
为什么不能直接返回 this 指针作为 shared_ptr 或 weak_ptr
返回 shared_ptr 时的危险: 直接返回 shared_ptr(this),即返回当前对象的 shared_ptr,会 增加引用计数,可能导致 循环引用 或 对象生命周期延长,从而导致内存泄漏。
返回 weak_ptr 的优势: 使用 weak_ptr 返回 this 指针时,不会改变引用计数,也不会导致循环引用,因此可以避免对象被意外地保留在内存中。可以在需要时通过 lock() 获取一个有效的 shared_ptr,如果对象已经被销毁,则返回空的 shared_ptr。
- 返回 this 指针的正确做法:使用 shared_from_this() 和 weak_ptr
如果你想在类成员函数中返回当前对象的智能指针,通常应该使用 std::enable_shared_from_this 来实现。std::enable_shared_from_this 是一个模板类,它使得对象能够返回一个 shared_ptr 指向自身。
shared_from_this():
std::enable_shared_from_this 允许对象在没有直接 shared_ptr 的情况下,安全地返回指向自身的 shared_ptr。
只有通过 shared_ptr 创建的对象才能调用 shared_from_this(),因此在对象创建时必须使用 shared_ptr 来管理对象的生命周期。
示例:使用 shared_from_this() 和 weak_ptr
#include <iostream>
#include <memory>
class MyClass : public std::enable_shared_from_this<MyClass> {
public:
MyClass() {
std::cout << "MyClass constructor\n";
}
~MyClass() {
std::cout << "MyClass destructor\n";
}
// 返回 shared_ptr 指向当前对象
std::shared_ptr<MyClass> getSharedPtr() {
// 返回一个 shared_ptr,该 shared_ptr 与当前对象共享所有权
return shared_from_this();
}
// 返回 weak_ptr 指向当前对象
std::weak_ptr<MyClass> getWeakPtr() {
return shared_from_this();
}
void showMessage() {
std::cout << "Hello from MyClass!" << std::endl;
}
};
int main() {
// 创建一个 shared_ptr 管理 MyClass 对象
std::shared_ptr<MyClass> sp1 = std::make_shared<MyClass>();
// 获取 shared_ptr 和 weak_ptr
std::shared_ptr<MyClass> sp2 = sp1->getSharedPtr(); // 返回 shared_ptr
std::weak_ptr<MyClass> wp = sp1->getWeakPtr(); // 返回 weak_ptr
sp2->showMessage(); // 调用对象的成员函数
std::cout << "use_count of sp1: " << sp1.use_count() << std::endl; // 输出引用计数,应该是 2
std::cout << "use_count of wp: " << wp.use_count() << std::endl; // weak_ptr 不增加引用计数,输出 2
// 清除 shared_ptr
sp1.reset();
std::cout << "use_count after sp1.reset(): " << sp2.use_count() << std::endl; // 输出 1,sp2 仍然有效
// weak_ptr 尝试提升为 shared_ptr
if (auto sp3 = wp.lock()) {
sp3->showMessage(); // 如果对象仍然存在,输出 "Hello from MyClass!"
} else {
std::cout << "Object has been deleted" << std::endl; // 如果对象已销毁,输出该消息
}
return 0;
}
3. weak_ptr解决循环引用问题
- 假设有两个类 A 和 B,它们互相持有对方的 shared_ptr,这就是典型的循环引用场景。
#include <iostream>
#include <memory>
class B; // 前向声明
class A {
public:
std::shared_ptr<B> b_ptr; // A 拥有 B 的 shared_ptr
A() {
std::cout << "A constructed\n";
}
~A() {
std::cout << "A destroyed\n";
}
};
class B {
public:
std::shared_ptr<A> a_ptr; // B 拥有 A 的 shared_ptr
B() {
std::cout << "B constructed\n";
}
~B() {
std::cout << "B destroyed\n";
}
};
int main() {
// 创建 A 和 B 的 shared_ptr
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
// 创建循环引用
a->b_ptr = b;
b->a_ptr = a;
// 对象 a 和 b 应该会被销毁,但由于循环引用,它们永远不会被销毁
return 0;
}
为了解决循环引用的问题,我们可以将其中一个 shared_ptr 替换为 weak_ptr。这样,当其中一个对象被销毁时,weak_ptr 不会阻止另一个对象的销毁。
- 解决循环引用的正确做法
#include <iostream>
#include <memory>
class B; // 前向声明
class A {
public:
std::shared_ptr<B> b_ptr; // A 拥有 B 的 shared_ptr
A() {
std::cout << "A constructed\n";
}
~A() {
std::cout << "A destroyed\n";
}
};
class B {
public:
std::weak_ptr<A> a_ptr; // B 拥有 A 的 weak_ptr,打破循环引用
B() {
std::cout << "B constructed\n";
}
~B() {
std::cout << "B destroyed\n";
}
};
int main() {
// 创建 A 和 B 的 shared_ptr
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
// 创建非循环引用
a->b_ptr = b;
b->a_ptr = a; // 这里使用 weak_ptr 而不是 shared_ptr
// 对象 a 和 b 在 main 函数结束时会被正常销毁
return 0;
}
解决方法解释:
- B 中的 a_ptr 改为 weak_ptr,表示 B 对 A 的引用不会增加 A 的引用计数,因此不会阻止A 的销毁。
- A 仍然拥有对 B 的 shared_ptr,这样可以保证 B 对 A 的引用是有效的。
- 当 main 函数结束时,a 和 b 会正确地被销毁,避免了循环引用导致的内存泄漏。
4 weak_ptr使用注意事项
weak_ptr在使用前需要检查合法性,在使用wp前需要调用wp.expired()函数判断一下
2. 右值引用和移动语义
避免无谓的复制,提高了程序性能
1. 什么是左值、右值
- 左值可以取地址、位于等号左边;
- 而右值没法取地址,位于等号右边。
- 有地址的变量就是左值,没有地址的字面值、临时值就是右值
2.什么是左值引用、右值引用
引用本质是别名,可以通过引用修改变量的值,传参时传引用可以避免拷贝
1. 左值引用
- 能指向左值,不能指向右值的就是左值引用
- const左值引用不会修改指向值,因此可以指向右值
2. 右值引用
- 右值引用的标志是 && ,顾名思义,右值引用专门为右值而生,可以指向右值,不能指
向左值 - 右值引用的用途:可以修改右值
3. 左右值引用的本质
- 右值引用有办法指向左值吗?
std::move 是一个类型转换工具,它将一个左值转换为右值引用类型(更准确地说,是一个 右值引用的左值)。通过这种方式,右值引用可以绑定到左值。
#include <iostream>
void test(int&& x) {
std::cout << "Right value reference: " << x << std::endl;
}
int main() {
int a = 10; // 左值
test(std::move(a)); // 使用 std::move 将左值转为右值引用
std::cout << "a after std::move: " << a << std::endl; // 注意:a 的值可能仍然有效,但状态未定义
return 0;
}
为什么右值引用不能直接指向左值?
右值引用的设计意图:
- 右值引用 主要用于捕获临时对象(右值),从而实现移动语义(例如,避免不必要的拷贝操作)。
- 如果右值引用可以直接绑定到左值,就会违背它的设计意图,也可能导致意料之外的行为。
- 左值在程序中是有名称并且可重复使用的,而右值引用本质上强调对临时对象的所有权转移,直接绑定左值可能引发潜在的错误。
- 左值引用、右值引用本身是左值还是右值?
被声明出来的左、右值引用都是左值。 因为被声明出的左右值引用是有地址的,也位于等号左边。
4.右值引用和std::move使用场景
右值引用在作为函数形参时更具灵活性
右值引用的一个重要目的是用来支持移动语义的。移动语义可以将资源(堆、系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,可以大幅度提高 C++ 应用程序的性能,消除临时对象的维护(创建和销毁)对性能的影响。
1. 移动语义
-
移动语义 是 C++11 引入的一种机制,旨在通过将资源从一个对象转移到另一个对象来避免不必要的深拷贝,从而提高程序的性能。它依赖于 右值引用(rvalue reference) 和 std::move。
-
为什么需要移动语义?
在传统 C++ 中,当一个对象需要将其数据传递到另一个对象时(如在函数返回或参数传递时),通常会使用拷贝构造函数。这种方法会创建一个新的副本,涉及内存分配和数据复制,代价高昂,尤其是对于资源密集型对象(如动态分配的内存、大型容器等)。
移动语义允许将资源直接转移,而不是复制。通过这种方式,可以显著提高效率,特别是在以下场景中:
- 返回局部对象。
- 将临时对象传递给函数。
- 容器操作(如插入或删除元素)。
- 如何实现移动语义?
移动语义主要通过以下两个机制实现:
- 右值引用(T&&):
用于绑定到临时对象(右值)。
允许转移资源的所有权。 - std::move:
是一个强制类型转换工具,用于将左值转换为右值引用,表示资源可以被“移动”。
移动构造函数与移动赋值运算符
移动语义的核心在于为类实现 移动构造函数 和 移动赋值运算符,从而支持资源的移动。
#include <iostream>
#include <utility> // std::move
class MyString {
private:
char* data;
size_t size;
public:
// 构造函数
MyString(const char* str) {
size = strlen(str);
data = new char[size + 1];
strcpy(data, str);
std::cout << "Constructed: " << data << std::endl;
}
// 拷贝构造函数
MyString(const MyString& other) {
size = other.size;
data = new char[size + 1];
strcpy(data, other.data);
std::cout << "Copied: " << data << std::endl;
}
// 移动构造函数
MyString(MyString&& other) noexcept : data(nullptr), size(0) {
// 资源转移
data = other.data;
size = other.size;
// 将源对象置为空
other.data = nullptr;
other.size = 0;
std::cout << "Moved: " << data << std::endl;
}
// 析构函数
~MyString() {
if (data) {
std::cout << "Destroyed: " << data << std::endl;
delete[] data;
} else {
std::cout << "Destroyed: nullptr" << std::endl;
}
}
// 打印内容
void print() const {
std::cout << "Data: " << (data ? data : "nullptr") << std::endl;
}
};
int main() {
MyString str1("Hello, World!"); // 普通构造
MyString str2 = std::move(str1); // 移动构造
str1.print(); // str1 已被移动,内容为 nullptr
str2.print(); // str2 接管了 str1 的资源
return 0;
}
2. forword完美接发
-
std::forward 是 C++11 引入的一个工具函数,用于在实现完美转发(perfect forwarding)时保持参数的值类别(左值或右值)。它通常与 右值引用 和 std::move 一起使用,目的是确保正确地转发函数参数的值类别,不会错误地修改其原始类别。
在函数模板中,如果希望转发参数到另一个函数,并保持其原始的值类别(即,如果是右值,就作为右值传递,如果是左值,就作为左值传递),你需要使用 std::forward。 -
为什么需要 std::forward?
当你编写一个模板函数并将参数传递给另一个函数时,C++ 的模板机制可能会“误用”右值引用。为了确保值类别在转发时保持一致(例如,右值保持右值,左值保持左值),需要使用 std::forward。
std::forward 使用示例
#include <iostream>
#include <utility> // std::forward
// 目标函数
void print(int& x) {
std::cout << "Lvalue: " << x << std::endl;
}
void print(int&& x) {
std::cout << "Rvalue: " << x << std::endl;
}
// 完美转发函数
template <typename T>
void forward_example(T&& arg) {
// 使用 std::forward 保持原始的左值或右值类别
print(std::forward<T>(arg));
}
int main() {
int x = 10;
// 左值转发
forward_example(x); // 会调用 print(int&)
// 右值转发
forward_example(20); // 会调用 print(int&&)
return 0;
}
/*
输出:
makefile
复制代码
Lvalue: 10
Rvalue: 20
*/
3. emplace_back 减少内存拷贝和移动
- 背景:传统的push_back
在 C++ 中,标准容器(如 std::vector)支持通过 push_back 向容器中添加元素。push_back 的工作方式如下:
- 接受一个元素对象作为参数。
- 将传入的对象复制或移动到容器中。
- 但在某些情况下,这种复制或移动可能带来额外的性能开销,尤其是对于复杂类型的对象。
- emplace_back 的优势
emplace_back 是 C++11 引入的一种方法,与 push_back 不同:
它直接在容器的存储空间中构造对象,而不是先创建一个对象再复制或移动它到容器中。
避免了不必要的拷贝或移动操作,提高性能。 - emplace_back 的原理:
通过传递构造函数的参数,在容器内直接调用目标类型的构造函数。省去了中间临时对象的创建或移动。
示例代码:对比 push_back 和 emplace_back
#include <iostream>
#include <vector>
#include <string>
class MyClass {
public:
MyClass(int x, std::string str) : x(x), str(std::move(str)) {
std::cout << "Constructed: " << this->str << std::endl;
}
MyClass(const MyClass& other) : x(other.x), str(other.str) {
std::cout << "Copied: " << this->str << std::endl;
}
MyClass(MyClass&& other) noexcept : x(other.x), str(std::move(other.str)) {
std::cout << "Moved: " << this->str << std::endl;
}
private:
int x;
std::string str;
};
int main() {
std::vector<MyClass> vec;
// 使用 push_back
std::cout << "Using push_back:" << std::endl;
MyClass obj(1, "Object1");
vec.push_back(obj); // 复制 obj
vec.push_back(MyClass(2, "Temp"));// 临时对象移动到容器
// 使用 emplace_back
std::cout << "\nUsing emplace_back:" << std::endl;
vec.emplace_back(3, "Emplace1"); // 直接构造,无复制或移动
vec.emplace_back(4, "Emplace2"); // 直接构造,无复制或移动
return 0;
}
3. 匿名函数lambda
1. 基本语法
捕获列表 mutable(可选) 异常属性 -> 返回类型 {
// 函数体
}
(捕获列表):定义 Lambda 表达式如何捕获外部变量。
(参数列表):定义函数的输入参数(类似普通函数)。
(返回类型):可以省略,编译器会自动推导;如果不想推导则显式指定。
(函数体):函数的具体实现。
#include <iostream>
int main() {
auto sum = [](int a, int b) { return a + b; };//捕获列表'[]'中为空,表示Lambda不能使用所在函数中的变量。
std::cout << "Sum: " << sum(3, 5) << std::endl; // 输出 8
return 0;
}
2.捕获列表
捕获列表用于捕获 Lambda 表达式外部的变量。捕获可以分为以下几种方式:
捕获方式:
- 按值捕获:捕获外部变量的值(拷贝)。
[x] // 按值捕获变量 x
[=] // 按值捕获所有外部变量
#include <iostream>
int main() {
int x = 10;
auto by_value = [x]() { std::cout << "Captured by value: " << x << std::endl; };
x = 20; // 修改外部变量不会影响捕获的值
by_value(); // 输出 10
return 0;
}
- 按引用捕获:捕获外部变量的引用(修改外部变量)。
[&x] // 按引用捕获变量 x
[&] // 按引用捕获所有外部变量
#include <iostream>
int main() {
int x = 10;
auto by_reference = [&x]() { x += 5; std::cout << "Captured by reference: " << x << std::endl; };
by_reference(); // 修改外部变量 x
std::cout << "Modified x: " << x << std::endl; // 输出 15
return 0;
}
- 混合捕获:
[x, &y] // 按值捕获 x,按引用捕获 y
#include <iostream>
int main() {
int x = 10, y = 20;
auto mix_capture = [x, &y]() {
std::cout << "Captured by value (x): " << x << std::endl;
std::cout << "Captured by reference (y): " << y << std::endl;
};
y += 5;
mix_capture(); // x 不变,y 更新为 25
return 0;
}
- 捕获this 指针:
[this]:捕获当前对象。
[=] 和 [&]:隐式捕获 this。
#include <iostream>
class Example {
public:
Example(int val) : value(val) {}
void printValue() {
auto lambda = [this]() { std::cout << "Value: " << value << std::endl; };
lambda();
}
private:
int value;
};
int main() {
Example obj(42);
obj.printValue(); // 输出 Value: 42
return 0;
}
- 表达式捕获
C++14 引入了 表达式捕获,允许在捕获列表中初始化变量,可以捕获计算后的值,而不仅仅是简单的变量。
init_capture = expr { function_body; }
- init_capture 是捕获的变量名。
- expr 是用于初始化捕获变量的表达式。
#include <iostream>
int main() {
int x = 10, y = 20;
// 使用表达式捕获
auto lambda = [sum = x + y]() {
std::cout << "Captured sum: " << sum << std::endl;
};
lambda(); // 输出:Captured sum: 30
return 0;
}
- 泛型 Lambda
C++14 引入了 泛型 Lambda,允许使用 自动类型推导(auto) 来定义参数类型,从而使 Lambda 可以处理不同类型的参数。
[capture](auto parameter1, auto parameter2) { function_body; }
#include <iostream>
int main() {
auto generic_lambda = [](auto a, auto b) {
return a + b;
};
std::cout << "Integers: " << generic_lambda(3, 5) << std::endl; // 输出:8
std::cout << "Doubles: " << generic_lambda(3.5, 2.5) << std::endl; // 输出:6
std::cout << "Strings: " << generic_lambda(std::string("Hello"), std::string(" World")) << std::endl;
// 输出:Hello World
return 0;
}
- 可变 Lambda
C++11 默认情况下,Lambda 表达式的捕获列表是 const,即捕获的变量在 Lambda 内部是只读的。如果需要修改捕获的变量,可以使用 mutable 关键字。
capture mutable { function_body; }
#include <iostream>
int main() {
int x = 10;
auto mutable_lambda = [x]() mutable {
x += 5;
std::cout << "Inside lambda: " << x << std::endl;
};
mutable_lambda(); // 输出:Inside lambda: 15
std::cout << "Outside lambda: " << x << std::endl; // 输出:Outside lambda: 10
return 0;
}
4. STL
C++标准库(STL,Standard Template Library)是一个强大的模板库,提供了一些高效的数据结构和算法。STL包含三大主要组件:容器(Containers)、迭代器(Iterators)和算法(Algorithms)。
1. 容器(Containers)
容器是用于存储和管理数据的类模板。STL提供了多种容器,主要分为顺序容器和关联容器。
- 顺序容器:数据按插入顺序存储。
vector:动态数组,支持随机访问。
deque:双端队列,支持从两端插入和删除。
list:双向链表,支持从两端插入和删除,但不支持随机访问。
array:固定大小的数组,性能优于vector,但大小固定。 - 关联容器:数据通过键值对(key-value)存储,常用于查找、插入、删除操作。
set:存储唯一的元素,按顺序排列。
map:存储键值对,按键排序。
unordered_set:存储唯一的元素,元素无序,基于哈希表。
unordered_map:存储键值对,元素无序,基于哈希表。 - 容器适配器:
stack:栈,遵循先进后出(LIFO)规则。
queue:队列,遵循先进先出(FIFO)规则。
priority_queue:优先队列,元素按优先级顺序排列。
2. 迭代器(Iterators)
迭代器是用于遍历容器元素的对象,类似于指针。迭代器支持对容器元素进行读取和修改。
- 常用迭代器类型:
begin():返回指向容器第一个元素的迭代器。
end():返回指向容器最后一个元素之后位置的迭代器。
rbegin():返回指向容器最后一个元素的逆向迭代器。
rend():返回指向容器第一个元素之前位置的逆向迭代器。
2. 迭代器操作:
*it:解引用,访问迭代器指向的元素。
it++:将迭代器移动到下一个元素。
it–:将迭代器移动到前一个元素。
3. 算法(Algorithms)
STL提供了大量的标准算法,用于对容器中的数据进行排序、查找、修改等操作。常用的算法包括:
- 排序:
sort():对容器中的元素进行排序。
stable_sort():稳定排序,保持相等元素的相对顺序。
reverse():反转容器中的元素顺序。
2. 查找:
find():查找指定元素,返回指向元素的迭代器。
binary_search():二分查找,返回布尔值,指示是否找到目标元素。
3. 修改:
fill():将容器中的所有元素设置为指定的值。
copy():将一个容器的元素复制到另一个容器中。
4. 集合操作:
merge():将两个有序的集合合并为一个。
set_union():计算两个集合的并集。
set_intersection():计算两个集合的交集。
5. 其他:
accumulate():对容器中的元素执行累加操作。
for_each():对容器中的每个元素执行给定的操作。
5. 正则表达式
提供了强大的文本处理能力。通过 std::regex 和相关函数,可以高效地进行文本的匹配、替换、分割等操作,极大提高了代码的灵活性和可维护性。掌握正则表达式在 C++ 中的使用,能帮助解决许多实际的字符串处理问题。
1. 主要组件
C++ 的 头文件定义了几个关键的组件:
std::regex:用于存储正则表达式的对象。
std::smatch 和 std::cmatch:分别用于存储 std::string 和 const char* 类型的匹配结果。
std::regex_match:用于检查整个字符串是否与正则表达式完全匹配。
std::regex_search:用于查找字符串中是否存在与正则表达式匹配的部分。
std::regex_replace:用于替换匹配的字符串。
2. 常用操作
- 正则表达式的构造
#include <iostream>
#include <regex>
int main() {
// 创建一个正则表达式对象,匹配数字
std::regex pattern(R"(\d+)"); // R"()" 语法用于避免反斜杠转义
return 0;
}
- std::regex_match - 检查完整匹配
std::regex_match 用于检查整个字符串是否完全匹配正则表达式。
#include <iostream>
#include <regex>
int main() {
std::string text = "12345";
std::regex pattern(R"(\d+)"); // 匹配一个或多个数字
if (std::regex_match(text, pattern)) {
std::cout << "Match!" << std::endl;
} else {
std::cout << "No match." << std::endl;
}
return 0;
}
- std::regex_search - 查找部分匹配
std::regex_search 用于查找字符串中是否包含正则表达式的匹配项。
#include <iostream>
#include <regex>
int main() {
std::string text = "There are 123 apples";
std::regex pattern(R"(\d+)"); // 匹配一个或多个数字
if (std::regex_search(text, pattern)) {
std::cout << "Found a match!" << std::endl;
} else {
std::cout << "No match." << std::endl;
}
return 0;
}
- std::regex_replace - 替换匹配项
std::regex_replace 用于替换字符串中与正则表达式匹配的部分。
#include <iostream>
#include <regex>
int main() {
std::string text = "I have 123 apples and 456 oranges";
std::regex pattern(R"(\d+)"); // 匹配数字
std::string result = std::regex_replace(text, pattern, "X");
std::cout << result << std::endl; // 替换数字为 X
return 0;
}
- std::smatch - 获取匹配结果
std::smatch 用于存储和提取正则表达式的匹配结果。
#include <iostream>
#include <regex>
int main() {
std::string text = "My phone number is 123-456-7890";
std::regex pattern(R"(\d{3}-\d{3}-\d{4})"); // 匹配电话号码格式
std::smatch match; // 存储匹配结果
if (std::regex_search(text, match, pattern)) {
std::cout << "Found phone number: " << match.str(0) << std::endl;
}
return 0;
}
- 使用捕获组
正则表达式的捕获组允许我们提取特定的匹配内容。
#include <iostream>
#include <regex>
int main() {
std::string text = "My email is [email protected]";
std::regex pattern(R"((\w+)@(\w+\.\w+))"); // 捕获用户名和域名
std::smatch match;
if (std::regex_search(text, match, pattern)) {
std::cout << "Username: " << match[1] << std::endl;
std::cout << "Domain: " << match[2] << std::endl;
}
return 0;
}