unique_ptr
前置知识
智能指针与RAII
RAII是一种编程技术,用于管理资源(如内存、文件句柄、网络连接等),其核心思想是将资源的生命周期与对象的生命周期绑定。
当对象被创建时,它获取必要的资源;当对象被销毁时,它释放这些资源。这样可以保证即使发生异常,资源也能被正确地释放。
智能指针是一种特殊的指针,它封装了原始指针,并在对象生命周期结束时自动释放其指向的资源。C++标准库提供了几种智能指针,如std::unique_ptr、std::shared_ptr和std::weak_ptr。
智能指针利用了RAII的概念,它们在构造时获取资源(通常是通过new操作符分配内存),并在析构时释放资源(通过delete操作符)。
智能指针是实现RAII的一种方式。通过使用智能指针,你可以确保动态分配的内存在不再需要时被自动释放,从而避免内存泄漏。
const修饰符
const 修饰成员函数仅用于类中的成员函数时,其作用是保证该成员函数不会修改所属对象的状态
1 | void display() const { |
default修饰符
显式默认构造函数
1 | class Student{ |
- 构造函数:如 Student() = default;,告诉编译器自动生成默认的无参构造函数。
- 析构函数:~Student() = default;,告诉编译器自动生成默认的析构函数。
- 拷贝构造函数和拷贝赋值运算符:如果没有特别的要求,使用 = default 可以让编译器生成默认的拷贝构造和赋值运算符。
当你需要让类有特定行为(如自定义构造、析构等),但仍然希望保留编译器生成的某些默认行为时,可以用 = default 显式要求编译器生成这些函数。
类型转换
static_cast
含义
static_cast
是一种类型安全的转换,可以在不进行运行时检查的情况下转换类型。
用途
- 用于已知类型之间的转换,如基本数据类型、类层次结构中的上行或下行转换(当确定转换是安全时)。
示例
1 | int a = 10; |
reinterpret_cast
含义
reinterpret_cast
用于进行低级别的类型转换,几乎可以将任何指针类型转换为任何其他指针类型。
用途
- 主要用于处理底层操作,如指针的位表示。由于它不进行任何类型检查,使用时需要小心。
示例
1 | int a = 10; |
const_cast
含义
const_cast
用于添加或去除对象的const
或volatile
属性。
用途
- 主要用于需要修改一个常量对象的场景,但要小心使用,因为这可能导致未定义行为。
示例
1 | const int a = 10; |
dynamic_cast
含义
dynamic_cast
用于在类层次结构中进行安全的向下转换(downcasting)。
用途
- 主要用于多态类型(即有虚函数的类)之间的安全转换,能够检查转换是否成功。如果转换失败,返回
nullptr
(对于指针)或抛出std::bad_cast
(对于引用)。
示例
1 | class Base { virtual void func() {} }; // 有虚函数 |
总结
**
const_cast
**:添加或去除const
属性。用于处理常量性。**
dynamic_cast
**:安全地在类层次结构中转换,支持运行时检查。用于多态类型的安全转换。**
static_cast
**:类型安全的转换,适用于已知的类型关系。基本类型之间转换**
reinterpret_cast
**:用于低级别转换,没有安全检查。
move/forward
左值
定义:左值是指有名称并且可以在内存中持久存在的对象。它们可以出现在赋值操作的左侧。
特点:
具有持久的地址,可以取地址(使用
&
运算符)。通常是变量或对象。
右值
定义:右值是指临时对象或字面量,它们没有名称,通常是不能在内存中持久存在的对象。右值出现在赋值操作的右侧。
特点:
- 不具有持久的地址,通常是临时计算结果。
- 不能取地址。
区别
- 持久性:左值有持久的内存地址,可以在代码中多次引用;而右值是临时的,通常只在表达式中存在一次。
- 可修改性:左值可以被修改,右值通常不能直接修改(因为它们没有名称)。
move
1 |
|
在这个示例中,process
函数的参数是一个 MyClass
对象:
当我们调用
process(std::move(a))
时,我们将a
转换为右值,以便在process
函数中通过移动构造函数创建obj
。这样,
obj
可能会通过移动构造来初始化,而不是通过复制构造。输出将会显示 “Moved”,而不是 “Copied”,这表明资源被成功转移而不是重复创建。在
main
函数中,a
是一个左值,因为它是一个有名称的对象,存在于内存中。当我们调用
std::move(a)
时,std::move
将a
转换为右值,这表示我们希望转移a
的资源到process
函数中。此时,a
可以被视为一个临时对象,所有权可以被移动。
移动语义
- 减少复制开销:右值可以通过移动构造函数或移动赋值运算符来转移资源,而不是复制。这对于管理大量数据的对象(如容器、文件句柄等)尤其重要。
- 效率:移动操作通常比复制操作要快,因为它只需要转移指针或其他简单的资源标识符,而不是复制整个对象的内容。
表示所有权转移
- 语义清晰:使用
std::move
明确表示我们不再需要原来的对象,可以将其资源转移到另一个对象中。这样,调用者(如process
函数)能够明确知道它将获得资源的所有权。
forward
noexcept
用于指示某个函数不会抛出异常。使用 noexcept
可以帮助编译器进行优化,生成更高效的代码,并在运行时提供更好的异常安全性
使用方式:
在函数声明中使用:
1
2
3void myFunction() noexcept {
// 该函数不会抛出异常
}在表达式中使用,判断一个函数是否可能抛出异常:
1
static_assert(noexcept(myFunction()), "myFunction may throw!");
static_assert(noexcept(myFunction()), "myFunction may throw!");
是一个编译时断言,用于检查 myFunction
是否声明为 noexcept
。
详细解释:
- **
noexcept(myFunction())
**:这个表达式检查myFunction
是否会抛出异常。如果myFunction
是noexcept
的,结果为true
;否则为false
。 - **
static_assert
**:这是一个在编译时进行检查的语句。如果其第一个参数为false
,编译器会产生错误,并输出第二个参数的错误信息。这样,开发者可以在编译时捕捉到潜在的问题。 - 错误信息:如果
myFunction
可能抛出异常(即noexcept(myFunction())
为false
),编译器将显示"myFunction may throw!"
的错误信息,提示开发者。
示例:
1 | void myFunction() noexcept { |
constexpr
用于指示某个函数或变量在编译时可被求值。它使得在编译期间进行常量计算成为可能,通常用于优化性能。
使用方式:
用于函数声明:
1
2
3constexpr int add(int a, int b) {
return a + b;
}用于变量声明:
1
constexpr int maxSize = 100; // 编译时常量
可以与
if
、for
等控制结构结合使用,使得复杂计算在编译时完成:1
2
3constexpr int factorial(int n) {
return (n <= 1) ? 1 : n * factorial(n - 1);
}
优势:
- 编译时计算:减少运行时计算的开销,提高程序性能。
- 类型安全:使用
constexpr
可以确保在编译时进行类型检查,从而降低运行时错误的可能性。 - 优化常量表达式:可以用于数组大小、模板参数等需要编译时常量的场合。
左值引用与右值引用
左值引用
通过&获得左值引用,左值引用只能绑定左值。
1 | int intValue1 = 10; |
不能将左值引用绑定到一个右值,但是const的左值引用可以,常量引用不能修改绑定对象的值
1 | int &intValue1 = 10;//错误 |
右值引用
通过&&获得右值引用,右值引用只能绑定右值
右值引用的好处是减少右值作为参数传递时的复制开销
1 | int intValue = 10; |
使用std::move可以获得绑定到一个左值的右值引用
1 | int intValue = 10; |
C++移动构造/拷贝构造
1 | // 拷贝构造函数 拷贝构造:复制资源,两个对象拥有自己的副本,可能导致性能问题。 |
移动构造函数的触发:
1 | Student student1("Ros", 95); |
相关api
std::unique_ptr 提供了一个 reset() 方法,用于重置智能指针的管理对象或将其指向新的对象。
1 | void reset(Student* ptr = nullptr); |
- 作用:reset 用于替换当前管理的指针对象。如果智能指针已经持有一个对象,reset() 会释放当前管理的对象,然后将其指向新对象(或为空)。
- 参数:它接受一个原始指针 ptr,该指针将成为新的管理对象。如果不传参数,则会将当前指针对象重置为 nullptr,释放原来的资源。
在 std::make_unique
在 std::make_unique
1 | std::make_unique源码: |
1.代码分析:
模板参数:
- typename _Tp:表示要创建的对象类型(比如 Student)。
- typename… _Args:表示任意数量的参数,允许传递构造对象所需的任意数量和类型的参数。
返回类型:
- typename _MakeUniq<_Tp>::__single_object:这里使用了一个类型别名,通常是 std::unique_ptr<_Tp>。这个返回类型表示这个函数返回一个指向 _Tp 类型的智能指针。
函数体:
- new _Tp(std::forward<_Args>(__args)…):这里调用了 Tp 的构造函数,传递了使用 std::forward 转发的参数。std::forward 用于完美转发参数,保持参数的值类别(左值或右值)。
- _return unique_ptr<_Tp>(…):创建并返回一个 std::unique_ptr,它管理新创建的对象的生命周期。
2.与 std::make_unique
当你调用 std::make_unique
- Student 被推断为 _Tp,即要创建的对象类型。
- 空的 () 表示没有构造参数,因此 _Args 为零参数。
- 这个调用实际上转发了空的参数给 make_unique 函数,即 __args 是一个空包(没有参数)。
3.小括号和尖括号的区别
尖括号 <>:用于指明模板参数类型。在 std::make_unique
() 中,尖括号用于指定模板类型 Student,这是模板函数的类型参数。 小括号 ():用于调用函数或构造对象。在 std::make_unique
() 中,小括号表示调用 make_unique 函数。如果有参数,则小括号中会包含这些参数;如果没有参数,如 std::make_unique (),则小括号为空。
4.参数数量的不同
- 在 make_unique 的实现中,使用了可变参数模板 (typename… Args) 来支持任意数量的构造参数。
- _在调用 std::make_unique
() 时,如果没有提供参数,_Args 就是一个空包,允许你创建一个 Student 对象的默认实例。
总结
std::make_unique
() 通过模板函数 make_unique 实现了创建智能指针的功能。 尖括号 <> 用于指定模板类型,而小括号 () 用于调用函数。
make_unique 的实现灵活地支持任意数量的构造参数,允许你根据需要创建不同的对象。通过这种方式,std::make_unique 不仅提高了代码的安全性和可读性,也减少了手动内存管理的负担。
不可拷贝特性
- 使用 std::move显式地将一个对象转换为右值引用,从而启用移动语义。它不会真的移动对象,而是指示编译器可以安全地窃取资源。
- 将 std::unique_ptr
移入 vector 中是不可以直接push_back(), 在 C++ 中,std::unique_ptr 是不可拷贝的。
1 | std::unique_ptr<Student> p1(new Student()); |
完整程序如下
1 |
|