《深度探索C++对象模型》读书笔记

1.在通常情况下C++实现了封装性,但并没有曾加成本,类中声明了成员变量和成员函数,在定义类对象的时候,类的开销就是成员变量的开销,对象中不会出现成员函数的实例。每一个non-inline member function只会诞生一个函数实例。至于每一个“拥有零个或一个定义”的inline function则会在其每一个使用者(模版)身上产生一个函数实例。类一定程度上支持了封装性质,而且并没有影响空间和执行期的的额外开销。其实C++在布局以及存取时间上主要的额外负担是由virtual引起的。包括

//virtual function机制 用以支持一个有效的“执行期绑定”

//virtual base class 用以实现“多次出现在继承体系中的base class”有一个单一而被共享的实例

2.C中的struct通常有这样一种用法,把单一元素的数组放在一个struct的末端,于是每个stuct objects可以拥有可变大小的数组:

struct mumble

{

   char pc[1];
};

//从文件或标准输入装置中取得一个字符串

//然后为struct本身和该字符串配置足够的内存

struct mumble *pmumb1 = (struct mumble*)malloc(sizeof(sruct mumble) + strlen( string ) + 1);

strcpy( &mumble.pc,string);

第一次见这么用,之前对C也不是很熟悉,还是感觉很惊讶,不过这毕竟是一个小窍门,不具有普遍性。

如果这里用class 来代替struct将不能保证可以实现同样的效果。

C++中凡处于同一个access section(这里简单理解为访问块)的数据,必定保证以其声明顺序出现在内存布局中。然而被放置在多个access section中的各笔数据,排列顺序就不一定了。下面的声明中,前述的C伎俩或许可以有效运行,或许不能,需视protected data members被放在private data members的前面或后面而定(放在前面才可以)

class stumble

{

    public:

       //operations

    protected:

      //protected stuff

    private:

    /*private stuff*/

      char pc[1];

};

同理,base classes和Derived class的data members的布局也未有谁先谁后的强制规定,因此也就不能保证前述的C伎俩一定有效。Virtual functions的存在也会使前述的伎俩的有效性称为一个问号。所以最好的忠告是不要那么做!如果一个程序员迫切需要一个相当复杂的C++ class的某部分数据,使他拥有C声明的那种摸样,那么那一部分是最好抽取出来称为一个独立的struct声明。或者继承,或者组合。


2.虽然你可以直接或间接处理继承体系中一个base class object,但只有通过pointer或reference的间接处理,才支持面向对象程序程序设计所需的多态性质。这句话要好好理解一下。如下代码段

Librar_materials *px = retrieve_some_material();
Librar_materials &rx = *px;
Librar_materials dx = *px;
你绝对没有办法确定地说出px或rx到底指向何种类型的objects,你只能够说它要么是一个Library_materials object,要么就是后者的一个子类型。不过,我们倒是可以去顶,dx只能是Library_materials class的一个object。而虽然对于object的多态操作要求此object必须可以经由一个pointer或reference来存取,然而C++中的pointer或reference的处理却不是多态的必要结果。在C++,多态只存在于一个个的public class体系中。Nopublic的派生行为以及类型为void*的指针可以说是多态的,但他们并没有被语言明确地支持,也就是说他们必须由程序员通过显示的转换操作来管理(你或许可以说它们并不是多态对象的一线选手)

3.一个class object所需内存。

    其nostatic data members的总和大小。

    加上任何由alignment的需求而填补上去的空间(可能存在于members之间,也可能存在于集合体边界)。

    加上为了支持virtual而由内部产生的任何额外负担(例如虚函数中的虚函数表指针)

   自定义一个类

class ZooAnimal
{
    public:
       ZooAnmial();
       virtual ~ZooAnmal();
       virtual void rotate();
    protected:
       int loc;
       string name;
}
所占内存分析int loc占用4个bytes。string name占用8-bytes(包括一个4-bytes的字符指针和一个用来表示字符串长度的整数,可以想象string也是一个类或着结构,其中有两个成员变量,一个字符指针指向存储字符串的数组,一个int 表示string大小)。_vptr_ZooAnimal指针大小一个4-bytes总共占用4+8+4大小空间。


