C++_Primer19.specialized_tools
特殊工具与技术
控制内存分配
重载 new 和 delete
string* sp = new string("string"); // 创建了一个 string 对象
string* arr = new string[10]; // 创建了一个 string 数组
delete sp;
delete [] arr;
当执行 delete 时,实际上执行了两步操作,第一步对sp所指的对象或arr指向的数组中所有的元素执行析构函数;第二步,编译器调用名为 operator delete(或 operator delete[])的标准库函数释放空间。
当编译器发现一条 new 或 delete 表达式后,将在程序中查找可供调用的 operator 函数。如果对象是类类型,则首先在类及其基类的作用域中查找;否则编译器在全局作用域查找匹配的函数。
可以使用作用域运算符,比如 ::new,指定要使用的版本。
标准库定义了8个重载版本:
// 可能抛出 bad_alloc 异常
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;
nothrow_t 是定义在 new 中的一个 struct,不包含任何成员。
与析构函数类似,operator delete 也不允许抛出异常,所以必须带有 noexcept。
自定义版本必须位于全局作用域或类作用域。当定义成类的成员时,他们是隐式静态的,无需显式声明 static。operator new 用在对象构造之前,operator delete 用在对象销毁之后,所以必须是静态的,而且他们不能操纵类的任何数据成员。
operator new 的第一个形参必须是 size_t,且不能含有默认实参。
当编译器调用 operator new 时,把对应类型所需的字节数传递给 size_t 形参;
调用 operator new[] 时,传入的是数组中所有元素所需的空间。
当调用 operator delete 或 operator delete[] 时,可以包含另外一个类型为 size_t 的形参,用于删除继承体系中的对象。
如果基类有一个虚析构函数,则形参的字节数将因待删指针所指对象的动态类型不同而有所区别。
实际运行的 operator delete 函数版本由对象的动态类型决定。
一般情况,我们可以重载任意形参的 operator new,但以下函数不能被用户重载:
// 不允许重新定义这个版本,该版本只供标准库使用
void* operator new(size_t, void*);
它部分陪任何内存,指示简单地返回指针实参。
标准库函数 operator new 和 operator delete 的名字容易让人误解,和其他 operator 函数不同,这两个函数并没有重载 new 表达式或 delete 表达式。实际上,我们根本无法自定义 new 表达式或 delete 表达式的行为。
一条 new 表达式执行时总是先调用 operator new 函数获取空间,然后在其中构造对象;
与之相反,一条 delete 表达式先销毁对象,然后调用 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 表达式
placement, 定位?占位?
在一个已经分配好的内存中(栈或者堆中)构造一个新的对象。
直接使用 new 表达式只是对内存的分配,并没有进行初始化,想要初始化,可以使用 new 的定位版本:
// 只分配内存,不带初始化
string* ps = new string; // 默认初始化为空string
int* pi = new int; // *pi的值未定义
// 使用定位 new 表达式进行赋值
int* pi1 = new(pi) int(1024); // pi1 等于 pi
// 分配内存同时初始化
string* ps1 = new string(); // 值初始化为空string
string* ps2 = new string(9, 's'); // 直接初始化为9个s
int* pi2 = new int(1024);
vector<int>* pv = new vector<int>{0,1,2,3,4,5};
定位 new 的形式:
new (place_address) type
new (place_address) type(initializers)
new (place_address) type [size]
new (place_address) type [size] { initializer list }
定位 new 表达式的对象不一定是堆内 new 的对象,所以它没有对应的 delete 表达式:
int a = 10;
string* ps = new(&a) string; // ps 等于 a 的地址
ps = "sss" // a被修改为 "sss"
// A 有一个接受一个int 参数的构造器,形参有默认实参
A* p1 = (A*)malloc(sizeof(A));
new(p1)A; // 使用默认实参
p1->~A();
free(p1);
A* p2 = (A*)operator new(sizeof(A));
new(p2)A(10);
p2->~A();
operator delete(p2);
运行时类型识别
RTTI: run-time type identification
由两个运算符实现:
- typeid,用于返回表达式的类型
- dynamic_cast,用于将基类的指针或引用安全地转换成派生类的指针或引用
一般情况,只要有可能,应尽量使用虚函数
如果某个指针或引用指向动态类型为衍生类的基类类型,想要使用不是虚函数的成员时,就要用到 RTTI 运算符。
与虚函数相比,RTTI 运算符蕴含着更多风险:程序员必须清楚地直到转换的mubiaoleiixng,并且检查类型转换是否被成功执行。
dynamic_cast 运算符
dynamic_cast<type*>(e) // e 必须是一个有效的指针
dynamic_cast<type&>(e) // e 必须是一个左值
dynamic_cast<type&&>(e) // e 不能是左值
以下三种情况,类型转换才会成功:e 的类型是目标 type 的公有派生类、e 的类型是目标 type 的公有基类或 e 的类型就是 type 的类型。
如果转换失败:如果转换目标是指针,则返回0;如果转换目标是引用,则抛出异常 bad_cast.
// 指针的转换
if (Derived* dp = dynamic_cast<Derived*>(bp)) {
...
} else {
...
}
// 引用的转换
void f(const Base& b) {
try {
const Derived& d = dynamic_cast<const Derived&>(b);
...
} catch (bad_cast) {
...
}
}
typeid 运算符
typeid 用于查询对象的类型。
typeid(e)
e 可以是任意表达式或类型的名字。typeid 的操作结果是一个常量对象的引用,该对象的类型是标准库类型 type_info 或 type_info 的公有派生类型。type_info 类定义在头文件 typeinfo 中。
对数组执行 typeid 查询得到的是数组类型而不是指针。
当运算对象不是类类型或是一个不包含任何虚函数的类时,返回对象的静态类型。
当运算对象是一个定义了至少一个虚函数的类的左值时,其结果直到运行时才会求得。
对于指针,必须是有效指针,空指针会导致 bad_typeid 异常。
Derived* dp = new Derived;
Base* bp = dp;
if (typeid(*dp) == typeid(*dp)) { // true
...
}
if (typeid(*bp) == typeid(Derived)) { // true
...
}
// false: bp 是指针,typeid(bp) 返回的是指向 Base 的指针(指针是在编译器求出)
if (typeid(bp) == typeid(Derived)) {
...
}
使用 RTTI
使用 == 对继承体系中的两个对象进行比较,首先判断类型,然后再对内部成员进行比较,衍生类内的实现只处理衍生类的成员,基类只处理基类成员:
class Base {
friend bool operator==(const Base&, const Base&);
public:
...
protected:
virtual bool equal(const Base&) const;
...
};
class Derived : public Base {
public:
...
protected:
virtual bool equal(const Base&) const;
...
};
bool operator==(const Base& lhs, const Base& rhs) {
return typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
}
bool Base::equal(const Base& rhs) const {
// Base 中成员的比较
...
}
bool Derived::equal(const Base& rhs) const {
// 这里的转换永远不会失败,因为我们事先用 typeid 进行了比较
auto rhs2 = dynamic_cast<Derived&> rhs;
// Derived 中成员的比较
...
}
type_info 类
| 操作 | 说明 |
|---|---|
| t1 == t2 | 类型相同返回 true,否则返回 false |
| t1 != t2 | 与上相反 |
t.name() |
返回一个C风格字符串,表示类型名字的可打印形式,生成方式因系统而异 |
| t1.before(t2) | 返回 bool 值,表示 t1 是否位于 t2 前。顺序关系依赖于编译器。 |
枚举类型
枚举类型是将一组整型常量组织在一起的新型类型,属于字面值常量类型。
枚举成员默认从0开始,依次加1, 也可以指定成员的值。
C++包含两种枚举:限定作用域的和不限定作用域的。
// 限定作用域,enum class 或 enum struct 开头
enum class open_modes {input, output, append};
// 不限定作用域,省略 class,名字可选
enum color {red, yellow, green};
// 如果是未命名的,只能在定义该 enum 时定义它的对象
enum {floatPrec=6, doublePrec = 10, double_doublePrec = 10};
作用域
对定作用域的枚举类型,枚举成员名字遵循常规作用域准则,枚举类型作用域外不可访问。而不限定作用域的类型,枚举成员的作用域与枚举类型本身的作用域相同。
enum color {red, yellow, green};
enum stoplight {red, yellow, green};
enum class peppers {red, yellow, green};
color eyes = green; // 正确
peppers p = green; // 错误,限定的作用域,枚举类型作用域外不能直接访问
color hair = color::red; // 正确
peppers p2 = peppers::red; // 正确
不限定作用域的枚举类型的枚举成员会隐式地转换成 int,而限定作用域的不能进行隐式转换:
int i = color::red; // 正确
int j = peppers::red; // 错误
指定类型
在名字后加上冒号和想要使用的类型,可以指定枚举类型使用的类型:
enum intValues : unsigned long long {
charTyp = 255, shortTyp = 65535, intTyp = 65535, longTyp = 4294967295ul,
long_longTyp = 18446744073709551615ull
};
不限定作用域的 enum 未指定成员的默认大小,所以在声明时必须指定成员的大小;而限定作用域的 enum 可以不指定其成员大小,这个值被隐式地定义成 int:
enum intValues : unsigned long long; // 必须指定成员类型
enum class open_modes; // 默认是 int 类型
类成员指针
成员之振可以指向类的非静态成员。
类的静态成员不属于任何对象,因此无序特殊的指向静态成员的指针,指向静态成员的指针与普通指针没有区别。
成员指针的类型包括了类的类型和成员的类型。
当初始化一个成员指针时,我们令其指向类的某个成员,但不指定所属对象,直到使用成员指针时,才提供成员所属的对象:
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;
};
const string Screen::*pdata;
pdata = &Screen::contents;
auto pdata2 = &Screen::contents;
Screen sc, *psc = sc;
auto s = sc.*pdata; // s 是 sc 的 contents 成员,string 类型
s = psc->*pdata;
指针的解析过程包括两步:首先解引用成员指针得到所需成员;然后像成员访问运算符一样,通过 .* 或 ->* 获取成员。
返回数据成员指针的函数
class Screen {
public:
static const std::string Screen::*data() {
return &Screen::contents;
}
};
// pdata 的类型是 const string Screen::*
const string Screen::*pdata = Screen::data();
Screen sc;
auto s = sc.*pdata;
成员函数指针
// pmf 的类型是 char (Screen::*)() const
auto pmf = &Screen:: get_cursor;
// 先声明,后赋值
char (Screen::*pmf2)(Screen::pos, Screen::pos) const;
pmf2 = &Screen::get;
// 错误。p 是一个非成员函数,返回一个 Screen 类的一个 char 成员;
// 因为是个普通函数,所以不能使用 const 限定符。
char Screen::*p(Screen::pos, Screen::pos) const;
// 与普通函数不同,成员函数和指向它的指针之间不存在自动转换规则,
// 所以必须使用取地址运算符
// pmf 指向的是无参数的 get 成员函数
pmf = &Screen::get;
pmf = Screen::get; // 错误
// 使用 .* 或 ->* 调用指向成员函数的指针
Screen sc1, *sc1p = &sc1;
char c1 = (sc1p->*pmf)();
char c2 = (sc1.*pmf2)(0, 0);
使用类型别名:
using Action = char (Screen::*)(Screen::pos, Screen::pos) const;
Action get = &Screen::get;
使用成员函数指针
// 第二个参数默认使用 get 成员函数
Screen& action(Screen&, Action = &Screen::get);
Screen sc;
// 以下调用等价
action(sc);
action(sc, get); // 使用上述定义的 get
action(sc, &Screen::get);
将成员函数用作可调用对象
function
从字符串组成的 vector 中找出第一个空 string:
// 错误,find_if 会以 fp(*it) 的形式使用 fp,而成员函数必须使用 ->* 运算符调用
auto fp = &string::empty;
find_if(svec.begin(), svec.end(), fp);
// 正确,fcn的调用变成了 ((*it).*p)(),p 是 fcn 内部指向成员函数的指针
function<bool (const string&)> fcn = &string::empty;
find_if(svec.begin(), svec.end, fcn);
通常,执行成员函数的对象将被传递给隐式的 this 形参,当使用 function 为成员函数生成一个可调用对象时,首先“翻译”该代码,使得隐式的形参变成显式的。
mem_fn
mem_fn 可以根据成员指针的类型推断可调用对象的类型,而无需用户显式地指定:
find_if(svec.begin(), svec.end(), mem_fn(&string::empty));
// mem_fn 生成了两个重载的函数调用运算符,f 可接受 string* 或 string&
auto f = mem_fn(&string::empty);
f(*svec.begin()); // f使用 .* 调用 empty
f(&svec[0]); // f使用 ->* 调用 empty
bind
auto it = find_if(svec.begin(), svec.end(), bind(&string::empty, _1));
auto f = bind(&string::empty, _1);
f(*svec.begin());
f(&svec[0]);
嵌套类
定义在类内部的类,称为嵌套类,或嵌套类型,常用于定义作为实现部分的类。
嵌套类是一个独立的类,与外层类基本没什么关系。
嵌套类的名字在外层类作用域中是可见的,外层类之外不可见。
// 嵌套类的声明
class TextQuery {
public:
using line_no = std::vector<std::string>::size_type;
class QueryResult;
...
};
// 类的定义
class TextQuery::QueryResult {
friend std::ostream& print(std::ostream&, const QueryResult&);
public:
QueryResult(std::string, std::shared_ptr<std::set<line_no>>,
std::shared_ptr<std::vector<std::string>>);
...
};
// 构造器的定义
TextQuery::QueryResult:QueryResult(std::string s,
std::shared_ptr<std::set<line_no>> p,
std::shared_ptr<std::vector<std::string>> f):
sought(s), lines(p), file(f) {}
静态成员
如果嵌套类声明了一个静态成员,则它的定义将位于外层类的作用域之外:
int TextQuery::QueryResult::static_mem = 1024;
union:一种节省空间的类
一个联合可以有多个数据成员,但任何时刻只有一个数据成员可以有值。当给某个成员赋值后,其他成员就变成了未定义状态。
分配给一个联合的空间至少要能容纳它的最大的数据成员。
union 不能含有引用类型的成员。可以包含含有构造函数或析构函数的类类型。
可以指定 public, protected, private 等保护标记,默认都是共有的。
union 可以包含包括构造函数和析构函数在内的成员函数。
union Token {
char cval;
int ival;
double dval;
};
Token t1 = {'a'}; // 初始化 cval 成员
Token t2; // 未初始化
Token* tp = new Token;
t2.cval = 'z';
pt->ival = 42;
// 既可以操作整个int,也可以只操作高16位或低16位
typedef union {
unsigned int Ax;
struct AX {
unsigned int AL:16;
unsigned int AH:16;
} sAX;
} example;
example a;
a.Ax = 0xAFAFBEBE;
int i = a.sAX.AH; // 获取高16位
匿名 union
匿名union 不能包含受保护成员或私有成员,也不能定义成员函数。
union {
char cval;
int ival;
double dval;
};
// 可以直接访问匿名 union 的成员
cval = 'c';
ival = 42;
含有类类型的union
略
局部类
定义在函数内部的类叫做局部类,局部类的成员受到严格限制,所有成员(包括函数)都必须完整定义在类的内部。
因为必须定义在类内,所以成员函数的复杂性不可能太高。
局部类中不允许声明静态数据成员。
局部类不能使用函数作用域中的变量,只能访问外层(函数内)作用域定义的类型名、静态变量以及枚举成员。
固有的不可移植的特性
为了支持底层编程,C++定义了一些固有的不可移植特性,他们因机器而异,移植时需要重新编写代码。
位域
类可以将(非静态)数据成员定义成位域,一个位域含有一定数量的二进制位。
位域在内存中的布局是与机器相关的。
当一个程序需要向其他程序或硬件设备传递二进制数据时,通常会用到位域。
位域的类型必须是整型或枚举类型,最使用无符号整型。因为带符号位域的行为是由具体实现确定的。
位域的声明形式是在成员名字后紧跟一个冒号和一个常量表达式,常量表达式用于指定成员所占的二进制位数:
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:
// 文件类型,8进制表示
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;
if (m & WRITE) {
... // 按照读写方式打开文件
}
return *this;
}
如果一个类定义了位域成员,则通常他也会定义一组内联的成员函数用于检验或设置位域的值:
inline bool File::isRead() const { return mode & READ; }
inline bool File::setWrite() { mode |= WRITE; }
volatile 限定符
程序可能包含一些特殊对象,它们不仅由程序控制,还被其他因素影响。比如被硬件影响,或由系统时钟定时更新。这些对象应声明为 volatile,告诉编译器不应该对这样的对象进行优化。
volatile 的使用方法与 const 类似:
volatile int display_register; // int 值可能被程序之外的因素改变
volatile Task* curr_task; // 指向一个 volatile 对象
volatile int iax[max_size]; // 每个元素都是 volatile
volatile Screen bitmapBuf; // 对象 bitmapBuf 内的每个成员都是 volatile
int* volatile vip; // 指针是 volatile
volatile int* ivp; // 指向一个 volatile 的 int 型变量
volatile int* volatile vivp; // 指针和所指变量都是 volatile
volatile int v;
int* ip = &v; // 错误,应声明为 volatile int*
ivp = &v; // 正确
vivp = &v; // 正确
与 const 的一个重要区别是不能使用合成的拷贝/移动构造函数和赋值运算符初始化 volatile 对象或从 volatile 对象赋值。合成的成员接受形参类型是非 volatile 常量引用,不能把一个非 volatile 引用绑定到一个 volatile 对象上。
可以对 volatile 对象定义拷贝和赋值操作,但一个更深层次的问题是拷贝 volatile 对象是否意义。
不同程序使用 volatile 目的各不相同,是否有意义与使用目的密切相关。
链接指示:extern "C"
对于C语言来说,编译器检查和调用函数的方式与C++相同,但生成的代码有所区别。
想要在其他语言(包括 C 语言)中同时使用C++,需要我们有权访问该语言的编译器,并且这个编译器与当前的 C++ 编译器是兼容的。
链接指示分两种,单个的和复合的。
链接指示不能出现在类定义或函数定义的内部;它必须在函数的每个声明中都出现。
extern "C" size_t strlen(const char*);
extern "C" {
int strcmp(const char*, const char*);
char* strcat(char*, const char*);
}
// 也可应用于整个头文件
extern "C" {
#include <string.h> // 操作C风格字符串的头文件
...
}
extern 后面跟的是一个字符串字面值常量,指出了编写函数所用的语言,"C" 指的是C语言。
另外,编译器也可能会支持其他语言的链接指示,如 extern "Ada", extern "FORTRAN" 等。
指向 extern "C" 函数的指针
extern "C" void (*pf)(int);
void (*pf1)(int);
pf = pf1; // 错误,C函数和 C++ 函数被认定是不同的类型
当使用 pf 调用函数时,编译器认定这是一个C函数。
链接指示对整个声明都有效:
// f1 是一个C函数,其形参是一个指向C函数的指针
extern "C" void f1(void(*)(int));
// 希望给 C++ 函数传入 C 函数指针时:
// f2 是 C++函数,形参是 C 函数指针
extern "C" typedef void FC(int);
void f2(FC*);
可被多种语言共享的函数的返回类型和形参类型收很多限制。比如,不太可能把C++类型的对象传递给C函数,因为C程序根本无法理解构造函数、析构函数等的操作。
对链接到C的预处理器的支持
有时需要同时在C和C++中编译同一个源文件,预处理器为C++程序定义了__cplusplus变量:
#ifdef __cplusplus
extern "C"
#endif
int strcmp(const char*, const char*);
重载函数与链接指示
C语言不支持重载,所以C链接指示只能用于说明一组重载函数中的某一个:
// 错误,两个C函数的名字相同
extern "C" void print(const char*);
extern "C" void print(int);
// C函数可以在C或C++程序中调用;C++重载了这个函数,可以在C++中调用
class SmallInt { ... };
class BigNum { ... };
extern "C" double calc(double);
extern SmallInt calc(const SmallInt&); // 这里的 extern 不是链接指示,
extern BigNum calc(const BigNum&); // 而是指外部声明,其定义在其他文件中
小结
C++ 为处理特殊问题设置了一些特殊的处理机制。
自定义 operator new 和 operator delete 可以实现内存的精确控制。
运行时类型识别(RTTI)可以实现运行时获取对象的动态类型,只对定义了虚函数的类有效;对普通类,只能返回静态类型。
类成员指针在声明时,指出了所属类的类型信息。成员指针可以绑定到任意一个具有指定类型的成员上。解引用时必须提供该指定类型的对象。
C++定义了另外集中聚集类型:
- 嵌套类,定义在其他类的作用域中,通常作为外层类的实现类
- union,包含多种数据成员,任意时刻只有使用一个成员有值,通常嵌套在其他类的内部
- 局部类,定义在函数内,成员的定义必须都在类内,局部类不包含静态数据成员
C++ 支持几种固有的不可移植的特性,其中位域和 volatile 使得程序更容易访问硬件。
链接指示使得程序更容易访问用其他语言编写的代码。

浙公网安备 33010602011771号