【核心基础】虚函数

本节研究虚函数的相关问题;

虚函数表

无继承

代码片段

class Animal {
public:
  Animal(int age) :
   _age(age)
  {
  }

  virtual void f()
  {
    cout << "Animal::f " << _age << endl;
  }

  virtual void g()
  {
    cout << "Animal::g " << _age << endl;
  }
private:
  int _age;
};

int main(void)
{
  Animal a(10);
  cout << "Animal size: "<< sizeof(a) << endl;

  cout << "_age address: "<< ((int*)&a+1) << " value: " << *((int*)&a+1)<< endl;
  cout << "virtual fun address: "<< (int*)&a << endl;
  cout << "virtual fun[1] address: "<< (int*)*(int*)&a << endl;

  cout << "virtual fun[2] address: "<< ((int*)*(int*)&a+1) << endl;

  typedef void (*Fun)(Animal*);
  Fun pf = (Fun)*((int*)(*(int*)&a));
  pf(&a);

  typedef void (*Fun2)();
  Fun2 pg = (Fun2)*((int*)(*(int*)&a)+1);
  pg();
}

输出如下:

Animal size: 8
_age address: 0xbf8347d4 value: 10
virtual fun address: 0xbf8347d0
virtual fun[1] address: 0x8048c18
virtual fun[2] address: 0x8048c1c
Animal::f 10
Animal::g 134515314

说明几点:

(1)在GCC中(vptr放在对象头部),因此Animal的结构体大小为8,第一个字节存放的虚函数指针(vptr),第二个字节存放的是_age数据;

(2)所有的Animal的对象的vptr都是一样的,vptr为Animal虚函数表的指针,虚函数表存放各个虚函数的函数地址;

(3)在void (*Fun)(Animal*);中我们在pf函数参数中传入a地址,通过pf函数指针也是可以进行Animal成员函数的;

虚函数示意图如下

单继承(无virtual覆盖)

代码片段

class Animal {
public:
  Animal(int age) :
   _age(age)
  {
  }

  virtual void f()
  {
    cout << "Animal::f " << _age << endl;
  }

  virtual void g()
  {
    cout << "Animal::g " << _age << endl;
  }
private:
  int _age;
};

class Dog : public Animal {
public:
  Dog(int age, int id) :
   Animal(age), _id(id)
  {
  }

private:
  int _id;
};

Dog d(10, 20);
Animal a(10);

int main(void)
{
  cout << "Dog ---------------- " << endl;
  cout << "Dog size: "<< sizeof(d) << endl;

  cout << "_age address: "<< ((int*)&d+1) << " value: " << *((int*)&d+1)<< endl;
  cout << "_id address: "<< ((int*)&d+2) << " value: " << *((int*)&d+2)<< endl;
  cout << "virtual fun address: "<< (int*)&d << endl;
  cout << "virtual fun[1] address: "<< (int*)*(int*)&d << endl;

  cout << "virtual fun[2] address: "<< ((int*)*(int*)&d+1) << endl;

  typedef void (*Fun)(Animal*);
  Fun pf = (Fun)*((int*)(*(int*)&d));
  pf(&d);

  typedef void (*Fun2)();
  Fun2 pg = (Fun2)*((int*)(*(int*)&d)+1);
  pg();

  cout << "Aniaml ---------------- " << endl;
  cout << "virtual fun address: "<< (int*)&a << endl;
  cout << "virtual fun[1] address: "<< (int*)*(int*)&a << endl;

  cout << "virtual fun[2] address: "<< ((int*)*(int*)&a+1) << endl;

  Animal c(10);

  cout << "virtual fun address: "<< (int*)&c << endl;
  cout << "virtual fun[1] address: "<< (int*)*(int*)&c << endl;
  cout << "virtual fun[2] address: "<< ((int*)*(int*)&c+1) << endl;
}

输出如下:

Dog ----------------
Dog size: 12
_age address: 0x804a338 value: 10
_id address: 0x804a33c value: 20
virtual fun address: 0x804a334
virtual fun[1] address: 0x8048f00
virtual fun[2] address: 0x8048f04
Animal::f 10
Animal::g 14073600
Aniaml ----------------
virtual fun address: 0x804a340
virtual fun[1] address: 0x8048f10
virtual fun[2] address: 0x8048f14
virtual fun address: 0xbfed4890
virtual fun[1] address: 0x8048f10
virtual fun[2] address: 0x8048f14

地址示意图如下:

单继承(有virtual覆盖)

代码片段

class Animal {
public:
  Animal(int age) :
   _age(age)
  {
  }

  virtual void f()
  {
    cout << "Animal::f " << _age << endl;
  }

  virtual void g()
  {
    cout << "Animal::g " << _age << endl;
  }
private:
  int _age;
};

class Dog : public Animal {
public:
  Dog(int age, int id) :
   Animal(age), _id(id)
  {
  }

  void f()
  {
    cout << "Dog::f hello" << endl;
  }

  virtual void h()
  {
    cout << "Dog::h hello" << endl;
  }
private:
  int _id;
};

地址示意图如下:

虚函数与重载

代码片段(提示:此代码段有陷阱)

