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


  本文的中篇已经介绍了虚的意思,便是要 直接 获得,而且举例 注明电视机的频道便是让人 直接 获得电视台频率的, 因此其从这个 意思上说是虚的,由于它可能操作失败--某个频道还未调好而招致一片雪花 。而且 注明了 直接的 好处,便是只用编好一段代码(按5频

  道),则每次执行它时可能有不同 后果(今日5频道被设置成中央5台,明天 可以被定成中央2台),进而使得前面编的程序(按5频道)显得很灵便 。 留神虚之所以 可以很灵便是由于它 定然通过“一种 目的”来 直接达到 目标,如每个频道记录着一个频率 。但这是不够的, 定然还有“另一段代码”能转变那种 目的的 后果(频道记录的频率),如调台 。

  先看虚继承 。它 直接从子类的实例中 获得父类实例的所在位置,通过虚类表实现(这是“一种 目的”),接着就必须 可以有“另一段代码”来转变虚类表的值以 体现其灵便性 。首先 可以自己来编写这段代码,但就要求清晰编译器将虚类表放在什么地方,而不同的编译器有不同的实现 步骤,则这样编写的代码兼容性很差 。C++固然给出了“另一段代码”,便是当某个类在同一个类继承体系中被 频繁虚继承时,就转变虚类表的值以使各子类 直接 获得的父类实例是同一个 。此操作的 性能很差,仅仅只不过节约内存而已 。

  如:

  struct A { long a; };

  struct B : virtual public A { long b; }; struct C : virtual public A { long c; };

  struct D : public B, public C { long d; };

  这里的D中有两个虚类表,分别从B和C继承而来,在D的 构造函数中,编译器会编写必要的代码以正确初始化D的两个虚类表以使得通过B继承的虚类表和通过C继承的虚类表而 获得的A的实例是同一个 。

  再看虚函数 。它的地址被 直接 获得,通过虚函数表实现(这是“一种 目的”),接着就必须还能转变虚函数表的内容 。同上,假如自己改写,代码的兼容性很差,而C++也给出了“另一段代码”,和上面一样,通过在派生类的 构造函数中填写虚函数表,依据目前派生类的状况来书写虚函数表 。它 定然将某虚函数表填充为目前派生类下,类型、名字和原来被定义为虚函数的那个函数尽量匹配的函数的地址 。

  如:

  struct A { virtual void ABC(), BCD( float ), ABC( float ); };

  struct B : public A { virtual void ABC(); };

  struct C : public B { void ABC( float ), BCD( float ); virtual float CCC( double ); };

  struct D : public C { void ABC(), ABC( float ), BCD( float ); };

  在A::A中,将两个A::ABC和一个A::BCD的地址填写到A的虚函数表中 。

  在B::B中,将B::ABC和继承来的B::BCD和B::ABC填充到B的虚函数表中 。

  在C::C中,将C::ABC、C::BCD和继承来的C::ABC填充到C的虚函数表中,并增加一个元素:C::CCC 。

  在D::D中,将两个D::ABC和一个D::BCD以及继承来的D::CCC填充到D的虚函数表中 。

  这里的D是 顺次继承自A、B、C,并没有由于多重继承而产生两个虚函数表,其惟独一个虚函数表 。 固然D中的成员函数没有用virtual 润饰,但它们的地址依然被填到D的虚函数表中,由于virtual只不过 示意 使用那个成员函数时需求 直接 获得其地址,与是不是填写到虚函数表中没有关系 。

  电视机为何要用频道来 直接 获得电视台的频率?由于电视台的频率人不容易记,而且假如晓得一个频率, 渐渐地调整共谐电容的电容值以使电路达到那个频率效率很低下 。而做10组共谐电路,每组电路的电容值调好后就不再动,通过切换不同的共谐电路来实现 快捷转换频率 。 因此 直接还 可以 普及效率 。还有,5频道 原来是中央5台,后来看腻了把它换成中央2台,则同样的动作(按5频道)将产生不同的 后果,“按5频道”这个程序编得很灵便 。

  由上面,至少 可以晓得: 直接用于简化操作、 普及效率和增加灵便性 。这里提到的 直接的三个 用处都基于这么一个想法--用“一种 目的”来达到 目标,用“另一段代码”来实现上面提的 用处 。而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::ABC变成一般类型,而非偏移类型,就 肃清了它们对A的实例的依赖,进而实现上面说的静态 。