logo头像
Snippet 博客主题

c++11新特性

前言

现代C++借鉴了很多脚本语言特性,编写越来越简洁高效了。下面就C++11新增特性做一个总结。

1. auto类型推导

auto只是一个占位符,在编译时会被替换为真正的类型。因此auto要求变量必须初始化。它很方便但是也有一些限制需要注意:

  1. auto不能在函数的参数中使用。
  2. auto不能用于类的非静态成员变量。
  3. auto不能定义数组。
  4. auto不能用于模板参数。

2. decltype类型推导

和auto作用一样也是用来自动推导类型的,用于补充auto无法使用的场景。auto是根据=号右边的值推导,而decltype则是根据表达式推导,它不要求变量必须初始化。形式:decltype(exp) varname;

decltype推导规则:

  • 如果 exp 是一个不被括号( )包围的表达式,或者是一个类成员访问表达式,或者是一个单独的变量,那么 decltype(exp) 的类型就和 exp 一致,这是最普遍最常见的情况。
  • 如果 exp 是函数调用,那么 decltype(exp) 的类型就和函数返回值的类型一致。
  • 如果 exp 是一个左值,或者被括号( )包围,那么 decltype(exp) 的类型就是 exp 的引用。

注释:左值是指那些在表达式执行结束后依然存在的数据,也就是持久性的数据;右值是指那些在表达式执行结束后不再存在的数据,也就是临时性的数据。

3. 返回值类型后置

用于解决函数返回值类型依赖于参数而导致难以确定返回值类型的问题。一般在模板函数中多见,如下:

1
2
3
4
5
6
// 综合了auto和decltype
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u)
{
return t + u;
}

4. 对模板实例化中连续右尖括号>>的改进

在c++98中,如果实例化模板的时候出现了>>,为了与右移操作区分,需要它们之间加一个空格分隔(> >),不然编译错误。

1
2
3
4
5
6
7
8
9
// 示例1:嵌套的模板标识
template <int i> class X {}; // 常量模板,一般用于优化编译
template <class T> class Y {}; // 类型模板
Y<X<1> > x1; // 编译成功
Y<X<1>> x2; // 编译失败

// 示例2:强制转换
const vector<int> v = static_cast<vector<int> >(v); // 编译成功
const vector<int> v = static_cast<vector<int>>(v); // 编译失败

在c++11中取消了这种限制,编译器可以智能自己识别了。但是有个特殊场景会带来和c++98不兼容的情况:

1
2
template <int i> class X {}; 
X < 1 >> 5 > x;

这个在c++98上可以编译通过,1 >> 5会认为是右移,然后得到形如X<0> x的模板实例。而在c++11上编译失败,解决的办法是加()

1
2
template <int i> class X {}; 
X <(1 >> 5)> x; // 编译成功

5. 使用using定义别名(替代typedef)

使用using定义别名有些场景比typedef更加简洁而且更符合人们的理解。

1
using dstname = srctype;

using定义别名可以全面替换掉typedef,下面举几个与typedef对比明显优势的例子:

示例1:模板取别名

1
2
3
4
5
6
7
8
// 使用typedef
template <typename Val>
struct str_map
{
typedef std::map<std::string, Val> type;
};
// ...
str_map<int>::type map1;
1
2
3
4
5
// 使用using
template <typename Val>
using str_map_t = std::map<std::string, Val>;
// ...
str_map_t<int> map1;

示例2:函数指针取别名

1
2
// 使用typedef
typedef void (*func_t)(int, int);
1
2
// 使用using(这种更好理解)
using func_t = void (*)(int, int);

6. 支持函数模板的默认模板参数

在c++11之前,只支持类模板的默认参数,到现在函数模板也支持默认模板参数了,而且编译器还可以自行推导出部分模板参数的类型。注意模板参数的默认类型与位置无关,这是区别函数默认参数的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename R = int, typename U>
R func(U val)
{
return val;
}

