什么是多态?
多态就是同一种事物的不同表现,比如开学,那么学校,老师,家长,学生会做不同的准备,这就是多态。
放在c++里面来说,多态表现的形式之一就是:用同一个函数名,实现不同的函数功能,即“一个接口,多种方法”。
我们知道在同一个类中,不能定义两个名字相同,参数个数和类型都相同的函数,否则就是重复定义;但在不同类中可以出现名字,参数个数和类型都相同的函数,但函数功能不相同,此时会根据同名覆盖原理来进行调用,例如:
所以人们提出,能否用一个调用形式,既能调用派生类又能调用基类的同名函数。在程序中,不通过对象名去调用,而是通过指针调用它,在调用前,给指针变量赋予不同的值,使之指向不同的类对象。。c++中虚函数就是用来解决这个问题的。
看代码如下:
class person
{
public:
void display()
{
cout << "买全价价票" << endl;
}
};
class student:public person
{
public:
void display()
{
cout << "买半价票" << endl;
}
};
void fun(person & p)//void fun(person *p)
{
p.display();
}
int main()
{
person p;
fun(p);
student s;
fun(s);
system("pause");
}
运行结果
跟预想结果不一样,我们希望调用哪个类,就调用属于它的函数,c++就提供多态来解决这个问题
稍微做改变:
class person
{
public:
virtual void display() //虚函数
{
cout << "买全价价票" << endl;
}
};
class student:public person
{
public:
virtual void display() //虚函数重写
{
cout << "买半价票" << endl;
}
};
void fun(person & p)//void fun(person *p)
{
p.display();
}
int main()
{
person p;
fun(p);
student s;
fun(s);
system("pause");
}
虚函数----实现多态调用
在谈虚函数名称前加virtual
了解几个概念:
- 函数的重载:在同一作用域当中,函数名相同,参数的类型和个数有一个不同,就构成重载,返回值可以不同。
- 函数的重定义(同名隐藏):不在同一个作用域,子类成员函数与父类成员函数名相同。
- 函数的重写(覆盖):避灾同一作用域,子类成员的虚拟函数与父类成员的虚拟成员函数完全一样(函数名,参数,返回值都一样),基类函数必须有virtual。
多态构成条件:
- 基类中必须有虚函数,且子类对基类的虚函数进行重写
- 通过基类对象 的指针或者引用调用函数
普通调用和多态调用
普通调用:
class person
{
public:
virtual void display()
{
cout << "买全价价票" << endl;
}
};
class student:public person
{
public:
virtual void display()
{
cout << "买半价票" << endl;
}
};
int main()
{
person p;
student s;
//普通调用
p.display();
s.display();
//普通调用与对象类型有关
system("pause");
}
多态调用:
class person
{
public:
virtual void display()
{
cout << "买全价价票" << endl;
}
};
class student:public person
{
public:
virtual void display()
{
cout << "买半价票" << endl; //重写父类虚函数
}
};
void fun(person & p)//void fun(person *p)
{
//多态调用:与对象有关,指向哪个对象,就调用哪个对象的函数
//多态调用的条件:1.虚函数的重写。2.p必须是父类的引用或者指针
//此为多态调用
p.display();
}
int main()
{
person p;
student s;
fun(p);
fun(s); // 传s时完成了切片
system("pause");
}
虚函数表(虚表)—多态原理
先看以下这个代码
class person
{
public:
virtual void display()
{
cout << "买全价价票" << endl;
}
};
class student:public person
{
public:
virtual void display()
{
cout << "买半价票" << endl;
}
};
void fun(person & p)//void fun(person *p)
{
//此为多态调用
p.display();
}
int main()
{
person p;
student s;
fun(p);
fun(s);
cout << sizeof(p) << endl; //打印结果是?为什么?
cout << sizeof(s) << endl;
system("pause");
}
与设想的应该打印出1(一字节)不同,原因--------虚表指针
编译器会为包含虚函数的类加上以一个成员变量,这个成员变量就是一个指向该虚函数表的指针,称为虚表指针,也就是说如果一个类有虚函数,那么这个类的对象都含有虚表指针,虚表指针指向虚表
class person
{
public:
virtual void display()
{
cout << "买全价价票" << endl;
}
virtual void fun()
{
cout << "this is base virtual fun" << endl;
}
};
class student:public person
{
public:
virtual void display()
{
cout << "买半价票" << endl;
}
virtual void fun()
{
cout << "this is devired virtual fun" << endl;
}
};
void fun(person & p)//void fun(person *p)
{
//此为多态调用
p.display();
}
int main()
{
person p;
student s;
system("pause");
}
虚函数表底层:虚函数表存放在哪里?首先不可能在栈,因为,栈一般用来发生函数栈帧,也不可能在堆,堆一般用来动态开辟,那么只能在静态区,或者常量区,一般放在静态区,因为这张表是一个类的所有对象所共享的。
虚函数在虚表中存放的位置按基类先后声明的顺序存放。
单继承中子类与父类都有自己的虚表,里面分别存放着自己的虚函数,若子类,同时,子类继承了父类,但是会覆盖掉父类的同名函数,只留一份。
多继承的虚表。
默认多继承子类的虚函数放在第一个继承的类中,即多继承子类的虚表:存放在先继承的第一个父类的虚函数表中,同名的会发生覆盖留下一份
协变—特殊的虚函数重写
- 虚函数重写要求参数,函数名,返回值都相同,但是,协变例外,协变就是参数,函数名都相同,返回值可以不同,但返回值必须是父子类的指针或者引用。
- 派生类的重写的虚函数可以不写virtual。
- 只有成员函数才可以定义才虚函数
//协变
class person
{
public:
virtual person& display()
{
cout << "买全价价票" << endl;
}
};
class student:public person
{
public:
virtual student & display()
{
cout << "买半价票" << endl;
}
};
- 静态成员函数不能是虚函数(因为静态成员没有this指针,不属于某个对象,属于全局,所以就没有虚表指针,也就没有虚表了),
- 构造函数、拷贝构造不能是虚函数(原因:它是初始化虚表指针,编译完成后为虚表指针开辟了空间,构造函数除了初始化成员,还会初始化虚表指针,即让它指向虚表;)
- operator=最好不要定义成虚函数,它们的参数不同,比如父类里面传的是父类的对象 ,子类里面传的是子类的对象,没有构成重写,没有意义。
- 析构函数最好定义成虚函数(特殊情况使用,假如没有定义成虚函数,则不构成重写,就不能多态调用,只能以类型调用。比如子类在堆上开辟了空间,父类却没有,析构函数没有形成多态,就会调用父类的析构,就不会进行资源的清理;另外,派生类和基类的析构函数名称不一样,但构成覆盖,这是因为编译器做了特殊的处理)
情景如下:
int main()
{
person *p = new student; //开空间,调用子类析构,若子类中有新增的内容
delete p; //调用基类的析构,无法析构子类新增内容,从而造成资源泄漏。
return 0;
}
- 内联函数不能定义成虚函数(因为内联函数可以说是没有地址)
抽象类—包含纯虚函数的类
在虚函数的形参列表后面写上0,则成员函数为纯虚函数。包含纯虚函数的类叫抽象类(也叫接口类),抽象类不能实例化出对象,纯虚函数在派生类中重新定义以后,派生类才能实例化出 对象。
class person
{
public:
virtual void display()=0 //纯虚函数
{
cout << "买全价价票" << endl;
}
};
int main()
{
person p1,p2; //抽象类不能实例化出对象
system("pause");
}
但是它的派生类可以实例化出对象
class person
{
public:
virtual void display()=0 ; //纯虚函数
};
class student:public person
{
public:
virtual void display() //派生类必须重写,不重写的话,子类也就是抽象类,不能实例化出对象
{
cout << "买半价票" << endl;
}
};
int main()
{
student s1; //可以实例化对象
return0;
}
既然抽象类完成了虚函数,那么就可以实现多态调用
class person
{
public:
virtual void display() = 0; //纯虚函数
};
class student1:public person
{
public:
virtual void display()
{
cout << "买半价票" << endl;
}
};
class student2 :public person
{
public:
virtual void display()
{
cout << "我也买半价票" << endl;
}
};
void fun(person &p) //void fun(person *p)
{
//此为多态调用
p.display();
}
int main()
{
student1 s1;
student2 s2;
fun(s1);
fun(s2);//用引用
//fun(&s1); //用指针
//fun(&s2);
system("pause");
}
问题:
什么是函数重载,同名隐藏,重写?
哪些函数不定义成虚函数?为什么?
- 不能被继承的函数
- 不能被重写的函数(普通函数,友元函数,构造函数,内联函数,静态成员函数)
静态绑定和动态绑定(了解)
静态绑定:发生在编译期间,简单来说就是普通调用
动态绑定:发生在运行期间,简单来说就是多态调用
通常,虚函数是动态绑定,非虚函数是静态绑定