Skip to content

Latest commit

 

History

History
531 lines (359 loc) · 33.3 KB

Class.md

File metadata and controls

531 lines (359 loc) · 33.3 KB

类的基本思想是数据抽象(封装)、继承和多态(动态绑定)

  • 数据抽象:把客观事物封装成抽象的类,同时将类的接口和实现分离。(优点:可以隐藏实现细节,使得代码模块化)
  • 继承:定义相似的类型,并对其相似关系建模。(优点:可以扩展已存在的代码模块)
  • 多态:一定程度上忽略相似类型的区别,以统一的方式使用它们的对象。

构造/析构/赋值运算

当我们定义一个空类时,C++ 编译器会默认为它合成 默认构造函数,copy构造函数,赋值操作符和一个析构函数。因此,如果我们写下:

class Empty(){};

这就好像写下这样的代码:

class Empty {
public:
    Empty() { ... }
    Empty(const Empty& rhs) { ... }
    ~Empty() { ... }
    Empty& operator=(const Empty& rhs) { ... }
};

不过要注意以下几点:

  1. 只有在被编译器需要的时候,它们才会被编译器创建,被合成出来的默认构造函数只执行编译器所需的行动。(详细见 《深度探索 C++ 对象模型》 第2章);
  2. 这些函数都是public的;
  3. 这些函数都是inline的(即函数定义在类的定义中)
  4. 如果显式地声明了其中一个函数,那么编译器将不再生成默认的函数。特别需要注意的是自定义的拷贝构造函数不仅会覆盖默认的拷贝构造函数,也会覆盖默认的构造函数
  5. 对于拷贝构造函数和赋值操作符来说,编译器创建的版本只是单纯地将来源对象的每一个 non-static 数据成员拷贝到目标对象中。
  6. 赋值操作符函数的行为与拷贝构造函数的行为基本是相同的,但是编译器生成赋值操作符函数是有条件的,如果会产生无法完成的操作,编译器将拒绝产生这一函数。
  7. 编译器生成的拷贝构造函数和赋值操作符都执行浅拷贝操作。当类里面有指针时,最好根据需要写执行深拷贝操作的拷贝构造函数和赋值操作符函数。

另外,还存在两种默认的函数:就是取地址运算符和取地址运算符的const版本,这两个函数在《Effective C++》中没有提及。

Empty* operator&() { ... }
const Empty* operator&() const { ... }

所以即使定义一个空类,下面的代码也是可以运行的:

Empty a;
const Empty *b = &a;
printf("%p\n", &a);     //调用取地址运算符
printf("%p\n", b);      //调用const取地址运算符

声明对象的坑

赋值还是构造

在面向对象程序设计中,对象间互相拷贝是经常进行的操作,那么调用的到底是拷贝构造函数还是赋值操作符呢?有一个简单的判定规则:

  • 左值对象已经存在的话调用的是赋值操作符,
  • 左值对象在当前语句第一个出现,那么调用拷贝构造函数。

还要注意函数形参不是引用时,会调用对象的拷贝构造函数,生成一个临时的形参对象,到函数结尾,会自动销毁。函数返回非引用对象时,也会调用拷贝构造函数创建一个对象返回。

具体看代码 Class_ConDesAssign.cpp

拷贝构造还是赋值

构造函数与析构函数

派生类构造函数调用顺序如下:

  1. 基类构造函数。如果有多个基类,则构造函数的调用顺序是基类在类派生表中出现的顺序。
  2. 若派生类中包含对象成员,还要进行对象成员初始化。如果有多个成员类对象则构造函数的调用顺序是对象在类中被声明的顺序。
  3. 派生类构造函数。

析构函数正好和构造函数相反。具体看下面程序 (constructor_derived_class.cpp)

关于异常抛出问题:

  1. 不建议在构造函数中抛出异常。构造函数抛出异常时,析构函数将不会被执行,需要手动的去释放内存;
  2. 析构函数不应该抛出异常。当析构函数中会有一些可能发生异常时,那么就必须要把这种可能发生的异常完全封装在析构函数内部,决不能让它抛出函数之外。因为如果对象抛出异常了,异常处理模块为了维护系统对象数据的一致性,避免资源泄露,有必要调用析构函数释放资源,这时如果析构过程又出现异常,那么谁来保证新对象的资源释放呢?前面的异常还没处理完又来了新的异常,这样可能会陷入无限的递归嵌套中。所以,从析构函数抛出异常,C++运行时系统会处于无法决断的境遇,因此C++语言担保,当处于这一点时,会调用 terminate()来杀死进程。