int main()
{
func(97); // R=int, U=int R没指定用默认int,U传递的97自动推导为int
func<char>(97); // R=char, U=int R指定为char,U传递的97自动推导为int
func<double, int>(97); // R=double, U=int R指定为double,U指定为int
return 0;
}

7. 在函数模板和类模板中使用可变参数

一、函数模板的可变参数:

1
2
// 声明形式
template<typename... Types>

其中...可接纳的模板参数个数是0个及以上的任意数量,需要注意包括0个

下面通过示例来了解具体用法:

我们定义了3个print的重载,注意首先匹配到的是中间带特化参数的可变参数模板,然后内部递归调用print,传递的是args…可变参数,还是优先匹配中间带特化参数的模板,直到递归到最后args…没有一个参数,匹配第一个无参print结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>

void print()
{
std::cout << "print (null)" << std::endl;
}

// 带特化参数
template <typename T, typename... Types>
void print(const T& firstArg, const Types&... args)
{
// sizeof...(args)获取可变参数个数
std::cout << firstArg << " " << sizeof...(args) << std::endl;
// 递归
print(args...);
}

// 不带特化参数
template <typename... Types>
void print(const Types&... args)
{
std::cout << "print(...)" << std::endl;
}

int main(int argc, char* argv[])
{
print("hello", "world", 7, 8);
return 0;
}
1
2
3
4
5
hello 3
world 2
7 1
8 0
print (null)

我们可以看到这种带特化参数的可变参数函数模板,在递归里比较有趣。我们再来看一个求一组数里最大的数,用这种方式的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

template <typename T>
T my_max(T value)
{
return value;
}

template <typename T, typename... Types>
T my_max(T value, Types... args)
{
return std::max(value, my_max(args...));
}

int main(int argc, char* argv[])
{
std::cout << my_max(1, 5, 8, 4, 6) << std::endl;

return 0;
}
1
结果: 8

二、类模板中的可变参数:

我们看一个最典型的tuple元组类,c++11已经提供了。下面是一个用类模板可变参数实现的简化版:

主要使用的场景是可以存储一组不同类型的数据,如用于函数返回值和传参等就不需要以前那样定义结构体了,很方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>

template <typename... Values>
class tuple;

template <>
class tuple<> { };

template <typename Head, typename... Tail>
class tuple<Head, Tail...> : private tuple<Tail...>
{
typedef tuple<Tail...> inherited;

public:
tuple() { }
tuple(Head v, Tail... vtail)
: m_head(v)
, inherited(vtail...)
{
}
Head& head() { return m_head; }
inherited& tail() { return *this; }

protected:
Head m_head;
};

int main(int argc, char* argv[])
{
tuple<int, double, std::string> t(1, 2.3, "hello");
std::cout << t.head() << " " << t.tail().head() << " " << t.tail().tail().head() << std::endl;

return 0;
}
1
1 2.3 hello

8. tuple元组

C++11标准新引入了一种类模板,命名为 tuple(中文可直译为元组)。tuple 最大的特点是:实例化的对象可以存储任意数量、任意类型的数据。tuple 的应用场景很广泛,例如当需要存储多个不同类型的元素时,可以使用 tuple;当函数需要返回多个数据时,可以将这些数据存储在 tuple 中,函数只需返回一个 tuple 对象即可。

1
2
#include <tuple>
using std::tuple;

tuple 本质是一个以可变模板参数定义的类模板,它定义在 头文件并位于 std 命名空间中。

实例化 tuple 模板类对象常用的方法有两种,一种是借助该类的构造函数,另一种是借助 make_tuple() 函数。

