1.类型推导的语法和规则
C++11提供了auto和decltype来静态推导类型。
1.1 auto 类型推导
C++11 赋予 auto 关键字新的含义,使用它来做自动类型推导。也就是说,使用了 auto 关键字以后,编译器会在编译期间自动推导出变量的类型,这样我们就不用手动指明变量的数据类型了。auto 是一个占位符,在编译器期间它会被真正的类型所替代。
auto 关键字基本的使用语法如下:
auto name = value;
name 是变量的名字,value 是变量的初始值。
auto推导示例:
1.auto a = 5; // a为int,auto推导为int
auto b = 1.5; // b为double,auto推导为double
auto c = "Hello World"
// auto推导为const char*
2.int n = 1;
auto *p1 = &x; //p1为int*,auto推导为int
auto p2 = &x; //p2为int*,auto推导为int*
auto &r1 = x; //r1为int&,auto推导为int
auto r2 = r1; //r2为int,auto推导为int
3.int x = 0;
const auto c1 = x; //c1为const int,auto被推导为int
auto c2 = c1; //c2为const int,auto被推导为int(const 属性被抛弃)
const auto &c3 = x; //c3为const int& 类型,auto被推导为int
auto &c4 = c3; //c4为const int& 类型,auto被推导为const int类型
//使用auto定义迭代器
4.for(vector<int>::iterator it = v.begin(); it != v.end(); ++it);
//等价于
for(auto it = v.begin(); it != v.end(); ++it);
1.2 decltype类型推导
decltype是C++ 11新增的一个关键字,它和auto的功能一样,都用来在编译时期进行自动类型推导,decltype主要用于获取一个表达式的类型,decltype关键字基本的使用语法如下:
decltype(exp) varname;
varname 表示变量名,exp 表示一个表达式。
原则上讲,exp 就是一个普通的表达式,它可以是任意复杂的形式,但是我们必须要保证 exp 的结果是有类型的,不能是 void;例如,当 exp 调用一个返回值类型为 void 的函数时,exp 的结果也是 void 类型,此时就会导致编译错误。
decltype(exp)推导规则如下:
-
如果 exp 是一个不被括号
( )
包围的表达式,或者是一个类成员访问表达式,或者是一个单独的变量,那么 decltype(exp) 的类型就和 exp 一致,这是最普遍最常见的情况。 -
如果 exp 是函数调用,那么 decltype(exp) 的类型就和函数返回值的类型一致。
-
如果以上两点都不是,且exp 是一个左值(lvalue),那么 decltype(exp) 的类型就是 T&。
decltype推导示例:
1.int a = 1, double b = 1.5;
decltype(a) c = 2; //c被推导成了int
decltype(b) d = 5.5; //d被推导成了double
decltype((a)) e = c; //e为int&,因为a是左值
2.基于范围的for循环
C++11中,为for循环添加了一种全新的语法格式,如下所示:
for(declaration : expression{
//循环体
}
declaration为定义的变量,该变量的类型为要遍历序列中变量的类型,可以用auto关键字表示;express为要遍历的序列。
使用示例
vector<int> nums;
for(int num: nums);
for(auto n: nums);
3.右值引用和移动构造函数
3.1 右值引用
C++ 11 标准之前,不允许修改右值,这就产生一个问题,实际开发中我们可能需要对右值进行修改(实现移动语义时就需要),为此,C++ 11标准引入了一种新的引用方式,称为右值引用,用&&表示。右值引用允许对右值进行修改,且只能用右值引用进行初始化,例如:
1.int a = 1;
int && b = 1;
int && c = a; //error,只能用右值初始化右值引用
2.int && x = 2;
x = 5;
cout << x << endl; //结果为5
3.2 移动构造函数
C++ 11 标准之前,如果想用其它对象初始化一个同类的新对象,只能借助类中的拷贝构造函数。C++ 11引入了右值引用的语法,借助它可以实现移动语义。
移动语义就是以移动而非深拷贝的方式初始化含有指针成员的类对象。对于程序执行过程中产生的临时对象,往往只用于传递数据(没有其它的用处),并且会很快会被销毁。因此在使用临时对象初始化新对象时,我们可以将其包含的指针成员指向的内存资源直接移给新对象所有,无需再新拷贝一份,这大大提高了初始化的执行效率。
注意:当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会调用拷贝构造函数。
默认情况下,左值初始化同类对象只能通过拷贝构造函数完成,如果想调用移动构造函数,则必须使用右值进行初始化。但可以使用move函数,它可以将左值强制转换成对应的右值,由此便可以使用移动构造函数。
移动构造函数示例:
#include <iostream>
using namespace std;
class Student
{
public:
//构造函数
Student():num(new int(0))
{
cout<<"构造函数"<<endl;
}
//拷贝构造函数
Student(const Student &stu):num(new int(*stu.num))
{
cout<<"拷贝构造函数"<<endl;
}
//移动构造函数
Student(Student &&stu):num(stu.num)
{
stu.num = nullptr;
cout<<"移动构造函数"<<endl;
}
~Student()
{
cout<<"析构函数"<<endl;
}
private:
int *num;
};
int main()
{
Student stu1 = Student();
Student stu2 = stu1;
Student stu3 = move(stu1);
//运行结果为:
/*
构造函数
拷贝构造函数
移动构造函数
析构函数
析构函数
析构函数
*/
return 0;
}
4.初始化列表
C++11统一了初始化列表的使用,可以直接在变量名后面跟上初始化列表,也可以用于任何类型对象的初始化,来进行对象的初始化。例如:
A a1 = A{1, 2};
A a2{5, 10};
5.constexpr关键字
在开发中我们经常会用到常量表达式,如下所示:
1.int a[10];
2.int N = 5;
int b[N]; //运行时error,编译器认为N是变量
常量表达式和非常量表达式的计算时间是不一样的,非常量表达式只能在程序运行阶段计算出结果;而常量表达式的计算往往发生在程序的编译阶段,这可以极大提高程序的执行效率,因为表达式只需要在编译阶段计算一次,节省了每次程序运行时都需要计算一次的时间。所以希望在编译阶段即可判断一个表达式是不是常量表达式。
C++ 11新引入了constexpr关键字,constexpr修饰的变量是一个编译期常量,修饰的函数是一个编译器常量函数表达式。但是constexpr不能修饰自定义类型。例如:
1.constexpr int N = 5;
int a[N]
2.constexpr int func(int x)
{
return x;
}
在C++ 11中,const关键字保留了“只读”的属性,而将“常量”的属性分给了constexpr。所以一般建议凡是表达“只读”语义的场景都使用 const,表达“常量”语义的场景都使用 constexpr。
6.nullptr空指针
实际开发中,避免产生“野指针”最有效的方法,就是在定义指针的同时完成初始化操作,即便该指针的指向尚未明确,也要将其初始化为空指针。C++ 11之前,一般初始化为0或者NULL。NULL是实现定义好的一个宏(#define NULL 0),使用NULL会引发如下问题:
void func(int n);
void func(char* c);
上述示例中,如果调用func(0),和func(NULL),实际都会调用到func(int n)。
C++ 11引入了nullptr关键字,nullptr 是 nullptr_t 类型的右值常量,专用于初始化空类型指针。nullptr 可以被隐式转换成任意的指针类型,例如:
int* p1 = nullptr;
double* p2 = nullptr;
char* p3 = nullptr;
7.final和override
C++11新增了final和override关键字,功能如下:
final关键字
-
将类标记为final,表示该类不可被继承
-
将方法标记为final,表示该方法不可被重写
override关键字
-
将方法标记为override,表示该方法需要被重写
使用示例:
class B1 final(){};
class D1 : B1{}; //error,B1不可被继承
class B2
{
virtual void func1(int);
virtual void func2(int) final;
}
class D2 : B2
{
virtual void func1(int, char) override; //error,需要重写父类方法,但是父类中无该方法
virtual void func2(int); //error,func2不可被重写
}
8.tuple元组
C++11 标准新引入了一种类模板,命名为 tuple,tuple 最大的特点是:实例化的对象可以存储任意数量、任意类型的数据。
STL中有pair类模板,该模板可以定义二元组,而tuple可以定义多元组,当需要存储多个不同类型的元素时,可以使用 tuple;当函数需要返回多个数据时,可以将这些数据存储在 tuple 中,函数只需返回一个 tuple 对象即可。
tuple的部分初始化方式如下所示:
tuple <string, int> tuple1;
tuple <string, double, int> tuple2("tuple2", 2.5, 5);
tuple <string, double, int> tuple3(tuple2);
// make_tuple()函数用于创建一个tuple右值对象
tuple <string, string, int> tuple4(make_tuple("tuple4", "test4", 1));
9.Lambda表达式
Lambda表达式主要提供了匿名函数的特性,利用Lamdba表达式可以编写内嵌的匿名函数,用以替换独立函数或者函数对象,Lambda表达式的语法格式如下:
[capture-list] (params) mutable(optional) constexpr(optional)(c++17) exception attribute -> ret { body }
[ ] 方括号用于向编译器表明当前是一个 Lambda 表达式,每当定义一个Lambda表达式后,编译器会自动生成一个匿名类,这个被称为闭包类型(closure type)。那么在运行时,这个Lambda表达式就会返回一个匿名的闭包实例,其实是一个右值。
表达式中的参数
-
capture - list:捕捉列表,闭包的一个强大之处是其可以通过传值或者引用的方式捕捉其封装作用域内的变量,该参数为空时,表示没有捕捉任何变量;
-
params:参数列表,可以省略(但是后面必须紧跟函数体);
-
mutable:可选,将lambda表达式标记为mutable后,函数体就可以修改传值方式捕获的变量;
-
constexpr:可选,C++17,可以指定lambda表达式是一个常量函数;
-
exception:可选,指定lambda表达式可以抛出的异常;
-
attribute:可选,指定lambda表达式的特性;
-
ret:可选,返回值类型;
-
body:函数执行体。
捕获的方式可以是引用也可以是复制,但是具体说来会有以下几种情况来捕获其所在作用域中的变量:
-
[]:默认不捕获任何变量;
-
[=]:默认以值捕获所有变量;
-
[&]:默认以引用捕获所有变量;
-
[x]:仅以值捕获x,其它变量不捕获;
-
[&x]:仅以引用捕获x,其它变量不捕获;
-
[=, &x]:默认以值捕获所有变量,但是x是例外,通过引用捕获;
-
[&, x]:默认以引用捕获所有变量,但是x是例外,通过值捕获;
-
[this]:通过引用捕获当前对象(其实是复制指针);
-
[*this]:通过传值方式捕获当前对象;
Lambda表达式的使用示例如下:
int main()
{
int x = 10;
auto add_x = [x](int a) { return a + x; }; // 复制捕捉x
auto multiply_x = [&x](int a) { return a * x; }; // 引用捕捉x
cout << add_x(10) << " " << multiply_x(10) << endl;
// 输出:20 100
return 0;
}
Lambda表达式与普通函数的使用区别示例如下:
/*Lambda表达式使用*/
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
int num[4] = {4, 2, 3, 1};
//对 a 数组中的元素进行排序
sort(num, num+4, [=](int x, int y) -> bool{ return x < y; } );
for(int n : num)
{
cout << n << " ";
}
return 0;
}
/*普通函数使用*/
#include <iostream>
#include <algorithm>
using namespace std;
//自定义的升序排序规则
bool sort_up(int x,int y)
{
return x < y;
}
int main()
{
int num[4] = {4, 2, 3, 1};
//对 a 数组中的元素进行排序
sort(num, num+4, sort_up);
for(int n : num)
{
cout << n << " ";
}
return 0;
}
//输出结果:1 2 3 4
10.智能指针
C++98中,支持使用 auto_ptr 智能指针来实现堆内存的自动回收,如下所示
auto_ptr<string> p1 (new string ("Hello World"));
auto_ptr<string> p2;
p2 = p1; //auto_ptr不会报错
此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。所以auto_ptr的缺点是:存在潜在的内存崩溃问题!
C++11 废弃了auto_ptr,增添了 unique_ptr、shared_ptr 以及 weak_ptr 这 3 个智能指针来实现堆内存的自动回收。
10.1 unique_ptr
unique_ptr独占式拥有或严格拥有概念,保证同一时间只有一个智能指针可以指向该对象。
unique_ptr<string> p3 (new string ("I reigned lonely as a cloud."));
unique_ptr<string> p4;
p4 = p3; // 会报错!
编译器认为p4=p3非法,避免了p3不再指向有效数据的问题,因此,unique_ptr比auto_ptr更安全。另外unique_ptr还有更聪明的地方:当程序试图将一个unique_ptr赋值给另一个时,如果unique_ptr是个临时右值,编译器允许这么做;如果unique_ptr将存在一段时间,编译器将禁止这么做。成员函数有:
-
operator=():重载了 = 赋值号,从而可以将 nullptr 或者一个右值 unique_ptr 指针直接赋值给当前同类型的 unique_ptr 指针。
-
operator*():获取当前 unique_ptr 智能指针对象指向的数据。
-
operator->(): 重载 -> 号,当智能指针指向的数据类型为自定义的结构体时,通过 -> 运算符可以获取其内部的指定成员。
-
operator :重载了 [] 运算符,当 unique_ptr 指针指向一个数组时,可以直接通过 [] 获取指定下标位置处的数据。
-
operator bool():unique_ptr 指针可直接作为 if 语句的判断条件,以判断该指针是否为空,如果为空,则为 false;反之为 true。
-
get(): 获取当前 unique_ptr 指针内部包含的普通指针。
-
get_deleter():获取当前 unique_ptr 指针释放堆内存空间所用的规则。
-
release(): 释放当前 unique_ptr 指针对所指堆内存的所有权,但该存储空间并不会被销毁。
-
reset(p): 其中 p 表示一个普通指针,如果 p 为 nullptr,则当前 unique_ptr 也变成空指针;反之,则该函数会释放当前 unique_ptr 指针指向的堆内存(如果有),然后获取 p 所指堆内存的所有权(p 为 nullptr)。
-
swap(x):交换当前 unique_ptr 指针和同类型的 x 指针。
#include <iostream>
#include <memory>
using namespace std;
int main()
{
std::unique_ptr<int> p5(new int);
*p5 = 10;
// p 接收 p5 释放的堆内存
int * p = p5.release();
cout << *p << endl;
//判断 p5 是否为空指针
if (p5)
{
cout << "p5 is not nullptr" << endl;
}
else
{
cout << "p5 is nullptr" << endl;
}
std::unique_ptr<int> p6;
//p6 获取 p 的所有权
p6.reset(p);
cout << *p6 << endl;;
return 0;
}
/*输出结果
10
p5 is nullptr
10
*/
10.2 shared_ptr
shared_ptr实现共享式有用概念。多个智能指针可以指向相同对象,shared_prt是强引用,该类型智能指针在实现上采用的是引用计数机制,即便有一个 shared_ptr 指针放弃了堆内存的“使用权”(引用计数减 1),也不会影响其他指向同一堆内存的 shared_ptr 指针(只有引用计数为 0 时,堆内存才会被自动释放。
shared_ptr是为了解决auto_ptr在对象所有权上的局限性(auto_ptr是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针。成员函数有:
-
operator=():重载赋值号,使得同一类型的 shared_ptr 智能指针可以相互赋值。
-
operator*():获取当前 shared_ptr 智能指针对象指向的数据。
-
operator->(): 重载 -> 号,当智能指针指向的数据类型为自定义的结构体时,通过 -> 运算符可以获取其内部的指定成员。
-
operator bool():判断当前 shared_ptr 对象是否为空智能指针,如果是空指针,返回 false;反之,返回 true。
-
use_count():返回引用计数的个数。
-
unique():返回是否独占所有权(use_count为1)。
-
swap():交换两个shared_ptr对象(即交换所拥有的对象)。
-
reset():放弃内部对象的所有权或拥有对象的变更,会引起原有对象的引用计数的减少。
-
get():返回内部对象(指针),由于已经重载了()方法,因此和直接使用对象是一样的。如shared_ptr<int> sp(new int(1));sp与sp.get()是等价的。
#include <iostream>
#include <memory>
using namespace std;
int main()
{
//构建 2 个智能指针
std::shared_ptr<int> p1(new int(10));
std::shared_ptr<int> p2(p1);
//输出 p2 指向的数据
cout << *p2 << endl;
p1.reset(); //引用计数减 1,p1为空指针
if (p1)
{
cout << "p1 不为空" << endl;
}
else
{
cout << "p1 为空" << endl;
}
//以上操作,并不会影响 p2
cout << *p2 << endl;
//判断当前和 p2 同指向的智能指针有多少个
cout << p2.use_count() << endl;
return 0;
}
/*输出结果
10
p1为空
10
1
*/
shared_ptr的线程安全性:
-
同一个shared_ptr对象可以被多线程同时读取;
-
不同的shared_ptr对象可以被多线程同时修改;
-
同一个shared_ptr对象不能被多线程直接修改,需要加锁
10.3 weak_ptr
weak_ptr是一种不控制对象生命周期的智能指针,它指向一个shared_ptr的管理对象,C++11标准虽然将 weak_ptr 定位为智能指针的一种,但该类型指针通常不单独使用(没有实际用处),只能和 shared_ptr 类型指针搭配使用。甚至于,我们可以将 weak_ptr 类型指针视为 shared_ptr 指针的一种辅助工具,进行该对象的内存管理的,是那个强引用的shared_ptr。weak_ptr只是提供了对管理对象的一个访问手段,它的构造和析构不会引起引用计数的增加或减少。
weak_ptr是用来解决shared_ptr相互引用时的死锁以及内存泄露问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。
weak_ptr成员函数:
-
operator=():重载 = 赋值运算符,是的 weak_ptr 指针可以直接被 weak_ptr 或者 shared_ptr 类型指针赋值。
-
swap(x):其中 x 表示一个同类型的 weak_ptr 类型指针,该函数可以互换 2 个同类型 weak_ptr 指针的内容。
-
reset():将当前 weak_ptr 指针置为空指针。
-
use_count(): 查看指向和当前 weak_ptr 指针相同的 shared_ptr 指针的数量。
-
expired():判断当前 weak_ptr 指针为否过期(指针为空,或者指向的堆内存已经被释放)。
-
lock():如果当前 weak_ptr 已经过期,则该函数会返回一个空的 shared_ptr 指针;反之,该函数返回一个和当前 weak_ptr 指向相同的 shared_ptr 指针。
#include <iostream>
#include <memory>
using namespace std;
class B;
class A
{
public:
shared_ptr<B> pb_;
~A()
{
cout << "A delete" << endl;
}
};
class B
{
public:
shared_ptr<A> pa_;
~B()
{
cout << "B delete" << endl;
}
};
void fun()
{
shared_ptr<B> pb(new B());
shared_ptr<A> pa(new A());
pb->pa_ = pa;
pa->pb_ = pb;
cout << pb.use_count() << endl; // 输出 2
cout << pa.use_count() << endl; // 输出 2
}
int main()
{
fun();
return 0;
}
上述程序中,可以看到fun函数中pa,pb相互引用,两个智能指针都是shared_ptr类型,两个资源的引用计数为2,当要跳出函数时,智能指针pa、pb析构两个资源引用计数会减1,但是两者引用计数还是为1。导致跳出函数时资源没有被释放,所以A,B的析构函数没有被调用。解决方法:把其中一个改为weak_ptr,如下所示:
#include <iostream>
#include <memory>
using namespace std;
class B;
class A
{
public:
weak_ptr<B> pb_;
~A()
{
cout << "A delete" << endl;
}
};
class B
{
public:
shared_ptr<A> pa_;
~B()
{
cout << "B delete" << endl;
}
};
void fun()
{
shared_ptr<B> pb(new B());
shared_ptr<A> pa(new A());
pb->pa_ = pa;
pa->pb_ = pb;
cout << pb.use_count() << endl; // 输出 2
cout << pa.use_count() << endl; // 输出 2
}
int main()
{
fun();
return 0;
}
把类A里面的shared_ptr pb改为weak_ptr pb,这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减1,同时pa析构使A的计数减一,那么A的计数为 0,A得到释放。
此外,C++ 11还在STL中增加了无序关联式容器,这将在其他的文章中讨论。