开始本文主题之前,先来看下面这个例子。
例1:
#include <iostream>
using namespace std;
class Animal{
public:
void talk(){cout << "Animal is talking..." << endl;}
};
class Person: public Animal
{
public:
void talk(){cout << "hello, How are you!" << endl;}
};
class Dog: public Animal
{
public:
void talk(){cout << "Wang! Wang!" << endl;}
};
class Cat:public Animal
{
public:
void talk(){cout << "Miao! Miao!" << endl;}
};
//该处的引用可以改为指针类型(需要修改对应程序),执行结果一样,下文会统一使用指针类型。
void how_talk(Animal &a)
{
a.talk();
}
int main(int argc, char *argv[])
{
Animal a;
a.talk();
Person p;
p.talk();
Dog d;
d.talk();
Cat c;
c.talk();
cout << "sizeof(Animal) = " << sizeof(Animal) << endl;
return 0;
}
执行结果如下:
Animal is talking...
hello, How are you!
Wang! Wang!
Miao! Miao!
sizeof(Animal) = 1
例2.修改例1中的main函数部分:修改内容如下所示:
int main(int argc, char *argv[])
{
Animal a;
Person p;
Dog d;
Cat c;
how_talk(a);
how_talk(p);
how_talk(d);
how_talk(c);
return 0;
}
执行结果如下:
Animal is talking...
Animal is talking...
Animal is talking...
Animal is talking...
通过例1,我们可以得到以下信息:
- 类的成员函数不占类的空间,也就是说无成员变量的类,不占内存空间(只不过为了标识其存在,编译器会默认给其分配1个字节的空间)。
- 类的继承会发生名字隐藏的现象。(同名函数,调用时调用的是子类的函数,而屏蔽了父类的函数)
对于例1的结果,应该都没有疑问,那么例2的输出结果可能就有疑惑了;为什么how_talk()传的参数不同,4次输出结果都一样呢,而且都是调用了父类的talk()方法?
我们调用how_talk()是在执行时调用的,编译器编译时并不知道我们要调用那个类的talk()方法,因此编译器会根据参数类型自动判断,参数类型为Animal的引用,编译器就会认为此处是要调用Animal这个类的talk()方法,然后编译器就将talk()绑定到了Animal类的talk()上去了。因此后面执行的时候,不管参数传什么,都会调用父类Animal的talk(),而不是我们所想的,传递不同参数,调用各自的talk()方法。这叫编译时绑定。
由此,C++又引入了一个新的面向对象特征:多态。有了多态,这个问题就迎刃而解了。
多态
多态提供了在程序运行时,依据对象的类型,使用父类指针或父类的引用来调用相应的函数的方法。没有多态的话,程序调用哪个函数只能在编译时决定,这样就没有考虑多态的机制灵活。多态使用virtual关键字。
多态的3要素
- 多态要有继承;
- 多态需要使用
virtual虚函数,子类中要实现和父类同名的函数,函数的名字和参数列表必须要一样。 - 多态使用父类的指针或者父类的引用来调用不同子类中的函数。
多态实例程序
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void talk(){cout << "Animal is talking..." << endl;} //virtual修饰的虚函数
};
//如果在子类中有与父类中的虚函数同名的函数(包括参数列表也要相同),那么子类中对应的函数也会成为虚函数
//因此子类中不加virtual修饰也可,加上也不会错。学完多态后看,可以自己测试。
class Person: public Animal
{
public:
void talk(){cout << "hello, How are you!" << endl;}
};
class Dog: public Animal
{
public:
void talk(){cout << "Wang! Wang!" << endl;}
};
class Cat:public Animal
{
public:
void talk(){cout << "Miao! Miao!" << endl;}
};
void how_talk(Animal *a)
{
a->talk();
}
int main(int argc, char *argv[])
{
Animal a;
Person p;
Dog d;
Cat c;
how_talk(&a);
how_talk(&p);
how_talk(&d);
how_talk(&c);
cout << "sizeof(Animal)" << sizeof(Animal) << endl;
return 0;
}
来看一下执行结果:
Animal is talking...
hello, How are you!
Wang! Wang!
Miao! Miao!
sizeof(Animal) = 4
对比本文最开始的两个例子,可以发现,Animal类占的空间变了,成了4;常见的4字节类型有int类型、指针类型等,暂时记一下,立马讨论该问题。还有一个不同就是,通过传递给how_talk()不同的参数,可以调用各自对应的talk()方法,而不再是编译时绑定的情况了,从执行结果来看,现在变成了执行时绑定,这就是多态的作用!!
由此引出了一个新概念:虚函数表。虚函数有一个对应的表,称为虚函数表,虚函数表中保存的是类中所有虚函数的地址,即各虚函数的函数指针。(马上揭晓sizeof(Animal) = 4的由来)
虚函数表
类中有一个或者多个虚函数时,编译器会为这个类添加一张类型独有的虚函数表,表中放着指向虚函数的函数指针。并且编译器会自动给对象添加一个指针成员(4字节问题的答案已经揭晓)。该指针位于类的前4个字节。这个指针指向的是虚函数表。相同类型的对象,这个指针应该指向同一张虚函数表。
下面来做个实验验证一下上述说法,改动main函数即可。如下所示:
int main(int argc, char *argv[])
{
Animal a, b;
Person p;
Dog d;
Cat c;
//取每个对象前4字节的内容
cout << hex << *(int *)&a << endl;
cout << *(int *)&b << endl;
cout << *(int *)&p << endl;
cout << *(int *)&d << endl;
cout << *(int *)&c << endl;
cout << dec << endl;
return 0;
}
输出结果如下
8048df8
8048df8
8048de8
8048dd8
8048dc8
a和b两个对象类型相同,得出前4个字节的值(即虚函数表的地址)相同;所以相同类型的对象,共用一份虚函数表。p,d,c3个对象的类型不同,得出的结果也不同;
通过上图的分析,我们得知,输出结果里的4个值便是虚函数表里存放各函数指针的地址了。如地址==8048df8==里保存的就是Animal类中虚函数talk()的函数指针。接下来我们尝试一下,通过地址的强制类型转换来执行一下这些函数,来真正的测试一下这些地址对应的,到底是不是我们各自类里的虚函数。
修改main函数,如下所示:
int main(int argc, char *argv)
{
Animal a, b;
Person p;
Dog d;
Cat c;
cout << "-----------------------" << endl;
void (*func)() = (void (*)())(*(int *)(*(int *)&a));
func();
func = (void (*)())(*(int *)(*(int *)&p));
func();
func = (void (*)())(*(int *)(*(int *)&d));
func();
func = (void (*)())(*(int *)(*(int *)&c));
func();
cout << "-----------------------" << endl;
return 0;
}
输出结果如下所示:
-----------------------
Animal is talking...
hello, How are you!
Wang! Wang!
Miao! Miao!
-----------------------
OK,经过实验,验证了我们之前所有的说法;下面在类里再添加一个虚函数,然后利用上述方式调用执行。
代码如下所示:
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void talk(){cout << "Animal is talking..." << endl;}
virtual void foo(){cout << "Animal foo..." << endl;}
};
class Person: public Animal
{
public:
void talk(){cout << "hello, How are you!" << endl;}
void foo(){cout << "hello, foo" << endl;}
};
class Dog: public Animal
{
public:
void talk(){cout << "Wang! Wang!" << endl;}
void foo(){cout << "Wang! foo" << endl;}
};
class Cat:public Animal
{
public:
void talk(){cout << "Miao! Miao!" << endl;}
void foo(){cout << "Miao! foo" << endl;}
};
void how_talk(Animal *a)
{
a->talk();
}
int main(int argc, char *argv[])
{
Animal a, b;
Person p;
Dog d;
Cat c;
cout << "-----------------------" << endl;
void (*func)() = (void (*)())(*(int *)(*(int *)&a));
func();
func = (void (*)())(*(int *)(*(int *)&p));
func();
func = (void (*)())(*(int *)(*(int *)&d));
func();
func = (void (*)())(*(int *)(*(int *)&c));
func();
cout << "-----------------------" << endl;
cout << "-----------------------" << endl;
//实现思想:指针偏移到下一个函数指针的位置,然后调用。
void (*f)() = (void (*)())(*((int *)(*(int *)&a)+1));
f();
f = (void (*)())(*((int *)(*(int *)&p)+1));
f();
f = (void (*)())(*((int *)(*(int *)&d)+1));
f();
f = (void (*)())(*((int *)(*(int *)&c)+1));
f();
cout << "-----------------------" << endl;
return 0;
}
执行结果如下:
-----------------------
Animal is talking...
hello, How are you!
Wang! Wang!
Miao! Miao!
-----------------------
-----------------------
Animal foo...
hello, foo
Wang! foo
Miao! foo
-----------------------
上述程序的执行过程,不再具体分析。
关于虚函数和虚函数表的相关知识,暂时介绍到这里。下面介绍一些虚函数的使用过程中需要注意的一些地方。
不能成为虚函数的函数
- 只能在类的定义范围内使用,不属于类的函数(如
main函数)不能成为虚函数。 - 类中的static静态成员函数也不能成为虚函数:
virtual static goo(); - 构造函数也不能成为虚函数。
通常情况下,应该把类的析构函数定义为虚函数。
如下所示,父类指针指向子类对象,会造成内存泄漏。
#include <iostream>
using namespace std;
class A
{
public:
A(int a):data(NULL)
{
cout << "A::A(int)" << endl;
data = new int(a);
}
~A()
{
cout << "A::~A()" << endl;
delete data;
}
private:
int *data;
};
class B:public A
{
public:
B():A(10), bdata(NULL)
{
cout << "B::B(int)" << endl;
bdata = new int(10);
}
~B()
{
cout << "B::~B()" << endl;
delete bdata;
}
private:
int *bdata;
};
int main(int argc, char *argv[])
{
A *pb = new B;
delete pb;
return 0;
}
输出结果如下:
A::A(int)
B::B(int)
A::~A()
通过输出发现,程序结束时,在执行delete语句时,触发了父类A的析构函数;但是一直到程序结束,也没有触发子类B的析构函数。这样就会造成内存泄漏,对程序带来了极大的不稳定性。现在对父类A的析构函数加virtual进行修饰,再尝试执行本程序。
#include <iostream>
using namespace std;
class A
{
...
virtual ~A() //析构函数定义为虚函数
{
cout << "A::~A()" << endl;
delete data;
}
...
};
class B: public A
{
...
}
int main(int argc, char *argv[])
{
A *pb = new B;
delete pb;
return 0;
}
此时程序的执行结果如下:
A::A(int)
B::B(int)
B::~B()
A::~A()
此时,程序就会正常执行了,并且也释放了之前申请的内存空间。这样就避免了内存泄漏的产生。
由此可见,把类的析构函数定义为虚函数,当程序调用该类的析构函数时,程序会自动的先调用其子类的析构函数,然后再调用自己的析构函数,通过这种方式来层层释放,最终申请的资源全都释放完毕,保证了程序的稳定性。
子类指针指向父类对象的bug
//演示子类指针指向父类对象的bug
#include <iostream>
using namespace std;
class A{
public:
int x;
};
class B:public A{
public:
int y;
};
int main(int argc, char *argv[])
{
int i = 10;
cout << "i=" << i <<endl;
A a;
B * pb = (B*)&a;
pb->y = 20;
cout << "i=" << i << endl;
return 0;
}
执行结果如下:
i=10
i=20
分析:如下图所示,检查main函数的栈可知,bp->y和i的地址相同;修改了bp->y的值,i的值也会相应的改变。
通过gdb调试,也可以检查出原因:
(gdb) print &i
$1 = (int *) **0xbffff3bc**
(gdb) print pb
$2 = (B *) 0xbffff3b8
(gdb) print &pb->x
$3 = (int *) 0xbffff3b8
(gdb) print &pb->y
$4 = (int *) **0xbffff3bc**
通过gdb打印变量i和pb->y的地址,可以看到,它们是一样的,也就是说i就是pb->y,pb->y就是i。修改两者任一个,另一个也会变化。