我的C++primer长征之路:动态内存

动态内存

C++内存分配模型

静态(全局)内存:存储局部static对象、类static数据成员、定义在任何函数之外的对象。static对象在使用之前分配内存,程序结束时销毁。

内存在程序编译的时候已经分配好,运行期间都存在。

栈内存:存储定义在函数内的非static对象(局部非静态变量)。仅在定义的函数块运行时才存在。

栈内存分配运算内置于指令集,效率高,但是容量有限。

堆内存(自由空间):程序运行时分配的对象,声明周期由程序来控制。

动态内存分配。

常量区:存放常量字符串,程序结束时由系统释放。

代码区:存放函数体(类成员函数和全局区)的2进制代码。
https://blog.csdn.net/cherrydreamsover/article/details/81627855

静态全局变量、全局变量、静态局部变量、局部变量的区别

静态全局变量、全局变量区别

  1. 静态全局变量和全局变量都属于全局区(静态区)。
  2. 静态全局变量只在本文件中有效,而全局变量在其他文件中可以调用。也就是说在其他文件定义相同的全局变量名会出错。

静态局部变量、局部变量区别

  1. 静态局部变量属于静态区,局部变量属于栈区。
  2. 静态局部变量在函数调用结束后不会被销毁,直到程序结束才销毁,别的函数无法调用该变量。
  3. 静态局部变量若没有初始化,则会被默认初始化为0,局部变量则会被随机初始化。
  4. 静态局部变量在编译期间只赋值一次,此后每次函数调用时,调用上次函数结束时的值。而局部变量每调用一次,赋值一次。

直接管理内存

直接使用new/delete的类不能使用类对象的拷贝、赋值和销毁的默认形式。
new分配的内存是无名的。

int *p = new int;

默认情况下,动态分配的对象是默认初始化的,也就是内置类型的值是未定义的,类类型的对象将使用默认构造函数构造。
也可以通过在类型名后面加上空括号来使用值初始化

int *p = new int(); //值初始化,*p=0

可以使用auto来推导类型

string obj("hi");
auto p = new auto(obj); //此时p为string*
auto p = new auto{a, b, c}; //错误,只能由单个初始化器

定位new

int *p = new int; //如果分配失败,抛出std::bad_alloc异常
int *p2 = new (nothrow) int; //定位new,如果分配失败,返回空指针,bad_alloc和nothrow定义在头文件new中

关于数组

int* p = new int[10](); //10个值初始化的int
delete [] p;

delete之后指针变成了悬空指针,在delete之后置为nullptr。

malloc/new、free/delete的区别

  1. new操作符在自由存储区为对象动态分配空间,malloc函数从堆区动态分配空间。关于自由存储区,可以是堆区,也可以是静态存储区,取决于operator new在哪里为对象分配内存。
  2. new操作符分配内存成功后返回对象类型的指针,而malloc返回的是void*指针,需要强制转换成所需类型。分配类型失败时,new会返回bad_alloc异常,malloc返回NULL
  3. malloc/free需要手动计算类型大小,而new/delete不需要。
  4. malloc/free只是动态分配和释放空间,而new/delete还会调用构造函数和析构函数进行初始化和清理。

智能指针

shared_ptr:允许多个指针同时指向同一个对象。
unique_ptr:只允许一个指针指向某一对象。
weak_ptr:弱引用,指向shared_ptr所管理的对象。
都定义在memory头文件中。

shared_ptr

shared_ptr特有操作
make_shared (args) 返回一个shared_ptr,指向一个动态分配的类型为T的对象,使用args初始化此对象。
shared_ptrp (q) p是shared_ptr q的拷贝:此操作会递增q中的计数器,q中的指针必须能转换成T*
p = q shared_ptr所保存的指针必须能互相转换,此操作会递减p的引用计数,递增q的引用计数,若p的引用计数变为了0,则释放其管理的原来的内存。
p.use_count() 返回与p共享对象的智能指针个数,可能会很慢。
p.unique() 若p.use_count()为1返回true,否则返回false
shared_ptr<int> p1 = make_shared<int> (42); //创建一个指向442的shared_ptr
shared_ptr<string> p2 = make_shared<string>(10, 'o');
auto p3 = make_shared<vector<string>>();
auto p4(p3); //p3p4指向相同对象

可以使用make_shared标准库函数来创建一个shared_ptr。