构造函数中调用虚函数
构造函数调用次数
析构的顺序
析构函数调用delete

禁止对象产生在堆(栈)中

一般情况下,编写一个类,是可以在栈或者堆分配空间。但有些时候,你想编写一个只能在栈或者只能在堆上面分配空间的类。例如说在嵌入式系统中工作,为了保证不发生内存泄漏,最好保证没有任何一个类型的对象可以从 heap 中分配出来。

在C++中,类的对象建立分为两种,一种是静态建立,如A a;另一种是动态建立,如 A* ptr=new A;这两种方式是有区别的。

  1. 静态建立类对象:是由编译器为对象在栈空间中分配内存,是通过直接移动栈顶指针,挪出适当的空间,然后在这片内存空间上调用构造函数形成一个栈对象。使用这种方法,直接调用类的构造函数。
  2. 动态建立类对象,是使用new运算符将对象建立在堆空间中。这个过程分为两步,第一步是执行operator new()函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数构造对象,初始化这片内存空间。这种方法,间接调用类的构造函数。

禁止对象建立在栈上

考虑当对象建立在栈上面时,由编译器分配内存空间,调用构造函数来构造栈对象。当对象使用完后,编译器会调用析构函数来释放栈对象所占的空间,编译器管理了对象的整个生命周期。如果编译器无法调用类的析构函数,情况会是怎样的呢?比如,类的析构函数是私有的,编译器无法调用析构函数来释放内存。所以,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性,如果类的析构函数是私有的,则编译器不会在栈空间上为类对象分配内存

只能在堆上分配类对象,就是不能静态建立类对象,可以通过将类的析构函数设为 private 来达到目的。

如下面的类 A:

class A
{
public:
    A(){}
    void destory(){delete this;}
private:
    ~A(){}
};  

如果使用A a;来建立对象,编译报错,提示析构函数无法访问。但是可以使用new 操作符来建立对象,构造函数是公有的,可以直接调用。此外,类中必须提供一个destory函数,来进行内存空间的释放。堆对象使用完成后,必须调用destory函数。

但是上面方法有以下缺点:

  1. 无法解决继承问题。如果A作为其它类的基类,则析构函数通常要设为virtual,然后在子类重写,以实现多态。因此析构函数不能设为private。
  2. 类的使用方法不统一,使用new建立对象,却使用destory函数释放对象,而不是使用delete。(使用delete会报错,因为delete对象的指针,会调用对象的析构函数,而析构函数类外不可访问)

还好C++提供了第三种访问控制,protected。将析构函数设为protected可以有效解决继承问题,使得类外无法访问protected成员,子类则可以访问。

为了统一类的使用方式(不要 new 和 destroy 搭配),可以将构造函数设为protected,然后提供一个public的static函数来完成构造,这样不使用new,而是使用一个函数来构造,使用一个函数来析构。代码如下:

class A
{
protected:
    A(){}
    ~A(){}
public:
    static A* create()
    {
        return new A();
    }
    void destory()
    {
        delete this;
    }
};

这样,可以像下面这样在堆上创建、销毁对象:

A *a = A::create();
a->destory();

禁止对象产生在堆上

只有使用 new 运算符,对象才会建立在堆上,因此,只要禁用new运算符就可以实现类对象只能建立在栈上。虽然不能影响new运算符的能力(因为是C++语言内建的),但是可以利用一个事实:new运算符总是先调用 operator new,而后者我们是可以自行声明重写的。因此,将operator new()设为私有即可禁止对象被new在堆上

class A
{
private:
    void* operator new(size_t t){}     // 注意函数的第一个参数和返回值都是固定的
    void operator delete(void* ptr){}  // 重载了new就需要重载delete
public:
    A(){}
    ~A(){}
};

只能new创建对象

构造函数初始值列表

定义变量时习惯对其初始化,而非先声明、再赋值。

string foo = "Hello";   // 定义并初始化
string bar;             // 默认初始化为空 string 对象
bar = "Hello";          // 为 bar 赋一个新值

就对象的数据成员来说,如果没有在构造函数的初始化列表中显式地初始化成员,则该成员在构造函数之前执行默认初始化,在构造函数中进行的是赋值操作。但是如果成员是 const 或者引用的时候,或者成员属于某种类类型且该类没有定义默认构造函数时,必须进行初始化。

