深度理解C++对象

C++对象模型

非静态数据成员被配置在每一个类对象内,静态数据成员则存放在类对象外
成员函数也存放在类对象外
虚拟函数则需要以下两个步骤支持:1每一个类产生一堆指向虚函数的指针,放在表格之中,这个表格就是虚函数表;2 每一个类对象被安插一个指针,指向相关的虚函数表,称为虚指针,它的设定和充值都有构造函数、析构函数和复制运算符自动完成的。 每一个类所关联的type_info object也经由虚函数表指出(用来支持runtime type identification,RTTI),通常放在希函数表的第一个slot
优点:空间和存取时间的效率,缺点是如果应用程序代码本身没有改变,但所用到的类的非静态成员函数有多改变,那么那些应用程序代码同样得重新编译

  • 不同类型的指针变量占据的内存空间都是相同的,但是其指向的变量占用不同大小的存储空间。
  • 一个对象里面的vptr是永远不会改变的,都会指向所属类型的虚函数表,这在编译时期就已经完全定义好了,因此不能通过对象实现多态,只能通过指针、引用实现多态,在被指定的objec的真实类型在每一个特定执行点之前,是无法解析的,也就是无法准确知道是哪种类型。

c++,多态只存在于public继承中,nonpublic、void*指针可以说是多态的,但它们没有被语言明确地支持,也就是说程序员需要通过显示的转换操作来管理。

private public protected
  • 第一: private,public,protected的访问范围:

    private: 只能由该类中的函数、其友元函数访问,不能被任何其他访问,该类的对象也不能访问.
    protected: 可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问
    public: 可以被该类中的函数、子类的函数、其友元函数访问,也可以由该类的对象访问
    注:友元函数包括两种:设为友元的全局函数,设为友元类中的成员函数

  • 第二:类的继承后方法属性变化:

    使用private继承,父类的所有方法在子类中变为private;
    使用protected继承,父类的protected和public方法在子类中变为protected,private方法不变;
    使用public继承,父类中的方法属性不发生改变;

  • 三种访问权限

    public:可以被任意实体访问

    protected:只允许子类及本类的成员函数访问

    private:只允许本类的成员函数访问

“指针类型”会教导编译器如何解释某个特定的内存内容及其大小,一个类型为void*的指针智能够持有一个地址,而不能通过它操作所指之的object, 所以转换(cast)其实是一种编译器指令,大部分情况下它并不改变一个指针所含的真正地址,它只影响“被指出之内存的大小和其内容”

一个类的内存大小:
  • 非静态数据成员的总和大小
  • padding部分(可能存在members之间,也可能村子与集合体边界)
  • 加上为了支持virtual而由内部产生的任何额外负担。

assignment操作,必须保证vptr的内容不会被改变

struct 实现多态

采用struct 实现多态其实就是参考c++类多态的思想,有一个虚函数表里面存放了函数指针。基类和派生类对这个函数表所指向的函数指针进行修改。
下面是例子:

#include <stdio.h>
#include <stdlib.h>

//虚函数表结构
struct base_vtbl
{
    void(*dance)(void *);
    void(*jump)(void *);
};

//基类
struct base
{
    /*virtual table*/
    struct base_vtbl *vptr;
};

void base_dance(void *this)
{
    printf("base dance\n");
}

void base_jump(void *this)
{
    printf("base jump\n");
}

/* global vtable for base */
struct base_vtbl base_table =
{
        base_dance,
        base_jump
};

//基类的构造函数
struct base * new_base()
{
    struct base *temp = (struct base *)malloc(sizeof(struct base));
    temp->vptr = &base_table;
    return temp;
}


//派生类
struct derived1
{
    struct base super;
    /*derived members */
    int high;
};

void derived1_dance(void * this)
{
    /*implementation of derived1's dance function */
    printf("derived1 dance\n");
}

