好有趣且卡哇伊的图

img

动态内存

程序用堆(heap)来存储动态分配(dynamically allocate)的对象。动态对象的生存期由程序控制。

动态内存与智能指针(Dynamic Memory and Smart Pointers)

C++中的动态内存管理通过一对运算符完成:new在动态内存中为对象分配空间并返回指向该对象的指针,可以选择对对象进行初始化;delete接受一个动态对象的指针,销毁该对象并释放与之关联的内存。

新标准库提供了两种智能指针(smart pointer)类型来管理动态对象。智能指针的行为类似常规指针,但它自动释放所指向的对象。这两种智能指针的区别在于管理底层指针的方式:shared_ptr允许多个指针指向同一个对象;unique_ptr独占所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在头文件memory中。

shared_ptr类(The shared_ptr Class)

智能指针是模板,创建时需要指明指针可以指向的类型。默认初始化的智能指针中保存着一个空指针。

1
2
shared_ptr<string> p1;      // shared_ptr that can point at a string
shared_ptr<list<int>> p2; // shared_ptr that can point at a list of ints

shared_ptrunique_ptr都支持的操作:

操作 含义
shared_ptr<T> sp unique_ptr<T> up 默认初始化为空指针
p p指向一个对象,则为true
*p 解引用p
p->mem 等价于(*p).mem
p.get() 返回p中的指针

shared_ptr独有的操作:

操作 含义
p.use_count() 返回p的引用计数
p.unique() p.use_count() == 1,则返回true
1
2
3
4
5
6
// shared_ptr that points to an int with value 42
shared_ptr<int> p3 = make_shared<int>(42);
// p4 points to a string with value 9999999999
shared_ptr<string> p4 = make_shared<string>(10, '9');
// p5 points to an int that is value initialized
shared_ptr<int> p5 = make_shared<int>();

这段代码使用了C++11中的智能指针 std::shared_ptrstd::make_shared 函数来创建三个智能指针,并分别指向了不同类型的对象。让我逐步解释:

  1. shared_ptr<int> p3 = make_shared<int>(42);: 这行代码创建了一个 shared_ptr 智能指针 p3,并使用 make_shared 函数将其初始化为指向一个整型对象,值为42。make_shared<int>(42) 创建了一个动态分配的整型对象,并将其初始化为42,并且返回一个指向该对象的 shared_ptr。因此,p3 现在指向了这个整型对象。

  2. shared_ptr<string> p4 = make_shared<string>(10, '9');: 这行代码创建了一个 shared_ptr 智能指针 p4,并使用 make_shared 函数将其初始化为指向一个字符串对象,该字符串由10个字符'9'组成。make_shared<string>(10, '9') 创建了一个动态分配的字符串对象,并用字符'9'重复初始化10次,返回一个指向该字符串对象的 shared_ptr。因此,p4 现在指向了这个字符串对象。

  3. shared_ptr<int> p5 = make_shared<int>();: 这行代码创建了一个 shared_ptr 智能指针 p5,并使用 make_shared 函数将其初始化为指向一个整型对象,该整型对象是通过值初始化得到的。make_shared<int>() 创建了一个动态分配的整型对象,并执行值初始化操作,将其值设为0(整型的默认初始化值),返回一个指向该整型对象的 shared_ptr。因此,p5 现在指向了这个整型对象。

综上所述,这段代码使用 std::make_shared 函数创建了三个 shared_ptr 智能指针,分别指向一个整型对象、一个字符串对象和一个值初始化的整型对象。这样做可以保证动态分配的对象会被正确地管理,并且避免了手动管理内存的复杂性。

进行拷贝或赋值操作时,每个shared_ptr会记录有多少个其他shared_ptr与其指向相同的对象。

1
2
3
auto p = make_shared<int>(42);  // object to which p points has one user
auto q(p); // p and q point to the same object
// object to which p and q point has two users

