C++11新特性


C++ 2.0 不仅仅增强了 C++ 语言自身的可用性,auto 关键字语义的修改使得我们更加有信心来操控极度复杂的模板类型。同时还对语言运行期进行了大量的强化,Lambda 表达式的出现让 C++ 具有了『匿名函数』的『闭包』特性,而这一特性几乎在现代的编程语言(诸如 Python/Swift/… )中已经司空见惯,右值引用的出现解决了 C++ 长期以来被人诟病的临时对象效率问题等等。

C++ 2.0 为自身的标准库增加了非常多的工具和方法,诸如在语言层面上提供了 std::thread 支持了并发编程,在不同平台上不再依赖于系统底层的 API,实现了语言层面的跨平台支持;std::regex提供了完整的正则表达式支持等等。

不要再打着C++的名号写C代码了,让我们快开始学习C++ 2.0新特性吧!

nullptr

提到nullptr,免不了要与NULL相比较。C++11之前会将NULL定义为

#define NULL ((void*)0)

或直接定义为0
C++11之前把空指针NULL赋给int或char指针时,会发生隐式类型转换

int *pi = NULL;
char * pc = NULL;

C++是强类型语言,不允许将void*隐式类型转换,在编译器头文件做了处理:

#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif

因此在C++中,NULL其实是0,编译

char * pc = NULL;

时,NULL被定义为0,这在重载时会导致错误
考虑两个函数:

void foo(char *);
void foo(int);

如果NULL被定义为0,foo(NULL)会去调用foo(int),导致错误
因此C++11引入nullptr关键字专门代表空指针

类型推导

在传统 C 和 C++中,参数的类型都必须明确定义,这其实对我们快速进行编码没有任何帮助,尤其是当我们面对一大堆复杂的模板类型时,必须明确的指出变量的类型才能进行后续的编码,这不仅拖慢我们的开发效率,也让代码变得又臭又长。

C++ 11 引入了 auto 和 decltype 这两个关键字实现了类型推导,让编译器来操心变量的类型。这使得 C++ 也具有了和其他现代编程语言一样,某种意义上提供了无需操心变量类型的使用习惯。

auto

auto 在很早以前就已经进入了 C++,但是他始终作为一个存储类型的指示符存在,与 register 并存。在传统 C++ 中,如果一个变量没有声明为 register 变量,将自动被视为一个 auto 变量。而随着 register 被弃用,对 auto 的语义变更也就非常自然了。

使用 auto 进行类型推导的一个最为常见而且显著的例子就是迭代器。在以前我们需要这样来书写一个迭代器:

for(vector<int>::const_iterator itr = vec.cbegin(); itr != vec.cend(); ++itr)

有了auto之后就可以:

// 由于 cbegin() 将返回 vector<int>::const_iterator
// 所以 itr 也应该是 vector<int>::const_iterator 类型
for(auto itr = vec.cbegin(); itr != vec.cend(); ++itr);

注意,auto不能用于函数传参,也不能用于推导数组类型

decltype

decltype 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的。用法和sizeof很类似

decltype(表达式)

只分析表达式的类型,而不计算其值

C++11之前使用模板函数:

template<typename R, typename T, typename U>
R add(T x, U y) {
    return x+y
}

这个程序有个问题,程序员使用这个模板函数的时候,必须明确指出返回类型。你可能会使用decltype推导x+y的类型,但这样的写法不能通过编译,因为在编译器读到 decltype(x+y) 时,x 和 y 尚未被定义。

为了解决这个问题,C++ 11 还引入了一个叫做尾返回类型(trailing return type),利用 auto 关键字将返回类型后置:

template<typename T, typename U>
auto add(T x, U y) -> decltype(x+y) {
    return x+y;
}

C++14开始可以直接让普通函数具备返回值推导:

template<typename T, typename U>
auto add(T x, U y) {
    return x+y;
}

初始化列表

初始化是一个非常重要的语言特性,最常见的就是对对象进行初始化。在传统 C++ 中,不同的对象有着不同的初始化方法,例如普通数组、POD (plain old data,没有构造、析构和虚函数的类或结构体)类型都可以使用 {} 进行初始化,也就是我们所说的初始化列表。而对于类对象的初始化,要么需要通过拷贝构造、要么就需要使用 () 进行。这些不同方法都针对各自对象,不能通用。

