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) 程序库(数学的特殊函数除外)。

C++11包括大量的新特性:包括lambda表达式,类型推导关键字auto、 decltype,和模板的大量改进。
 

类型推导:auto

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

通过auto的自动类型推导,可以大大简化我们的编程工作:

#include <iostream>
#include <vector>
#include <string>
using namespace std;

double foo() {}

void func(vector<string> & tmp)
{
    for (auto i = tmp.begin(); i < tmp.end(); i++)
    {
        // 一些代码
    }
}

int main()
{
    auto x = 1;      // x的类型为int
    auto y = foo();  // y的类型为double
    struct m { int i; }str;
    auto str1 = str;    // str1的类型是struct m

    auto z;     // err, 无法推导,无法通过编译
    z = x;

    return 0;
}

注意点:

void fun(auto x =1) {}  // 1: auto函数参数,有些编译器无法通过编译

struct str
{
    auto var = 10;   // 2: auto非静态成员变量,无法通过编译
};

int main()
{
    char x[3];
    auto y = x;
    auto z[3] = x; // 3: auto数组,无法通过编译

    // 4: auto模板参数(实例化时),无法通过编译
    vector<auto> x = {1};

    return 0;
}

decltype

decltype实际上有点像auto的反函数, auto可以让你声明一个变量,而decltype则可以从一个变量或表达式中得到其类型,如下:

#include <typeinfo>
#include <iostream>
#include <vector>
using namespace std;

int main()
{
    int i;
    decltype(i) j = 0;
    cout << typeid(j).name() << endl;   // 打印出"i", g++表示integer

    float a;
    double b;
    decltype(a + b) c;
    cout << typeid(c).name() << endl;   // 打印出"d", g++表示double

    vector<int> vec;
    typedef decltype(vec.begin()) vectype; // decltype(vec.begin()) 改名为 vectype

    vectype k;  // 这是auto无法做到的
    //decltype(vec.begin()) k;  // 这是auto无法做到的
    for (k = vec.begin(); k < vec.end(); k++)
    {
        // 做一些事情
    }

    enum {Ok, Error, Warning}flag;   // 匿名的枚举变量
    decltype(flag) tmp = Ok;

    return 0;
}

追踪返回类型

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

int func(int, int);
auto func2(int, int) -> int;

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

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

int main()
{
    auto a = 3;
    auto b = 4L;
    auto pi = 3.14;

    auto c = mul( sum(a, b), pi );
    cout << c << endl;  // 21.98

    return 0;
}

 

初始化

类内成员初始化

class Mem
{
public:
    Mem(int i): m(i){} //初始化列表给m初始化
    int m;
};
class Group
{
public:
    Group(){}

private:
    int data = 1;       // 使用"="初始化非静态普通成员,也可以 int data{1};
    Mem mem{2}; // 对象成员,创建对象时,可以使用{}来调用构造函数
    string name{"mike"};
};

列表初始化

C++11引入了一个新的初始化方式,称为初始化列表(List Initialize),具体的初始化方式如下:

int a[]{1, 3, 5};
int i = {1};  
int j{3}; 

初始化列表可以用于初始化结构体类型,例如:

struct Person  
{  
  std::string name;  
  int age;  
};  

int main()  
{  
    Person p = {"Frank", 25};  
    std::cout << p.name << " : " << p.age << std::endl;  
} 

防止类型收窄

类型收窄指的是导致数据内容发生变化或者精度丢失的隐式类型转换。使用列表初始化可以防止类型收窄。

int main(void)
{
    const int x = 1024;
    const int y = 10;

    char a = x;                 // 收窄,但可以通过编译
    char* b = new char(1024);   // 收窄,但可以通过编译

    char c = { x };             // err, 收窄,无法通过编译
    char d = { y };             // 可以通过编译
    unsigned char e{ -1 };      // err,收窄,无法通过编译

    float f{ 7 };               // 可以通过编译
    int g{ 2.0f };              // err,收窄,无法通过编译
    float * h = new float{ 1e48 };  // err,收窄,无法通过编译
    float i = 1.2l;                 // 可以通过编译

    return 0;
}

