第十九章 特殊工具与技术

本章介绍C++的几种未被广泛使用的特征。

一、控制内存分配

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

(一)重载new和delete 

1.new和delete的运行过程 

当调用一条new表达式时:

string *sp=new string("a value");//分配并初始化一个string对象
string *arr=new string[10];//分配10个默认初始化的string对象

 实际执行了三步操作:

1.new表达式调用一个名为operator new(或operator new[])的标准库函数。该函数分配一块足够大的、原始的、未命名的内存空间以便存储特定类型的对象(或对象的数组)。

2.编译器运行相应的构造函数以构造这些对象,并为其传入初始值。

3.对象被分配了空间并构造完成,返回一个指向该对象的指针。

当调用一条delete表达式时:

delete sp;//销毁*sp,然后释放sp指向的内存空间
delete[] arr;//销毁数组中的元素,然后释放对应的内存空间

 实际指向了两步操作:

1.对sp所指的对象或arr所指的数组中的元素执行对应的析构函数。

2.编译器调用名为operaotr delete(或operator delete[])的标准库函数释放内存空间。

当编译器发现一条new表达式或delete表达式后,将在程序中查找可供调用的operator函数,若找到了用户自定义的版本,则使用该版本执行new表达式或delete表达式;若未找到,则使用标准库定义的版本。

可以使用作用域运算符令new表达式或delete表达式忽略定义在类中的函数,直接执行全局作用域中的版本。例如,::new只在全局作用域中查找匹配的operator new函数,::delete与之类似。 

2.new和delete的重载版本 

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

//这些版本可能抛出异常
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,在这个类型中不包含任何成员。new头文件还定义了一个名为nothrow的const对象,用户可以通过这个对象请求new的非抛出版本。

应用程序可以自定义上面函数版本中的任意一个,前提是自定义的版本必须位于全局作用域或类作用域中。当我们将上述运算符函数定义成类的成员时,它们是隐式静态的,无须显示地声明static。

若想要自定义operator new函数,则可以为它提供额外的形参。此时,用到这些自定义函数的new表达式必须使用new的定位形式将实参传给新增的形参。

注:下面的这个函数无论如何都不能被用户重载:

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

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

当将operator delete或operator delete[]定义成类的成员时,该函数可以包含另外一个类型为size_t的形参。此时,该形参的初始值是第一个形参所指对象的字节数。size_t形参可用于删除继承体系中的对象。

(二)定位new表达式

对于operator new分配的内存空间应该使用new的定位new形式构造对象。

new的这种形式为分配函数提供了额外的信息。可以使用定位new传递一个地址,此时定位new的形式如下:

1.new (place_address) type

2.new (place_address) type (initializers)

3.new (place_address) type [size]

4.new (place_address) type [size] {braced initializer list}

//place_address必须是一个指针,同时在initializer中提供一个(可能为空的)以逗号分隔的初始值列表,该初始值列表将用于构造新分配的对象。

定位new允许我们在一个特定的、预先分配的内存地址上构造对象。

当只传入一个指针类型的实参时,定位new表达式构造对象但不分配内存。

二、运行时类型识别

运行时类型识别(RTTI)的功能由两个运算符实现:

1.typeid运算符,用于返回表达式的类型。

2.dynamic_cast运算符,用于将基类的指针或引用安全地转换成派生类的指针或引用。 

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

(一)dynamic_cast运算符 

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

1.dynamic_cast<type*>(e)//e必须是一个有效的指针

2.dynamic_cast<type&>(e)//e必须是一个左值

3.dynamic_cast<type&&>(e)//e不能是左值

//type必须是一个类类型,并且通常情况下该类型应该含有虚函数。

在上面的所有形式中,e的类型必须符合以下三个条件中的任意一个:

e的类型是目标type的公有派生类、e的类型是目标type的公有基类或者e的类型就是目标type的类型。若符合,则类型转换可以成功,否则,转换失败。若一条dynamic_cast语句的转换目标是指针类型并且失败了,则结果为0。若转换目标是引用类型并且失败了,则dynamic_cast运算符将抛出一个bad_cast异常。

(二)typeid运算符

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

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

typeid运算符可以作用于任意类型的表达式。若表达式是一个引用,则typeid返回该引用所引对象的类型。当typeid作用于数组或函数时,并不会执行向指针的标准类型转换。即,若对数组a执行typeid(a),则所得的结果是数组类型而非指针类型。

当运算对象不属于类类型或者是一个不包含任何虚函数的类时,typeid运算符指示的是运算对象的静态类型。当运算对象是定义了至少一个虚函数的类的左值时,typeid的结果直到运行时才会求得。

