C++从零开始(十一)上篇——类的相关知识

C++从零开始(十一)上篇

——类的相关知识

    前面已经介绍了自定义类型的成员变量和成员函数的概念,并给出它们各自的语义,本文继续说明自定义类型剩下的内容,并说明各自的语义。


权限

    成员函数的提供,使得自定义类型的语义从资源提升到了具有功能的资源。什么叫具有功能的资源?比如要把收音机映射为数字,需要映射的操作有调整收音机的频率以接收不同的电台;调整收音机的音量;打开和关闭收音机以防止电力的损耗。为此,收音机应映射为结构,类似下面:
    struct Radiogram
    {
        double Frequency;  /* 频率 */  void TurnFreq( double value );   // 改变频率
        float  Volume;     /* 音量 */  void TurnVolume( float value );  // 改变音量
        float  Power;      /* 电力 */  void TurnOnOff( bool bOn );      // 开关
        bool   bPowerOn;   // 是否开启
    };
    上面的Radiogram::Frequency、Radiogram::Volume和Radiogram::Power由于定义为了结构Radiogram的成员,因此它们的语义分别为某收音机的频率、某收音机的音量和某收音机的电力。而其余的三个成员函数的语义也同样分别为改变某收音机的频率、改变某收音机的音量和打开或关闭某收音机的电源。注意这面的“某”,表示具体是哪个收音机的还不知道,只有通过成员操作符将左边的一个具体的收音机和它们结合时才知道是哪个收音机的,这也是为什么它们被称作偏移类型。这一点在下一篇将详细说明。
    注意问题:为什么要将刚才的三个操作映射为结构Radiogram的成员函数?因为收音机具有这样的功能?那么对于选西瓜、切西瓜和吃西瓜,难道要定义一个结构,然后给它定义三个选、切、吃的成员函数??不是很荒谬吗?前者的三个操作是对结构的成员变量而言,而后者是对结构本身而言的。那么改成吃快餐,吃快餐的汉堡包、吃快餐的薯条和喝快餐的可乐。如果这里的两个吃和一个喝的操作变成了快餐的成员函数,表示是快餐的功能?!这其实是编程思想的问题,而这里其实就是所谓的面向对象编程思想,它虽然是很不错的思想,但并不一定是合适的,下篇将详细讨论。
    上面我们之所以称收音机的换台是功能,是因为实际中我们自己是无法直接改变收音机的频率,必须通过旋转选台的那个旋钮来改变接收的频率,同样,调音量也是通过调节音量旋钮来实现的,而由于开机而导致的电力下降也不是我们直接导致,而是间接通过收听电台而导致的。因此上面的Radiogram::Power、Radiogram::Frequency等成员变量都具有一个特殊特性——外界,这台收音机以外的东西是无法改变它们的。为此,C++提供了一个语法来实现这种语义。在类型定义符中,给出这样的格式:<权限>:。这里的<权限>为public、protected和private中的一个,分别称作公共的、保护的和私有的,如下:
    class Radiogram
    {
    protected: double m_Frequency; float m_Volume; float m_Power;
    private:   bool   m_bPowerOn;
    public:    void TurnFreq( double ); void TurnVolume( float ); void TurnOnOff( bool );
    };
    可以发现,它和之前的标号的定义格式相同,但并不是语句修饰符,即可以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; };。
    什么意思?很简单,公共的成员外界可以访问,保护的成员外界不能访问,私有的成员外界及子类不能访问。关于子类后面说明。先看公共的。对于上面,如下将报错:
    Radiogram a; a.m_Frequency = 23.0; a.m_Power = 1.0f; a.m_bPowerOn = true;
    因为上面对a的三次操作都使用了a的保护或私有成员,编译器将报错,因为这两种成员外界是不能访问的。而a.TurnFreq( 10 );就没有任何问题,因为成员函数Radiogram::TurnFreq是公共成员,外界可以访问。那么什么叫外界?对于某个自定义类型,此自定义类型的成员函数的函数体内以外的一切能写代码的地方都称作外界。因此,对于上面的Radiogram,只有它的三个成员函数的函数体内可以访问它的成员变量。即下面的代码将没有问题。
    void Radiogram::TurnFreq( double value ) { m_Frequency += value; }
    因为m_Frequency被使用的地方是在Radiogram::TurnFreq的函数体内,不属于外界。
    为什么要这样?表现最开始说的语义。首先,上面将成员定义成public或private对于最终生成的代码没有任何影响。然后,我之前说的调节接收频率是通过调节收音机里面的共谐电容的容量来实现的,这个电容的容量人必须借助元件才能做到,而将接收频率映射成数字后,由于是数字,则CPU就能修改。如果直接a.m_Frequency += 10;进行修改,就代码上的意义,其就为:执行这个方法的人将收音机的接收频率增加10KHz,这有违我们的客观世界,与前面的语义不合。因此将其作为语法的一种提供,由编译器来进行审查,可以让我们编写出更加符合我们所生活的世界的语义的代码。
    应注意可以union ABC { long a; private: short b; };。这里的ABC::a之前没有任何修饰,那它是public还是protected?相信从前面举的那么多例子也已经看出,应该是public,这也是为什么我之前一直使用struct和union来定义自定义类型,否则之前的例子都将报错。而前篇说过结构和类只有一点很小的区别,那就是当成员没有进行修饰时,对于类,那个成员将是private而不是public,即如下将错误。
    class ABC { long a; private: short b; }; ABC a; a.a = 13;
    ABC::a由于前面的class而被看作private。就从这点,可以看出结构用于映射资源(可被直接使用的资源),而类用于映射具有功能的资源。下篇将详细讨论它们在语义上的差别。


