《C++Primer》第十二章 动态内存

2024-04-16 08:38

本文主要是介绍《C++Primer》第十二章 动态内存,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

静态内存、栈内存和堆

我们前面只提到了静态内存或栈内存:

  • 静态内存:用来保存局部static、类static数据成员和定义在任何函数之外的变量
  • 栈内存:保存定义在函数内的非static对象

分配在静态内存或者栈内存的对象由编译器自动创建和销毁。对于栈对象仅在其定义的程序块运行时才存在,static对象在使用之前分配,在程序结束时销毁。

每个程序还拥有一个内存池(被称为自由空间free store或堆heap)。程序用堆来存储动态分配的对象,当动态对象不再使用时,我们的代码必须显式销毁它们。

动态内存和智能指针

c++中,动态内存的管理是通过一对运算符来完成的:

  • new:在动态内存中为对象分配空间并返回一个指向该对象的指针
  • delete:接收一个动态对象的指针,销毁该对象并释放与之关联的内存空间

这种管理方式有两个问题:

  • 如果我们忘记释放内存,就会造成内存泄漏
  • 如果在有指针引用内存的情况下我们就释放它,就会出现“野指针”

新标准库提供了三种智能指针smart pointer

  • shared_ptr:允许多个指针指向同个对象
  • unique_ptr:“独占”所指向的对象
  • weak_ptr:弱引用,指向shared_ptr所管理的对象
1. shared_ptr类

shared_ptrunique_ptr都支持的操作:

  • shared_ptr<T> spunique_ptr<T> up:空智能指针,可以指向类型为T的对象
  • p:将p作为一个条件判断,如果p指向一个对象则为true
  • *p:解引用p,获得它指向的对象
  • p->mem:等价于(*p).mem
  • p.get():返回p中保存的指针,要小心使用。如果智能指针释放了其对象,那么返回的指针所指向的对象也就消失了
  • swap(p, q):交换pq中的指针
  • p.swap(q):同上

shared_ptr独有的操作:

  • make_shared<T>(args):返回一个shared_ptr,指向一个动态分配的类型为T的对象,使用args初始化该对象
  • shared_ptr<T>p(q)pshared_ptr q的拷贝,此操作会增加q的计数器,q中指针必须得能转换为T*
  • p=qpq都是shared_ptr,所保存的指针必须能相互转换,这一步会递减p的引用计数,递增q的引用计数,若p的引用计数为0则将其管理的原内存释放
  • p.unique()p.use_count()1则返回true
  • p.use_count():返回和p共享对象的智能指针数量,可能很慢,主要用于调试
1.1 make_shared函数
// p4指向一个值为"9999999999"的string
shared_ptr<string> p4 = make_shared<string>(10, '9');
// p5指向一个值初始化的int, 即值为0
shared_ptr<int> p5 = make_shared<int>();

类似于顺序容器的emplace成员,make_shared用其参数来构造给定类型的对象。即调用make_shared<string>传递的参数必须与string某个构造函数相匹配。如果我们不传递任何参数,那么就进行值初始化。

1.2 shared_ptr的拷贝和赋值

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

auto p = make_shared<int>(42);  // p指向的对象只有p一个引用者
auto q(p);  // p和q指向同一对象,此对象有两个引用者

每个shared_ptr都有一个与之关联的引用计数器reference count,无论何时我们拷贝一个shared_ptr,计数器都会递增:

  • 用一个shared_ptr初始化另一个shared_ptr
  • shared_ptr作为参数传递给一个函数
  • 作为函数的返回值

当我们给一个shared_ptr赋予一个新值,或者shared_ptr被销毁(比如一个局部的shared_ptr离开其作用域)时计数器就会递减。

一旦一个shared_ptr的计数器变成0,它就会自动释放所管理的对象:

auto r = make_shared<int>(42); // r指向的int只有一个引用者
r = q; // 给r赋值,令它指向另一地址// 递增q指向的对象的引用计数// 递减r原来指向对象的引用计数// r原来指向的对象已经没有引用者, 会自动释放
1.3 shared_ptr自动销毁所管理的对象

当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr通过对象的一个特殊成员函数——析构函数destructor完成销毁工作的。析构函数一般用于释放对象所分配的资源。

shared_ptr<Foo> use_factory(T arg)
{shared_ptr<Foo> p = factory(arg);// 使用preturn p; // 当我们返回p时,引用计数执行了递增操作
} // p离开了作用域,但它指向的内存不会被释放掉

由于在最后一个shared_ptr销毁前内存都不会释放,保证shared_ptr在无用之后不再保留就很有必要。有一种常见的例子是:

你将shared_ptr存放在一个容器之中,随后重排了容器不再需要某些元素。在这种情况下,你应该使用erase删除那些不再需要的shared_ptr元素。

1.4 使用了动态生存期的资源的类

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

  1. 程序不知道自己需要使用多少对象
  2. 程序不知道所需对象的准确类型
  3. 程序需要在多个对象之间共享数据

容器类是出于第一种原因使用的动态内存的典型例子,第十五章的面向对象程序设计会介绍出于第二种原因使用动态内存的例子。本节中我们定义一个类StrBlob类,它使用动态内存主要是为了让多个对象能共享相同的底层数据。

1.5 定义StrBlob类

我们将使用vectorStrBlob中保存元素,但是如果我们在一个StrBlob对象中直接保存vetor,那么对象销毁时对应的成员也会销毁。比如b1b2是两个StrBlob对象,如果此vector保存在b2中,那么当b2离开作用域时此vector也会被销毁。为了保证此vector中的元素继续存在,我们将vector保存在动态内存中。

class StrBlob {
public:typedef std::vector<std::string>::size_type size_type;StrBlob();StrBlob(std::initializer_list<std::string> il);size_type size() const { return data->size(); }bool empty() const { return data->empty(); }// 添加和删除元素void push_back(const std::string &t) {data->push_back(t);}void pop_back();// 元素访问std::string& front();std::string& back();
private:std::shared_ptr<std::vector<std::string>> data;// 如果data[i]不合法,抛出一个异常void check(size_type i, const std::string &msg) const;
}// 构造函数:两个都使用初始化列表来初始化data成员, 令它指向一个动态分配的vector
StrBlob::StrBlob() : data(make_shared<vector<string>>()) { }
StrBlob::StrBlob(initializer_list<string> il) :data(make_shared<vector<string>>(il)) { }

pop_backfrontback在试图访问元素之前都必须确保元素存在,我们为StrBlob定义了一个名为checkprivate工具函数,用于确定索引是否在合法范围内。

void StrBlob::check(size_type i, const string &msg) const
{if (i >= data->size())throw out_of_range(msg);
}string& StrBlob::front()
{// 如果vector为空, check会抛出来一个异常check(0, "front on empty StrBlob");return data=>front();
}string& StrBlob::back()
{check(0, "back on empty StrBlob");data->pop_back();
}
1.6 StrBlob的拷贝、赋值和销毁

StrBolb使用默认版本的拷贝、赋值和销毁成员函数来对此类型的对象进行这些操作。因为StrBolb只有一个shared_ptr数据成员,因此当我们拷贝、赋值或销毁一个StrBlob对象时,它的shared_ptr成员会被拷贝、赋值或销毁。

拷贝一个shared_ptr会递增其引用计数,将一个shared_ptr赋予另一个shared_ptr会递增赋值号右侧的shared_ptr的引用计数,递减左侧sahred_ptr的引用计数。当一个shared_ptr的引用计数变为0之后它指向的对象会被自动销毁。

2. 直接管理内存
2.1 使用new动态分配和初始化对象