4.一个指针类型为void*的指针,将涵盖怎样的地址空间呢?我们不知道,这就是为什么一个类型为void*的指针只能够持有一个地址,而不能够通过它操作所指之object的缘故。所以转换(cast)其实是一种编译指令。大部分情况下它并不改变一个指针所含的真正地址,它只影响“被指出之内存的大小和其内容”的解释方式。


5.我所理解的ADT(抽象数据类型),虽然叫着很高级,但要明确的是它也是数据类型,与其他(如int char bool等基础数据类型)数据类型相比,它添加了封装性(如string实际上是char数组和表示数组大小的int类型组成)。这样通过抽象的描述是类型更加被容易理解和使用。但是它并不是面向对象,面向对象是以对象为基础的,对象也不仅仅限于类型的描述。

6.多态的描述。如果Bear继承于ZooAnmial。现在这样来实现

Bear b;
ZooAnmial *pz = &b;
Bear *pb = &b;
他们每个都指向Bear object的第一个byte。其间的差别是,pb所涵盖的地址包含整个Bear object,而pz所涵盖的地址只包含Bear object中的ZooAnimal subobject。除了ZooAnimal subobject中出现的members,你不能够使用pz来直接处理Bear的任何members。为一个例外是通过virtual机制。pz类型将在编译时期(注意是编译时期不是执行时期或这链接时期)决定一下两点:

   固定的可用接口。也就是说pz只能够调用ZooAnimal的public接口。

   该接口的access level(例如rotate()是ZooAnimal的一个public member)

在每一个执行点(注意这里是执行时期)就,pz所指的object类型可以决定rotate()所调用的实例。类型的信息的封装并不是维护于pz之中,而是维护于link之中,此link存在于“object的vptr”和“vptr所指的virtual table”之间。

Bear b;
ZooAnmial za = b;//这里会引起切割
//调用ZooAnimal::rotate();
za.rotate();
为什么roate()所调用的是ZooAnimal实例而不是Bear实例?此外如果初始化函数将一个object内容完全拷贝到另一个object去,为什么za的vptr不指向Bear的virtual table?

后者的答案是,编译器必须保确保如果某个object含有一个或一个以上的vptrs,那些vptrs的内容不会被base class object初始化或改变。在类型定义的时候其实已经初始化了,其中已经包含了vptr,再对其进行赋值操作,vptr将不会改变。至于第一个问题,za并不是(而且也绝对不会是)一个Bear,它是(并且只能是)一个ZooAnimal。多态所造成的“一个以上的类型”的潜在力量,并不能直接发挥在“直接存取objects”这件事情上。一个pointer或一个reference之所以支持多态,是因为他们并不引发内存中任何“与类型有关的内存委托操作”:会受到改变的,只有他们所指向的内训“大小和内容解释方式”而已。而当一个base class object被直接初始化为(或是被指定)一个derived class object时,derived object就会被切割以塞入较小的base type内从中,derived type将没有留下任何蛛丝马迹。多态于是不再呈现,而一个严格的编译器可以在编译时期解析一个“通过此object而出发的virtual function调用操作”,因为回避virtual机制。如果virtual function被定义为inline,则更有效率上的大收获。


7.关于默认构造函数的生成。首先要确认,如果没有为自定义的类定义合适的构造函数,编译器会为此类生成一个默认的构造函数(注意是编译器生成的,在编译时期就会生成)。这时生成的构造函数有两种一种是trival(没用的,实际上是不会被生成的)和notrivial。这些规则是C++ Standard中定义的。这种不是人为定义的构造函数都称为implicit default constructor。下面4中情况是编译器生成的构造函数是nontivial default constructor的。

     1)“带有Default Constructor”的Member Class Object

           解释:代码如下

           