构造和析构

    了解了上面所提的东西,很明显就有下面的疑问:
    struct ABC { private: long a, b; }; ABC a = { 10, 20 };
    上面的初始化赋值变量a还正确吗?当然错误,否则在语法上这就算一个漏洞了(外界可以借此修改不能修改的成员)。但有些时候的确又需要进行初始化以保证一些逻辑关系,为此C++提出了构造和析构的概念,分别对应于初始化和扫尾工作。在了解这个之前,让我们先看下什么叫实例(Instance)。
    实例是个抽象概念,表示一个客观存在,其和下篇将介绍的“世界”这个概念联系紧密。比如:“这是桌子”和“这个桌子”,前者的“桌子”是种类,后者的“桌子”是实例。这里有10只羊,则称这里有10个羊的实例,而羊只是一种类型。可以简单地将实例认为是客观世界的物体,人类出于方便而给各种物体分了类,因此给出电视机的说明并没有给出电视机的实例,而拿出一台电视机就是给出了一个电视机的实例。同样,程序的代码写出来了意义不大,只有当它被执行时,我们称那个程序的一个实例正在运行。如果在它还未执行完时又要求操作系统执行了它,则对于多任务操作系统,就可以称那个程序的两个实例正在被执行,如同时点开两个Word文件查看,则有两个Word程序的实例在运行。
    在C++中,能被操作的只有数字,一个数字就是一个实例(这在下篇的说明中就可以看出),更一般的,称标识记录数字的内存的地址为一个实例,也就是称变量为一个实例,而对应的类型就是上面说的物体的种类。比如:long a, *pA = &a, &ra = a;,这里就生成了两个实例,一个是long的实例,一个是long*的实例(注意由于ra是long&所以并未生成实例,但ra仍然是一个实例)。同样,对于一个自定义类型,如:Radiogram ab, c[3];,则称生成了四个Radiogram的实例。
    对于自定义类型的实例,当其被生成时,将调用相应的构造函数;当其被销毁时,将调用相应的析构函数。谁来调用?编译器负责帮我们编写必要的代码以实现相应构造和析构的调用。构造函数的原型(即函数名对应的类型,如float AB( double, char );的原型是float( double, char ))的格式为:直接将自定义类型的类型名作为函数名,没有返回值类型,参数则随便。对于析构函数,名字为相应类型名的前面加符号“~”,没有返回值类型,必须没有参数。如下:
struct ABC { ABC(); ABC( long, long ); ~ABC(); bool Do( long ); long a, count; float *pF; };
ABC::ABC() { a = 1; count = 0; pF = 0; }
ABC::ABC( long tem1, long tem2 ) { a = tem1; count = tem2; pF = new float[ count ]; }
ABC::~ABC() { delete[] pF; }
bool ABC::Do( long cou )
{
    float *p = new float[ cou ];
    if( !p )
        return false;
    delete[] pF;
    pF = p;
    count = cou;
    return true;
}
extern ABC g_ABC;
void main(){ ABC a, &r = a; a.Do( 10 ); { ABC b( 10, 30 ); } ABC *p = new ABC[10]; delete[] p; }
ABC g_a( 10, 34 ), g_p = new ABC[5];
    上面的结构ABC就定义了两个构造函数(注意是两个重载函数),名字都为ABC::ABC(实际将由编译器转成不同的符号以供连接之用)。也定义了一个析构函数(注意只能定义一个,因为其必须没有参数,也就无法进行重载了),名字为ABC::~ABC。
    再看main函数,先通过ABC a;定义了一个变量,因为要在栈上分配一块内存,即创建了一个数字(创建装数字的内存也就导致创建了数字,因为内存不能不装数字),进而创建了一个ABC的实例,进而调用ABC的构造函数。由于这里没有给出参数(后面说明),因此调用了ABC::ABC(),进而a.a为1,a.pF和a.count都为0。接着定义了变量r,但由于它是ABC&,所以并没有在栈上分配内存,进而没有创建实例而没有调用ABC::ABC。接着调用a.Do,分配了一块内存并把首地址放在a.pF中。
    注意上面变量b的定义,其使用了之前提到的函数式初始化方式。它通过函数调用的格式调用了ABC的构造函数ABC::ABC( long, long )以初始化ABC的实例b。因此b.a为10,b.count为30,b.pF为一内存块的首地址。但要注意这种初始化方式和之前提到的“{}”方式的不同,前者是进行了一次函数调用来初始化,而后者是编译器来初始化(通过生成必要的代码)。由于不调用函数,所以速度要稍快些(关于函数的开销在《C++从零开始(十五)》中说明)。还应注意不能ABC b = { 1, 0, 0 };,因为结构ABC已经定义了两个构造函数,则它只能使用函数式初始化方式初始化了,不能再通过“{}”方式初始化了。
    上面的b在一对大括号内,回想前面提过的变量的作用域,因此当程序运行到ABC *p = new ABC[10];时,变量b已经消失了(超出了其作用域),即其所分配的内存语法上已经释放了(实际由于是在栈上,其并没有被释放),进而调用ABC的析构函数,将b在ABC::ABC( long, long )中分配的内存释放掉以实现扫尾功能。
    对于通过new在堆上分配的内存,由于是new ABC[10],因此将创建10个ABC的实例,进而为每一个实例调用一次ABC::ABC(),注意这里无法调用ABC::ABC( long, long ),因为new操作符一次性就分配了10个实例所需要的内存空间,C++并没有提供语法(比如使用“{}”)来实现对一次性分配的10个实例进行初始化。接着调用了delete[] p;,这释放刚分配的内存,即销毁了10个实例,因此将调用ABC的析构函数10次以进行10次扫尾工作。
    注意上面声明了全局变量g_ABC,由于是声明,并不是定义,没有分配内存,因此未产生实例,故不调用ABC的构造函数,而g_a由于是全局变量,C++保证全局变量的构造函数在开始执行main函数之前就调用,所有全局变量的析构函数在执行完main函数之后才调用(这一点是编译器来实现的,在《C++从零开始(十九)》中将进一步讨论)。因此g_a.ABC( 10, 34 )的调用是在a.ABC()之前,即使它的位置在a的定义语句的后面。而全局变量g_p的初始化的数字是通过new操作符的计算得来,结果将在堆上分配内存,进而生成5个ABC实例而调用了ABC::ABC()5次,由于是在初始化g_p的时候进行分配的,因此这5次调用也在a.ABC()之前。由于g_p仅仅只是记录首地址,而要释放这5个实例就必须调用delete(不一定,也可不调用delete依旧释放new返回的内存,在《C++从零开始(十九)》中说明),但上面并没有调用,因此直到程序结束都将不会调用那5个实例的析构函数,那将怎样?后面说明异常时再讨论所谓的内存泄露问题。
    因此构造的意思就是刚分配了一块内存,还未初始化,则这块内存被称作原始数据(Raw Data),前面说过数字都必须映射成算法中的资源,则就存在数字的有效性。比如映射人的年龄,则这个数字就不能是负数,因为没有意义。所以当得到原始数据后,就应该先通过构造函数的调用以保证相应实例具有正确的意义。而析构函数就表示进行扫尾工作,就像上面,在某实例运作的期间(即操作此实例的代码被执行的时期)动态分配了一些内存,则应确保其被正确释放。再或者这个实例和其他实例有关系,因确保解除关系(因为这个实例即将被销毁),如链表的某个结点用类映射,则这个结点被删除时应在其析构函数中解除它与其它结点的关系。


