C ++ 11新功能
- 最新的线程库。
- Lambda表达式
- Automatic Type Deduction and decltype
- 统一初始化语法
- Delete函数和Default函数
- nullptr
- 委托构造器
- 右值引用
在本文中,我将解释语言的最大变化以及它们为何如此重要。如您所见,线程库并不是唯一的更改。新标准以数十年的专业知识为基础,并使C ++更加重要。
首先,让我们看一下一些著名的C ++ 11核心语言功能。
Lambda表达式
lambda函数是可以在源代码中inline编写的函数,通常传递给另一个函数,类似于函数指针的概念。使用lambda,创建快速函数变得更加容易,这意味着您不仅可以在以前需要编写单独的命名函数的地方使用lambda;而且可以编写更多代码,而这些代码依赖于创建快速简便的函数。
Lambda表达式使您可以在调用位置本地定义函数,从而消除了函数对象引起的许多繁琐和安全风险。 Lambda表达式的形式为:
[capture clause](parameters)->return-type {body}
- 所有的lambda都以一对平衡的括号开头。括号内是可选的capture clause子句。
- Lambda的参数(parameters)在括号之间。如果lambda不带参数,则可以省略括号。
- Lambda表达式可以有显式的返回类型,在符号->之后。如果编译器可以确定lambda的返回类型,或者如果lambda不返回任何内容,则可以省略该返回类型。
- 最后,lambda的身体出现在一对大括号内。与普通函数一样,它包含零个或多个语句。
捕获子句(Capture Clause)
Lambda可以在其主体中引入新变量,也可以访问或捕获其周围范围的变量。 Lambda以Capture Clause开头,该子句指定要捕获哪些变量,以及是按值;还是按引用进行捕获。带有&前缀的变量,表示通过引用进行访问;而没有前缀&的变量,表示通过值进行访问。
空的捕获子句[]表示,lambda表达式的主体不访问任何外部变量。
您可以使用默认捕获模式,来指示如何捕获lambda中引用的任何外部变量:
[&] 表示您引用的所有变量,都是通过引用捕获
[=] 表示它们是按值捕获。
您可以使用默认的捕获模式,然后为特定变量明确指定相反的模式。例如,如果lambda主体按引用访问外部变量total,而按值访问外部变量factor,则以下捕获子句是等效的
[&total, factor]
[factor, &total]
[&, factor]
[factor, &]
[=, &total]
[&total, =]
参数表(parameters)
除了捕获变量之外,lambda还可以接受输入参数。参数列表是可选的,并且在大多数方面类似于函数的参数表。
返回类型(return-type)
lambda表达式的返回类型会自动推导。除非您指定了尾随返回类型,否则不必使用auto关键字。尾随返回类型类似于普通方法或函数的返回类型部分。但是,返回类型必须在参数列表之后,并且必须在返回类型之前,包含return-type关键字->。
如果lambda主体仅包含一个return语句,或者该表达式没有返回值,则可以省略lambda表达式的return-type部分。
如果lambda主体包含一个return语句,则编译器会从return表达式的类型中推断出return类型。否则,编译器推断返回类型为void。
Lambda函数体
lambda表达式的lambda函数体可以包含普通方法或函数的主体可以包含的任何内容。
- 从封闭的范围中捕获变量。
- 参数
- 局部声明的变量
- 类数据成员,当在类中声明, this被捕获
- 静态存储类型的任何变量,例如,全局变量
假设您要计算一个字符串包含多少个大写字母。下面的lambda表达式使用for_each()遍历一个char数组,确定每个字母是否都大写。对于找到的每个大写字母,lambda表达式都会递增Uppercase,这是一个在lambda表达式外部定义的变量:
例子
。
int main()
{
char s[]="Hello World!";
int Uppercase = 0; //modified by the lambda
for_each(s, s+sizeof(s),
[&Uppercase] (char c) {
if (isupper(c))
Uppercase++;
}
);
cout<< Uppercase<<" uppercase letters in: "<< s<<endl;
}
就像您定义了一个函数,将其主体放置在另一个函数调用中一样。 [&Uppercase]中的&表示lambda主体获得了对大写的引用,因此可以对其进行修改。如果没有与号,则大写字母将按值传递。 C ++ 11 lambda也包含用于成员函数的构造。
自动类型推导和decltype
在许多情况下,对象的说明都包含初始化。 C ++ 11充分利用了这一点,可以在不指定对象类型的情况下说明对象:
例子
auto x=0; //x has type int because 0 is int
auto c='a'; //char
auto d=0.5; //double
auto national_debt=14400000000000LL; //long long
当对象的类型是冗长的或自动生成时(在模板中),自动类型推导非常有用。
例子
void func(const vector<int> &vi)
{
vector<int>::const_iterator ci=vi.begin();
}
相反,您可以这样说明迭代器:
auto ci=vi.begin();
关键字auto不是新词;它实际上可以追溯到ANSI C之前的时代。但是,C ++ 11改变了它的含义。
- auto不再指定具有自动存储类型的对象。
- 用它说明一个对象,该对象的类型可从其初始值设定项推导出。
为避免混淆,从C ++ 11中删除了auto的旧含义。
C ++ 11提供了一种类似的机制来捕获对象或表达式的类型。新的运算符decltype接受一个表达式,并返回其类型:
例子
const vector<int> vi;
typedef decltype (vi.begin()) CIT;
CIT another_const_iterator;
例子
。
1 #include <iostream>
2
3 using namespace std;
4
5 template <class A, class B>
6 auto min(A a, B b) ->decltype(a < b ? a : b)
7 {
8 return (a<b) ? a : b;
9 }
10
11 template <class A>
12 auto max_value(A a, A b) ->decltype(a < b)
13 {
14 return a < b;
15 }
16
17 class Person {
18 int age;
19 public:
20 Person(int age1) : age(age1){};
21 friend bool operator<(Person &a, Person &b) {return a.age > b.age;};
22 };
23
24 int main()
25 {
26 Person a(10);
27 Person b(20);
28
29 cout << min(1, 1.2) << endl;
30 cout << max_value(a, b) << endl;
31 }
统一初始化语法
C ++至少有四个不同的初始化符号,其中一些重叠。
- 带括号的初始化
std::string s(“hello”);
int m=int(); //default initialization - =赋值符号
std::string s=“hello”;
int x=5; - 使用花括号, POD聚合
int arr[4]={0,1,2,3};
struct tm today={0}; - 构造函数使用成员初始化程序
struct S {
int x;
S(): x(0) {}
};
这种分散的不仅引起新手,也容易引起混乱。更糟糕的是,在C ++ 03中,您无法初始化使用new []分配的POD数组成员和POD数组。 C ++ 11使用统一的大括号表示法清除了这些混乱情况:
例子
class C
{
int a;
int b;
public:
C(int i, int j);
};
C c {0,0}; //C++11 only. Equivalent to: C c(0,0);
int* a = new int[3] { 1, 2, 0 }; // C++11 only
class X {
int a[4];
public:
X() : a{1,2,3,4} {} //C++11, member array initializer
};
可以调用一长串push_back()初始化容器。在C ++ 11中,可以直观地初始化容器:
例子
// C++11 container initializer
vector<string> vs={ "first", "second", "third"};
map<string,string> corps =
{ {"Google", "San Jose, CA"},
{"Intel", "Santa Clara, CA"}
};
同样,C ++ 11支持在类定义中,初始化数据成员。
例子
class C
{
int a=7; //C++11 only
public:
C();
};
Delete函数和Default函数
C ++自动为类声明
- 一个默认构造函数
- 一个默认析构函数
- 一个默认复制构造函数
- 一个默认赋值运算符
struct A
{
A()=default; //C++11
virtual ~A()=default; //C++11
};
被称为默认函数。=default指示编译器生成该函数的默认实现。默认函数有两个优点:
它们比手动实现更有效,并且使程序员摆脱了手动定义这些函数的麻烦。
默认功能的相反是删除默认函数定义:
int func()=delete;
删除默认函数定义对于防止对象复制很有用。要禁用复制,声明这两个特殊的成员函数=delete:
例子
。
struct NoCopy
{
NoCopy &operator =( const NoCopy & ) = delete;
NoCopy( const NoCopy & ) = delete;
};
NoCopy a;
NoCopy b(a); //compilation error, copy ctor is deleted
nullptr
nullptr是C ++一个新增加的关键字,它指定空指针常量。 nullptr替换了容易出错的NULL宏和多年来用作空指针的常量0。 nullptr是强类型的:
例子
。
1 #include <iostream>
2
3 using namespace std;
4
5 void f(int val)
6 {
7 cout << val << endl;
8 }
9
10 int f(char *str)
11 {
12 if (str == nullptr)
13 cout << "string is empty" << endl;
14 }
15
16 int main()
17 {
18
19 f(10);
20 f(nullptr);
21 // f(NULL); // error: call of overloaded ‘f(NULL)’ is ambiguous
22 }
输出
10
string is empty
nullptr适用于所有指针类别,包括函数指针和成员指针:
- 指针比较
const char *pc=str.c_str(); // data pointers
if (pc!=nullptr)
cout<<pc<<endl;
Person *find_member(vector<Person *> *ptr, int ndx)
{
if (ptr == nullptr)
{
return nullptr;
}
…
} - 成员函数指针
class driver {
public:
string *name = nullptr;
int (*get_fuel)()=nullptr;
…
}
int (Car::*get_miles)()=nullptr; // pointer to member function - 函数指针
void (*get_speed)()=nullptr; // pointer to function
例子 一
。
1 #include <iostream>
2 #include <vector>
3
4 using namespace std;
5
6 class Person {
7 public:
8 int age;
9 string name;
10
11 Person(int age1) : age(age1){};
12 Person(char *name1) : name(name1){};
13
14 friend bool operator<(Person &a, Person &b) {return a.age > b.age;};
15 bool operator==(int age) {return this->age == age;}
16 };
17
18 Person *find_person(vector<Person *> persons, int age)
19 {
20 for (auto person : persons)
21 if (*person == age)
22 return person;
23 return nullptr;
24 }
25
26 int main()
27 {
28 Person a(10);
29 Person b(20);
30 Person c(nullptr);
31 Person d(NULL); // error: call of overloaded ‘Person(NULL)’ is ambiguous
32
33 vector<Person *> persons = {&a, &b};
34
35 auto p = find_person(persons, 10);
36 if (p != nullptr)
37 cout << p->age << endl;
38 p = find_person(persons, 15);
39 if (p != nullptr)
40 cout << p->age << endl;
41 }
例子 二
。
1 #include <iostream>
2
3
4 using namespace std;
5
6 class Driver {
7 public:
8 Driver()=default;
9 string *name = nullptr;
10 int (*get_fuel)()=nullptr;
11 };
12
13 class Truck_driver : public Driver {
14 public:
15 int (*get_fuel)()=[]()->int{ cout << "truck" << endl;};
16 };
17
18 int (*get_fuel)()=nullptr;
19
20 int main()
21 {
22 Driver d1;
23 Truck_driver d2;
24 Driver *ptr;
25 Truck_driver *ptr2;
26
27 ptr = &d1;
28 if (ptr->get_fuel == nullptr)
29 cout << "get_fuel emptr" << endl;
30
31 ptr2 = &d2;
32 if (ptr2->get_fuel != nullptr)
33 ptr2->get_fuel();
34
35 ptr = ptr2;
36 if (ptr->get_fuel == nullptr)
37 cout << "get_fuel emptr #2" << endl;
38 }
输出
get_fuel emptr
truck
get_fuel emptr #2
委托构造函数
在C ++ 11中,构造函数可以调用同一类的另一个构造函数。被调用的函数被称为目标构造函数,调用函数被称为委托构造函数。
如果一个类具有多个构造函数,而且这些构造函数执行一些相同的初始化步骤。
例子
。
class memory_allocator {
public:
memory_allocator (int count)
{
total_size = count * 1;
ptr = new char [total_size];
}
memory_allocator (int count, int elem_size)
{
total_size = count * elem_size;
ptr = new char [total_size];
}
memory_allocator (int count, int elem_size, int extra)
{
total_size = count * elem_size + extra;
ptr = new char [total_size];
}
private:
int total_size;
char *ptr;
};
这三个构造函数具有相同的函数体。重复的代码使维护很困难。如果要添加更多成员或更改现有成员的类型,则必须进行三次相同的更改。
为了避免代码重复,可以将常见的初始化步骤移到了成员函数上。构造函数通过调用此成员函数来实现相同的功能。
例子
。
class memory_allocator {
public:
memory_allocator (int count)
{
init(count, 1, 0);
}
memory_allocator (int count, int elem_size)
{
init(count, elem_size, 0);
}
memory_allocator (int count, int elem_size, int extra)
{
init(count, elem_size, extra);
}
private:
int total_size;
char *ptr;
void init(int count, int elem_size, int extra)
{
total_size = count * elem_size + extra;
ptr = new char [total_size];
}
};
此修订版消除了代码重复,但带来下列新问题:
- 其他成员函数可能会意外调用init(),这将导致意外结果。
- 输入类成员函数后,所有类成员均已构建。调用成员函数来完成类成员的构造工作为时已晚。
C ++ 11提供委托构造函数来解决这些问题。
将常见的初始化步骤集中在一个构造函数中,该构造函数称为目标构造函数。其他构造函数可以调用目标构造函数进行初始化。这些构造函数称为委托构造函数。
例子
。
class memory_allocator {
public:
memory_allocator (int count, int elem_size, int extra)
{
total_size = count * elem_size + extra;
ptr = new char [total_size];
}
memory_allocator (int count)
{
memory_allocator (count, 1);
}
memory_allocator (int count, int elem_size)
{
memory_allocator (count, elem_size, 0);
}
private:
int total_size;
char *ptr;
};
委托构造函数使程序清晰而简单。在此示例中,memory_allocator (int count)委托给memory_allocator (int count, int elem_size),因此memory_allocator (int count, int elem_size)是memory_allocator (int count)的目标构造函数。
委托和目标构造函数具有与其他构造函数相同的接口。从示例中可以看到,委托构造函数可以是另一个委托构造器的目标构造函数,从而形成委托链。
右值引用
C ++ 11引入了新的引用类型类别,称为右值引用。右值引用可以绑定到右值,例如临时对象。
添加右值引用的主要原因是移动语义。
与传统复制不同,移动意味着目标对象获得了源对象的资源,而使源处于“空”状态。
在某些情况下,复制对象既昂贵,又不必要,则可以使用移动操作来代替。
例子
。
void naiveswap(string &a, string & b)
{
string temp = a;
a=b;
b=temp;
}
复制字符串需要分配原始内存,并将字符从源复制到目标。
移动字符串仅交换两个数据成员,而没有分配内存,复制char数组和删除内存。
例子
。
void moveswapstr(string& empty, string & filled)
{
//pseudo code, but you get the idea
size_t sz=empty.size();
const char *p= empty.data();
//move filled's resources to empty
empty.setsize(filled.size());
empty.setdata(filled.data());
//filled becomes empty
filled.setsize(sz);
filled.setdata(p);
}
如果要实现支持移动的类,则可以这样说明一个移动构造函数和一个移动赋值运算符:
例子
。
class Person
{
Person(Person&&obj){ // move constructor
// Move constructor
// It will simply shift the resources,
// without creating a copy.
cout << "Calling Move constructor\n";
this->ptr = obj.ptr;
obj.ptr = NULL;
}
Person&& operator=(Movable&&); // move assignment operator
};
...
vector<Person> persons;
persons.push_back(Person("jason"));
右值引用支持移动语义的实现,这可以显着提高应用程序的性能。移动语义使您可以编写将资源(例如动态分配的内存)从一个对象转移到另一个对象的代码。
移动语义之所以起作用,是因为它可以临时对象转移, 而且临时对象从程序中其他地方无法引用。
源为右值的复制和赋值操作将自动利用移动语义。与默认的复制构造函数不同,编译器不提供默认的move构造函数。
为了更好地理解移动语义,考虑将元素插入向量对象。
如果超出了矢量对象的容量,则矢量对象必须为其元素重新分配内存,然后将每个元素复制到另一个内存位置,以为插入的元素腾出空间。
当插入操作复制元素时,它会创建一个新元素,调用复制构造函数将数据;从前一个元素复制到新元素,然后销毁前一个元素。
但移动语义直接移动对象,而不必执行昂贵的内存分配和复制操作。
例子
。
1 #include <iostream>
2 #include <vector>
3
4 using namespace std;
5
6 class Person
7 {
8 public:
9 Person() : name(nullptr){};
10
11 Person(string name_str) {
12 int i;
13 char *ptr = const_cast<char *>(name_str.c_str());
14
15 name = new char[name_str.length()+1];
16 for (i=0; i<name_str.length()+1; i++)
17 name[i] = ptr[i];
18 }
19
20 // move constructor
21 Person(Person&& obj){
22 // Move constructor
23 // It will simply shift the resources,
24 // without creating a copy.
25 cout << "Calling Move constructor\n";
26 this->name = obj.name;
27 obj.name = nullptr;
28 }
29
30 // move assignment operator
31 Person&& operator=(Person &&obj) {
32 this->name = obj.name;
33 obj.name = nullptr;
34 cout << "Calling Move assignment\n";
35 }
36
37
38 char *name;
39 };
40
41 void myswap(Person &a, Person &b)
42 {
43 Person temp;
44
45 temp = move(a);
46 a = move(b);
47 b = move(temp);
48 }
49
50 int main()
51 {
51 Person m("bill");
52 Person w("linda");
53
54 cout << "before" << endl;
55 cout << "w: " << w.name <<endl;
56 cout << "m: " << m.name <<endl;
57 myswap(m, w);
58
59 cout << "w: " << w.name << endl;
60 cout << "m: " << m.name <<endl;
61
62 vector<Person> persons;
63 persons.push_back(Person("jason"));
63 }
输出
before
w: linda
m: bill
Calling Move assignment
Calling Move assignment
Calling Move assignment
w: bill
m: linda
Calling Move constructor
完美的转发减少了对重载函数的需求,并有助于避免转发问题。当编写一个将引用作为参数的通用函数,并将其传递(或转发)到另一个函数时,可能会发生转发问题。
例如,如果通用函数采用const T&类型的参数,则被调用函数无法修改该参数的值。
如果通用函数采用T&类型的参数,则无法使用右值(例如临时对象或整数文字)来调用该函数。
要解决此问题,通常提供通用函数的不同重载版本,这些通用函数的参数是T&, 或const T&。结果,重载函数的数量随参数的数量呈指数增长。
使用右值引用,能够编写一个函数版本的版本,该版本接受任意参数,并将其转发给另一个函数,就像直接调用了另一个函数一样。
例子
。
1 #include <iostream>
2
3 using namespace std;
4
5 class AClass {
6 public:
7 AClass(int &, int &){};
8 };
9
10 class BClass {
11 public:
12 BClass(int &, int &&){};
13 };
14
15 class CClass {
16 public:
17 CClass(int &&, int &&){};
18 };
19
20 template <typename T, typename A1, typename A2>
21 T* factory(A1&& a1, A2&& a2)
22 {
23 return new T(forward<A1>(a1), forward<A2>(a2));
24 }
25
26
27 int main ()
28 {
29 int a=6;
30 int b=8;
31
32 AClass *pa = factory<AClass>(a,b);
33 BClass *pb = factory<BClass>(a,2);
34 CClass *pc = factory<CClass>(2,2);
35 }
上述的例子代码使用右值引用作为工厂函数的参数。 std :: forward函数的目的是将工厂函数的参数转发给模板类的构造函数。
编译器如何处理左值和右值
编译器将命名的右值引用视为左值,将未命名的右值引用视为右值。
当一个函数的参数是右值引用时,该参数在函数主体中被视为左值。
编译器将命名的右值引用视为左值,因为命名的对象可以被程序的多个部分引用;允许程序的多个部分修改或删除该对象的资源将很危险。例如,如果程序的多个部分尝试从同一对象转移资源,则只有第一个转移资源调用能成功转移资源。