2022-9-20-C++11新特性(一)

简介: 2022-9-20-C++11新特性

什么是C++11

C++11标准为C++编程语言的第三个官方标准,正式名叫ISO/IEC 14882:2011 - Information technology – Programming languages – C++。在正式标准发布前,原名C++0x。它将取代C++标准第二版ISO/IEC 14882:2003 - Programming languages – C++成为C++语言新标准。

C++11是对目前C++语言的扩展和修正, C++11不仅包含核心语言的新机能,而且扩展了C++的标准程序库(STL) ,并入了大部分的C++ Technical Report 1(TR1) 程序库(数学的特殊函数除外)。


类型推导

auto

auto的自动类型推导,用于从初始化表达式中推断出变量的数据类型。从这个意义上讲,auto并非一种“类型”声明,而是一个类型声明时的“占位符”,编译器在编译时期会将auto替换为变量实际的类型。

五个不能用:

  1. 1.定义变量时必须初始化;
  2. 不支持函数形参(C++11);
  3. 不能作为自定义类型的成员变量;
  4. 不能作为模板实例化时的参数;
  5. 不能出现在顶级数组类型。
void fun(auto x = 1) {}  // 2: auto函数参数,有些编译器无法通过编译
struct str
{
    auto var = 10;   // 3: auto非静态成员变量,无法通过编译
};
int main()
{
    auto a;     // 1: 无法推导,无法通过编译
    vector<auto> b = {1}; // 4: auto模板参数(实例化时),无法通过编译
    auto c[3] = { 1, 2, 3 }; // 5: auto数组,无法通过编译
    return 0;
}

decltype

decltype可以从一个变量或表达式中得到其类型。

返回类型后置:在函数名和参数列表后面指定返回类型。

template <typename T1, typename T2>
auto mul(const T1 & t1, const T2 & t2) -> decltype(t1 * t2)
{
    return t1 * t2;
}

易用性的改进

初始化

  1. 类内成员初始化
  2. 初始化列表
  3. 使用列表初始化可以防止类型收窄
    基于范围的for循环
    使用基于范围的for循环,其for循环迭代的范围必须是可确定的。
int func(int a[])//形参中数组是指针变量,无法确定元素个数
{
    for(auto e: a) // err, 编译失败
    {
        cout << e;
    }
}
int main()
{
    int a[] = {1, 2, 3, 4, 5};
    func(a);
    return 0;
}

静态断言

C/C++提供了调试工具assert,这是一个宏,用于在运行阶段对断言进行检查,如果条件为真,执行程序,否则调用abort()。

C++ 11新增了关键字static_assert,可用于在编译阶段对断言进行测试。

静态断言的好处:

  • 更早的报告错误,我们知道构建是早于运行的,更早的错误报告意味着开发成本的降低
  • 减少运行时开销,静态断言是编译期检测的,减少了运行时开销

noexcept

C++11 使用noexcept替代throw()代表此函数不能抛出异常,如果抛出,就会异常。

nullptr

nullptr是为了解决原来C++中NULL的二义性问题而引进的一种新的类型,因为NULL实际上代表的是0。

强类型枚举

C++ 11引入了一种新的枚举类型,即“枚举类”,又称“强类型枚举”。声明强类型枚举非常简单,只需要在enum后加上使用class或struct。如:

enum Old{Yes, No};          // old style
enum class New{Yes, No};    // new style
enum struct New2{Yes, No};  // new style

“传统”的C++枚举类型有一些缺点:它会在一个代码区间中抛出枚举类型成员(如果在相同的代码域中的两个枚举类型具有相同名字的枚举成员,这会导致命名冲突),它们会被隐式转换为整型,并且不可以指定枚举的底层数据类型。

int main()
{
    enum Status{Ok, Error};
    //enum Status2{Ok, Error};//err, 导致命名冲突, Status已经有成员叫Ok, Error
    return 0;
}

在C++11中,强类型枚举解决了这些问题:

int main()
{
    enum class Status {Ok, Error};
    enum struct Status2{Ok, Error};
    //Status flag2 = Ok; // err,必须使用强类型名称
    Status flag3 = Status::Ok;
    enum class C : char { C1 = 1, C2 = 2};//指定枚举的底层数据类型
    enum class D : unsigned int { D1 = 1, D2 = 2, Dbig = 0xFFFFFFF0U };
    cout << sizeof(C::C1) << endl;   // 1
    cout << sizeof(D::D1) << endl;     // 4
    cout << sizeof(D::Dbig) << endl;   // 4
    return 0;
}

常量表达式

常量表达式主要是允许一些计算发生在编译时,即发生在代码编译而不是运行的时候。

这是很大的优化:假如有些事情可以在编译时做,它将只做一次,而不是每次程序运行时都计算。

