explicit与隐式类型转换
在调用函数时,如果传给函数的参数和任何一个已经声明的同名函数都不匹配,那么c++编译器会想方设法地尝试各种类型转换以使函数调用能够成立,这其中包括我们用类型转换操作符显式定义的类型转换和利用单参数构造函数来隐式进行类型转换,其中后者往往是不符合预期的,可能导致不知不觉中引入bug。因此通常在声明构造函数时,会使用explicit来进行修饰,以告诉编译器不要尝试使用该构造函数进行隐式的类型转换。
C/C++的宏与字符串化
在宏定义中可以使用连续的两个井号来粘连两个字符串使其称为一个新的字符串;此外还可以在宏参数前使用单个的井号来引用宏参数内容字符串:
#define DEFINE_METHOD(methodName)\
void test##methodName() {\
cout << "this is " << #methodName << endl;\
}
使用gcc时,可以使用-E来查看展开后的结果。
链接
链接分为两种,分别是内部链接(internal linkage)和外部链接(external linkage)。
在一个程序中,每个变量的存储空间的位置和大小都必须被确定。编译器根据诸如变量的类型和其所在的作用域这样的信息来确定变量的存储空间。
内部链接只为当前正在被编译的文件创建存储空间,不同的文件可以声明相同名字的变量,但编译器不会理会当前文件以外的变量定义。声明变量时使用static关键字来将变量的链接规则指定为内部链接。
外部链接创建一片存储空间,这块存储空间包含了所有文件中的外部变量。在声明变量时,如果不加static关键字,变量就会被声明并定义为外部链接变量。使用extern声明变量表示在当前文件中使用了一个外部变量,并告诉编译器某个名字的变量存在于其它文件中,避免编译器在编译当前文件时抱怨找不到这个名字的变量的定义。
常量
从前,在C中,定义常量必须使用预处理:
#define PI 3.14159
预处理定义常量有很多问题,比如PI并没有具体的类型,也不是一个变量,既无法在使用到PI的地方进行类型检查,也无法获取PI的地址,而且PI的作用域是整个文件。
为了更清晰的常量语义,C++引入了命名常量的概念,并将其加入了C的标准。命名常量就是一个禁止改变值的变量。
C++规定,在定义const常量时,必须同时指定常量的值。
volatile关键字
定义变量时使用volatile关键字表示该变量的值随时可能被(非当前线程中执行的代码)改变,每次编译器读取该关键字修饰的值之前,都必须从该变量所在内存地址中读取,禁止了编译器的一些优化行为。多线程编程对可能被其它线程修改的变量可能使用该关键字,硬件编程中如果将某些变量映射到硬件寄存器地址,也需要使用该关键字以告诉编译器每次都从变量实际地址读取变量值。
类型转换
用一对括号括起来的类型是C中的经典转换运算符,通常也叫做强制类型转换。使用强制类型转换,强迫编译器将某个变量当作另一个类型而不进行任何检查,错误使用强制类型转换可能导致程序bug。而且,强制类型转换的括号很容易和程序的其它代码混在一起,不便于检查程序中哪些地方使用了强制类型转换。
C++引入了新的类型转换语法,分别是static_cast
,const_cast
, reinterpret_cast
, dynamic_cast
。
static_cast
用于明确定义的变换。能够使用static_cast的地方都是可以不加static_cast直接依赖编译器的隐式类型转换的,只不过编译器可能会对隐式类型转换发出警告,使用static_cast显示指定类型转换告诉编译器我们的确想要进行类型转换,从而抑制警告。可以用static_cast<void *>将一个指针转换为无类型指针,也可以使用static_cast<int>将一个long类型变量转换为int型变量。这两种转换都可以由编译器隐式进行,使用static_cast抑制了编译器对可能导致信息丢失的隐式类型转换的警告。
const_cast
用于将const变量或者volatile变量转换为普通变量。仅此而已。
reinterpret_cast
,重解释转换,是最不安全的一种转换,这种转换也是C风格强制类型转换不受欢迎的主要原因,是最低级的位级别的转换,将一个变量强制看作是另一个类型。在Android Framework源码中,经常通过static_cast将一个C++变量地址赋给java中的long变量,随后再拿到long变量并将其赋给void *指针,然后通过reinterpret_cast将无类型指针转换回这个指针指向的变量的真正类型。这里只能用reinterpret_cast来完成,但正常的C++编程中应该不需要用到这种类型转换。
dynamic_cast
是类型安全的向下转换。C++中向上类型转换是安全的,因此不需要显示指定转换。但是向下类型转换是不安全的,在必须进行向下类型转换时,使用dynamic_cast,可以让编译器检查对象的实际类型是否和要转换的类型匹配,只有当匹配时才会将执行转换并返回转换后对象的指针,如果不匹配,就会返回0。但是,dynamic_cast由于需要进行额外的检查(事实上dynamic_cast使用了RTTI特性),大量的dynamic_cast可能导致性能问题。如果能够确定(通常并不能确定)向下类型转换的类型一定匹配,可以使用static_cast进行无检查的向下类型转换。
句柄类和实现隐藏技术
C++中类定义往往定义在头文件中,和实现分离,这样的C++库在提供给其他人使用时,只要提供编译好的库文件和头文件即可,这样可以避免使用方看到不必要的细节,而且在实现变更时,使用方可以只更新库文件而不用同时更新头文件。但是,类在定义时,需要同时定义private方法,即使这些方法不应该被使用库的程序员了解。这是因为C++设计规定类只能在一个地方定义,并且必须一次定义完整的类。
为了将不希望用户看到的功能隐藏起来,需要使用一种特殊的“实现隐藏”技巧,绕过C++的这个限制:
# 头文件声明ClassImpl但不定义
# PublicClass.h
class PublicClass {
public:
void get();
void post();
private:
struct ClassImpl;
ClassImpl* impl;
}
# 实现文件再定义并实现ClassImpl
# PublicClass.cpp
struct PublicClass::ClassImpl {
...
}
RTTI
RTTI全称是RunTime Type Information,即运行时类型信息,指的是程序运行时保留每个对象的类型信息。C++中的dynamic_cast和typeid运算符都是通过RTTI实现的。启用了RTTI就可以使用在<typeinfo>中定义的函数,在运行时查询对象的类型信息了,
C++的异常
C++标准提供了异常,但很多人都不喜欢C++中的异常,因此不使用它,gcc可以使用-fno-exception来禁止使用异常,指定了该选项后,源码中如果使用了异常就会报错。
可以在任何地方使用throw关键字抛出异常,可以使用try {…} catch (exceptionType e) {…}来捕获异常,catch可以捕获在try块中抛出的和exceptionType类型匹配的异常。可以将exceptionType写成...来捕获所有类型的异常。
RAII
RAII全称是Resource Acquisition Is Initialization,是一种编程惯例,用于解决C++异常导致的资源管理问题。
RAII要求资源的有效期与对象的生命期严格绑定,对象的构造函数完成资源的获取,析构函数完成资源的释放。这样,只要对象能够被正确地释放,就不会出现资源泄漏问题。
C++标准库提供的类如文件的输入输出流都是按照RAII实现的,因此通常不需要显式调用关闭流函数,一旦流对象被析构,流使用的文件就会被析构函数关闭。但是在使用C的文件描述符时,需要手动关闭文件描述符,如果希望C的文件描述符也能够被自动地关闭,就需要使用RAII的一个变种RRID(Resource Release Is Destruction)方式管理对象:实现一个能够指定析构函数的对象,在文件描述符打开时同时在栈上创建一个这样的对象,将析构函数指定为关闭文件描述符,这样,由于文件描述符同这个特殊的对象处于同一个作用域,特殊对象会和文件描述符一同失效,其析构函数将保证文件描述符在同时被关闭。