这段代码展示了如何使用 std::make_shared 创建 shared_ptr 智能指针,并且演示了共享指针的引用计数机制。让我逐步解释:

  1. auto p = make_shared<int>(42);: 这行代码创建了一个 shared_ptr 智能指针 p,并使用 make_shared 函数将其初始化为指向一个整型对象,值为42。这个对象的引用计数为1,因为此时只有 p 指向它。

  2. auto q(p);: 这行代码创建了另一个 shared_ptr 智能指针 q,并将其初始化为指向 p 所指向的对象。这时候,pq 都指向同一个对象,因此这个对象的引用计数增加到2。

每个shared_ptr都有一个与之关联的计数器,通常称为引用计数(reference count)。拷贝shared_ptr时引用计数会递增。例如使用一个shared_ptr初始化另一个shared_ptr,或将它作为参数传递给函数以及作为函数的返回值返回。给shared_ptr赋予新值或shared_ptr被销毁时引用计数会递减。例如一个局部shared_ptr离开其作用域。一旦一个shared_ptr的引用计数变为0,它就会自动释放其所管理的对象。(感觉像py,也许是先学py的先入为主)

1
2
3
4
5
auto r = make_shared<int>(42);  // int to which r points has one user
r = q; // assign to r, making it point to a different address
// increase the use count for the object to which q points
// reduce the use count of the object to which r had pointed
// the object r had pointed to has no users; that object is automatically freed

这段代码涉及了智能指针的赋值操作,它展示了当一个 shared_ptr 指向另一个对象时,引用计数的变化。让我逐步解释:

  1. auto r = make_shared<int>(42);: 这行代码创建了一个 shared_ptr 智能指针 r,并使用 make_shared 函数将其初始化为指向一个整型对象,值为42。这个对象的引用计数为1,因为此时只有 r 指向它。

  2. r = q;: 这行代码将 r 赋值为 q,即让 r 指向 q 所指向的对象。由于 qr 都是 shared_ptr 智能指针,赋值操作会使得两个智能指针共享同一个对象。在这里,r 原本指向的对象(值为42的整型对象)的引用计数减少到0,因为没有任何指针指向它,所以会被自动释放。而 q 指向的对象的引用计数增加到3,因为此时有 pqr 三个智能指针指向它。

综上所述,这段代码展示了赋值操作对智能指针引用计数的影响:当一个智能指针指向另一个对象时,原对象的引用计数会减少,新对象的引用计数会增加,从而确保内存资源的正确管理。

shared_ptr的析构函数会递减它所指向对象的引用计数。如果引用计数变为0,shared_ptr的析构函数会销毁对象并释放空间。

如果将shared_ptr存放于容器中,而后不再需要全部元素,而只使用其中一部分,应该用erase删除不再需要的元素。

程序使用动态内存通常出于以下三种原因之一:

  • 不确定需要使用多少对象。
  • 不确定所需对象的准确类型。
  • 需要在多个对象间共享数据。

直接管理内存(Managing Memory Directly)

相对于智能指针,使用newdelete管理内存很容易出错。

默认情况下,动态分配的对象是默认初始化的。所以内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化。

1
2
string *ps = new string;    // initialized to empty string
int *pi = new int; // pi points to an uninitialized int

可以使用值初始化方式、直接初始化方式、传统构造方式(圆括号())或新标准下的列表初始化方式(花括号{})初始化动态分配的对象。

1
2
3
4
5
6
7
8
int *pi = new int(1024);            // object to which pi points has value 1024
string *ps = new string(10, '9'); // *ps is "9999999999"
// vector with ten elements with values from 0 to 9
vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};
string *ps1 = new string; // default initialized to the empty string
string *ps = new string(); // value initialized to the empty string
int *pi1 = new int; // default initialized; *pi1 is undefined
int *pi2 = new int(); // value initialized to 0; *pi2 is 0

只有当初始化的括号中仅有单一初始化器时才可以使用auto

