c++11的特性
前言
本文是记录个人对c++11新特性的学习,文中所用可能会采用其他大神的图、思路、例子等,请大神们见谅。本博客文章是在学习过程的一些总结,整理出来,分享给大家,希望对各位读者有帮助,文章中的总结可能存在很多不完整或有错误的地方,也希望读者指出。
1、auto关键字
本章节为auto关键字的c++11的新语法特点;在C++98中auto关键字用于声明变量为自动变量,这是多余的,因为就算不使用auto声明,变量依旧拥有自动的生命期。
1.1、auto关键字概述
c++11中auto关键字作用:
1)声明变量时根据初始化表达式自动推断该变量的类型
2)声明函数时函数返回值的占位符,需要与关键字 decltype 一起使用
类型推到规则:
1)auto变量(不是auto&),忽视掉初始化表达式的顶层const
2)auto&变量,保持初始化表达式的顶层const或volatile 属性
3)若希望auto推导的是顶层const,加上const,即const auto
举例1:
int i = 0, &ri = i;
auto a = i; //a为int型变量
auto a1 = ri; //a1为int型变量
auto p = &i;// &i 是一个普通int指针,p是一个整型指针int *
auto p1 = &ri; //同上
举例2:
const int ci = 2, &rci = ci , ci2 = 9;
auto b = ci;//b为int型变量,因为规则1,b`并不是一个const int型的常量`
auto b1 = rci;//同上
b = 4;b1 = 5;//b和b1的值可以改变
auto cp = &ci;//cp是一个指向常量的指针const int* ,因为&ci对常量对象取地址是底层const,无顶层const属性
cp = &ci2;//cp的指向可以改变
举例3:
int z = 9,z1 = 10;
int* const pz= &z;
auto apz = pz; //因为规则1,忽视pz的顶层const属性,所以apz为int*
apz = &z1;
pz为常量指针,声明apz,用pz初始化,pz可以改变指向;但是如果pz的声明是const int* const,这apz为const int* 类型
注意:对常量对象取地址总是看作为一种底层的const,即不是对指针(也就是指向)的常量,而是对指针指向内存中的值是常量const,如举例2中的&ci是const int* 类型
举例4:const auto
int i = 0, &ri = i;
const int ci = 2, &rci = ci ;
const auto cb = i; //cb为const int型。因为规则3,cb被提升为const
const auto cb1 = ci; //同上
const auto ca1 = &i;//cal为常量指针。&i本是int*,因为规则2,强行将cal提升为常量指针int *const
const auto ccp = &ci;//本来&ci为const int *,因为规则2,加了const后,提示为const int * const
举例5:auto &
int i = 0, &ri = i;
const int ci = 2, &rci = ci ;
auto & j = i; //j为int &
auto & k = ci; // k为const int &
auto & h = 42; //错误,不能将非常量引用绑定字面值,这是引用&规则决定的
const auto &j2 = i; //j2为const int &,因为规则3,j2被提升为顶层const
const auto &k2 = ci; //k2为const int &
const auto &h2 = 42; //正确,可以为常量绑定字面值
auto& m = &i;//Error,无法从“int *”转换为“int *&” ,这是引用&规则决定的
auto& m1 = &ci;// Error,无法从“const int *”转换为“const int *&” ,这是引用&规则决定的
const auto &m2 = &i;//m2为int * const &
const auto &m3 = &ci;//m3为const int * const &
上述错误原因:引用不能绑定表达式的计算结果,除非使用const。并且对于指针而言,它提升到顶层const,只能为常量指针
小结:使用auto &的时候不光需要知道auto&的推到规则,还要明白引用(&)的使用限制。我们首先看的就是&的使用限制
1.2、运用场景
1)用于代替冗长复杂、变量使用范围专一的变量声明
举例:
#include<string>
#include<vector>
int main()
{
std::vector<std::string> vs;
for (std::vector<std::string>::iterator i = vs.begin(); i != vs.end(); i++)
{
//...
}
for (auto i = vs.begin(); i != vs.end(); i++)
{
//..
}
}
2)在定义模板函数时,用于声明依赖模板参数的变量类型
template <typename _Tx,typename _Ty>
void Multiply(_Tx x, _Ty y)
{
auto v = x*y;
std::cout << v;
}
3)模板函数依赖于模板参数的返回值
template <typename _Tx, typename _Ty>
auto multiply(_Tx x, _Ty y)->decltype(_Tx*_Ty)
{
return x*y;
}
1.3、注意事项
1)auto 变量必须在定义时初始化,这类似于const关键字
2)定义在一个auto序列的变量必须始终推导成同一类型
3)auto关键字做类型自动推导规则简述
a.如果初始化表达式是引用,则去除引用语义
int a = 10;
int &b = a;
auto c = b;//c的类型为int而非int&(去除引用)
auto &d = b;//此时c的类型才为int&
c = 100;//a =10;
d = 100;//a =100;
b.如果初始化表达式为const或volatile(或者两者兼有),则除去const/volatile语义
const int a1 = 10;
auto b1= a1; //b1的类型为int而非const int(去除const)
const auto c1 = a1;//此时c1的类型为const int
b1 = 100;//合法
c1 = 100;//非法
c.如果auto关键字带上&号,则不去除const语意
const int a2 = 10;
auto &b2 = a2;//因为auto带上&,故不去除const,b2类型为const int
b2 = 10; //非法
d.初始化表达式为数组时,auto关键字推导类型为指针, 除非为数组且auto带上&,则推导类型为数组类型
int a3[3] = { 1, 2, 3 };
auto b3 = a3;
cout << typeid(b3).name() << endl; /* 程序输出是int* */
auto & b7 = a7;
cout << typeid(b7).name() << endl; /* 程序输出是int[3] */
e.函数或者模板参数不能被声明为auto
f.时刻要注意auto并不是一个真正的类型。仅仅是一个占位符,它并不是一个真正的类型,不能使用一些以类型为操作数的操作符,如sizeof或者typeid
2、nullptr关键字
1)null和nullptr的对比:
C++11标准之前,编译器进行解释程序时,NULL会被直接解释成0,而C语言中将null作为(void *)类型
C++11的出现后为了规避这个问题,nullptr在C++11中就是代表空指针
2)nullptr和void * 对比:
nullptr习惯被称作指针空 ;void*习惯被称作无类型指针
3)nullptr和nullptr_t 对比:
nullptr习惯被称作指针空 ;
nullptr_t习惯被称作指针空值类型 ,即表示指针空值类型并非仅有nullptr一个实例,可以通过nullptr_t来声明一个指针空值类型的变量
总结:
std::nullptr_t,空指针常数可以转换为任意类型的指针类型,但(void )不能转化为任意类型的指针,即 int *p=(void)是错误的,但int *p=nullptr是正确的。
拓展-C++11标准严格规定了数据间的关系规则:
1)所有定义为nullprt_t类型的数据都是等价的,行为也完全一致
2)nullprt_t类型数据可以隐式转换成任意一个指针类型,不能转换为非指针类型, 使用reinterpret_cast<nullptr_t>也是不可以
3)nullptr_t类型数据不适用算术运算表达式,nullptr_t类型数据可以用于关系运算表达式,但仅能与nullptr_t类型数据或者指针类型数据进行比较,当且仅当关系运算符为==,<=,>=等时返回true
3、using关键字
1)using 在 C++11之前主要用于名字空间、类型、函数与对象的引入,实际上是去除作用域的限制
//引入名字空间
using namespace std;
//引入类型
using std::iostream;
//引入函数
using std::to_string;
//引入对象
using std::cout;
2)通过using引入函数可以解除函数隐藏
派生类的函数屏蔽了与其同名的基类函数,规则如下:
1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)
2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)
class Base{
public:
void func() { cout << "in Base::func()" << endl; }
void func(int n) { cout << "in Base::func(int)" << endl;}
};
class Sub : public Base {
public:
using Base::func; //引入父类所有同名函数func,解除函数隐藏
void func() { cout<<"in Sub::func()"<<endl;}
};
3)使用 using 代替 typedef,给类型命名
using uint8 = unsigned char; //等价于typedef unsigned char uint8;
using FunctionPtr = void (*)(); //等价于typedef void (FunctionPtr)();
template using MapString = std::map<T, char>; //定义模板别名,注意typedef无法定义模板别名,因为typedef只能作用于具体类型而非模板
4、decltype关键字
decltype与auto关键字一样,用于进行编译时类型推导,decltype则可以从一个变量或表达式中得到类型
注意:decltype 仅仅“查询”表达式的类型,并不会对表达式进行“求值”
map<string, float> cell;
decltype(cell)::value_type elem;//推断容器获得类型
4.1 decltype推导原则
如果e是一个没有带括号的标记符表达式或者类成员访问表达式,那么的decltype(e)就是e所命名的实体的类型。此外,如果e是一个被重载的函数,则会导致编译错误
否则 ,假设e的类型是T,如果e是一个将亡值,那么decltype(e)为 T&&
否则,假设e的类型是T,如果e是一个左值,那么decltype(e)为 T&
否则,假设e的类型是T,则 decltype(e)为 T
1)推导出表达式类型
// 1-基本用法
double a = 2;
decltype(a) a1; cout << typeid(a1).name() << endl;
// 2-只推断类型,不调用函数
decltype(foo()) b; cout << typeid(b).name() << endl;
// 3-与 const 结合
double c = 3.0;
decltype(c) c1; cout << typeid(c1).name() << endl;
decltype(c) c2 = 3.2;
const double d = 5.0;
// decltype(d) d1; // err, 推断出师 const double,所以必须初始化赋值
decltype(d) d2 = 6.3; cout << typeid(d2).name() << endl;
decltype(d) d3 = 5.2; // err, 不允许赋值
const double * const e = &c;
// decltype(e) e2 = 1.2; // err, 不允许赋值
decltype(e) e3 = &a;
// *e3 = 3.5; // err, 不允许改变值
// 4-与 reference 结合
int f = 0, &rf = f;
decltype(rf) rf1 = f; // 推断出为 reference, 正确
// decltype(rf) rf2 = 0; // 必须引用变量,不能使常量
// decltype(rf) rf3; // 必须初始化
decltype((f)) rf4 = f; // f 为左值,(f)为左值表达式即为引用,所以引用要初始化
const int g = 1, &rg = g;
decltype(rg) rg1 = g; // 引用 const 变量
decltype(rg) rg2 = 1; // 引动常量
// decltype(rg) rg3; // err, 必须初始化
// 5-与指针结合
int h = 2;
int *ptrH= &h;
decltype(ptrH) ptrH2;
// decltype(*ptrH) ptrH3; // err, 表达式内容为解指针操作,ptrH3 为一个引用,引用必须初始化,故编译不过
struct A { double x; };
const A* a = new A{0};
//第6种情况
decltype(a->x) y; // type of y is double
decltype((a->x)) z = y; // type of z is const double&,因为a一个常量对象指针
//第7种情况
int* aa=new int;
decltype(*aa) y=*aa; //type of y is int&,解引用操作
//第8种情况
decltype(5) y; //type of y is int
decltype((5)) y; //type of y is int
const int&& RvalRef() { return 1; }
decltype ((RvalRef())) var = 1; //type of var is const int&&
2)与using/typedef合用,用于定义类型
using size_t = decltype(sizeof(0));//sizeof(a)的返回值为size_t类型
using ptrdiff_t = decltype((int*)0 - (int*)0);
using nullptr_t = decltype(nullptr);
vector<int>vec;
typedef decltype(vec.begin()) vectype;
for (vectype i = vec.begin; i != vec.end(); i++){...}
3)重用匿名类型
struct {
int d ;
doubel b;
}anon_s;
decltype(anon_s) as ; //定义了一个上面匿名的结构体
注意:匿名类型有其匿名的原因,一般情况下,匿名类型不应该被重用,应尽量避免这种用法
4)泛型编程中结合 auto,用于追踪函数的返回值类型,这是 decltype的最大用途
template <typename _Tx, typename _Ty> auto multiply(_Tx x, _Ty y)->decltype(x*y) { return x*y; }
5.constexpr 关键字
constexpr 在 C++11 中用于申明常量表达式 (const expression),可作用于函数返回值、函数参数、数据申明以及类的构造函数等
constexpr的好处:
1)是一种很强的约束,更好地保证程序的正确语义不被破坏。
2)编译器可以在编译期对constexpr的代码进行非常大的优化,比如将用到的constexpr表达式都直接替换成最终结果等。
3)相比宏来说,没有额外的开销,但更安全可靠。
5.1 constexpr应用
1)常量表达式函数
如果函数返回值在编译时期可以确定,那么可以使用constexpr修饰函数返回值,使函数成为常量表达式函数
constexpr int f(){return 1;}
注意:constexpr修饰函数返回值需要满足如下条件:
(a)函数必须有返回值;
(b)函数体只有单一的return语句;
(c)return语句中的表达式也必须是一个常量表达式;
(d)函数在使用前必须已有定义。
2)常量表达式值
如果认定变量是一个常量表达式,那就把它声明为constexpr类型
在constexpr声明中,如果定义了一个指针,constexpr仅对指针有效,与指针所指对象无关
const int *p=nullptr; //p是一个指向整型常量的指针(pointer to const)
constexpr int *p1=nullptr; //p1是一个常量指针(const pointer)
自定义类型对象为常量表达式,那么在定义自定义类型时,需要将constexpr作用于自定义类型的构造函数
struct MyType {
int i;
constexpr MyType(int x):i(x){}
};
constexpr MyType myType(1);
constexpr作用于自定义类型的构造函数需要满足如下条件:
(a)构造函数体必须为空;
(b)初始化列表只能使用常量表达式
3)常量表达式作用于函数模板
可以作用于函数模板,但是由于函数模板参数的不确定性,实例化后的模板函数可能不满足常量表达式的条件,此时,C++11标准规定,自动忽略constexpr
struct NotConstType {
int i;
NotConstType(int x) :i(x) {}
};
NotConstType myType;
//constexpr作用于函数模板
template <typename T> constexpr T ConstExpFunc(T t) {
return t;
}
int main(){
NotConstType objTmp = ConstExpFunc(myType); //编译通过,ConstExpFunc实例化为普通函数,constexpr被忽略
constexpr NotConstType objTmp1 = ConstExpFunc(myType); //编译**失败**
constexpr int a = ConstExpFunc(1); //编译通过,ConstExpFunc实例化为常量表达式函数
}
4)constexpr元编程
constexpr可以作用于递归函数来实现编译时期的数值计算,即constexpr元编程。C++11标准规定,常量表达式应至少支持512层递归
constexpr int Fibonacci(int n){
return (n == 1) ? 1 : (n == 2 ? 1 : Fibonacci(n - 1) + Fibonacci(n - 2));
}
int main(){
constexpr int fib8 = Fibonacci(8); //编译期常量等于21
}
5.2 constexpr 与 const 的区别
1)const 可以修饰函数参数、函数返回值、函数本身、类等,在不同的使用场景下,const具有不同的意义,不过大多数情况下,const描述的是“运行时常量性”,即在运行时数据具有不可更改性
2)constexpr可以修饰函数参数、函数返回值、变量、类的构造函数、函数模板等,是一种比const更加严格的约束,它修饰的表达式除了具有“运行时常量性”,也具有“编译时常量性”,即constexpr修饰的表达式的值在编译期间可知
const int getConst(){ return 1; }
enum{ e1=getConst(),e2}; //编译出错
//换成constexpr即可在编译期确定函数返回值用于初始化enum常量
constexpr int getConst(){ return 1; }
enum{ e1=getConst(),e2}; //编译OK