欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

c++ primer 第五版 笔记 第十九章(完)

程序员文章站 2022-06-01 11:38:06
...

第十九章 特殊工具与技术

19.1 控制内存分配

19.1.1 重载new和delete

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

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

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

delete sp;
delete [] arr;
  1. 对sp所指的对象或者arr所指的数组中的元素执行对应的析构函数

  2. 第二步,编译器调用operator delete(或operator delete[])的标准库函数释放内存空间。

operator new接口和operator delete接口

标准库定义了operator new函数和operator delete函数的8个重载版本。其中前四个版本可能抛出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;

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

应用程序可以自定义上面函数版本中的任意一个,前提是自定义的版本必须位于全局作用域或者类作用域中。当我们将上述运算符函数定义成类的成员时,他们是隐式静态的。我们无须声明static,当然这么做也不会引发错误。

因为operator new用在对象构造之前而operator delete用在对象销毁之后,所以这两个成员必须是静态的,而且他们不能操作类的任何数据成员

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

尽管在一般情况下我们可以自定义具有任何形参的operator new,但是下面这个函数却无论如何也不能被用户重载:

void * operator new (size_t,void *);

19.1.2 定位new表达式

格式如下:

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 *)"分配"它的内存。这是一个我们无法自定义的operator new版本。该函数不分配任何内存,它只是简单地返回指针实参;然后由new表达式负责在指定的地址初始化对象以完成整个工作。事实上,定位new允许我们再一个特定的,预先分配的内存地址上构造对象

显式的析构函数调用

string *sp = new string("a value");
sp->~string();

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

19.2 运行时类型识别

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

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

19.2.1 dynamic_cast运算符

形式如下:

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

在上面的所有形式中,e的类型必须符合以下三个条件中的任意一个:e的类型是目标type的公有派生类、e的类型是目标type的公有基类或者e的类型就是目标type的类型。

如果一条dynamic_cast语句的转换目标是指针类型并且失败了,则结果为0.如果转换目标是引用类型并且失败了,则dynamic_cast运算符将抛出一个bad_cast异常

指针类型的dynamic_cast

例子如下:

if(Derived *dp = dynamic_cast<Derived*>(bp)){
    //使用dp指向的Derived对象
}else{
    //使用bp指向的对象
}

上面需要注意一下,我们再条件部分定义了dp,这样做的好处可以在一个操作中同时完成类型转换和条件检查两项任务。而且,指针dp在if语句外部是不可访问的。

引用类型的dynamic_cast
例如:

void f(const Base &b){
    tye{
        const Derived &d = dynamic_cast<const Derived&>(b);
        //使用b引用的Derived对象
    }catch(bad_cast){
        //处理类型转换失败的情况
    }
}

19.2.2 typeid运算符

形式如下:

typeid(e)

其中e可以是任意表达式或类型名字。结果是一个常量对象的引用,该对象的类型是标准库类型type_info或者type_info的公有派生类型。

当typeid作用于数组或函数时,并不会向指针的标准类型转换。因此,对于数组a来说,typeid(a),得到的结果是数组类型而非指针类型

当运算对象不属于类类型或者是一个不包含任何虚函数的类时,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而不是bp

if(typeid(bp)==typeid(Derived)){
    //此处代码永远不会执行
}

这个条件比较的是Base*和Derived。尽管指针所指的对象类型是一个含有虚函数的类,但是指针本身并不是一个类类型的对象。类型Base*将在编译时求值,显然它与Derived不同,因此不论bp所指的对象到底是什么类型,上面条件都不会满足。

19.2.4 type_info类

type_info类的精确定义随着编译器的不同而略有差异。不过c++标准规定type_info类必须定义在typeinfo头文件中,并且至少提供了表19.1所列的操作

c++ primer 第五版 笔记 第十九章(完)

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

type_info 类的name成员函数返回一个c风格字符串,表示对象的类型名字。对于某种给定的类型来说,name的返回值因编译器而异并且不一定与在程序中使用的名字一致。对于name返回值的唯一要求是,类型不同则返回的字符串必须有所区别。

19.3 枚举类型

c++包含两种枚举:限定作用域和不限定作用域

限定作用域类型:关键字enum class(或者enum struct),随后是枚举类型名字以及用花括号括起来的以逗号分隔的枚举成员,最后是分号:

enum class open_modes(input,output,append);

不限定作用域的枚举类型:就是忽略关键字class或(struct)

enum color {red,yellow,green};

枚举成员

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

与之相反,在不限定作用域的枚举类型中,枚举成员的作用域与枚举类型本身的作用域相同

默认情况下,枚举值从0开始,依次加1,也可以如下:

enum class intTypes{
    charType = 8,shorType = 16, intType = 16,
    longType = 32,long_longType = 64
};

枚举成员是const的。

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

