C++ -- 面向对象之继承

一.继承的概念

1.继承的定义:继承是一种复用手段,通过继承关系,子类可以从父类获得一些成员变量或成员函数,共享公有的东西,实现各自本质不同的东西。

2.继承的书写:在子类的类名后面加上":"再加上继承关系,再加上父类的名字。

class Person

{

private:

      string _name;

};

class Student : public Person   //Student类继承了父类Person

{

public:

      int _stunum;

};

3.如果子类继承了父类,则父类的成员(成员变量或成员函数)都会(不管是公有、私有还是保护的成员)都会变成子类的一部分,但是私有的是不可见(不可见不是在子类中不存在,而是在子类中不能访问这些成员)。

例如,对于上面的继承关系,我们定义一个子类对象 Student s;

则监视窗口如下:

可以看到父类Person类的私有数据成员_name也被继承下来了,但是不能在Student类里访问这个私有成员(并且我们也不能通过Person类的对象访问该数据成员,因为其访问限定符是私有的)。

二.三种继承关系与三种类成员访问限定符

1.三种继承关系

①public继承  ②protected继承  ③private继承

2.三种类成员访问限定符

(1)我们知道C++类里面的访问限定符有3种,分别是public、private、protected;

(2)如果访问限定符为保护则在类外可以通过对象来访问;如果访问限定符为private和protected,则再类外不可访问。

(3)访问限定符直接决定了能否在类外访问该成员。

3.三种继承关系与三种访问限定符有什么样的关系?

(1)如果父类成员的访问属性为private,则无论是何种继承方式,父类成员在子类都不可见。(不可见不是在子类中不存在,而是在子类中不能访问这些成员)。

(2)当父类成员的访问属性不是private时:

①对于public继承:父类所有成员在子类的访问属性都不变;(如果父类成员的访问属性是公有则在子类里面的访问属性也是公有,如果父类成员的访问属性是保护则在子类里面的访问属性也是保护)

②对于protected继承:父类所有的成员在子类的访问属性都变为protected;(不论父类的成员访问限定符是public还是protected,父类成员在子类访问属性都是protected)

③对于private继承:父类所有的成员在子类的访问属性都变为private。(不论父类的成员访问限定符是public还是protected,父类成员在子类访问属性都是private)

4.面试题:如何定义一个不能被继承的类

可以将父类的构造函数定义为私有的,因为如果一个类继承了父类的私有成员,则该成员在这个子类中是不可见的,因为子类的构造函数是合成的,它会调用父类构造函数,但是父类的构造函数又不可访问,所以相当于父类不能被继承。如果将父类的构造函数定义为私有,则父类也不能定义对象,则可以再这个类里写一个公有函数,使其可以调用该构造函数,进而可以生成对象,但是一般的成员函数必须通过对象来访问,如何能够不通过对象来访问成员函数,则可以将成员函数定义为static,因为静态成员可以通过类名来进行访问。

三.父类与子类的赋值兼容规则

(1)子类可以赋值给父类(可以将子类对象赋值给父类对象,子类对象的指针(或引用)也可以赋值给父类对象的指针(或引用));

  • 这种方式称为切片或切割(注意虽然父类和子类的类型不同,将不同类型的对象进行赋值在C语言中会发生类型转换,但是这里没有隐式类型的转换)
  • 如何验证没有发生隐式类型的转换:
int main()

{

      Person p;

      Student s;

      p = s;

      Person &p1 = s;  //如果发生隐式类型的转换,因为p1和s不是统类型,则p1引用s时就会生成一个临时变量,p1引用的是这个临时变量,临时变量具有常性,则必须在前面加上const关键字,但是这里没有加const会编过,说明这里没有隐式类型的转换



      //例如下面具有隐式类型转换的类型:

      int i = 1;

      double d = 2.0;

      int& m = d; //会出错

      const int& m = d;  //前面必须要加const ,因为有隐式类型转换,中间会产生一个临时变量,mu引用的是这个临时变量(具有常性),所以必须加const



      system("pause");

      return 0;

}

(2)父类对象不能赋值给子类对象;(编译器会直接给出出错提示)

(3)父类对象的指针(或引用)也不能赋值给子类对象的指针(或引用)

  • 但是将父类对象的指针(或引用)强转为子类对象的指针(或引用),则编译没有问题,但是如果访问数据成员就会出错,这里发生了越界(因为将子类指针赋值给父类指针,编译器会多开一些空间)。

四.继承的分类

1.单继承

(1)定义:一个子类只有一个直接父类;

(2)定义一个B类型的b,则监视窗口如下:

2.多继承:

(1)定义:一个子类有两个或两个以上的直接父类(两个父类之间用逗号分隔);

例如给定如下代码:

class A

{

public:

      int _a;

};

class B

{

public:

      int _b;

};

class C :public A, public B

{

public:

      int _c;

};

(2)定义了一个C类型的对象c,则监视窗口如下(可以看到c里面既有A类的成员又含有B类的成员):

五.派生类的默认成员函数

1.派生类的六个默认成员函数都是合成的。(子类不用管父类的成员,只需要实现对自己成员变量的相关操作,父类成员的操作调用父类的默认成员函数)

①构造函数和拷贝构造函数都需要在初始化列表中自己手动的去调用父类的构造或拷贝构造函数不用加上域限定符;

      Student(const char* name,int num)

           :Person(name)

           , _num(num)

      {

           cout << "Student()" << endl;

      }

      Student(const Student& s)

           :Person(s)

           , _num(s._num)

      {

           cout << "Student(const Student& s)" << endl;

      }

②赋值运算符也需要自己手动的去调用父类的赋值运算符,但是前面必须加上父类的域名,因为子类与父类的赋值运算符函数名相同(都是operator=),子类会隐藏父类的赋值运算符,调用时,不加域限定符,就会调用子类的赋值运算符,就会出现递归死循环,从而导致栈溢出。   

  Student& operator=(const Student& s)

      {

           cout << "operator=(const Student& s)" << endl;

           if (this != &s)

           {

                 Person::operator=(s);

                 _num = s._num;

           }

           return *this;

      }

③析构函数不用自己去调用父类的析构函数,在调用子类的析构函数之后编译器会自动调用父类的析构函数。(因为先构造父类对象,再构造子类对象,所以会先析构子类对象再析构父类对象,如果在子类的析构函数里显示的调用父类的析构函数,编译器会认为子类已经被析构了,如果析构函数里还有对子类的一些操作,编译器都不会再执行,就有可能导致程序出错,编译器为了防止这种情况,所以会在子类对象销毁之后自动调用父类的析构函数)   

  ~Student()

      {

           cout << "~Student()";

      }

六.菱形继承

1.定义:若有四个类A,B,C,D;   B和C同时单继承A, D多继承B和C,则这个继承体系构成菱形继承。​

2.菱形继承对象模型:(我们可以发现先继承的在上面)

3.菱形继承带来的缺点:

①二义性;(如果定义一个D的一个对象d,通过d访问_a就不知道访问的是哪个_a,这就是二义性(解决二义性方法可以是加上域作用限定符,但是这个方法不能解决数据冗余)。)

②数据冗余。(因为B和C同时继承A,若A有成员变量_a,则B和C都继承了A的_a,而D同时继承了B和C,则D里面就会有两个_a,但是D只需要一个_a就可以了,这就是数据冗余。)

例1:给定如下代码:

class A

{

public:

      int _a;

};

class B : public A

{

public:

      int _b;

};

class C : public A

{

public:

      int _c;

};

class D : public B,public C

{

public:

      int _d;

};

void Test2()

{

      D d;

      d._a = 1;

}



运行就会出错,错误是对"_a“的访问不明确

​4.如何解决菱形继承带来的缺点呢?

(1)首先可以想到:因为d里面含有两个_a,所以访问会出现二义性的问题,为了解决这个问题,我们可以在_a前面加上域限定符,可以限定是那个类域里面的_a;

例如:

​(2)但是这种方式并不能解决数据冗余。

5.如何解决数据冗余呢?(通过虚继承的方式)

①虚继承是在两个单继承时加上virtual关键字;

例如:

class A

{

public:

      int _a;

};

class B : virtual public A

{

public:

      int _b;

};

class C : virtual public A

{

public:

      int _c;

};

class D : public B,public C

{

public:

      int _d;

};

void Test2()

{

      D d;

      d._a = 1;  //此时d里面只有一个_a

}

运行结果如下:

②监视窗口如下:(此时监视窗口的不准确,监视窗口上仍然有两个_a,但是通过改变_a的值会发现两个_a同时改变,说明这里的两个_a是同一个_a,这里主要是编译器方便我们去观察,但是内存一定是准确的)

③内存窗口如下:

  • 通过赋值发现b在0x006FF9E4地址处,c在0x006FF9EC地址处:
  • 我们可以发现c0 dd 13 01  和 38 e2 13 01像地址(注意这是小端机),然后通过内存窗口观察这两个地址里面存放了什么:

  • 我们可以发现在第一张表中,通过B的地址(那个存放存储偏移量地址的地址)0x006FF9E0加上20刚好就是0x006FF9F4,这个地址里面存放的就是_a;
  • 同样的可以发现通过C的(存放存储偏移量地址的)地址0x006FF9E8加上12刚好就是0x006FF9F4,这个地址里面存放的就是_a,这就达到了菱形继承里面只有一个_a。

猜你喜欢

转载自blog.csdn.net/xu1105775448/article/details/80025400