1.C++新特性-灵析社区

菜鸟码转

C++ 11 与 C++ 98 相比,引入新特性有很多,从面试的角度来讲,如果面试官问到该问题,常以该问题作为引子,对面试者提到的知识点进行深入展开提问。面试者尽可能的列举常用的并且熟悉的特性,尽可能的掌握相关原理,下文只是对相关知识点进行了简单的阐述,有关细节还需要结合相关知识点的相关问题。下面主要介绍 C++ 11 中的一些面试中经常遇到的特性。

1.auto 类型推导:

auto 关键字:自动类型推导,编译器会在 编译期间 通过初始值或者函数返回值推导出变量的类型,通过 auto 定义的变量必须有初始值。

auto 关键字基本的使用语法如下:

C++

auto var = val1 + val2; // 根据 val1 和 val2 相加的结果推断出 var 的类型
auto ret = [](double x){return x*x;}; // 根据函数返回值推导出 ret 的类型
auto al = { 10, 11, 12 }; //类型是std::initializer_list<int>

使用 auto 关键字做类型自动推导时,依次施加以下规则:

首先,如果初始化表达式是引用,首先去除引用;

上一步后,如果剩下的初始化表达式有顶层的 const 或 volatile 限定符,去除掉。使用 auto 关键字声明变量的类型,不能自动推导出顶层的 const 或者 volatile,也不能自动推导出引用类型,需要程序中显式声明,比如以下程序:

C++

const int v1 = 101;
auto v2 = v1;       // v2 类型是int,脱去初始化表达式的顶层const
v2 = 102;            // 可赋值
int a = 100;
int &b = a; 
auto c = b;          // c 类型为int,脱去初始化表达式的 &

初始化表达式为数组,auto 关键字推导的类型为指针。数组名在初始化表达式中自动隐式转换为首元素地址的右值。

C++

int a[9]; 
auto j = a; // 此时j 为指针为 int* 类型,而不是 int(*)[9] 类型
std::cout << typeid(j).name() << " "<<sizeof(j)<<" "<<sizeof(a)<< std::endl;

注意:编译器推导出来的类型和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则。

2.decltype 类型推导:

decltype 关键字:decltype 是 “declare type” 的缩写,译为“声明类型”。和 auto 的功能一样,都用来在编译时期进行自动类型推导。如果希望从表达式中推断出要定义的变量的类型,但是不想用该表达式的值初始化变量,这时就不能再用 auto。decltype 作用是选择并返回操作数的数据类型。

区别:

C++

auto var = val1 + val2; 
decltype(val1 + val2) var1 = 0; 

auto 根据 = 右边的初始值 val1 + val2 推导出变量的类型,并将该初始值赋值给变量 var;decltype 根据 val1 + val2 表达式推导出变量的类型,变量的初始值和与表达式的值无关。auto 要求变量必须初始化,因为它是根据初始化的值推导出变量的类型,而 decltype 不要求,定义变量的时候可初始化也可以不初始化。

类似于 sizeof 操作符,decltype 不对其操作数求值。decltype(e) 返回类型前,进行了如下推导:

  • 若表达式 e 为一个无括号的变量、函数参数、类成员访问,那么返回类型即为该变量或参数或类成员在源程序中的“声明类型”;
  • 否则的话,根据表达式的值分类(value categories),设 T 为 e 的类型:

若 e 是一个左值(lvalue,即“可寻址值”),则 decltype(e) 将返回T&;

若 e 是一个临终值(xvalue),则返回值为 T&& ;

若 e 是一个纯右值(prvalue),则返回值为 T。

const int&& foo();
const int bar();
int i;
struct A { double x; };
const A* a = new A();
decltype(foo()) x1; // 类型为const int&&
decltype(bar()) x2; // 类型为int
decltype(i) x3; // 类型为int
decltype(a->x) x4; // 类型为double
decltype((a->x)) x5; // 类型为const double&

3.lambda 表达式

lambda 表达式,又被称为 lambda 函数或者 lambda 匿名函数。

lambda 匿名函数的定义:

C++

[capture list] (parameter list) -> return type
{
function body;
};

其中:

  • capture list:捕获列表,指 lambda 所在函数中定义的局部变量的列表。定义在与 lambda 函数相同作用域的参数引用也可以被使用,一般被称
