C++11,17新特性
参考学习自Effective Modern C++
{},别名using,nullptr,新for循环,智能指针,移动语义,强枚举类型,右值引用,default与delete
创建对象时注意区分()和{}
-
大括号初始化可应用语境最宽泛
有三种指定初始化值的方式
int x(0); //方式1,不能为非静态成员指定默认初始化值 int y=0; //方式2,不能为不可复制对象初始化,如std:atomic int z{0}; //方式3,等价于int z={0};
- C++11引入统一初始化,即可用于一切场合的初始化,基础为大括号
- 大括号可以指定容器初始内容,可以进行上述方式12不能执行的初始化
- 大括号初始化新特性:禁止内建型别之间进行隐式窄化型别转换,而方式12则不会检查,并可能直接截断初始化值
-
C++有最令人苦恼的解析语法:任何能够解析为声明的都要解析为声明,大括号可以避免
class Student { Student(const char*name):student_name(name){} const char*student_name; } //main函数里 Student s1("小明"); //报错,请输入正确的类型 //此处编译器会将该语句视为函数声明 Student s1{"小明"}; //正确,函数声明不能用大括号指定形参列表
-
C++在构造函数重载决议期间,只要有任何可能(可隐式转换),大括号初始化物就会与带有std::initializer_list型别的形参相匹配,即使其他重载版本有更匹配的形参表
std::initializer_list<T>
类型对象是一个访问const T
类型对象数组的轻量代理对象,相当于存放常量值的vector作用是方便了对于STL的container的初始化,所以container一般有该类型参数的构造函数
-
C++规定,当使用一对空大括号来构造对象,对象既支持默认构造函数,又支持带有std::initializer_list型别形参的构造函数时,会执行默认构造函数
-
若执意要调用一个带有std::initializer_list型别形参的构造函数,并传入一个空的std::initializer_list可采用以下两种
-
Student s1({}); Student s2{{}};
-
-
-
容器初始化结果不同
-
std::vector<int> v1(10,20); //含有10个元素,所以元素值为20 std::vector<int> v1{10,20}; //含有2个元素,分别为10和20
-
nullptr
- nullptr 本身是指针类型,不能转化为整数类型,可以转换成任意其他指针类型
- NULL:预处理变量,是一个宏,它的值是 0,定义在头文件
中,即 #define NULL 0
- 因为 NULL 本质上是 0,在函数调用过程中,若出现函数重载并且传递的实参是 NULL,可能会出现,不知和哪一个函数匹配的情况;但是传递实参 nullptr 就不会出现这种情况。
新for循环
int main() {
char arc[] = "abcde";
vector<char>myvector(arc, arc + 5);
//for循环遍历并修改容器中各个字符的值
for (auto &ch : myvector) {
ch++;
}
//for循环遍历输出容器中各个字符
for (auto ch : myvector) {
cout << ch;
}
return 0;
}
新语法格式的 for 循环遍历某个序列时,如果需要遍历的同时修改序列中元素的值,实现方案是在 declaration 参数处定义引用形式的变量
Lambda
Lambda 表达式,实际上就是提供了一个类似匿名函数的特性, 而匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用的,一般常见于自定义函数(sort里的比较函数),匿名委托
基本语法如下
[捕获列表](参数列表) mutable(可选) 异常属性 -> 返回类型 {
// 函数体
}
lambda 表达式内部函数体在默认情况下是不能够使用函数体外部的变量的, 这时候捕获列表可以起到传递外部数据的作用。
auto不能用于模板参数表,但可以用于Lambda表达式的参数表中
- 左值捕获
- 值捕获:[name]
- 前提是变量可以拷贝,与参数传值类似
- 在函数体内对捕获的变量进行改变,实际是改变拷贝后的变量,外部变量不会受影响
- 若想修改外部变量可以通过mutable
- 引用捕获:[&name]
- 与引用传参类似
- 隐式捕获
- [] 空捕获列表
- [name1, name2, ...] 捕获一系列变量
- [&] 引用捕获, 让编译器自行推导捕获列表
- [=] 值捕获, 让编译器执行推导引用列表
- 值捕获:[name]
- 右值捕获
- 表达式捕获
- C++14 允许捕获的成员用任意的表达式进行初始化,即允许了右值的捕获, 被声明的捕获变量类型会根据表达式进行判断,本质同auto
- 表达式捕获
智能指针
智能指针将基本类型指针封装为类对象指针,并在析构函数里编写delete语句删除指针指向的内存空间。
所有的智能指针类都有一个explicit构造函数,以指针作为参数。因此不能自动将指针转换为智能指针对象,必须显式调用
auto_ptr
-
在 auto_ptr 对象销毁时,他所管理的对象也会自动被 delete 掉。
-
auto_ptr 采用 copy 语义来转移指针资源,转移指针资源的所有权的同时将原指针置为 NULL,拷贝后
原对象变得无效,再次访问原对象时会导致程序崩溃。
unique_ptr
由 C++11 引入,旨在替代不安全的 auto_ptr。
unique_ptr 则禁止了拷贝语义,但提供了移动语义,即可以使用std::move() 进行控制权限的转移
它持有对对象的独有权——两个 unique_ptr 不能指向一个对象,即 unique_ptr 不共享它所管理的对象。
内存资源所有权可以转移到另一个 unique_ptr,并且原始 unique_ptr 不再拥有此资源。
shared_ptr
shared_ptr第二个参数支持自定义释放规则,也可以借助lambda
//指定 default_delete 作为释放规则
std::shared_ptr<int> p6(new int[10], std::default_delete<int[]>());
//自定义释放规则
void deleteInt(int*p) {
delete []p;
}
//初始化智能指针,并自定义释放规则
std::shared_ptr<int> p7(new int[10], deleteInt);
shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针,当然这需要额外的开销:
- shared_ptr 对象除了包括一个所拥有对象的指针外,还必须包括一个引用计数代理对象的指针;
- 时间上的开销主要在初始化和拷贝操作上, * 和 -> 操作符重载的开销跟 auto_ptr 是一样;
环形引用:智能指针互相指向了对方,导致自己的引用计数一直为1,所以没有进行析构,这就造成了内存泄漏。
weak_ptr
weak_ptr 只对 shared_ptr 进行引用,而不改变其引用计数
- 当被观察的 shared_ptr 失效后,相应的 weak_ptr 也相应失效,其lock方法将返回空。
- weak_ptr并没有重载operator->和operator *操作符,因此不可直接通过weak_ptr使用对象。
- 可解决环形引用问题,让其中一方使用weak_ptr
强枚举类型
传统枚举
枚举值对应整型数值,默认从 0 开始
- 同作用域同名枚举值会报重定义错误。传统 C++ 中枚举常量被暴漏在同一层作用域中
enum Fruits{Apple,Tomato,Orange};
enum Vegetables{Cucumber,Tomato,Pepper}; //编译报Tomato重定义错误
- 枚举常量占用存储空间以及符号性不确定。C++ 标准规定 C++ 枚举所基于的“基础类型”是由编译器来具体实现
enum A{A1=1,A2=2,ABig=0xFFFFFFFFU};
enum B{B1=1,B2=2,BBig=0xFFFFFFFFFUL};
int main()
{
cout<<sizeof(A1)<<endl; //4
cout<<ABig<<endl; //4294967295
cout<<sizeof(B1)<<endl; //8
cout<<BBig<<endl; //68719476735
}
- 由于枚举类型被设计为常量数值的“别名”,所以枚举常量总是可以被隐式转换为整型,且用户无法为枚举常量定义类型。
C++11对传统枚举类型进行了扩展。
底层的基本类型可以在枚举名称后加上":type",其中type可以是除wchar_t以外的任何整型,比如:
enum Type:char{Low,Middle,High};
强枚举类型
使用enum class/struct
强类型枚举具有如下几个优点:
(1)强作用域,强类型枚举成员的名称不会被输出到其父作用域,所以不同枚举类型定义同名枚举成员编译不会报重定义错误。进而使用枚举类型的枚举成员时,必须指明所属范围,比如Enum::VAL1,而单独的VAL1则不再具有意义;
(2)转换限制,强类型枚举成员的值不可以与整型发生隐式相互转换。比如比如Enumeration::VAL4==10;会触发编译错误;
enum class Status { Ok, Error };
enum struct Status2 { Ok, Error };
//Status flag1 = 10; // err,无法隐式转换为int类型
//Status flag2 = Ok; // err,必须使用强类型名称
Status flag3 = Status::Ok;
(3)可以指定底层类型。强类型枚举默认的底层类型是int,但也可以显示地指定底层类型。具体方法是在枚举名称后面加上":type",其中type可以是除wchar_t以外的任何整型。比如:
enum class Type:char{Low,Middle,High};
移动语义,右值引用与完美转发
形参总是左值,即是型别是右值引用
C++引入右值引用之后,可以通过右值引用,充分使用临时变量,或者即将不使用的变量即右值的资源,减少不必要的拷贝,提高效率
三种引用
- 左值引用
- 形如T&
- 右值引用
- 若型别声明不准确具备T&&形式,或者型别推导未发生(全特化),则T&&代表右值引用
- 万能引用
- 当函数模板形参具备T&&型别,且T型别是由推导而来,或对象使用auto&&声明型别时,该形参或对象就是万能引用
- 用左值初始化万能引用得到左值引用;用右值初始化万能引用得到右值引用
右值引用常用于移动构造函数,移动构造函数的调用时机是:用同类的右值对象初始化新对象。那么,用当前类的左值对象(有名称,能获取其存储地址的实例对象)初始化同类对象时,是否就无法调用移动构造函数了,C++给出了解决方案:实现移动语义的move
简单来说:拷贝语义(调用拷贝构造函数),移动语义(调用移动构造函数)
std::move() 实现原理:
- 利用引用折叠原理将右值经过 T&& 传递类型保持不变还是右值,而左值经过 T&& 变为普通的左值引用,以保证模板可以传递任意实参,且保持类型不变;
- 然后通过 remove_refrence 移除引用,得到具体的类型 T;
- 最后通过 static_cast<> 进行强制类型转换,返回 T&& 右值引用。
- std::move
- 实施无条件的向右值型别的强制型别转换,实际不会执行移动操作
- std::forward
- 仅当传入实参被绑定到右值时,std::forward才针对该实参实施向右值型别的强制类型转换
有的时候,我们需要将一个函数中某一组参数原封不动的传递给另一个函数,这里不仅仅需要参数的值不变,而且需要参数的类型属性(左值/右值)保持不变-完美转发;
首先由于C++引入了万能引用,既可以接收右值,也可以接收左值,所以参数即是用到万能引用
template <typename T>
void function(T&& t) {
otherdef(t);
}
int n = 10;
int & num = n;
function(num); // T 为 int&
int && num2 = 11;
function(num2); // T 为 int &&
C++ 11标准为了更好地实现完美转发,特意为其指定了新的类型匹配规则,又称为引用折叠规则(假设用 A 表示实际传递参数的类型):
- 当实参为左值或者左值引用(A&)时,函数模板中 T&& 将转变为 A&(A& && = A&);
- 当实参为右值或者右值引用(A&&)时,函数模板中 T&& 将转变为 A&&(A&& && = A&&)
即在实现完美转发时,只要函数模板的参数类型为 T&&,则 C++ 可以自行准确地判定出实际传入的实参是左值还是右值。
但无论传入的形参是左值还是右值,对于函数模板内部来说,形参既有名称又能寻址,因此它都是左值;所以还需要解决一个问题,如何将左值,右值属性一起传递给调用函数,C++11又给出了方案:用forward将参数的原本左右值属性传递给被调用函数
//重载被调用函数,查看完美转发的效果
void otherdef(int & t) {
cout << "lvalue\n";
}
void otherdef(const int & t) {
cout << "rvalue\n";
}
//实现完美转发的函数模板
template <typename T>
void function(T&& t) {
otherdef(forward<T>(t));
}
int main()
{
function(5);
int x = 1;
function(x);
return 0;
}
//输出
//rvalue
//lvalue
类型推导
模板类型推导
//函数模板
template<tyepname T>
void f(ParamType param);
f(expr);//一次调用
- 情况1:ParamType是个指针或引用,但不是万能引用
- 若expr具有引用型别,先将引用型别忽略
- 需注意容器operator[]会返回T&,但由于模板推导&会被忽略导致左值变右值
- 然后对expr与ParamType型别进行模式匹配,决定T型别
- 若expr具有引用型别,先将引用型别忽略
- 情况2:ParamType是个万能引用
- 若expr是个左值,T和ParamType都会被推导为左值引用
- 若expr是个右值,则引用情况1规则
- 情况3:Param既非指针也非引用,而是按值传递
- 在传入参数时会进行拷贝,即param是一个传入对象的副本
- 从而去除引用性,常量const性,与volatile
- 若传入const char* const ptr,则param将会被推导为const char*,即自身const去掉,ptr指涉对象的常量性得以保留
- 数组与指针
- 当传入型别为数组const char name[]时,而模板定义为void(T param);则T将会被推导为指针型别const char*
- 当传入型别为数组const char name[]时,而模板定义为void(T& param);则T将会被推导为实际数组型别const char [],这个型别会包含数组尺寸,从而可用来推导数组大小
auto
auto与模板类型推导真正唯一的区别在于,auto会假定用大括号括起来的初始化表达式代表一个std::initializer_list,但模板型别推导不会
auto x={233};//型别为std::initializer_list<int>,值为{233}
template<typename T>
void f(T param);
f({233}); //无法推导T型别
C++14允许auto说明函数返回值需要推导,以及lambda里形参类型可声明为auto;但这两处auto用的是模板型别推导,即无法推导大括号
decltype
绝大多数情况下,decltype就是鹦鹉学舌,变量或表达式是啥型别就得出啥型别
C++11中decltype主要用于声明返回值型别依赖于形参型别的函数模板
//C++11 auto无实际意义,只是说明这里是返回值型别尾序语法
template<typename Container,typename Index>
auto authAndAccess(Container&c,Index i)
->decltype(c[i])
{
authenticateUser();
return c[i];
}
//C++14auto说明会发生型别推导
template<typename Container,typename Index>
auto authAndAccess(Container&c,Index i)
{
authenticateUser();
return c[i];
}
//当这里是错的,容器operator[]会返回T&,而auto使用的是模板推导,会去掉&;正确写法如下
//推导过程采用decltype的规则,则不会去掉一些特性
template<typename Container,typename Index>
decltype(auto) authAndAccess(Container&c,Index i)
{
authenticateUser();
return c[i];
}
decltype的特殊情况
decltype(auto) f1()
{
int x=0;
return x;//返回int
}
decltype(auto) f1()
{
int x=0;
return (x);//返回int&,且是局部变量的引用,将产生未定义行为
}
auto varname = value; //auto的语法格式
decltype(exp) varname [= value]; //decltype的语法格式
auto与decltype
- decltype 会保留 cv 限定符,而 auto 有可能会去掉 cv 限定符。
- 如果表达式的类型不是指针或者引用,auto 会把 cv 限定符直接抛弃,推导成 non-const 或者 non-volatile 类型。
- 如果表达式的类型是指针或者引用,auto 将保留 cv 限定符。
- decltype 会保留引用类型,而 auto 会抛弃引用类型,直接推导出它的原始类型
C++17
std::any
引入了any,定义在any
头文件中:#include <any>
是一个可用于任何类型单个值的类型安全的容器.
- std::any a = 1;: 声明一个any类型的容器,容器中的值为int类型的1
- a.type(): 得到容器中的值的类型
- std::any_cast
(a);: 强制类型转换, 转换失败可以捕获到std::bad_any_cast类型的异常 - has_value(): 判断容器中是否有值
- reset(): 删除容器中的值
- std::any_cast
(&a): 强制转换得到容器中的值的地址