1
2
3
auto p1 = new auto(obj);    // p points to an object of the type of obj
// that object is initialized from obj
auto p2 = new auto{a,b,c}; // error: must use parentheses for the initializer

这段代码展示了使用 auto 关键字与 new 运算符结合动态创建对象的方法。让我逐步解释:

  1. auto p1 = new auto(obj);: 这行代码使用了 auto 关键字来推断指针 p1 所指向对象的类型,并且通过 new 运算符动态地创建了这个对象。对象的类型与 obj 相同,并且通过 obj 进行初始化。换句话说,p1 指向了一个类型与 obj 相同,并且由 obj 初始化的对象。

  2. auto p2 = new auto{a,b,c};: 这行代码尝试使用 auto 关键字来推断指针 p2 所指向对象的类型,并且通过 {a, b, c} 进行初始化。然而,这里使用了花括号 {} 来初始化对象,但是这种初始化方式在使用 auto 关键字时是不允许的,必须使用圆括号 ()。因此,这行代码会导致编译错误。

可以用new分配const对象,返回指向const类型的指针。动态分配的const对象必须初始化。

默认情况下,如果new不能分配所要求的内存空间,会抛出bad_alloc异常。使用定位new(placement new)可以阻止其抛出异常。定位new表达式允许程序向new传递额外参数。如果将nothrow传递给new,则new在分配失败后会返回空指针。bad_allocnothrow都定义在头文件new中。

1
2
3
// if allocation fails, new returns a null pointer
int *p1 = new int; // if allocation fails, new throws std::bad_alloc
int *p2 = new (nothrow) int; // if allocation fails, new returns a null pointer

使用delete释放一块并非new分配的内存,或者将相同的指针值释放多次的行为是未定义的。

由内置指针管理的动态对象在被显式释放前一直存在。

注意下面一点:

delete一个指针后,指针值就无效了(空悬指针,dangling pointer)。为了防止后续的错误访问,应该在delete之后将指针值置空。

shared_ptrnew结合使用(Using shared_ptrs with new)e

可以用new返回的指针初始化智能指针。该构造函数是explicit的,因此必须使用直接初始化形式。

1
2
shared_ptr<int> p1 = new int(1024);    // error: must use direct initialization
shared_ptr<int> p2(new int(1024)); // ok: uses direct initialization

这段代码展示了使用std::shared_ptr来管理动态分配的整型对象。让我逐步解释:

  1. shared_ptr<int> p1 = new int(1024);: 这行代码试图使用std::shared_ptr来管理动态分配的整型对象,但采用了拷贝初始化的方式。然而,std::shared_ptr的构造函数并没有接受一个裸指针作为参数的版本,因此这种方式是错误的。C++中的智能指针需要使用直接初始化的方式来构造,而不能使用拷贝初始化。

  2. shared_ptr<int> p2(new int(1024));: 这行代码使用了直接初始化的方式来创建一个std::shared_ptr对象p2,并让其指向一个动态分配的整型对象,值为1024。这种方式是正确的,因为std::shared_ptr的构造函数具有接受裸指针作为参数的重载版本,用于直接初始化指针。这样,p2就成功地管理了动态分配的整型对象,并在适当的时候自动释放内存,以避免内存泄漏。

综上所述,正确的方式是使用直接初始化来创建std::shared_ptr对象,以确保正确地管理动态分配的内存。

默认情况下,用来初始化智能指针的内置指针必须指向动态内存,因为智能指针默认使用delete释放它所管理的对象。如果要将智能指针绑定到一个指向其他类型资源的指针上,就必须提供自定义操作来代替delete

不要混合使用内置指针和智能指针。当将shared_ptr绑定到内置指针后,资源管理就应该交由shared_ptr负责。不应该再使用内置指针访问shared_ptr指向的内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ptr is created and initialized when process is called
void process(shared_ptr<int> ptr)
{
// use ptr
} // ptr goes out of scope and is destroyed

