目录
多态的引入
先看以下例子,人类用手吃饭是本能,英国人用刀叉恰饭,而我们用筷子恰饭,而我们问这个人怎么吃饭,应该根据其国别来回答,而不是简单地说"用手吃"
#include <iostream>
#include <string.h>
#include <unistd.h>
using namespace std;
class Human {
public:
void eating(void) { cout<<"use hand to eat"<<endl; }
};
class Englishman : public Human {
public:
void eating(void) { cout<<"use knife to eat"<<endl; }
};
class Chinese : public Human {
public:
void eating(void) { cout<<"use chopsticks to eat"<<endl; }
};
void test_eating(Human& h)
{
h.eating();
}
int main(int argc, char **argv)
{
Human h;
Englishman e;
Chinese c;
h.eating();
e.eating();
c.eating();
test_eating(h);
test_eating(e);
test_eating(c);
return 0;
}
执行结果如下,可以看到,一开始是覆写实现的,后面通过函数引用就不能自动的识别出是哪个国家的,本能的都是用手吃,里面的机制是静态联编,后面解释,因此我们需要以多态来实现自动识别
use hand to eat
use knife to eat
use chopsticks to eat
use hand to eat
use hand to eat
use hand to eat
怎么实现,在基类中加入虚拟函数,在eating函数前加上virtual,修改程序如下,派生类继承了基类的eating函数,因此也变成了virtual函数,对于派生类的eating函数可以不写virtual
...
class Human {
private:
int a;
public:
virtual void eating(void) { cout<<"use hand to eat"<<endl; }
};
...
int main(int argc, char **argv)
{
Human h;
Englishman e;
Chinese c;
test_eating(h);
test_eating(e);
test_eating(c);
cout<<"sizeof(Human) = "<<sizeof(h)<<endl;
cout<<"sizeof(Englishman) = "<<sizeof(e)<<endl;
cout<<"sizeof(Chinese) = "<<sizeof(c)<<endl;
return 0;
}
执行结果如下,可以看到自动的识别出各国的吃饭方法,而不是简单的用手吃,上述程序中,实现的机制是所谓的动态联编
use hand to eat
use knife to eat
use chopsticks to eat
sizeof(Human) = 16
sizeof(Englishman) = 16
sizeof(Chinese) = 16
对于非虚函数,采用的是静态联编,编译时就确定调用哪个函数,因此不能自动识别出哪国人,只是简单的用手吃;对于虚函数,采用动态联编,有虚函数的对象里含有一个指针如下图,指向虚函数表,调用虚函数表时,会根据对象里的指针找到表,从表中取出函数来执行,对于静态联编效率高,而动态联编支持多态,可以看到上述原本对象4字节变量大小,多了12字节的指针执行虚函数表
多态的限制
1、使用指针或者引用来使用对象才会有多态,修改程序如下
...
void test_eating(Human h)
{
h.eating();
}
...
执行结果如下,对于test_eating函数中传值的时候没有指针,因此属于静态联编,例如"test_eating(e);",e会先变成human类,e中有a(基类)和指针,转换为human类型就只剩基类部分,因此只能是静态联编,调用的只能是Human的eating函数
use hand to eat
use hand to eat
use hand to eat
sizeof(Human) = 16
sizeof(Englishman) = 16
sizeof(Chinese) = 16
2、只有类的成员函数才能声明为虚函数
3、静态成员函数不能是虚函数
4、内联函数不能是虚函数
5、构造函数不能是虚函数
6、析构函数一般都声明为虚函数,修改程序如下
#include <iostream>
#include <string.h>
#include <unistd.h>
using namespace std;
class Human {
private:
int a;
public:
virtual void eating(void) { cout<<"use hand to eat"<<endl; }
virtual ~Human() { cout<<"~Human()"<<endl; }
};
class Englishman : public Human {
public:
void eating(void) { cout<<"use knife to eat"<<endl; }
virtual ~Englishman() { cout<<"~Englishman()"<<endl; }
};
class Chinese : public Human {
public:
void eating(void) { cout<<"use chopsticks to eat"<<endl; }
virtual ~Chinese() { cout<<"~Chinese()"<<endl; }
};
void test_eating(Human h)
{
h.eating();
}
int main(int argc, char **argv)
{
Human* h = new Human;
Englishman* e = new Englishman;
Chinese* c = new Chinese;
Human *p[3] = {h, e, c};
int i;
for (i = 0; i < 3; i++)
{
p[i]->eating();
delete p[i];
}
return 0;
}
执行结果如下,如果基类中的析构函数不设置为virtual,在main函数中三者都调用基类的析构函数,而不是各自的析构函数;而基类Human中的析构函数设置为virtual,对于Englishman会先调用自己的析构函数~Englishman(),再调用基类的析构函数~Human()
use hand to eat
~Human()
use knife to eat
~Englishman()
~Human()
use chopsticks to eat
~Chinese()
~Human()
7、重载不可设为虚函数,对于覆盖:函数参数、返回值相同,可以设为虚函数
8、返回值例外:函数参数相同,但返回值是当前对象的指针或者引用时,也可以设置为虚函数
如下程序,返回值不是对象的指针或者引用,因此这样编译会报错
class Human {
private:
int a;
public:
virtual void eating(void) { cout<<"use hand to eat"<<endl; }
virtual ~Human() { cout<<"~Human()"<<endl; }
virtual void test(void) {cout<<"Human's test"<<endl; }
};
class Englishman : public Human {
public:
void eating(void) { cout<<"use knife to eat"<<endl; }
virtual ~Englishman() { cout<<"~Englishman()"<<endl; }
virtual int test(void) {cout<<"Englishman's test"<<endl; return 1; }
};
class Chinese : public Human {
public:
void eating(void) { cout<<"use chopsticks to eat"<<endl; }
virtual ~Chinese() { cout<<"~Chinese()"<<endl; }
virtual int test(void) {cout<<"Chinese's test"<<endl; return 1; }
};
修改程序
...
class Human {
private:
int a;
public:
virtual void eating(void) { cout<<"use hand to eat"<<endl; }
virtual ~Human() { cout<<"~Human()"<<endl; }
virtual Human* test(void) {cout<<"Human's test"<<endl; return this; }
};
class Englishman : public Human {
public:
void eating(void) { cout<<"use knife to eat"<<endl; }
virtual ~Englishman() { cout<<"~Englishman()"<<endl; }
virtual Englishman* test(void) {cout<<"Englishman's test"<<endl; return this; }
};
class Chinese : public Human {
public:
void eating(void) { cout<<"use chopsticks to eat"<<endl; }
virtual ~Chinese() { cout<<"~Chinese()"<<endl; }
virtual Chinese* test(void) {cout<<"Chinese's test"<<endl; return this; }
};
void test_return(Human& h)
{
h.test();
}
int main(int argc, char **argv)
{
Human h;
Englishman e;
Chinese c;
test_return(h);
test_return(e);
test_return(c);
return 0;
}
编译不会报错,执行结果如下
Human's test
Englishman's test
Chinese's test
~Chinese()
~Human()
~Englishman()
~Human()
~Human()
类型转换
分为两种,一种是隐式转换,另外一种是显式转换
隐式转换
如下C代码,其中"int i = d",double隐式转换转为int,"int *p = str",char*隐式转换转为int*
double d = 100.1;
int i = d;
char *str = "xiaoma";
int *p = str;
显式转换
其中"int *p = (int *)str",char*显式强制转换转为int* ,打印语句在在64位系统下会有警告,指针是8字节,而unsigned int类型是4个字节
char *str = "xiaoma";
int *p = (int *)str;
printf("i = %d, str = 0x%x, p = 0x%x\n", i, (unsigned int)str, (unsigned int)p);
在C++中用以下四个函数进行转换(模板函数)
reinterpret_cast
格式:reinterpret_cast<type-id>(expression)
相当于C风格的用小括号"(type-id)"实现的强制类型转换
- type-id必须是一个指针、引用、算术类型、函数指针或者成员指针
- 它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针
- 跟C风格的强制转换类似,没有安全性检查
char *str = "xiaoma";
int *p = reinterpret_cast<int *>(str);
const_cast
格式:const_cast<type_id>(expression)
该运算符用来去除原来类型的const或者volatile属性,除了const或者volatile修饰之外,type_id和expression的类型是一样的
看如下代码,reinterpret_cast不能直接强制转换str类型,不能去掉const属性,因此通过const_cast先把const属性去掉,再利用reinterpret_cast强制转换为int*类型
const char *str = "100ask.taobao.com";
char *str2 = const_cast<char *>(str);
int *p = reinterpret_cast<int *>(str2);
dynamic
格式:dynamic_cast<type-id>(expression)
该运算符把expression转换为type-id类型的对象,type-is必须是类的指针、类的引用或者void *;如果type-id是类指针类型,那么expression也必须是一个指针;如果type-id是一个引用,那么expression也必须是一个引用。
- 用于多态场合,即:必须有虚函数
- 主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换
- 在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。
在多态的例子中,想在test_eating函数中实现分辨人是英国人还是中国人
void test_eating(Human& h)
{
Englishman *pe;
Chinese *pc;
h.eating();
/* 想分辨这个"人"是英国人还是中国人? */
if (pe = dynamic_cast<Englishman *>(&h))
cout<<"This human is Englishman"<<endl;
if (pc = dynamic_cast<Chinese *>(&h))
cout<<"This human is Chinese"<<endl;
}
int main(int argc, char **argv)
{
Human h;
Englishman e;
Chinese c;
test_eating(h);
test_eating(e);
test_eating(c);
return 0;
}
执行结果如下,实现的机制,对于动态类型转换,在对象总通过指针找到虚函数表,在虚函数表中不仅含有虚函数信息,同时还含有继承信息,根据继承信息来判断
use hand to eat
use knife to eat
This human is Englishman
use chopsticks to eat
This human is Chinese
~Chinese()
~Human()
~Englishman()
~Human()
~Human()
在中国人类中派生出广西人,修改程序如下
class Guangximan : public Chinese {
public:
void eating(void) { cout<<"use chopsticks to eat, I come from guangxi"<<endl; }
};
void test_eating(Human& h)
{
Guangximan *pg;
h.eating();
/* 想分辨这个"人"是英国人还是中国人? */
if (pe = dynamic_cast<Englishman *>(&h))
cout<<"This human is Englishman"<<endl;
if (pc = dynamic_cast<Chinese *>(&h))
cout<<"This human is Chinese"<<endl;
if (pg = dynamic_cast<Guangximan *>(&h))
cout<<"This human is Guangximan"<<endl;
}
int main(int argc, char **argv)
{
Guangximan g;
test_eating(g); //广西人还是中国人,在类信息中还含有那些继承信息
return 0;
}
执行结果如下,因为虚函数表中含有继承信息,因此既能转换为中国人,也能转换为广西人
use chopsticks to eat, I come from guangxi
This human is Chinese
This human is Guangximan
~Chinese()
~Human()
把指针改为引用,修改程序如下,如果是广西人转换为英国人,这里就会出现异常,转换不成功,程序直接崩溃因此程序在"Englishman& pe = dynamic_cast<Englishman&>(h)"就崩溃了,其中基类转换为派生类实现下行转换,对于引用的动态转换,转换过程有可能成功,也有可能失败,比强制类型转换更安全
void test_eating(Human& h)
{
Englishman& pe = dynamic_cast<Englishman&>(h);
Chinese& pc = dynamic_cast<Chinese&>(h); //下行转换
Guangximan& pg = dynamic_cast<Guangximan&>(h);
h.eating();
}
int main(int argc, char **argv)
{
Guangximan g;
test_eating(g);
return 0;
}
修改为强制类型转换,对于广西人强制转换为英国人就有问题,如果涉及英国人的操作就会有问题,但是编译能通过,因此不安全
void test_eating(Human& h)
{
Englishman& pe = reinterpret_cast<Englishman&>(h);
Chinese& pc = reinterpret_cast<Chinese&>(h);
Guangximan& pg = reinterpret_cast<Guangximan&>(h);
h.eating();
}
static_cast
格式:static_cast<type-id>(expression)
该运算符把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。
- 用于类层次结构中基类和子类之间指针或引用的转换。
- 进行上行转换(把子类的指针或引用转换成基类表示)是安全的;
- 进行下行转换(把基类指针或引用转换成子类指针或引用)时,由于没有动态类型检查,所以是不安全的。
- 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum:这种转换的安全性也要开发人员来保证。
- 把void指针转换成目标类型的指针(不安全!!)
- 把任何类型的表达式转换成void类型。
注意:static_cast不能转换掉expression的const、volitale、或者__unaligned属性。
修改main函数,对于static_cast来说上行转换是安全的,下行转换检查不出来,有安全隐患
int main(int argc, char **argv)
{
Human h;
Guangximan g;
Englishman *pe;
pe = static_cast<Englishman *>(&h); //下行转换,但Human不一定是英国人,不安全,有安全隐患,static_cast检查不出来
//Englishman *pe2 = static_cast<Englishman *>(&g); 上行转换,编译器会检查出错误,广西人不能够转换为英国人
Chinese *pc = static_cast<Chinese *>(&g); //上行转换是安全的
return 0;
}