C++11概览
自从1998年C++第一个标准定档以来,作为C++真正意义上的第二个大标准,C++11带来了数量巨大的变化,其中包含140多个新特性,以及600多个针对 C++98/03 的缺陷修正。C++11能更好地用于系统开发和库开发,语法更加泛化和简单化,更加稳定和安全,在保证功能强大的同时提高开发效率。
C++11也有很多不完善的地方,除此之外,还有很多鸡肋的、被人吐槽的新特性和功能。本文只对在实际中多见的、有价值的新用法进行讨论。
新关键字
auto
在 C++11 中,auto是一个类型指示符。使用 auto 定义变量时必须进行初始化,在编译期间,编译器会根据auto右边的表达式推导auto的实际类型。因此auto是一种类型声明时的占位符,编译器在编译期间会将auto替换为变量实际的类型。在实际使用中,可以依此实现对长类型的替换。
auto 的使用有如下细则:
- 用 auto 声明指针类型时,auto 和 auto* 没有区别,但引用必须使用auto&;
- 使用 auto 在同一行定义多个变量时,这些变量必须是同一类型的,否则编译器会报错。编译器是根据第一个变量推导 auto 的类型的,并以此类型定义后面的变量;
- auto 不能作为函数的参数;
- auto 不能用来声明数组。
decltype
decltype 可以将变量声明为表达式所指定的类型。与auto不同的是,auto 只是一个类型声明时的占位符,本身的作用是根据表达式做类型推导,不具有声明指定类型变量的功能,而decltype可以根据表达式类型,进行指定类型变量的声明。
template<class T1, class T2>
void func(T1 x, T2 y)
{
decltype(x, y) ret = x * y; //以 x * y 的类型定义变量 ret
cout << typeid(ret).name() << endl;
}
nullptr
在C语言中,NULL 指针是一个宏,它在C头文件(stddef.h)中的定义如下:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
为了保证类型安全问题,且更好地支持重载,同时为了兼容C语言,C++新定义了一个关键字nullptr。nullptr是指针类型的,对于nullptr,有#define ((void*)0) nullptr
。
void func(int x)
{
cout << "void func(int x)" << endl;
}
void func(int* x)
{
cout << "void func(int* x)" << endl;
}
void test()
{
func(NULL); //void func(int x)
func(nullptr); //void func(int* x)
cout << typeid(NULL).name() << endl; //int
cout << typeid(nullptr).name() << endl; //std::nullptr
}
explicit
explicit用于禁止自动的隐式类型转换,一般用于构造函数。例如,对具有单参数或多参数构造函数的对象,可以直接进行隐式类型转换对其构造,可以使用explicit阻止这种行为。
class demo
{
public:
explicit demo(int a)
:_a(a)
{ }
private:
int _a;
}
void test()
{
//隐式类型转换,直接调用单参数的构造函数
//demo d = 10; //当使用explicit禁止demo进行这种类型转换后,编译无法通过
}
explicit是在编译期间进行检查的。
final
final修饰类,使该类不能被继承;final修饰基类的虚函数,使该虚函数不能被子类重写。
//修饰类
class A final
{
public:
virtual void func()
{
cout << "A::func()" << endl;
}
};
//error
//class B : public A
//{
//public:
// virtual void func()
// {
// cout << "B::func()" << endl;
// }
//};
class B
{
public:
virtual void func() final //修饰基类的虚函数
{
cout << "A::func()" << endl;
}
};
class C : public B
{
public:
//error
//virtual void func()
//{
// cout << "B::func()" << endl;
//}
};
override
override修饰子类的虚函数,帮助用户检查是否对父类对应的虚函数完成重写。
class A
{
public:
virtual void func()
{
cout << "A::func()" << endl;
}
};
class B : public A
{
public:
virtual void func() override //修饰子类的虚函数
{
cout << "B::func()" << endl;
}
};
final和override都是在编译期间起作用的,目的是帮助用户进行预期检查,如果使用final/override后不符合规则/未进行预期操作,编译就无法通过。
STL新容器
initializer_list
在C++中,一切皆可使用{ }
初始化,但是各有其原理,例如下面的代码:
class point
{
public:
//explicit point(int x, int y)
point(int x, int y)
:_x(x), _y(y)
{
cout << "point" << endl;
}
point(const point& p)
:_x(p._x), _y(p._y)
{
cout << "point(const point& p)" << endl;
}
private:
int _x;
int _y;
};
void test6()
{
point p1(1, 2); //调用构造函数
point p2 = { 3, 4 }; //多参数构造函数的隐式类型转换,直接调用构造函数
point p3{ 5, 6 }; //构造 + 拷贝构造 -> 直接构造
vector<point> v{ {1, 2}, {3, 4}, {5, 6} };//构造 + 列表初始化
}
对于24行来说,p2进行的是多参数构造函数的隐式类型转换,本质是由“构造 + 拷贝构造”优化而来,与25行类似;对于26行,每个vector的元素point进行隐式类型转换(同p2),而vector是由 initializer_list 进行初始化的,类似于:
vector<point> v{ p01, p02, p03 };
initializer_list
是一个与vector类似的容器,initializer_list的对象由编译器自动构造,例如如果写出{ p01, p02, p03 }
这样的代码,编译器会自动将其构造为一个initializer_list对象。之所以会支持26行那样对vector的构造,是因为vector中含有针对initializer_list的构造函数,类似为:
template<class T>
class vector
{
public:
/*…………*/
vector(initializer_list<T> il)
{ }
/*…………*/
}
同样,initializer_list拥有迭代器,便于访问其中的元素,所以上述vector的构造函数可以写为:
/*…………*/
vector(initializer_list<T> il)
{
auto begin = il.begin();
while(begin != il.end()) { push_back(*begin); ++begin; }
}
/*……*/
initializer_list的底层与vector类似,在此不做讨论。需要注意的是,initializer_list虽然是隐式使用的,但是需要显式地包含<initializer_list>
头文件,这个头文件在顺序容器中一般已经被包含,所以不需要用户手动进行。
unordered_map
unordered_set
关于unordered系列容器的详细讨论请见数据结构_哈希表和unordered系列容器的封装实现。
右值引用的移动语义
左值和右值
左值可以取地址,一般可以对左值赋值,左值可以出现在赋值运算符的左边,也可以出现在赋值运算符的右边。在判断中认为,能取地址就是左值。
右值一般是个表达式,包括字面常量、临时变量等,右值不能取地址。右值不能出现在赋值运算符的左边。内置类型的右值被称为纯右值,自定义类型的右值被称为将亡值,因为其即将被释放。常量字符串是左值,因为可以对其进行取地址(首元素地址)。
void test()
{
int a = 10; //10是右值
int b = a; //a和b都是左值
int sum = a + b;// a + b是右值
const char* p = "hello"; //"hello"是左值
}
左值引用和右值引用
左值引用(lvalue reference)即是给左值取别名,右值引用(rvalue reference)即是给右值取别名,在此之前(C++98),我们讨论的引用均为左值引用。
const左值引用可以给右值取别名,这是因为对右值进行引用时,编译器会以右值生成一个具有常属性的临时对象,const左值引用实际上是这个临时对象的别名。使用const左值引用对右值取别名,从现象上看是延长了右值的生命周期。
右值引用可以直接对右值取别名。右值引用不能直接对左值取别名,而需要先对左值进行move()操作。move()函数可以强制对值进行移动语义,move()的返回值都会被视为右值。接收被move()的值后,被move()的值要么被销毁,要么被赋了一个新值。虽然const左值引用和右值引用都可以对右值取别名,但是两者可以构成函数重载,且不会造成调用歧义。下文会看到,右值引用的移动语义是支持类对象的移动构造和移动赋值的基础。
void test()
{
int a = 10;
int b = a;
int&& rrSum = a + b; //对 a + b 进行右值引用
const int& rlSum = a + b; //使用const左值引用对右值取别名
int&& rra = move(a); //强制对 a 进行移动语义,move()的返回值被视为右值
}
完美转发
一如上文所说,右值不能取地址,不能修改,但是右值引用可以取地址,可以修改,即右值引用的属性会被编译器识别为左值。完美转发(Perfect forwarding)可以保持引用的原有属性。是否使用完美转发,需要视两类场景进行选择:
- 需要将右值引用的属性视为左值的情景,不需要进行完美转发,例如移动构造、移动赋值;
- 需要将右值引用的属性视为右值的情景,需要进行完美转发,例如STL容器插入接口的右值引用版本。
使用forward
模板函数对引用进行完美转发,具体的使用示例见下文。
万能引用
万能引用(universal reference)即可以接收左值,也可以接收右值。当万能引用对左值进行引用时,被称为引用折叠(reference collapsing)。万能引用的使用前提是函数模板。
void Func(int& x) { cout << "左值引用" << endl; }
void Func(const int& x) { cout << "const 左值引用" << endl; }
void Func(int&& x) { cout << "右值引用" << endl; }
void Func(const int&& x) { cout << "const 右值引用" << endl; }
//万能引用
//实参为左值,t即为左值引用; 实参为右值,t即为右值引用
//实参为左值时,被称为引用折叠
template<class T>
void referenceTransmit(T&& t)
{
Func(t);
}
//完美转发,保持引用的原有属性
template<class T>
void perfectForward(T&& t)
{
Func(forward<T>(t));
}
void test()
{
int a = 10;
int b = a;
referenceTransmit(a); // "左值引用"
referenceTransmit(a + b); // "左值引用"
perfectForward(a); // "左值引用"
perfectForward(a + b); // "右值引用"
}
可变模板参数
在这里不追究参数包的底层实现细节,只做应用层面的讨论。
可变参数即是数量、类型不定的参数,例如C语言中的printf函数原型:
int printf( const char *format [, argument]... );
其中argument即可视为一个可变参数。在C++11中,引入了可变模板参数,又称参数包,使用template<class...Args>
声明参数包模板,其中Args为模板参数,使用Args...args
定义参数包args。参数包可以接受和包含>=0
个参数,可以使用递归拿出参数包中的内容,但是一般不这样做。
void cppPrint()
{
//当参数包为空时,调用这个重载函数
cout << "end";
}
template<class T, class...Args>
void cppPrint(T argu, Args...args)
{
//argu接收第一个参数,剩下的参数传给参数包
cout << argu << " "; //打印第一个参数
cppPrint(args...); //递归进行
}
void test()
{
cppPrint(1, "hello", 3, 'a'); //1 hello 3 a end
}
参数包中的参数可以与函数的的参数列表进行数量与类型匹配,具体使用情景见下文的emplace系列接口。
STL容器的新功能&接口
列表初始化
在C++98中,{ }只能用来初始化数组,在C++11中,一切皆可用{ }初始化。initializer_list和容器的相关构造函数是支持列表初始化的原因。在C++11中,几乎所有的容器都增加了initializer_list版本的构造函数,关于具体的初始化流程,已经在上文STL新容器部分进行讨论,这里不在赘述。
移动构造和移动赋值
在以往进行函数传参时,往往会用一个左值引用接收参数,目的是减少拷贝,提高效率,但是只能针对左值。假如现在有一个自定义类型的、需要进行深拷贝的类,必须进行传值返回,左值引用便不能派上用场。
class demo
{
public:
/*...构造函数...*/
demo(const demo& d)
:_a(d._a), _b(d._b)
{
cout << "demo(const demo& d), 深拷贝" << endl;
}
demo& operator=(const demo& d)
{
/*资源拷贝*/
cout << "demo& operator=(const demo& d), 深拷贝" << endl;
return *this;
}
private:
/*资源*/
};
demo getTmpObj()
{
demo d;
return d;
}
void test()
{
/*第一种情况*/
demo ret_1 = getTmpObj(); //demo(const demo& d), 深拷贝
/*第二种情况*/
demo ret_2;
ret_2 = getTmpObj(); //demo(const demo& d), 深拷贝 demo& operator=(const demo& d), 深拷贝
}
程序的输出为:
可见,对于第一种编译器优化的情况,需要进行1次深拷贝;对于第二种情况,需要进行两次深拷贝,这个结果对于追求极致效率的C++来说如眼中之钉。此时右值引用就可发挥作用。
在讨论右值引用如何生效之前,先对对象传值返回的过程做一讨论。对于第一种同一行连续两次拷贝构造的情况,编译器会优化为一次拷贝构造如下:
return 语句执行时,上图中的 d 对象在将数据深拷贝给 ret_1 后,getTempObj() 函数已执行完毕,d 对象会被编译器销毁,既然如此,就没有必要大费周折调用一次拷贝构造,将 d 的数据深拷贝给 ret_1,再将 d 对象销毁,而是直接进行资源转移,将 d 的资源转移给 ret_1 。对于大部分情况来说,资源转移只要交换指针即可,远远比深拷贝来的方便。
上述资源转移的工作,即是由移动构造(move constructor)来完成的。移动构造是一个构造函数,这个构造函数接收一个将亡的右值对象,将这个将亡对象的资源转交给当前对象以实现构造。
demo(demo&& d)
{
/*资源转移*/
cout << "demo(demo&& d), 移动构造" << endl;
}
移动构造能被调用的前提和原因是,编译器会将符合将死值特征的自定义类型左值识别为右值。
对于上面代码35行所示的第二种情况,它的调用情况如下:
有了移动构造后,第1次的深拷贝已被避免;对于第2次的赋值,与上述第一种情况同理,getTmpObj() 的返回值是一个将亡值,可以接收这个将亡值,将有效资源转交给ret_2。这个工作是由移动赋值(move assignment)来完成的:
demo& operator=(demo&& d)
{
/*资源转移*/
cout << "demo& operator=(demo&& d), 移动赋值" << endl;
return *this;
}
完成移动构造和移动赋值后,再次运行程序的结果为:
与第一次程序运行的结果相比,这次避免了三次深拷贝,对于一些大对象,可以节省大量的资源。
插入接口的右值引用版本
引入插入接口的右值引用版本,归根结底是为了解决当插入一个临时对象时,最终会调用构造函数进行一次多余的深拷贝的问题,例如有如下程序:
list<demo> l;
l.push_back(demo());
它的底层调用逻辑为:
在进行第3步时,问题如讨论移动构造和移动赋值时所说,val 是一个 demo 的临时对象,在第3步阶段,可以将其视为一个将亡对象,所以不必再大费周折调用一次 demo 的拷贝构造进行深拷贝。
为了顺利调用到 demo 的移动构造,需要引入插入接口的右值引用版本,同时为了保持 val 的右值属性,使用完美转发对右值进行传递:
emplace系列接口
emplace 系列接口不同于普通的插入接口,它接受的是一个参数包,并将这个参数包不断向下传递,直到遇到数据元素的构造函数,以参数包中的参数为参数直接调用元素的构造函数。使用emplace系列接口,可以避免一次拷贝构造。
有了移动构造和移动赋值之后,emplace系列接口的效率提高并非想象中的那么明显。
类的新功能
默认移动构造和默认移动赋值
C++11中,增加了两个类的默认成员函数:默认移动构造函数和默认移动赋值函数。当一个类中用户没有显式声明拷贝构造函数、且没有声明赋值运算符重载、且没有声明析构函数时(拷贝构造、赋值重载、析构往往是三位一体的,同时出现于需要进行深拷贝的类),编译器会自动生成一个默认移动构造函数。编译器生成的默认移动构造函数:
- 对内置类型进行浅拷贝;
- 对自定义类型,如果有移动构造,则调用其的移动构造函数,否则调用其的拷贝构造函数。
默认移动赋值的自动生成条件与默认移动构造相同。自动生成的默认移动赋值:
- 对自定义类型进行浅拷贝;
- 对自定义类型,如果其有移动赋值,则调用其移动赋值函数,否则调用其的赋值运算符重载。
default关键字
在类中,如果用户显式声明了某个默认成员函数,那编译器便不会自动生成该函数,但是在C++11中,可以使用default
关键字强制编译器生成默认成员函数。
class demo
{
public:
/*…………*/
demo() = default; //强制编译器生成默认构造函数
demo(const demo& d)
:_a(d._a), _b(d._b)
{
cout << "demo(const demo& d), 深拷贝" << endl;
}
/*…………*/
private:
/*…………*/
};
delete关键字
用delete修饰类的成员函数,可以使成员函数被禁用。delete 修饰成员函数的用法常见于设计模式,例如设计一个不能被拷贝的类如下:
class demo
{
public:
demo()
{ }
demo(const demo&) = delete;
demo& operator=(const demo&) = delete;
private:
};
delete同样可以避免某些情况下的类型转换,例如对下面对象的构造:
class demo
{
public:
demo(int a, int b)
{
cout << "demo(int a, int b)" << endl;
}
private:
};
int main()
{
demo(3.14, 5.18); //demo(int a, int b)
}
为了避免上述的类型转换,可以进行操作:
demo(double, double) = delete;
如此,上面14行的代码便不能通过编译。
lambda表达式
概述
C++11中新增了一个可调用对象,即lambda表达式。在此之前,C++的两个可调用对象,其一为函数指针,声明较为繁琐;其二为仿函数,在某些场景下对其定义显得较为繁重。lambda可以实现类似函数的功能,同时声明和定义更加简洁和轻量化,这便是选择lambda表达式的理由。lambda表达式主要针对一些简单的操作,一般在函数内部直接定义使用。
定义
一个完整的lambda表达式包括4个部分:捕捉列表、参数列表、返回值和语句。
[](int x, int y)->int { return x + y; }; //一个返回x + y的lambda表达式
参数列表的作用类似于函数的参数列表,特性也与函数的参数列表相同,当不需要接收参数时,参数列表可以省略。返回值类型用->
指定,编译器会根据 return 的返回值自动推导返回类型。语句的作用与函数中的语句相同,这里不再赘述。
在一个lambda表达式中,可以将其语句部分视为一个独立的块作用域,在这个块作用域中默认无法访问父作用域(包含lambda表达式的语句块)的变量,为了拿到父作用域中的变量,需要将指定变量名放入lambda表达式的捕捉列表中。捕捉列表有两种捕捉方式:值传递捕捉和引用传递捕捉。
值传递捕捉类似于函数的传值传参,采用值传递捕捉后,lambda中的变量是父作用域中变量的一份拷贝,且这份拷贝默认被const修饰,即在lambda中无法对这个变量进行修改。引用传递捕捉类似于函数的传引用传参,在lambda中可以对变量进行修改,且会影响父作用域中的对应变量。
[var] :表示值传递捕捉变量var
[=] :表示值传递捕捉父作用域中的所有变量
[&var] :表示引用传递捕捉变量var
[&] :表示引用传递捕捉父作用域中的所有变量
此外,捕捉列表可以由多个捕捉项组成,各捕捉项用逗号分隔:
[=, &var] :以引用传递捕捉变量var,以值传递捕捉其他所有变量
[&, var] :以值传递捕捉变量var,以引用传递捕捉其他所有变量
捕捉列表仅能捕捉父作用域中的局部变量,捕捉任何父作用域或者非局部变量都会导致编译报错。
调用
可以将lambda表达式理解为一个无名函数对象,一般会对其进行临时的直接调用,也可以将其赋值给一个变量,并用 auto 接收。
[](int a, int b) { return a + b; }(10, 20); //直接以匿名方式调用
auto sum = [](int a, int b) { return a + b; };
sum(10, 20); //使用变量sum进行调用
lambda表达式之间不能相互赋值,这是因为编译器会对lambda的命名进行随机编号,各个lambda表达式的类型都不相同。
类比仿函数
有如下代码:
struct sumFunc
{
int operator()(int a, int b)
{
return a + b;
}
};
void test2()
{
auto sum = [](int a, int b) { return a + b; };
sum(10, 20); //调用lambda表达式
sumFunc()(10, 20); //调用仿函数
}
从使用层面上看,labmda的使用方式与仿函数相似,不仅如此,通过观察汇编代码,也会发现编译器调用lambda的方式也与仿函数类似:
由此可知,底层对于lambda表达式的处理方式,其实就是完全按照仿函数的方式处理的,如果定义了一个lambda表达式,编译器便会自动生成一个类(仿函数),这个类中重载了operator()。
包装器
function包装器
至此,我们已经有了 3 种可调用对象:函数指针、仿函数和lambda表达式。毋庸置疑,这些可调用对象的类型是不同的,但是在使用中可能会有将这些可调用对象进行统一存储和使用的需求,function包装器便是为此而生。
function包装器是一种适配器模式,在C++中本质是一个类模板。function包装器可以对可调用对象进行包装,对可调用对象的类型进行统一,便于以一个统一的方式对可调用对象进行存储、调用。function的原型如下:
/*
param:
@Ret: 返回值类型
@Args: 被调用函数的形参包
*/
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
function可以对可调用对象进行包装:
int sumFunc(int a, int b)
{
return a + b;
}
struct sumClass
{
int operator()(int a, int b)
{
return a + b;
}
};
int main()
{
function<int(int, int)> f1 = sumFunc; //接收函数指针
function<int(int, int)> f2 = [](int a, int b) { return a + b; }; //接受lambda表达式
function<int(int, int)> f3 = sumClass(); //接收仿函数
cout << f1(10, 20) << " " << f2(10, 20) << " " << f3(10, 20) << endl;
return 0;
}
另一个实例:
unordered_map<string, function<int(int, int)>> operMap {
{"+", [](int a, int b) { return a + b; }},
{"-", [](int a, int b) { return a - b; }},
{"*", [](int a, int b) { return a * b; }},
{"/", [](int a, int b) { return a / b; }}
};
bind包装器
bind也是一种包装器,它可以接收一个可调用对象,并生成一个新的可调用对象来适应原对象的参数列表。bind 本身是一个函数模板,它的原型为:
/*
param:
@Fn: 接收的可调用对象
@Args: 适应原可调用对象的参数列表
*/
//simple(1)
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
//with return type (2)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
结合上面的函数模板原型,bind的一般调用形式为:auto newCallable = bind(callable,arg_list);
当调用 newCallable 时,newCallable 会调用callable,并将 arg_list 中的参数传给 callable。arg_list 中的参数可能包含形如_n
的名字,其中 n 是一个整数,这些参数是“占位符”,表示 newCallable的参数,它们占据了传递给 newCallable 的参数的“位置”。数值 n 表示生成的可调用对象中参数的位置:_1 为 newCallable 的第一个参数,_2 为第二个参数,以此类推。
一般用function包装器接收bind的返回值,或者用auto自动识别返回值类型。
int subFunc(int a, int b)
{
return a - b;
}
//接收一个新的可调用对象ff1
function<int(int, int)> ff1 = bind(subFunc, placeholders::_2, placeholders::_1);
cout << ff1(10, 20) << endl; //-10
四种类型转换
在C/C++中,大致分有两种类型转换:隐式类型转换和强制类型转换。隐式类型转换往往见于内置类型之间的转换,在C++中,也存在单参数构造函数的隐式类型转换和多参数构造函数的隐式类型转换。强制类型一般存在于不相关类型之间的转换,例如指针/整型之间的的类型转换。在C++中,编译器的类型检查更加严格,所以强制类型转换的使用会比C更频繁。
为了提供更安全的类型转换机制,C++11提出了四种类型转换规范。
static_cast
static_cast适用于相关类型之间的转换,一般是内置类型之间的转换,例如int/double之间的转换。使用格式为static_cast<aimType>(var);
其中aimType为目标类型,var为被转换变量。下文的其他类型转换规范也是这种用法。
double pi = 3.14159;
int a = static_cast<int>(pi); //将pi转换为int类型
reinterpret_cast
reinterpret_cast适用于不相关类型之间的转换,例如指针和整型之间的转换。使用这种类型转换规范时要注意谨慎处理,因为转换结果往往与平台和编译器有关。
int x = 10;
int* px = &x;
int y = reinterpret_cast<int>(px);
double* pd = reinterpret_cast<double*>(px);
const_cast
const_cast适用于将一个const类型的对象转换为非const类型,即去除对象的const属性的转换。
在C++中,去除变量的const属性是由风险的,例如下面的代码:
const int x = 10;
int* px = const_cast<int*>(&x);
*px = 20;
cout << "x = " << x << "; *px = " << *px << endl;
程序运行的结果是:x = 10; *px = 20
。这是因为在C++中,const变量会被编译器视为常量(const变量本身是一个变量),针对const变量,编译器会进行干预,将值存放在寄存器中,当使用const变量时,从寄存器中取值。为了避免上面的情况,可以使用volatile
修饰const变量,被volatile修饰的const变量,被使用时编译器会直接从地址中进行取值。
volatile const int x = 10;
int* px = const_cast<int*>(&x);
*px = 20;
cout << "x = " << x << "; *px = " << *px << endl; //x = 20; *px = 20
因此,使用const_cast时要格外小心。
dynamic_cast
dynamic_cast应用于继承体系的转换中,有两个作用,其一,它可以判断向下转换和向上转换:
class Base
{
public:
virtual void func()
{
cout << "Base:func()" << endl;
}
private:
int _a;
};
class Derive : public Base
{
public:
virtual void func()
{
cout << "Derive::func()" << endl;
}
virtual void testFunc()
{
cout << "Derive::testFunc()" << endl;
}
private:
int _b;
};
int main()
{
Base* pb1 = new Base;
Base* pb2 = new Base;
Derive* pd1 = new Derive;
Derive* pd2 = new Derive;
pd1 = dynamic_cast<Derive*>(pb1);
pb2 = dynamic_cast<Base*>(pd2);
cout << pd1 << endl; //nullptr
cout << pb2 << endl; //
return 0;
}
在这类场景中,如果是向上转换,则dynamic_cast正常返回子类指针;如果是向下转换,则返回nullptr。
其二,在多态应用中,dynamic_cast可以检验向下转换的合法性:
/*
Base与Derive的声明和继承关系同上
*/
void func(Base* p)
{
//检查向下转换的合法性
Derive* pd = dynamic_cast<Derive*>(p);
if (pd) { pd->testFunc(); }
else { cout << "null" << endl; }
}
int main()
{
func(new Derive); //Derive::testFunc()
func(new Base); //null
return 0;
}
在这类场景中,dynamic_cast 会检查实际传来的是否是合法的子类指针,如果是子类指针,则返回正常的转换后的结果,此时可以支持访问子类的其他成员;如果不是合法的子类指针,则dynamic_cast返回nullptr,避免非法访问。
智能指针
由于篇幅限制,智能指针会另起一篇文章进行详谈。