C++Primer(中文第五版)学习笔记(第3天)

前置constexpr关键字声明constexpr构造函数,其必须初始化所有数据成员,其初始值或使用constexpr构造函数或是一条常量表达式。

类的静态成员static
类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。其类型可以是常量、引用、指针、类类型等。类似的,静态成员函数也不与任何对象绑定在一起,它们不包含this指针,故而静态成员函数不能声明成const的,且我们也不能再static函数体内使用this指针。
我们可以使用作用域运算符直接访问静态成员,但我仍然可以通过类的对象、引用或指针来访问:

static double num;//声明string类中的静态数据成员num
double a=String::num;//通过类名直接访问静态成员
String s; a=s.num;//通过类的对象访问静态成员

不能在类的内部初始化静态成员(除const),必须在类的外部定义和初始化每个静态成员且只能定义一次。类似全局变量,静态数据成员定义在任何函数之外,因此一旦被定义,就将一直存在于程序的整个生命周期中。

double String::num=0;//类外定义并初始化一个静态成员

可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr,初始值必须是常量表达式。

static constexpr int cnum=10;//在类内初始化,初始值必须是常量表达式

如果在类内提供了一个初始值,则成员定义不能再指定一个初始值了。但即使一个常量静态数据成员在类内部被初始化了,通常情况下也应该在类的外部定义一下该成员:

constexpr int String::cnum;//类外定义,但初始值类内定义提供

静态成员可以作为成员函数的默认实参,因为静态成员独立于任何对象;但普通成员不行,因为普通成员的值本身属于对象的一部分。另外静态数据成员的类型可以是它所属的类类型,而非静态数据成员则受到限制,只能声明成它所属的指针或引用:

static String sstr;//正确,静态成员可以是不完全类型
String *pstr;//正确,指针成员可以是不完全类型
String str;//错误,数据成员必须是完全类型

动态内存

C++内存划分为4个区域:
1. 静态/全局内存:存储全局变量及静态局部变量,在使用之前分配,在程序结束时销毁。
2. 栈内存:保存定义在函数内的非static对象,仅在其定义的程序块运行时才存在。静态和栈中的对象均由编译器自动创建和销毁。
3. 堆/自由空间:存储动态分配的对象即那些在程序运行时分配的对象。其生存期有程序来控制,当动态对象不再使用时,必须在代码中显式地销毁它们。
4. 常量存储区:存储不允许进行写操作的常量表达式。

new/delete
经典:new在动态内存中为对象分配空间并返回一个指向该对象的指针。delete接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。delete释放一个空指针总是没错的。

注意用new分配const对象是合法的:
const int *p=new const int(10);//分配并初始化一个const int
同样使用delete释放一个const动态对象也是合法的:
delete p;

当我们使用一条new 表达式时:
1. new表达式调用一个名为operator new(或operator new[])的标准库函数。该函数分配一块足够大、原始的、未命名的内存空间以便存储待定类型的对象(或对象的数组)。
2. 编译器运行相应的构造函数以构造这些对象,并为其传入初始值。
3. 对象被分配了空间并构造完成,返回一个指向该对象的指针。

当我们使用一条delete表达式删除一个动态分配的对象时:
1. 对p所指的对象或者所指的数组中的元素执行对应的析构函数。
2. 编译器调用名为operator delete(或operator delete[])的标准库函数释放内存空间。

由new管理的动态内存在被显式delete释放前一直都会存在!!

智能指针:是模版,其行为类似常规指针,但它复杂自动释放所指向的对象。包括shared_ptr 允许多个指针指向同一个对象,unique_ptr 则独占所指向的对象。除此之外还有weak_ptr 弱引用指向shared_ptr所管理的对象。

智能指针需要#include<memory>

shared_ptr

shared_ptr<int> p;//默认初始化的智能指针中保存着一个空指针。
shared_ptr<int> q(new int(12));//初始值为12的int类型的智能指针,注意是直接初始化形式!
q=new int(12);//错误!接受指针参数的智能指针构造函数是explicit的,因此我们不能将一个内置指针隐式的转换为一个智能指针

