提高类型安全方面
强枚举类型“枚举类”
C++98:
在老标准的C/C++中,如果程序员希望为一些有意义的数值定义各自的名字,则经常使用三种方法:宏、枚举和静态常量。
使用宏的弱点在于其定义的只是在预处理阶段的名字,预处理结束后所有的名字都会被定义值替换。这在有的时候可能会干扰到正常代码(如果宏名与某变量名重定义)。
枚举定义的名字都是编译时期的名字,会得到编译器的检查,相比宏的实现,枚举不会干扰正常代码。
静态常量(const static type 变量名;)的名字同样得到编译时期检查,由于是静态常量,其名字作用域也被局限于文件内。相对于枚举,静态常量不仅仅是一个编译时期的名字,编译器还会为静态常量名在在目标代码中产生实际的数据,这会增加一点存储空间。
但是C/C++的枚举有一些缺陷:
(1)有名字的枚举类型的名字,以及枚举成员的名字是全局可见的,因此枚举是非强作用域类型。比如,以下定义会编译报错:
enum Type {A,B,C,D};
enum Test {B,C,D}
Type与Test中的成员命名重复。
(2)由于C中枚举被设计为常量数值的“别名”,所以枚举的成员总是可以被隐式地转换为整型。很多时候,这是不安全的。
(3)此外,枚举类型所占用的空间大小也是一个不确定量。标准规定,C++枚举所基于的“基础类型”是由编译器来具体制定实现的,这会导致枚举类型成员的基本类型的不确定性。
C++11:
C++11引入了一种新的枚举类型,即枚举类,又称强枚举类型。以此来解决非强类型作用域,允许隐式转换为整型,占用存储空间及符号性不确定问题。声明强类型枚举非常简单,只需要在enum后面加上关键字class。比如:
enum class Type {General,High,Low};
强类型枚举具有以下优势:
强作用域:强类型枚举成员的名称不会被输出到其父作用域空间。
转换限制:强类型枚举成员的值不可以与整形隐式转换。
可以指定底层类型:强类型枚举默认的底层类型为int,但也可以显示的指定底层类型。具体方法为在枚举名称后面加上“: type”,其中type可以是除wchar_t以外的任何整形,比如:
enum class Type :char {General,High,Low};
堆内存管理
显式堆内存管理
C/C++的程序在运行时经常会遇到类似运行时突然退出,或者占用内存越来越多,最后不得不重启的一些症状。这些症状的源头在于C/C++的显式堆内存管理。显式的处理堆内存如果处理不好会出现以下问题:
(1)悬垂指针(野指针):一些内存单元已被释放,之前指向它的指针却还在被使用。这些被释放的内存单元如果被系统重新分配给程序使用,会导致不可预知的错误。
(2)重复释放:程序试图去释放已经被释放的内存单元,或者释放已经被重新分配过的内存单元,就会导致重复释放错误。
(3)内存泄露:不再需要使用的内存单元没有被释放,导致内存泄露,导致内存占用增加。
改进的智能指针
显式堆内存管理在性能上有一定优势,但也是非常容易出错的。为此,C++98中在标准库中引入了智能指针。C++98中的智能指针通过一个模板类型”auto_ptr”来实现。auto_ptr以对象的方式管理堆内存分配,并在适当的时间(比如析构)释放所获得的内存。这种堆内存管理的方式只需要程序员将new操作返回的指针作为auto_ptr的初始值即可,程序员不用再显式调用delete。
auto_ptr有一些缺点(拷贝时返回一个左值,不能调用delete[]等),所以C++11中去除了auto_ptr;将智能指针进行了改进,改用unique_ptr、shared_ptr及weak_ptr等来自动回收堆分配对象,更加适合实际的应用需求。通过以下代码解释这几种智能指针的用法:
#include <iostream>
#include <memory>
using namespace std;
void check(weak_ptr<int> &wp) {
shared_ptr<int> sp = wp.lock(); //调用成员lock()可以返回指向内存的一个shared_ptr对象
if(sp != nullptr) //当该shared_ptr对象无效时返回指针空值nullptr
cout << "still" << *sp << endl;
else
cout << "pointer is invalid" << endl;
}
int main() {
//unique_ptr
unique_ptr<int> unip1(new int(1)); //构造不可复制的int型指针对象 unip1
//unique_ptr<int> unip2 = unip1; //不可复制,编译error
cout << *unip1 << endl; //unique_ptr<>重载了*操作符,可以使用*操作对象unip1,以得到内存值。
unique_ptr<int> unip3 = move(unip1); //使用move()将unip1改为右值,调用unique_ptr<>的移动构造函数,窃取unip1指向的内存。
cout << *unip3 << endl; //打印出窃取的内存值
//cout << *unip1 << endl; //unip1内存被窃取,使其指向nullptr,这里打印 将会运行时错误
unip3.reset(); //显示释放内存
unip1.reset(); //虽然内存已被窃取,但再次显式释放不会报错
//cout << *unip3 << endl; //unip3已经显式释放,这里会运行时错误
//shared_ptr
shared_ptr<int> sp1(new int(2)); //构造可以共享的int型指针独享sp1
shared_ptr<int> sp2 = sp1; //将sp1指向的内存分享给sp2,shared_ptr引用计数+1
cout << *sp1 << endl; //打印堆内存值
cout << *sp2 << endl; //打印堆内存值
sp1.reset(); //释放sp1指向的内存
cout << *sp2 << endl; //引用计数未清零,可以打印。
//weak_ptr
weak_ptr<int> wp = sp2; //weak_ptr可以指向shared_ptr,但并不拥有该内存
check(wp); //输出still 2
sp2.reset();
check(wp); //输出pointer is invalid
}
unique_ptr可以直接替代C++98中的auto_ptr,shared_ptr和weak_ptr用于需要引用计数的地方。
垃圾回收
C++11标准已经有定义,但目前没有编译器实现,暂时跳过。
提高性能及操作硬件方面
常量表达式“constexpr”
C++11新加入的常量表达式关键字constexpr。它与const不同,const表达的是运行时的常量性;而constexpr表达的是编译时常量性,允许一些计算发生在编译时。这是很大的优化:假如有些事情可以在编译时做,它将只做一次,而不是每次程序运行时。比如:
<pre name="code" class="cpp">constexpr int multiply (int x, int y) {
return x * y;
}
// 将在编译时计算
const int val = multiply( 10, 10 );
除了编译时计算的性能优化,constexpr的另外一个优势是,它允许函数被应用在以前调用宏的所有场合。例如,你想要一个计算数组size的函数,size是10的倍数。如果不用constexpr,你需要创建一个宏或者使用模板,因为你不能用函数的返回值去声明数组的大小。但是用constexpr,你就可以调用一个constexpr函数去声明一个数组。
constexpr int getDefaultArraySize (int multiplier) {
return 10 * multiplier;
}
int my_array[ getDefaultArraySize( 3 ) ];
但是constexpt函数必须遵守一些规则:
(1)
函数体只有单一的return语句(但允许包含typedefs、 using declaration && directives、静态断言等);
(2)
函数必须有返回值;
(3)
在使用前必须已有定义;
(4)
Return返回语句表达式中不能使用非常量表达式的函数、全局数据,且必须是一个常量表达式。
一个声明为constexpr的函数同样可以在运行时被调用,当这个函数的参数是非常量的。比如:
int n,m;
cin >> n >> m;
multiply(n,m);
这意味着你不需要分别写运行时和编译时的函数。
Constexpr支持编译时使用对象,假设有下面一个类:
class Circle
{
public:
Circle (int x, int y, int radius) : _x( x ), _y( y ), _radius( radius ) {}
double getArea () const
{
return _radius * _radius * 3.1415926;
}
private:
int _x;
int _y;
int _radius;
};
如果希望在编译期像下面一样构造一个Circle,接着算出他的面积:
constexpr Circle c( 0, 0, 10 );
constexpr double area = c.getArea();
可以给Circle类做一些小的修改以完成这件事。首先,我们需要将构造函数声明为constexpr,接着我们需要将getarea函数声明为constexpr。将构造函数声明为constexpr则运行构造函数在编译期运行,只要这个构造函数的参数为常量,且构造函数仅仅包含成员变量的constexpr构造(所以默认构造可以看成constexpr,只要成员变量都有constexpr构造)。
class Circle
{
public:
constexpr Circle (int x, int y, int radius) : _x( x ), _y( y ), _radius( radius ) {}
constexpr double getArea ()
{
return _radius * _radius * 3.1415926;
}
private:
int _x;
int _y;
int _radius;
};
但要注意,常量表达式的构造函数有两点约束:
(1)
构造函数函数体必须为空;
(2)
初始化列表只能由常量表达式来赋值。
变长模板(略)
略。
原子类型与原子操作(略)
略
快退快出“quick_exit”与“at_quick_exit”
在C++程序中,常常会看到一些有关“终止”的函数,如terminate、abort、exit等。它们是有很大区别的,原因是它们对应的是“正常退出”和“异常退出”两种情况。
对于terminate函数,它实际上是C++中异常处理的一部分,在<exception>头文件里。一般而言,没有被捕捉的异常就会导致terminate函数的调用。如果用noexcept关键字修饰的函数抛出了异常,也会调用terminate函数。直观来说,只要程序中出现了非程序员语气行为,都有可能导致terminate的调用。而terminate函数在默认情况下,是去调用abort函数的。不过用户可以通过set_terminate函数来改变默认行为。因此,在C++层面,terminate就是“终止”。
对于abort函数,它源自于C中的<cstdlib>头文件中,更加底层。Abort函数不会调用任何析构函数,同样terminate也不会。它默认情况下,会想合乎posix标准的系统抛出一个信号:SIGABRT。操作系统会执行signal handler,默认释放掉进程的所有资源,从而终止程序。倘若被终止的程序进程与其它程序有一些交互,本程序意外终止可能会导致这些交互进程处于一些中间状态。
对于exit函数,它属于正常退出的程序终止,不太可能会有以上的问题。exit函数会正常调用自动变量的析构函数,并且还会调用atexif注册的函数。这跟main函数结束时的清理工作是一样的。比如:
#include <cstdlib>
#include <iostream>
using namespace std;
void openDev() { cout << "device is opened." << endl;}
void resetDevStat() { cout << "device stat is reset." << endl;}
void closeDev() { cout << "device is closed."<< endl;}
int main() {
atexit(closeDev);
atexit(resetDevStat);
openDev();
exit(0);
}
输出结果为:
device is opened.
device stat is reset.
device is closed.
可以看出,使用atexit注册了两个函数resetDevStat和closeDev,在程序调用exit时,所有注册的函数按照相反的注册顺序被调用。Exit和atexit函数来自于C,通过两者的配合可以灵活的处理进程的清理工作。
但是,对于C++代码中会有很多类,这些类在堆空间分配了大量的零散内存,而main或者exit函数调用会导致类的析构函数一次将这些零散的内存还给操作系统,这是一件费时的工作。而实际上,这些堆内存将在进程结束时由操作系统统一回收,操作系统会释放掉一些进程相关的数据结构,然后将一些物理内存标记为未使用,这样操作非常快。如果这些堆内存对其他程序不产生任何影响,那么在程序结束时释放堆内存的析构过程往往是没有意义的。因此这种情况下往往希望快速的退出程序。
另外,在多线程情况下,要使用exit函数来退出程序的话,通常需要向线程发出一个信号,并等待县城结束后再执行析构函数、atexit注册的函数等。这从语法上很正确,但这样的退出方式有时候并不能像预期那样工作,比如线程中的程序在等待I/O运行结束等。在一些更为复杂的情况下,可能还会遭遇到一些因为信号顺序而导致的死锁状况。一旦出现这样的问题,程序往往会卡死而无法退出。
为此,C++引入了quick_exit函数,该函数并不执行析构函数而只使程序终止,并且与exit同属正常退出。此外使用at_quick_exit注册的函数(至少32个)也可以在quick_exit的时候被调用。这样一来,我们同样可以像exit一样做一些清理工作。比如:
#include <cstdlib>
#include <iostream>
using namespace std;
class A {
public:
~A() {
cout << "destruct A" << endl;
}
};
void closeDev() {
cout << "dev is closed." << endl;
}
int main() {
A a;
at_quick_exit(closeDev);
quick_exit(0);
}
正确的话,A的析构函数将不会被调用。
其它
nullptr
先来看一下NULL宏的定义:
#undef NULL
#if defined(_cplusplus)
#define NULL 0
#else
#define NULL ((void*)0)
#endif
从代码中可以看到NULL可以是0或者是((void*)0),无论哪种都会出现这样那样的麻烦,所以在c++11中就出现了nullptr这个强类型。
void f(int); //#1
void f(char *);//#2
//C++03
f(0); //二义性
//C++11
f(nullptr) //无二义性,调用f(char*)
在c++11用,我们要用nullptr来初始化指针。
类中默认函数的显式缺省与显式删除
所谓类中的默认函数为,编译器会默认帮助生成未定义的成员函数,比如默认构造函数,拷贝构造函数,赋值操作符,移动构造函数,移动赋值操作符,析构函数等。C++11中通过重新使用default关键字来控制默认版本函数的生成,从而显式的指示编译器生成该函数的默认版本。使用时直接在默认函数的定义或者声明时加上"=default"。
同时,C++11还使用delete关键字来控制默认函数的删除,即在函数的定义或者声明时加上"=delete",则编译器不会默认生成该默认函数。
Lambda表达式
C++11引入了lambda表达式,使得程序员可以定义匿名函数,该函数是一次性执行的,既方便了编程,又能防止别人的访问。
Lambda表达式具体形式如下:
[capture](parameters) mutable ->return-type{body}
其中:
[capture]:捕捉列表,捕捉列表总是出现在lambda表达式的开始出。[]为lambda引出符。编译器根据该引出符判断接下来的代码是否是lambda表达式。捕捉列表中的捕捉项可以视为表达式的初始状态。Lambda表达式的运算是基于初始状态的运算,这与函数简单基于参数的运算有所不同。语法上捕捉列表由多个捕捉项组成,并以逗号分隔。Lambda表达式可以使用捕捉列表引用在它之外声明的变量(父作用域). 这些变量的集合叫做一个闭包. 这个机制允许这些变量被按值或按引用捕获.
捕捉列表有以下几种形式:
[] //未定义变量.试图在Lambda内使用任何外部变量都是错误的.
[x, &y] //x 按值捕获, y 按引用捕获.
[&] //用到的任何外部变量都隐式按引用捕获
[=] //用到的任何外部变量都隐式按值捕获
[&, x] //x显式地按值捕获. 其它变量按引用捕获
[=, &z] //z按引用捕获. 其它变量按值捕获
(parameters):参数列表。与普通 函数的参数列表一致。如果不需要参数传递,则可以连同()一起省略。
Mutable:mutable修饰符。默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。在使用该修饰符时,参数列表不可省略,即使参数为空。
->return-type:返回类型。用追踪返回类型形式声明函数的返回类型。不需要返回值的时候可以连同符号->一起省略。此外,在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行推导。
{body}:函数体。内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量。
因此再lambda函数的定义中,参数列表和返回类型都是可选部分,而捕捉列表和函数体都可能为空。C++中最简单的lambda表达式只需要声明为:[]{}。