class Foo{ public:Foo(),Foo(int)...};
class Bar{public:Foo foo;char *str;}//不是继承是内含
void foo_bar()
{
    Bar bar;//Bar::foo必须在此处初始化,因为其有自己的构造函数,Bar::fooshi 是一个member object,而其class Foo拥有默认构造函数
    if(str){}...
}
被合成的Bar Default Constructor内含必要的代码,能够调用class Foo的default Constructor来处理member object Bar::foo,但它并不产生任何代码来初始化Bar::str。是的,将Bar::foo初始化是编译器的责任,将Bar::str初始化则是程序员的责任。如果自己定义一个构造函数,这个构造函数没有对类中object对象初始化,此时编译器的行动是:“如果class A内含一个或一个以上的member class object foo。由于default constructor已经被显示地定义出来,编译器没办法合成第二个default constructor。那么class A的每一个constructor必须调用每一个member classes的default constructor”。编译器会扩张已存在的constructors,在其中安插一些代码,使得user code被执行之前,先调用必要的default constructor。如果多个class member object都要求constructor初始化操作,C++将以member objects在类中的声明顺序进行调用相应的构造函数初始化。

     2)“带有Default Constructor”的Base Class

          解释:派生类必须合成一个default constructor来调用基类中的default constructor。这个合成的constructor与“被显式提供的default constructor”相同。而且如果设计者提供多个constructors(这里说的派生类中),但其中都没有default constructor,编译器会扩张现有的每一个constructors,将“用以调用所有必要之default constructors”的程序代码加进去。它不会合成一个新的default constructor,因为其他“由user所提供的constructors”存在的缘故。如果同时还存在“嗲有default constructors”的member class objects,那些default constructors也会被调用,不过是在所有base class constructor都被调用之后。

     3)“带有一个Virtual Function”的Class

         解释:如果有虚函数的话,编译器会合成一个default constructor,用来初始化vptr和vtbl。

class Widget
{
    public:
        virtual void flip() = 0;
};

void flip(const Widget& widget){widget.flip();}
widget.flip()的虚拟调用操作在编译期间会被重写,以使用widget的vptr和vtbl中的flip()条目:

(*widget.vptr[1])(&widget)

其中:

1表示flip()在vitrual table 中的固定索引:

&widget代表要交给“被调用的某个flip()函数实例”的this指针。
为了让这个机制发挥功效,编译器必须为每一个Widget(或其派生类的)object的vptr设定初值,放置适当的virtual table地址。对于class所定义的每一个constructor,编译器会安插一些代码来做这样的事情。对于那些未声明任何constructors的classes,编译器会为他们合成一个default constructor,以便正确地初始化每一个class object的vptr。

     4)“带有一个Virtual Base Class”的Class

         解释:这里的Virtual Base Class首先要说明的是虚继承(虚继承!不清楚的话,去回顾回顾),派生类在虚继承的需要生成一些额外的信息来表明这些关系,并且来对基类进行一些初始化操作(虚继承的一个特性就是派生类中最多只能有一个基类副本)

总结两个错误:1.任何class如果没有定义default constructor,就会被合成出一个来。2.编译器合成出来的default constructor会显示设定“class内每一个data member的默认值”


8.Default Memberwise initialization(默认逐个成员初始化)。这个概念是紧接着上一个而来的,这里讨论是类的初始化的情况。当用一个类的对象来初始一个类的对象的时候所发生的事情。如果class没有提供一个explicit copy constructor时,class object以“相同class的另一个object”作为初值,其内部是以所谓的Default memberwise initialization手法完成的,也就是把每一个内建的或派生的data member的值,从某个object拷贝一分到另一个object身上。不过它并不会拷贝其中的member class object,而是以递归的方式试试memberwise initialization。(就是对这个类再实施上述步骤)。需要注意的是Default constructor和copy constructors都是在必要的时候由编译器产生出来。说一说“如果一个class未定义出copy constructor,编译器就自动为它产生出一个”这句话不对!一个class object可用两种方式复制得到,一个是初始化,一个是赋值。


