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 使得程序更容易访问硬件。

链接指示使得程序更容易访问用其他语言编写的代码。

posted @ 2023-07-08 17:51  keep-minding  阅读(14)  评论(0)    收藏  举报