打印

翻了一下前几年的C++教案,觉得还有点价值,将摘选帖出

[复制链接]
19454|93
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
HWM|  楼主 | 2008-4-8 21:03 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
沙发
HWM|  楼主 | 2008-4-8 21:05 | 只看该作者

C++ 随笔-1,C和C++简史

C编程语言最初是在AT&T贝尔实验室,于1972年由Dennis Ritchie写成的,供16位的 DEC PDP 11小型计算机使用。用它把尚不成熟的UNIX操作系统模块化并用高级语言编写了大部分操作系统和系统应用程序。C语言开始就设计为既可作为系统编程语言也可作为应用程序语言。在一年里,C被移植到各种其他机器体系结构。1978年,Ritchie和Steve Johnson编写了可移植的C编译器,从那时起,它与UNIX操作系统一起已经被移植到几乎每种可以想象到的计算机体系结构。
在1972年到1978年之间,C语言有某些重要发展,Brian Kernighan和Denis Ritchie出版了《C编程语言》。从那时起,他们的书就已经定义了称为“K&R C”的C的版本。当然,经销商都给他们的各个版本增加了自己的额外特性——也称为不兼容性。1983年,ANSI标准委员会确定了C的一个标准版本。该委员会从各种来源,包括称为C++的C的另一个新的分支,调查了解情况。1989年,ANSI委员会公布了它的标准;并且,1990年,联合的ANSI/ISO委员会公布了我们现在使用的国际标准。
1979年,Bjarne Stroustrup有了给C增加面向对象的类的想法,他的第一个语言,C with Classes,1980年在贝尔实验室内部报道过,1982年在贝尔实验室范围外报道。1984年,这种语言成长为C++。称为C front的最初版本是作为前端预处理器实现的,它将C++转换成为C,供一个原始C编译器编译。从1987年开始,各种不同的编译器实现了既作C的前端又作为原始编译器的语言,并且建立了几种编程支持环境。
1985至1991年间,在Bjarne Stroustrup的书《C++编程语言》的第一版和第二版之间的时间里,C++语言有了极大的发展,这部分归功于USENIX C++讨论会在这些年的召开。1990年,ANSI C++标准委员会首次开会,Stroustrup提交了他的新Annotated C++ Reference Manual (ARM),开始了标准化工作。1991年,ISO和ANSI委员会开始一起开会,使其得到的标准成为真正的国际标准。
1995年,提交了认可Draft Standard(草拟标准)作为标准的申请。从那时起,它被接受,并且是第一个ANSI/ISO C++ Standard。

使用特权

评论回复
板凳
HWM|  楼主 | 2008-4-8 21:10 | 只看该作者

C++ 随笔-2,封装


封装概念很普遍,类似也可以称之为“包装”。人要包装,商品要包装,一切的一切都需要包装。为何会有如此需求呢?究其原因无非有二,其一是炫耀,其二是遮丑。经过包装后其价值会有不同程度的提高,这是因为通过包装展现出来的是其最有价值的一面。另一方面通过包装又可保证其内部具有一定的私密性,这是包装(或封装)的一个相当重要的特性。

现在来考察一下C和C++中的封装。在C中我们知道有数据结构struct类型,它就是某种程度上对数据的封装。但仔细看来这种封装只是将一堆相关的数据放在一起,从外面来看并没有多大新意,就内部而言又丝毫没有一点私密性。所以如此的包装既无从炫耀又无法遮丑。反过来分析一下C++中的类class类型,它是对“状态机”的封装(或抽象点说就是对“对象”的封装)。一方面,类似struct类型,class包含有一堆相关的数据(或称状态);而另一方面其又包含有一堆相关的“处理功能”,合起来正好就是一完美的状态机。这一点是值得大大地炫耀一下的。有了其光鲜的外表还不够,class类型还确保了其内部的私密性。这种私密性在语法层面上保证了软件系统的完整性和安全性,体现了软件工程学中的模块分割原则。


考察一个状态机的封装

我们知道一个状态机若用数学形式表达的话就是:

Y = Fy ( X, S )
S = Fs ( X, S )

其中Y为输出,X为输入,S为状态。