基于范围的for循环

在C++中for循环可以使用基于范围的for循环,示例代码如下

    int a[] = { 1, 2, 3, 4, 5 };
    int n = sizeof(a) / sizeof(*a); //元素个数

    for (int i = 0; i < n; ++i)
    {
        int tmp = a[i];
        cout << tmp << ", ";
    }
    cout << endl;

    for (int tmp : a)
    {
        cout << tmp << ", ";
    }
    cout << endl;


    for (int i = 0; i < n; ++i)
    {
        int &tmp = a[i];
        tmp = 2 * tmp;
        cout << tmp << ", ";
    }
    cout << endl;

    for (int &tmp : a)
    {
        tmp = 2 * tmp;
        cout << tmp << ", ";
    }
    cout << endl;

使用基于范围的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()。

int main()
{
    bool flag = false;

    //如果条件为真,程序正常执行,如果为假,终止程序,提示错误
    assert(flag == true); //#include <cassert>或#include <assert.h>
    cout << "Hello World!" << endl;

    return 0;
}

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

静态断言的好处:

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

语法如下:

    static_assert(常量表达式,提示字符串)
    注意:只能是常量表达式,不能是变量
int main()
{
    //该static_assert用来确保编译仅在32位的平台上进行,不支持64位的平台
    static_assert( sizeof(void *)== 4, "64-bit code generation is not supported."); 
    cout << "Hello World!" << endl;

    return 0;
}

noexcept修饰符、nullptr、原生字符串字面值

noexcept修饰符

void func3() throw(int, char) //只能够抛出 int 和char类型的异常
{//C++11已经弃用这个声明
     throw 0;
}

void BlockThrow() throw() //代表此函数不能抛出异常,如果抛出,就会异常
{
    throw 1;
}

//代表此函数不能抛出异常,如果抛出,就会异常
//C++11 使用noexcept替代throw()
void BlockThrowPro() noexcept
{
    throw 2;
}

nullptr

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

void func(int a)
{
    cout << __LINE__ << " a = " << a <<endl;
}

void func(int *p)
{
     cout << __LINE__ << " p = " << p <<endl;
}

int main()
{
    int *p1 = nullptr;
    int *p2 = NULL;

    if(p1 == p2)
    {
        cout << "equal\n";
    }

    //int a = nullptr; //err, 编译失败,nullptr不能转型为int

    func(0); //调用func(int), 就算写NULL,也是调用这个
    func(nullptr);

    return 0;
}

原生字符串字面值

int main(void)
{
    cout << R"(hello, \n world)" << endl;
    cout << "(hello, \n world)" << endl;
    string str = R"(helo \4 \r 
    abc, mike
    hello\n)";
    cout << endl;
    cout << str << endl;

    return 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 flag1 = 10; // err,无法隐式转换为int类型
    //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 << (unsigned int)D::Dbig << endl;   // 编译器输出一致,4294967280
    cout << sizeof(D::D1) << endl;     // 4
    cout << sizeof(D::Dbig) << endl;   // 4


    return 0;
}

常量表达式

 

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

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

使用constexpr,你可以创建一个编译时的函数:

constexpr int GetConst()
{
    return 3;
}

int main()
{
    int arr[ GetConst() ] = {0};
    enum { e1 = GetConst(), e2 };

    constexpr int num = GetConst();

    return 0;
}

constexpr函数的限制:

  • 函数中只能有一个return语句(有极少特例)
  • 函数必须返回值(不能是void函数)
  • 在使用前必须已有定义
  • return返回语句表达式中不能使用非常量表达式的函数、全局数据,且必须是一个常量表达式
//err,函数中只能有一个return语句
constexpr int data()
{
    constexpr int i = 1;
    return i;
}

