Skip to content

Commit cfeff56

Browse files
committed
Updated to Chapter 9, Section 1
1 parent a862cb2 commit cfeff56

18 files changed

+583
-112
lines changed

Structure.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -669,16 +669,24 @@
669669

670670
### 继承中的类型转换
671671

672-
这一节很重要。
672+
#### 对象之间的类型转换
673673

674-
向上类型转换和向下类型转换如何实现。
674+
#### 指针/引用之间的类型转换
675675

676-
动态类型转换 `dynamic_cast` 又是怎么回事
676+
向上类型转换和向下类型转换如何实现,为什么 `static_cast` 在向下类型转换时不好用了
677677

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

680680
### 虚函数`virtual`与多态
681681

682+
#### 为何要向下类型转换?
683+
684+
只剩下一种可能性:为了使用同名而不同功能的成员函数。
685+
686+
#### 动态类型转换
687+
688+
`dynamic_cast` 需要依赖多态的基类,我们要靠虚函数来实现。
689+
682690
#### 编程示例:不同动物的叫声
683691

684692
`animal`类继承出若干派生类`dog`, `cat`等,用同一个函数`roar`来表示叫声,对不同的派生类有不同的叫声,比如`"wolf"`, `"meow"`等。

generalized_parts/09_class_inheritance/03_private_inheritance.tex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ \subsection*{构造与初始化}
9797
const char* p()const { return _p; } //当Arr的对象是常量时调用
9898
};
9999
\end{lstlisting}
100-
但是这里存在另一个问题:C++17标准并没有保证不同访问权限的成员间的初始化顺序——换句话说,这样会把初始化变成一个未定义行为。为了预防这种情况的发生,我们还是要把 \lstinline@_arr@ 和 \lstinline@_p@ 都放在同一访问权限之下。\par
100+
但是这里存在另一个问题:C++17标准并没有保证不同访问权限的成员间的初始化顺序\footnote{这个问题貌似在C++23标准中得到了解决。}——换句话说,这样会把初始化变成一个未定义行为。为了预防这种情况的发生,我们还是要把 \lstinline@_arr@ 和 \lstinline@_p@ 都放在同一访问权限之下。\par
101101
然而,如果把它们都放在 \lstinline@private@ 访问权限下,那么 \lstinline@_cap@ 就不能被派生类使用,这不符合我们的初衷;如果把它们都放在 \lstinline@protected@ 访问权限下,那么 \lstinline@_p@ 就可以被 \lstinline@string@ 类直接接触,起不到隔离的作用。为此,我们还需要更加取巧的方法才行,那就是——引用。
102102
\begin{lstlisting}
103103
class Arr {
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
\section{继承中的类型转换}
2+
无论对于何种继承方式,我们都可以作这样的理解:一个派生类对象当中内嵌了一个基类对象。这个基类对象只拥有基类的成员;而派生类的对象拥有基类成员在内的所有成员。在上一章的代码中,我们也反复看到了派生类对象到基类对象的隐式类型转换。那么,这个转换在什么条件下是可以的,什么条件下是禁止的?基类对象能转换为派生类对象吗?对于指针和引用来说,情况是否又会有些不同呢?这些问题的答案就在本节当中。\par
3+
\subsection*{对象的类型转换}
4+
在继承时,如果我们没有人为提供转换构造函数或者自定义转换函数,那么编译器就会生成一个隐式的,从派生类到基类的类型转换函数。这个类型转换函数允许派生类的对象通过 \lstinline@static_cast@ 转换为基类的对象。
5+
\begin{lstlisting}
6+
struct Base { /*...*/ };
7+
struct Derived : Base { /*...*/ }; //内置了一个Derived到Base类型的转换函数
8+
\end{lstlisting}\par
9+
但是,这个转换函数的可见性是取决于继承方式的:
10+
\begin{itemize}
11+
\item 如果 \lstinline@Derived@ 类公开继承自 \lstinline@Base@ 类,那么这个转换函数相当于是 \lstinline@Derived@ 的公有成员。
12+
\item 如果 \lstinline@Derived@ 类受保护继承自 \lstinline@Base@ 类,那么这个转换函数相当于是 \lstinline@Derived@ 的受保护成员。
13+
\item 如果 \lstinline@Derived@ 类受私有继承自 \lstinline@Base@ 类,那么这个转换函数相当于是 \lstinline@Derived@ 的私有成员。
14+
\end{itemize}\par
15+
对于上一章中的 \lstinline@Husky@ 类的对象来说,这个类型转换函数是 \lstinline@Husky@ 类的公有成员,所以我们可以在任何一个地方把 \lstinline@Husky@ 类的对象转换成 \lstinline@Dog@ 类。\par
16+
而上一章中的 \lstinline@stack@ 则不同,我们只能在 \lstinline@stack@ 类当中进行类型转换;而在类之外,这个转换函数不可见,我们不能进行 \lstinline@stack@ 到 \lstinline@std::vector<int>@ 的类型转换。\par
17+
那么,基类对象能否隐式类型转换为派生类对象呢?答案是否定的。原因也很简单:无论何种继承方式,派生类对象所需要的信息量都比基类对象更多\footnote{至少是不少于。}。也就是说,如果要把派生类对象转换为基类对象,我们不需要提供额外信息,单纯把内嵌于其中的基类对象挖出来就足够了;但如果把基类对象转换为派生类对象,我们就需要提供额外的信息,否则编译器就不知道要如何补全缺失数据。\par
18+
所以,如果我们希望允许基类到派生类之间的类型转换,就必须自行设计转换构造函数或者自定义转换函数。还是以我们写的 \lstinline@stack@ 为例,我们就在 \lstinline@stack@ 类中定义了一个 \lstinline@explicit@ 的转换构造函数。
19+
\begin{lstlisting}
20+
public:
21+
explicit stack(const std::vector<int>&); //接收一个std::vector<int>对象
22+
\end{lstlisting}
23+
这样一来我们就可以用显式类型转换把基类 \lstinline@std::vector<int>@ 对象转换成派生类 \lstinline@stack@ 对象了。\par
24+
\subsection*{指针/引用的向上类型转换}
25+
对于指针/引用类型来说,情况会稍显复杂。\par
26+
我们还是来想一下内存吧。对于一个派生类的对象来说,在它的内存空间中,既有属于基类的成员对象,又有属于派生类的成员对象。比如下面这段代码,作为基类成员的 \lstinline@Base::a@ 在 \lstinline@Derive@ 对象的内存空间之中拥有自己的一席之地。
27+
\begin{lstlisting}
28+
class Base {
29+
int a;
30+
};
31+
class Derived : public Base{
32+
int b;
33+
};
34+
int main() {
35+
std::cout << sizeof(Base) << ' ' << sizeof(Derived);
36+
}
37+
\end{lstlisting}
38+
这段代码的运行结果如下:\\\noindent\rule{\linewidth}{.2pt}\texttt{
39+
4 8
40+
}\\\noindent\rule{\linewidth}{.2pt}\par
41+
你瞧,\lstinline@Derived@ 所占用的内存空间大小为8字节,这说明它包含了 \lstinline@Base::a@ 和 \lstinline@Derived::b@ 两个成员。那么当我们对一个 \lstinline@Derived*@ 指针取内容时,程序将会取该字节起的8个字节作为一个 \lstinline@Derived@ 对象。\par
42+
那么假如我把它转换成 \lstinline@Base*@ 指针然后取内容呢?程序只会取该字节起的4个字节作为一个 \lstinline@Base@ 对象。\par
43+
所以我们不难发现,因为 \lstinline@Base@ 对象是内嵌在 \lstinline@Derived@ 对象当中的,我们就可以用 \lstinline@Base*@ 指针来取 \lstinline@Derived@ 对象中的基类部分,如图10.1所示。
44+
\begin{figure}[htbp]
45+
\centering
46+
\includegraphics[width=.7\textwidth]{../images/generalized_parts/10_derived_class_object_and_subobject_in_memory_300.png}
47+
\caption{用基类指针指向内嵌于派生类中的基类对象}
48+
\end{figure}
49+
所以对于公有继承来说,我们可以把派生类的指针隐式类型转换为基类的指针。
50+
\begin{lstlisting}
51+
Derived d {/*...*/};
52+
Base *b = &d; //&d被隐式类型转换为Base*类型,并赋值给b
53+
\end{lstlisting}\par
54+
至于私有继承和受保护继承,它们的规律和对象之间的类型转换很相似——受保护继承的派生类指针只有在派生类及``派生类的派生类''中可以转换为基类指针;私有继承的派生类指针只能在这个类中转换为基类指针。\par
55+
无论继承权限如何,我们都把这种\textbf{派生类指针到基类指针的类型转换称为向上类型转换(Upcasting)}\footnote{我们在习惯上会把继承关系图中的基类画在上方,派生类画在下方,因此得名。}。向上类型转换总是安全的,因为基类指针访问的范围总是不大于派生类指针,所以就不会发生访问内存``越界''这样的问题。\par
56+
引用也可以进行类型转换,道理相同:派生类的对象或者引用类型可以向上类型转换成基类的引用。所以如果我们要写一个对所有狗类都通用的函数,我们就不必为 \lstinline@Husky@, \lstinline@Retriever@ 等类型写一大堆重载,只需要写 \lstinline@Dog@ 一个类就行了。
57+
\begin{lstlisting}
58+
void fun(const Dog &dog) { //可以接收Dog类及其public派生类的对象
59+
//...
60+
}
61+
int main() {
62+
Husky mine {/*...*/};
63+
fun(mine); //Husky对象到Dog引用的向上类型转换
64+
}
65+
\end{lstlisting}\par
66+
这里我们看到的都是指针/引用的隐式类型转换。如果要做显式类型转换的话,我们用 \lstinline@static_cast@ 来实现就可以了。\par
67+
\subsection*{指针/引用的向下类型转换}
68+
派生类的指针/引用可以转换为基类的引用,只要在特定继承方式下这种类型转换是可见的就行。相反地,基类的指针/引用也可以类型转换为派生类。但是相较于前者,这种基类到派生类的转换不仅多了限制,还可能存在危险。\par
69+
基类指针/引用到派生类指针/引用的类型转换必须显式地进行——换句话说,你必须知道你正在做什么,不能稀里糊涂地就把类型转换给执行了。\par
70+
为什么编译器要把这个功能限制地如此严格呢?这是因为,这种\textbf{基类指针/引用到派生类指针/引用的向下类型转换(Downcasting)}存在着访问越界的潜在风险。\par
71+
\begin{figure}[htbp]
72+
\centering
73+
\includegraphics[width=.7\textwidth]{../images/generalized_parts/10_possible_issues_in_downcasting_300.png}
74+
\caption{用派生类指针指向基类对象,这是未定义行为}
75+
\end{figure}
76+
如图10.2所示,如果我们定义了一个 \lstinline@Base@ 类对象而用 \lstinline@Derived*@ 指针指向它,那么这个 \lstinline@Derived*@ 指针指向的内存范围除了这个 \lstinline@Base@ 对象以外,还有4个字节的其它空间。这4个字节可能存储了其它变量的部分信息,或者只是一些存储着杂乱无章信息的未使用空间。但夫论如何,当我们试图访问这些空间中的内容时,将会得到不确定的结果——这是一种未定义行为。\par
77+
那么什么情况下这种类型转换是有意义的呢?只有一种情况:这个指针原本就是指向派生类对象的,只不过它出于某种原因转换成了 \lstinline@Base*@ 而已。这样一来,我们就能确保这个基类指针到派生类指针的转换是安全的。但另一个问题在于,当我们拿到一个基类指针时,我们怎么才能知道这个基类指针到底是不是原本指向派生类对象的呢?很遗憾,因为指针的指向是可以任意改变的,所以我们不能在编译时确定它是不是指向派生类对象的。\par
78+
\lstinline@static_cast@ 的解决方法是不解决——它只是死板地执行类型转换的操作而已,至于这里潜在的风险,它是不会管的。也正因如此,在向下类型转换的过程中,我们再使用 \lstinline@static_cast@ 已经不合适,必须寻求一种能在运行时判断类型并给出相应转换结果的方案,这就是动态类型转换 \lstinline@dynamic_cast@。\par
79+
动态类型转换可以保证程序能够安全地进行向下类型转换。当我们试图把基类指针转换为派生类指针时,程序会进行运行时检查:如果这个基类指针是指向派生类的,那么它可以成功地进行类型转换;否则,这个类型转换只能返回一个 \lstinline@nullptr@。当我们试图把基类引用转换为派生类引用时,程序会进行运行时检查:如果这个基类引用是对派生类对象的引用\footnote{其本质说白了还是指针的指向。引用都是靠指针来实现的。},那么它可以成功地进行类型转换;否则,就会抛出 \lstinline@std::bad_cast@ 异常\footnote{我们会在第十二章中讲解异常。}。\par
80+
\lstinline@dynamic_cast@ 的用法和 \lstinline@static_cast@ 非常相仿,但是我们先别急着用它。动态类型转换要求基类比须是多态的——接下来的一节马上就来讲解这个问题。\par
44 Bytes
Loading
105 KB
Loading
109 KB
Loading

0 commit comments

Comments
 (0)