资讯·论坛·笑话大全·QQ表情·设为首页
 15 12
发新话题
打印

[C++] C++从零开始

C++从零开始(十)——何谓类打造最好的电脑自学交流论坛# y! v* F2 A, m7 s- }% }1 J
前篇已经说明编程时,拿到算法后该干的第一件事就是把资源映射成数字,而前面也说过“类型就是人为制订的如何解释内存中的二进制数的协议”,也就是说一个数字对应着一块内存(可能4字节,也可能20字节),而这个数字的类型则是附加信息,以告诉编译器当发现有对那块内存的cao作语句(即某种cao作符)时,要如何编写机器指令以实现那个cao作。比如两个char类型的数字进行加法cao作符cao作,编译器编译出来的机器指令就和两个long类型的数字进行加法cao作的不一样,也就是所谓的“如何解释内存中的二进制数的协议”。由于解释协议的不同,导致每个类型必须有一个唯一的标识符以示区别,这正好可以提供强烈的语义。
9 ^0 ^- i# q; X' c8 |4 b* I我爱电脑技术社区--打造最好的电脑技术自学交流平台
. S+ ?9 w% K" z' s' @3 g, bwww.520diannao.comtypedef
8 Q& v3 V! p9 S2 m8 Q我爱电脑技术社区--打造最好的电脑技术自学交流平台打造最好的电脑自学交流论坛& I. y' F) b; ?6 m5 l
    提供语义就是要尽可能地在代码上体现出这句或这段代码在人类世界中的意义,比如前篇定义的过河方案,使用一char类型来表示,然后定义了一数组char sln[5]以期从变量名上体现出这是方案。但很明显,看代码的人不一定就能看出sln是solution的缩写并进而了解这个变量的意义。但更重要的是这里有点本末倒置,就好像这个东西是红苹果,然后知道这个东西是苹果,但它也可能是玩具、CD或其它,即需要体现的语义是应该由类型来体现的,而不是变量名。即char无法体现需要的语义。我爱电脑技术论坛6 z* x) G4 d% [4 n$ S! z3 b# k. @
    对此,C++提供了很有意义的一个语句——类型定义语句。其格式为typedef <源类型名> <标识符>;。其中的<源类型名>表示已存在的类型名称,如char、unsigned long等。而<标识符>就是程序员随便起的一个名字,符合标识符规则,用以体现语义。对于上面的过河方案,则可以如下:
! E' P: t+ B- O0 e$ i打造最好的电脑自学交流论坛    typedef char Solution; Solution sln[5];我爱电脑技术论坛% p; Q' k' q+ r; G8 B* T+ I
    上面其实是给类型char起了一个别名Solution,然后使用Solution来定义sln以更好地体现语义来增加代码的可读性。而前篇将两岸的人数分布映射成char[4],为了增强语义,则可以如下:www.520diannao.com- Y0 _! {% p2 n9 ~9 W; S* r
    typedef char PersonLayout[4]; PersonLayout oldLayout[200];
% N4 o, U" y0 O' dwww.520diannao.com    注意上面是typedef char PersonLayout[4];而不是typedef char[4] PersonLayout;,因为数组修饰符“[]”是接在被定义或被声明的标识符的后面的,而指针修饰符“*”是接在前面的,所以可以typedef char *ABC[4];但不能typedef char [4]ABC*;,因为类型修饰符在定义或声明语句中是有固定位置的。
" i5 C" t' `. i2 e2 e3 m' M! _) N我爱电脑技术论坛    上面就比char oldLayout[200][4];有更好的语义体现,不过由于为了体现语义而将类型名或变量名增长,是否会降低编程速度?如果编多了,将会发现编程的大量时间不是花在敲代码上,而是调试上。因此不要忌讳书写长的变量名或类型名,比如在Win32的Security SDK中,就提供了下面的一个函数名:
* m" S% u% K# F! S4 d电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站    BOOL ConvertSecurityDescriptorToStringSecurityDescriptor(…);2 j1 Q' X2 i# H
    很明显,此函数用于将安全描述符这种类型转换成文字形式以方便人们查看安全描述符中的信息。我爱电脑技术社区--打造最好的电脑技术自学交流平台& y. @/ A2 D0 p- o9 p0 |
    应注意typedef不仅仅只是给类型起了个别名,还创建了一个原类型。当书写char* a, b;时,a的类型为char*,b为char,而不是想象的char*。因为“*”在这里是类型修饰符,其是独立于声明或定义的标识符的,否则对于char a[4], b;,难道说b是char[4]?那严重不符合人们的习惯。上面的char就被称作原类型。为了让char*为原类型,则可以:typedef char *PCHAR; PCHAR a, b, *c[4];。其中的a和b都是char*,而c是char**[4],所以这样也就没有问题:char **pA = &a;。电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站. p# g8 G0 m  D& C  m& \

3 o2 n* j4 [2 A  r5 A* @我爱电脑技术社区--打造最好的电脑技术自学交流平台
6 M! [3 [$ [* L电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站结构
1 E3 M" e9 S! {1 Y电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站www.520diannao.com5 ]% s$ k7 X( u6 ^: D' l4 y& P
    再次考虑前篇为什么要将人数布局映射成char[4],因为一个人数可以用一个char就表示,而人数布局有四个人数,所以使用char[4]。即使用char[4]是希望只定义一个变量就代表了一个人数分布,编译器就一次性在栈上分配4个字节的空间,并且每个字节都各自代表一个人数。所以为了表现河岸左侧的商人数,就必须写a[0],而左侧的仆人数就必须a[1]。坏处很明显,从a[0]无法看出它表示的是左岸的商人数,即这个映射意义(左岸的商人数映射为内存块中第一个字节的内容以补码格式解释)无法从代码上体现出来,降低了代码的可读性。我爱电脑技术社区--打造最好的电脑技术自学交流平台; F' @8 h, t: V' n% g  Y
    上面其实是对内存布局的需要,即内存块中的各字节二进制数如何解释。为此,C++提出了类型定义符“{}”。它就是一对大括号,专用在定义或声明语句中,以定义出一种类型,称作自定义类型。即C++原始缺省提供的类型不能满足要求时,可自定义内存布局。其格式为:<类型关键字> <名字> { <声明语句> …}。<类型关键字>只有三个:struct、class和union。而所谓的结构就是在<类型关键字>为struct时用类型定义符定义的原类型,它的类型名为<名字>,其表示后面大括号中写的多条声明语句,所定义的变量之间是串行关系(后面说明),如下:
5 L! c* O* s, m0 s- w我爱电脑技术社区--打造最好的电脑技术自学交流平台    struct ABC { long a, *b; double c[2], d; } a, *b = &a;
% b2 y. h9 _7 Y9 x$ M# w/ g我爱电脑技术社区--打造最好的电脑技术自学交流平台    上面是一个变量定义语句,对于a,表示要求编译器在栈上分配一块4+4+8*2+8=32字节长的连续内存块,然后将首地址和a绑定,其类型为结构型的自定义类型(简称结构)ABC。对于b,要求编译器分配一块4字节长的内存块,将首地址和b绑定,其类型为结构ABC的指针。
3 I7 r9 M5 G  t! H    上面定义变量a和b时,在定义语句中通过书写类型定义符“{}”定义了结构ABC,则以后就可以如下使用类型名ABC来定义变量,而无需每次都那样,即:; `3 Z7 D: P% T+ `1 y; r2 v2 L. f7 m3 K
    ABC &c = a, d[2];
6 d0 t# V5 m; i* \电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站    现在来具体看清上面的意思。首先,前面语句定义了6个映射元素,其中a和b分别映射着两个内存地址。而大括号中的四个变量声明也生成了四个变量,各自的名字分别为ABC::a、ABC::b、ABC::c、ABC::d;各自映射的是0、4、8和24;各自的类型分别为long ABC::、long* ABC::、double (ABC::) [2]、double ABC::,表示是偏移。其中的ABC::表示一种层次关系,表示“ABC的”,即ABC::a表示结构ABC中定义的变量a。应注意,由于C++是强类型语言,它将ABC::也定义为类型修饰符,进而导致出现long* ABC::这样的类型,表示它所修饰的标识符是自定义类型ABC的成员,称作偏移类型,而这种类型的数字不能被单独使用(后面说明)。由于这里出现的类型不是函数,故其映射的不是内存的地址,而是一偏移值(下篇说明)。与之前不同了,类型为偏移类型的(即如上的类型)数字是不能计算的,因为偏移是一相对概念,没有给出基准是无法产生任何意义的,即不能:ABC::a; ABC::c[1];。其中后者更是严重的错误,因为数组cao作符“[]”要求前面接的是数组或指针类型,而这里的ABC::c是double的数组类型的结构ABC中的偏移,并不是数组类型。
0 G! k7 H4 z: Z/ f$ q2 v2 m5 i  h    注意上面的偏移0、4、8、24正好等同于a、b、c、d顺次安放在内存中所形成的偏移,这也正是struct这个关键字的修饰作用,也就是前面所谓的各定义的变量之间是串行关系。
, O4 Z0 Q, ]) w电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站    为什么要给偏移制订映射?即为什么将a映射成偏移0字节,b映射成偏移4字节?因为可以给偏移添加语义。前面的“左岸的商人数映射为内存块中第一个字节的内容以补码格式解释”其实就是给定内存块的首地址偏移0字节。而现在给出一个标识符和其绑定,则可以将这个标识符起名为LeftTrader来表现其语义。
& B, |6 w0 h; D" _7 C1 \7 r我爱电脑技术论坛    由于上面定义的变量都是偏移类型,根本没有分配内存以和它们建立映射,它们也就很正常地不能是引用类型,即struct AB{ long a, &b; };将是错误的。还应注意上面的类型double (ABC::)[2],类型修饰符“ABC::”被用括号括起来,因为按照从左到右来解读类型cao作符的规则,“ABC::”实际应该最后被解读,但其必须放在标识符的左边,就和指针修饰符“*”一样,所以必须使用括号将其括住,以表示其最后才起修饰作用。故也就有:double (*ABCD::)[2]、double (**ABCD::)[2],各如下定义:
# H4 E, \( K/ l0 \$ G6 K# n5 U4 T& ?www.520diannao.com    struct ABCD { double ( *pD )[2]; double ( **ppD )[2]; };7 m+ j* ?6 o; h! v
    但应注意,“ABCD::”并不能直接使用,即double ( *ABCD:: pD )[2];是错误的,要定义偏移类型的变量,必须通过类型定义符“{}”来自定义类型。还应注意C++也允许这样的类型double ( *ABCD::* )[2],其被称作成员指针,即类型为double ( *ABCD:: )[2]的指针,也就是可以如下:
0 ^4 K. [# |" i; W9 v: ]    double ( **ABCD::*pPPD )[2] = &ABC::ppD, ( **ABCD::**ppPPD )[2] = &pPPD;我爱电脑技术论坛3 y( o& `" m2 y5 K  y; ~- c' Y% q* [5 Y
    上面很奇怪,回想什么叫指针类型。只有地址类型的数字才能有指针类型,表示不计算那个地址类型的数字,而直接返回其二进制表示,也就是地址。对于变量,地址就是它映射的数字,而指针就表示直接返回其映射的数字,因此&ABCD::ppD返回的数字其实就是偏移值,也就是4。打造最好的电脑自学交流论坛; p( x4 W. _2 u# J4 C  u
    为了应用上面的偏移类型,C++给出了一对cao作符——成员cao作符“.”和“->”。前者两边接数字,左边接自定义类型的地址类型的数字,而右边接相应自定义类型的偏移类型的数字,返回偏移类型中给出的类型的地址类型的数字,比如:a.ABC::d;。左边的a的类型是ABC,右边的ABC::d的类型是double ABC::,则a.ABC::d返回的数字是double的地址类型的数字,因此可以这样:a.ABC::d = 10.0;。假设a对应的地址是3000,则a.ABC::d返回的地址为3000+24=3024,类型为double,这也就是为什么ABC::d被叫做偏移类型。由于“.”左边接的结构类型应和右边的结构类型相同,因此上面的ABC::可以省略,即a.d = 10.0;。而对于“->”,和“.”一样,只不过左边接的数字是指针类型罢了,即b->c[1] = 10.0;。应注意b->c[1]实际是( b->c )[1],而不是b->( c[1] ),因为后者是对偏移类型运用“[]”,是错误的。
' V, _( I# h; Y    还应注意由于右边接偏移类型的数字,所以可以如下:打造最好的电脑自学交流论坛* [! v" |) u. s/ _4 x
    double ( ABC::*pA )[2] = &ABC::c, ( ABC::**ppA )[2] = &pA;我爱电脑技术社区--打造最好的电脑技术自学交流平台8 p- |9 E( Z9 Y8 V' _' b# }/ K
    ( b->**ppA )[1] = 10.0; ( a.*pA )[0] = 1.0;我爱电脑技术论坛2 w: d, a! ?1 D
    上面之所以要加括号是因为数组cao作符“[]”的优先级较“*”高,但为什么不是b->( **ppA )[1]而是( b->**ppA )[1]?前者是错误的。应注意括号cao作符“()”并不是改变计算优先级,而是它也作为一个cao作符,其优先级被定得很高罢了,而它的计算就是计算括号内的数字。之前也说明了偏移类型是不能计算的,即ABC::c;将错误,而刚才的前者由于“()”的加入而导致要求计算偏移类型的数字,故编译器将报错。
% r2 d5 p0 ]* p6 n4 y8 S2 p' A    还应该注意,成员指针是偏移类型的指针,即装的是偏移,则可以程序运行时期得到偏移,而前面通过ABC::a这种形式得到的是编译时期,由编译器帮忙映射的偏移,只能实现静态的偏移,而利用成员指针则可以实现动态的偏移。不过其实只需将成员定义成数组或指针类型照样可以实现动态偏移,不过就和前篇没有使用结构照样映射了人数布局一样,欠缺语义而代码可读性较低。成员指针的提出,通过变量名,就可以表现出丰富的语义,以增强代码的可读性。现在,可以将最开始说的人数布局定义如下:打造最好的电脑自学交流论坛' Y0 P3 E' T1 a+ w
    struct PersonLayout{ char LeftTrader, LeftServitor, RightTrader, RightServitor; };我爱电脑技术论坛* R, H6 L9 S; `: q
    PersonLayout oldLayout[200], b;
. m0 E) ]9 C' r7 [* J电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站    因此,为了表示b这个人数分布中的左侧商人数,只需b.LeftTrader;,右侧的仆人数,只需b.RightServitor;。因为PersonLayout::LeftTrader记录了偏移值和偏移后应以什么样的类型来解释内存,故上面就可以实现原来的b[0]和b[3]。很明显,前者的可读性远远地高于后者,因为前者通过变量名(b和PersonLayout::LeftTrader)和成员cao作符“.”表现了大量的语义——b的左边的商人数。
% e$ F/ \  T3 _/ }( q7 D0 h" ?我爱电脑技术社区--打造最好的电脑技术自学交流平台    注意PersonLayout::LeftTrader被称作结构PersonLayout的成员变量,而前面的ABC::d则是ABC的成员变量,这种叫法说明结构定义了一种层次关系,也才有所谓的成员cao作符。既然有成员变量,那也有成员函数,这在下篇介绍。
1 d) V0 B" |- D$ w5 Y  ]我爱电脑技术论坛    前篇在映射过河方案时将其映射为char,其中的前4位表示仆人数,后4位表示商人数。对于这种使用长度小于1个字节的用法,C++专门提供了一种语法以支持这种情况,如下:打造最好的电脑自学交流论坛: t! M) Q* I* Z
    struct Solution { ServitorCount : 4; unsigned TraderCount : 4; } sln[5];
, j5 f- E( N- X+ K# l( Q! d# j. rwww.520diannao.com    由于是基于二进制数的位(Bit)来进行cao作,只准使用两种类型来表示数字,原码解释数字或补码解释数字。对于上面,ServitorCount就是补码解释,而TraderCount就是原码解释,各自的长度都为4位,而此时Solution::ServitorCount中依旧记录的是偏移,不过不再以字节为单位,而是位为单位。并且由于其没有类型,故也就没有成员指针了。即前篇的( sln[ cur[ curSln ] ] & 0xF0 ) >> 4等效于sln[ cur[ curSln] ].TraderCount,而sln[ cur[ curSln ] ] & 0xF0等效于sln[ cur[ curSln] ].ServitorCount,较之前具有了更好的可读性。我爱电脑技术社区--打造最好的电脑技术自学交流平台$ k9 z3 e# b3 P( R- l5 K
    应该注意,由于struct AB { long a, b; };也是一条语句,并且是一条声明语句(因为不生成代码),但就其意义上来看,更通常的叫法把它称为定义语句,表示是类型定义语句,但按照不生成代码的规则来判断,其依旧是声明语句,并进而可以放在类型定义符“{}”中,即:
% V- ]- S$ A; c, m) H5 O+ L我爱电脑技术论坛    struct ABC{ struct DB { long a, *b[2]; }; long c; DB a; };www.520diannao.com6 D2 f3 L: [2 w1 [
    上面的结构DB就定义在结构ABC的声明语句中,则上面就定义了四个变量,类型均为偏移类型,变量名依次为:ABC::DB::a、ABC::DB::b、ABC::c、ABC::a;类型依次为long ABC::DB::、long* (ABC::DB::)[2]、long ABC::、ABC::DB;映射的数值依次为0、4、0、4。这里称结构DB嵌套在结构ABC中,其体现出一种层次关系,实际中这经常被使用以表现特定的语义。欲用结构DB定义一个变量,则ABC::DB a;。同样也就有long* ( ABC::DB::*pB )[2] = &ABC::DB::b; ABC c; c.a.a = 10; *( c.a.b[0] ) = 20;。应注意ABC::DB::表示“ABC的DB的”而不是“DB的ABC的”,因为这里是重复的类型修饰符,是从右到左进行修饰的。
# M* U$ y# }, B, v% C我爱电脑技术论坛    前面在定义结构时,都指明了一个类型名,如前面的ABC、ABCD等,但应该注意类型名不是必须的,即可以struct { long a; double b; } a; a.a = 10; a.b = 34.32;。这里就定义了一个变量,其类型是一结构类型,不过这个结构类型没有标识符和其关联,以至于无法对其运用类型匹配等比较,如下:
6 x) z5 k# {6 V8 c( h* ^2 _: g2 o* F电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站    struct { long a; double b; } a, &b = a, *c = &a; struct { long a; double b; } *d = &a;
; _2 v0 J& f) p$ E我爱电脑技术社区--打造最好的电脑技术自学交流平台    上面的a、b、c都没有问题,因为使用同一个类型来定义的,即使这个类型没有标识符和其映射,但d将会报错,即使后写的结构的定义式和前面的一模一样,但仍然不是同一个,只是长得像罢了。那这有什么用?后面说明。
; M. f2 ]2 a+ H; s" W5 q电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站    最后还应该注意,当在复合语句中书写前面的声明语句以定义结构时,之前所说的变量作用域也同样适用,即在某复合语句中定义的结构,出了这个复合语句,它就被删除,等于没定义。如下:打造最好的电脑自学交流论坛: a, x# z; e) ]3 I9 `4 h( E1 h
复制内容到剪贴板代码:
: b$ u7 Y4 T4 u6 z& v4 [电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站void ABC()电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站  M; v+ }. n$ `
{www.520diannao.com+ [7 c' h8 h: z9 w; Y6 c0 M( R
    struct AB { long a, b; };
2 k3 ]+ C$ Q2 V8 a5 mwww.520diannao.com    AB d; d.b = 10;www.520diannao.com. w: v7 ?/ O! u6 u% B, V) L/ N
}
4 @6 D7 a; m& u( O5 N电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站void main()电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站; f  X: h8 e4 `, ?8 _# m
{
9 p+ f# s* }4 C- @' A( E打造最好的电脑自学交流论坛    {打造最好的电脑自学交流论坛/ o" |* J* D% R; g; L3 A
        struct AB{ long a, b, e; };
# l2 @8 ]2 Q2 S- n: m1 g+ }: m        AB c; c.e = 23;电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站. w4 }' J- W- S# b+ k+ U( a3 [2 F: X
    }打造最好的电脑自学交流论坛% ^3 I) M+ Y8 k9 h2 V0 L  x, R
    AB a;  // 将报错,说AB未定义,但其他没有任何问题电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站4 h3 m* [4 }, z" k2 P! w
}我爱电脑技术社区--打造最好的电脑技术自学交流平台2 I! e' N5 p, J; R) ^
初始化电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站) m4 C* M, U% v" E4 K

& E. J: u9 j' Twww.520diannao.com    初始化就是之前在定义变量的同时,就给在栈上分配的内存赋值,如:long a = 10;。当定义的变量的类型有表示多个元素时,如数组类型、上面的结构类型时,就需要给出多个数字。对此,C++专门给出了一种语法,使用一对大括号将欲赋的值括起来后,整体作为一个数字赋给数组或结构,如下:1 T4 c( _% O4 M" |. J8 y, a, i
    struct ABC { long a, b; float c, d[3]; };我爱电脑技术论坛8 x1 u* y7 r  L
    ABC a = { 1, 2, 43.4f, { 213.0f, 3.4f, 12.4f } };
  K, O# ^, V/ R9 ]$ w! _% K电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站    上面就给出了为变量a初始化的语法,大括号将各元素括起来,而各元素之间用“,”隔开。应注意ABC::d是数组类型,其对应的初始化用的数字也必须用大括号括起来,因此出现上面的嵌套大括号。现在应该了解到“{}”只是用来构造一个具有多个元素的数字而已,因此也可以有long a = { 34 };,这里“{}”就等同于没有。还应注意,C++同意给出的大括号中的数字个数少于相应自定义类型或数组的元素个数,即:ABC a = { 1, 2, 34 }, b = { 23, { 34 }, 65, { 23, 43 } }, c = { 1, 2, { 3, { 4, 5, 6 } } };
# g5 ^" s" G: D6 E6 p3 U/ ?. f打造最好的电脑自学交流论坛    上面的a.d[0]、a.d[1]、a.d[2]都为0,而只有b.d[2]才为0,但c将会报错,因为嵌套的第一个大括号将{ 4, 5, 6 }也括了起来,表示c.c将被一个具有两个元素的数字赋值,但c.c的类型是float,只对应一个元素,编译器将说初始化项目过多。而之前的a和b未赋值的元素都将被赋值为0,但应注意并不是数值上的0,而是简单地将未赋值的内存的值用0填充,再通过那些补码原码之类的格式解释成数值后恰好为0而已,并不是赋值0这个数字。
9 E7 O9 X1 H6 l6 [    应注意,C++同意这样的语法:long a[] = { 34, 34, 23 };。这里在定义a时并没有给出元素个数,而是由编译器检查赋值用的大括号包的元素个数,由其来决定数组的个数,因此上面的a的类型为long[3]。当多维数组时,如:long a[3][2] = { { 1, 2 }, { 3, 4 }, { 5, 6 } };。因为每个元素又是需要多个元素的数字,就和前面的ABC::d一样。再回想类型修饰符的修饰顺序,是从左到右,但当是重复类型修饰符时,就倒过来从右到左,因此上面就应该是三个long[2],而不是两个long[3],因此这样将错误:long a[3][2] = { { 1, 2, 3 }, { 4, 5, 6 } };。www.520diannao.com. h/ e% V# z. v6 Z5 F; e5 j
    还应注意,C++不止提供了上面的“{}”这一种初始化方式,对于字符串,其专门提供如:char a[] = "ABC";。这里a的类型就为char[4],因为字符串"ABC"需要占4个字节的内存空间。除了这两种初始化方式外,C++还提供了一种函数式的初始化函数,下篇介绍。我爱电脑技术社区--打造最好的电脑技术自学交流平台! J4 B* C: X3 Y0 }9 f. h' X$ {
我爱电脑技术社区--打造最好的电脑技术自学交流平台# r3 Q6 C7 Z5 P5 l. @4 M6 y8 i
打造最好的电脑自学交流论坛$ v+ n# V4 `4 l& ^2 C4 m. f
类型的运用www.520diannao.com2 g0 v1 {* D' g$ w; {/ }
电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站8 g1 N  D6 |  B8 p! `
    char a = -34; unsigned char b = ( unsigned char )a;
; ?, P5 G; M" F' [) D3 l- K  I3 b" i    上面的b等于222,将-34按照补码格式写成二进制数11011110,然后将这个二进制数用原码格式解释,得数值222。继续:
4 |' U+ {2 j; |    float a = 5.6f; unsigned long b = ( unsigned long )a;
9 Q% o) W) m* d% ]电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站    这回b等于5。为什么?不是应该将5.6按照IEEE的real*4的格式写成二进制数0X40B33333(这里用十六进制表示),然后将这个二进制数用原码格式解释而得数值1085485875吗?因为类型转换是语义上的类型转换,而不是类型变换。
, M7 v2 o* z: X$ g' E$ O$ Y我爱电脑技术社区--打造最好的电脑技术自学交流平台    两个类型是否能够转换,要视编译器是否定义了这两个类型之间的转换规则。如char和unsigned char,之所以前面那样转换是因为编译器把char转unsigned char定义成了那样,同样float转unsigned long被编译器定义成了取整而不是四舍五入。电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站3 q. \+ P3 `3 M- S/ R) L# o
    为什么要有类型转换?有什么意义?的确,像上面那样的转换,毫无意义,仅仅只是为了满足语法的严密性而已,不过由于C++定义了指针类型的转换,而且定义得非常地好,以至于有非常重要的意义。我爱电脑技术论坛+ U9 `0 M" h; o2 Z% c& n' P/ S
    char a = -34; unsigned char b = *( unsigned char* )( &a );电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站3 ^* O6 w; [6 D
    上面的结果和之前的一样,b为222,不过是通过将char*转成unsigned char*,然后再用unsigned char来解释对应的内存而得到222,而不是按照编译器的规定来转换的,即使结果一样。因此:
5 ?! ?; X- ^1 E; L: x电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站    float a = 5.6f; unsigned long b = *( unsigned long* )( &a );我爱电脑技术论坛5 S; D; I7 `( X6 F: B7 h) `" j
    上面的b为1085485875,也就是之前以为的结果。这里将a的地址所对应的内存用unsigned long定义的规则来解释,得到的结果放在b中,这体现了类型就是如何解释内存中的内容。上面之所以能实现,是因为C++规定所有的指针类型之间的转换,数字的数值没有变化,只有类型变化(但由于类的继承关系也是可能会改变,下篇说明),因此上面才说b的值是用unsigned long来解释a对应的内存的内容所得的结果。因此,前篇在比较oldLayout[ curSln ][0~3]和oldLayout[ i ][0~3]时写了四个“==”以比较了四次char的数字,由于这四个char数字是连续存放的,因此也可如下只比较一次long数字即可,将节约多余的三次比较时间。
! w7 m* u) q; m( O& S) Jwww.520diannao.com    *( long* )&oldLayout[ curSln ] == *( long* )&oldLayout[ i ]
6 N. F& n5 ^( W% x6 p% ?* t打造最好的电脑自学交流论坛    上面只是一种优化手段而已,对于语义还是没有多大意义,不过由于有了自定义类型,因此:
; w6 [, z* n3 `9 p, X6 |( t* p打造最好的电脑自学交流论坛    struct AB { long a1; long a2; }; struct ABC { char a, b; short c; long d; };我爱电脑技术社区--打造最好的电脑技术自学交流平台# }$ s$ s, V8 T
    AB a = { 53213, 32542 }; ABC *pA = ( ABC* )&a;
. h1 w' q4 j1 C; c9 L3 T    char aa = pA->a, bb = pA->b, cc = pA->c; long dd = pA->d;我爱电脑技术论坛% f' H% R' Z1 o
    pA->a = 1; pA->b = 2; pA->c = 3; pA->d = 4;www.520diannao.com9 U# [  K: x0 G: H* }* ]3 o0 m9 D
    long aa1 = a.a1, aa2 = a.a2;
% t( a) E1 d: K+ {$ l$ U/ y3 X我爱电脑技术论坛    上面执行后,aa、bb、cc、dd的值依次为-35、-49、0、32542,而aa1和aa2的值分别为197121和4。相信只要稍微想下就应该能理解为什么没有修改a.a1和a.a2,结果它们的值却变了,因为变量只不过是个映射而已,而前面就是利用指针pA以结构ABC来解释并cao作a所对应的内存的内容。
; M6 C( Z, {3 x我爱电脑技术论坛    因此,利用自定义类型和指针转换,就可以实现以什么样的规则来看待某块内存的内容。有什么用?传递给某函数一块内存的引用(利用指针类型或引用类型),此函数还另有一个参数,比如是long类型。当此long类型的参数为1时,表示传过去的是一张定单;为2时,表示传过去的是一张发货单;为3时表示是一张收款单。如果再配上下面说明的枚举类型,则可以编写出语义非常完善的代码。我爱电脑技术论坛& o1 N, l( ]7 w- ]7 S! w- Z
    应注意由于指针是可以随便转换的,也就有如下的代码,实际并没什么意义,在这只为加深对成员指针的理解:
7 y3 O; ?( |& i* {# j" s% V我爱电脑技术论坛    long AB::*p = ( long AB::* )( &ABC::b ); a.a1 = a.a2 = 0; a.*p = 0XAB1234CD;打造最好的电脑自学交流论坛( M* s$ @9 C6 [$ L& D1 F( P
    上面执行后,a.a1为305450240,a.a2为171,转成十六进制分别为0X1234CD00和0X000000AB。
% M! ~& x1 Y0 W6 T  F- R: W# Bwww.520diannao.com我爱电脑技术社区--打造最好的电脑技术自学交流平台- R6 s- Y% T& y! f. \$ T

4 x5 V- Z8 b( D0 U. v/ X* |枚举
/ L" k, I- E; y4 `1 I! q, o3 L* d我爱电脑技术社区--打造最好的电脑技术自学交流平台我爱电脑技术论坛2 p7 a- U7 {% U/ H% H* p5 P1 L) p
    上面欲说明1时为定单,2时为发货单而3时为收款单,则可以利用switch或if语句来进行判断,但是语句从代码上将看见类似type == 1或type == 2之类,无法表现出语义。C++专门为此提供了枚举类型。打造最好的电脑自学交流论坛, f! Y. k. g- Q6 @# u) F! r
    枚举类型的格式和前面的自定义类型很像,但意义完全不同,如下:www.520diannao.com7 j1 [+ e* j. s/ C* V
    enum AB { LEFT, RIGHT = 2, UP = 4, DOWN = 3 }; AB a = LEFT;我爱电脑技术社区--打造最好的电脑技术自学交流平台  @* h; Z" T( H& L- F( l
    switch( a )
0 U: V( t3 ], \' n4 Y* F我爱电脑技术社区--打造最好的电脑技术自学交流平台    {打造最好的电脑自学交流论坛( I) R) u' Y% A: `+ w2 F2 q
        case LEFT:;  // 做与左相应的事我爱电脑技术社区--打造最好的电脑技术自学交流平台" ^) V$ ~5 R( v  [$ I2 r" w
        case UP:;    // 做与上相应的事
& H; K7 V# N9 V: }# O    }
7 X8 G6 h, X; W; Z$ q: D电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站    枚举也要用“{}”括住一些标识符,不过这些标识符即不映射内存地址也不映射偏移,而是映射整数,而为什么是整数,那是因为没有映射浮点数的必要,后面说明。上面的RIGHT就等同于2,注意是等同于2,相当于给2起了个名字,因此可以long b = LEFT; double c = UP; char d = RIGHT;。但注意上面的变量a,它的类型为AB,即枚举类型,其解释规则等同于int,即编译成在16位cao作系统上运行时,长度为2个字节,编译成在32位cao作系统上运行时为4个字节,但和int是属于不同的类型,而前面的赋值cao作之所以能没有问题,可以认为编译器会将枚举类型隐式转换成int类型,进而上面没有错误。但倒过来就不行了,因为变量a的类型是AB,则它的值必须是上面列出的四个标识符中的一个,而a = b;则由于b为long类型,如果为10,那么将无法映射上面的四个标识符中的一个,所以不行。
$ h$ ^* l; e- c% F8 E5 G5 _$ g2 q2 s打造最好的电脑自学交流论坛    注意上面的LEFT没有写“=”,此时将会从其前面的一个标识符的值自增一,由于它是第一个,而C++规定为0,故LEFT的值为0。还应注意上面映射的数字可以重复,即:
- p" x# [4 E8 }- L% }$ V打造最好的电脑自学交流论坛    enum AB { LEFT, RIGHT, UP = 5, DOWN, TOP = 5, BOTTOM };
; W1 y, ?7 n5 }; g; ^) y5 ^; zwww.520diannao.com    上面的各标识符依次映射的数值为0、1、5、6、5、6。因此,最开始说的问题就可以如下处理:电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站1 ^+ Z6 e$ J8 y  W1 D7 V* |
    enum OperationType { ORDER = 1, INVOICE, CHECKOUT };www.520diannao.com# l, Z( y. g' v( f  n
    而那个参数的类型就可以为OperationType,这样所表现的语义就远远地超出原来的代码,可读性高了许多。因此,当将某些人类世界的概念映射成数字时,发现它们的区别不表现在数字上,比如吃饭、睡觉、玩表示一个人的状态,现在为了映射人这个概念为数字,也需要将人的状态这个概念映射成数字,但很明显地没有什么方便的映射规则。这时就强行说1代表吃饭,2代表睡觉,3代表玩,此时就可以使用将1、2、3定义成枚举以表现语义,这也就是为什么枚举只定义为整数,因为没有定义成浮点数的必要性。我爱电脑技术社区--打造最好的电脑技术自学交流平台' W: p6 |- j" [: N( z$ w

+ Z9 U1 g. O1 b( M& Z3 d+ U
* I: Y# O) R3 F8 B; u我爱电脑技术社区--打造最好的电脑技术自学交流平台联合5 d( X1 L9 s$ Y& e: `
7 `, h; t. _! }3 N/ z+ n
    前面说过类型定义符的前面可以接struct、class和union,当接union时就表示是联合型自定义类型(简称联合),它和struct的区别就是后者是串行分布来定义成员变量,而前者是并行分布。如下:; R, x& Z# y4 N' {2 M( k1 e
    union AB { long a1, a2, a3; float b1, b2, b3; }; AB a;4 T  [/ f( I0 d# q2 P
    变量a的长度为4个字节,而不是想象的6*4=24个字节,而联合AB中定义的6个变量映射的偏移都为0。因此a.a1 = 10;执行后,a.a1、a.a2、a.a3的值都为10,而a.b1的值为多少,就用IEEE的real*4格式来解释相应内存的内容,该多少是多少。
: S' R, _- @5 x5 k5 \电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站    也就是说,最开始的利用指针来解释不同内存的内容,现在可以利用联合就完成了,因此上面的代码搬到下面,变为:5 ^9 ?6 T8 [7 \4 Z" K
复制内容到剪贴板代码:打造最好的电脑自学交流论坛( P* }0 M9 b* h% n$ m' S, W
    union AB
+ C5 f" j( z7 x8 s: Y我爱电脑技术论坛    {
# u1 I/ o8 h4 i5 [) C打造最好的电脑自学交流论坛        struct { long a1; long a2; };
% ?8 w, X, }4 @+ N我爱电脑技术社区--打造最好的电脑技术自学交流平台        struct { char a, b; short c; long d; };
! ]! u" ^% J3 X3 B( Q0 ~    };我爱电脑技术论坛. V7 ]1 G# S* W# J& n
    AB a = { 53213, 32542 };
  F% a$ J! }: P2 z- g# J! C  r电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站    char aa = a.a, bb = a.b, cc = a.c; long dd = a.d;我爱电脑技术论坛; y( v$ F% N  g2 X' s
    a.a = 1; a.b = 2; a.c = 3; a.d = 4;我爱电脑技术论坛! @. L! {! ^/ ]7 w0 c) t
    long aa1 = a.a1, aa2 = a.a2;
# k' C$ C1 y0 {* i, _我爱电脑技术论坛结果不变,但代码要简单,只用定义一个自定义类型了,而且没有指针变量的运用,代码的语义变得明显多了。www.520diannao.com  [' H$ i0 F2 p
    注意上面定义联合AB时在其中又定义了两个结构,但都没有赋名字,这是C++的特殊用法。当在类型定义符的中间使用类型定义符时,如果没有给类型定义符定义的类型绑定标识符,则依旧定义那些偏移类型的变量,不过这些变量就变成上层自定义类型的成员变量,因此这时“{}”等同于没有,唯一的意义就是通过前面的struct或class或union来指明变量的分布方式。因此可以如下:
, N7 H* @5 }+ R打造最好的电脑自学交流论坛    struct AB
6 \" o4 @3 e: t打造最好的电脑自学交流论坛    {
: y* O& B- o4 N% c, s( x/ }- j打造最好的电脑自学交流论坛        struct { long a1, a2; };电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站) Y* C1 J) y7 `4 y9 x
        char a, b;
2 H* i/ D: E# m. k; s* k- O- j1 }www.520diannao.com        union { float b1; double b2; struct { long b3; float b4; char b5; }; };
- @1 ~  R9 _# M! Rwww.520diannao.com        short c;
2 T) r8 G4 d5 F% t2 T* S    };我爱电脑技术社区--打造最好的电脑技术自学交流平台: Y0 T9 l) J& W5 f5 G* F% g
    上面的自定义类型AB的成员变量就有a1、a2、a、b、b1、b2、b3、b4、b5、c,各自对应的偏移值依次为0、4、8、9、10、10、10、14、18、19,类型AB的总长度为21字节。某类型的长度表示如果用这个类型定义了一个变量,则编译器应该在栈上分配多大的连续空间,C++为此专门提供了一个cao作符sizeof,其右侧接数字或类型名,当接数字时,就返回那个数字的类型需要占的内存空间的大小,而接类型名时,就返回那个类型名所标识的类型需要占的内存空间的大小。我爱电脑技术论坛" i% }; L& _) C1 i- H) y
    因此long a = sizeof( AB ); AB d; long b = sizeof d;执行后,a和b的值都为40。怎么是40?不应该为21吗?而之前的各成员变量对应的偏移也依次实际为0、4、8、9、16、16、16、20、24、32。为什么?这就是所谓的数据对齐。打造最好的电脑自学交流论坛  r! O( V3 e/ ^) U( Z
    CPU有某些指令,需要处理多个数据,则各数据间的间隔必须是4字节或8字节或16字节(视不同的指令而有不同的间隔),这被称作数据对齐。当各个数据间的间隔不符合要求时,CPU就必须做附加的工作以对齐数据,效率将下降。并且CPU并不直接从内存中读取东西,而要经一个高速缓冲(CPU内建的一个存取速度比内存更快的硬件)缓冲一下,而此缓冲的大小肯定是2的次方,但又比较小,因此自定义类型的大小最好能是2的次方的倍数,以便高效率的利用高速缓冲。
' f) D( e; Z! h电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站    在自定义类型时,一个成员变量的偏移值一定是它所属的类型的长度的倍数,即上面的a和b的偏移必须是1的倍数,而c的偏移必须是2的倍数,b1的偏移必须是4的倍数。但b2的偏移必须是8的倍数,而b1和b2由于前面的union而导致是并行布局,因此b1的偏移必须和b2及b3的相同,因此上面的b1、b2、b3的偏移变成了8的倍数16,而不是想象的10。
% J( H, A; h2 o- Cwww.520diannao.com    而一个自定义类型的长度必须是其成员变量中长度最长的那个成员变量的长度的倍数,因此struct { long b3; float b4; char b5; };的长度是4的倍数,也就是12。而上面的无名联合的成员变量中,只有double b2;的长度最长,为8个字节,所以它的长度为16,并进而导致c的偏移为b1的偏移加16,故为32。由于结构AB中的成员变量只有b2的长度最长,为8,故AB的长度必须是8的倍数40。因此在定义结构时应尽量将成员和其长度对应起来,如下:我爱电脑技术社区--打造最好的电脑技术自学交流平台0 y2 A% D& I+ ~% t6 `8 W
    struct ABC1 { char a, b; char d; long c; };
7 C$ i- k2 E$ i% _9 \3 S我爱电脑技术社区--打造最好的电脑技术自学交流平台    struct ABC2 { char a, b; long c; char d; };我爱电脑技术社区--打造最好的电脑技术自学交流平台! m3 T* _9 ^3 o' {0 N0 C
    ABC1的长度为8个字节,而ABC2的长度为12个字节,其中ABC1::c和ABC2::c映射的偏移都为4。
, h9 e8 h: S* z打造最好的电脑自学交流论坛    应注意上面说的规则一般都可以通过编译选项而进行一定的改变,不同的编译器将给出不同的修改方式,在此不表。我爱电脑技术社区--打造最好的电脑技术自学交流平台8 \3 k( u6 k$ T) u- s
    本篇说明了如何使用类型定义符“{}”来定义自定义类型,说明了两种自定义类型,实际还有许多自定义类型的内容未说明,将在下篇介绍,即后面介绍的类及类相关的内容都可应用在联合和结构上,因为它们都是自定义类型。

TOP

C++从零开始(十一)(上)——类的相关知识电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站0 K# _+ E4 B- k5 V  O
前面已经介绍了自定义类型的成员变量和成员函数的概念,并给出它们各自的语义,本文继续说明自定义类型剩下的内容,并说明各自的语义。6 i- Y: Z4 `5 z' E

- t- B/ ~' A; V( r) H( wwww.520diannao.comwww.520diannao.com3 h4 V$ _' M1 L' c* O- E
权限www.520diannao.com& C  L6 Y8 V' t7 O& n
我爱电脑技术论坛, o# f+ @1 }4 K( q2 v. O; u
    成员函数的提供,使得自定义类型的语义从资源提升到了具有功能的资源。什么叫具有功能的资源?比如要把收音机映射为数字,需要映射的cao作有调整收音机的频率以接收不同的电台;调整收音机的音量;打开和关闭收音机以防止电力的损耗。为此,收音机应映射为结构,类似下面:打造最好的电脑自学交流论坛9 P4 ?& E, a1 B0 [2 @' [
复制内容到剪贴板代码:电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站3 y; s2 W' ~% u/ V/ H2 R* n
   struct Radiogram
& [8 y. ]8 T# u* F% V8 s- o9 vwww.520diannao.com    {
% j1 l; S* x% M0 j' N, S$ W电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站        double Frequency;  /* 频率 */  void TurnFreq( double value );   // 改变频率电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站. I' ~+ d3 C) W3 N, w5 h
        float  Volume;     /* 音量 */  void TurnVolume( float value );  // 改变音量
4 ^5 V1 S) g; c6 W我爱电脑技术论坛        float  Power;      /* 电力 */  void TurnOnOff( bool bOn );      // 开关
  X& L+ S- d3 K9 J; K9 ?我爱电脑技术论坛        bool   bPowerOn;   // 是否开启打造最好的电脑自学交流论坛% C6 ^! K. |" ]! i, V7 B3 a; P
    };
; h$ ]4 o+ |- j% n- @2 x% B我爱电脑技术社区--打造最好的电脑技术自学交流平台上面的Radiogram::Frequency、Radiogram::Volume和Radiogram::Power由于定义为了结构Radiogram的成员,因此它们的语义分别为某收音机的频率、某收音机的音量和某收音机的电力。而其余的三个成员函数的语义也同样分别为改变某收音机的频率、改变某收音机的音量和打开或关闭某收音机的电源。注意这面的“某”,表示具体是哪个收音机的还不知道,只有通过成员cao作符将左边的一个具体的收音机和它们结合时才知道是哪个收音机的,这也是为什么它们被称作偏移类型。这一点在下一篇将详细说明。
, w+ |. O, R; l* i8 j* ?/ v! gwww.520diannao.com    注意问题:为什么要将刚才的三个cao作映射为结构Radiogram的成员函数?因为收音机具有这样的功能?那么对于选西瓜、切西瓜和吃西瓜,难道要定义一个结构,然后给它定义三个选、切、吃的成员函数??不是很荒谬吗?前者的三个cao作是对结构的成员变量而言,而后者是对结构本身而言的。那么改成吃快餐,吃快餐的汉堡包、吃快餐的薯条和喝快餐的可乐。如果这里的两个吃和一个喝的cao作变成了快餐的成员函数,表示是快餐的功能?!这其实是编程思想的问题,而这里其实就是所谓的面向对象编程思想,它虽然是很不错的思想,但并不一定是合适的,下篇将详细讨论。
6 j! W) h$ }( j6 i# X电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站    上面我们之所以称收音机的换台是功能,是因为实际中我们自己是无法直接改变收音机的频率,必须通过旋转选台的那个旋钮来改变接收的频率,同样,调音量也是通过调节音量旋钮来实现的,而由于开机而导致的电力下降也不是我们直接导致,而是间接通过收听电台而导致的。因此上面的Radiogram::Power、Radiogram::Frequency等成员变量都具有一个特殊特性——外界,这台收音机以外的东西是无法改变它们的。为此,C++提供了一个语法来实现这种语义。在类型定义符中,给出这样的格式:<权限>:。这里的<权限>为public、protected和private中的一个,分别称作公共的、保护的和私有的,如下:
; G( f5 ^4 {  S9 @打造最好的电脑自学交流论坛复制内容到剪贴板代码:电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站8 G! u* `5 u% ~1 m% B9 B1 W2 T* t& M4 s
    class Radiogram打造最好的电脑自学交流论坛; B: r' ~8 V/ z% b
    {
8 J" B% T& [3 l8 I; Nwww.520diannao.com    protected: double m_Frequency; float m_Volume; float m_Power;我爱电脑技术论坛3 S  V; ]4 l  N( {
    private:   bool   m_bPowerOn;电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站' u7 u3 a8 c- O$ l* _9 d
    public:    void TurnFreq( double ); void TurnVolume( float ); void TurnOnOff( bool );我爱电脑技术论坛* d$ [' h4 @6 i4 b
    };我爱电脑技术论坛# y/ z. u7 D! c9 D$ R# U
可以发现,它和之前的标号的定义格式相同,但并不是语句修饰符,即可以struct ABC{ private: };。这里不用非要在private:后面接语句,因为它不是语句修饰符。从它开始,直到下一个这样的语法,之间所有的声明和定义而产生的成员变量或成员函数都带有了它所代表的语义。比如上面的类Radiogram,其中的Radiogram::m_Frequency、Radiogram::m_Volume和Radiogram::m_Power是保护的成员变量,Radiogram::m_bPowerOn是私有的成员变量,而剩下的三个成员函数都是公共的成员函数。注意上面的语法是可以重复的,如:struct ABC { public: public: long a; private: float b; public: char d; };。
) C/ t3 x( c+ j$ n- n9 D8 ~# C0 ^我爱电脑技术社区--打造最好的电脑技术自学交流平台    什么意思?很简单,公共的成员外界可以访问,保护的成员外界不能访问,私有的成员外界及子类不能访问。关于子类后面说明。先看公共的。对于上面,如下将报错:
% \; C+ B8 c# |打造最好的电脑自学交流论坛    Radiogram a; a.m_Frequency = 23.0; a.m_Power = 1.0f; a.m_bPowerOn = true;
- \7 P, a, X, h% ~# Y* g' }电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站    因为上面对a的三次cao作都使用了a的保护或私有成员,编译器将报错,因为这两种成员外界是不能访问的。而a.TurnFreq( 10 );就没有任何问题,因为成员函数Radiogram::TurnFreq是公共成员,外界可以访问。那么什么叫外界?对于某个自定义类型,此自定义类型的成员函数的函数体内以外的一切能写代码的地方都称作外界。因此,对于上面的Radiogram,只有它的三个成员函数的函数体内可以访问它的成员变量。即下面的代码将没有问题。
3 y! S& h5 B* R, v  h# ^' v电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站    void Radiogram::TurnFreq( double value ) { m_Frequency += value; }我爱电脑技术社区--打造最好的电脑技术自学交流平台7 F- i& U$ _4 P) F1 [2 \+ `0 i0 F2 S- ^5 L
    因为m_Frequency被使用的地方是在Radiogram::TurnFreq的函数体内,不属于外界。
" i/ G# E& i1 p2 Q$ K; l我爱电脑技术论坛    为什么要这样?表现最开始说的语义。首先,上面将成员定义成public或private对于最终生成的代码没有任何影响。然后,我之前说的调节接收频率是通过调节收音机里面的共谐电容的容量来实现的,这个电容的容量人必须借助元件才能做到,而将接收频率映射成数字后,由于是数字,则CPU就能修改。如果直接a.m_Frequency += 10;进行修改,就代码上的意义,其就为:执行这个方法的人将收音机的接收频率增加10KHz,这有违我们的客观世界,与前面的语义不合。因此将其作为语法的一种提供,由编译器来进行审查,可以让我们编写出更加符合我们所生活的世界的语义的代码。
3 B) R: o& a. L/ M( V我爱电脑技术论坛    应注意可以union ABC { long a; private: short b; };。这里的ABC::a之前没有任何修饰,那它是public还是protected?相信从前面举的那么多例子也已经看出,应该是public,这也是为什么我之前一直使用struct和union来定义自定义类型,否则之前的例子都将报错。而前篇说过结构和类只有一点很小的区别,那就是当成员没有进行修饰时,对于类,那个成员将是private而不是public,即如下将错误。9 |) ~8 P' M8 ^0 U" @
    class ABC { long a; private: short b; }; ABC a; a.a = 13;www.520diannao.com* g- c# G. C9 `- n4 {* x  k  {
    ABC::a由于前面的class而被看作private。就从这点,可以看出结构用于映射资源(可被直接使用的资源),而类用于映射具有功能的资源。下篇将详细讨论它们在语义上的差别。www.520diannao.com- z# y7 u  G& Q( G+ z
打造最好的电脑自学交流论坛  ^+ {1 C4 L! F! d3 n4 G- H" y2 p

8 E, u( V$ T+ b4 L4 T7 a构造和析构打造最好的电脑自学交流论坛, V1 [! N/ l6 a9 z9 J
我爱电脑技术社区--打造最好的电脑技术自学交流平台6 \; e9 \" ^% [& H' O6 g7 Z0 O! G
    了解了上面所提的东西,很明显就有下面的疑问:# `% B/ K3 ^9 F$ v" x. C5 P; x
    struct ABC { private: long a, b; }; ABC a = { 10, 20 };
- ?0 i0 d6 K1 @) }# h2 T8 Q打造最好的电脑自学交流论坛    上面的初始化赋值变量a还正确吗?当然错误,否则在语法上这就算一个漏洞了(外界可以借此修改不能修改的成员)。但有些时候的确又需要进行初始化以保证一些逻辑关系,为此C++提出了构造和析构的概念,分别对应于初始化和扫尾工作。在了解这个之前,让我们先看下什么叫实例(Instance)。- b2 l% v7 G: o0 }$ a
    实例是个抽象概念,表示一个客观存在,其和下篇将介绍的“世界”这个概念联系紧密。比如:“这是桌子”和“这个桌子”,前者的“桌子”是种类,后者的“桌子”是实例。这里有10只羊,则称这里有10个羊的实例,而羊只是一种类型。可以简单地将实例认为是客观世界的物体,人类出于方便而给各种物体分了类,因此给出电视机的说明并没有给出电视机的实例,而拿出一台电视机就是给出了一个电视机的实例。同样,程序的代码写出来了意义不大,只有当它被执行时,我们称那个程序的一个实例正在运行。如果在它还未执行完时又要求cao作系统执行了它,则对于多任务cao作系统,就可以称那个程序的两个实例正在被执行,如同时点开两个Word文件查看,则有两个Word程序的实例在运行。
, u/ V: b0 U2 R' D打造最好的电脑自学交流论坛    在C++中,能被cao作的只有数字,一个数字就是一个实例(这在下篇的说明中就可以看出),更一般的,称标识记录数字的内存的地址为一个实例,也就是称变量为一个实例,而对应的类型就是上面说的物体的种类。比如:long a, *pA = &a, &ra = a;,这里就生成了两个实例,一个是long的实例,一个是long*的实例(注意由于ra是long&所以并未生成实例,但ra仍然是一个实例)。同样,对于一个自定义类型,如:Radiogram ab, c[3];,则称生成了四个Radiogram的实例。电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站% _9 }4 |/ e' k4 d7 F
    对于自定义类型的实例,当其被生成时,将调用相应的构造函数;当其被销毁时,将调用相应的析构函数。谁来调用?编译器负责帮我们编写必要的代码以实现相应构造和析构的调用。构造函数的原型(即函数名对应的类型,如float AB( double, char );的原型是float( double, char ))的格式为:直接将自定义类型的类型名作为函数名,没有返回值类型,参数则随便。对于析构函数,名字为相应类型名的前面加符号“~”,没有返回值类型,必须没有参数。如下:
9 s9 Z( `0 D  m; F2 z打造最好的电脑自学交流论坛复制内容到剪贴板代码:
. Q7 b4 h( [( g% G0 a我爱电脑技术论坛struct ABC { ABC(); ABC( long, long ); ~ABC(); bool Do( long ); long a, count; float *pF; };打造最好的电脑自学交流论坛. h+ F; v) X2 L7 Z# O
ABC::ABC() { a = 1; count = 0; pF = 0; }我爱电脑技术社区--打造最好的电脑技术自学交流平台( P$ y1 j+ s3 A4 q% P
ABC::ABC( long tem1, long tem2 ) { a = tem1; count = tem2; pF = new float[ count ]; }www.520diannao.com" ~8 u2 b/ T, f/ A" e  d+ U
ABC::~ABC() { delete[] pF; }
6 c- ]# G$ {! A7 z4 F- P电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站bool ABC::Do( long cou )电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站7 h( a2 T( a6 j7 ^8 \1 @
{
5 g5 F0 D1 P- P6 D    float *p = new float[ cou ];
% G+ L& M' S0 t/ T9 K0 \我爱电脑技术论坛    if( !p )www.520diannao.com$ s, {' b" h, _! V) s6 e
        return false;www.520diannao.com) L% C; ~4 C( f+ w4 W) E3 E, j
    delete[] pF;电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站+ i4 M5 @) Z% K. T
    pF = p;
$ C2 R% d4 {+ |0 ~+ Y我爱电脑技术社区--打造最好的电脑技术自学交流平台    count = cou;
. ~. g, X) e1 p' _! E' H& a, Z我爱电脑技术社区--打造最好的电脑技术自学交流平台    return true;
2 N/ R+ t: j: p8 R我爱电脑技术论坛}www.520diannao.com% d) j4 F: V( N% i' c( T" {
extern ABC g_ABC;打造最好的电脑自学交流论坛/ }' ]4 c) N: W/ d
void main(){ ABC a, &r = a; a.Do( 10 ); { ABC b( 10, 30 ); } ABC *p = new ABC[10]; delete[] p; }
* h8 A/ O' ]/ H1 h我爱电脑技术社区--打造最好的电脑技术自学交流平台ABC g_a( 10, 34 ), g_p = new ABC[5];
1 p3 g6 B5 r$ A' K$ c, v3 P( }6 ?我爱电脑技术论坛上面的结构ABC就定义了两个构造函数(注意是两个重载函数),名字都为ABC::ABC(实际将由编译器转成不同的符号以供连接之用)。也定义了一个析构函数(注意只能定义一个,因为其必须没有参数,也就无法进行重载了),名字为ABC::~ABC。www.520diannao.com5 K. u1 n( g; @5 o1 l. }
    再看main函数,先通过ABC a;定义了一个变量,因为要在栈上分配一块内存,即创建了一个数字(创建装数字的内存也就导致创建了数字,因为内存不能不装数字),进而创建了一个ABC的实例,进而调用ABC的构造函数。由于这里没有给出参数(后面说明),因此调用了ABC::ABC(),进而a.a为1,a.pF和a.count都为0。接着定义了变量r,但由于它是ABC&,所以并没有在栈上分配内存,进而没有创建实例而没有调用ABC::ABC。接着调用a.Do,分配了一块内存并把首地址放在a.pF中。
( Q/ L& k; Y, D' ^我爱电脑技术社区--打造最好的电脑技术自学交流平台    注意上面变量b的定义,其使用了之前提到的函数式初始化方式。它通过函数调用的格式调用了ABC的构造函数ABC::ABC( long, long )以初始化ABC的实例b。因此b.a为10,b.count为30,b.pF为一内存块的首地址。但要注意这种初始化方式和之前提到的“{}”方式的不同,前者是进行了一次函数调用来初始化,而后者是编译器来初始化(通过生成必要的代码)。由于不调用函数,所以速度要稍快些(关于函数的开销在《C++从零开始(十五)》中说明)。还应注意不能ABC b = { 1, 0, 0 };,因为结构ABC已经定义了两个构造函数,则它只能使用函数式初始化方式初始化了,不能再通过“{}”方式初始化了。
7 _. L% q& j+ Q' k! y# U( _& f我爱电脑技术社区--打造最好的电脑技术自学交流平台    上面的b在一对大括号内,回想前面提过的变量的作用域,因此当程序运行到ABC *p = new ABC[10];时,变量b已经消失了(超出了其作用域),即其所分配的内存语法上已经释放了(实际由于是在栈上,其并没有被释放),进而调用ABC的析构函数,将b在ABC::ABC( long, long )中分配的内存释放掉以实现扫尾功能。, U2 P& L. z: g
    对于通过new在堆上分配的内存,由于是new ABC[10],因此将创建10个ABC的实例,进而为每一个实例调用一次ABC::ABC(),注意这里无法调用ABC::ABC( long, long ),因为newcao作符一次性就分配了10个实例所需要的内存空间,C++并没有提供语法(比如使用“{}”)来实现对一次性分配的10个实例进行初始化。接着调用了delete[] p;,这释放刚分配的内存,即销毁了10个实例,因此将调用ABC的析构函数10次以进行10次扫尾工作。
0 D5 g8 i, @" F4 d我爱电脑技术社区--打造最好的电脑技术自学交流平台    注意上面声明了全局变量g_ABC,由于是声明,并不是定义,没有分配内存,因此未产生实例,故不调用ABC的构造函数,而g_a由于是全局变量,C++保证全局变量的构造函数在开始执行main函数之前就调用,所有全局变量的析构函数在执行完main函数之后才调用(这一点是编译器来实现的,在《C++从零开始(十九)》中将进一步讨论)。因此g_a.ABC( 10, 34 )的调用是在a.ABC()之前,即使它的位置在a的定义语句的后面。而全局变量g_p的初始化的数字是通过newcao作符的计算得来,结果将在堆上分配内存,进而生成5个ABC实例而调用了ABC::ABC()5次,由于是在初始化g_p的时候进行分配的,因此这5次调用也在a.ABC()之前。由于g_p仅仅只是记录首地址,而要释放这5个实例就必须调用delete(不一定,也可不调用delete依旧释放new返回的内存,在《C++从零开始(十九)》中说明),但上面并没有调用,因此直到程序结束都将不会调用那5个实例的析构函数,那将怎样?后面说明异常时再讨论所谓的内存泄露问题。
. P4 N% N4 }& [; d' v打造最好的电脑自学交流论坛    因此构造的意思就是刚分配了一块内存,还未初始化,则这块内存被称作原始数据(Raw Data),前面说过数字都必须映射成算法中的资源,则就存在数字的有效性。比如映射人的年龄,则这个数字就不能是负数,因为没有意义。所以当得到原始数据后,就应该先通过构造函数的调用以保证相应实例具有正确的意义。而析构函数就表示进行扫尾工作,就像上面,在某实例运作的期间(即cao作此实例的代码被执行的时期)动态分配了一些内存,则应确保其被正确释放。再或者这个实例和其他实例有关系,因确保解除关系(因为这个实例即将被销毁),如链表的某个结点用类映射,则这个结点被删除时应在其析构函数中解除它与其它结点的关系。我爱电脑技术论坛. v8 x8 e6 s! |8 f' e$ r/ {
我爱电脑技术论坛- t3 S" s4 U$ P# U

* G! j- k7 y" J2 }) \' q: n0 B我爱电脑技术社区--打造最好的电脑技术自学交流平台派生和继承2 Q# [8 c3 {# M( Y) d  S
电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站8 m4 B( u  N: J4 E% L) n5 p& Q7 C
    上面我们定义了类Radiogram来映射收音机,如果又需要映射数字式收音机,它和收音机一样,即收音机具有的东西它都具有,不过多了自动搜台、存储台、选台和删除台的功能。这里提出了一个类型体系,即一个实例如果是数字式收音机,那它一定也是收音机,即是收音机的一个实例。比如苹果和梨都是水果,则苹果和梨的实例一定也是水果的实例。这里提出三个类型:水果、苹果和梨。其中称水果是苹果的父类(父类型),苹果是水果的子类(子类型)。同样,水果也是梨的父类,梨是水果的子类。这种类型体系是很有意义的,因为人类就是用这种方式来认知世界的,它非常符合人类的思考习惯,因此C++又提出了一种特殊语法来对这种语义提供支持。打造最好的电脑自学交流论坛& W' }0 F4 E5 j5 a, C
    在定义自定义类型时,在类型名的后面接一“:”,然后接public或protected或private,接着再写父类的类型名,最后就是类型定义符“{}”及相关书写,如下:
2 ^, a' D6 a+ c. M% `打造最好的电脑自学交流论坛复制内容到剪贴板代码:- O/ q4 J6 q4 }- F4 e' P' E4 k
    class DigitalRadiogram : public Radiogramwww.520diannao.com" G% G! I; n3 Z9 W6 \* F
    {
5 [' O$ c- ^; p8 p: S电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站    protected:  double m_Stations[10];我爱电脑技术社区--打造最好的电脑技术自学交流平台0 ~) ^- T0 _: _
    public:     void SearchStation();                void SaveStation( unsigned long );打造最好的电脑自学交流论坛. I. M2 _. q  Q$ K" A2 n, a
                void SelectStation( unsigned long ); void EraseStation( unsigned long );8 X2 b0 t+ O% X, h
    };我爱电脑技术论坛, b& F. J( k, s3 t" X% m2 R9 C) H* ?
上面就将Radiogram定义为了DigitalRadiogram的父类,DigitalRadiogram定义成了Radiogram的子类,被称作类Radiogram派生了类DigitalRadiogram,类DigitalRadiogram继承了类Radiogram。
# Q* R* {% s8 \1 B3 z/ j) `4 w: Bwww.520diannao.com    上面生成了5个映射元素,就是上面的4个成员函数和1个成员变量,但实际不止。由于是从Radiogram派生,因此还将生成7个映射,就是类Radiogram的7个成员,但名字变化了,全变成DigitalRadiogram::修饰,而不是原来的Radiogram::修饰,但是类型却不变化。比如其中一个映射元素的名字就为DigitalRadiogram::m_bPowerOn,类型为bool Radiogram::,映射的偏移值没变,依旧为16。同样也有映射元素DigitalRadiogram::TurnFreq,类型为void ( Radiogram:: )( double ),映射的地址依旧没变,为Radiogram::TurnFreq所对应的地址。因此就可以如下:www.520diannao.com1 f* r9 H# V- j
    void DigitalRadiogram::SaveStation( unsigned long index )我爱电脑技术社区--打造最好的电脑技术自学交流平台5 U; f9 c$ ]3 K
    {
. e; D" W; z# m打造最好的电脑自学交流论坛        if( index >= 10 ) return;
. ~  C, B% J2 h我爱电脑技术社区--打造最好的电脑技术自学交流平台        m_Station[ index ] = m_Frequency; m_bPowerOn = true;电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站: v) C/ w* Q( j4 v( y7 O  n( w
    }
' D! F) u9 ?1 n$ t6 t电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站    DigitalRadiogram a; a.TurnFreq( 10 ); a.SaveStation( 3 );
0 y' ]  w% H* [1 Y电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站    上面虽然没有声明DigitalRadiogram::TurnFreq,但依旧可以调用它,因为它是从Radiogram派生来的。注意由于a.TurnFreq( 10 );没有书写全名,因此实际是a.DigitalRadiogram::TurnFreq( 10 );,因为成员cao作符左边的数字类型是DigitalRadiogram。如果DigitalRadiogram不从Radiogram派生,则不会生成上面说的7个映射,结果a.TurnFreq( 10 );将错误。我爱电脑技术论坛5 G0 H% |5 C' |. e
    注意上面的SaveStation中,直接书写了m_Frequency,其等同于this->m_Frequency,由于this是DigitalRadiogram*(因为在DigitalRadiogram::SaveStation的函数体内),所以实际为this->DigitalRadiogram::m_Frequency,也因此,如果不是派生自Radiogram,则上面将报错。并且由类型匹配,很容易知道:void ( Radiogram::*p )( double ) = DigitalRadiogram::TurnFreq;。虽然这里是DigitalRadiogram::TurnFreq,但它的类型是void ( Radiogram:: )( double )。
4 N5 K( @) M  x# J* @0 T打造最好的电脑自学交流论坛    应注意在SaveStation中使用了m_bPowerOn,这个在Radiogram中被定义成私有成员,也就是说子类也没权访问,而SaveStation是其子类的成员函数,因此上面将报错,权限不够。
5 G% W# K7 Y- L, X5 g$ g4 D我爱电脑技术社区--打造最好的电脑技术自学交流平台    上面通过派生而生成的7个映射元素各自的权限是什么?先看上面的派生代码:电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站% ]* z: E- E# @& d+ r
    class DigitalRadiogram : public Radiogram {…};
, V7 p, ~' A$ _0 g. n8 X8 s0 @" [电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站    这里由于使用public,被称作DigitalRadiogram从Radiogram公共继承,如果改成protected则称作保护继承,如果是private就是私有继承。有什么区别?通过公共继承而生成的映射元素(指从Radiogram派生而生成的7个映射元素),各自的权限属性不变化,即上面的DigitalRadiogram::m_Frequency对类DigitalRadiogram来说依旧是protected,而DigitalRadiogram::m_bPowerOn也依旧是private。保护继承则所有的公共成员均变成保护成员,其它不变。即如果保护继承,DigitalRadiogram::TurnFreq对于DigitalRadiogram来说将为protected。私有继承则将所有的父类成员均变成对于子类来说是private。因此上面如果私有继承,则DigitalRadiogram::TurnFreq对于DigitalRadiogram来说是private的。www.520diannao.com# T" t) o' t# v
    上面可以看得很简单,即不管是什么继承,其指定了一个权限,父类中凡是高于这个权限的映射元素,都要将各自的权限降低到这个权限(注意是对子类来说),然后再继承给子类。上面一直强调“对于子类来说”,什么意思?如下:打造最好的电脑自学交流论坛; ]# R6 C9 h& K+ P
复制内容到剪贴板代码:www.520diannao.com: \8 N4 [3 l9 w2 S0 h7 k
    struct A { long a; protected: long b; private: long c; };
& S7 N, h& H1 B/ f, w; ?7 {    struct B : protected A { void AB(); };
8 _$ b+ {$ s$ S, U$ R! q1 }6 P1 \: U; i; @    struct C : private B { void ABC(); };
$ v) j+ m! ]9 S7 H  U" wwww.520diannao.com    void B::AB() { b = 10; c = 10; }$ c3 b3 r& y; a: o) N9 M
    void C::ABC() { a = 10; b = 10; c = 10; AB(); }
! H; r( k" Y  i, a' G, j$ F我爱电脑技术社区--打造最好的电脑技术自学交流平台    A a; B b; C c; a.a = 10; b.a = 10; b.AB(); c.AB();电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站* t. s- z/ a: ]: ]9 ?
    上面的B的定义等同于struct B { protected: long a, b; private: long c; public: void AB(); };。www.520diannao.com. e" i9 t; I7 h& e* O
    上面的C的定义等同于struct C { private: long a, b, c; void AB(); public: void ABC(); };
* E) n; Y& ?( ?2 b打造最好的电脑自学交流论坛因此,B::AB中的b = 10;没有问题,但c = 10;有问题, 因为编译器看出B::c是从父类继承生成的,而它对于父类来说是私有成员,因此子类无权访问,错误。接着看C::ABC,a = 10;和b = 10;都没问题,因为它们对于B来说都是保护成员,但c = 10;将错误,因为C::c对于父类B来说是私有成员,没有权限,失败。接着AB();,因为C::AB对于父类B来说是公共成员,没有问题。
' `  s' \" I* x" F我爱电脑技术论坛    接着是a.a = 10;,没问题;b.a = 10;,错误,因为B::a是B的保护成员;b.AB();,没有问题;c.AB();,错误,因为C::AB是C的私有成员。应注意一点:public、protected和private并不是类型修饰符,只是在语法上提供了一些信息,而继承所得的成员的类型都不会变化,不管它保护继承还是公共继承,权限起作用的地方是需要运用成员的地方,与类型没有关系。什么叫运用成员的地方?如下:我爱电脑技术论坛4 k- i" I4 _4 h" @7 V' a% W" r4 A
    long ( A::*p ) = &A::a; p = &A::b;www.520diannao.com9 g2 {" L; E4 y, J4 @/ v0 E. `
    void ( B::*pB )() = B::AB; void ( C::*pC )() = C::ABC; pC = C::AB;
# n8 ?4 I- [! f' lwww.520diannao.com    上面对变量p的初始化cao作没有问题,这里就运用了A::a。但是在p = &A::b;时,由于运用了A::b,则编译器就要检查代码所处的地方,发现对于A来说属于外界,因此报错,权限不够。同样下面对pB的赋值没有问题,但pC = C::AB;就错误。而对于b.a = 10;,这里由于成员cao作符而运用了类B的成员B::a,所以在这里进行权限检查,并进而发现权限不够而报错。www.520diannao.com/ }7 [0 P" ]: C
    好,那为什么要搞得这么复杂?弄什么保护、私有和公共继承?首先回想前面说的为什么要提供继承,因为想从代码上体现类型体系,说明一个实例如果是一个子类的实例,则它也一定是一个父类的实例,即可以按照父类的定义来cao作它。虽然这也可以通过之前说的转换指针类型来实现,但前者能直接从代码上表现出类型继承的语义(即子类从父类派生而来),而后者只能说明用不同的类型来看待同一个实例。
+ \2 p2 `% ?- G8 w* ~    那为什么要给继承加上权限?表示这个类不想外界或它的子类以它的父类的姿态来看待它。比如鸡可以被食用,但做成标本的鸡就不能被食用。因此子类“鸡的标本”在继承时就应该保护继承父类“鸡”,以表示不准外界(但准许其派生类)将它看作是鸡。它已经不再是鸡,但它实际是由鸡转变过来的。因此私有和保护继承实际很适合表现动物的进化关系。比如人是猴子进化来的,但人不是猴子。这里人就应该使用私有继承,因为并不希望外界和人的子类——黑种人、黄种人、白种人等——能够把父类“人”看作是猴子。而公共继承就表示外界和子类可以将子类的实例看成父类的实例。如下:
1 |1 H( e0 I  Z: T& _7 |1 c复制内容到剪贴板代码:  ~7 ^, g2 r! L$ c" o7 _
struct A { long a, b; };打造最好的电脑自学交流论坛2 r: u0 {& d  U. A
struct AB : private A { long c; void ABCD(); };打造最好的电脑自学交流论坛1 H2 h" O7 A+ s; X$ F  g
struct ABB : public AB { void AAA(); };我爱电脑技术社区--打造最好的电脑技术自学交流平台0 H/ p8 D. E* g" s
struct AC : public A { long c; void ABCD(); };www.520diannao.com% _0 b! i: F3 d' e/ l
void ABC( A *a ) { a->a = 10; a->b = 20; }
) }% V; ?6 i% D; `9 }, V5 d我爱电脑技术论坛void main() { AB b; ABC( &b ); AC c; ABC( &c ); }我爱电脑技术社区--打造最好的电脑技术自学交流平台) F4 Y2 I8 I+ r. ^. {7 O' z
void AB::ABCD() { AB b; ABC( &b ); }- ^; y( q/ {" l, @7 \9 U: a: N
void AC::ABCD() { AB b; ABC( &b ); }
3 L# r. Y. `+ p, ]打造最好的电脑自学交流论坛void ABB::AAA() { AB b; ABC( &b ); }我爱电脑技术论坛9 ^5 P6 c* ^, I. ?
上面的类AC是公共继承,因此其实例c在执行ABC( &c );时将由编译器进行隐式类型转换,这是一个很奇特的特性,本文的下篇将说明。但类AB是私有继承,因此在ABC( &b );时编译器不会进行隐式类型转换,将报错,类型不匹配。对于此只需ABC( ( A* )&b );以显示进行类型转换就没问题了。
7 C& J9 k8 G3 a1 e$ G8 w' @! D我爱电脑技术论坛    注意前面的红字,私有继承表示外界和它的子类都不可以用父类的姿态来看待它,因此在ABB::AAA中,这是AB的子类,因此这里的ABC( &b );将报错。在AC::ABCD中,这里对于AB来说是外界,报错。在AB::ABCD中,这里是自身,即不是子类也不是外界,所以ABC( &b );将没有问题。如果将AB换成保护继承,则在ABB::AAA中的ABC( &b );将不再错误。我爱电脑技术论坛  g+ I/ z  G; A6 j* h  |6 O
    关于本文及本文下篇所讨论的语义,在《C++从零开始(十二)》中会专门提出一个概念以给出一种方案来指导如何设计类及各类的关系。由于篇幅限制,本文分成了上下两篇,剩下的内容在本文下篇说明。

TOP

C++从零开始(十一)(中)——类的相关知识我爱电脑技术论坛/ \% G9 |" _% g# g3 h
由于篇幅限制,本篇为《C++从零开始(十一)》的中篇,说明多重继承、虚继承和虚函数的实现方式。
+ J1 |/ i* K$ |% P$ U" `打造最好的电脑自学交流论坛打造最好的电脑自学交流论坛% x- \/ E+ c' |- R7 O' s. R5 u  o

: l( |6 |3 s& _1 D, V我爱电脑技术论坛多重继承打造最好的电脑自学交流论坛. H% l7 K1 o8 l1 |  Q* g/ f. D

( ~9 x# u( T  q/ f1 C1 v6 o: u我爱电脑技术社区--打造最好的电脑技术自学交流平台    这里有个有趣的问题,如下:电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站* b8 U4 F+ O7 |
    struct A { long a, b, c; char d; }; struct B : public A { long e, f; };
: F- v) u: _) L我爱电脑技术论坛    上面的B::e和B::f映射的偏移是多少?不同的编译器有不同的映射结果,对于派生的实现,C++并没有强行规定。大多数编译器都是让B::e映射的偏移值为16(即A的长度,关于自定义类型的长度可参考《C++从零开始(九)》),B::f映射20。这相当于先把空间留出来排列父类的成员变量,再排列自己的成员变量。但是存在这样的语义——西红柿即是蔬菜又是水果,鲸鱼即是海洋生物又是脯乳动物。即一个实例既是这种类型又是那种类型,对于此,C++提供了多重派生或称多重继承,用“,”间隔各父类,如下:我爱电脑技术论坛1 e, |+ m" U8 a% @+ f; U8 }
复制内容到剪贴板代码:) H( V# K' V; k+ z7 }
    struct A { long A_a, A_b, c; void ABC(); }; struct B { long c, B_b, B_a; void ABC(); };1 G7 Z* C( S5 [% W1 M7 N% m
    struct AB : public A, public B { long ab, c; void ABCD(); };
+ j: t' w" [1 [; z; c; i打造最好的电脑自学交流论坛    void A::ABC() { A_a = A_b = 10; c = 20; }www.520diannao.com/ U: q2 s) R9 D1 _/ P! v3 t& I) D
    void B::ABC() { B_a = B_b = 20; c = 10; }4 a! d, r0 R  O  ?
    void AB::ABCD() { A_a = B_a = 1; A_b = B_b = 2; c = A::c = B::c = 3; }
8 M6 I5 U8 _" y* h3 _打造最好的电脑自学交流论坛    void main() { AB ab; ab.A_a = 3; ab.B_b = 4; ab.ABC(); }www.520diannao.com7 e! ~# m1 K& h  G% G
上面的结构AB从结构A和结构B派生而来,即我们可以说ab既是A的实例也是B的实例,并且还是AB的实例。那么在派生AB时,将生成几个映射元素?照前篇的说法,除了AB的类型定义符“{}”中定义的AB::ab和AB::c以外(类型均为long AB::),还要生成继承来的映射元素,各映射元素名字的修饰换成AB::,类型不变,映射的值也不变。因此对于两个父类,则生成8个映射元素(每个类都有4个映射元素),比如其中一个的名字为AB::A_b,类型为long A::,映射的值为4;也有一个名字为AB::B_b,类型为long B::,映射的值依旧为4。注意A::ABC和B::ABC的名字一样,因此其中两个映射元素的名字都为AB::ABC,但类型则一个为void( A:: )()一个为void( B:: )(),映射的地址分别为A::ABC和B::ABC。同样,就有三个映射元素的名字都为AB::c,类型则分别为long A::、long B::和long AB::,映射的偏移值依次为8、0和28。照前面说的先排列父类的成员变量再排列子类的成员变量,因此类型为long AB::的AB::c映射的值为两个父类的长度之和再加上AB::ab所带来的偏移。
" S+ Y' K& z. ?3 A* n$ e. s打造最好的电脑自学交流论坛    注意问题,上面继承生成的8个映射元素中有两对同名,但不存在任何问题,因为它们的类型不同,而最后编译器将根据它们各自的类型而修改它们的名字以形成符号,这样连接时将不会发生重定义问题,但带来其他问题。ab.ABC();一定是ab.AB::ABC();的简写,因为ab是AB类型的,但现在由于有两个AB::ABC,因此上面直接书写ab.ABC将报错,因为无法知道是要哪个AB::ABC,这时怎么办?
$ E1 M0 }: I2 q4 R6 M% O    回想本文上篇提到的公共、保护、私有继承,其中说过,公共就表示外界可以将子类的实例当作父类的实例来看待。即所有需要用到父类实例的地方,如果是子类实例,且它们之间是公共继承的关系,则编译器将会进行隐式类型转换将子类实例转换成父类实例。因此上面的ab.A_a = 3;实际是ab.AB::A_a = 3;,而AB::A_a的类型是long A::,而成员cao作符要求两边所属的类型相同,左边类型为AB,且AB为A的子类,因此编译器将自动进行隐式类型转换,将AB的实例变成A的实例,然后再计算成员cao作符。我爱电脑技术论坛" v9 y* ?1 p* p( ?2 R
    注意前面说AB::A_b和AB::B_b的偏移值都为4,则ab.A_b = 3;岂不是等效于ab.B_b = 3;?即使按照上面的说法,由于AB::A_b和AB::B_b的类型分别是long A::和long B::,也最多只是前者转换成A的实例后者转换成B的实例,AB::A_b和AB::B_b映射的偏移依旧没变啊。因此变的是成员cao作符左边的数字。对于结构AB,假设先排列父类A的成员变量再排列父类B的成员变量,则AB::B_b映射的偏移就应该为16(结构A的长度加上B::c引入的偏移),但它实际映射为4,因此就将成员cao作符左侧的地址类型的数字加上12(结构A的长度)。而对于AB::A_b,由于结构A的成员变量先被排列,故只偏移0。假设上面ab对应的地址为3000,对于ab.B_b = 4;,AB类型的地址类型的数字3000在“.”的左侧,转成B类型的地址类型的数字3012(因为偏移12),然后再将“.”右侧的偏移类型的数字4加上3012,最后返回类型为long的地址类型的数字3016,再继续计算“=”。同样也可知道ab.A_a = 3;中的成员cao作符最后返回long类型的地址类型的数字3000,而ab.A_b将返回3004,ab.ab将返回3024。
1 c  A8 q  B; p$ O. y    同样,这样也将进行隐式类型转换long AB::*p = &AB::B_b;。注意AB::B_b的类型为long B::,则将进行隐式类型转换。如何转换?原来AB::B_b映射的偏移为4,则现在将变成12+4=16,这样才能正确执行ab.*p = 10;。
2 `0 C6 K; v3 F- w1 b& W* ^电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站    这时再回过来想刚才提的问题,AB::ABC无法区别,怎么办?注意还有映射元素A::ABC和B::ABC(两个AB::ABC就是由于它们两个而导致的),因此可以书写ab.A::ABC();来表示调用的是映射到A::ABC的函数。这里的A::ABC的类型是void( A:: )(),而ab是AB,因此将隐式类型转换,则上面没有任何语法问题(虽然说A::ABC不是结构AB的成员,但它是AB的父类的成员,C++允许这种情况,也就是说A::ABC的名字也作为类型匹配的一部分而被使用。如假设结构C也从A派生,则有C::a,但就不能书写ab.C::a,因为从C::a的名字可以知道它并不属于结构AB)。同样ab.B::ABC();将调用B::ABC。注意上面结构A、B和AB都有一个成员变量名字为c且类型为long,那么ab.c = 10;是否会如前面ab.ABC();一样报错?不会,因为有三个AB::c,其中有一个类型和ab的类型匹配,其映射的偏移为28,因此ab.c将会返回3028。而如果期望运用其它两个AB::c的映射,则如上通过书写ab.A::c和ab.B::c来偏移ab的地址以实现。我爱电脑技术论坛# P" B- @# g9 n4 k. X+ A! l
    注意由于上面的说法,也就可以这样:void( AB::*pABC )() = B::ABC; ( ab.*pABC )();。这里的B::ABC的类型为void( B:: )(),和pABC不匹配,但正好B是AB的父类,因此将进行隐式类型转换。如何转换?因为B::ABC映射的是地址,而隐式类型转换要保证在调用B::ABC之前,先将this的类型变成B*,因此要将其加12以从AB*转变成B*。由于需要加这个12,但B::ABC又不是映射的偏移值,因此pABC实际将映射两个数字,一个是B::ABC对应的地址,一个是偏移值12,结果pABC这个指针的长度就不再如之前所说的为4个字节,而变成了8个字节(多出来的4个字节用于记录偏移值)。电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站6 u9 W+ B5 ?' H
    还应注意前面在AB::ABCD中直接书写的A_b、c、A::c等,它们实际都应该在前面加上this->,即A_b = B_b = 2;实际为this->A_b = this->B_b = 2;,则同样如上,this被偏移了两次以获得正确的地址。注意上面提到的隐式类型转换之所以会进行,是因为继承时的权限满足要求,否则将失败。即如果上面AB保护继承A而私有继承B,则只有在AB的成员函数中可以如上进行转换,在AB的子类的成员函数中将只能使用A的成员而不能使用B的成员,因为权限受到限制。如下将失败。
( w0 ?( L' P6 @! j; {  u电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站    struct AB : protected A, private B {…};
5 p. @; ?6 f9 ]1 X' P我爱电脑技术社区--打造最好的电脑技术自学交流平台    struct C : public AB { void ABCD(); };我爱电脑技术论坛: }( t/ c9 s3 o! D6 I6 d6 w
    void C::ABCD() { A_b = 10; B_b = 2; c = A::c = B::c = 24; }- [9 Q1 p& Y, q0 y, E4 H5 T
    这里在C::ABCD中的B_b = 2;和B::c = 24;将报错,因为这里是AB的子类,而AB私有继承自B,其子类无权将它看作B。但只是不会进行隐式类型转换罢了,依旧可以通过显示类型转换来实现。而main函数中的ab.A_a = 3; ab.B_b = 4; ab.A::ABC();都将报错,因为这是在外界发起的调用,没有权限,不会自动进行隐式类型转换。我爱电脑技术论坛/ y6 u' s" w/ X& `6 m0 b. g
    注意这里C::ABCD和AB::ABCD同名,按照上面所说,子类的成员变量都可以和父类的成员变量同名(上面AB::c和A::c及B::c同名),成员函数就更没有问题。只用和前面一样,按照上面所说进行类型匹配检验即可。应注意由于是函数,则可以参数变化而函数名依旧相同,这就成了重载函数。
8 V' f9 H% b+ f0 ]* ^! K9 ^电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站打造最好的电脑自学交流论坛4 z$ s/ Q7 W1 B& Q- @4 K5 K
: r7 Q# o. D$ E8 ?3 _
虚继承
2 D, ~1 `/ e1 k, S8 r电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站+ V, g/ ~4 g( K3 G
    前面已经说了,当生成了AB的实例,它的长度实际应该为A的长度加B的长度再加上AB自己定义的成员所占有的长度。即AB的实例之所以又是A的实例又是B的实例,是因为一个AB的实例,它既记录了一个A的实例又记录了一个B的实例。则有这么一种情况——蔬菜和水果都是植物,海洋生物和脯乳动物都是动物。即继承的两个父类又都从同一个类派生而来。假设如下:打造最好的电脑自学交流论坛$ H. D% B4 n% {
复制内容到剪贴板代码:电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站- F/ M8 ~: T1 p* H5 c  G7 B
    struct A { long a; };打造最好的电脑自学交流论坛  H, g3 q( T% R1 ~& K- I
    struct B : public A { long b; }; struct C : public A { long c; };
# j6 g+ d; O  s2 j( O4 O打造最好的电脑自学交流论坛    struct D : public A, public C { long d; };
' ^9 r5 b3 `; w# E, {# C电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站    void main() { D d; d.a = 10; }我爱电脑技术论坛1 y% z, j( W" r, I
上面的B的实例就包含了一个A的实例,而C的实例也包含了一个A的实例。那么D的实例就包含了一个B的实例和一个C的实例,则D就包含了两个A的实例。即D定义时,将两个父类的映射元素继承,生成两个映射元素,名字都为D::a,类型都为long A::,映射的偏移值也正好都为0。结果main函数中的d.a = 10;将报错,无法确认使用哪个a。这不是很奇怪吗?两个映射元素的名字、类型和映射的数字都一样!编译器为什么就不知道将它们定成一个,因为它们实际在D的实例中表示的偏移是不同的,一个是0一个是8。同样,为了消除上面的问题,就书写d.B::a = 1; d.C::a = 2;以表示不同实例中的成员a。可是B::a和C::a的类型不都是为long A::吗?但上面说过,成员变量或成员函数它们自身的名字也将在类型匹配中起作用,因此对于d.B::a,因为左侧的类型是D,则看右侧,其名字表示为B,正好是D的父类,先隐式类型转换,然后再看类型,是A,再次进行隐式类型转换,然后返回数字。假设上面d对应的地址为3000,则d.C::a先将d这个实例转换成C的实例,因此将3000偏移8个字节而返回long类型的地址类型的数字3008。然后再转换成A的实例,偏移0,最后返回3008。
. l* m8 A2 ^. N! v! gwww.520diannao.com    上面说明了一个问题,即希望从A继承来的成员a只有一个实例,而不是像上面那样有两个实例。假设动物都有个饥饿度的成员变量,很明显地鲸鱼应该只需填充一个饥饿度就够了,结果有两个饥饿度就显得很奇怪。对此,C++提出了虚继承的概念。其格式就是在继承父类时在权限语法的前面加上关键字virtual即可,如下:
1 D% o  Z' D  ~/ d. U电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站    struct A { long a, aa, aaa; void ABC(); }; struct B : virtual public A { long b; };打造最好的电脑自学交流论坛3 Y1 N( Z1 M$ O, R- c! C7 e# Z
    这里的B就虚继承自A,B::b映射的偏移为多少?将不再是A的长度12,而是4。而继承生成的3个映射元素还是和原来一样,只是名字修饰变成B::而已,映射依旧不变。那么为什么B::b是4?之前的4个字节用来放什么?上面等同于下面:电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站5 Y4 R9 `9 e& @9 N2 E1 l6 a2 U
    struct B { long *p; long b; long a, aa, aaa; void ABC(); };
8 }5 y& V6 l- G( t电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站    long BDiff[] = { 0, 8 }; B::B(){ p = BDiff; }打造最好的电脑自学交流论坛$ K5 D- O8 g/ s, J
    上面的B::p指向一全局数组BDiff。什么意思?B的实例的开头4个字节用来记录一个地址,也就相当于是一个指针变量,它记录的地址所标识的内存中记录着由于虚继承而导致的偏移值。上面的BDiff[1]就表示要将B实例转成A实例,就需要偏移BDiff[1]的值8,而BDiff[0]就表示要将B实例转成B实例需要的偏移值0。为什么还要来个B实例转B实例?后面说明。但为什么是数组?因为一个类可以通过多重派生而虚继承多个类,每个类需要的偏移值都会在BDiff的数组中占一个元素,它被称作虚类表(Virtual Class Table)。
+ W  H8 X! E2 J; K! }  m9 q    因此当书写B b; b.aaa = 20; long a = sizeof( b );时,a的值为20,因为多了一个4字节来记录上面说的指针。假设b对应的地址为3000。先将B的实例转换成A的实例,本来应该偏移12而返回3012,但编译器发现B是虚继承自A,则通过B::p[1]得到应该的偏移值8,然后返回3008,接着再加上B::aaa映射的8而返回3016。同样,当b.b = 10;时,由于B::b并不是被虚继承而来,直接将3000加上B::b映射的偏移值4得3004。而对于b.ABC();将先通过B::p[1]将b转成A的实例然后调用A::ABC。
& p2 @& v- H3 F: M' X我爱电脑技术论坛    为什么要像上面那样弄得那么麻烦?首先让我们来了解什么叫做虚(Virtual)。虚就是假象,并不是真的。比如一台老式电视机有10个频道,即它最多能记住10个电视台的频率。因此可以说1频道是中央1台、5频道是中央5台、7频道是四川台。这里就称频道对我们来说代表着电台频率是虚假的,因为频道并不是电台频率,只是记录了电台频率。当我们按5频道以换到中央5台时,有可能有人已经调过电视使得5频道不再是中央5台,而是另一个电视台或者根本就是一片雪花没有信号。因此虚就表示不保证,其可能正确可能错误,因为它一定是间接得到的,其实就相当于之前说的引用。有什么好处?只用记着按5频道就是中央5台,当以后不想再看中央5台而换成中央2台,则同样的“按5频道”却能得到不同的结果,但是程序却不用再编写了,只用记着“按5频道”就又能实现换到中央2台看。所以虚就是间接得到结果,由于间接,结果将不确定而显得更加灵活,这在后面说明虚函数时就能看出来。但虚的坏处就是多了一道程序(要间接获得),效率更低。我爱电脑技术社区--打造最好的电脑技术自学交流平台: Q, d3 `% V; L. l  o, h7 K
    由于上面的虚继承,导致继承的元素都是虚的,即所有对继承而来的映射元素的cao作都应该间接获得相应映射元素对应的偏移值或地址,但继承的映射元素对应的偏移值或地址是不变的,为此红字的要求就只有通过隐式类型转换改变this的值来实现。所以上面说的B转A需要的偏移值通过一个指针B::p来间接获得以表现其是虚的。打造最好的电脑自学交流论坛1 H9 c$ K8 W4 T, M! H. v
    因此,开始所说的鲸鱼将会有两个饥饿度就可以让海洋生物和脯乳动物都从动物虚继承,因此将间接使用脯乳动物和海洋生物的饥饿度这个成员,然后在派生鲸鱼这个类时,让脯乳动物和海洋生物都指向同一个动物实例(因为都是间接获得动物的实例的,通过虚继承来间接使用动物的成员),这样当鲸鱼填充饥饿度时,不管填充哪个饥饿度,实际都填充同一个。而C++也正好这样做了。如下:
% E  v( `  L) C9 K: N我爱电脑技术社区--打造最好的电脑技术自学交流平台复制内容到剪贴板代码:www.520diannao.com6 }' B! \9 e7 _7 _$ w8 S: ]3 R
    struct A { long a; };
. `9 u$ Q6 V; e! _我爱电脑技术论坛    struct B : virtual public A { long b; }; struct C : virtual public A { long c; };打造最好的电脑自学交流论坛( _/ s  J7 l* X% G" d6 N
    struct D : public B, virtual public C { long d; };www.520diannao.com0 `/ R' b3 D8 G  [4 L4 w' m" H( P3 L
    void main() { D d; d.a = 10; }
2 U/ u* D5 h% e& \电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站当从一个类虚继承时,在排列派生类时(就是决定在派生类的类型定义符“{}”中定义的各成员变量的偏移值),先排列前面提到的虚类表的指针以实现间接获取偏移值,再排列各父类,但如果父类中又有被虚继承的父类,则先将这些部分剔除。然后排列派生类自己的映射元素。最后排列刚刚被剔除的被虚继承的类,此时如果发现某个被虚继承的类已经被排列过,则不用再重复排列一遍那个类,并且也不再为它生成相应的映射元素。www.520diannao.com; D+ `1 {$ N7 l+ b; f9 ~  O4 @
    对于上面的B,发现虚继承A,则先排列前面说过的B::p,然后排列A,但发现A需要被虚继承,因此剔除,排列自己定义的映射元素B::b,映射的偏移值为4(由于B::p的占用)。最后排列A而生成继承来的映射元素B::a,所以B的长度为12。
' O! D) J0 J2 ^1 y- G; n8 l) w我爱电脑技术论坛    对于上面的D,发现要从C虚继承,因此:www.520diannao.com2 o9 s1 Q8 K% {* M
    排列D::p,占4个字节。
5 y- }- R7 i8 G3 ?www.520diannao.com    排列父类B,发现其中的A是被虚继承的,剔除,所以将继承映射元素B::b(还有前面编译器自动生成的B::p),生成D::b,占4个字节(编译器将B::p和D::p合并为一个,后面说明虚函数时就了解了)。www.520diannao.com" v9 a% d9 z  [5 Y) r
    排列父类C,发现C需要被虚继承,剔除。
: X7 _. H9 q( P3 b  f我爱电脑技术社区--打造最好的电脑技术自学交流平台    排列D自己定义的成员D::d,其映射的偏移值就为4+4=8,占4个字节。www.520diannao.com: u6 x4 ~6 @/ T8 l+ B
    排列A和C,先排列A,占4个字节,生成D::a。打造最好的电脑自学交流论坛  O0 E  ~; l* [/ Y# y' k/ f' [" U" {$ Z
    排列C,先排列C中的A,结果发现它是虚继承的,并发现已经排列过A,进而不再为C::a生成映射元素。接着排列C::p和C::c,占8个字节,生成D::c。打造最好的电脑自学交流论坛9 _6 o% X: }5 r+ C, r( R7 n
    所以最后结构D的长度为4+4+4+4+8=24个字节,并且只有一个D::a,类型为long A::,偏移值为0。
3 B( l, `) r  ]8 M  g: l8 s( s    如果上面很昏,不要紧,上面只是给出一种算法以实现虚继承,不同的编译器厂商会给出不同的实现方法,因此上面推得的结果对某些编译器可能并不正确。不过应记住虚继承的含义——被虚继承的类的所有成员都必须被间接获得,至于如何间接获得,则不同的编译器有不同的处理方式。打造最好的电脑自学交流论坛: G* |# i& _# G) u* n
    由于需要保证间接获得,所以对于long D::*pa = &D::a;,由于是long D::*,编译器发现D的继承体系中存在虚继承,必须要保证其某些成员的间接获得,因此pa中放的将不再是偏移值,否则d.*pa = 10;将导致直接获得偏移值(将pa的内容取出来即可),违反了虚继承的含义。为了要间接访问pa所记录的偏移值,则必须保证代码执行时,当pa里面放的是D::a时会间接,而D::d时则不间接。很明显,这要更多和更复杂的代码,大多数编译器对此的处理就是全部都使用间接获得。因此pa的长度将为8字节,其中一个4字节记录偏移,还有一个4字节记录一个序号。这个序号则用于前面说的虚类表以获得正确的因虚继承而导致的偏移量。因此前面的B::p所指的第一个元素的值表示B实例转换成B实例,是为了在这里实现全部间接获得而提供的。
  T! p0 }1 Z& r  J% D我爱电脑技术社区--打造最好的电脑技术自学交流平台    注意上面的D::p对于不同的D的实例将不同,只不过它们的内容都相同(都是结构D的虚类表的地址)。当D的实例刚刚生成时,那个实例的D::p的值将是一随机数。为了保证D::p被正确初始化,上面的结构D虽然没有生成构造函数,但编译器将自动为D生成一缺省构造函数(没有参数的构造函数)以保证D::p和上面从C继承来的C::p的正确初始化,结果将导致D d = { 23, 4 };错误,因为D已经定义了一个构造函数,即使没有在代码上表现出来。
/ q' i) {$ ~# z5 R/ O打造最好的电脑自学交流论坛    那么虚继承有什么意义呢?它从功能上说是间接获得虚继承来的实例,从类型上说与普通的继承没有任何区别,即虚继承和前面的public等一样,只是一个语法上的提供,对于数字的类型没有任何影响。在了解它的意义之前先看下虚函数的含义。
3 b! A# x% g! l8 \. E" V我爱电脑技术社区--打造最好的电脑技术自学交流平台
9 e# g1 |2 {+ U$ A; T2 |我爱电脑技术社区--打造最好的电脑技术自学交流平台电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站9 `( u* I+ M( D
虚函数www.520diannao.com: ^# o% Z, v/ W+ u- ~+ y* |

; J7 |$ I+ y# z/ l( }. @    虚继承了一个函数类型的映射元素,按照虚继承的说法,应该是间接获得此函数的地址,但结果却是间接获得this参数的值。为了间接获得函数的地址,C++又提出了一种语法——虚函数。在类型定义符“{}”中书写函数声明或定义时,在声明或定义语句前加上关键字virtual即可,如下:
' z1 H: k5 v0 ]9 @: X! h我爱电脑技术社区--打造最好的电脑技术自学交流平台复制内容到剪贴板代码:
* m9 ^+ l2 i% S2 t1 S' m7 I2 [打造最好的电脑自学交流论坛    struct A { long a; virtual void ABC(), BCD(); };
+ y/ @0 G; \. [9 M; ]我爱电脑技术论坛    void A::ABC() { a = 10; } void A::BCD() { a = 5; }我爱电脑技术社区--打造最好的电脑技术自学交流平台% q, o' y% s. ~# f2 w
    上面等同于下面:3 {8 r4 I/ E" \* b( \4 X# H9 ^4 J
    struct A { void ( A::*pF )(); long a; void ABC(), BCD(); A(); };我爱电脑技术论坛9 X) d% ]9 W4 T6 Y
    void A::ABC() { a = 10; } void A::BCD() { a = 5; }( _5 Z# u% j( X6 i0 S( c0 Q
    void ( A::*AVF[] )() = { A::ABC, A::BCD }; void A::A() { pF = AVF; }
- l" s& {# B" k' }: o6 F6 V. j, u我爱电脑技术论坛这里A的成员A::pF和之前的虚类表一样,是一个指针,指向一个数组,这个数组被称作虚函数表(Virtual Function Table),是一个函数指针的数组。这样使用A::ABC时,将通过给出A::ABC在A::pF中的序号,由A::pF间接获得,因此A a; a.ABC();将等同于( a.*( a.pF[0] ) )();。因此结构A的长度是8字节,再看下面的代码:! H/ v* n8 }; G1 n. {; [5 v4 ^# X
    struct B : public A { long b; void ABC(); }; struct C : public A { long c; virtual void ABC(); };
8 T6 Y1 z6 z& J( U) ~" h打造最好的电脑自学交流论坛    struct BB : public B { long bb; void ABC(); }; struct CC : public C { long cc; void ABC(); };打造最好的电脑自学交流论坛5 @2 F2 m, N, H, j: ]* V
    void main() { BB bb; bb.ABC(); CC cc; cc.cc = 10; }
( t+ C8 m) ]' R, W我爱电脑技术论坛    首先,上面执行bb.ABC()但没有给出BB::ABC或B::ABC的定义,因此上面虽然编译通过,但连接时将失败。其次,上面没有执行cc.ABC();但连接时却会说CC::ABC未定义以表示这里需要CC::ABC的地址,为什么?因为生成了CC的实例,而CC::pF就需要在编译器自动为CC生成的缺省构造函数中被正确初始化,其需要CC::ABC的地址来填充。接着,给出如下的各函数定义。. ~* O( \. H8 _; y4 y
    void B::ABC() { b = 13; } void C::ABC() { c = 13; }
8 h# S' T( @: }% V% J2 ^) I$ Uwww.520diannao.com    void BB::ABC() { bb = 13; b = 10; } void CC::ABC() { cc = 13; c = 10; }我爱电脑技术论坛" X5 J& I6 k9 K
    如上后,对于bb.ABC();,等同于bb.BB::ABC();,虽然有三个BB::ABC的映射元素,但只有一个映射元素的类型为void( BB:: )(),其映射BB::ABC的地址。由于BB::ABC并没有用virtual修饰,因此上面将等同于bb.BB::ABC();而不是( bb.*( pF[0] ) )();,bb将为13。对于cc.ABC();也是同样的,cc将为13。; E; M% V" K# C) X, f
    对于( ( B* )&bb )->ABC();,因为左侧类型为B*,因此将为( ( B* )&bb )->B::ABC();,由于B::ABC并没被定义成虚函数,因此这里等同于( ( B* )&bb )->B::ABC();,b将为13。对于( ( C* )&cc )->ABC();,同样将为( ( C* )&cc )->C::ABC();,但C::ABC被修饰成虚函数,则前面等同于C *pC = &cc; ( pC->*( pC->pF[0] ) )();。这里先将cc转换成C的实例,偏移0。然后根据pC->pF[0]来间接获得函数的地址,为CC::ABC,c将为10。因为cc是CC的实例,在其被构造时将填充cc.pF,那么如下:www.520diannao.com" m- M1 U) p$ J! o
    void ( CC::*CCVF[] )() = { CC::ABC, CC::BCD }; CC::CC() { cc.pF = &CCVF; }电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站' V( o2 l! z4 J  l
    因此导致pC->ABC();结果调用的竟是CC::ABC而不是C::ABC,这正是由于虚的缘故而间接获得函数地址导致的。同样道理,对于( ( A* )&cc )->ABC();和( ( A* )&bb )->ABC();都将分别调用CC::ABC和BB::ABC。但请注意,( pC->*( pC->pF[0] ) )();中,pC是C*类型的,而pC->pF[0]返回的CC::ABC是void( CC:: )()类型的,而上面那样做将如何进行实例的隐式类型转换?如果不进行将导致cao作错误的成员。可以像前面所说,让CCVF的每个成员的长度为8个字节,另外4个字节记录需要进行的偏移。但大多数类其实并不需要偏移(如上面的CC实例转成A实例就偏移0),此法有些浪费资源。VC对此给出的方法如下,假设CC::ABC对应的地址为6000,并假设下面标号P处的地址就为6000,而CC::A_thunk对应的地址为5990。
+ b. d( ~1 L0 v& J; h9 C    void CC::A_thunk( void *this )我爱电脑技术社区--打造最好的电脑技术自学交流平台% l0 b7 J' N; V4 p( ?2 j9 q$ C
    {
$ }/ A; f( k: s9 v: f, }: v电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站        this = ( ( char* )this ) + diff;www.520diannao.com' [% }: H, D6 T' v; b
    P:
. a) |3 ~# d3 x' L6 x) r我爱电脑技术论坛        // CC::ABC的正常代码打造最好的电脑自学交流论坛" g% k# s) g$ Q' ]# a/ v
    }我爱电脑技术论坛1 S( o+ |0 x, y: p$ D- e1 [3 s
    因此pC->pF[0]的值为5990,而并不是CC::ABC对应的6000。上面的diff就是相应的偏移,对于上面的例子,diff应该为0,所以实际中pC->pF[0]的值还是6000(因为偏移为0,没必要是5990)。此法被称作thunk,表示完成简单功能的短小代码。对于多重继承,如下:
' L' ~0 ?& C: C4 y$ r打造最好的电脑自学交流论坛    struct D : public A { long d; };
& {( H( G, a6 B$ f5 o/ ^    struct E : public B, public C, public D { long e; void ABC() { e = 10; } };打造最好的电脑自学交流论坛; Q( N) }3 s: j
    上面将有三个虚函数表,因为B、C和D都各自带了一个虚函数表(因为从A派生)。结果上面等同于:www.520diannao.com  ?* L" ~  u. ?& M- y2 N5 K
复制内容到剪贴板代码:
' H9 R& ?2 k/ e8 j" m& F    struct E8 V) ^9 u/ H; Y( z" d6 Y4 k
    {打造最好的电脑自学交流论坛6 c, f" Z% v7 d
        void ( E::*B_pF )(); long B_a, b;我爱电脑技术论坛& ~  C+ M! ], w# x- a8 ]' }: p
        void ( E::*C_pF )(); long C_a, c;
) G( D0 U9 @( |# \( w, \: q1 _我爱电脑技术社区--打造最好的电脑技术自学交流平台        void ( E::*D_pF )(); long D_a, d; long e; void ABC() { e = 10; } E();我爱电脑技术社区--打造最好的电脑技术自学交流平台2 @! a# ?# ~3 }( r7 k
        void E_C_thunk_ABC() { this = ( E* )( ( ( char* )this ) – 12 ); ABC(); }
+ D$ |+ p1 ]1 c打造最好的电脑自学交流论坛        void E_D_thunk_ABC() { this = ( E* )( ( ( char* )this ) – 24 ); ABC(); }
! D" V7 m& H# A4 g* }打造最好的电脑自学交流论坛    };电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站9 X4 \% m) G; x
    void ( E::*E_BVF[] )() = { E::ABC, E::BCD };
; P2 G' C+ J2 Y+ S+ v& G    void ( E::*E_CVF[] )() = { E::E_C_thunk_ABC, E::BCD };
2 v7 n) B5 k2 S! P电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站    void ( E::*E_DVF[] )() = { E::E_D_thunk_ABC, E::BCD };我爱电脑技术论坛* R8 O6 b) T9 F$ E) `
    E::E() { B_pF = E_BVF; C_pF = E_CVF; D_pF = E_DVF; }
8 m+ m2 R& J, a, l: P5 A我爱电脑技术论坛结果E e; C *pC = &e; pC->ABC(); D *pD = &e; pD->ABC();,假设e的地址为3000,则pC的值为3012,pD的值为3024。结果pC->pF的值就是E_CVF,pD->pF的值就是E_DVF,如此就解决了偏移问题。同样,对于前面的虚继承,当类里有多个虚类表时,如:
& Y" E/ Z) r" p: ?# ?$ d电脑,技术,IT,学习,交流,网络安全,QQ,硬件,软件,编程,教程,建站    struct A {};
6 r, J9 E( X2 k3 b我爱电脑技术社区--打造最好的电脑技术自学交流平台    struct B : virtual public A{}; struct C : virtual public A{}; struct D : virtual public A{};
4 H) l6 W3 E: J1 Z/ {5 [9 ^打造最好的电脑自学交流论坛    struct E : public B, public C, public D {};我爱电脑技术论坛7 G8 d& Q- w% ^5 x( F5 n4 L
    这是E将有三个虚类表,并且每个虚类表都将在E的缺省构造函数中被正确初始化以保证虚继承的含义——间接获得。而上面的虚函数表的初始化之所以那么复杂也都只是为了保证间接获得的正确性。
, z; c# X, B+ Y4 [$ z3 K  {我爱电脑技术社区--打造最好的电脑技术自学交流平台    应注意上面将E_BVF的类型定义为void( E::*[] )()只是由于演示,希望在代码上尽量符合语法而那样写,并不表示虚函数的类型只能是void( E:: )()。实际中的虚函数表只不过是一个数组,每个元素的大小都为4字节以记录一个地址而已。因此也可如下:6 H, |7 k- p" A$ Y6 G
    struct A { virtual void ABC(); virtual float ABC( double ); };
) h# @, f- j# P6 T+ R1 u    struct B : public A { void ABC(); float ABC( double ); };' F" D0 ?5 x; _! B; s. d
    则B b; A *pA = &b; pA->ABC();将调用类型为void( B:: )()的B::ABC,而pA->ABC( 34 );将调用类型为float( B:: )( double )的B::ABC。它们属于重载函数,即使名字相同也都是两个不同的虚函数。还应注意virtual和之前的public等,都只是从语法上提供给编译器一些信息,它们给出的信息都是针对某些特殊情况的,而不是所有在使用数字的地方都适用,因此不能作为数字的类型。所以virtual不是类型修饰符,它修饰一个成员函数只是告诉编译器在运用那个成员函数的地方都应该间接获得其地址。
: l9 N" V8 n/ [* j  v9 S$ v& r我爱电脑技术社区--打造最好的电脑技术自学交流平台    为什么要提供虚这个概念?即虚函数和虚继承的意义是什么?出于篇幅限制,将在本文的下篇给出它们意义的讨论,即时说明多态性和实例复制等问题。

TOP

C++从零开始(十一)(下)——类的相关知识
# F0 D5 c6 P2 d0 ~" @. a0 u打造最好的电脑自学交流论坛本文的中篇已经介绍了虚的意思,就是要间接获得,并且举例说明电视机的频道就是让人间接获得电视台频率的,因此其从这个意义上说是虚的,因为它可能cao作失败——某个频道还未调好而导致一片雪花。并且说明了间接的好处,就是只用编好一段代码(按5频道),则每次执行它时可能有不同结果(今天5频道被设置成中央5台,明天可以被定成中央2台),进而使得前面编的程序(按5频道)显得很灵活。注意虚之所以能够很灵活是因为它一定通过“一种手段”来间接达到目的,如每个频道记录着一个频率。但这是不够的,一定还有“另一段代码”能改变那种手段的结果(频道记录的频率),如调台。
1 L1 T( K! s! t6 Q4 e6 U2 i% s我爱电脑技术论坛    先看虚继承。它间接从子类的实例中获得父类实例的所在位置,通过虚类表实现(这是“一种手段”),接着就必须能够有“另一段代码”来改变虚类表的值以表现其灵活性。首先可以自己来编写这段代码,但就要求清楚编译器将虚类表放在什么地方,而不同的编译器有不同的实现方法,则这样编写的代码兼容性很差。C++当然给出了“另一段代码”,就是当某个类在同一个类继承体系中被多次虚继承时,就改变虚类表的值以使各子类间接获得的父类实例是同一个。此cao作的功能很差,仅仅只是节约内存而已。如:
: d* b  o' ^# z9 ~3 ]. o2 _打造最好的电脑自学交流论坛    struct A { long a; };我爱电脑技术社区--打造最好的电脑技术自学交流平台+ D$ Q; y9 e9 @. [; ~
    struct B :