【C++知识】特殊工具与技术

前言

       这是最后一章内容了,内容依旧很多,介绍了C++为解决某些特殊问题设置了一系列特殊的处理机制。想了解更多详细知识,建议大家先自行查看书籍,这里主要介绍一些细节问题。

最后,如果有理解不对的地方,希望大家不吝赐教,谢谢!

十六、特殊工具与技术

控制内存分配

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

重载new和delete

     在使用new时,实际执行了三步操作:第一步,new表达式调用一个名为operator new(或者operator new[])的标准库函数。该函数分配一块足够大的、原始的、未命名的内存空间以使存储特定类型的对象(或者对象的数组)。第二步,编译器运行相应的构造函数以构造这些对象,并为其传入初始值。第三步,对象被分配了空间并构造完成,返回一个指向该对象的指针。

     在使用delete时,实际执行了两步操作:第一步,对要删除指的对象或者所指的数组中的元素执行对应的析构函数。第二步,编译器调用名为operator delete(或者operator delete[])的标准库函数释放内存空间。

    如果应用程序希望控制内存分配的过程,则它们需要定义自己的operator new函数和operator delete函数。

operator new接口和operator delete接口

     与析构函数类似,operator delete也不会允许抛出异常,当我们重载这些运算符时,必须使用noexcept异常说明符指定其不抛出异常。

    自定义的版本必须位于全局作用域中。当我们将运算符函数定义成类的成员时,它们是隐式静态的。因为operator new用在对象构造之前而operator delete用在对象销毁之后,所以这两个成员(new和delete)必须是静态的,而且它们不能操纵类的任何数据成员。

    对于operator new函数或者operator new[]函数来说,它的返回类型必须是void*,第一个形参的类型必须是size_t且该形参不能含有默认实参,存放所需字节数。

    如果想要自定义operator new函数,则可以为它提供额外的形参。但有个函数却不能被用户重载:

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

    对于operator delete函数或者operator delete[]函数来说,它们返回类型必须是void,第一个形参的类型必须是void*。当我们将operator delete或operator delete[]定义成类的成员时,该函数可以包含另外一个类型为size_t的形参。

术语:new表达式与operator new函数

     我们提供新的operator new函数和operator delete函数的目的在于改变内存分配的方式,但是不管怎样,我们都不能改变new运算符和delete运算符的基本含义。

malloc函数与free函数

     malloc函数接受一个表示待分配字节数的size_t,返回指向分配空间的指针或者返回0以表示分配失败。free函数接受一个void*,它是malloc返回的指针的副本,free将相关内存返回给系统,调用free(0)没有任何意义。

如下所示是编写operator new和operator delete的一种简单方式:

void *operator new(size_t size)
{
    if(void *mem=malloc(szie))
        return mem;
    else
        throw bad_alloc();
}
void operator delete(void *mem) noexcept{ free(mem); }

定位new表达式

      应用程序如果想把内存分配与初始化分离开来的话,需要调用operator new和operator delete。它们负责分配或释放内存空间,但是不会构造或销毁对象。与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分配的内存,传给定位new表达式的指针甚至不需要指向动态内存。

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

显式的析构函数调用

      对析构函数的显式调用与使用destory很类似,我们既可以通过对象调用析构函数,也可以通过对象的指针或引用调用析构函数,这与调用其他成员函数没什么区别。析构函数的形式是波浪线(~)加上类型的名字。

      和调用destory类似,调用析构函数可以清楚给定的对象但是不会释放该对象所在的空间,如果需要的话,我们可以重新使用该空间。

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

运行时类型识别

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

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

    适用于以下情况:我们想使用基类对象的指针或引用执行某个派生类操作并且该操作不是虚函数。

    使用RTTI必须要加倍小心,在可能的情况下,最好定义虚函数而非直接接管类型管理的重任。

dynamic_cast运算符

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

dynamic_cast<type*>(e)
dynamic_cast<type&>(e)
dynamic_cast<type&&>(e)

其中,type必须是一个类类型,并且通常情况下该类型应该含有虚函数。e的类型必须符合以下三个条件中的任意一个:e的类型是目标type的公有派生类、e的类型是目标type的公有基类或者e的类型就是目标type的类型。

指针类型的dynamic_cast

     我们可以对一个空指针执行dynamic_cast,结果是所需类型的空指针。

     在条件部分执行dynamic_cast操作可以确保类型转换和结果检查在同一条表达式中完成。

