[搬运]如何在C++中实现多态性

也没什么好说的,仅仅做了个测试,了解一下为什么会有一些莫名其妙的规定。

以前学C++时我对这些是一直没弄懂的,但愿对某些人还是有所帮助的~~
下述源代码在VC++6.0下通过。
Tab变成只占1格了,将就看看吧=。=或者copy到编辑器中=。=



// File Name : polymorphism_test.cpp
// Author : keakon
// Create Date : 2006/5/11
// Last Edited Date : 2006/5/26
// 通过3次测试,演示了如何实现多态性。

#include <iostream>
#include <iterator>
#include <ostream>
#include <string>

using std::cerr;
using std::cout;
using std::string;

//实覆盖
class A1
{
public:
 A1(string const& name) : m_Name(name) {cout << getName() << "A1::A1()/n";}
 ~A1() {cout << getName() << "A1::~A1()/n";}
 void print() const {cout << getName() << "A1::print()/n";}
protected:
 string const& getName() const {return m_Name;}
private:
 string m_Name;
};

class A2 : public A1
{
public:
 A2(string const& name) : A1(name) {cout << getName() << "A2::A2()/n";}
 ~A2() {cout << getName() << "A2::~A2()/n";}
 void print() const {cout << getName() << "A2::print()/n";}
};

//虚覆盖
class B1
{
public:
 B1(string const& name) : m_Name(name) {cout << getName() << "B1::B1()/n";}
 virtual ~B1() {cout << getName() << "B1::~B1()/n";}
 virtual void print() const {cout << getName() << "B1::print()/n";}
protected:
 string const& getName() const {return m_Name;}
private:
 string m_Name;
};

class B2 : public B1
{
public:
 B2(string const& name) : B1(name) {cout << getName() << "B2::B2()/n";}
 virtual ~B2() {cout << getName() << "B2::~B2()/n";}
 virtual void print() const {cout << getName() << "B2::print()/n";}
};

//虚覆盖,并使用实析构函数(这是个错误)
class C1
{
public:
 C1(string const& name) : m_Name(name) {cout << getName() << "C1::C1()/n";}
 ~C1() {cout << getName() << "C1::~C1()/n";}
 virtual void print() const {cout << getName() << "C1::print()/n";}
protected:
 string const& getName() const {return m_Name;}
private:
 string m_Name;
};

class C2 : public C1
{
public:
 C2(string const& name) : C1(name) {cout << getName() << "C2::C2()/n";}
 ~C2() {cout << getName() << "C2::~C2()/n";}
 virtual void print() const {cout << getName() << "C2::print()/n";}
};

//分开各部分的输出
void printLine(unsigned int height = 1, unsigned int length = 20,
        char ch = ‘-‘, std::ostream& out = cout)
{
 const string LINE(length, ch);

for (; height != 0; --height)
 {
  //在out上输出一行ch
  std::copy(LINE.begin(), LINE.end(), std::ostream_iterator<char>(out));
  out << ‘/n‘;
 }
}

