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

第12章 动态内存

12.1 动态内存与智能指针

采用智能指针的目的:更好的管理动态内存,防止产生内存泄漏
memory头文件中提供了两种智能指针:

  1. shared_ptr, 允许多个指针指向同一个对象
  2. unique_ptr,指针只能指向一个对象
  3. 提供了一弱引用,weak_ptr,用来指向shared_ptr

12.1.1 shared_ptr

采用make_shared函数来给动态内存分配对象并初始化,使用的时候需要指定想要创建的对象的类型,如果不传递任何对象会进行值初始化

#include <memory>
#include <string>
#include <iostream>

int main(int argc, char const *argv[])
{
    std::shared_ptr<std::string> p1 = std::make_shared<std::string>();
    if (p1 && p1->empty()) *p1 = "gaoxiang";
    std::cout << *p1 << " " << p1 << std::endl; 
    return 0;
}

shared_ptr的拷贝和赋值

在进行拷贝和赋值操作的时候每个,shared_ptr会记录有多少个指向相同对象的shared_ptr,每一个shared_ptr都有一个计数器,成为引用计数,进行指针的拷贝操作时计数器会递增,进行计数器复制操作时,比如auto r = p1,p1的计数器的值会递增并赋值给r,但是r原来指向的对象的计数器的值会递减。

int a = 42;
auto p1 = std::make_shared<int>(a);
auto p2 = std::make_shared<int> (a);
std::cout << "输出P1和P2的引用计数" << p1.use_count() << " " << p2.use_count() << std::endl;
auto r = p1;
std::cout << "输出P1和r的引用计数" << r.use_count() << " " << p1.use_count() << " " << p2.use_count() << std::endl;
int b = 0;
auto p3 = std::make_shared<int> (b);
p1 = p3;
std::cout << p1.use_count() << " " << p3.use_count() << " " << r.use_count() << " " << p2.use_count() << std::endl;
//输出P1和P2的引用计数1 1
//输出P1和r的引用计数2 2 1
//2 2 1 1

shared_pr会自动释放相关联的内存
当shared_ptr的引用计数变为0之后,就会自动释放对象所分配的资源,但是只要有一个对象还在引用,shared_ptr就不会释放

std::shared_ptr<FOO> factory(FOO arg) {
    return std::make_shared<FOO>(arg);
}

void use_factory(FOO arg) {
    std::shared_ptr<FOO> p = factory(arg);
    //离开了作用域p的内存就会被自动释放
}

std::shared_ptr<FOO> use_factory1(FOO arg) {
    std::shared_ptr<FOO> p = factory(arg);
    return p;//返回P是引用计数会递增,p离开了作用域其内存也不会被释放
}

使用动态内存的三种原因:

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

12.1.2直接管理内存

使用new动态分配和初始化对象

int *p = new int这种方式会在自由空间构造一个int对象,并返回一个指针,但是这种构造方式对象的值是未定义的会返回个默认的初始化值。
int *p = new int(1024)会返回一个指向值为1024的指针。
如果采用一个括号包围的初始化器,可以使用auto自动推导其类型。
auto *p1 = new auto(obj) //p指向一个与Obj相同类型的对象,该对象采用obj初始化
auto *p2 = new auto{a,b, c}; //error,初始化器只能有一个
同理,还可以动态分配const对象
const int *pc1 = new const int (1024),分配并初始化一个const int.

释放动态内存
delete p,执行两个动作:1. 删除对象p.2. 释放P所占用的资源。
delete释放的指针必须指向动态分配(new 分配)的内存或者是空指针。

int i, *pi = i, *pi2 = nullptr;
double *pd = new double(33.0), *pd2 = pd;
delete i;   //error,i不是一个指针
delete pi;  //error,pi是一个局部变量
delete pi2; //right,可以释放一个空指针
delete pd;  //right,释放一个由new分配的动态内存
delete pd2; //error,pd2指向的内存已经被释放了
const int *pci = new const int (1024);
delete pci; //right,释放一个const对象

动态对象的生存期直到被释放时为止

Foo factory() {
    return new Foo(arg);    //调用者负责释放此内存
}
void use_factory(T arg) {
    Foo *p = factory(arg);  //p是一个内置指针类型,当use_factory返回时,p指向的内存还没有被释放,虽然对象P被销毁
}
//解决办法
void use_factory(T arg) {
    Foo *p = factory(arg);
    delete p;
}
void use_factory(T arg) {
    Foo *p = factory(arg);
    return p;   //由函数的调用者释放内存
}