typeid用于比较两条表达式的类型是否相同,或者比较一条表达式的类型是否与指定类型相同:

Derived *dp=new Derived;
Base *bp=dp;
if(typeid(*bp)==typeid(*dp))
{
    //判断bp和dp是否指向同一类型的对象
}

typeid是否需要运行时检查决定了表达式是否会被求值。只有当类型含有虚函数时,编译器才会对表达式求值。反之,若类型不含有虚函数,则typeid返回表达式的静态类型,编译器无须对表达式求值也能直到表达式的静态类型。

若表达式的动态类型可能与静态类型不同,则必须在运行时对表达式求值以确定返回的类型。这条规则适用于typeid(*p)的情况。若指针p所指的类型不含有虚函数,则p不必非得是一个有效的指针。否则,*p将在运行时求值,此时p必须是一个有效的指针。若p是一个空指针,则typeid(*p)将抛出一个名为bad_typeid的异常。

(三)使用RTTI

在某些情况下RTTI非常有用,例如当我们想为具有继承关系的类实现相等运算符时。对于两个对象来说,若它们的类型相同并且对应的数据成员取值相同,则说这两个对象是相等的。

(四)type_info类 

type_info类的精确定义随着编译器的不同而略有差异。但,C++标准规定type_info类必须定义在typeinfo头文件中。

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

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

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

三、枚举类型 

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

(一)定义枚举类型 

C++包含两种枚举:

1.限定作用域的

2.不限定作用域的

C++11新标准引入了限定作用域的枚举类型。

限定作用域的枚举类型的一般形式:

enum class(或enum struct) 枚举类型名字{枚举成员列表}; 

 例:

enum class open_mode{input,output,append};

定义不限定作用域的枚举类型的一般形式:

enum 枚举类型名字{枚举成员列表}; 

enum color{red,yellow,green};

 (二)枚举类型

在限定作用域的枚举类型中,枚举成员的名字遵循常规的作用域准则,并且在枚举类型的作用域外是不可访问的。与之相反,在不限定作用域的枚举类型中,枚举成员的作用域与枚举类型本身的作用域相同:

enum colorP{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。若没有显示地提供初始值,则当前枚举成员的值等于之前枚举成员的值加1。

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

(三)指定enum类型

在C++11新标准中,可以在enum的名字后加上冒号以及想在该enum中使用的类型:

enum intValues: unsigned long long{
    charTyp=255, shortTyp=65535
};

 若没有指定enum的潜在类型,则默认情况下限定作用域的enum成员类型是int。对于不限定作用域的枚举类型,其枚举成员不存在默认类型。

(四)枚举类型的前置声明

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

//不限定作用域的枚举类型intValues的前置声明
enum intValues: unsigned long long;//不限定作用域的,必须指定成员类型
enum class open_modes;//限定作用域的枚举类型可以使用默认成员类型int

 不能在同一个上下文中先声明一个不限定作用域的enum名字,然后再声明一个同名的限定作用域的enum:

enum class intValues;
enum intValues;//错误,intValues已经被声明为限定作用域的enum
enum intValues :long;//错误,intValues已经被声明成int

四、类成员指针

成员指针指可以指向类的非静态成员的指针。

成员指针的类型囊括了类的类型以及成员的类型。当初始化一个类成员指针时,令其指向类的某个成员,但不指定该成员所属的对象;知道使用成员指针时,才提供成员所属的对象。

(一)数据成员指针

和其他指针一样,在声明成员指针时也使用*来表示当前声明的名字是一个指针。与普通指针不同,成员指针还必须包含成员所属的类。

语法:

指针数据类型 classname::*指针名; 

例:

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

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

例:

padata=&Screen::contents;

1.使用数据成员指针

有两种成员指针访问运算符可以访问指针所指成员:

1. .*

2. ->*

例:

//.*解引用pdata以获得myScreen对象的contents成员
auto s=myScreen.*pdata;
//->*解引用pdata以获得pScreen所指对象的contents成员
s=pScreen->*pdata;

 (二)成员函数指针

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

指向成员函数的指针需要指定目标函数的返回类型和形参列表。与普通的函数指针类似,若成员存在重载的问题,必须显示地声明函数类型以明确指出想要使用的是哪个函数。

例:

char (Screen::*pmf2)(Screen::pos,Screen::pos) const;
pmf2=&Screen::get;

