Skip to content

Commit ab2dfe8

Browse files
committed
Updated to Chapter 9, Section 2
1 parent 4ae761a commit ab2dfe8

22 files changed

+627
-99
lines changed

Structure.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -673,12 +673,14 @@
673673

674674
## 继承中的常见问题
675675

676-
### 基类指针和基类引用
676+
### 动态类型转换 `dynamic_cast`
677677

678678
这一节很重要。
679679

680680
基类指针可以指向派生类对象;基类引用可以引向派生类对象。它们能操作哪些成员。
681681

682+
动态类型转换 `dynamic_cast` 又是怎么回事。
683+
682684
### 虚函数`virtual`
683685

684686
#### 编程示例:不同动物的叫声

generalized_parts/01_welcome_to_cpp/02_data_and_information.tex

+16-6
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,34 @@ \section{数据与信息}
33
早上醒来,看一眼天气预服,发现今天的最低气温是-5\textdegree C,比昨天低了好多,看来要加衣服了;匆忙赶到教室,看了一眼时间,发现刚好是7:59,庆幸自己没有迟到;中午去超市购物,发现面包促销,全场打8折,就买了好多回去;睡前称一下体重,发现比上个月瘦了2斤,心中暗暗窃喜。像这样,温度、时间、价格、体重等等,都是具象的\textbf{数据(Data)}。\par
44
我们还可能遇到抽象的数据。学生的学号、员工的工号、公民身份证号,这些都是“编号”,它们比那些一眼就明白含义的数据要抽象,如果不加解释,我们就很难看懂,只会觉得那是一串无规律的数字。\par
55
有些数据的形式不太常规,比如一个人的姓名,它看上去又像数据,又不像数据——我们怎么能把汉字当成数字一样的东西呢?要解决这个困惑,我们可以给所有汉字进行\textbf{编码(Encoding)}。比如说,``赵钱孙李''分别定为``1, 2, 3, 4'',那么我们就可以把``''记为``1'',把``''记为``3''。像这样把所有汉字都编成表,接下来我们就可以用这张表来把汉字翻译成数字了。\par
6-
\begin{longtable}{|c|c|c|c|c|c|c|c|}
7-
\caption{一个简单的汉字-数字编码表}\\
6+
\begin{table}[htbp]
7+
\centering
8+
\begin{tabular}{cccccccc}
89
\hline
10+
\rule{0pt}{2.4ex}
911
\textbf{汉字} & \textbf{编码} & \textbf{汉字} & \textbf{编码} & \textbf{汉字} & \textbf{编码} & \textbf{汉字} & \textbf{编码}\\
1012
\hline\hline
13+
\rule{0pt}{2.4ex}
1114
赵 & 1 & 钱 & 2 & 孙 & 3 & 李 & 4 \\
1215
\hline
13-
\vdots & \vdots & \vdots & \vdots & \vdots & \vdots & \vdots & \vdots \\
16+
\rule{0pt}{2.4ex}
17+
... & ... & ... & ... & ... & ... & ... & ... \\
1418
\hline
19+
\rule{0pt}{2.4ex}
1520
何 & 21 & 吕 & 22 & 施 & 23 & 张 & 24 \\
1621
\hline
17-
\vdots & \vdots & \vdots & \vdots & \vdots & \vdots & \vdots & \vdots \\
22+
\rule{0pt}{2.4ex}
23+
... & ... & ... & ... & ... & ... & ... & ... \\
1824
\hline
25+
\rule{0pt}{2.4ex}
1926
一 & 569 & 二 & 570 & 三 & 571 & 四 & 572 \\
2027
\hline
21-
\vdots & \vdots & \vdots & \vdots & \vdots & \vdots & \vdots & \vdots \\
28+
\rule{0pt}{2.4ex}
29+
... & ... & ... & ... & ... & ... & ... & ... \\
2230
\hline
23-
\end{longtable}\par
31+
\end{tabular}
32+
\caption{一个简单的汉字-数字编码表}
33+
\end{table}
2434
于是我们根据这个表,可以查出``张三''对应``24-571'',而``李四''对应``4-572''。这就是一个简单的编码过程。\par
2535
同样的道理,我们如果拿到了``24-571'',就可以根据这张表查出``张三'';拿到了``4-572'',就可以根据这张表查出``李四''。这就是一个简单的\textbf{解码(Decoding)}过程。\par
2636
地址也是数据,我们可以对地址进行编码。例如,中国邮政使用六位数字组成的邮政编码来划分邮件投递区域,比如说北京市是``10××××'',上海市是``20××××''。这样就可以用数据来指代我们想要表达的信息了。\par