一、类构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1) 默认构造函数
constexpr tuple();
// 2) 拷贝构造函数
tuple (const tuple& tpl);
// 3) 移动构造函数
tuple (tuple&& tpl);
// 4) 隐式类型转换构造函数
template <class... UTypes>
tuple (const tuple<UTypes...>& tpl); //左值方式
template <class... UTypes>
tuple (tuple<UTypes...>&& tpl); //右值方式
// 5) 支持初始化列表的构造函数
explicit tuple (const Types&... elems); //左值方式
template <class... UTypes>
explicit tuple (UTypes&&... elems); //右值方式
// 6) 将pair对象转换为tuple对象
template <class U1, class U2>
tuple (const pair<U1,U2>& pr); //左值方式
template <class U1, class U2>
tuple (pair<U1,U2>&& pr); //右值方式

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <tuple>
using std::tuple;

int main()
{
std::tuple<int, char> first; // 1) first{}
std::tuple<int, char> second(first); // 2) second{}
std::tuple<int, char> third(std::make_tuple(20, 'b')); // 3) third{20,'b'}
std::tuple<long, char> fourth(third); // 4)的左值方式, fourth{20,'b'}
std::tuple<int, char> fifth(10, 'a'); // 5)的右值方式, fifth{10.'a'}
std::tuple<int, char> sixth(std::make_pair(30, 'c')); // 6)的右值方式, sixth{30,''c}
return 0;
}

二、make_tuple()函数

1
2
3
4
// make_tuple() 函数创建了 tuple 对象
auto first = std::make_tuple (10,'a'); // tuple < int, char >
const int a = 0; int b[3];
auto second = std::make_tuple (a,b); // tuple < int, int* >

最后看下tuple常用的函数:

函数或类模板 描 述
tup1.swap(tup2)
swap(tup1, tup2)
tup1 和 tup2 表示类型相同的两个 tuple 对象,tuple 模板类中定义有一个 swap() 成员函数, 头文件还提供了一个同名的 swap() 全局函数。swap() 函数的功能是交换两个 tuple 对象存储的内容。
get(tup) tup 表示某个 tuple 对象,num 是一个整数,get() 是 头文件提供的全局函数,功能是返回 tup 对象中第 num+1 个元素。
tuple_size::value tuple_size 是定义在 头文件的类模板,它只有一个成员变量 value,功能是获取某个 tuple 对象中元素的个数,type 为该tuple 对象的类型。
tuple_element<I,type>::type tuple_element 是定义在 头文件的类模板,它只有一个成员变量 type,功能是获取某个 tuple 对象第 I+1 个元素的类型。
forward_as_tuple<args…> args… 表示 tuple 对象存储的多个元素,该函数的功能是创建一个 tuple 对象,内部存储的 args… 元素都是右值引用形式的。
tie(args…) = tup tup 表示某个 tuple 对象,tie() 是 头文件提供的,功能是将 tup 内存储的元素逐一赋值给 args… 指定的左值变量。
tuple_cat(args…) args… 表示多个 tuple 对象,该函数是 头文件提供的,功能是创建一个 tuple 对象,此对象包含 args… 指定的所有 tuple 对象内的元素。

下面是演示示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <tuple>
int main()
{
int size;
//创建一个 tuple 对象存储 10 和 'x'
std::tuple<int, char> mytuple(10, 'x');
//计算 mytuple 存储元素的个数
size = std::tuple_size<decltype(mytuple)>::value;
//输出 mytuple 中存储的元素
std::cout << std::get<0>(mytuple) << " " << std::get<1>(mytuple) << std::endl;
//修改指定的元素
std::get<0>(mytuple) = 100;
std::cout << std::get<0>(mytuple) << std::endl;
//使用 makde_tuple() 创建一个 tuple 对象
auto bar = std::make_tuple("test", 3.1, 14);
//拆解 bar 对象,分别赋值给 mystr、mydou、myint
const char* mystr = nullptr;
double mydou;
int myint;
//使用 tie() 时,如果不想接受某个元素的值,实参可以用 std::ignore 代替
std::tie(mystr, mydou, myint) = bar;
//std::tie(std::ignore, std::ignore, myint) = bar; //只接收第 3 个整形值
//将 mytuple 和 bar 中的元素整合到 1 个 tuple 对象中
auto mycat = std::tuple_cat(mytuple, bar);
size = std::tuple_size<decltype(mycat)>::value;
std::cout << size << std::endl;
return 0;
}
1
2
3
10 x
100
5

