了解c++11

一、简介

  起草本文的目的主要有两点:第一点,在c++11学习的过程中做一些摘要总结,尽量取其精华,为自己梳理出来一个比较清晰的记忆脉络;第二点,不得不说的就是随着年纪的增长,记忆力也开始变得并不那么可靠,做些记录,定期回头看看总是好的。
  文章会对c++11(其实是c++1x更准确些)的背景进行简要的概述,并尽量详细且准确的阐述c++11的新的特性。共同学习,不断进步。

1.1 c++11概述

  我们生活中常常谈论的c++更多的是指传统c++,又或是可以称为c++98的版本。c++11也并不是什么神秘的新语言,而是基于传统c++发布13年后的第一次重大修订。它主要基于现在软件行业发展的新需求与新诉求进行的一种调整——增加新特性并弃用部分特性。

1.2 文章小贴士

1.2.1 笔者开发运行环境

1
2
3
4
5
6
7
8
9
10
11
12
> lennybai@lennybai~$ lsb_release -a
> No LSB modules are available.
> Distributor ID: Ubuntu
> Description: Ubuntu 16.04.1 LTS
> Release: 16.04
> Codename: xenial
> lennybai@lennybai:~$ g++ --version
> g++ (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609
> Copyright (C) 2015 Free Software Foundation, Inc.
> This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

1.2.2 小贴士

g++ main.cpp -std=c++11  

若使用c++11,请记得添加编译选项

1.2.3 原创提醒

  本文主要目的在于个人学习与记录,如果能够有幸帮助到同样在修行的同僚那自然是件幸事,引用还望注明出处。笔者在写作过程中也会大量的阅读各种文章以增强理解,当然,会尽最大可能去注明文章引用,如果存在问题,希望指正。


二、正篇

  对于学习新东西,我的习惯一般都是先google或者baidu看大量的博客来给自己洗脑,让自己有一定的知识轮廓。接下来就会开始啃官方文档,或者阅读刚刚浏览过的博客中推荐的各种书籍。当然,如果有个不错的视频教程让自己快进着点一点最好不过了,很多效果一目了然。

  在stackoverflow上,有人给出了不错的书籍大纲,这些对于积累知识的初学者更有益。对于那些对于传统c++已经很熟悉,只是希望快速的了解c++11的新特性,并择机使用的选手来说,实验楼的《C++ 11/14 高速上手教程》免费在线课程可能会更有帮助。

  本文主要基于实验楼的课程大纲进行整理,并进行部分知识点的拓展完善。

2.1 弃用(deprecated)特性

  c++11提及到部分弃用特性。这里弃用并不等于废除并从标准中移除,而是警醒程序员在使用c++的时候能够意识到某些特性应避免使用。其实可能为了保证兼容性,这些特性可能永远不会被剔除出去(通常情况下是在编译的时,会显示一个弃用的warning),依旧可以被使用。

1
2
3
4
lennybai@lennybai:~/Desktop/cpp11$ g++ main.cpp -std=c++11
main.cpp: In function ‘int main()’:
main.cpp:7:15: warning: ISO C++ forbids converting a string constant to ‘char*’ [-Wwrite-strings]
char* a = "hello world";

  但是我们应该尽量遵循语言的发展与变迁,适当的改变自己对语言的使用方式。一个不恰当的例子,就好比黑白彩电置于当今社会,依然可以被使用,但是有更好的选择时为什么不去尝试呢?

2.1.1 被移除或者弃用的特性

  • std::auto_ptr 被 std::unique_ptr 取代。
  • exception specifications
  • bool 类型的 ++ 操作被弃用。
  • export: 用法已被去掉,但关键字还是被保留了,给将来可能的特性使用
  • 函数对象的基类(std::unary_function, std::binary_function)、函数指针适配器、类型成员指针适配器以及绑定器 (binder)。
  • 顺序点 (sequence point): 这个术语正被更为易懂的描述所取代。一个运算可以发生 (is sequenced before) 在另一个运算之前; 又或者两个运算彼此之间没有顺序关系 (are unsequenced)。

  就像前面提到的,被弃用的特性应该引起一定的重视。这里主要参考维基百科上面提到的内容列出了一些已经被弃用的特性。在以后的学习过程中也会不断完善这一部分。

2.2 新特性

  对于c++11的新特性应该是阅读本文的客官们更关心的。c++11新特性包括但不完全是:lambda表达式,类型推断关键字auto、decltype和对模板的大量改进。在这一章节我们进行详细阐述。

2.2.1 自动类型推断(auto/decltype)

  对于传统c++而言,变量的类型都必须明确定义。虽然已经习惯传统c++编程的朋友们在声明或者定义基本类型(如int、char等)的变量时,并没有太多不妥的感觉。然而,可能也会在使用复杂模板时,觉得代码有点又丑又长。比如常见的迭代器声明:

1
for ( map < int,UserDefClass<userDefTemptype> >::iterator itr= userMap.begin(); itr !=userMap.end(); ++itr)

  这里我列出了一个比较夸张的例子,目的是希望指出自动类型推断存在的意义。它其实已经很普遍的存在于现代编程语言之中(如python, javascript等脚本语言)。而在c++11中,通过对传统c++中auto关键字的修改得到自动类型推断的支持。
  auto关键字在传统c++中用来指定存储期,一般用于指明具有自动生命周期的变量。最常见的就是函数内的局部变量。编译器在做编译时,对一般不是static修饰的变量,则默认赋予auto类型,所以auto关键字几乎很少被使用。在传统c++中,auto关键字的使用方法如下:

1
auto int a;

  而在c++11中,auto的功能变为了类型自动推断,编译器会根据初始代码推断所声明变量的真实类型。将类型判断的工作由程序员交给了编译器,这样,在一定程度提高了代码开发的效率。而且,大部分情况下会直观的发现,代码变的更简短(当然int类型还是要比auto少一个字符)。这里是使用auto对上面繁重代码的优化:

1
for ( auto itr= userMap.begin(); itr !=userMap.end(); ++itr)

  有没有很酷!接下来我们来看一下auto的具体使用,来更详细的了解一下它的功能。同时,也要学习一下它的使用规则。auto在c++11中,最容易理解的使用方式莫过于变量定义时的类型推断:

1
2
3
4
5
6
auto a = 10;
auto b = 'A';
auto c("hello");
cout << "a:" << typeid(a).name() << endl;  // “a:i”
cout << "b:" << typeid(b).name() << endl;  // "b:c"
cout << "c:" << typeid(c).name() << endl;  // "c:PKc"

  这里我们可以看到,对于变量可以不用明确的去定义其类型,而是用auto代替。其最终效果与传统c++的变量定义效果没有差别。这里可以通过typeid函数进行验证(#include< typeinfo >).
  在很多对于auto的讲解中,它常常被称作占位符。所谓占位符,我的理解是它不能像数据类型那样去声明一个变量。所以使用auto的变量必须进行初始化。理所当然的,也就不能使用c++类型转化将变量转换为auto类型。

1
2
3
4
5
auto a; //error: declaration of ‘auto a’ has no initializer
int value1 = 10;
auto value2 = (auto)value1; // error: invalid use of ‘auto’
auto value3 = static_cast<auto>(value1); // error: invalid use of ‘auto’

  c++11已经彻底废弃了原有auto的使用方式,如代码所示:

1
auto int a = 10; //error: two or more data types in declaration of ‘a’

  auto可以接受c++的变量名表列的方式定义多个变量,但所有变量应具有相同的数据类型。

1
2
auto a1=10, a2=20, a3=30;
auto a4=10, a5=20.0, a6 = 'A'; // error: conflicting declaration

  auto在初始化时,会自动去除引用、const、volatile的语意。这种说法可能很难理解。其实就是如果原始变量为引用、const、volatile类型,auto初始化时需要自行指明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int refa = 10;
int &refb = refa;
auto refc = refb;
auto &refd = refb;
refa = 100;
cout << "refa: " << typeid(refa).name() << " " << refa << endl; // 100
cout << "refb: " << typeid(refb).name() << " " << refb << endl; // 100
cout << "refc: " << typeid(refc).name() << " " << refc << endl; // 10
cout << "refd: " << typeid(refd).name() << " " << refd << endl; // 100
int arrA[3] = { 1, 2, 3 };
auto arrB = arrA;
auto &refArrB = arrA;
cout << "arrA: " << typeid(arrA).name() << endl; // A3_i
cout << "arrB: " << typeid(arrB).name() << endl; //Pi
cout << "refArrB: " << typeid(refArrB).name() << endl; // A3_i

  auto关键字不仅可以配合变量使用,它在对传统c++函数的代码优化上也可以说是大放异彩。特别是针对那些具有模板类型的函数,更是让我们在编写时变的简单高效。我们来看第一个例子。这里我们在模板函数中使用auto定义了一个站位变量。这样就使得该变量在随着传入值的类型发生变化时,自动进行类型转换。是不是很方便。

1
2
3
4
5
6
7
8
9
10
template <typename _Tx,typename _Ty>
void func(_Tx x, _Ty y)
{
auto v = x*y;
cout << v << endl;
}
//call func
func3(1,2); // 2
func3(1.1,2.2); // 2.42

  auto关键字可以在函数中使用,那我们自然会想到,它能不能作为函数的返回值占位符。c++11自然提供了这种机制。但是用法可能和我们预期有一定的差距。

1
2
3
4
auto func(int x, int y)->int
{
return x+y;
}

  在c++11中,这样的返回值看起来很奇葩。使用auto来标注函数返回值时,还需要在末尾指定返回类型。会不会觉得还不如不用auto类型。但我觉得制定标准的人肯定也会考虑到这个问题。自然就去搜索了一些相关内容:

  1. 在函数返回值特别长的时候放在末尾,会显得好看。
  2. c++14会把返回值去掉,让编译器直接支持自动推倒(c++14确实是做到了)
  3. 出现在函数后面的表达式在代码编译时也是出现在函数之后的,这就使得很多函数中的临时变量可以出现在这个表达式中。这个我们在后面的decltype使用中可以看到效果。(显然,这个才是我认为最重要的)
1
2
3
4
5
//c++14 支持
auto func(int x, int y)
{
return x+y;
}

  这种auto作为函数返回值占位符的用法,会使编写复杂的模板函数时变得相当简单和灵活。

1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename _Tx, typename _Ty>
auto func(_Tx x, _Ty y) -> decltype(x*y)
{
return x*y;
}
//call func
auto funcA = func(1,2);
cout << funcA << endl; // 2
funcA = func(1.1,2.2);
cout << funcA << endl; // 2
auto funcB = func(1.1,2.2);
cout << funcB << endl; // 2.42

这里用到了decltype(declared type),它可以用来通过传入的参数来声明类型。这样func这个模板函数的会很方便的随着模板传入类型来自动改变返回值类型。bravo!

decltype不存在太多的歧义,这里直接列出代码。

1
2
3
4
5
6
7
8
9
int declint = 10;
decltype(declint) declintt;
vector<int> vec;
typedef decltype(vec.begin()) vectype;
decltype(declint) temp1 = declint;
decltype((declint)) temp2 = declint; //decltype(())定义的是引用类型
decltype((declint)) temp3; // error: ‘temp3’ declared as reference but not initialized

  接下来我们说一下刚才我们提到的函数返回类型后置的问题。我们已经知道通过decltype可以根据传入参数获取最终的数据类型。那么我们将下面代码进行修改:

1
2
3
4
5
6
// origin, ok~
template <typename _Tx, typename _Ty>
auto func(_Tx x, _Ty y) -> decltype(x*y)
{
return x*y;
}

  一般想法是认为,返回值不就是decltype(x*y)类型么? 那我们直接放前面不是更方便。好的,我们改写一下.

1
2
3
4
5
6
// update
template <typename _Tx, typename _Ty>
decltype(x*y) func(_Tx x, _Ty y)
{
return x*y;
}

然而:

1
2
3
4
5
6
7
8
lennybai@lennybai:~/project/cpp11$ g++ main.cpp -std=c++11
main.cpp:16:10: error: ‘x’ was not declared in this scope
decltype(x*y) func4(_Tx x, _Ty y)
^
main.cpp:16:10: error: ‘x’ was not declared in this scope
main.cpp:16:12: error: ‘y’ was not declared in this scope
decltype(x*y) func4(_Tx x, _Ty y)
^

  是的,结果就是这么神奇。这也说明 ’function->expr‘ 中的expr是出现在函数之后的。如果不使用auto占位符,而直接使用decltype(x*y)声明函数类型。由于x,y在函数运行前尚未明确类型,所以这里就会出现编译错误。

  在这一小节最后需要指出的是,auto是不能作为参数占位符出现的。

1
2
3
4
5
// 客官不可以哦
void func2(auto x)
{
//do something
}

2.2.2 指针空值nullptr

  在传统c++中,通常使用NULL来标识空指针。NULL通常是一个与0相关的宏定义。这里是/usr/lib/gcc/x86_64-linux-gnu/5/include/stddef.h中对NULL的定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
#if defined (_STDDEF_H) || defined (__need_NULL)
#undef NULL /* in case <stdio.h> has defined it. */
#ifdef __GNUG__
#define NULL __null
#else /* G++ */
#ifndef __cplusplus
#define NULL ((void *)0)
#else /* C++ */
#define NULL 0
#endif /* C++ */
#endif /* G++ */
#endif /* NULL not defined and <stddef.h> or need NULL. */
#undef __need_NULL

  我们发现在传统C++中,NULL被定义为0。由于没有类型限制,它可以被强制转换为各种其他数据类型。最常见的例子就是当出现函数重载时会发生混乱。

1
2
3
4
void func(int n) { cout << "call func(int n) : " << endl; };
void func(int *p) { cout << "call func(int *p) : " << endl; };
func(NULL); // error: call of overloaded ‘func(NULL)’ is ambiguous

  为了区分不同类型的空指针,C++11引入了nullptr_t类型的指针空值nullptr。它可以自动隐性的转换为所需的成员指针或者函数指针的类型。从而使程序更加清晰明了。值得注意的是,nullptr只能用来标识指针类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;
void func(int n) { cout << "call func(int n) : " << endl; };
void func(int *p) { cout << "call func(int *p) : " << endl; };
int main()
{
int nIntNull = NULL; // ok
int *pIntNull = NULL; // ok
int *pIntNullptr = nullptr; // ok
int nIntNullptr = nullptr; // error: cannot convert ‘std::nullptr_t’ to ‘int’ in initialization
func(NULL); // error: call of overloaded ‘func(NULL)’ is ambiguous
func(nullptr); // call func(int *p)
return 0;
}

2.2.3 常量区分符constexpr

  常量表达式在c++中还是很常见的,特别是在数组或者容器等初始化时传入的大小通常为常量表达式。在C++中,常量表达式通常被解释为在编译时可以明确其值的表达式。最常见的常量表达式就是被const标识的变量:

1
2
3
4
int n = 1;
std::array<int, n> a1; // error, n is not a constant expression
const int cn = 2;
std::array<int, cn> a2; // OK, cn is a constant expression

  其实,在我们进行程序设计的时候,常常会发现很多情况是需要根据运行情况去获取一个值,然后再进行数据或者容器大小的分配。而这些值我们在给定条件值之后,很容易就会被推导出来。而对于计算机来说,却需要在每次运行时不断重复相同的推倒工作。那么我们可能会觉得,如果可以像常量表达式那样在编译时就确定其值,是不是在运行时就可以得到更高的运行效率呢。c++为我们提供了常量区分符constexpr来实现这一效果。
  constexpr用来标示一个变量或者函数为常量表达式,也是告诉编译器需要去验证它们是不是一个常量表达式。

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
int x;
struct A {
constexpr A(bool b) : m(b?42:x) { }
int m;
};
constexpr int v = A(true).m; // OK
constexpr int w = A(false).m; // error: non-const x
constexpr int mf = 20; // 20是常量表达式
constexpr int limit = mf + 1; // mf + 1是常量表达式
constexpr int sz = size(); // 只有当size是一个constexpr函数时才是一条正确的声明语句
constexpr int A()
{
return 10;
}
constexpr int fibonacci1(const int n) {
return n == 1 || n == 2 ? 1 : fibonacci1(n-1)+fibonacci1(n-2);
}
// c++11 error error: body of constexpr function ‘constexpr int fibonacci2(int)’ not a return-statement
// c++14 OK
constexpr int fibonacci2(const int n) {
if(n == 1) return 1;
  if(n == 2) return 1;
  return fibonacci2(n-1)+fibonacci2(n-2);
}
int i1 = 10;
const int i2 = i1;
const int i3 = 10;
constexpr int x1 = i1; // error: the value of ‘i1’ is not usable in a constant expression
constexpr int x2 = i2; // error: the value of ‘i2’ is not usable in a constant expression
constexpr int x3 = i3; // ok
constexpr int x4 = 10; // ok
constexpr int x5 = A(); // ok
constexpr int x6 = fibonacci1(10); // ok
constexpr int x7 = cin.get(); // !error
constexpr int x8 = fibonacci2(10);
int a[i2]; // ok
int b[x4]; // ok

  从上面的代码中我们可以大概了解到constexpr的作用与用法。这里以上代码进行一些说明:

  1. c++11中对constexpr修饰的函数只能直接返回常量表达式。c++14中进行了增强,它可以支持局部变量和条件语句等。
  2. const int i2为运行时赋值,所以x2 = i2 报错; const int i3为常量表达式,所以x3=i3没问题

    对于constexpr可以在这里了解更多

2.2.4 range-based for loop

  在cppreference中,这个特性被描述为:

Executes a for loop over a range.Used as a more readable equivalent to the traditional for loop operating over a range of values, such as all elements in a container. 

 
  这个描述有些官方和笼统。其实,range-based for loop在c++11中加入,它使得c++开始支持类似于’foreach’的范式格式。这种新格式使得我们获取或者操作数组时变得更容易,前提是我们不去关心索引,迭代器和数量的前提下。range-based for loop可以支持迭代所有的c类型数组,初始化列表和重载了begin()和end()函数的容器。

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
#include <iostream>
#include <map>
#include <vector>
#include <string>
using namespace std;
int main(int argc, char *argv[])
{
map<string, vector<int>> m;
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
m["my_vector"] = v;
for(auto& tempM : m)
{
cout << tempM.first << endl;
for(auto& tempV : tempM.second)
{
cout << tempV << endl;
}
}
int arr[] = {1,2,3,4,5};
for(int& e : arr)
{
cout << e << endl;
e = e*e;
}
for(int& e : arr)
{
cout << e << endl;
}
return 0;
}

2.2.5 快速初始化列表

  在c++11中,它提供了更便捷,或者说是更人性化的初始化方式。

1
2
3
4
5
6
7
8
9
//c++ 98 error: in C++98 ‘m’ must be initialized by constructor, not by ‘{...}’
//c++ 11
vector<int> v = {1,2,3,4,5};
cout << v[3] << endl;
//c++98 error: could not convert ‘{{1, 2}}’ from ‘<brace-enclosed initializer list>’ to ‘std::map<int, int>’
//c++11 ok
map<int, int> m = {{1,2}};
cout << m[1] << endl;

对于对象的内部属性初始化,我们可以通过调用参数为initializer_list的方法快速初始化。

1
2
3
4
5
6
7
8
struct myclass {
myclass (int,int);
myclass (initializer_list<int>);
/* definitions ... */
};
myclass foo {10,20}; // calls initializer_list ctor
myclass bar (10,20); // calls first constructor

2.2.6 类型别名

  传统c++同常使用typedef为类型定义一个新的名称。给函数声明去做个别名是比较常见的使用方式。

typedef void (*SigHandler)(int);

  这个是unix中信号处理的圆形函数。它讲一个参数为int并返回值为void的函数原型重命名为SigHandler.这个原型相对还比较容易理解。但在我们平时的编程过程中,遇到的情况往往比这要复杂得多。c++11为我们提供了一个更符合人类思维和更直观的表述方式(这话是我自己说的,不要追究责任233333)。

using SigHandler = void(*)(int);

  这里就提高了using关键字。在传统c++中,对于using 我们可能唯一的用法就是using namespace。而在c++11中,它获得了更多的功能。下面我们继续说一些其他的别名使用情况。提到类型别名,我们有的时候会想到,能不能讲某些又臭又长的模板类型给做个别名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<typename X, typename Y, typename Z>
class MyTest
{
public:
MyTest();
virtual ~MyTest();
private:
X x;
Y y;
Z z;
};
template<typename U>
typedef MyTest<int,U,int> mm; //error: template declaration of ‘typedef’

在传统c++中,它并不支持对上述初始化部分类型的模板类进行再次重命名。而c++11中可以通过using关键字得到实现。

1
2
template<typename U>
using mm = MyTest<int,U,int>;


3 参考文献

------ 本文结束 ------
扫二维码
扫一扫,用手机访问本站

扫一扫,用手机访问本站

发送邮件