C++ Primer阅读心得(第十二章)

1.c++中使用的内存分为三部分:

  • 栈内存:用来保存函数内部的局部对象(非static),由编译器自动创建和释放
  • 堆内存:用来动态分配对象,由程序自行分配和释放
  • 静态内存:用来保存全局对象以及局部static对象,由编译器自动创建和释放

2.c++中定义了new操作符来分配堆内存。new操作符的行为包括两步:(1).从堆上分配内存;(2).调用对应的构造函数。所以可以在new操作符后面的类型之后加上(),填入传递给构造函数的参数。c++11中还可以使用列表初始化,用{}来代替()。

int *p = new int(1024); //初始化一个指针p,p指向的int类型的值为1024
class A;
A *q = new A(...); //初始化一个指针q,q指向A类型的动态对象
vector<int> *r = new vector<int>{1,2,3,4}; //列表初始化动态vector<int>对象

c++11中新增的auto可以和new一起工作,但是仅限于类型后的初始化器()中传递一个参数的情况

auto s = new auto(*q); //ok,编译器自动推断s的类型为A*
auto t = new auto(1); //ok,编译器自动推断t的类型为int*
auto t = new auto(1,2,3); //错误,编译器无法推断类型,究竟应该调用哪个构造函数?

当系统内存耗尽时,使用new分配堆内存会导致bad_alloc异常,我们可以通过使用placement new nothrow来让new操作符不再抛出bad_alloc,只返回一个空指针代表内存耗尽。

int *p = new (nothrow) int(1024); //当内存耗尽时,p == nullptr

3.c++中定义了delete操作符来释放堆内存。delete操作符的行为包括两步:(1).调用对应的析构函数;(2).释放堆内存。所以delete void*类型的指针时需要特别注意,因为不会调用析构函数,可能会造成资源泄露。另外,还需要注意不要delete同一个指针两次(double free)。

int *p = new int(1024);
int *q = p;
delete p; //ok,正确的释放
delete q; //错误,double free
class A;
A *r = new A();
delete r; //ok,先析构再释放内存
A *t = new A();
void *s = static_cast<void *>t;
delete s; //错误,没有析构就释放内存

4.悬垂指针:悬垂指针是指delete操作符处理之后的指针,该指针所指向的内存空间虽然已经释放,但是指针本身指向的地址依然保留,这时对此指针进行解引用等操作会得到意想不到的结果,产生很大的错误。所以指针delete之后最好将其赋值为NULL(0)或者c++11中新增的nullptr。

5.new和delete除了可以分配和释放单个动态对象之外,还可以用来分配和释放动态数组。分配动态数组时可以在类型的后面加上()进行值初始化,c++11中也支持列表初始化。

int *p = new int[10]; //默认初始化,数组内的值未确定
int *q = new int[10](); //值初始化,数组值全部为0
string *r = new string{"a","b","c"}; //列表初始化

注意:new分配动态数组返回的指针是指向此动态数组第一个对象的指针,而不是指向整个数组的指针,所以c++11提供的begin和end函数都不能用,同理,c++11新增的范围for也同样不能使用。

int *p = new int[10]();
for (auto q=begin(p);q!=end(p);q++) {...} //错误,p不是数组的指针(int *[10])而是int *
for (auto q : p) {...} //错误,原因同上

另外,因为初始化的()中不能放入参数,所以不能使用auto自动推断动态数组。

auto p = new auto[10](); //错误,编译器无法推断类型

在delete的后面加上[]就可以释放动态分配的数组了,注意[]不能省略,否则行为是未定义的。

int *p = new int[10];
delete [] p; //ok,释放了动态数组
int *q = new int[10];
delete q; //错误。只释放了第一个指针,你要闹哪样???

6.默认初始化vs值初始化:new操作符分配的动态对象或者动态数组都是默认初始化的,在类型后加上()就可以将它们值初始化。对于类类型来说,默认初始化和值初始化没有任何区别,都调用了默认构造函数。但是对于内置类型来说,默认初始化后的值是未确定的,值初始化后的值都是确定的系统默认值。