一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。若将智能指针绑定到一个指向其它类型的资源的指针上,必须提供自己的操作来替代delete。

shared_ptr<int> p(r,d);//p接管了内置指针r所指向的对象的所有权,r必须能转换为int* 类型,p将使用可调用对象d来代替delete来释放对象。
可调用对象:对于一个对象或一个表达式,若可以对其使用调用运算符,则称它为可调用的。
调用运算符():一对圆括号,里面放置实参列表,用于调用一个函数。

智能指针中的常用函数

p.get();//返回p中保存的指针
swap(p,q);//交换p和q的指针
p.use_count(); //返回与p共享的智能指针数量
p.reset(); //若p是唯一指向其对象的shared_ptr,reset会释放此对象,将p置为空。
p.reset(q); //另p指向q,并释放p所指向的对象

最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数,此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。

shared_ptr<int> q=make_shared<int>(12);//指向一个值为12的int 的shared_ptr
shared_ptr<string> str=make_shared<string>(5,'9'); //返回一个值为”99999”的string,传递的参数必须与string的某个构造函数相匹配
auto r=make_shared<vector<string>>();//指向一个动态分配的空string向量,使用auto来保存结果

当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其它shared_ptr指向相同的对象

auto p=make_shared<int>();//p指向的对象只有p一个引用者
auto q(p);//p和q指向相同对象,此对象有两个引用者

可认为每个shared_ptr都有个关联的计数器,称引用计数。无论何时拷贝一个shared_ptr时,计数器都会递增。当给一个shared_ptr赋予一个新值或shared_ptr被销毁时,计数器会递减。一旦一个shared_ptr的计数器变为0即最后一个shared_ptr被销毁时,它就会自动释放自己所管理的对象。

auto r=make_shared<int>();//r指向的int只有一个引用者
r=q;//给r赋值令它指向另一个地址->递增q指向的对象的引用计数->递减r原来指向对象的引用计数->r原来指向的对象已没有引用,会自动释放

注意只要有其它shared_ptr指向这块内存,它就不会被释放掉:

shared_ptr<int> get_number(const int arg) {
    shared_ptr<int> p=number(arg);
    return p; //当我们返回p时,引用计数进行了递增操作,因为return语句向此函数的调用者返回了一个p的拷贝
} //p离开了作用域,但它指向的内存不会被释放

此时我们就可以使用动态内存来允许多个对象共享相同的状态,从而避免以下状况:

vector<string> str1;
{ //新作用域
    vector<string> str2={"1","2","3"};
    str1=str2; //从str2拷贝元素到str1中    
} //str2被销毁,其中的元素也被销毁。但str1中有三个元素,是str2中元素的拷贝

现在我们只需要把数据成员声明成shared_ptr即可:

shared_ptr<vector<string>> data;
并把创造函数声明成如下即可:
String::String(initializer_list<string> il):data(make_shared<vector<string>>(il)) {}
//使用make_shared<vector<string>>(il) 初始化数据成员
对于拷贝、赋值和销毁使用默认版本的即可。

不要使用一个内置指针来访问一个智能指针所负责的对象!如下函数:

void process(shared_ptr<int> ptr){
    //使用ptr
} //ptr离开作用域并销毁

注意process的参数是传值方式传递的,因此实参会被拷贝到ptr中,从而递增其引用计数为2,但当process结束时,ptr计数递减但不为0,当局部变量被销毁时,ptr指向的内存不会被释放。当我们传递一个普通指针而非shared_ptr时:

int *x(new int(12)); //x是一个普通指针非智能指针
process(x); //错误!int*无法隐式转换为shared_ptr<int>
process(shared_ptr<int>(x)); //合法,但内存会被释放
int j=*x; //未定义的:x是一个空悬指针!

unique_ptr
注意某个时刻只能有一个unique_ptr指向一个给定的对象,当unique_ptr被销毁时,它所指向的对象也被销毁。另外unique_ptr没有make_shared函数:

unique_ptr<double> p(new int(12)); //p指向一个值为12的double