constexpr int data2()
{
    //一个constexpr函数,只允许包含一行可执行代码
    //但允许包含typedef、 using 指令、静态断言等。
    static_assert(1, "fail");
    return 100;
}

int a = 3;
constexpr int data3()
{
    return a;//err, return返回语句表达式中不能使用非常量表达式的函数、全局数据
}

int main()
{
    constexpr int func(); //函数声明,定义放在main函数后面
    constexpr int c = func();  //err, 无法通过编译, 在使用前必须已有定义

    return 0;
}

constexpr int func()
{
    return 1;
}

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

  • 函数体必须为空
  • 初始化列表只能由常量表达式来赋值
struct Date
{
    constexpr Date(int y, int m, int d): year(y), month(m), day(d) {}

    constexpr int GetYear() { return year; }
    constexpr int GetMonth() { return month; }
    constexpr int GetDay() { return day; }

private:
    int year;
    int month;
    int day;
};

int main()
{
    constexpr Date PRCfound {1949, 10, 1};
    constexpr int foundmonth = PRCfound.GetMonth();

    cout << foundmonth << endl;  // 10

    return 0;
}

用户定义字面量

用户自定义字面值,或者叫“自定义后缀”更直观些,主要作用是简化代码的读写

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;
}

 

根据 C++ 11 标准,只有下面参数列表才是合法的

char const *
unsigned long long
long double
char const *, size_t
wchar_t const *, size_t
char16_t const *, size_t
char32_t const *, size_t

最后四个对于字符串相当有用,因为第二个参数会自动推断为字符串的长度。例如:

size_t operator"" _len(char const * str, size_t size)
{
    return size;
}

int main()
{
    cout << "mike"_len <<endl; //结果为4

    return 0;
}

对于参数char const *,应该被称为原始字面量 raw literal 操作符。例如:

char const * operator"" _r(char const* str)
{
    return str;
}

int main()
{
    cout << 250_r <<endl; //结果为250

    return 0;
}

类的改进

继承构造

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

#include <iostream>
using namespace std;

//基类
class A
{
public:
    A(int x, int y)
    {
        a = x;
        b = y;
    }

protected:
    int a;
    int b;
};

//派生类
class B:public A
{
public:
#if 0
    //通过参数列表给基类构造函数传参
    B(int x, int y): A(x, y)
    {

    }
#endif
    //继承构造
    using A::A;

    void display()
    {
        cout << "a = " << a << ", b = " << b << endl;
    }

    //没有增加新的成员变量
    int tmp;
};

int main()
{
    //派生类对象
    B obj(10, 20);
    obj.display();

    return 0;
}

注意:

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

继承控制:final和override

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

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

final阻止类的进一步派生和虚函数的进一步重写:

//final阻止类的进一步派生,虚函数的进一步重写
#if 0
class A1 final //加上final,指定A1不能派生
{
    int a;
};

class A2: public A1 //err, 基类不能再派生了
{

};
#endif

//基类
class B1
{
public:
    virtual void func() final {} //这是最终版本的虚函数,不能再重写

};

//派生类重写基类的虚函数
class B2: public B1
{
public:
    //virtual void func() {} //err, 基类中的虚函数是最终版本,不能再重写
};

override确保在派生类中声明的函数跟基类的虚函数有相同的签名:

class A1
{
public:
    //这是第一个虚函数,没有重写,不能用override修饰
    virtual int func(int a)
    {

    }
};

class A2:public A1
{
public:
    //在重写虚函数地方,加上override, 要求重写的虚函数和基类一模一样
    virtual int func(int b) override
    {

    }
};

defaulted 和 deleted 函数

defaulted 函数

背景问题

C++ 的类有四类特殊成员函数,它们分别是:默认构造函数、析构函数、拷贝构造函数以及拷贝赋值运算符。这些类的特殊成员函数负责创建、初始化、销毁,或者拷贝类的对象。如果程序员没有显式地为一个类定义某个特殊成员函数,而又需要用到该特殊成员函数时,则编译器会隐式的为这个类生成一个默认的特殊成员函数。