int *x(new int(1024)); // dangerous: x is a plain pointer, not a smart pointer
process(x); // error: cannot convert int* to shared_ptr<int>
process(shared_ptr<int>(x)); // legal, but the memory will be deleted!
int j = *x; // undefined: x is a dangling pointer!

shared_ptr<int> p(new int(42)); // reference count is 1
process(p); // copying p increments its count; in process the reference count is 2
int i = *p; // ok: reference count is 1

这段代码展示了关于智能指针和裸指针之间的一些潜在问题。让我逐步解释:

  1. int *x(new int(1024));: 这行代码创建了一个动态分配的整型对象,并将其地址赋给了裸指针 x。裸指针不具备智能指针的内存管理功能,需要手动释放内存。

  2. process(x);: 这行代码试图将裸指针 x 传递给接受 shared_ptr<int> 类型参数的函数 process()。然而,由于 process() 函数需要的是一个智能指针,因此无法将裸指针传递给它,会导致编译错误。

  3. process(shared_ptr<int>(x));: 这行代码通过使用 shared_ptr<int> 的临时对象来传递裸指针 xprocess() 函数。这样做是合法的,但是需要注意的是,shared_ptr 会假定它所管理的资源是通过 new 创建的,因此会尝试释放 x 所指向的内存,这样做会导致内存泄漏或者程序崩溃,因为 x 不是通过 new 创建的。

  4. int j = *x;: 这行代码试图使用裸指针 x 来访问其所指向的内存中的值。然而,由于之前已经将 x 所指向的内存释放了,因此 x 成为了悬空指针,访问它的值会导致未定义行为。

  5. shared_ptr<int> p(new int(42));: 这行代码创建了一个动态分配的整型对象,并用 shared_ptr<int> 对象 p 来管理它。智能指针 p 具有内存管理功能,可以确保在适当的时候自动释放内存。

  6. process(p);: 这行代码将 shared_ptr<int> 对象 p 传递给了 process() 函数。智能指针 p 的引用计数会增加,因为 process() 函数会对其进行拷贝,这样在函数内部也会有一个指向相同内存的智能指针。

  7. int i = *p;: 这行代码试图使用智能指针 p 来访问其所管理的内存中的值。由于智能指针 p 是有效的,它所管理的内存也是有效的,因此可以正常地使用 p 来访问其所指向的值。

智能指针的get函数返回一个内置指针,指向智能指针管理的对象。主要用于向不能使用智能指针的代码传递内置指针。使用get返回指针的代码不能delete此指针。

不要使用get初始化另一个智能指针或为智能指针赋值。

1
2
3
4
5
6
7
shared_ptr<int> p(new int(42));    // reference count is 1
int *q = p.get(); // ok: but don't use q in any way that might delete its pointer
{ // new block
// undefined: two independent shared_ptrs point to the same memory
shared_ptr<int>(q);
} // block ends, q is destroyed, and the memory to which q points is freed
int foo = *p; // undefined; the memory to which p points was freed

这段代码涉及了在使用 shared_ptr 时通过 get() 函数获取其内部裸指针的情况。让我逐步解释:

  1. shared_ptr<int> p(new int(42));: 这行代码创建了一个动态分配的整型对象,并用 shared_ptr<int> 对象 p 来管理它。智能指针 p 的引用计数为1。

  2. int *q = p.get();: 这行代码通过 get() 函数获取了 shared_ptr 对象 p 内部所指向的裸指针,并将其赋值给了裸指针 q。但是需要注意的是,尽管 q 指向了与 p 相同的内存地址,但它不会增加引用计数。get() 函数的目的是为了兼容传统接口,但是使用裸指针 q 的任何地方都需要格外小心,以避免在 p 的生命周期内使用了已经释放的内存。

  3. {} 内的新块: 在这个新块中,创建了一个匿名的 shared_ptr<int> 对象,其构造函数接受了裸指针 q。由于这个 shared_ptr 对象是在一个新的作用域中创建的,所以它会在块结束时被销毁。因此,当块结束时,引用计数会减少,并且由 q 所指向的内存也会被释放。

  4. int foo = *p;: 这行代码试图使用智能指针 p 来访问其所管理的内存中的值。然而,在上一步中,由于块结束时释放了 q 所指向的内存,因此 p 现在成为了悬空指针,访问它的值将导致未定义行为。

