- 第十九章 特殊工具与技术
第十九章 特殊工具与技术
19.1 控制内存分配
19.1.1 重载new和delete
-
new实际上执行了三步操作:
-
- 调用operator new(或者operator new[]),分配一块足够大的、原始的、未命名的内存空间
-
- 编译器运行相应的构造函数,并传入初始值
-
- 返回指向该对象的指针
-
-
delete实际执行了两步操作:
-
- 对所指向的对象执行析构函数
-
- 调用operator delete(或者operator delete[])释放内存空间
-
标准库定义了8个重载版本,4个可能抛出异常,4个不抛出,可以在全局作用域或类作用域中自定义函数版本中的任意一个。当定义为类成员时,它们是隐式静态的。
如果自定义operator new函数,可以提供额外的形参,但是时候时候必须使用new的定位形式
对于operator delete返回类型必须是void,第一个形参必须是void*
malloc函数与free函数
在头文件cstdlib头文件中。
malloc接收一个表示待分配字节数的size_t,返回指向分配空间的指针或者返回0以表示分配失败。 free函数接受一个void*,他是malloc返回的指针的副本,free将内存返回给系统。
19.1.2 定位new表达式
分离开内存分配与初始化,可以调用operator new 和 operator delete,它们负责分配和释放内存空间,不会构造和销毁对象。
new (place_address) type
使用定位new形式构造对象,可以使用定位new传递一个地址。
调用析构函数会销毁对象,但是不会释放内存空间
19.2 运行时类型识别
RTTI 运行时类型识别由两个运算符实现:
- typeid运算符,返回表达式的类型
- dynamic_cast运算符,将基类的指针或引用安全地转换成派生类的指针或引用
19.2.1 dynamic_cast运算符
//type是类类型
dynamic_cast<type*>(e)
dynamic_cast<type&>(e)
dynamic_cast<type&&>(e)
指针类型的dynamic_cast
如果转换目标是指针类型并且失败了,返回0
在条件部分执行dynamic_cast操作可以确保类型转换和结果检查在同一条表达式中完成。
引用类型的dynamic_cast
如果转换目标是引用并且失败了,程序抛出bad_cast异常,该异常定义在typeinfo标准库头文件中。
19.2.2 typeid运算符
typeid(e)
,e可以是任意表达式或类型的名字,操作结果是一个常量对象的引用,类型是type_info或者其公有派生类型,type_info在typeinfo头文件中。
如果e是一个引用,则返回该引用所引用对象的类型,如果作用域数组或函数,不会执行类型转换。
当运算对象不属于类类型或者是一个不包含任何虚函数的类时,typeid运算符指示的是运算对象的静态类型,而当运算对象是定义了至少一个虚函数的类的左值时,typeid的结果直到运行时才会求得。
使用typeid运算符
比较两表达式类型是否一致
当作用于指针时(不是指向的对象),返回的结果是该指针的静态编译类型
19.2.3 使用RTTI
首先要明确:如果参与比较的两个对象类型不同,则比较结果是false。
定义一个虚函数比较两个对象是否相等,派生类要定义自己的比较函数,执行对象转换成运算符所属的类类型。
19.2.4 type_info类
定义在头文件typeinfo中,创建type_info对象的唯一途径是使用typeid运算符,其name成员返回C风格字符串表示对象类型的名字,返回的字符串根据编译器不同而返回不同名字,不一定与在程序中使用的名字一致。
19.3 枚举类型
枚举类型将一组整形常量组织在一起,枚举属于字面值常量类型。
两种枚举:限定作用域的 和 不限定作用域的
//定义限定作用域的枚举类型,也可使用struct
enum class open {input, output, append};
//定义不限定作用域枚举类型,省略class或struct,名字可要可不要
enum color {red, blue, green};
enum {f = 3, c = 2};//如果不要名字,则只能在定义enum时定义其对象。
枚举成员
- 限定作用域的枚举类型:枚举成员的名字遵循常规的作用域准则,并且在枚举类型的作用域外是不可访问的。
- 不限定作用域的枚举类型:枚举成员的作用域与枚举类型本身的作用域相同。
- 对于限定作用域的,要使用作用域运算符,例如
open n = open::input;
- 默认情况下,枚举值从0开始依次加1,我们也可以自己制定成员的专门值,如果没有显式提供初始值,当前枚举值等于之前枚举成员的值加1
- 枚举成员是const,必须用常量表达式初始化
- 只要enum有名字,我们就能定义并初始化该类型的成员
- 一个不限制作用域的枚举类型的对象或枚举成员自动地转换成整型,我们可以在需要整型值的地方使用。
指定enum的大小
可以在enum名字后加上冒号以及想使用的类型enum v : unsigned long long {/* */}
。如果没有指定enum的潜在类型,限定作用域的enum成员类型是int,不限定作用域的成员没有默认类型。
如果指定了潜在类型(包括限定作用域的隐式指定),如果枚举成员超过该值,则引发错误
枚举类型的前置声明
使用前置声明必须指定其成员的大小enum v : unsigned long long;
限定作用域的enum成员大小可以不指定,隐式被定义为int
不能定义同名的限定作用域enum和不限定作用域的enum
形参匹配域枚举类型
初始化enum对象,必须使用该enum类型的另一个对象或它的一个枚举成员。即使某个整型值恰好和枚举成员相等,也不能作为函数的enum实参
可以将一个不限定作用域的枚举类型的对象或枚举成员传给整形形参,此时enum的值提升为int或更大的整形。
19.4 类成员指针
成员指针是指可以指向类的非静态成员的指针。
19.4.1 数据成员指针
- 声明:使用*表示指针,成员指针还需要包含成员所属的类,必须在*之前添加类名和作用域运算符::
- 初始化:指定它所指的成员,可以令其指向非特定类对象的成员,而是类的成员
- 声明成员指针的简单方法是使用auto和decltype
const string ClassName::* p;
p = &ClassName::data;
//使用auto
auto p = &Classname::data;
使用数据成员指针
我们初始化一个成员指针或为成员指针赋值时,该指针并没有指向任何数据,成员指针制定了成员而非该成员所属的对象,只有当解引用成员指针时我们才提供对象的信息。
使用.* 或是 ->*来解引用指针并获得该对象的成员
Screen myScreen, *pScreen = &myScreen;
auto s = myScreen.*p; //使用.*
s = pScreen->*p; //使用->*
返回数据成员指针的函数
一般成员函数是私有的,通常定义一个函数返回指向该成员的指针。
const string ClassName::* get() { return &ClassName::data; }
19.4.2 成员函数指针
可以定义指向类的成员函数的指针,最简单的方法是使用auto来推断类型
auto pmf = &ClassName::get;
char (ClassName::*pmf2)(ClassName::pos, ClassName::pos) const; //pmf2的类型是指向一个这样的成员函数的指针
pmf2 = &ClassName::get; //指向get函数
使用成员函数指针
使用.* 或是 ->*来解引用指针并获得该对象的成员函数
括号必不可少(myScreen.*pmf2)();
或(myScreen->*pmf2)();
使用成员指针的类型别名
使用类型别名方便理解成员指针,成员函数指针可以作函数参数和返回类型
using action = char (ClassName::*)(ClassName::pos, ClassName::pos) const;
成员指针函数表
如果一个类函数几个校内沟通类型的成员,则将其指向成员函数的指针存入一个函数表,使用的时候从函数表中选择一个。
19.4.3 将成员函数用作可调用对象
要想通过一个指向成员函数的指针进行函数调用,必须首先利用.*和 ->*将该指针绑定到特定的对象上。 因此与普通的函数指针不同,成员指针不是一个可调用对象,不支持函数调用运算符。
使用function生成一个可调用对象
需要用户显式提供成员的调用形式
vector<string*> p;
function<bool (const string&)> fc = &string::empty;
find_if(p.begin(), p.end(), fc);
// 我们告诉function:empty是一个接收string参数并返回bool值的函数。我们可以认为在find_if中使用调用运算符的部分转换成了使用.*运算符来处理指向成员函数的指针。
使用mem_fn生成一个可调用对象
定义在functional头文件中。可以从成员指针生成一个可调用对象,编译器负责推断成员的类型,无需用户显示指定。
find_if(p.begin(), p.end(), mem_fn(&string::empty));
auto f = mem_fn(&string::empty);
f(*p.begin()); //通过对象调用,使用.*调用empty
f(&p[0]); //通过指针调用,使用->*调用empty
mem_fn生成的可调用对象可以通过对象调用,也可以通过指针调用
使用bind生成一个可调用对象
find_if(p.begin(), p.end(), bind(&string::empty, _1));
auto f = bind(&string::empty, _1);
f(*p.begin()); //通过对象调用,使用.*调用empty
f(&p[0]); //通过指针调用,使用->*调用empty
bind生成的可调用对象可以通过对象调用,也可以通过指针调用
19.5 嵌套类
一个定义在另一个类内部的类称为嵌套类或嵌套类型,嵌套类常用于定义作为实现部分的类。
- 嵌套类是一个独立的类,与外层类基本没什么关系
- 嵌套类的名字在外层类作用域中是可见的,在外层类作用域之外不可见
- 外层类对嵌套类的成员没有特殊的访问权限,嵌套类对外层类的成员也没有特殊权限
- 嵌套类也使用访问运算符控制外界对其成员的访问权限
- 嵌套类在其外层类中定义了一个类型成员,该类型的访问权限由外层类决定
声明一个嵌套类
在类中声明嵌套类class ClassName
在外层类之外定义一个嵌套类
class OutClass::InClass { /**/};
要指明外层类,该嵌套类属于外层类,可以直接使用外层类的成员,不用加限定符。
定义嵌套类的成员
//定义嵌套类的析构函数
OutClass::InClass::InClass { /**/};
嵌套类的静态成员定义
int OutClass::InClass::data = 3;
嵌套类作用域中的名字查找
因为嵌套类是外层类的一个成员,所以外层类的成员可以直接使用嵌套类的名字,名字查找规则同样适用于嵌套类,嵌套类本身是一个嵌套作用域,因此还要查找嵌套类的外层作用域。
嵌套类和外层类是相互独立的
嵌套类的对象只包含嵌套类定义的成员;外层类的对象只包含外层类的定义的成员
19.6 union:一种节省空间的类
联合 是一种特殊的类。一个union可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值,分配给一个unino对象的存储空间至少要能容纳它的最大的数据成员。
- union不能含有引用类型的成员
- union的成员可以指定访问限定符,默认情况union的成员都是公有的public
- union可以定义构造、析构函数,union不能继承自其他类,也不能作为基类使用,所以union中不能包含有虚函数。
定义union
union T {
char a;
int c;
double d;
};
//使用
T fir = { 'f' };//初始化,默认初始化第一个成员
T sec; //未初始化的T对象
T* p = new T; //指向未初始化的T对象的指针
//使用成员访问运算符访问union对象成员
sec.a = 'z';
p->a = 32;
//为union的一个数据成员赋值会令其它数据成员变成未定义状态
匿名union
匿名union是一个未命名的union,匿名union不能包含受保护的成员或私有成员,也不能定义成员函数。
匿名union的成员可以直接访问。
含有类类型成员的union
当我们将union的值改为类类型成员的对应的值时,必须运行该类型的构造函数;反之,当我们将类类型成员的值改为一个其他值时,必须运行该类型的析构函数。
当union包含的是内置类型成员,则我们使用普通的赋值语句改变union保存的值,同时编译器会按照成员次序依次合成默认构造函数或拷贝控制成员。
使用类管理union成员
union构造或销毁类类型的成员必须执行非常复杂的操作,因此我们通常把含有类类型成员的union内嵌在另一个类中。
为了追踪union中到底存储了什么类型的值,我们通常会定义一个独立的对象,该对象称为union 的判别式。可以创建个枚举类型。
19.7 局部类
类可以定义在某个函数的内部,称为局部类。局部类定义的类型只在定义它的作用域内可见。局部类所有成员必须完整定义在类的内部。
- 局部类中不允许声明静态变量
- 局部类不能使用函数作用域中的变量
- 局部类对其外层作用域中的名字的访问权限受到很多限制,局部类只能访问外层作用域定义的类型名、静态变量和枚举成员。 如果局部类定义在某个函数内部,则该函数的普通局部变量不能被该局部类使用。
- 常规的访问保护规则对局部类同样适用。
- 嵌套的局部类,可以在局部类的内部再嵌套一个类,此时这个嵌套类也是局部类。
19.8 固有的不可移植的特性
不可移植的特性是指因机器而异的特性
算数类型的大小在不同机器上不一样,这是典型的不可移植特性的例子。
19.8.1 位域
类可以将其非静态数据成员定义成位域,在一个位域中含有一定数量的二进制位,当程序需要向其他程序或硬件设备传递二进制数据时,通常会用到位域。
位域在内存中的布局时与机器相关的。
位域的类型必须是整形或者枚举类型。
位域的声明形式:在成员名字之后紧跟一个冒号以及一个常量表达式,通常使用无符号类型保存一个位域,常量表达式用于指定成员所占的二进制位数。unsigned int mode : 1
如果可能的话,在类内部连续定义的位域压缩在同一个unsigned int中,这些二进制位能否压缩以及如何压缩与机器相关。
取地址运算符不能作用于位域,因此任何指针都无法指向类的位域。
使用位域
- 访问位域的方式与访问类的其他数据成员的方式非常类似。
- 通常使用内置的位运算符操作超过1位的位域。
19.8.2 volatile限定符
volatile的确切含义与机器有关,只能通过阅读编译器文档来理解。使用了volatile的程序在移植到新机器或新编译器后仍然有效,通常需要对程序进行某些改变。
直接处理硬件的程序常常包含这样的数据元素,它们的值由程序直接控制之外的过程控制,当对象的值可能在程序的控制或检测之外被改变时,应该将该对象声明为volatile,关键字volatile告诉编译器不应该对这样 的对象进行优化。
- const 和 volatile用法相似,而且两者可以同时使用
- volatile和指针的关系和const和指针的关系类似
- 类的成员函数也能够定义为volatile,只有volatile的成员函数才能被volatile的对象调用
- 只能将一个volatile对象的地址(或拷贝一个指向volatile的指针)赋给一个指向volatile的指针
- 只有当某个引用时volatile的时,才能使用一个volatile对象初始化该引用
- 合成的拷贝对volatile对象无效,因为合成的成员接收的形参类型是常量引用(非volatile的)。如果想使用,那必须自定义拷贝或移动操作,可以将其形参类型指定为volatile const 的
19.8.3 链接指示:extern "C"
C++程序有时候调用其他语言编写的函数,其他语言中的函数名字也要在C++中进行声明,并且指定返回类型和形参列表。C++使用链接指示指出任意非C++函数所用的语言。
要想C++代码和其他语言放在一起使用,我们必须有权访问该语言的编译器,并且这两个编译器兼容。
声明一个非C++函数
链接指示可以有两种形式:单个的 或 复合的
链接指示不能出现在类定义或函数定义的内部
//声明方式:extern后加上字符串字面值常量以及一个函数声明,字面值常量表示编写函数所用的语言。
extern "C" size_t strlen(const char*); //单个的
//复合 的
extern "C" {
int strcmp();
char *strcat(char*);
}
链接指示与头文件
extern "C" {
#include <string.h>
}
如果花括号内是#include,则头文件中所有的普通函数声明都被认为是由链接指示的语言编写的,链接指示可以嵌套
指向extern "C"函数的指针
指针必须与函数本身具有相同的链接指示
extern "C" void (*pf)(int);
//pf指向一个C函数,该函数接收int返回void
链接指示对整个声明都有效
链接指示对函数有效,对作为函数的返回类型或形参类型的函数指针也有效
extern "C" void f1(void(*)(int));
//f1是C函数,它的参数是一个函数指针也是C函数
如果希望给C++函数传入一个指向C函数的指针,则必须使用类型别名
extern "C" typedef void FC(int);
void f2(FC *);
//f2是C++函数,参数是指向C函数的指针
导出C++函数到其他语言
可以使用链接指示对函数进行定义,可以令一个C++函数在其它语言编写的程序中可用:
extern "C" double cal(double p) { /*...*/ }
//这个包含了定义,不只是声明哦
//这个函数可以被C程序调用
//但是返回类型和参数类型受到很多限制
对链接到C的预处理器的支持
有时候需要在C和C++中编译同一个源文件,编译C++文件时候预处理器定义__cplusplus ,利用这个变量,可以在编译c++时候有条件地包含进来一些代码
//使用如下代码,编译C++时候就会加上extern "C"
#ifdef __cplusplus
extern "C"
#endif
int strcmp(const char*, const char*);
重载函数与链接指示
如果目标语言支持重载函数,则为该语言实现链接指示的编译器很可能也支持重载这些C++的函数
C语言不支持函数重载,一个C链接指示只能用于说明一组重载函数中的某一个。