如果是有限状态机,则S可用有限离散量表示,即S0,S1,…,Sn。下面给出一个有限状态机的C++程序框架。注意,这只是对有限状态机数学形式的直接封装。

// 有关"状态机"的封装

enum status_type { S0, S1, S2, S3, S4,…,Sn };
    
class class_SM
{
    protected:
    
    status_type status;
    
    virtual int Fy( int x );
    virtual void Fs( int x );
    
    public:
    
    class_SM( void ) { status = S0; }
    class_SM( status_type S ) { status = S; }
    
    int Do_it( int x );
    
    ~class_SM() {}
};

int class_SM :: Fy( int x )
{
// return Fy( x, status )

    return x;
}

void class_SM :: Fs( int x )
{
// status <- Fs( x, status )
}

int class_SM :: Do_it( int x )
{
    int result;
    
    result = Fy( x ); Fs( x );
    
    return result;
}

这个封装有壳,有头,有尾,还有躯干。下面就将其解剖一下:

首先是“壳”

class class_SM
{
};

由关键字class引入了一个类型class_SM,注意,在C++中同样可以由struct引入一个类型,这在C中是不可以的。

其次来看一下“头”和“尾”

    class_SM( void ) { status = S0; }
    class_SM( status_type S ) { status = S; }
    
    ~class_SM() {}

在类中若用类名定义一个函数,则此函数具有特殊的地位,并给它一个称谓——构造函数。这个特殊的函数就是类的“头”,对象 (类的一个实现)的诞生就是由构造函数引领的。当进入对象所在的作用域时,系统首先分配空间,紧接着就自动调用“合适”的构造函数来初始化整个对象。在这里我们看到有两个构造函数,它们的形式差异就在于输入参数的不同,由不同参数形式来区分不同函数的调用是C++的另一个重要的特性,在此不作详叙。下面给出两种不同的对象定义:

class_SM SM; // 调用class_SM( void ) { status = S0; }

class_SM SM( S0 );  // 调用class_SM( status_type S ) { status = S; }

在前面可以看到有这样一条定义语句:

enum status_type { S0, S1, S2, S3, S4,…,Sn };

它是被放在类的定义“壳”外的,这是因为类的使用者有可能自己决定状态机的初始状态(即调用class_SM( status_type S ) { status = S; })。若没有这样的选项(即只有第一个构造函数class_SM( void ) { status = S0; } ),则就可以将定义enum status_type { S0, S1, S2, S3, S4,…,Sn }封在类的定义“壳”内,不对外开放。

看过了“头”再看“尾”,在class_SM类中有一个奇怪的函数,就是在类名前加上“~”符号的那个函数,此函数被称之为析构函数。虽然在此未定义任何具体的操作,但我们从它的调用位置就可以看出它的特殊性。析构函数是在对象消亡前被系统调用去处理相关后事的那个函数,所以它通常被调用的位置是在即将退出对象所在的作用域时。这样我们就清楚的知道,对于类的诞生和死亡,系统会自动地调用一些相关的函数去处理某些重要的事情,而这些事情的具体内容是可以由程序设计者来参与的。这就是类封装的一大特点,且具有相当高的价值。

另外对象还可以“动态”地诞生和消亡(使用对象指针和相关的系统操作),在此仅点到为止。

有了头和尾,当然不能没有躯干。在此我们所言的躯干就是下列一些成员:

    status_type status;
    
    virtual int Fy( int x );
    virtual void Fs( int x );
        
    int Do_it( int x );

其中status是类型status_type的数据变量,称之为类class_SM的“成员变量”;而Fy、Fs、Do_it是函数,称之为类class_SM的“成员函数”。另外要说明的是,前面所提到的构造和析构函数同样是类class_SM的“成员函数”,不过由于这两类函数地位比较特殊,所以给他们另外起了两个特殊的称呼。一般的成员函数调用是由程序设计者自己显式指定的,并非由系统自动调用。


类封装的私密性

现在我们已经大致的了解了一个类的封装的基本结构和各部份粗略的功能特性。下面进一步看一下其中有点象标号的那些玩意儿:

pretected:

public:

private:

最后一个虽然未出现,但它却是类的缺省特性(未加说明就是private)。

上面所列的三个东西并非是标号,就单词本身它们是C++的预留关键字,在类中使用它们来说明其下面的成员(包括成员变量和成员函数等)具有相应的被使用权限(注意未加说明则权限就是private)。由此可见,类封装不仅把它的成员包装了起来,而且限定了使用它们的权限。缺省情况下,其特性为private(私密的),这样从类的外面就看不见它们,这非常类似于我们日常所说的私密性。私密性是C++中类封装的又一大特点,它使类这种新的类型更加符合软件工程学中的模块基本准则。

当然如果封装在类中的成员全都是私密的,这将是铁桶一个,毫无意义,就算阿拉伯妇女都会亮出她们那双美丽的眼睛。为了能把类的光辉炫耀出来,就必须把有使用价值的成员呈现给外界,而这就是有public(公用)来加以说明的。使用public将改变其下面所列成员的使用权限,使其能被外界存取或调用。这里还有一个定义使用权限的关键字protected,它与类的继承性有关,在此不阐述。


类的实例化和对象的使用

类被定义以后只是一堆代码(特殊情况下会伴随一些表格),必须将其实例化后才会作为“对象”实实在在的存在,随后被使用。在此需强调的是“类”不是“对象”,反过来“对象”也不是“类”,它们不是同层次的概念,这有点类似于“类型”和“变量”的关系。类是产生对象的模子,而对象则是由类这个模具生产出来的产品。所以一个类可以产生多个对象,这个过程就是类的实例化。

类被实例化后形成的实体——对象,从存储在内存中的形式上看类似于结构(struct),系统为每个对象的所有成员变量分配相应的空间。每个同类的对象共享它们的成员函数,换句话说就是,类中所定义的代码在实例化过程中并未被复制。

类的实例化过程除了分配空间外还有一件相当重要的事情就是调用构造函数,这是和一般变量或结构类型的实例化具有本质差异的地方。通过构造函数,对象在被使用之前自动对自己的状态进行初始化,所以对象在诞生之时就已作好了被使用的准备,而一般的变量或结构在引用前必须对其进行赋值。

作为类实例化的产物——对象,一旦诞生就可以被使用。具体来说,使用对象分两个方面,其一是对其成员变量的赋值或引用(虽然并不建议这样做),其二是调用其成员函数。关于成员函数的使用存在有几种异化的形式,如对象的“复制”和操作符的“重载”。在这种变异的情况下,对象仅以其名出现(如出现在表达式内),但实际上只是对它的某一特定的成员函数进行了调用。相关内容在此不作详表。

现在再回过头来看一下前面定义的状态机,其内有一成员函数int Do_it( int x )。它是唯一能被外界使用的成员(注意:构造和析构函数是对象诞生和消亡时由系统调用的)。沿用前面的实例化语句,相关对象的使用形式为:

Y = SM.Do_it( X );

从上面的调用语句可以看出,Do_it表面上使用了两个参数——X、Y。但实际上还有一个隐形的参数,就是指向对象SM数据存储区的指针变量——this。因此可以这么说,就是若成员函数一个参数也没定义的话,也存在着一个指针(this)作为函数的隐形参数。这就是类封装的“代价”,虽然并不是很大,但毕竟是要付出的。

现在来分析一下为何要有这样一个隐形参数——this,传给成员函数。就拿前面定义的另外一个成员函数Fs( int x )来看,形式上它只有一个参数x作为输入,但实际上它还要使用类的一个成员变量status。对于类class_SM的不同对象,成员变量status的地址是不同的。因此必须将相应对象的成员变量存储区地址传给成员函数,因为成员函数已经知道status的偏移量,所以只要将成员变量存储区的首地址this(这也是对象SM的地址)传给成员函数即可。

使用特权

评论回复
地板
HWM|  楼主 | 2008-4-8 21:12 | 只看该作者

C++ 随笔-3,类的嵌套


类的嵌套是指在类定义中含有另外一个类的定义,某种程度上这种嵌在类定义中的类定义可被称之为类的“成员类”。在类的实例化过程中成员类不会被实例化,只有存在成员类的对象时才会产生相应的对象实例化。下面给出一个类嵌套的实例

