C++ Primer 19 特殊工具与技术

特殊工具与技术

本章将介绍 C++ 语言的几种为被广泛使用的特征。


1 控制内存分配

解决问题:某些应用程序对内存分配有特殊的需求,因此我们无法将标准内存管理机制直接应用于这些程序。它们常常需要字段定义内存分配的细节,比如使用关键字 new 将对象放置在特定的内存中。为了实现这一目的,应用程序需要重载 new 运算符和 delete 运算符以控制内存分配的过程。

1.1 重载 new 和 delete

当我们使用一条 new 表达式时:

// 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 函数,也可以将它们定义为成员函数。当编译器发现一条 new 表达式或 delete 表达式后,将在程序中查找可供调用的 operator 函数。如果被分配(释放)的对象是类类型,则编译器首先在类及其基类的作用域中查找,然后在全局作用域中查找匹配的函数。如果还没好到,则使用标准库定义的版本。

我们可以使用作用域运算符令 new 表达式和 delete 表达式忽略定义在类中的函数,直接执行全局作用域中的版本:

::new
::delete
operator new 接口和 operator delete 接口

标准库定义了 operator new 函数和 operator delete 函数8个重载版本。其中前4个版本可能抛出 bad_alloc 异常,后4个版本不抛出异常:

// 这些版本可能抛出异常
void *operetor new(size_t);
void *operetor new[](size_t);
void *operetor delete(void *) noexcept;
void *operetor delete[](void *) noexcept;
// 这些版本承诺不会抛出异常
void *operetor new(size_t, nothrow_t&) noexcept;
void *operetor new[](size_t, nothrow_t&) noexcept;
void *operetor delete(void *, nothrow_t&) noexcept;
void *operetor delete[](void *, nothrow_t&) noexcept;

其中 nothrow_t 是定义在 new 头文件中的一个 struct,在这个类型中不包含任何成员。new 头文件还定义了一个名为 nothrow 的 const 对象,用户可以通过这个对象请求 new 的非抛出版本。

与析构函数类似,operator delete 也不允许抛出异常。当我们重载这些运算符时,必须使用 noexcept 异常说明符指定其不抛出异常,且自定义的版本必须位于全局作用域或类作用域中。当我们将上述运算符函数定义成类的成员时,它们是隐式静态的,因为 operator new 用在对象构造之前,而 operator delete 用在对象销毁之后。

下面这种函数形式无论如何都不能被用户重载:

void *operator new(size_t, void*);  // 不允许重新定义这个版本

这种形式只供标准库使用,不能被用户重新定义。

1.2 定位 new 表达式

与 allocator 不同的是,对于 operator new 分配的内存空间来说我们无法使用 construct 函数构造对象。相反,我们应该还使用 new 的定位 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 *) “分配”它的内存。这是一个我们无法自定义的 operator new 版本。该函数不分配任何内存,它只是简单地返回指针实参;然后由 new 表达式负责在指定的地址初始化对象以完成整个工作。
注: 当只传入一个指针类型的实参时,定位 new 表达式构造对象但是不分配内存。

显式的析构函数调用

我们既可以通过对象调用析构函数,也可以通过对象的指针或引用调用析构函数:

string *sp = new string("a value");  // 分配并初始化一个 string 对象
sp->~string();

但是需要注意,调用析构函数会销毁对象,但不会释放内存。


2 运行时类型识别 RTTI

运行时类型识别的功能有两个运算符实现:

  • typeid 运算符,用于返回表达式的类型。
  • dynamic_cast 运算符,用于将基类的指针或引用安全地转换成派生类的指针或引用。

当我们将这两个运算符用于某种类型的指针或引用,并且该类型含有虚函数时,运算符将使用指针或引用所绑定对象的动态类型。

这两个运算符特别适用于以下情况:我们想使用基类对象的指针或引用执行某个派生类操作并且该操作不是虚函数。一般来说,只要有可能我们应该尽可能使用虚函数。当操作被定义成虚函数时,编译器将根据对象的动态类型自动地选择正确的函数版本。

