委员长 发表于 2025-1-10 22:06:54

C++Primer学习笔记12.动态内存分配

到目前为止,我们编写的程序中所使用的对象都有着严格定义的生存期。全局对象在程序启动时分配,在程序结束时销毁。对于局部自动对象,当我们进入其定义所在的程序块时被创建,在离开块时销毁。局部 static 对象在第一次使用前分配,在程序结束时销毁。

除了自动和 static 对象外,C++ 还支持动态分配对象。动态分配的对象的生存期与它们在哪里创建是无关的,只有当显式地被释放时,这些对象才会销毁。动态对象的正确释放被证明是编程中极其容易出错的地方。为了更安全地使用动态对象,标准库定义了两个智能指针类型来管理动态分配的对象。当一个对象应该被释放时,指向它的智能指针可以确保自动地释放它。

静态内存用来保存局部 static 对象、类 static 数据成员以及定义在任何函数之外的变量。栈内存用来保存定义在函数内的非 static 对象。分配在静态或栈内存中的对象由编译器自动创建和销毁。对于栈对象,仅在其定义的程序块运行时才存在;static 对象在使用之前分配,在程序结束时销毁。

除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称作自由空间(free store)或堆(heap)。程序用堆来存储动态分配(dynamically allocate)的对象——即,那些在程序运行时分配的对象。动态对象的生存期由程序来控制,也就是说,当动态对象不再rffgASDFG

使用时,我们的代码必须显式地销毁它们。

### 动态内存和智能指针

C++ 可以使用 new 和 delete 完成动态内存的分配和释放。但是对象释放的时机很难把握,容易忘记释放内存或过早释放内存。为了便于管理动态内存,C++11 引入了智能指针。智能指针可以自动的释放内存。只要合理合规使用智能指针,可以极大避免内存泄漏的问题。

C++ 标准库提供了两种智能指针:shared_ptr 允许多个指针指向同一个对象;unique_ptr 则独占所指向的对象。标准库还定义了一个名未 weak_ptr 的伴随类,它是一种若引用,指向 shared_ptr 所管理的对象。这三中类型均在 memory 头文件中。

#### shared_ptr类

shared_ptr 多个指针指向一个对象。

与 vector 的语法类似,智能指针也是模板。`shared_ptr<string> sp;` 默认初始化的智能指针保存着一个空指针。其用法与普通指针类似,只是增加了自动释放内存的功能。

```cpp
#include<iostream>
#include <memory>

int main() {
    // 智能指针的基本使用
    std::shared_ptr<int> p1;
    std::shared_ptr<std::string> p2;
    if (!p1) {
      std::cout << "p1 is empty" << std::endl;
    }

    if (!p2) {
      std::cout << "p2 is empty" << std::endl;
    }
    return 0;
}
```

<div align="center"><h6>shared_ptr 和 unique_ptr 都支持的操作</h6></div>


| 操作                                     | 说明                                                                        |
| ------------------------------------------ | ------------------------------------------------------------------------------- |
| shared_ptr\<T\> sp<br>unique_ptr\<T\> up | 空智能指针,可以指向类型为 T 的对象                                           |
| p                                        | 将 p 用作一个条件判断,p 若指向一个对象则为 true                              |
| *p                                       | 解引用,得到它指向的对象                                                      |
| p->mem                                 | 等价于(*p).mem                                                                |
| p.get()                                  | 返回 p 中保存的指针。若智能指针释放了其对象,返回的指针所指向的对象也就消失了 |
| swap(p,q)<br>p.swap(q)                   | 交换 p 和 q 中的指针                                                          |

<div align="center"><h6>shared_ptr 独有操作</h6></div>


| 操作                   | 说明rgs                                                                                                                                                               |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| make_shared\<T\>(args) | 返回一个 shared_ptr,指向一个动态分配的类型为 T 的对象。使用 args 初始化此对象。                                                                                        |
| shared_ptr\<T\>p(q)    | p 是 shapred_ptr q 的拷贝;此操作会递增 q 中的计数器。 q 中的指针必须能转换为 *T 类型。                                                                                 |
| p = q                  | p 和 q 都是 shared_ptr,所保存的指针必须能相。<span style="color:orange">此操作会递减 p 的引用计数,递增 q 的引用计数;若 p 计数变为 0,则将其管理的原内存释放。</span> |
| p.unique()             | 若 p.use_count() 为 1,返回 true;否则返回 false                                                                                                                        |
| p.use_count()          | 返回与 p 共享对象的智能指针的数量;主要用于调试;                                                                                                                     |

> <b>make_shapred 的基本使用</b>