需要注意如下几点:

  • 动态分配的对象执行默认初始化:内置类型或组合类型的对象的值是未定义的,而类类型对象的值用默认构造函数进行初始化
  • 值初始化的内置类型对象有着良好定义的值,但是默认初始化的对象的值是未定义的
  • 一个动态分配的const对象必须初始化,对于定义了默认构造函数的类类型可以隐式初始化,但是其他类型的对象必须显式初始化;由于分配的对象是const的,new返回的指针是一个指向const对象的指针
  • 如果程序用光内存,那么new就会抛出一个类型为bad_alloc的异常,我们可以通过int *pi2 = new (nothrow) int;来使它在分配失败时返回一个空指针

初始化相关:

// 默认初始化
int *pi = new int;       // pi指向一个未初始化的int
string *ps = new string; // 初始化为空string// 直接初始化
int *pi = new int(1024);
string *ps = new string(10, '9');
vector<int> *pv = new vector<int>{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};// 值初始化
string *ps1 = new string;   // 默认初始化为空string
string *ps = new string();  // 值初始化为空string
int *pi1 = new int;         // 默认初始化, 值未定义
int *pi2 = new int();       // 值初始化为0

动态分配const对象:

// 分配并初始化一个const int
const int *pci = new const int(1024);
// 分配并默认初始化一个const的空string
const string *pcs = new const string;
2.2 释放动态内存

我们可以通过delete将动态内存归还给系统,执行了两个动作:

  • 销毁给定的指针指向的对象
  • 释放对应的内存
delete p; // p必须指向一个动态分配的对象或者是一个空指针

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

动态对象的生存期直到被释放为止,对于一个由内置指针管理的动态对象,直到被显式释放之前它都是存在的。返回指向动态内存指针的函数给其调用者增加了一个额外负担——调用者必须记得释放内存:

// factory返回一个指针,指向一个动态分配的对象
Foo* factory(T arg)
{// 处理argreturn new Foo(arg); // 调用者负责释放此内存
}void use_factory(T arg)
{Foo *p = factory(arg);// 使用p但不delete它
} // p离开了它的作用域,但是它所指向的内存还没有被释放!

两种处理方法,方法一就是在use_factory函数内记得释放内存:

void use_factory(T arg)
{Foo *p = factory(arg);// 使用pdelete p;
}

方法二是系统中其他代码可能要使用use_factory所分配的对象,我们应该修改此函数让他返回一个指针:

void use_factory(T arg)
{Foo *p = factory(arg);// 使用preturn p; // 交给use_factory函数的调用者释放内存
}

使用newdelete管理内存存在三个最常见的问题:

  • 忘记delete内存:这种情况下内存再也不可能归还给自由空间,也就是我们所说的“内存泄漏”问题
  • 使用已经释放掉的对象:通过在释放内存后将指针置为空,有时可以检测出这种问题
  • 同一块内存释放两次:当有两个指针指向相同的动态分配对象时可能发生这种错误,第一次delete时对象的内存就被归还给自由空间了,第二次delete可能破坏自由空间

坚持只使用智能指针,就可以避免上述的所有问题。

delete一个指针后指针值就变为无效了,但是很多机器上指针仍然保存着(已经释放了的)动态内存的地址。在delete之后指针就变成了“空悬指针”dangling pointer,即指向一块曾经保存数据对象但是现在已经失效的内存的指针。有一种可以避免空悬指针的做法:在指针即将离开其作用域之前释放掉它所关联的内存,这样在指针关联的内存被释放掉之后,就没有任何机会继续使用指针了。另一种做法是在delete之后将指针赋值为nullptr,这样可以清楚地指出指针不指向任何对象。然而可能有多个指针指向同一块内存:

int *p(new int(42)); // p指向动态内存
auto p = q; // p和q指向同一块内存
delete p;   // p和q均变得无效
p = nullptr; // 指出p不再绑定任何对象// 这时候q变成“空悬指针”,查找指向相同内存的所有指针是异常困难的
2.3 shared_ptr和new结合使用

