打印

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

[复制链接]
楼主: HWM
手机看帖
扫描二维码
随时随地手机跟帖
21
HWM|  楼主 | 2008-4-11 12:33 | 只看该作者 回帖奖励 |倒序浏览

C++ 随笔-13,对象的返祖

返祖是生物乃至人类的一种遗传缺陷,是祖先遗传基因的突显。其实生物乃至人类的基因中本来就存在有祖先的基因成分,只是正常情况下被隐藏在深处而已。

在前面讲述类的成员函数覆盖时,曾经说过被覆盖的成员函数在当前类和所有派生类中将不再呈现出来。但不呈现不等于消失了,只是被后辈的相关特性(成员函数)给覆盖了。那么在C++中类似的“返祖”可否实现,其用处又在那里?从下面的例子中可见一斑。先列出程序如下:

class point
{
    protected:
    int x, y;

    public:
    point( int x0, int y0 ) { x = x0; y = y0; }
    void Display( void );
};

void point::Display( void )
{
// display a point, 坐标为(x,y)
}

class line : public point
{
    protected:
    int r, d;

    public:
    line( int x0, int y0, int r0, int d0 ) : point( x0, y0 ) { r = r0; d = d0; }
    void Display( void );
};

void line::Display( void )
{
// display a line, 以(x,y)为一点,长度为r,角度为d
}

class circle : public line
{
    public:
    circle( int x0, int y0, int r0 ) : line( x0, y0, r0, 0 ) {}
    void Display( void );
};

void circle::Display( void )
{
// display a circle,以(x,y)为圆心,r为半径
}

circle C(0, 0, 10);

int main( void )
{
    C.Display();
    
    while ( 1 )
    {
    }
}

上叙程序中定义了三个类,分别是:point,line和circle。其中由point派生line,再由line派生circle。它们都有一个成员函数Display()。由于存在上面的继承关系,所以派生类的Display()覆盖了基类的Display()。这样一来可以看到,最终对象C中的显示功能(C.Displya())是显示一个以原点(0,0)为圆心以10为半径的圆。如果要想利用对象C来显示其半径或圆心(这是CAD中经常遇到的事情)是否有可能呢?答案是肯定的。下面给出两种实现方法:

一,作用域指定

    C.line::Display();
    C.point::Display();

利用作用域符“::”直接改变类的作用域(只能向基类方向改),使C原先被覆盖的显示功能呈现出来。


二,利用函数返祖

void Display_line( line& L )
{
    L.Display();
}

void Display_point( point& P )
{
    P.Display();
}

上面是增加的两个附加函数。它们的参数分别是以circle的两个基类为引用类型,因此一旦进入到函数体内对象就将呈现出其祖先的面貌,所以其显示功能就具有了返祖的特性。下面是具体的两个函数的调用语句:

    Display_line( C );
    Display_point( C );

它们同样分别显示了圆的半径和圆心。

最后要提醒的是,对于虚函数,上述两种方法结果不一样(具体以后再表)。所以这两钟方法不是完全等价的。

使用特权

评论回复
22
HWM|  楼主 | 2008-4-11 14:29 | 只看该作者

C++ 随笔-14,“种子类”


现在闲聊一点C++程序设计技巧上面的问题

C++语言是为完成某个目标设计类的。通常从粗略的类需求开始,随着工程的成熟不断加进越来越多的细节。有时会产生两个有一定相似之处的类。如下列两个类:

class class_A
{
    A
    C
};

class class_B
{
    B
    C
};

为避免这些类中代码的重复,必须在此时将类分解,将公共特征归结成基类,再将不同的部分组成独立的派生类。变化如下:

class class_C
{
    C
};

class class_A : public class_C
{
    A
};

class class_B : public class_C
{
    B
};

仅仅为了共享派生类中的代码而创建的类称为“种子类”。种子类并不一定是抽象类,不过其本身一般没什么用处。

有时能预先判断两个类将会有公共特征,但却不知道公共特征出现在何处。此时可以使它们派生自一个假定类,并使该假定类为空。随着不断的细化,逐渐把两个类的相同部分移入假定类中,最终形成一个非空的种子类。