[]      // 没有定义任何变量。使用未定义变量会引发错误。
[x, &y] // x以传值方式传入(默认),y以引用方式传入。
[&]     // 任何被使用到的外部变量都隐式地以引用方式加以引用。
[=]     // 任何被使用到的外部变量都隐式地以传值方式加以引用。
[&, x]  // x显式地以传值方式加以引用。其余变量以引用方式加以引用。
[=, &z] // z显式地以引用方式加以引用。其余变量以传值方式加以引用。

比如下面以引用的方式调用 a:

C++

int main()
{
    int a = 10;
    auto f = [&a](int x)-> int {
        a = 20;
        return a + x;
    };
    cout<<a<<endl; // 10
    cout<<f(10)<<endl; // 30
    cout<<a<<endl; // 20
    return 0;
}

return type、parameter list、function body:分别表示返回值类型、参数列表、函数体,和普通函数一样。

举例:

C++

#include <iostream>
#include <algorithm>
using namespace std;

int main()
{
    int arr[4] = {4, 2, 3, 1};
    //对 a 数组中的元素进行升序排序
    sort(arr, arr + 4, [=](int x, int y) -> bool{ return x < y; } );
    auto f = [&](int x)-> int {
        return arr[0] + x;
    }

    for(int n : arr){
        cout << n << " ";
    }
    return 0;
}

需要注意的是 lambda 函数按照值方式捕获的环境中的变量,在 lambda 函数内部是不能修改的。否则,编译器会报错。其值是 lambda 函数定义时捕获的值,不再改变。如果在 lambda 函数定义时加上 mutable 关键字,则该捕获的传值变量在 lambda 函数内部是可以修改的,对同一个 lambda 函数的随后调用也会累加影响该捕获的传值变量,但对外部被捕获的那个变量本身无影响。

C++

4.范围 for 语句:

语法格式:

C++

for (declaration : expression){
    statement
}

参数的含义:

expression:必须是一个序列,例如用花括号括起来的初始值列表、数组、vector,string 等,这些类型的共同特点是拥有能返回迭代器的 beign、end 成员。

declaration:此处定义一个变量,序列中的每一个元素都能转化成该变量的类型,常用 auto 类型说明符。

实例:

C++

#include <iostream>
#include <vector>
using namespace std;
int main() {
    char arr[] = "hello world!";
    for (char c : arr) {
        cout << c;
    }  
    return 0;
}
/*
程序执行结果为:
hello world!
*/

5.右值引用:

C++ 表达式中的 “值分类”(value categories)属性为左值或右值。其中左值是对应(refer to)内存中有确定存储地址的对象的表达式的值,而右值是所有不是左值的表达式的值。因而,右值可以是字面量、临时对象等表达式。能否被赋值不是区分 C++ 左值与右值的依据,C++ 的 const 左值是不可赋值的;而作为临时对象的右值可能允许被赋值。左值与右值的根本区别在于是否允许取地址 & 运算符获得对应的内存地址。C++ 标准定义了在表达式中左值到右值的三类隐式自动转换:

  • 左值转化为右值;如整数变量 i 在表达式 (i+3);
  • 数组名是常量左值,在表达式中转化为数组首元素的地址值;
  • 函数名是常量左值,在表达式中转化为函数的地址值;

C++ 03 在用临时对象或函数返回值给左值对象赋值时的深度拷贝(deep copy),因此造成性能低下。考虑到临时对象的生命期仅在表达式中持续,如果把临时对象的内容直接移动(move)给被赋值的左值对象(右值参数所绑定的内部指针复制给新的对象,然后把该指针置为空),效率改善将是显著的。右值引用就是为了实现 move 与 forward 所需要而设计出来的新的数据类型。右值引用的实例对应于临时对象;右值引用并区别于左值引用,用作形参时能通过函数重载来区别对象是调用拷贝构造函数还是移动拷贝构造函数。实际上无论是左值引用还是右值引用,从编译后的反汇编层面上,都是对象的存储地址的引用。右值引用与左值引用的变量都不能悬空,也即定义时必须初始化从而绑定到一个对象上。

C++ 右值引用即绑定到右值的引用,用 && 来获得右值引用,右值引用只能绑定到要销毁的对象。为了和右值引用区分开,常规的引用称为左值引用。左值引用是绑定到左值对象上;右值引用是绑定到临时对象上。左值对象是指可以通过取地址 & 运算符得到该对象的内存地址;而临时对象是不能用取地址 & 运算符获取到对象的内存地址,具体的引用绑定规则如下:

  • 非常量左值引用(X &):只能绑定到 X 类型的左值对象;
  • 常量左值引用(const X &):可以绑定到 X、const X 类型的左值对象,或 X、const X 类型的右值;
  • 非常量右值引用(X &&):只能绑定到 X 类型的右值;
  • 常量右值引用(const X &&):可以绑定规定到 X、const X 类型的右值。