如果我们不初始化一个智能指针,它就会被初始化为一个空指针。我们还可以用new返回的指针来初始化智能指针:

shared_ptr<double> p1; // shared_ptr可以指向一个double
shared_ptr<int> p2(new int(42)); // p2指向一个值为42的int

需要注意的是接收指针参数的智能指针构造函数是explicit的,因此我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针:

shared_ptr<int> p1 = new int(1024);   // 错误:必须使用直接初始化形式
shared_ptr<int> p2(new int(1024));    // 正确:使用了直接初始化形式

一个用于初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。

定义和改变shared_ptr的其他方法:

  • shared_ptr<T> p(q)p管理内置指针q所指向的对象,q必须指向new分配的内存,且能够转化为T*类型
  • shared_ptr<T> p(u)punique_ptr u那里接管了对象的所有权,将u置为空
  • shared_ptr<T> p(q, d)p接管了内置指针q所指向的对象的所有权,q必须能转换为T*类型。p将使用可调用对象d来代替delete
  • shared_ptr<T> p(p2,d)pshared_ptr p2的拷贝,唯一的区别是p将用可调用对象d来代替delete
  • p.reset(); p.reset(q); p.reset(q,d):若p是唯一指向其对象的shared_ptrreset会释放此对象。若传递了可选的参数内置指针q,会令p指向q,否则会将p置为空。若还传递了参数d,将会调用d而不是delete来释放q

只能指针类型定义了一个名为get的函数,它返回一个内置指针,指向智能指针管理的对象。此函数是为了这样一种情况而设计的:我们需要向不能使用智能指针的代码传递一个内置指针。使用get返回的指针的代码不能delete指针

虽然编译器不会报错,但是将另一个智能指针也绑定到get返回的指针上是错误的。首先你只有在确定代码不会delete指针的情况下才能使用get;另外不要用get初始化另一个智能指针或者为另一个智能指针赋值。

shared_ptr<int> p(new int(42)); // 引用计数为1
int *q = p.get(); // 正确:但使用q时要注意不要让它管理的指针被释放
{// 未定义:两个独立的shared_ptr指向相同的内存shared_ptr<int>(q);
} // 程序块结束, q被销毁, 它指向的内存被释放
int foo = *p; // 未定义:p指向的内存已经被释放了

我们不能将一个指针赋予shared_ptr,但是我们可以通过reset将一个新的指针赋予shared_ptr

p = new int(1024);       // 错误:不能将一个指针赋予shared_ptr
p.reset(new int(1024));  // 正确:p指向一个新对象

resetunique经常一起使用,来控制多个shared_ptr共享的对象。在改变底层对象之前,我们得先检查一下自己是不是当前对象仅有的用户。如果不是,在改变之前需要制作一份新的拷贝:

if (!p.unique())p.reset(new string(*p));  // 我们不是唯一用户;分配新的拷贝
*p += newVal; // 现在我们知道自己是唯一的用户,可以改变用户的值
2.4 智能指针和异常

为了确保使用异常处理的程序能在异常发生后资源能被正确地释放,一个简单的确保资源被释放的方法是使用智能指针。如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放:

void f()
{shared_ptr<int> sp(new int(42));  // 分配一个新对象// 这段代码抛出来一个异常,且在f中未被捕获
} // 在函数结束时shared_ptr自动释放内存
2.5 智能指针和哑类

包括所有标准库在内的很多C++类都定义了析构函数负责清理对象使用的资源。但是不是所有的类都是这么良好定义的,特别是为CC++两种语言设计的类,通常都要求用户手动释放所用的任何资源。与管理动态内存类似,我们可以使用类似的技术来管理不具有良好定义的析构函数。例如我们正在使用一个CC++都使用的网络库,我们通过disconnect来显示释放:

struct destination;  // 表示我们正在连接什么
struct connection;   // 使用连接所需的信息
connection connect(destination *);  // 打开连接
void disconnect(connection);        // 关闭给定的连接
void f(destination &d /* 其他参数 */)
{// 获得一个连接, 注意在使用完后要关闭它connection c = connect(&d);// 使用连接// 如果我们再f退出时忘记使用disconnect, 就无法关闭c了
}

由于connection没有析构函数,因此不能在f结束时由析构函数自动关闭连接。使用shared_ptr来管理这种哑类已经被证明是一种有效的方法。

使用shared_ptr管理动态对象时,它默认地对它管理的指针进行delete操作。我们可以使用一个函数来代替delete

void end_connection(connection *p) { disconnect(*p); }
void f(destination &d /* 其他参数 */) 
{connection c = connect(&d);shared_ptr<connection> p(&c, end_connection);// 使用连接// 当f退出时(即使是异常退出), connection也能被正确关闭
}
2.6 智能指针使用注意事项

为了正确使用智能指针,我们需要遵守一些规范:

  • 不使用相同的内置指针值初始化或reset多个智能指针
  • delete get()返回的指针
  • 不使用get()初始化或reset另一个智能指针
  • 如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,那你的指针就无效了
  • 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器
2.7 unique_ptr

一个unique_ptr拥有它所指向的对象,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上:

unique_ptr<int> p2(new int(42));

由于unique_ptr拥有它指向的对象,因此不接受普通的拷贝和赋值:

unique_ptr<string> p1(new string("Stegisaurus"));
unique_ptr<string> p2(p1);  // 不支持拷贝
unique_ptr<string> p3;
p3 = p1; // 不支持赋值

unique_ptr支持的操作包括:

  • unique_ptr<T> u1:空unique_ptr,可以指向类型为T的对象,u1会调用delete来释放指针
  • unique_ptr<T, D> u2:同上,但是会调用D的可调用对象来释放它的指针
  • unique_ptr<T, D> u(d):空unique_ptr,指向类型为T的对象,用类型为D的对象d来代替delete
  • u = nullptr:释放u指向的对象,将u置为空
  • u.release()u放弃对指针的控制权,释放指针,并将u置为空
  • u.reset():释放u指向的对象
  • u.reset(q); u.reset(nullptr):如果提供了内置指针q,令u指向这个对象;否则将u置为空

虽然我们不能拷贝或者赋值unique_ptr,但可以通过调用releasereset将指针的所有权从一个(非const)unique_ptr转移给另一个unique_ptr