```cpp
void testCount() {
    // 触发构造方法
    Tmp *t = new Tmp;
    // 会触发构造方法吗,不会只是创建了一个指向对象 Tmp 的 shared_ptr 指针。多个指针指向一个对象。
    std::shared_ptr<Tmp *> p2 = std::make_shared<Tmp *>(t);
    {
      // p3 也指向对象 t 了
      auto p3 = p2;
      // 此时引用计数都是 2,因为一共两个指针指向了对象 t
      std::cout << p2.use_count() << std::endl; // 2
      std::cout << p3.use_count() << std::endl; // 2
    }
    // p3 作用域消失了,触发了 shared_ptr 的析构方法,将计数减一,此时计数为1
    std::cout << p2.use_count() << std::endl; // 1
}
```

如何正确使用 shapred_ptr / make_shared 关联动态内存的对象呢?

`shapred_ptr<Foo> f(new Foo)`,此时传递的是指针,不会发生对象的拷贝构造,之后就利用传入的指针,让 shapred_ptr 指向所关联的动态对象的地址。也可以先声明一个空指针,后面再 reset 重新设置关联的对象。

`make_shared<Foo>(foo构造方法所需的参数)`,即便构造方法是 explicit 修饰的可以可以正常创建,或许是用了 `static_cast<Foo>(xx)` 强转。

```cpp
#include<iostream>
#include <memory>

class Foo {
public:

    Foo(int no) {
      this->no = no;
      std::cout << "Foo" << no << std::endl;
    }

    ~Foo() {
      std::cout << "~Foo" << no << std::endl;
    }

    void say() {
      std::cout << "hello" << std::endl;
    }

    int no;
};

void create(Foo arg) {}

int main() {

    // Foo *f = new Foo(1);
    // std::shared_ptr<Foo> s1;
    // s1.reset(f);
    std::shared_ptr<Foo> s1(new Foo(1));

    std::shared_ptr<Foo> s2 = std::make_shared<Foo>(2);
    // 为s设置关联的指针对象
    s1 = s2;
    std::cout << "hello" << std::endl;
}
```

> <b>shared_ptr 的拷贝和赋值</b>

shared_ptr 的拷贝和赋值会是计数器的值发生改变。

```cpp
void testCopy() {
    auto p = std::make_shared<int>(45);
    auto q(p); // 计数变为 2
    std::cout << q.use_count() << std::endl;
}
```

```cpp
void testCopy2() {
    auto p = std::make_shared<int>(45);
    auto q(p); // 计数变为 2
    auto r = std::make_shared<int>(45);
    r = q; // r 原先指向对象的引用计数递减,r 原来指向的对象已经没有引用者会自动释放
    std::cout << r.use_count() << std::endl;
}
```

> <b>shared_ptr 自动销毁所管理的对象</b>

<span style="color:orange">shared_ptr 的析构函数会递减它所指向的对象的引用计数。如果引用计数变为 0,shared_ptr 的析构函数就会销毁对象,并释放它所占用的内存。</span>

此处用的是 C++ 的隐式类型转换构造的对象。

```cpp
#include<iostream>
#include <memory>

class Foo {
public:

    Foo(int no) {
      this->no = no;
      std::cout << "Foo" << no << std::endl;
    }
    ~Foo() {
      std::cout << "~Foo" << no << std::endl;
    }
    int no;
};

std::shared_ptr<Foo> factory(int arg) {
    // 触发隐式类型转换,通过调用 Foo(1) 来创建对象,然后智能指针关联
    // 该对象,相当于 make_shared 内部创建的对象,可以避免构造拷贝。
    return std::make_shared<Foo>(arg);
}

void use_factory(int arg) {
    std::shared_ptr<Foo> p = factory(arg);
} // p 离开作用域会被销毁,调用析构,引用数变为 0,p 关联的对象也会被销毁

int main() {
    use_factory(1); // 析构
}
```

基本上,最后一个 shared_ptr 销毁前内存都不会十分,所以要确保 shared_ptr 在无用之后不再保留。shared_ptr 在无用之后仍然保留的情况之一是:将 shared_ptr 存放在一个容器中,随后重排了容器,从而不再需要某些元素。在这种情况下,你应该确保用 erase 删除那些不再需要的 shared_ptr 元素。

<span style="color:red">因此,如果将 shared_ptr 放到了容器中,如果不再需要使用,一顶要记得用 erase 删除那些无用的元素。</span>

> shared_ptr 练习

假定有这样一个需求,我们希望实现一个容器,当对这个容器对象进行拷贝时不拷贝容器内数据的副本,而是直接共享容器中的数据。

使用 shared_ptr 可以完成该功能。将 vector 定义为 shared_ptr 类型的,然后拷贝的时候 shared_ptr 的引用计数会 ++,当计数为 0 时就会释放对象内存。

