跳转至

C++11 新特性

auto 与 decltype

auto 可以让编译器在编译的时候就自动推导出右值的类型,然后赋给左值。因此,使用 auto 来定义编译必须附上初始值。

auto i = v.begin();
vector<int>::iterator i = v.begin();

decltype 可以像函数一样传入一个变量作为参数,然后“返回”变量的类型用与使用。这种方法在定义变量的时候与 auto 很像,但是更多地用作定义新的类型使用。

vector<int> a;
decltype(a) b = a;

// 在标准库当中
typedef decltype(nullptr) nullptr_t;

左右值

  • 左值:有命名、可以取地址的对象;
  • 右值:不能取地址、没有名字的对象。

这样子可能不是很理解,可以看一段代码。

int a = 1, b = 2;
int c = a + b;

在这里,\(a\)\(b\) 都是左值,是可以赋值的对象,并且有名字。\(1\)\(2\) 还有 \(a+b\) 都是右值,他们是不能赋值、只能在等号右边的东西。其中,\(a+b\) 是“纯右值”,是在运算过程中产生出的临时变量。

左值与右值的引用也完全不同。左值的引用使用的是 &,表示引用一个左值。右值的引用是 &&,表示引用的是一个右值(而不是引用引用)。

int a, &b = a;      // 引用左值
b = 4;              // a 也变成了 4
const int &c = 10;  // 常引用,引用常量

int a = 1, b = 2;
int &&c = a + b;         // ok,引用了纯右值
int &&d = std::move(a);  // 转化为右值

范围 for 循环

对于有迭代器的对象(包括数组),可以使用范围 for 循环简单地获取里面所有的值。有迭代器的 STL:vector, list, deque……注意默认只能正向迭代,反向迭代还是要手写。

// 甚至 C++11 之前连 auto 都不能使
for (auto i = v.begin(); i != v.end(); i++) {
  cout << *i << ' ';
}
for (int i : v) {
  cout << i << ' ';
}

列表初始化

在 C++11 中,可以使用一对花括号来对数组、vector 和对象进行初始化,也可以用于函数的返回值的初始化。

struct A {
  int a;
  A(const int &a) {
    this->a = a;
  }
}

A a(114514);
A b = {114514};
A c {114514};

vector<int> v {1, 1, 4, 5, 1, 4};
int a[] {1, 1, 4, 5, 1, 4};

可以使用 std::initializer_list 来接受列表,并且支持任意长度。比如,一下代码就可以在多个值当中求出最大值,但是需要使用花括号括起来。

template <typename Ty>
Ty max(std::initializer_list<Ty> list) {
  if (list.empty()) {
    return Ty();
  }
  Ty mx = *list.begin();
  for (int i : list) {
    mx = max(mx, i);
  }
  return mx;
}

Lambda 表达式

在 C++11 中,可以使用 Lambda 表达式来定义匿名函数,让你的代码变得更加灵活简介。特别是在排序的时候了,这时你就没有必要去思考这个排序函数到底需要使用什么名字,而是直接调用就行了。

Lambda 表达式的定义方式如下:

[调用方式](参数列表) -> 返回值 {
  函数体
};

比如,可以使用这样子的方式定义一个匿名的计算两数之和的函数,并赋给 plus 这个对象。

auto plus = [](int a, int b) {
  return a + b;
};

你肯定会说,这里的调用方式是空的啊。别急,我来详细讲解:

调用方式 最终结果
[] 什么也不会捕获,Lambda 表达式无法使用外部变量
[=] 按赋值方式捕获所有的外部变量
[&] 按引用方式捕获所有的外部变量
[=,&a] 引用捕获 \(a\),其他按赋值方式捕获
[&,a] 复制捕获 \(a\),其他按引用方式捕获
[a,&b] 以指定方式捕获指定变量
[this] 捕获 this 指针

比如,可以这样获得每一个数排序之前的位置。

for (int i = 1; i <= n; i++) {
  id[i] = i;
}
sort(id + 1, id + n + 1, [](int &x, int &y)) {
  return a[x] < b[y]; 
};

constexpr

constexpr 是 C++11 的新型关键字,用于修饰编译时的常量。与 const 不同的是,const 修饰的变量只是在运行的时候不会被更改,但是在内存中它仍然是动态的。constexpr 才是真正的常量。

当它修饰一个常量的时候,这个常量在编译的时候就会替换后面的值。当它修饰一个函数的时候,如果这个函数的值可以直接在编译的时候算出来,那么它就会直接算出来,否则就像普通的函数一样处理。

template <typename Ty>
constexpr Ty max(const Ty &a, const Ty &b) {
  return a > b ? a : b;
}

int a, b;

int main() {
  cin >> a >> b;
  cout << max(0, 1) << '\n';  // 直接算出来
  cout << max(a, b) << '\n';  // 当做普通的函数
  return 0;
}