示例 1:

 class X
 { 
 private: 
  int a; 
 }; 

 X obj;

在示例 1 中

该自动生成的默认构造函数没有参数,包含一个空的函数体,即 X::X(){ }。虽然自动生成的默认构造函数仅有一个空函数体,但是它仍可用来成功创建类 X 的对象 obj,示例 1 也可以编译通过。

但是,如果程序员为类 X 显式的自定义了非默认构造函数,却没有定义默认构造函数的时候,示例 2 将会出现编译错误:

示例 2:

class X
{ 
 public: 
  X(int i)
  { 
    a = i; 
  }     
 private: 
  int a; 
 }; 

 X obj;  // 错误 , 默认构造函数 X::X() 不存在

示例 2 编译出错的原因在于:类 X 已经有了用户自定义的构造函数,所以编译器将不再会为它隐式的生成默认构造函数。

如果需要用到默认构造函数来创建类的对象时,程序员必须自己显式的定义默认构造函数。例如:

示例 3:

class X
{ 
 public: 
  X(){};  // 手动定义默认构造函数
  X(int i)
  { 
    a = i; 
  }     
 private: 
  int a; 
 }; 

 X obj;   // 正确,默认构造函数 X::X() 存在

从示例 3 可以看出,原本期望编译器自动生成的默认构造函数却需要程序员手动编写了,即程序员的工作量加大了。此外,手动编写的默认构造函数的代码执行效率比编译器自动生成的默认构造函数低。

类的其它几类特殊成员函数也和默认构造函数一样,当存在用户自定义的特殊成员函数时,编译器将不会隐式的自动生成默认特殊成员函数,而需要程序员手动编写,加大了程序员的工作量。类似的,手动编写的特殊成员函数的代码执行效率比编译器自动生成的特殊成员函数低。

defaulted 函数的提出

为了解决如示例 3 所示的两个问题:
1. 减轻程序员的编程工作量;
2. 获得编译器自动生成的默认特殊成员函数的高的代码执行效率。

C++11 标准引入了一个新特性:defaulted 函数。程序员只需在函数声明后加上“=default;”,就可将该函数声明为 defaulted 函数,编译器将为显式声明的 defaulted 函数自动生成函数体。例如:

示例 4:

class X
{ 
 public: 
  X()= default; 
  X(int i)
  { 
    a = i; 
  }     
 private: 
  int a; 
 }; 

 X obj;

在示例 4 中,编译器会自动生成默认构造函数 X::X(){},该函数可以比用户自己定义的默认构造函数获得更高的代码效率。

defaulted 函数的用法及示例

defaulted 函数特性仅适用于类的特殊成员函数,且该特殊成员函数没有默认参数。例如:

示例 5:

 class X
  { 
 public: 
  int f() = default;     // 错误 , 函数 f() 非类 X 的特殊成员函数
  X(int, int) = default;  // 错误 , 构造函数 X(int, int) 非 X 的特殊成员函数
  X(int = 1) = default;   // 错误 , 默认构造函数 X(int=1) 含有默认参数
 };

defaulted 函数既可以在类体里(inline)定义,也可以在类体外(out-of-line)定义。例如:

示例 6:

class X
{ 
 public:  
   X() = default; //Inline defaulted 默认构造函数
   X(const X&); 
   X& operator = (const X&); 
   ~X() = default;  //Inline defaulted 析构函数
 }; 

 X::X(const X&) = default;  //Out-of-line defaulted 拷贝构造函数
 X& X::operator = (const X&) = default;     //Out-of-line defaulted  拷贝赋值操作符

在 C++ 代码编译过程中,如果程序员没有为类 X 定义析构函数,但是在销毁类 X 对象的时候又需要调用类 X 的析构函数时,编译器会自动隐式的为该类生成一个析构函数。该自动生成的析构函数没有参数,包含一个空的函数体,即 X::~X(){ }。例如:

示例 7:

class X
{
private:
    int x;
};

