C++学习从零开始(六)

来源:网络时间:2012-04-05 16:04:56

  再看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中被定义成私有成员,也 上面通过派生而生成的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来说是公共成员,没有问题。

  #p#副标题#e#

  接着是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++从零开始(十二)》中会专门提出一个概念以给出一种方案来指导如何设计类及各类的关系。由于篇幅限制,本文分成了上中下三篇,剩下的内容在本文的后两篇说明

  #p#副标题#e#

文章内容来源于网络,不代表本站立场,若侵犯到您的权益,可联系我们删除。(本站为非盈利性质网站) 联系邮箱:rjfawu@163.com