constexpr函数的限制:

  • 函数中只能有一个return语句(有极少特例)
  • 函数必须返回值(不能是void函数)
  • 在使用前必须已有定义(不能先声明)
  • return返回语句表达式中不能使用非常量表达式的函数、全局数据,且必须是一个常量表达式

常量表达式的构造函数有以下限制:

  • 函数体必须为空
  • 初始化列表只能由常量表达式来赋值
    用户定义字面量
    用户自定义字面值,或者叫“自定义后缀”更直观些,主要作用是简化代码的读写。
long double operator"" _mm(long double x) { return x / 1000; }
long double operator"" _m(long double x)  { return x; }
long double operator"" _km(long double x) { return x * 1000; }
int main()
{
    cout << 1.0_mm << endl; //0.001
    cout << 1.0_m  << endl; //1
    cout << 1.0_km << endl; //1000
    return 0;
}

原生字符串字面值

很多时候,当我们需要一行字符串的时候,字符串转义往往成了一个负担,写和读都带了很大的不便。例如,对于如下路径"D:\workdataDJ\code\vas_pgg_proj",我们必须通过反斜杠进行转义。

原生字符串字面值(raw string literal)使用户书写的字符串“所见即所得”。C++11中原生字符串的声明相当简单,只需在字符串前加入前缀,即字母R,并在引号中使用括号左右标识,就可以声明该字符串字面量为原生字符串了。

int main()
{
    cout << R"(hello,\n
         world)" << endl;
    return 0;
}

C++中同样可以将原生字符串进行连接,但不要将不同编码的字符串进行连接,因为C++尚不支持这种做法。

#include <iostream> 
using namespace std;
int main()
{
  char string[] = R"(你好)"R"(=hello)";
  char u8u8string[] = u8R"(你好)"u8R"(=hello)";
  //char u8ustring[] = u8R"(你好)"uR"=hello"; //编译报错
  cout << string<< endl;
  cout << u8string << endl;
  cout << sizeof(string) << endl;
  cout << sizeof(u8u8string) << endl;
  return 0;
}
//程序编译选项:g++ -finput-charset=utf-8 test.cpp

类的改进

继承构造

C++ 11允许派生类继承基类的构造函数(默认构造函数、复制构造函数、移动构造函数除外)。

class A
{
public:
    A(int i) { cout << "i = " << i << endl; }
    A(double d, int i) {}
    A(float f, int i, const char* c) {}
    // ...
};
class B : public A
{
public:
    using A::A; // 继承构造函数
    // ...
    virtual void ExtraInterface(){}
};

注意:

  • 继承的构造函数只能初始化基类中的成员变量,不能初始化派生类的成员变量
  • 如果基类的构造函数被声明为私有,或者派生类是从基类中虚继承,那么不能继承构造函数
  • 一旦使用继承构造函数,编译器不会再为派生类生成默认构造函数

委托构造

和继承构造函数类似,委托构造函数也是C++11中对C++的构造函数的一项改进,其目的也是为了减少程序员书写构造函数的时间。

如果一个类包含多个构造函数,C++ 11允许在一个构造函数中的定义中使用另一个构造函数,但这必须通过初始化列表进行操作,如下:

class Info
{
public:
    Info() : Info(1) { }    // 委托构造函数
    Info(int i) : Info(i, 'a') { } // 既是目标构造函数,也是委托构造函数
    Info(char e): Info(1, e) { }
private:
    Info(int i, char e): type(i), name(e) { /* 其它初始化 */ } // 目标构造函数
    int  type;
    char name;
    // ...
};

继承控制

C++11之前,一直没有继承控制关键字,禁用一个类的进一步衍生比较麻烦。

C++ 11添加了两个继承控制关键字:final和override。

  • final阻止类的进一步派生和虚函数的进一步重写
  • override确保在派生类中声明的函数跟基类的虚函数有相同的签名
    类默认函数的控制
  • =default
    编译器将为显式声明的 =default函数自动生成函数体
    仅适用于类的特殊成员函数,且该特殊成员函数没有默认参数
    函数既可以在类体里(inline)定义,也可以在类体外(out-of-line)定义
  • =delete
    在函数声明后加上=delete,就可将该函数禁用
    可用于禁用类的某些转换构造函数,从而避免不期望的类型转换
    禁用某些用户自定义的类的 new 操作符,从而避免在自由存储区创建类的对象

模板的改进

右尖括号的改进

在C++98/03的泛型编程中,模板实例化有一个很繁琐的地方,就是连续两个右尖括号>>会被编译解释成右移操作符,而不是模板参数表的形式,需要一个空格进行分割,以避免发生编译时的错误。

在实例化模板时会出现连续两个右尖括号,同样static_cast、dynamic_cast、reinterpret_cast、const_cast表达式转换时也会遇到相同的情况。C++98标准是让程序员在>>之间填上一个空格,在C++11中,这种限制被取消了。在C++11标准中,要求编译器对模板的右尖括号做单独处理,使编译器能够正确判断出>>是一个右移操作符还是模板参数表的结束标记。

