前言:
开学过去一个半月了,说来十分惭愧,由于和女友最后还是分开了,导致这段时间一直在沉沦,每天晚上回去打打lol或者cs,就睡觉,基本上把我自己定下的自学目标给抛弃了。好在这段时间里还是凭借以前的基础投了不少岗位,也笔试了不少公司,基本都通过了笔试。第一次面试是网易,结果在最后一轮的技术面上挂了下来。其实回想起来,当时问的问题我其实之前都有仔细的专研过,只不过时间太久忘了罢了。这也要怪我自己准备不够充分。之前腾讯的笔试,我其实感觉自己是做砸了的,不过没想到还是得到了面试机会,就在两天之后的下午。这是一个机会,我要好好把握住它,让自己重新振作起来,证明我自己还是挺不赖的。
我的github:
我的github本来到这个时间应该是刷完了的,不过由于沉沦期间,没干什么正经事,现在还停留在200题,不过相信之后我会很快搞定它的。
https://github.com/YinWenAtBIT
一、C++11 常用特性:
1. auto类型推导:
2. nullptr:
void foo(int* p) {}
void bar(std::shared_ptr<int> p) {}
int* p1 = NULL;
int* p2 = nullptr;
if(p1 == p2)
{
}
foo(nullptr);
bar(nullptr);
bool f = nullptr;
int i = nullptr;
// error: A native nullptr can only be converted to bool or, using reinterpret_cast, to an integral type
3. Range-based for loops (基于范围的for循环):
std::map<std::string, std::vector<int>> map;
std::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
map["one"] = v;
for(const auto& kvp : map)
{
std::cout << kvp.first << std::endl;
for(auto v : kvp.second)
{
std::cout << v << std::endl;
}
}
int arr[] = {1,2,3,4,5};
for(int& e : arr)
{
e = e*e;
}
4. Override和final:
我总觉得 C++中虚函数的设计很差劲,因为时至今日仍然没有一个强制的机制来标识虚函数会在派生类里被改写。vitual关键字是可选的,这使得阅读代码变得很费劲。因为可能需要追溯到继承体系的源头才能确定某个方法是否是虚函数。为了增加可读性,我总是在派生类里也写上virtual关键字,并且也鼓励大家都这么做。即使这样,仍然会产生一些微妙的错误。看下面这个例子:
1
2
3
4
5
6
7
8
9
10
11
|
class
B
{
public
:
virtual
void
f(
short
) {std::cout <<
"B::f"
<< std::endl;}
};
class
D :
public
B
{
public
:
virtual
void
f(
int
) {std::cout <<
"D::f"
<< std::endl;}
};
|
D::f 按理应当重写 B::f。然而二者的声明是不同的,一个参数是short,另一个是int。因此D::f(原文为B::f,可能是作者笔误——译者注)只是拥有同样名字的另一个函数(重载)而不是重写。当你通过B类型的指针调用f()可能会期望打印出D::f,但实际上则会打出 B::f 。
另一个很微妙的错误情况:参数相同,但是基类的函数是const的,派生类的函数却不是。
1
2
3
4
5
6
7
8
9
10
11
|
class
B
{
public
:
virtual
void
f(
int
)
const
{std::cout <<
"B::f "
<< std::endl;}
};
class
D :
public
B
{
public
:
virtual
void
f(
int
) {std::cout <<
"D::f"
<< std::endl;}
};
|
同样,这两个函数是重载而不是重写,所以你通过B类型指针调用f()将打印B::f,而不是D::f。
幸运的是,现在有一种方式能描述你的意图。新标准加入了两个新的标识符(不是关键字)::
- override,表示函数应当重写基类中的虚函数。
- final,表示派生类不应当重写这个虚函数。
第一个的例子如下:
1
2
3
4
5
6
7
8
9
10
11
|
class
B
{
public
:
virtual
void
f(
short
) {std::cout <<
"B::f"
<< std::endl;}
};
class
D :
public
B
{
public
:
virtual
void
f(
int
) override {std::cout <<
"D::f"
<< std::endl;}
};
|
现在这将触发一个编译错误(后面那个例子,如果也写上override标识,会得到相同的错误提示):
1
|
'D::f' : method with override specifier 'override' did not override any base class methods
|
另一方面,如果你希望函数不要再被派生类进一步重写,你可以把它标识为final。可以在基类或任何派生类中使用final。在派生类中,可以同时使用override和final标识。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
class
B
{
public
:
virtual
void
f(
int
) {std::cout <<
"B::f"
<< std::endl;}
};
class
D :
public
B
{
public
:
virtual
void
f(
int
) override final {std::cout <<
"D::f"
<< std::endl;}
};
class
F :
public
D
{
public
:
virtual
void
f(
int
) override {std::cout <<
"F::f"
<< std::endl;}
};
|
被标记成final的函数将不能再被F::f重写。
5. Strongly-typed enums 强类型枚举
传统的C++枚举类型存在一些缺陷:它们会将枚举常量暴露在外层作用域中(这可能导致名字冲突,如果同一个作用域中存在两个不同的枚举类型,但是具有相同的枚举常量就会冲突),而且它们会被隐式转换为整形,无法拥有特定的用户定义类型。
在C++11中通过引入了一个称为强类型枚举的新类型,修正了这种情况。强类型枚举由关键字enum class标识。它不会将枚举常量暴露到外层作用域中,也不会隐式转换为整形,并且拥有用户指定的特定类型(传统枚举也增加了这个性质)。
1
2
|
enum
class
Options {None, One, All};
Options o = Options::All;
|
6. Smart Pointers 智能指针
已经有成千上万的文章讨论这个问题了,所以我只想说:现在能使用的,带引用计数,并且能自动释放内存的智能指针包括以下几种:
- unique_ptr: 如果内存资源的所有权不需要共享,就应当使用这个(它没有拷贝构造函数),但是它可以转让给另一个unique_ptr(存在move构造函数)。
- shared_ptr: 如果内存资源需要共享,那么使用这个(所以叫这个名字)。
- weak_ptr: 持有被shared_ptr所管理对象的引用,但是不会改变引用计数值。它被用来打破依赖循环(想象在一个tree结构中,父节点通过一个共享所有权的引用(chared_ptr)引用子节点,同时子节点又必须持有父节点的引用。如果这第二个引用也共享所有权,就会导致一个循环,最终两个节点内存都无法释放)。
另一方面,auto_ptr已经被废弃,不会再使用了。
7. Lambdas
在python中,匿名函数用的非常多,在这里c++11也添加了它。匿名函数(也叫lambda)已经加入到C++中,并很快异军突起。这个从函数式编程中借来的强大特性,使很多其他特性以及类库得以实现。你可以在任何使用函数对象或者函子(functor)或std::function的地方使用lambda。你可以从这里(http://msdn.microsoft.com/en-us/library/dd293603.aspx)找到语法说明。
Lambda表达式的语法通过下图来介绍:
这里假设我们定义了一个如上图的lambda表达式。现在来介绍途中标有编号的各个部分是什么意思。
- Lambda表达式的引入标志,在‘[]’里面可以填入‘=’或‘&’表示该lambda表达式“捕获”(lambda表达式在一定的scope可以访问的数据)的数据时以什么方式捕获的,‘&’表示一引用的方式;‘=’表明以值传递的方式捕获,除非专门指出。
- Lambda表达式的参数列表
- Mutable 标识
- 异常标识
- 返回值
- “函数”体,也就是lambda表达式需要进行的实际操作
默认情况下,即捕获字段为 [] 时,lambda表达式是不能访问任何外部变量的,即表达式的函数体内无法访问当前作用域下的变量。
如果要设定表达式能够访问外部变量,可以在 [] 内写入 & 或者 = 加上变量名,其中 & 表示按引用访问,= 表示按值访问,变量之间用逗号分隔,比如 [=factor, &total] 表示按值访问变量 factor,而按引用访问 total。
不加变量名时表示设置默认捕获字段,外部变量将按照默认字段获取,后面在书写变量名时不加符号表示按默认字段设置
lambda表达式的参数列表基本和函数的一致,不过有如下限制:
参数列表不能有默认参数
不能是可变参数列表
所有的参数必须有个变量名
如果你不提供 mutable-specification, exception-specification, 以及 lambda-return-type-clause,参数列表是也可以省略的。
如果在参数列表后加上了 mutable,则表示表达式可以修改按值捕获的外部变量的拷贝。
异常设置
和函数一样,可以用 throw 来限定表达式能够抛出哪些异常。
返回类型
如果设置返回类型,你需要在类型名前面加上 ->。如果你只有一个返回语句的话,返回类型可以省略,编译器将会为你做出判断。
8. 非成员begin()和end()
也许你注意到了,我在前面的例子中已经用到了非成员begin()和end()函数。他们是新加入标准库的,除了能提高了代码一致性,还有助于更多地使用泛型编程。它们和所有的STL容器兼容。更重要的是,他们是可重载的。所以它们可以被扩展到支持任何类型。对C类型数组的重载已经包含在标准库中了。
我们还用上一个例子中的代码来说明,在这个例子中我打印了一个数组然后查找它的第一个偶数元素。如果std::vector被替换成C类型数组。代码可能看起来是这样的:
我们还用上一个例子中的代码来说明,在这个例子中我打印了一个数组然后查找它的第一个偶数元素。如果std::vector被替换成C类型数组。代码可能看起来是这样的:
1
2
3
4
5
6
7
8
9
|
int
arr[] = {1,2,3};
std::for_each(&arr[0], &arr[0]+
sizeof
(arr)/
sizeof
(arr[0]), [](
int
n) {std::cout << n << std::endl;});
auto is_odd = [](
int
n) {
return
n%2==1;};
auto begin = &arr[0];
auto end = &arr[0]+
sizeof
(arr)/
sizeof
(arr[0]);
auto pos = std::find_if(begin, end, is_odd);
if
(pos != end)
std::cout << *pos << std::endl;
|
如果使用非成员的begin()和end()来实现,就会是以下这样的:
1
2
3
4
5
6
7
|
int
arr[] = {1,2,3};
std::for_each(std::begin(arr), std::end(arr), [](
int
n) {std::cout << n << std::endl;});
auto is_odd = [](
int
n) {
return
n%2==1;};
auto pos = std::find_if(std::begin(arr), std::end(arr), is_odd);
if
(pos != std::end(arr))
std::cout << *pos << std::endl;
|
这基本上和使用std::vecto的代码是完全一样的。这就意味着我们可以写一个泛型函数处理所有支持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
|
template
<
typename
Iterator>
void
bar(Iterator begin, Iterator end)
{
std::for_each(begin, end, [](
int
n) {std::cout << n << std::endl;});
auto is_odd = [](
int
n) {
return
n%2==1;};
auto pos = std::find_if(begin, end, is_odd);
if
(pos != end)
std::cout << *pos << std::endl;
}
template
<
typename
C>
void
foo(C c)
{
bar(std::begin(c), std::end(c));
}
template
<
typename
T,
size_t
N>
void
foo(T(&arr)[N])
{
bar(std::begin(arr), std::end(arr));
}
int
arr[] = {1,2,3};
foo(arr);
std::vector<
int
> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
foo(v);
|
9. static_assert和 type traits
static_assert提供一个编译时的断言检查。如果断言为真,什么也不会发生。如果断言为假,编译器会打印一个特殊的错误信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
template
<
typename
T,
size_t
Size>
class
Vector
{
static_assert(Size < 3,
"Size is too small"
);
T _points[Size];
};
int
main()
{
Vector<
int
, 16> a1;
Vector<
double
, 2> a2;
return
0;
}
|
1
2
3
4
5
6
7
|
error C2338: Size is too small
see reference to class template instantiation 'Vector<T,Size>' being compiled
with
[
T=double,
Size=2
]
|
10. Move semantics (Move语义)
这是C++11中所涵盖的另一个重要话题。就这个话题可以写出一系列文章,仅用一个段落来说明显然是不够的。因此在这里我不会过多的深入细节,如果你还不是很熟悉这个话题,我鼓励你去阅读更多地资料。
C++11加入了右值引用(rvalue reference)的概念(用&&标识),用来区分对左值和右值的引用。左值就是一个有名字的对象,而右值则是一个无名对象(临时对象)。move语义允许修改右值(以前右值被看作是不可修改的,等同于const T&类型)。
C++的class或者struct以前都有一些隐含的成员函数:默认构造函数(仅当没有显示定义任何其他构造函数时才存在),拷贝构造函数,析构函数还有拷贝赋值操作符。拷贝构造函数和拷贝赋值操作符提供bit-wise的拷贝(浅拷贝),也就是逐个bit拷贝对象。也就是说,如果你有一个类包含指向其他对象的指针,拷贝时只会拷贝指针的值而不会管指向的对象。在某些情况下这种做法是没问题的,但在很多情况下,实际上你需要的是深拷贝,也就是说你希望拷贝指针所指向的对象。而不是拷贝指针的值。这种情况下,你需要显示地提供拷贝构造函数与拷贝赋值操作符来进行深拷贝。
如果你用来初始化或拷贝的源对象是个右值(临时对象)会怎么样呢?你仍然需要拷贝它的值,但随后很快右值就会被释放。这意味着产生了额外的操作开销,包括原本并不需要的空间分配以及内存拷贝。
现在说说move constructor和move assignment operator。这两个函数接收T&&类型的参数,也就是一个右值。在这种情况下,它们可以修改右值对象,例如“偷走”它们内部指针所指向的对象。举个例子,一个容器的实现(例如vector或者queue)可能包含一个指向元素数组的指针。当用一个临时对象初始化一个对象时,我们不需要分配另一个数组,从临时对象中把值复制过来,然后在临时对象析构时释放它的内存。我们只需要将指向数组内存的指针值复制过来,由此节约了一次内存分配,一次元数组的复制以及后来的内存释放。
以下代码实现了一个简易的buffer。这个buffer有一个成员记录buffer名称(为了便于以下的说明),一个指针(封装在unique_ptr中)指向元素为T类型的数组,还有一个记录数组长度的变量。
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
|
template
<
typename
T>
class
Buffer
{
std::string _name;
size_t
_size;
std::unique_ptr<T[]> _buffer;
public
:
// default constructor
Buffer():
_size(16),
_buffer(
new
T[16])
{}
// constructor
Buffer(
const
std::string& name,
size_t
size):
_name(name),
_size(size),
_buffer(
new
T[size])
{}
// copy constructor
Buffer(
const
Buffer& copy):
_name(copy._name),
_size(copy._size),
_buffer(
new
T[copy._size])
{
T* source = copy._buffer.get();
T* dest = _buffer.get();
std::copy(source, source + copy._size, dest);
}
// copy assignment operator
Buffer& operator=(
const
Buffer& copy)
{
if
(
this
!= ©)
{
_name = copy._name;
if
(_size != copy._size)
{
_buffer = nullptr;
_size = copy._size;
_buffer = _size > 0 >
new
T[_size] : nullptr;
}
T* source = copy._buffer.get();
T* dest = _buffer.get();
std::copy(source, source + copy._size, dest);
}
return
*
this
;
}
// move constructor
Buffer(Buffer&& temp):
_name(std::move(temp._name)),
_size(temp._size),
_buffer(std::move(temp._buffer))
{
temp._buffer = nullptr;
temp._size = 0;
}
// move assignment operator
Buffer& operator=(Buffer&& temp)
{
assert
(
this
!= &temp);
// assert if this is not a temporary
_buffer = nullptr;
_size = temp._size;
_buffer = std::move(temp._buffer);
_name = std::move(temp._name);
temp._buffer = nullptr;
temp._size = 0;
return
*
this
;
}
};
template
<
typename
T>
Buffer<T> getBuffer(
const
std::string& name)
{
Buffer<T> b(name, 128);
return
b;
}
int
main()
{
Buffer<
int
> b1;
Buffer<
int
> b2(
"buf2"
, 64);
Buffer<
int
> b3 = b2;
Buffer<
int
> b4 = getBuffer<
int
>(
"buf4"
);
b1 = getBuffer<
int
>(
"buf5"
);
return
0;
}
|
默认的copy constructor以及copy assignment operator大家应该很熟悉了。C++11中新增的是move constructor以及move assignment operator,这两个函数根据上文所描述的move语义实现。如果你运行这段代码,你就会发现b4构造时,move constructor会被调用。同样,对b1赋值时,move assignment operator会被调用。原因就在于getBuffer()的返回值是一个临时对象——也就是右值。
你也许注意到了,move constuctor中当我们初始化变量name和指向buffer的指针时,我们使用了std::move。name实际上是一个string,std::string实现了move语义。std::unique_ptr也一样。但是如果我们写_name(temp._name),那么copy constructor将会被调用。不过对于_buffer来说不能这么写,因为std::unique_ptr没有copy constructor。但为什么std::string的move constructor此时没有被调到呢?这是因为虽然我们使用一个右值调用了Buffer的move constructor,但在这个构造函数内,它实际上是个左值。为什么?因为它是有名字的——“temp”。一个有名字的对象就是左值。为了再把它变为右值(以便调用move constructor)必须使用std::move。这个函数仅仅是把一个左值引用变为一个右值引用。
更新:虽然这个例子是为了说明如何实现move constructor以及move assignment operator,但具体的实现方式并不是唯一的。在本文的回复中Member 7805758同学提供了另一种可能的实现。为了方便查看,我把它也列在下面:
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
|
template
<
typename
T>
class
Buffer
{
std::string _name;
size_t
_size;
std::unique_ptr<T[]> _buffer;
public
:
// constructor
Buffer(
const
std::string& name =
""
,
size_t
size = 16):
_name(name),
_size(size),
_buffer(size?
new
T[size] : nullptr)
{}
// copy constructor
Buffer(
const
Buffer& copy):
_name(copy._name),
_size(copy._size),
_buffer(copy._size?
new
T[copy._size] : nullptr)
{
T* source = copy._buffer.get();
T* dest = _buffer.get();
std::copy(source, source + copy._size, dest);
}
// copy assignment operator
Buffer& operator=(Buffer copy)
{
swap(*
this
, copy);
return
*
this
;
}
// move constructor
Buffer(Buffer&& temp):Buffer()
{
swap(*
this
, temp);
}
friend
void
swap(Buffer& first, Buffer& second) noexcept
{
using
std::swap;
swap(first._name , second._name);
swap(first._size , second._size);
swap(first._buffer, second._buffer);
}
};
|
总结
这部分的C++11新特性总结,主要来自别人的博客,我在阅读博客的同时,尝试自己再写出来一遍,这样的过程让自己可以更彻底了解新的特性,并且新的特性的由来。这样做的结果也就是学习的时间变长了,一篇博客,引申出许多需要读的博文,最后花了大约5个小时完成了学习。