```cpp
#include<iostream>
#include <memory>
#include <vector>

class Blob {
public:
    typedef std::vector<std::string>::size_type size;

    Blob() : Blob(new std::vector<std::string>) {};

    Blob(std::vector<std::string> *vec) {
      this->vec.reset(vec);
    }

    Blob(std::initializer_list<std::string> li) : vec(std::make_shared<std::vector<std::string>>(li)) {}

    bool empty() { return vec->empty(); }

    void push_back(const std::string &s) { vec->push_back(s); }

    void pop_back() { vec->pop_back(); }

    size get_size() { return vec->size(); }

    std::string &front() { return vec->front(); };


    std::string &back() {
      if (vec->empty()) {
            throw std::out_of_range("数组越界");
      }
      return vec->back();
    };
private:
    std::shared_ptr<std::vector<std::string>> vec;
};

int main() {
    Blob blob;
    blob.push_back("hello");
    blob.push_back("world");
    Blob blob2 = blob;
    blob2.push_back("ccc");

    Blob b = {"ni", "hao", "ya", "c", "f"};

    std::cout << b.get_size() << std::endl;
    std::cout << blob.get_size() << std::endl;
    std::cout << "============" << std::endl;
}
/*
5
3
=========
*/
```

#### 直接管理内存

> <b>使用 new 动态分配和初始化对象</b>

可以使用 new 和 delete 来直接管理内存。相比于智能指针,它们非常容易出错。

自己直接管理内存的类不能依赖默认合成的拷贝构造函数,通常都需要自己定义。而使用了智能指针的类则可以使用默认拷贝构造函数的版本。

new 无法为分配的对象命名,只是返回一个指针。

默认情况下,动态分配的对象被默认初始化。可以用直接初始化或列表初始化或值初始化初始动态分配的对象。

```cpp
int* p = new int;   //默认初始化
string* sp = new string(10,'g');//直接初始化
vector<int>* vp = new vector<int>{0,1,2,3};//列表初始化
```

> <b>区分值初始化和默认初始化</b>

对于类来说,值初始化与默认初始化没有什么区别,对于内置类型来说,值初始化对象会有一个良好的值,默认初始化对象值未定义。

值初始化只需加括号即可。

```cpp
int* p1 = new int;   // 默认初始化,p1 所指对象的值是未定义的
int* p2 = new int(); // 值初始化,p2 所指对象的值初始化为 0   
```

建议对动态分配的对象进行值初始化,如同变量初始化一样。

> <b>使用 auto</b>

当用括号提供了单个初始化器,就可以使用 auto(前后都用 auto)

```cpp
auto p1 = new auto(a);      // p1 指向一个与 a 类型相同的对象,该对象用 a 初始化
auto p1 = new auto{a, b, c};// 错误,不是单一初始化器,有多个。
```

> <b>动态分配的 const 对象</b>

可以使用 new 分配 const 对象,前后都要加 const

```cpp
const int* pci = new const int(10);
```

动态分配的 const 对象必须初始化,类类型可以隐式初始化。

> <b>内存耗尽</b>

如果没有可用内存了,new 就会失败。默认情况下,如果 new 失败,会爆出一个 `bad_alloc` 类型的异常。使用`定位 new` 可以向 new 传递参数,传递 nothrow 可以阻止 new 在分配失败的情况下抛出异常。

<span style="color:red">bad_alloc 和 nothrow 都定义在头文件 new 中</span>

```cpp
int* p = new(nothrow) int;//如果分配失败,返回一个空指针         
```

> <b>释放动态内存</b>

使用 delete 表达式来释放动态内存,包括动态分配的 const 对象也是直接 delete 即可。

delete 执行两个动作:

1. 销毁指针所指对象(但没有销毁指针本身)
2. 释放对应内存

```cpp
delete p; // p 必须指向一个动态分配的对象或是一个空指针
```

释放一个不是动态分配的指针和相同的指针释放多次的行为都是未定义的,即具体的行为由不同的编译器自己决定。

通常编译器不能分辨 delete 的对象是动态还是静态分配的对象,也不能分辨一个指针所指的内存是否已被释放。动态对象直到被显式释放前都是存在的。

> <b>指针和 delete</b>

delete 一个指向局部变量的指针是未定义操作;重复 delete 对象也是未定义操作;但是 delete 一个空指针是正确的

```cpp
int i, *pi = &i, *pi2 = nullptr;
double *pd = new double(33), *pd2 = pd;
delete i; // 错误 i 不是指针
delete pi1; // 未定义,pi1 指向一个局部变量
delete pd; // 正确
delete pd2;        // 未定义 pd2 指向的内存已经被释放了。
delete pi2;        // 正确,释放一个空指针总是没错的。
```

对于这些 delete 表达式,虽然它们是错误的但是大多数编译器会编译通过。

> <b>两种特殊情况</b>