使用特权

评论回复
23
HWM|  楼主 | 2008-4-12 10:01 | 只看该作者

C++ 随笔-15,重载

本来想直接摘录教案上的定义,后来百度了一下发现其解释相当不错,故先引之:

“什么是覆盖和重载?

这里有一个初学者经常混淆的概念。覆盖(override)和重载(overload)。覆盖是指子类重新定义父类的虚函数(应理解为接口完全一致的成员函数--HWM)的做法。而重载,是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。其实,重载的概念并不属于“面向对象编程”,重载的实现是:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对于编译器来说是这样的)。如,有两个同名函数:function func(p:integer):integer;和function func(p:string):integer;。那么编译器做过修饰后的函数名称可能是这样的:int_func、str_func。对于这两个函数的调用,在编译器间就已经确定了,是静态的(记住:是静态)。也就是说,它们的地址在编译期就绑定了(早绑定),因此,重载和多态无关!真正和多态相关的是“覆盖”。当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态(记住:是动态!)的调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(调用的子类的虚函数的地址无法给出)。” 

先不管上引文中的多态性,可以看到“重载”其实是引入了一个“名字分裂”的概念,就是将具有相同名而不同参数形式的函数(包括算符)分裂成几个独立的函数,而每个函数都有一个独立的标识。此外可以看到,为了能区分不同的重载函数,必须实现类型的强行管制--即强类型语言特性。这其实从防止出错的角度来看也是有利的。

重载不仅仅针对于类而言,对于一般的函数或一般类型的操作符都可以使用重载,只是在类中重载更能伸展其拳脚。

使用特权

评论回复
24
HWM|  楼主 | 2008-4-12 13:07 | 只看该作者

C++ 随笔-16,函数重载

一个函数其对外呈现的外貌可分三个部分,即:名字,参数和返回。如果在同一个作用域中定义了两个同名而不同参数(包括类型)的函数,函数重载就形成了。有两点要注意的,1)就是必须在同一作用域内,2)返回类型不影响重载。

下面的两个函数若定义在同一个作用域内将被认为是定义了两个同样的函数,编译将会报错。

void Display( int x );

long Display( int x );

而下面的就是重载,是正确的。

void Display( int x );

long Display( char x );

对于非成员函数的重载,理解起来相对比较简单一些。而对于成员函数来说,由于存在继承关系,相对要复杂一点。下面给出一例子加以说明:

class A
{
    public:
    int foo( int i ) { return ( i+1 ); }
};

class B : public A
{
    public:
    int foo( float f ) { return ( f+10 ); }
};

int main( void )
{
    B b;

    int i = b.foo( 2 ); // A::foo or B::foo called ?

    while ( 1 )
    {
    }
}

类B具有两个称为foo()的函数,一个是继承来的,另一个是自己定义的。通过上面的程序执行后i变量是多少呢?答案是12。编译器用了函数B::foo( float f ),尽管此时需要隐式类型转换将int变成float,而基类中的函数不需要类型转换。这是错误吗?不时,因为重载仅在同一作用域内才合法。这里类B中定义的函数与类A中定义的函数虽然名字相同,但由于处在不同的作用域内定义,所以不能视作重载。派生类中重定义的函数掩盖了所有基类中具有相同名字的函数。必须注意的是:即使基类中函数的参数与派生类中的函数参数完全不同,函数还是被覆盖。

使用特权

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

C++ 随笔-17,参数匹配


在前面论述重载时已经说过,重载是在同一作用域中定义一组同名而不同参数(返回不算)函数,其中作用域可以是文件作用域(一般函数)或类定义作用域(成员函数)。重载函数组(两个以上)一旦定义好后就有可能被调用,而具体调用哪个函数是完全有参数形式决定的。在选择具体函数时,若能找到参数形式完全一致的自然最好。如果找不到,则须找一个替代函数,此时编译器将实在参数与所有的重载函数的形式参数比较,找出相近的,这个过程称为参数匹配。

下面是同一作用域中重载函数的匹配原则:

    1)若能完全匹配,则毫无疑问就是此函数。
    2)若不能完全匹配,先按途径char -> int -> long -> float -> double 搜索,找到即可。
    3)若再找不到,就出错。