9.在进行复制的时候,逐个成员初始化在有些情况下是不使用的。因为有些情况下直接进行成员函数的复制会出现无法预测的问题,这个时候编译器需要自动识别,并做出处理。在下面4种情况,编译器将杜绝逐个成员初始化(直接赋值)。

    1)当class内含一个member object而后者的class声明一个copy constructor时(不论是被class设计者显式地声明,或是被编译器合成)。

    2)当class继承自一个base class而后者存在一个copy constructor时(再次强调,不论是被显式声明或是被合成)

    3)当class 声明了一个或多个virual functions时。

     4)当class派生自一个继承串链,其中有一个或多个virtual base classes时。

解释:前两个其实说的内容差不多,因为继承的基类本身也可以看成是派生类中的一部分,相当于派生类中含有的成员对象(这么说肯定是错的,但是可以想象有时候派生和组合在很多情况下可以达到同样的功效。两者在设计理念上有一定的相关性)。在对类对象进行赋值的时候,必须考虑类中的成员类对象和基类部分,所以如果成员类对象(是对象,不是指针哦!)或基类存在一个copy constructor时,在进行赋值时就不能简单的逐个初始化,要么设计者构造一个copy constructor,要么编译器自动构造一个copy constructor。后面两个其实也可以理解,因为牵涉到virtual的时候就设计到虚函数表的操作,当进行赋值的时候要考虑到这个表的实际操作,在派生类和基类之间赋值,不能直接进行赋值,因为不一样!!!


10.copy constructor的应用,迫使编译器多多少少对你的程序代码做部分转化。尤其是当一个函数以传值的方式传回一个class object,而该class有一个copy constructor(不论是显示定义出来的,或是合成的)时。这将导致深奥的程序转化-不论在函数的定义上还是在使用上。此外,编译器也将copy constructor的调用操作优化,以一个额外的第一参数(素质被直接存放于其中)取代NRV。程序员如何了解那些转换,以及copy constructor优化后的可能状态,就比较能够控制其程序的执行效率。


11.初始化列表。在下面几种情况下必须要使用初始化列表,不能使用在构造函数中进行赋值操作。1.当初始化一个reference member时;2.当初始化一个const member时;3.当调用一个base class的constructor,而它拥有一组参数时;4.当调用一个member class的constructor,而它拥有一组参数时。

class Word
{
    String _name;
    int _cnt;
   public:
     Word()
       {
           _name = 0;
           _cnt = 0;
       }
}
在编译的时候等同于

Word::Word(/*this pointer goes here*/)
{
    //调用String的default constructor
    _name.String::String();
    //产生临时性对象
    String temp = String(0);
     //"Memberwise"地拷贝_name
     _name.String::operator = ( temp );
    //摧毁临时性对象
     temp.String::~String();
     _cnt = 0;
}
可以看出效率是多么的低。

如果是在初始化列表中就等同于

_name.String::String(0);

效率得到提高,但是有写时候初始化列表会有一定的陷阱。要谨记初始化列表中的顺序并不是成员变量初始化的顺序。初始化的顺利是跟变量声明的顺序保持一致的!!!

所以在使用一个成员变量初始化另一个成员变量的时候,一定要注意,前一个成员变量是在后一个成员变量的初始化之前还是在之后。还有一个重要的知识点,就是使用成员函数初始化成员变量的时候时候是可以的。因为成员函数是通过this指针进行调用的。this生成在初始化列表之前,所以可以。


12.空类大小

class X{};
class Y:public virtual X{};
class Z:public virtual X{};
class A:public Y,public Z{};
情况一:X:1byte,Y:8bytes,Z:8byte,A:12bytes

情况二:X:1byte,Y:4bytes,Z:4bytes,A:8bytes