1️⃣指针不在内存还在:当指针是一个局部变量,因超出作用域而被销毁时,其指向的动态内存不会自动释放。当没有指针指向这块内存时,就无法再释放了。这就是忘记 delete 产生的内存泄漏的问题。

2️⃣指针还在内存不在:delete 一个指针后,指针值已经无效,但是指针还是保存着地址,此时就变成了空悬指针。有两个解决方法

- delete 之后将指针置为空指针
- 在指针作用域的末尾 delete

如果有多个指针指向同一块动态内存,只能 delete 一个指针,因为 delete 的是空间,如果 delete 两个指针,可能会破坏自由空间。但必须将多个指针都重置。

使用 new 和 delete 的三个常见错误

1. 忘记 delete 内存:内存泄漏。
2. 使用已释放的的对象。
3. 同一块内存释放两次。

一个会导致内存泄漏的例子

```cpp
bool b() {
    // p 是一个 int 型指针
    int* p = new int;   
        // 函数返回值是 bool 类型,将 int 型指针转换为 bool 类型会使内存无法释放,造成内存泄漏
    return p;
}      
```

动态内存管理非常容易出错。坚持只使用智能指针可以避免所有这些问题,只有在没有任何智能指针指向它的情况下,智能指针才会自动释放它。

#### shared_ptr和new结合使用

可以使用 new 初始化智能指针。但是最好还是用 make_shared。

```cpp
#include<iostream>
#include <memory>

class Demo {
public:
    Demo() {
      std::cout << "构造" << std::endl;
    }

    ~Demo() {
      std::cout << "析构" << std::endl;
    }
};


int main() {
    // std::shared_ptr<Demo> d(new Demo);
    // 隐式转换为智能指针,调用了无参构造
    std::shared_ptr<Demo> dd = std::make_shared<Demo>();
}
```

接受指针参数的智能指针构造参数是 explicit 的,不能将内置指针隐式地转换为智能指针。因此不能使用赋值,只能用直接初始化。

```cpp
shared_ptr<double> p1(new int(42));// 正确:调用了转换构造函数
shared_ptr<double> p2 = new int(42); // 错误:转换构造函数是 explicit 的,不能隐式转换         
```

默认情况下用于初始化智能指针的普通指针只能指向动态内存,因为智能指针默认使用 delete 释放对象。

如果将智能指针绑定到一个指向其他类型资源的指针上,要定义自己的删除器(函数) 来代替 delete。

> <b>建议不要混用智能指针和普通指针</b>

shared_ptr 可以协调对象的析构,但仅限于自身的拷贝之间。这就是推荐使用 make_shared 而不是 new 的原因。

使用普通指针(即 new 返回的指针)来创建一个 shared_ptr 有两个易错之处:

1. 使用普通指针创建 shared_ptr 后,又使用该普通指针访问动态对象。普通指针并不知道该对象何时被 shared_ptr 所释放,随时可能变成空悬指针。
2. 使用同一个普通指针创建了多个 shared_ptr ,这就将同一块内存绑定到多个独立创建的 shared_ptr 上了。

当将一个 shared_ptr 绑定到一个普通指针后,就不要再用内置指针来访问所指内存了。

> <b>不要使用 get 初始化另一个智能指针或为智能指针赋值</b>

智能指针的 get 函数返回一个内置指针。

```cpp
shared_ptr<int> p(new int(42));
// 这是正确的,但是要极小心地使用,这会非常容易出错。
int* q = p.get();
```

注意:不要使用 get 初始化另一个智能指针或为智能指针赋值。也不能通过 get 返回的指针来 delete 此指针。

shared_ptr 的关联计数只应用于自己的拷贝,如果使用某智能指针的 get 函数初始化另一个智能指针,两个指针的计数是不关联的,销毁一个就会直接释放内存使另一个成为空悬指针。

一个错误的例子

```cpp
auto sp = make_shared<int>();
auto p = sp.get();
delete p;   //错误,这会造成 double free。
```

> <b>其他 shared_ptr 操作</b>

我们可以使用 reset 来将一个新的指针赋予一个 shared_ptr

```cpp
p.reset(new int(10)); // p 指向一个新对象
p = new int(10); // 错误,不能将一个指针赋予 shared_ptr
```

与赋值类似,reset 会更新引用计数,如果需要的话,会释放 p 指向的对象。reset 成员经常与 unique 一起使用,来控制多个 shared_ptr 共享的对象。在改变底层对象之前,我们检查自己是否是当前对象仅有的用户。如果不是,在改变之前要制作一份新的拷贝。

```cpp
if(!p.unique())
    // 让智能指针指向一个拷贝的对象,在拷贝的对象上作操作?
    p.reset(new string(*p)); // 我们不是唯一用户;分配新的拷贝
*p+=newVal; // 我是唯一的用户,可以改变对象的值。
```
页: [1]
查看完整版本: C++Primer学习笔记12.动态内存分配