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, ...] 捕获一系列变量
      • [&] 引用捕获, 让编译器自行推导捕获列表
      • [=] 值捕获, 让编译器执行推导引用列表
  • 右值捕获
    • 表达式捕获
      • 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 是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针,当然这需要额外的开销:

  1. shared_ptr 对象除了包括一个所拥有对象的指针外,还必须包括一个引用计数代理对象的指针;
  2. 时间上的开销主要在初始化和拷贝操作上, * 和 -> 操作符重载的开销跟 auto_ptr 是一样;

环形引用:智能指针互相指向了对方,导致自己的引用计数一直为1,所以没有进行析构,这就造成了内存泄漏。

weak_ptr

weak_ptr 只对 shared_ptr 进行引用,而不改变其引用计数

  • 当被观察的 shared_ptr 失效后,相应的 weak_ptr 也相应失效,其lock方法将返回空。
  • weak_ptr并没有重载operator->和operator *操作符,因此不可直接通过weak_ptr使用对象。
  • 可解决环形引用问题,让其中一方使用weak_ptr

强枚举类型

传统枚举

枚举值对应整型数值,默认从 0 开始

  1. 同作用域同名枚举值会报重定义错误。传统 C++ 中枚举常量被暴漏在同一层作用域中
enum Fruits{Apple,Tomato,Orange};
enum Vegetables{Cucumber,Tomato,Pepper};	//编译报Tomato重定义错误
  1. 枚举常量占用存储空间以及符号性不确定。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
}
  1. 由于枚举类型被设计为常量数值的“别名”,所以枚举常量总是可以被隐式转换为整型,且用户无法为枚举常量定义类型。

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() 实现原理:

  1. 利用引用折叠原理将右值经过 T&& 传递类型保持不变还是右值,而左值经过 T&& 变为普通的左值引用,以保证模板可以传递任意实参,且保持类型不变;
  2. 然后通过 remove_refrence 移除引用,得到具体的类型 T;
  3. 最后通过 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型别
  • 情况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): 强制转换得到容器中的值的地址
posted @ 2021-05-05 13:24  AMzz  阅读(454)  评论(0编辑  收藏  举报
//字体