第十八章 特殊工具与技术
1. 优化内存分配
C++ 提供下面两种方法分配和释放未构造的原始内存:
1)allocator 类,它提供可感知类型的内存分配。这个类支持一个抽象接口,以分配内存并随后使用该内存保存对象。
2)标准库中的operator new 和 operator delete,它们分配和释放需要大小的原始的、未类型化的内存。
C++ 还提供不同的方法在原始内存中构造和撤销对象:
1)allocator 类定义了名为 construct 和 destroy 的成员,其操作正如它们的名字所指出的那样:construct 成员在未构造内存中初始化对象,destroy 成员在对象上运行适当的析构函数。
2)定位 new 表达式接受指向未构造内存的指针,并在该空间中初始化一个对象或一个数组。
3)可以直接调用对象的析构函数来撤销对象。运行析构函数并不释放对象所在的内存。
4)定制算法 uninitialized_fill 和 uninitialized_copy 像 fill 和 copy 算法一样执行,除了它们的目的地构造对象而不是给对象赋值之外。
现代 C++ 程序一般应该使用 allocator 类来分配内存,它更安全更灵活。但是,在构造对象的时候,用 new 表达式比 allocator::construct 成员更灵活。有几种情况下必须使用 new。
allocator 类:allocator 类是一个模板,它提供类型化的内存分配以及对象构造与撤销。
allocator 类将内存分配和对象构造分开。当 allocator 对象分配内存的时候,它分配适当大小并排列成保存给定类型对象的空间。但是,它分配的内存是未构造的,allocator 的用户必须分别 construct 和 destroy 放置在该内存中的对象。
allocator<T> a; //定义名为 a 的 allocator 对象,可以分配内存或构造 T 类型的对象
a.allocate(n) //分配原始的未构造内存以保存 T 类型的 n 个对象
a.construct(p, t) //在 T* 指针 p 所指内存中构造一个新元素。运行 T 类型的复制构造函数用 t 初始化该对象
a.destroy(p) //运行 T* 指针 p 所指对象的析构函数
a.deallocate(p, n) //释放内存,在名为 p 的 T* 指针中包含的地址处保存 T 类型的 n 个对象。运行调用 deallocate 之前在该内存中构造的任意对象的 destroy 是用户的责任
使用 allocator 管理类成员数据:vector 所用存储开始是未构造内存,它还没有保存任何对象。将元素复制或增加到这个预分配空间的时候,必须使用 allocator 类的 construct 成员构造元素。
实现 vector 的一小部分:
template <class T> class Vector {
public:
Vector(): elements(0), first_free(0), end(0) { }
void push_back(const T&);
// ...
private:
static std::allocator<T> alloc; // object to get raw memory
void reallocate(); // get more space and copy existing elements
T* elements; // pointer to first element in the array
T* first_free; // pointer to first free element in the array
T* end; // pointer to one past the end of the array
// ...
};
template <class T>
void Vector<T>::push_back(const T& t)
{
// are we out of space?
if (first_free == end)
reallocate(); // reallocate 分配新空间并复制现存元素,将指针重置为指向新分配的空间。
alloc.construct(first_free, t);
++first_free;
}
template <class T>
void Vector<T>::reallocate()
{
// compute size of current array and allocate space for twice as many elements
std::ptrdiff_t size = first_free - elements;
std::ptrdiff_t newcapacity = 2 * max(size, 1);
// allocate space to hold newcapacity number of elements of type T
T* newelements = alloc.allocate(newcapacity);
// construct copies of the existing elements in the new space
uninitialized_copy(elements, first_free, newelements);
// destroy the old elements in reverse order
for (T *p = first_free; p != elements; /* empty */ )
alloc.destroy(--p);
// deallocate cannot be called on a 0 pointer
if (elements)
// return the memory that held the elements
alloc.deallocate(elements, end - elements);
// make our data structure point to the new elements
elements = newelements;
first_free = elements + size;
end = elements + newcapacity;
}
operator new 函数和 operator delete 函数
当使用 new 表达式:
string * sp = new string("initialized");
的时候,实际上发生三个步骤。首先,该表达式调用名为 operator new 的标准库函数,分配足够大的原始的未类型化的内存,以保存指定类型的一个对象;接下来,运行该类型的一个构造函数,用指定初始化式构造对象;最后,返回指向新分配并构造的对象的指针。
当使用 delete 表达式:
delete sp;
删除动态分配对象的时候,发生两个步骤。首先,对 sp 指向的对象运行适当的析构函数;然后,通过调用名为 operator delete 的标准库函数释放该对象所用内存。
new 表达式与 operator new 函数:通过调用 operator new 函数执行 new 表达式获得内存,并接着在该内存中构造一个对象,通过撤销一个对象执行 delete 表达式,并接着调用 operator delete 函数,以释放该对象使用的内存。
operator new 和 operator delete 函数有两个重载版本,每个版本支持相关的 new 表达式和 delete 表达式:
void *operator new(size_t); // allocate an object
void *operator new[](size_t); // allocate an array
void *operator delete(void*); // free an object
void *operator delete[](void*); // free an array
operator new 和 operator delete 函数可以用来获得未构造内存,类似 allocate 类的 allocator 和 deallocate 成员。例如,代替使用 allocator 对象,可以在 Vector 类中使用 operator new 和 operator delete 函数:
// allocate space to hold newcapacity number of elements of type T
T* newelements = alloc.allocate(newcapacity);
这可以重新编写为
// allocate unconstructed memory to hold newcapacity elements of type T
T* newelements = static_cast<T*>
(operator new[](newcapacity * sizeof(T)));
类似地,在重新分配由 Vector 成员 elements 指向的旧空间:
// return the memory that held the elements
alloc.deallocate(elements, end - elements);
这可以重新编写为
// deallocate the memory that they occupied
operator delete[](elements);
这些函数的表现与 allocate 类的 allocator 和 deallocate 成员类似。但是,它们在一个重要方面有不同:它们在 void* 指针而不是类型化的指针上进行操作。一般而言,使用 allocator 比直接使用 operator new 和 operator delete 函数更为类型安全。allocate 成员分配类型化的内存,所以使用它的程序可以不必计算以字节为单位的所需内存量,它们也可以避免对 operator new 的返回值进行强制类型转换。类似地,deallocate 释放特定类型的内存,也不必转换为 void*。
定位 new 表达式:定位 new 表达式在已分配的原始内存中初始化一个对象,它与 new 的其他版本的不同之处在于,它不分配内存。相反,它接受指向已分配但未构造内存的指针,并在该内存中初始化一个对象。实际上,定位 new 表达式使我们能够在特定的、预分配的内存地址构造一个对象。
定位 new 表达式的形式是:
new (place_address) type
new (place_address) type (initializer-list)
其中 place_address 必须是一个指针,而 initializer-list 提供了(可能为空的)初始化列表,以便在构造新分配的对象时使用。
可以使用定位 new 表达式代替 Vector 实现中的 construct 调用。原来的代码
// construct a copy t in the element to which first_free points
alloc.construct (first_free, t);
可以用等价的定位 new 表达式代替
// copy t into element addressed by first_free
new (first_free) T(t);
定位 new 表达式比 allocator 类的 construct 成员更灵活。定位 new 表达式初始化一个对象的时候,它可以使用任何构造函数,并直接建立对象。construct 函数总是使用复制构造函数。例如,可以用下面两种方式之一,从一对迭代器初始化一个已分配但未构造的 string 对象:
allocator<string> alloc;
string *sp = alloc.allocate(2); // allocate space to hold 2 strings
// two ways to construct a string from a pair of iterators
new (sp) string(b, e); // construct directly in place
alloc.construct(sp + 1, string(b, e)); // build and copy a temporary
定位 new 表达式使用了接受一对迭代器的 string 构造函数,在 sp 指向的空间直接构造 string 对象。当调用 construct 函数的时候,必须首先从迭代器构造一个 string 对象,以获得传递给 construct 的 string 对象,然后,该函数使用 string 的复制构造函数,将那个未命名的临时 string 对象复制到 sp 指向的对象中。通常,这些区别是不相干的:对值型类而言,在适当的位置直接构造对象与构造临时对象并进行复制之间没有可观察到的区别,而且性能差别基本没有意义。但对某些类而言,使用复制构造函数是不可能的(因为复制构造函数是私有的),或者是应该避免的,在这种情况下,也许有必要使用定位 new 表达式。
显式析构函数的调用:正如定位 new 表达式是使用 allocate 类的 construct 成员的低级选择,可以使用析构函数的显式调用作为调用 destroy 函数的低级选择。
在使用 allocator 对象的 Vector 版本中,通过调用 destroy 函数清除每个元素:
// destroy the old elements in reverse order
for (T *p = first_free; p != elements; /* empty */ )
alloc.destroy(--p);
对于使用定位 new 表达式构造对象的程序,显式调用析构函数:
for (T *p = first_free; p != elements; /* empty */ )
p->~T(); // call the destructor
显式调用析构函数的效果是适当地清除对象本身。但是,并没有释放对象所占的内存,如果需要,可以重用该内存空间
调用 operator delete 函数不会运行析构函数,它只释放指定的内存。
类特定的 new 和 delete:默认情况下,new 表达式通过调用由标准库定义的 operator new 版本分配内存。通过定义自己的名为 operator new 和 operator delete 的成员,类可以管理用于自身类型的内存。优化 new 和 delete 的行为的时候,只需要定义 operator new 和 operator delete 的新版本,new 和 delete 表达式自己照管对象的构造和撤销。
类成员 operator new 函数必须具有返回类型 void* 并接受 size_t 类型的形参。由 new 表达式用以字节计算的分配内存量初始化函数的 size_t 形参。类成员 operator delete 函数必须具有返回类型 void。它可以定义为接受单个 void* 类型形参,也可以定义为接受两个形参,即 void* 和 size_t 类型。由 delete 表达式用被 delete 的指针初始化 void* 形参,该指针可以是空指针。如果提供了 size_t 形参,就由编译器用第一个形参所指对象的字节大小自动初始化 size_t 形参。
除非类是某继承层次的一部分,否则形参 size_t 不是必需的。当 delete 指向继承层次中类型的指针时,指针可以指向基类对象,也可以指向派生类对象。派生类对象的大小一般比基类对象大。如果基类有 virtual 析构函数,则传给 operator delete 的大小将根据被删除指针所指对象的动态类型而变化;如果基类没有 virtual 析构函数,那么,通过基类指针删除指向派生类对象的指针的行为,跟往常一样是未定义的。
这些函数隐式地为静态函数,不必显式地将它们声明为 static,虽然这样做是合法的。成员 new 和 delete 函数必须是静态的,因为它们要么在构造对象之前使用(operator new),要么在撤销对象之后使用(operator delete),像任意其他静态成员函数一样,new 和 delete 只能直接访问所属类的静态成员
操作符 new[] 和操作符 delete[]:也可以定义成员 operator new[] 和 operator delete[] 来管理类类型的数组。如果这些 operator 函数存在,编译器就使用它们代替全局版本。
类成员 operator new[] 必须具有返回类型 void*,并且接受的第一个形参类型为 size_t。用表示存储特定类型给定数目元素的数组的字节数值自动初始化操作符的 size_t 形参。成员操作符 operator delete[] 必须具有返回类型 void,并且第一个形参为 void* 类型。用表示数组存储起始位置的值自动初始化操作符的 void* 形参。类的操作符 delete[] 也可以有两个形参,第二个形参为 size_t。如果提供了附加形参,由编译器用数组所需存储量的字节数自动初始化这个形参。
一个内存分配器基类:CachedObj 类【Coding】
2. 运行时类型识别
通过运行时类型识别(RTTI),程序能够使用基类的指针或引用来检索这些指针或引用所指对象的实际派生类型。通过下面两个操作符提供 RTTI:
1)typeid 操作符,返回指针或引用所指对象的实际类型。
2)dynamic_cast 操作符,将基类类型的指针或引用安全地转换为派生类型的指针或引用。
这些操作符只为带有一个或多个虚函数的类返回动态类型信息,对于其他类型,返回静态(即编译时)类型的信息。对于带虚函数的类,在运行时执行 RTTI 操作符,但对于其他类型,在编译时计算 RTTI 操作符。
通常,从基类指针获得派生类行为最好的方法是通过虚函数。但是,在某些情况下,不可能使用虚函数。在这些情况下,RTTI 提供了可选的机制。然而,这种机制比使用虚函数更容易出错:程序员必须知道应该将对象强制转换为哪种类型,并且必须检查转换是否成功执行了。
dynamic_cast 操作符:可以使用 dynamic_cast 操作符将基类类型对象的引用或指针转换为同一继承层次中其他类型的引用或指针。与 dynamic_cast 一起使用的指针必须是有效的——它必须为 0 或者指向一个对象。与其他强制类型转换不同,dynamic_cast 涉及运行时类型检查。如果绑定到引用或指针的对象不是目标类型的对象,则 dynamic_cast 失败。如果转换到指针类型的 dynamic_cast 失败,则 dynamic_cast 的结果是 0 值;如果转换到引用类型的 dynamic_cast 失败,则抛出一个 bad_cast 类型的异常。
假定 Base 是至少带一个虚函数的类,并且 Derived 类派生于 Base 类。如果有一个名为 basePtr 的指向 Base 的指针,就可以像这样在运行时将它强制转换为指向 Derived 的指针:
if (Derived *derivedPtr = dynamic_cast<Derived*>(basePtr))
在运行时,如果 basePtr 实际指向 Derived 对象,则转换将成功,并且 derivedPtr 将被初始化为指向 basePtr 所指的 Derived 对象;否则,转换的结果是 0,意味着将 derivedPtr 置为 0,并且 if 中的条件失败。
也可以使用 dynamic_cast 将基类引用转换为派生类引用,这种 dynamic_cast 操作的形式如下:
dynamic_cast< Type& >(val)
这里,Type 是转换的目标类型,而 val 是基类类型的对象。当转换失败的时候,它抛出一个 std::bad_cast 异常,该异常在库头文件 typeinfo 中定义。
typeid 操作符:typeid(e),这里 e 是任意表达式或者是类型名。typeid 操作符可以与任何类型的表达式一起使用。内置类型的表达式以及常量都可以用作 typeid 操作符的操作数。如果操作数不是类类型或者是没有虚函数的类,则 typeid 操作符指出操作数的静态类型;如果操作数是定义了至少一个虚函数的类类型,则在运行时计算类型。typeid 操作符的结果是名为 type_info 的标准库类型的对象引用,要使用 type_info 类,必须包含库头文件 typeinfo
Base *bp;
Derived *dp;
// compare type at run time of two objects
if (typeid(*bp) == typeid(*dp)) {
// bp and dp point to objects of the same type
}
// test whether run time type is a specific type
if (typeid(*bp) == typeid(Derived)) {
// bp actually points to a Derived
}
第一个 if 中,比较 bp 所指对象与 dp 所指对象的实际类型,如果它们指向同一类型,则测试成功。类似地,如果 bp 当前指向 Derived 对象,则第二个 if 成功。
注意,typeid 的操作数是表示对象的表达式——测试 *bp,而不是 bp:测试指针(相对于指针指向的对象)返回指针的静态的、编译时类型。
// test always fails: The type of bp is pointer to Base
if (typeid(bp) == typeid(Derived)) {
// code never executed
}
如果指针 p 的值是 0,那么,如果 p 的类型是带虚函数的类型,则 typeid(*p) 抛出一个 bad_typeid 异常;如果 p 的类型没有定义任何虚函数,则结果与 p 的值是不相关的。正像计算表达式 sizeof一样,编译器不计算 *p,它使用 p 的静态类型,这并不要求 p 本身是有效指针
RTTI 的使用,设计一个类层次。【Coding】
3. 类成员的指针
成员指针包含类的类型以及成员的类型。成员指针只应用于类的非 static 成员。static 类成员不是任何对象的组成部分,所以不需要特殊语法来指向 static 成员,static 成员指针是普通指针。
class Screen {
public:
typedef std::string::size_type index;
char get() const;
char get(index ht, index wd) const;
private:
std::string contents;
index cursor;
index height, width;
};
Screen 类的 contents 成员的类型为 std::string。contents 的完全类型是“Screen 类的成员,其类型是 std::string”。因此,可以指向 contents 的指针的完全类型是“指向 std::string 类型的 Screen 类成员的指针”,这个类型可写为
string Screen::*
可以将指向 Screen 类的 string 成员的指针定义为
string Screen::*ps_Screen;
可以用 contents 的地址初始化 ps_Screen,代码为
string Screen::*ps_Screen = &Screen::contents;
成员函数的指针必须在三个方面与它所指函数的类型相匹配:
1)函数形参的类型和数目,包括成员是否为 const。
2)返回类型。
3)所属类的类型。
例如,不接受形参的 get 版本的 Screen 成员函数的指针具有如下类型:
char (Screen::*)() const
// pmf points to the Screen get member that takes no arguments
char (Screen::*pmf)() const = &Screen::get;
也可以将带两个形参的 get 函数版本的指针定义为
char (Screen::*pmf2)(Screen::index, Screen::index) const;
pmf2 = &Screen::get;
包围 Screen::* 的括号是必要的,没有这个括号,编译器就将下面代码当作(无效的)函数声明:
// error: non-member function p cannot have const qualifier
char Screen::*p() const;
为成员指针使用类型别名:下面的类型别名将 Action 定义为带两个形参的 get 函数版本的类型的另一名字:
// Action is a type name
typedef char (Screen::*Action)(Screen::index, Screen::index) const;
Action get = &Screen::get;
使用类成员指针:类似于成员访问操作符 . 和 ->,.* 和 .-> 是两个新的操作符,它们使我们能够将成员指针绑定到实际对象。这两个操作符的左操作数必须是类类型的对象或类类型的指针,右操作数是该类型的成员指针。
1)成员指针解引用操作符(.*)从对象或引用获取成员。
2)成员指针箭头操作符(->*)通过对象的指针获取成员。
// pmf points to the Screen get member that takes no arguments
char (Screen::*pmf)() const = &Screen::get;
Screen myScreen;
char c1 = myScreen.get(); // call get on myScreen
char c2 = (myScreen.*pmf)(); // equivalent call to get
Screen *pScreen = &myScreen;
c1 = pScreen->get(); // call get on object to which pScreen points
c2 = (pScreen->*pmf)(); // equivalent call to get
因为调用操作符(())比成员指针操作符优先级高,所以调用 (myScreen.*pmf)() 和 (pScreen->*pmf)() 需要括号。
也可以在通过成员函数指针进行的调用中传递实参:
char (Screen::*pmf2)(Screen::index, Screen::index) const;
pmf2 = &Screen::get;
Screen myScreen;
char c1 = myScreen.get(0,0); // call two-parameter version of get
char c2 = (myScreen.*pmf2)(0,0); // equivalent call to get
成员指针操作符用于访问数据成员:
Screen::index Screen::*pindex = &Screen::width;
Screen myScreen;
// equivalent ways to fetch width member of myScreen
Screen::index ind1 = myScreen.width; // directly
Screen::index ind2 = myScreen.*pindex; // dereference to get width
Screen *pScreen;
// equivalent ways to fetch width member of *pScreen
ind1 = pScreen->width; // directly
ind2 = pScreen->*pindex; // dereference pindex to get width
成员指针函数表
4. 嵌套类
可以在另一个类内部定义一个类,这样的类是嵌套类,也称为嵌套类型。嵌套类最常用于定义执行类。嵌套类是独立的类,基本上与它们的外围类不相关,因此,外围类和嵌套类的对象是互相独立的。嵌套类型的对象不具备外围类所定义的成员,同样,外围类的成员也不具备嵌套类所定义的成员。嵌套类的名字在其外围类的作用域中可见,但在其他类作用域或定义外围类的作用域中不可见。嵌套类的名字将不会与另一作用域中声明的名字冲突。嵌套类定义了其外围类中的一个类型成员。像任何其他成员一样,外围类决定对这个类型的访问。在外围类的 public 部分定义的嵌套类定义了可在任何地方使用的类型,在外围类的 protected 部分定义的嵌套类定义了只能由外围类、友元或派生类访问的类型,在外围类的 private 部分定义的嵌套类定义了只能被外围类或其友元访问的类型。
template <class Type> class Queue {
// interface functions to Queue are unchanged
private:
// public members are ok: QueueItem is a private member of Queue
// only Queue and its friends may access the members of QueueItem
struct QueueItem {
QueueItem(const Type &);
Type item; // value stored in this element
QueueItem *next; // pointer to next element in the Queue
};
QueueItem *head; // pointer to first element in Queue
QueueItem *tail; // pointer to last element in Queue
};
在其类外部定义的嵌套类成员,必须定义在定义外围类的同一作用域中。在其类外部定义的嵌套类的成员,不能定义在外围类内部,嵌套类的成员不是外围类的成员。
// defines the QueueItem constructor
// for class QueueItem nested inside class Queue<Type>
template <class Type>
Queue<Type>::QueueItem::QueueItem(const Type &t): item(t), next(0) { }
在外围类外部定义嵌套类:
template <class Type> class Queue {
// interface functions to Queue are unchanged
private:
struct QueueItem; // forward declaration of nested type QueueItem
QueueItem *head; // pointer to first element in Queue
QueueItem *tail; // pointer to last element in Queue
};
template <class Type>
struct Queue<Type>::QueueItem {
QueueItem(const Type &t): item(t), next(0) { }
Type item; // value stored in this element
QueueItem *next; // pointer to next element in the Queue
};
在看到在类定义体外部定义的嵌套类的实际定义之前,该类是不完全类型,应用所有使用不完全类型的常规限制。
嵌套类作用域中的名字查找:
class Outer {
public:
struct Inner {
// ok: reference to incomplete class
void process(const Outer&);
Inner2 val; // error: Outer::Inner2 not in scope
};
class Inner2 {
public:
// ok: Inner2::val used in definition
Inner2(int i = 0): val(i) { }
// ok: definition of process compiled after enclosing class is complete
void process(const Outer &out) { out.handle(); }
private:
int val;
};
void handle() const; // member of class Outer
};
5. 联合:节省空间的类
联合是一种特殊的类。一个 union 对象可以有多个数据成员,但在任何时刻,只有一个成员可以有值。当将一个值赋给 union 对象的一个成员的时候,其他所有都变为未定义的。使用 union 对象时,我们必须总是知道 union 对象中当前存储的是什么类型的值。通过错误的数据成员检索保存在 union 对象中的值,可能会导致程序崩溃或者其他不正确的程序行为。像任何类一样,一个 union 定义了一个新的类型。
联合提供了便利的办法表示一组相互排斥的值,这些值可以是不同类型的。
union TokenValue {
char cval;
int ival;
double dval;
};
每个 union 对象的大小在编译时固定的:它至少与 union 的最大数据成员一样大。
没有静态数据成员、引用成员或类数据成员:像任何类一样,union 可以指定保护标记使成员成为公用的、私有的或受保护的。默认情况下,union 表现得像 struct:除非另外指定,否则 union 的成员都为 public 成员。union 也可以定义成员函数,包括构造函数和析构函数。但是,union 不能作为基类使用,所以成员函数不能为虚数。union 不能具有静态数据成员或引用成员,而且,union 不能具有定义了构造函数、析构函数或赋值操作符的类类型的成员:
union illegal_members {
Screen s; // error: has constructor
static int is; // error: static member
int &rfi; // error: reference member
Screen *ps; // ok: ordinary built-in pointer type
};
union 的名字是一个类型名:
TokenValue first_token = {'a'}; // initialized TokenValue
TokenValue last_token; // uninitialized TokenValue object
TokenValue *pt = new TokenValue; // pointer to a TokenValue object
默认情况下 union 对象是未初始化的。可以用与显式初始化简单类对象一样的方法显式初始化 union 对象。但是,只能为第一个成员提供初始化式。该初始化式必须括在一对花括号中。
可以使用普通成员访问操作符(. 和 ->)访问 union 类型对象的成员:
last_token.cval = 'z';
pt->ival = 42;
避免通过错误成员访问 union 值的最佳办法是,定义一个单独的对象跟踪 union 中存储了什么值。这个附加对象称为 union 的判别式。
union 最经常用作嵌套类型,其中判别式是外围类的一个成员:
class Token {
public:
// indicates which kind of value is in val
enum TokenKind {INT, CHAR, DBL};
TokenKind tok;
union { // unnamed union
char cval;
int ival;
double dval;
} val; // member val is a union of the 3 listed types
};
经常使用 switch 语句测试判别式,然后根据 union 中当前存储的值进行处理。
匿名联合:匿名 union 的成员的名字出现在外围作用域中。
class Token {
public:
// indicates which kind of token value is in val
enum TokenKind {INT, CHAR, DBL};
TokenKind tok;
union { // anonymous union
char cval;
int ival;
double dval;
};
};
因为匿名 union 不提供访问其成员的途径,所以将成员作为定义匿名 union 的作用域的一部分直接访问。如下:
Token token;
switch (token.tok) {
case Token::INT:
token.ival = 42; break;
case Token::CHAR:
token.cval = 'a'; break;
case Token::DBL:
token.dval = 3.14; break;
}
6. 局部类
可以在函数体内部定义类,这样的类称为局部类。一个局部类定义了一个类型,该类型只在定义它的局部作用域中可见。与嵌套类不同,局部类的成员是严格受限的。局部类的所有成员(包括函数)必须完全定义在类定义体内部。不允许局部类声明 static 数据成员。
局部类不能使用函数作用域中的变量。局部类可以访问的外围作用域中的名字是有限的。局部类只能访问在外围作用域中定义的类型名、static 变量和枚举成员,不能使用定义该类的函数中的变量:
int a, val;
void foo(int val)
{
static int si;
enum Loc { a = 1024, b };
// Bar is local to foo
class Bar {
public:
Loc locVal; // ok: uses local type name
int barVal;
void fooBar(Loc l = a) // ok: default argument is Loc::a
{
barVal = val; // error: val is local to foo
barVal = ::val; // ok: uses global object
barVal = si; // ok: uses static local object
locVal = b; // ok: uses enumerator
}
};
// ...
}
外围函数对局部类的私有成员没有特殊访问权。不过局部类可以将外围函数设为友元。
局部类中 private 成员几乎是不必要的,通常局部类的所有成员都为 public 成员。
局部类中的名字查找:局部类定义体中的名字查找方式与其他类的相同。类成员声明中所用的名字必须在名字使用之前出现在作用域中,成员定义中所用的名字可以出现在局部类作用域的任何地方。没有确定为类成员的名字首先在外围局部作用域中进行查找,然后在包围函数本身的作用域中查找。
嵌套的局部类:可以将一个类嵌套在局部类内部。这种情况下,嵌套类定义可以出现在局部类定义体之外,但是,嵌套类必须在定义局部类的同一作用域中定义。照常,嵌套类的名字必须用外围类的名字进行限定,并且嵌套类的声明必须出现在局部类的定义中:
void foo()
{
class Bar {
public:
// ...
class Nested; // declares class Nested
};
// definition of Nested
class Bar::Nested {
// ...
};
}
嵌套在局部类中的类本身是一个带有所有附加限制的局部类。嵌套类的所有成员必须在嵌套类本身定义体内部定义。
7. 固有的不可移植的特征
位域:当程序需要将二进制数据传递给另一程序或硬件设备的时候,通常使用位域。位域在内存中的布局是机器相关的。位域必须是整型数据类型,可以是 signed 或 unsigned。通过在成员名后面接一个冒号以及指定位数的常量表达式,指出成员是一个位域:
typedef unsigned int Bit;
class File {
Bit mode: 2;
Bit modified: 1;
Bit prot_owner: 3;
Bit prot_group: 3;
Bit prot_world: 3;
// ...
};
mode 位域有两个位,modified 只有一位,其他每个成员有三个位。通常最好将位域设为 unsigned 类型。存储在 signed 类型中的位域的行为由实现定义。位域不能是类的静态成员。
通常使用内置按位操作符操纵超过一位的位域:
enum { READ = 01, WRITE = 02 }; // File modes
int main() {
File myFile;
myFile.mode |= READ; // set the READ bit
if (myFile.mode & READ) // if the READ bit is on
cout << "myFile.mode READ is set\n";
}
volatile 限定符:volatile 的确切含义与机器相关,只能通过阅读编译器文档来理解。使用 volatile 的程序在移到新的机器或编译器时通常必须改变。
直接处理硬件的程序常具有这样的数据成员,它们的值由程序本身直接控制之外的过程所控制。例如,程序可以包含由系统时钟更新的变量。当可以用编译器的控制或检测之外的方式改变对象值的时候,应该将对象声明为 volatile。关键字 volatile 是给编译器的指示,指出对这样的对象不应该执行优化。
用与 const 限定符相同的方式使用 volatile 限定符。volatile 限定符是一个对类型的附加修饰符:
volatile int display_register;
volatile Task *curr_task;
volatile int ixa[max_size];
volatile Screen bitmap_buf;
用与定义 const 成员函数相同的方式,类也可以将成员函数定义为 volatile,volatile 对象只能调用 volatile 成员函数。
对待 const 和 volatile 的一个重要区别是,不能使用合成的复制和赋值操作符从 volatile 对象进行初始化或赋值。合成的复制控制成员接受 const 形参,这些形参是对类类型的 const 引用,但是,不能将 volatile 对象传递给普通引用或 const 引用。
如果类希望允许复制 volatile 对象,或者,类希望允许从 volatile 操作数或对 volatile 操作数进行赋值,它必须定义自己的复制构造函数和/或赋值操作符版本:
class Foo {
public:
Foo(const volatile Foo&); // copy from a volatile object
// assign from a volatile object to a non volatile objet
Foo& operator=(volatile const Foo&);
// assign from a volatile object to a volatile object
Foo& operator=(volatile const Foo&) volatile;
// remainder of class Foo
};
链接指示 extern "C":调用用其他程序设计语言编写的函数,像任何名字一样,必须声明用其他语言编写的函数的名字,该声明必须指定返回类型和形参表。编译器按处理普通 C++ 函数一样的方式检查对外部语言函数的调用,但是,编译器一般必须产生不同的代码来调用用其他语言编写的函数。C++ 使用链接指示指出任意非 C++ 函数所用的语言。
声明非 C++ 函数:链接指示有两种形式:单个的或复合的。链接指示不能出现在类定义或函数定义的内部,它必须出现在函数的第一次声明上
头文件 cstdlib 中声明的一些 C 函数:
// illustrative linkage directives that might appear in the C++ header <cstring>
// single statement linkage directive
extern "C" size_t strlen(const char *);
// compound statement linkage directive
extern "C" {
int strcmp(const char*, const char*);
char *strcat(char*, const char*);
}
第一种形式由关键字 extern 后接字符串字面值,再接“普通”函数声明构成。字符串字面值指出编写函数所用的语言。第二种形式通过将几个函数的声明放在跟在链接指示之后的花括号内部,可以给它们设定相同的链接。花括号的作用是将应用链接指示的声明聚合起来,忽略了花括号,花括号中声明的函数名就是可见的,就像在花括号之外声明函数一样
可以将多重声明形式应用于整个头文件。假定头文件中的所有普通函数声明都是用链接指示的语言编写的函数。例如,C++ 的 cstring 头文件可以像这样:
// compound statement linkage directive
extern "C" {
#include <string.h> // C functions that manipulate C-style strings
}
导出 C++ 函数到其他语言:通过对函数定义使用链接指示,使得用其他语言编写的程序可以使用 C++ 函数:
// the calc function can be called from C programs
extern "C" double calc(double dparm) { /* ... */ }
当编译器为该函数产生代码的时候,它将产生适合于指定语言的代码。
用链接指示定义的函数的每个声明都必须使用相同的链接指示。
有时需要在 C 和 C++ 中编译同一源文件。当编译 C++ 时,自动定义预处理器名字 __cplusplus(两个下划线),所以,可以根据是否正在编译 C++ 有条件地包含代码。
#ifdef __cplusplus
// ok: we're compiling C++
extern "C"
#endif
int strcmp(const char*, const char*);
重载函数与链接指示:如果语言支持重载函数,则为该语言实现链接指示的编译器很可能也支持 C++ 的这些函数的重载。C++ 保证支持的唯一语言是 C。C 语言不支持函数重载。在一组重载函数中只能为一个 C 函数指定链接指示。用带给定名字的 C 链接声明多于一个函数是错误的:
// error: two extern "C" functions in set of overloaded functions
extern "C" void print(const char*);
extern "C" void print(int);
在 C++ 程序中,重载 C 函数很常见,但是,重载集合中的其他函数必须都是 C++ 函数:
class SmallInt { /* ... */ };
class BigNum { /* ... */ };
// the C function can be called from C and C++ programs
// the C++ functions overload that function and are callable from C++
extern "C" double calc(double);
extern SmallInt calc(const SmallInt&);
extern BigNum calc(const BigNum&);
可以从 C 程序和 C++ 程序调用 calc 的 C 版本。其余函数是带类型形参的 C++ 函数,只能从 C++ 程序调用。声明的次序不重要。
编写函数所用的语言是函数类型的一部分。为了声明用其他程序设计语言编写的函数的指针,必须使用链接指示:
// pf points to a C function returning void taking an int
extern "C" void (*pf)(int);
C 函数的指针与 C++ 函数的指针具有不同的类型,不能将 C 函数的指针初始化或赋值为 C++ 函数的指针(反之亦然)。
应用于整个声明的链接指示:使用链接指示的时候,它应用于函数和任何函数指针,作为返回类型或形参类型使用:
// f1 is a C function; its parameter is a pointer to a C function
extern "C" void f1(void(*)(int));
因为链接指示应用于一个声明中的所有函数,所以必须使用类型别名,以便将 C 函数的指针传递给 C++ 函数:
// FC is a pointer to C function
extern "C" typedef void FC(int);
// f2 is a C++ function with a parameter that is a pointer to a C function
void f2(FC *);