int arr[3] = {1,2,3};   // 列表初始化

class Foo {
private:
    int value;
public:
    Foo(int) {}
};

Foo foo(1);             // 普通构造初始化

为了解决这个问题,C++11 首先把初始化列表的概念绑定到了类型上,并将其称之为 std::initializer_list,允许构造函数或其他函数像参数一样使用初始化列表,这就为类对象的初始化与普通数组和 POD 的初始化方法提供了统一的桥梁,例如:

#include <initializer_list>

class Magic {
public:
    Magic(std::initializer_list<int> list) {}
};

Magic magic = {1,2,3,4,5};
std::vector<int> v = {1, 2, 3, 4};

这种构造函数被叫做初始化列表构造函数,具有这种构造函数的类型将在初始化时被特殊关照。

初始化列表除了用在对象构造上,还能将其作为普通函数的形参,例如:

void func(std::initializer_list<int> list) {
    return;
}

func({1,2,3});

其次,C++11 提供了统一的语法来初始化任意的对象,例如:

struct A {
    int a;
    float b;
};
struct B {

    B(int _a, float _b): a(_a), b(_b) {}
private:
    int a;
    float b;
};

A a {1, 1.1};    // 统一的初始化语法
B b {2, 2.2};

右值引用

左值和右值

左值指的是表达式结束后依然存在的持久化对象,右值指表达式结束后不再存在的临时对象。区分左值和右值的便捷方法是取地址,如果能对表达式取地址则为左值,不能则为右值

左值引用

C++11之前的引用都称为左值引用(Lvalue Reference),比如

int a = 10;
int& reA = a;  //reA是a的别名,对别名的修改就是对a的修改,左值引用
int& b = 1;  //编译错误,1是右值,不能使用左值引用

右值引用

C++11中的右值引用使用&&

int&& a = 1;  //实质是将匿名变量取别名
int b = 1;
int&& c = b;  //编译错误,b是左值,不能使用右值引用

class D{
public:
  int d;
}
D getTemp(){
  return D();
}
D && d = getTemp();  //getTemp()返回右值

上面例子中,getTemp返回的右值在表达式结束后,生命应该终结(临时变量),但通过右值引用后,该右值又重获新生,生命期与右值引用类型变量a的一样。实质是给临时变量取了个别名

Move Semantics

实现一个字符串类示例如下,这里使用cstring,管理一个char*数组

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

class MyString
{
public:
    static size_t CCtor; //统计调用拷贝构造函数的次数
public:
    // 构造函数
   MyString(const char* cstr=0){
       if (cstr) {
          m_data = new char[strlen(cstr)+1];
          strcpy(m_data, cstr);
       }
       else {
          m_data = new char[1];
          *m_data = '\0';
       }
   }

   // 拷贝构造函数
   MyString(const MyString& str) {
       CCtor ++;
       m_data = new char[ strlen(str.m_data) + 1 ];
       strcpy(m_data, str.m_data);
   }
   // 拷贝赋值函数 =号重载
   MyString& operator=(const MyString& str){
       if (this == &str) // 避免自我赋值
          return *this;

       delete[] m_data;
       m_data = new char[ strlen(str.m_data) + 1 ];
       strcpy(m_data, str.m_data);
       return *this;
   }

   ~MyString() {
       delete[] m_data;
   }
   char* get_c_str() const { return m_data; }
private:
   char* m_data;
};
size_t MyString::CCtor = 0;

int main()
{
    vector<MyString> vecStr;
    vecStr.reserve(1000); //先分配好1000个空间,不这么做,调用的次数可能远大于1000
    for(int i=0;i<1000;i++){
        vecStr.push_back(MyString("hello"));
    }
    cout << MyString::CCtor << endl;
}

代码执行了1000次拷贝构造函数,给1000个MyString对象分配内存,并且每个MyString对象又调用拷贝构造函数,使用new为MyString(“hello”)分配内存。
MyString(“hello”)是临时对象,拷贝操作造成没有意义的资源申请和释放操作。而移动语义(Move Semantics)能直接使用临时对象申请的资源,消除不必要的资源申请和释放。