只要enum有名字,我们就能定义并初始化该类型的成员。要想初始化enum对象或者位enum对象赋值,必须使用该类型的一个枚举成员或者该类型的另一个对象:

open_modes om = 2;//错误;2不属于open_modes
om = open_modes::input;//正确:input是open_modes的一个枚举成员

一个不限定作用域的枚举类型的对象或枚举成员自动地转换成整形。因此,我们可以在任何需要整形值得地方使用他们:

int i = color::red;//正确:不限定作用域的枚举类型成员隐式地转换成int

指定enum大小

尽管每个enum都定义了唯一的类型,但实际上enum是由某种整数类型表示的。在c++11新标准中,我们可以在enum的名字后面加上冒号以及我们想在该enum中使用的类型:

enum intValues:unsigned long long{
    charTyp = 255,shortTyp = 65535,
    intTyp = 65535,
    longTyp = 4294967295UL,
    long_longTyp = 184467440737095516115ULL
};

如果没有指定enum的潜在类型,则默认情况下限定作用域的enum成员类型是int。

对于不限定作用域的枚举类型来说,其枚举成员不存在默认类型,我们只知道成员的潜在类型足够大,肯定能容纳枚举值。

如果我们指定了枚举成员的潜在类型,则一旦某个枚举成员的值超出了该类型所能容纳的范围,将引发程序错误

枚举类型的前置声明

可以提前声明enum。enum的前置声明必须指定其成员的大小:

enum intValues:unsigned long long;//不限定作用域,必须指定成员类型
enum class open_modes;//限定作用域的枚举类型可以使用默认的成员类型int
//错误:所有的声明和定义必须对enum是限定作用域还是不限定作用域保持一致
enum class intValues;
//错误:intValues已经被声明成限定作用域的enum
enum intValues;
//错误:intValues已经被声明称int了
enum intValues:long;

形参匹配与枚举类型

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;
}
void newf(unsigned char);
void newf(int);
unsigned char uc = VIRTUAL;

newf(VIRTUAL);//调用newf(int)
newf(uc);//调用newf(unsigned char)

19.4 类成员指针

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

指向静态成员的指针,与普通指针没什么区别。

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

19.4.1 数据成员指针

const string Screen::*pdata;

在*之前添加所属类。上例表示:pdata可以指向一个Screen对象的const string成员

pdata = &Screen::contents;

上面对pdata进行赋值,令其指向Screen对象的contents成员

使用数据成员指针

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

19.4.2 成员函数指针

//pmf是一个指针,它可以指向Screen的某个常量成员函数
//前提是该函数不接受任何实参,并且返回一个char
auto pmf = &Screen::get_cursor;
char (Screen::*pmf2)(Screen::pos,Screen::pos) const;
pmf2 = &Screen::get;

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

//pmf是指向一个Screen成员,该成员不接受任何实参且返回类型为char
pmf = &Screen::get;
pmf = Screen::get;//错误:在成员函数和指针之间不存在自动转换规则

使用成员函数指针

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

注意:因为函数调用运算符的优先级高,所以在声明指向成员函数的指针并使用这样的指针进行函数调用时,括号必不可少

使用成员指针的类型别名

using Action = char (Screen::*)(Screen::pos,Screen::pos) const;

Action get = &Screen::get;

使用function生成可调用对象

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

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

上述的function被定义为:接受string参数并返回bool值。

而实际上,string的empty函数并不需要参数。但是在调用对象的成员函数时,隐含有一个this实参。因此传递给fcn的参数,并当做了this。所以上述的例子才能成立.

所以,可以总结如下:

当我们定义一个function对象时,必须指定该对象所能表示的函数类型,即可调用对象的形式。如果可调用对象是一个成员函数,则第一个形参必须是表示该成员是在哪个对象上面执行的。同时,我们提供给function的形式中还必须指明对象是否是以指针或引用的形式传入的。

例如:

vector<string *> pvec;
function<bool (const string *)> fp = &string::empty;
find_if(pvec.begin(),pvec.end(),fp);

使用mem_fn

mem_fn来让编译器负责推断成员的类型,并且可以从一个成员指针生成一个可调用对象。

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

mem_fn生成的可调用对象可以通过对象调用,也可以通过指针调用:

auto f = mem_fn(&string::empty);
f(*svec.begin());
f(&svec[0]);

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

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

19.5 嵌套类

嵌套类在其外层类中定义了一个类型成员。和其他成员类似,该类型的访问权限由外层类决定。位于外层类public部分的嵌套类实际上定义了一种可以随处访问的类型;位于外层类protected部分的嵌套类定义的类型只能被外层类及其友元和派生类访问;位于外层类private部分的嵌套类定义的类型只能被外层类的成员和友元访问。

声明一个嵌套类