综上所述,尽管使用 get() 函数获取了智能指针的裸指针,但需要特别小心地使用裸指针,以确保不会在智能指针的生命周期内使用已释放的内存,避免出现未定义行为。

可以用reset函数将新的指针赋予shared_ptr。与赋值类似,reset会更新引用计数,如果需要的话,还会释放内存空间。reset经常与unique一起使用,来控制多个shared_ptr共享的对象。

1
2
3
if (!p.unique())
p.reset(new string(*p)); // we aren't alone; allocate a new copy
*p += newVal; // now that we know we're the only pointer, okay to change this object

智能指针和异常(Smart Pointers and Exceptions)

如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放。

1
2
3
4
5
6
7
8
9
10
11
12
void f()
{
int *ip = new int(42); // dynamically allocate a new object
// code that throws an exception that is not caught inside f
delete ip; // free the memory before exiting
}

void f()
{
shared_ptr<int> sp(new int(42)); // allocate a new object
// code that throws an exception that is not caught inside f
} // shared_ptr freed automatically when the function ends

默认情况下shared_ptr假定其指向动态内存,使用delete释放对象。创建shared_ptr时可以传递一个(可选)指向删除函数的指针参数,用来代替delete

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct destination;    // represents what we are connecting to
struct connection; // information needed to use the connection
connection connect(destination*); // open the connection
void disconnect(connection); // close the given connection
void end_connection(connection *p)
{
disconnect(*p);
}

void f(destination &d /* other parameters */)
{
connection c = connect(&d);
shared_ptr<connection> p(&c, end_connection);
// use the connection
// when f exits, even if by an exception, the connection will be properly closed
}

智能指针规范:

  • 不使用相同的内置指针值初始化或reset多个智能指针。
  • 不释放get返回的指针。
  • 不使用get初始化或reset另一个智能指针。
  • 使用get返回的指针时,如果最后一个对应的智能指针被销毁,指针就无效了。
  • 使用shared_ptr管理并非new分配的资源时,应该传递删除函数。

unique_ptrunique_ptr

shared_ptr不同,同一时刻只能有一个unique_ptr指向给定的对象。当unique_ptr被销毁时,它指向的对象也会被销毁。

make_unique函数(C++14新增,定义在头文件memory中)在动态内存中分配一个对象并初始化它,返回指向此对象的unique_ptr

1
2
3
unique_ptr<int> p1(new int(42));
// C++14
unique_ptr<int> p2 = make_unique<int>(42);

由于unique_ptr独占其指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作。

release函数返回unique_ptr当前保存的指针并将其置为空。

reset函数成员接受一个可选的指针参数,重新设置unique_ptr保存的指针。如果unique_ptr不为空,则它原来指向的对象会被释放。

1
2
3
4
5
// transfers ownership from p1 (which points to the string Stegosaurus) to p2
unique_ptr<string> p2(p1.release()); // release makes p1 null
unique_ptr<string> p3(new string("Trex"));
// transfers ownership from p3 to p2
p2.reset(p3.release()); // reset deletes the memory to which p2 had pointed