2.1 dynamic_cast 运算符

dynamic_cast 运算符的使用形式如下所示:

dynamic_cast<type*>(e);  // e 是有效指针
dynamic_cast<type&>(e);  // e 是左值
dynamic_cast<type&&>(e);  // e 不能是左值

其中,type 必须是一个类类型,且通常情况下该类型应该含有虚函数。e 的类型必须符合以下三个条件中的任意一个:

  • e 的类型是目标 type 的公有派生类。
  • e 的类型是目标 type 的公有基类。
  • e 的类型就是目标 type 的类型。

如果符合,则类型转换可以成功。否则,转换失败。如果一条 dynamic_cast 语句的转换目标是类型指针并且失败了,结果为 0。如果转换目标是引用类型并且失败了,则抛出一个 bad_cast 异常。

指针类型 dynamic_cast

假定 Base 类至少含有一个虚函数,Derived 是 Base 的公有派生类。如果有一个指向 Base 的指针 bp,则我们可以在运行时将他转换成指向 Derived 的指针:

if (Derived *dp = dynamic_cast<Derived *>(bp)) {
    // 使用 dp 指向的 Derived 对象
}
else {  // bp 指向一个 Base 对象
    // 使用 bp 指向一个 Base 对象
}

如果 bp 指向 Derived 对象,则上述的类型转换初始化 dp 并令其指向 bp 所指向的 Derived 对象。此时,if 语句内部使用 Derived 操作的代码是安全的。否则,类型转换的结果为 0,dp 为 0 意味着 if 语句条件失败,此时 else 自居执行相应的 Base 操作。
注: 我们可以对一个而空指针执行 dynamic_cast,结果是所需类型的空指针。

引用类型的 dynamic_cast

引用类型的 dynamic_cast 与指针类型的 dynamic_cast 在表示错误发生的方式上略有不同。因为不存在所谓的空引用,所以对于引用类型来说无法使用与指针类型完全相同的错误报告策略。当对引用类型转换失败时,程序抛出一个名为 std::bad_cast 的异常,该异常定义在 typeinfo 标准库头文件中。

void f(const Base &b) {
    try {
        const Derived &d = dynamic_cast<const Derived&>(b);
	    // 使用 b 引用的 Derived 对象
    } catch(bad_cast) {
        // 处理类型转换失败的情况
    }
}

2.2 typeid 运算符

typeid 运算符,它允许程序向表达式提问:你的对象是什么类型?

typeid 表达式的形式是 type(id),其中 e 可以是任意表达式或类型的名字。typeid 运算符操作的结果是一个常量对象的引用,该对象的类型是标准库类型 type_info 或 type_info 公有派生类型。type_info 类定义在 typeinfo 头文件中。

typeid 运算符可以用作任意类型表达式,其规则如下:

  • 顶层 const 将被忽略;
  • 如果表达式是一个引用,则 typeid 返回该引用所引对象的类型;
  • 当 typeid 作用于数组或函数时,并不会执行向指针的标准类型转。也就是说,如果我们对数组 a 执行 typeid(a),则所得到的结果时数组类型而非指针类型;
  • 当运算对象不属于类类型或时一个不包含任何虚函数的类时,typeid 运算符指示的时运算对象的静态类型
  • 当运算对象是定义了至少一个虚函数的类的左值时,typeid 的结果直到运行时才会求得。
使用 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 作用于指针时(而非指针所指的对象),返回的结果时该指针的静态编译时类型。

typeid 是否需要运行时间差决定了表达式是否会被求值。只有当类型含有虚函数时,编译器才会对表达式求值。繁殖,如果类型不含有虚函数,则 typeid 返回表达式的静态类型;编译器无序对表达式求值也能直到表达式的静态类型。

2.3 type_info 类