举例:

C++

#include <iostream>
#include <vector>
using namespace std;
int main()
{
    int var = 42;
    int &l_var = var;
    int &&r_var = var; // error: cannot bind rvalue reference of type 'int&&' to lvalue of type 'int' 错误:不能将右值引用绑定到左值上
    int &&r_var2 = var + 40; // 正确:将 r_var2 绑定到求和结果上
    int &&r_var3 = std::move(var) // 正确
    return 0;
}

6.标准库 move() 函数

move() 函数:通过该函数可获得绑定到左值上的右值引用。通过 move 获取变量的右值引用,从而可以调用对象的移动拷贝构造函数和移动赋值构造函数。

7.智能指针:

auto_ptr 在 C++ 11 中被,取而代之的是 unique_ptr。智能指针在第一章中已经详细,可以参考第一章第 9 节。

8.使用或禁用对象的默认函数:

在旧版本的 C++ 中,若用户没有提供,则编译器会自动为对象生成默认构造函数(default constructor)、复制构造函数(copy constructor),赋值运算符(copy assignment operator operator=)以及析构函数(destructor)。另外,C++ 也为所有的类定义了数个全局运算符(如operator delete 及 operator new)。当用户有需要时,也可以提供自定义的版本改写上述的函数。由于无法精确地控制这些默认函数的生成,要让类不能被拷贝,必须将复制构造函数与赋值运算符声明为 private,并不去定义它们,尝试使用这些未定义的函数会导致编译期或链接期的错误。此外,编译器产生的默认构造函数与用户定义的构造函数无法同时存在。若用户定义了任何构造函数,编译器便不会生成默认构造函数; 但有时同时部分场景下需要同时具有两者提供的构造函数。C++ 11 中允许显式地表明采用或拒用编译器提供的内置函数。

  • 允许编译器生成默认的构造函数:default 函数:= default 表示编译器生成默认的函数,例如:生成默认的构造函数。
  • 禁止编译器使用类或者结构体中的某个函数:

delete 函数:= delete 修改某个函数则表示该函数不能被调用。与 default 不同的是,= delete 也能适用于非编译器内置函数,所有的成员函数都可以用 =delete 来进行修饰。

程序示例:

C++

#include <iostream>
using namespace std;

class A
{
public:
	A() = default; // 表示使用默认的构造函数
	~A() = default;	// 表示使用默认的析构函数
	A(const A &) = delete; // 表示类的对象禁止拷贝构造
	A &operator=(const A &) = delete; // 表示类的对象禁止拷贝赋值
};
int main()
{
	A ex1;
	A ex2 = ex1; // error: use of deleted function 'A::A(const A&)'
	A ex3;
	ex3 = ex1; // error: use of deleted function 'A& A::operator=(const A&)'
	return 0;
}

9.constexpr:

常量表示式对编译器来说是优化的机会,编译器时常在编译期执行它们并且将值存入程序中。同样地,在许多场合下,C++ 标准要求使用常量表示式。例如在数组大小的定义上,以及枚举值(enumerator values)都要求必须是常量表示式。常量表示式不能含有函数调用或是对象构造函数。所以像是以下的例子是不合法的:

C++

int g() {return 5;}
int f[g() + 10]; // 不合法的C++ 写法

由于编译器无从得知函数 g() 的返回值为常量,因此表达式 g() + 10 就不能确定是常量。C++ 11 引进关键字 constexpr 允许用户保证函数或是对象构造函数是编译期常量,编译器在编译时将去验证函数返回常量。

constexpr int g() {return 5;}
int f[g() + 10]; // 合法

用 constexpr 修饰函数将限制函数的行为。

  • 函数的回返值类型不能为void;
  • 函数体不能声明变量或定义新的类型;
  • 函数体只能包含声明、null语句或者一段return语句;
  • 函数的内容必须依照 "return expr" 的形式,在参数替换后,expr 必须是个常量表达式;
  • 这些常量表达式只能够调用其他被定义为 constexpr 的函数,或是其他常量形式的参数。

constexpr 修饰符的函数直到在该编译单元内被定义之前是不能够被调用的。声明为 constexpr 的函数也可以像其他函数一样用于常量表达式以外的调用。

