|
| 1 | +\section{公开继承与受保护成员} |
| 2 | +\subsection*{基本语法} |
| 3 | +继承的基本语法就是在定义\footnote{虽然在声明类时偶尔也可以这样做,但是我不推荐如此,因为在进行继承时编译器必须已经知道对应基类的定义。}一个类时,用冒号加``继承方式''和``基类名''的形式来表明它按何种方式继承自哪个类。 |
| 4 | +\begin{lstlisting} |
| 5 | +class Base { |
| 6 | + //... |
| 7 | +}; //Base类定义已毕 |
| 8 | +class Derived : public Base { //按public方式继承自Base类 |
| 9 | + //... |
| 10 | +}; //Derived公开继承自Base |
| 11 | +\end{lstlisting}\par |
| 12 | +其中的继承方式分为 \lstinline@public@, \lstinline@protected@ 和 \lstinline@private@ 三种,在本节中我们先讲 \lstinline@public@,即公有继承。\par |
| 13 | +\subsection*{继承方式与访问权限} |
| 14 | +我们已经很熟悉``访问权限''这个概念了。访问权限关乎一个成员是否对外界可见: |
| 15 | +\begin{itemize} |
| 16 | + \item \lstinline@public@ 成员对外完全可见。 |
| 17 | + \item \lstinline@private@ 成员对外完全不可见。 |
| 18 | + \item \lstinline@protected@ 成员比较特殊,我们稍后讲到。这些成员对外界不可见,但对该类的派生类可见。 |
| 19 | +\end{itemize} |
| 20 | +我们说,一个派生类对象具有基类的成员,比如 \lstinline@Husky@ 就有 \lstinline@age@ 和 \lstinline@weight@。那么在派生类中,这些基类的成员又是何种访问权限呢?这是由``基类成员访问权限''和``继承方式''二者共同决定的,见表9.1。\par |
| 21 | +\begin{table}[htbp] |
| 22 | +\centering |
| 23 | +\begin{tabular}{cccccc} |
| 24 | +\hline |
| 25 | +\rule{0pt}{2.4ex} |
| 26 | +\multirow{2}{*}{继承方式} & 基类成员权限 & \multirow{2}{*}{\lstinline@public@} & \multirow{2}{*}{\lstinline@protected@} & \multirow{2}{*}{\lstinline@private@} \\ |
| 27 | +\cline{2-2} |
| 28 | +\rule{0pt}{2.4ex} |
| 29 | +& 派生类成员权限 & & & \\ |
| 30 | +\hline |
| 31 | +\hline |
| 32 | +\rule{0pt}{2.4ex} |
| 33 | +\lstinline@public@ & & \lstinline@public@ & \lstinline@protected@ & 不可见\\ |
| 34 | +\hline |
| 35 | +\rule{0pt}{2.4ex} |
| 36 | +\lstinline@protected@ & & \lstinline@protected@ & \lstinline@protected@ & 不可见\\ |
| 37 | +\hline |
| 38 | +\rule{0pt}{2.4ex} |
| 39 | +\lstinline@private@ & & \lstinline@private@ & \lstinline@private@ & 不可见\\ |
| 40 | +\hline |
| 41 | +\end{tabular} |
| 42 | +\caption{派生类的成员访问权限取决于基类成员访问权限和继承方式} |
| 43 | +\end{table} |
| 44 | +其中的 \lstinline@protected@ 成员(受保护成员)访问权限对于读者来说是个全新的概念,下面我就来解释一下它的作用和优缺点。\par |
| 45 | +\subsection*{\texttt{protected}成员} |
| 46 | +我们谈过,\lstinline@private@ 成员的优点在于它是封闭的,只对类内可见。但对于继承来说,它也可能是个麻烦。\par |
| 47 | +举个例子,\lstinline@Husky@ 类中的 \lstinline@destory@ 成员函数需要用到 \lstinline@weight@ 这个成员变量。如果我们像上一节中那样在 \lstinline@Dog@ 类中把 \lstinline@weight@ 定义为 \lstinline@public@\footnote{我们此前提过,\lstinline@struct@ 成员的默认访问权限均为 \lstinline@public@;\lstinline@struct@ 类的默认继承方式也是 \lstinline@public@。所以 \lstinline@Dog@ 类中的成员都是公有成员,而 \lstinline@Husky@ 类以公开方式继承 \lstinline@Dog@ 类。},那么无论 \lstinline@Husky@ 类还是其它无关的外界类/函数都有了修改 \lstinline@weight@ 的权限,这是很危险的;然而,如果我们把 \lstinline@weight@ 定义成 \lstinline@private@,那么无论 \lstinline@Husky@ 还是其它外界类/函数都不能访问 \lstinline@weight@\footnote{一种观点认为,\lstinline@private@ 成员是``不可继承''的,换句话说,基类的私有成员并不是派生类的成员。这种看法是合理的,因为一个类的成员总该对这个类可见;如果对这个类都不可见,那么它也算不上是这个类的成员。不过笔者在这里顾及理解上方便,还是选择这样讲。}。\par |
| 48 | +而 \lstinline@protected@ 则能很好地解决这个问题。基类的 \lstinline@protected@ 成员对于外界来说是不可见的,但它对于这个类的派生类来说则是可见的,如图9.2所示。\par |
| 49 | +\begin{figure}[htbp] |
| 50 | + \centering |
| 51 | + \includegraphics[width=\textwidth]{../images/generalized_parts/09_protected_members_300.png} |
| 52 | + \caption{\lstinline@protected@ 成员对外的可见性} |
| 53 | +\end{figure} |
| 54 | +有了 \lstinline@protected@ 之后,我们可以这样写: |
| 55 | +\begin{lstlisting} |
| 56 | +class Dog { |
| 57 | +protected: //受保护成员,对外不可见,但对Dog的派生类可见 |
| 58 | + unsigned _age; //所有狗都有年龄属性 |
| 59 | + double _weight; //所有狗都有体重属性 |
| 60 | +}; |
| 61 | +class Husky : public Dog { //公有方式继承自Dog |
| 62 | + //这个类拥有成员age和weight,成员权限为protected |
| 63 | +public: //公有成员,对外界可见 |
| 64 | + void destroy() { /*...*/ }; //哈士奇独特的拆家本领 |
| 65 | +}; |
| 66 | +\end{lstlisting}\par |
| 67 | +这样一来,\lstinline@_age@ 和 \lstinline@_weight@ 就对 \lstinline@Husky@ 类可见,使 \lstinline@Husky@ 类的函数可以方便地访问这些成员;同时它们又对无关的其它类不可见,保证了成员不受篡改的安全性。\par |
| 68 | +\subsection*{构造与初始化} |
| 69 | +派生类拥有它基类的成员,这个关系很像是,一个派生类对象当中内嵌了一个基类对象。 |
| 70 | +\begin{figure}[htbp] |
| 71 | + \centering |
| 72 | + \includegraphics[width=.8\textwidth]{images/generalized_parts/09_built_in_base_class_object_in_derived_class_300.png} |
| 73 | + \caption{一个派生类对象当中内嵌了一个基类对象} |
| 74 | +\end{figure} |
| 75 | +所以当我们需要对基类对象进行初始化时,也不是按照``成员''进行初始化,而是把基类的对象当作一个整体,调用基类的构造函数进行初始化。\par |
| 76 | +\begin{lstlisting} |
| 77 | +class Dog { |
| 78 | +protected: //受保护成员,对外不可见,但对Dog的派生类可见 |
| 79 | + Dog(unsigned age, double weight) : _age {age}, _weight {weight} {} |
| 80 | + //如果把Dog的构造函数定义成protected权限,那么只有其派生类才能正常调用它 |
| 81 | + unsigned _age; //所有狗都有年龄属性 |
| 82 | + double _weight; //所有狗都有体重属性 |
| 83 | +}; |
| 84 | +class Husky : public Dog { //哈士奇类,公有方式继承自Dog |
| 85 | + //这个类拥有成员age和weight,成员权限为protected |
| 86 | +private: |
| 87 | + int _destroy_ability; //拆家能力 |
| 88 | +public: |
| 89 | + Husky(unsigned age, double weight, int ability) |
| 90 | + : Dog {age, weight}, _destroy_ability {ability} |
| 91 | + {} //初始化内嵌Dog对象的成员,以及_destroy_ability成员 |
| 92 | + void destroy() { /*...*/ }; //哈士奇独特的拆家本领 |
| 93 | +}; |
| 94 | +class Retriever : public Dog { //金毛寻回犬类,公有方式继承自Dog |
| 95 | + //这个类拥有成员age和weight,成员权限为protected |
| 96 | +private: |
| 97 | + int _guide_ability; //导盲能力 |
| 98 | +public: |
| 99 | + Retriever(unsigned age, double weight, int ability) |
| 100 | + : Dog {age, weight}, _guide_ability {ability} |
| 101 | + {} //初始化内嵌Dog对象的成员,以及_guide_ability成员 |
| 102 | + void guide() { /*...*/ } //金毛独特的导盲本领 |
| 103 | +}; |
| 104 | +\end{lstlisting}\par |
| 105 | +在这里,我们把 \lstinline@Dog@ 类的构造函数定义成受保护成员。按前文所述,这些成员对派生类以外的外界都是不可见的,所以我们不能在外部使用这个函数来进行构造\footnote{其实这个类还有一个作为公有成员的默认拷贝构造函数,所以确实还存在这样的方法使我们能够在外部定义对象。}。换言之,我们这样就保证了:在外界中不能直接定义 \lstinline@Dog@ 类对象,只能定义它的派生类的对象\footnote{不过,从另一个意义上讲,派生类的对象也是基类的对象。}。\par |
| 106 | +两个派生类 \lstinline@Husky@ 和 \lstinline@Retriever@ 对象的构造函数都是公有的,我们就可以在类外调用了。它们都接收三个参数,分别是 \lstinline@age@, \lstinline@weight@, \lstinline@ability@。所以我们可以这样定义对象: |
| 107 | +\begin{lstlisting} |
| 108 | +int main() { |
| 109 | + Husky mine {5, 27, 1000000}; |
| 110 | + Retriever zhang3 {4, 25.67, 99}; |
| 111 | +} |
| 112 | +\end{lstlisting} |
| 113 | +在创建 \lstinline@mine@ 对象时,程序将调用 \lstinline@Husky::Husky(unsigned,double,int)@ 函数,那么这个函数做了什么呢? |
| 114 | +\begin{lstlisting} |
| 115 | +public: |
| 116 | + Husky(unsigned age, double weight, int ability) |
| 117 | + : Dog {age, weight}, _destroy_ability {ability} |
| 118 | + {} |
| 119 | +\end{lstlisting}\par |
| 120 | +我们看到,这个函数在初始化时,其 \lstinline@Dog@ 成员部分是通过调用 \lstinline@Dog@ 类的构造函数来集中解决的;而独属于 \lstinline@Husky@ 的那部分,才是单独处理的。这一点对三种继承方式来说都是通用的——来自基类的成员,需要通过基类的构造函数来进行初始化。即便我们在代码中没有这么写,基类的构造函数也是会被调用的。我们可以用这段代码来验证之: |
| 121 | +\begin{lstlisting} |
| 122 | +struct Base { //struct成员默认为public成员,这里图方便就用struct了 |
| 123 | + Base() { //Base的默认构造函数 |
| 124 | + std::cout << "Base::Base() is called." << std::endl; |
| 125 | + } |
| 126 | +}; |
| 127 | +struct Derived : Base { //struct类的默认继承方式也是公开继承 |
| 128 | + Derived() { //在Derived构造函数当中并没有写明要调用Base的构造函数 |
| 129 | + std::cout << "Derived::Derived() is called." << std::endl; |
| 130 | + } |
| 131 | +}; |
| 132 | +int main() { |
| 133 | + Derived de; //定义Derived类的对象,预期会调用Derived类的构造函数 |
| 134 | +} |
| 135 | +\end{lstlisting} |
| 136 | +这段代码的运行结果如下:\\\noindent\rule{\linewidth}{.2pt}\texttt{ |
| 137 | +Base::Base() is called.\\ |
| 138 | +Derived::Derived() is called. |
| 139 | +}\\\noindent\rule{\linewidth}{.2pt} |
| 140 | +这段代码能体现出两条信息:其一,在调用派生类的构造函数时,基类的构造函数也会被自动调用;其二,基类构造函数体的执行要早于派生类的构造函数体\footnote{注意,这是函数体的执行顺序!这个实验并没有验证初值列的顺序。}。\par |
| 141 | +\subsection*{析构} |
| 142 | +不只是构造函数,派生类的析构函数在调用时,也会调用基类的析构函数。我们同样根据一个例子来看它的效果。 |
| 143 | +\begin{lstlisting} |
| 144 | +struct Base { |
| 145 | + ~Base() { //Base的析构函数 |
| 146 | + std::cout << "Base::~Base() is called." << std::endl; |
| 147 | + } |
| 148 | +}; |
| 149 | +struct Derived : Base { |
| 150 | + ~Derived() { //Derived的析构函数 |
| 151 | + std::cout << "Derived::~Derived() is called." << std::endl; |
| 152 | + } |
| 153 | +}; |
| 154 | +int main() { |
| 155 | + Derived de; |
| 156 | +} |
| 157 | +\end{lstlisting} |
| 158 | +这段代码的运行结果如下:\\\noindent\rule{\linewidth}{.2pt}\texttt{ |
| 159 | +Derived::~Derived() is called.\\ |
| 160 | +Base::~Base() is called. |
| 161 | +}\\\noindent\rule{\linewidth}{.2pt} |
| 162 | +我们发现,对象销毁时析构函数的调用顺序,与对象创建时构造函数的调用顺序,刚好是相反的。在析构的时候,派生类的析构函数先调用,然后才是基类的析构函数。\par |
| 163 | +也正因为,派生类的析构函数总是要调用基类的析构函数,所以我们根本不需要在写派生类时还要为了``基类成员的内存泄漏''而提心吊胆(只要你写的基类没有这个问题)。 |
| 164 | +\begin{lstlisting} |
| 165 | +class stack : std::vector<int> { //class的默认继承方式为private |
| 166 | +public: |
| 167 | + //... |
| 168 | + ~stack() {} //无需为std::vector<int>的动态内存而担扰!自有它的析构函数来回收 |
| 169 | +}; |
| 170 | +\end{lstlisting}\par |
0 commit comments