void derived1_jump(void * this)
{
    /*implementation of derived1's jump function */
    struct derived1* temp = (struct derived1 *)this;
    printf("derived1 jump:%d\n", temp->high);
}

/*global vtable for derived1 */
struct base_vtbl derived1_table =
{
    (void(*)(void *))&derived1_dance,
    (void(*)(void *))&derived1_jump
};

//派生类的构造函数
struct derived1 * new_derived1(int h)
{
    struct derived1 * temp= (struct derived1 *)malloc(sizeof(struct derived1));
    temp->super.vptr = &derived1_table;
    temp->high = h;
    return temp;
}

 

int main(void)
{

    struct base * bas = new_base();
    //这里调用的是基类的成员函数
    bas->vptr->dance((void *)bas);
    bas->vptr->jump((void *)bas);

 

    struct derived1 * child = new_derived1(100);
    //基类指针指向派生类
    bas  = (struct base *)child;
     
    //这里调用的其实是派生类的成员函数
    bas->vptr->dance((void *)bas);
    bas->vptr->jump((void *)bas);
    return 0;
}

上面代码不仅实现了多态的性质,其实也在模拟C++中的类的继承。主函数中基类指针 bas 一开始指向的是基类对象,因此调用 dance 和jump是基类的dance jump 。下面把bas 指向派生类的对象,再调用dance 和jump 的时候就不是基类的dance 和jump了,而转化成了派生类的dance 和jump.

class 引入的不仅仅是一个关键词,还代表它所支持的封装和继承的哲学,如果是抽象的base struct,其中要内涵一个或更多个的virtual base struct

C++中处于同一个access section的数据,必定保证以其声明顺序出现在内存中,但是被放置在多个access section中的各笔数据,排列顺序就不一定了。

C struct 在C++中的一个合理用途是采用组合的方式传递一个复杂数据结构

编译器合成构造函数的情况

编译器需要的构造函数,为nontrivial,被合成的默认构造函数只满足编译器的需要,而不是程序的需要,如果是程序需要的部分,需要程序员初始化。

  • (1) 带有default construct 的member class object

如果一个类内含一个带有构造函数的类,如果这个类没有构造函数,则编译器会合成一个默认构造函数,如果类中已经有构造函数,那么编译器会扩张已经存在的构造函数,按类的声明顺序在用户定义的代码执行前插入。

  • (2)带有default constructor的base class

如果一个没有任何构造函数的类派生自一个带有默认构造函数的基类,那么这个类的默认构造函数会被编译器合成。同时如果设计者已经提供了多个构造函数,但是其中没有默认构造函数,编译器不会去合成新的构造函数而是会在其他构造函数中进行扩张,如果同时存在带有默认构造函数的member class object的情况,在所有base class construct 构造之后,member class 的默认构造函数也会被调用。

  • (3)带有一个虚函数的类

1 class 声明(继承)一个虚函数
2 class 派生自一个继承串联,其中有一个或更多的虚基类。
不管是以上哪种情况,由于缺乏user声明的construct,编译器会详细记录合成一个默认构造函数的信息。
在编译器期间,有两个扩张行动会发生, 1虚函数表会被编译器产生出来,内放类的虚拟函数地址。2在每一个class object 中,一个额外的虚指针会被编译器合成出来,内含相关的类的虚函数表地址。
为了让多态机制发挥作用,编译器必须为每一个class object的虚指针设定初始值,放置适当的虚函数表地址。同样,如果类中存在构造函数,编译器会安插代码,如果没有构造函数则会合成一个默认构造函数。

+(4) 带有一个virtual base class 的类

在构造期间,编译器会产生指向虚基类的指针。

总结

被合成出来的构造函数只能满足编译器的需要,它之所以能够完成任务是借着“调用member object或base class 的default construct”或是为每一个object初始化其virtual function机制或virtual base class 机制而完成的。
在合成的默认构造函数中,只有base class subobject 和member class object 会被初始化, 所有其他额定 数据成员,如int*,是不回被初始化的,需要程序员提供初始化。