//测试
int main()
{
 try
 {
  //实覆盖
  cout << "Real override 1:/n";
  {
   printLine();
   A1 a1("a1.");
   printLine();
   A2 a2("a2.");
   printLine(2);
   
   a1.print();
   printLine();
   a2.print(); //调用的是a2.A2::print()
   printLine();
   static_cast<A1>(a2).print();
   //上句调用A1::A1(A1&)或A1::A1(A1 const&)生成一个临时的A1类的变量
   //然后这个临时变量调用A1::print()
   //在脱离作用域后(该语句结束时)将调用A1::~A1()
   //其余见注释4
   printLine();
   a2.A1::print(); //不同于上句,此处不调用析构函数
   
   printLine(2);
  } //超出作用域,调用析构函数;下面也按照同样的格式使用花括号
  
  printLine(3);

//使用指针实现实覆盖,可能会产生错误
  cout << "Real override 2:/n";
  {
   printLine();
   A1* pa1 = new A1("pa1->");
   printLine();
   A1* pa2 = new A2("pa2->"); //注意是基类的指针
   printLine();
   A2* pa3 = new A2("pa3->");
   printLine();
   A2 a4("a4.");
   A1* pa4 = &a4; //注意是基类的指针
   A1& ra2 = *pa2; //注意是基类的引用   
   printLine(2);
   
   pa1->print(); //未检查指针,因为出错时new会抛出异常
   printLine();
   pa2->print(); //调用的是pa2->A1::print()
   printLine();
   pa3->print(); //调用的是pa3->A2::print()
   printLine();
   pa4->print(); //调用的是a4.A1::print(),即pa4->A1::print()
   printLine();
   ra2.print();
   printLine(2); //调用的是pa2->A1::print()
   
   delete pa1;
   printLine();
   delete pa2; //错误,(*pa2).A2::~A2()不会被调用
   printLine();
   delete pa3;
   printLine();
   pa1 = NULL;
   pa2 = NULL;
   pa3 = NULL;
   pa4 = NULL; //pa4不是new出来的,不用delete
  }

printLine(3);
  
  //下面不再重复实覆盖中一些相同的测试
  
  //虚覆盖
  cout << "Virtual override 1:/n";
  {
   printLine();
   B1 b1("b1.");
   printLine();
   B2 b2("b2.");
   printLine();
   
   b1.print();
   printLine();
   b2.print(); //调用的是b2.B2::print(),同实函数一样
   printLine();
  }

printLine(3);
  
  cout << "Virtual override 2:/n";
  {
   printLine();
   B1* pb1 = new B1("pb1->");
   printLine();
   B1* pb2 = new B2("pb2->"); //注意是基类的指针
   printLine();
   B1& rb2 = *pb2; //注意是基类的引用
   printLine();
   
   pb1->print();
   printLine();
   pb2->print(); //调用的是pb2->B2::print()
   printLine();
   rb2.print(); //调用的是pb2->B2::print()
   printLine();
   
   delete pb1;
   printLine();
   delete pb2;
   pb1 = NULL;
   pb2 = NULL;
  }

printLine(3);
  
  //虚覆盖,并使用实析构函数(这是个错误)
  cout << "Virtual override 1, using real destruction:/n";
  {
   printLine();
   C1 c1("c1.");
   printLine();
   C2 c2("c2.");
   printLine(2);
   
   c1.print();
   printLine();
   c2.print();
   printLine(2);
  }
  
  printLine(3);

cout << "Virtual override 2, using real destruction:/n";
  {
   printLine();
   C1* pc1 = new C1("pc1->");
   printLine();
   C1* pc2 = new C2("pc2->"); //注意是基类指针
   printLine(2);
   
   pc1->print();
   printLine();
   pc2->print(); //pc2->C2::~C2()不会被调用
   printLine(2);
   
   delete pc1;
   printLine();
   delete pc2;
   cout << std::endl;
   pc1 = NULL;
   pc2 = NULL;
  }
  
  return 0;
 }

catch (std::bad_alloc&)
 //如果内存不够,new抛出std::bad_alloc
 //似乎在VC++下永远不会抛出该异常,但也不会出错;在g++等编译器中则会抛出该异常
 {
  cerr << "/nNo enough memory!/n";
  return 1;
 }

catch (...)
 {
  cerr << "/n发现未知异常,也许你人品有问题。";
  return 2;
 }
}

/*
注:其实格式完全不必这样写,很多都无需测试;但为了便于对照,我还是这样写了。

结论:

1.通过对象直接调用成员函数时,始终默认使用该对象的类的成员函数(除非用::显示指定类名)。

2.通过指向对象的指针或引用调用成员函数时:
如果该函数是实函数,则调用该指针或引用的类的成员函数;
如果该函数是虚函数,则调用该指针或引用指向的对象的类的成员函数。

3.基类析构函数在此情况下必须为虚函数(此时,其派生类的析构函数也将是虚函数):
用new来创建派生类对象,并且delete时使用的指针为基类的指针。
若为实析构函数,则派生类部分的成员将不会被析构。

4.若未定义复制构造函数,不要将一个派生类对象强制类型转换为基类对象。
因为若未显式定义复制构造函数,则编译器会生成一个默认的复制构造函数。
然而这个默认的复制构造函数将不会做你希望做的其他的事,如:
如果要在构造函数中维护一个类对象的计数器,则这个函数不会导致计数器增加;
如果要在构造函数中new一个对象,则这个函数也不会new该有的对象。
这个默认的复制构造函数进行的是浅拷贝,使用的成员都是原来的对象的。
因此这个临时对象在析构时,delete的是原来的对象new出来的对象;
而原来的对象在析构时将再次delete这个对象;这属于未定义行为,可能引起程序崩溃。
并且,即使在析构函数中,在delete这个对象后,将指向这个对象的指针赋值为NULL也无效;
因为更改的是临时对象的指针,而原对象的指针仍指向那个被delete了的对象。

关于第4点的结论,我实际上是写了另一个测试代码证明的。
不过那个测试代码在最后一次测试时被我改得太乱了,就不给出了。
有兴趣的可以将A1和A2类添加静态的对象计数器、对象指针和复制构造函数,再自己去研究。
我在VC++6.0 SP6上测试时,debug和release模式下运行状态居然不同(前者崩溃,后者看上去正常)。
然而我一直没太多时间好好研究它,问了好多人却没人给出正确回答。
最后我只好自己跟踪反汇编代码了。能得出上述的结论,还得感谢VC++的调试器。
*/

时间: 2024-12-25 23:56:11