// 将所有权从p1转移给p2
unique_ptr<string> p2(p1.release()); // release将p1置为空
unique_ptr<string> p3(new string("Trex");
// 将所有权从p3转移给p2
p2.reset(p3.release()); // reset释放了p2原来指向的内存

release成员返回unique_ptr当前保存的指针并将其置为空,因此p2被初始化为p1原来保存的指针而p1被置为空。reset接收一个可选的指针参数,令unique_ptr重新指向给定的指针,如果unique_ptr不为空,它原来指向的对象被释放。

需要注意的是调用release会切断unique_ptr和它元拿来管理的对象之间的联系。release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。如果我们不用另一个智能指针来保存release返回的指针,我们就要负责资源的释放:

p2.release();  // 错误:p2不会释放内存,而且我们丢失了指针
auto p = p2.release(); // 正确,但是我们必须记得delete(p)

前面我们提到不能拷贝或者赋值一个unique_ptr,但是有一个例外:我们可以拷贝或者赋值一个将要被销毁的unique_ptr,最常见的就是从函数返回unique_ptr

// 下面两段代码编译器都知道要返回的对象即将被销毁,因此会执行特殊的“拷贝”
unique_ptr<int> clone(int p) {// 正确:从int* 创建一个unique_ptr<int>return unique_ptr<int>(new int(p));
}// 返回一个局部对象的拷贝
unique_ptr<int> clone(int p) {unique_ptr<int> ret(new int (p));// ...return ret;
}

类似于shared_ptrunique_ptr默认情况下使用delete释放它指向的对象,我们重载一个删除器,但是unique_ptr管理删除器的方式和shared_ptr不同,我们将在十六章介绍。重载一个unique_ptr中的删除器会影响到unique_ptr类型一级如何构造(或reset)该类型的对象。与重载关联容器的比较操作类似,在创建或reset一个unique_ptr对象时必须提供一个指定类型的可调用对象作为删除器:

// p指向一个ObjT类型对象,并使用一个delT类型的对象来释放objT对象
// 它会调用一个名为fcn的delT类型对象
unique_ptr<objT, delT> p (new objT, fcn);

重写一下前面的连接程序,用unique_ptr来代替shared_ptr

void f(destination &d /* 其他需要的参数 */)
{connection c = connect(&d); // 打开连接// 当p被销毁时,连接将被关闭unique_ptr<connection, decltype(end_connection)*>p(&c, end_connection);// 使用连接// 当f退出时(即使是异常退出),connection也会被正确关闭
}
2.8 weak_ptr

weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也还是会被释放。weak_ptr的操作包括:

  • weak_ptr<T> w:空weak_ptr可以指向类型为T的对象
  • weak_ptr<T> w(sp):与shared_ptr sp指向相同对象的weak_ptr,赋值后wp共享对象
  • w = pp可以是一个shared_ptr或者weak_ptr,赋值后wp共享对象
  • w.reset():将w置为空
  • w.use_count():与w共享对象的shared_ptr的数量
  • w.expired():若w.use_count()0则返回true
  • w.lock():如果expiredtrue,返回一个空shared_ptr,否则返回指向w对象的shared_ptr

我们创建一个weak_ptr是时需要用一个shared_ptr来初始化它。另外由于对象可能不存在,所以我们不能直接使用weak_ptr直接访问对象,而必须直接调用lock:此函数会检查weak_ptr指向的对象是不是仍存在:

autp p = make_share<int>(42);
weak_ptr<int> wp(p); // wp弱共享p; p的引用计数为0if (shared_ptr<int> np = wp.lock()) { // 如果np不为空则条件成立// 在if中, np和p共享对象
}

动态数组

newdelete运算符一次分配/释放一个对象,但某些应用需要我们一次为很多对象分配内存。当一个应用需要可变数量的对象时,我们更推荐使用vector或其他标准库容器。

大多数应用应该使用标准库而不是动态分配的数组。使用容器更为简单,更不容易出现内存管理错误并且可能有更好的性能。

使用容器的类可以使用默认版本的拷贝、赋值和析构操作。分配动态数组的类则必须定义自己版本的操作,在拷贝、复制以及销毁对象时管理所关联的内存。

1. new和数组

new分配要求数量的对象,并在分配成功后返回指向第一个对象的指针:

// 调用get_size确定分配多少个int
int *pia = new int[get_size()]; // pia指向第一个int// 另一种写法
typedef int arrT[42]; // arrT表示42个int的数组类型
int *p = new arrT;    // 分配一个42个int的数组; p指向第一个int

虽然我们通常称new T[]分配的内存为“动态数组”,当用new分配一个数组时,我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。

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

默认情况下,new分配的对象都是执行默认初始化的,可以对数组中的元素执行值初始化,方法是在大小之后跟一对空括号:

int *pia = new int[10];    // 10个未初始化的int
int *pia2 = new int[10](); // 10个值初始化为0的int
string *psa = new string[10]; // 10个空string
string *psa2 = new string[10](); // 10个空string

在新标准中,我们可以提供一个元素初始化器的花括号列表:

// 10个int分别用列表中对应的初始化器初始化
int *pia3 = new int[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
// 10个string, 前4个用给定的初始化器初始化,剩余的进行值初始化
// 如果初始化器数目大于元素数目,则new表达式失败不会分配任何内存
string *psa3 = new string[10]{"a", "an", "the", string(3, 'x')};

动态分配一个空数组是合法的,当我们用new分配一个大小为0的数组时,new返回一个合法的非空指针,此指针保证与new返回的其他任何指针都不相同。对于零长度的数组来说,此指针就像尾后指针一样。但这个指针不能解引用,因为它不指向任何元素。

char arr[0];   // 错误:不能定义长度为0的数组
char *cp = new char[0]; // 正确:但cp不能解引用
2. 释放动态数组
delete [] pa; // pa必须指向一个动态分配的数组或为空

该语句会销毁pa指向的数组中的元素并释放对应的内存。数组中的元素按逆序销毁,即最后一个元素首先销毁,以此类推。

如果我们在delete一个指向数组的指针时忽略了方括号,或者在delete一个指向单一对象的指针时使用了方括号,其行为是未定义的。

3. 智能指针和动态数组

标准库提供了一个可以管理new分配的数组的unique_ptr版本:

// up指向一个包含10个未初始化int的数组
unique_ptr<int []> up(new int[10]);
up.release(); // 自动用delete[]销毁其指针for (size_t i = 0; i != 10; ++i)up[i] = i; // 为每个元素赋予一个值

指向数组的unique_ptr支持的操作:

  • unique_ptr<T[]> uu可以指向一个动态分配的数组,数组元素类型为T
  • unique_ptr<T[]> u(p)u指向内置类型p所指向的动态分配的数组,p必须能转换成类型T*
  • u[i]:返回第i个对象

shared_ptr不支持直接管理动态数组,必须提供自己定义的删除器:

shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; });
sp.reset(); // 使用我们提供的lambda释放数组,它使用delete[]// shared_ptr未定义下标运算符,并且不支持指针的算术运算
for (size_t i = 0; i != 10; ++i) {*(sp.get() + i) = i;
}

allocator类

new有一些灵活性的限制,其中一方面表现在它将内存分配对象构造组合在一起,同样delete将对象析构和内存释放组合在一起。当我们分配单个对象时是有必要的,因为我们几乎肯定知道对象应该有什么值。当分配大块内存时,我们通常计划在这块内存上按需构造对象,因此我们希望将内存分配和对象构造分离。

这意味着我们可以分配大块内存,而只有在真正需要时才真正执行对象创建操作(付出一定开销)。

之所以有这个需求,是因为一般情况下将内存分配和对象构造组合在一起可能会导致不必要的浪费:

string *const p = new string[n]; // 构造n个空string
string s;
string *q = p; // q指向第一个p
while (cin >> s && q != p + n)*q++ = s;   // 赋予*q一个新值
const size_t size = q - p;
// 使用数组
delete []p;  // p指向一个数组, 记得用delete[]释放

在上面这个例子中,new表达式分配并初始化了nstring。一方面我们可能不需要nstring,因此我们可能创建了一些永远也用不到的对象。另一方面,对于那些确实要使用的对象,我们也在初始化之后立即赋予了它们新值,这样每个使用到的元素都被赋值了两次。

1. allocator类

该类提供一种类型感知的内存分配算法,它分配的内存是原始的、未构造的。当一个allocator对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置:

allocator<string> alloc;   // 可以分配string的allocator对象
auto const p = alloc.allocate(n); // 分配n个未经初始化的string

allocator支持的操作包括:

  • allocator<T> a:定义了一个名为aallocator对象,它可以为类型为T的对象的分配内存
  • a.allocate(n):分配一段原始的、未构造的内存,保存n个类型为T的对象
  • a.deallocate(p, n):释放从T*指针p中地址开始的内存,这块内存你保存了n个类型为T的对象;p必须是一个先前由allocate返回的指针, 且n必须是p创建时所要求的大小。在调用deallocate之前,用户必须对每个在这块内存中创建的对象调用destroy
  • a.construct(p, args)p必须是一个类型为T*的指针,指向一块原始内存;arg被传递给类型为T的构造函数,用来在p指向的内存中构造一个函数
  • a.destory(p)p为类型T*的指针,此算法对p指向的对象执行析构函数

