大纲 继承概念 面向对象程序设计有 4 个主要特点:抽象、封装、继承和多态。面向对象程序设计的两个重要特征一数据抽象与封装,两者已经能够设计出基于对象的程序,这是面向对象程序设计的基础。要较好地进行面向对象程序设计,还必须了解面向对象程序设计另外两个重要特征 —— 继承和多态。继承是面向对象程序设计最重要的特征,可以说,如果没有掌握继承,就等于没有掌握类和对象的精华,就是没有掌握面向对象程序设计的真谛。
类之间的关系 类之间一般有三种关系:
has-A
:包含关系,用以描述一个类由多个 “部件类” 构成。实现 has-A
关系可以用类成员表示,即一个类中的数据成员是另一种已经定义的类。uses-A
:一个类部分地使用另一个类。类之间成员函数的联系,可以通过定义友元或者对象参数传递来实现。is-A
:机制称为 “继承” 。关系具有传递性,不具有对称性。继承关系举例
继承相关概念
派生类的定义
值得一提的是,C++ 中的继承方式(public、private、protected)会影响子类的对外访问属性。
继承重要说明 (a) 子类拥有父类的所有成员变量和成员函数 (b) 子类可以拥有父类没有的方法和属性 (c) 子类就是一种特殊的父类 (d) 子类对象可以当作父类对象使用 继承使用案例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #include <iostream> using namespace std;class Parent {public : Parent (int a = 0 , int b = 0 ) { this ->a = a; this ->b = b; } void print () { cout << "a=" << this ->a << ", b=" << this ->b << endl; } public : int a; int b; }; class Child : public Parent {public : Child (int a = 0 , int b = 0 , int c = 0 ) { this ->a = a; this ->b = b; this ->c = c; } void echo () { cout << "a=" << this ->a << ", b=" << this ->b << ", c=" << this ->c << endl; } private : int c; }; int main () { Child child (1 , 2 , 3 ) ; child.print (); child.echo (); return 0 ; }
程序运行的输出结果如下:
派生类的访问控制 派生类(子类)继承了基类(父类)的全部成员变量和成员函数(除了所有构造函数和析构函数之外的成员函数),但是这些成员的访问属性,在派生过程中是可以调整的。
单个类的访问控制 在 C++ 中,类成员变量和类成员函数的访问级别分为:public
、private
、protected
private
:修饰的成员变量和成员函数,只能在类的内部被访问public
:修饰的成员变量和成员函数,可以在类的内部和类的外部被访问protected
:修饰的成员变量和成员函数,可以在派生类(子类)的内部访问,不能在派生类(子类)的外部被访问特别注意
在类中没有声明访问控制级别的成员变量和成员函数,默认都是 private
访问级别的。
继承成员的访问控制
在 C++ 中,不同的继承方式(public
、private
、protected
)会改变继承成员的访问属性:
public 继承
:父类成员在子类中保持原有的访问级别private 继承
:父类成员在子类中都变为 private
成员protected 继承
:父类中 public
成员会变成 protected
,父类中 private
成员仍然为 private
,父类中 protected
成员仍然为 protected
特别注意
父类的 private
成员在子类中依然存在,但是无法访问到的,即无论使用哪种方式继承父类,子类都不能直接使用父类的 private
成员。
继承成员访问控制的 “三看” 原则 在 C++ 中,不同的继承方式(public
、private
、protected
)会改变继承成员的访问属性,最终可总结为以下三个原则(通过某一个原则,判断是否可以被访问):
(a) 看调用语句是写在子类的内部还是外部 (b) 看子类如何从父类继承(public
、private
、protected
) (c) 看父类中的访问级别(public
、private
、protected
) 派生类成员访问级别的控制原则 对于派生类自身的成员,访问级别的控制原则如下:
(a) 需要被外界访问的成员直接设置为 public
(b) 只能在当前类中访问的成员设置为 private
(c) 只能在当前类和子类中访问的成员设置为 protected
使用工具查看类的内存布局信息 Windows 系统环境 在 Visual Studio 开发人员命令提示窗口内,可以使用以下命令查看类的内存布局信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 class A {public : int m_index; }; class B : public A {public : int m_age; };
使用命令查看类的内存布局信息,其中 YYY
是类的名称,xxx
是 C++ 源文件的名称 1 cl /d1 reportSingleClassLayoutYYY xxx.cpp
Linux 系统环境 在 Linux 系统中,可以使用 GNU Compiler Collection(GCC)提供的一些工具来查看类的内存布局。其中一个最常用的工具是 g++
,它是 GCC 的 C++ 编译器。可以使用 g++
编译器的 -fdump-class-hierarchy
选项来生成类的内存布局信息,如下所示:
1 g++ -fdump-class -hierarchy xxx .cpp
这将生成一个名为 xxx.cpp.class
的文件,其中包含了类的内存布局信息,可以打开这个文件查看生成的信息。另外,还可以使用 objdump
命令来反汇编一个已编译的程序,然后查看类的内存布局。这种方法更加复杂,但在某些情况下也很有用。
继承中的构造和析构 类型兼容原则 类型兼容规则是指在需要基类(父类)对象的任何地方,都可以使用公有派生类(公有继承) 的对象来替代。通过公有继承,派生类得到了基类中除构造函数、析构函数之外的所有成员。这样,公有派生类实际就具备了基类的所有功能,凡是基类能解决的问题,公有派生类都可以解决。值得一提的是,在替代之后,派生类对象就可以作为基类的对象使用,但是只能使用从基类继承得到的成员,类型兼容规则是多态的重要基础之一。类型兼容规则中所指的替代包括以下情况:
子类对象可以当作父类对象使用 子类对象可以直接赋值给父类对象 子类对象可以直接初始化父类对象 父类指针可以直接指向子类对象 父类引用可以直接引用子类对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 #include <iostream> using namespace std;class Parent {public : void printParent () { cout << "I'm parent" << endl; } private : int a; }; class Child : public Parent {public : void printChild () { cout << "I'm child" << endl; } private : int c; }; void howToPrint (Parent* p) { p->printParent (); } void howToPrint (Parent& p) { p.printParent (); } int main () { Parent p1; p1.printParent (); Child c1; c1.printChild (); c1.printParent (); cout << "1-1" << endl; Parent* p2 = NULL ; p2 = &c1; p2->printParent (); cout << "1-2" << endl; howToPrint (&p1); howToPrint (&c1); cout << "2-1" << endl; Parent& p3 = c1; p3.printParent (); cout << "2-2" << endl; howToPrint (p1); howToPrint (c1); cout << "3-1" << endl; Parent p4 = c1; p4.printParent (); cout << "4-1" << endl; Parent p5; p5 = c1; p5.printParent (); return 0 ; }
程序运行输出的结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 I'm parent I'm child I'm parent 1-1 I'm parent 1-2 I'm parent I'm parent 2-1 I'm parent 2-2 I'm parent I'm parent 3-1 I'm parent 4-1 I'm parent
继承中的对象模型 类在 C++ 编译器的内部可以理解为结构体,子类是由父类成员叠加子类新成员得到的。
父类与子类的构造函数、析构函数的关系如下:
在子类对象构造时,需要调用父类构造函数对其继承得来的成员进行初始化 在子类对象析构时,需要调用父类析构函数对其继承得来的成员进行清理 继承中的构造与析构的调用原则 (a) 子类对象在创建时,会首先调用父类的构造函数 (b) 父类构造函数执行结束后,再执行子类的构造函数 (c) 当父类只存在有参构造函数时,必须在子类的初始化列表中显示调用父类的构造函数 (d) 析构函数调用的先后顺序与构造函数相反,即先调用子类的析构函数,再调用父类的析构函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 #include <iostream> using namespace std;class Parent {public : Parent (int a, int b) { this ->a = a; this ->b = b; cout << "父类的构造函数被调用" << endl; } ~Parent () { cout << "父类的析构函数被调用" << endl; } void printParent () { cout << "I'm parent, a = " << this ->a << ", b = " << this ->b << endl; } private : int a; int b; }; class Child : public Parent {public : Child (int a, int b, int c) : Parent (a, b) { this ->c = c; cout << "子类的构造函数被调用" << endl; } ~Child () { cout << "子类的析构函数被调用" << endl; } void printChild () { cout << "I'm child, c = " << this ->c << endl; } private : int c; }; int main () { Child c1 (1 , 2 , 3 ) ; c1.printParent (); c1.printChild (); return 0 ; }
程序运行的输出结果如下:
1 2 3 4 5 6 父类的构造函数被调用 子类的构造函数被调用 I'm parent, a = 1, b = 2 I'm child, c = 3 子类的析构函数被调用 父类的析构函数被调用
继承与组合混搭情况下的构造与析构 继承与组合对象混搭使用的情况下,构造函数与析构函数的调用原则如下:
构造函数的调用顺序
:先构造父类,再构造成员变量,最后构造自身析构函数的调用顺序
:先析构自身,再析构成员变量,最后析构父类1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 #include <iostream> using namespace std;class Object {public : Object (int a, int b) { this ->a = a; this ->b = b; cout << "Object类的构造函数被调用" << endl; } ~Object () { cout << "Object类的析构函数被调用" << endl; } void printObject () { cout << "I'm object, a = " << this ->a << ", b = " << this ->b << endl; } protected : int a; int b; }; class Parent : public Object {public : Parent (char * p) : Object (1 , 2 ) { this ->p = p; cout << "Parent类的构造函数被调用" << endl; } ~Parent () { cout << "Parent类的析构函数被调用" << endl; } void printParent () { cout << "I'm parent, p = " << p << endl; } protected : char * p; }; class Child : public Parent {public : Child (char * c) : obj1 (3 , 4 ), obj2 (5 , 6 ), Parent (c) { this ->c = c; cout << "Child类的构造函数被调用" << endl; } ~Child () { cout << "Child类的析构函数被调用" << endl; } void printChild () { cout << "I'm child, p = " << p << endl; } protected : char * c; Object obj1; Object obj2; }; int main () { char * str = new char [3 ]; str[0 ] = 'J' ; str[1 ] = 'i' ; str[2 ] = 'm' ; Child c1 (str) ; c1.printChild (); c1.printParent (); c1.printObject (); return 0 ; }
程序运行的输出结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 Object类的构造函数被调用 Parent类的构造函数被调用 Object类的构造函数被调用 Object类的构造函数被调用 Child类的构造函数被调用 I'm child, p = Jim I'm parent, p = Jim I'm object, a = 1, b = 2 Child类的析构函数被调用 Object类的析构函数被调用 Object类的析构函数被调用 Parent类的析构函数被调用 Object类的析构函数被调用
继承中的同名成员的处理方式 当子类成员与父类成员同名时,子类不会覆盖父类的同名成员,子类依旧可以从父类继承同名成员。 在子类中通过作用域限定运算符 ::
进行同名成员的区分(即在子类中使用父类的同名成员时,需要显式地使用类名限定符),其作用类似 Java 中的 super
关键字。 如果子类和父类的成员函数同名,子类会把父类的所有的同名成员函数都隐藏掉,子类必须通过作用域限定运算符 ::
显式调用父类的同名成员函数。 特别注意,同名成员存储在内存中的不同位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 #include <iostream> using namespace std;class Parent {public : Parent (int a, int b) { this ->a = a; this ->b = b; } void print () { cout << "I'm parent, a = " << a << ", b = " << b << endl; } public : int a; int b; }; class Child : public Parent {public : Child (int a, int b) : Parent (a, b) { this ->a = a + 5 ; this ->b = b + 5 ; } void print () { cout << "I'm child, a = " << a << ", b = " << b << endl; } public : int a; int b; }; int main () { Child child (1 , 2 ) ; child.print (); cout << "child's a = " << child.a << endl; cout << "child's b = " << child.b << endl; child.Parent::print (); cout << "parent's a = " << child.Parent::a << endl; cout << "parent's b = " << child.Parent::b << endl; return 0 ; }
程序运行的输出结果如下:
1 2 3 4 5 6 I'm child, a = 6, b = 7 child's a = 6 child's b = 7 I'm parent, a = 1, b = 2 parent's a = 1 parent's b = 2
派生类中的 static 关键字使用 在 C++ 的普通类中,static
关键字的使用可以看 这里 ,而派生类中 static
关键字的使用说明如下:
基类定义的静态成员,将被所有派生类共享 根据静态成员自身的访问特性和派生类的继承方式,在类层次体系中具有不同的访问性质(遵守派生类成员访问级别控制的原则) 在派生类中访问基类的静态成员,需要显式说明,对应的语法是:类名 :: 成员
或者通过对象访问:对象名 . 成员
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 #include <iostream> using namespace std;class Parent {public : static void print () { cout << "a = " << a << ", b = " << b << endl; } public : static int a; private : static int b; }; int Parent::b = 50 ;int Parent::a = 30 ;class Child : public Parent {public : int getA () { return this ->a; } int getA2 () { return Parent::a; } int getB () { return 0 ; } void print2 () { this ->print (); } void print1 () { Parent::print (); } }; int main () { Parent::a++; Parent::print (); cout << endl; cout << "a = " << Child::a << endl; Child::print (); cout << endl; Child c1; cout << "a = " << c1.getA () << endl; cout << "a = " << c1.getA2 () << endl; cout << "a = " << c1.Parent::a << endl; cout << endl; c1.print1 (); c1.print2 (); c1.Parent::print (); return 0 ; }
程序运行的输出结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 a = 31, b = 50 a = 31 a = 31, b = 50 a = 31 a = 31 a = 31 a = 31, b = 50 a = 31, b = 50 a = 31, b = 50