软件发布| 专题库| 优优资讯| 苹果专区| 安卓专区| 软件下载| 首页
优优资讯 电脑教程 安卓教程 安卓攻略 苹果教程 苹果攻略 新闻资讯

C++基础入门教程之类的相关知识三

时间:2015-05-11 来源:本站整理 我要评论

  C++类的内容很丰富,继前两天的课程介绍,今天带给大家的是C++类最后一部分知识。

  虚的使用

  由于C++中实现“虚”的方式需要借助派生的手段,而派生是生成类型,因此“虚”一般映射为类型上的间接,而不是上面频道那种通过实例(一组共谐电路)来实现的间接。注意“简化操作”实际就是指用函数映射复杂的操作进而简化代码的编写,利用函数名映射的地址来间接执行相应的代码,对于虚函数就是一种调用形式表现多种执行结果。而“提高效率”是一种算法上的改进,即频道是通过重复十组共谐电路来实现的,正宗的空间换时间,不是类型上的间接可以实现的。因此C++中的“虚”就只能增加代码的灵活性和简化操作(对于上面提出的三个间接的好处)。

  比如动物会叫,不同的动物叫的方式不同,发出的声音也不同,这就是在类型上需要通过“一种手段”(叫)来表现不同的效果(猫和狗的叫法不同),而这需要“另一段代码”来实现,也就是通过派生来实现。即从类Animal派生类Cat和类Dog,通过将“叫(Gnar)”声明为Animal中的虚函数,然后在Cat和Dog中各自再实现相应的Gnar成员函数。如上就实现了用Animal::Gnar的调用表现不同的效果,如下:

  Cat cat1, cat2; Dog dog; Animal *pA[] = { &cat1, &dog, &cat2 };

  for( unsigned long i = 0; i < sizeof( pA ); i++ ) pA[ i ]->Gnar();

  上面的容器pA记录了一系列的Animal的实例的引用(关于引用,可参考《C++从零开始(八)》),其语义就是这是3个动物,至于是什么不用管也不知道(就好象这台电视机有10个频道,至于每个是什么台则不知道),然后要求这3个动物每个都叫一次(调用Animal::Gnar),结果依次发出猫叫、狗叫和猫叫声。这就是之前说的增加灵活性,也被称作多态性,指同样的Animal::Gnar调用,却表现出不同的形态。上面的for循环不用再写了,它就是“一种手段”,而欲改变它的表现效果,就再使用“另一段代码”,也就是再派生不同的派生类,并把派生类的实例的引用放到数组pA中即可。

  因此一个类的成员函数被声明为虚函数,表示这个类所映射的那种资源的相应功能应该是一个使用方法,而不是一个实现方式。如上面的“叫”,表示要动物“叫”不用给出参数,也没有返回值,直接调用即可。因此再考虑之前的收音机和数字式收音机,其中有个功能为调台,则相应的函数应该声明为虚函数,以表示要调台,就给出频率增量或减量,而数字式的调台和普通的调台的实现方式很明显的不同,但不管。意思就是说使用收音机的人不关心调台是如何实现的,只关心怎样调台。因此,虚函数表示函数的定义不重要,重要的是函数的声明,虚函数只有在派生类中实现有意义,父类给出虚函数的定义显得多余。因此C++给出了一种特殊语法以允许不给出虚函数的定义,格式很简单,在虚函数的声明语句的后面加上“= 0”即可,被称作纯虚函数。如下:

  class Food; class Animal { public: virtual void Gnar() = 0, Eat( Food& ) = 0; };

  class Cat : public Animal { public: void Gnar(), Eat( Food& ); };

  class Dog : public Animal { void Gnar(), Eat( Food& ); };

  void Cat::Gnar(){} void Cat::Eat( Food& ){} void Dog::Gnar(){} void Dog::Eat( Food& ){}

  void main() { Cat cat; Dog dog; Animal ani; }

  上面在声明Animal::Gnar时在语句后面书写“= 0”以表示它所映射的元素没有定义。这和不书写“= 0”有什么区别?直接只声明Animal::Gnar也可以不给出定义啊。注意上面的Animal ani;将报错,因为在Animal::Animal中需要填充Animal的虚函数表,而它需要Animal::Gnar的地址。如果是普通的声明,则这里将不会报错,因为编译器会认为Animal::Gnar的定义在其他的文件中,后面的连接器会处理。但这里由于使用了“= 0”,以告知编译器它没有定义,因此上面代码编译时就会失败,编译器已经认定没有Animal::Gnar的定义。

  但如果在上面加上Animal::Gnar的定义会怎样?Animal ani;依旧报错,因为编译器已经认定没有Animal::Gnar的定义,连函数表都不会查看就否定Animal实例的生成,因此给出Animal::Gnar的定义也没用。但映射元素Animal::Gnar现在的地址栏填写了数字,因此当cat.Animal::Gnar();时没有任何问题。如果不给出Animal::Gnar的定义,则cat.Animal::Gnar();依旧没有问题,但连接时将报错。

  注意上面的Dog::Gnar是private的,而Animal::Gnar是public的,结果dog.Gnar();将报错,而dog.Animal::Gnar();却没有错误(由于它是虚函数结果还是调用Dog::Gnar),也就是前面所谓的public等与类型无关,只是一种语法罢了。还有class Food;,不用管它是声明还是定义,只用看它提供了什么信息,只有一个--有个类型名的名字为Food,是类型的自定义类型。而声明Animal::Eat时,编译器也只用知道Food是一个类型名而不是程序员不小心打错字了就行了,因为这里并没有运用Food。

  上面的Animal被称作纯虚基类。基类就是类继承体系中最上层的那个类;虚基类就是基类带有纯虚成员函数;纯虚基类就是没有成员变量和非纯虚成员函数,只有纯虚成员函数的基类。上面的Animal就定义了一种规则,也称作一种协议或一个接口。即动物能够Gnar,而且也能够Eat,且Eat时必须给出一个Food的实例,表示动物能够吃食物。即Animal这个类型成了一张说明书,说明动物具有的功能,它的实例变得没有意义,而它由于使用纯虚函数也正好不能生成实例。

  如果上面的Gner和Eat不是纯虚函数呢?那么它们都必须有定义,进而动物就不再是一个抽象概念,而可以有实例,则就可以有这么一种动物,它是动物,但它又不是任何一种特定的动物(既不是猫也不是狗)。很明显,这样的语义和纯虚基类表现出来的差很远。

  那么虚继承呢?被虚继承的类的成员将被间接操作,这就是它的“一种手段”,也就是说操作这个被虚继承的类的成员,可能由于得到的偏移值不同而操作不同的内存。但对虚类表的修改又只限于如果重复出现,则修改成间接操作同一实例,因此从根本上虚继承就是为了解决上篇所说的鲸鱼有两个饥饿度的问题,本身的意义就只是一种算法的实现。这导致在设计海洋生物和脯乳动物时,无法确定是否要虚继承父类动物,而要看派生的类中是否会出现类似鲸鱼那样的情况,如果有,则倒过来再将海洋生物和脯乳动物设计成虚继承自动物,这不是好现象。

  static(静态)

  在《C++从零开始(五)》中说过,静态就是每次运行都没有变化,而动态就是每次运行都有可能变化。C++给出了static关键字,和上面的public、virtual一样,只是个语法标识而已,不是类型修饰符。它可作用于成员前面以表示这个成员对于每个实例来说都是不变的,如下:

  struct A { static long a; long b; static void ABC(); }; long A::a;

  void A::ABC() { a = 10; b = 0; }; void main() { A a; a.a = 10; a.b = 32; }

  上面的A::a就是结构A的静态成员变量,A::ABC就是A的静态成员函数。有什么变化?上面的映射元素A::a的类型将不再是long A::而是long。同样A::ABC的类型也变成void()而不是void( A:: )()。

  首先,成员要对它的类的实例来说都是静态的,即成员变量对于每个实例所标识的内存的地址都相同,成员函数对于每个this参数进行修改的内存的地址都是不变的。上面把A::a和A::ABC变成普通类型,而非偏移类型,就消除了它们对A的实例的依赖,进而实现上面说的静态。

  由于上面对实例依赖的消除,即成员函数去掉this参数,成员变量映射的是一确切的内存地址而不再是偏移,所以struct A { static long a; };只是对变量A::a进行了声明,其名字为A::a,类型为long,映射的地址并没有给出,即还未定义,所以必须在全局空间中(即不在任何一个函数体内)再定义一遍,进而有long A::a;。同样A::ABC的类型为void(),被去除了this参数,进而在A::ABC中的b = 10;等同于A::b = 10;,发现A::b是偏移类型,需要this参数,则等同于this->A::b = 10;。结果A::ABC没有this参数,错误。而对于a = 10;,等同于A::a = 10;,而已经有这个变量,故没任何问题。

  注意上面的a.a = 10;等同于a.A::a = 10;,而A::a不是偏移类型,那这里不是应该报错吗?对此C++特别允许这种类型不匹配的现象,其中的“a.”等于没有,因为这正是前面我们要表现的静态成员。即A a, b; a.a = 10; b.a = 20;执行后,a.a为20,因为不管哪个实例,对成员A::a的操作都修改的同一个地址所标识的内存。

  什么意义?它们和普通的变量的区别就是名字被A::限定,进而能表现出它们的是专用于类A的。比如房子,房子的门的高度和宽度都定好了,有两个房子都是某个公司造的,它们的门的高度和宽度相同,因此门的高度和宽度就应该作为那个公司造的房子的静态成员以记录实际的高度和宽度,但它们并不需要因实例的不同而变化。

  除了成员,C++还提供了静态局部变量。局部变量就是在函数体内的变量,被一对“{}”括起来,被限制了作用域的变量。对于函数,每次调用函数,由于函数体内的局部变量都是分配在栈上,按照之前说的,这些变量其实是一些相对值,则每次调用函数,可能由于栈的原因而导致实际对应的地址不同。如下:

  void ABC() { long a = 0; a++; } void BCD() { long d = 0; ABC(); }

  void main() { ABC(); BCD(); }

  上面main中调用ABC而产生的局部变量a所对应的地址和由于调用BCD,而在BCD中调用ABC而产生的a所对应的地址就不一样,原理在《C++从零开始(十五)》中说明。因此静态局部变量就表示那个变量的地址不管是通过什么途径调用它所在的函数,都不变化。如下:

  void ABC() { static long a = 0; a++; } void BCD() { long d = 0; d++; ABC(); }

  void main() { ABC(); BCD(); }

  上面的变量a的地址是固定值,而不再是原来那种相对值了。这样从main中调用ABC和从BCD中调用ABC得到的变量a的地址是相同的。上面等同于下面:

  long g_ABC_a = 0; void ABC() { g_ABC_a++; } void BCD() { long d = 0; d++; ABC(); }

  void main() { ABC(); BCD(); }

  因此上面ABC中的静态局部变量a的初始化实际在执行main之前就已经做了,而不是想象的在第一次调用ABC时才初始化,进而上面代码执行完后,ABC中的a的值为2,因为ABC的两次调用。

  它的意义?表示这个变量只在这个函数中才被使用,而它的生命期又需要超过函数的执行期。它并不能提供什么语义(因为能提供的“在这个函数才被使用”使用局部变量就可以做到),只是当某些算法需要使用全局变量,而此时这个算法又被映射成了一个函数,则使用静态变量具有很好的命名效果--既需要全局变量的生存期又应该有局部变量的语义。

  inline(嵌入)

  函数调用的效率较低,调用前需要将参数按照调用规则存放起来,然后传递存放参数的内存,还要记录调用时的地址以保证函数执行完后能回到调用处(关于细节在《C++从零开始(十五)》中讨论),但它能降低代码的长度,尤其是函数体比较大而代码中调用它的地方又比较多,可以大幅度减小代码的长度(就好像循环10次,如果不写循环语句,则需要将循环体内的代码复制10遍)。但也可能倒过来,调用次数少而函数体较小,这时之所以还映射成函数是为了语义更明确。此时可能更注重的是执行效率而不是代码长度,为此C++提供了inline关键字。

  在函数定义时,在定义语句的前面书写inline即可,表示当调用这个函数时,在调用处不像原来那样书写存放、传递参数的代码,而将此函数的函数体在调用处展开,就好像前面说的将循环体里的代码复制10遍一样。这样将不用做传递参数等工作,代码的执行效率将提高,但最终生成的代码的长度可能由于过多的展开而变长。如下:

  void ABCD(); void main() { ABCD(); } inline void ABCD() { long a = 0; a++; }

  上面的ABCD就是inline函数。注意ABCD的声明并没有书写inline,因为inline并不是类型修饰符,它只是告诉编译器在生成这个函数时,要多记录一些信息,然后由连接器根据这些信息在连接前视情况展开它。注意是“视情况”,即编译器可能足够智能以至于在连接时发现对相应函数的调用太多而不适合展开进而不展开。对此,不同的编译器给出了不同的处理方式,对于VC,其就提供了一个关键字__forceinline以表示相应函数必须展开,不用去管它被调用的情况。

  前面说过,对于在类型定义符中书写的函数定义,编译器将把它们看成inline函数。变成了inline函数后,就不用再由于多个中间文件都给出了函数的定义而不知应该选用哪个定义所产生的地址,因为所有调用这些函数的地方都不再需要函数的地址,函数将直接在那里展开。

  const(常量)

  前面提到某公司造的房子的门的高度和宽度应该为静态成员变量,但很明显,在房子的实例存在的整个期间,门的高度和宽度都不会变化。C++对此专门提出了一种类型修饰符--const。它所修饰的类型表示那个类型所修饰的地址类型的数字不能被用于写操作,即地址类型的数字如果是const类型将只能被读,不能被修改。如:const long a = 10, b = 20; a++; a = 4;(注意不能cosnt long a;,因为后续代码都不能修改a,而a的值又不能被改变,则a就没有意义了)。这里a++;和a = 4;都将报错,因为a的类型为cosnt long,表示a的地址所对应的内存的值不能被改变,而a++;和a = 4;都欲改变这个值。

  由于const long是一个类型,因此也就很正常地有const long*,表示类型为const long的指针,因此按照类型匹配,有:const long *p = &b; p = &a; *p = 10;。这里p = &a;按照类型匹配很正常,而p是常量的long类型的指针,没有任何问题。但是*p = 10;将报错,因为*p将p的数字直接转换成地址类型,也就成了常量的long类型的地址类型,因此对它进行写入操作错误。

  注意有:const long* const p = &a; p = &a; *p = 10;,按照从左到右修饰的顺序,上面的p的类型为const long* const,是常量的long类型的指针的常量,表示p的地址所对应的内存的值不能被修改,因此后边的p = &a;将错误,违反const的意义。同样*p = 10;也错误。不过可以:

  long a = 3, *const p = &a; p = &a; *p = 10;

  上面的p的类型为long* const,为long类型的常量,因此其必须被初始化。后续的p = &a;将报错,因为p是long* const,但*p = 10;却没有任何问题,因为将long*转成long后没有任何问题。所以也有:

  const long a = 0; const long* const p = &a; const long* const *pp = &p;

  只要按照从左到右的修饰顺序,而所有的const修饰均由于取内容操作符“*”的转换而变成相应类型中指针类型修饰符“*”左边的类型,因此*pp的类型是const long* const,*p的类型是const long。

  应注意C++还允许如下使用:

  struct A { long a, b; void ABC() const; };

  void A::ABC() const { a = 10; b = 10; }

  上面的A::ABC的类型为void( A:: )() const,其等同于:

  void A_ABC( const A *this ) { this->a = 10; this->b = 10; }

  因此上面的a = 10;和b = 10;将报错,因为this的类型是const A*。上面的意思就是函数A::ABC中不能修改成员变量的值,因为各this的参数变成了const A*,但可以修改类的静态成员变量的值,如:

  struct A { static long c; long a, b; void ABC() const; } long A::c;

  void A::ABC() const { a = b = 10; c = 20; }

  等同于:void A_ABC( const A *this ) { this->a = this->b = 10; A::c = 20; }。故依旧可以修改A::c的值。

  有什么意义?出于篇幅,有关const的语义还请参考我写的另一篇文章《语义的需要》。

  friend(友员)

  发信机具有发送电波的功能,收信机具有接收电波的功能,而发信机、收信机和电波这三个类,首先发信机由于将信息传递给电波而必定可以修改电波的一些成员变量,但电波的这些成员应该是protected,否则随便一个石头都能接收或修改电波所携带的信息。同样,收信机要接收电波就需要能访问电波的一些用protected修饰的成员,这样就麻烦了。如果在电波中定义两个公共成员函数,让发信机和收信机可以通过它们来访问被protected的成员,不就行了?这也正是许多人犯的毛病,既然发信机可以通过那个公共成员函数修改电波的成员,那石头就不能用那个成员函数修改电波吗?这等于是原来没有门,后来有个门却不上锁。为了消除这个问题,C++提出了友员的概念。

  在定义某个自定义类型时,在类型定义符“{}”中声明一个自定义类型或一个函数,在声明或定义语句的前面加上关键字friend即可,如:

  class Receiver; class Sender;

  class Wave { private: long b, c; friend class Receiver; friend class Sender; };

  上面就声明了Wave的两个友员类,以表示Receiver和Sender具备了Wave的资格,即如下:

  class A { private: long a; }; class Wave : public A { … };

  void Receiver::ABC() { Wave wav; wav.a = 10; wav.b = 10; wav.A::a = 10; }

  上面由于Receiver是Wave的友员类,所以在Receiver::ABC中可以直接访问Wave::a、Wave::b,但wav.A::a = 10;就将报错,因为A::a是A的私有成员,Wave不具备反问它的权限,而Receiver的权限等同于Wave,故权限不够。

  同样,也可有友员函数,即给出函数的声明或定义,在语句前加上friend,如下:

  class Receiver { public: void ABC(); };

  class A { private: long a; friend void Receiver::ABC(); };

  这样,就将Receiver::ABC作为了A的友员函数,则在Receiver::ABC中,具有类A具有的所有权限。

  应注意按照给出信息的思想,上面还可以如下:

  class A { private: long a; friend void Receiver::ABC() { long a = 0; } };

  这里就定义了函数Receiver::ABC,由于是在类型定义符中定义的,前面已经说过,Receiver::ABC将被修饰为inline函数。

  那么友员函数的意义呢?一个操作需要同时操作两个资源中被保护了的成员,则这个操作应该被映射为友员函数。如盖章需要用到文件和章两个资源,则盖章映射成的函数应该为文件和章的友员函数。

  名字空间

  前面说明了静态成员变量,它的语义是专用于某个类而又独立于类的实例,它与全局变量的关键不同就是名字多了个限定符(即“::”,表示从属关系),如A::a是A的静态成员变量,则A::a这个名字就可以表现出a从属于A。因此为了表现这种从属关系,就需要将变量定义为静态成员变量。

  考虑一种情况,映射采矿。但是在陆地上采矿和在海底采矿很明显地不同,那么应该怎么办?映射两个函数,名字分别为MiningOnLand和MiningOnSeabed。好,然后又需要映射在陆地勘探和在海底勘探,怎么办?映射为ProspectOnLand和ProspectOnSeabed。如果又需要映射在陆地钻井和在海底钻井,在陆地爆破和在海底爆破,怎么办?很明显,这里通过名字来表现语义已经显得牵强了,而使用静态成员函数则显得更加不合理,为此C++提供了名字空间,格式为namespace <名字> { <各声明或定义语句> }。其中的<名字>为定义的名字空间的名字,而<各声明或定义语句>就是多条声明或定义语句。如下:

  namespace OnLand { void Mining(); void Prospect(); void ArtesianWell(){} }

  namespace OnSeabed { void Mining(); void Prospect(); void ArtesianWell(){} }

  void OnLand::Mining() { long a = 0; a++; } void OnLand::Prospect() { long a = 0; a++; }

  void OnSeabed::Mining() { long a = 0; a++; } void OnSeabed::Prospect() { long a = 0; a++; }

  上面就定义了6个元素,每个的类型都为void()。注意上面OnLand::ArtesianWell和OnSeabed::ArtesianWell的定义直接写在“{}”中,将是inline函数。这样定义的六个变量它们的名字就带有限定符,能够从名字上体现从属关系,语义表现得比原来更好,OnSeabed::Prospect就表示在海底勘探。注意也可以如下:

  namespace A { long b = 0; long a = 0; namespace B { long B = 0; float a = 0.0f } }

  namespace C { struct ABC { long a, b, c, d; void ABCD() { a = b = c = d = 12; } } ab; }

  namespace D { void ABC(); void ABC() { long a = 0; a++; } extern float bd; }

  即名字空间里面可以放任何声明或定义语句,也可以用于修饰自定义结构,因此就可以C::ABC a; a.ABCD();。应注意C++还允许给名字空间别名,比如:namespace AB = C; AB::ABC a; a.ABCD();。这里就给名字空间C另起了个名字AB,就好像之前提过的typedef一样。

  还应注意自定义类型的定义的效果和名字空间很像,如struct A { long a; };将生成A::a,和名字空间一样为映射元素的名字加上了限定符,但应该了解到结构A并不是名字空间,即namespace ABC = A;将失败。名字空间就好像所有成员都是静态成员的自定义结构。

  为了方便名字空间的使用,C++提供了using关键字,其后面接namespace和名字空间的名字,将把相应名字空间中的所有映射元素复制一份,但是去掉了名字前的所有限定符,并且这些元素的有效区域就在using所在的位置,如:

  void main() { { using namespace C; ABC a; a.ABCD(); } ABC b; b.ABCD(); }

  上面的ABC b;将失败,因为using namespace C;的有效区域只在前面的“{}”内,出了就无效了,因此应该C::ABC b; b.ABCD();。有什么用?方便书写。因为每次调用OnLand::Prospect时都要写OnLand::,显得有点烦琐,如果知道在某个区域内并不会用到OnSeabed的成员,则可以using namespace OnLand;以减小代码的繁杂度。

  注意C++还提供了using更好的使用方式,即只希望去掉名字空间中的某一个映射元素的限定符而不用全部去掉,比如只去掉OnLand::Prospect而其它的保持,则可以:using OnLand::Prospect; Prospect(); Mining();。这里的Mining();将失败,而Prospect();将成功,因为using OnLand::Prospect;只去掉了OnLand::Prospect的限定符。

  至此基本上已经说明了C++的大部分内容,只是还剩下模板和异常没有说明(还有自定义类型的操作符重载,出于篇幅,在《C++从零开始(十七)》中说明),它们带的语义都很少,很大程度上就和switch语句一样,只是一种算法的包装而已。下篇介绍面向对象编程思想,并给出“世界”的概念以从语义出发来说明如何设计类及类的继承体系。
 

用户评论

(已有0条评论)
表情
注:您的评论需要经过审核才能显示哦,请文明发言!
还没有评论,快来抢沙发吧!
快速检索
0-9 A B C D E F G H I J K L M N O P Q R S T U V W X Y Z