下面用一个例子说明:

int foo( int x )
{
    return 1;
}

int foo( float x )
{
    return 2;
}

int foo( double x )
{
    return 3;
}

int main( void )
{
    char a;
    long b;
    int y;

    y = foo( a ); // y <- 1
    y = foo( b ); // y <- 2

    while ( 1 )
    {
    }
}

上面a和b分别匹配类型int和float。

使用特权

评论回复
26
littgh1982| | 2008-4-13 15:07 | 只看该作者

头晕了

看着看着头就晕了,还是收藏起来慢慢看吧

使用特权

评论回复
27
wjcy131421| | 2008-4-14 08:38 | 只看该作者

ding~!1

使用特权

评论回复
28
fzdzm| | 2008-4-14 11:47 | 只看该作者

收藏了

看不懂我也ding,哈哈

使用特权

评论回复
29
fsaok| | 2008-4-14 12:19 | 只看该作者

很好

赞一个,

语言形象生动,透彻,不为讲解而讲解,继续听课中

使用特权

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

C++ 随笔-18,利用构造函数的重载实现类型转换


C++具有转换内部类型的一些规则,如:char类型可升级为int,根据环境int类型还可以升级为long,float或double。那么普通类型是否可以升级为“类”呢?类之间是否也可以转换呢?答案是肯定的,前提是转换规则得要自己定义。

在谈论类的复制时(见9L)曾经提到过类的初始复制。如果将构造函数按所需转换的类型重载,形成一组不同的构造函数,就可按自己的定义实现其他类型向当前类型的转换。下面用一例子说明:

class Counter
{
    long value;

    public:

    Counter( void ) { value = 0; }        // default constructor
    Counter( int i ) { value = i; }        // conversion from int
    Counter( long l ) { value = l; }             // conversion from long
    Counter( double d ) { value = d+0.5; }    // conversion from double
};

void foo( Counter c )
{ // do nothing now
}

int main( void )
{
    Counter C

    foo( 1 );                // convert from int to Counter
    foo( 2L );            // convert from long to Counter
    foo( 3.14 );            // convert from float(double) to Counter
    foo( C );                // no conversions necessary
}

上例中有四个重载构造函数,一个用于缺省实例化(初始化为0),其他三个用于实例化类型转换。其中第四个重载构造函数实现了四舍五入的转换功能。

使用特权

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

C++ 随笔-19,操作符的重载

操作符是一种特殊的函数,一般可用下面形式描述:

    一目操作符,<OP> a

    <type> operator <OP> ( <type> a )
    {
    }

    二目操作符,a <OP> b

    <type> operator <OP> ( <type> a, <type> b )
    {
    }

以上形式不是专门针对类的,具有一般特性。对于类而言,由于可以采用成员函数重载操作符,而当前类又可当作其中的一个操作对象,因此相关操作符的重载定义函数(成员函数的一种)的参数可以减少一个。具体形式如下

    一目操作符,<OP> a

    <type> operator <OP> ( void )
    {
    }

    二目操作符,a <OP> b
    
    <type> operator <OP> ( <type> b )
    {
    }

其中a为当前类(即操作符重载函数是它的成员函数)。

操作符的重载是建立在强类型基础之上的。由于有了强类型匹配,使得C++中的操作符能够根据操作对象类型的不同而实现不同的语义。操作符的语义多数情况下是不能覆盖的,别定义如下的重载函数

    int opeator + ( int x, int y )
    {
        return x-y;
    }

那是徒劳(编译也不允许)。因为对于类型int,加号的语义已经深入人心,无法改变。

赋值操作符的情况有些不同。对于C++内定的几种类型,如int和float等,同样不能对其赋值语义进行覆盖。而对于其它自定义的类型,如struct和class等,由于C++只允许以成员函数形式重定义赋值操作符,而struct没有成员函数,所以不能覆盖struct的赋值原始语义。但对于类class来说,由于可以定义赋值符重载,所以可以覆盖类的赋值缺省语义。

下面给出一个完整的复数操作符的重载,包括了两种定义形式:

struct complex
{
    float r, i;
};

complex operator + ( complex& x, complex& y )
{
    complex z;
    
    z.r = x.r + y.r;
    z.i = x.i + y.i;
    
    return z;
}

complex operator - ( complex& x, complex& y )
{
    complex z;
    
    z.r = x.r - y.r;
    z.i = x.i - y.r;
    
    return z;
}

complex operator * ( complex& x, complex& y )
{
    complex z;
    
    z.r = x.r * y.r - x.i * y.i;
    z.i = x.r * y.i + x.i * y.r;
    
    return z;
}

complex operator / ( complex& x, complex& y )
{
    complex z;
    float a;
    
    a = y.r * y.r + y.i * y.i;
    z.r = ( x.r * y.r + x.i * y.i ) / a;
    z.i = ( x.i * y.r - x.r * y.i ) / a;
    
    return z;
}

class class_complex
{
    float r, i;
    
    public:
    
    class_complex( void ) {}
    class_complex( float r0, float i0 ) { r = r0; i = i0; }

    float get_r( void ) { return r; }
    float get_i( void ) { return i; }

    class_complex operator + ( class_complex& y );
    class_complex operator - ( class_complex& y );
    class_complex operator * ( class_complex& y );
    class_complex operator / ( class_complex& y );
};

class_complex class_complex::operator + ( class_complex& y )
{
    class_complex z;
    
    z.r = r + y.r;
    z.i = i + y.i;
    
    return z;
}

class_complex class_complex::operator - ( class_complex& y )
{
    class_complex z;
    
    z.r = r - y.r;
    z.i = i - y.r;
    
    return z;
}

class_complex class_complex::operator * ( class_complex& y )
{
    class_complex z;
    
    z.r = r * y.r - i * y.i;
    z.i = r * y.i + i * y.r;
    
    return z;
}

class_complex class_complex::operator / ( class_complex& y )
{
    class_complex z;
    float a;
    
    a = y.r * y.r + y.i * y.i;
    z.r = ( r * y.r + i * y.i ) / a;
    z.i = ( i * y.r - r * y.i ) / a;
    
    return z;
}

complex c1, c2, c3, c4;

class_complex C1(1, 1), C2(2, 2), C3, C4;

int main( void )
{
    c1.r = 1; c1.i = 1;
    c2.r = 2; c2.i = 2;
    
    c3 = c1 * c2;
    c4 = c3 / c1;
    
    C3 = C1 * C2;
    C4 = C3 / C1;
    
    while ( 1 )
    {
    }
}

再此强调,操作符重载成员函数不能继承,原因就在于强类型匹配,即重载的操作符只用于特定类型对象的操作。因此上面的类class_complex应是顶层类,即不宜再有派生类。

使用特权

评论回复
32
小李志| | 2008-4-15 08:54 | 只看该作者

哈哈

此贴最牛,无一人回,居然自穿裤子,牛,牛

使用特权

评论回复
33
HWM|  楼主 | 2008-4-15 09:27 | 只看该作者

C++ 随笔-20,题外话:初学C++不晕才怪


初次接触C++,头晕是自然。我当初学的时候也着实晕了一阵。因为C和C++有着相当大的差异(若想充分利用C++功能的话)。简单言之,C是平面的而C++则是立体的,这有点象平面几何和立体几何的差别。C程序再复杂,其细节都能相对直接地呈现在你面前。而C++则不然,它是形式简约内涵丰富,你非得深入下去才能探清其中的奥秘所在。

既然要往下探,就要找准门道。有几个关键点是要着重领会的:

    一,封装

    要弄清楚封装的含义(无论如何封装对外还是一个变量名,只是比较特殊用“对象”来起一个别名),结构和各成分的作用。关键点在于代码的封装,这是OOP的精髓。由于有了代码的封装,使得对象(一种特殊的变量)活了起来。

    二,继承

    正因为有了继承,才使C++具有了立体性。虽然继承性在理解C++中是一个坎,但若能从一般常理一点点的深入下去,应该不会太难。当然对于多重继承,由于用处不大而其又有一定的复杂性可以不去理会,况且并不是所有的编译器都支持多重继承。

    三,重载

    关于重载只要弄明白何谓“名字分裂”就行。虽然被重载的对象(函数或操作符)表面上具有相同的标识,但由于编译器为它们重新分配了不同的内部标识名,所以本质上来说它们都是一些具有不同内部标识的对象(函数或操作符)。

    四,多态性

    多态性才是C++的本质性的特点,也是真正具有相当难度的地方。若能将这部分彻底搞通了,才可以说真正领会了C++的真缔。我会用相当篇幅阐述这方面的内容,希望能对相关概念的理解有所帮助。