class class_Outer
{
    class class_Inner
    {
        public:
        
        int inner_x;
        
        class_Inner( void );
    };
    
    public:
    
    class_Inner Inner;
    
    int outer_x;
    
    class_Outer();
};

class_Outer :: class_Inner :: class_Inner( void )
{
    inner_x = 10;
}

class_Outer :: class_Outer( void )
{
    class_Inner Inner;
    
    this->Inner.inner_x = 20;
    outer_x = Inner.inner_x;
}

class_Outer Outer;

int main( void )
{
    while ( 1 )
    {
    }
}

上例中类class_Inner是在类class_Outer的定义中定义的。由于前面没有权限设定,所以缺省为private,因此类class_Inner只能在class_Outer的作用域内用来定义对象。注:class_Outer的作用域为其定义范围内和成员函数体内。

使用类嵌套的好处在于能将某一类的定义封装在另外一个类的定义之内。这样一来被封装在内的类定义对外是不可见的,即实现了类定义的“私有化”,这是类封装的私密性在类定义上的拓展。

使用特权

评论回复
5
HWM|  楼主 | 2008-4-8 21:14 | 只看该作者

C++ 随笔-4,静态成员变量和静态成员函数


考察下面的例子:

class class_A
{
    int x;
    static int a;
    static int add( int i, int j ) { return (i + j); }
    
    public:
    
    class_A( void ) { x = 10; a = 20; }
    int get_x( void ) { return x; }
    void set_x( int i ) { x = i; }
    void set_a( int i ) { a = i; }
    void Do_it( void ) { x = add( x, a ); }
};

int class_A::a;
class_A A, B;
int Y, Z;

int main( void )
{
    A.set_x( 30 );
    A.set_a( 40 );
    A.Do_it();
    B.Do_it();
    Y = A.get_x();
    Z = B.get_x();
    
    while ( 1 )
    {
    }
}

我们发现有两个奇怪的成员和一个奇怪的变量,下面把它们提取出来:

static int a;
static int add( int i, int j ) { return (i + j); }

int class_A::a;

首先分析一下static int a,这是类class_A的一个成员变量。特殊的是在变量说明之前加了一个关键字static,这样成员变量a就变成了“静态”的了。这里静态的意思就是在存储器中分配一个“固定的”存储空间,使类class_A的所有实例化对象中的相应成员变量a都被定位在这个固定的存储空间上,实现了类class_A的所有对象(在此是A和B)共享变量a。为了表明这个“固定的”存储空间的存在,在全局变量定义中增加了一条相关的定义,即int class_A::a;,而这就是我们看到的那个奇怪的变量。

现在再看看另一个奇怪的成员static int add( int i, int j ) { return (i + j); },这是类class_A的一个成员函数。特殊的是在函数说明之前同样加了一个关键字static,这样一来函数add也变成“静态”的了。在类的封装中我们曾经说过,成员函数有一个隐形指针(this)作为输入参数,这也是封装所付出的代价。这个代价是否可以避免,若能又可在何种情况下避免呢?首先来分析一下为何要使用这样一个指针。我们知道成员函数代码是被类的所有实例化对象所共享的,成员函数只知道所在类的成员地址在对象中的相对偏移量,而不清楚具体对象存储空间的绝对地址。所以如果要使用所在类的其他成员就必须得到相应对象的地址——this。但从另一方面来说,如果成员函数不使用所在类中的其他成员,这个对象地址硬传给它不就成多余的吗?是否存在不用传对象地址的成员函数呢?回答是肯定的。前面我们看到的那个变成“静态”的函数恰恰就是我们所要的函数——静态函数。静态函数和非静态函数的差别就在于是否传递对象指针。

使用特权

评论回复
6
sz_kd| | 2008-4-8 21:14 | 只看该作者

支持,顶

使用特权

评论回复
7
HWM|  楼主 | 2008-4-8 21:16 | 只看该作者

C++ 随笔-5,带有"const this"或"volatile this"的成员函数


先看一下下面的程序:

class example_class
{
    int x;

