主题 | 概要 |
---|---|
C++新特性 | C++11与C++14新特性 |
编辑 | 时间 |
新建 | 20181106 |
序号 | 参考资料 |
1 | C++ Primer(第五版) |
长整型,最小64位。
以花括号的方式进行初始化。 通常C++的几种不同的初始化方式可以相互等价的使用。但如果提供的是初始元素值的列表,则只能把初始值都放在花括号里进行列表初始化,而不能放在圆括号里。
std::vector<std::string> v1{"a","an","the"}; //--正确
std::vector<std::string> v2( "a","an","the" ); //--错误
空指针不使用任何对象,几个生成空指针的方法:
int *p1 = nullptr;
int *p2 = 0;
int *p3 = NULL;
最直接的方法是用字面值nullptr来初始化指针,C++11新标准中引入的方法。nullptr指针是一种特殊类型的字面值,可以被转换成任意其他的指针类型。
常量表达式(const expression)是指值不会改变,并且在编译过程中就能得到计算结果的表达式。字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。
常量表达式:
const int max_files = 20;
const int limit = max_files + 1;
非常量表达式:
int staff_size = 20;
const int sz = get_size();
尽管staff_size的初始值是个字面值常量,但由于它的数据类型只是一个普通int而不是const int,所以它不属于常量表达式。另外,尽管sz本身是一个常量,但它的具体值要到运行时才能获取到,所以也不是常量表达式。
constexpr变量
在一个复杂系统中,很难(几乎不可能)分辨一个初始值到底是不是一个常量表达式。c++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化。
constexpr int mf = 20;
constexpr int limit = mf + 1;
constexpr int sz = size(); //--当size()是一个constexpr函数时才是一条正确的声明
常量表达式的值需要在编译时就计算得到,声明constexpr时用到的类型必须为“字面值类型”。基本数据类型,算术类型,引用和指针都属于字面值类型。
尽管指针和引用都能定义成constexpr,但它们的初始值受到严格限制。一个constexpr指针的初始值必须是nullptr或者0或者存储于某个固定地址中的对象。
类型别名(type alias)是一个名字,它是某种类型的同义词。 有两种方法可用于定义类型别名,传统的方法是使用关键字 typedef:
typedef double wages; //--wages是double的同义词
typedef wages base, *p; //--base是double的同义词,p是double *的同义词
新标准规定了一种新的方法,使用别名声明(alias declaration)来定义类型的别名:
using SI = Sales_item; //--SI是Sales_item的同义词
编程时常常需要把表达式的值赋值给变量,这就要求在声明变量的时候,清楚的知道表达式的类型。然而做到这一点并不容易,为了解决这个问题c++新标准引入了auto类型说明符,用它让编译器替我们去分析表达式所属的类型。auto让编译器通过初始值来推算变量的类型。 编译器推断出来的auto类型有时候和初始值的类型并不完全一样,编译器会适当的改变结果类型,使其更符合初始化规则。
首先,使用引用其实是使用引用的对象,特别是当引用被用作初始值时,真正参与初始化的其实是引用对象的值。此时,编译器以引用对象的类型作为auto类型:
int i = 0, &r = i;
auto a = r; //--a是一个整数
其次,auto一般会忽略掉顶层const,同时底层const则会保留下来,比如当初始值是一个指向常量的指针时:
const int ci = i, &cr = ci;
auto b = ci; //--b是一个整数(ci的顶层const特性被忽略掉了)
auto c = cr; //--c是一个整数(cr是ci的别名,ci本身是一个顶层const)
auto d = &i; //--d是整型指针
auto e = &ci; //--e是一个指向整数常量的指针(对常量对象取对象是一种底层const)
如果希望推倒出来的auto类型是一个顶层const,需要明确指出:
const auto f = ci; //--ci的推演类型是int,f是const int
还可以将引用的类型设为auto,此时原来的初始化规则仍然适用:
auto & g = ci; //--g是一个整型常量引用,绑定到ci
auto & h = 42; //--错误:不能为非常量引用绑定字面值
const auto & j = 42; //--正确:可以为常量引用绑定字面值
要在一条语句中定义多个变量时,切记,符号&和*只从属于某个声明符,而非基本数据类型的一部分,因此,初始值必须是同一种类型。
auto k = ci, &l = i; //--k是整数,l是整型引用
auto &m = ci, *p = &ci; //--m是对整型常量的引用,p是指向整型常量的指针
auto &n = i, *p2 = &ci; //--错误:i的类型是int,而&ci的类型是const int
注: 顶层const与底层const: 指针本身是一个对象,它又可以指向另外一个对象。因此指针是不是一个常量及指针所指的是不是一个常量就是两个相互独立的问题。用名词顶层const(top-level const)表示指针本身是一个常量,用名词底层const(low-level const)表示指针所指的对象是一个常量。
有时希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。为了满足这一要求,C++11新标准引入了第二种类型说明符decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。
decltype(f()) sum = x; //--sum的类型就是函数f()的返回类型
C++新标准规定,可以为数据成员提供一个类内初始化值,创建对象时,类内初始值将用于初始化数据成员。没有初始值的成员将被默认初始化。需要注意的是,初始化时,或者放在花括号里,或者放在等号右边,不能使用圆括号。
C++11使用范围for语句遍历给定序列中的每个元素,并对序列中的每个值执行某种操作,语法形式为:
for(declaration:expression)
statement
其中,expression部分是一个对象,用于表示一个序列。declaration部分负责定义一个变量,该变量将被用于访问序列中的基础元素。每次迭代,declaration部分的变量会被初始化为expression部分的下一个元素值。
容器中,begin和end返回的具体类型由对象是否是常量决定,如果对象是常量,它们返回的是const_iterator;如果对象不是常量,则返回iterator。
vector<int> v;
const vector<int> cv;
auto it1 = v.begin(); //--it1的返回类型是vector<int>::iterator
auto it2 = cv.begin(); //--it2的返回类型是vector<int>::const_iterator
有时候这种默认的行为并不是我们想要的,有时只想要常量类型。为了便于专门得到const_iterator类型的返回值,c++11中引入了两个新函数,分别是cbegin(),cend()。不论容器对象本身是否是常量,返回值都是const_iterator。
使用指针遍历数组时,为了方便首尾指针,c++11中引入两个名为begin和end的库函数,这两个函数与容器中的两个同名成员功能相似,但是使用数组作为它们的参数。
int ia[] = {0,1,2,3,4,5,6,7,8,9};
int * beg = begin(ia); //--指向ia首元素地址
int * last = end(ia); //--指向ia尾元素的下一位置的地址
C++11新标准允许我们使用作用域运算符来获取类成员的大小。通常情况下只有通过类的对象才能访问类的成员,但是sizeof无须我们提供一个具体的对象,因为要想知道类成员的大小无须真的获取该成员。
class Sales_data
{
public:
int revenue;
};
auto n = sizeof Sales_data::revenue;
如果函数的实参数量未知但是全部实参的类型都相同时,可以使用initializer_list类型的形参,实现可变长参数函数。initializer_list是一种模版类型,定义initializer_list对象时,必须指明列表中所含元素的类型。而且initializer_list对象中的元素永远是常量值。
void error_msg(initializer_list<string> il)
{
for (auto & i:il)
{
cout << i.c_str() << " ";
}
cout << endl;
}
error_msg({"level1 ","level2","level3"}); //--调用方式
c++11新标准规定,函数可以返回花括号包围的值的列表。类似于其它返回结果,此处的列表也用来对表示函数返回值的临时量进行初始化。如果列表为空,临时量执行初始化;否则,返回的值由函数的返回类型决定。
vector<string> process(int type)
{
if (0==type)
{
return{ "type0" };
}
else
{
return{ "type1", "type 2", "type 3" };
}
}
如果函数返回的值是内置类型,则花括号包围的列表最多包含一个值,而且该值所占空间不应该大于目标类型的空间。
因为数组不能被拷贝,所以函数不能返回数组。不过函数可以返回数组的指针或引用,常见例子:
int(*func(int i))[10];
可以按照以下顺序来理解该声明的含义:
- func(int i),表示调用func时需要一个int类型的实参;
- (*func(int i)),表示可以对函数调用的结果执行解引用操作;
- (*func(int i))[10],表示解引用后得到一个大小是10的数组;
- int(*func(int i))[10],表示数组中的类型是int型。
C++11新标准中引入了一种简化上述func声明的方法,就是使用尾置类型。尾置返回类型跟在形参列表后面并以一个 -> 符号开头,在本应该出现返回类型的地方放置一个auto:
auto func(int i)-> int(*)[10]; //--返回一个指针,该指针指向含有10个整数的数组
这种形式对于返回类型比较复杂的函数最为有效。
constexpr函数是指能用于constexpr表达式的函数。遵循:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条语句。
constexpr int new_sz() { return 42; }
constexpr int foo = new_sz(); //--常量表达式
允许constexpr函数的返回值并非一个常量:
constexpr int new_sz() { return 42; }
constexpr size_t scale(size_t cnt) { return new_sz()*cnt; }
int arr[scale(2)]; //--正确
int i = 2;
int arr2[scale(i)]; //--错误:scale(i)不是常量表达式
当scale的实参是常量表达式时,返回值也是常量表达式,反之则不然。
注:返回类型是字面值类型,所谓字面值类型是指编译时就能得到结果的类型,具体包括算术类型、引用和指针。对于引用和指针,其限定比较严格。不是所有的指针都是常量表达式。只有那些在编译时就确 定地址指向的指针是才常量表达式,引用同理。
c++中,如果我们为一个类已经定义了其他构造函数,那么也必须定义一个默认构造函数。在C++11新标准中,如果我们需要默认的行为,那么可以通过在参数列表后面写上=default来要求编译器生成默认构造函数。
C++新标准扩展了构造函数初始值的功能,称做委托构造函数。一个委托构造函数使用它所属类的其它构造函数执行它自己的初始化过程,或者说它把自己的一些(或全部)职责委托给了其他构造函数。
如:
class Sales_data
{
public:
Sales_data(string s,unsigned cnt,double price) {}
Sales_data() :Sales_data("",0,0) {}
Sales_data(string s) :Sales_data(s,0,0) {}
Sales_data(double price) :Sales_data() {}
};
当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行,然后控制权交还给委托者的函数体。
常用的顺序容器有:
- vector 可变大小数组,支持快速随机访问,在尾部之外的位置插入或删除元素可能很慢
- deque 双端队列,支持快速随机访问,在头尾位置插入或删除速度很快
- list 双向链表,只支持双向顺序访问,在任何位置进行插入/删除操作都很快
- string 与vector相似的容器,但专门用于保存字符
另外c++11新增了两个容器:
- forward_list 单向链表,只支持单向顺序访问,在任何位置进行插入/删除操作都很快
- array 固定大小数组,支持快速随机访问,不能添加和删除元素
通常,使用vector是最好的选择,除非你有很好的理由选择其它容器。如程序要求在容器的中间插入或删除元素,可使用list或forward_list。
从c++11起,提供泛化版的swap函数。swap操作交换两个相同类型容器的内容。 注意: 除array以外,swap不对任何元素进行拷贝、删除或插入操作。swap只是交换了两个容器的内部数据结构。
在旧版本的标准库中,insert操作的返回值是void,新版本返回指向第一个新加入元素的迭代器。
新标准引入3个新成员——emplace_front,emplace,emplace_back。这些操作构造而不是拷贝元素。这些操作分别对应push_front,insert,push_back,允许把元素放在容器头部,一个指定位置之前或容器尾部。 当调用push或insert时把元素类型的对象传递给它们,这些对象被拷贝到容器中。 而当调用一个emplace成员时,则是将参数传递给元素类型的构造函数,emplace成员使用这些参数在容器管理的内存空间中直接构造元素。需注意,参数必须与元素类型的构造函数匹配。
在新标准库中,可以调用shrink_to_fit来要求deque,vector,或string退回不需要的内存空间。此函数指出我们不需要多余的内存空间,但是,具体的实现可以忽略此请求,调用shrink_to_fit也并不保证一定退回内存空间。
新标准中引入了lambda表达式,一个lambda表达式表示一个可调用的代码单元,可以将其理解为一个未命名的内联函数。与任何函数类似,一个lambda具有一个返回类型,一个参数列表和一个函数体。但与函数不同,lambda可能定义在函数内部,一个lambda表达式形式为:
[capture list] (parameter list) -> return type { function body }
其中capture list是一个lambda所在函数中定义的局部变量的列表(通常为空),return type,parameter list,function body与任何普通函数一样。
可以忽略参数列表和返回值类型,但必须永远包含捕获列表和函数体:
auto f = [] {return 42; };
忽略括号和参数列表等价于指定一个空参数列表。如果忽略返回类型,lambda根据函数体中的代码推断出返回类型,如果没有return语句,则返回类型为void。
一个lambda通过将局部变量包含在其捕获列表中使用这些变量,捕获列表指引lambda在其内部包含访问局部变量所需的信息。
新标准中引入bind函数,进行参数绑定。可以将bind函数看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来适应原对象的参数列表。 调用bind的一般形式为:
auto newCallable=bind(callable,arg_list)
其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。即,当我们调用newCallable时,newCallable会调用callable,并传递给它arg_list。arg_list中的参数可能包含形如_n的名字,其中n是一个整数。这些参数是占位符,表示newCallabel的参数,它们占据了传递给newCallable的参数的“位置”。如:_1为newCallable的第一个参数,_2为第二个参数,依此类推。
如:
void f_be_bind(int a,int b ,int c,int d ,int e)
{
cout << "a:"<<a<<" ,b:"<<b<<",c:"<<c<<",d:"<<d<<",e:"<<e <<endl;
}
auto f_bind = bind(f_be_bind,1,_2,3,4,_1);
f_bind(5, 2);
f_be_bind有5个参数,第一个参数被绑定为1,第三个参数被绑定为3,第四个参数为4,而第二个参数被绑定为新函数对象的第二个参数,第五个参数被绑定为新函数对象的第一个参数。
须知,_1,_2等占位符,在std::placeholders名字空间使用。
c++11提供了四种无序容器:
- unordered_map
- unordered_set
- unordered_multimap
- unordered_multiset
无序容器和有序容器有相同的操作,无序容器使用哈希函数进行组织,而普通容器使用红黑树进行组织。
为了更容易,也更安全的使用动态内存,新的标准中提供了两种智能指针类型为管理动态对象。shared_ptr允许多个指针指向同一个对象,unique_ptr则“独占”所指向的对象。
shared_ptr和unique_ptr实际上是模版类,分别结合make_shared和make_unique模版函数,能消除显示的new,delete操作。
注:make_unique是在C++14里引入的。
还有一个为了在特定场合使用的weak_ptr指针。weak_ptr是一种不控制所指向对象生存周期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数.一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使有weak_ptr指向对象。
weak_ptr的使用场合可以参考文章:
使用shared_ptr可能因循环引用而导致内存泄露,可以使用weak_ptr对资源时行监控。
我们可以通过将拷贝控制成员定义为=default来显示的要求编译器生成合成(默认)的版本。
class SalesData
{
public:
SalesData() = default; //--内联函数
SalesData(const SalesData &) = default;
SalesData & operator=(const SalesData &);
~SalesData() = default;
};
SalesData & SalesData::operator=(const SalesData &) = default; //--非内联函数
当在内中使用 = default修饰时,合成函数隐式声明为内联的,如果想不为内联,应该在类外定义。
对于拷贝构造函数,拷贝赋值运算符和析构函数,C++语言并不要求我们定义所有这些操作。那怎么决定是否定义?
-
需要析构函数的类也需要定义拷贝和赋值操作
通常,是否需要析构函数会很明显,如需要释放动态申请的内存。如果一个类需一个析构函数时,几乎可以肯定也需要一个拷贝构造函数和一个拷贝赋值运算符。因为,通常意味着有引用类型,需要深层拷贝。
-
需要拷贝操作的类也需要赋值操作,反之亦然
如果一个类需要拷贝构造函数,几乎可以肯定需要拷贝赋值运算符,反之亦然。但是不一定需要析构函数。
虽然大多数类应该定义拷贝构造函数和拷贝赋值运算符,但对某些类来说,这些操作没有合理的意义。在此情况下,定义类时必须采用某种机制阻止拷贝或赋值。为了阻止拷贝,看起来可能应该不定义拷贝控制成员。但是,这种策略是无效的:如果我们的类未定义这些操作,编译器为它生成默认的版本。
在新标准下,我们可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数(deleted function)来阻止拷贝。删除的函数是指:我们虽然声明了它们,但不能以任何方式使用它们。在函数的参数列表后面加在=delete来指出我们希望将它定义成为删除的:
class NoCopy
{
NoCopy() = default;
NoCopy(const NoCopy &) = delete; //--阻止拷贝
NoCopy & operator =(const NoCopy &) = delete; //--阻止赋值
~NoCopy() = default;
};
需要注意:
- 与=default不同,=delete必须出现在函数第一次声明的时候;
- 与=default不同,我们可以对任何函数指定=delete,虽然删除函数主要用途是用来阻止拷贝控制成员,但当我们希望引导函数匹配过程时,删除函数有时也是有用的;
- 不能删除析构函数,如果析构函数被删除,就无法销毁此类型的对象了;
- 在新标准之前,类是通过将其拷贝构造函数和拷贝赋值运算符声明为private的来阻止拷贝。
假设我们自己要为一个string数组写一个重新分配内存的成员函数,忽略具体细节,它应该:
- 为一个新的、更大的string数组分配内存
- 在内存空间的前一部分构造对象,保存现有元素
- 销毁原内存空间中的元素,并释放这块内存
可以看到,重新分配内存空间时,会把string从旧内存空间拷贝到新内存空间,而string具有类值行为,拷贝时会有两个副本,而拷贝完后再删除原有副本。因此,拷贝这些string中的数据是多余的,在重新分配内存空间时,如果我们避免分配和释放string的开销,性能会有很大提升。
通过使用新标准库引入的两种机制,我们可以避免string的拷贝。
- 第一个机制:移动构造函数
有一些标准类库,包括string,都定义了所谓的”移动构造函数“。移动构造函数如何工作和具体实现细节都未公布。但是,移动构造函数通常是将资源从给定对象”移动“而不是拷贝到正在创建的对象。而且,标准库保证”移后源“(moved-from)仍然保持一个有效的,可析构状态。对于string,可以想象每个string都有一个指向char数组的指针。可以假定string的移动构造函数进行了指针的拷贝,而不是为字符分配内存空间然后拷贝字符。
- 第二个机制:std::move函数
1) 调用move函数表示希望使用移动构造函数,如果漏掉了move调用,将会使用拷贝构造函数; 2) 通常不会为move调用using声明(TODO,为什么?),使用时,直接调用std::move而不是move。
为了支持移动操作,新标准引入了一种新的引用类型——右值引用(rvalue reference)。所谓右值引用就是必须绑定到右值的引用,通过&&而不是&获得右值引用。右值引用有一个重要的性质——只能绑定到一个将要销毁的对象。因此,可以自由地将一个右值引用的资源”移动“到另一个对象中。
类似任何引用,一个右值引用也不过是某个对象的另一个名字而已。对于常规引用(为了与右值引用区分,通常称之为左值引用),我们不能把它绑定到要求转换的表达式,字面常量或是返回右值的的表达式。右值引用有着完全相反的绑定特性:我们可以将一个右值引用绑定到这类表达式上,但不能将一个右值引用直接绑定到一个左值上:
int i = 42;
int & r = i; //--正确
int && rr = i; //--错误:不能将一个右值引用绑定到一个左值上,因为变量是左值
int & r2 = i * 42; //--错误:非常量引用的初始值必须为左值,而i*42为右值
const int & r3 = i * 42; //--正确
int && rr2 = i * 42; //--正确
考察左值和右值表达式的列表,两者相互区别很明显:左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。由于右值引用只能绑定到临时对象,因此我们得知:
- 所引用的对象将要被销毁;
- 该对象没有其他用户;
即,使用右值引用的代码可以自由地接管所引用的对象的资源。
变量可以看作只有一个运算对象而没有运算符的表达式,类似其他任何表达式,变量表达式也有左值/右值属性。变量表达式都是左值,带来的结果就是,我们不能将一个右值引用绑定到一个右值引用类型的变量上,所有有些经讶:
int && rrr1 = 42;
int && rrr2 = rrr1;
备注:左值与右值
C++的表达式要不然是左值要不然是右值,在C语言中:左值可以位于赋值语句的左侧,右值则不能;
在C++语言中,更加难以区分。一个左值表达式的求值结果是一个对象或一个函数,然而以常量对象为代表的某些左值实际上不能作为赋值语句的左侧运算对象。虽然某些表达式的求值结果是对象,但它们是右值而非左值。可以作一个简单的归纳:当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。
虽然不能将一个右值引用直接绑定到一个左值上,但我们可以显示的将一个左值转换为对应的右值引用类型。更合理的是,调用move标准库函数来获得绑定到左值上的右值引用,move函数返回给定对象的右值引用:
int && rrr3 = std::move(rrr1); //--正确
move调用告诉编译器:我们有一个左值,但我们想像右值一样使用它。必须认识到:调用move后就意味着除了对rrr1赋值或销毁它外,我们将不再使用它。在调用move之后,我们不能对移后源对象的值做任何假设。
为了让我们自己的的类型支持移动操作,需要为其定义移动构造函数和移动赋值运算符,这两个成员类似对应的拷贝操作,但它们从给定的对象”窃取“资源而不是拷贝资源。类似拷贝构造函数,移动构造函数的第一个参数是该类型的一个引用,不过该引用参数是一个右值引用。与拷贝构造函数一样,任何额外的参数都必须要有默认实参。 除了完成资源移动,移动构造函数还必须确保移后源对象处于这样一个状态——销毁它是无害的。特别是,一旦资源完成移动,源对象必须不再指向移动的资源——这些资源的所有权已经归属新创建的对象。
class SalesDataVet
{
public:
SalesDataVet() = default;
SalesDataVet(SalesDataVet && s) noexcept //--移动操作不应抛出任何异常
:elements(s.elements)
{
s.elements = nullptr; //--源对像必须不再指向被移动的资源
}
private:
SalesData * elements;
};
noexcept是新标准中引入的,表示一个函数不会抛出异常。
由于移动构造操作”窃取“资源,通常不会分配任何资源。因此,移动操作通常不会抛出任何异常,而且需要使用noexcept关键字显式通知给标准库。如果不显示通知,标准库会变得”保守“,比如该使用移动的地方而使用了拷贝。
移动赋值运算符执行与析构函数和移动构造函数相同的工作,也应该将它标记为noexcept。类似拷贝赋值运算符,移动赋值运算符必须正确处理自赋值:
class SalesDataVet
{
public:
SalesDataVet() = default;
SalesDataVet(SalesDataVet && s) noexcept //--移动操作不应抛出任何异常
:elements(s.elements)
{
s.elements = nullptr; //--源对像必须不再指向被移动的资源
}
SalesDataVet & operator=(SalesDataVet && s) noexcept
{
if (this != & s) //--检测自赋值
{
delete elements; //--释放已有元素
elements = nullptr;
elements = s.elements; //--接管资源
s.elements = nullptr;
}
return *this;
}
private:
SalesData * elements;
};
新标准中定义了一种移动迭代器适配器。一个移动迭代器通过改变给定迭代器的解引用运算符的行为来适配此迭代器。一般来说,一个迭代器的解引用运算符返回一个指向元素的左值。然而,移动迭代器的解引用运算符生成一个右值引用。 通过调用标准库的make_move_iterator将一个普通迭代器转换为一个移动迭代器。
在旧准中,没有办法阻止右值赋值,如:
string s1 = "value1";
string s2 = "value2";
s1 + s2 = "wow!";
此处,对两个字符的连接结果,一个右值进行了赋值。
为了维持向后兼容,新标准仍然允许向右赋值,但是我们可以在自己的类中阻止此类用法,可以强制左侧运算对象(即,this指向的对象)是一个左值;
指定方式与定义const成员函数相同,即,在参数列表后放置一个引用限定符。
引用限定符可以是&或&&,分别指出this可以指向一个左传或右值,类似const限定符,引用限定符只能用于(非static)成员函数,且必须出现在函数的声明和定义中。
class Foo
{
public:
Foo sorted() && ; //--右值限定符,调用对象必须为右值
Foo sorted() const &; //--const和左值限定符,调用对象为const或左值
private:
vector<int> data;
};
//--调用对象为右值,因此可以原址排序
Foo Foo::sorted() &&
{
sort(data.begin(),data.end());
return *this;
}
//--调用对象为const或左值,哪种情况都不能对其进行原址排序
Foo Foo::sorted() const &
{
Foo ret(*this);
sort(ret.data.begin(),ret.data.end()); //--对副本排序
return ret;
}
对象是右值,意味着没有其他用户,因此我们可以改变对象。
注意:如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符。
c++中有几种可调用的对象:函数,函数指针,lambda表达式,bind创建的对象以及重载了函数调用运算符的类。使用function类,两个不同类型的可调用对象能够共享同一种调用形式。例如,以前经常用map来存放函数指针,现在除了函数指针,可以把不同形式的可调用对象存放到同一个map中:
//--普通函数
int add(int a, int b) { return a + b; }
//--lambda函数
auto mod = [](int i, int j) {return i%j; };
//--函数对象类
struct devide
{
int operator() (int denominator,int divisor)
{
return denominator / divisor;
}
};
map<string, function<int(int, int)>> binops =
{
{"+",add},
{"-",std::minus<int>{}},
{"/",devide()},
{"%",mod}
};
类型转换符是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型。类型转换函数的一般形式如下:
operator type() const;
其中type表示某种类型。类型运算符可以面向任意类型(除了void外)进行定义,只要该类型能做为函数的返回类型。
类型转换运算符既没有显示的返回类型,也没有形参,而且必须定义成类的成员函数,而且通常定义成const成员。
一个例子:
定义一个SmallInt类,另其表示0~255间的整数。
class SmallInt
{
public:
SmallInt() = default;
SmallInt(int i):val(i) //--值转换为对象
{
if (i<0 || i>255)
{
throw std::out_of_range("Bad value");
}
}
operator int() const //--对象转换为值
{
return val;
}
private:
size_t val;
}
该类,构造函数将算术类型的值转为对象类型,而类类型转换符将对象类型转为int。
SmallInt si;
si = 4; //--首先把4隐式转换成SmallInt,然后调用SmallInt::operator=
si + 3; //--首先把si转换成int,再执行加法
更进一步,我们可以将任何算术类型传递给SmallInt的构造函数。类似的,也能使用类型转换运算符将一个SmallInt对象转换成int,然后再将int转换成任何其他算术类型。
si = 3.14; //--内置类型转换,double先转为int,然后调用SmallInt(int)构造函数
si + 3.14; //--类型转换运算符先把si转为int,然后内置类型转换,int转为double
此类类型转换符的隐式调用可能会引起意想不到的问题,比如当istream含有向bool的转换时,下面的代码将编译通过:
int i=42;
cin << i;
这段代码试图将输出运算符<<作用于输入流,而原本istream没有定义<<,编译器应该报错。然而,istream使用隐式的类型转换符,把cin转换为bool型,而这个bool值被提升为int型,并用作左移运算符的左侧对象。这样一来,提升后的bool值最终会被左移42位,与我们的预期大相径庭。
为了防止这样的情况发生,C++11新标准引入了显示的类型转换运算符:
class SmallInt
{
public:
SmallInt() = default;
SmallInt(int i):val(i) //--值转换为对象
{
if (i<0 || i>255)
{
throw std::out_of_range("Bad value");
}
}
explicit operator int() const //--使用explicit关键字,对象不再隐式转换为值
{
return val;
}
private:
size_t val;
};
此时:
SmallInt si;
si = 4;
si + 3; //--错误,没有与这些操作匹配的'+'运算符
static_cast<int>(si) + 3; //--正确,显示地请求类型转换
有时会定义这样一种类,我们不希望其它类继承它,或者不想考虑它是否适合作为一个基类。c++新标准提供了一种防止继承发生的方法,即在类名后面跟一个关键字final。
class NoDerived final
{
// do something
};
派生类如果定义了一个函数与基类中的虚函数的名字相同但是形参列表不同,这仍然是合法的行为,编译器认为新定义的这个函数与基类中原有的函数是相互独立的。这时,派生类的函数并没有覆盖掉基类中的版本。在编程实践中,这种声明往往是意味着发生了错误,本意应该是希望派生类的函数覆盖掉基类中的函数。
要调试此类错误非常困难,在C++11中可以使用override关键字来说明派生类的的虚函数,让编译器帮我们发现错误。如果我们使用了override关键字标记了某个函数,但该函数并没有覆盖已存在的虚函数,此时编译器将报错:
struct B
{
virtual void f1(int) const;
virtual void f2();
void f3();
};
struct D1 :B
{
void f1(int) const override; //--正确
void f2(int) override; //--错误,基类中没有f2(int)的定义
void f3() override; //--错误,f3()不是虚函数
void f4() override; //--错误,基类中没有f4()定义
};
还可以把某个函数指定为final,之后任何尝试覆盖该函数的操作都会引起错误:
struct B
{
virtual void f1(int) const;
virtual void f2();
virtual void f3() final;
};
struct D1 :B
{
void f1(int) const override final;
};
struct D2 :D1
{
void f1(int) const override; //--错误,D1中指定为final
void f2() override; //--正确
void f3() override; //--错误,B中指定为final
};
在C++11新标准中,派生类能够重用其直接基类定义的构造函数。这些构造函数并非以常规的方式继承而来(而是使用using关键字继承)。一个派生类只初始化它的直接基类,同样的原因,一个类也只继承其直接基类的构造函数,类不能继承默认、拷贝和移动构造函数。
class Disc_quote
{
public:
Disc_quote(const string & book,double price);
};
class Bulk_quote:Disc_quote
{
public:
using Disc_quote::Disc_quote; //--继承自Disc_quote的构造函数
};
通常情况下,using关键字只是令某个名字在当前作用域内可见。而当作用于构造函数时,using声明语句将令编译器产生代码。对于基类的每个构造函数,编译器都生成一个与之对应的派生类构造函数。上面的例子中,等价于编译器生成了一个构造函数:
Bulk_quote(const string & book, double price) :Disc_quote(book, price) {}
如果派生类有自己的成员,则这些成员将被默认初始化。
在新标准中,我们可以将模版类型参数声明为友元:
template <typename Type>
class Bar
{
friend Type; //--将访问权限授予用来实例化bar的类型
};
对于某个类型名为Foo,Foo将成为Bar的友元,SalesData将成为Bar的友元。
类模版的一个实例定义了一个类型,因此可以定义一个typedef来引用实例化的类:
typedef Bob<string> StrBob;
但是,由于一个模版不是一个类型,我们不能定义一个typedef引用一个模版,即,无法定义:
typedef Bob<T> TBob;
但是,新标准中允许我们为类模版定义一个类型别名:
template <typename T> using twin = pair<T,T>;
我们将twin定义为成员类型相同的pair的别名,这样twin的用户只需指定一次类型:
twin<string> authors; //--等价于pair<string,string> authors;
twin<int> price; //--等价于pair<int,int> price;
就像我们能为函数参数提供默认参数一样,我们也可以提供默认模版实参。在新标准中,我们可以为函数和类模版提供默认实参。而更早的C++标准只允许为类模版提供默认实参。
//--compare有一个默认模板实参less<T>和一个默认函数实参F()
template <typename T,typename F=less<F>>
int compare(const T & v1,const T & v2,F f=F())
{
}
当模版被使用时才会进行实例化,这一特性意味着,相同的实例可能出现在多个对象文件中。当两个或多个独立编译的源文件使用了相同的模版,并提供了相同的模版参数时,每个文件中就都会有该模版的一个实例。
在大系统中,在多个文件中实例化相同模版的额外开销可能非常严重。在新标准中,可以通过显示实例化(explicit instantiation)来避免这种开销,一个显示实例化有如下形式:
extern template declaration; //实例化声明
template declaration; //实例化定义
declaration是一个类或函数声明,其中所有模版参数已被替换为模版实参。例如:
extern template class Bob<string>; //--声明
当编译器遇到extern模版声明时,它不会在本文件中生成实例化代码。对于一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义。
现在有一个模版函数f3:
template<typename T>
void f3(T &&) //--右值引用
{
}
假定i是一个int对象,我们可能认为像f3(i)这样的调用是不合法的。毕竟,i是一个左值,而通常我们不能将一个右值引用绑定到一个左值上,但是C++语言在正常绑定规则之外定义了两个例外规则:
-
编译器推断右值引用参数的类型
当我们将一个左值(如i)传递给函数的右值引用参数,且此右值引用指向模版类型参数(如T &&)时,编译器推断模版类型参数为实参的左值引用。因此,当我们调用f3(i)时,编译器推断T的类型为int &,而非int。T被推断为int &,好像意味着f3的函数参数应该是一类型为int &的右值引用。通常,我们不能(直接) 定义一个引用的引用,但是通过类型别名或通过模版类型参数间接定义是可以的。
-
引用折叠
如果我们间接创建一个引用的引用,则这些引用形成了”折叠“。在所有情况下(除了一个例外),引用会折 叠成一个普通的左值引用类型。在新标准中,折叠规则扩展到右值引用。只有一种特殊情况下,引用会折叠成 右值引用:右值引用的右值引用。即,对于一个给定类型X:
- X& &,X& &&和 X&& &都折叠成X&;
- 类型X&& && 折叠成 X&&;
注:在我们的编程实践中,应该避免类似定义。
某些函数需要将其一个或多个实参连同类型不变地转发给其他函数。在此情况下,我们需要保持被转发实参的所有性质,包括实参类型是否是const的以及实参是左值还是右值。
作为一个例子,定义一个翻转函数,它接受一个可调用表达式和两个额外实参。该函数将调用给定的可调用对象,将两个参数逆序传递给它:
template<typename F,typename T1,typename T2>
void flip1(F f,T1 t1,T2 t2)
{
f(t2, t1);
}
这个函数一般情况下工作得很好,但当我们希望用它调用一个接受引用参数的函数时会出现问题:
void f(int v1,int & v2) //--注意v2是一个引用
{
cout << v1 << ++v2<<endl;
}
此时:
int i = 50;
f(42,i); //--f改变了实参i
int j = 50;
flip1(f,j,42); //--flip1不会改变实参j
问题在于j被传递给flip1的参数t1。此参数是一个普通的,非引用的类型int,而非int &。因此,这个flip1调用实例化为:
void flip1(void(*fcn)(int ,int &),int t1,int t2);
j的值被拷贝到t1中。f中的引用参数被绑定到t1,而非j,从而其改变不会影响j。
为了通过翻转函数传递一个引用,我们需要重写函数,使其参数能保持给定实参的"左值性”。通过将一个函数参数定义为一个指向模版类型参数的右值引用,我们可以保持其对应实参的所有类型。如果把函数参数定义成T1 && 和 T2 &&,通过引用折叠,就可以保持翻转实参的左值/右值属性。
template<typename F,typename T1,typename T2>
void flip2(F f,T1 && t1,T2 && t2)
{
f(t2,t1);
}
与flip1的版本一样,如果我们调用flip2(f,j,42),将传递给参数t1一个左值j。但是flip2中,由于引用折叠,推断出T1的类型为int &。由于是引用类型,f中值做了改变时,同时也改变了j的值。
注:如果一个函数参数是指向模版类型参数的右值引用(如 T&& ),它对应的实参的const属性和左值/右值属性将得到保持。
该版本的flip2解决了一半问题,它对于接受一个左值引用的函数工作得很好,但不能用于接受右值引用参数的函数。例如:
void g(int && i,int & j)
{
cout << i << ' ' << j << endl;
}
如果试图通过flip2调用g,则参数t2将被传递给g的右值引用参数,即使我们传递一个右值给flip2:
flip2(g,i,42); //--编译器报:无法将参数从int转变为int &&
存在以上种种情况,新标准中提供了forward标准库,它能保持原始实参的类型。与move不同,forward必须通过显示模版来实参调用。forward返回该显示实参类型的右值引用。即,forward返回类型是T&&。
以forward重写flip版本:
template<typename F, typename T1, typename T2>
void flip(F f, T1 && t1, T2 && t2)
{
f(std::forward<T2>(t2),std::forward<T1>(t1));
}
如果我们调用:
flip(g,i,42);
i将以类型int &传递给g,42将以类型int &&传递给g。
一个可变参数模版就是一个接受可变数目参数的模版函数或模版类。可变数目的参数被称为参数包。存在两种参数包:模版参数包,表示零个或多个模版参数;函数参数包,表示零个或多个函数参数。
用一个省略号来指出一个模版参数或函数表示一个包,如:
template<typename ... Args>
void foo(const Args & ... rest)
{
}
声明了foo是一个可变参数函数模版,表示可以接受任意类型的参数。
当我们需要知道包中有多少个元素时,可以使用sizeof...运算符,返回一个常量表达式。
template<typename ...Args>void args_num(Args ... args)
{
cout <<"number of class type: "<< sizeof...(Args) << endl; //--类型参数的数目
cout << "number of fun params: " << sizeof...(args) << endl; //--函数参数数目
}
tuple是类似pair的模板,每个pair的成员类型都不相同,但每个pair都恰好有两个成员。不同tuple类型的成员类型也不一样,但一个tuple可以有任意数量的成员。当我们希望将一些数据组合成单一的对象,但又不想麻烦地定义一个新数据结构来表示这些数据时,tuple是非常有用的。
c++新标准引入了限定作用域的枚举类型,形式是:关键字enmu class(或enum struct),随后是枚举类型名字及用花括号括起来的以逗号分隔的枚举成员列表,最后是一个分号:
enum class open_mode {input,output,append};
平时常使用的,不使用关键字class(struct)的枚举类型,就是不限定作用域的类型:
enum color {red,yellow,green};
在限定作用域的枚举类型中,枚举成员的类型遵循常规的作用域准则,并且在枚举类型的作用域以外是不可访问的。而在不限定作用域的枚举类型中,枚举成员的作用域与枚举类型本身的作用域相同。
enum color {red,yellow,green};
enum stoplight {red,yellow}; //--错误,重复定义了枚举成员
enum class papertype {red,yellow,green}; //--正确,枚举成员被隐藏
papertype p1 = red; //--错误,red是stoplight的成员,而不是papertype的,类型错误
papertype p2 = papertype::red; //--正确
尽管每个enum都定义了唯一的类型,但实际上enum是由某种整数类型表示的。在新标准中,我们可以在enum的名字后加上冒号以及我们想在该enum中使用的类型:
enum nav_mode :char
{
adf,
vor,
mmr,
gps
};
默认情况下限定作用域的默认类型是int,而不限定作用域没有默认类型,只需知道成员的潜在类型足够大,肯定能容纳枚举值。
指定enum潜在类型的能力使得我们可以控制不同实现环境中使用的类型,确保可移植性。
=================以下为C++14特性====================
在C++11中,lambda函数的形式参数需要被声明为具体的类型。C++14放宽了这一要求,允许lambda函数的形式参数声明中使用类型说明符auto。
auto lambda = [](auto x, auto y) {return x + y;}
C++11的lambda函数通过值拷贝(by copy)或引用(by reference)捕获(capture)已在外层作用域声明的变量。这意味着lambda捕获的变量不可以是move-only的类型。C++14允许lambda成员用任意的被捕获表达式初始化。这既允许了capture by value-move,也允许了任意声明lambda的成员,而不需要外层作用域有一个具有相应名字的变量。这称为广义捕获(Generalized capture)。即在捕获子句(capture clause)中增加并初始化新的变量,该变量不需要在lambda表达式所处的闭包域(enclosing scope)中存在;即使在闭包域中存在也会被新变量覆盖(override)。新变量类型由它的初始化表达式推导。用途是可以从外层作用域中捕获只供移动的变量并使用它。
这是通过使用一个初始化表达式完成的:
auto lambda = [value = 1] {return value;}
lambda函数lambda的返回值是1,说明value被初始化为1。被声明的捕获变量的类型会根据初始化表达式推断,推断方式与用auto声明变量相同。
另外,也可以通过move捕获:
auto ptr = std::make_unique<int>(10);
auto lambda = [value = std::move(ptr)] {return *value;}
C++11允许lambda函数根据return语句的表达式类型推断返回类型,C++14拓展了原有的规则,使得函数体并不是{return expression;}形式的函数也可以使用返回类型推导。为了启用返回类型推导,函数声明必须将auto作为返回类型,但没有C++11的尾置返回类型说明符:
auto DeduceReturnType(); //返回类型由编译器推断
如果函数实现中含有多个return语句,这些表达式必须可以推断为相同的类型。使用返回类型推导的函数可以前向声明,但在定义之前不可以使用。这样的函数中可以存在递归,但递归调用必须在函数定义中的至少一个return语句之后:
auto Correct(int i) {
if (i == 1)
return i; // 返回类型被推断为int
else
return Correct(i-1)+i; // 正确,可以调用
}
auto Wrong(int i)
{
if(i != 1)
return Wrong(i-1)+i; // 不能调用,之前没有return语句
else
return i; // 返回类型被推断为int
}
C++11中有两种推断类型的方式。auto根据给出的表达式产生具有合适类型的变量。decltype可以计算给出的表达式的类型。 C++14增加了decltype(auto)的语法,可以用来声明变量以及指示函数返回类型。
当decltype(auto)被用于声明变量时,该变量必须立即初始化。假设该变量的初始化表达式为e,那么该变量的类型将被推导为decltype(e)。也就是说在推导变量类型时,先用初始化表达式替换decltype(auto)当中的auto,然后再根据decltype的语法规则来确定变量的类型。
decltype(auto)也可以被用于指示函数的返回值类型。假设函数返回表达式e,那么该函数的返回值类型将被推导为decltype(e)。也就是说在推导函数返回值类型时,先用返回值表达式替换decltype(auto)当中的auto,然后再根据decltype的语法规则来确定函数返回值的类型。
int a = 0;
decltype(auto) i1 = a;
decltype(auto) fun_deduced_by_decltypeauto()
{
return 10;
}
在C++之前的版本中,模板可以是函数模板或类模板。C++14现在也可以创建变量模板。在提案中给出的示例是变量pi,其可以被读取以获得各种类型的pi的值(例如,当被读取为整数类型时为3;当被读取为float,double,long double时,可以是近似float,double或long double精度的值)包括特化在内,通常的模板的规则都适用于变量模板的声明和定义。
template<typename T>
constexpr T pi = T(3.141592653589793238462643383);
// 适用于特化规则 :
template<>
constexpr const char* pi<const char*> = "pi";
C++11增加了default member initializer,如果构造函数没有初始化某个成员,并且这个成员拥有default member initializer,就会用default member initializer来初始化成员。聚合类(aggregate type)的定义被改为明确排除任何含有default member initializer的类类型,因此,如果一个类含有default member initializer,就不允许使用聚合初始化。
C++14放松了这一限制,含有default member initializer的类型也允许聚合初始化。如果在定义聚合体类型的对象时,使用的花括号初始化列表没有指定该成员的值,将会用default member initializer初始化它。
struct CXX14_aggregate {
int x;
int y = 42;
};
CXX14_aggregate a = {1}; // C++14允许。a.y被初始化为42
C++14的数字可以用二进制形式指定,其格式使用前缀0b或0B。
C++14引入单引号(')作为数字分位符号,使得数值型的字面量可以具有更好的可读性。
auto integer_literal = 100'0000;
auto floating_point_literal = 1.797'693'134'862'315'7E+308;
auto binary_literal = 0b0100'1100'0110;
auto silly_example = 1'0'0'000'00;
deprecated属性允许标记不推荐使用的实体,该实体仍然能合法使用,但会让用户注意到使用它是不受欢迎的,并且可能会导致在编译期间输出警告消息。 deprecated可以有一个可选的字符串文字作为参数,以解释弃用的原因和/或建议替代者。
[[deprecated]] int f();
[[deprecated("g() is thread-unsafe. Use h() instead")]]
void g( int& x );
void h( int& x );
void test() {
int a = f(); // 警告:'f'已弃用
g(a); // 警告:'g'已弃用:g() is thread-unsafe. Use h() instead
}