这两种情况视编译器而定,如果不是空的话,情况只有一种,具体参照第三章


13.在类中,对member functions本体的分析之前,会直到整个class的声明都出现了才开始。例如

extern int x;
class Point3d
{
    public:
      //对于函数本体的分析将延迟,直至class声明的右大括号出现才开始
     float X() const{return x;}
    private:
      float x;
}
这里X()返回的是类内部的x,不是外部的x。就是因为对member functions本体的分析之前,会直到整个class的声明都出现了才开始。但是typedef int length一定要放在前面。

14.Data Member的布局。如下

class Point3d
{
    private:
       float x;
       static List<Point3d*> *freeList;
       float y;
       static const int chunkSize = 250;
       float z;
}
Nonstatic data members在class object中的排列顺序将和其被声明的顺序一样,任何中间介入的static data members如freeList和chunkSize都不会被放进对象布局之中。在上述例子里,每一个Point3d对象是由三个float组成的,顺序是xyz。static data members存放在程序的data segment中,和个别的class objects无关。而且xyz顺序一般是这样的,但并不是连续的。两个成员变量之间可能穿插一些边界调整,或者编译器生成的从参数,例如vptr。           


15.对成员变量的存取。如果是static成员变量无论是继承,派生类中的成员变量,其存取效率都是一样。跟在结构体中,clas,单一继承,多重继承都是一样的。直接用类名存取,跟具体的类对象没有关系。但是如果是nonstatic成员变量的话,情况就会完全不一样了。nonstatic成员变量必须要使用类对象的起始地址加上成员变量的偏移量-1。减1很重要,其解释是这样的(虽然有点不太理解),因为指向data member的指针,用以指出class的第一个member和一个指向data member的指针,没有指出任何member两种情况。origin.x = 0.0和pt-> x = 0.0在其继承结构中有一个virtual base class时有重大差异。这时候我们不能够说pt必然指向哪一种class taye(因此,我们也就不知道编译时期这个member真正的offset位置),所以这个存取操作必须延迟至执行期,经由一个额外的间接导引,才能够解决。但如果使用origin,就不会有这些问题,其类型无疑是Point3d class,而即使它继承自virtual base class,members的offest位置也在编译时期就固定了。一个积极进去的编译器甚至可以静态地经由origin就解决掉对x的存取。

16.类对象的内存分配情况

class Concrete1
{
    private:
       int val;
       char bit1;
};

class Concrete2:public Concrete1
{
    private:
       char bit2;
};

class Concrete3:public Concrete2
{
    private:
      char bit3;
}
上面各类定义一个对象,所占用的内存分别为8bytes,12bytes,16bytes,并不是8bytes,8bytes,8bytes。因为类的继承是完全复制。不然的话在将基类指针赋值给派生类指针的时候,会有未知错误发生。



17.多态,多重继承,虚继承中类对象中成员分布,和内存占用情况。首先说多态,这里说的多态是先避开多重继承(就是有多个基类)和虚继承的情况。简单的在基本单一继承的基础上加上虚函数。这时候会生成一个虚函数表(注意只有一个,无论有多少类对象),不同的类对象中会有一个vptr(虚函数表指针)指向这个表。为了兼容C中的struct,这个指针往往放在类的最后(有些编译器是放在最前面,目前主流编译器是放在前面的)。这个时候进行继承的时候,之前也说过派生类中的整个都会被copy下来,包括这个指针,位置也是不变,派生类中自己的成员变量是在这个后面添加上来的。sizeof(baseclass)可以获取这个指针的offset。

多重继承,多重继承要复杂的多,因为不能简单的通过sizeof来获取非首个基类的偏移量。不过基类们在派生类中排列顺序一般也是按照声明顺序排列的。当进行多态的时候,后面的基类需要通过前面所有基类sizeof(baseclasses)来获取正确的地址,不是简单的将派生类地址赋值给基类指针(除非是继承顺序中的第一个)。如果每一个基类中都含有虚函数,那么派生类中将含有多个vptr。派生类会继承基类的虚函数表,只是这个表要根据派生类情况进行覆写(派生类中覆写了一个基类中的虚函数时)。不同基类中的虚函数表并不会合并,派生类对象中的虚函数指针也将有多个。