函数模板的默认模板参数

C++11之前,类模板是支持默认的模板参数,却不支持函数模板的默认模板参数。类模板的默认模板参数必须从右往左定义,数模板的默认模板参数则没这个限定。

可变参数模板

在C++11之前,类模板和函数模板只能含有固定数量的模板参数。C++11增强了模板功能,允许模板定义中包含0到任意个模板参数,这就是可变参数模板。

可变参数模板和普通模板的语义是一样的,只是写法上稍有区别,声明可变参数模板时需要在typename或class后面带上省略号…:

#include <iostream>
using namespace std;
//可变参数的模板函数
template<class ... T> //T叫模板参数包
void func(T... args)//args叫函数参数包
{
}
int main()
{
    func();
    func<int>(10);
    func<int, int>(10, 20);
    func<char, int>('a', 10);
    func<char, const char*, int>('a', "abc", 250);
    return 0;
}

省略号…的作用有两个:

  • 声明一个参数包,这个参数包中可以包含0到任意个模板参数
  • 在模板定义的右边,可以将参数包展开成一个一个独立的参数

应用:

  1. 函数的递归调用
  2. 类的递归继承

右值引用

左值引用、右值引用

左值引用是对一个左值进行引用的类型,右值引用则是对一个右值进行引用的类型。

左值引用和右值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。

左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。

int &a = 2;       // 左值引用绑定到右值,编译失败, err
int b = 2;        // 非常量左值
const int &c = b; // 常量左值引用绑定到非常量左值,编译通过, ok
const int d = 2;  // 常量左值
const int &e = c; // 常量左值引用绑定到常量左值,编译通过, ok
const int &b = 2; // 常量左值引用绑定到右值,编程通过, ok

const 类型 &为 “万能”的引用类型,它可以接受非常量左值、常量左值、右值对其进行初始化。

右值引用,使用&&表示:

int && r1 = 22;
int x = 5;
int y = 8;
int && r2 = x + y;
T && a = ReturnRvalue();

通常情况下,右值引用是不能够绑定到任何的左值的。

移动语义

右值引用是用来支持转移语义的。转移语义可以将资源 ( 堆,系统对象等 ) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能。临时对象的维护 ( 创建和销毁 ) 对性能有严重影响。

转移语义是和拷贝语义相对的,可以类比文件的剪切与拷贝,当我们将文件从一个目录拷贝到另一个目录时,速度比剪切慢很多。

通过转移语义,临时对象中的资源能够转移其它的对象里。


目录
相关文章
|
1月前
|
编译器 程序员 定位技术
C++ 20新特性之Concepts
在C++ 20之前,我们在编写泛型代码时,模板参数的约束往往通过复杂的SFINAE(Substitution Failure Is Not An Error)策略或繁琐的Traits类来实现。这不仅难以阅读,也非常容易出错,导致很多程序员在提及泛型编程时,总是心有余悸、脊背发凉。 在没有引入Concepts之前,我们只能依靠经验和技巧来解读编译器给出的错误信息,很容易陷入“类型迷路”。这就好比在没有GPS导航的年代,我们依靠复杂的地图和模糊的方向指示去一个陌生的地点,很容易迷路。而Concepts的引入,就像是给C++的模板系统安装了一个GPS导航仪
104 59
|
1月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(三)
【C++】面向对象编程的三大特性:深入解析多态机制
|
1月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(二)
【C++】面向对象编程的三大特性:深入解析多态机制
|
1月前
|
编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(一)
【C++】面向对象编程的三大特性:深入解析多态机制
|
1月前
|
存储 安全 编译器
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(一)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
|
28天前
|
C++
C++ 20新特性之结构化绑定
在C++ 20出现之前,当我们需要访问一个结构体或类的多个成员时,通常使用.或->操作符。对于复杂的数据结构,这种访问方式往往会显得冗长,也难以理解。C++ 20中引入的结构化绑定允许我们直接从一个聚合类型(比如:tuple、struct、class等)中提取出多个成员,并为它们分别命名。这一特性大大简化了对复杂数据结构的访问方式,使代码更加清晰、易读。
32 0
|
2月前
|
编译器 C++ 计算机视觉
C++ 11新特性之完美转发
C++ 11新特性之完美转发
50 4
|
2月前
|
Java C# C++
C++ 11新特性之语法甜点1
C++ 11新特性之语法甜点1
33 4
|
2月前
|
安全 程序员 编译器
C++ 11新特性之auto和decltype
C++ 11新特性之auto和decltype
39 3
|
2月前
|
设计模式 缓存 安全
C++ 11新特性之week_ptr
C++ 11新特性之week_ptr
35 2