copy constructor 的构造操作

决定编译器是否合成出一个copy constructor,判断一个copy constructor 是否为trivial的标准在于Class是否展现所谓的“bitwise copy semantics”,也就是位逐次拷贝,如果不是位逐次拷贝,那么就是nontrivial.
以下这4种不展现出bitwise copy semantics;

  • (1)当class内含一个member object 而 后者声明有一个copy constructor(无论显示声明或被合成而得)
  • (2)当class继承自一个base class 而后者存在一个copy constructor(无论显示声明或被合成而得)
  • (3)当class 声明了一个或多个虚函数
  • (4)当class 派生子一个继承串联,其中一个或多个虚基类

对于情况3 4,因为编译器要导入一个虚指针到class中,该class就不再展现bitwise semantics 了,对于同一类的对象可以直接复制,可以直接靠bitwise copy semantics 完成。

但是不同“层级”的类对象,也就是基类和其派生类的对象的复制,虚指针会正确地初始化,而不是直接从等号右边的值拷贝,主要是要保证虚指针的正确性,同样对于虚继承,要确保虚基类指针的初始值的正确性。

在以上这四种情况,如果缺乏一个已声明的copy constructor,编译器会为了正确吹“以一个class object 作为另一个class object 的初值”,必须合成出一个copy constructor。

函数的返回值如何从局部对象xx中拷贝过来的?
方法是双阶段转换

1 首先加上一个额外的参数,类型是class object 的一个reference。这个参数用来放置“
拷贝建构“而得的返回值
2 在return指令之前安插一个copy constructor 调用操作,以便将欲传回的object的内容当作上述新增参数的初值

在编译器层面优化 称为 Named Return Value (NRV)优化,需要提供拷贝构造函数,可以提高效率。

如果在大量提供memberwise 初始化操作时,提供拷贝构造函数进行优化

memset()、memcpy()不能随便用,如果有virtual 函数或者内含虚基类,会改变相对应的虚指针

成员的初始化队伍

以下情况中,必须采用list的方式初始化

1当初始化一个 reference member;
2 当初始化一个const member
3当调用一个base class的constructor,而它拥有一组参数
4 当调用一个member class的constructor,而它拥有一组参数时

  • 之所以要用list的 方式初始化,是因为效率高,如果采用construct函数内的方式初始化,会需要产生一个临时性对象,将它初始化后用assignment运算符将临时性object指定给变量,随后再摧毁临时性object.
  • 采用list的方式初始化,编译器会在constructor内安插初始化操作,且安插顺序由class中member声明顺序决定的,同时安插的代码会置于任何 explicit user code 之前.

析构

如果class没有定义析构函数,那么只有在class内含的member object(抑或class自己的base class)拥有析构函数的情况下,编译器才会自动合成出一个来,否则,析构函数被视为不需要,也就不需要合成。
析构顺序与构造顺序相反

1析构函数本体首先被执行
2如果class拥有member class objects,而后者拥有析构函数,那么他们会以声明顺序的相反顺序被调用。
如果object内含一个vptr,现在被重新设定,指向适当的base class的virtual table;
如果有任何直接的(上一层)非虚 基类有析构函数,他们会以其声明的相反顺序调用
如果有任何虚基类有析构函数,而目前讨论的这个class是最尾端的class,那么它们会以其原来的构造顺序的相反顺序被调用。

DATA Member

早期C++的两种防御性程序设计风格

1 把所有的data members 放在class声明起头处,以确保正确的绑定
2 把所有的inline functions 不管大小都放在class声明之外。

Data Member 布局

同一个access section 中,members的排列要符合“较晚出现的members在class中有较高的位置”

vptr的位置,传统上它被放在所有显示声明的members的最后。

数据成员的存取