采用new和delete来管理动态内存会出现的常见问题:

  1. 忘记delete内存
  2. 使用已经释放掉的对象
  3. 重复释放对象
    坚持使用智能指针能解决上述所有问题

12.1.3 shared_ptr和new结合使用

用new返回的指针来初始化智能指针,但是智能指针必须使用直接初始化的方式。

shared_ptr<int> p1 = new int(42);   //error
shared_ptr<int> p2(new int(42));    //right

shared_ptr<int> clone(int p) {
    return new int(p);  //error,隐式转化为shared_ptr<int>
}

shared_ptr<int> clone(int p) {
    return shared_ptr<int>(new int(p)); //right,显示的创造shared_ptr<int>
}

shared_ptr的构造函数为explicit的,不能将内置指针隐式的转化为智能指针,同时转换为智能指针的内置指针必须使用new分配的动态内存,因为智能指针默认使用delete来释放其所关联的对象。

不要混合使用普通指针和智能指针......

void process(shared_ptr<int> ptr) {
    //use ptr
}   //ptr离开作用域,被销毁

shared_ptr<int> p(new int(42));
process(p);
int r = *p; //p的引用计数为1

该函数的形参采用的是传值方式,因此实参通过拷贝的方式传递给Ptr,也就是说ptr的引用计数至少为2,process使用结束局部变量ptr被销毁但是其指向的内存不会被释放.

int *x(new int(1024));
process(x); //error,内置指针无法转化为智能指针
process(shared_ptr<int>(x));    //right,通过创建一个临时shared_ptr来存储x
int j = *x; //错误,此时x为一个空悬指针

通过创建一个临时智能指针来存储x,此时内置指针指向的内存的管理权就交给了智能指针,当函数运行结束后,智能指针的引用计数就会变为0指向的内存空间就会被释放,这时候x就变成了一个空悬指针。
不要用一个内置指针来访问一个空悬指针所负责的对象

......不要使用get初始化另一个智能指针或为智能指针赋值
get函数返回一个内置指针,一般用于不能使用智能指针的代码,使用的时候一定要确保赋值的内置指针不会被delete.

p = new int(1024);  //error
p.reset(new int(1024)); //right,p指向一个新对象

利用reset将p指向一个新的对象,这个函数一般和unique一起使用,先检查自己是否是当前对象的唯一用户,如果不是需要制作一份新的拷贝。

if (!p.unique())
    p.reset(new string(*p));
*p += 1024; //现在是指向的对象的唯一用户,可以改变对象的值

智能指针使用规范:

  • 不使用相同的内置指针值初始化(或reset)多个智能指针
  • 不delete get()返回的指针
  • 不适用get()初始化或reset另一个智能指针
  • 如果使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了
  • 如果使用智能指针管理的资源不是new分配的内存,记住传递给他一个删除器
//删除器举例
string c = connect(&d);
shared_ptr<string> p(&c, end_connection);   //end_connection就是一个删除器,由删除器来执行内存释放的工作

12.1.5 unique_ptr

unique_ptr拥有他所指向的对象,某个时刻只能有一个unique_ptr指向一个给定的对象(可以理解为当unique_ptr指向一个对象时这个对象就被unqiue_ptr独占了),因此unique_ptr不能赋值和拷贝。unqiue_ptr被销毁其指向的对象也就被销毁了。unique_ptr只能绑定到一个由new返回的指针上面采用直接初始化的方法初始化。

unique_ptr<int> p(new int(42));
auto q = p; //error,unque_ptr无法赋值
unique_ptr<int> r(p);   //不能拷贝

unique_ptr可以通过release和reset的方式将unique_ptr的所有权转移给另一个。
u.release()返回当前所指的对象并将u置为空,注意:release返回的指针通产用来初始化另一个智能指针或者给另一个智能指针赋值,否则会出现错误。
u.reset()释放u所指的对象

unique_ptr<string> p2(p1.release());    //将P1所指对象给了p2,并将p1置为空
unique)ptr<string> p3(new string("gaoxiang"));
p2.reset(p3.release()); //reset释放了p2原来指向的内存