int *p = new int[10]; //默认初始化,值未确定
int *p = new int[10](); //值初始化,全部为0
class A;
A *p = new A[10]; //默认初始化,调用默认构造函数
A *p = new A[10](); //值初始化,明确调用默认构造函数

7.在c++11中新增了shared_ptr智能指针模板类来协助管理动态对象(boost你赢了),shared_ptr允许多个指针指向同一个动态对象。shared_ptr中包含一个引用计数(reference count),来表示当前保存的动态对象被多少个其他shared_ptr所引用;一旦引用计数变为0,则shared_ptr自动销毁它所管理的动态对象。拷贝、赋值、reset函数调用以及离开作用域时shared_ptr的析构都会导致引用计数的变化:

shared_ptr<int> p = make_shared<int>(42);
{
    shared_ptr<int>q(p); //拷贝:引用计数+1
} //离开了q的作用域,q析构,引用计数-1
shared_ptr<int> r = make_shared<int>(40);
r = p; //赋值:左侧的引用计数-1,右侧的引用计数+1
r.reset(); //重置,内部引用计数-1
r.reset(p); //同赋值,r的引用计数-1,p的引用计数+1
return p; //返回一个shared_ptr,引用计数先+1,再-1

默认初始化的shared_ptr保存着一个空指针,解引用shared_ptr就得到了其中包含的动态对象的指针,可以把shared_ptr放入if语句中来判空。

shared_ptr<string> p; //默认初始化,p指向一个空指针
shared_ptr<string> q = make_shared<string>("abc");
size s = q->size(); //解引用p得到对象的指针
if(p){} //p是空指针,多以条件为false
if(q){} //q不是空指针,所以条件为true

c++11增加了一个标注库函数make_shared,用来生成一个动态对象并返回指向它的shared_ptr。还以将new操作符返回的指针来初始化shared_ptr,注意:shared_ptr的这个构造函数是explicit的。除了这些之外,shared_ptr还允许用户自定义deleter来释放内部保存的动态对象。

auto p = make_shared<vector<int>>(1,10); //使用make_shared构造一个vector<int>的shared_ptr,注意后面的括号中要包含对应的构造函数参数
int *q = new int(43);
shared_ptr<int> r(q); //使用new操作符返回的指针初始化shared_ptr
shared_ptr<DBConnection> c(new DBConnection(...), ReleaseDB); //自定义deleter
shared_ptr<int> funcA() {
    return new int(0); //错误,shared_ptr是explicit的
    return make_shared<int>(0); //正确
}

在代码中使用了异常时,使用智能指针要比使用普通指针能够获得更多的保障。

{
    int *p = new int(0);
    Connection *c = GetConnection();
    throw xxx; //p占用的内存没有释放,c打开的连接没有关闭
}
{
    auto p = make_shared<int>(0);
    shared_ptr<Connection> c(GetConnection(), ReleaseConnection);
    throw xxx; //没关系,在异常退出这部分代码的时候,p的内存和c的连接都被shared_ptr自动释放了
}

注意:不要在代码中混用普通指针和智能指针,也不要把get函数返回的指针传递给另外一个shared_ptr.

int *p = new int(1024);
{
    shared_ptr<int> q(p); //ok,将p交给shared_ptr来管理
} //q离开作用域,析构,p被释放!
int r = *p; //未定义的操作,p已经被释放了
{
    auto s = make_shared<int>(1024);
    int *t = s.get(); //取出s保管的指针
    shared_ptr<int> u(t); //u也保管了这个指针,但是引用计数没有+1!
    return 0; 
} //错误发生,s和u保存同一动态对象,代码段结束时,这个对象被delete了两次!

8.c++11增加了智能指针模板类unique_ptr来管理动态对象。与shared_ptr不同,unique_ptr对动态对象是“独占”的。没有类似make_shared的函数来构造unique_ptr,必须使用new操作符返回的指针直接初始化。除了作为函数的返回值之外(move语义还是编译器的优化呢?),unique_ptr不支持拷贝和赋值操作。

