文章目录
- 三种对象的分类
- 三种内存的区别
- 动态内存
- 概念
- 智能指针允许的操作
- 智能指针的使用规范
- new
- 概念
- 内存耗尽/定位new
- 初始化
- 默认初始化
- 直接初始化
- 值初始化
- delete
- 概念
- 手动释放动态对象
- 空悬指针
- shared_ptr类
- 格式
- 独有的操作
- make_shared函数
- shared_ptr的计数器
- 通过new用普通指针初始化shared_ptr
- unique_ptr
- 概念、初始化、特性
- 支持的操作
- weak_ptr
- 概念
- 关于普通指针和智能指针
- 不能使用内置指针来访问shared_ptr所指向的内存
- get()函数
- reset函数
- 处理异常
- 动态数组
- 概念
- new分配对象数组
- 两种方法
- new的返回值
- 初始化
- 动态分配一个空数组是合法的
- 释放动态数组
- 动态数组和unique_ptr
- 动态数组和shared_ptr
- allocator类
- new的局限性(使用allocator的原因)
- 概念即创建销毁操作
- construct
- destroy
- deallocate
- 两个伴随算法
- 使用了动态生存期的资源的类
- 实例
三种对象的分类
三种对象:
- 全局对象在程序启动时分配,在程序结束时销毁。
- 局部自动对象,当我们进入其定义所在的程序块时被创建,在离开块时销毁。
- 局部static对象在第一次使用前分配,在程序结束时销毁。
三种内存的区别
- 静态存储区:主要存放static静态变量、全局变量、常量。这些数据内存在编译的时候就已经为他们分配好了内存,生命周期是整个程序从运行到结束。
- 栈区:存放局部变量。在执行函数的时候(包括main这样的函数),函数内的局部变量的存储单元会在栈上创建,函数执行完自动释放,生命周期是从该函数的开始执行到结束。线性结构。
- 堆区:程序员自己申请的任意大小的内存。一直存在直到被释放。链表结构。
前两种内存中的将对象由编译器自动创建和销毁。堆也被称作自由空间,被用来存储动态分配的对象(程序运行时分配的对象),动态对象的生存期由程序来控制——当动态对象不被使用时,必须显式地消灭他们。
动态内存
概念
为什么要使用动态内存:
- 程序不知道自己需要使用多少对象
- 程序不知道所需对象的准确类型
- 程序需要在多个对象间共享数据
动态内存的分配与释放通过一对运算符来完成:
- new:在动态内存中为对象分配空间并返回一个指向该对象的指针,可以选择对对象进行初始化;
- delete:接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
使用动态内存时容易出现的问题:
- 忘记释放内存,产生的内存泄漏。这种内存永远不可能被归还给自由空间了。查找本错误是非常困难的,通常应用程序运行很长时间之后,真正耗尽内存时,才能检测到这种错误。
- 在尚有指针引用内存的情况下释放内存,产生引用非法内存的指针
- 释放一个已经被delete的内存,产生double free的问题。出现此操作时,自由空间就可能被破坏。
为了避免上述问题,c++11提供了两种智能指针(smart pointer)类型来管理动态对象,两种指针的区别在于管理底层指针的方式:
- shared_ptr:允许多个指针指向同一个对象
- unique_ptr:独占所指向的对象
除此之外,标准库还定义了一个名为weak_ptr的伴随类,他是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memory头文件中。
智能指针允许的操作
智能指针的使用规范
- 不使用相同的内置指针值初始化(或reset)多个智能指针。
- 不delete get()返回的指针。
- 不使用get()初始化或reset另一个智能指针。
- 如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。
- 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。
new
概念
在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针:
int *pi = new int; // pi指向一个动态分配的、未初始化的无名对象
当然用new分配const的对象也是合法的,但是一个动态分配的const对象必须进行初始化:
- 定义了默认构造函数的类类型,const动态对象可以隐式初始化
- 其他类型必须显式初始化
- 由于分配的对象是const的,因此new返回的指针是一个指向const的指针
内存耗尽/定位new
值得一提的是,如果一个程序用光了它所有可用的内存,new表达式就会失败。 默认情况下。如果new不能分配所要求的内存空间,就会抛出一个bad_alloc的异常。可以改变使用new的方式来阻止它抛出异常:
我们称上面形式的new未定位new(placement new),定位new表达式允许我们向new传递额外的参数。上例中,我们传递给它一个由标准库定义的名为nothrow的对象,将nothrow传递给new,意图是告诉它不能抛出异常。
bad_alloc和northrow都定义在头文件new中。
初始化
默认初始化
默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化:
直接初始化
为了避免未定义行为,最好使用直接初始化的方式来初始化一个动态分配的对象:
- 可以用圆括号
- 可以用列表初始化
值初始化
也可以使用值初始化,只需要直接在类型名之后跟一对空括号即可:
值得一提的是:
- 对于定义了自己的构造函数的类类型来说,要求值初始化是没有意义的——不管采用什么形式,对象都会通过默认构造函数来初始化。
- 对于内置类型,两种形式的差别就很大了;值初始化的内置类型对象有着良好定义的值,而默认初始化的对象的值则是未定义的。类似的,对于类中那些依赖于编译器合成的默认构造函数的内置类型成员,如果它们未在类内被初始化,那么它们的值也是未定义的。
delete
概念
为了防止内存耗尽,在动态内存使用完毕后,必须通过delete表达式将动态内存归还给系统。
默认情况下shared_ptr和unique_ptr都使用delete释放指向的对象,但也都允许重载默认的删除器(delete)。
delete执行两个动作:
- 销毁给定的指针指向的对象
- 释放对应的内存
delete表达式接受一个指针,指向我们想要释放的对象,该指针必须指向动态分配的内存,或者是一个空指针。
通常情况下,
- 编译器不能分辨一个指针指向静态还是动态分配的对象
- 编译器不能分辨一个指针所指向的内存是否已经被释放
对于上述两种情况,大多数编译器会编译通过,尽管他们是错误的。
因此,释放一块非new分配的内存或者将相同的指针值多次释放,其行为是未定义的:
另外,const对象的值虽然不能够被改变,但是其本身可以被销毁:
const int *pci = new const int(1024);
delete pci; // 正确:释放一个const对象
手动释放动态对象
智能指针可以在计数值为0时自动释放动态对象,而delete是一种手动释放动态对象的方式,这就要求程序员不能忘记delete这一步骤。
与类类型不同,内置类型的对象被销毁时什么也不会发生。 特别是,当一个指针离开其作用域时,它所指向的对象什么也不会发生。如果这个指针指向的是动态内存,那么内存将不会被自动释放。
举个例子:
foo *factory(T arg){return new Foo(arg); // 调用factory的对象负责释放动态内存
}void use_factory(T arg){Foo *p = factory(arg);
} // p离开了它的作用域,但实际所指向的内存没有被释放
本例中,一旦use_factory返回,程序就没有办法释放这块内存了。修正这个错误的唯一方法是在use_factory中记得释放内存:
void use_factory(T arg){Foo *p = factory(arg);delete p;
}
空悬指针
执行delete p;
后,p并不指向空指针,相反的,p的值(指向的地址)不变,但不能再使用p处理该地址的内容(指针失效),也不能重复delete p。 此时p
就变成了空悬指针(dangling pointer),即指向一块曾经保存数据对象但现在已经无效的内存的指针。
不能重复delete p
:
但是可以重复 delete
空指针:
避免空悬指针有两种方法:
- 在指针即将要离开其作用域之前释放掉它所关联的内存。这样,在指针关联的内存被释放掉后,就没有机会继续使用指针了。
- 也可以在delete之后将nullptr赋予指针,这样就清楚地指出指针不指向任何对象。
但重置指针地方法仍然不是完美的,动态内存的一个基本问题是可能有多个指针指向相同的内存。在delete内存之后重置指针的方法只对这个指针有效,对其他任何仍指向(已释放的)内存的指针是没有作用的,然而在实际中,查找只想相同内存地所有指针也是异常困难的:
shared_ptr类
格式
shared_ptr<类型>
默认初始化的智能指针中保存着一个空指针。
独有的操作
关于上面两表的具体操作将在下面的普通指针和智能指针中指出。
make_shared函数
shared_ptr可以协调对象的析构,但这仅限于其自身的拷贝(也是shared_ptr)之间。因此最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数,而不是new。这样,我们就能在分配对象的同时就将shared_ptr与之绑定,从而避免了无意中将同一块内存绑定到多个独立创建的shared_ptr上。
make_shared函数定义在头文件memory中。
功能:在动态内存中分配一个对象并初始化它,返回此对象的shared_ptr。
实例:
调用make_shared<T>
时传递的参数必须与T的某个构造函数相匹配,换言之,调用make_shared的行为的底层操作其实是调用对应类型的构造函数。
当然,用auto定义一个对象来保存make_shared的结果也是可以的:
auto p = make_shared<vector<string>>();
shared_ptr的计数器
因为shared_ptr允许多个指针指向同一个对象。因此每个shared_ptr都有一个关联的计数器,通常称其为引用计数(reference count),用来记录有多少个其他shared_ptr指向相同的对象。
当
- 用一个shared_ptr初始化另一个shared_ptr
- shared_ptr作为参数传递给一个函数
- shared_ptr作为函数的返回值
时,shared_ptr所关联的计数器就会递增。
当
- 给shared_ptr赋予一个新值(旧值计数器递减,新值计数器递增)
- shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域)
时,shared_ptr所关联的计数器就会递减。
一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象。
由于在最后一个shared_ptr销毁前内存都不会释放, 保证shared_ptr在无用之后不再保留就非常重要了。如果你忘记了销毁程序不再需要的shared_ptr,程序仍会正确执行,但会浪费内存。
share_ptr在无用之后仍然保留的一种可能情况是,你将shared_ptr存放在一个容器中,随后重排了容器,从而不再需要某些元素。在这种情况下,你应该确保用erase删除那些不再需要的shared_ptr元素。
通过new用普通指针初始化shared_ptr
可以使用new返回的指针来初始化智能指针。接受指针参数的智能指针构造函数是explicit的。因此,我们不能进行内置指针到智能指针间的隐式转换,必须使用直接初始化形式来初始化一个智能指针:
p1的初始化隐式地要求编译器将一个new返回的int*隐式转换成一个shared_ptr,这是不被允许的。
同样的,一个返回shared_ptr的函数不能在其返回语句中隐式转换一个普通指针:
必须将shared_ptr显式绑定到一个想要返回的指针上:
unique_ptr
概念、初始化、特性
某个时刻只能有一个unique_ptr指向一个给定对象。unique_ptr被销毁时,它所指向的对象也被销毁。
unique_ptr没有类似make_shared的标准库函数。定义一个unique_ptr时,需要将其绑定到一个new返回的指针上,且必须采用直接初始化形式:
unique_ptr<double> pb;
unique_ptr<int> pi(new int(2));
根据“独占”的特性,unique_ptr不支持普通的拷贝或赋值操作:
不能拷贝unique_ptr的规则有个例外:可以拷贝或赋值一个将要被销毁的unique_ptr。
常见的例子是从函数返回一个unique_ptr:
或者返回一个局部对象的拷贝:
支持的操作
可以通过release或reset起到类似拷贝或赋值的作用:
release会切断unique_ptr和它原来管理的对象间的联系,返回的指针常被用来初始化另一个智能指针或给另一个智能指针赋值。但是,如果不用另一个智能指针来保存release返回的指针,就要记得手动释放资源:
weak_ptr
概念
weak_ptr是一种不控制所指向对象生存期的智能指针,它指向一个由shared_ptr管理的对象。
具有以下特点:
- 将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数
- 引用计数归零时,即使仍有weak_ptr指向对象,对象还是会被释放
由于对象可能不存在,我们不能使用weak_ptr直接访问对象,必须调用lock。因此可以这样使用:
关于普通指针和智能指针
不能使用内置指针来访问shared_ptr所指向的内存
当将一个shared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr,不应该再使用内置指针来访问shared_ptr所指向的内存了。
使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被销毁。
举例:
对于上面的函数,以智能指针作为参数以传值方式传递是安全的,当process结束时,ptr的引用计数为1,因此虽然局部ptr被销毁,但是ptr指向的内存不会被释放:
但同时也可以传递给process一个用内置指针显式构造的临时shared_ptr。但是这样做的风险是很大的:
process(x);
结束时,临时对象被销毁,其引用计数为0,指向的内存会被释放,此时x变成了空悬指针。
get()函数
get函数返回一个内置指针,指向智能指针管理的对象。
函数是为了这种情况设计的:我们需要向不能使用智能指针的代码传递一个内置指针。
使用get返回的指针的代码不能delete此指针。
虽然编译器不会给出错误信息,但是将另一个智能指针也绑定到get返回的指针上是错误的:
上述代码中,shared_ptr<int>(q);
将另一个指针绑定到get返回的指针上,会导致程序块结束时p指向的内存被释放,p变成空悬指针。
reset函数
reset将一个新的指针赋予shared_ptr:
shared_ptr<int> p = new int(1024); // error:不能将一个普通指针赋予shared_ptr
p.reset(new int(1024)); // 正确:p指向一个新对象
与赋值类似,reset会更新引用计数,在需要的时候,可以释放p指向的对象。
reset成员经常与unique一起使用,来控制多个shared_ptr共享的对象。在改变底层对象之前,我们检查自己是否是当前对象仅有的用户。如果不是,在改变之前要制作一份新的拷贝:
if(!p.unique()){p.reset(new string(*p)); // 不是旧对象仅有的指针,分配新拷贝}*p += newVal; // 是旧对象仅有的指针,直接改变对象的值,因为不会再有别的指针访问旧对象
处理异常
当处理异常时,经常会使程序块过早结束,也就是如果使用普通指针管理内存,可能在遇到detele之前推出程序块:
void f()
{int *ip = new int(2);// throw一个异常且在f中未被捕获delete ip; //没能正常退出因此无法调用本句释放内存
}
如果ip是shared_ptr类型则不会出现内存泄漏的情况,在程序块结束时,自动释放内存。
动态数组
概念
动态数组并不是数组类型。
new和delete运算符一次分配/释放一个对象,但某些应用需要一次为很多对象分配内存的功能。
- 使用容器的类可以使用默认版本的拷贝、赋值和析构操作。
- 分配动态数组的类必须定义自己版本的操作,在拷贝、复制以及销毁对象时管理所关联的内存。
new分配对象数组
两种方法
方法一:在类型名之后跟一对方括号,在其中指明要分配的对象的数目,方括号中的大小必须是整形,但不必是常量。
例如:
// 调用get_size确定分配多少个int
int *pia = new int[get_size()]; // pia指向第一个int
方法二:也可以用一个表示数组类型的类型别名来分配一个数组,这样,new表达式中就不需要方括号了:
typedef int arrT[10]; // arrT表示10个int的数组类型
int *p = new arrT; // 分配一个10个int的数组;p指向第一个int
但编译器在执行这个表达式时还是会用new[]:
int *p = new int[42];
new的返回值
当用new分配一个数组时,我们并未得到一个数组类型对象,而是得到一个数组元素类型的指针。因此:
- 不能对动态数组调用begin或end。这些函数使用数组维度来返回指向首元素和尾后元素的指针。
- 不能用范围for语句来处理动态数组中的元素
初始化
默认情况下,new分配的对象,不管是单个的还是数组中的,都是默认初始化的。也可以通过一对空括号进行值初始化:
以及提供一个元素初始化器的花括号列表:
- 初始化器数目小于元素数目,剩余元素进行值初始化。
- 初始化器数目大于元素数目,new表达式失败,不会分配任何内存。
new表达式失败时会抛出一个类型为bad_array_new_length的异常。类似bad_alloc,定义在头文件new中。
值得一提的是: 虽然我们用空括号对数组中元素进行值初始化,但不能在括号中给出初始化器,这意味着不能用auto分配数组。 因为auto是编译器根据初始化值来判断类型的,使用auto就必须有初始值,没有初始值(这里是初始化器)auto自然也就不可以用了。
动态分配一个空数组是合法的
虽然我们不能创建一个大小为0的静态数组对象,但当n等于0时,调用new[n]是合法的:
当我们用new分配一个大小为0的数组时,new返回一个合法的非空指针。此指针保证与new返回的其他任何指针都不相同。对于零长度的数组来说,此指针就像尾后指针一样,我们可以像使用尾后迭代器一样使用这个指针。可以用此指针进行比较操作,但此指针不能解引用——毕竟它不指向任何元素。
释放动态数组
为了释放动态数组,可以使用一种特殊的delete——在职阵前加上一个方括号对。
第二条语句销毁pa指向的数组中的元素,并释放对应的内存。数组中的元素按逆序销毁,即,最后一个元素首先被销毁,然后是倒数第二个,依此类推。
当我们释放一个指向数组的指针时,空方括号对是必需的:它指示编译器此指针指向一个对象数组的第一个元素。如果我们在delete一个指向数组的指针时忽略了方括号(或者在delete一个指向单一对象的指针时使用了方括号),其行为是未定义的。
动态数组和unique_ptr
- 当一个unique_ptr指向一个数组时,我们不能使用点和箭头成员运算符。毕竟unique_ptr指向的是一个数组而不是单个对象,因此这些运算符是无意义的。
- 我们可以使用下标运算符来访问数组中的元素
实例:
类型说明符中的方括号(<int[]>)指出up指向一个int数组而不是一个int。由于up指向一个数组,当up销毁它管理的指针时,会自动使用delete[]。
动态数组和shared_ptr
shared_ptr不直接支持管理动态数组。如果希望用shared_ptr管理,必须提供自己的删除器:
shared_ptr不直接支持动态数组管理这一特性会影响我们如何访问数组中的元素:
shared_ptr未定义下标运算符,而且智能指针类型不支持指针算术运算。 因此,为了访问数组中的元素,必须用get获取一个内置指针,然后用它来访问数组元素。
allocator类
new的局限性(使用allocator的原因)
- new将内存分配和对象构造组合在了一起。
- delete将对象析构和内存释放组合在了一起
上述特性在灵活性上是有一定局限性的。这样在分配单个对象时当然是好的,可以明确知道对象应该有什么值。但是分配大块内存时,我们希望将内存分配和对象构造分离。这意味着我们可以先分配大块内存,只有在真正需要时才执行对性的创建操作。
实例:
有如下问题:
- 我们可能不需要n个string,可能只用到了少量的string。因此我们可能创建了一些永远也用不到的对象。
- 对于确实需要使用的对象,每个都被赋值了两次:第一次是在默认初始化时,第二次是在赋值时。
- 没有默认构造函数的类就不能动态分配数组了。
概念即创建销毁操作
- allocator定义在头文件memory中。
- allocator分配的内存是未构造的。还未构造对象的情况下就是用原始内存是错误的。
construct
构造对象是通过construct完成的:
- construct成员函数接受一个指针和零个或多个额外参数, 在给定位置构造一个元素。
- 额外参数用来初始化构造的对象。类似make_shared的参数,这些额外参数必须是与构造的对象的类型相匹配的合法的初始化器:
allocator<string> alloc;
auto const p = alloc.allocate(20);
auto q = p;
// q指向最后构造的元素之后的位置
// p指向分配的内存的首地址
alloc.construct(q++, 5, 'x'); // *p为xxxxx
cout << *p << endl; // 正确:使用string的输出运算符
cout << *q << endl; // 灾难:q指向未构造的内存
为了理解上面的代码,用下面的代码查看一下构造对象前后p和q分别指向的地址:
可以看到,p一直指向分配的内存的首地址,q指向最后构造的元素之后的地址,因此 *p 可以访问已经构造的对象;而 *p 访问的是未构造对象的原始内存,这种行为是错误的。
destroy
用完对象后,必须对每个构造的元素调用destory来销毁它们。
destory接受一个指针,对指向的对象执行析构函数:
while (q != p) {alloc.destroy(--q); // 释放我们真正构造的string
}
不妨来查看一下执行上述代码之后的地址指向情况即内存分配的对象的值:
可以看到,执行完while之后,q指向的地址已经和p一样了,而再访问p中的对象——执行*p也无法输出”xxxxx“了。
但是在atom里面尝试运行的时候发现和预期的不一样。。。。destroy之后解引用p仍然能得到”xxxxx“:
(吐槽:同一段代码在不同的编译器上得到的结果不同,猜测可能是底层的编译环境不同导致的。 emmmmm……还是更倾向于相信vs的运行结果,如果有大佬看到这个问题知道原因的话,请不吝赐教,孩子实在不知道为什么会这样。)
- 我们只能对真正构造了的元素进行destory操作。
- 一旦元素被销毁后,可以重新使用这部分内存来保存其他的string,也可将内存归还给系统。
deallocate
释放内存通过deallocate来完成:
alloc.deallocate(p, 20);
- 传递给deallocate的指针不能为空,必须指向由allocate分配的内存。
- 传递给deallocate的大小参数必须与调用allocate分配内存时提供的大小参数具有一样的值。
两个伴随算法
- 用来初始化内存中创建的对象
实例:
- uninitialized_copy返回递增后的目的位置迭代器,指向最后一个构造元素之后的位置。
使用了动态生存期的资源的类
大多数类中,分配的资源都与对应对象生存期一致。 例如:每个vector(对象)“拥有”其自己的元素(分配的资源)。当我们拷贝一个vector时,原vector和副本vector中的元素是相互分离的。
某些类分配的资源具有与原对象相独立的生存期(可能一个资源被两个对象共同引用)。 换言之,如果两个对象共享底层的数据,当某个对象被销毁时,我们不能单方面地销毁底层数据。
实例
构建一个类A,用share_ptr
管理vector<string>
:
#pragma once
#include <vector>
#include <string>
#include <memory>using namespace std;class A{
public:A(): vs(make_shared<vector<string>>()){} // 分配一个空的vectorA(initializer_list<string> il): vs(make_shared<vector<string>>(il)){}// 接受一个初始化器的花括号列表,将il当作make_shared的参数初始化vs// 通过调用底层vector的成员函数来完成size、empty、push_back、pop_backvector<string>::size_type size() const{return vs->size();}bool empty() const{return vs->empty();}void push_back(const string& s){vs->push_back(s);}// pop_back、front、back操作需要先检查操作对象是否为空void pop_back(){check(0, "pop_back on empty A");vs->pop_back();}string& front(){check(0, "front on empty A");return vs->front();}string& back(){check(0, "back on empty A");return vs->back();}// 针对const的A对象的front和back函数重载const string& front() const;const string& back() const;
private:shared_ptr<vector<string>> vs;// check函数提供判空功能,如果操作对象为空抛出一个异常void check(vector<string>::size_type si, const string &s) const{if(si >= vs->size()){throw out_of_range(s);}}
};const string& A::front() const{check(0, "front on empty A");return vs->front();
}
const string& A::back() const{check(0, "back on empty A");return vs->back();
}
用一个简单的A的使用程序。测试类的正确性:
#include <iostream>using namespace std;#include "my_A.h"int main(int argc, char const *argv[]) {A a1;{A a2 = {"a", "an", "the"};a1 = a2;a2.push_back("about");cout << a2.size() << endl;}cout << a1.size() << endl;cout << a1.front() << " " << a1.back() << endl;const A a3 = a1;cout << a3.front() << " " << a3.back() << endl;return 0;
}
输出结果: