1.不要返回局部对象的引用或指针。
函数完成后,它所占用的存储空间也随之被释放掉。因此函数终止意味着局部变量的引用将指向不再有效的内存区域:
//严重错误:这个函数试图返回局部对象的引用
const string &manip()
{
string ret;
//以某种方式改变一下ret
if(!ret.empty())
return ret; //错误:返回局部对象的引用
else
return “Empty”; //错误:”Empty”是一个局部临时量
}
同样,返回局部对象的指针也是错误的。一旦函数完成,局部对象被释放,指针将指向一个不存在的对象。
2. func(int i)表示调用func函数时需要一个int类型的实参。
(*func(int i)) 意味着我们可以对函数调用的结果执行解引用操作。
(*func (int i))[10]表示解引用func的调用将得到一个大小是10的数组。
int(*func(int i))[10]表示数组中的元素是int类型。
使用尾置返回类型:在c++11新标准中还有一种可以简化上述func声明的方法,就是使用尾置返回类型。任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效,比如返回类型是数组的指针或者数组的引用。尾置返回类型跟在形参列表后面并以一个->符号开头。为了表示函数真正的返回类型跟在形参列表之后,我们本应该出现返回类型的地方放置一个auto:
//func接受一个int类型的参数,返回一个指针,该指针指向含有10个整数的数组
auto func(int i) -> int(*)[10];
因为我们把函数的返回类型放在了形参列表之后,所以可以清楚地看到func函数返回的是一个指针,并且该指针指向了含有10个整数的数组。
3.函数指针
函数指针指向的是函数而非对象。和其他指针一样,函数指针指向某种特定类型。函数的类型由它的返回类型和形参类型共同决定,与函数名无关。例如:
//比较两个string对象的长度
bool lengthCompare(const string &,const string &);
该函数的类型是bool(const string &,const string &)。要想声明一个可以指向该函数的指针,只需要用指针替换函数名即可:
//pf指向一个函数,该函数的参数是两个const string的引用,返回值是bool类型
bool (*pf) (const string&,const string&);//未初始化
从我们声明的名字开始观察,pf前面有个*,因此pf是指针:右侧是形参列表,表示pf指向的是函数;在观察左侧,发现函数的返回类型是布尔值。因此,pf就是一个指向函数的指针,其中该函数的参数是两个const string的引用,返回值是bool类型。
【注】*pf两端的括号必不可少。如果不写这对括号,则pf是一个返回值为bool指针的函数:
//声明一个名为pf的函数,该函数返回bool*
bool *pf(const string&,const string &);
使用函数指针
当我们把函数名作为一个值使用时,该函数自动地转换为指针。例如:按照如下形式我们可以将lengthCompare的地址赋给pf:
pf = lengthCompare; //pf指向名为lengthCompare的函数
pf=&lengthCompare; //等价的赋值语句:取地址符是可选定的
此外,我们还能直接使用指向函数的指针调用该函数,无须提前解引用指针:
bool b1 = pf(“hello”,”goodbye”); //调用lengthCompare函数
bool b2 = (*pf)(“hello”,”goodbye”); //一个等价的调用
bool b3 = lengthCompare(“hello”,”goodbye”); //另一个等价的调用
在指向不同函数类型的指针不存在转换规则。但是和往常一样,我们可以为函数指针赋一个nullptr或者0的整型常量表达式,表示该指针没有指向任何一个函数:
string::size_type sumLength(const string&,const string &);
bool cstringCompare(const char*,const char*);
pf = 0; //正确:pf不指向任何函数
pf = sumLength; //错误:返回类型不匹配
pf = cstringCompare; //错误:形参类型不匹配
pf = lengthCompare; //正确:函数和指针的类型精确匹配
重载函数的指针
当我们使用重载函数时,上下文必须清晰地界定到底应该选用哪个函数。如果定义了指向重载函数的指针
void ff(int*);
void ff(unsigned int);
void (*pf1)(unsigned int) = ff; //pf1指向ff(unsigned)
编译器通过指针类型决定选用哪个函数,指针类型必须与重载函数中的某一个精确匹配
void (*pf2)(int) = ff; //错误:没有任何一个ff与该形参列表匹配
double(*pf3)(int*) = ff; //错误:ff和pf3的返回类型不匹配
函数指针形参
和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。此时,形参看起来是函数类型,实际上却是当成指针使用:
//第三个形参是函数类型,它会自动地转换成指向函数的指针
void useBigger(const string &s1,const string &s2, bool(*pf)(const string &,const string &));
我们可以直接把函数作为实参使用,此时它会自动转换成指针:
useBigger(s1,s2,lengthCompare);
正如useBigger的声明语句所示,直接使用函数指针类型显得冗长而繁琐。类型别名让我们简化使用了函数指针的代码:
//Func 和Func2是函数类型
typedef bool Func(const string&,const string&);
typedef decltype(lengthCompare) Func2; //等价的类型
//FuncP和FuncP2是指向函数的指针
typedef bool(*FuncP)(const string&,const string&);
typedef decltype(lengthCompare) *&FuncP2; //等价的类型
我们使用typedef定义自己的类型。Func和Func2是函数类型,而FuncP和FuncP2是指针类型。需要注意的是,decltype返回函数类型,此时不会将函数类型自动转换为指针类型。因为decltype的结果是函数类型,所有只有在结果前加上*才能得到指针。可以使用如下的形式重新声明useBigger:
//useBigger(const string&,const string &,Func);
void useBigger(const string&,const string&,Func);
void useBigger(const string&,const string&,FuncP2);
这两个声明语句声明的是同一个函数,在第一条语句中,编译器自动地将Func表示的函数类型转换成指针。
返回指向函数的指针
和数组类似,虽然不能返回一个函数,但是能返回指向函数类型的指针。然而,我们必须把返回类型写成指针形式,编译器不会自动地将函数返回类型当成对应的指针类型处理。与往常一样,要想声明一个返回函数指针的函数,最简单的办法是使用类型别名:
using F =int(int*,int); //F是函数类型,不是指针
using PF = int (*)(int*,int); //PF是指针类型
其中我们使用类型别名将F定义成函数类型,将PF定义成指向函数类型的指针。必须时刻注意的是,和函数类型的形参不一样,返回类型不会自动地转换成指针。我们必须显式地将返回类型指针为指针:
PF f1(int); //正确:PF是指向函数的指针,f1返回指向函数的指针
F f1(int); //错误:F是函数 类型,f1不能返回一个函数
F *f1(int); //正确:显式地指定返回类型是指向函数的指针
当然,我们也能用下面的形式直接声明f1:
int (*f1(int)) (int*,int);
按照由内向外的顺序阅读这条声明语句:我们看到f1有形参列表,所以f1是个函数;f1前面有*,所以f1返回一个指针;进一步观察发现,指针的类型本身也包含形参列表,因此指针指向函数,该函数的返回类型是int。
出于完整性的考虑,我满还可以使用尾置返回类型的凡是声明一个返回函数指针的函数:
auto f1(int) ->int(*)(int*,int);
将auto和decltype用于函数指针类型
如果我们明确返回的函数是哪一个,就能用decltype简化书写函数指针返回类型的过程。例如假定有两个函数,它们的返回类型都是string::size_type,并且各有两个const string&类型的形参,此时我们可以编写第三个函数, 它接受一个string类型的参数,返回一个指针,该指针指向前两个函数中的一个;
string::size_type sumLength(const string&,const string&);
string::size_type largerLength(const string&,const string&);
//根据其形参的取值,getFcn函数返回指向sumLength或者largerLength的指针
decltype(sumLength) *getFcn(const string &);
声明getFun唯一需要注意的地方是,牢记当我们将decltype作用于某个函数时,它返回函数类型而非指针类型。因此,我们显示地加上*以表明我们需要返回指针,而非函数本身。
4.类的静态成员
定义静态成员
和其他的成员函数一样,我们既可以在类的内部不可以在类的外部定义静态成员函数。当在类的外部定义静态成员时,不能重复static关键字,该关键字只出现在类内部的声明语句:
void Account::rate(double newRate)
{
interestRate = newRate;
}
因为静态数据成员不属于类的任何一个对象,所以它们并不是在创建类的对象时被定义的。这意味着它们不是由累的构造函数初始化的。而且一般来说,我们不能在类的内部初始化静态成员。相反的,必须在类的外部定义和初始化每个静态成员,和其他对象一样,一个静态数据成员只能定义一次。
类似于全局变量,静态数据成员定义在任何函数之外。因此一旦它被定义,就将一直存在于程序的整个生命周期中。
我们定义静态数据成员的方式和在类的外部定义成员函数差不多。我们需要指定对象的类型名,然后是类名、作用域运算符以及成员自己的名字:
//定义并初始化一个静态成员
double Account::interestRate = initRate();
这条语句定义了interestRate的对象,该对象是类Account的静态成员,其类型是double。从类名开始,这条定义语句的剩余部分就都位于类的作用域之内了。因此,我们可以直接使用initRate函数。注意,虽然initRate是私有的,我们也能用它初始化interestRate。和其他成员的定义一样,interestRate的定义也可以访问类的私有成员。
【注】要想确保对象只定义一次,最好的办法是把静态数据成员的定义与其他非内联函数的定义放在同一个文件中。
静态成员的类内初始化
通常情况下,类的静态成员不应该在类的内部初始化。然而,我们可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr。初始值必须是常量表达式,因为这些成员本身就是常量表达式,所以它们能用在所有适合于常量表达式的地方。例如,我们可以用一个初始化了的静态数据成员指定数组成员的维度:
class Account {
public:
static double rate() { return interestRate;}
static void rate(double);
private:
static constexpr int period = 30; //period是常量表达式
double daily_tbl[period];
}
如果某个静态成员的应用场景仅限于编译器可以替换它的值的情况,则一个初始化的const或constexpr static不需要分别定义。相反,如果我们将它用于值不能替换的场景中,则该成员必须有一条定义语句。
例如,如果period的唯一用途就是定义daily_tbl的维度,则不需要在Account外面专门定义period。此时,我们忽略了这条定义,那么对程序非常微小的改动也可能造成编译错误,因为程序找不到该成员的定义语句。举个例子,当需要把Account::period传递给一个接受const int&的函数时,必须定义period。
如果在类的内部提供了一个初始值,则成员的定义不能再指定一个初始值了:
//一个不带初始值的静态成员的定义
constexpr int Account::period //初始值在类的定义内提供
【注】即使一个常量静态数据成员在类内部被初始化了,通常情况下也应该在类的外部定义一下该成员。
静态成员能用于某些场景,而普通成员不能
如我们所见,静态成员独立于任何对象。因此,在某些非静态数据成员可能非法的场合,静态成员却可以正常地使用。举个例子,静态数据成员可以是不完全类型。特别的,静态数据成员的类型可以就是它所属的类类型。而非静态数据成员则受到限制,只能声明成它所属类的指针或引用:
class Bar{
public:
private:
static Bar mem1; //正确:静态成员可以是不完全类型
Bar *mem2; //正确:指针成员可以是不完全类型
Bar mem3; //错误:数据成员必须是完全类型
};
静态成员和普通成员的另一个区别是我们可以使用静态成员作为默认实参:
class Screen{
public:
//bkground表示一个在类中稍后定义的静态成员
Screen &clear(char = bkground);
private:
static const char bkground;
};
非静态数据成员不能作为默认实参,因为它的值本身属于对象的一部分,这么做的结果是无法真正提供一个对象以便从中获取成员的值,最终引发错误。
5. 顺序容器
//假定noDefault是一个某有默认构造函数的类型
vector<noDefault> v1(10,init); //正确:提供了元素初始化器
vector<noDefault> v2(10); //错误:必须提供一个元素初始化器
反向容器的额外成员(不支持forward_list)
reverse_iterator 按逆序寻址元素的迭代器
const_reverse_iterator 不能修改元素的逆序迭代器
c.rbegin(),c.rend() 返回指向c的尾元素和首元素位置的迭代器
c.crbegin(),c.crend() 返回const_reverse_iterator
只有顺序容器(不包含array)的构造函数才能接受大小参数
C seq(n) seq包含n个元素,这些元素进行了值初始化;此构造函数是explicit的。(string不适用)
C seq(n,t) seq包含n个初始化值t的元素
vector<const char*>articles = {“a”,”an”,”the”};
std::vector<string> words(articles); //错误:元素类型不匹配
std::forward_list<string> word(articles.begin(), articles.end()); //正确:可以将const char*元素转换为string。
【注】当将一个容器初始化为另一个容器的拷贝时,两个容器的容器类型和元素类型都必须相同。不过,当传递迭代器参数来拷贝一个范围时,就不要求容器类型是相同的了,而且新容器和原容器中的元素类型也可以不同,只要能将要拷贝的元素转换为要初始化的容器的元素类型即可。
标准库array具有固定大小
与内置数组一样,标准库array的大小也是类型的一部分。当定义一个array时,除了指定元素类型,还要指定容器的大小:
array<int,42> ial //类型为:保存42个int的数组,会默认初始化
array<int,42> ia2 = {42}; //ia2[0]为42,剩余元素为0
为了使用array类型,我们必须同时指定元素类型和大小:
arrar<int,10>::size_type i; //数组类型包括元素类型和大小
array<int>::size_type j; //错误:array<int>不是一个类型
int digs[2] = {1,2};
int cpy[2] = digs; //错误:内置数组不支持拷贝或赋值
array<int,2> digits = {1,2};
array<int,2> copy = digits; //正确:只有数组类型匹配即可
【注】虽然我们不能对内置数组类型进行拷贝或对象赋值操作,但array并无此限制,但要求元素类型和大小都一样,因为大小是array类型的一部分。
assign操作不使用于关联容器和array
seq.assign(b,e) 将seq中的元素替换为迭代器b和e所表示的范围中的元素。迭代器b和e不能指向seq中的元素。
seq.assign(il) 将seq中的元素替换为初始化列表il中的元素
seq.assign(n,t) 将seq中的元素替换为n个值为t的元素
swap操作将容器内容交换不会导致指向容器的迭代器、引用和指针失效(容器类型为array和string的情况除外)。
【注】除array外,swap不对任何元素进行拷贝、删除或插入操作,因此可以保证在长时间内完成。
forward_list支持max_size和empty,但不支持size。
每个容器类型都支持相等运算符(==和!=);除了无序关联容器外的所有容器类型都支持关系运算符(<、>=、<、<=)。关系运算符左右两边的运算对象必须是相同类型的容器,且必须保存相同类型的元素。
管理容量的成员函数
//shrink_to_fit只适用于vector、string和deque。
//capacity和reserve只适用于vector和string。
c.shrink_to_fit() 请将capacity()减少为于size()相同大小
c.capacity() 不重新分配内存空间的话,c可以保存多少元素
c.reserve(n) 分配至少能容纳n个元素的内存空间
构造string的其他方法
//n、len2和pos2都是无符号值
string s(cp,n) s是cp指向的数组前n个字符的拷贝。此数组至少应该包含n个字符
string s(s2,pos2) s是string s2从下标pos2开始的字符的拷贝。若pos2>s2.size(),构造函数的行为未定义
string s(s2,pos2,len2) s是string s2从下标pos2开始len2个字符的拷贝。若pos2>s2.size(),构造函数的行为未定义,不管len2的值是多少,构造函数至多拷贝s2.size() – pos2个字符。
substr操作
substr操作返回一个string,它是原始string的一部分或全部的拷贝。可以传递给substr一个可选的开始位置和计数值;
如果开始位置超过了string的大小,则substr函数抛出一个out_of_range异常。如果开始位置加上计数值大于string的大小,则substr会调整计数值,只拷贝到string的末尾。例如:
string s(“hello world”);
string s2 = s.substr(12); //抛出一个out_of_range异常
string s3 = s.substr(0,5)l //s3 = hello
string s4 = s.substr(6); //s4= world
除了接收迭代器insert和erase版本外,string还提供了接受下标的版本。下标指出了开始删除的位置,或是insert到给定值之前的位置;
s.insert(s.size(),5,’!’); //在s末尾插入5个感叹号
s.erase(s.size() -5,5); //从s删除最后5个字符
s2.replace(11,3,”5th”); //从string s2的位置11开始,删除3个字符并插入”5th”
6.关联容器
#include <iostream>
#include <map>
using namespace std;
int main()
{
multimap<int, int> mMap;
mMap.insert(make_pair(1, 2));
mMap.insert(make_pair(2, 100));
mMap.insert(make_pair(3, 13));
mMap.insert(make_pair(3, 23));
mMap.insert(make_pair(4, 20));
mMap.insert(make_pair(4, 10));
mMap.insert(make_pair(5, 30));
mMap.insert(make_pair(3, 33));
for (auto iter = mMap.equal_range(3); iter.first != iter.second; ++ iter.first)
{
cout << “a: ” << iter.first->second << endl;
cout << “b: ” << iter.second->second << endl;
}
system(“pause”);
return 0;
}
输出 :
a: 13
b:20
a:23
b:20
a:23
b:20
【注】1>equal_range(n)返回的结果中的first元素是pair类型,它为关键字n的第一个值,second元素也是pair,它指向n下一个元素。2>lower_bound和upper_bound不适用于无序容器。 3>下标和at操作只适用于非const的map和unordered_map。
7.无序容器 —管理桶
无序容器在存储上组织为一组桶,每个桶保存零个或多个元素。无序容器适用一个哈希函数将元素映射到桶。为了访问一个元素,容器首先计算元素的哈希值,它指出应该搜索哪个桶。容器将具有一个特定哈希值的所有元素都保存在相同的桶中。如果容器允许重复关键字,所有具有相同关键字的元素也都会在同一个桶中。因此,无序容器的行性能依赖于哈希函数的质量和桶的数量和大小。
对于相同的参数,哈希函数必须总是产生相同的结果。理想情况下,哈希函数还能将每个特定的值映射到唯一的桶。但是,将不同关键字的元素映射到相同的桶也是允许的。当一个桶保存多个元素时,需要顺序搜索这些元素来查找我们想要的那个。计算一个元素的哈希值和在桶中搜索通常都是很快的操作。但是,如果一个桶中保存了很多元素,那么查找一个特定元素就需要大量比较操作。
8.动态内存
1>分配一个数组会得到一个元素类型的指针
虽然我们通常称new T[ ]分配的内存为“动态数组”,但这种叫法某种程度上有些误导。当用new分配一个数组时,我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。即使我们使用类型别名定义了一个数组类型,new也不会分配一个数组类型的对象。
由于分配的内存并不是一个数组类型,因此不能对动态数组调用begin或end。这些函数使用数组维度来返回指向首元素和尾后元素的指针。处于相同的原因,也不能用范围for语句来处理(所谓的)动态数组中的元素。
【注】要记住我们所说的动态数组并不是数组类型,这是很重要的。
2>初始化动态分配对象的数组
int *pia = new int[10]; //10个未初始化的int
int *pia2 = new int[10](); //10个值初始化为0的int
string *psa = new string[10]; //10个空string
string *psa2 = new string[10](); //10个空string
int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};
string *psa3 = new string[10]{“a”,”an”,”the”,string(3,’x’)}; //剩余的进行值初始化
与内置数组对象的列表初始化一样,初始化器会用来初始化动态数组中开始部分的元素。如果初始化器数目小于元素数组,剩余元素
将进行值初始化。如果初始化器数目大于元素数目,则new表达式失败,不会分配任何内存,new会抛出一个类型为bad_array_new_length
的异常,类似bad_alloc,此类型定义在头文件new中。
虽然我们用空括号对数组中元素进行值初始化,但不能再括号中给出初始化器,这意味着不能用auto分配数组。
3>动态分配一个空数组是合法的
可以用任意表达式来确定要分配的对象的数目:
size_t n = get_size(); //get_size返回需要的元素的数目
int *p = new int[n]; //分配数组保存元素
for(int *q = p ; q != p + n; ++q)
/*处理数组*/;
这产生了一个有意思的问题:如果get_size返回0,会发成什么?答案是代码仍能正常工作。虽然我们不能创建一个大小为0
的静态数组对象,但当n等于0时,调用new[n]是合法的:
char arr[0]; //错误:不能定义长度为0的数组
char *cp = new char[0]; //正确:但cp不能解引用
在我们假想的循环中,若get_size返回0,则n也是0,new会分配0个对象,for循环中的条件会失败。因此,循环体不会被执行。
4>释放动态数组
为了释放动态数组,我们使用一种特殊形式的delete—在指针前加上一个空方括号对:
delete p; //p必须是一个动态分配的对象或为空
delete [] pa; //pa必须指向一个动态分配的数组或为空
第二条语句销毁pa指向的数组中的元素,并释放对应的内存。数组中的元素按逆序销毁,即,最后一个元素首先被销毁,
然后是倒数第二个,依次类推。
9.拷贝控制
1>拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。
class Foo {
public:
Foo(); //默认构造函数
Foo(const Foo&); //拷贝构造函数
//…
};
拷贝构造函数的第一个参数必须是一个引用类型。虽然我们可以定义一个接受非const引用的拷贝构造函数,但此参数
几乎总是一个const的引用。拷贝构造函数在几种情况下都会被隐试地使用。因此,拷贝构造函数通常不应该是explicit的。
2>拷贝初始化
拷贝初始化不仅在我们用=定义变量时会发生,在下列情况下也会发生
<1>.将一个对象作为实参传递给一个非引用类型的参数。
<2>.从一个返回类型为非引用类型的函数返回一个对象。
<3>.用花括号列表初始化一个数组中的元素或一个聚合类中的成员。
某些类类型还会对它们所分配的对象使用拷贝初始化。例如,当我们初始化标准库容器或是调用其insert或
push成员时,容器会对其元素进行拷贝初始化。与之相对,用emplace成员创建的元素都进行直接初始化。
3>拷贝初始化的限制
如果我们使用的初始化值要求通过一个explicit的构造函数来进行类型转换,那么使用拷贝初始化就不是无关紧要的了:
vector<int> v1(10); //正确:直接初始化
vector<int> v2 = 10; //错误:接受大小参数的构造函数是explicit的
void f(vector<int>); //f的参数进行拷贝初始化
f(10); //错误:不能用一个explicit的构造函数拷贝一个实参
f(vector<int>(10)); //正确:从一个int直接构造一个临时vector。
4>拷贝赋值运算符
与类控制其对象如果初始化一样,类也可以控制其对象如何赋值:
Sales_data trans,accum;
trans = accum; //使用Sales_data的拷贝赋值运算符
与拷贝构造函数一样,如果类未定义自己的拷贝赋值运算符,编译器会为它合成一个。
拷贝赋值运算符接受一个与其所在类相同类型的参数:
class Foo {
public:
Foo& operator=(const Foo&); //赋值运算符
//…
};
5>析构函数不能是删除的成员
对于删除了析构函数的类型,虽然我们不能定义这种类型的变量或成员,但可以动态分配这种类型的对象。但是,
不能释放这些对象:
struct NoDtor {
NoDtor() = default; //使用合成默认构造函数
~NoDtor() = delete; //我们不能销毁NoDtor类型的对象
};
NoDtor nd; //错误:NoDtor的析构函数是删除的
NoDtor *p = new NoDtor(); //正确:但我们不能delete p
delete p; //错误:NoDtor的析构函数是删除的
【注】对于析构函数已删除的类型,不能定义该类型的变量或释放指向该类型动态分配对象的指针。
6>合成的拷贝控制成员可能是删除的
如果一个类有数据成员不能默认构造、拷贝、复制或销毁,则对应的成员函数将被定义为删除的。
7>private拷贝控制
希望阻止拷贝的类应该使用=delete来定义它们自己的拷贝构造函数和拷贝赋值运算符,而不应该将它们
声明为private的。
8>变量是左值
我们不能将一个右值引用绑定到一个右值引用类型的变量上。毕竟,变量是持久的,直至离开作用域时才被销毁。
【注】变量是左值,因此我们不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不行。
9>标准库move函数
虽然不能将一个右值直接绑定到一个左值上,但我们可以显示地将一个左值转换为对应的右值引用类型。我们还可以通过调用一个名为move的新标准库函数来获得绑定到左值上的右值引用,此函数定义在头文件utility中。
int &&rr3 = std::move(rr1); //ok
move调用告诉编译器:我们有一个左值,但我们希望像右值一样处理它。我们必须认识到,调用move就意味着承诺:除了对rr1赋值或销毁它外,我们将不再使用它。在调用move之后,我们不能对移后源对象的值做任何假设。
【注】我们可以销毁一个移后源对象,也可以赋予它新值,但不能使用一个移后源对象的值。
如前所述,与大多数标准库名字的使用不同,对move我们不提供using声明。我们直接调用std::move而不是move。
【注】使用move的代码应该使用std::move而不是move。这应做可以避免潜在的名字冲突。
10>移动操作、标准库容器和异常
在一个构造函数中,noexcept出现在参数列表和初始化列表开始的冒号之间:
class StrVec{
public:
StrVec(StrVec&&) noexcept; //移动构造函数
//其他成员的定义,如前
};
StrVec::StrVec(StrVec &&s) noexcept : /*成员初始化器*/
{ /*构造函数体*/ }
我们必须在类头文件的声明中和定义中(如果定义在类外的话)都指定noexcept。
【注】不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept。
11>移动赋值运算符
StrVec &StrVec::operator=(StrVec &&rhs) noexcept
{
//直接检测自赋值
if(this != &rhs){
free(); //释放已有元素
elements = rhs.elements; //从rhs接管资源
first_free = rhs.first_free;
cap = rhs.cap;
//将rhs置于可析构状态
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
return *this;
}
【注】移后源对象必须可析构
12>合成的移动操作
只有当一个类没有定义任何自己版本的拷贝控制成员,且它的所有数据成员都能移动构造或移动赋值时,编译器才会为它合成移动构造函数或移动赋值运算符。
如果一个类有一个可用的拷贝构造函数而没有移动构造函数,则其对象是通过拷贝构造函数来“移动“的。拷贝赋值运算符和移动赋值运算符的情况类似。
10.面向对象程序设计
1>当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分将被忽略掉。
2>要想理解在具有继承关系的类之间发生的类型转换,有三点非常重要:首先,从派生类向基类的类型转换只对指针或引用类型有效。其次,基类向派生类不存在隐式类型转换,使用dynamic_cast或static_cast来转换。最后,和任何其他成员一样,派生类向基类的类型转换也可能会由于访问受限而变得不可行。
3>当且仅当对通过指针或引用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同。
4>基类中的虚函数在派生类中隐含地也是一个虚函数。当派生类覆盖了某个虚函数时,该函数在基类中的形参必须与派生类中的形参严格匹配。
5>改变个别成员的可访问性
有时我们需要改变派生类继承的某个名字的访问级别,通过使用using声明可以达到这一目的:
class Base {
public:
std::size_t size() const { return n;}
protected:
std::size_t n;
};
class Derived : private Base { //注意:private继承
public:
//保持对象尺寸相关的成员的访问级别
using Base::size;
protected:
using Base::n;
};
【注】派生类只能为那些它可以访问的名字提供using声明。
6>虚析构函数将阻止合成移动操作
如果一个类定义了析构函数,即使它通过=default的形式使用了合成的版本,编译器也不会为这个类合成移动操作。
11.模板与泛型
1>模板类型别名
由于模板不是类型,我们不能定义一个typedef引用一个模板,即,无法定义一个typedef引用Blob<T>。
但是,新标准运行我们为类模板定义一个类型别名:
template<typename T> using twin = pair<T,T>;
twin<string> authors; //authors是一个pair<string,string>
2>模板参数与作用域
在模板内不能重用模板参数名:
typedef double A;
template <typename A,typename B> void f(A a, B b)
{
A tmp = a; //tmp的类型为模板参数A的类型,而非double
double B; //错误,重声明模板参数B
}
由于参数名不能重用,所以一个模板参数名在一个特定模板参数列表中只能出现一次:
//错误:非法重用模板参数名V
template <typename V,typename V> //…
3>默认模板实参
我们重写compare,默认使用标准库的less函数对象模板:
//compare有一个默认模板实参less<T>和一个默认函数实参F()
template <typename T,typename F = less<T>>
int compare(const T &v1,const T &v2,F f = F())
{
if(f(v1,v2)) return -1;
if(f(v2,v1)) return 1;
return 0;
}
调用compare函数时,可提供自己的比较操作,但这并不是必需的:
bool i = compare(0,42); //使用less; i为-1
//结果依赖于item1和item2中的isbn
Sales_data item1(cin),item2(cin);
bool j = compare(item1,item2,compareIsbn);
4>模板默认实参于类模板
无论何时使用一个模板,我们都必须在模板名之后接上尖括号。尖括号指出类必须从一个模板实例化而来。特别是,如果一个类模板为其所有模板参数都提供了默认实参,且我们希望使用这些默认实参,就必须在模板名之后跟一个空尖括号对:
template <class T = int> class Numbers { //T默认为int
public:
Numbers(T v = 0) : val(v){}
//对数值的各种操作
private:
T val;
};
Numbers<long double> lots_of_precision;
Numbers<> average_precision; //空<>表示我们希望使用默认类型
5>尾置返回类型与类型转换
当我们希望用户确定返回类型时,用显示模板实参表示模板函数的返回类型是很有效的。但在其他情况下,要求显示指定模板实参会给用户增添额外负担,而且不会带来什么好处。例如,我们可能希望编写一个函数,接受表示序列的一对迭代器和返回序列中一个元素的引用:
template <typename T>
??? &fcn(It beg,It end)
{
//处理序列
return *beg; //返回序列中一个元素的引用
我们并不知道返回结果的准确类型,但知道所需类型是所处理的序列的元素类型:
vector<int> vi = { 1,2,3,4,5 };
Blob<string> ca = {“hi”,”bye”};
auto &i = fcn(vi.begin(),vi.end()); //fcn应该返回int&
auto &s = fcn(ca.begin(),ca.end()); //fcn应该返回string&
}
此例中,我们知道函数应该返回*beg,而且知道我们可以用decltype(*beg)来获取此表达式的类型。但是,在编译器遇到函数的参数列表之前,beg都是不存在的,为了定义此函数,我们必须使用尾置返回类型。由于尾置返回出现在参数列表之后,它可以使用函数的参数:
//尾置返回允许在参数列表之后声明返回类型
template <typename It>
auto fcn<It beg,It end> -> decltype(*beg)
{
//处理序列
return *beg;
}
12.tuple类型
1>定义和初始化tuple
当我们定义一个tuple时,需要指出每个成员的类型:
tuple<size_t,size_t,size_t> threeD; //三个成员都设置为0
tuple<string,vector<doble>,int,list<int>> someVal(“contants”,{3.14,2.245},42,{0,1,2,3,4,5});
当我们创建一个tuple对象时,可以使用tuple的默认构造函数,它会对每个成员进行值初始化;也可以像本例中初始化someVal一样,为每个成员提供一个初始值。tuple的这个构造函数是explicit的,因此我们必须使用直接初始化语法:
tuple<size_t,size_t,size_t> threeD = {1,2,3}; //错误
tuple<size_t,size_t,size_t> threeD{1,2,3}; //正确
类似make_pair函数,标准库定义了make_tuple函数,我们还可以用它来生成tuple对象:
//表示书店交易记录的tuple,包含:ISBN、数量和每册书的价格
auto item = make_tuple(“0-999-78345-X”,3,20.00);
类似make_pair,make_tuple函数使用初始值的类型来推断tuple的类型。在本例中,item是一个tuple,类型为tuple<const char*,int,double>。
2>访问tuple的成员
一个pair总是有两个成员,这样,标准库就可以为它们命名(如,first和second)。但这种命名方式对tuple是不可能的,因为一个tuple类型的成员数目是没有限制的。因此,tuple的成员都是未命名的。要访问一个tuple的成员,就要使用一个名为get的标准库函数模板。为了使用get,我们必须制定一个显示模板实参,它指出我们想要访问第几个成员。我们传递给get一个tuple对象,它返回指定成员的引用:
auto book = get<0>(item); //返回item的第一个成员
auto cnt = get<1>(item); //返回item的第二个成员
auto price = get<2>(item) /cnt; //返回item的最后一个成员
get<2>(item) *= 0.8; //打折20%
尖括号中的值必须是一个整型常量表达式。与往常一样,我们从0开始计数,意味着get<0>是第一个成员。
如果不知道一个tuple准确的类型细节信息,可以用两个辅助类模板类查询tuple成员的数量和类型:
typedef decltype(item) trans; //trans是item的类型
//返回trans类型对象中成员的数量
size_t sz = tuple_size<trans>::value; //返回3
//cnt的类型与item中第二个成员相同
tuple_element<1,trans>::type cnt = get<1>(item); //cnt是一个int
为了使用tuple_size或tuple_element,我们需要知道一个tuple对象的类型。与往常一样,确定一个对象的类型的最简单方法就是使用decltype。在本例中,我们使用decltype来为item类型定义一个类型别名,用它来实例化两个模板。
tuple_size有一个名为value的public static数据成员,它表示给定tuple中成员的数量。tuple_element模板除了一个tuple类型外,还接受一个索引值。它有一个名为type的public类型成员,表示给定tuple类型中指定成员的类型。类似get,tuple_element所使用的索引也是从0开始计数的。
3>关系和相等运算符
tuple的关系和相等运算符的行为类似容器的对应操作。这些运算符逐对比较左侧tuple和右侧tuple的成员。只有两个tuple具有相同数量的成员时,我们才可以比较它们。而且,为了使用tuple的相等或不等运算符,对每对成员使用==运算符必须都是合法的;为了使用关系运算符,对每对成员使用<必须都是合法的。例如:
tuple<string,string> duo(“1″,”2”);
tuple<size_t,size_t> twoD(1,2);
bool b = (duo == twoD); //错误:不能比较size_t和string
tuple<size_t,size_t,size_t> threeD(1,.2,3);
b = (twoD < threeD); //错误:成员数量不同
tuple<size_t,size_t> origin(0,0);
b = (origin < twoD); //正确:b为ture
【注】由于tuple定义了<和==运算符,我们可以将tuple序列传递给算法,并且可以在无序容器中将tuple作为关键字类型。
13.枚举类型
1>枚举成员
在限定作用域的枚举类型中,枚举成员的名字遵循常规的作用域准则,并且在枚举类型作用域外是不可访问的。与之相反,在不限定作用域的枚举类型中,枚举成员的作用域枚举类型本身的作用域相同:
enum color {red,yellow,green}; //不限定作用域的枚举类型
enum stoplight{red,yellow,green}; //错误:重复定义了枚举成员
enum class peppers { red,yellow,green}; //正确:枚举成员被隐藏了
color eyes = green; //正确:不限定作用域的枚举类型的枚举成员位于有效的作用域中
peppers p = green; //错误:peppers的枚举成员不在有效的作用域中,color::green在有效的作用域中,但是类型错误
color hair = color::red; //正确,允许显示地访问枚举成员
peppers p2 = peppers::red; //正确:使用pappers的red
默认情况下,枚举值从0开始,依次加1。不过我们也能为一个或几个枚举成员指定专门的值:
enum class intTypes {
charTyp = 8,shortTyp = 16,intTyp = 16,
long Typ = 32,long_longTyp=64
};
由枚举成员intTyp和shortTyp可知,枚举值不一定唯一。如果我们没有显示地提供初始值,则当前枚举成员的值等于之前枚举成员的值加1.
枚举成员时const,因此在初始化枚举成员时提供的初始值必须是常量表达式。也就是说,每个枚举成员本身就是一条常量表达式,我们可以在任何需要常量表达式的地方使用枚举成员。例如:我们可以定义枚举类型的constexpr变量:
constexpr intTypes charbits = intTypes::charTyp;
类似的,我们也可以将一个enum作为switch语句的表达式,而将枚举值作为case标签。处于同样的原因,我们还能降枚举类型作为一个非类型模板形参使用;或者在类的定义中初始化枚举类型的静态数据成员。
14.const指针和constexpr指针的区别
const int *p = nullptr; //p是一个指向整型常量的指针
constexpr int *q = nullptr; //q是一个指向整数的常量指针
15.typedef指针
typedef char *pstring;
const pstring cstr = 0; //cstr是指向char的常量指针
const pstring *ps; //ps是一个指针,它的对象是指向char的常量指针