为了使用allocate返回的内存,我们必须用construct构造对象,使用未构造的内存,其行为是未定义的。当我们使用完对象之后,必须对每个构造的元素使用destroy来销毁它们。

一旦元素被销毁后,就可以重新使用这部分内存来保存其他string,也可以将其归还给系统。释放内存通过deallocate来完成。

2. 拷贝和填充未初始化内存的算法

allocator算法包括:

  • uninitialized_copy(b,e,b2):从迭代器be指出的输入范围中拷贝元素到迭代器b2指定的未构造的原始内存中
  • uninitialized_copy_n(b,n,b2):从迭代器b指向的元素开始,拷贝n个元素到b2开始的内存中
  • uninitialized_fill(b,e,t):在迭代器be指定的原始内存范围内创建对象,对象的值均为t的拷贝
  • uninitialized_fill_n(b,n,t):在迭代器b指向的内存地址开始创建n个对象,b必须指向足够大的未构造的原始内存,能够容纳给定数量的对象

举个例子,我们希望把一个intvecotr中的元素拷贝到一个动态数组中,并且这个动态数组的长度是它的两倍,剩下的元素用一个给定值填充:

// 分配比vi元素所占空间大一倍的动态内存
auto p = alloc.allocate(vi.size() * 2);
// 通过拷贝vi中的元素来构造从p开始的元素
auto q = uninitialized_copy(
vi.begin(), vi.end(), p);
// 将剩余元素初始化为42
uninitialized_fill_n(q, vi.size(), 42);

这篇关于《C++Primer》第十二章 动态内存的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/908302

相关文章

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

06 C++Lambda表达式

lambda表达式的定义 没有显式模版形参的lambda表达式 [捕获] 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 有显式模版形参的lambda表达式 [捕获] <模版形参> 模版约束 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 含义 捕获:包含零个或者多个捕获符的逗号分隔列表 模板形参:用于泛型lambda提供个模板形参的名

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

【C++高阶】C++类型转换全攻略:深入理解并高效应用

📝个人主页🌹:Eternity._ ⏩收录专栏⏪:C++ “ 登神长阶 ” 🤡往期回顾🤡:C++ 智能指针 🌹🌹期待您的关注 🌹🌹 ❀C++的类型转换 📒1. C语言中的类型转换📚2. C++强制类型转换⛰️static_cast🌞reinterpret_cast⭐const_cast🍁dynamic_cast 📜3. C++强制类型转换的原因📝

C++——stack、queue的实现及deque的介绍

目录 1.stack与queue的实现 1.1stack的实现  1.2 queue的实现 2.重温vector、list、stack、queue的介绍 2.1 STL标准库中stack和queue的底层结构  3.deque的简单介绍 3.1为什么选择deque作为stack和queue的底层默认容器  3.2 STL中对stack与queue的模拟实现 ①stack模拟实现

c++的初始化列表与const成员

初始化列表与const成员 const成员 使用const修饰的类、结构、联合的成员变量,在类对象创建完成前一定要初始化。 不能在构造函数中初始化const成员,因为执行构造函数时,类对象已经创建完成,只有类对象创建完成才能调用成员函数,构造函数虽然特殊但也是成员函数。 在定义const成员时进行初始化,该语法只有在C11语法标准下才支持。 初始化列表 在构造函数小括号后面,主要用于给

2024/9/8 c++ smart

1.通过自己编写的class来实现unique_ptr指针的功能 #include <iostream> using namespace std; template<class T> class unique_ptr { public:         //无参构造函数         unique_ptr();         //有参构造函数         unique_ptr(