unique_ptr<int> p(new int(42)); //使用new返回的指针初始化
unique_ptr<int> q(p); //错误,不能拷贝
q = p; //错误,不能赋值
unique_ptr<int> funcA() {
    return unique_ptr<int>(new int(43)); //ok,拷贝一个即将销毁的unique_ptr
}

release函数让unique_ptr放弃对当前持有的动态对象的所有权,reset函数可以让unqiue_ptr管理另外一个动态对象并释放当前持有的动态对象。

unique_ptr<int> p(new int(1024));
int *q = p.release(); //ok,释放指针
int a = *p; //错误,已经释放了,p是空指针
p.reset(q); //ok,p又指回去啦
unique_ptr<int> r(new int(1024));
r.reset(p.release()); //ok,r释放原来的,指向了p里面的指针
r.reset(); //ok,释放了原指针,将r置空

与shared_ptr类似,unique_ptr可以自定义deleter,需要注意的是unqiue_ptr的delete是声明在模板里的,编译时就会绑定此deleter。

Connection *c = GetConnection();
unique_ptr<Connection, decltype(CloseConnection)*> p(c, CloseConnection) ; //ok,p离开作用域时会调用CloseConnecton关闭连接

unique_ptr还可以管理动态数组,离开作用域时会自动调用delete []来释放它,可以用下标操作符[]来访问动态数组中的内容。

unique_ptr<int []> p_array(new int[10]); //管理动态数组,别忘了类型后面加个[]
for (size_t i = 0; i < 10; i++) {
    p_array[i] = 0; //使用下标访问
}
p_array.release(); //释放动态数组

9.除了shared_ptr和unique_ptr之外,c++11还定义了一种不管理动态对象生命周期的智能指针weak_ptr。你需要用一个shared_ptr来初始化weak_ptr;当一个weak_ptr绑定到shared_ptr之后,并不会增加shared_ptr中的引用计数,所以访问weak_ptr之前必须使用lock函数来判断一下对应的动态对象是否仍然存在。weak_ptr除了解除shared_ptr的循环引用的作用之外,当你把一个task传入线程池时,task内含(lambda捕获、this隐含指向等)的ptr要使用weak_ptr以防备线程执行时ptr已经被终结导致的各种问题(shared_ptr也能防止被释放,但是一则有内存浪费,二则对象内部的状态shared_ptr管不着,即使没被释放调用也会导致各种问题)。

shared_ptr<int> p = make_shared<int>(1024);
weak_ptr<int> w(p); //w指向了p
p.reset(); //释放p
if (auto r = q.lock()) { //r是一个空指针的shared_ptr,所以这里是false
    ....
}

10.虽然new和delete能够对动态数组进行管理,但是它们仍然不够灵活。首先,内存必须整块的分配和回收;其次,new在分配内存的同时也对对象进行默认构造(某些时候这是很浪费的行为)。为了能够灵活的分配未经初始化的内存,标准库提供了allocator类。我们可以使用allocate按需分配未经初始化的内存,使用construct调用任意构造函数进行初始化(c++11),使用destroy调用析构函数释放对象,使用deallocate释放内存。

allocator<string> alloc;
auto p = alloc.allocate(n); //分配n个未经初始化的string
auto beg = p;
alloc.construct(p++, "abc"); //将第一个string初始化为abc
alloc.construct(p++, 10, 'c'); //将第二个string初始化为10个c
alloc.destroy(beg+1); //释放第二个string
alloc.destroy(beg); //释放第一个string
alloc.deallocate(beg, n); //将这n个对象全部释放

为了让allocator与标准库中的其他容器一起工作,标准库还提供了四个函数uninitialized_copy、uninitialized_copy_n、unintialized_fill和uninitialized_fill_n。

vector<int> vi{1,2,3,4,5};
allocator<int> alloc;
auto p = alloc.allocate(vi.size()*2);
auto q = uninitialized_copy(vi.begin(), vi.end(), p); //将动态数组前一半填充为vi中的内容
uninitialized_fill_n(q, vi.size(), 10); //将动态数组的后一半填充为10

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值