派生和继承

    上面我们定义了类Radiogram来映射收音机,如果又需要映射数字式收音机,它和收音机一样,即收音机具有的东西它都具有,不过多了自动搜台、存储台、选台和删除台的功能。这里提出了一个类型体系,即一个实例如果是数字式收音机,那它一定也是收音机,即是收音机的一个实例。比如苹果和梨都是水果,则苹果和梨的实例一定也是水果的实例。这里提出三个类型:水果、苹果和梨。其中称水果是苹果的父类(父类型),苹果是水果的子类(子类型)。同样,水果也是梨的父类,梨是水果的子类。这种类型体系是很有意义的,因为人类就是用这种方式来认知世界的,它非常符合人类的思考习惯,因此C++又提出了一种特殊语法来对这种语义提供支持。
    在定义自定义类型时,在类型名的后面接一“:”,然后接public或protected或private,接着再写父类的类型名,最后就是类型定义符“{}”及相关书写,如下:
    class DigitalRadiogram : public Radiogram
    {
    protected:  double m_Stations[10];
    public:     void SearchStation();                void SaveStation( unsigned long );
                void SelectStation( unsigned long ); void EraseStation( unsigned long );
    };
    上面就将Radiogram定义为了DigitalRadiogram的父类,DigitalRadiogram定义成了Radiogram的子类,被称作类Radiogram派生了类DigitalRadiogram,类DigitalRadiogram继承了类Radiogram。
    上面生成了5个映射元素,就是上面的4个成员函数和1个成员变量,但实际不止。由于是从Radiogram派生,因此还将生成7个映射,就是类Radiogram的7个成员,但名字变化了,全变成DigitalRadiogram::修饰,而不是原来的Radiogram::修饰,但是类型却不变化。比如其中一个映射元素的名字就为DigitalRadiogram::m_bPowerOn,类型为bool Radiogram::,映射的偏移值没变,依旧为16。同样也有映射元素DigitalRadiogram::TurnFreq,类型为void ( Radiogram:: )( double ),映射的地址依旧没变,为Radiogram::TurnFreq所对应的地址。因此就可以如下:
    void DigitalRadiogram::SaveStation( unsigned long index )
    {
        if( index >= 10 ) return;
        m_Station[ index ] = m_Frequency; m_bPowerOn = true;
    }
    DigitalRadiogram a; a.TurnFreq( 10 ); a.SaveStation( 3 );
    上面虽然没有声明DigitalRadiogram::TurnFreq,但依旧可以调用它,因为它是从Radiogram派生来的。注意由于a.TurnFreq( 10 );没有书写全名,因此实际是a.DigitalRadiogram::TurnFreq( 10 );,因为成员操作符左边的数字类型是DigitalRadiogram。如果DigitalRadiogram不从Radiogram派生,则不会生成上面说的7个映射,结果a.TurnFreq( 10 );将错误。
    注意上面的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 )。
    应注意在SaveStation中使用了m_bPowerOn,这个在Radiogram中被定义成私有成员,也就是说子类也没权访问,而SaveStation是其子类的成员函数,因此上面将报错,权限不够。
    上面通过派生而生成的7个映射元素各自的权限是什么?先看上面的派生代码:
    class DigitalRadiogram : public Radiogram {…};
    这里由于使用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的。
    上面可以看得很简单,即不管是什么继承,其指定了一个权限,父类中凡是高于这个权限的映射元素,都要将各自的权限降低到这个权限(注意是对子类来说),然后再继承给子类。上面一直强调“对于子类来说”,什么意思?如下:
    struct A { long a; protected: long b; private: long c; };
    struct B : protected A { void AB(); };
    struct C : private B { void ABC(); };
    void B::AB() { b = 10; c = 10; }
    void C::ABC() { a = 10; b = 10; c = 10; AB(); }
    A a; B b; C c; a.a = 10; b.a = 10; b.AB(); c.AB();
    上面的B的定义等同于struct B { protected: long a, b; private: long c; public: void AB(); };。
    上面的C的定义等同于struct C { private: long a, b, c; void AB(); public: void ABC(); };
    因此,B::AB中的b = 10;没有问题,但c = 10;有问题, 因为编译器看出B::c是从父类继承生成的,而它对于父类来说是私有成员,因此子类无权访问,错误。接着看C::ABC,a = 10;和b = 10;都没问题,因为它们对于B来说都是保护成员,但c = 10;将错误,因为C::c对于父类B来说是私有成员,没有权限,失败。接着AB();,因为C::AB对于父类B来说是公共成员,没有问题。
    接着是a.a = 10;,没问题;b.a = 10;,错误,因为B::a是B的保护成员;b.AB();,没有问题;c.AB();,错误,因为C::AB是C的私有成员。应注意一点:public、protected和private并不是类型修饰符,只是在语法上提供了一些信息,而继承所得的成员的类型都不会变化,不管它保护继承还是公共继承,权限起作用的地方是需要运用成员的地方,与类型没有关系。什么叫运用成员的地方?如下:
    long ( A::*p ) = &A::a; p = &A::b;
    void ( B::*pB )() = B::AB; void ( C::*pC )() = C::ABC; pC = C::AB;
    上面对变量p的初始化操作没有问题,这里就运用了A::a。但是在p = &A::b;时,由于运用了A::b,则编译器就要检查代码所处的地方,发现对于A来说属于外界,因此报错,权限不够。同样下面对pB的赋值没有问题,但pC = C::AB;就错误。而对于b.a = 10;,这里由于成员操作符而运用了类B的成员B::a,所以在这里进行权限检查,并进而发现权限不够而报错。
    好,那为什么要搞得这么复杂?弄什么保护、私有和公共继承?首先回想前面说的为什么要提供继承,因为想从代码上体现类型体系,说明一个实例如果是一个子类的实例,则它也一定是一个父类的实例,即可以按照父类的定义来操作它。虽然这也可以通过之前说的转换指针类型来实现,但前者能直接从代码上表现出类型继承的语义(即子类从父类派生而来),而后者只能说明用不同的类型来看待同一个实例。
    那为什么要给继承加上权限?表示这个类不想外界或它的子类以它的父类的姿态来看待它。比如鸡可以被食用,但做成标本的鸡就不能被食用。因此子类“鸡的标本”在继承时就应该保护继承父类“鸡”,以表示不准外界(但准许其派生类)将它看作是鸡。它已经不再是鸡,但它实际是由鸡转变过来的。因此私有和保护继承实际很适合表现动物的进化关系。比如人是猴子进化来的,但人不是猴子。这里人就应该使用私有继承,因为并不希望外界和人的子类——黑种人、黄种人、白种人等——能够把父类“人”看作是猴子。而公共继承就表示外界和子类可以将子类的实例看成父类的实例。如下:
struct A { long a, b; };
struct AB : private A { long c; void ABCD(); };
struct ABB : public AB { void AAA(); };
struct AC : public A { long c; void ABCD(); };
void ABC( A *a ) { a->a = 10; a->b = 20; }
void main() { AB b; ABC( &b ); AC c; ABC( &c ); }
void AB::ABCD() { AB b; ABC( &b ); }
void AC::ABCD() { AB b; ABC( &b ); }
void ABB::AAA() { AB b; ABC( &b ); }
    上面的类AC是公共继承,因此其实例c在执行ABC( &c );时将由编译器进行隐式类型转换,这是一个很奇特的特性,本文的下篇将说明。但类AB是私有继承,因此在ABC( &b );时编译器不会进行隐式类型转换,将报错,类型不匹配。对于此只需ABC( ( A* )&b );以显示进行类型转换就没问题了。
    注意前面的红字,私有继承表示外界和它的子类都不可以用父类的姿态来看待它,因此在ABB::AAA中,这是AB的子类,因此这里的ABC( &b );将报错。在AC::ABCD中,这里对于AB来说是外界,报错。在AB::ABCD中,这里是自身,即不是子类也不是外界,所以ABC( &b );将没有问题。如果将AB换成保护继承,则在ABB::AAA中的ABC( &b );将不再错误。
    关于本文及本文下篇所讨论的语义,在《C++从零开始(十二)》中会专门提出一个概念以给出一种方案来指导如何设计类及各类的关系。由于篇幅限制,本文分成了上中下三篇,剩下的内容在本文的后两篇说明。

发表于 @ 2004年07月25日 17:20:00|评论(8)|编辑

新一篇: C++从零开始(十一)中篇——类的相关知识 | 旧一篇: C++从零开始(十)——何谓类bighan 发表于2004年7月26日 9:59:00  IP:举报
一口气看完十一篇,受益匪浅!感谢作者并期待下篇.wangpeng 发表于2005年4月12日 22:01:00  IP:举报
人是猴子进化来的,但人不是猴子。这里人就应该使用私有继承,因为并不希望外界和人的子类——黑种人、黄种人、白种人等——能够把父类“人”看作是猴子。而公共继承就表示外界和子类可以将子类的实例看成父类的实例
class monkey { char *type; int age; char * adr; };
class people :private monkey
{ char *name; char * country ;
int age;
};
class YellowPeople :public people
{ };
好像没什么意义。wangpeng 发表于2005年4月21日 12:01:00  IP:举报
你说的结构是自定义的资源,在vc中还有程序中总听到资源这两个字,从底层看资源是内存,但在VC中,有时叫对话框的定义叫资源
我指的是直接给出的那样的对话框,我想问的是.这样的对话框是不是用c中的结构作的,而看那个资源代码,是那种象Fortran的代码写的.我想知道这种代码做出的资源是怎样与c++的代码接口的,请你详细介绍一些这些接口的内幕.十分感谢.
我、看了你写的com那些文章,但是不懂,COM到底都做了些什么具体的功能?组件对象模型.能提供接口,就知道这些.如何提够的接口,这些接口怎么就能与那些模块通信呢?我不知道我说的你懂没?感谢
lop5712 发表于2005年4月23日 13:25:00  IP:举报
关于私有继承,上面的解释也许不好,我最近给公司编个小程序时刚好使用了保护继承,如下:
要实现记录字体的容器,它只提供两个函数:
HFONT AddFont( LOGFONT lf );
void DeleteFont( HFONT hFont );
关键是调用AddFont时,给出的lf会和当前容器中已有的字体比较,如果lf表征的字体已经用CreateFont创建出一个,并得到其句柄,则直接返回那个句柄HFONT,否则创建一个,并且增加其引用记数,当DeleteFont调用时,要和hFont的引用记数为0后才DeleteObject。
这个字体容器我使用一个数组来实现,CArray<>。很明显,这个字体容器由于上述逻辑规则而不希望用户直接调用CArray的任何成员以破坏了逻辑规则,而只能通过上面的两个函数来操作,因此这里:
class CFontList : protected CArray< FONTSTRUCT, FONTSTRUCT& >
{
...
};
这里使用保护继承就表明CFontList并不希望别人把它看作CArray,而只能用它暴露的接口访问它。
因此“人”并不希望被他人用“猴子”的接待方式来处理,应该使用保护继承。lop5712 发表于2005年4月23日 14:12:00  IP:举报
把葱切碎作拌料,其只知道将长条形的物体,将其横向切成多段,而不是纵向切成多片。这里的“葱”是资源,“切葱”是个方法。
方法“切葱”不知道葱是什么,只知道获得的资源应该至少是长条形的,然后将其横向切成多段。

在轴上面铣一个键槽,将轴装夹定位后,用立铣刀铣一个槽出来。这里的“轴”是资源,“铣键槽”是个方法。
方法“铣键槽”不知道轴是什么,只知道获得的资源应该至少是圆柱形的,然后在这个圆柱形的表面弄一个槽出来。

每一个方法都一定要操作某个资源,没有不操作资源的方法,即使“关机”这个方法,都使得机器中本来存在的电流不存在了。即资源就是记录着某些信息的东西,这些信息被称作资源的状态,而方法就是能操作那个资源的状态。

方法能操作资源的状态,一定是方法和资源之间有某种协调性,比如切葱,葱一定切必须是长条形的;铣键槽,轴一定且必须是圆柱形的。
长条形就是“切葱”和“葱”之间的协调性,即使用“切葱”的方法,我们也可以将面条切碎,因为都是长条形。同样也可以在水管上面铣键槽,因为外表都是圆柱形。
这种协调性,就被称作接口。则称“葱”实现了“长条形”这个接口,“轴”实现了“外表圆柱形”这个接口。
如两插的电源插头只能插在两插的插座中,因为插头插脚间的距离和插座孔之间的距离相等的程度在一个范围内(即偏差在允许接受的范围内)。这时称两插插头和插座都实现了“两插”这个接口。
作为三插的插座,它实现了“三插”这个接口,因此可以和三插插头连接。将它的某两个孔扩得大一点,使得其间的距离也满足上面的“两插”的要求,则三插插座实现了两个接口——“两插”和“三插”——则它也就能插两插插头了。

电脑能操作内存,是因为内存实现了“读”和“写”这两个接口,都是通过先设置地址总线上的电位,然后向读引脚发出高电位或向写引脚发出高电位。由于端口也实现了“读”和“写”这两个接口,因此电脑也能读写端口。当硬盘亦实现了那两个接口后,也就可以读写硬盘了。虽然实际比上面要复杂地多,不会内存、端口、硬盘使用同一套接口,但它们之所以能和CPU交互,是因为大家都认同同一套接口。就好象我和你能交流,必须使用同一种语言一样。

因此,所谓的资源,就是实现了某一套协议,或称实现了某个接口的东西。而它之所以能实现接口,它一定保存了信息。比如“长条形”至少有长度和宽度两个信息,“圆柱形”有外径和长度两个信息,而资源的使用方,就是要获得那个信息,才会要求那样的接口。

上面从理论上阐述了接口和资源,是想说明接口并不是什么COM特有的,那是一个通用概念,只不过COM提供了一个接口的实现方式而已。同样,资源并不是什么电脑特有的,电脑中的资源只不过是资源的一个实现方式罢了。lop5712 发表于2005年4月23日 14:55:00  IP:举报
下面可以看你所提出的VC中的菜单、加速键等资源了。
操作系统要绘制出菜单,需要得知菜单有多少个菜单项,每个菜单项的名字。但由于是用电脑来实现,因此必须使用内存来表征菜单的各个状态——各菜单项的名字、关联ID、Enabled、Checked等。
使用内存来表征时,内存是以字节为单位的一维的,即只能通过地址这么一个参数来指定。当获得某个地址后,比如1000,称头四个字节是菜单项的个数,然后接着的四个字节是一个地址。那个地址所标识的内存中,比如2000,称头两个字节是“关联ID”,然后一个字节是“Enabled”,再一个字节是“Checked”,最后四个字节是一个char*,是以/0结尾的一个字符串,用来表示菜单项的名称。
上述换成C++上面的代码就是定义一个结构,如下:
struct MENUITEM
{
unsigned short nID;
bool bEnabled;
bool bChecked;
unsigned char *Caption;
};
struct MENU
{
unsigned long Count;
MENUITEM *pChildren;
};
上面只是一个简单定义,当某个函数知道这个结构时,它可能是:
BOOL CreateMenu( const MENU &menu );
这表示它要求传递一个地址,而那个地址中的内容“它将用MENU所表征的结构来解释”。
注意引号中的说法,部长给了发图员一叠图纸,说“第1到3张是发给生产部的,第4到7张是发给金工车间的,第8到12张是发给装配车间的”。这就是说发图员将按照部长说的结构来解释那12张图纸相应各自的目的地,如果部长不小心把第1张和第8张对调了,那么图纸将发送错误。

