C++特性 多态
多态的概念
多态分为编译时多态(静态多态)运行时多态(动态多态)
编译时多态主要是函数重载和函数模板,他们通过传不同的参数就可以调用不同的函数,通过参数不同达到多种形态,因为实参传递给形参的参数匹配是在编译时确定的,编译时叫做静态,运行时叫做动态。
运行时多态,就是完成某个行为(函数),通过传不同的对象就会完成不同的行为,就会达到多种形态。
多态的实现及定义
多态是一个继承关系下的 类对象,去调用同一函数,产生不同的行为。
Student继承了person。Person对象买全价票,Student对象买半价票。
class Person
{
public:
//虚函数 类成员函数前加virtual
virtual void BuyTicket()
{
cout << "全价买票" << endl;
}
};
class Student :public Person
{
public:
virtual void BuyTicket()
{
cout << "半价买票" << endl;
}
};
void fun(Person* p)
{
// 这里可以看到虽然都是Person指针Ptr在调用BuyTicket
// 但是跟ptr没关系,而是由ptr指向的对象决定的
p->BuyTicket();
}
void fun(Person& p)
{
p.BuyTicket();
}
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
fun(p1);
fun(p2);
Person pp;
Student st;
fun(pp);
fun(st);
return 0;
}
class Animal
{
public:
virtual void talk()const
{
}
};
class Dog :public Animal
{
public:
virtual void talk()const
{
cout << "汪汪" << endl;
}
};
class Cat :public Animal
{
public:
virtual void talk()const
{
cout << "喵喵" << endl;
}
};
void lestHear(const Animal& animal)
{
animal.talk();
}
int main()
{
Cat c;
Dog d;
lestHear(c);
lestHear(d);
return 0;
}
实现多态的两个必要条件
1必须是基类的指针或者引用,因为只有基类的指针或者引用才能既指向基类对象又指向派生类对象;调用虚函数。
2必须对基类的虚函数完成重写/覆盖,重写或者覆盖了,基类和派生类之间才能有不同的函数,多态的不同效果才能达到。被调用的函数必须是虚函数。
/////////////////////////////////////////////////////////////////////////////////////////////
//class A
//{
//public:
// virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
// virtual void test() { func(); }
//};
//class B : public A
//{
//public:
// void func(int val = 0) { std::cout << "B->" << val << std::endl; }
//};
//int main(int argc, char* argv[])
//{
// B* p = new B;
// p->test();
// return 0;
//}
//////////////////////////////////////////////////////////////////////////////////////////////////////
虚函数
在类成员函数前面加virtual修饰,那么这个成员函数被叫做虚函数。非成员函数不能加virtual修饰。
class Person
{
public:
//虚函数 类成员函数前加virtual
virtual Person* BuyTicket()
{
cout << "全价买票" << endl;
}
};
class Student :public Person
{
public:
virtual Student* BuyTicket()
{
cout << "半价买票" << endl;
}
};
int main()
{
return 0;
}
虚函数的重写或者覆盖
派生类中有一个跟基类完全相同的虚函数(即派生类虚函数和基类虚函数 函数名字 参数列表完全相同 返回值类型),称派生类重写了基类的虚函数。
在派生类不加virtual关键字时,虽然也可以构成重写(因为基类的虚函数继承到派生类中依旧保持虚函数属性)。很不规范。不建议这样使用。
虚函数重写(协变和析构函数)
派生类对象重写虚函数时,与基类函数的返回值不同,基类返回基类的指针或者引用,派生类返回派生类的指针或者引用。称为协变。
class Person
{
public:
//虚函数 类成员函数前加virtual
virtual Person* BuyTicket()
{
cout << "全价买票" << endl;
}
};
class Student :public Person
{
public:
virtual Student* BuyTicket()
{
cout << "半价买票" << endl;
}
};
int main()
{
return 0;
}
class A
{
};
class B :public A
{
};
class Person
{
public:
//虚函数 类成员函数前加virtual
virtual A* BuyTicket()
{
cout << "全价买票" << endl;
return nullptr;
}
};
class Student :public Person
{
public:
virtual B* BuyTicket()
{
cout << "半价买票" << endl;
return nullptr;
}
};
void func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person pp;
Student st;
func(pp);
func(st);
return 0;
}
为什么基类的析构函数建议设计为虚函数
基类析构函数为虚函数,此时派生类函数只要定义,无论是否加virtual关键字都与派生类构成重写,原因是编译器对析构函数的函数名做了特殊处理,编译后统一处理成destructor,所以基类函数加virtual修饰,派生类的析构函数就造成了重写。
//析构函数的重写
class A
{
public:
virtual ~A()
{
cout << "~A" << endl;
}
};
class B :public A
{
public:
virtual ~B()
{
cout << "~B" << endl;
}
protected:
int* p = new int[10];
};
int main()
{
A* p1 = new A;
A* p2 = new B;
delete p1;
delete p2;//调用析构 operator delete = free
return 0;
}
override 和 finall关键字
C++对虚函数重写要求是比较严格的,但是在编译期间,有些情况不会报错,只有在运行阶段才会报错,比如函数名写错无法构成重写,C++提供了override,可以帮助用户检测是否构成重写。
不想让派生类重写这个虚函数可以用finall修饰
class A
{
public:
virtual ~A ()//final//final修饰的虚函数不可以被重写
{
cout << "~A" << endl;
}
};
class B :public A
{
public:
virtual ~B()
{
cout << "~B" << endl;
}
protected:
int* p = new int[10];
};
int main()
{
A* p1 = new A;
A* p2 = new B;
delete p1;
delete p2;//调用析构 operator delete = free
return 0;
}
class Car {
public:
virtual void Dirve()
{
}
};
class Benz :public Car {
public:
virtual void Dirve() override { cout << "Benz-舒适" << endl; }
};
int main()
{
return 0;
}
重载 重写 隐藏
重载 两个函数在同一作用域内,函数名必须相同。参数列表不同(个数,类型),返回值可同,可不同。
重写 两个函数在基类和派生类两个不同的作用域,函数名,参数列表,返回值都必须相同(协变除外)两个函数都必须是虚函数。
隐藏 成员函数隐藏和成员变量隐藏 函数名相同 成员变量相同 两个函数只要不构成隐藏就是重写。
纯虚函数和抽象类
在虚函数后面➕ =0,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现也没什么意义因为要被派生类重写,语法上可以实现),只要声明就可以了。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象 ,如果派生类不重写虚函数,那么派生类也是抽象类。无法实例化出对象,所以纯虚函数强制派生类重写虚函数。
Car就是抽象类
class Car
{
public:
virtual void Drive() = 0;//纯虚函数
//virtual void Drive()
//{
//}
protected:
};
class BMA
{
public:
virtual void Drive()
{
cout << "宝马" << endl;
}
};
class Benz
{
public:
virtual void Drive()
{
cout << "丰田" << endl;
}
};
int main()
{
//Car car;
BMA bma;
Benz benz;
bma.Drive();
benz.Drive();
BMA* p1 = new BMA;
Benz* p2 = new Benz;
p1->Drive();
p2->Drive();
return 0;
}
多态的原理
虚函数表指针
:虚函数计算成员大小 除了成员变量,在其前面还有一个虚函数表指针
一个含有虚函数的类中都至少有一个虚函数表指针,因为一个类中的所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也叫虚表。
class Base
{
virtual void Func()
{
cout << "virtual void Func()" << endl;
}
protected:
int _name;
char _ch;
};
int main()
{
Base b;
cout << sizeof(b) << endl;
return 0;
}
多态的原理
满足多态条件后,底层不在是编译时通过调用对象确定函数地址,而是在运行时到指定对象的虚表中确定对应的虚函数地址。