要实现移动语义必须增加移动构造函数和移动赋值函数:

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

class MyString
{
public:
    static size_t CCtor; //统计调用拷贝构造函数的次数
    static size_t MCtor; //统计调用移动构造函数的次数
    static size_t CAsgn; //统计调用拷贝赋值函数的次数
    static size_t MAsgn; //统计调用移动赋值函数的次数

public:
    // 构造函数
   MyString(const char* cstr=0){
       if (cstr) {
          m_data = new char[strlen(cstr)+1];
          strcpy(m_data, cstr);
       }
       else {
          m_data = new char[1];
          *m_data = '\0';
       }
   }

   // 拷贝构造函数
   MyString(const MyString& str) {
       CCtor ++;
       m_data = new char[ strlen(str.m_data) + 1 ];
       strcpy(m_data, str.m_data);
   }
   // 移动构造函数
   MyString(MyString&& str) noexcept
       :m_data(str.m_data) {
       MCtor ++;
       str.m_data = nullptr; //不再指向之前的资源了
   }

   // 拷贝赋值函数 =号重载
   MyString& operator=(const MyString& str){
       CAsgn ++;
       if (this == &str) // 避免自我赋值
          return *this;

       delete[] m_data;
       m_data = new char[ strlen(str.m_data) + 1 ];
       strcpy(m_data, str.m_data);
       return *this;
   }

   // 移动赋值函数 =号重载
   MyString& operator=(MyString&& str) noexcept{
       MAsgn ++;
       if (this == &str) // 避免自我赋值
          return *this;

       delete[] m_data;
       m_data = str.m_data;
       str.m_data = nullptr; //不再指向之前的资源
       return *this;
   }

   ~MyString() {
       delete[] m_data;
   }

   char* get_c_str() const { return m_data; }
private:
   char* m_data;
};
size_t MyString::CCtor = 0;
size_t MyString::MCtor = 0;
size_t MyString::CAsgn = 0;
size_t MyString::MAsgn = 0;
int main()
{
    vector<MyString> vecStr;
    vecStr.reserve(1000); //先分配好1000个空间
    for(int i=0;i<1000;i++){
        vecStr.push_back(MyString("hello"));
    }
    cout << "CCtor = " << MyString::CCtor << endl;
    cout << "MCtor = " << MyString::MCtor << endl;
    cout << "CAsgn = " << MyString::CAsgn << endl;
    cout << "MAsgn = " << MyString::MAsgn << endl;
}

/* 输出
CCtor = 0
MCtor = 1000
CAsgn = 0
MAsgn = 0
*/

可以看到,移动构造函数与拷贝构造函数的区别在于,移动构造函数的参数是右值引用(MyString&& str),而拷贝构造函数的参数是常量左值引用(const MyString& str)
MyString(“hello”)是临时对象,是右值,优先进入移动构造函数。移动构造函数并没有重新申请空间,而是将自己的指针指向别人的资源,然后将别人的指针修改为nullptr

C++11提供str::move()方法将左值转换为右值,从而使有些左值(局部变量,生命周期短)也能使用移动语义

Perfect Forwarding

所谓转发,就是通过一个函数将参数继续转交给另一个函数进行处理,原参数可能是右值也可能是左值。如果转发还能保持参数的原有特征,那么就是完美转发(Perfect Forwarding)

智能指针

首先说明普通指针的问题

  • 挂起引用(Dangling References):如果一块内存被多个指针引用,但其中的一个指针释放而其余指针不知道,这种情况称为挂起引用。
  • 内存泄漏:从堆中申请内存不释放,发生内存泄漏

在实际开发环境中,程序可能确实有申请和释放的代码,但实际运行中程序可能发生异常而运行不到释放内存的语句。
使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域时,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。

unique_ptr

shared_ptr

weak_ptr

匿名函数

C++ 11 中的 Lambda 表达式用于定义并创建匿名的函数对象,以简化编程工作。
Lambda 的语法形式如下:(主要分为五个部分)

