12章之前的程序中使用的对象都有严格定义的生存期。
- 全局对象在程序启动时分配,在程序结束时销毁。
- 对于局部自动对象,当进入其定义所在的程序块时被创建,在离开块时销毁。
- 局部static对象在第一次使用前分配,在程序结束时销毁。
除了自动和static对象,还支持动态分配对象。这些对象的生存期与创建位置无关,显式被释放时才会被销毁。
为了安全使用动态对象,标准库中有两个智能指针类型管理动态分配的对象。当一个对象应该被释放时,指向它的智能指针可以确保自动地释放它。
静态内存和栈内存
- 静态内存用来保存局部static对象、类static数据成员以及定义在任何函数之外的变量。
- 栈内存用来保存定义在函数内的非static对象。
- 分配在静态或栈内存中的对象由编译器自动创建和销毁。栈对象仅在其定义的程序块运行时存在;static对象在使用前分配,程序结束时销毁。
除了静态内存和栈内存,每个程序还拥有一个内存池。这些内存称作自由空间(free store)或堆(heap)。
程序用堆来存储动态分配(dynamically allocated)对象,即在程序运行时分配的对象。动态对象的生存期由程序控制,因此在不需要时需要显式销毁。
动态内存和智能指针
C++中的动态内存管理是通过一堆运算符完成的:
-
new
在动态内存中为对象分配空间并返回一个指向该对象的指针; -
delete
接受一个动态对象的指针,销毁该对象并释放关联的内存。
标准库中提供了两种智能指针(smart pointer)类型管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。新标准库提供的这两种智能指针的区别在于管理底层指针的方式:
-
shared_ptr
允许多个指针指向同一个对象 -
unique_ptr
“独占”指向的对象
标准库还有一个名为weak_ptr
的伴随类,是一种弱引用,指向shared_ptr
所管理的对象。
上述三种类型都定义在
头文件中。
shared_ptr
类似vector,智能指针也是模板,因此创建一个智能指针时,必须给出指向的类型:
std::shared_ptr p1;
std::shared_ptr> p2;
默认初始化的智能指针中保存着一个空指针。智能指针的使用方式类似普通指针,可以解引用返回对象。
shared_ptr和unique_ptr都支持的操作
expression | - |
---|---|
shared_ptr | unique_ptr |
空智能指针 |
p |
将p用作一个条件判断,若指向一个对象则true |
*p |
解引用 |
p->mem |
*p.mem |
p.get() |
返回p中保存的指针。注意是否已经释放了对象 |
swap(p, q) |p.swap(q) |
shared_ptr独有操作
expression | - |
---|---|
make_shared |
返回一个shared_ptr,指向一个动态分配的类型为T的对象,使用args初始化该对象 |
shared_ptr |
p是shared_ptr q的拷贝。该操作会递增q中的计数器,q中的指针必须可以转换为T* |
p=q |
二者都是shared_ptr且保存的指针必须可以相互转换。该操作会递减p的引用计数,递增q的引用计数。若p的引用计数变为0,则将其管理的原内存释放。 |
p.unique() |
若p.use_count() 为1(独占状态)则true,否则false |
p.use_count() |
返回与p共享对象的智能指针数量;主要用于调试 |
make_shared函数
make_shared是最安全的分配和使用动态内存的方法,避免了在定义后才初始化可能造成的错误。该函数同样定义在
中:
int main(){
std::shared_ptr p1 = std::make_shared(42);
std::shared_ptr> p2 = std::make_shared>(10, 0);
auto p3 = std::make_shared>();
}
使用make_shared构造智能指针时可以使用auto方式。
若不传递任何参数,则会使用值初始化。
类似顺序容器的emplace,make_shared用其参数构造给定类型对象时传递的参数必须与string的某个构造函数相匹配。
可以使用make_shared和初始化列表
auto sp = std::make_shared>(std::initializer_list({1,2,32}));
//或者
auto sp_map = std::make_shared>();
*sp_map = {{"A", 0},{"B", 1}};
shared_ptr的拷贝和赋值
进行拷贝和赋值操作,每个shared_ptr都会记录有多少个其他shared_ptr指向相同对象:
auto p = make_shared(42);
auto q(p);
每个shared_ptr都有一个关联的计数器,通常称为引用计数(reference count)。无论何时拷贝一个shared_ptr,计数器都会递增。
- 将一个shared_ptr初始化另一个shared_ptr,或将其作为参数传给另一个函数以及作为函数的返回值,则它所关联的计数器就会递增。
- 当给shared_ptr一个新值或者被销毁(如局部shared_ptr)离开作用域,计数器会递减,
- 一旦一个shardd_ptr的计数器变为0,则会自动释放自己管理的对象。
int main(){
int a = 0;
int b = 1;
auto sp3 = std::make_shared(a);
auto sp4 = std::make_shared(b);
std::cout<(b); //令sp3指向其他对象,原对象a的引用计数-1
std::cout<
shared_ptr自动销毁所管理的对象
当指向一个对象的最后一个shared_ptr被销毁(所指向对象的引用计数归0),shared_ptr类会自动调用该对象类的析构函数(destructor)销毁该对象
shared_ptr自动释放相关联的内存
动态对象不再使用时,shared_ptr会自动释放动态对象。
例如有一个函数返回shared_ptr,指向一个Foo类型的动态分配的对象:
struct Foo{
public:
std::string s;
int i;
Foo(std::string str, int ii);
};
Foo::Foo(std::string str, int ii) {
this->i = ii;
this->s = str;
}
std::shared_ptr foo(std::string s, int i){
return std::make_shared(s, i);
}
auto try_ptr(std::string s, int i, bool return_ptr = 0){
auto p = foo(s, i);
//离开该作用域,p指向的对象被自动释放
//若存在返回,则指向对象的引用计数+1,因此不会被释放
return return_ptr? p: nullptr;
}
int main(){
std::string str = "A";
int ii = 1;
auto ptr = foo(str, ii);
std::cout<i<<'\t'<s<
除此之外,shared_ptr会在无用之后依然保留的情况是,将shared_ptr放在一个容器中,之后重排了容器,从而不需要某些元素。这种情况下应该使用erase删除那些不必要的shared_ptr元素。
使用了动态生存期资源的类
程序使用动态内存的原因:
- 不知道自己需要多少对象
- 不知道对象的准确类型
- 需要在多个对象间共享数据
对于容器类,是由于第二种原因而使用的。
对于vector,拷贝一个vector时原有的vector和副本vector中的元素时相互分离的:
std::vector v1;
{
std::vector v2 = {0, 1, 2};
v1 = v2;
}
//此时离开作用域,v2及其中的元素被销毁但是v1拷贝的元素存在
假定类Blob会在不同对象的拷贝间共享相同的元素,则离开作用域时
Blob b1
{
Blob b2 = {0, 1, 2};
b1 = b2;
}
//离开作用域时需保证b2元素不能销毁
定义StrBlob类、构造函数、成员函数
class StrBlob{
public:
typedef std::vector::size_type size_type;
//默认构造函数
StrBlob();
//以接受初始化列表的构造函数
StrBlob(std::initializer_list il);
//基本方法
size_type size() const {return data->size();};
bool empty() const {return data->empty();};
void push_back(const std::string &t) {
std::cout<<"push_back"<push_back(t);};
void pop_back();
std::string& front();
std::string& back();
std::vector::iterator begin();
std::vector::iterator end();
private:
//使用shared_ptr管理装入的容器类型
std::shared_ptr> data;
//检查是否合法。data[i]不合法时抛出异常
void check(size_type i, const std::string &msg) const;
};
//类外定义构造函数,类成员写在函数体之前、函数名的冒号之后
StrBlob::StrBlob(): data(std::make_shared>()) {};
StrBlob::StrBlob(std::initializer_list il):
data(std::make_shared>(il)) {};
void StrBlob::check(size_type i, const std::string &msg) const {
if (i >= data->size()) throw std::out_of_range(msg);
}
//类外定义的成员方法
void StrBlob::pop_back() {
check(0, "no element to pop");
std::cout<<"pop_back"<pop_back();
}
std::string& StrBlob::front() {
check(0, "no element in the front");
std::cout<<"front"<front();
}
std::string& StrBlob::back() {
check(0, "no element in the back");
std::cout<<"back"<back();
}
std::vector::iterator StrBlob::begin() {
check(0, "no element");
return data->begin();
}
std::vector::iterator StrBlob::end() {
check(0, "no element");
return data->end();
}
int main(){
StrBlob s1 = {"a", "b", "c"};
s1.pop_back();
//使用范围for必须实现begin和end
s1.push_back("d");
std::ostream_iterator a(std::cout, " ");
for (auto j: s1)
a = j;
}
直接管理内存
使用new动态分配和初始化对象
自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针
int* pi = new int;
内置类型和组合类型通常被默认初始化,因此值是ub。类类型使用默认构造函数初始化
string* ps = new string;
int* pi = new int;
可以使用直接初始化、列表初始化等构造方式动态分配对象。
int* p = new int(10);
std::string s = new string(10, '!');
auto v = new std::vector{0, 1, 2, 3, 5};
也可以使用值初始化,加括号即可:
int* p1 = new int; //默认初始化
int* p2 = new int(); //值初始化
std::cout<<*p1<
若提供了括号包围的初始化器,则可以使用auto。注意当括号中只有单一初始化器才可以使用auto。
动态分配const对象
可以使用new动态分配一个const对象。
int main(){
int i = 1024;
const auto* p = new int(i);
std::cout<
内存耗尽
当某个程序用尽可用内存,new会失败。默认情况下若new不出所需内存空间则会抛出类型为bad_alloc
的异常。可以改变使用new的方式阻止其抛出异常:
#include
int* p1 = new int;
int* p2 - new(nothrow) int
//分配失败时不throw而是返回一个空指针
这种形式的new称为定位new(placement new),允许向new传递额外的参数。如此例中的nothrow。
bad_alloc和nothrow都定义在头文件
中。
释放动态内存
为了防止内存耗尽,通过delete表达式(delete expression)将动态内存归还给系统。delete接受一个指针,指向想要释放的对象。
与new类似,该表达式执行两个动作:销毁给定的指针指向的对象;释放对象对应的内存。
指针值和delete
传递给delete的指针必须指向动态分配的内存,或是一个空指针。释放一块并非new分配的内存或将相同的指针值多次释放是UB。
释放一个空指针总是不会发生错误。
int main(){
int i = 0, *p = &i, *pn = nullptr;
delete p; //报错。不是动态分配的地址
delete pn; //总是不报错
}
动态对象的生存期直到被释放时为止
shared_ptr管理的内存在销毁时被自动释放。但对于内置指针管理的内存,在显式释放之前都是存在的。就算离开作用域,该处的内存依然未被释放。
int main() {
int ** addr;
int* p = new int;
{
int* ps = new(std::nothrow) int(100);
p = ps;
addr = &ps;
}
std::cout<<*p<<**addr<
delete之后则上述*p
和**addr
均变为ub:
int main() {
int ** addr;
int* p = new int;
{
int* ps = new(std::nothrow) int(100);
p = ps;
addr = &ps;
delete ps;
}
std::cout<<*p<<**addr<
delete之后重置指针值(仅提供有限的保护)
当delete一个指针后,指针值变为无效,但指针依然保存着被释放的动态内存地址。delete之后该指针成为了空悬指针(dangling pointer),即指向一块曾保存对象而已经无效的内存的指针。
空悬指针具有未初始化指针的所有缺点。
避免方式为:在指针即将离开其作用域之前释放掉所关联的内存。若需要保留指针本身,则需在delete之后将nullptr赋予指针。
但是如此操作仅对该指针有效。若存在多个指针指向一块内存地址的情况,则对其他指针无效。例如:
int* p = new int(0);
auto q = p;
delete p;
p = nullptr;
//此时q依然是空悬指针
结合使用shared_ptr和new
不初始化的智能指针是一个空指针。
此外,可以使用new返回的指针来初始化智能指针。
接受指针参数的智能指针构造函数是explicit的,因此需要直接初始化。不能直接将内置指针转化为智能指针:
shared_ptr p1 = new int(100); //错误
shared_ptr p2(new int(100)); //正确:直接初始化
也可以使用make_shared(相当于仅仅用了new出的指针的对象本身而不是这个指针),注意类型:
int* p = new int(42);
auto a = std::make_shared(*p); //只是利用了*p的值
std::shared_ptr b(p);
另外,若要返回一个shared_ptr,在return语句中的正确写法为:
return shared_ptr(new int(p))
而不是简单的return new int(p)
定义和改变shared_ptr
的其他方法
- | - |
---|---|
shared_ptr |
p管理内置指针q指向的对象;q必须指向new分配到内存并且能转换为T*类型 |
shared_ptr |
p从unique_ptr u处接管了对象所有权:将u置空 |
shared)ptr |
p接管了内置指针q指向的对象的所有权,q必须能转换为T*类型。p将使用可调用对象d代替delete |
shared_ptr |
p是shared_ptr p2的拷贝,使用可调用对象d代替delete |
p.reset() | p.reset(q) | p.reset(q, d) |
若p是唯一指向其对象的shared_ptr,释放此对象。若传递了q,则令p指向q,否则置空。若传递了d,则会调用可调用对象d而不是delete。 |
不要混合使用智能指针和普通指针
shared_ptr可以协调对象的析构,但仅限于自身的拷贝之间。因此推荐使用make_shared而不是new。这样就能在分配对象的同时将shared_ptr与之绑定,避免无意中将同一块内存绑定到多个独立创建的shared_ptr上。
考虑如下函数:
void process ptr>{
//do something
}; //离开作用域即被销毁
该函数的传参属于传值方式,实参被拷贝到ptr中,会增加引用计数。若传递给该函数一个shared_ptr:
shared_ptr p; //引用计数为1
precess(p); //在拷贝传参时,引用计数+1,为2
auto i = *p; //正确,p的引用计数为1
若传递一个由内置指针转化了的shared_ptr:
T* x(new T());
process(x); //错误,参数类型不一致
process(shared_ptr(x)); //正确,但括号内语句作为临时指针,在该表达式结束后就被销毁
auto j = *x; //错误:x已经是空悬指针
将一个shared_ptr绑定到一个普通指针时,内存的管理责任已经属于该shared_ptr,一旦这样做了就不应该再使用内置指针访问该地址。
不要使用get初始化另一个智能指针或为智能指针赋值
智能指针类型定义了名为get
的函数,返回一个内置指针,该内置指针指向智能指针指向的对象。
get的设计情况是:需要向不能使用智能指针的代码传递一个内置指针。也就是将指针的访问权限传递给代码
使用get返回的指针的代码不能delete该指针。因此只有在确定代码不delete指针的情况下才能使用get。
编译器不会报错,但不能将另一个智能指针也绑定到get返回的这个内置指针。永远不要用get初始化另一个智能指针或为另一个智能指针赋值
其他shared_ptr操作
可以使用reset将新的指针赋予一个shared_ptr。与赋值类似,会更新引用计数。
reset常和unique一起使用来控制多个shared_ptr共享的对象。在改变底层对象之前检查自己是否是当前对象仅有的用户。若不是,则制作一份新的拷贝。
if(!p.unique()) p.reset(new string(*p)); //用对象的值分配新的拷贝,而不是指向原来的对象
*p += newVal; //拷贝后改变对象的值
另外,reset不接受智能指针作为参数,因此下列操作非法:
b.reset(std::make_shared(*b));
b.reset(std::shared_ptr(*b));
智能指针和异常
异常处理程序能在异常发生后零程序继续,而该类程序需要确保在异常发生后资源能被正确释放。一个简单的确保资源正常释放的方法就是使用智能指针。
函数退出的两种可能:正常处理结束、发生异常。两种情况都会销毁局部对象。
如果使用智能指针,即使程序块过早结束也能确保在内存不再需要时将其释放:
void foo(){
shared_ptr sp(new int(42));
//假设该处抛出一个未捕获的异常
}//函数结束后自动释放内存
与之相对,发生异常时直接管理的静态内存即使发生异常也不会在delete之前不会自动释放。
智能指针和哑类
析构函数负责清理对象使用的资源。但是并非所有类都良好定义了析构函数,因此需要用户显式释放使用的资源。若在资源分配和释放之间发生异常,则程序会发生资源泄漏。
对于没有析构函数的类,使用智能指针相当有效。
使用自己的释放操作
在shared_ptr销毁时默认对管理的指针进行delete操作,但可以定义一个删除器(deleter)代替默认的delete操作。例如:
void end_connection(connection* p){disconnect(*p);}
该函数接受一个connection类的指针,来进行指定的释放操作。
在创建shared_ptr时即可传递一个指向删除器函数的参数:
void f(destination& d){
connection c = connect(&d);
shared_ptr p (&c, end_connection);
//使用connection类
//即使程序异常也能正常释放
}
unique_ptr
unique_ptr“拥有”其指向的对象,当其被销毁时,指向的对象也被销毁。
不同于shared_ptr,unique_ptr没有类似make_shared的函数返回一个unique_ptr。因此定义时只能通过绑定一个new出的指针上。
类似shared_ptr,使用new返回的指针进行初始化只能采用直接初始化。
由于独占指向对象,unique_ptr不支持普通的拷贝和赋值操作。
但是可以通过release或reset将指针的所有权从一个非const的unique_ptr转移给另一个unique_ptr:
int main(){
std::unique_ptr p1(new int(1));
//将所有权从p1转给p2的同时p1置空
std::unique_ptr p2(p1.release());
std::cout<<*p2< p3(new int(2));
//将所有权从p3转给p2并释放p2原来指向的内存
p2.reset(p3.release());
std::cout<<*p2<
总之,对于unique_ptr,要交出所有权的一方作为参数均需要.release()
。调用release会切断unique_ptr和其原来管理的对象之间的联系。release返回的指针通常被用来初始化另一个指针或者为另一个指针赋值。
- | - |
---|---|
unique_ptr | unique_ptr |
空unique_ptr,可以指向类型T的对象。u1使用delete释放它的指针;u2使用类型D的可调用对象释放他的指针。 |
unique_ptr |
空unique_ptr,指向类型为T的对象,用类型为D的对象d代替delete |
u = nullptr |
释放u指向的对象 |
u.release() |
u放弃控制权并置空,返回指向原对象的(内置)指针 |
u.reset() | u.reset(q) | u.reset(nullptr) |
若提供了内置指针则指向该对象,否则置空 |
传递unique_ptr参数和返回unique_ptr
不能拷贝unique_ptr的规则有一个例外:可以拷贝一个将要被销毁的unique_ptr。最常见的例子是从函数返回一个unique_ptr:
unique_ptr clone(int p){
return unique_ptr(new int(p));
}
或返回一个局部对象的拷贝:
unique_ptr clone(int p){
unique_ptr ret(new int(p));
return ret;
}
这是一种特殊“拷贝”。在13.6.2节详述。
向unique_ptr传递删除器
重载unique_ptr中的默认删除器和shared_ptr的机制不同。
重载unique_ptr的删除器会影响到unique_ptr的类型和构造,必须在尖括号中指名删除器类型(shared_ptr仅影响构造,尖括号内类型只有一个)。
weak_jptr
- weak_ptr不控制所指向对象生存期,它指向一个由shared_ptr管理的对象。
- 将一个weak_ptr绑定到一个shared_ptr不会改变其引用次数。
- 一旦最后一个指向对象的shared_ptr被销毁,对象就被释放,无论是否有weak_ptr。
因此,名称符合其weak的特点
expression | - |
---|---|
weak_ptr |
空weak_ptr可以指向类型为T的对象 |
weak_ptr |
与shared_sp指向相同对象的weak_ptr,T必须能转换为sp指向的类型 |
w = p |
p可以是一个shared_ptr或者weak_ptr,复制后w和p共享对象 |
w.reset() |
将w置空 |
w.use_count() |
返回与w共享的shared_ptr的数量 |
w.expired() |
如果w.use_count()为0则返回true,否则false |
w.lock() |
如果expired为true则返回空shared_ptr,否则返回指向w的对象的shared_ptr |
创建一个weak_ptr需要用一个shared_ptr进行初始化:
auto p = make_shared(42);
weak_ptr wp(p);
由于weak_ptr的对象可能不存在,不能直接使用其访问对象,而是利用lock检查指向的对象是否依然存在。如果返回的shared_ptr存在,则其指向的底层对象也一直存在。
核查指针类
尝试为StrBlob定义一个伴随指针类StrBLobPtr,该类会保存一个weak_ptr指向StrBlob的data成员,这是初始化时提供的。通过使用weak_ptr不会影响给定的StrBlob的生存期,但是可以阻止用户访问一个不存在的vector。
含有运算符重载、shared_ptr、weak_ptr的“完全体”StrBlob和StrBlobPtr类:
#include
#include
#include
#include
#include
#include
class StrBlobPtr;
class StrBlob{
public:
friend class StrBlobPtr;
typedef std::vector::size_type size_type;
//默认构造函数
StrBlob();
//以接受初始化列表的构造函数
StrBlob(std::initializer_list il);
//基本方法
[[nodiscard]] size_type size() const {return data->size();};
[[nodiscard]] bool empty() const {return data->empty();};
void push_back(const std::string &t) {
std::cout<<"push_back"<push_back(t);};
void pop_back();
std::string& front();
std::string& back();
StrBlobPtr begin();
StrBlobPtr end();
private:
//使用shared_ptr管理装入的容器类型
std::shared_ptr> data;
//检查是否合法。data[i]不合法时抛出异常
void check(size_type i, const std::string &msg) const;
};
//类外定义构造函数,类成员写在函数体之前、函数名的冒号之后
StrBlob::StrBlob(): data(std::make_shared>()) {};
StrBlob::StrBlob(std::initializer_list il):
data(std::make_shared>(il)) {};
void StrBlob::check(size_type i, const std::string &msg) const {
if (i >= data->size()) throw std::out_of_range(msg);
}
//类外定义的成员方法
void StrBlob::pop_back() {
check(0, "no element to pop");
std::cout<<"pop_back"<pop_back();
}
std::string& StrBlob::front() {
check(0, "no element in the front");
std::cout<<"front"<front();
}
std::string& StrBlob::back() {
check(0, "no element in the back");
std::cout<<"back"<back();
}
class StrBlobPtr{
public:
StrBlobPtr(): curr(0) {};
StrBlobPtr(StrBlob &a, size_t sz = 0): wptr(a.data), curr(sz) {};
std::string& operator*() const;
StrBlobPtr& operator++();
bool operator==(StrBlobPtr&) const;
bool operator!=(StrBlobPtr&) const;
private:
[[nodiscard]]std::shared_ptr> check (std::size_t, const std::string&) const;
std::weak_ptr> wptr;
std::size_t curr;
};
std::shared_ptr> StrBlobPtr::check(std::size_t i, const std::string& s) const {
auto ret = wptr.lock();
if(!ret) throw std::runtime_error("unbound StrBlobPtr");
if(i >= ret->size()) throw std::out_of_range(s);
return ret;
}
//重载运算符方式
std::string& StrBlobPtr::operator*() const {
auto p = check(curr, "dereference past end"); //是lock返回的指针
return (*p)[curr];
}
StrBlobPtr& StrBlobPtr::operator++() {
check(curr, "increment past end of StrBlobPtr"); //已经是尾后则不可递增
++curr;
return *this;
}
StrBlobPtr StrBlob::begin() {
check(0, "no element");
return StrBlobPtr(*this);
}
StrBlobPtr StrBlob::end() {
check(0, "no element");
auto ret = StrBlobPtr(*this, data->size());
return ret;
}
bool StrBlobPtr::operator==(StrBlobPtr& p) const{
return this->curr == p.curr? true : false;
}
bool StrBlobPtr::operator!=(StrBlobPtr& p) const{
return this->curr == p.curr? false : true;
}
int main(){
StrBlob s1 = {"aa", "bb", "cc"};
s1.pop_back();
//使用范围for必须实现begin和end成员
s1.push_back("dd");
auto sp = StrBlobPtr(s1, 0);
//注意#include
std::cout<<*sp<