1、新增算术类型
longlong,最小不比long小,一般为64位。
2、列表初始化
int units_sold = {0};或者 int units_sold{0};非11标准下的C++中,只有特定的情况下才能使用该形式。 比如数组的初始化,类构造函数的初始化,结构体的填充。相比传统的赋值初始化,如果右侧的数值类型相对于左侧类型更大的话,侧对于这种窄化现象,编译器会 报错。如:int k = {3.14};一个double是8个字节,int一般是4个字节,这时编译器就会报错。
在容器中,也支持这种方法,比如std::map容器,比如我们定义一个std::map<string,int> m_map,以前我们需要对它进行插入,一般做法是 m_map.insert(value_type("test",1));,这样用一个函数来生成pare<string,int>再进行插入,但现在有种更方便的方法,让我们能在初始化时进行赋值,像下面这样:
map<string,int> m_map = {{"test",1},{"test2",2}};
每一组值都用{}括起来,跟列表初始化使用上是一致的。
3、列表初始化返回值
如果我们有一个函数,它要求我们返回一个vector<string>类型的对象,那么我们或者是这样写的:
vector<string> test()
{
vector<string> temp("1","2","3");
return temp;
}
函数体内定义一个临时的vector变量,返回时直接返回临时变量(PS:别说什么生命周期,这不是引用,这是值复制)。而C++11定义了列表初始化返回值:
return {}
上面的函数可以写成如下方式:
vector<string> test()
{
return{"1","2","3"};
}
这样直接返回一个初始化列表,用于初始化vector变量,这样同样会生成临时变量,但却可以减少代码量。
4、商一律向0取整(即直接切除小数部分)
21 % -5;
/* 结果是 1 */
运算符%俗称"取余"或"取模"运算符,负责计算两个整数相除所得的余数,参与取余运算的运算对象必须是整数类型:
- int ival = 42;
- double dval = 3.14;
- ival % 12; // 正确:结果是6
- ival % dval; // 错误:运算对象是浮点类型
在除法运算中,如果两个运算对象的符号相同则商为正(如果不为0的话),否则商为负。C++语言的早期版本允许结果为负值的商向上或向下取整,C++11新标准则规定商一律向0取整(即直接切除小数部分)。
根据取余运算的定义,如果m和n是整数且n非0,则表达式(m/n)*n+m%n的求值结果与m相等(这里的又乘又除的,无非是去掉小数部分)。隐含的意思是,如果m%n不等于0,则它的符号和m相同。C++语言的早期版本允许m%n的符号匹配n的符号,而且商向负无穷一侧取整,这一方式在新标准中已经被禁止使用了。除了 m导致溢出的特殊情况,其他时候(-m)/n和m/(-n)都等于-(m/n),m%(-n)等于m%n,(-m)%n等于-(m%n)。具体示例如下:
- 21 % 6; /* 结果是3 */ 21 / 6; /* 结果是3 */
- 21 % 7; /* 结果是0 */ 21 / 7; /* 结果是3 */
- -21 % -8; /* 结果是-5 */ -21 / -8; /* 结果是2 */
- 21 % -5; /* 结果是1 */ 21 / -5; /* 结果是-4 */
5、nullptr常量
其实这个常量跟NULL一样,都是0值,只是NULL是预处理变量,在C++11中,应该尽量减少NULL的使用。宏定义往往会导致非程序员本身需要的目 的(大部分是运算符优先级问题,比如#define duplicate(x) x*x,如果你是想算两个x+1后的结果再相乘,如果你写成duplicate(x+1),编译器展开后会是 x+1*x+1,这样明显与原来的意图完全不同),而且,排除稍困难,一般来说,都会尽量避免使用。
6、constexpr
constexpr可以说是对const变量的扩展,它只是约束const变量的右侧的式子必须能在编译时就能算出值。要理解这点,我们首先要搞清楚 const和constexpr之间的同异处.首先,无论是constexpr或者const变量,值都是定义后不能改变的,而且,它们等号右侧同样都可 以为一个表达式;两者的一个很重要的区别是const常量能在运行时获得值,而constexpr却是带有约束性质的,它约束右侧的式子只有在编译时就能 算出值,如果不能的话,编译器侧会报错。比如:const int max = 20; const int limit = max + 1;它们都是一个const变量同时,右侧也是一个常量表达式。但const int sz = str.size();它是一个在运行时取得具体值的const变量,但在编译时却无法取得具体值,所以右侧的表达式明显不是一个常量表达式。往往程序员 希望某一变量必须是常量以防止在运行过程中产生非意料之外的值(该过程大都由粗心导致的疏忽),所以C++11中才会加入一个新的constexpr关键 字,这样的话,当定义constexpr int sz = str.size();时,就会由编译器发出提示,而避免上述情况的发生。
7、constexpr函数
constexpr函数是指能用于常量表达式的函数。比如上面的如果constepr int size = size() + 1;成立的话,size(int max)就是一个constexpr函数,它定义或者如果下:
int size() { return 30;}
对于constexpr函数,有以下几点需要注意:
1)constexpr只能有一个return;
2)因为需要在编译时展开constexpr函数,所以constexpr函数默认为inline函数。
3)主要语句不会在运行时被执行(注意,是运行时不会被执行,别想着a = 1;这种也算,这是运行时才被执行的语句,是无法通过的,说到底,只有第4点的语句才会合法),constexpr函数体内可包含任意数量语句。
4)constexpr函数体内可以包括空语句,类型别名和using声明,主要是编译能执行的语句都可以。
本质上,constexpr函数跟constexpr表达式一样,只是用于编译时进行常量检测。
8、constexpr构造函数
在说这个前,我们应该要注意的重要的一点是:constexpr构造函数无论名称怎么变,但它仍然需要遵守constexpr函数的规则。这些规则总结为以下几点:
1)constexpr构造函数可以声明为=default或者删除函数的形式。前者相当于编译器合成的默认构造函数,一般情况下几乎不执行任务初始化工作,所以不会与constexpr函数的约束冲突;后者相当于屏蔽类的某个功能,现在先不用管,后面会提到。
2)对于第1种情况外的constexpr构造函数,constexpr的限制就比较多了:它既要满足构造函数的限制条件,也必须得满足自身的限制条件。前者让它不能有返回语句(构造函数是没有返回值的),后者限制了函数体内的内容必须是能在编译时确定的,也即是说,除了返回语句外其它语句都不成立(当然还可以有类型别名之类的语句,但这些语句真的想不出有什么必要声明在一个构造函数中),所以通常它的函数体都是空的。
3)constexpr构造函数必须初始化所有数据成员,而且初始值只能是常量,比如一个constexpr函数的委托构造函数的形式或者一条常量表达式。
// 如C++ Primer中的例子
class Debug
{
public:
constexpr Debug(bool b = true):hw(b),io(b),other(b){}
constexpr Debug(bool n,bool i,bool o):hw(h),io(i).other(0){}
private:
bool hw,io,other;
}
可以看出,初始化列表中的变量都是常量,true和false都是常量表达式。如果是int类型,如果赋值是一个字面常量也是可以的,初始化列表中的右值主要是常量表达式都是正确的。
9、类型别名
类型别名是C和C++一样存在的,以前的typedef的用法为typedef typeA typeB,把typeA定义为typeB的一个别名,这种用法,更像是宏定义的用法,并没有等号表达式直观;C++11中新增一种定义类型别名的方法, 该方法更符合我们平时的使用习惯:using typeA = typeB;这种方法在平时可能并无法表现多大的优越性,但如果用于函数指针时,却是很直观的。比如有如下函数 int add(int a,int b);如果定义指针,以前的做法是typedef int (add*)(int a,int b);这种语法,对于新手来说,是很难理解的,如果再复杂点,返回值还是一个函数指针的话,那更难以理解了。新手一种会按我们上面说的那种基本语法去理 解,typedef typeA typeB,一个函数指针,很难找出那部分是对应typeA,那部分是对应typeB,事实上,typeA就是add,它可以是任何名称,只是我这里刚好 与函数名是一样而已,如果写作typedef int (addptr*)(int a,int b);你就会知道,定义一个函数指针与函数名完全无关,而只是与返回类型和参数类型有关(甚至可以没有参数名称:typedef int (add*)(int,int);),但无论怎么说,这种方法仍然是不太直观的,现在C++11中可以 这样用了。
using addptr = int (int,int);它很明确指出,addptr就是一个函数指针,它指向的函数类型为:返回值是int,并带有两个int形参的所有函数。
10、auto类型
auto是C++11中新增加的特性,对于我们平时编写代码,我建议是少用应该尽量少用,只有在写代码时知道我每步会产生的结果时才会写出健壮的程序,仅 auto来为我们判断类型,事实与我们静态语言的原意相悖了;auto更多应用于泛型编程中,它能减少你编写特化模板的工作量。对于auto,我们只需要 记住以下几点:
1)auto是编译时得到的类型,也就是说,它是让编译器替我们分析表达式所属的类型的,所以有时候auto产生的值,如果对它没有足够的理解,你会被弄糊涂。
2)auto只能推断一种类型,比如当一条语句中声明多个变量时,如果变量类型不同,是会产生错误的:auto sz = 0,pi = 3.14;一个为int型,一个为double型,这时auto是不能正常工作的。
3) 当右值是一个引用类型时,auto的类型不是一个引用类型,而是引用类型的值。比如double &PI = pi;auto ref = PI;ref的类型不是double&而是跟pi的类型一样,为一个double类型。当我们确实需要一个引用类型时,我们可以写成这样auto &ref = PI;
4)说这点前,先说明下C++ Primer中提到的顶层const和底层const的概念。当一个const变量自身无法改变时,我们称为顶层const,当一个const变量本身可 改变,但关联的变量无法改变时我们称为底层const。也即是顶层也就是表面,底层也就是表面掩住的内容。如果:const double pi = 3.14;double *const ptr = π(注意,不要写成double const *ptr,这个是跟const double* ptr一样的), 这 2个都是顶层const,他们本身的值无法改变;const double *ptr2 = π;const double PI = π这2个都是底层const,事实上,指向常量的指针和声明引用的const都是底层const; auto会忽略掉顶层const的性质,但保留底层const;比如auto x = pi;这时x的类型为double而非const double,同样地,当auto x = ptr时,x的类型为double*;而auto x = ptr2,此时的x为const double*类型,它说明指向的值不可改变。如果我们想保留顶层const,只能手动写上符号,如auto const x = π
总的来说,auto能判断内置类型,能判断指针类型,但却无法正确判断出引用类型和顶层const类型,对于顶层const类型和引用类型(所有的const引用类型都是顶层const类型)只能通过手动增加的方法保留特性。
5)由编译器确定动态分配数据的类型,C++11的auto在new中也有新的定义,之前我们必须确实地指定new类型和指针类型才能正确地在堆上分配内存空间,现在,C++11允许由编译器推断分配类型,用法如下:
auto p1 = new auto(std::string); // p1为指向string类型的指针
11、decltype
我觉得C++11新增的这个特性,比auto更奇葩,要理解这个东西,说真的,难度不是一般的大,说不得,真想深入研究,你应该重新翻翻编译原理。以下几点,是我们必须注意的:
1)decltype是推断类型的,它允许变量和表达式,但无论是那一种,它都忽略值而只关注类型的,比如表达式,并不会调用表达式而只是推断当表达式被调用时,会返回一个什么样的类型。
2)decltype 在处理顶层const跟auto不同,它返回的类型是包括顶层const的,也就是说当double *const ptr = π时auto x = ptr;类型是double* ,但如果用decltype(ptr)得到类型却是完整的double *const类型。对于引用,也是一样的。但引用必须注意,如果const int &x = pi;decltype(x)后得是必须要初始化的,因为引用类型必须定义的同时初始化。
3)注意这种情况:decltype(x + 0)——这里我们假定x是int&类型,引用类型进行decltype是作为一条赋值语句的左值的。最后的结果应该是int类型。
4) 这点是最重要的:加不加括号,我们得到的类型可能是会不同的。比如int i = 0;当我们调用decltype(i)时,这很容易理解,得到的类型肯定是int类型;但如果我们加上一重或多重括号如:decltype((i)),我 们得到的是int&类型。事实上,变量也是可以作为一种赋值语句左值的特殊表达式,从C++的初始化就可以看出,比如int i(0);,可以看出,这是一个函数调用的形式,事实上,这个比较难理解,我也不明白,而这也仅仅是一个数学到的概念而已,a+b = 0是代数式,那a和b都是代数式。反正decltype是C++11新增的机制。
5) decltype可用于返回类型:比如下面这段代码:(选自C++ primer)
int odd[] = {1,3,5,7,9};
int even[] = {0,2,,4,,6,8};
// 返回一个指针,该指针指向含有5个整数的数组
decltype(odd) * arrPtr(int i)
{
return (i%2) ? &odd:&even; // 返回一个指向数组的指针
}
这里是特别需要注意的,函数的返回的是数组的地址,但decltype(odd)得到的类型却是一个含有5个元素的数组,它不会帮我们把数组转换成指针类型,所以还要在addPtr前加上*表示返回类型的是一个指向含有5个元素的数组的指针。
主要是我们记住,如果加了括号,declstype的类型永远是引用。最后总结是,这样的:
如果e是一个没有外层括弧的标识符表达式或者类成员访问表达式,那么decltype(e)就是e所命名的实体的类型。
如果e是一个函数调用或者一个重载操作符调用(忽略e的外层括弧),那么decltype(e)就是该函数的返回类型。
否则,假设e的类型是T:若e是一个左值,则decltype(e)就是T&;若e是一个右值,则decltype(e)就是T。
12、范围for
看C#,JAVA,都有for...each语法,javaScript有for...in语法,OC也有自己的迭代遍历语法,总的来说,对于特定的数据 的遍历,这些方法确实足够简单,但说真的,这些方法方便的地方在于能动态判断类型,但我个人认为,这种范围for对于C++来说,作用真不算强大,因为 C++不像C#这些一样,一个数组内能混合不同的类型,C#的同一个数组内能加入基本类型,也能加入类类型,但C++只能加入特定一种类型,所以,每个数 组都是特定类型,用auto其实作用不大,而传统的for语句,跟使用范围for相比,并没有多大的不方便吧。
C++11新加入for(auto value:data);
for
(auto c : str)
//对于str中的每个字符
cout<<c<<endl;
// 输出当前字符,后面紧跟一个换行符
13、cbegin()和cend()【容器】
传统的C++中,标准库中的所有容器都有begin()和end()函数,分别取得容器的首元素和最后元素的下一次位置。但以上两个函数,返回的都是 iterator类型。而新增加的cbegin()和cend()函数,返回的是const_iterator类型。
14、beign()和end()【标准库】
比如有如下数组:
int arr[] = {1,2,3,4,5,.........};
传统C++要遍历会作出如下操作:
for(int i = 0; i != sizeof(arr) / sizeof(arr[0]); ++i){};
或者
for(int *p = arr[0]; p != arr[0] + sizeof(arr) / sizeof(arr[0]; ++i){};
前者在某种程度上是安全的,但对于后者,要取得有效数据的下一个位置时,却是极易出错的。所以,为了让指针操作更安全,C++11提供了begin()和end()函数,分别指向数据的起始和结束位置的下一位置【他们都是指针类型】。我们现在可以更安全地遍历数组等数据了:
for(int *p = begin(arr); p != end(arr); ++p){};
甚 至,这两个函数也提供了跟迭代器一样的操作,退两个迭指针相减的结果就是它们之间的与数据类型相关的距离,比如,arr如果有5个元素,则auto diff = end(arr) - begin(arr),diff的值为5,而且,diff的类型是标准库类型ptrdiff_t,这是一种带符号的类型,因为差值可能会为负数。
15、initializer_list类
传统的可变参数一般是用va_list的一组宏来解决的。现在,C++11新增了模板类initializer_list,在便利性上得到提升。它跟 va_list最大区别是,va_list中的va_arg的参数可以为任意类型,但 initilizer_list只允许未知的形参使用相同的类型。
对于initializer_list,一个最重要的概念是,initializer_list内所有的元素都是常量,这与vector这些容器是不一样的。
initializer_list因为有一种构造方法为initializer_list<T> lst{a,b,c.....};使得它可以实现可变参数(我知道,vector,list一样可以这样做,但问题是,vector和list总是一开始 就分配内存空间的,相比较下,initializer_list是轻量级的。PS:这货不支持下标操作的,只能用指针,或者在标准库中叫迭代器更好点),大概方法如下:
void CoutList(initializer_list<string> il)
{
// 一般为了方便,我们不清楚il.begin()的类弄,可以 用auto代替,但事实上, begin返回的是一个以模板实例相同 的const类型。
for(auto beg = il.begin(); beg != il.end(); ++beg)
{
cout << *beg << endl;
}
调 用时可以这样,CoutList({"1","2","3"});,相较传统的va_list宏而言,这种方法虽然简结了,但对于传统的C++程序员来 说,多了个花括号,可能已经不算正规的函数调用了。不过,它为我们提供了一种方法,如果上面的函数,用va_list是无法达到的,原因就 是,va_list必须至少带一个参数,以作为va_start的起始点,上面的函数,我们可以增加一个表示传入参数数量的形参,可以可以这样写:
void CountList(int nNumber,...)
{
va_list lt; //定义一个 va_list 参数表
va_start(lt,nNumber); // 初始化va_list,并让其指针指向参数表中的第一个参数
// 因为是栈,所以栈底就是第一个参数,+1是从栈顶向栈低取值,用第一个参数判断循环结束是可以的
for(int i = 0; i != nNumber; ++i)
{
// 取得参数表中的第i个参数,总设置string类型,此时lt会自动指向当前位置的下一位置(ap是先移动一个位置再返回值的。)
string tmp = va_arg(lt,string);
cout << tmp << endl;
}
va_end(lt); // 用完清空是好习惯
}
调用时可以用如下方法:CoutList(3,"1","2","3");
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) // 这个是字节对齐,自己参考文档。(http://yangtaixiao.blog.163.com/blog/static/42235441201441233325695/)
#define _crt_va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
#define _crt_va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define _crt_va_end(ap) ( ap = (va_list)0 )
但总体来说,因为initializer_list是轻量的,所以使用它应该更方便一点。
16、尾置返回类型
想像一下,当你要定义一个函数,当你需要一个函数,它接受一个int类型参数,并返回一个指向含有10个int的数组,你应该怎么写?我就犯过傻,聪明地 直接返回数组,接收时却也是用数组,但这是错误的,数组是不能拷贝的,明显是不能直接返回数组,只能返回数组的指针或者引用,而且,一般来说,返回指针, 在语法上会更烦琐点。要写一个返回指向10个int数组的指针,一般会用如下方法:
1)定义别名
typedef int arrT[10]; 或者用新标准 using arrT = int[10];
//定义函数
arrT* func(int);
2) 不定义别名
int (*func(int i))[10];
这个函数的语法本身就让人难以理解,所以为了解决这种复杂的返回类型(我觉得C和C++之所以难学,这语法的复杂度也是其实一个因素了),C++新增了尾置返回类型,就是把返回类型提后,我们只需要把返回类型清晰地标明就可以了,比如func就能写成这样:
auto func(int i)->int(*)[10];
另外,尾置法有一个特别的地方在于:普通的函数,函数的返回值是会比函数的参数先确定类型的,但尾置法却是在确认函数参数后再确认返回类型的。
17、default生成默认构造函数
我们都清楚,如果我们自己本身没有对类定义构造函数,编译器会为我们合成一个构造函数。但事实上,这个简单的构造函数,在定义时可能根本不会调用,更是别 指望它会帮你初始化成员变量(自己试下就知道了,除了全局的内置变量,C++任务位置的变量,都不会自动帮你初始化)。而当我们自己定义一个构造函数后, 编译器就不会再为我们合成默认构造函数了,如果我们在需要自定义的构造函数时,却仍然希望编译器为我们合成默认构造函数,我们就需要用到新标准中的 default关键字了。用法如下:
class A
{
public:
void A(int a):iNumber(a){}
A() = default; // 当在类内部定义时,跟合成的默认构造函数一样,它是inline的
A(){} // 当我们存在合成默认构造函数时,我们手动定义就会提示重定义了,这个应该都清楚
}
类外定义
class A
{
public:
A() ;
}
A::A() = default; // 类外定义时,该合成默念构造函数就不再是inline的了。
18、类内初始值
我会告诉你我很讨厌这个吗?我是感觉这个完全破坏了构造函数存在的意义。
C++11中,允许通过以符号=或者花括号为类内一个变量提供类内初始值。如:
class B
{
private:
int Size = 0;
std::vector<int> vt{1,2,3};
};
当然,我们也可以在构造函数中进行初始化,只是这样或者更直观一点。
19、委托构造函数
看到这个名字时,我希望大家不要跟我一样直接想到IOS的委托和C#的委托,其实根本不是这么一回事。C++11新扩展的这个函数功能是使委托构造函数可以执行它所属类的其它构造函数。
具体方法如下:
class A
{
public:
A(int size):iSize(size){}
A():A(30)
{
area = 90;
}
private:
int iSize;
double area;
};
委托构造函数在语法上类似派生类调用基类构造函数,而且,同样地,是先执行被委托的构造函数,再执行自身括号内的内容。
20、文件流对象的文件名支持标准库string
旧版本的标准库中的fstream只支持C风格字符数组,如果我们用标准库中的string还得string.c_str()转换一下,但现在C++11的fstream已经允许直接使用string字符串了。
21、标准库新增forward_list和array类型
forward_list是单向链表,在链表的任何位置插入/删除数据速度都很快。
array行为跟内置数组一样,大小也是固定的,也不支持添加/删除或者改变容量大小的操作。
22、容器列表初始化【容器】
C++11支持以花括号形式的列表初始化方法,同时,标准库几乎所有的容器都能享受此种新特性。
如:vector<int> vt("1","2","3");旧标准只能用这种方式函数构造函数进行初始化。而C++11中能使用如下方式:
vector<int> vt = {"1","2","3"};
这种方式的初始化,其实也不过是调用vector的赋值构造构造函数。
23、提供了swap函数的非成员版本【标准库】
旧的C++的标准库版本只提供成员函数版本的swap,而现在也提供了非成员版本的swap函数。它在泛型中使用会更方便。(不太了解泛型,也不明白这个……)
24、改进的inser操作【容器】
新标准中,insert数据后,它会返回一个指向第一个新增加元素的迭代器,不同于以往,它是返回一个void值。
25、emplace_front、emplace和emplace_back 【容器】
C++11新增加的这个新成员,它允许我们通过提供数据的值来直接初始化容器内的数据。比如我们有如下一个类:
class em
{
public:
em(string name,int age,bool sex)
{
m_name = name;
m_age = age;
m_sex = sex;
}
private:
string m_name;
int m_age;
bool m_sex;
};
如果我们用一个vector装载它,如果我们要在后面插入一个新成员,我们一般做法是这样:
vector<em> vt;
vt.pushback(em("stupid",30,0));
这样做首先是构造一个临时的局部em对象,然后再通过拷贝的方式把这个临时对象拷贝到容器内。但如果我们使用新的emplace函数,我们可以这样做:
vt.emplace("stupid",30,0);
这样做的结果是直接在容器内的内存空间中创建对象,而不必要浪费再构建临时变量和进行拷贝了。
很明显,emplace比传统的插入函数具有更高的效率。
26、shrink_to_fit【容器】
C++11标准为有预分配内存功能的容器(vector,deque,string)提供一个名为shrink_to_fit的函数,他能将容器的 capacity()减少为与size()相同大小,即让容器把多出来的内存空间释放,但该函数并不一定保证能100%释放这些多余的空间。
27、string数值转换【标准库】
旧版C++中,要把一个数值字符转化成字符串,我们只能使用itoa()的C函数或者sprintf()等诸如此类要手动事先分配内存的方法。但 C++11标准为string提供了一组函数(也是定义在string头文件内),使得能把几乎所有的数值类型转化为string,而不需要我们事先手动 分配内存空间。同样地,也定义了一组从string转换为数个类型的函数。如:
int i = 30;
string s = to_string(i); //数值转为string,to_string()是一个重载函数
double pi = stod(s); // 把字符串"30"转化为一个double值
如果string不能转换为一个数值,抛出invalid_argument异常;如果得到的数值无法用任何类型来表示,则抛出一个out_of_range类型。
函数 | 说明 |
to_string | 这个函数是重载函数,能满足所有内置类型,而且,有相对应的宽字节to_wstring |
stoi(s,p,b) | p 表示一个size_t*的指针类型,默认为空值。当它不为空,转换成功时就表示第一个为非数值字符的下标,一般情况下,因为它是直接char型指针,通过 把最后非数值字符的地址值和起始地址值相减的,所以也即表示了成功转换的字符的数量,如"10"转成功为数值10时,这个值为2。b表示转换基准,这个跟 atoi一样,默认是10,表示10进制。s则重载了string和wstring版本。 |
stol(s,p,b) | 同上 |
stoul(s,p,b) | 同上 |
stoll(s,p,b) | 同上 |
stoull(s,p,b) | 同上 |
stof(s,p) | p的含义同上 |
stod(s,p) | p的含义同上 |
stold(s,p) | p的含义同上 |
28、lambda
C++中的闭包命名为lambda,或者匿名函数,他遵守闭包的特性,子函数可以使用父函数中的局部变量。
我们要记住一点,lambda是一个类,而且,没有带合成默认构造函数的类,在我们定义时,编译器会为我们自动生成这样一个类。
它格式如下:[capture list](parameter list)->return type {function body}
[捕获列表](形参列表)->返回类型 {函数体}
首先,这种格式不是固定的,其中某些项能省略,或者隐式调用,但无论如何,捕获列表和函数体是必须要永远包含的,那怕捕获列表和函数体为空。lambda在函数体内或函数体外都能定义。例如:
auto fun = []{return 0;}; // 不要忘记最后的分号
为了说明lambda的本质,有必要把某些内容提前进行说明。
lambda的本质,我们可以这样理解:编译器以lambda表达式为基础生成一个未命名的类。该类中的成员数据就是捕获列表中罗列的所有数据。但lambda表达式产生的类,却与传统的类有点细致区别,主要表现为:
1)lambda产生的类不含默认构造函数、赋值运算符及默认析构函数。
2)lambda产生的类含有默认拷贝构造函数、默认移动构造函数。当然,这得视捕获列表中的变量的数据类型决定,一般地,一个类如果没有定义自身的拷贝 构造函数,而且,本身的非static成员变量都能移动的话,就会合成默认移动构造函数,反之,则合成默认拷贝构造函数。
比如有这样一个lambda表达式:[sz](const string&s) {return s.size() >= sz;};如果sz为size_t类型,则编译器为我们生成类似这样的一个未知名的类(别以为就是这样,这只是类似而已,并非就是一模一样,这点要清楚 的):
class unnamed
{
public:
// 它不会合成默认构造函数,但却会根据捕获列表生成相应的构造函数,用以初始化成员变量
unnamed(size_t data):sz(data){}
// 它会根据lambda表达式的函数体的return语句推断返回类型,并重载调用运算符,值捕获时一般都是const成员函数,这是lambda所要求的——值捕获到的值一般不会改变基值。
bool operator() (const string&s) const // 因为形参是const类型,所以成员函数也声明为const成员函数
{
return s.size() >= sz;
}
private:
size_t sz;
}
现在我们应该已经理解了,lambda就是一个未命名的类,它重载了调用运算符。
下面分步来解释lambda的几个要点:
[形参列表]:
1)如果没有提供形参表,就表示指定一个空参数列表;lambda的形参不能带有默认值。
[捕获列表]:
1)如果捕获列表为空,则表示不捕获任何局部变量。如果没有提供返回类型,但lambda能根据函数体的代码来推断返回类型:1.如果有return,则根据return后的表达式来推断;2.如果没有return就是void。
2)如果捕获列表为空,则我们不能在函数体内定义对局部变量进行任何操作。下面这样会报错:
int GetBiger(double first, double second)
{
double result = (first > second) ? first : second;
auto func = []{return result; };
return func();
}
当然,上面的函数完全是没有意义的,但这个是演示,我们在lambda里使用了GetBiger的名叫result的临时变量,但却没有在捕获列表里列出,所以这是非法的,编译器会报错;如果我们确实想使用,就应该把局部变量放于捕获列表中:
int GetBiger(double first, double second)
{
double result = (first > second) ? first : second;
auto func = [result]{return result; };
return func();
}
这样就没有问题了。
[值捕获]:
lambda如果采用值捕获(即不带引用符号)时,与传值参数行为一致,即由lambda表达式生成的新类型(下面会说,lambda本质是一个未命名的 类类型)中的成员数据在构造时会拷贝父函数中局部临时变量的一个副本,在这种情况下,更改lambda内的值不会影响父函数变量的值,同样地,更改父函数 局部变量的值也不会影响lambda内的捕获值。如:
void func()
{
size_t v1 = 42; // 函数内局部临时变量
// 捕获列表为值捕获,在创建lambda时(生成类时调用构造函数)就已经拷贝
auto f2 = [v1]{return v1;};
v1 = 0;
auto j = f2();
}
因为是值拷贝,所以修改父函数的局部变量,不会影响lambda生成的类的成员变量,所以j的值仍然为42。
[ 引用捕获]:
如果上面的式子改写成如下形式
void func()
{
size_t v1 = 42; // 函数内局部临时变量
auto f2 = [&v1]{return v1;}; // 在创建lambda时时引用类型,格式为引用符号写在捕获变量前面
v1 = 0;
auto j = f2();
}
因为是引用初始化,所以改变局部变量的值,lambda表达式生成的未命名的类内数据成员也跟着改变值。结果是j的值为0。
[隐式捕获]:
有时我们在式子中要用到某些变量,但为了节约时间,我们不愿意把变量一个个写上去,或者说,我们要用到的变量很多,不想一个个写上去。那么我们这时就可以使用隐匿捕获,让编译器根据我们的函数体来推断我们将要用到那些变量。隐式捕获有三种方式:
1) 隐式值捕获
void func()
{
size_t v1 = 42;
auto f2 = [=]{return v1;}; // 值捕获
v1 = 0;
auto j = f2();
}
上面的式子,捕获列表中,我们用=代替了我们要捕获的变量。
=表示采用值捕获方式。
2)隐式引用捕获
void func()
{
size_t v1 = 42;
auto f2 = [&]{return v1;}; // 引用捕获
v1 = 0;
auto j = f2();
}
上面的式子,捕获列表中,我们用&代替了我们要捕获的变量。
%表示采用值捕获方式。
3)混合使用隐式捕获和显式捕获
void func()
{
size_t v1 = 42;
size_t v2 = 30;
auto f2 = [&,v2]{return v1 + v2;}; // 混合使用隐式和显示捕获
v1 = 0;
auto j = f2();
}
这里v2显式地写在捕获列表中,则其它的变量都是隐式引用捕获。在使用混合捕获时必须知道几点:
3.1) 捕获列表中第一个元素必须是一个&或=。
3.2) 显式捕获的变量的捕获方式必须与隐式捕获的方式不同。如果上面如果我们写成auto f2 = [&,&v2]{return v1 + v2;};这就是一个错误的lambda表达式,因为捕获列表中,隐式的变量和显示的变量都是引用方式。
[可变lambda]:
1)当lambda表达式的一个形参是值拷贝的话,lambda是不会改变其内的初始值的(还记得上面提到的那个未命名的类类型吗?里面的调用重载函数是 const类型的,它是不会改变类内的变量的值的,而lambda只有一个可调用重载函数是允许用户调用,所以这个const的成员函数没有权限改变 lambda内的值。)如果下面的表达式:
void func()
{
size_t v1 = 42;
auto f2 = [v1]{return ++v1;}; // 值捕获
v1 = 0;
auto j = f2();
}
这种情况下,如果我们如果上面一样,改变v1变量的值,那么编译器会提示你必须是可修改的左值;要想确实修改这个值,我们可以用一个关键字解决:mutable。
(顺便说下C++中最容易让人忘记的几个关键字吧,
第一个explicit:
这货我们平时很少用,但如果你看过MFC的代码,你会发现,其实这货还是很上镜的。这个关键字是必须在类内构造函数中使用,而且,它还有几个限制条 件:1、构造函数必须只允许接受一个形参,当然,如果你有3个,而其它的都带有默认值,也是可以的。2、explicit只能在类内定义。如果你想在类外 explicit A::A(int a)这样定义是不允许的。explicit是阻止了数据的隐式转换,比如,A(double a){},当我们int a = 10;A(a);时,会把int型的a类型上升为double,但如果explicit A(double a){}的话,A(a)则是不允许的。当然,如果你要A((double)a)的话,这又是可以的。
第二个mutable:
当某数据定义成mutable后,无论在任何时候他都是可变的。包括const类——事实上,const类也只能调用const成员变量和成员函数,好 吧,这个说了其实没有意义,一句话,其实才是我想说的:就算是const成员函数中,它都是可变的。另外,mutable只能作用于非const的变量, 如果mutable const int a; 这样是不允许的。正因为这个关键字破坏了类的封装性,所以我们是比较少用的。
第三个volatile:
这个是C引过来的。或者做移植时经常用到,但说真的,平时根本用不上,它主要是告诉编译器,不要对被它定义的变量进行优化,因为没有用过,所以没有什么好说的。
)
说完就回正文吧。
在我们把上面的lambda的参数列表首加上mutable,改为:
void func()
{
size_t v1 = 42;
auto f2 = [v1] () mutable{return ++v1;}; // 值捕获
v1 = 0;
auto j = f2();
}
这样是完全没有问题的。上面的参数列表就算是空,你也必须带上(),不然你编译不过。
2)当我们为引用捕获时,变量应该说是任何时候都是可变的,但有个例外,当你的捕获类型本身是const类型时,你的变量是无法改变的。例如:
void func()
{
const size_t v1 = 42;
auto f2 = [&v1]{return ++v1;}; //引用捕获
v1 = 0;
auto j = f2();
}
这样是错误的。或者聪明的你会想着,我可以加上mutable呀,这样不就行了吗?好吧,如果你是新手,会这样想,说明你还是学得很快的。但事实是这样 的,值捕获时,我们是直接拷贝父函数临时变量的值的,我们lambda生成的类内的变量不是const的(虽然我们的调用重载函数是const的),所以 我们加上mutable实际上只是对我们lambda的类内变量有影响,跟父函数的临时变量没有半毛钱关系。引用却是不同的,当我们定义一个引用时,比如 下面代码:
int a = 10; int b = 20; int &ref = a; ref = b; // 这样我们是可行的,大家都能理解。
const int a = 10; const int b = 20; int &ref = a; ref = b; // const变量只能用const引用,红色部分是不允许的,所以如果父函数的临时变量是一个const变量,那么lambda的捕获列表中的参数是不能更改的。
而一个const的变量,是无法加上mutable关键字的。
[指定lambda返回类型]:
前面说到了,如果lambda内有一条return语句,则lambda会根据表达式推断返回类型;如果没有返回语句,就会返回void类型,比如下面的例子:
[](int i){return (i > 0) ? i : -i;}
它能很好工作,但如果变成这样呢?
[](int i){ (i > 0) ? i : -i;}
这样的话,没有return,它只返回void,完全与我们意图不符合。
如果改成这样:
[](int i){ if(i > 0) return i; else return -i;}或者[](int i){ if(i > 0) return i; return -i;}
这样是无法编译通过的(有些编译器还是可以通过的,VS2013就可以)。我们要说的只是指定返回类型。
我们可以这样指定lambda表达式返回特定的类型:
[](int i)->int { if(i > 0) return i; else return -i;}
29、bind函数【标准库】
在C++的标准库中,所有与查找或者比较有关的算法函数,都会接受一个所谓的谓词(其实它就是一个函数指针的形参),这个谓词一般情况下都会是一元谓词 (即只接受一个形参的函数),但往往只接受一个形参的函数并不能满足我们的需求。比如,我们调用find_if,查找某个string内所有满足大于6个 字母的单词,这时我们可以定义一个比较函数(C++ Primer)。
bool check_size(const string &s,string::size_type sz)
{
return s.size() >= sz;
}
我们需要调用find_if,但这个函数的谓词是一元谓词,我们根本无法让它指向一个只含一个形参的函数式。而C++11中提供了一个新的函数——bind;它会为我们生成一个类,跟lambda类似,它可能(我猜的)也含有一个重载了的调用运算符。它的语法如下(为什么这货跟sokcet的bind同名呀……)使用它要包含头文件#include <functional>,而且,它重载了很多个版本:
auto newCallable = bind(callable,arg_list);
bind()函数能作为通用适配器是因为它有“占位符”。它的占位符以_n的形式表示,数值n表示生成的可调用对象中参数的位置:_1表示第一个参数。_2表示第二个,依此类推。
我们可以这样绑定check_size函数,让它“转”化为只需要一个形参就可以调用的函数。
auto check6 = bind(check_size,_1,6);
这个表达式,用bind绑定了,生成一个新的函数。bind()内的语句,我们可以这样理解:首先,我们要绑定的是check_size这个函数,然后, 它的第一个参数用了一个占位符,表示我们要求在check6中有一个形参,用来代替_1的位置(即check_size的第一个参数);最后的6表示第二 个参数。
所以我们可以这样调用check6:
string s = "test";
bool b1 = check6(s);
所以调用find_if可以这样:
auto wc = find_if(words.begin(),words.end(),bind(check_size,_1,sz));
或者
auto wc = find_if(words.begin(),words.end(),check6(sz));
1)我们再来看下占位符还有什么特性。占位符位于placeholders名称空间。
而占位符最特别的地方是可以重整参数顺序,比如有一面这样一条函数:
auto g = bind(f,a,b,_2,c,_1);
这个函数,g,只有两个参数,当我们g(A,B)时,第一个参数会被_1占用,而第二个参数会被_2占用,所以g(A,B)实际上就是f(a,b,B,c,A),这样调用的。利用这个原理,可以实现一个函数,可以比较大于,也可以比较小于。比如有两个过滤函数,是比较小于的,但我们当前有这样一个函数:
bool compare(int a,int b) { return a > b;}
如果bind(_2,_1),把a和b反过来,那就相当于比较a < b。
2)bind的非占位符,默认情况下,他们都是拷贝复制的,但有时我们确实需要他们进行引用复制,我们可以用ref函数或者cref函数来解决非占位符参数的引用传值,ref和cref都会生成相关的类。
ostream& print(ostream &os,const string &s,char c)
{
return os << s << c;
}
如果我们这样绑定:
auto f = bind(print,os,_1,‘‘);
这就是一个错误的表达式,因为ostream类型不允许复制,必须引用传参。我们可以用ref函数解决。
auto f = bind(print,ref(os),_1,‘‘);
跟ref一样,我们还有一个cref函数,只不过,它是生成一个保存const引用的类。
30、无序容器【容器】
我们都知道,维护元素的序列所花费的代价是很高的,在我们学数据结构时,以最简单的数组为例,我们如果插入一个数组,并且需要保持其序列性,就算有好的算法,但也难免要进行多次的比较(注意,一般是通过比较运算符进行的),当数据结构复杂,数据量大时,这个代价就是很大的。C++11为我们定义了4个无序关联容器:unordered_set、unordered_multiset、unordered_map和unordered_multimap。他们跟其它有序容器比较,多了一个叫哈希策略的操作。事实上,无序容器在组织元素不再是按比较运算符进行了,而是通过一个哈希函数(即使每个元素都有一个自变量,而能通过某一式子可以通过自变量得到结果的这个式子就叫哈希函数)来组织的,所以,它也是散列的。这里不给具体的例子,这些容器的操作方式也是跟有序的大同小异的,具体自行参考MSDN.
31、智能指针【标准库】
还记得前座卖文胸的山田同学吗……额,说错,还记得以前我们要写一个内置有指针数据类型的类有多痛苦吗?
比如我们定义一个类,类内需要一个能装很多string类型的成员变量,它或者是读取下载图片的每个JSON的地址,如果我们有1000个图片,就要维护1000个这样的数据,量是很大的,如果我们脑残地在类内把该成员定义成vector<string>,想像下,如果我们要赋值,如果我们要拷贝复制,那要浪费多少算法时间和内存空间呀,或者你已经想到,你可以重载赋值和拷贝复制函数,直接传递一个指针,对,就是这样,但我们要先解决引用的问题,如果我们这个类A test; A test2(test); 如果你这样做,不处理好引用计数问题,当我们test超过生命周期了,但test2是复制test的内容的,所以指针的值也一起复制了;这样,test2内的数据成员就指向一个未知的内存地址了。所以,我们需要智能指针,如果引用计数,当拷贝复制时,我们需要这个引用计数+1,这样,当我析构时,我就能判断,当前引用计数数量,如果仍然有其它对象引用我当前的内存地址,我则不能马上析构。
以前的解决方案一般分为1、在每个类中定义一个含有引用计数的指针类,它种方式叫值类型。当然,为了这个值类型(它一般定义成我们自定义类中的私有成员)能完整操作我们类内的成员,完全可以定义成某一个类的友元,这样就能访问类内私有成员了。2、定义一个叫句柄类的类,这个类并非像值类型一样,在每个需要智能指针的类内都要定义一个能用性较差的类,而这个类主要目的仅仅是提供引用计数的只剩。句柄类的行完全是一个普通指针的行为(通过重载一系列的操作符实现),当它定义为模板类时,也解决了通用性的问题。而C++11标准库中,就为我们新增加了这样的句柄类型的智能指针类:shared_ptr,unique_ptr和weak_ptr。
1、shared_ptr
这个类就是一个中规中纪的句柄类,它的行为大致上跟一个指针完全是一样的,只是多了几个函数。(这部分不说了,自己看下头文件或者查下资料吧,要说太长,也没有必要)这个类主要与一个位于memory中的标准函数库中的,叫make_shared的类配合使用,以做到最安全的分配(make_shared的返回类型就是一个与模板类型相关的shared_ptr类型)。详细的类定义,不会写,因为随便网上找一大堆,我只是记录学习笔记的而已,用法一般这样。
class A
{
...........
private:
shared_ptr<vector<string>> = make_shared<vector<string>("1","2")>;
};
2、unique_ptr
这个类的行为,跟旧版本的auto_ptr有部分相似,但我并不想说他们的区别,毕竟我以前就几乎没怎么用过auto_ptr,真要用到智能指针,也是手动自己写一个功能有限的。
unique_ptr正如其名,同一时刻,它永远只能得到一个对象的所有权。我们要记住一个名词,就是当对它赋值时,我们不叫某某指针指向什么值,我们应该习惯性地说某某unique_ptr拥有某个值的所有权,因为这样我们更容易理解它的一些方法 。
首先,当unique_ptr拥有某一个值的所有权时,我们不能简单地通过赋值或者拷贝的方法对其进行重新绑定其它值,我们只能通过它内的release或reset改变其所有权。如:
unique_ptr<int> p1(new int(0)); // unique_ptr的构造接受的是一个指针变量
unique_ptr<int> p2 (new int(1));
p2 = p1; // 这是不允许的
我们可以用如下方法改变其所有权。
1) 在构建p2时传一个指针,而这个指针是由p1释放后返回的
// 记住这里,p1.release()返回的是p1这个智能指针容器所保存的指针
unique_ptr<int> p2 (p1.release());
注意:release()只是切断p1这个智能指针容器对new int(0)这个语句分配的指针的控制,但它只是返回这个指针的值,而并不会自动delete这个指针,所以当我们直接p1.realease()这样操作,不仅不会释放掉new int(0)这个内存块,还会造成我们无法再次找回这个指针的地址。
2)利用reset释放
reset()有一个可选参数,这个参数是可以释放指针所指向的对象并置指针为空,比如p1.reset()就会释放p1所指向的内存块,并让p1这个指针为null,这就是可以解决上述红色部分的问题。当它带一个参数时,这个参数表示重新指向给定的指针,如:
p2.reset(p1.release());
这个表达式就是先释放p2所指向的内存块,再重新指向p1的内存地址。或者重新分配地址。
p2.reset(new int(3));
3、weak_ptr
现在的高级语言,我们通常都会听到所谓的“弱指针”,比如OC,经常需要用到weak,strong来给变量定义一个属性。现在C++11也引进了这个“弱”的概念,虽然跟OC还是有点区别,但其实作用却是一样的,都是为了更安全地处理指针。
一个weak_ptr必须与一个shared_ptr绑定才能使用,而且,就算绑定了也不会增加shared_ptr的任何引用计数。正因为这种与其绑定或者说共享的对象的关系是如此不密切,所以才说它是"弱",就是说,他只是象征性指向一个对象,而不论这个对象是否已经销毁。
auto p = make_shared<int>(42); // 创建一个shared_ptr对象
weak_ptr<int> wp(p); // wp绑定p,p引用计数不变
正是因为它不增加,所以我们明显不能直接无所顾忌地使用它对我们的指针对象进行操作(果然是弱呀,这跟我们内置的指针差不多,唯一的好处就是作为一个类,还是有不少函数可以使用,大大减少我们的写代码的数量,如果我们需要临时的指针指向shared_ptr,而又不想改变原有对象的生命周期(引用计数是影响对象的生命周期的),我们就可以用用这个弱得很的智能指针了),我们需要先用lock成员函数来判断wp所绑定的对象是否仍然存在。当存在时就返回这个shared_ptr对象,否则就返回一个空的shared_ptr对象。
if(shared_ptr<int> np = wp.lock()) // 返回一个shared_ptr对象,if这个对象不为空时才执行下面的语句
{
// 对wp返回的shared_ptr进行操作
}
32、定位new
旧标准的C++,当我们new一个内存区域时,如果空间不够,会直接抛出一个bad_alloc的错误,并中断程序,而指针的值侧为null。现在,C++11有一种新形式,我们称之为定准new,它我们向new传递额外的参数,如:
int *p = new (nonthrow) int; // 当分配失败时,不抛出bad_alloc,而且,p指针为nullptr
上面的表达式,向new传递一个nonthrow的参数,告诉new,不抛出异常。
33、类内初始值
创建对象时,类内初始值将用于初始化数据成员。没有初始值的成员将被默认初始化。如果我们没有提供任何的初始值,也没有在构造函数里定义任何相关的初始化操作。那就按旧版本一样,全局的内置类型由系统决定初始值,局部的仍然是不确定的值。
class CC
{
public:
CC() {}
~CC() {}
private:
int a = 7; // 类内初始化,C++11 可用
}
34、允许使用作用域运算符来获取类成员的大小
在标准C++,sizeof可以作用在对象以及类别上。但是不能够做以下的事:
struct ST
{
int a = 0; // 声明时就必须要定义
static int b;
};
cout << sizeof(ST::b) << endl; // 静态成员本来就是这样取值的,所以跟新的有冲突,只有非staic可以
cout << sizeof(hasY::a) << endl; // 直接由hasY型別取得非静态成員的大小,C++03不行。 C++11允許
以前我们要取一个数据的大小,必须要在有类生成了相应的实例后才行。
35、=default
旧版本的C++中,当一个类内我们没有定义构造函数、拷贝构造函数、拷贝赋值运算符和析构函数时,则编译器会为我们定义一个合成的版本以让我们的类能正常运行。对于默认构造函数,当我们定义了自己版本的构造函数,编译器则不会再为我们合成默认构造函数了,而对于另外的三个成员函数,无论我们是否定义了自己的版本,编译器都会为我们合成一个。(事实上,当一个类需要我们自己来控制其析构时,也几乎肯定了必须要我们定义自己的拷贝构造函数和拷贝赋值运算符)。
C++11为我们提供了一个=default来显式地要求编译器生成合成的版本。
class test
{
public:
// 这个构造函数跟编译器合成的默认构造函数完全一样,但它100%不是我们编译器为我们合成的而是我们自己写上的,所以我们需要=default让编译器生成合成的版本,这样这个构造函数的作为就跟编译器默认为我们合成的合成构造函数的位为一样。
test() {} = default;
test(bool b) = default
{
cout << b << ednl;
}
test(const test&) = default; // 拷贝构造函数
test(const test& T,int i = 0);{}// 拷贝构造函数,他们能重载,但这个不能加=default后面会说
// 下面这个只是一个带两参数的构造函数,因为C++中对拷贝构造函数定义是这样的:如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。这个对于下面说的移动拷贝构造函数同样适合。
test(const test& T,double i);{}
test& operator=(const test&); // 拷贝赋值运算符
~test() = default; // 析构函数
};
test& test::operator=(const test&) = default; // 类外指定,即在类定义时指定=default
=default在类内出现时,所修饰的成员函数都为inline版本,而类外出现时,则为非inline版本。
嗯,我刚接触这个时,做了一件很脑残的事。上面有注译。这是编译过不去的,不多说,我只是脑残地试了下。
test(const test&,int i = 0) = default; // 错误
当然,如果没有=default,这个成员函数是正确的,它是一个拷贝构造函数,当这种除了第一个参数是本身类的引用外,如还带有额外参数时,这个额外的参数必须要有默认实参——如果它不带默认实参的话,就相当于是有两个(这点对拷贝赋值运算符是无效的,因为operator=对参数是有要求的),但无论如何,它都不是正确的由编译器合成的拷贝构造函数的格式。
=default可以肯定地说,它只能用于能合成的成员函数。
注意:上述的我们一般只声明而不定义,因为如果我们定义的话就跟=default有冲突,=default完全是说明由编译器生成我们成员函数内的其它代码,所以它只是声明而非定义。
36、删除函数
如果我们用过单例这种设计模式时,又刚好需要定义一个类,这个类不能多次构造,只能在整个程序中只有一个实例存在,这时我们一般都会阻止这个类有构造函数。还记得我们一般做法是如何的吗?是的,我们通过先写一个构造函数,通过这个函数阻止了编译器的合成构造函数,再把这个构造函数定义为private让它不能在类外调用;然后再小心翼翼地处理拷贝和拷贝赋值运算符。读过这种代码的人都清楚,如果没有人告诉你这个是一个单例,我估计你真得愣个10来秒才能看明白意图。
现在C++11为我们提供了更直观的方法(C++11新增了很多后置的关键字,包括后面会提到的override,都是使语言看起来更简洁,C++开始不断向简洁的语言风格进化着)。通过使用=delete来删除能合成的成员函数。=delete有一个跟=default不同的地方,即=delete必须定义在类的声明中,以确保编译器能尽早知道那个函数是我们指定删除的。而类是否合成默认版本,则是编译器在类定义时决定的。所以=default和=delete这种差异,并不与C++的原本逻辑相悖。
class test
{
public:
test() {} = delete;
test(const test&) = delete; // 拷贝构造函数
test& operator=(const test&) = delete; // 拷贝赋值运算符
~test() = delete; // 析构函数
};
用这个方法,在设计类似单例模式这种类时,我们可以不需要管我们的类的属性是否为private,直接后置=delete就行了。你也可以把拷贝和赋值都干掉,这个实例类连引用计数都可以不用了。但这个类却只有简单的处理逻辑的功能了。这是题外话,不多说。
另外,当我们把一个析构函数定义为不可访问或者删除函数,我们的合成构造函数和拷贝构造函数(拷贝赋值运算符仍然能合成,因为有时我们虽然不能构造A对象,但如果B派生了A,在B有构造函数的话,仍然可以进行赋值操作的,当然,A现在无法构造,这个前提也不成立,但C++语义就是允许合成拷贝赋值运行符)是被设置成=delete的,因为不能释放对象,所以编译器也不允许我们创建对象,但我们仍然可以自定义构造函数,达到创建对象的目的,但这样的话,如果存在指针对象,就会无法释放了。
class hasY
{
public:
hasY() = default;
hasY(const hasY&)
{
cout << "copy" << endl;
}
hasY& operator=(const hasY&)
{
cout << "=copy" << endl;
return *this;
}
~hasY() = delete;
};
hasY hy;
hasY hy2 = hy;
hasY hy3;
hy3 = hy;
当我们定义了一个移动构造函数和/或一个移动赋值运算符时,则该类的合成拷贝函数和拷贝赋值运算符将会被定义为删除的。但100%情况是当需要自定义移动操作函数时,我们都需要先定义拷贝操作函数。总而言之,移动操作函数和拷贝操作函数是相类的。
*拷贝构造函数和拷贝赋值函数的不同之处
这个不算C++11的内容,是老生常谈了,但我还是想写,主要是如果不弄明白,移动操作函数和拷贝操作函数之间的相互关系可能就会乱了(注意那个蓝色的和/或)。在C++中,列表初始化,初始化,赋值,赋值初始化是要区分的:
int i(0); // 旧版初始化
int i{0};或int i ={0}; // C++11列表初始化
int i = 0; // 这是赋值初始化,是初始化
int j; j = i; // 这个才是赋值,而拷贝操作函数也有相同性质。
class hasY
{
public:
hasY() = default;
hasY(const hasY&)
{
cout << "copy" << endl;
}
hasY& operator=(const hasY&)
{
cout << "=copy" << endl;
return *this;
}
};
1、hasY hy; //调用合成构造函数
2、hasY hy2 = hy; // 输出copy,编译器跳过=号,相当于hasY hy2(hy)
3、hasY hy3; // 调用合成构造函数
4、hy3 = hy; // 输出=copy
上面的代码,可能很多人会觉得2处是调用拷贝赋值运算符,其实这个跟前面的内置类型一样,这个是赋值初始化,而是赋值。有些书上会叫赋值构造函数,所以我为什么要叫拷贝赋值运算符就是不想弄乱。
37、右值引用
C++11为了支持移动操作,新引用了一种名为右值引用。正如其名,右值引用必须绑定到右值。对于左值和右值的定义,可能一时是难以理解的,按概念来说,它已经不是以前C那样以等号左侧或右侧来区分了,所以也变得更艰涩难明了。但我们只记住这样一点:
有名字且可以取地址的就是左值;没有名字且无法取地址的就是右值。(const左值引用算例外)
这样从代码行为上来理解,会更容易掌握点。
我用通过&&来获取一个右值的引用,如:
int i = 0;
int &r = i; // 正确,左值引用
int &&rr = i; // 错误,i有名字能取地址,是一个左值,左值不能绑定右值引用
// 错误,i * 42这个表达式会生成一个临时变量,我们无法取地址,所以它是一个右值,右值不能绑定左值引用int &r2 = i * 42;
int &&rr2 = i * 42; // 正确,右值引用绑定一个右值
// 正确,但这个引用之所以成立,是被const限制的,const要求被绑定的值是一个右值,所以这个表达式成立。const int &r3 = i * 42;
当一个原则上不能再改变的右值被成功绑定后,我们就能通过右值引用这个别名对其进行与左值变量相同的操作,比如改变其值。因为它引当于有名字了且可以取地址了。
int &&rr2 = i * 42;
cout << rr2 << " " << ++rr2 << endl; // 输出为42 43
尽管要求不能把一个左值绑定到一个右值引用,但C++11中仍然有方法解决这一问题。
可以可以使用std::move(别落下std::,下面会解释的)来把一个左值显式地转换为右值。
int &&rr3 = std::move(r);
std::move的作用就是返回一个右值引用,这个右值引用绑定的值是把move的参数r通过强制类型引用转换成右值类型。通过引用折叠机制,move的参数可以接受一个左值也可以接受一个右值,但无论如何,我们应该都要遵守:尽量不使用一个移后源对象的值。其实看下头文件我们也可以看到,std::move就是把类型转换下再返回而已(它本身是模板函数,类型会根据传入的参数类型进行实参推断),但它返回的是与原来类型相关的右值引用,即type&&类型。
正因为只是进行类型转化,所以,无论我们++i还是++r,都能改变i的值,但不提倡这样使用。(C++ primer这部分我真不理解,郁闷!不过,大家用了std::move后,除了重新赋值和释放这个变量外真不要作任务操作,这是C++11的约定,大家应该都遵守,如果你要搞特化,别人看你代码,不仅看得一头雾水,还要骂句SB的。)
还有更重要的一点:右值被引用后,即右值引用本身也是一个左值。更详细的请看下面的引用折叠。
38、移动构造函数
移动构造函数是C++11新引进的概念,目的在于让当以某个临时变量,或者说即将结束生命周期的对象(也就是右值)作为构造参数时,可以达到比旧版的拷贝构造更快的效率。移动构造函数跟拷贝构造一样,要求每个参数必须为一个本身类类型的右值引用,任何额外的参数都必须有默认实参。先拿个例子比较下拷贝函数和构造函数区别吧。
class A
{
public:
A(){cout << "construct" << endl;} //1
A(const A&){cout << "copy" << endl;} //2
A(const A&&) {cout << "move" << endl;} //3
};
void main()
{
A a;
A b(a);
A c(std::move(b));
}
上面代码输出的结果如下图:
红框里的输出结果就是了。这里应该很清晰了,当我们利用std::move把类b转成一个右值时,就调用了移动构造函数。前面我们的例子太简单了,一般的移动构造函数至少要遵守这样的原由:必须让被窃取的对象与原来的资源切断联系,这里特别要注意指针。
class A
{
public:
// 这里不能用const,因为下面要更改source内的值
A(const A&& source) noexcept:a(source.a),b(source.b)
{
source.a = source.b = nullptr;
}
public:
int *a;
char *b;
};
上面的移动构造函数的实质是怎么样的呢?
从图中可以看出,移动构造函数实质就是“窍取”资源,它本身不分配任何资源,所以也没有调用到相应的构造函数。上图这种情况,当我们“窍取”了b的资源后,所以b的资源的归属权已经变更了,根据移动函数的约定:当移动后的源应该是销毁无害的,但如果我们不处理的话,当我们b生命周期结束,肯定会影响到对象c的。
事实上,移动构造函数是解决指针,特别是临时指针在复制时,如果数据量消耗效率的问题。比如上面的函数,如果我们是一个内置类型,结果会如何呢?:
class A{
public:
// 因为有了移动构造函数,不会再合成默认构造函数了
A():a(0),b(new char(‘a‘)){}
A(A& source):a(source.a)
{
// 不像移动构造函数,拷贝构造函数一般都是另外分配内存空间后再进行赋值
b = new char(*(source.b));
}
// 这里不能用const,因为下面要更改source内的值
A(A&& source) noexcept:a(source.a),b(source.b)
{
source.b = nullptr;
}
public:
int a;
char *b;
};
void main()
{
A a;
cout << (void*)a.b << endl;
A b(std::move(a));
cout << &(a.a) << " " &(b.a) << endl;
cout << (void*)a.b << " " << (void*)b.b << endl;
cout << &(a.b) << " " << &(b.b) << endl;
}
如果你前面让我误导了(我承认我那图很烂,我们我只写了堆和栈,但并不详尽,反正堆一般是new分配的),认为移动函数是一种机制,那输出的结果就让你哭了。
说到底,移动函数也是符合C++语义的,如果你什么都不做,它也是什么都不处理。上面的输出结果中,a和b是两个类,有各自的内存空间,所以输出内置型的数据时(它在栈上,包括b这个指针本身,也是在栈上),它因为是两个对象分别控制的,所以第二行输出的地址不同;第三行就比较有意思,它输出的是移动后的源和当前变量的指针地址,它们是相同的,为什么第一个是空指针呢?因为我们在移动后,把移后源设置为nullptr,但我在第一行cout输出了未清空的地址。第四行,它也是输出栈上的指针变量,所以它的地址也是不同的。
考虑一下,只是为了解决指针的复制问题,我们仍然可以利用拷贝构造函数执行跟移动构造函数一样的操作(代码一样时),但这样虽然有移动构造函数的功能,但它有两个问题(假设旧版本没有std::move情况下):
1、它无法将一个临时变量快速处理(即无法支持右值引用)
2、假设我们对拷贝构造函数这样做了,那很明显,当我们需要对象连堆都一起分离出来该怎么办呢?所以说,这种情况下,我们这样做是不现实的。事实上它也一般定义为const。C++引进移动构造函数后,就鱼和熊掌都可兼得了。
最后,我们注意到那个noexcept,这也是C++11新引进的。它位于参数列表和puvcwx列表开始的冒号之间。它指示程序不抛出任何异常,其实可以想像,我们只是“窍取”数据,并没有进行任何移动类的操作,所以根本不会发生什么错误。标准库中的vector类中的pushback也有这种东西,失败时并不会弹出一个异常。
39、移动赋值函数
移动赋值函数跟移动构造函数差不多,只是比后者多了两个步骤:
1、每次赋值前,都要先把原来的数据清空。如果有数据成员指向分配好的内存空间,我们侧要释放这部分内存。不然肯定会出现悬挂问题的。
2、解决自赋值问题,这个问题拷贝赋值运算符也有的,当一个对象是其自身时(虽然我们一般不会遇到这种情况,但仍然要小心有些程序员就是二愣子),我们不应该进行赋值,特别是清空数据问题,如果我们在赋值前清空了数据,则源对象因为是自身,也会马上清空数据的,这样我们的赋值不仅没有意义,还要导致错误。
以前面的例子为例,我们弄一个简单的移动赋值函数。
class A{
public:
// 因为有了移动构造函数,不会再合成默认构造函数了
A():a(0),b(new char(‘a‘)){}
A(A& source):a(source.a)
{
// 不像移动构造函数,拷贝构造函数一般都是另外分配内存空间后再进行赋值
b = new char(*(source.b));
}
// 这里不能用const,因为下面要更改source内的值
A(A&& source) noexcept:a(source.a),b(source.b)
{
source.b = nullptr;
}
A& operator=(A&& source) noexcept
{
if(this != &source) // 比较地址,不能自身进行赋值构造
{
// 清空数据
free();
this->a = source.a;
this->b = source.b;
// 记得处理好移动后源的数据,指针的一定要清空,保证它销毁是无害的
source.b = nullptr;
}
return *this;
}
public:
int a;
char *b;
};
40、编译器合成的移动构造函数和移动赋值函数
我们都知道,在C++11中(旧版本也是一样只是没有=delete行为),除非我们显式删除拷贝构造函数和拷贝赋值运算符,不然编译器都会为我们合成一个默认的版本。但对于新增加的移动构造函数和移动赋值函数,一般情况下,编译器根本不会为我们合成这些移动操作的函数,但在特定的条件下,却还是会为我们合成移动操作的函数。这里我们还要加上另一条重要的信息:定义一个移动函数或者移动赋值运算符的类必须也定义自己的拷贝操作法函数。否则,这些成员默认地被定义为删除的。
1、当我们有任何版本的自定义拷贝构造函数,拷贝赋值运算符或者析构函数时,编译器不会为我们合成移动操作类函数(有任何一个拷贝操作函数时都不会再合成移动操作函数)。对于存在拷贝构造函数和拷贝赋值运算符,我们可能比较容易理解,因为当我们定义了这两个函数后,就算没有移动操作函数,我们的类仍然能工作得很好:对于临时变量,我们可以把临时变量的堆数据完整拷贝过来。说到低,移动操作类函数,只是为了让类工作得更快,而非不可缺。但对于析构函数,我们可能会有所疑惑,我们应该样想:要析构的地方,都是我们的类存在指针时,而当类需要析构函数时,几乎也肯定了我们必须需要拷贝构造函数和拷贝赋值运算符了。而也正因为带有指针操作,编译器无法推断我们需要做的操作,在不安全的前提下,C++11规定(我说了,移动操作函数只是为了类更好工作,而非不可缺)有析构函数时不合成移动操作函数是明智的。
2、当类没有定义任何版本的拷贝控制成员时(即有合成的却没有自定义的拷贝构造函数、拷贝赋值运算符或者析构函数时),且类的每个非static数据成员都可以移动(前面说了,这里移动其实就指“窍取”,但有些成员是无法移动的,我们都知道,尽管内存空间可以不变,但栈内的指向该空间的指针变量是不同位置的,即移后源和当前类栈内的变量是不同的,这样应该清楚了吧,比如,类内有自定义类时时,而该类本身又没有定义移动操作函数的这种情况就是其中一种存在不可移动数据成员的情况了。),编译器才会为我们合成移动构造函数或移动赋值函数。
但无论如何,编译器都不会隐式把移动操作函数定义为删除函数。
再来讨论下=default和=delete,后者是几乎用不上的,它不同于拷贝操作函数,就算有自定义的,编译器也会为我们合成,移动操作函数原来能合成的条件就比较苛刻,所以真的几乎用不上。至于=default,它的行为就有点奇怪了,就算我们显示定义一个移动操作函数为=default时,它却在特定条件下,无视=default而表示为删除函数。情况有如下这些(以上条件都是在显示=default时才会发生,没有的话只会发生前面提到的那2种情况):
1、当显示定义=default移动操作函数时,但类本身却有成员不能移动,则编译器会把移动操作定义为删除函数。(这种情况几乎都是类内有类成员变量,且该类没有定义移动操作函数才出现的)如:
struct hasY
{
hasY() = default;
hasY(hasY&&) = default; // 因为Y,它是=delete的
//假设这里的类Y定义有自己的拷贝构造函数(不会再合成移动操作函数),
却也没有定义自己的移动操作法函数
Y mem;
};
hasY hy,hy2 = std::move(hy); // 错误,没有移动构造法函数,它是删除的
2、如果类成员内有成员的移动构造函数或移动赋值运算符被定义为删除的(用=delete显示定义删除,或编译器隐式在特定条件下把=default改变为=delete,再次说明,编译器永远不会在正常情况下把移动操作函数隐式定义为=delete)或者声明为不可访问的(即private或protected),则类的移动构造函数或移动赋值运算符被定义为删除的。
3、类似拷贝构造函数(33点说到)。如果析构函数被定义删除或不可访问的,则类的移动构造函数被定义为删除的。移动赋值运算符仍然有效,但却无意义。
4、类似拷贝赋值运算类。当类中有const成员或者引用时,则类的移动赋值运算符定义为删除的。这里顺便解释下为什么拷贝赋值运算符在类中有const成员或者引用时会被删除合成的。我们都知道,在C++中,const变量和引用类型变量,都是在定义的同时要初始化的,而类中,const变量是在声明时直接定义的,而引用类型则是是在构造函数中进行列表化初始化的,所以这种情况下,拷贝赋值运算符不能复制const对象,他只会自己原生有一个const变量,值不会变。它也不能复制引用成员,因为他必须在构造函数的列表中给定,所以编译器为了更安全点,直接就是在遇到这种情况下,不再合成拷贝赋值运算符,而是交由程序员自己处理了。移动赋值运算也是相同原理。移动拷贝仍然可用,跟拷贝构造函数一样,合成的拷贝函数,逐值拷贝,把拷贝源对象的值作为新构造对象的值传递。
我们再谈下关于拷贝操作函数和移动操作函数之间的交互。如果一个类,本身没有定义任何的移动操作函数,那么,我们能否处理一个右值呢?假如有如下这样一个类:
class A{ public: A() = default; A(const A&) // 定义了拷贝构造函数后,编译器不会再合成任何的移动构造函数 { cout << "copy" << endl; }};A x;A y(x); // 拷贝构造函数,x为一个左值,它能取地址,有名称的A z(std::move(x)); //尽管std::move后的类型为A&&类型,但没有移动操作函数时,仍然是调用拷贝构造函数,而且是肯定安 全的。这时会进行隐匿的类型转换,把一个右值引用A&&转换为一个const A&引用,即使我们拷贝构造函数 定义为A(&),它仍然能接受一个A&&的右值类型。*前面说右值引用时提过的const变量应该记得吧?任务一个引用都要求右边的值是左值,但如果是const引 用,则右边的值可以是一个右值,因为const要求一个常量值,而常量值都是一个右值。构造函数形参可以是非const而安全接受右值,其实原理也相当简单:对于构造函数来说,主要责任是构造一个新对象,即便修改了右值,也无所谓,反正临时的东西,扔了就行了。但是普通函数,目的不是为了别的,就是为了处理传进来的值,传进来的引用,是既能in又能out的。所以这里如果非const引用一个右值,那肯定会让out的结果失效。所以对于普通函数而言,要想接受一个右值必须是一个const类型的形参;没有所谓的机制,只是编译器是这样定义的。void hit(const int &) //假如没有const说明右边可以是右值,则std::move后的值不能用于参数
{
cout << "function" << endl;
}
int iv = 4;
hit(std::move(iv)); // 正确
类的输出结果:
有个提醒,我不建议在除了类定义外的移动操作函数。因为它非常不稳定,你不能确定移后源对象的确切状态,是否仍然有其它的对象在使用它。我们说过,移后源应该是可以随时释放清空而不影响其它操作的,而且,我们不能对其内数据进行操作。但谁又能100%知道当前移后源的状态呢,特别是大项目中,这个更是致命。
41、移动迭代器【标准库】
新标准库中新增加了一种移动迭代器适配器,它主要通过调用标准库的make_move_iterator函数将一个普通迭代器转换为一个移动迭代器,而且,返回后的迭代器的解引用运算返回的是一个右值引用。还记得标准库中新加入的begin()和end()吗?主要我们这样操作:
make_move_iterator(begin())就可以返回一个移动迭代器,想像一下,如果你的类支持移动操作,如果直接传递一个右值就可以达到优化效率的效果。
38、引用函数
在说这个之前,我们先回故一下类内有关const区分重载版本的内容。
看到了吗?我们的const版本函数,就算跟普通成员名称,返回值和参数完全一致,但它仍然是一个重载版本。而对于以const作为形参的函数。却不是重载函数,因为C++规定,顶层const作为形参时,是会被忽略const标识的。
还有一种情况。
string s1,s2;
s1 + s2 = "test";
上面这段代码,s1+s2后是由s1返回一个临时变量(即右值),但我们却对右值进行了重新赋值。当然,在语法上是没有问题,但使用上却几乎无法重获这个"test"字符串,除非我们std::move()了它。这种用法是很普遍的,但我们却很想禁止这种用法。以前不行,C++11为我们准备了新的工具。
好了,现在我们说下新引进的“引用函数”吧。
1、语法
我们通过在移动操作函数或拷贝操作函数后增加引用限定符,就可以阻某些用法。
引用限定符(只能用于非static)有两种,分别是"&"和"&&",前者表示this(类对象本身)只可以指向一个左值,而后者表示this只可以指向一个右值。
class Foo
{
public:
Foo &operator= (const Foo&) &;
};
Foo& Foo::operator=(const Foo& )&
{
return *this;
}
这个类的赋值运算符只允许左侧的操作数即类对象本身是一个左值。如果我们这样操作:
Foo f,i;
std::move(f) = i;
这是无法成立的。
2、重载和引用函数
C++11中新增加的这种引用函数,跟const成员函数一样,可通过引用限定符来区分重载版本。
前面已经看到,我们定义const成员函数时,可以定义两个版本,唯一罚没是一个有const限定,一个没有。而对于有引用限定符的成员函数,有一点不一样:*如果我们定义两个(两个是重载的最低要求,不然还叫什么重载)或两个以上具有相同名字和相同参数列表的成员函数(C++中,返回值不作为重载的考量依据)时,就必须对所有重载的几个函数都加上引用限定符,让它变为引用限定类型的函数,或者所有不加作为普通成员函数定义。
class Foo
{
public:
Foo storted() && ;
// 错误:要么把前面的变为普通成员函数,要么在const后面也加上&&(记得,是&&不是&,引用限定符是分两种的)
Foo sorted() const;
Foo sorted(Comp*);
Foo sorted(Comp*) const;
};
*注意点:
1)当一个成员函数带有引用限定符,那么无论是声明还是定义,它都必须要带上。
2)引用限定符必须写在函数的最后位置,如果是const成员函数,就写在const后面。