    public:

    int get_x( void ) const;
};

int example_class :: get_x( void ) const
{
    return ( x );
}

const关键词都很熟悉,但在这里的使用有点怪。这里的const并非修饰成员函数的返回值,而是对成员函数的隐含参数this的修饰。由于传给成员函数的隐含参数是const this,所以成员函数就不能对由this所指定的本对象内任何成员变量进行修改。由此可以看到,如果某成员函数不对本对象的成员变量进行修改,利用向成员函数传具有常量特性的this指针可以在语法层面上确保不会对成员变量的修改。

类似的也可以用volatile来修饰this指针(用volatile 替代const),这样一来相应的一些优化将被取消。

使用特权

评论回复
8
平常人| | 2008-4-8 21:16 | 只看该作者

坐下来听教授讲课

嘘—— 不要交头接耳。。。。

使用特权

评论回复
9
HWM|  楼主 | 2008-4-8 21:17 | 只看该作者

C++ 随笔-6,类的复制


谈到类的复制涉及到两方面的含义,其一是类的初始复制,其二是类的赋值复制。具体看下列程序:

class copy_example
{
    int x;

    public:

    copy_example( void ) { x = 0; };
    copy_example( int x0 ) { x = x0; };
    copy_example( copy_example& ref );
};

copy_example :: copy_example( copy_example& ref)
{
    x = ref.x;
}

先看初始复制:

copy_example Example1(100); // Example1.x <- 100
copy_example Example2 = Example1; // Example2.x <- Example1.x

上面有两个定义,第一个定义了对象Example1,并将x初始化成100;第二个定义了对象Example2,并将对象Example1的成员变量x复制到Example2的成员变量x上。这样一来Example2的成员变量x的值也为100。

看过了初始复制,再看赋值复制:

copy_example Example1(100); // Example1.x <-100
copy_example Example2; // Example2.x <- 0

Example2 = Example1; // Example2.x <- Example1.x

上面有两个定义,第一个定义了对象Example1,并将x初始化成100;第二个定义了对象Example2,并将x初始化成0。在定义之下有一条赋值语句。由于未对赋值符重载定义(关于重载以后再论),所以其“缺省”语义就是将对象Example1的全部内容原封不动的复制到Example2上。这样一来Example2的成员变量x的值也为100。

使用特权

评论回复
10
HWM|  楼主 | 2008-4-8 21:19 | 只看该作者

C++ 随笔-7,为朋友开个后门(友元)


在阐述类的封装时,曾经强调过私密性。但有时也会对特定对象开个后门(注意:这有别于共用性),这样有利于特定对象间或对象与特定函数间的联系。

下面给出相关的两个例子:

class class_A
{
    int x;
    
    friend class class_B; // class_B is a friend
    
    class_A( void ) { x = 100; };
};

class class_B
{
    int y;
    
    class_A A;
    
    public:
    
    int get_x( void ) { return A.x; };
};

class_B B;

int z;

int main ( void )
{
    z = B.get_x();
    
    while ( 1 )
    {
    }
}

由于在类class_A中声明了类class_B是其友元(用关键词friend),所以在类class_B中,对象A(class_A的实例)的所有成员(包括变量和函数)都是可访问的,虽然它们都具有私有特性。

另一例子:

class class_A
{
    int x;
    
    friend int get_x( void ); // get_x is a friend
    
    class_A( void ) { x = 100; };
};

int get_x( void )
{
    class_A A;

    return ( A.x );
}

int z;

int main ( void )
{
    z = get_x();
    
    while ( 1 )
    {
    }
}

在上例中友元是一个函数。在此函数作用域(或函数体)内,类class_A的所有成员同样也是开放的。

使用特权

评论回复
11
程序匠人| | 2008-4-8 23:26 | 只看该作者

不错,建议加裤置顶

使用特权

评论回复
12
HWM|  楼主 | 2008-4-9 21:09 | 只看该作者

谢谢匠人捧场

使用特权

评论回复
13
HWM|  楼主 | 2008-4-9 21:11 | 只看该作者

C++ 随笔-8,继承