class Y : public X 
{
public:
    Y()
    {
        p = new int;
        cout << "Y构造函数\n";
    }

    ~Y()
    {
        delete p;
        cout << "Y析构函数\n";
    }

private:
    int *p;
};

int main()
{
    X *p = new Y;
    delete p;
    return 0;
}

在示例 7 中,程序员没有为基类 X 定义析构函数,当在主函数内 delete 基类指针 p 的时候,需要调用基类的析构函数。于是,编译器会隐式自动的为类 X 生成一个析构函数,从而可以成功的销毁 p 指向的派生类对象中的基类子对象。

但是,这段代码存在内存泄露的问题,当利用 delete 语句删除指向派生类对象的指针 p 时,系统调用的是基类的析构函数,而非派生类 Y 类的析构函数,因此,编译器无法析构派生类的已经在堆区分配空间 p 。

因此,一般情况下我们需要将基类的析构函数定义为虚函数,当利用 delete 语句删除指向派生类对象的基类指针时,系统会调用相应的派生类的析构函数(实现多态性),从而避免内存泄露。

但是编译器隐式自动生成的析构函数都是非虚函数,这就需要由程序员手动的为基类 X 定义虚析构函数,例如:

示例 8:

class X
{
public: 
  virtual ~X(){};     // 手动定义虚析构函数
private:
    int x;
};

class Y : public X 
{
public:
    Y()
    {
        p = new int;
        cout << "Y构造函数\n";
    }

    ~Y()
    {
        delete p;
        cout << "Y析构函数\n";
    }

private:
    int *p;
};

int main()
{
    X *p = new Y;
    delete p;
    return 0;
}

在示例 8 中,由于程序员手动为基类 X 定义了虚析构函数,当利用 delete 语句删除指向派生类对象的基类指针 p 时,系统会调用相应的派生类 Y 的析构函数以及基类 X 的析构函数,从而将派生类对象完整的销毁,可以避免内存泄露。

但是,在示例 8 中,程序员需要手动的编写基类的虚构函数的定义(哪怕函数体是空的),增加了程序员的编程工作量。更值得一提的是,手动定义的析构函数的代码执行效率要低于编译器自动生成的析构函数。

为了解决上述问题,我们可以将基类的虚析构函数声明为 defaulted 函数,这样就可以显式的指定编译器为该函数自动生成函数体。例如:

示例 9:

class X
{
public: 
   virtual ~X()= defaulted; // 编译器自动生成 defaulted 函数定义体
private:
    int x;
};

class Y : public X 
{
public:
    Y()
    {
        p = new int;
        cout << "Y构造函数\n";
    }

    ~Y()
    {
        delete p;
        cout << "Y析构函数\n";
    }

private:
    int *p;
};

int main()
{
    X *p = new Y;
    delete p;
    return 0;
}

在示例 9 中,编译器会自动生成虚析构函数 virtual X::X(){},该函数比用户自己定义的虚析构函数具有更高的代码执行效率。

deleted 函数

为了能够让程序员显式的禁用某个函数,C++11 标准引入了一个新特性:deleted 函数。程序员只需在函数声明后上“=delete;”,就可将该函数禁用。

例如,我们可以将类 X 的拷贝构造函数以及拷贝赋值操作符声明为 deleted 函数,就可以禁止类 X 对象之间的拷贝和赋值。

示例 11:

class X
{           
public: 
    X(); 
    X(const X&) = delete;  // 声明拷贝构造函数为 deleted 函数
    X& operator = (const X &) = delete; // 声明拷贝赋值操作符为 deleted 函数
}; 

 int main()
 { 
      X obj1; 
      X obj2=obj1;   // 错误,拷贝构造函数被禁用
      X obj3; 
      obj3=obj1;     // 错误,拷贝赋值操作符被禁用
 }

