首页 > 代码库 > c++编程思想---第14章 继承和组合

c++编程思想---第14章 继承和组合

1,实现代码重用两种方式

组合:我简单的在新类中创建一个已存在的类的对象。因为新类是由已存在类的对象组合而成,称之为组合。 这样就可以把已存在类的功能加到了新的类中去。

继承:扩展父类。

2,基类的private成员,只能通过基类提供的接口来访问。

3,调用基类的函数,隐藏的或者覆盖的可以通过作用域运算符来访问。

4,重载

  (1)相同的范围(在同一个类中)

  (2)函数名字相同

  (3)参数不同

  (4)virtual 关键字可有可无

   覆盖(派生类函数覆盖基类函数)

  (1)不同的范围(分别位于派生类与基类)

  (2)函数名字相同

  (3)参数相同

  (4)基类函数必须有 virtual 关键字

   隐藏(派生类的函数屏蔽了与其同名的基类函数)只要子类自己定义了一个和基类重名的函数(无论返回值,参数列表是否相同),只要名字相同,基类的版本都是会被隐藏的。

  (1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无 virtual 关键字,基类的函数将被隐藏(注意别与重载混淆)

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

5,初始化列表的作用:初始化类的成员有两种方式,一是使用初始化列表,二是在构造函数体内进行赋值操作。使用初始化列表主要是基于性能问题,对于内置类型,如int, float等,使用初始化类表和在构造函数体内初始化差别不是很大,但是对于类类型来说,最好使用初始化列表,为什么呢?使用初始化列表少了一次调用默认构造函数的过程,这对于数据密集型的类来说,是非常高效的。未使用初始化列表的情况是:当类的有成员变量时,而且该成员变量是类class类型的,编译器会先调用默认的构造函数,然后调用赋值的函数,进行复制操作。影响效率。

6,除了性能问题之外,有些时场合初始化列表是不可或缺的,以下几种情况时必须使用初始化列表:

  (1)常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面

  (2)引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面

  (3)没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化。

7,C++的一个强化机制,在没有构建完基类、成员对象(class型的,内建类型)时,是没有办法进入该类的构造函数的,为了防止在构造函数中调用成员对象内的函数,父类的函数。先调用基类的构造函数,然后调用成员对象的构造函数,然后进入自己的构造函数。

8,伪构造函数语法 int i(100);

9,构造函数的调用顺序是从根往下顺序构造,析构函数是从下往根的逆序方向调用。构造和析构的方向是正好相反的。多个类成员变量的构造顺序不是按照初始化列表的顺序进行的,是根据在类中声明的位置进行先后调用的。

10,编译器自动生成的有

  (1)默认构造函数。只要自己定义了一个构造函数(无论什么构造函数)编译器就不会自动生成了。

  (2)拷贝构造函数。

  (3)赋值运算符 =。

  (4)析构函数

11,不能够被继承的函数有

  (1)构造函数。因为它的作用是针对自己的那一层的。

  (2)析构函数。因为它的作用也是针对自己的那一层的。

  (3)“=”,赋值运算符,子类会有一个默认的赋值运算符,然后会把父类的赋值运算符给隐藏掉了。

12,静态成员函数与非静态成员函数的共同点

  (1)他们均可以被继承。

  (2)如果我们定义了一个静态成员函数,则它基类中同名的函数也是会被隐藏的。

13,静态成员函数不能为虚函数。因为他没有this指针。

14,组合:为产生新功能,车的例子,车是有很多个零部件构成的。has-a的关系

15,继承:使用旧的接口,定义新功能。is-a的关系。决定什么时候用组合还是构造,还有一个判断点是,考虑是否需要用到向上类型转换。

16,三种继承权限:

  public 继承

  protect 继承

  private 继承

  组合结果:

  基础类中     继承方式     子类中

  public & public继承 => public
  public & protected继承 => protected 
  public & private继承 => private

  protected & public继承 => protected
  protected & protected继承 => protected 
  protected & private继承 => private 

  private & public继承 => 子类无权访问
  private & protected继承 => 子类无权访问
  private & private继承 => 子类无权访问

由以上组合结果可以看出()这些权限下的成员到了子类中,但是子类的成员,权限是由继承方式来决定的。

  (1)、public继承不改变基类成员的访问权限
  (2)、private继承使得基类所有成员在子类中的访问权限变为private
  (3)、protected继承将基类中public成员变为子类的protected成员,其它成员的访问 权限不变。
  (4)、基类中的private成员不受继承方式的影响,子类永远无权访问。

17,我们对C++类三种方式控制权限总结如下:

 

访问权限publicprotectedprivate
对本类 可见 可见 可见
对子类 可见 可见 不可见
对外部(调用方) 可见 不可见 不可见

18,私有继承,当与运行时类型识别相连时,私有继承特别复杂,应注意。(后面章节有学习到)

19,私有继承成员公有化(仅限于基类的成员函数是public、protected的权限下,private是不可以的,实验过了)

class Car
{
public :
    Car(){}

    void hello()
    {
        qDebug() << "hello !";
    }

protected:
    void haha()
    {
        qDebug() << "haha !";
    }
};

class BMW : private Car
{
public :

    using Car::hello;
    using Car::haha;

};

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    Car car;

    BMW bmw;
    bmw.hello();
    bmw.haha();

    return a.exec();
}