[搬运]如何在C++中实现多态性的相关文章

如何在makfile中查看变量的值

在makefile中查看变量的取值是多少应该是一个比较麻烦的问题,但是本大神自己研究出一个十分方便的方法.这个方法十分简单.现在介绍如下 如果在一个十分复杂庞大的makefile文件中,有个地方用到一个变量SRC_FILE,你很想知道makefile运行到此处的时候这个变量的值为多少.那么你可以在这个变量的下面写两行东东: $(shell echo $(SRC_FILE) > readme.txt ) rrrrrrrrrrrrrrrrrrr 第一行其实是调用shell命令来将这个变量的值输入到r

如何在Angular2-cli中使用插件(不使用配置)

重要点: 要把插件放在src/assets文件中 如何在angular-cli中使用jquery插件 a.在index.html中引入 <script  src="./assets/jquery-1.8.3.min.js"></script> b. 在要使用jquery的组件中声明(XX.component.ts) declare var $ : any; c.导入onInit import { OnInit} from '@angular/core'; d.输

Excel技巧|如何在Excel中快速的批量将unix时间戳转化为北京时间

本文标签:  Excel技巧 unix时间戳转化北京时间 Excel时间戳转化北京时间 互联网杂谈 批量将将unix时间戳转化为北京时间 方法/步骤 单击要获得北京时间的那一列,右键,选择[设置单元格格式],在弹出的窗口中,左侧选择 [日期],右侧选择你想要的时间格式,点击确定. 选中其中一个单元格,输入公式 =(A2+8*3600)/86400+70*365+19 其中,A2是要转化的时间戳的单元格. 输入完公式,按下[回车键]. 该时间戳即转化为北京时间. 选中上面转化好的北京时间单元格,鼠

如何在Excel中少犯二(I)

作者:何明科链接:https://zhuanlan.zhihu.com/p/23472480来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. 收到不少建议,要求开知乎Live来谈谈Excel.然而考虑到知识储备有限,还是先写文章来满足大家的需求,同时从特别窄的话题开始:"如何在Excel中少犯二".从这个话题开始的理由特别简单:首先,时常在别人的Excel数据模型中发现各种细小错误,哪怕作者花费再多的时间把图表搞得再精美,顿时对整个模型的结论产生怀疑:其次

如何在html中插入视频

如何在html中插入视频 1,插入优酷视频: 在优酷分享界面有个html代码,直接复制放入body中,定义div的align居中即可 2.插入本地视频:用video属性  用mp4格式 <video>标签的属性 src :视频的属性 poster:视频封面,没有播放时显示的图片 preload:预加载 autoplay:自动播放 loop:循环播放 controls:浏览器自带的控制条 width:视频宽度 height:视频高度

如何在java中使用sikuli进行自动化测试

很早之前写过一篇介绍sikuli的文章.本文简单介绍如何在java中使用sikuli进自动化测试. 图形脚本语言sikuli sikuli IDE可以完成常见的单击.右击.移动到.拖动等鼠标操作,java引用sikuli-script.jar同样可以执行这些常见的鼠标操作,因此即可方便的编写java实现识别图片并模拟点击/拖动目标控件. sikuli-script.jar:http://download.csdn.net/download/hqd1986/4557974 将sikuli-scri

安卓开发_WebView如何在Fragment中使用

之前学习了如何在activity中使用WebView控件来显示网页. 在我的实际开发中,有需要在Fragment中用到WebView控件的,那么就百度学习了一下 其实很简单,但是当然不是和在Activity中使用的方法一样 具体看代码 1 package com.example.qunxiong; 2 3 import android.os.Bundle; 4 import android.support.v4.app.Fragment; 5 import android.view.Layout

如何在iOS中使用Block

如何在iOS中使用Block Block可以帮助我们组织独立的代码段,并提高复用性和可读性.iOS4在UIKit中引入了该特征.超过100个的Apple API都使用了Block,所以这是一个我们必须开始熟悉的知识. Block是什么样的? 你可以使用^操作符来声明一个Block变量,它表示一个Block的开始. int num1 = 7; int(^aBlock)(int) = ^)int num2) { return num1+nunm2; }; 在如上代码中我们将Block声明为一个变量,

如何在Javascript中利用封装这个特性

对于熟悉C#和Java的兄弟们,面向对象的三大思想(封装,继承,多态)肯定是了解的,那么如何在Javascript中利用封装这个特性呢? 我们会把现实中的一些事物抽象成一个Class并且把事物的属性(名词)作为Class的Property把事物的动作(动词)作为Class的methods.在面向对象的语言中(C#等)都会有一些关键字来修饰类或者属性(Private,public,protect),这些关键词描述了访问的权限,不多做解释.泗阳县民用航空局 我们来看看Javascript的易变的特性