这段代码展示了如何使用 release()reset() 函数在 unique_ptr 之间转移所有权。让我逐步解释:

  1. unique_ptr<string> p2(p1.release());
    • p1.release() 函数释放了 p1 所管理的内存,同时返回了指向该内存的指针,并且将 p1 置空(指向 nullptr)。
    • 然后,将这个指针传递给了 unique_ptr 构造函数,用于构造一个新的 unique_ptr 对象 p2,从而完成了所有权的转移。现在,p2 拥有了原先由 p1 管理的内存,而 p1 不再拥有该内存。
  2. unique_ptr<string> p3(new string("Trex"));
    • 创建了一个动态分配的字符串对象,并用 "Trex" 来初始化它,并将其管理权交给 unique_ptr 对象 p3
  3. p2.reset(p3.release());
    • p3.release() 函数释放了 p3 所管理的内存,并返回了指向该内存的指针,同时将 p3 置空。
    • 然后,reset() 函数接受这个指针作为参数,并释放了 p2 所管理的内存,然后将 p2 重新指向了这块新的内存。这样,所有权从 p3 转移到了 p2

综上所述,release() 函数将 unique_ptr 对象的所有权释放,并返回指向管理的内存的指针,同时将 unique_ptr 置空。而 reset() 函数允许将 unique_ptr 重新指向新的内存,并释放原先管理的内存。通过这两个函数的组合使用,可以实现 unique_ptr 之间的所有权转移。

调用release会切断unique_ptr和它原来管理的对象之间的联系。release返回的指针通常被用来初始化另一个智能指针或给智能指针赋值。如果没有用另一个智能指针保存release返回的指针,程序就要负责资源的释放。

1
2
p2.release();   // WRONG: p2 won't free the memory and we've lost the pointer
auto p = p2.release(); // ok, but we must remember to delete(p)

这段代码使用了 release() 函数来释放 unique_ptr 对象 p2 所管理的内存,但是存在一些问题。让我详细解释一下:

  1. p2.release();
    • 这行代码调用了 release() 函数,该函数释放了 p2 所管理的内存,并返回了指向该内存的指针。但是需要注意的是,release() 函数只是释放了内存,而并没有将 unique_ptr 置空。
    • 这是一个错误的用法,因为 release() 函数只是释放了内存,而并没有将 unique_ptr 置空,所以 p2 仍然持有对原始内存的所有权,但是丢失了对原始指针的控制,这样会导致内存泄漏。
  2. auto p = p2.release();
    • 这行代码调用了 release() 函数,将 p2 所管理的内存释放,并返回了指向该内存的指针。然后,使用 auto 关键字声明一个指针 p,将返回的指针赋给了 p
    • 这种用法也是正确的,但是需要注意的是,现在需要手动管理内存的释放。在使用 auto 声明的指针 p 后,需要在适当的时候使用 delete p 手动释放内存,否则会导致内存泄漏。

总的来说,release() 函数用于释放 unique_ptr 对象所管理的内存,并返回指向该内存的裸指针,但是需要注意确保在适当的时候手动管理内存的释放,以避免内存泄漏。

不能拷贝unique_ptr的规则有一个例外:可以拷贝或赋值一个即将被销毁的unique_ptr(移动构造、移动赋值)。

1
2
3
4
5
6
unique_ptr<int> clone(int p)
{
unique_ptr<int> ret(new int (p));
// . . .
return ret;
}

weak_ptrweak_ptr

weak_ptr是一种不控制所指向对象生存期的智能指针,它指向一个由shared_ptr管理的对象。将weak_ptr绑定到shared_ptr不会改变shared_ptr的引用计数。如果shared_ptr被销毁,即使有weak_ptr指向对象,对象仍然有可能被释放。

操作 含义
weak_ptr<T> w(sp) wshared_ptr sp指向相同对象
w.use_count() 返回与w共享对象的shared_ptr的数量·
w.expired() w.use_count() == 0,则返回true
w.lock() w.expired() == true,则返回空shared_ptr;否则返回一个指向w的对象的shared_ptr

创建一个weak_ptr时,需要使用shared_ptr来初始化它。

1
2
auto p = make_shared<int>(42);
weak_ptr<int> wp(p); // wp weakly shares with p; use count in p is unchanged