class ConstRef{
public:
    // 正确, 使用构造函数初始化列表显式地初始化引用和 const 成员.
    ConstRef(int num):const_i(num), ref_j(num){}
    /*
    ConstRef(int num){
        const_i = num;  // 错误,不能给 const 赋值
        ref_j = num;    // 错误, ref_j 没有初始化
    }
    */
private:
    const int const_a = 0;
    const int const_i;
    int &ref_j;
};

构造函数初始值列表只说明用于初始化非静态成员的值,而不限定初始化的具体执行顺序。成员的初始化顺序与它们在类定义中的出现顺序一致,第一个成员先被初始化,然后第二个,以此类推。如果一个成员是用另一个成员来初始化的,那么两个成员的初始化顺序就很关键了!(可能的话,尽量避免使用某些成员初始化其他成员)。

初始化顺序
类定义static与const
必须通过构造函数初始化列表的变量

数据成员与成员函数

数据成员

类的成员变量(数据成员)和普通变量一样,也有数据类型和名称,占用固定长度的内存空间,一般有以下几种成员变量:

  • 普通变量:可以在构造函数中进行赋值,也可以在构造函数的初始化列表中进行初始化。
  • 静态变量(static):属于类所有,而不属于类的对象,因此不管类被实例化了多少个对象,该变量都只有一个。
  • 常量变量(const):需要进行类内进行初始化,可以在定义时初始化,或者在构造函数的初始化列表中进行。
  • 引用型变量:和const变量类似,需要在类内进行初始化。
  • static const integral 变量:对于既是const又是static 而且还是整形变量,可以直接在类的定义中初始化。short可以,但float的不可以哦。(short可以,但float的不可以)

static const integral 变量的示例如下:

class A{
public:
    static const int a=1;
    static const int b;
    // non-const static data member must be initialized out of line
    // static int c = 3;
    static int d;
    static const float e;
    // static const float f=1;
};

const int A::b = 2;
int A::d = 4;
const float A::e = 5;

特别注意的是在继承的时候,允许子类存在与父类同名的成员变量,但是并不覆盖父类的成员变量,他们同时存在。

派生类重复定义基类数据成员

类的静态成员

在类成员的声明之前加上关键字 static 使得成员与类本身直接相关,而不是与类的各个对象保持关联。和其他成员一样,静态成员可以是 public 或 private 的,静态数据成员的类型可以是常量、引用、指针、类类型等。类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。

类的静态数据成员不属于类的任何一个对象,所以它们不是在创建类的对象时被定义的,也就是说不是由类的构造函数初始化的(不能用构造函数初始化静态数据成员)。通常情况下,类的静态成员不应该在类的内部初始化,必须在类的外部定义和初始化每个静态成员。一种例外情况是,为静态数据成员提供 const 整数类型的类内初始值。不过即使是 static const 也不能用构造函数初始化列表来进行初始化,这是因为:

  1. static属于类,它在未实例化的时候就已经存在了,而构造函数的初始化列表,只有在实例化的时候才执行。
  2. static成员不属于对象。我们在调用构造函数自然是创建对象,一个跟对象没直接关系的成员要它做什么呢。

类似的,静态成员函数也不与任何对象绑定在一起,不包含 this 指针。静态成员函数不能声明为 const 的(本来就不会去改变对象的值,所以没有必要定义为const),而且不能在 static 函数体内使用 this 指针。既可以在类的内部定义静态成员函数,也可以在外部定义静态成员函数。在类的外部定义静态成员函数时,不能重复关键字 static。

虽然类的静态成员不属于类的某个对象,但我们仍可以使用类的对象、引用或者指针来访问静态成员。

静态成员可以应用于某些普通成员不能应用的场景:

  1. 静态数据成员可以是不完全类型,甚至可以是它所属的类类型。而非静态数据成员则受到限制,只能声明成它所属类的指针或引用;
  2. 可以使用静态数据成员作为默认实参。普通数据成员不能作为默认实参,因为它的值本身属于对象的一部分。

成员函数

成员函数也可以被重载,只要满足重载的要求,即同一个作用域内的几个函数名字相同形参列表不同,成员函数的 virtual 关键字可有可无。

const函数的操作

