0%

C++智能指针与内存管理

unique_ptr

前置知识

智能指针与RAII

RAII是一种编程技术,用于管理资源(如内存、文件句柄、网络连接等),其核心思想是将资源的生命周期与对象的生命周期绑定。
当对象被创建时,它获取必要的资源;当对象被销毁时,它释放这些资源。这样可以保证即使发生异常,资源也能被正确地释放。

智能指针是一种特殊的指针,它封装了原始指针,并在对象生命周期结束时自动释放其指向的资源。C++标准库提供了几种智能指针,如std::unique_ptr、std::shared_ptr和std::weak_ptr。
智能指针利用了RAII的概念,它们在构造时获取资源(通常是通过new操作符分配内存),并在析构时释放资源(通过delete操作符)。

智能指针是实现RAII的一种方式。通过使用智能指针,你可以确保动态分配的内存在不再需要时被自动释放,从而避免内存泄漏。

const修饰符

const 修饰成员函数仅用于类中的成员函数时,其作用是保证该成员函数不会修改所属对象的状态

1
2
3
void display() const {
std::cout << "Student: " << name << ", Score: " << score << std::endl;
}

default修饰符

显式默认构造函数

1
2
3
4
class Student{
public:
Student() = default;
};
  • 构造函数:如 Student() = default;,告诉编译器自动生成默认的无参构造函数。
  • 析构函数:~Student() = default;,告诉编译器自动生成默认的析构函数。
  • 拷贝构造函数和拷贝赋值运算符:如果没有特别的要求,使用 = default 可以让编译器生成默认的拷贝构造和赋值运算符。

当你需要让类有特定行为(如自定义构造、析构等),但仍然希望保留编译器生成的某些默认行为时,可以用 = default 显式要求编译器生成这些函数。

类型转换

static_cast

含义

  • static_cast 是一种类型安全的转换,可以在不进行运行时检查的情况下转换类型。

用途

  • 用于已知类型之间的转换,如基本数据类型、类层次结构中的上行或下行转换(当确定转换是安全时)。

示例

1
2
3
4
5
6
7
8
int a = 10;
double b = static_cast<double>(a); // 基本数据类型转换

class Base {};
class Derived : public Base {};

Base* basePtr = new Derived();
Derived* derivedPtr = static_cast<Derived*>(basePtr); // 注意:没有安全检查
reinterpret_cast

含义

  • reinterpret_cast 用于进行低级别的类型转换,几乎可以将任何指针类型转换为任何其他指针类型。

用途

  • 主要用于处理底层操作,如指针的位表示。由于它不进行任何类型检查,使用时需要小心。

示例

1
2
3
int a = 10;
void* ptr = reinterpret_cast<void*>(&a); // 将 int* 转换为 void*
int* intPtr = reinterpret_cast<int*>(ptr); // 再次转换回 int*
const_cast

含义

  • const_cast 用于添加或去除对象的 constvolatile 属性。

用途

  • 主要用于需要修改一个常量对象的场景,但要小心使用,因为这可能导致未定义行为。

示例

1
2
3
const int a = 10;
int* b = const_cast<int*>(&a); // 去除 const 属性
*b = 20; // 这是不安全的,可能导致未定义行为
dynamic_cast

含义

  • dynamic_cast 用于在类层次结构中进行安全的向下转换(downcasting)。

用途

  • 主要用于多态类型(即有虚函数的类)之间的安全转换,能够检查转换是否成功。如果转换失败,返回 nullptr(对于指针)或抛出 std::bad_cast(对于引用)。

示例

1
2
3
4
5
6
7
8
class Base { virtual void func() {} }; // 有虚函数
class Derived : public Base {};

Base* basePtr = new Derived();
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); // 安全转换
if (derivedPtr) {
// 转换成功
}
总结
  • **const_cast**:添加或去除 const 属性。用于处理常量性。

  • **dynamic_cast**:安全地在类层次结构中转换,支持运行时检查。用于多态类型的安全转换。

  • **static_cast**:类型安全的转换,适用于已知的类型关系。基本类型之间转换

  • **reinterpret_cast**:用于低级别转换,没有安全检查。