在示例 11 中,虽然只显式的禁用了一个拷贝构造函数和一个拷贝赋值操作符,但是由于编译器检测到类 X 存在用户自定义的拷贝构造函数和拷贝赋值操作符的声明,所以不会再隐式的生成其它参数类型的拷贝构造函数或拷贝赋值操作符,也就相当于类 X 没有任何拷贝构造函数和拷贝赋值操作符,所以对象间的拷贝和赋值被完全禁止了。

deleted 函数的用法及示例

class X
{
public:
    X(double);
    X(int) = delete;
};

int main()
{
    X obj1(1.2);
    X obj2(2); // 错误,参数为整数 int 类型的转换构造函数被禁用          
}

deleted 函数特性还可以用来禁用某些用户自定义的类的 new 操作符,从而避免在自由存储区创建类的对象。例如:

示例 13:

class X
{
public:
    void *operator new(size_t) = delete;
    void *operator new[](size_t) = delete;
};

int main()
{
    X *pa = new X;      // 错误,new 操作符被禁用
    X *pb = new X[10];  // 错误,new[] 操作符被禁用
}

必须在函数第一次声明的时候将其声明为 deleted 函数,否则编译器会报错。即对于类的成员函数而言,deleted 函数必须在类体里(inline)定义,而不能在类体外(out-of-line)定义。例如:

示例 14:

int add(int, int) = delete;

int main()
{
    int a, b;
    add(a, b); // 错误,函数 add(int, int) 被禁用
}

值得一提的是,在示例 15 中,虽然 add(int, int)函数被禁用了,但是禁用的仅是函数的定义,即该函数不能被调用。但是函数标示符 add 仍是有效的,在名字查找和函数重载解析时仍会查找到该函数标示符。如果编译器在解析重载函数时,解析结果为 deleted 函数,则会出现编译错误。例如:

示例 16:

int add(int, int) = delete;
double add(double a, double b)
{
    return a + b;
}
int main()
{
    cout << add(1, 3) << endl;    // 错误,调用了 deleted 函数 add(int, int) 
    cout << add(1.2, 1.3) << endl;
    return 0;
}

 

lambda基础使用

lambda 表达式(lambda expression)是一个匿名函数,lambda表达式基于数学中的 λ 演算得名。

C++11中的lambda表达式用于定义并创建匿名的函数对象,以简化编程工作。

lambda表达式的基本构成:

这里写图片描述

① 函数对象参数

[],标识一个lambda的开始,这部分必须存在,不能省略。函数对象参数是传递给编译器自动生成的函数对象类的构造函数的。函数对象参数只能使用那些到定义lambda为止时lambda所在作用范围内可见的局部变量(包括lambda所在类的this)。函数对象参数有以下形式:

  • 空。没有使用任何函数对象参数。
  • =。函数体内可以使用lambda所在作用范围内所有可见的局部变量(包括lambda所在类的this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)。
  • 。函数体内可以使用lambda所在作用范围内所有可见的局部变量(包括lambda所在类的this),并且是引用传递方式(相当于编译器自动为我们按引用传递了所有局部变量)。
  • this。函数体内可以使用lambda所在类中的成员变量。
  • a。将a按值进行传递。按值进行传递时,函数体内不能修改传递进来的a的拷贝,因为默认情况下函数是const的。要修改传递进来的a的拷贝,可以添加mutable修饰符。
  • &a。将a按引用进行传递。
  • a, &b。将a按值进行传递,b按引用进行传递。
  • =,&a, &b。除a和b按引用进行传递外,其他参数都按值进行传递。
  • &, a, b。除a和b按值进行传递外,其他参数都按引用进行传递。

 

② 操作符重载函数参数
标识重载的()操作符的参数,没有参数时,这部分可以省略。参数可以通过按值(如:(a,b))和按引用(如:(&a,&b))两种方式进行传递。

③ 可修改标示符
mutable声明,这部分可以省略。按值传递函数对象参数时,加上mutable修饰符后,可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)

④ 错误抛出标示符
exception声明,这部分也可以省略。exception声明用于指定函数抛出的异常,如抛出整数类型的异常,可以使用throw(int)