引用类型的dynamic_cast

      引用类型的dynamic_cast与指针类型的dynamic_cast在表示错误发生的方式上略有不同。当对引用的类型转换失败时,程序抛出一个名为std::bad_cast的异常。

typeid运算符

      typeid运算符,它允许程序向表达式提问:你的对象是什么类型?typeid表达式的形式是typeid(e),其中e可以是任意表达式或类型的名字。typeid操作的结果是一个常量对象的引用,该对象的类型是标准库类型type_info或者type_info的公有派生类型。

      typeid运算符可以作用域任意类型的表达式,和往常一样,顶层const被忽略,如果表达式是一个引用,则typeid返回该引用所引对象的类型。不过当typeid作用于数组或函数时,并不会执行指针的标准类型转换。也就是说,如果我们对数组a执行typeid(a),则所得的结果是数组类型而非指针类型。 当运算对象是定义了至少一个虚函数的类的左值时,typeid的结果直到运行时才会求得。

     当typeid作用于指针时(而非指针所指的对象),返回的结果是该指针的静态编译时类型。

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

虚equal函数

     继承体系中的每个类必需定义自己的equal函数。派生类的所有函数要做的第一件事都是相同的,那就是将实参的类型转换为派生类类型。 

type_info类

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

    type_info类没有默认构造函数,而且它的拷贝和移动构造函数以及赋值运算符都被定义成删除的。因此创建type_info对象的唯一途径是使用typeid运算符。type_info类的name成员函数返回一个C风格字符串,表示对象的类型名字。

    type_info类在不同的编译器上有所区别,有的编译器提供了额外的成员函数以提供程序中所用类型的额外信息。 

枚举类型

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

     有两种枚举:限定作用域的和不限定作用域的。定义限定作用域的枚举类型一般形式是:首先是关键字enum class,随后是枚举类型名字以及用花括号括起来的以逗号分隔的枚举成员列表,最后是一个分号。 

enum class open_modes{input,output,append};

定义不限定作用域的枚举类型时省略掉关键字class,枚举类型的名字是可选的。如果enum是未命名的,则我们只能在定义该enum时定义它的对象。

枚举成员

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

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

     只要enum有名字,我们就能定义并初始化该类型的成员。要想初始化enum对象或者为enum对象赋值,必须使用该类型的一个枚举成员或者该类型的另一个对象。一个不限定作用域的枚举类型的对象或枚举成员自动地转换成整形。因此我们可以在任何需要整型值的地方使用它们。

指定enum的大小

     实际上enum是由某种整数类型表示的,我们可以在enum的名字后加上冒号以及我们想在该enum中使用的类型。默认情况下,限定作用域的enum成员类型是int,而不限定作用域的枚举类型,枚举成员不存在默认类型,我们只知道成员的潜在类型足够大,肯定能够容纳枚举值。

     指定enum潜在类型的能力使得我们可以控制不同实现环境中使用的类型,我们将可以确保在一种实现环境中编译通过的程序所生成的代码与其他实现环境中生成的代码一致。

枚举类型的前置声明

     我们可以提前声明enum。enum的前置声明必须指定成员的大小。和其他声明语句一样,enum的声明和定义必须匹配,这意味着在该enum的所有声明和定义中成员的大小必须一致。

形参匹配与枚举类型

     要想初始化一个enum对象,必须使用该enum类型的另一个对象或者它的一个枚举成员。因此,即使某个整型值恰好与枚举成员的值相等,它也不能作为函数的enum实参使用。

类成员指针

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

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

数据成员指针

      在声明成员指针时我们也需要使用*来表示当前声明的名字是一个指针。与普通指针不同的是,成员指针还必须包含所属的类。因此,必须在*之前添加classname::以表示当前定义的指针可以指向classname的成员。例如:

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

当初始化一个成员指针(或者向它赋值)时,需指定它所指的成员。例如:pdata=&Screen::contents;当然声明成员指针最简单的方法是使用auto或decltype:auto pdata=&Screen::contents;

使用数据成员指针

    当我们初始化一个成员指针或为成员指针赋值时,该指针并没有指向任何数据。成员指针指定了成员而非该成员所属的对象,只有当解引用成员指针时我们才提供对象的信息。

返回数据成员指针的函数

      因为数据成员一般情况下是私有的,所以我们通常不能直接获得数据成员的指针。如果一个像Screen这样的类希望我们可以访问它的私有成员contents成员,最好定义一个函数,令其返回值是指向该成员的指针。