C++ 11 中的常量表达式中的变量都必须是常量,可以使用 constexpr 关键字来定义表达式中的变量:

C++

constexpr double PI = 3.14;
constexpr double Degree = PI * 2.0;

如果创建用户定义类型的常量表达式,则自定义类型的构造函数必须用 constexpr 声明,函数体仅包含声明或 null 语句,不能声明变量或定义类型。构造函数的实参值应该是常量表达式,直接初始化类的数据成员。同时该类型对象的拷贝构造函数应该也定义为 constexpr,以允许 constexpr 函数返回一个该类型的对象。C++ 14 以后的规则有所改动。

10.初始化列表 initializer list:

C++ 11 把初始化列表的定义为标准类型,称作 std::initializer_list。允许构造函数或其他函数像参数般地使用初始化列表,在对象中可以定义初始化列表构造函数。初始化列表是常量;一旦被创建,其成员均不能被改变,成员中的资料也不能够被变动。在 C++ 11 中初始化列表是标准类型,除了对象的构造函数之外还能够被用在其他地方,一般的函数能够使用初始化列表作为形参。

C++

void f(std::initializer_list<int> list);
f({1, 2, 3, 4, 5}); // 初始化列表作为形参
vector<int> arr1 = {1, 2, 3, 4, 5}; // 初始化列表构造函数
vector<int> arr2({1, 2, 3, 4, 5});

11.nullptr:

在 C 语言中,常量 0 带有常量及空指针的双重身份。C 使用宏定义 NULL 表示空指针,让 NULL 及 0 分别代表空指针及常量 0。 NULL 可被定义为 ((void*)0) 或是 0。这样容易引起语义歧义,比如 char* c = NULL,NULL 只能定义为 0,这样可能使得函数重调用错误,比如调用 f(NULL),NULL 隐式被转换为 0,这样实际编译器可能会调用 f(int),但实际上可能希望调用 f(char *)。

C++

void f(char *);
void f(int);

C++ 11 引入了新的关键字来代表空指针常量:nullptr,将空指针和整数 0 的概念拆开。nullptr 的类型为 nullptr_t,能隐式转换为任何指针或是成员指针的类型,也能和它们进行相等或不等的比较。

C++

typedef decltype(nullptr) nullptr_t;

nullptr 不能隐式转换为整数,也不能和整数做比较,因此就避免上述的语义歧义。值得注意的是的 f(nullptr_t) 被隐式转换为 foo(char *) 只会发生在该函数不存在其它的指针类型重载(比如 f(int *), f(double *) 等)时候,否则就会产生歧义错误(可以通过显示声明一个 foo(nullptr_t) 来消除该歧义),如果存在多个指针类型重载,此时需要 f(nullptr) 时,则需要显示声明一个函数来消除歧义。

C++

void f(char *);
void f(int *);
void f(int);
void f(nullptr_t);

12.可扩展的随机数功能:

C++ 11 将会提供产生伪随机数的新方法。C++ 11 的随机数功能分为两部分:

  • 随机数生成引擎,其中包含该生成引擎的状态,用来产生随机数。
  • 随机数分布,这可以用来决定产生随机数的范围,也可以决定以何种分布方式产生随机数。
  • 随机数生成对象即是由随机数生成引擎和分布所构成。

针对产生随机数的机制,C++ 11 将会提供三种算法,每一种算法都有其强项和弱项:

  • linear_congruential:可以产生整数,速度较慢,随机数质量较差;
  • subtract_with_carry: 可以产生整数和随机数,速度较快,随机数质量中等;
  • mersenne_twister:可以产生整数,速度较快,随机数质量较好;

C++ 11 将会提供一些标准分布:uniform_int_distribution(离散型均匀分布),bernoulli_distribution(伯努利分布),geometric_distribution(几何分布),poisson_distribution(卜瓦松分布),binomial_distribution(二项分布),uniform_real_distribution(离散型均匀分布),exponential_distribution(指数分布),normal_distribution(正态分布)和 gamma_distribution(伽玛分布)。

C++

std::uniform_int_distribution<int> distribution(0, 99); // 离散型均匀分布
std::mt19937 engine; // 随机数生成引擎
auto generator = std::bind(distribution, engine); // 将随机数生成引擎和分布绑定生成函数
int random = generator();  // 产生随机数


阅读量:2030

点赞量:0

收藏量:0