⑤ 函数返回值
->返回值类型,标识函数返回值的类型,当返回值为void,或者函数体中只有一处return的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略。

⑥ 是函数体
{},标识函数的实现,这部分不能省略,但函数体可以为空。

class Test
{
public:
    int i = 0;

    void func(int x, int y)
    {
        auto x1 = []{ return i; };          //err, 没有捕获外部变量
        auto x2 = [=]{ return i+x+y; };     //ok, 值传递方式捕获所有外部变量
        auto x3 = [=]{ return i+x+y; };     //ok, 引用传递方式捕获所有外部变量
        auto x4 = [this]{ return i; };      //ok, 捕获this指针
        auto x5 = [this]{ return i+x+y; };  //err, 没有捕获x, y
        auto x6 = [this, x, y]{ return i+x+y; };//ok, 捕获this指针, x, y
        auto x7 = [this]{ return i++; };        //ok, 捕获this指针, 并修改成员的值
    }
};

int main()
{
    int a = 0, b = 1;
    auto f1 = []{ return a; };      //err, 没有捕获外部变量
    auto f2 = [=]{ return a; };     //ok, 值传递方式捕获所有外部变量
    auto f3 = [=]{ return a++; };   //err, a是以赋值方式捕获的,无法修改
    auto f4 = [=]() mutable { return a++; };   //ok, 加上mutable修饰符后,可以修改按值传递进来的拷贝
    auto f5 = [&]{ return a++; };               //ok, 引用传递方式捕获所有外部变量, 并对a执行自加运算
    auto f6 = [a]{ return a+b; };               //err, 没有捕获变量b
    auto f7 = [a,&b]{ return a+(b++); };        //ok, 捕获a, &b
    auto f8 = [=,&b]{ return a+(b++); };        //ok, 捕获所有外部变量,&b

    return 0;
}

值传递和引用传递区别:

#include <iostream>
using namespace std;

int main()
{
    int j = 12;
    auto by_val_lambda = [=] { return j + 1;};
    auto by_ref_lambda = [&] { return j + 1;};
    cout << "by_val_lambda: " << by_val_lambda() << endl;
    cout << "by_ref_lambda: " << by_ref_lambda() << endl;

    j++;
    cout << "by_val_lambda: " << by_val_lambda() << endl;
    cout << "by_ref_lambda: " << by_ref_lambda() << endl;

    return 0;
}

第3次调用结果还是13,原因是由于by_val_lambda中,j被视为了一个常量,一旦初始化后不会再改变。

lambda与仿函数

#include <iostream>
using namespace std;

class MyFunctor
{
public:
    MyFunctor(int tmp) : round(tmp) {}
    int operator()(int tmp) { return tmp + round; }

private:
    int round;
};

int main()
{
    //仿函数
    int round = 2;
    MyFunctor f1(round);//调用构造函数
    cout << "result1 = " << f1(1) << endl; //operator()(int tmp)

    //lambda表达式
    auto f2 = [=](int tmp) -> int { return tmp + round; } ;
    cout << "result2 = " << f2(1) << endl;

    return 0;
}

 

这里写图片描述

通过上面的例子,我们看到,仿函数以round初始化类,而lambda函数也捕获了round变量,其它的,如果在参数传递上,两者保持一致。

除去在语法层面上的不同,lambda和仿函数有着相同的内涵——都可以捕获一些变量作为初始化状态,并接受参数进行运行。

而事实上,仿函数是编译器实现lambda的一种方式,通过编译器都是把lambda表达式转化为一个仿函数对象。因此,在C++11中,lambda可以视为仿函数的一种等价形式。

模板的改进

右尖括号>改进

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

template <int i> class X{};
template <class T> class Y{};

int main()
{
    Y<X<1> > x1;    // ok, 编译成功
    Y<X<2>> x2;     // err, 编译失败

    return 0;
};

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

模板的别名

#include <iostream>
#include <type_traits> //std::is_same
using namespace std;