静态数据成员只有一个实例,并放在了程序的数据段中
例如 Point3d oeigin ,*pt=&origin
对于静态数据成员, 是C++种通过一个指针和通过一个对象来存取member,结论完全相同的唯一一种情况。主要原因是静态数据成员不在类实例中,存取静态数据成员不需要通过类实例,即使静态数据陈冠是从一个复杂继承关系中继承而来,其存取路径仍然是那么直接。
对一个静态数据成员取地址,会得到一个指向其数据类型的指针,而不是一个指向其class member的指针。原因还是因为静态数据成员并不包含在一个类实例中。

如果多个class都声明了一同一名称的静态数据成员,当它们都放在程序的data segment时,就会导致名称冲突,解决冲突的方法是暗中对每一个静态数据成员编码(name-mangling)以获得独一无二的程序识别代码

对一个非静态数据成员进行存取操作是,编译器需要把类实例的起始地址加上数据成员的偏移地址,有一个要注意的是,指向数据成员的指针,其offset值总是被加上1,这样可以使编译系统区分出“一个指向数据成员的指针,用来指出类的第一个成员”和“一个指向数据成员的指针,没有指向任务成员”的两种情况, 因为存取操作加上offset地址时要减去1,例如
origin._y=0;
&origin._y=&origin+(&point3d::_y-1)

每一个非静态数据成员的偏移位置在编译时期可获知,存取非静态数据结构的效率和存取一个C结构成员和非派生的成员是一样的

虚拟继承时,当被读取的成员是由虚拟继承而得,那这个存取操作必须延迟到执行期。

在写继承时易犯的问题是

1重复设计相同操作的函数
2 把class分解为两层或更多层,导致膨胀所需要的空间。

边界调整是由处理器决定的

加上多态的空间和存取时间上的额外负担:

1 导入虚函数表 存放虚函数地址,表格的个树是虚函数的个树加上一个或两个slot用来支持 runtime type identification
2 在类实例中导入vptr,提供执行期的链接,使得每一个对象找到对应的虚函数表。
3加强构造函数,为vptr设定初值
4 加强析构函数,抹消vptr,顺序是反向的

在设计多态时,要考虑额外负担的冲击

如果vptr放在了前端,代价是丧失了C语言的兼容性

多重继承

单一继承时,把一个派生类对象指定给基类的指针或引用,这个操作不需要编译器去调停或者修改地址,它可以最自然地发生,而且提供最佳的执行效率
多重基层优势虚拟继承的情况下,编译器更有介入的必要
对于多重派生对象,将其地址指定给第一个基类的指针,情况将和单一继承时相同,因为两者都是指向相同的起始地址。需付出的成本只有地址的指定操作而已。至于是第二个或者是后继的基类地址指定操作,则需要将地址修改过,加上或减去中间的base class subobject大小,需要注意的是对于指针需要对0值做防卫,而引用不需要,因为引用不可能参考到无物
如果要存取后继基类的一个数据成员是不需要付出额外的成本的,因为成员的位置在编译时就固定 了,因此只是一个简单的offset运算

虚拟继承

class内含一个或多个虚基类,将被分割成两部分,一个不变区域和一个共享区域。
至于共享区域,所表现就是virtual base class subobject ,这部分的数据,其位置会因每次的派生操作而有变化,所以他们只可以被间接存取。这是不同编译器实现技术的差异

解决存取继承而来的成员的问题:编译器会在派生的类对象中安排一个指针指向虚基表,该表格中放置了虚基类的偏移地址。(微软是在虚基表中存放基类地址)以上可以让类对象有固定的负担同时有固定的存取时间,不会因为虚基类个数和虚拟派生的深度改变。

一般而言,虚基类最有效的一种运用形式是:一个抽象的虚基类,没有任何数据成员。

指向member的指针的效率低, 虚拟继承的额外间接性会降低“把所有的处理都办到寄存器中执行的能力。”

静态成员函数

静态成员函数的主要特征是它没有this指针。

this指针把“在成员函数中存取的非静态类成员”绑定于对象对应的成员之上

静态成员函数的特性