 与普通函数指针不同,在成员函数和指向该成员的指针之间不存在自动转换规则:

//pmf指向一个Screen成员,该成员不接受任何实参且返回类型是char
pmf=&Screen::get;//必须显式地使用取地址运算符
//pmf=Screen::get;//错误,在成员函数和指针间不存在自动转换规则

同样使用.*或->*运算符来使用成员函数指针:

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

五、嵌套类

一个类可以定义在另一个类的内部,前者称为嵌套类或嵌套类型。

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

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

外层类对嵌套类的成员没有特殊的访问权限,嵌套类对外层类的成员也没有特殊的访问权限。 

嵌套类在其外层类中定义了一个类型成员。和其他成员类似,该类型的访问权限由外层类决定。

六、union

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

union不能含有引用类型的成员,除此之外,它的成员可以是绝大多数类型。在C++11新标准中,含有构造函数或析构函数的类类型也可以作为union的成员类型。union可以为其成员指定public、protected和private等保护标记。

默认情况下,union的成员都是公有的。

(一)定义union

例:

union Token{
    //默认情况下成员是公有的
    char cval;
    int ival;
    double dval;
};

 (二)使用union类型

若提供了初始值,则该初始值被用于初始化第一个成员:

Token first_token={'a'};//初始化cval成员

 使用通用的成员访问运算符访问一个union对象的成员:
 

first_token.cval='z';
pt->ival=42;

(三)匿名union

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

union{
    char cval;
    int ival;
    double dval;
};
cval='c';//为刚刚定义的未命名的匿名union对象赋一个新值
ival=42;//该对象当前保存的值是42

 在匿名union的定义所在的作用域内该union的成员都是可以直接访问的。

(四)含有类类型成员的union

对于含有特殊类类型的成员的union,若想将union的值改为类类型成员对应的值,或者将类类型成员的值改为一个其他值,则必须分别构造或析构该类类型的成员:当将union的值改为类类型成员对应的值时,必须运行该类型的构造函数;反之,当将类类型成员的值改为一个其他值时,必须运行该类型的析构函数。

当union包含的是内置类型的成员时,编译器将按照成员的次序依次合成默认构造函数或拷贝控制成员。但若union含有类类型的成员,并且该类型自定义了默认构造函数或拷贝控制成员,则编译器将为union合成对应的版本并将其声明为删除的。

例如,string类定义了五个拷贝控制成员以及一个默认构造函数。若union含有string类型的成员,并且没有自定义默认构造函数或某个拷贝控制成员,则编译器将合成缺少的成员并将其声明成删除的。若在某个类中含有union成员,而且该union含有删除的拷贝控制成员,则该类与之对应的拷贝控制操作也将是删除的。

七、局部类 

类可以定义在某个函数的内部,称为局部类。

局部类定义的类型只在定义它的作用域内可见。和嵌套类不同,局部类的成员受到严格限制。

局部类的所有成员(包括函数在内)都必须完整定义在类的内部。因此局部类的作用与嵌套类相比相差很远。

在局部类中不允许声明静态数据成员,因为没法定义这样的成员。

(一)局部类可使用的变量 

局部类只能访问外层作用域定义的类型名、静态变量以及枚举成员。若局部类定义在某个函数内部,则该函数的普通局部变量不能被该局部类使用:

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;//正确,使用一个枚举成员
        }
    }
}

 八、固有的不可移植的特性

为了支持底层编程,C++定义了一些固有的不可移植的特性。

不可移植的特性是指因机器而异的特性,当将含有不可移植特性的程序从一台机器转移到另一台机器上时,通常需要重新编写该程序。

(一)位域 

类可以将其(非静态)数据成员定义成位域,在位于中含有一定数量的二进制位。

当一个程序需要向其他程序或硬件设备传递二进制数据时,通常会用到位域。

位域在内存中的布局是与机器相关的。

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

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

struct File{
    unsigned int mode:2;//mode占2位
    unsigned int modified:1;//modified占1位
};

 取地址运算符(&)不能用于位域,因此任何指针都无法指向类的位域。

(二)volatile限定符

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

直接处理硬件的程序常常包含这样的数据元素,它们的值由程序直接控制之外的过程控制。

当对象的值可能在程序的控制或检测之外被改变时,应该将该对象声明为volatile。关键字volatile告诉编译器不应对这样的对象进行优化。

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

volatile int display;//该int值可能发生改变

 也可以将成员函数定义成volatile的,只有volatile的成员函数才能被volatile的对象调用。

可以声明volatile指针、指向volatile对象的指针以及指向volatile对象的volatile指针:

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对象,则该类必须自定义拷贝或移动操作。

(三)链接指示:extern "C"

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语言的链接指示。

1.指向extern "C"函数的指针

//pf指向一个C函数,该函数接受一个int返回void
extern "C" void (*pf)(int);
//当使用pf调用函数时,编译器认定当前调用的是一个C函数

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值