generalized_parts/09_class_inheritance/01_the_concepts_of_inheritance.tex

+23-2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,27 @@ \subsection*{属与种}
3030
这时你就会发现一个问题:好像我家的哈士奇同时属于``家犬''``哈士奇''哎。其实不只如此,它还属于``狼种(Canis lupus)''``犬属(Canis)''``犬科(Canidae)''``食肉目(Carnivora)''`` 哺乳纲(Mammalia)''``脊索动物门(Chordata)''``动物界(Animalia)''。看上去是不是很复杂?很吓人?\par
3131
问题来了。在我们既有的认知中,一个对象只能是单个类的对象,那么现在它同时是这么多类的对象,这岂不是乱套了?其实并不会,因为这些类之间存在着一种\textbf{``属与种''的关系。用英文可以阐述为``Is-a relationship'',在C++中可以诠释为``基类与派生类''\footnote{值得一提的是,一般我们只用公开继承/受保护继承来表示``Is-a relationship'';至于私有继承,它更适合用来表示``Has-a relationship''。}}\par
3232
``属与种''的关系描述的是两个类的关系,它们有点像数学上的集合间包含关系——如果一个对象是类 \lstinline@A@ 的对象,那么它一定也是类 \lstinline@B@ 的对象。在这个关系当中,\lstinline@A@ 就是基类,而 \lstinline@B@ 是派生类。对于派生类的对象来说,它既有基类的共性——比如我家哈士奇能``汪汪''叫,王五家的泰迪也能,又有派生类的独特性——比如我家哈士奇是拆家小能手,但王五家的泰迪就不能胜任了。\par
33-
\subsection*{继承的优点}
33+
\subsection*{何为继承?}
3434
\textbf{继承(Inheritance)}是一种代码重用的好方法。如果我们已经写了一个类 \lstinline@Dog@,而我们想要再写一个 \lstinline@Husky@ 类,那么我们没必要把 \lstinline@Dog@ 已有的那部分功能再写一遍,直接让 \lstinline@Husky@ 类继承 \lstinline@Dog@ 类就足够了——然后我们只需要集中精力去写诸如``拆家''之类的独特功能即可。\par
35-
继承也使得多态变得可能。我们会在第十章讲到这点。\par
35+
\begin{lstlisting}
36+
struct Dog { //狗类
37+
unsigned age; //所有的狗都有年龄属性
38+
double weight; //所有的狗都有体重属性
39+
//...
40+
};
41+
struct Husky : Dog { //哈士奇类,公开继承自狗类
42+
void destroy() { /*...*/ } //哈士奇独特的拆家本领
43+
};
44+
int main() {
45+
Husky mine {5, 27}; //哈士奇类的对象拥有狗类的成员
46+
std::cout << mine.age << ' '; //输出mine的年龄
47+
std::cout << mine.weight; //输出mine的体重
48+
mine.destroy(); //拆家,开始执行
49+
}
50+
\end{lstlisting}
51+
在这个继承操作当中,\lstinline@Husky@ 类继承自 \lstinline@Dog@ 类。其中的 \lstinline@Husky@ 称为\textbf{派生类(Derived class)},而 \lstinline@Dog@ 称为\textbf{基类(Base class)}。派生类的对象拥有基类的成员,所以 \lstinline@mine@ 可以使用 \lstinline@age@, \lstinline@weight@ 等基类成员,就好像它们定义在了 \lstinline@Husky@ 类当中一样,如图所示。
52+
\begin{figure}[htbp]
53+
\centering
54+
\includegraphics[width=.8\textwidth]{../images/generalized_parts/09_dog_and_husky_relationship_300.png}
55+
\caption{\lstinline@Dog@ 类的成员与 \lstinline@Husky@ 类的成员}
56+
\end{figure}\par
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
% "Multiple inheritance ... was widely supposed to be very difficult to implement efficiently. For example, in a summary of C++ in his book on Objective C, Brad Cox actually claimed that adding multiple inheritance to C++ was impossible. Thus, multiple inheritance seemed more of a challenge. Since I had considered multiple inheritance as early as 1982 and found a simple and efficient implementation technique in 1984, I couldn't resist the challenge. I suspect this to be the only case in which fashion affected the sequence of events." -Bjarne Stroustrup
Loading
Loading
Loading

0 commit comments

Comments
 (0)