9. 初始化列表 (统一了初始化方式)

c++11以前对象的初始化有多种形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//1.初始化列表
int i_arr[3] = { 1, 2, 3 }; //普通数组
struct A
{
int x;
struct B
{
int i;
int j;
} b;
} a = { 1, { 2, 3 } }; //POD类型

//2.拷贝初始化(copy-initialization)
int i = 0;
class Foo
{
public:
Foo(int) {}
} foo = 123; //需要拷贝构造函数

//3.直接初始化(direct-initialization)
int j(0);
Foo bar(123);

现在,在c++11中,所有对象初始化都可以用初始化列表的形式了,全部统一了。很方便,以后再要初始化任何对象都可以用初始化列表的形式了。

1
2
3
// 带不带等号两种形式是等价的
Foo a1 = {123};
Foo a2 {123};

10. lambda匿名函数

又称lambda函数或者lambda表达式。这个在以前都是一些脚本语言常见的,现在咱们c++11也支持了。在有些场景使用lambda函数可以使代码更加简洁,如sort排序指定排序规则函数等。

lambda匿名函数其实就是没有名字的函数(当然你也可以给它赋值名字),本质还是函数,给函数提供了另一种简写形式。

1
2
3
4
5
6
7
8
// lambda函数定义格式
[外部变量访问形式](参数) mutable noexcept/throw() ->返回值类型
{
函数体;
};

// 最简lambda函数,其它部分可缺省
[]{};
  1. [外部变量访问形式]

    不能省略,指定外部变量的访问形式,外部变量是指与lambda函数位于同一作用域内的所有局部变量。对于全局变量则不受这里影响,可以随便访问和修改。

外部变量格式 功能
[] 空方括号表示当前 lambda 匿名函数中不使用任何外部变量。
[=] 表示以值传递的方式导入所有外部变量,lambda函数体内不能修改外部变量的值。
[&] 表示以引用传递的方式导入所有外部变量,lambda函数体内可以修改外部变量的值。
[val1, val2, …] 表示以值传递的方式导入 val1、val2 等指定的外部变量。
[&val1, &val2, …] 表示以引用传递的方式导入 val1、val2等指定的外部变量。
[val1, &val2, …] 以上 2 种方式还可以混合使用。
[=, &val1, …] 表示除 val1 以引用传递的方式导入外,其它外部变量都以值传递的方式导入。
[this] 表示以值传递的方式导入当前的 this 指针。
  1. (参数)

    和普通函数一样,lambda匿名函数也可以传递参数,如果不需要传递参数,可以省略。

  2. mutable

    此关键字可以省略,如果使用则之前的 () 小括号将不能省略。默认情况下,对于以值传递方式引入的外部变量,不允许在 lambda 表达式内部修改它们的值。而如果想修改它们,就必须加 mutable 关键字。

    注意,对于以值传递方式引入的外部变量,lambda 表达式修改的是拷贝的那一份,并不会修改真正的外部变量。

  3. noexcept/throw()

    可以省略,如果使用,在之前的 () 小括号将不能省略。默认情况下,lambda 函数的函数体中可以抛出任何类型的异常。而标注 noexcept 关键字,则表示函数体内不会抛出任何异常;使用 throw() 可以指定 lambda 函数内部可以抛出的异常类型

  4. ->返回值类型

    如果 lambda 函数体内只有一个 return 语句,或者该函数返回 void,则编译器可以自行推断出返回值类型,此情况下可以直接省略。

  5. 函数体

    和普通函数一样,没什么说的。