函数隐藏是指派生类的函数屏蔽了与其同名的基类函数,规则如下:

  1. 如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无 virtual 关键字,基类的函数将被隐藏(注意别与重载混淆)。
  2. 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有 virtual 关键字(否则是覆盖)。此时,基类的函数被隐藏(注意别与覆盖混淆)

当然,派生类还可以覆盖基类函数,以实现多态。这三种情况的执行可以总结为以下:

  • 重载:看参数。
  • 隐藏:用什么就调用什么。
  • 覆盖:调用派生类。

C++中成员函数能否同时用static和const进行修饰?

不行!这是因为C++编译器在实现const的成员函数的时候为了确保该函数不能修改类的中参数的值,会在函数中添加一个隐式的参数const this*。但当一个成员为static的时候,该函数是没有this指针的,也就是说此时const的用法和static是冲突的。

更详细的解释如下:在定义一个类对象的时候,实际上只给该对象的非静态的数据成员分配内存空间(假设没有虚函数),而该类的静态成员数据以及该类的函数都在编译的时候分配到一个公共的空间里,所有,在定义一个对象并调用类对象的函数的时候,函数根本不知道到底是哪个对象调用了他,怎么解决这个问题呢?

C++利用传递this指针的方式来实现,调用一个类对象里的函数的时候,将把这个对象的指针传递给他,以便函数对该对象的数据进行操作,对于一个定义为const的函数,传递的是const的this指针,说明不能更改对象的属性,而对static成员的函数不传递this指针,所以不能用const来修饰static的成员函数了!

从对象模型上来说,类的非static成员函数在编译的时候都会扩展加上一个this参数,const的成员函数被要求不能修改this所指向的这个对象;而static函数编译的时候并不扩充加上this参数,自然无所谓const。

如果在编写const成员函数时,不慎修改了数据成员,或调用了其他非const成员函数,编译器就会报错。如果想在const函数中改变某个成员变量的值,那么可以将该变量声明为 mutable 类型。

此外,要注意const函数与同名的非const函数是重载函数,类的const对象只能调用const函数,非const对象可以调用const函数和非const成员函数。

具体的示例在 C++_Class_Func.cpp

继承

继承是类的重要特性。通过继承联系在一起的类构成一种层次关系,通常在层次关系的根部有一个基类,其他类则直接或者间接地从基类继承而来,这些继承得到的类称为派生类。基类负责定义在层次关系中所有类公同拥有的成员,而每个派生类定义各自特有的成员。

派生类可以继承定义在基类中的成员,但是派生类的成员函数不一定有权访问从基类继承而来的成员,访问权限受下面因素影响。

  • 继承方式;
  • 基类成员的访问权限(即public/private/protected)。

继承有三种方式,即公有(Public)继承、私有(Private)继承、保护(Protected)继承。(私有成员不能被继承)

  • 公有继承就是将基类的公有成员变为自己的公有成员,基类的保护成员变为自己的保护成员。
  • 保护继承是将基类的公有成员和保护成员变成自己的保护成员。
  • 私有继承是将基类的公有成员和保护成员变成自己的私有成员。

三种继承方式的比较

具体代码示例参见 C++_Inheritance.cpp

一个派生类对象包含有多个组成部分:一个含有派生类自己定义的(非静态)成员的子对象,以及一个与该派生类继承的基类对应的子对象,如果有多个基类,那么对应的子对象有多个(C++ 没有明确规定派生类的对象在内存中怎样分布)。继承中的父类的私有变量是在子类中存在的,不能访问是编译器的行为,可以通过指针操作内存来访问的。

具体代码示例参见 C++_Inheritance_2.cpp

因为在派生类对象中含有与其基类对应的组成部分,所以可以把派生类的对象当成基类对象来使用,而且也可以将基类的指针或引用绑定到派生类对象中基类部分上。这种转换通常称为 派生类到基类的(derived-to-base) 类型转换。派生类向基类的转换是否可访问由使用该转换的代码决定,同时派生类的派生访问说明符也会有影响。

如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。不论从基类派生出来多少个派生类,对于每个静态成员来说都只存在唯一的实例。

派生类存储,隐式转换
子类继承父类所有对象

虚拟继承

虚拟继承是多重继承中特有的概念,虚拟基类是为解决多重继承而出现的。如:类D继承自类B1、B2,而类B1、B2都继承自类A,因此在类D中两次出现类A中的变量和函数。为了节省内存空间,可以将B1、B2对A的继承定义为虚拟继承,而A就成了虚拟基类。实现的代码如下:

class A
class B1:public virtual A;
class B2:public virtual A;
class D:public B1,public B2;

虚拟继承在一般的应用中很少用到,所以也往往被忽视,这也主要是因为在C++中,多重继承是不推荐的,也并不常用,而一旦离开了多重继承,虚拟继承就完全失去了存在的必要因为这样只会降低效率和占用更多的空间。

由于有了间接性和共享性两个特征,所以决定了虚继承体系下的对象在访问时必然会在时间和空间上与一般情况有较大不同。

  • 时间:在通过继承类对象访问虚基类对象中的成员(包括数据成员和函数成员)时,都必须通过某种间接引用来完成,这样会增加引用寻址时间(就和虚函数一样),其实就是调整this指针以指向虚基类对象,只不过这个调整是运行时间接完成的。
  • 空间:由于共享所以不必要在对象内存中保存多份虚基类子对象的拷贝,这样较之多继承节省空间。虚拟继承与普通继承不同的是,虚拟继承可以防止出现diamond继承时,一个派生类中同时出现了两个基类的子对象。也就是说,为了保证这一点,在虚拟继承情况下,基类子对象的布局是不同于普通继承的。因此,它需要多出一个指向基类子对象的指针。

多态

C++ 中,基类必须将它的两种成员函数区分开来:一种是基类希望其派生类进行覆盖的函数,另一种是基类希望派生类直接继承而不要改变的函数。对于前者,基类通常将其定义为虚函数(virtual)。当我们使用指针或引用调用虚函数时,该引用将被动态绑定。根据引用或指针所绑定的对象不同,该调用可能执行基类的版本,也可执行某个派生类的版本。(成员函数如果没有被声明为虚函数,则其解析过程发生在编译时而非运行时)

虚函数

基类通过在其成员函数的声明语句之前加上 virtual 关键字使得该函数执行动态绑定,任何构造函数之外的非静态函数都可以是虚函数。如果基类把一个函数声明为虚函数,则该函数在派生类中隐式地也是虚函数(派生类可以不重写虚函数,必须重写纯虚函数)。[C++ primer P528]

C++中的虚函数的作用主要是实现多态机制。关于多态,简而言之就是用父类型的指针指向其子类的实例,然后通过父类的指针调用子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。

虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。每个包含有虚函数的类有一个虚表,在有虚函数的类的实例中保存了虚表的指针,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表像一个地图一样,指明了实际所应该调用的函数。

C++的编译器保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能)。对于多继承来说,情况稍微有点复杂,先来看下面的例子:

class ClassA {
public:
    virtual ~ ClassA() { };

    virtual void FunctionA() { };
};

class ClassB {
public:
    virtual void FunctionB() { };
};

class ClassC : public ClassA, public ClassB {
public:
};

ClassC aObject;
ClassA *pA = &aObject;
ClassB *pB = &aObject;
ClassC *pC = &aObject;

pA,pB 和 pC 大小一样吗?要回到这个问题,需要知道多重继承中内存的布局,详细内容可以参考陈皓的文章,简单来说,是因为:

多重继承时,以声明顺序在内存中存储A/B的空间(即虚表+数据),再存储C的数据;C中重新实现的虚函数会在A/B的虚表中取代原有的虚表项,C中新加的寻函数会加在A中虚表的最后。

所以,针对上面的多重继承,内存分布如下图:

构造与析构

为了能够正确的调用对象的析构函数,一般要求具有层次结构的顶级类定义其析构函数为虚函数。因为在delete一个抽象类指针时候,必须要通过虚函数找到真正的析构函数。如下所示是正确的用法:

class Base
{
public:
    Base(){             cout << "Create Base..." << endl;}
    virtual ~Base(){    cout << "Delete Base..." <<endl;}
};

class Derived: public Base
{
public:
    Derived(){  cout << "Create Derived..." << endl;}
    ~Derived(){ cout << "Delete Derived..." <<endl;}
};

void foo()
{
    Base *pb;
    pb = new Derived;
    delete pb;
//    Create Base...
//    Create Derived...
//    Delete Derived...
//    Delete Base...
}

如果析构函数不加virtual,delete pb 将会导致未定义行为,对大多数编译器来说,只会执行Base的析构函数,而不是真正的Derived析构函数。这是因为如果不是virtual函数,调用哪个函数依赖于指向指针的静态类型,这里来说就是 Base。