type_info 类的精确定义随着编译器的不同而略有差异。不过,C++ 标准规定 type_info 类必须定义在 typeinfo 头文件中,并至少提供下标所列的操作:

操作含义
t1 == t2
t1 != t2
判断 t1 和 t2 类型是否是同一种类型
t.name()返回一个 C 风格字符串,表示类型名字的可打印形式。类型名字的生成方式因系统而异
t1.before(t2)返回一个 bool 值,表示 t1 是否位于 t2 之前。before 所采用的顺序关系依赖编译器

type_info 类一般是作为一个基类出现,所以它还应该提供一个公有的虚析构函数。当编译器希望提供额外的类型信息时,通常在 type_info 的派生类完成。

type_info 类没有默认构造函数,而且它的拷贝和移动构造函数以及赋值运算符都被定义成删除的。创建 type_info 对象的唯一途径是使用 typeid 运算符

type_info 类的 name 成员函数返回一个 C 风格字符串,表示对象的类型名字。对于某种给定的类型来说,name 的返回值因编译器而异,且不一定于在程序中使用的名字一致。对于 name 返回值的唯一要求是:类型不同返回的字符串必须有所区别


3 枚举类型

枚举类型是我们可以将一组整型常量组织在一起。和类一样,每个枚举类型定义了一种新的类型。枚举属于字面值常量类型

C++包含两种枚举:限定作用域的和不限定作用域的。C++11 新标准引入了西纳丁作用域的枚举类型。

限定作用域的枚举类型,使用关键字 enum class(或 enum struct),随后时枚举类型名字以及用花括号括起来的以后号分隔的枚举成员,形式如下:

enum class open_modes {input, output, append};

不限定作用域的枚举类型,省略关键字 class(或 struct)即可:

enum color {red, yellow, green};

枚举类型的名字是可选的,如果 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 inTypes {
	charTyp = 8, shortTyp = 16, intTyp = 16
};

枚举成员时 const,因此在初始化枚举成员时提供的初始值必须是常量表达式。

和类一样,枚举也定义新的类型

只要 enum 有名字,就能定义并初始化该类型的成员。想要初始化 enum 对象或者为 enum 对象赋值,必须使用该类型的一个枚举成员或该类型的另一个对象

open_modes om = 2;  // 错误 2 不属于类型 open_modes
om = open_modes::input;  // 正确 input 是 open_modes 的一个枚举成员

一个不限定作用域的枚举类型的对象或枚举成员自动地转换成整形:

int i = color::red;  // 正确
int j = peppers::red;  // 错误 限定作用域的枚举类型不会进行隐式转换
指定 enum 的大小

尽管每个 enum 都定义了唯一的类型,但实际上 enum 是由某种整数类型表示的。在 C++11 新标准中,我们可以在 enum 的名字后面加上冒号以及我们想在该 enum 中使用的类型:

enum intValues:unsigned long long {
    charTyp = 255,shortTyp = 65535,
    intTyp = 65535,
    longTyp = 4294967295UL,
    long_longTyp = 184467440737095516115ULL
};

如果没有指定 enum 的潜在类型,则默认情况下限定作用域的 enum 成员类型是 int。

枚举类型的前置声明

在 C++11 新标准中,我们可以提前声明 enum。enum 的前置声明(无论隐式地还是显式地)必须指定其成员的大小:

enum intValues : unsigned long long;  // 不限定作用域,必须指定成员类型
enum class open_modes;  // 限定作用域的枚举类型可以使用默认的成员类型 int
// 所有的声明和定义必须对该 enum 是限定作用域还是不限定作用域的保持一致

4 类成员指针

成员指针是指可以指向类的非静态成员的指针。一般情况下,指针指向一个对象,但是成员指针指示的是类的成员,而非类的对象。类的静态成员不属于任何对象,因此无需特殊的指向静态成员的指针,指向静态成员的指针与普通指针没有什么区别。