使用weak_ptr访问对象时,必须先调用lock函数。该函数检查weak_ptr指向的对象是否仍然存在。如果存在,则返回指向共享对象的shared_ptr,否则返回空指针。

1
2
3
4
5
if (shared_ptr<int> np = wp.lock())
{
// true if np is not null
// inside the if, np shares its object with p
}

动态数组(Dynamic Arrays)

使用allocator类可以将内存分配和初始化过程分离,这通常会提供更好的性能和更灵活的内存管理能力。

new和数组(new and Arrays)

使用new分配对象数组时需要在类型名之后跟一对方括号,在其中指明要分配的对象数量(必须是整型,但不必是常量)。new返回指向第一个对象的指针(元素类型)。

1
2
// call get_size to determine how many ints to allocate
int *pia = new int[get_size()]; // pia points to the first of these ints

由于new分配的内存并不是数组类型,因此不能对动态数组调用beginend,也不能用范围for语句处理其中的元素。

默认情况下,new分配的对象是默认初始化的。可以对数组中的元素进行值初始化,方法是在大小后面跟一对空括号()。在新标准中,还可以提供一个元素初始化器的花括号列表。如果初始化器数量大于元素数量,则new表达式失败,不会分配任何内存,并抛出bad_array_new_length异常。

1
2
3
4
5
6
7
8
9
int *pia = new int[10];     // block of ten uninitialized ints
int *pia2 = new int[10](); // block of ten ints value initialized to 0
string *psa = new string[10]; // block of ten empty strings
string *psa2 = new string[10](); // block of ten empty strings
// block of ten ints each initialized from the corresponding initializer
int *pia3 = new int[10] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
// block of ten strings; the first four are initialized from the given initializers
// remaining elements are value initialized
string *psa3 = new string[10] { "a", "an", "the", string(3,'x') };

虽然可以使用空括号对new分配的数组元素进行值初始化,但不能在括号中指定初始化器。这意味着不能用auto分配数组。

动态分配一个空数组是合法的,此时new会返回一个合法的非空指针。对于零长度的数组来说,该指针类似尾后指针,不能解引用。

使用delete[]释放动态数组。

1
2
delete p;       // p must point to a dynamically allocated object or be null
delete [] pa; // pa must point to a dynamically allocated array or be null

如果在delete数组指针时忘记添加方括号,或者在delete单一对象时使用了方括号,编译器很可能不会给出任何警告,程序可能会在执行过程中行为异常。

unique_ptr可以直接管理动态数组,定义时需要在对象类型后添加一对空方括号[]

1
2
3
// up points to an array of ten uninitialized ints
unique_ptr<int[]> up(new int[10]);
up.release(); // automatically uses delete[] to destroy its pointer
  1. 自动使用 delete[]
    • release() 函数被调用时,unique_ptr 对象会自动根据其类型来决定使用 delete 还是 delete[] 操作符来销毁其所管理的内存。
    • 对于 unique_ptr<int[]> 类型,unique_ptr 知道它所管理的是一个动态分配的数组,因此在调用 release() 函数后,它会使用 delete[] 操作符来销毁该数组。

unique_ptr不同,shared_ptr不直接支持动态数组管理。如果想用shared_ptr管理动态数组,必须提供自定义的删除器。

1
2
3
// to use a shared_ptr we must supply a deleter
shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; });
sp.reset(); // uses the lambda we supplied that uses delete[] to free the array
  1. shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; });
    • 这行代码创建了一个名为 spshared_ptr 对象,用于管理一个动态分配的整型数组。
    • 构造函数的第一个参数是指向动态分配的数组的指针,第二个参数是一个 lambda 表达式,用于自定义删除器。
    • lambda 表达式 [](int *p) { delete[] p; } 接受一个 int* 类型的指针参数,并使用 delete[] 操作符来释放指向数组的内存。这样就确保了当 shared_ptr 对象销毁时,使用自定义删除器来正确释放动态分配的数组内存。
  2. sp.reset();
    • 这行代码调用了 reset() 函数,该函数将 shared_ptr 对象 sp 置空,释放其所管理的内存。
    • 在调用 reset() 函数时,shared_ptr 对象将会调用之前提供的自定义删除器 lambda 表达式,使用 delete[] 操作符来释放动态分配的数组内存。
    • 这样可以确保在使用 shared_ptr 管理动态分配数组的情况下,内存能够正确释放,避免内存泄漏问题。