move/forward

左值

定义:左值是指有名称并且可以在内存中持久存在的对象。它们可以出现在赋值操作的左侧。

特点

  • 具有持久的地址,可以取地址(使用 & 运算符)。

  • 通常是变量或对象。

右值

定义:右值是指临时对象或字面量,它们没有名称,通常是不能在内存中持久存在的对象。右值出现在赋值操作的右侧。

特点

  • 不具有持久的地址,通常是临时计算结果。
  • 不能取地址。
区别
  • 持久性:左值有持久的内存地址,可以在代码中多次引用;而右值是临时的,通常只在表达式中存在一次。
  • 可修改性:左值可以被修改,右值通常不能直接修改(因为它们没有名称)。
move
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <utility>

class MyClass {
public:
MyClass() { std::cout << "Constructed\n"; }
MyClass(const MyClass&) { std::cout << "Copied\n"; }
MyClass(MyClass&&) noexcept { std::cout << "Moved\n"; }
};

void process(MyClass obj) {
// Do something with obj
}

int main() {
MyClass a; // a 是左值
process(std::move(a)); // std::move(a) 是右值
return 0;
}

在这个示例中,process 函数的参数是一个 MyClass 对象:

  • 当我们调用 process(std::move(a)) 时,我们将 a 转换为右值,以便在 process 函数中通过移动构造函数创建 obj

  • 这样,obj 可能会通过移动构造来初始化,而不是通过复制构造。输出将会显示 “Moved”,而不是 “Copied”,这表明资源被成功转移而不是重复创建。

  • main 函数中,a 是一个左值,因为它是一个有名称的对象,存在于内存中。

  • 当我们调用 std::move(a) 时,std::movea 转换为右值,这表示我们希望转移 a 的资源到 process 函数中。此时,a 可以被视为一个临时对象,所有权可以被移动。

移动语义
  • 减少复制开销:右值可以通过移动构造函数或移动赋值运算符来转移资源,而不是复制。这对于管理大量数据的对象(如容器、文件句柄等)尤其重要。
  • 效率:移动操作通常比复制操作要快,因为它只需要转移指针或其他简单的资源标识符,而不是复制整个对象的内容。
表示所有权转移
  • 语义清晰:使用 std::move 明确表示我们不再需要原来的对象,可以将其资源转移到另一个对象中。这样,调用者(如 process 函数)能够明确知道它将获得资源的所有权。
forward

noexcept

用于指示某个函数不会抛出异常。使用 noexcept 可以帮助编译器进行优化,生成更高效的代码,并在运行时提供更好的异常安全性

使用方式

  • 在函数声明中使用:

    1
    2
    3
    void myFunction() noexcept {
    // 该函数不会抛出异常
    }
  • 在表达式中使用,判断一个函数是否可能抛出异常:

    1
    static_assert(noexcept(myFunction()), "myFunction may throw!");

static_assert(noexcept(myFunction()), "myFunction may throw!"); 是一个编译时断言,用于检查 myFunction 是否声明为 noexcept

详细解释:

  1. **noexcept(myFunction())**:这个表达式检查 myFunction 是否会抛出异常。如果 myFunctionnoexcept 的,结果为 true;否则为 false
  2. **static_assert**:这是一个在编译时进行检查的语句。如果其第一个参数为 false,编译器会产生错误,并输出第二个参数的错误信息。这样,开发者可以在编译时捕捉到潜在的问题。
  3. 错误信息:如果 myFunction 可能抛出异常(即 noexcept(myFunction())false),编译器将显示 "myFunction may throw!" 的错误信息,提示开发者。

示例:

1
2
3
4
5
6
7
8
9
10
11
void myFunction() noexcept {
// 不会抛出异常
}

static_assert(noexcept(myFunction()), "myFunction may throw!"); // 通过检查