虚继承,虚继承引来的问题(1)每一个对象必须针对每一个virtual base class背负一个额外的指针。然而理想上我们却希望class object有固定的负担,不因为其virtual base classes的个数而有所变化。(2)由于虚函数继承串链的加长,导致间接存取层次的增加。这里的意思是,如果我有三层虚拟派生,我就需要三次间接存取(经由三个virtual base 指针)。然而理想上我们却希望有固定的存取时间,不因为虚拟派生的深度而改变。

针对第一个问题,Microsoft编译器引入所谓的virtual base class table。每一个class object如果有一个或多个virtual base classes,就会由编译器安插一个指针,指向virtual base class table。至于真正的virtual base class 指针,当然是被放在一个该表格中。还有一种解决方法是在virtual function table中放置virtual base class的offset(而不是地址)。意思就是将virtual base class offset和virtual function table(这个是必须存在的,无法避免的,将前者整合在一起,一定程度上避免了额外的开销)整合在一起。在新近的sun编译器中,virtual function table可经由正值或负值来索引。如果是正值,很显然就是索引到virtual functions;如果是负值,则是索引到virtual base class offsets。


18.类成员函数的调用成本跟非成员函数的概率是一样的。类成员函数调用的时候,编译器会转换成非成员函数的类型。针对同一个类中的不同的同名函数,编译器会有一个命名规则。


19.多重继承中的虚函数表。在多重继承中(就是继承于很多基类)如果基类中有虚函数的话,每一个基类都会对应有一个虚函数表,并且每一个类对象都会有一个虚函数表指针。派生类中会重新生成自己的虚函数表,派生类对象中针对每一个虚函数都会有个相应的虚函数表指针,再加上自己的虚函数表指针。


20.member function指针,从名称来看也很明显,就是指向类成员函数的指针(注意与函数指针的差别),其定义形式double(Point::* pmf)();Point::表示此指针是一个成员函数指针。其使用方法coord = &Point::y;(origin.*coord)();或者(ptr->*coord)();在编译器中会分别转化成(coord)(&origin)和(coord)(ptr);这种形式在前面也提到过可以避免构造函数的调用和赋值,提高效率。取一个nonstatic member function的地址,如果该函数是nonvirtual,得到的记过是它在内存中真正的地址。然而这个值也不是完全的。它也需要被绑定于某个class object的低智商,才能够通过它调用该函数。所有的nonstatic member functions都需要对象的地址(以参数this指出)。并且使用一个member function指针,如果并不用于virtual function、多重继承、virtual base class等情况的话,并不会比使用一个nonmember function指针的成本更高。


21.指向virtual Member Functions的指针,考虑下面程序

float (Point::*pmf)() = &Point::z;
Point *ptr = new Point3d;
class Point
{
    public:
      virtual ~Point();
      float x();
      float y();
      virtual float z();
}
pmf,一个指向member function的指针,被设置为Point::z()(一个virtual function)的地址。ptr则被指定一个Point3d对象(派生于Point类),直接经由ptr调用z()为:prr->z();

被调用的是Point3d::z()。但是如果从pmf间接调用z(),(ptr->*pmf)();仍然是Point3d::z()。也就是说虚拟机只仍然能够在使用指向member function之指针的情况下运行。对于一个nonstatic member function取其地址,将获得该函数在内存中的地址。然而面对一个virtual function,其地址在编译器时期是未知的,所能知道的仅是virtual function在其相关之virtual table中的索引值。也就是说,对一个virtual function取其地址,所能获得的是一个索引值。如&Point::~Point();的到结果为1,&Point::x();得到的结果就是函数在内存中得知,&Point::z()结果为2.通过pmf来调用z(),会被内部转化为一个编译时期的式子,一般形式如下:(*ptr->vptr[(int)pmf])(ptr);这里提出了一个问题如果定义一个指针float(Point::*pmf)();此指针必须可以寻址出nonvirtual x()和virtual z()两个member function,而那两个函数有这相同的原型,只不过其中一个代表内存地址(一长串),另一个代表vitual table中的索引值(一小段)。因此,编译器必须定义pmf,使它能够持有两种数值,更重要的是其数值可以被区别代表内存地址还是Virtual table中的索引值。