1 它不能够直接存取其class的nonstatic member;
2 它不能够被声明为const /volatile或virtual
3它不需要经由class object 才被调用

如果去一个静态成员函数的地址,获得的将是其在内存中的位置,也就是其地址。由于静态成员函数没有this指针,所以地址类型并不是一个指向类成员函数的指针,而是一个“nonmember函数指针”,也就是得到类似 unsigned int(*)()指针

因为没有this指针,所以它可以成为一个callback函数。

虚拟成员函数

每一个虚拟函数都被指派一个固定的索引值,这个索引值在整个继承体系中保持与特定的虚函数的关系

多重继承下的虚函数

base1* pbase1=new Derived;
base2*pbase2=new Derived;
当base1是最左端的基类时,不需要调整this指针,因为它已经指向派生对象的起始处,其虚函数表slot需要放置真正的析构函数地址

当base2是第二个或之后的基类时,需要调整this指针,其虚函数表slot需要相关的thunk地址。所谓的thunk是一小段assembly代码,用来 (1)以适当的offset值调整this指针,(2)跳到虚函数去。

在多重继承下,一个派生类内含n-1个额外的虚函数表,n表示其上一层的基类个数,因此单一继承不会有额外的虚函数表。根据指向的基类指针,采用对应的虚函数表处理
针对每一个虚函数表,派生类对象应该都有对应的vptr;

thunk的技术允许虚函数slot内继续内含一个简单的指针,因此多重继承不需要任何空间上的额外负担。

为了调节执行器链接器的效率,sun编译器将多个虚函数表连锁为一个,指向次要表格的指针,可由主要表格的名称加上一个offset获得,在这样的策略之下,每一个类只有一个具名的虚函数表。

以下情况需要调整this指针
1 通过一个“指向第二个基类”的指针,调用派生类的虚函数。
2通过一个指向派生类的指针,调用第二个基类中继承而来的虚函数。
3 发生于语言扩充性质之下:允许一个虚函数的返回值类型有所变化。

指向成员函数的指针

取一个非静态的成员函数地址,如果该函数是非虚函数,得到的结果是它在内存中的真正地址。这个值是不完全的,需要被绑定于类对象的地址上,才能通过它调用该函数。
一个指向成员函数的指针,可以声明如下:
double(point::*pmf)();
书上写面对虚函数时,其地址在便宜时期是未知的,能知道的仅仅是在虚函数表中的索引值,也就是说,对一个虚函数取其地址,所能获得的只是一个索引值。(但是 我做实验也不是返回这个索引值,返回的也是一个地址?)

虚函数表中所指向的函数地址和函数指针所指向的地址都不是该函数的真正入口地址,虚函数表所指向的函数地址是该函数的符号地址【1.符号地址就是预先定义的,用替换符号代替地址的地址。是编译器在生成机器码时会自动计算替代成绝对地址的。2.绝对地址就是内存中的地址。】;而函数指针所指向的地址则是编译器为了满足函数指针类型定义而生成的函数指针的调用地址,在代码编译后,会针对每个函数指针的类型,定义各自的调用函数,而且每个调用函数也不是真的指向原函数的真正的入口地址。原理:类的成员函数指针和普通函数指针不一样,成员函数指针是一个结构体指针,里面包含了偏移量,标志(是否是虚函数),真实地址等。。总之,他们是无法进行比较的!

	typedef void(*Fun)(void); //函数指针
	A a;//A带有虚函数的类
	cout << (int*)*((int*)&a) << endl;
	Fun pfun = (Fun)*((int *)*(int *)(&a));  //vitural f();
  

对于求虚函数地址的解释 (int*)&a 是将对象a的地址解释成int型指针,为了后面取四个字节,为虚表指针。
取了虚表指针之后,按我的理解因为虚函数表中存放的是指针,所以是二级指针的用法,因此要加上(int*)*(int *)&a,
最后定位到某一个虚函数后,要解引用,把指向的值赋值给函数指针。
【但是现在遇到的问题是编译器会报错??】