上面的结构就是一个协议,也就是一个接口,即CreateMenu支持MENU这个接口,而
MENU a;
// 对a的初始化
这里的a实现了MENU这个接口,因此当
CreateMenu时不会发生任何错误。
另外注意:
struct ABCD
{
unsigned short ty;
bool bd;
bool bfd;
unsigned char *dn;
};
struct DCBA
{
unsigned long t;
ABCD *p;
};
DCBA b;
// 初始化b
CreateMenu( *reinterpret_cast< MENU* >( &b ) );
上面只要在初始化b时,将其当作MENU来处理,一样能成功创建菜单,注意虽然b不是MENU类型,但它的结构和MENU相同,名字只是帮助编译器来标识不同的结构罢了。即部长给上面那种图纸的排列方式起名为AA,而部长说按BB方式发放图纸,而BB方式也是上面那种方式,则图纸照样可以正确放送,“AA”还是“BB”都只不过是个名字罢了。

因此所谓的VC中的资源,其实只不过是Windows操作系统在提供创建菜单、对话框、位图时,lop5712 发表于2005年4月23日 16:02:00  IP:举报
关于COM,是需要一些预备知识的,在此我并不想说明那些预备知识,如果有不明白的地方只有抱歉了。

不管是用Pascal还是Basic或是C或C++编写的程序,只要是编译成在Windows平台上运行的可执行代码,就都具有Windows平台上的共性。即要编写在Windows平台上的可执行代码,可以利用一个编译器和其支持的语言来简化可执行代码的编写,当然也可以直接手工书写(虽然理论上可行)。

在Windows平台上,模块就是指加载在进程中的某个可执行文件,比如.exe、.dll、.sys等。各模块之间要通信,当在同一个进程中,直接相互之间调用函数就可以了,因为在同一个虚拟地址空间下,可以直接修改IP寄存器的值来在各模块之间任意跳转。
但要通信不是跳转就够了的,比如上面调用CreateMenu函数,光调用就可以了,但如果传递进去的参数并不是一个MENU的实例,则可能失败。
即相互之间要通信不光只要能够调用,还需要有统一的协议。因此相互之间的统一是很重要的,就好象为什么一个国家要使用统一的货币,有一个统一的语言一样,交流的基础就是在某方面双方有共识。

COM是一个协议,如果你在编写可执行代码时,遵照这个协议,则可以和其他的也遵照这个协议的可执行代码进行交流(即调用相互之间的函数,传递参数)。
COM这个协议的第一个好处是所谓的语言无关性。这其实并没那么值得骄傲。
用VB编写的dll,VC中可以很正常的调用。而VC写的DLL也可以很容易地在VB中调用,就像VB调用Windows API一样。之所以能这样是因为VB和VC在代码实现上有共识(都是Windows平台上的可执行代码,函数调用规则都使用__stdcall),在资源结构上有共识(VB的String类型等效于VC中的char*),所以双方可以交流。
但语言很多种,为了相互协调,COM在语言无关上的贡献就是函数统一使用__stdcall调用,并提供了一些基础数据结构(或称作原始数据类型,如short等),这些基础数据结构是仿照C语言来书写的,因此感觉COM好象和C或C++密切相关。

在语言无关后,COM提出接口这个概念。上面已经对接口这个概念进行了说明,很容易发现它其实就是一个函数声明。而COM的贡献之重就是它并不使用一个函数的地址来实现接口,而使用一个函数指针数组来实现。后者和前者的区别就是后者代表了多个函数,而前者只代表了一个函数。
后者代表的多个函数具有某种关联性,因为是用一个函数指针数组表征的,这就提供了一个语义的表现,即函数指针数组所表征的各函数之间是有某种关联的,是什么不知道,那是设计接口的人决定的,可以利用其表征强烈的语义。
由于接口是一个函数指针数组,在C++中,虚函数表就是一个函数指针数组,因此C++中使用一个纯虚基类表征接口。

上面已经对COM的最基本和最重要的概念做了介绍,但那并不是COM协议的内容,其内容包括IUnknown的细节、基础数据类型、类型库、相关注册表项等大量内容,不是这里能说明的,请自行参考相关资料。
而COM具体所做的工作,包括提供COM运行时期库(实现上面的COM协议的内容,包括各注册表项的处理,类型库的维护等),提供大量的基础功能接口,比如IStorage、IDataTarget等,并实现一些最基本的接口。而Microsoft在COM的基础上,基于Windows,提供了OLE接口,现在称作ActiveX接口,是自动

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/lop5712/archive/2004/07/25/51433.aspx

你可能感兴趣的:(c&amp;c++,c++,struct,编译器,float,menu,windows)