示例: 值传递和引用传递的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
using namespace std;
//全局变量
int all_num = 0;
int main()
{
//局部变量
int num_1 = 1;
int num_2 = 2;
int num_3 = 3;
cout << "lambda1:\n";
auto lambda1 = [=]{
//全局变量可以访问甚至修改
all_num = 10;
//函数体内只能使用外部变量,而无法对它们进行修改
cout << num_1 << " "
<< num_2 << " "
<< num_3 << endl;
};
lambda1();
cout << all_num <<endl;

cout << "lambda2:\n";
auto lambda2 = [&]{
all_num = 100;
num_1 = 10;
num_2 = 20;
num_3 = 30;
cout << num_1 << " "
<< num_2 << " "
<< num_3 << endl;
};
lambda2();
cout << all_num << endl;
return 0;
}
1
2
3
4
5
6
lambda1:
1 2 3
10
lambda2:
10 20 30
100

11. 非受限联合体(union)

C++11之前使用union,对其内的成员有很多非必要的限制,在c++11上放开了这些限制。任何非引用类型都可以成为union的数据成员。

示例1:union可以含非POD类型成员

1
2
3
4
5
6
7
8
9
10
11
union T{
Student s; // 含有非POD类型的成员,gcc-5.1.0 版本报错
char name[10];

public:
T(bool g, int a): s(g, a) { }
~T() { }
};

// 注意非POD类型拥有的自定义构造函数、默认拷贝构造函数、拷贝赋值操作符以及析构函数会被编译器删除。
// 此时要构造union需要在其内自己实现构造/析构等函数。

示例2:union的匿名形式可以用于类内部声明可变的数据成员,这种类被称为”枚举式类”。根据调用的构造函数不同其类的数据结构也不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <cstring>
using namespace std;
class Student {
public:
Student(bool g, int a)
: gender(g)
, age(a)
{
}
bool gender;
int age;
};
class Singer {
public:
enum Type { STUDENT,
NATIVE,
FOREIGENR };
Singer(bool g, int a)
: s(g, a)
{
t = STUDENT;
}
Singer(int i)
: id(i)
{
t = NATIVE;
}
Singer(const char* n, int s)
{
int size = (s > 9) ? 9 : s;
memcpy(name, n, size);
name[s] = '\0';
t = FOREIGENR;
}
~Singer() { }

private:
Type t;
union {
Student s;
int id;
char name[10];
};
};
int main()
{
Singer(true, 13);
Singer(310217);
Singer("J Michael", 9);
return 0;
}

12. for循环扩展

C++11对for循环添加了一种新语法:

1
2
3
for(变量 : 序列) {
// 循环体
}
  • 变量:该变量的类型为要遍历序列中存储元素的类型,可以用auto定义变量,编译器自行推导类型。
  • 序列:常见的普通数组或容器,还可以是{1,2,3,…}大括号初始化的序列。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <vector>
using namespace std;
int main() {
char arc[] = "abcde";
vector<char>myvector(arc, arc + 5);
//for循环遍历并修改容器中各个字符的值
for (auto &ch : myvector) {
ch++;
}
//for循环遍历输出容器中各个字符
for (auto ch : myvector) {
cout << ch;
}
return 0;
}

注意:如果要修改遍历元素的值,要用&引用,如果不需要修改元素值,建议用const &形式,比普通变量效率更高,避免了底层复制变量的过程。

13. constexpr

constexpr是C++11新增的定义常量的关键字,在此之前只有const,那为什么要新增这个关键字了?

先说结论:

  1. 为了解决const的双重语意问题,容易引起歧义。
  2. 提高程序运行效率。

下面举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <array>
using namespace std;

void dis_1(const int x){
//错误,x是只读的变量
array <int,x> myarr{1,2,3,4,5};
cout << myarr[1] << endl;
}

void dis_2(){
const int x = 5;
array <int,x> myarr{1,2,3,4,5};
cout << myarr[1] << endl;
}

int main()
{
dis_1(5);
dis_2();
}