为了允许能够寻址除非虚函数和虚函数,那么这两个函数要有相同的原型,只不过一个代表内存地址,另一个代表在虚函数中的索引值。判断技巧是看PMF与~127与之后,如果等于0意味着是虚函数,否则是非虚函数

inline 函数

处理一个inline函数有两个阶段
1 分析函数定义,以决定是否可以成为inline,如果不能成为inline,那么它会被转为一个static函数
2真正的inline操作是在调用那一个点上。

形式参数
  • 1实际参数是一个常量表达式时,在调换之前先完成求值
  • 2如果有会带来副作用的实际参数,通常需要引入临时性对象。
  • 3如果不是以上两种,直接替换就行。

例如
inline int min(int i,int j)
{
return i<j?i:j;
}
当在一个函数内调用这个内联函数,例如返回值为minval,如果填入min(foo(),bar()+1)

那么就是要引入临时对象
int t1;int t2;
minval=(t1=foo()),(t2=bar()+1),
t1<t2?1:t2;

  • 当内联函数中有局部变量时,为了维护其局部变量,在每次拓展中都需要自己的一组局部变量,如果再加上有副作用的参数,会导致大量临时性对象的产生。(因此内联函数最好就不要有局部变量,我认为)

如果一个inline函数被调用太多次,会产生大量的扩展码,使程序大小暴涨。

如果inline中有inline会因其连锁复杂度没有办法拓展开来。

纯虚函数

如果构造了一个析构函数的纯虚函数,那么类的设计者一定要定义这个函数,因为每一个派生的类的析构函数会被编译器加以扩张,以静态调用的方式调用其每一个虚基类以及上一层基类的析构函数,因此只要缺乏任何一个基类析构函数的定义就会导致链接失败。

这是因为编译器不能够合成一个纯虚析构函数。所以一个比较好的替代方法是 不要把虚析构函数声明为pure.

"无继承"情况下的对象构造

  • 全局定义的对象
    构造函数在程序起始处(startup)被调用,而析构函数在程序的exit()处(系统产生的,放在main()结束之前)被调用。

  • 局部对象
    如果只是像 Point local; 这样声明local对象,那么是没有调用构造函数初始化的。(如果是struct结构的对象,没有声明构造函数)

抽象数据类型

当有构造函数时,要对声明的对象有条件地调用构造函数,也就是判断是否成功申请了内存。

如果我们没有提供析构函数,调用delete时,并不会导致析构函数被调用。

c++编译器会尽量延迟nontrivial members的实际合成操作,直到真正遇到其使用场合为止。
例如 *heap=local , 就会触发copy assignment operator的合成。

##继承体系下的对象构造
构造函数中会有大量的隐藏码,因为编译器会扩充每一个构造函数

1 虚基类(1 在member list中,显示指定的参数要传递进去,没在member lis 的有默认构造函数要调用。2 偏移位置,3如果是最底层的class,构造函数要被调用)
2 基类(1 在member list中,显示指定的参数要传递进去 2 没在member lis 的有默认构造函数要调用。3 this 指针)
3 虚基表指针 (对于vptrs 也是一样在这个位置)
4 成语初始化列表
5 如果有member 没有在列表中,但是有默认构造函数,需要调用

在新手供应的copy operator中忘记检查自我指派是否失败,是很常见的问题

虚拟继承

对于虚基类的初始化,由最底层的class实现。最底层的类会压制上面的类的构造函数的调用操作。
“虚拟基类构造函数的被调用”有着明确的定义:只有当一个完整的class objec被定义出来了,它才会被调用,如果object只是某个完整的类的subobject,它就不会被调用。