可以拷贝和赋值一个将要被销毁的unique_ptr。比如从函数中返回unique_ptr。

unique_ptr<int> clone(int p) {
    return unique_ptr<int>(new int(p));
}

unique_ptr<int> clone(int p) {
    unique_ptr<int> ret(new int(p));
    return ret;
}

向unique_ptr传递删除器
unique_ptr<objT, delT> p (new objT, fcn); ,p创建一个objT类型的对象,并使用一个delT的对象释放objT对象,会调用一个名为fcn的delT类型的对象。

void f(destination &d) {
    connection c = connect(&d);
    unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection);
}

12.1.6 weak_ptr

weak_ptr是一种不控制所指对象生存期的智能指针,指向一个shared_ptr管理的对象,且不会改变shared_ptr的引用计数。即使有weak_ptr所指,也不影响对象被释放。weak_ptr是一种弱共享。

auto p =make_shared<int>(42);
weak_ptr<int> wp(p);    //wp弱共享p,p的引用计数没有改变,weak_ptr由shared_ptr来初始化
if (shared_ptr<int> np = wp.lock()) {   //如果np不为空条件成立
    //if中,np 与 p共享对象
}

12.2动态数组

12.2.1 new 和 数组

int *pia = new int[24] new分配一个数组并返回第一个指向Int的指针。
注意动态数组不是数组类型,不能使用begin和end来返回首元素和尾后元素指针,也不能使用for来遍历数组。

初始化动态分配对象的数组

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
int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};
string *psa3 = new string[3]{"gaoxiang", "c++","primer"};

动态分配一个空数组是合法的,但是无法对空数组进行解引用,可以对这个指针进行比较和运算操作。

char arr[0];    //error
char *cp = new char[0]; //right,但是cp无法解引用

释放动态数组

delete p;   //p指向一个动态分配的对象或为空
delete [] pa;   //pa必须指向一个动态分配的数组或为空

动态数组元素的释放是从后往前释放的。

智能指针和动态数组

unique_ptr<int[]> up(new int[10]);
up.release();   //自动使用delete[]销毁其指针

int[]说明了Up指向一个Int数组而不是Int。
指向动态数组的unique_ptr不支持成员访问运算符,要想访问其中的成员可以使用下标运算符。

shared_ptr不直接支持动态管理动态数组,如果要使用shared_ptr管理动态数组,需要自定义删除器。

shared_ptr<int> sp(new int[10],  [](int *p) { delete [] p;});
sp.release();   //使用提供的lambda释放数组,使用delete[];

如果未提供删除器,则这段代码是未定义的。shared_ptr未定义下标运算符无法直接访问动态数组中的元素。

for (size_t i = 0; i != 10; ++ i)
    *(sp.get() + i) = i;    //使用get获得一个内置指针

12.2.2 allocator类

new在内存分配上具有一定的局限性,new把内存分配和对象构造组合在一起这样做会造成内存的浪费,有的时候需要分配大量内存空间,当需要一段空间的时候在构造对象,这个时候就需要allocator.

allocator类及其算法

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

allocate分配未构造的内存
allocator分配的内存是未构造的,按需在分配的内存中构造对象。

allocator<string> alloc;
auto const p = alloc.allocate(n);
auto q = p;     //q指向最后构造元素的位置
alloc.construct(q++);  
alloc.construct(q++, 10, 'c');  //*q 为ccccccccc
alloc.construct(q++, "hi");     //*q为Hi

为构造的内存是未定义的。

构造的对象用完之后必须对每个构造的元素调用destory来销毁他们。

while(q != p) allco.destory(--q);   //释放真正构造的string

元素被销毁后对应的内存可以用来存储其他元素了,要想释放对应的内存需要调用deallocate。

拷贝和填充未初始化内存的算法
uninitialized_copy(b, e, b2) 将迭代器b、e指定范围内的元素拷贝到b2指定的为构造的内存
uninitialized_copy_n(b, n, b2) 从迭代器b指向的元素开始,拷贝n个元素到b2开始的内存
uninitialized_fill(b, e, t) 在迭代器b、e指定的原始内存范围中创建对象,对象的值为t的拷贝
uninitialized_fill_n(b, n, t) b指向的内存地址开始创建N个对象,对象的值为t。

posted on 2022-01-18 15:00  翔鸽  阅读(40)  评论(0编辑  收藏  举报