免费建设网站怎么样,新闻型网站建设,东莞专业微网站建设价格低,咸宁抖音seo收费标准文章目录特殊工具与技术19.1控制内存分配19.1.1重载new和deleteoperator new接口和operator delete接口malloc函数与free函数19.1.2定位new表达式显式的析构函数调用19.2运行时类型识别(run-time type identification, RTTI)19.2.1dynamic_cast运算符指针类型的dynamic_cast引用…
文章目录特殊工具与技术19.1控制内存分配19.1.1重载new和deleteoperator new接口和operator delete接口malloc函数与free函数19.1.2定位new表达式显式的析构函数调用19.2运行时类型识别(run-time type identification, RTTI)19.2.1dynamic_cast运算符指针类型的dynamic_cast引用类型的dynamic_cast19.2.2typeid运算符使用typeid运算符19.2.3使用RTTI19.2.4type_info类19.3枚举类型枚举成员和类一样枚举也定义新的类型指定enum的大小枚举类型的前置声明形参匹配与枚举类型19.4类成员指针19.4.1数据成员指针使用数据成员指针返回数据成员指针的函数19.4.2成员函数指针使用成员函数指针使用成员指针的类型别名成员指针函数表19.4.3将成员函数用作可调用对象使用function生成一个可调用对象使用mem_fn生成一个可调用对象使用bind生成一个可调用对象19.5嵌套类声明一个嵌套类在外层类之外定义一个嵌套类定义嵌套类的成员嵌套类的静态成员定义嵌套类作用域中的名字查找嵌套类和外层类是相互独立的19.6union(联合)一种节省空间的类定义union使用union类型匿名union含有类类型成员的union使用类管理union成员管理判别式并销毁string管理需要拷贝控制的联合成员19.7局部类局部类不能使用函数作用域中的变量常规的访问保护规则对局部类同样适用局部类中的名字查找嵌套的局部类特殊工具与技术
19.1控制内存分配 某些应用程序对内存分配有特殊的需求需要自定义内存分配的细节比如使用new将对象放置在特定的内存空间中。因此需要重载new和delete运算符以控制内存分配的过程。 19.1.1重载new和delete 当使用new表达式时 string *sp new string(a value); // 分配并初始化一个string对象
string *arr new string[10]; // 分配10个默认初始化的string对象实际执行了三步操作 调用名为operator new(或者operator new[])的标准库函数分配一块足够大的、原始的、未命名的内存空间以便存储特定类型的对象(或者对象数组)。运行相应的构造函数以构造这些对象并传入初始值。返回指向该对象的指针。 当使用delete表达式时 delete sp; // 销毁*sp然后释放sp指向的内存空间。
delete[] arr; // 销毁数组中的元素然后释放对应的内存空间。实际执行了两步操作 对所指对象或数组中的元素执行对应的析构函数。调用名为operator delete(或者operator delete[])的标准库函数释放内存空间。 因此如果应用程序希望控制内存分配的过程则需要定义自己的operator new和operator delete函数。编译器会优先使用自定义的版本。 当编译器发现一条new表达式或delete表达式后将在程序中查找可供调用的operator函数。如果被分配(释放)的对象是类类型则编译器首先在类及其基类的作用域中查找。此时如果该类含有相应的成员则进行调用。否则编译器在全局作用域中查找。此时如果编译器找到了自定义的版本则进行调用如果没找到则使用标准库定义的版本。 operator new接口和operator delete接口 标准库定义了operator new和operator delete函数的8个重载版本。当将这些运算符函数定义成类的成员时它们是隐式静态的而且不能操纵类的任何数据成员因为operator new用在对象构造之前而operator delete用在对象销毁之后 // 这些版本可能抛出异常。与析构函数类似operator delete也不允许抛出异常。
void *operator new(size_t); // 分配一个对象
void *operator new[](size_t); // 分配一个数组
void operator delete(void *) noexcept; // 释放一个对象
void operator delete[](void *) noexcept; // 释放一个数组// 这些版本承诺不会抛出异常通过定义在new头文件中的nothrow对象请求非抛出版本。
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 new[]函数来说返回类型必须是void*第一个形参类型必须是size_t且不能含有默认实参当给对象或者数组分配空间时传入所需的字节数。 如果想要自定义operator new函数则可以提供额外的形参。此时必须使用new的定位形式进行传递。 需要注意的是下面这个函数无论如何不能被用户重载 // 只供标准库使用
void *operator new(size_t, void*);对于delete new或者delete new[]函数来说返回类型必须是void第一个形参的类型必须是void*指向待释放的内存。 如果定义成类的成员时该函数可以包含类型为size_t的形参初始值是对象的字节数。size_t形参可用于删除继承体系中的对象。如果基类有一个虚析构函数则传递给operator delete的字节数将因待删除指针所指对象的动态类型不同而有所区别。而且实际运行的operator delete函数版本也由对象的动态类型决定。 需要注意的是提供新的operator new和operator delete函数的目的在于改变内存分配的方式但是不管怎样都不能改变new和delete运算符的基本含义。 malloc函数与free函数
// 编写operator new和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);
}19.1.2定位new表达式 尽管operator new和operator delete函数一般用于new表达式然而它们毕竟是标准库的两个普通函数因此普通的代码也可以直接调用它们。 在c的早期版本中应用程序如果想把内存分配与初始化分离开来的话需要调用operator new和operator delete。它们负责分配或释放内存空间但是不会构造或销毁对象。此时应该使用new的**定位new**形式构造对象从而为分配函数提供额外的信息 // place_address必须是一个指针。
// initializers提供一个(可能为空的)以逗号分隔的初始值列表将用于构造新分配的对象。
new(place_address) type
new(place_address) type(initializers)
new(place_address) type[size]
new(place_address) type[size] { braced initializer list }当仅通过一个地址值调用时定位new使用operator new(size_t, void*)。这是一个无法自定义的版本。该函数不分配任何内存只是简单地返回指针实参然后由new表达式负责在指定的地址初始化对象以完成整个工作。事实上定位new允许在一个特定的、预先分配的内存地址上构造对象。 定位new与allocator的contruct成员一个重要的区别是传给construct的指针必须指向同一个allocator对象分配的空间但是传给定位new的指针无须指向operator new分配的内存。实际上传给定位new表达式的指针甚至不需要指向动态内存。 显式的析构函数调用 既可以通过对象调用析构函数也可以通过对象的指针或引用调用 string *sp new string(a value);
sp-~string();调用析构函数会销毁对象但是不会释放内存。如果需要的话可以重新使用该空间。 19.2运行时类型识别(run-time type identification, RTTI) 运行时类型识别的功能由两个运算符实现 typeid用于返回表达式的类型。dynamic_cast用于将基类的指针或引用安全地转换成派生类的指针或引用。 当这两个运算符用于某种类型的指针或引用并且该类型含有虚函数时运算符将使用指针或引用所绑定对象的动态类型。 这两个运算符特别适用于以下情况想使用基类对象的指针或引用执行某个派生类操作并且该操作不是虚函数。一般来说只要有可能应该尽量使用虚函数。 然而并非任何时候都能定义一个虚函数。假设无法使用虚函数则可以使用一个RTTI运算符。另一方面与虚成员函数相比使用RTTI运算符蕴含着更多潜在的风险程序员必须清楚地知道转换的目标类型并且必须检查类型转换是否被成功执行。 19.2.1dynamic_cast运算符 dynamic_cast运算符的使用形式 // type必须是一个类类型并且通常情况下该类型应该含有虚函数。// e必须是一个有效的指针
dynamic_casttype*(e)
// e必须是一个左值
dynamic_casttype(e)
// e不能是左值
dynamic_casttype(e)e的类型必须符合以下条件中的任意一个 e的类型是目标type的公有派生类。e的类型是目标type的公有基类。e的类型就是目标type的类型。 如果符合则类型转换可以成功。否则转换失败。如果转换目标是指针类型并且失败了则结果为0。如果转换目标是引用类型并且失败了则抛出一个bad_cast异常。 指针类型的dynamic_cast
// 假定Base类至少含有一个虚函数Derived是Base的公有派生类。
// 如果有一个指向Base的指针bp则可以在运行时将它转换成指向Derived的指针。// 在条件部分定义了dp好处是可以在一个操作中同时完成类型转换和条件检查两项任务。
// 而且dp在if外部是不可访问的。一旦转换失败即使后续的代码忘了做相应判断也不会
// 接触到这个未绑定的指针从而确保程序是安全的。
if (Derived *dp dynamic_castDerived*(bp)) {// 使用dp指向的Derived对象
} else {// 使用bp指向的Base对象
}可以对一个空指针执行dynamic_cast结果是所需类型的空指针。 引用类型的dynamic_cast 因为不存在所谓的空引用所以对于引用类型来说无法使用与指针类型完全相同的错误报告策略。 void f(const Base b) {try {const Derived d dynamic_castconst Derived(b);// 使用b引用的Derived对象} catch(bad_cast) {// 处理类型转换失败的情况}
}19.2.2typeid运算符 typeid的结果是一个常量对象的引用该对象的类型是标准库类型type_info或者type_info的公有派生类型。 // 向表达式提问对象是什么类型
// e可以是任意表达式或类型的名字
typeid(e)顶层const被忽略如果表达式是一个引用则typeid返回该引用所引对象的类型。不过当typeid作用于数组或函数时并不会执行向指针的标准类型转换即typeid(arr)所得的结果是数组而非指针类型。 当运算对象不属于类类型或者是一个不包含任何虚函数的类时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的类型是指向Base的指针。
if (typeid(bp) typeid(Derived)) {// 此处的代码永远不会执行
}如果表达式的动态类型可能与静态类型不同则必须在运行时对表达式求值以确定返回的类型。 // 如果p所指的类型不含有虚函数则p不必非得是一个有效的指针。否则
// *p将在运行时求值此时p必须是一个有效的指针。如果p是一个空指针
// 则typeid(*p)将抛出一个名为bad_typeid的异常。
typeid(*p)19.2.3使用RTTI 在某些情况下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函数
// 将只能使用基类的成员而不能比较派生类独有的成员。因此需要借助RTTI
// 来解决上述问题。
bool Derived::equal(const Base rhs) const {// 此时清楚这两个类型是相等的所以转换过程不会抛出异常。auto r dynamic_castconst Derived(rhs);// 执行比较两个Derived对象的操作并返回结果
}// 无须类型转换*this和形参都是Base类型。
bool Base::equal(const Base rhs) const {// 执行比较Base对象的操作
}19.2.4type_info类 type_info的精确定义随着编译器的不同而略有差异。不过c标准规定其必须定义在typeinfo头文件中并且至少提供以下操作 type_info一般作为一个基类出现所以应该提供一个公有的虚析构函数。当编译器希望提供额外的类型信息时通常在type_info的派生类中完成。type_info没有默认构造函数而且它的拷贝和移动构造函数以及赋值运算符都被定义为删除的。因此创建type_info对象的唯一途径是使用typeid运算符。 19.3枚举类型 枚举属于字面值常量类型可以将一组整型常量组织在一起。C包含两种枚举限定作用域的和不限定作用域的。 // 限定作用域
enum class open_modes { input, output, append };
// 不限定作用域
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的枚举成员不在有效的作用域中。color::green在有效的作用域中但是类型错误。
peppers p green;
color hair color::red; // 正确允许显式地访问枚举成员。
peppers p2 peppers::red; // 正确使用peppers的red。默认情况下枚举值从0开始依次加1。不过也可以指定专门的值。如果没有显式地提供初始值则当前枚举成员的值等于之前枚举成员的值加1。 enum class intTypes {charTyp 8, shortTyp 16, intTyp 16,longTyp 32, long_longTyp 64
};枚举成员是const因此在初始化枚举成员时提供的初始值必须是常量表达式。因此 可以定义枚举类型的constexpr变量 constexpr intTypes charbits intTypes::charTyp;可以将一个enum作为switch的表达式而将枚举值作为case标签。还能将枚举类型作为一个非类型模板形参使用。或者在类的定义中初始化枚举类型的静态数据成员。 和类一样枚举也定义新的类型
open_modes om 2; // 错误2不属于类型open_modes。
om open_modes::input; // 正确input是open_modes的一个枚举成员。一个不限定作用域的枚举类型的对象或枚举成员自动地转换成整型 int i color::red; // 正确不限定作用域的枚举类型的枚举成员隐式地转换成int。
int j peppers::red; // 错误限定作用域的枚举类型不会进行隐式转换。指定enum的大小 尽管每个enum都定义了唯一的类型但实际上enum是由某种整数类型表示的。 在c11新标准中可以在enum的名字后加上冒号以及想在该enum中使用的类型 enum intValues : unsigned long long {charTyp 255, shortTyp 65535, intTyp 65535,longTyp 4294967295UL,long_longTyp 18446744073709551615ULL
};如果没有指定enum的潜在类型则默认情况下限定作用域的enum成员类型是int。不限定作用域的枚举类型的枚举成员不存在默认类型只知道成员的潜在类型足够大肯定能够容纳枚举值。 如果指定了枚举成员的潜在类型(包括对限定作用域的enum的隐式指定)则一旦某个枚举成员的值超出了该类型所能容纳的范围将引发程序错误。 指定enum潜在类型的能力使得可以控制不同实现环境中使用的类型将可以确保在一种实现环境中编译通过的程序所生成的代码与其他实现环境中生成的代码一致。 枚举类型的前置声明 在c11新标准中可以提前声明enum其前置声明(无论隐式地还是显式地)必须指定其成员的大小 enum intValues : unsigned long long; // 不限定作用域的必须指定成员类型。
enum class open_modes; // 限定作用域的枚举类型可以使用默认成员类型int同样的enum的声明和定义必须匹配因此成员的大小必须一致。而且不能在同一个上下文中先声明一个不限定作用域的enum名字然后再声明一个同名的限定作用域的enum // 错误所有的声明和定义必须对该enum是限定作用域的还是不限定作用域的保持一致。
enum class intValues;
enum intValues; // 错误intValues已经被声明成限定作用域的enum。
enum intValues : long; // 错误intValues已经被声明成int。形参匹配与枚举类型 要想初始化一个enum对象必须使用该enum类型的另一个对象或者它的一个枚举成员。 // 不限定作用域的枚举类型潜在类型因机器而异。
enum Tokens { INLINE 128, VIRTUAL 129 };
void ff(Tokens);
void ff(int);int main() {Tokens curTok INLINE;ff(128); // 精确匹配ff(int)ff(INLINE); // 精确匹配ff(Tokens)ff(curTok); // 精确匹配ff(Tokens)return 0;
}但是可以将一个不限定作用域的枚举类型的对象或枚举成员传给整型形参。此时enum的值提升成int或更大的整型实际提升的结果由枚举类型的潜在类型决定 // Tokens中较大的那个值是129该枚举类型可以用unsigned char来表示
// 因此很多编译器使用unsigned char作为Tokens的潜在类型。不管Tokens
// 的潜在类型到底是什么它的对象和枚举成员都提升成int。尤其是枚举成员
// 永远不会提升成unsigned char即使枚举值可以用unsigned char存储也是如此。
void newf(unsigned char);
void newf(int);
unsigned char uc VIRTUAL;
newf(VIRTUAL); // 调用newf(int)
newf(uc); // 调用newf(unsigned char)19.4类成员指针 成员指针是指可以指向类的非静态成员的指针。其囊括了类的类型以及成员的类型。当初始化一个这样的指针时令其指向类的某个成员但是不指定该成员所属的对象直到使用成员指针时才提供成员所属的对象。 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;
};19.4.1数据成员指针
// pdata可以指向一个常量(非常量)Screen对象的string成员。
const string Screen::*pdata;当初始化一个成员指针(或者向它赋值)时需指定它所指的成员。 // 将取地址运算符作用于类的成员而非内存中的一个该类对象
pdata Screen::contents;
// C11新标准最简单的方法是使用auto或decltype
auto pdata Screen::contents;使用数据成员指针 必须清楚的一点是当初始化一个成员指针或为成员指针赋值时该指针并没有指向任何数据。成员指针指定了成员而非该成员所属的对象只有当解引用成员指针时才提供对象的信息。 有两种成员指针访问运算符.*和-* Screen myScreen, *pScreen myScreen;
// .*解引用pdata以获得myScreen对象的contents成员
auto s myScreen.*pdata;
// -*解引用pdata以获得pScreen所指对象的contents成员
s pScreen-*pdata;这些运算符执行两部操作首先解引用成员指针以得到所需的成员然后像成员访问运算符一样通过对象(.*)或指针(-*)获取成员。 返回数据成员指针的函数 因为数据成员一般情况下是私有的所以通常不能直接获得数据成员的指针。如果确实有这样的需求最好定义一个函数令其返回值是指向该成员的指针 // 之前对于pdata的使用必须位于类的成员或友元内部
class Screen {
public:// data是一个静态成员返回一个成员指针。static const std::string Screen::*data() {return Screen::contents;}// 其他成员保持一致
};const string Screen::*pdata Screen::data();
auto s myScreen.*pdata;19.4.2成员函数指针 要想创建一个指向成员函数的指针最简单的办法是使用auto来推断类型 auto pfm Screen::get_cursor;指向成员函数的指针也需要指定目标函数的返回类型和形参列表。如果成员函数是const成员或者引用成员则必须将const限定符或引用限定符包含进来。 如果成员存在重载的问题则必须显式地声明函数类型以明确指出想要使用的是哪个函数 // 指向两个形参的get。其中指针两端的括号必不可少否则声明的是一个函数。
char (Screen::*pfm2)(Screen::pos, Screen::pos) const;
pfm2 Screen::get;和普通函数指针不同的是在成员函数和指向该成员的指针之间不存在自动转换规则 pmf Screen::get; // 必须显式地使用取地址运算符
pmf Screen::get; // 错误在承运商和指针之间不存自动转换规则。使用成员函数指针
Screen myScreen, *pScreen myScreen;
// 通过pScreen所指的对象调用pmf所指的函数
char c1 (pScreen-*pmf)();
// 通过myScreen对象将实参0, 0传给含有两个形参的get函数
char c2 (myScreen.*pmf)(0, 0);使用成员指针的类型别名 通过使用类型别名可以令含有成员指针的代码更易读写 using Action char (Screen::*)(Screen::pos, Screen::pos) const;
Action get Screen::get;可以将指向成员函数的指针作为某个函数的返回类型或形参类型。其中指向成员的指针形参也可以拥有默认实参 Screen action(Screen , Action Screen::get);
Screen myScreen;
// 等价的调用
action(myScreen); // 使用默认实参
action(myScreen, get); // 使用之前定义的变量get
action(myScreen, Screen::get); // 显式地传入地址成员指针函数表 对于普通函数指针和指向成员函数的指针来说一种常见的用法是将其存入一个函数表当中。如果一个类含有几个相同类型的成员则这样一张表可以帮助从这些成员中选择一个。 class Screen {
public:// 其他成员保持一致// Action是一个指针可以用任意一个光标移动函数对其赋值。using Action Screen (Screen::*)();// 光标移动函数Screen home();Screen forward();Screen back();Screen up();Screen down();enum Directions { HOME, FORWARD, BACK, UP, DOWN };// 可以调用任意一个光标移动函数并执行对应的操作Screen move(Directions);
private:// 函数表依次保存每个光标移动函数的指针这些函数// 将按照Directions中枚举成员对应的偏移量存储。static Action Menu[];
};// Directions中的默认值从0开始与数组对应。
Screen Screen::move(Directions cm) {// 运行this对象中索引值为cm的元素return (this-*Menu[cm])(); // Menu[cm]指向一个成员函数
}Screen::Action Screen::Menu[] {Screen::home,Screen::forward,Screen::back,Screen::up,Screen::down
};Screen myScreen;
myScreen.move(Screen::HOME); // 调用myScreen.home
myScreen.move(Screen::DOWN); // 调用myScreen.down19.4.3将成员函数用作可调用对象 由于对成员函数的指针进行调用需要绑定到特定的对象上因此其不是一个可调用对象所以不能直接将一个指向成员函数的指针传递给算法。 使用function生成一个可调用对象 从指向成员函数的指针获取可调用对象的一种方法是使用标准库模板function。 当定义一个function对象时必须指定该对象所能表示的函数类型即可调用对象的形式。如果可调用对象是一个成员函数则第一个形参必须表示该成员是在哪个(一般是隐式的)对象上执行的。同时提供给function的形式中还必须指明对象是否是以指针或引用的形式传入的。 functionbool (const string ) fcn string::empty;
find_if(svec.begin(), svec.end(), fcn);vectorstring* pvec;
functionbool (const string *) fp string::empty;
// fp接受一个指向string的指针然后使用-*调用empty。
find_if(pvec.begin(), pvec.end(), fp);使用mem_fn生成一个可调用对象 和function不同的是通过使用标准库功能mem_fn来让编译器负责推断成员的类型从而使得用户无须显式地指定 // 使用mem_fn(string::empty)生成一个可调用对象
// 接受一个string实参返回一个bool值。
find_if(svec.begin(), svec.end(), mem_fn(string::empty));mem_fn生成的可调用对象可以通过对象调用也可以通过指针调用 // 可以认为mem_fn生成的可调用对象含有一对重载的函数调用运算符
// 一个接受string*另一个接受string。
auto f mem_fn(string::empty); // f接受一个string或者一个string*
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, std::placeholders::_1));和function类似的地方是当使用bind时必须将函数中用于表示执行对象的隐式形参转换成显式的。和mem_fn类似的地方是bind生成的可调用对象的第一个实参既可以是指针也可以是引用。 auto f bind(string::empty, std::placeholders::_1);
f(*svec.begin()); // 正确实参是一个stringf使用.*调用empty。
f(svec[0]); // 正确实参是一个string的指针f使用-*调用empty。19.5嵌套类 嵌套类常用于定义作为实现部分的类。其是一个独立的类与外层类基本没什么关系。特别是外层类的对象和嵌套类的对象是相互独立的。 嵌套类的名字在外层类作用域中是可见的在外层类作用域之外不可见。因此嵌套类的名字不会和别的作用域中的同一个名字冲突。 嵌套类在其外层类中定义了一个类型成员。和其他成员类似该类型的访问权限(public、protected、private)由外层类决定。 声明一个嵌套类
class TextQuery {
public:class QueryResult; // 嵌套类稍后定义// 其他成员保持一致
};在外层类之外定义一个嵌套类 当在外层类之外定义一个嵌套类时必须以外层类的名字限定嵌套类的名字 class TextQuery::QueryResult {// 位于类的作用域内因此不必对QueryResult形参进行限定。friend std::ostream print(std::ostream , const QueryResult );public:// 无须定义QueryResult::line_no。嵌套类可以直接使用外层类// 的成员无须对该成员的名字进行限定。QueryResult(std::string, std::shared_ptrstd::setline_no, std::shared_ptrstd::vectorstd::string);// 其他成员保持一致
};嵌套类在其外层类之外完成真正的定义之前它都是一个不完全类型。 定义嵌套类的成员
TextQuery::QueryResult::QueryResult(string s, shared_ptrsetline_no p, shared_ptrvectorstring f) : sought(s), lines(p), file(f) {
}嵌套类的静态成员定义 如果嵌套类声明了一个静态成员则该成员的定义将位于外层类的作用域之外 int TextQuery::QueryResult::static_mem 1024;嵌套类作用域中的名字查找 名字查找的一般规则在嵌套类中同样适用。当然因为嵌套类本身是一个嵌套作用域所以还必须查找嵌套类的外层作用域。 由于嵌套类是其外层类的一个类型成员因此外层类的成员可以像使用任何其他类型成员一样使用嵌套类的名字。 // 返回类型不在类的作用域中因此必须指明QueryResult是一个嵌套类。
TextQuery::QueryResult
TextQuery::query(const string sought) const {// 如果没有找到sought则返回set的指针。static shared_ptrsetline_no nodata(new setline_no);// 使用find而非下标以避免向wm中添加单词auto loc wm.find(sought);if (loc wm.end()) {return QueryResult(sought, nodata, file); // 没有找到} else {return QueryResult(sought, loc-second, file);}
}嵌套类和外层类是相互独立的 嵌套类的对象只包含嵌套类定义的成员同样外层类的对象只包含外层类定义的成员 // 第二条return语句使用了TextQuery的数据成员。因为在一个QueryResult对象中不包含
// 其外层类的成员所以必须使用上述成员构造返回的QueryResult对象。
return QueryResult(sought, loc-second, file);19.6union(联合)一种节省空间的类 一个union可以有多个数据成员但是在任意时刻只有一个数据成员可以有值。当给union的某个成员赋值之后该union的其他成员就变成未定义的状态了。分配给一个union对象的存储空间至少要能容纳它的最大的数据成员。 union不能含有引用类型的成员除此之外它的成员可以是绝大多数类型。在c11新标准中含有构造函数或析构函数的类类型也可以作为union的成员类型。 union可以为其成员指定public、protected和private等保护标记。默认情况下union的成员都是公有的。 union可以定义包括构造函数和析构函数在内的成员函数。但是由于union既不能继承自其他类也不能作为基类使用所以在union中不能含有虚函数。 定义union union提供了一种有效的途径使得可以方便地表示一组类型不同的互斥值。 // 假设需要处理一些不同种类的数字数据和字符数据
union Token {char cval;int ival;double dval;
};使用union类型 默认情况下union是未初始化的。如果提供了初始值则该初始值被用于初始化第一个成员。 Token first_token { a }; // 初始化cval成员
Token last_token; // 未初始化的Token对象
Token *pt new Token; // 指向一个未初始化的Token对象的指针last_token.cval z;
pt-ival 42;匿名union 一旦定义了一个匿名union编译器会自动地为该union创建一个未命名的对象 union { // 匿名unionchar cval;int ival;double dval;
}; // 定义了一个未命名的对象可以直接访问它的成员。
cval c;
ival 42; // 该对象当前保存的值是42匿名union不能包含受保护的成员或私有成员也不能定义成员函数。 含有类类型成员的union 当union包含的是内置类型的成员时可以使用普通的赋值语句改变其保存的值。但是对于含有特殊类类型成员的union如果想将值改为类类型成员对应的值或者将类类型成员的值改为一个其他值则必须分别构造或析构该类类型的成员。 当union包含的是内置类型的成员时编译器将按照成员的次序依次合成默认构造函数或拷贝控制成员。但是如果union含有类类型的成员并且该类型自定义了默认构造函数或拷贝控制成员则编译器将为union合成对应的版本并将其声明为删除的。 使用类管理union成员 通常情况下把含有类类型成员的union内嵌在另一个类当中。这个类可以管理并控制与union的类类型成员有关的状态转换。 为了追踪union中到底存储了什么类型的值通常会定义一个独立的对象该对象称为union的判别式。可以使用判别式辨认union存储的值。 class Token {
public:// 因为union含有一个string成员所以Token必须定义拷贝控制成员。Token() : tok(INT), ival(0) {}// 当在拷贝构造函数中调用copyUnion时本对象的union成员将被默认初始化// 这意味着编译器会初始化union的第一个成员。因为string不是第一个成员// 所以无需考虑其他因素但是赋值运算符则不一样有可能union已经存储了一个string。Token(const Token t) : tok(t.tok) { copyUnion(t); }Token operator(const Token );// 因为union含有一个定义了析构函数的成员所以必须为union也定义一个析构函数// 以进行销毁操作。作为union组成部分的类成员无法自动销毁。因为析构函数不清楚// union存储的值是什么类型所以无法确定应该销毁哪个成员。~Token() {if (tok STR) {sval.~string();}}// 下面的赋值运算符负责设置union的不同成员Token operator(const std::string );Token operator(char);Token operator(int);Token operator(double);private:// 为了保持union与判别式同步将判别式也作为Token的成员。enum {INT, CHAR, DBL, STR} tok; // 判别式union { // 匿名unionchar cval;int ival;double dval;std::string sval;}; // 每个Token对象含有一个该未命名union类型的未命名成员// 检查判别式然后酌情拷贝union成员。void copyUnion(const Token );
};管理判别式并销毁string
// 和析构函数一样在为union赋新值前必须首先销毁string。
// double和char版本的赋值运算符与int非常相似。
Token Token::operator(int i) {// 如果当前存储的是string则释放它。if (tok STR) {sval.~string();}ival i; // 为成员赋值tok INT; // 更新判别式return *this;
}Token Token::operator(const std::string s) {if (tok STR) { // 如果当前存储的string可以直接赋值。sval s;} else {// 否则找不到一个已存在的string对象来调用赋值运算符。此时// 必须先利用定位new表达式在内存中为sval构造一个string然后// 进行初始化操作。new(sval) string(s);}tok STR; // 更新判别式return *this;
}管理需要拷贝控制的联合成员
// 拷贝构造函数和赋值运算符也需要先检验判别式以明确拷贝
// 所采用的方式因此定义一个成员来方便后续操作。
void Token::copyUnion(const Token t) {switch (t.tok) {case Token::INT:ival t.ival;break;case Token::CHAR:cval t.cval;break;case Token::DBL:dval t.dval;break;case Token::STR:new(sval) std::string(t.sval);break;}
}// 赋值运算符必须处理string成员的三种可能情况
// 1.左侧运算对象和右侧运算对象都是string。
// 2.两个运算对象都不是string。
// 3.只有一个运算对象是string。
Token Token::operator(const Token t) {// 如果此对象的值是string而t的值不是则必须释放原来的string。if (tok STR t.tok ! STR) {sval.~string();}if (tok STR t.tok STR) {sval t.sval; // 无须构造一个新string} else {copyUnion(t); // 如果t.tok是STR则需要构造一个string。}tok t.tok;return *this;
}19.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; // 正确使用一个枚举成员。}};
}常规的访问保护规则对局部类同样适用 外层函数对局部类的私有成员没有任何访问权限。当然局部类可以将外层函数声明为友元或者更常见的情况是局部类将其成员声明成公有的。 在程序中有权访问局部类的代码非常有限。局部类已经封装在函数作用域中通过信息隐藏进一步封装就显得没什么必要了。 局部类中的名字查找 局部类内部的名字查找次序与其他类相似。 嵌套的局部类 可以在局部类的内部再嵌套一个类。此时嵌套类的定义可以出现在局部类之外。不过嵌套类必须定义在与局部类相同的作用域中。 void foo() {class Bar {public:// ...class Nested; // 声明Nested类};// 定义Nested类class Bar::Nested {// ...};
}局部类的嵌套类也是一个局部类必须遵循局部类的各种规定。此时嵌套类的所有成员都必须定义在嵌套类的内部。