shared_ptr未定义下标运算符,智能指针类型也不支持指针算术运算。因此如果想访问shared_ptr管理的数组元素,必须先用get获取内置指针,再用内置指针进行访问。

1
2
3
// shared_ptrs don't have subscript operator and don't support pointer arithmetic
for (size_t i = 0; i != 10; ++i)
*(sp.get() + i) = i; // use get to get a built-in pointer

allocator类(The allocator Class)

allocator类是一个模板,定义时必须指定其可以分配的对象类型。

1
2
allocator<string> alloc;    // object that can allocate strings
auto const p = alloc.allocate(n); // allocate n unconstructed strings

这段代码创建了一个名为 allocallocator<string> 对象,该对象可以用来分配字符串类型的内存空间。然后,使用 allocate() 函数来分配了 n 个未构造的字符串对象,并将分配的内存空间的起始地址赋值给了 p

allocator分配的内存是未构造的,程序需要在此内存中构造对象。新标准库的construct函数接受一个指针和零或多个额外参数,在给定位置构造一个元素。额外参数用来初始化构造的对象,必须与对象类型相匹配。

1
2
3
4
auto q = p;     // q will point to one past the last constructed element
alloc.construct(q++); // *q is the empty string
alloc.construct(q++, 10, 'c'); // *q is cccccccccc
alloc.construct(q++, "hi"); // *q is hi!
  1. auto q = p;
    • 这行代码将 p 的值赋给了 q,使得 q 指向分配的内存空间的起始位置。
  2. alloc.construct(q++);
    • 这行代码调用了 construct() 函数,用于在 q 指向的位置构造一个未初始化的字符串对象。
    • q++ 表示先使用 q 的值进行构造,然后递增 q 指向下一个位置。因此,此时 q 指向下一个未构造的字符串对象的位置。
    • 构造后的字符串对象是空字符串,因为未提供任何参数来初始化它。
  3. alloc.construct(q++, 10, 'c');
    • 这行代码调用了 construct() 函数,用于在 q 指向的位置构造一个未初始化的字符串对象。
    • 10, 'c' 是传递给 construct() 函数的参数,用于初始化构造的字符串对象。这表示该字符串对象由10个字符 'c' 组成。
    • q++ 递增了 q,使得 q 指向下一个未构造的字符串对象的位置。
  4. alloc.construct(q++, "hi");
    • 这行代码调用了 construct() 函数,用于在 q 指向的位置构造一个未初始化的字符串对象。
    • "hi" 是传递给 construct() 函数的参数,用于初始化构造的字符串对象。这表示该字符串对象的内容为 "hi"
    • q++ 递增了 q,使得 q 指向下一个未构造的字符串对象的位置。

直接使用allocator返回的未构造内存是错误行为,其结果是未定义的。

对象使用完后,必须对每个构造的元素调用destroy进行销毁。destroy函数接受一个指针,对指向的对象执行析构函数。

1
2
while (q != p)
alloc.destroy(--q); // free the strings we actually allocated

deallocate函数用于释放allocator分配的内存空间。传递给deallocate的指针不能为空,它必须指向由allocator分配的内存。而且传递给deallocate的大小参数必须与调用allocator分配内存时提供的大小参数相一致。

1
alloc.deallocate(p, n);

czs108/Cpp-Primer-5th-Notes-CN: 📚 《C++ Primer中文版(第5版)》笔记 (github.com)