由于unique_ptr拥有它所指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作:

unique_ptr<double> q(p); //错误!unique_ptr不支持拷贝
unique_ptr<double> r; r=p; //错误!unique_ptr不支持赋值

unique_ptr常用函数:

u=nullptr; u.release(); u.reset(); //释放u所指的对象,并置u为空
u.reset(q); // 另u指向内置指针q

reset成员接受一个可选的指针参数,令unique_ptr重新指向给定的指针,若非空则还会释放原来指向的对象。release会切断unique_ptr和它原来管理的对象间的联系,其返回一个指针通常用于初始化或赋值给另一个智能指针。

不能直接拷贝和赋值unique_ptr但可以有一个例外:我们可以拷贝或赋值一个将要被销毁的不能直接拷贝和赋值unique_ptr,比如函数返回一个unique_ptr、返回一个局部对象的拷贝、通过release和reset将指针的所有权从一个(非const)unique_ptr转移到另一个:

unique_ptr<int> clone(int p) {return unique_ptr<int>(new int(p));} //函数返回值
unique_ptr<int> clone(int p) {unique_ptr<int> res(new int(p)); return res;} //返回局部对象的拷贝
unique_ptr<string> q(p.release()); //release函数
q.reset(p.release()); //reset函数
auto r=p.release(); //正确!但必须记得delete(r)

unique_ptr管理动态数组:

unique_ptr<int[]> arr(new int [10]); //arr指向一个包含10个未初始化int的数组
int x=arr[2]; //读取数组中的数据
arr.release(); //自动用delete[]销毁其指针

但注意指向数组的unique_ptr不支持成员访问(.和->),其它操作不变。再注意shared_ptr不直接支持管理动态数组,若希望其支持,必须提供自己定义的删除器:

shared_ptr<int> arr(new int[10], [](int *p){delete[] p;}); //第二个参数为自定义的删除器函数
int y=*(arr.get()+2); //shared_ptr不支持[]及指针的算术运算,这里我们使用get获取一个内置指针
arr.reset(); //使用自定义删除器销毁数组

weak_ptr
weak_ptr是一种不控制所指向对象生存周期的智能指针,它指向一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数,一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也还是被释放。

我们创建一个weak_ptr时,要用一个shared_ptr来初始化它:

auto p=make_shared<int>(12); weak_ptr<int> wp(p);//wp弱共享p,p的引用计数未改变

weak_ptr常用函数:

wp.reset(); //将wp置为空
wp.use_count(); //与wp共享对象的shared_ptr的数量
wp.expired(); //若wp.use_count()为0返回true,否则false
wp.lock(); //若wp.expired()为true返回空shared_ptr,否则返回一个指向wp对象的shared_ptr

由于对象可能不存在,我们不能直接使用weak_ptr访问对象,而必须调用lock:

if(shared_ptr<int> np=wp.lock()) {} //若np不为空则条件成立,并使用np访问共享对象

allocator分配内存
new/delete将内存分配、销毁和对象构造、析构组合在了一起,导致了不必要的浪费。使用allocator类可以将内存分配和对象构造分离开来。当一个allocator对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置。

allocator<int> alloc; //可以分配int的allocator对象
auto const p=alloc.allocate(n); //分配一段原始的、未构造的内存,保存n个类型为int的对象,返回一个int* 指针
auto q=p; //q指向最后构造的元素之后的位置
alloc.construct(q++,args); //args是被传递给int的构造函数参数,用来在p指向的内存中构造一个对象。args为空即调用默认构造函数
int z=*(--q); //构造对象后方能使用,注意z=*q; 是错误的!q指向的是未构造的内存
alloc.destroy(q); //对q所指向的对象(真正构造了的元素)执行析构函数,之后可以选择重新使用这部分内存保存其它int
alloc.deallocate(p,n); //释放从p地址开始的内存,这块内存保存了n个类型为int的对象,参数必须为allocate中的p与n,且在之前必须先对每个在这块内存中创建的对象调用destroy

猜你喜欢

转载自blog.csdn.net/sinat_30477313/article/details/79754257