shared_ptr<string>sp = make_shared<string>(10, 'c'); //sp指向一个值为cccccccccc的string
//sp2指向一个值初始化的int,即值为0
shared_ptr<int>sp2 = make_shared<int>();

shared_ptr只有在引用计数为0时才会销毁其所指向的对象。

//factory返回一个shared_ptr,指向一个动态分配的对象
shared_ptr<string> factory(){
    //一些操作
    return make_shared<string>("hi");
}

void use_factory(T arg){
    shared_ptr<string> p = factory();
    //使用p

}//p离开函数作用域,引用计数变为0,所指向的内存的被释放

shared_ptr<string> use_factory(){
    shared_ptr<string> p = make_shared<string>("hi");
    //
    return p; //返回p时,会递增引用计数
} //p离开了作用域,但是所指向的内存没有被释放。

shared_ptr和new的结合使用
接受指针参数的智能指针的构造函数是explicit的,也就是说不能将一个内置指针隐式转换成一个智能指针。

shared_ptr<int> p1 = new int(1024); //错误,不能使用赋值初始化
shared_ptr<int> p2(new int(1024));  //正确,直接初始化

不要混合使用智能指针和内置指针

void process(shared_ptr<int> ptr){ //参数是传值方式,也就是调用时实参会被拷贝,引用计数会增加
    //使用ptr
}   //ptr离开作用域,引用计数减一

//正确使用方法,传递一个shared_ptr
shared_ptr<int> p(new int(10));
process(p); //正确,拷贝p会增加它的引用计数,在process内的引用计数至少为2
int i = *p;  //正确, 引用计数至少为1

//错误用法
int*x(new int(1024));
process(x); //错误,不能将一个int*转换成shared_ptr<int>
process(shared_ptr<int>(x));  //可以传递,但是函数执行完后x所指向的内存会被释放
int j = *x; //错误,此时x指向的内存已被释放。

不要使用get来初始化智能指针或者为智能指针赋值
get()返回一个指向智能指针管理对象的内置指针。
使用get返回指针的代码不能delete此指针。

shared_ptr<int> sp (new int(10));
int *p = sp.get(); //正确
{
    //两个独立的shared_ptr指向相同的内存,这两个shared_ptr的引用计数都是1,该程序块内的shared_ptr失效后会销毁其所指向的对象。
    shared_ptr<int> q(p);
}
int foo = *sp; //错误,此时sp所指向的内存已被释放

reset()操作

shared_ptr<int> sp = make_shared<int>(10);
sp.reset(); //此时sp的引用计数为0,会释放其所指向的对象
sp.reset(new int(1024)); //sp指向一个新的值为1024的int对象

使用智能指针,即使程序出现异常提前结束,智能指针也能保证正确地释放指针指向的内存。而内置指针无法实现这一点。

注意陷阱

  1. 不要使用相同的内置指针值初始化(或者reset)多个智能指针。因为这样其中某一个智能指针失效后会销毁所指向的对象,从而造成另一个智能指针指向的对象不存在了。
  2. 不要delete get()返回的指针。
  3. 不要用get()初始化或者reset另一智能指针。
  4. 如果智能指针管理的资源不是new分配的内存,需要传递一个删除器。
  5. 记住,使用get()返回的指针,在最后一个对应的智能指针被销毁后,返回的指针就无效了。

unique_ptr

某个时刻只能有一个unique_ptr指向一个给定对象,不支持普通的拷贝和赋值操作。
初始化unique_ptr必须使用直接初始化形式。

unique_ptr<int> up (new int(10));
unique_ptr<int> up2 (up); //错误。不支持拷贝
unique_ptr<int> up3 = up; //错误,不支持赋值
unique_ptr操作
unique_ptr<T, D> u1; 空的unique_ptr,会使用类型为D的可调用对象来释放它所指向空间。D默认为delete操作符
unique_ptr<T, D> u1 (d); 空unique_ptr,调用类型为D的对象d来代替delete
u = nullptr; 释放u指向的对象,将u置空
u.release(); u放弃对指针的控制权,返回所指向的指针,并置空u
u.reset(); 释放u所指向的对象
u.reset(q); 令u指向内置指针q,否则置空u

release()返回unique_ptr当前保存的指针并将其置为空,常用来初始化或给另一智能指针赋值。

unique_ptr<string> p1 (new string("hi"));
unique_ptr<string> p2 (new string("jack"));