22.C++的设计准则之一就是:nonstatic member function至少必须和一般的nonmember function有相同的效率。也就是说,如果我们要在一下两个函数之间做选择

float magnitude3d(const Point3d *_this){}
float Point3d::magnitude3d() const {}
选择member function不应该带来什么额外负担。这是因为编译器内部已将member函数实例转换为对等的nonmber函数实例。


23.名称特殊处理,编译器会对成员变量,或成员函数,以及nonmember functions(没有声明extern “C”的情况下),进行独一无二的命名。编译的时候有一种“确保类型安全的链接行为(type-safe lingage)”,其只可以捕捉函数标记(函数名称+参数个数+参数类型)错误;如果返回类型声明错误,就没办法检查出来!在有些编译器编译的错误消息用的是程序源码函数名称,然而链接器却不,它用的是经过特殊处理的内部名称。因为名称处理是在编译的时候进行,所以在链接的时候就会出现名称并不是源代码中函数名称,在很多链接器报错的时候,出现名称一大串,不要见怪就好了。


24.一般而言,class的data member应该被初始化,并且只在constructor中或是在class的其他member function中指定初值。其他任何操作都将破坏封装性质,是class的维护和修改更加困难。


25.可以定义和定义一个pure virtual function,不过它只能被静态地调用,不能经由虚拟机制调用。如下:

inline void Abstract_base::interface() const//译注:请注意,先前曾经声明这是一个pure virtual constructor
{
   function
}

inline void Concrete_derived::interface()const
{
    Abstract_base::interface();\\译注:请注意我们竟然能够调用一个pure virtual function
}
要不要这样做全由class设计者决定。唯一的例外就是pure virtual destructor:class设计者一定得定义它。因为每一个derived class destuctor会被编译器加以扩张,以静态调用的方式调用其每一个virtual base class以及上一层base class的destructor。因此,只要缺乏任何一个base class destuctor的定义,就会导致链接失败。


26.从这里开始接触template,确实学到了不少东西,这里记录一些自己的理解,虽然很表面的东西。很深入的东西可能还需要进一步探索才行。首先定义一个template,如下

template <class Type>
{
    public:
      enum Status {unallocated,normalized};
      Point(Type x=0.0,Type y=0.0,Type z=0.0);
      ~Point();
      void* operator new(size_t);
      void operator delete(void*,size_t);
    private:
      static Point<Type>*freeList;
      static int chunkSize;
      Type _x,_y,_z;
};;

当编译器看到template class声明时,其什么也不会做,也就是说static data members并不用,nested enum或者其enumberators也是一样的。虽然enum Status的真正类型在所有的Point instantiations中都一样,其enumerators也是,但他们每一个都只能通过template Point class的某个实例来存取或操作。因此我们可以这样写

Point<float>::Status s;

但不能这样写

Point::Status s;

即使两种类型抽象地来说是一样的(而且,最理想的情况下,我们希望这个enum只有一个实例被产生出来。如果不是这样,我们可能会想要把这个enum抽出到一个nontemplate base class中,以避免多分拷贝)。同样道理,freeList和chunkSize对程序而言也还不可用。我们不能够这样写

Point::freeList;//error

我们必须显式地指定类型,才能够使用freeList,如下:

Point<float>::freeList;//ok

而且像上面这样使用static member,会使其一份实例与Point class的float instantiation在程序中产生关联。如果我们写

