第十九章 特殊工具与技术
控制内存分配
// new 表达式
string* sp = new string("a value"); // 分配并初始化一个 string 对象
string* arr = new string[10]; // 分配 10 个默认初始化的 string 对象
实际执行了三步步骤。
new
表达式调用一个名为operator new( 或者 operator new[] )
的标准库函数。该函数分配一块足够大的、原始的、未命名的内存空间以便存储特定类型的对象( 或者对象的数组 )。- 编译器运行相应的构造函数以构造这些对象,并为其传入初始值。
- 对象被分配了空间并构造完成,返回一个指向该对象的指针
当用 delete
表达式删除一个动态分配的对象时
delete sp; // 销毁 *sp,然后释放 sp 指向的内存空间
delete[] arr; // 销毁数组中的元素,然后释放对应的内存空间
实际执行了两步步骤。
- 对
sp
所指的对象或arr
所指的数组中的元素执行相应的析构函数 - 编译器调用名为
operator delete( 或者 operator delete[] )
的标准库函数释放内存空间
如果应用程序希望控制内存分配的过程,则它们需要定义自己的 operator new
函数和 operator delete
函数。
当自定义了全局的
operator new
函数和operator delete
函数后,我们就负担起了控制动态内存分配的职责。这两个函数必须是正确的;因为它们是程序整个处理过程中至关重要的一环
operator new 接口和 operator delete 接口
标准库定义了 operator new
函数和 operator delete
函数的 8 个重载版本。其中前 4 个版本可能抛出 bad_alloc
异常,后 4 个不会
void *operator new(size_t); // 分配一个对象
void *operator new[](size_t); // 分配一个数组
void *operator delete(void*) noexcept; // 释放一个对象
void *operator delete[](void*) noexcept; // 释放一个数组
void *operator new(size_t, nothrow_t&) noexcept;
void *operator new[](size_t, nothrow_t&) noexcept;
void *operator delete(void*, nothrow_t&) noexcept;
void *operator delete[](void*, nothrow_t&) noexcept;
如果我们像自定义 operator new
函数,则可以为它提供额外的形参。此时,用到这些自定义函数的 new
表达式必须使用 new
的定位形式将实参传给新增的形参。尽管在一般情况下我们可以自定义具有任何形参的 operator new
,但下面的函数不能被用户重载
void *operator new(size_t, void*); // 不允许重新定义这个版本
malloc 函数和 free 函数
malloc
函数接受一个表示待分配字节数的 size_t
,返回指向发呢配空间的指针或者返回 0 以表示分配失败。free
函数接收一个 void*
,它是 malloc
返回的指针的副本,free
将相关内存返回 给系统。调用 free(0)
没有任何意义
如下是编写 operator new
和 operator delete
的一种简单方式
void *operator new(size_t size){
if( void* mem = malloc(size) )
return mem;
else
throw bad_alloc();
}
void operator delete(void* mem) noexcept { free(mem); }
定位 new 表达式
与 allocator
不同的是,对于 operator new
分配的内存空间来说,我们无法使用 construct
函数构造对象。相反,我们应该使用 new
的定位 new( placement new ) 形式构造对象。new
的这种形式为分配函数提供了额外的信息。我们可以使用定位 new
传递一个地址,此时定位 new
的形式如下所示:
new (place_address) type
new (place_address) type (initializers)
new (place_address) type [size]
new (place_address) type [size] { braced initializer list }
其中 place_address
必须是一个指针,同时在 initializers
中提供一个( 可能为空的 )以逗号分隔的初始值列表,该初始值列表将用于构造新分配的对象
当仅通过一个地址值调用时,定位 new
使用 operator new(size_t, void*)
“分配” 它的内存。该函数不分配任何内存,它只是简单地返回指针实参;然后由 new
表达式负责在指定的地址初始化对象以完成整个工作。事实上,定位 new
允许我们在一个特定地、预先分配地内存低地址上构造对象。
当只传入一个指针类型的实参时,定位 new 表达式构造对象但是不分配内存
尽管在很多时候使用定位 new
与 allocator
的 construct
成员非常相似,但在它们之间也有一个重要的区别。我们传给 construct
的指针必须指向同一个 allocator
对象分配的空间,但是传给定位 new
的指针无须指向 operator new
分配的内存。实际上,传给定位 new
表达式的指针甚至不需要执行动态内存
运行时类型识别
**运行时类型识别 ( run-time type identification, RTTI )**的功能由两个运算符实现:
typeid
运算符,用于返回表达式的类型dynamic_cast
运算符,用于将基类的指针或引用安全地转换成派生类地指针或引用
当我们将这两个运算符用于某种类型的指针或引用,并且该类型含有虚函数时,运算符将使用指针或引用所绑定对象的动态类型
dynamic_cast 运算符
dynamic_cast 运算符的使用形式如下:
dynamic_cast<type*>(e)
dynamic_cast<type&>(e)
dynamic_cast<type&&>(e)
其中,type
必须是一个类类型,并且通常情况下该类型应该含有虚函数。
在上面的所有形式中,e
地类型必须复合下面三个条件中的任意一个:e
的类型是目标 type
的公有派生类、e
的类型是目标 type
的公有基类或者 e
的类型就是目标 type
的类型。
指针类型的 dynamic_cast
假定 base
类至少含有一个虚函数,Derived
是 Base
的公有派生类,如果有一个指向 Base
的指针 bp
,则我们可以在运行时将它转换成指向 Derived
的指针
if( Derived* dp = dynamic_cast<Derived*>(bp) )
{
// 使用 dp 指向的 Derived 对象
}
else
{
// 使用 bp 指向的 Base 对象
}
引用类型的 dynamic_cast
引用类型的与指针类型在表示错误发生方式上略有不同。因为不存在所谓的空引用,所以对于引用类型来说无法使用与指针类型完全相同的错误报告策略
void f(const Base& b){
try{
const Derived& d = dynamic_cast<const Derived&>(b);
// 使用 b 引用的 Derived 对象
} catch(bad_cast){
// 处理类型转换失败的情况
}
}
typeid 运算符
typeid(e)
e
可以是任意表达式或类型的名字。typeid
操作的结果是一个常量对象的引用,该对象的类型是标准库类型 type_info
或 typeid
的公有派生类型。
当我们对数组 a
执行 typeid(a)
所得到的结果是数组类型而非指针类型。
当运算对象不属于类类型或者是一个不包含任何虚函数的类时,typeid
运算符指示的是运算对象的静态类型。而当运算对象是定义了至少一个虚函数的类的左值时,typeid
的结果直到运行时才会求得
Derived* dp = new Derived;
Base* bp = dp; // 两个指针都指向 Derived 对象
// 在运行时比较两个对象的类型
if(typeid(*bp) == typeid(*dp)){
// bp 和 dp 指向同一类型的对象
}
// 检查运行时类型是否时某种指定的类型
if(typeid(*bp) == typeid(Derived)){
// bp 实际指向 Derived 对象
}
注意:typeid
应该作用于对象,因此使用 *bp
而非 bp
// 下面的检查永远是失败的; bp 的类型是指向 Base 的指针
if(typeid(bp) == typeid(Derived)){
// 此处的代码永远不会执行
}
当
typeid
作用于指针时,返回的结果是该指针静态编译时的类型
使用 RTTI
定义两个实例类:
class Base{
friend bool operator==(const Base&, const Base&);
public:
// 接口
protected:
virtual bool equal(const Base&) const;
// ...
};
class Derived: public Base{
public:
// ...
protected:
bool equal(const Base&) const;
// ...
};
类型敏感的相等运算符
bool operator==(const Base& lhs, const Base& rhs){
// 如果 typeid 不相同,返回 false; 否则虚调用 equal
return typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
}
虚 equal 函数
继承体系中的每个类必须定义自己的 equal
函数。派生类的所有函数要做的第一件事都是相同的,那就是将实参的类型转换为派生类类型
bool Derived::equal(const Base& rhs) const{
auto r = dynamic_cast<const Derived&>(rhs);
// 执行比较两个 Derived 对象的操作并返回结果
}
基类 equal 函数
bool Base::equal(const Base& rhs) const{
// 执行比较 Base 对象的操作
}
枚举类型
C++ 包含两种枚举:限定作用域和不限定作用域的。C++11 新标准引入限定作用域的枚举类型。定义限定作用域的枚举类型的一般形式是:首先是关键字 enum class
或者等价地使用 enum struct
,随后是枚举类型名字以及用花括号括起来地以逗号分隔的 枚举成员 列表,随后是一个分号
enum class open_modes { input, output, append };
定义了一个名为 open_modes
的枚举类型,它包含三个枚举成员:input、output 和 append
定义不限定作用域的枚举类型时忽略掉关键字 class
或 struct
, 枚举类型的名字时可选的
enum color{red, yellow, green};
// 未命名的、不限定作用域的枚举类型
enum{floatPrec = 6, doublePrec = 10, double_doublePrec = 10};
如果 enum
是未命名的,则我们智能在定义该 enum
时定义它的对象。和类的定义类似,我们需要在 enum
定义的右侧花括号和最后的分号之间提供逗号分隔的声明列表
枚举成员
在限定作用域的枚举类型中,枚举成员的名字遵循常规的作用域准则,并且在枚举类型的作用域外是不可访问的。与之相反,在不限定作用域的枚举类型中,枚举成员的作用域与枚举类型本身的作用域相同
enum color { red, yellow, green }; // 不限定作用域的枚举类型
enum stoplight { red, yellow, green }; // 错误:重复定义了枚举成员
enum class peppers { red, yellow, green }; // 正确:枚举成员被隐藏了
color eyes = green; // 正确:不限定作用域的枚举类型的枚举成员位于有效的作用域中
peppers p = green; // 错误:peppers 的枚举成员不再有效的作用域中
// color::green 在有效的作用域中,但是类型错误
color hair = color::red; // 正确:允许显式地访问枚举成员
peppers p2 = peppers::red; // 正确:使用 peppers 的 red
默认情况下,枚举类型值从 0 开始,依次加 1.不过也能为一个或几个枚举成员指定专门的值
enum class intTypes{
charTyp = 8, shortTyp = 16, intTyp = 16,
longTyp = 32, long_longTyp = 64;
};
类成员指针
成员指针(point to member) 是指可以指向类的非静态成员的指针。类的静态成员不属于任何对象,因此无须特殊的指向静态成员的指针,指向静态成员的指针与普通指针没有什么区别
成员指针的类型囊括了类的类型以及成员的类型。当初始化这样一个指针时,我们令其指向类的某个成员,但是不指定该成员所属的对象;直到使用成员指针时,才提供成员所属的对象
class Screen{
public:
typedef std::string::size_type pos;
char get_cursor() const { return contents[cursor]; }
char get() const;
char get(pos ht, pos wd) const;
private:
std::string contents;
pos cursor;
pos height, width;
};
// pdata 可以指向一个常量 Screen 对象的 string 成员
const string Screen::*pdata;
上述语句将 pdata
声明成 “一个指向 Screen
类的 const string
成员的指针”。常量对象的数据成员本身也是常量,因此将我们的指针声明成指向 const string
成员的指针意味着 pdata
可以指向任何 Screen
对象的一个成员,而不管该 Screen
对象是否时常量。作为交换条件,我们只能使用 pdata
读取它所指的成员,而不能向它写入内容
初始化一个成员指针或者向它赋值时,需指定它所指的成员
pdata = &Screen::contents;
auto pdata = &Screen::contents;
使用数据成员指针
Screen myScreen, *pScreen = &myScreen;
// .* 解引用 pdata 以获得 myScreen 对象的 contents 成员
auto s = myScreen.*pdata;
// ->*解引用 pdata 以获得 pScreen 所指对象的 contents 成员
s = pScreen->*pdata;
从概念上来说,这些运算符执行两部操作:首先解引用成员指针以得到所需的成员;然后像成员访问运算符一样,通过对象( .*
)或指针( ->*
)获取成员
成员函数指针
和普通函数指针不同的是,在成员函数和指向该成员的指针之间不存在自动转换规则:
// pmf 指向一个 Screen 成员,该成员不接受任何实参且返回类型是 char
pmf = &Screen::get; // 必须显式地使用取地址运算符
pmf = Screen::get; // 错误:在成员函数和指针之间不存在自动转换规则
使用成员函数指针
Screen myScreen, *pScreen = &myScreen;
// 通过 pScreen 所指地对象调用 pmf 所指的函数
char c1 = (pScreen->*pmf)();
// 通过 myScreen 对象将私产0, 0 传给含有两个形参的 get 函数
char c2 = (myScreen.*pmf2)(0, 0);
使用成员指针的类型别名
// Action 是一种可以指向 Screen 成员函数的指针,它接收两个 pos 实参,返回一个 char
using Action =
char (Screen::*)(Screen::pos, Screen::pos) const;
Action get = &Screen::get; // get 指向 Screen 的 get 成员
和其他函数指针类似,我们可以将指向成员函数的指针作为某个函数的返回类型或形参类型
Screen& action(Screen&, Action = &Screen::get);
Screen myScreen;
// 等价的调用
action(myScreen);
action(myScreen, get); // 使用之前定义的变量 get
action(myScreen, &Screen::get); // 显式地传入地址
成员指针函数表
class Screen{
public:
// 其他接口和实现成员与之前一致
Screen& home(); // 光标移动函数
Screen& forward();
Screen& back();
Screen& up();
Screen& down();
};
定义一个 move
函数,使其可以调用上面地任意一个函数并执行对应的操作。为了支持这个新韩淑,将在 Screen
中添加一个静态成员,该成员是指向光标移动函数的指针的数组
class Screen{
public:
// ...
// Action 是一个指针,可以用任意一个光标移动函数对其赋值
using Action = Screen& (Screen::*));
// 指定具体要移动的方向
enum Directions { HOME, FORWARD, BACK, UP, DOWN };
Screen& move(Directoins);
private:
static Action Menu[]; // 函数表
};
Screen& Screen::move(Directions cm){
// 运行 this 对象中索引值为 cm 的元素
return (this->*Menu[cm])(); // Menu[cm] 指向一个成员函数
}
Screen myScreen;
myScreen.move(Screen::HOME); // 调用 myScreen.home
myScreen.move(Screen::DOWN); // 调用 myScreen.down
剩下就是定义并初始化函数表本身
Screen::Action Screen::Menu[] = {
&Screen::home,
&Screen::forward,
&Screen::back,
&Screen::up,
&Screen::down
};
union: 一种节约空间的类
联合( union ) 是一种特殊的类。一个 unioin
可以有多个数据成员,但是在任一时刻只有一个数据成员可以有值。当我们给 union
的某个成员赋值之后,该 union
的其他成员就变成未定义的状态了。分配一个 union
对象的存储空间至少要能容纳它的最大的数据成员。
类的某些特性对 union
同样适用,但并非所有特性都如此。union
不能含有引用类型的成员,除此之外,他的成员可以是绝大多数类型。默认情况下 union
的成员是公有的。
union
可以定义包括构造函数和析构函数在内的成员函数。但是由于 union
既不能继承自其他类,也不能作为基类使用,所以在 union
中不能含有虚函数
定义 union
// Token 类型的对象只有一个成员,该成员的类型可能是下列类型中的任意一种
union Token{
// 默认情况下成员是公有的
char cval;
int ival;
double dval;
};
使用 union 类型
默认情况下,union
是未初始化的。
Token first_token = { 'a' }; // 初始化 cval 成员
Token last_token; // 未初始化的 Token 对象
Token* pt = new Token; // 指向一个未初始化的 Token 对象的指针
如果提供了初始值,则该初始值被用于初始化第一个成员。因此,first_token
的初始化过程实际上是给 cval
成员赋了一个初值
last_token.cval = 'z';
pt->ival = 42;
匿名 union
一旦我们定义了一个匿名 union
,编译器就自动地位该 union
创建一个未命名对象
union {
char cval;
int ival;
double dval;
}; // 定义一个未命名对象,我们可以直接访问它的成员
cval = 'c'; // 为刚刚定义的未命名的匿名 union 对象赋一个新值
ival = 42; // 该对象当前保存的值是 42
在匿名 union
的定义所在的作用域内该 union
的成员都是可以直接访问的
位域
类可以将其( 非静态 )数据成员定义成 位域( bit-field ), 在一个位域中含有一定数量的二进制位。当一个程序需要向其他程序或硬件设备传递二进制数据时,通常会用到位域
volatile 限定符
volatile 的确切含义与及其有关,只能通过阅读编译器文档来理解。
直接处理硬件的程序常常包含这样的数据元素,它们的值由程序直接控制之外的过程控制。例如,程序可能包含一个由系统时钟定时更新的变量。当对象的值可能在程序的控制或检测之外被改变时,应该将该对象声明为 volatile
。关键字 volatile
告诉编译器不应该对这样的对象进行优化
volatile
限定符的用法和 const
相似,它起到对类型额外修饰的作用
volatile int display_register; // 该 int 值可能发生改变
volatile Task* curr_task; // curr_task 自画像一个 volatile 对象
volatile int iax[max_size]; // iax 的每个元素都是 volatile
volatile Screen bitmapBuf; // bitmapBuf 的每个成员都是 volatile
合成的拷贝对 volatile 对象无效
const
和 volatile
的一个重要区别是我们不能使用合成的拷贝/移动构造函数及赋值运算符初始化 volatile
对象或从 volatile
对象赋值。合成的成员接收的形参类型是 ( 非 volatile
) 常量引用,显然我们不能把一个非 volatile
引用绑定到一个 volatile
对象上
如果一个类希望拷贝、移动或赋值它的 volatile
对象,则该类必须自定义拷贝或移动操作。例如,我们可以将形参类型指定为 const volatile
引用,这样就可以利用任意类型的 Foo
进行拷贝或赋值操作了
class Foo{
public:
Foo(const volatile Foo&); // 从一个 volatile 对象进行拷贝
// 将一个 volatile 对象赋值给非 volatile 对象
Foo& operator=(volatile const Foo&);
// 将一个 volatile 对象赋值给一个 volatile 对象
Foo& operator=(volatile csont Foo&) volatile;
// ...
};
extern “C”
声明一个非 C++ 的函数
链接提示可以由两种形式:单个的或者复合的。链接提示不能出现在类定义或函数定义的内部。同样的链接提示必须在函数的每个声明中都出现
// 可能出现在 C++ 头文件 <cstring> 中的链接提示
// 单语句链接提示
extern "C" size_t strlen(const char*);
// 复合语句链接提示
extern "C" {
int strcmp(const char*, const char*);
char* strcat(char*, const char*);
}
链接提示的第一种形式包含一个关键字 extern
,后面是一个字符串字面值常量以及一个“普通的”函数声明
其中的字符串字面值常量指出了编写函数所用的语言。编译器应该支持对 C
语言的链接提示。此外,编译器也可能会支持其他语言的链接指示,如 extern "Ada"、extern "FORTRAN"
等。