void anotherFunction() {
throw std::runtime_error("Error");
}

static_assert(noexcept(anotherFunction()), "anotherFunction may throw!"); // 编译时错误

constexpr

用于指示某个函数或变量在编译时可被求值。它使得在编译期间进行常量计算成为可能,通常用于优化性能。

使用方式

  • 用于函数声明:

    1
    2
    3
    constexpr int add(int a, int b) {
    return a + b;
    }
  • 用于变量声明:

    1
    constexpr int maxSize = 100; // 编译时常量
  • 可以与 iffor 等控制结构结合使用,使得复杂计算在编译时完成:

    1
    2
    3
    constexpr int factorial(int n) {
    return (n <= 1) ? 1 : n * factorial(n - 1);
    }

优势

  1. 编译时计算:减少运行时计算的开销,提高程序性能。
  2. 类型安全:使用 constexpr 可以确保在编译时进行类型检查,从而降低运行时错误的可能性。
  3. 优化常量表达式:可以用于数组大小、模板参数等需要编译时常量的场合。

左值引用与右值引用

左值引用

通过&获得左值引用,左值引用只能绑定左值。

1
2
3
4
5
6
7
int intValue1 = 10;
//将intValue1绑定到intValue2和intValue3
int &intValue2 = intValue1, &intValue3 = intValue2;
intValue2 = 100;
std::cout << intValue1 << std::endl;//100
std::cout << intValue2 << std::endl;//100
std::cout << intValue3 << std::endl;//100

不能将左值引用绑定到一个右值,但是const的左值引用可以,常量引用不能修改绑定对象的值

1
2
int &intValue1 = 10;//错误
const int &intValue2 = 10;//正确
右值引用

通过&&获得右值引用,右值引用只能绑定右值
右值引用的好处是减少右值作为参数传递时的复制开销

1
2
3
int intValue = 10;
int &&intValue2 = 10;//正确
int &&intValue3 = intValue;//错误

使用std::move可以获得绑定到一个左值的右值引用

1
2
int intValue = 10;
int &&intValue3 = std::move(intValue);

C++移动构造/拷贝构造

1
2
3
4
5
6
7
8
9
10
11
12
// 拷贝构造函数 拷贝构造:复制资源,两个对象拥有自己的副本,可能导致性能问题。
Student(const Student& other) : name(other.name), score(other.score) {
std::cout << "Copied: " << name << "\n";
}

// 移动构造函数 移动构造:转移资源的所有权,通常更加高效,源对象在移动后可能处于未定义状态。
Student(Student&& other) noexcept
: name(std::move(other.name)), score(other.score) {
std::cout << "Moved: " << name << "\n";
other.score = 0; // 清空源对象的状态
other.name = "Unknown";
}

移动构造函数的触发:

1
2
Student student1("Ros", 95);
Student student2 = std::move(student1); // 这里触发了移动构造

相关api

std::unique_ptr 提供了一个 reset() 方法,用于重置智能指针的管理对象或将其指向新的对象。

1
void reset(Student* ptr = nullptr);
  • 作用:reset 用于替换当前管理的指针对象。如果智能指针已经持有一个对象,reset() 会释放当前管理的对象,然后将其指向新对象(或为空)。
  • 参数:它接受一个原始指针 ptr,该指针将成为新的管理对象。如果不传参数,则会将当前指针对象重置为 nullptr,释放原来的资源。

在 std::make_unique() 中,<> 表示这是一个模板函数的调用。具体来说,std::make_unique 是一个模板,Student 是其模板参数,表示要创建的对象类型。

在 std::make_unique() 的末尾有一个空的圆括号 (),这表示调用 make_unique 函数

1
2
3
4
5
std::make_unique源码:
template<typename _Tp, typename... _Args>
inline typename _MakeUniq<_Tp>::__single_object
make_unique(_Args&&... __args)
{ return unique_ptr<_Tp>(new _Tp(std::forward<_Args>(__args)...)); }

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 不仅提高了代码的安全性和可读性,也减少了手动内存管理的负担。