继承(inheritance),又可谓遗传,是生物乃至人类社会的基本特征。如果没有了遗传,生物进化就无从谈起;而若没有了继承,人类文明也就不复存在。继承(或遗传)是对祖辈特性有选择的裁减或拓展,是一种优化提炼,也是一种细化过程。

回到C++,若没有继承会是怎样的一个情形。在论述C++的类封装中曾经说过,在每一个类中一般都封装了一堆代码。如果在类之间没有某种联系机制,这就意味着每个类的代码必须独立存在,否则就会破坏类封装的特性。

下面用两个类来进一步说明这个问题:

class class_A
{
    A
};

class class_AB
{
    A
    B
};

其中A、B分别表示两组不同代码的定义

如果不存在某种联系机制,为了保证类的封装性就必须分别存储代码A和A、B。这样不仅浪费了存储空间,也不利于软件循序渐进的开发过程。为了解决这个问题,就必须引入类之间的某种联系机制,而这种机制早已有之,即继承关系。

下面用继承关系重新建立上述两个类:

class class_A
{
    A
};

class class_AB : public class_A
{
    B
}

现在类class_A和class_AB不再是毫无关系的两个类了,它们间存在着刚引入的继承关系(由class class_AB : public class_A定义),即class_AB继承了class_A的某些特性(此例为代码A)。另外再分析一下两相关类的代码存放情况,在class_A中代码没发生什么变化,而class_AB由于继承了class_A的代码A,所以其内只需存储代码B即可。由此可见继承关系的存在,使得代码的利用率得到了很大的提高。此外由于class_AB具有同样的完整性,所以类的封装性并没有被破坏。

下面给出一个程序,来看一下具体的细节:

// 有关"状态机"的继承

enum status_type { S0, S1, S2, S3, S4 };

class class_SM
{
    protected:
    
    status_type status;
    
    virtual int Fy( int x );
    virtual void Fs( int x );
    
    public:
    
    class_SM( void ) { status = S0; }
    class_SM( status_type S ) { status = S; }
    
    int Do_it( int x );
    
    ~class_SM() {}
};

int class_SM :: Fy( int x )
{
// return Fy( x, status )

    return x;
}

void class_SM :: Fs( int x )
{
// status <- Fs( x, status )
}

int class_SM :: Do_it( int x )
{
    int result;
    
    result = Fy( x ); Fs( x );
    
    return result;
}

class class_My_SM1 : public class_SM
{
    virtual int Fy( int x );
    virtual void Fs( int x );
    
    public:
    
    class_My_SM1( void ) : class_SM() {}
    class_My_SM1( status_type S ) : class_SM( S ) {}
};

int class_My_SM1 :: Fy( int x )
{
    switch ( status )
    {
        case S0:
            // return Fo_S0( x )
        case S1:
            // return Fo_S1( x )
        case S2:
            // return Fo_S2( x )
        case S3:
            // return Fo_S3( x )
        case S4:
            // return Fo_S4( x )
        default: return x; // do nothing
    }
}

void class_My_SM1 :: Fs( int x )
{
    switch ( status )
    {
        case S0:
            // status <- Fs_S0( x )
            break;
        case S1:
            // status <- Fs_S1( x )
            break;
        case S2:
            // status <- Fs_S2( x )
            break;
        case S3:
            // status <- Fs_S3( x )
            break;
        case S4:
            // status <- Fs_S4( x )
            break;
        default: ; // do nothing
    }
}

class class_My_SM2 : public class_SM
{
    virtual int Fy( int x );
    
    public:
    
    class_My_SM2( void ) : class_SM() {}
    class_My_SM2( status_type S ) : class_SM( S ) {}
};

int class_My_SM2 :: Fy( int x )
{
    int result;
    
    switch ( status )
    {
        case S0:
            // result <- Fo_S0( x )
            // status <- Fs_S0( x )
            return result;
        case S1:
            // result <- Fo_S1( x )
            // status <- Fs_S1( x )
            return result;
        case S2:
            // result <- Fo_S2( x )
            // status <- Fs_S2( x )
            return result;
        case S3:
            // result <- Fo_S3( x )
            // status <- Fs_S3( x )
            return result;
        case S4:
            // result <- Fo_S4( x )
            // status <- Fs_S4( x )
            return result;
        default: return x; // do nothing
    }
}