#include <iostream>
using namespace std;

class Point
{
public:
  Point(int x = 1, int y = 3) :
      _x(x), _y(y)
  {
  }

  virtual void print()
  {
    cout << "Point::print" << endl;
  }

private:
  int _x, _y;
};

class Point3D: public Point
{
public:
  void print() const
  {
    cout << "Point3D::print" << endl;
  }

private:
  int _z;
};

void print(Point a, Point* b, Point& c)
{
  a.print();
  (*b).print();
  b->print();
  c.print();
}

int main(void)
{
  Point3D point;
  print(point, &point, point);

  return 0;
}

说明几点:

(1)void print(Point a, Point* b, Point& c)函数中的,4个输出均为Point::print;

(2)请注意Point::print() 表示传入的this为一个指向常量的指针,this本身是一个常量指针,因此传入函数的是一个指向常量的常量指针;而对于print中a,b,c并不发生virtual机制,对于a,发生切割,表现为ADT行为,一定调用的是Point::print() 函数,对于b,c指向地址空间只是Point3D的中Point部分(指针的不同,主要表现为所寻址的object的不同),只能解释Point所覆盖范围的部分,由于Point::print()和Point32::print()
 const不是同一函数,因此不会发生virtual调用,只会Point::print() const;

(3)注意如果将将print函数修改为void print(Point a, const Point* b, const Point& c),此例子是无法编译通过的,因为b和c调用所传入的this指针均为const * Point const this;而对于Point::print()不为const成员函数,故将发生将指向常量的指针转换成普通指针的编译错误;

(4)修改print函数如下,此时,p输出的内容为Point3D::print;主要原因此时p所指向的地址空间为整个Point3D的地址空间,是一个显式的向下类型转换,无论如何都会变成一个Point3D类型指针的;请注意p转换成const Point3D*类型,否则不能编译通过,主要是因为Point3D提供的print函数为const函数;

void print(Point a, Point* b, Point& c)
{
  a.print();
  (*b).print();
  b->print();
  c.print();

  const Point3D* p = static_cast<const Point3D *>(b);
	p->print();
}

(5)修改main函数如下,此时并不会出现段错误,原因是在p->print()在调用Point3D中的void print() const将被解释成xx_print_xx(const Point3D * const p),而由于在此函数xx_print_xx中p并没有访问相关的成员变量,如果访问将会出现段错误;

int main(void)
{
  Point3D* p = NULL;
  p->print();

  return 0;
}

虚函数与默认实参

代码片段如下

#include <iostream>
using namespace std;

class A {
public:
   virtual int X(int x = 1) {
     return x;
   }
};

class B : public A {
public:
  int X(int x = 5) {
     cout << x << endl;
     return x;
  }

};

int main(void) {
  B b;
  cout << b.X() << endl;

  cout << "------------------------\n";
  A* a = &b;
  cout << a->X() << endl;
  return 0;
}

说明几点:

(1)对于B b并不会发生多态调用,因为C++中只有通过指针和引用才能进行动态调用,就因此b.X()就编译时已经确定好了,如果B中未重新定义X(int x=5),那么根据域的查找原则,可以调用父类的X();

(2)对于a->X()其实也是调用的B中的X(),X()中cout会执行,但是虚函数的默认实参是由本次调用的静态类型A决定的,也就是x会等于1;

(3)假设将A类去除掉virtual,修改为如下:

class A {
public:
   int X(int x = 1) {
     return x;
   }
};

(3.1)那么a->X()将会调用父类的X(),而不会发生动态调用;因为对非虚函数的调用是编译时决定的;名字查找,首先确定静态类型,找到与静态类型对应的类(或父类)中找到,找到以后,判断调用是否是虚函数,若是函数,编译器产生的代码将在运行时决定该虚函数的哪个版本否则将是一次常规的函数调用;而本例对应后一种情况,不会发生虚函数调用;

虚函数与构造(析构)函数

代码片段如下

#include <iostream>
using namespace std;

class A
{
public:
  A()
  {
    cout << "----------\n";
    X();
    cout << "A ctor\n";
  }

  virtual void X()
  {
    cout << "A::X()" << endl;
  }

  virtual ~A()
  {
    cout << "----------\n";
    X();
    cout << "A dtor\n";
  }
};

class B : public A
{
public:
  void X()
  {
    cout << "B::X()" << endl;
  }

  ~B()
  {
    cout << "----------\n";
    X();
    cout << "B dtor\n";
  }

};

int main(void)
{
  A* a = new B;
  delete a;
  return 0;
}

说明几点:

(1)A的析构函数,应该声明为virtual,因为delete a时将会执行A的析构函数,如果A的析构函数不是virtual,将不会发生动态调用,使得B对象处于半死状态;所以A的析构函数应该声明为析构函数,这样首先调用B的析构函数,继而调用A的析构函数

(2)在A的构造函数和析构函数调用虚函数,并不会发生动态调用;因为执行到A的构造函数时,B的除A的其他部分还未初始化;而执行到A的析构函数时,B除A的其他部分已经销毁掉了;因此执行父类中虚函数时,整个对象处于未完成状态;而编译器将会将不会进行动态调用,而是执行所属类型的对应版本;