成员指针的类型囊括了类的类型和成员的类型。当初始化这样一个指针时,我们令其指向类的某个成员,但是不指定该成员所属的对象;直到使用成员指针时,才提供成员所属的对象。

4.1 数据成员指针

在声明成员指针时我们也使用 * 来表示当前声明的名字是一个指针,成员指针还必须包含成员所属的类:

// pdata 可以指向一个常量(非常量)Screen 对象的 string 成员
const string Screen::*pdata;

上述语句将 pdata 声明成,一个指向 Screen 类的 const string 成员的指针。

当我们初始化一个成员指针(或向它赋值)时,需指定它所指的成员:

pdata = &Screen::contents;
使用数据成员指针

与成员访问运算符 . 和 -> 类似,也有两种成员指针访问运算符: .* 和 ->*,这两个运算符使得我们可以解引用指针并获得该对象的成员:

Screen myScreen, *pScreen = &myScreen;
//.* 解引用 pdata 以获得 myScreen 对象的 contents 成员
auto s = myScreen.*pdata;
// ->* 解引用 pdata 以获得 pScreen 所指对象的 contents 成员
s = pScreen->*pdata
返回数据成员指针的函数

常规的访问控制规则对成员指针同样有效。例如,Screen 的 contents 成员是私有的,因此之前对于 pdata 的使用必须位于 Screen 类的成员或友元内部,否则程序将发生错误。

因为数据成员一般情况下时私有的,所以我们通常不能直接获得数据成员的指针,最好定义一个函数,令其返回值是指向该成员的指针:

class Screen {
public:
	// data 是一个静态成员,返回一个成员指针
	static const std::string Screen::*data() {
		return &Screen::contents;
	}
};

4.2 成员函数指针

也可以定义指向类的成员函数的指针:

// pmf 是一个指针,它可以指向 Screen 的某个常量成员函数
// 前提是该函数不接受任何实参,并且返回一个 char
auto pmf = &Screen::get_cursor;

和指向数据成员的指针一样,我们可以使用 class name:: * 的形式声明一个指向成员函数的指针。如果成员存在重载问题,我们必须显式地声明函数类型,以明确指出我们想要使用的哪个函数:

char (Screen::*pmf2)(Screen::pos, Screen::pos) const;
pmf2 = &Screen::get;
使用成员函数指针

我们使用 .* 或 ->* 运算符作用于指向成员函数的指针,以调用类的成员函数:

Screen myScreen, *pScreen = &myScreen;
// 通过 pScreen 所指的对象调用 pmf 所指的函数
char c1 = (pScreen->*pmf)();
// 通过 myScreen 对象将实参 0, 0 传递给链各个形参的 get 函数
char c2 = (myScreen.*pmf2)(0, 0);

注: 因为函数调用运算符的优先级较高,所以在声明指向成员函数的指针并使用这样的指针进行函数调用时,括号必不可少:(X::*P)(parms) 和 (obj.*P)(args)。

4.3 将成员函数用作可调用对象

要想通过一个指向成员函数的指针进行函数调用,必须首先利用 .* 或 ->* 运算符将该指针绑定到特定对象上。因此与普通的函数指针不同,成员指针不是一个可调用对象,这一的指针不支持函数调用运算符。

因为成员指针不是可调用对象,所以我们不能直接将一个指向成员函数的指针传递给算法。所以不能使用下面的语句:

auto fp = &string::empty;  // fp 指向 string 的 empty 函数
// 错误 必须使用  .\* 或 ->\* 调用成员指针
find_if(sevc.begin(), svec.end(), fp);
使用 function 生成一个可调用对象

从指向成员函数的指针获取可调用对象的一种方法是使用标准库模板 function:

function<bool (const string &)> fcn = &string::empty;
find_if(svec.begin(), svec.end(), fcn);

我们告诉 function 一个事实:即 empty 是一个接受 string 参数并返回 bool 值的函数。