class_My_SM2 My_SM( S1 );

int main( void )
{
    int X, Y;
    
    Y = My_SM.Do_it( X );
    
    while ( 1 )
    {
    }
}

上面的程序是对前面(封装)例程的细化(五状态有限状态机)。其中的类class_My_SM1和class_My_SM2都是从类class_SM继承而来的。被继承的类(如class_SM)一般称为基类或父类,而继承而来的类则一般称为派生类或子类(如class_My_SM1和class_My_SM2)。

在论述封装时,曾经提到一个成员访问权限关键字——protected,它是用来描述这样的一种访问权限,就是其下面的类成员可以且仅可以被自身或派生类访问。所以作为class_SM的派生类class_My_SM1和class_My_SM2具有访问class_SM的成员变量status的权限。不过还有一点要注意的,就是在继承关系定义中(如class class_My_SM1 : public class_SM)用了一个关键字public。它是对访问权限继承关系的确认,如果加了public,基类的访问权限将继承到派生类中,否则将不被继承(默认为private)。

在上例中关于成员函数Fs和Fy的继承关系,由于涉及到多态性,以后再表。

使用特权

评论回复
14
HWM|  楼主 | 2008-4-9 21:15 | 只看该作者

C++ 随笔-9,什么是不可被继承的

并不是所有的东西都可被继承的,下面四项就被列入不可继承之列:

  1) 构造函数
  2) 析构函数
  3) 重载操作符
  4) 友元

下面一一给出理由:

构造函数和析构函数只属于本类(在以后还要详表),即每一个类必须具备只属于自己的构造函数和析构函数。所以不能从基类中继承相关的函数。

由于C++存在强类型定义,所以重载操作符也不能被继承,因为重载操作符只能作用于定义它们的那个类(类也是一种类型)。

友元不能继承理解起来比较直接,就是因为父亲的朋友并不一定是儿子的朋友,理由就这么简单。

使用特权

评论回复
15
HWM|  楼主 | 2008-4-9 21:35 | 只看该作者

C++ 随笔-10,继承关系在软件设计中的作用

学过软件工程都知道,系统自上而下是典型的分析过程,即将问题一步步的细化,最终达到可直接编程的目的。而这种细化分析过程和类由基类向派生类继承的过程相当相似。

另外由于继承具有累加效应,因此在一些软件包的设计中通常使用了类和类的继承。这样一来,随着日积月累,一个庞大的软件库包就会不知不觉的形成。MS的MFC和Borland的包容类库就是这样形成的。

使用特权

评论回复
16
iC921| | 2008-4-9 22:36 | 只看该作者

楼主不妨加一点东西

作为新手,应该学习里面的哪些东西好呢?

有劳了!

使用特权

评论回复
17
HWM|  楼主 | 2008-4-10 08:38 | 只看该作者

回版主:新手若未接触过C/C++,建议找一本相关教科书认真看

在这里不可能系统地讲C/C++,只是将一些重点概念和本人认为有用的语言点阐述一下(大部分摘自我数年前上课的备课笔记),不能保证阐述得完全正确,若有错误望及时指正。

使用特权

评论回复
18
HWM|  楼主 | 2008-4-10 10:07 | 只看该作者

C++ 随笔-11,有背常理的生死律,类栈而非类队

class class_A
{
    int x;

    public:

    class_A( void ) { x = 0; };
    ~class_A();
};

class class_B : public class_A
{
    int y:

    public:

    class_B( void ) : class_A() { y = 0; };
    ~class_B();
};

class class_C : public class_B
{
    int z:

    public:

    class_C( void ) : class_B() { z = 0; };
    ~class_C();
};

void function( void )
{
  class_C object_C;
}

int main( void )
{
    function();

    while ( 1 )
    {
    }
}

上面程序呈现了三个类的继承关系,从class_A中派生出class_B,而又从class_B中再派生出class_C。这三个类各自都有自己的构造和析构函数。当进入函数function的作用域时发现有类class_C的一个实例化对象object_C,按照类的实例化过程,在分配好空间后就要调用相关的构造函数,那么如何调用呢?实际调用次序是:

    class_A()
    class_B()
    class_C()