class TextQuery{
public:
    class QueryResult;
};
//下面的类负责定义QueryResult
class TextQuery::QueryResult{
    friend std::ostream& print(std::ostream&,const QueryResult&);

public:
    QueryResult(std::string,
        std::shared_ptr<std::set<line_no>>,
        std::shared_ptr<std::vector<std::string>>);
};
//下面的代码实现QueryResult的成员

TextQuery::QueryResult::QueryResult(std::string,
        std::shared_ptr<std::set<line_no>>,
        std::shared_ptr<std::vector<std::string>>):
        sought(s),lines(p),file(f){}

嵌套类的作用域查找和嵌套命名空间的作用域查找类似

19.6 union:一种节省空间的类

union(联合):可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。因此,分配给一个union对象的存储空间至少要能容纳它的最大的数据成员。

union具有下面的特殊性:

  1. 不能含有引用类型的成员

  2. 在c++11新标准中,含有构造函数或析构函数的类类型也可以作为union的成员类型。

  3. union可以指定其成员为public,protected,private。默认情况下为public

  4. union可以定义包括构造函数和析构函数在内的成员函数。但是由于union既不能继承自其它类,也不能作为基类使用,所以union中不能含有虚函数

定义unio

union Token{
    char cval;
    int ival;
    double dval;
};

使用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,编译器就自动为该union创建一个未命名的对象

union{
    char cval;
    int ival;
    double dval;
};

cval = 'c';
ival = 42;

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

含有类类型成员的union

当union包含的是内置类型的成员时,编译器将按照成员的次序合成默认构造函数或拷贝控制成员。

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

对于union来说,要想构造或销毁类类型的成员必须执行非常复杂的操作,因此我们通常把含有类类型成员的union内嵌在另外一个类当中。这个类可以管理并控制与union的类类型成员有关的状态转换。

19.7 局部类

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

局部类的所有成员都必须完整定义在类的内部。因此,局部类的作用于嵌套类相比相差很远

同样,局部类中也不允许声明静态数据成员。

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

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

int a,val;
void foo(int val){
    static int si;
    enum Loc{a = 1024,b};
    struct Bar{
        Loc locVal;//正确:使用局部类型名
        int barVal;
        void fooBar(Loc l = a){//正确默认实参时Loc::a
            barVal = val;//错误:val是foo的局部变量
            barVal = ::val;//正确:使用一个全局对象
            barVal = si;//正确:使用一个静态局部对象
            locVal = b;//正确:使用一个枚举成员
        }
    };
    //...
}

19.8 固有的不可移植的特性

19.8.1 位域

类可以将其数据成员定义为位域。

注意:位域在内存中的布局是与机器相关的

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

typedef unsigned int Bit;
class File{
    Bit mode:2;//mode占2位
    Bit modified:1;//modified占1位 
    Bit prot_owner:3;//prot_owner占3位
    Bit prot_gourp:3;//prot_group占3位
    Bit prot_world:3;//prot_world占3位
publicenum modes{READ = 01,WRITE = 02,EXECUTE = 03};
    File &open(modes);
    void close();
    void write();
    bool isRead() const;
    void setWrite();
};

mode位域占2个二进制位,modified占1个,其他成员各占3个。如果可能的话,在类的内部连续定义的位域压缩成一个整数的相邻位,从而提供存储压缩。

例如,这五个位域可能会存储在一个unsigned int中。至于二进制位是否能压缩到一个整数中以及如何压缩是与机器相关的。

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

访问位域的方式与访问类的其他数据成员的方式类似

19.8.2 volatile限定符

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

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

关键字volatile告诉编译器不应对这样的对象进行优化

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

注意:合成的拷贝对volatile对象无效

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

class Foo{
public:
    //从一个volatile对象进行拷贝
    Foo(const volatile  Foo&);
    //将一个volatile对象赋值给一个非volatile对象
    Foo& operator=(volatile const Foo&);
    //将一个volatile对象赋值给一个volatile对象
    Foo& operator=(volatile const Foo&) volatile;
};

19.8.3 链接指示:extern “C”

声明一个非c++的函数

extern "C" size_t strlen(const char *);

extern "C" {
    int strcmp(const char *,const char *);
    char *strcat(char *,const char *);
}

指向extern “C”函数的指针

//pf指向一个c函数,该函数接受一个int返回void
extern "C" void (*pf)(int);

注意:指向c函数的指针与指向c++函数的指针是不一样的类型。一个指向c函数的指针不能用在执行初始化或赋值操作后指向c++函数,反之亦然。

void (*pf1)(int);//指向c++函数
extern "C" void (*pf2)(int);//指向c函数
pf1 = pf2;//错误:pf1和pf2的类型不同

链接指示对整个声明有效

//f1是一个c函数,它的形参是一个指向c函数的指针
extern "C" void f1(void(*)(int));

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

//calc函数可以被c程序调用
extern "C" double calc(double dparm){}

本章完

c++终于完结,下一个系列,armlink