在构造函数中,虚拟机制不会发生作用。因为基类的构造函数在派生类构造函数之前执行,当基类构造函数运行时,派生类的数据成员还没有初始化。如果基类构造期间调用的虚函数向下匹配到派生类,派生类的函数理所当然会涉及到本地数据成员,但是那些数据成员还没有初始化,而调用涉及一个对象还没有初始化的部分自然是危险的,因此虚函数不会向下匹配到派生类,而是直接执行基类的函数

派生类对象构造期间进入基类的构造函数时,对象类型变成了基类类型,而不是派生类类型。
同样,进入基类析构函数时,对象也是基类类型。

判断是否安全可行,应该根据vptr是否被成功设置的时机

copy assignment list 缺少 member assignment list

建议:不要允许一个虚基类的拷贝操作 ,甚至可以不要在虚基类中声明数据

一般而言 会把对象尽可能放在使用它的那个程序区段附件,这么做事可以节省非必要的对象产生操作和摧毁操作。

全局对象

为每一个需要静态初始化的文件产生一个_sti()函数
为每一个需要静态的内存释放操作的文件,产生一个_std()函数
提供一组runtime library 一个_main()函数(调用执行文件中的_sti函数),exit()函数(执行_std()函数),分别安插在函数的起始处和结尾处。

局部静态对象

构造函数必须只执行一次
析构函数必须只执行一次

对象数组

例如 Point knots[10]

如果point定义了一个默认构造函数,需要轮流施行与每一个元素上。
vec_new()函数,产生出以class object 构造而成的数组。
如果提供了一个或多个明显初值给一个class object的数组 ,对于那些明显后的初值的元素,vec_new()不再有必要。

类似 point *ptr=new Point3d[10] ; delete []ptr;
这种完全不是好主意,因为施行与数组上的析构函数是根军交给vec_delete()函数的“被删除的指针类型的析构函数”,本例会传 point 的析构函数 同时需要传进每个元素的大小,这里传进的是point的大小而不是point3d的大小 。因此不只是因为执行了错误的析构函数,同时自从第一个元素之后,该析构函数施行与不争取的内存区块。

因此需要避免以一个基类指针指向一个派生类指针所组成的数组。
如果非要这样,解决的方法是程序员迭代走完整个数组,采用delete的方式。

placement operator new的语意

是一个预先定义好的 重载new运算符
调用方式如下:
Point2w * ptw=new(arena ) Point2w;
其中arena指向内存中的一个内存,用来放置新产生出来的point2w 对象,意思就是在已经申请的内存块上,调用placement Operaor new 来构造对象。

Placement new 存在的理由

1.用placement new 解决buffer的问题

问题描述:用new分配的数组缓冲时,由于调用了默认构造函数,因此执行效率上不佳。若没有默认构造函数则会发生编译时错误。如果你想在预分配的内存上创建对象,用缺省的new操作符是行不通的。要解决这个问题,你可以用placement new构造。它允许你构造一个新对象到预分配的内存上。

2.增大时空效率的问题

使用new操作符分配内存需要在堆中查找足够大的剩余空间,显然这个操作速度是很慢的,而且有可能出现无法分配内存的异常(空间不够)。placement new就可以解决这个问题。我们构造对象都是在一个预先准备好了的内存缓冲区中进行,不需要查找内存,内存分配的时间是常数;而且不会出现在程序运行中途出现内存不足的异常。所以,placement new非常适合那些对时间要求比较高,长时间运行不希望被打断的应用程序。

Placement new使用步骤

在很多情况下,placement new的使用方法和其他普通的new有所不同。这里提供了它的使用步骤。

第一步 缓存提前分配

有三种方式:

1.为了保证通过placement new使用的缓存区的memory alignment(内存队列)正确准备,使用普通的new来分配它:在堆上进行分配
class Task ;
char * buff = new [sizeof(Task)]; //分配内存
(请注意auto或者static内存并非都正确地为每一个对象类型排列,所以,你将不能以placement new使用它们。)

2.在栈上进行分配
class Task ;
char buf[N*sizeof(Task)]; //分配内存