Point<double>::freeList;//OK:另一个实例
就会出现第二个freeList实例,与Point class的double instantiation产生关联。


如果我们定义一个指针,指向特定的实例,像这样:

Point<float>*ptr = 0;
再一次,程序中什么也没有发生。为什么呢?因为一个指向class object的指针,本身并不是一个class object,编译器不需要知道与该class有关的任何members的数据或object布局数据。所以将Point的一个float实例实例化也就没有必要了。在C++ Standard完成之前,声明一个指针指向某个template class这件事情并未被强制定义,编译器可以自行决定要或不要将template实例化。C++ Standard已经禁止编译器这么做了。



如果不是pointer而是reference的话,情况就不一样了

const Point<float>&ref = 0
这时其会实例化一个Point的float实例。像下面这样扩展:

Point<float> temporary(float (0));
const Point<float> &ref = temporary;
为什么?因为reference并不是无物(no object)的代名词。0被视为整数,必须被转换成一下类型的一个对象:

Point<float>
如果没有转换的可能,这个定义就是错误的,会在编译时被挑出来。


member functions(至少对于那些没有被使用过的)不应该被实例化。只有在member functions被使用的时候,C++ Standard才会要求它们被实例化,主要原因如下:

1,空间和时间效率的考虑。如果类中有100个member functions,但你的程序只针对某个类型使用其中两个,所以如果都进行实例化的话,将会华为大量的时间和空间。

2,尚未实现的机能,比如某个成员函数中使用了一些类型不支持的操作符,将会导致不可知的错误。

函数在什么时候实例化,目前比较流行的策略如下:

1.编译的时候。那么函数将实例化于origin和p存在的那个文件中

2.在链接的时候。那么编译器会被一些辅助工具重新激活。template函数实例化可能被放在这一个文件中,别的文件中或一个分离的存储位置。


27.在template应用中,编译器对错误的检查跟你想象的可能不太一样,类型检查是在实例化的时候才会检查出来。目前编译器,面对一个template声明,在它被一组实际参数实例化之前,只能实施以有限的错误检查。template中那些与语法无关的错误,程序员可能人为十分明显,编译器却让它通过了,只有在特定实例被定义之后,才会发出抱怨。这是目前实现技术上的一个大问题。Nonmeber和member template functions在实例化行为发生之前也一样没有做到完全的类型检验。这会导致某些十分露骨的template错误声明竟然得以通过编译。


28.Template中的名称决议,很有意思,好好体会一下。C++ Standard中名称主要分两种1.scope of the template definition ,也就是定义出template的程序端。2.scope of  the template instantiation,也就是实例化template的程序端。如下实例:

//scope of the templete definition
extern double foo(double)

templete<class type>
class ScopeRules
{
    public:
      void invariant(){_member = foo(_val);}
      type type_dependent(){return foo(_member);}
    private:
      int _val;
      type _member;
};
这里是template的定义端。下面是实例化端:

//scope of the templete instantiation
extern int foo(int );
ScopeRules<int>sr0;
这时候如果使用

sro.invariant();

那么在invariant()中调研那个的是那一个foo()函数实例呢?

//调用的是那一个foo函数实例呢?
_member  = foo(_val);

//有下面两个函数实例
extern double foo(double);
extern int foo(int);
而_val的类型是int。答案是上面那一个。不是下面的!!!

原因是Template之中,对于一个nonmember name的决议结果,是根据这个name的使用是否与“用以实例化该Template的参数类型”有关而定的。如果其使用互不相关,那么就以scope of the template declaration来决定name。如果其使用互有关联,那么就以scope of the template instantiation来决定name.

28.EH(Exception Handling)异常处理,要知道添加异常处理的过程中,需要付出时间可空间代价的。添加异常处理的过程中,需要生成一个异常对象,而且在catch(&p)和catch(p)的过程中对这个异常对象的错方式是不一样的,注意异常构造和析构所付出的时间代价。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值