不可拷贝特性

  1. 使用 std::move显式地将一个对象转换为右值引用,从而启用移动语义。它不会真的移动对象,而是指示编译器可以安全地窃取资源。
  2. 将 std::unique_ptr 移入 vector 中是不可以直接push_back(), 在 C++ 中,std::unique_ptr 是不可拷贝的。
1
2
3
4
5
6
7
8
9
10
11
std::unique_ptr<Student> p1(new Student());
std::unique_ptr<Student> p2 = p1; // 错误:不能拷贝 unique_ptr
如果你想要将 p1 的所有权转移给 p2,必须使用移动语义,即使用 std::move(),将 p1 变成右值引用:

std::unique_ptr<Student> p2 = std::move(p1); // 合法:p1 的所有权转移到 p2
在这之后,p1 会变为空 (nullptr),而 p2 将接管 p1 原本指向的资源。

students.push_back(std::move(student));
将 unique_ptr 对象放入 std::vector 中,原因如下:
1.unique_ptr 不可拷贝:由于 unique_ptr 是独占的,不能进行拷贝操作。如果直接尝试 students.push_back(student),编译器会报错,因为 push_back 要求对 student 进行拷贝
2.转移所有权:使用 std::move(student),你告诉编译器,student 的资源可以被转移到 students 容器中,student 之后将不再拥有资源(即变为空)。

完整程序如下

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


class Student{
private:
std::string name;
double score;
public:
Student() = default;
void set(const std::string& n, double s)
{
this->name = n;
this->score = s;
}
double get_score() const {return score;}
const std::string& get_name() const {return name;}
void display() const {
std::cout << "name : " << name << "\t score " << score << std::endl;
}
~Student(){

}
};

int compare(const std::unique_ptr<Student>& a, const std::unique_ptr<Student>& b)
{
if(a->get_score() < b->get_score()) return 1;
if(a->get_score() > b->get_score()) return -1;
return -1;
}


int main()
{
//max(new Student())和max(new Student)效果一样 都使用默认构造函数
//!智能指针写法 两种后者更好一些
// std::unique_ptr<Student> max(new Student());
// std::unique_ptr<Student> min(new Student);

// std::unique_ptr<Student> min = std::make_unique<Student>();
// std::unique_ptr<Student> max = std::make_unique<Student>();

//!这样是开辟在栈上;注意野指针 由于最后还是会指向栈上的指针,这里就不必要new开辟内存
// Student *min = nullptr;
// Student *max = nullptr;

//! 下面这种写法是最傻逼的,一个变量一行!//
// Student *min,*max = new Student();
//! 这样语法正确,但是此场景存在内存泄漏,因为max min后面改变指针指向,指向了栈上的元素
// Student *min = new Student();
// Student *max = new Student();


std::vector<std::unique_ptr<Student>> students;
std::string stu_name[] = { "Rose","Mike","Eve","Micheal","Jack" };
double stu_score[] = { 95,84,88,64,100 };

// 动态创建学生对象并存储在智能指针中
for(int i = 0; i < 5; i ++){
//使用 std::make_unique<Student>() 创建动态分配的学生对象,确保它们都在堆上分配。
auto student = std::make_unique<Student>();
student->set(stu_name[i], stu_score[i]);
student->display();
students.push_back(std::move(student));
}

std::unique_ptr<Student> *min = &students[0];
std::unique_ptr<Student> *max = &students[0];

for(int i = 0; i < 5; i ++){
if(compare(*min, students[i]) == 1)
min = &students[i]; // min指向新的智能指针
if(compare(*max, students[i]) == -1)
max = &students[i]; // max指向新的智能指针
}

//!min 和 max 指向 std::unique_ptr<Student> 的指针 解引用一层才是指向Student的指针 才可以使用->
std::cout << "The worst student : " << (*min)->get_name() << std::endl;
std::cout << "The best student : " << (*max)->get_name() << std::endl;
return 0;
}

shared_ptr