可以看到,dis_1() 和 dis_2() 函数中都包含一个 const int x,但 dis_1() 函数中的 x 无法完成初始化 array 容器的任务,而 dis_2() 函数中的 x 却可以。

这是因为,dis_1() 函数中的“const int x”只是想强调 x 是一个只读的变量,其本质仍为变量,无法用来初始化 array 容器;而 dis_2() 函数中的“const int x”,表明 x 是一个只读变量的同时,x 还是一个值为 5 的常量,所以可以用来初始化 array 容器。

所以C++11标准中,为了规范const的双重语意问题,保留了 const 表示“只读”的语义,而将“常量”的语义划分给了新添加的 constexpr 关键字。

因此 C++11 标准中,建议将 const 和 constexpr 的功能区分开,即凡是表达“只读”语义的场景都使用 const,表达“常量”语义的场景都使用 constexpr。

同时,使用constexpr可以提高执行效率,是因为对于constexpr修饰的常量在编译期间就会将其结果计算出来,而无需等到程序运行阶段。

const则不一定,如修饰函数时,const是运行期间计算。

1
2
3
4
5
6
7
constexpr int sqr1(int arg){
return arg*arg;
}

const int sqr2(int arg){
return arg*arg;
}

14. long long超长整形

C++11加入了long long超长整形,整数后一般添加ll/ull表示。

1
2
3
4
5
6
7
8
9
10
11
12
#include <climits>
#include <iomanip>
#include <iostream>
using namespace std;
int main()
{
cout << "long long最小值:" << LLONG_MIN << " " << hex << LLONG_MIN << "\n";
cout << dec << "long long最大值:" << LLONG_MAX << " " << hex << LLONG_MAX << "\n";
cout << dec << "unsigned long long最大值:" << ULLONG_MAX << " " << hex << ULLONG_MAX << "\n";
cout << "long long int当前平台所占字节数:" << sizeof(long long int);
return 0;
}
1
2
3
4
long long最小值:-9223372036854775808 8000000000000000
long long最大值:9223372036854775807 7fffffffffffffff
unsigned long long最大值:18446744073709551615 ffffffffffffffff
long long int当前平台所占字节数:8

15. 右值引用

C++11之前只有左值引用,在C++11又加入了右值引用(主要用于移动语义、完美转发)。下面是它们的定义形式:

1
2
3
4
5
6
// 左值引用
int a = 1;
int &b = a; // b可以看作是a的别名

// 右值引用
int && a = 1;

通常有两种方法判断某个表达式是左值还是右值:

  1. 可位于赋值号(=)左侧的表达式就是左值;反之,只能位于赋值号右侧的表达式就是右值。举个例子:

    1
    2
    int a = 5;  // a是左值
    5 = a; //错误,5 不能为左值
  2. 有名称的、可以获取到存储地址的表达式即为左值;反之则是右值。

右值引用使用场景:

  • 移动语义:主要用于对拷贝构造函数的优化。
  • 完美转发:主要用于函数模板内调用其它函数,优雅的传递左值或右值参数属性。

移动语义

移动语义也叫移动构造函数,即用其它对象初始化一个同类的新对象时,以前是调用类中的拷贝构造函数,如果有指针还需要深度拷贝,这个过程产生多次临时对象,而使用移动语义则是把已有对象的内存空间移为己用,不再新创建对象空间。

要深刻理解,还得跟我一起逐步分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
using namespace std;

class demo{
public:

demo():num(new int(0)){
cout<<"construct!"<<endl;
}

//拷贝构造函数
demo(const demo &d):num(new int(*d.num)){
cout<<"copy construct!"<<endl;
}

~demo(){
cout<<"class destruct!"<<endl;
}

private:
int *num;
};

demo get_demo(){
return demo();
}

int main(){
demo a = get_demo();
return 0;
}

这段程序在老版本编译器上执行过程:

1
2
3
4
5
6
7
8
9
10
construct!            <-- 执行 demo()
copy construct! <-- 执行 return demo()
class destruct! <-- 销毁 demo() 产生的匿名对象
copy construct! <-- 执行 a = get_demo()
class destruct! <-- 销毁 get_demo() 返回的临时对象
class destruct! <-- 销毁 a

// 注意新版编译器一般都做了优化了,只会执行:
construct!
class destruct!

可以看到进行了两次拷贝(而且是深拷贝)操作,如果临时对象再复杂点,那么这种方式势必影响执行效率。

我们使用移动语义改造下上面示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
using namespace std;

class demo{

public:
demo():num(new int(0)){
cout<<"construct!"<<endl;
}

demo(const demo &d):num(new int(*d.num)){
cout<<"copy construct!"<<endl;
}

//添加移动构造函数
demo(demo &&d):num(d.num){
d.num = NULL; // 避免同一块内存释放多次,转移给别人,自己清空。
cout<<"move construct!"<<endl;
}

~demo(){
cout<<"class destruct!"<<endl;
}

private:
int *num;
};
demo get_demo(){
return demo();
}
int main(){
demo a = get_demo();
return 0;
}

可以看到,在之前 demo 类的基础上,我们又手动为其添加了一个构造函数。和其它构造函数不同,此构造函数使用右值引用形式的参数,又称为移动构造函数。并且在此构造函数中,num 指针变量采用的是浅拷贝的复制方式,同时在函数内部重置了 d.num,有效避免了“同一块对空间被释放多次”情况的发生。

1
2
3
4
5
6
construct!
move construct!
class destruct!
move construct!
class destruct!
class destruct!

通过执行结果我们不难得知,当为 demo 类添加移动构造函数之后,使用临时对象初始化 a 对象过程中产生的 2 次拷贝操作,都转由移动构造函数完成。

我们知道,程序执行结果中产生的临时对象(例如函数返回值、lambda 表达式等)既无名称也无法获取其存储地址,属于右值。

当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。

在实际开发中,通常在类中自定义移动构造函数的同时,会再为其自定义一个适当的拷贝构造函数,由此当用户利用右值初始化类对象时,会调用移动构造函数;使用左值(非右值)初始化类对象时,会调用拷贝构造函数。

如果是左值,我又想用移动构造函数的话,C++11新引入了std::move(arg)函数,它可以将左值强制转换成对应的右值。其中,arg 表示指定的左值对象。该函数会返回 arg 对象的右值形式。

完美转发

完美转发指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。因此很多场景中是否实现完美转发,直接决定了该参数的传递过程使用的是拷贝语义(调用拷贝构造函数)还是移动语义(调用移动构造函数)。

c++11之前,函数模板中将自己的参数传递给内部调用的其它函数,很难保证参数左值或右值传递。还得采用函数模板重载的方式实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
using namespace std;

//重载被调用函数,查看完美转发的效果
void otherdef(int & t) {
cout << "lvalue\n";
}

void otherdef(const int & t) {
cout << "rvalue\n";
}

//重载函数模板,分别接收左值和右值
//接收右值参数
template <typename T>
void function(const T& t) {
otherdef(t);
}

//接收左值参数
template <typename T>
void function(T& t) {
otherdef(t);
}

int main()
{
function(5);//5 是右值
int x = 1;
function(x);//x 是左值
return 0;
}
1
2
rvalue
lvalue

显然,使用重载的模板函数实现完美转发也是有弊端的,此实现方式仅适用于模板函数仅有少量参数的情况,否则就需要编写大量的重载函数模板,造成代码的冗余。为了方便用户更快速地实现完美转发,C++ 11 标准中允许在函数模板中使用右值引用来实现完美转发。

同样上面的示例,在C++11标准中实现完美转发,只需要编写如下一个模板函数即可:

1
2
3
4
template <typename T>
void function(T&& t) {
otherdef(forward<T>(t));
}
  1. 用右值引用T&& t,既可以接收左值,也可以接收右值。
  2. 对于内部传递的参数otherdef(t),无论外部t传入的是左值还是右值,其形参都是左值(既能寻址也有名字)。为了解决这一问题,c++11新引入了一个模板函数forword<T>(param),可以把接收到的形参连同其左、右值属性,一起传递给被调用的函数。

16. 引用限定符

默认情况下,类中的public修饰的成员函数,即可以被左值对象调用,又可以被右值对象调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;
class demo {
public:
demo(int num):num(num){}
int get_num(){
return this->num;
}
private:
int num;
};

int main() {
demo a(10);
cout << a.get_num() << endl; // 左值对象调用
cout << move(a).get_num() << endl; // 右值对象调用
return 0;
}

某些场景中,我们可能需要限制调用成员函数的对象的类型(左值还是右值),为此 C++11 新添加了引用限定符。所谓引用限定符,就是在**成员函数的后面添加 “&” 或者 “&&”**,从而限制调用者的类型(左值还是右值)。

1
2
3
4
5
6
7
8
9
// 成员函数后面添加 & 限定符,该函数只能被左值对象调用。
int get_num()& {
return this->num;
}

// 成员函数后面添加 && 限定符,该函数只能被右值对象调用。
int get_num()&& {
return this->num;
}

注意:引用限定符不适用于静态成员函数和友元函数。

如果成员函数限定符与const一起使用,首先语法是const 必须位于引用限定符前面,然后需要注意的一点是,当 const && 修饰类的成员函数时,调用它的对象只能是右值对象;当 const & 修饰类的成员函数时,调用它的对象既可以是左值对象,也可以是右值对象。无论是 const && 还是 const & 限定的成员函数,内部都不允许对当前对象做修改操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
using namespace std;

class demo {
public:
demo(int num,int num2) :num(num),num2(num2) {}

//左值和右值对象都可以调用
int get_num() const &{
return this->num;
}

//仅供右值对象调用
int get_num2() const && {
return this->num2;
}
private:
int num;
int num2;
};

int main() {
demo a(10,20);
cout << a.get_num() << endl; // 正确
cout << move(a).get_num() << endl; // 正确

//cout << a.get_num2() << endl; // 错误
cout << move(a).get_num2() << endl; // 正确
return 0;
}

17. nullptr初始化空指针

C++11之前我们要初始化一个空指针,一般有下面两种形式:

1
2
int *p = 0;
int *p = NULL; //推荐使用

这里NULL本质其实就是0(#define NULL 0),是C++为我们事先定义好的一个宏。可以看到,我们可以将指针明确指向 0(0x0000 0000)这个内存空间。一方面,明确指针的指向可以避免其成为野指针;另一方面,大多数操作系统都不允许用户对地址为 0 的内存空间执行写操作,若用户在程序中尝试修改其内容,则程序运行会直接报错。

C++ 中将 NULL 定义为字面常量 0,虽然能满足大部分场景的需要,但个别情况下,它会导致程序的运行和我们的预期不符。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
using namespace std;

void isnull(void *c){
cout << "void*c" << endl;
}

void isnull(int n){
cout << "int n" << endl;
}

int main() {
isnull(0);
isnull(NULL);
return 0;
}
1
2
int n
int n

如果想匹配指针版必须得显示强转。

1
2
isnull( (void*)NULL );
isnull( (void*)0 );

由于NULL已经大量使用了,为了兼容,C++11保留了它。又为了解决上面的问题,C++11新增了一个关键字nullptr来表示空指针。

1
2
isnull(NULL);
isnull(nullptr); // nullprt可以隐式转换为任意类型的指针,它本质就是指针类型,不像NULL本质是整形了。
1
2
int n
void*c

总之在 C++11 标准下,相比 NULL 和 0,使用 nullptr 初始化空指针可以令我们编写的程序更加健壮。

18. 智能指针

  • shared_ptr
  • unique_ptr
  • weak_ptr

详情参考我另一篇文章:【c++11智能指针】