using uint = unsigned int;
typedef unsigned int UINT;
using sint = int;

int main()
{
    //std::is_same 判断类型是否一致
    //这个结构体作用很简单,就是两个一样的类型会返回true
    cout << is_same<uint, UINT>::value << endl; // 1

    return 0;
}

左值引用、右值引用

 

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

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

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

左值引用

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();

测试示例:

#include <iostream>
using namespace std;

void process_value(int & i) //参数为左值引用
{
    cout << "LValue processed: " << i << endl;
}

void process_value(int && i) //参数为右值引用
{
    cout << "RValue processed: " << i << endl;
}

int main()
{
    int a = 0;
    process_value(a); //LValue processed: 0
    process_value(1); //RValue processed: 1

    return 0;
}

这里写图片描述

移动语义

#include <iostream>
using namespace std;

class Test
{
public:
    Test(int a = 0)
    {//普通构造函数
        d = new int(a);
        cout << "构造函数\n";
    }

    Test(const Test & tmp)
    {//拷贝构造函数
        d = new int;
        *d = *(tmp.d);
        cout << "拷贝构造函数\n";
    }

    ~Test()
    {//析构函数
        if(d != NULL)
        {
            delete d;
            cout << "delete d\n";
        }
        cout << "析构函数\n";
    }

    int * d;
};

Test GetTmp()
{
    Test h;
    cout << "Resource from " << __func__ << ": " << (void *)h.d << endl;
    return h;
}

int main()
{
    Test obj = GetTmp();
    cout << "Resource from " << __func__ << ": " << (void *)obj.d << endl;

    return 0;
}

这里写图片描述

编译器会对返回值进行优化,简称RVO,是编译器的一项优化技术,它涉及(功能是)消除为保存函数返回值而创建的临时对象。

-fno-elide-constructors,此选项作用是,在 g++ 上编译时关闭 RVO。

通过上面的例子看到,临时对象的维护 ( 创建和销毁 ) 对性能有严重影响。

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

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

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

在现有的 C++ 机制中,我们可以定义拷贝构造函数和赋值函数。要实现转移语义,需要定义转移构造函数,还可以定义转移赋值操作符。对于右值的拷贝和赋值会调用转移构造函数和转移赋值操作符。

如果转移构造函数和转移拷贝操作符没有定义,那么就遵循现有的机制,拷贝构造函数和赋值操作符会被调用。

普通的函数和操作符也可以利用右值引用操作符实现转移语义
转移构造函数

class Test
{
public:
    Test(int a = 0)
    {
        ptr = new int(a);
        cout << " 构造函数" << endl;
    }
    Test(Test &tmp)
    {
        ptr = new int;
        *ptr = *(tmp.ptr);
        tmp.ptr = NULL;
        cout << "拷贝构造函数" << endl;
    }
// Test &operator=(Test &tmp)
// {
//     if (this != &tmp)
//     {
//         delete ptr;
//         ptr = new int;
//         *ptr = *(tmp.ptr);
//     }
//     cout << "赋值构造函数" << endl;
//     return *this;
// }

    Test(Test &&tmp)
    { // 移动构造函数
        ptr = tmp.ptr;
        tmp.ptr = NULL; // 将临时值的指针成员置空
        cout << "移动构造函数" << endl;
    }

    Test &operator=(Test &&tmp)
    { //转移赋值函数
        if (&tmp == this)
        {
            return *this;
        }

        *ptr = *(tmp.ptr;
        tmp.ptr = NULL;
        cout << "转移赋值函数\n";

        return *this;
    }


    ~Test()
    {
        if (ptr)
        {
            delete ptr;
            ptr = NULL;
        }
        cout << "析构函数" << endl;
    }

    int *ptr;
};

Test test()
{
    Test h;
    cout << "Resource from :" << __func__ << ":" << h.ptr << endl;
    return h;
}

int main()
{
    Test obj = test();//转移
    cout << "Resource from :" << __func__ << ":" << obj.ptr << endl;
    return 0;
}

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值