析构函数可以是纯虚的,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的,如下示例:

class Base
{
public:
    Base(){}
    // virtual ~Base();     // Link Error if we define a derived child object.
    virtual ~Base()=0;
};
Base::~Base() { }   // Link Error if we define a derived child object, but with no function body.

class Derived: public Base
{
};
int main(){
    Derived d;
    return 0;
}

更多

虚函数要么必须有定义,要么必须声明为一个纯虚函数。

A virtual function declared in a class shall be defined, or declared pure (10.4) in that class, or both; but no diagnostic is required (3.2).
-- C++03 Standard: 10.3 Virtual functions [class.virtual]

这是因为定义派生类对象时,链接器需要知道虚函数表中基类的虚函数指针,如果虚函数没有定义,就找不到该指针。如下示例:

class Base
{
public:
    virtual void test();
};

class Derived: public Base
{
};
int main(){
    Derived d;      // 链接错误,如果没有该定义语句,则不会链接出错。
    return 0;
}

而对于纯虚函数来说,由于不能生成纯虚函数的对象,所以不需要知道纯虚函数的定义。不过这里有一个例外,前面有提起过如果将析构函数声明为纯虚函数,那么必须提供定义(可以为空函数体),因为派生类的析构函数隐含了对基类析构函数的调用,所以链接器必须要能够找到函数地址。

此外,要知道常见的不能声明为虚函数的有:普通函数(非成员函数);静态成员函数;内联成员函数;构造函数;友元函数。分别如下:

  1. 为什么C++不支持普通函数为虚函数?

    普通函数(非成员函数)只能被overload,不能被override,声明为虚函数也没有什么意思,因此编译器会在编译时绑定函数。

  2. 为什么C++不支持构造函数为虚函数?

    构造函数一般是用来初始化对象,只有在一个对象生成之后,才能发挥多态的作用,如果将构造函数声明为virtual函数,则表现为在对象还没有生成的情况下就使用了多态机制,因而是行不通的

  3. 为什么C++不支持内联成员函数为虚函数?

    内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。(再说了,inline函数在编译时被展开,虚函数在运行时才能动态的邦定函数)

  4. 为什么C++不支持静态成员函数为虚函数?

    这也很简单,静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,也没有动态邦定的必要性。

  5. 为什么C++不支持友元函数为虚函数?

    C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。

虚函数地址分配
虚函数表被置为0
缺省参数是静态绑定的

抽象类

为了方便使用多态特性,常常需要在基类中定义虚拟函数。但是在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
 为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数,则编译器要求在派生类中必须予以重载以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象,这样就很好地解决了上述两个问题。

纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”,比如:

virtual ReturnType Function()= 0;


抽象类是一种特殊的类,它是为了抽象和设计的目的而建立的,它处于继承层次结构的较上层。抽象类是不能定义对象的,在实际中为了强调一个类是抽象类,可将该类的构造函数说明为保护的访问控制权限,这样就无法静态或者 new 创建该类的栈或者堆对象。

抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。**如果派生类没有重新定义纯虚函数,而派生类只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。**如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体类了。

抽象类的规定如下:

  1. 抽象类只能用作其他类的基类,不能建立抽象类对象(构造函数设为 protect)。
  2. 抽象类不能用作参数类型、函数返回类型或显式转换的类型。
  3. 可以定义指向抽象类的指针和引用,此指针可以指向它的派生类,进而实现多态性。

抽象类对象指针

友元

友元关系是单向的,不是对称,不能传递。关于传递性,有人比喻:父亲的朋友不一定是儿子的朋友。那关于对称性,是不是:他把她当朋友,她却不把他当朋友?

友元特征
友元访问类所有成员?

// TODO

更多阅读

《深度探索C++对象模型》
Effective C++ 05
More Effective C++ 条款 27

C++对象的内存布局(上)
C++编译器自动生成的函数
如何让类对象只在栈(堆)上分配空间?
构造函数:C++
C++ 虚函数表解析
深入理解C++的动态绑定和静态绑定
C++ 抽象类
关于C++中的虚拟继承的一些总结
类中的const成员
C++函数中那些不可以被声明为虚函数的函数

Destructors
Should a virtual function essentially have a definition
When to use virtual destructors?
Should every class have a virtual destructor?