这比较好理解,现有父辈再有子辈是天经地义的事情。再考察一下退出过程,当从函数function中返回时,由于退出了函数的作用域,对象object_C将消亡。在其消亡前要调用相关的析构函数,那么又如何调用呢?实际调用次序是:

    ~class_C()
    ~class_B()
    ~class_A()

这和常理就大相径庭的,子辈先于父辈而亡。

现在来分析一下为何如此。如果反过来基类先于派生类调用析构函数,那么派生类从基类中继承的某些成员在派生类对它们进行后事处理之前就可能已经失效(如动态空间的消失使指针变成空)。另外采用类栈(FILO)形式也更有利于存储空间的分配管理。

对于成员对象上述法则同样成立,见下面程序

class class_A
{
    int x;

    public:

    class_A( void ) { x=0; };
    ~class_A();
};

class class_B
{
    int y;
    class_A A;

    public:

    class_B( void ) { y=0; };
    ~class_B();
};

void function( void )
{
  class_B object_B;
}

int main( void )
{
    function();

    while ( 1 )
    {
    }
}

其中A是类class_B的一个成员对象(成员变量的一种)。在对象object_B诞生和消亡过程中,相关构造和析构函数的调用次序如下:

构造过程

    class_A()
    class_B()

析构过程

    ~class_B()
    ~class_A()

使用特权

评论回复
19
iC921| | 2008-4-10 11:08 | 只看该作者

老师好!谢谢您!

只是我觉得,告诉他们为什么这样,也就是谈一谈体会,比较容易让更多的人接受。

顺着这个思路,再建议:按照不同的性质,单个发帖。

使用特权

评论回复
20
HWM|  楼主 | 2008-4-10 11:59 | 只看该作者

C++ 随笔-12,继承不总是做加法

在论述类的继承时(见13L)曾经给出了下面的形式:

class class_A
{
    A
};

class class_AB : public class_A
{
    B
};

可以看出类class_AB在其基类class_A的基础上增加了代码B,形成了自己的一个新的代码封装A加B,很明显这是功能的累加。继承是否总是这样的简单累加呢?答案是否定的,具体再看下面程序形式:

class class_A
{
    A
    AB
};

class class_B : public class_A
{
    AB
    B
};

其中A,AB,B表示成员函数的定义部分。可以看到在两个类的成员函数定义块中有一部分是重叠的(名字相同),即AB。这样一来类class_B和其所有的派生类将不再看得见class_A中的成员函数AB,虽然class_A是它们的共同基类。这就是类继承过程中所谓的成员函数覆盖作用。要注意的是只要成员函数的名字相同就被覆盖,不管其参数是否一致。

现在可以看到类class_B的成员函数分别为下列三部分

    A,从基类class_A中继承而来。
    B,自己产生。
    AB,同样是自己产生,且覆盖了基类中的具有相同名字的成员函数。

从上可见类class_A中的成员函数AB在类class_B中不再可见,当然在类class_B的所有派生类中也不会看见class_A中的成员函数AB。

利用成员函数在继承中的覆盖特性,可以修改基类所呈现的“顶层”成员函数的特性。注意,这里的“顶层”指的是相关成员函数没有被类内其他成员函数直接或间接调用。对于非“顶层”成员函数的覆盖,由于涉及到虚函数概念,以后再表。

下面给出一个具体的具有成员函数覆盖的程序片段:

class PushButton
{
    protected:
    int state;

    Box* outline;
    Box* button;
    
    public:

    PushButton( int px, int py );
    void Display();
    ~PushBotton() {}
};

class SimplePushButton : public PushButton
{
    public:

    SimplePushButton( int x, int y );
    void Display();
    ~SimplePushButton() {}
};

其中类PushButton是一个通用的按钮(如3D),而类SimplePushButton是一个简单的按钮(如2D)。可以看出SimplePushButton通过对其基类中void Display()成员函数的覆盖实现了其显示功能的裁减。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

HWM

1230

主题

20953

帖子

150

粉丝