当我们定义一个 function 对象时,必须指定该对象所能表示的函数类型,即可调用对象的形式。如果可调用对象是一个成员函数,则第一个形参必须是表示该成员是在哪个(一般是隐式的)对象上面执行的。同时,我们提供给 function 的形式中还必须指明对象是否是以指针或引用的形式传入的。

使用 mem_fn 生成一个可调用对象

通过上面可以直到,要想使用 function,我们必须提供成员的调用形式。

标准库功能 mem_fn 可以让编译器负责推断成员的类型,和 function 一样 mem_fn 也定义在 functional 头文件中,并且可以从成员指针生成一个可调用对象;和 function 不同的是,mem_fn 可以根据成员指针的类型推断可调用对象 类型,无序用户显示指定:

find_if(svec.begin(), svec.end(), mem_fn(&string::empty));

mem_fn 生成的可调用对象可以通过对象调用,也可以通过指针调用:

auto f = mem_fn(&string::empty);
f(*svec.begin());  // 正确 传入一个 string 对象,f 使用 .* 调用 empty
f(&svec[0]);  // 正确 传入一个 string 指针,f 使用 ->* 调用 empty
使用 bind 生成一个可调用对象

我们还可以使用 bind 从成员函数生成一个可调用对象:

// 选择范围中的每个 string,并将其 bind 到 empty 的第一个隐式实参上
auto it = find_if(svec.begin(), svec.end(),
    bind(&string::empty, _1));

和 function 类似的地方是,当我们使用 bind 时,必须将函数中用于表示对象的隐式形参转换成显式的。和 mem_fn 类似的是,bind 生成的可调用对象的第一个实参既可以是 string 的指针,也可以是 string 的引用:

auto f = bind(&string::empty, _1);
f(*sevc.begin());  // 正确 实参是一个 string,f 使用 .* 调用 empty
f(&svec[0]);  // 正确 实参是一个 string 的指针,f 使用 ->* 调用 empty

5 嵌套类

一个类可以定义在另一个类的内部,前者称为 嵌套类嵌套类型。嵌套类长哟个与定义作为实现部分的类。

嵌套类是一个独立的类,与外层类基本没什么关系。特别是,外层类的对象和嵌套类的对象是相互独立的。在嵌套类的对象中不包含任何外层类定义的成员;类似的,在外层类的对象中也不包含任何嵌套类定义的成员。

嵌套类的名字在外层类作用域中式可见的,在外层类作用域之外不可见。和其他嵌套类的名字一样,嵌套类的名字不会和别的作用域的同一个名字冲突。

嵌套类中成员的种类与非嵌套类是一样的。和其他类类似,嵌套类也是用访问限定符来控制外界对其成员的访问权限。外层类对嵌套类的成员没有特殊的访问权限,同样,嵌套类对外层类的成员也没有特殊的访问权限。

嵌套类在其外层类中定义了一个类型成员。和其他成员类似,该类型的访问权限由外层类决定。位于外层类 public 部分的嵌套类实际上定义了一种可以随处访问的类型;位于外层类 protected 部分的嵌套类定义的类型只能被外层类及其友元和派生类访问;位于外层类 private 部分的嵌套类定义的类型只能被外层类的成员和友元访问。


6 union:一种节省空间的类

联合是一种特殊的类。一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当我们给 union 的某个成员赋值之后,该 union 的其他成员就变成未定义的状态了。因此,分配给一个 union 对象的存储空间至少要能容纳它的最大的数据成员

  • union 不能含有引用类型的成员
  • 在 C++11 新标准中,含有构造函数或析构函数的类类型也可以作为 union 的成员类型。
  • union 可以指定其成员为 public,protected 和 private 等保护标记。默认情况下,union 的成员都是共有的。
  • union 可以定义包括构造函数和析构函数在内的成员函数。
  • 由于 union 既不能继承自其它类,也不能作为基类使用,所以 union 中不能含有虚函数。
匿名 union

匿名 union 是一个未命名的 union,并且在右花括号和分号之间没有任何声明。一旦我们定义了一个匿名 union,编译器就会自动的为该 union 创建一个未命名对象:

union {  // 匿名 union
    char cval;
    int ival;
    double dval;
};  // 定义一个未命名的对象,我们可以直接访问它的成员
cval = 'c';  // 为钢哥定义的未命名的匿名 union 对象复制一个新值
ival = 42;  // 该对象当前保存的值是 42

在匿名 union 的定义所在的作用域内该 union 的成员都是可以直接访问的。
注: 匿名 union 不能包含受保护的成员或私有成员,也不能定义成员函数。


7 局部类

类可以定义在某个函数的内部,我们称这一的类为局部类。局部类定义的类型值在定义它的作用域内可见。和嵌套类不同,局部类的成员收到严格显式。
注: 局部类的所有成员(包括函数在内)都必须完整定义在类的内部。因此,局部类的作用域与嵌套类相比相差很远。类似的,在局部类中也不允许声明静态数据成员,因为我们没法定义这样的成员。

局部类不能使用函数作用域中的变量

局部类对其外层作用域中名字的访问权限受到很多限制,局部类只能访问外层作用域定义的类型名、静态变量以及枚举成员。如果局部类定义在某个函数内部,则该函数的普通局部变量不能被该局部类使用:

int a, val;
void foo(int val) {
    static int 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;  // 正确 使用一个枚举成员
        }
    };
    //...
}

8 固有的不可移植的特性

为了支持低层编程,C++ 定义了一些固有的不可移植的特性。所谓不可移植的特性是指因为机器而异的特性,当我们将含有不可移植特性的程序从一台机器转移到另一台机器上时,通常需要重新编写该程序。算术类型的大小在不同机器上不一样,这是我们使用过的不可移植特性的一个典型示例。

8.1 位域

类可以将其(非静态)数据成员定义成位域,在一个位域中含有一定数量的二进制位。当一个程序需要向其他程序或硬件设备传递二进制数据时,通常会用到位域
注: 位域在内存中的布局是与机器相关的。

位域的类型必须是整形或枚举类型。因为带符号位域的行为是由具体实现确定的,所以在通常情况下我们使用无符号类型保存一个位域。

位域的声明形式时在成员名字后紧跟着一个冒号以及一个常量表达式,该表达式用于指定成员所占的二进制位数

typedef unsigned int Bit;
class File {
    Bit mode: 2;  // mode 占 2 位
    Bit modified: 1;  // modified 占 1 位 
    Bit prot_owner: 3;  // prot_owner 占 3 位
    Bit prot_gourp: 3;  // prot_group 占 3 位
    Bit prot_world: 3;  // prot_world 占 3 位
    // File 的操作和数据成员
public:
	// 文件类型以八进制的形式表示
    enum modes{READ = 01, WRITE = 02, EXECUTE = 03};
    File &open(modes);
    void close();
    void write();
    bool isRead() const;
    void setWrite();
};

mode 位域占 2 个二进制位,modified 占 1 个,其他成员各占 3 个。如果可能的话,在类的内部连续定义的位域压缩成一个整数的相邻位,从而提供存储压缩。例如,这五个位域可能会存储在一个 unsigned int 中。至于二进制位是否能压缩到一个整数中以及如何压缩是与机器相关的。

取地址运算符(&)不能作用于位域,因此任何指针都无法指向类的位域。
注: 通常情况下最好将位域设为无符号类型,存储在带符号类型中的位的行为将因具体实现而定。

使用位域

访问位域的方式与访问类的其他数据成员的方式类似:

void File::write() {
	modified = 1;
}

8.2 volatiole 限定符

volatile 的确切含义与机器有关,只能通过阅读编译器文档来理解。要想让使用了 volatile 的程序在移植到新机器或新编译器后仍然有效,通常需要对该程序进行某些改变。