3.还有一种方式,就是直接通过地址来使用。(必须是有意义的地址)
void* buf = reinterpret_cast<void*> (0xF00F);

第二步:对象的分配

在刚才已分配的缓存区调用placement new来构造一个对象。
Task *ptask = new (buf) Task

第三步:使用

按照普通方式使用分配的对象:

ptask->memberfunction();

ptask-> member;

//…

第四步:对象的析构

一旦你使用完这个对象,你必须调用它的析构函数来毁灭它。按照下面的方式调用析构函数:
ptask->~Task(); //调用外在的析构函数

第五步:释放

你可以反复利用缓存并给它分配一个新的对象(重复步骤2,3,4)如果你不打算再次使用这个缓存,你可以象这样释放它:delete [] buf;

跳过任何步骤就可能导致运行时间的崩溃,内存泄露,以及其它的意想不到的情况。如果你确实需要使用placement new,请认真遵循以上的步骤。

临时对象

T c =a+b的方式比 c=a+b的方式更有效率地被编译器转换,主要在于是否有临时对象的产生

临时对象的被摧毁,应该是对完整表达式求值过程中的最后一步,该完整表达式造成临时对象的产生。
例外:
1、表达式被用来初始化一个对象。这个临时对象应该留存到对象初始化操作完成为止。
2.一个临时对象绑定于一个引用。(临时对象将残留,直到被初始化的声明结束,或直到临时对象的沈方明范畴结束。)

模板

关于模板类中的静态成员,每一种模板的真正类型都会有一个静态成员与其相关联。

模板在实例化行为发生之前也一样没有做到完全的类型检查。

模板中的名称决议法

有两种scope意义:1 定义出模板的程序端
2 实例化模板的程序端
如果现在分别有一个同名函数在不同的scope内,如果在成员函数中会调用这个同名函数,那么究竟调用哪一个函数实例呢?
判断方法如下
根据这个函数的name的使用是否与“用以实例化该模板参数化的参数类型”有关而决定的,如果其使用互不相关,那就用定义出模板的程序端来决定。如果使用互有关联,那么就以实例化模板的程序端来调用。

意味着编译器必须保持两个scope 上下文。

异常捕获

1 throw
2 catch
3 try

execption被抛出去时,控制权会从函数调用中释放出来,并寻找一个吻合的catch子句。如果都没有吻合,那么会将堆栈中每一个函数的调用也被推离。 在每一个函数被推离堆栈之前,函数的局部对象的析构函数会被调用。

需要注意exception handling 在资源管理上会带来的影响,例如没有unlock共享内存。

支持EH ,会使构造函数更加复杂,例如class X有member A B C,如果A的构造函数发生异常,ABC都不用调用析构函数,如果B的构造函数发生异常,A的析构函数必须被调用,但C不用。这是编译器的责任。
同理 对对象数组发生异常时,也要对前面的元素进行析构

program counter(在INTEL的CPU是EIP寄存器)的起始值和结束值存储在一个表格中。用来判断目前的区域是否在try区段中。

执行期类型识别 (RTTI)

虚函数表中的第一个slot存放了RTTI 对象地址。额外负担编程,每一个类对象只多花费一个指针,它是被编译器静态设定的,而非执行期由类的构造函数设定

dynamic_cast 在执行期通过vptr获取类对象的类型描述器

dynamic_cast会对一个类指针返回true/false

如果传回真正的地址,表示这个类的动态类型被确认了
如果传回0,表示没有指向任何的对象。

如果dynamic_cast用于引用上
如果将引用设为0,会引起一个临时性对象,因此

如果引用参考到适当的派生类 则继续执行
如果并不是一个真正的派生类,由于不能够传回0,因此抛出一个bad_cast exception

可以采用typied,就有可能以一个引用达到相同执行器替代路线。
typied运算符传回一个const 引用,类型是type_info

总结

以上都是我看书的看书笔记,以及有部分是遇到不懂在网上摘抄的笔记,有侵权可联系删除

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值