时间: 2024-10-01 05:47:30

【核心基础】虚函数的相关文章

[基础] 虚函数

1. 虚函数会导致的结果:直接上个经典例子... class A { public: void a() { cout<<"A.a"<<endl; } virtual void b() { cout<<"A.b"<<endl; } }; class B: public A { public: void a() { cout<<"B.a"<<endl; } void b() {

C++基础篇--虚函数原理

虚函数算是C++最关键和核心的内容之一,是组件的基础.下面先列出一些相关名词,再围绕它们举例说明虚函数的本质实现原理. 基础概念(英文部分来自C++编程思想) 1)绑定:Connectinga function call to a function body is called binding.(把函数调用和函数实现关联的过程) 2)早绑定:Whenbinding is performed before the program is run (by the compiler and linker

C++基础(纯虚函数与抽象类)

C++基础之纯虚函数与抽象类 引言 纯虚函数在C++编程中的地位很重要,其关联到了设计模式中"接口"的概念. 语法 纯虚函数的语法: 1.  将成员函数声明为virtual 2.  后面加上 = 0 3.  该函数没有函数体 1 class <类名> 2 { 3 virtual <类型><函数名>(<参数表>) = 0; 4 - 5 }; 例如: 1 class CmdHandler 2 { 3 virtual void OnComman

C++学习基础十二——纯虚函数与抽象类

一.C++中纯虚函数与抽象类: 1.含有一个或多个纯虚函数的类成为抽象类,注意此处是纯虚函数,而不是虚函数. 2.如果一个子类继承抽象类,则必须实现父类中的纯虚函数,否则该类也为抽象类. 3.如果一个类中含有虚函数,则必须将该类的析构函数声明为虚函数. 4.虚函数与纯虚函数的声明: virtual void draw();//虚函数 virtual void draw() = 0;//纯虚函数 5.C++中支持两种多态性: 编译时多态:通过重载函数实现. 运行时多态:通过虚函数实现. 二.Jav

C++基础:虚函数、重载、覆盖、隐藏&lt;转&gt;

转自:http://www.2cto.com/kf/201404/291772.html 虚函数总是跟多态联系在一起,引入虚函数可以使用基类指针对继承类对象进行操作! 虚函数:继承接口(函数名,参数,返回值),但是实现不继承(函数体) 非虚函数:继承接口,也继承实现: 1)虚析构函数(当一个类打算作为基类使用时候,其析构函数必须是虚函数) 构造函数可以为虚函数吗? 不可以,在生成对象的时候,必须向编译器明确指定要生成什么类型的对象,因而不存在虚函数的问题:只有当对象已经存在,我用什么接口去操作它

c++ 基础(七) 函数覆盖,虚函数,纯虚函数对比

1.函数覆盖 ClassA , ClassB ,其中ClassB继承ClassA 类定义如下: #ifndef _CLASSA_H #define _CLASSA_H #include <iostream> using namespace std; class ClassA { public: ClassA(void); ~ClassA(void); void method(); }; #endif #include "stdafx.h" #include "Cl

c# 基础(重写与覆盖:接口与抽象,虚函数与抽象函数)

总结 1:不管是重写还是覆盖都不会影响父类自身的功能(废话,肯定的嘛,除非代码被改). 2:当用子类创建父类的时候,如 C1 c3 = new C2(),重写会改变父类的功能,即调用子类的功能:而覆盖不会,仍然调用父类功能. 3:虚方法.实方法都可以被覆盖(new),抽象方法,接口 不可以. 4:抽象方法,接口,标记为virtual的方法可以被重写(override),实方法不可以. 5:重写使用的频率比较高,实现多态:覆盖用的频率比较低,用于对以前无法修改的类进行继承的时候. 6:关键字:vi

C#基础(七)虚函数

若一个实例方法声明前带有virtual关键字,那么这个方法就是虚方法.虚方法与非虚方法的最大不同是,虚方法的实现可以由派生类所取代,这种取代是通过方法的重写实现的(以后再讲)虚方法的特点:虚方法前不允许有static,abstract,或override修饰符虚方法不能是私有的,因此不能使用private修饰符虚方法的执行:我们知道一般函数在编译时就静态地编译到了执行文件中,其相对地址在程序运行期间是不发生变化的,而虚函数在编译期间是不被静态编译的,它的相对地址是不确定的,它会根据运行时期对象实

C++基础知识 基类指针、虚函数、多态性、纯虚函数、虚析构

一.基类指针.派生类指针 父类指针可以new一个子类对象 二.虚函数 有没有一个解决方法,使我们只定义一个对象指针,就可以调用父类,以及各个子类的同名函数? 有解决方案,这个对象指针必须是一个父类类型,我们如果想通过一个父类指针调用父类.子类中的同名函数的话,这个函数是有要求的: 在父类中,eat函数声明之前必须要加virtual声明eat()函数为虚函数. 一旦某个函数被声明为虚函数,那么所有派生类(子类)中eat()函数都是虚函数. 为了避免你在子类中写错虚函数,在C++11中,你可以在函数