成员函数指针

      我们也可以定义指向类的成员函数的指针。和指向数据成员一样,使用classname::*的形式声明一个指向成员函数的指针。指向成员函数的指针也需要指定目标函数的返回类型和形参列表。如果成员函数是const成员或者引用成员,则必须将const限定符或引用限定符包含进来。

      和普通的函数指针类似,如果成员存在重载的问题,则必须显式地声明函数类型以明确指出我们想要使用的是哪个函数。例如,我们可以声明一个指针,令其指向含有两个形参的get:

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

出于优先级考虑,上述声明中Screen::*两端的括号必不可少。

使用成员函数指针

      和使用指向数据成员的指针一样,使用.*或者->*运算符作用于指向成员函数的指针,以调用类的成员函数。

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

使用成员指针的类型别名

     使用类型别名或typedef可以让成员指针更容易理解。例如,下面的类型别名将Action定义为两参数get函数的同义词:

using Action=char (Screen::*)(Screen::pos,Screen::pos) const;
Action get=&Screen::get; //get指向Screen的get成员

      和其他函数指针类似,我们可以将指向成员函数的指针作为某个函数的返回类型或形参类型,其中指向成员的指针形参也可以拥有默认实参:

//action接受一个Screen的引用,和一个指向Screen成员函数的指针
Screen& action(Screen&,Action=&Screen::get);

action是包含两个形参的函数,其中一个形参是Screen对象的引用,另一个形参是指向Screen成员函数指针。

成员指针函数表

     对于普通函数指针和指向成员函数的指针来说,一种常见的用法是将其存入一个函数表当中。如果一个类含有几个相同类型的成员,则这样一张表可以帮助我们从这些成员中选择一个。

class Screen{
public:
    Screen& home();
    Screen& forward();
    Screen& back();
};

//我们希望定义一个move函数,使其可以调用上面的任意一个函数并执行对应的操作,为了支持这个新函数,
//我们将在Screen中添加一个静态成员,该剩余是指向光标移动函数的指针的数组
class Screen{
public:
    using Action=Screen& (Screen::*)();
    enum Directions(HOME,FORWARD,BACK);
    Screen& move(Directions);
private:
    static Action Menu[]; //函数表
};

Screen& Screen::move(Direction cm)
{
    return (this->*Menu[cm])();  //Menu[cm]指向一个成员函数
}

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

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

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

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

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

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

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

     通过上面的介绍可知,要想使用function,我们必须提供成员的调用形式。我们也可以采用另外一种方法,通过使用标准库功能mem_fn来让编译器负责推断成员的类型。mem_fn也定义在functional头文件中,并且可以从成员指针生成一个可调用对象;和function不同的是,mem_fn可以根据成员指针的类型推断可调用对象的类型,而无需用户显式地指定

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

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

auto it=find_if(svec.begin(),svec.end(),bind(&string::empty,_1));

 嵌套类

     一个可以定义在另一个类的内部,前者称为嵌套类或者嵌套类型。嵌套类常用于定义作为实现部分的类。

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

     嵌套类的名字在外层类作用域中是可见的,在外层作用域之外不可见。外层类对嵌套类的成员没有特殊的访问权限,同样,嵌套类对外层类的成员也没有特殊的访问权限。

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

在外层类之外定义一个嵌套类

     嵌套类必须声明在类的内部,但是可以定义在类的内部或者外部。当我们在外层类之外定义一个嵌套类时,必须以外层类的名字限定嵌套类的名字。

嵌套类和外层类是相互独立的

     尽管嵌套类定义在其外层类的作用域中,但是外层类的对象和嵌套类的对象没有任何关系。 

union:一种节省空间的类

     union提供了一种有效的途径使得我们可以方便地表示一组类型不同的互斥值

//Token类型的对象只有一格成员,该成员的类型可能是下列类型中的任意一种
union Token{
    char cval;
    int ival;
    double dval;
};

使用union类型

     union的名字是一个类型名。和其他内置类型一样,默认情况下union是未初始化的。我们可以像显式地初始化聚合类一样使用一对花括号内的初始值显式地初始化一个union。为union的一个数据成员赋值会令其它数据成员变成未定义的状态。因此,当我们使用union时,必须清楚地知道当前存储在union中的值到底是什么类型。

匿名union

      匿名union是一个未命名的union,并且在右花括号和分号之间灭有任何声明。一旦我们定义了一个匿名union,编译器就自动地为该union创建一个未命名的对象。在匿名union的定义所在的作用域内该union的成员都是可以直接访问的。

     匿名union不能包含受保护的成员或私有成员,也不能定义成员函数。

