# Chapter 19 Specialized Tools and Techniques
# Controlling Memory Allocation
# 控制内存分配
# 重载 new 和 delete
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
函数的 8 个重载版本:
// 这些版本可能抛出异常 | |
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
函数和operator delete
函数的目的在于改变内存分配的方式,但是不管怎样,都不能改变new
运算符和delete
运算符的基本含义。 - 使用从 C 语言继承的函数
malloc
和free
函数能实现以某种方式执行分配内存和释放内存的操作:
#include <cstdlib> | |
void *operator new(size_t size) { | |
if(void *mem = malloc(size)) | |
return mme; | |
else | |
throw bad_alloc(); | |
} | |
void operator delete(void *mem) noexcept { | |
free(mem); | |
} |
# 定位 new 表达式
- 应该使用 new 的定位
new(placement 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
表达式构造对象但是不分配内存。 - 调用析构函数会销毁对象,但是不会释放内存。
string *sp = new string("a value"); // 分配并初始化一个 string 对象 | |
sp->~string(); |
# Exercise 19.1
使用 malloc 编写你自己的 operator new (sizt_t) 函数,使用 free 编写 operator delete (void *) 函数。
# Exercise 19.2
默认情况下,allocator 类使用 operator new 获取存储空间,然后使用 operator delete 释放它。利用上一题中的两个函数重新编译并运行你的 StrVec 程序。
# Run-Time Type Identification
# 运行时类型识别
- 运行时类型识别
(run-time type identification, RTTI)
的功能由两个运算符实现:typeid
运算符, 用于返回表达式的类型。dynamic_cast
运算符,用于将基类的指针或引用安全地转换曾派生类的指针或引用。
- 使用
RTTI
必须要加倍小心。在可能的情况下,最好定义虚函数而非直接接管类型管理的重任。
# dynamic_cast 运算符
- dynamic_cast 运算符的使用形式如下:
dynamic_cast<type*>(e) //e 必须是一个有效的指针 | |
dynamic_cast<type&>(e) //e 必须是一个左值 | |
dynamic_cast<type&&>(e) //e 不能是左值 | |
// 以上,type 类型必须时一个类类型,并且通常情况下该类型应该含有虚函数。 | |
//e 的类型必须符合三个条件中的任意一个,它们是: | |
// 1. e 的类型是目标 type 的公有派生类; | |
// 2. e 的类型是目标 type 的共有基类; | |
// 3. e 的类型就是目标 type 的类型; | |
// 指针类型的 dynamic_cast | |
// 假设 Base 类至少含有一个虚函数,Derived 是 Base 的共有派生类。 | |
if (Derived *dp = dynamic_cast<Derived*>(bp)) { | |
// 使用 dp 指向的 Derived 对象 | |
} else { //bp 指向一个 Base 对象 | |
// 使用 dp 指向的 Base 对象 | |
} | |
// 引用类型的 dynamic_cast | |
void f(const Base &b) { | |
try { | |
const Derived &d = dynamic_cast<const Derived&>(b); | |
// 使用 b 引用的 Derived 对象 | |
} catch (bad_cast) { | |
// 处理类型转换失败的情况 | |
} | |
} |
- 可以对一个空指针执行
dynamic_cast
,结果是所需类型的空指针。
# typeid 运算符
typeid运算符(typeid operator)
,它允许程序向表达式提问:你的对象是什么类型?typeid
表达式的形式是typeid(e)
,其中e
可以是任意表达式或类型的名字,它操作的结果是一个常量对象的引用。它可以作用于任意类型的表达式。- 通常情况下,使用 typeid 比较两条表达式的类型是否相同,或者比较一条表达式的类型是否与指定类型相同:
Derived *dp = new Derived; | |
Base *bp = dp; | |
if (typeid(*bp) == typeid(*dp)) { | |
//bp 和 dp 指向同一类型的对象 | |
} | |
if (typeid(*bp) == typeid(Derived)) { | |
//bp 实际指向 Derived 对象 | |
} |
- 当 typeid 作用于指针时 (而非指针所指向的对象),返回的结果是该指针的静态编译时类型。
// 下面的检查永远是失败的:bp 的类型是指向 Base 的指针 | |
if (typeid(bp) == typeid(Derived)) { | |
// 永远不会执行 | |
} |
# 使用 RTTI
- 用途:为具有继承关系的类实现相等运算符时。对于两个对象来说,如果它们的类型相同并且对应的数据成员取值相同,则说这两个对象是相等的。
// 类的层次关系 | |
class Base { | |
friend bool operator==(const Base&, const Base&); | |
public: | |
// Base 的接口成员 | |
protected: | |
virtual bool equal(const Base&) const; | |
// Base 的数据成员和其他用于实现的成员 | |
}; | |
class Derived: public Base { | |
public: | |
// Derived 的其他接口成员 | |
protected: | |
bool equal(const Base&) const; | |
// Derived 的数据成员和其他用于实现的成员 | |
}; | |
// 类型敏感的相等运算符 | |
bool operator==(const Base &lhs, const Base &rhs) { | |
// 如果 typeid 不相同,返回 false;否则虚调用 equal | |
return typeid(lhs) == typeid(rhs) && lhs.equal(rhs); | |
} | |
// 虚 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 对象的操作 | |
} |
# type_info 类
# Exercise 19.3
已知存在如下的类继承体系,其中每个类分别定义了一个公有的默认构造函数和一个析构函数:
class A { /* ... */}; | |
class B : public A { /* ... */}; | |
class C : public B { /* ... */}; | |
class D : public B, public A { /* ... */}; |
下面哪个 dynamic_cast 将失败?
(a) A *pa = new C; | |
B *pb = dynamic_cast<B*>(pa); | |
(b) B *pb = new B; | |
C *pc = dynamic_cast<C*>(pb); | |
(c) A *pa = new D; | |
B *pb = dynamic_cast<B*>(pa); |
# Exercise 19.4
使用上一个练习定义的类改写下面的代码,将表达式 * pa 转换成类型 C&:
if (C *pc = dynamic_cast<C*>(pa)) | |
{ | |
// 使用 C 的成员 | |
} else { | |
// 使用 A 的成员 | |
} |
# Exercise 19.5
在什么情况下你应该用 dynamic_cast 替代虚函数?
# Exercise 19.6
编写一条表达式将 Query_base 指针动态转换为 AndQuery 指针。分别使用 AndQuery 的对象以及其他类型的对象测试转换是否有效。打印一条表示类型转换是否成功的信息,确保实际输出的结果与期望的一致。
# Exercise 19.7
编写与上一个练习类似的转换,这一次将 Query_base 对象转换为 AndQuery 的引用。重复上面的测试过程,确保转换能正常工作。
# Exercise 19.8
编写一条 typeid 表达式检查两个 Query_base 对象是否指向同一种类型。再检查该类型是否是 AndQuery。
# Exercise 19.9
编写与本节最后一个程序类似的代码,令其打印你的编译器为一些常见类型所起的名字。如果你得到的输出结果与本书类似,尝试编写一个函数将这些字符串翻译成人们更容易读懂的形式。
# Exercise 19.10
已知存在如下的类继承体系,其中每个类定义了一个默认公有的构造函数和一个虚析构函数。下面的语句将打印哪些类型名字?
class A { /* ... */ }; | |
class B : public A { /* ... */ }; | |
class C : public B { /*...*/ }; | |
(a) A *pa = new C; | |
cout << typeid(pa).name() << endl; | |
(b) C cobj; | |
A& ra = cobj; | |
cout << typeid(&ra).name() << endl; | |
(c) B *px = new B; | |
A& ra = *px; | |
cout << typeid(ra).name() << endl; |
# Enumerations
# 枚举类型
- 枚举类型
(enumeration)
使我们可以将一组整型常量组织在一起。枚举属于字面值常量类型。 - 限定作用域的枚举类型 (scoped enumeration):首先是关键字
enum class(或enum struct)
,随后是枚举类型名字以及用花括号括起来的以逗号分隔的枚举成员列表,最后是一个分号。
enum class open_modes {input, output, append}; |
- 不限定作用域的枚举类型
(unscoped enumeration)
:省略关键字class(或struct)
,枚举类型的名字是可选的。
enum color {red, yellow, green}; | |
enum {floatPrec = 6, doublePrec = 10, double_doublePrec = 10}; |
# Pointer to Class Member
# 类成员指针
成员指针:指可以指向类的非静态成员的指针。
# 数据成员指针
- 和其他指针一样,在声明成员指针时也使用 * 来表示当前声明的名字是一个指针。与普通指针不同的时,成员指针还必须包含成员所属的类。
//pdata 可以指向一个常量 (非常量) Screen 对象的 string 成员 | |
const string Screen::*pdata; | |
// C++11 | |
auto pdata = &Screen::contents; |
- 当我们初始化一个成员指针或为成员指针赋值时,该指针没有指向任何数据。成员指针指定了成员而非该成员所属的对象,只有当解引用成员指针时才提供对象的信息。
Screen myScreen, *pScreen = &myScreen; | |
auto s = myScreen.*pdata; | |
s = pScreen->*pdata; |
# 成员函数指针
- 因为函数调用运算符的优先级较高,所以在声明指向成员函数的指针并使用这些的指针进行函数调用时,括号必不可少:
(C::*p)(parms)
和(obj.*p)(args)
。
# 将成员函数用作可调用对象
# Exercise 19.11
普通的数据指针和指向数据成员的指针有何区别?
# Exercise 19.12
定义一个成员指针,令其可以指向 Screen 类的 cursor 成员。通过该指针获得 Screen::cursor 的值。
# Exercise 19.13
定义一个类型,使其可以表示指向 Sales_data 类的 bookNo 成员的指针。
# Exercise 19.14
下面的代码合法吗?如果合法,代码的含义是什么?如果不合法,解释原因。
auto pmf = &Screen::get_cursor; | |
pmf = &Screen::get; |
# Exercise 19.15
普通函数指针和指向成员函数的指针有何区别?
# Exercise 19.16
声明一个类型别名,令其作为指向 Sales_data 的 avg_price 成员的指针的同义词。
# Exercise 19.17
为 Screen 的所有成员函数类型各定义一个类型别名。
# Exercise 19.18
编写一个函数,使用 count_if 统计在给定的 vector 中有多少个空 string。
# Exercise 19.19
编写一个函数,令其接受 vector<Sales_data> 并查找平均价格高于某个值的第一个元素。
# Nested Classes
# 嵌套类
- 一个类可以定义在另一个类的内部,前者称为嵌套类 (nested class) 或嵌套类型 (nested type)。嵌套类常用于定义作为实现部分的类。
- 嵌套类是一个独立的类,与外层类基本没有什么关系。特别是,外层类的对象和嵌套类的对象是相互独立的。
- 嵌套类的名字在外层类作用域中是可见的,在外层类作用域之外不可见。
# Exercise 19.20
将你的 QueryResult 类嵌套在 TextQuery 中,然后重新运行 12.3.2 节中使用了 TextQuery 的程序。
# union: A Space-Saving Class
# union:一种节省空间的类
联合(union)
是一种特殊的类。一个union
可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。它不能含有引用类型的成员和虚函数。
// Token 类型的对象只有一个成员,该成员的类型可能是下列类型中的任意一种 | |
union Token { | |
// 默认情况下成员是共有的 | |
char cval; | |
int ival; | |
double dval; | |
}; |
匿名union(anonymous union)
是一个未命名的union
,并且在右花括号和分号之间没有任何声明。
union { | |
char cval; | |
int ival; | |
double dval; | |
}; | |
// 可以直接访问它的成员 | |
cal = 'c'; | |
ival = 42; |
- 注意:
匿名union
不能包含受保护的成员或私有成员,也不能定义成员函数。
# Exercise 19.21
编写你自己的 Token 类。
# Exercise 19.22
为你的 Token 类添加一个 Sales_data 类型的成员。
# Exercise 19.23
为你的 Token 类添加移动构造函数和移动赋值运算符。
# Exercise 19.24
如果我们将一个 Token 对象付给它自己将发生什么情况?
# Exercise 19.25
编写一系列赋值运算符,令其分别接收 union 中各种类型的值。
# Local Classes
# 局部类
局部类(local class)
:可以定义在某个函数的内部的类。它的类型只在定义它的作用域内可见。和嵌套类不同,局部类的成员受到严格限制。- 局部类的所有成员 (包括函数在内) 都必须完整定义在类的内部。因此,局部类的作用与嵌套类相比相差很远。
- 局部类不能使用函数作用域中的变量。
int a, val; | |
void foo(int val) { | |
static inti si; | |
enum loc { a = 1024, b}; | |
// Bar 是 foo 的局部类 | |
struct Bar { | |
Loc locVal; // 正确:使用一个局部类型名 | |
int barVal; | |
void fooBar(Loc l = a) { // 正确:默认实参是 Loc::a | |
barVal = val; // 错误:val 是 foo 的局部变量 | |
barVal == ::val; // 正确:使用一个全局对象 | |
barVal = si; // 正确:使用一个静态局部对象 | |
locVal = b; // 正确:使用一个枚举成员 | |
} | |
}; | |
} |
# Inherently Nonportable Features
# 固有的不可移植的特性
所谓不可移植的特性是指因机器而异的特性,当将含有不可移植特性的程序从一台机器转移到另一台机器上时,通常需要重新编写该程序。
# 位域
- 类可以将其 (非静态) 数据成员定义成位域 (bit-field),在一个位域中含有一定数量的二进制位。当一个程序需要向其他程序或硬件设备传递二进制数据时,通常会用到位域。
- 位域在内存中的布局是与机器相关的。
- 位域的类型必须是整型或枚举类型。因为带符号位域的行为是由具体实现确定的,通常情况下我们使用无符号类型保存一个位域。
typedef unsigned int Bit; | |
class File { | |
Bit mode: 2; | |
Bit modified: 1; | |
Bit prot_owner: 3; | |
Bit prot_group: 3; | |
Bit prot_world: 3; | |
public: | |
enum modes {READ = 01, WRITE = 02, EXECUTE = 03}; | |
File &open(modes); | |
void close(); | |
void write(); | |
bool isRead() const; | |
void setWrite(); | |
} | |
// 使用位域 | |
void File::write() { | |
modified = 1; | |
// ... | |
} | |
void File::close() { | |
if( modified) | |
//... 保存内容 | |
} | |
File &File::open(File::modes m) { | |
mode |= READ; // 按默认方式设置 READ | |
// 其他处理 | |
if(m & WRITE) // 如果打开了 READ 和 WRITE | |
// 按照读 / 写方式打开文件 | |
return *this; | |
} |
# volatile 限定符
- 当对象的值可能在程序的控制或检测之外被改变时,应该将该对象声明为
volatile
。关键字volatile
告诉编译器不应对这样的对象进行优化。 const
和volatile
的一个重要区别是不能使用合成的拷贝 / 移动构造函数及赋值运算符初始化volatile
对象或者从volatile
对象赋值。
# 链接指示:extern "C"
C++
使用链接指示(linkage directive)
指出任意非C++
函数所用的语言。- 要想把
C++
代码和其他语言 (包括C
语言) 编写的代码放在一起使用,要求我们必须有权访问该语言的编译器,并且这个编译器与当前的C++
编译器是兼容的。 C++
从 C 语言继承的标准库函数可以定义为C
函数,但并非必须:决定使用C
还是C++
实现的C
标准库,是每个C++
实现的事情。- 有时需要在 C 和 C 中编译同一个源文件,为了实现这一目的,在编译 C 版本的程序时预处理器定义
__cplusplus
。
#ifdef __cplusplus | |
extern "C" | |
#endif | |
int strcmp(const char*, const char*); |
# Exercise 19.26
说明下列声明语句的含义并判断它们是否合法:
extern "C" int compute(int *, int); | |
extern "C" double compute(double *, double); |
# Chapter Summary
🍓:)