20,多重继承存在争议(后续学习)。

21,向上类型转换:子类到父类是安全的。向上类型转换失去子类独有的成员变量(除子类重写的虚函数外)。

Child child;

Parent * parent = &child;

这时parent只能操作child的中有的属于基类的部分(重写的虚函数除外)。

22,另一个经常发生的和继承有关的类似问题是在实现派生类的拷贝构造函数时。看看下面这个构造函数,其代码和上面刚讨论的类似:

class base
{
public:
  base(int initialvalue = http://www.mamicode.com/0): x(initialvalue)
  {
      qDebug() << "i am base constructor !";
  }
  base(const base& rhs): x(rhs.x)
  {
      qDebug() << "i am base copy-constructor !";
  }

private:
  int x;
};

class derived: public base
{
public:
  derived(int initialvalue = http://www.mamicode.com/1) : base(initialvalue), y(initialvalue)
  {
      qDebug() << "i am derived constructor !";
  }
  derived(const derived& rhs) : y(rhs.y)
  {
      qDebug() << "i am derived copy-constructor !";
  }                    // 错误的拷贝构造函数,需要在初始化列表中显示的调用父类的拷贝构造函数。这时调用的是基类的默认的构造函数,只要不是我们显示调用哪个构造函数,默认调用的都是默认的构造函数。
             //如果子类的这个拷贝构造函数注掉的话,编译器会自动生成一个拷贝构造函数,该构造函数会调用基类的拷贝构造函数。

private:
  int y;
};

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    derived child1;

    derived child2 = child1;

    return a.exec();
}

没有注掉子类的拷贝构造函数的结果:
i am base constructor !
i am derived constructor !
i am base constructor !
i am derived copy-constructor !
注掉之后的结果是:
i am base constructor !
i am derived constructor !
i am base copy-constructor !
可以看出来默认生成的拷贝构造函数是可以调用基类的拷贝的构造函数,如果自己声明了拷贝构造函数,则必须在初始化列表中显示的调用基类的拷贝构造函数,不然默认调用的都是默认的构造函数。


类derived展现了一个在所有c++环境下都会产生的bug:当derived的拷贝创建时,没有拷贝其基类部分。当然,这个derived对象的base部分还是创建了,但它是用base的缺省构造函数创建的,成员x被初始化为0(缺省构造函数的缺省参数值),而没有顾及被拷贝的对象的x值是多少!

为避免这个问题,derived的拷贝构造函数必须保证调用的是base的拷贝构造函数而不是base的缺省构造函数。这很容易做,只要在derived的拷贝构造函数的成员初始化列表里对base指定一个初始化值:

class derived: public base {
public:
  derived(const derived& rhs): base(rhs), y(rhs.y) {}

  ...

};

现在,当用一个已有的同类型的对象来拷贝创建一个derived对象时,它的base部分也将被拷贝了。

如果子类没有实现拷贝构造函数,编译器会自动生成一个拷贝构造函数,它将首先自动地调用基类的拷贝函数,然后再是个成员对象的拷贝构造函数(或者在内建类型上执行位拷贝)。

结论:必须记住无论何时我们在自己创建了自己的拷贝构造函数的时候,都要正确地调用基类的拷贝构造函数(就像编译器做的那样)。

c++编程思想---第14章 继承和组合