class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
private:
string _name;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-打折" << endl; }
private:
string _id;
};
class Soldier : public Person {
public:
virtual void BuyTicket() { cout << "买票-优先" << endl; }
private:
string _codename;
};
void Func(Person* ptr)
{
// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket
// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
ptr->BuyTicket();
}
int main()
{
// 其次多态不仅仅发⽣在派⽣类对象之间,多个派⽣类继承基类,重写虚函数后
// 多态也会发⽣在多个派⽣类之间。
Person ps;
Student st;
Soldier sr;
Func(&ps);
Func(&st);
Func(&sr);
return 0;
}
动态绑定和静态绑定
对于不满足多态条件(指针或者引用+调用虚函数)的函数调用是在编译时绑定的,也就是编译时确定调用函数的地址叫做静态绑定。
满足多态条件的函数调用是在运行时绑定,也就是运行到指定对象的虚函数表中找到调用函数的地址,也叫做动态绑定
虚函数表
- 基类对象的虚函数表中存放基类所有虚函数的地址,同一类型的对象共用同一个虚函数表。不同类型的对象各自有独立的虚表。
- 派生类由两部分组成,继承下来的基类或者自己的成员,一般情况下,继承下来的基类中有虚函数表指针就不会自己再生成虚函数表指针。注意继承下来的基类的部分的虚函数指针和基类对象的虚函数指针不是同一个,就像基类对象的成员和派生类对象中的基类对象也是独立的。
- 派生类重写虚函数,派生类虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。
- 派生类的虚函数表中包括(1)基类的虚函数地址(2)派生类重写虚函数地址完成覆盖,派生类自己的虚函数地址三个部分。
- 虚函数表本质是一个存虚函数指针的指针数组,一般情况下这个数组最后放一个0x0000000标记。(C++并没有规定,vs会放,g++不会)
- 虚函数存在哪里,和普通函数一样,编译后都是一段指令,都放在代码段,只是虚函数的地址又存放在虚表中了。
- 虚函数表存放在哪里 C++并没有规定 vs是在代码段(常量区)
更多推荐

所有评论(0)