C/C++已经成为了嵌入式领域的一种“文化”,要想进入到嵌入式系统的研发,C/C++是必须要掌握的。就象我们现在谈论到所谓国际化就会联想到国际语言——英语。别太着急,先找本C/C++教科书,系统认真的看几遍。再抓住重点,深入下去。结合实际应用,学会C/C++不是十分难的事。

使用特权

评论回复
34
xushouxue| | 2008-4-15 15:12 | 只看该作者

一句没看 继续

使用特权

评论回复
35
happytoday| | 2008-4-15 17:46 | 只看该作者

好贴留名

使用特权

评论回复
36
tomystory| | 2008-4-16 00:36 | 只看该作者

楼主牛人啊

前段时间正好在看C++,看了楼主这些刚好可以加强一些概念。

不过感觉自己还是要最终去写一些实际代码的好

使用特权

评论回复
37
HWM|  楼主 | 2008-4-16 08:37 | 只看该作者

to 36楼:确实,语言(包括任何工具)最终都是拿来用的。

看懂了只是学好了一半。要融会贯通,成为自己一种潜意识,才能说真正的掌握了。

使用特权

评论回复
38
今晚打老虎| | 2008-4-16 09:10 | 只看该作者

丢个坨坨,标记下

使用特权

评论回复
39
HWM|  楼主 | 2008-4-16 10:42 | 只看该作者

C++ 随笔-21,两个形而上学的玩意儿,

函数操作符和下标操作符的重载

在C/C++中有两个特殊的符号,即“()”和“[]”,前者可用于函数而后者则用于数组。它们分别作用于函数参数和数组下标,所以在C++中它们又分别被称为函数操作符和下标操作符,且可以被重载。下面给出两个例子:

一,用重载函数符支持多维矩阵(就形式而言)

class A
{
    int value[10][10];

    public:

    A( void )
    {
        for ( int i = 0; i < 10; i++ )
            for ( int j = 0; j < 10; j++ ) value[j] = 0;
    }

    int& operator () ( int i, int j ) { return value[j]; }
}

int main( void )
{
    A a, b, c;
    int i, j;

    for ( i = 0; i < 10; i++ )
        for ( j = 0; j < 10; j++ ) a( i, j ) = 1;

    b = a;

    for ( i = 0; i < 10; i++ )
        for ( j = 0; j < 10; j++ ) c( i, j ) = a( i, j ) + b( i, j );

    while ( 1 )
    {
    }
}

以上用类A封装了一个数组int[10][10]。由于函数操作符不限参数个数,所以可以封装任意维的数组。另外函数操作符使对象形式上可以作为一个“算子”(类似函数名)出现在表达式中。

二,用下标操作符重载封装一维数组

用函数操作符重载来封装数组,形式上看还是有些别扭。对于一维数组来说就可以直接用下标操作符重载来实现相关的封装,具体看下面程序:

class A
{
    int value[10];

    public:

    A( void )
    {
        for ( int i = 0; i < 10; i++ ) value = 0;
    }

    int& operator [] ( int i ) { return value; }
}

int main( void )
{
    A a, b, c;
    int i;

    for ( i = 0; i < 10; i++ ) a = 1;

    b = a;

    for ( i = 0; i < 10; i++ ) c = a + b;

    while ( 1 )
    {
    }
}

注意:下标操作符只能带一个参数(下标)。

使用特权

评论回复
40
hotpower| | 2008-4-16 15:49 | 只看该作者

特别喜欢C++~~~现在几乎在所有的MCU,ARM,DSP用它

使用特权

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

本版积分规则