C++学习随笔10 面向对象编程(3)- 继承
- 公有(public)、私有(private)、保护(protected)继承
为了便于大家记忆,我给访问权限的大小定义:public > protected > private > no access(no access就是即使是该类内部的成员函数都无法访问),基于这个定义给出我总结的一个小诀窍,就是:遇升不升,遇降则降,遇平则平,私有除外。访问权限的插图如下:
一个类成员的访问权限确定后,又对应了两种情况,一种是在类内,一种是在类外。总结一下:
在类内部:只要不是no access,那么我们都可以访问该成员。也就是说类内部的访问权限比较松,凡是可访问的权限都能访问,不论是public、protected还是private。
在类外部:只要不是public,那么我们都不可以访问该成员。也就是说在类的外部访问权限比较严格,只要访问权限不是public的成员,在类外部都不能访问它。
C++默认是private继承。
调整访问控制:
在派生类中可以调整基类成员的访问控制属性。
1 class Base{ 2 int b1; 3 protected: 4 int b2; 5 public: 6 int b3; 7 }; 8 9 class Pri : private Base{ 10 public: 11 using Base::b3; //将私有成员调整为公有成员 12 }; 13 14 int main(){ 15 Pri pri; 16 pri.b3 = 1; //OK 17 }
调整访问控制属性的前提是在派生类中该成员必须是可见的。(即b1就无法在派生类中进行访问属性的调整)
- 派生类的构造函数
基类的构造函数不能被继承,在声明派生类时,对继承过来的成员变量的初始化工作也要由派生类的构造函数来完成。事实上,通过派生类创建对象时必须要调用基类的构造函数,这是语法规定。也就是说,定义派生类构造函数时最好显示调用基类构造函数;如果没有显示调用,就会自动调用基类的默认构造函数(不带参数的构造函数);如果没有默认构造函数,那么编译失败。如果基类有默认构造函数(无参构造函数),那么在派生类构造函数中可以不指明,系统会默认调用;如果没有,那么必须要指明,否则系统不知道如何调用基类的构造函数。
派生类构造函数的一般形式为:
派生类构造函数名(总参数表列): 基类构造函数名(参数表列), 子对象名(参数表列)
{
派生类中新增数据成员初始化语句
}
派生类构造函数的总参数表列中的参数,应当包括基类构造函数和子对象的参数表列中的参数以及新增数据成员。基类构造函数和子对象的次序可以是任意的
1 #include <iostream> 2 #include <string> 3 using namespace std; 4 class Student//声明基类 5 { 6 public: //公用部分 7 Student(int n, string nam ) //基类构造函数 8 { 9 num=n; 10 name=nam; 11 } 12 void display( ) //成员函数,输出基类数据成员 13 { 14 cout<<"num:"<<num<<endl<<"name:"<<name<<endl; 15 } 16 protected: //保护部分 17 int num; 18 string name; 19 }; 20 class Student1: public Student //声明公用派生类Student1 21 { 22 public: 23 Student1(int n, string nam,int n1, string nam1,int a, string ad):Student(n,nam),monitor(n1,nam1)//派生类构造函数,显示调用基类和子对象的构造函数 24 { 25 age=a; 26 addr=ad; 27 } 28 void show( ) 29 { 30 cout<<"This student is:"<<endl; 31 display(); //输出num和name 32 cout<<"age: "<<age<<endl; //输出age 33 cout<<"address: "<<addr<<endl<<endl; //输出addr 34 } 35 void show_monitor( ) //成员函数,输出子对象 36 { 37 cout<<endl<<"Class monitor is:"<<endl; 38 monitor.display( ); //调用基类成员函数 39 } 40 private: //派生类的私有数据 41 Student monitor; //定义子对象(班长) 42 int age; 43 string addr; 44 }; 45 int main( ) 46 { 47 Student1 stud1(10010,"Wang-li",10001,"Li-sun",19,"115 Beijing Road,Shanghai"); 48 stud1.show( ); //输出学生的数据 49 stud1.show_monitor(); //输出子对象的数据 50 return 0; 51 }
派生类构造函数执行的顺序是:
1.调用基类构造函数,对基类数据成员初始化;
2.调用子对象构造函数,对子对象数据成员初始化;(如果有多个对象,其调用的顺序按类定义中对象声明的顺序排定)
3.再执行派生类构造函数本身,对派生类数据成员初始化。
另外,析构函数的执行顺序,在有基类的情况下,也是与构造的顺序严格相反的。
- 拷贝构造与赋值(与构造函数类似)
- 基类对象与派生类对象之间的赋值关系
基类对象和派生类对象之间允许有下述的赋值关系(允许将派生类对象“当作”基类对象来使用):
只赋“共性成员”部分 ,反方向的赋值不被允许
下述赋值不允许:指向派生类对象的指针 =基类对象的地址。
注:访问非基类成员部分时,要经过指针类型的强制转换
下述赋值不允许:派生类的引用 = 基类对象。
注:通过引用只可以访问基类成员部分
- 多继承
多继承(Multiple Inheritance)是指从多个直接基类中产生派生类的能力,多继承的派生类继承了所有父类的成员。尽管概念上非常简单,但是多个基类的相互交织可能会带来错综复杂的设计问题,命名冲突就是不可回避的一个。
多继承时很容易产生命名冲突,即使我们很小心地将所有类中的成员变量和成员函数都命名为不同的名字,命名冲突依然有可能发生,比如典型的是菱形继承,如下图所示:

图1:菱形继承
类 A 派生出类 B 和类 C,类 D 继承自类 B 和类 C,这个时候类 A 中的成员变量和成员函数继承到类 D 中变成了两份,一份来自 A-->B-->D 这条路径,另一份来自 A-->C-->D 这条路径。
在一个派生类中保留间接基类的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,但大多数情况下这是多余的:因为保留多份成员变量不仅占用较多的存储空间,还容易产生命名冲突。假如类 A 有一个成员变量 a,那么在类 D 中直接访问 a 就会产生歧义,编译器不知道它究竟来自 A -->B-->D 这条路径,还是来自 A-->C-->D 这条路径。下面是菱形继承的具体实现:
1 //间接基类A 2 class A{ 3 protected: 4 int m_a; 5 }; 6 //直接基类B 7 class B: public A{ 8 protected: 9 int m_b; 10 }; 11 //直接基类C 12 class C: public A{ 13 protected: 14 int m_c; 15 }; 16 //派生类D 17 class D: public B, public C{ 18 public: 19 void seta(int a){ m_a = a; } //命名冲突 20 void setb(int b){ m_b = b; } //正确 21 void setc(int c){ m_c = c; } //正确 22 void setd(int d){ m_d = d; } //正确 23 private: 24 int m_d; 25 }; 26 int main(){ 27 D d; 28 return 0; 29 }
这段代码实现了上图所示的菱形继承,第 25 行代码试图直接访问成员变量 m_a,结果发生了错误,因为类 B 和类 C 中都有成员变量 m_a(从 A 类继承而来),编译器不知道选用哪一个,所以产生了歧义。
为了消除歧义,我们可以在 m_a 的前面指明它具体来自哪个类:
1 void seta(int a){ B::m_a = a; }
这样表示使用 B 类的 m_a。当然也可以使用 C 类的:
void seta(int a){ C::m_a = a; }
- 虚继承
为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。
在继承方式前面加上 virtual 关键字就是虚继承,请看下面的例子:
1 //间接基类A 2 class A{ 3 protected: 4 int m_a; 5 }; 6 //直接基类B 7 class B: virtual public A{ //虚继承 8 protected: 9 int m_b; 10 }; 11 //直接基类C 12 class C: virtual public A{ //虚继承 13 protected: 14 int m_c; 15 }; 16 //派生类D 17 class D: public B, public C{ 18 public: 19 void seta(int a){ m_a = a; } //正确 20 void setb(int b){ m_b = b; } //正确 21 void setc(int c){ m_c = c; } //正确 22 void setd(int d){ m_d = d; } //正确 23 private: 24 int m_d; 25 }; 26 int main(){ 27 D d; 28 return 0; 29 }
这段代码使用虚继承重新实现了上图所示的菱形继承,这样在派生类 D 中就只保留了一份成员变量 m_a,直接访问就不会再有歧义了。
虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。
虚继承构造函数的执行顺序:
首先执行虚基类的构造函数,多个虚基类的构造函数按照被继承的顺序构造;
执行基类的构造函数,多个基类的构造函数按照被继承的顺序构造;
执行成员对象的构造函数,多个成员对象的构造函数按照声明的顺序构造;
执行派生类自己的构造函数;
析构以与构造相反的顺序执行;
注:
从虚基类直接或间接派生的派生类中的构造函数的成员初始化列表中都要列出对虚基类构造函数的调用,否则,表示使用该虚基类的缺省构造函数。但只有用于建立对象的最派生类的构造函数才调用虚基类的构造函数,此时最派生类的所有基类中列出的对虚基类的构造函数的调用在执行过程中都被忽略,从而保证对虚基类子对象只初始化一次。

浙公网安备 33010602011771号