含有类类型成员的union

     当union包含的是内置类型的成员时,我们可以使用普通的赋值语句改变union保存的值。如果我们想将union的值改为类类型成员对应的值,或者将类类型成员的值改为一个其他值,则必须分别构造或析构该类类型的成员:当我们将union的值改为一个其他值时,必须运行该类型的构造函数;反之,当我们将类类型成员的值改为一个其他值时,必须运行该类型的析构函数。

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

使用类管理union成员

      我们通常把含有类类型成员的union内嵌在另一个类当中,这个类可以管理并控制与union的类类型成员有关的状态转换。为了追踪union中到底存储了什么类型的值,我们通常会定义一个独立的对象,该对象称为union的判别式。我们可以使用判别式辨认union存储的值。我们的类将定义一个枚举类型的成员来追踪其union成员的状态。

局部类

    类可以定义在某个函数的内部,我们称这样的类为局部类。局部类定义的类型只在定义它的作用域内可见。和嵌套类不同,局部类定义的类型只在定义它的作用域内可见。和嵌套类不同,局部类的成员受到严格限制。

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

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

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

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

常规的访问保护规则对局部类同样适用

     外层函数对局部类的私有成员没有任何访问特权。

嵌套的局部类

     可以在局部类的内部再嵌套一个类。此时,嵌套类的定义可以出现在局部类之外,不过,嵌套类必须定义在与局部类相同的作用域中。

      局部类的嵌套类也是一个局部类,必须遵循局部类的各种规定。嵌套类的所有成员都必须定义在嵌套类内部。

固有的不可移植的特性

        所谓不可移植的特性是指因机器而异的特性,当我们将含有不可移植特性的程序从一台机器转移到另一台机器时,通常需要重新编写该程序。在这里主要介绍三种不可移植特性:位域、volatile限定符和链接指示。

位域

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

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

      位域类型必须是整型或枚举类型。位域声明形式是在成员名字之后紧跟一个冒号以及一个常量表达式,该表达式用于指定成员所占的二进制位数。

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

     通常情况下最好将位域设为无符号类型,存储在带符号类型中的位域的行为将因具体实现而定。

使用位域

     访问位域的方式与访问类的其他数据成员的方式非常相似。通常使用内置的位运算符操纵超过1位的位域。如果一个类定义了位域成员,则它通常也会定义一组内联的成员函数以检验或设置位域的值。

volatile限定符

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

      volatile限定符的用法和const很相似,它起到对类型额外修饰的作用。const和volatile限定符互相没什么影响。

合成的拷贝对volatile对象无效

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

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

链接指示:extern "C"

     编译器检查其调用的方式与处理普通C++函数的方式相同,但是生成的代码有所区别。C++使用链接指示指出任意非C++函数所用的语言。

      要想把C++代码和其他语言(包括C语言)编写的代码放在一起使用,要求我们必须有权访问该语言的编译器,并且这个编译器与当前的C++编译器是兼容的。

声明一个非C++的函数

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

     链接指示的第一种形式包含一个关键字extern,后面是一个字符串字面值常量以及一个“普通的”函数声明。其中的字符串字面值常量指出了编写函数所用的语言。比如extern "C"表示编译器应该支持对C语言的链接指示。此外,编译器也可能会支持其他语言的链接指示,如extern "Ada"、extern "FORTRAN"等。

链接指示与头文件

     我们可以令链接指示后面跟上花括号括起来的若干函数的声明,从而一次性建立多个链接。花括号的作用是将适用于该链接指示的多个声明聚合在一起,否则花括号就会被忽略,花括号中声明的函数名字就是可见的,就好像在花括号之外声明的一样。

     当一个#include指示被放置在复合链接指示的花括号中时,头文件中的所有普通函数声明都被认为是由链接指示的语言编写的。

    C++从C语言继承的标准库函数可以定义成C函数,但并非必须:决定使用C还是C++实现C标准库,是每个C++实现的事情。

指向extern "C"函数的指针

     编写函数所用的语言是函数类型的一部分。因此,对于使用链接指示定义的函数来说,它的每个声明都必须使用相同的链接指示。

链接指示对整个声明都有效

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

导出C++函数到其他语言

      通过使用链接指示对函数进行定义,我们可以令一个C++函数在其他语言编写的程序中可用。

extern "C" double calc(double dparm){}

编译器将为该函数生成适合于指定语言的代码。

重载函数与链接指示

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

烊萌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值