将这篇文章在作业部落中的链接放置于此,对格式有轻微强迫的童鞋,请移步这里。
第 19 章 特殊工具与技术
标签: C++Primer 学习记录 运行时类型识别 枚举类型 类成员指针
19.1 控制内存分配
-
当使用一条 new表达式时
string *sp = new string("value");
,实际执行了三步操作。- 第一步,new表达式调用一个名为
operator new
的标准库函数,来分配一块足够大的,原始的,未命名的内存空间,以便存储特定类型的对象(或者对象的数组)。 - 第二步,编译器运行相应的构造函数,以构造这些对象,并为其传入初始值。
- 第三步,对象被分配的空间并构造完成,返回一个指向该对象的指针。
- 第一步,new表达式调用一个名为
-
当我们使用一条 delete表达式删除一个动态分配的对象时
delete sp;
,实际执行了两步操作。- 第一步,对 sp所指的对象执行对应的析构函数。
- 第二步,编译器调用名为
operator delete
的标准库函数释放内存空间。
-
尽管我们说能够 “重载new和delete”,但实际是定义自己需要的
operator new
和operator delete
函数,来控制内存分配的过程。 -
应用程序可以在全局作用域中定义
operator new
和operator delete
函数,也可以将它们定义为成员函数。当编译器发现一条 new表达式或 delete表达式后,将在程序中查找可供调用的 operator函数。如果被分配(释放)的对象是类类型,则查找顺序如下。- 首先在类及其基类的作用域中查找。
- 接着在全局作用域查找匹配的函数,如果编译器找到了用户自定义的版本,则就会调用这些函数。
- 最后没找到,才会使用标准库定义的版本。
-
在类中自定义的
operator new
和operator delete
函数是隐式静态的。因为operator new
用在对象构造之前而operator delete
用在对象销毁之后,所以这两个成员必须是静态的,而且他们不能操纵的任何数据成员。 -
在自定义自己的
operator new
和operator delete
函数时,通常会用到malloc
和free
函数。void *operator new(size_t size) { if (void *men = malloc(size)) return mem; else throw bad_alloc(); } void operator delete(void *mem) { free(mem); }
-
当只传入一个指针类型的实参时,
定位 new表达式
构造对象但是不分配内存。允许我们在一个特定的预先分配的内存地址上构造对象new (&sval) string(s);
。创建对象但是不分配内存,而是在已有的内存块上面创建对象。用于需要反复 创建并删除的对象上,可以降低分配释放内存的性能消耗。 -
析构函数可以显示调用,会清除给定的对象,但是不会释放该对象所在的空间。如果需要的话,可以重新使用该空间。
string *sp = new string("value"); sp->~string();
-
使用定位new运算符应注意:
- 注意内存覆盖
- delete不能与定位new运算符配合使用,因此要显式地为 定位new运算符创建的对象调用析构函数
- 删除顺序:后创建的对象先删除,所有用定位new运算符创建的对象都被删除后再释放用于存储这些对象的缓冲区
#include <iostream> #include <string> #include <new> using namespace std; const int BUF = 512; class JustTesting { private: string words; int number; public: JustTesting(const string & s = "Just Testing", int n = 0) { words = s; number = n; cout << words << "constructed\n"; } JustTesting() { cout << words << "destroyed\n"; } void show()const { cout << words << "," << number << endl; } }; int main() { char * buffer = new char[BUF]; JustTesting *pc1, *pc2; // 在 buffer的 0~sizeof(JustTesting)处构建对象 pc1 = new (buffer) JustTesting; // 开辟新的堆空间并构建对象 pc2 = new JustTesting("Heap1", 20); // 在 buffer的 sizeof(JustTesting)+1~2*sizeof(JustTesting)处构建对象 pc3 = new (buffer + sizeof(JustTesting)) JustTesting("Better idea",4); // 开辟新的堆空间并构建对象 pc4 = new JustTesting("Heap2", 10); // 析构对象并回收使用 new表达式开辟的空间 delete pc2; delete pc4; // 显式的调用析构函数来析构对象 pc3->~JustTesting(); pc1->~JustTesting(); // 最后再释放用于存储这些对象的空间 delete[]buffer; return 0; }
19.2 运行时类型识别
-
**运行时类型识别(RTTI)**的功能由两个运算符实现:
- typeid运算符,用于返回表达式的类型
- dynamic_cast运算符,用于将基类的指针或引用安全地转换为派生类的指针或引用。
-
当我们将这两个运算符用于某种类型的指针或引用,并且该类型含有虚函数时,运算符将使用指针或引用所绑定对象的动态类型。当我们想使用基类对象的指针或引用执行某个派生类操作,并且该操作不是虚函数时,上面两个运算符,就可以发挥作用。
-
dynamic_cast运算符的使用形式如下:
dynamic_cast<type *> (e)
dynamic_cast<type &> (e)
dynamic_cast<type &&> (e)- e的类型必须符合以下三个条件中的任意一个:e的类型是目标 type的公有派生类、e的类型是目标 type的公有基类或者 e的类型就是目标 type的类型。
- 转换目标是指针类型且失败了之后,结果为0。
- 转换目标是引用类型且失败了,则 dynamic_cast运算符将抛出 bad_cast异常。
-
可以对空指针进行dynamic_cast,结果是所需的空指针,在条件部分执行dynamic_cast可以确保类型转换和检查结果在同一条表达式中完成!这里基类的指针或引用转化为派生类的指针或者引用**,需要注意的是该基类的指针需要指向的是派生类的对象才可以,否则指向基类对象的基类指针或引用是不可能转化为派生类的指针或引用的**!
// 指针类型的 dynamic_cast if (Derived *dp = dynamic_cast<Derived*>(bp)) { // 使用dp指向的Derived对象 } else { // bp指向一个Base对象 // 使用bp指向的Base对象 } // 引用类型的 dynamic_cast void f(const Base &b) { try { const Derived &d = dynamic_cast<const Derived&>(b); // 使用 b引用的Derived对象 } catch (bad_cast) { // 处理类型转换失败的情况 } }
-
typeid(e)
,返回结果是一个常量对象的引用,该对象的类型是标准库类型type_info
或其公有派生类型。typeid运算符可以作用于任意类型的表达式。- 和往常一样,顶层 const会被忽略。
- 如果表达式是一个引用,则返回该引用所引对象的类型。
- 作用于数组或函数时,并不会执行向指针的标准类型转换。
- 当运算对象不属于类类型(指针)或者是一个不包含任何虚函数的类时,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)) { //此处的代码永远不会执行 }
-
对于
type_info
类对象 t,t.name()
返回一个 C风格字符串,表示类型名字的可打印形式,类型名字的生成方式因系统而异。
19.3 枚举类型
-
枚举类型,将一组整型常量组织在一起。和类一样,每个枚举类型定义了一种新的类型。C++包含两种枚举,限定作用域的和不限定作用域的。对于不限定作用域的枚举类型,名字是可选的。如果 enum是未命名的,必须在定义的同时定义它的对象。
// 限定作用域的枚举类型 enum class open_modes {input, output, append}; // 不限定作用域的枚举类型 enum color {red, yellow, green}; // 未命名的、不限定作用域的枚举类型 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; // 错误:peppers的枚举成员不在有效的作用域中 // color::green在有效的作用域中,但是类型错误 color hair = color:: red; // 正确:允许显式地访问枚举成员 peppers p2 = peppers::red; // 正确:使用 pappers的 red
-
默认情况下,枚举值从 0开始依次加 1。不过也可以专门指定某些成员的值,其值不一定唯一。如果没有显式的提供初始值,否则当前枚举成员的值等于之前枚举成员的值加 1。
enum class intTypes { charTyp = 8, shortTyp = 16, intTyp = 16, longTyp = 32, long_longTyp = 64 };
-
枚举成员是 const,因此,在初始化枚举成员时提供的初始值必须是常量表达式,也可以在任何需要常量表达式的地方使用枚举成员。
-
要想初始化 enum对象或者为 enum对象赋值,必须使用该类型的一个枚举成员或者该类型的另一个对象。一个不限定作用域的枚举类型的对象或枚举成员,可以自动的转化成整型,而限定作用域的枚举类型则不会进行隐式转换。
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的潜在类型。默认情况下,限定作用域的 enum成员类型是 int;而对于不限定作用域的枚举类型来说,其枚举成员不存在默认类型,只知道其足够大,肯定能够容纳枚举值。
-
在 C++11新标准中可以提前声明 enum,前置声明必须指定其成员的潜在类型。因为不限定作用域的 enum未指定成员的默认潜在类型,因此每个声明必须指定成员类型;而对于限定作用域的 enum来说,可以不指定其成员类型,这个值被隐式地定义成 int。
//不限定作用域的枚举类型 intValues的前置声明 enum intValues : unsigned long long; //不限定作用域的,必须指定成员类型 enum class open_modes; // 限定作用域的枚举类型可以使用默认成员类型 int
-
即使某个整型值恰好与枚举成员的值相等,它也不能作为函数的 enum实参使用。另外,枚举成员永远不会提升成 unsigned char,即使枚举值可以用 unsigned char存储也是如此。这两点,在当函数形参是枚举类型时需要特别注意。
19.4 类成员指针
-
普通的数据指针指向对象,而指向数据成员的指针,在初始化时令其指向类的某个成员,并不指定该成员所属对象,使用时才提供成员所属的对象。总的来说,指向的是类的数据成员,而非类的对象。
// 定义一个数据成员指针,pdata可以指向一个常量(非常量) Screen对象的 string成员 const string Screen::*pdata; // 初始化 pdata = &Screen::contens; // C++11新标准中,声明成员指针最简单的方法是使用 auto或 decltype: auto pdata = &Screen::contens;
-
初始化一个成员指针或为成员指针赋值时,该指针并没有指向任何数据。只有当解引用成员指针时,我们才提供对象的信息。使用成员指针访问运算符:
.*
或->*
。Screen myScreen, *pScreen = &myScreen; auto s = myScreen.*pdata; s = pScreen->*pdata;
-
因为数据成员一般情况下是私有的,不能直接获得数据成员的指针。可以在类的内部定义一个成员函数,另其返回值是指向该成员的指针。
class Screen { public: // data是一个静态成员,返回一个成员指针 static const std::string Screen::*data() { return &Screen::contens; } // 其他成员与之前的版本一致 };
-
同样的也可以定义指向类的成员函数的指针,最简单的方法是使用 auto来推断类型。
// pmf是一个指针,它可以指向 Screen的某个常量成员函数 // 前提是该函数不接受任何实参,并且返回一个 char auto pmf = &Screen::get_cursor;
-
和普通的函数指针类似,如果成员存在重载的问题,则我们必须显式地声明函数类型以明确指出我们想要使用的是哪个函数。和使用指向数据成员的指针一样,在使用成员函数指针时可以使用
.*
或者->*
运算符。// 因为函数调用运算符的优先级较高,所以在声明指向成员函数的指针, // 并使用这样的指针进行函数调用时,括号必不可少 char (Screen::*pmf2)(Screen::pos, Screen::pos) const; pmf2 = &Screen::get; Screen myScreen, *pScreen = &myScreen; char c1 = (myScreen.*pmf2)(0, 0); char c2 = (pScreen->*pmf2)(0, 0);
-
和普通函数指针不同的是,在成员函数和指向该成员的指针之间不存在自动转换规则。
// pmf指向一个 Screen成员,该成员不接受任何实参,且返回类型是 char pmf = &Screen::get; // 必须显示的使用取地址运算符 pmf = Screen::get; // 错误:在成员函数和指针之间不存在自动转换规则
-
通过使用类型别名,可以另含有成员指针的代码更易读写。
// Action是一种可以指向 Screen成员函数的指针,它接受两个 pos实参,返回一个 char using Action = char (Screen::*)(Screen::pos, Screen::pos) const; // 通过 Action,可以简化指向 get的指针定义 Action get = &Screen::get;
-
和其他函数指针类似,可以将指向成员函数的指针作为某个函数的返回类型或形参类型。其中指向成员的指针形参也可以拥有默认实参。
// action接受一个 Screen的引用,和一个指向 Screen成员函数的指针 Screen& action(Screen&, Action = &Screen::get);
-
与普通的函数指针不同,成员指针不是一个可调用对象,这样的指针不支持函数调用运算符。有三种方法可以从指向成员函数的指针中生成可调用对象。
- 使用标准库模板
function
。需要指明对象是否是以指针或引用的形式传入。
vector<string*> pvec; function<bool (const string*)> fp = &string::empty; // fp接受一个指向 string的指针,然后使用 ->*调用 empty find_if(pvec.begin(), pvec.end(), fp);
- 使用标准库函数
mem_fn
。根据成员指针的类型推断可调用对象的类型,而无需用户显示的指定。生成的可定对象可以通过对象调用,也可以通过指针调用。
find_if(pvec.begin(), pvec.end(), mem_fn(&string::empty));
- 使用
bind
生成一个可调用对象。生成的对象既可以是指针,也可以是引用。
find_if(pvec.begin(), pvec.end(), bind(&string::empty, _1));
- 使用标准库模板
19.5 嵌套类
-
嵌套类是指一个类定义在另一个类的内部。
- 外层类的对象和嵌套类的对象是相互独立的。在嵌套类的对象中不包含任何外层类定义的成员;类似的,在外层类的对象中也不包含任何嵌套类定义的成员。
- 嵌套类的名字在外层类作用域中是可见的,在外层内作用域之外的可见性由其访问权限修饰符决定。嵌套类可以直接使用外层内的成员,无需对该成员的名字进行限定。
-
嵌套类必须声明在类的内部,但是可以定义在类的内部或者外部。当在外层类之外定义一个嵌套类时,必须以外层类的名字限定嵌套类的名字。
// QueryResult类嵌套在 TextQuery类中 TextQuery::QueryResult::QueryResult() {}
-
如果嵌套类声明了一个静态成员,则该成员的定义将位于外层类的作用域之外。
// QueryResult类嵌套在 TextQuery类中 // 下面的代码为 QueryResult类一个静态成员 int TextQuery::QueryResult::static_mem = 1024;
19.6 union:一种节省空间的类
-
union
是一种特殊的类,一个 union可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当给 union的某个成员赋值之后,其他成员就变成未定义的状态了。- union不能含有引用类型的成员,除此之外,它的成员可以是绝大多数类型。
- 含有构造函数或析构函数的类类型,也可以作为 union的成员类型。
- union可以为其成员指定 public、proteced和 private等保护标记,但默认情况下,其成员都是公有的。
- union既不能继承自其他类,也不能作为基类使用,所以在其中不能含有虚函数。
-
union的名字是一个类型名。与其他内置类型一样,默认情况下 union是未初始化的,可以使用一对花括号内的初始值显式地初始化一个 union。如果提供了初始值,则该初始值被用于初始化第一个成员。
// Token类型的对象只有一个成员,该成员的类型可能是下列类型中的任意一种 union Token { // 默认情况下,成员是公有的 char cval; int ival; double dval; }; Token first_token = {'a'}; // 初始化 cval成员 Token last_token; // 未初始化的 Token对象 Token *pt = new Token; // 指向一个未初始化的 Token对象的指针
-
匿名 union
是一个未命名的 union,并且在右花括号和分号之间没有任何声明。在匿名 union
的定义所在的作用域内,该 union的成员都是可以直接访问的。匿名 union
不能包含受保护的成员或私有成员也不能定义成员函数。union { // 匿名 union char cval; int ival; double dval; }; //定义一个未命名的对象,我们可以直接访问它的成员 cval = 'c' // 为刚刚定义的未命名的匿名 union对象賦一个新值 ival = 42; // 该对象当前保存的值是 42
-
当 union包含的是内置类型的成员,使我们可以使用普通的赋值语句改变 union保存的值。但是对于含有特殊类类型成员的 union就没这么简单了。如果我们想将 union的值改为类类型成员对应的值,或者将类类型成员的值改为一个其他值,则必须分别构造或析构该类型的成员。通常把含有类类型成员的 union内嵌在另一个类当中,这个类可以管理并控制与 union的类类型成员有关的状态转换。
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,所有成员都要在 Nested内部定义 class Bar::Nested { // ... }; }
19.8 固有的不可移植的特性
-
有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个开关量时,只有 0和 1 两种状态,用一位二进位即可。为了节省存储空间,并使处理简便,便引入了
位域
的概念。它是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。typedef unsigned int Bit; Bit mode: 2; // mode占 2位 Bit modified: 1; // modified占 1位
-
通常情况下,最好将位域设置为无符号类型,存储在带符号类型中的位域的行为将因具体实现而定。
-
取地址运算符不能作用于位域,因此任何指针都无法指向类的位域。
-
直接处理硬件的程序常常包含这样的数据元素,它们的值由程序直接控制之外的过程控制。例如,程序可能包含一个由系统时钟定时更新的变量**。当对象的值可能在程序的控制或检测之外被改变时,应该将该对象声明为
volatile
**。关键字volatile
告诉编译器不应对这样的对象进行优化。- 其用法和 const很相似,起到类型额外修饰的作用。
volatile int display_register; // 该 int值可能发生改变 volatile int iax[max_size]; // iax的每个元素都是 volatile
- const和 volatile限定符互相没什么影响,某种类型可能既是 const的也是 volatile的,此时它同时具有二者的属性。
- 可以将成员函数定义成 volatile的,只有 volatile的成员函数才能被 volatile对象调用。
- volatile与指针和引用的关系也和 const类似。
volatile int v; // v是一个 volatile int int *volatile vip; // vip是一个 volatile指针,它指向 int volatile int *ivp; // ivp是一个指针,它指向一个 volatile int // vivp是一个 volatile指针,它指向一个 volatile int volatile int *volatile vivp; int *ip = &v; // 错误,必须使用指向 volatile的指针 ivp = &v; // 正确,ivp是一个指向 volatile的指针 vivp = &v; // 正确,vivp是一个指向 volatile的 volatile指针
-
const和 volatile的一个重要区别是,我们不能使用合成的拷贝/移动构造函数及赋值运算符初始化 volatile对象或从 volatile对象赋值。这是因为合成的成员接受的形参类型是(非 volatile)常量引用,显然不能把一个非 volatile引用绑定到一个 volatile对象上。此时需要自定义相应操作。
-
C++程序有时需要调用其它语言编写的函数,像所有其他名字一样,其他语言中的名字也必须在 C++中进行声明,并且该声明必须指定返回类型和形参列表。对于其他语言编写的函数来说,编译器生成的代码会有所不同,C++使用
链接指示
指出任意非 C++函数使用的语言。 -
链接指示可以有两种形式:单个的或符合的。链接指示不能出现在类定义或函数定义的内部,同样的链接指示必须在函数的每个声明中都出现。
// 单语句链接指示 extern "C" size_t strlen(const char*); // 复合语句链接指示 extern "C" { int strcmp(const char*, const char*); char *strcat(char*, const char*); }
-
多重声明的形式可以应用于整个头文件,当一个#include指示被放置在复合链接指示的花括号中时,头文件中的所有普通函数声明都被认为是由链接指示的语言编写的。
// 复合语句链接指示 extern "C" { #include <string.h> // 操作 C风格字符串的 C函数 }
-
指向其他语言编写的函数的指针,必须与函数本身使用相同的链接指示。指向 C函数的指针已指向 C++函数的指针是不一样的类型。
// pf指向一个 C函数,该函数接受一个 int返回 void // 当我们使用 pf调用函数时,编译器认定当前采用的是一个 C函数 extern "C" void (*pf)(int); void (*pf1)(int); // 指向一个 C++函数 extern "C" void (*pf2)(int); // 指向一个 C函数 pf1 = pf2; // 错误,pf1和 pf2的类型不同
-
链接指示不仅对函数有效,而且对作为返回类型或形参类型的函数指针也有效。因为链接指示同时作用于声明语句中的所有函数,所以如果我们希望给 C++函数传入一个指向 C函数的指针,则必须使用类型别名。
// f1是一个 C函数,他的形参是一个指向 C函数的指针
extern "C" void f1(void(*)(int));
// FC是一个 C函数的函数类型
extern "C" typedef void FC(int);
// f2是一个 C++函数,该函数的形参是指向 C函数的指针
void f2(FC *);
- 通过使用链接指示对函数进行定义,我们可以令一个 C++函数在其他语言编写的程序中可用,编译器将为该函数生成适合于指定语言的代码。
// calc函数可以被 C程序调用
extern "C" double calc(double dparm) { /* ... */ }