直接处理硬件的程序常常包含这样的数据元素:他们的值由程序直接控制之外的过程控制。例如,程序可能包含一个由系统时钟定时更新的变量。当对象的值可能在程序的控制或检测之外被改变时,应该将对象声明为 volatiole。关键字 volatile 告诉编译器不应对这样的对象进行优化

volatile 限定符的用法和 const 很相似,它起到对类额外修饰的作用:

volatile int display_register;  // 该 int 值可能发生改变
volatile Task *curr_task;  // curr_task 指向一个 volatile 对象
volatile int iax[max_size];  // ima 的每个元素都是 volatile
volatile Screen bitmapBuf;  // bitmapBuf 的每个成员都是 volatile

const 和 volatile 限定符互相没有什么影响。就像一个类可以定义 const 成员函数一样,它也可以将成员函数定义为 volatile。只有 volatile 的成员函数才能被 volatile 的对象调用。

合成的拷贝对 volatile 对象无效

const 和 volatile 的一个重要区别是我们不能使用合成的拷贝/移动构造函数及赋值运算符初始化 volatile 对象或从 volatile 对象赋值。合成的成员接受的形参类型是(非 volatile)常量引用,显然我们不能把一个非 volatile 引用绑定到一个 volatile 对象上。

如果一个类希望拷贝、移动或赋值它的 volatile 对象,则该类必须自定义拷贝或移动操作。

class Foo {
public:
    // 从一个 volatile 对象进行拷贝
    Foo(const volatile  Foo&);
    // 将一个 volatile 对象赋值给一个非 volatile 对象
    Foo& operator=(volatile const Foo&);
    // 将一个 volatile 对象赋值给一个 volatile 对象
    Foo& operator=(volatile const Foo&) volatile;
};

8.3 链接指示:extern “C”

C++ 程序有时需要调用其它语言编写的函数,最常见的是调用 C 语言编写的函数。像其他所有名字一样,其它语言中的函数也必须在 C++ 中进行声明,并且该声明必须指定返回类型和形参列表。对于其它语言比那些的程序来说,编译器检查其调用的方式与处理普通 C++ 函数的方式相同,但是生成的代码有所区别。C++ 使用链接指示指出任意非 C++ 函数所用的语言。
注: 要想把 C++ 代码和其他语言扁你恶的代码放在一起使用,要求我们必须有权访问该语言的编译器,并且这个编译器与当前的 C++ 编译器是兼容的。

声明一个非 C++ 函数

链接指示可以由两种形式:单个的和复合的。链接指示不能出现在类定义或函数定义的内部。同样的链接指示必须在函数的每个声明中都出现。

下面的声明显式了 cstring 头文件的某些 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" {
    #include <string.h>  // 操作 C 风格字符串的 C 函数
}
指向 extern “C” 函数的指针
// pf 指向一个 C 函数,该函数接受一个 int 返回 void
extern "C" void (*pf)(int);

指向 C 函数的指针与指向 C++ 函数的指针是不一样的类型。一个指向 C 函数的指针不能用在执行初始化或赋值操作后指向 C++ 函数,反之亦然。

void (*pf1)(int);  // 指向 C++ 函数
extern "C" void (*pf2)(int);  // 指向 C 函数
pf1 = pf2;  // 错误 pf1 和 pf2 的类型不同
链接指示对整个声明都有效

当我们使用链接指示时,它不仅对函数有效,而且对作为返回类型或形参类型的函数指针也有效:

// f1 是一个 C 函数,它的形参是一个指向 C 函数的指针
extern "C" void f1(void(*)(int));
导出 C++ 函数到其他语言

铜鼓哦使用链接指示对函数进行定义,我们可以另一个 C++ 函数在其他语言编写的程序中可用:

// calc 函数可以被 C 程序调用
extern "C" double  calc(double dparm) { /* ... */ }
重载函数与链接指示

链接指示与重载函数的相互作用依赖于目标语言。如果目标语言支持重载函数,则为该语言实现链接指示的编译器很可能也支持重载这些 C++ 的函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值