[函数对象参数] (操作符重载函数参数) mutable 或 exception 声明 -> 返回值类型 {函数体}

  1. 函数对象参数
    标识一个 Lambda 表达式的开始,这部分不能省略。函数对象参数是传递给编译器自动生成的函数对象类的构造
    函数的。函数对象参数只能使用那些到定义 Lambda 为止时 Lambda 所在作用范围内可见的局部变量。
  2. 操作符重载函数参数
    标识重载的 () 操作符的参数,没有参数时,这部分可以省略。参数可以通过按值和按引用两种方式进行传递。
  3. mutable 或 exception 声明
    这部分可以省略。按值传递函数对象参数时,加上 mutable 修饰符后,可以修改传递进来的拷贝。
    exception 声明用于指定函数抛出的异常。
  4. 返回值类型
    标识函数返回值的类型,当返回值为 void,或者函数体中只有一处 return 的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略。
  5. 函数体
    函数体是标识函数的出现,不能省略但可以为空。

总结
编译器实现lambda函数的结果是按引用捕获的任何变量,Lambda 函数实际存储的应该是这些变量在
创建这个 Lambda 函数的函数的栈指针,而不是 Lambda 函数本身栈变量的引用。

新增容器

array

forward_list

tuple

正则表达式

其他

constexpr

C++ 本身已经具备了常数表达式的概念,比如 1+2、3*4 这种表达式总是会产生相同的结果并且没有任何副作用。如果编译器能够在编译时就把这些表达式直接优化并植入到程序运行时,将能增加程序的性能。

#define LEN 10

int len_foo() {
    return 5;
}

int main() {
    char arr_1[10];
    char arr_2[LEN];
    int len = 5;
    char arr_3[len+5];          // 非法
    const int len_2 = 10;
    char arr_4[len_2+5];        // 合法
    char arr_5[len_foo()+5];  // 非法
    return 0;
}

在 C++11 之前,可以在常量表达式中使用的变量必须被声明为 const,在上面代码中,len_2 被定义成了常量,因此 len_2+5 是一个常量表达式,所以能够合法的分配一个数组;

而对于 arr_5 来说,C++98 之前的编译器无法得知 len_foo() 在运行期实际上是返回一个常数,这也就导致了非法的产生。

C++ 11 提供了 constexpr 让用户显式的声明函数或对象构造函数在编译器会成为常数,这个关键字明确的告诉编译器应该去验证 len_foo 在编译器就应该是一个常数。

此外,constexpr修饰的函数可以使用递归:

constexpr int fibonacci(const int n) {
    return n == 1 || n == 2 ? 1 : fibonacci(n-1)+fibonacci(n-2);
}

从 C++ 14 开始,constexpr 函数可以在内部使用局部变量、循环和分支等简单语句,但 C++ 11 中是不可以的。例如下面的代码在 C++ 11 的标准下是不能够通过编译的:

constexpr int fibonacci(const int n) {
    if(n == 1) return 1;
    if(n == 2) return 1;
    return fibonacci(n-1)+fibonacci(n-2);
}

区间迭代

区间迭代是指基于范围的 for 循环。

终于,C++ 11 引入了基于范围的迭代写法,我们拥有了能够写出像 Python 一样简洁的循环语句:

int array[] = {1,2,3,4,5};
for(auto &x : array) {
    std::cout << x << std::endl;
}

最常用的 std::vector 遍历将从原来的样子:

std::vector<int> arr(5, 100);
for(std::vector<int>::iterator i = arr.begin(); i != arr.end(); ++i) {
    std::cout << *i << std::endl;
}

变得非常的简单:

// & 启用了引用, 如果没有则对 arr 中的元素只能读取不能修改
for(auto &i : arr) {
    std::cout << i << std::endl;
}

模板增强

面向对象增强

强类型枚举

被弃用的特性

  1. 如果类有析构函数,为其生成拷贝构造函数和拷贝赋值运算符的特性被弃用
  2. 不再允许字符串字面值常量赋值给一个char*,如果需要初始化一个char*,应使用const char* 或 auto
  3. C++98 异常说明、 unexpected_handler、set_unexpected() 等相关特性被弃用,应该使用 noexcept
  4. auto_ptr 被弃用,应使用 unique_ptr。
  5. register被弃用
  6. bool类型的+=操作被弃用
  7. C 语言风格的类型转换被弃用,应该使用 static_cast、reinterpret_cast、const_cast 来进行类型转换。

C++17

(待补充)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值