final 与 override

两者都是 C++ 在类与对象当中的补充。final 可以修饰一个类,表示这一个类禁止被继承。override 用来修饰派生类里面重写的基类函数,避免重写错误。

struct A final {
  void out() {
    cout << "好鱼鱼炸裂!" << endl;
  }
};

struct B : public A {  // 错误,无法继承
  void out() override {  // 重写成功
    cout << "好渴鹅炸裂!" << endl;
  }

  void print() override {  // 错误,父类没有该函数
    cout << "输出!" << endl;
  }
}

default 与 delete

在 C++11 之前,自己定义的类如果已经自己定义了其他构造函数,那么编译器就不会自动生成默认构造函数,因此你就无法直接初始一个对象。现在,只需要在构造函数后面加上 =default,那么编译器就会自动生成默认构造函数和拷贝构造函数等。

struct Node {
  Node(int a) { }
} a;  // 找不到构造函数
struct Node {
  Node() = default;
  Node(int a) { }
} a;  // ok

而如果你的类里面没有定义拷贝的成员函数,那么编译器就会自动生成拷贝的构造函数和 operator= 运算符。但是有时候我们并不想要对象进行拷贝操作,那么我们就可以在后面标上 =delete 的标识,表示让编译器不要自动生成。

struct Node {
  Node() = default;
  Node(const Node &n) = delete;
  Node &operator=(const Node &n) = delete;
};

这样就不能进行拷贝操作了。

explicit

explicit 是用来修饰构造函数的,被修饰的构造函数只能被显式地构造,而不能隐式转换,通俗来说就是需要强制类型转换。比如:

struct A {
  A(int x) { }
};

struct B {
  explicit B(int x) { }
};

A a = 1;  // 可以转换
B b = 1;  // 不行
b(1);     // 可以

模板的改进

右尖括号改进

在 C++11 以前,我们定义一个小根堆需要写 std::priority_queue<int, std::vector<int>, std::greater<int> >,很多人搞不懂为什么两个右尖括号之间需要空上一格,其实是因为不加空格的话,编译器会认为这是一个右移运算符而不是模板的参数。而在 C++11 之后,这个空格可以省略了,只需要写 std::priority_queue<int, std::vector<int>, std::greater<int>> 即可。

值得注意的是,greater<int> 当中的 int 也可以省略,以为默认模板参数已经定义成了 int 了。

std::priority_queue<int, std::vector<int>, greater<>> q;  // ok

函数模板默认参数

而在 C++11 之后,函数模板也支持了默认参数了。(而在以前只有类模板可以使用)

template <class Ty = int, class Tp>
Ty max(Ty a, Tp b) {
  return a;
}

甚至可以直接跳过 Ty,把参数传入 Tp 这种写法也可以。(在函数形参里面只有最后面几个形参支持默认参数)

max<double>(1, 1.0);  // ok

加强版 using

C++11 还引入了 using 来定义别名。很多人觉得它跟 typedef 没有什么不同,但实际上 using 的功能更加强大,因为它支持使用模板。比如可以以以下的方式来定义一个小根堆:

template <class Ty>
using heap = std::priority_queue<Ty, std::vector<Ty>, std::greater<Ty>>;

heap<int> q;  // ok

C++11 还对参数模板进行了优化,衍生出了“变长参数模板”。比如,可以以一下方式来定义一个输出若干个数的函数:

template <class Ty>
void debug(const Ty &x) {  // 递归的边界
  cout << x << '\n';       // 只输出一个数
}

template <class Ty, class... More>
void debug(const Ty &x, More... more) {
  cout << x << ' ';  // 输出当前的第一个数
  debug(more...);    // 输出后面的数
}

线程

thread

C++11 引入了 std::thread 用来创建线程。话不多说,看代码可能更直观一些:

using namespace std;

auto haokee = [](int x = 0) {
  cout << "我是好渴鹅!" << x << endl;
  this_thread::sleep_for(1s);
};

thread t1(haokee);          // 不添加参数
t1.join();                  // 堵住当前线程来执行,程序会堵上一秒
cout << "执行完成" << endl;  // 执行完成
thread t2(haokee, 114514);  // 加入参数
t2.detach();                // 不等它一起运行,可能没有输出(因为主线程已经结束了)

对于一个线程,我们可以使用 join 来让其堵住一秒的时间;可以使用 detach 来让这个线程与主线程一起执行。需要注意的是,当主线程已经执行完了的话,那么其他还没有执行完的线程就会在后台运行,直到运行结束为止。还有一个函数是 joinable,使用来判断一个线程能否 joindedtach 的。

注意一个线程必须调用这两个函数当中的一个一次,不然就会发生错误。

评论