//将p1的所有权转移给p2
p2.reset(p1.release());

唯一一个可以拷贝unique_ptr的例子就是从函数返回一个unique_ptr。

unique_ptr<int> clone(p){
    return unique_ptr<int>(new int(p));
}
//这样也行
unique_ptr<int> clone(p){
    unique_ptr<int> ret (new int(p));
    return ret;
}

auto_ptr是一个早期版本的智能指针,具备部分unique_ptr功能,但已被废弃,尽量不要使用。

weak_ptr

weak_ptr是一种不控制所指向对象生命周期的智能指针,指向一个由shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr上不会改变shared_ptr的引用计数。
不能直接使用weak_ptr访问所指向的对象,而需要使用lock()成员函数。

auto p = make_shared<int>(42);
weak_ptr<int> wkp (p); //直接初始化wkp

auto mp = wkp.lock(); //mp指向wkp所指向的对象。

动态数组

关于动态数组,使用new T[]分配返回的并不是一个数组,而是一个指向数组元素类型的指针。动态数组并不是数组类型。

auto dp = new int[10]; //10个int的数组,返回指向首元素的指针
auto ds = new string[10]; //同理
auto de = new int[0]; //动态分配一个空的数组是合法的,但是不能解引用
delete [] dp;
delete [] ds; //释放动态数组空间,数组元素按逆序销毁。
delete [] de;

标准库提供了一个可以管理new分配的动态数组的unique_ptr。

unique_ptr<int[]> up (new int[10]);
up.release(); //自动调用delete[]销毁指针
指向数组的unique_ptr的操作
指向数组的unique_ptr不支持成员访问运算符(.和->)
unique_ptr<T[]> u; u可以指向一个动态分配的数组
unique_ptr<T[]> u(q); u指向内置指针所指向的动态数组
u[i] 返回u拥有的数组中的第i个元素。

shared_ptr默认不支持管理动态数组,如果想使用shared_ptr来管理动态数组,必须提供自己的删除器。

shared_ptr<int> sp (new int[10], [](int *p) {delete [] p;});
sp.reset(); //调用lambda表达式delete[]数组

allocator类

使用allocator类将内存的分配和对象的构造分离开来。
可以在分配的内存上按需构造对象,提高效率。
如果不使用allocator,没有默认构造函数的类就无法使用动态分配数组了。

allocator类及其算法
allocator a; 定义allocator对象a,可以为类型T的对象分配内存
a.allocator(n); 分配一个保存n个类型为T的对象的未构造内存
a.construct(p, args); p必须是指向类型为T*的未构造的内存,args被传递给类型为T的构造函数,用于在内存中构造对象。
a.destroy§; 对p指向的对象执行析构函数。
a.deallocate(p, n); 释放从T* p所指向的位置开始的n个类型为T的对象,p必须是由allocator返回的指针,n必须是创建时的大小。在deallocate之前,内存中的对象必须先被destroy。

allocator分配的内存是原始的,未构造的。

allocator<string> alloc;
auto p = alloc.allocate(10); //分配10个未初始化的string
auto q = p; //q指向最后构造的元素之后的位置
alloc.construct(q++); //q为空字符串
alloc.construct(q++, 10, 'c'); //*q为cccccccccc

cout << *p << endl; //正确,因为p所指向的内存已有对象
cout << *q << endl; // 错误,q指向未构造的内存,因为q++指向下一个未构造的内存了。

while(q != p){
    alloc.destory(--q); //销毁q指向的对象
}
//之后可以将分配的内存释放掉
alloc.deallocate(p, 10); //之所以是10是因为deallocate的大小必须和allocate的大小一致

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

如果觉得向上面一个一个construct比较麻烦,可以使用标准库的拷贝填充算法。

allocator拷贝填充算法
uninitialized_copy(b, e, b2); 从迭代器b和e指出的输入范围中拷贝元素到迭代器b2指定的未构造的原始内存中,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
//p指向一个分配大小为vi两倍的内存
auto p = alloc.allocate(vi.size() * 2);
//将vi中的元素拷贝到p指向的内存中, 返回的q指向最后一个构造的对象的后一个位置
auto q = uninitialized_copy(vi.begin(), vi.end(), p); 
uninitialized_fill_n(q, vi,size(), 42); //将剩余的内存全部初始化为42

猜你喜欢

转载自blog.csdn.net/weixin_40313940/article/details/106819935
今日推荐