1.概念
多态:通俗来说,就是 多种形态。具体说就是去完成某个行为时,不同的对象去完成会 产生出不同的状态,这就是多态。 比如,同样是 买票,当 普通人买票时,是全价买票;而 学生买票时,是半价买票; 军人买票时,是优先买票。
2.构成条件
C++里,在继承中要 构成多态有 两个条件: 1. 虚函数重写 2. 父类的指针或者引用去调用虚函数
举例:
class Person
{
public:
//注意:这里的virtual和虚继承的virtual关系不大
virtual void BuyTicket() //虚函数
{
cout << "普通人买票-全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "学生买票-半价" << endl;
}
};
void Func(Person& p)
{
p.BuyTicket();
//和之前的调用方式已经完全不一样了
//之前的普通调用,是看类型是什么,就去调用对应的函数,没有对应函数就会报错
//而现在是多态调用,p指向谁,就去调用谁的函数
//p指向父类就调用父类的函数,指向子类,就调用子类的函数
}
int main()
{
Person ps;
Student st;
// 因为Func函数的参数是父类的引用,
// 所以这里可以传父类对象,也可以传子类对象
Func(ps);
Func(st);
return 0;
}
2.1虚函数
何为虚函数:
被virtual修饰的类成员函数称为虚函数。
注意:virtual只能修饰成员函数。
2.2虚函数重写
如何 构成虚函数重写: 在继承关系中,父、子类的 两个虚函数,要求 三同
1.返回值 2.函数名 3.参数类型
虚函数的重写(覆盖): 子类中 有一个跟父类 完全相同的虚函数, 即子类虚函数与父类虚函数的 返回值类型、函数名字、参数列表完全相同, 此时称 子类的虚函数重写了父类的虚函数。
2.3虚函数重写的两个例外
2.3.1协变
协变是指,父类与子类虚函数的返回值类型可以在不相同的情况下,构成虚函数重写,但此时,父子类虚函数的返回值类型必须同时为某个父类类型的指针或者引用。
派生类 重写基类虚函数时,与基类虚函数 返回值类型不同。此时基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
class Person
{
public:
virtual Person* BuyTicket()
//virtual Person& BuyTicket()
{
cout << "普通人买票-全价" << endl;
return nullptr;
//Person p;
//return p;
}
};
class Student : public Person
{
public:
virtual Student* BuyTicket()
//virtual Student& BuyTicket()
{
cout << "学生买票-半价" << endl;
return nullptr;
//Student s;
//return s;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
返回值类型是其它父子类的指针或者引用时,也是可以构成协变的。
class A
{
public:
int _a;
};
class B: public A
{
public:
int _b;
};
class Person
{
public:
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 ps;
Student st;
Func(ps);
Func(st);
return 0;
}
2.3.2子类虚函数前可以不加virtual
在重写父类虚函数时,子类的虚函数前不加virtual关键字时,是可以构成重写的。
class Person
{
public:
virtual void BuyTicket()
{
cout << "普通人买票-全价" << endl;
}
};
class Student : public Person
{
public:
void BuyTicket()
{
cout << "学生买票-半价" << endl;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
注意:
1.子类重写的虚函数前不加virtual时,虽然也可以构成重写,因为继承后,父类的虚函数被继承下来了,在子类中依旧保持虚函数属性,但是这种写法不是很规范,不建议使用。
2.只有父类函数前加了virtual可以构成虚函数重写,但如果只有子类函数前加了virtual是不构成虚函数重写的
2.3.3析构函数的重写
析构函数的使用,是使得C++的多态要设计子类虚函数前可以不加virtual的这种用法的重要原因。
通过了解析构函数,可以更深刻地理解到,为什么C++的多态要设计子类虚函数前可以不加virtual的这种用法。
2.3.3.1普通的析构函数
class Person
{
public:
virtual void BuyTicket()
{
cout << "普通人买票-全价" << endl;
}
~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "学生买票-半价" << endl;
}
~Student()
{
cout << "~Student()" << endl;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p;
Student s;
return 0;
}
日常使用中,这样书写析构函数是没有问题的。
2.3.3.2析构要写成虚函数的情景
class Person
{
public:
virtual void BuyTicket()
{
cout << "普通人买票-全价" << endl;
}
~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "学生买票-半价" << endl;
}
~Student()
{
cout << "~Student()" << endl;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person* p = new Person;
delete p;
p = new Student;
delete p;
return 0;
}
所以,只有析构是虚函数时,才能正确调用析构函数,去释放空间
否则,如果派生类的析构去进行了资源清理,但它不是虚函数,就不会去调用它,此时就会出现内存泄露。
父类加virtual,子类不加virtual就可以构成虚函数重写
此时只要保证父类析构函数是虚函数,子类就算不写virtual,也完成了虚函数的重写,这样就大大降低了内存泄露的可能。正常情况下,不需要析构函数是虚函数,但是父子类对象是new出来的情况下,就需要析构函数是虚函数了。
2.3.3.3正确的析构虚函数写法
class Person
{
public:
virtual void BuyTicket()
{
cout << "普通人买票-全价" << endl;
}
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "学生买票-半价" << endl;
}
~Student()
{
cout << "~Student()" << endl;
}
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person* p = new Person;
delete p;
p = new Student;
delete p;
return 0;
}
析构函数的函数名不相同,不能构成虚函数重写,编译器为了解决这个问题,把析构函数统一处理为了destructor。
2.4对比重载、重写(覆盖)和隐藏(重定义)
三个概念的对比:
一、重载
要求两个函数在同一个作用域中,函数名相同、参数不同(类型、个数、顺序)时,构成重载。
二、重写(覆盖)
要求两个函数分别在父类和子类的作用域中,必须都是虚函数,
并且要求 1.返回值 2.函数名 3.参数类型 都要相同,协变例外。
三、隐藏(重定义)
同样要求两个函数分别在父类和子类的作用域中,但只要求二者的函数名相同,就构成隐藏。
两个父类与子类里的同名函数,不构成重写,那就是隐藏。
2.5 C++11的final和override
2.5.1 final
一、final可以修饰类,使得这个类不能被继承。
二、final还可以 修饰虚函数, 表示该虚函数不能再被重写。class Car
{
public:
virtual void Drive() final {}//此时这个函数就不能被重写了
};
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒适" << endl;}
};
2.5.2 override
override用来修饰子类的虚函数,来检查是否完成重写。
如果没有重写会编译报错。class Car {
public:
virtual void Drive() {}
};
class Benz :public Car {
public:
//override修饰子类的虚函数,用来检查是否完成重写。
virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
3.抽象类
3.1概念
在 虚函数的后面写上 =0 ,则这个函数为 纯虚函数。 包含纯虚函数的类叫做 抽象类,也叫 接口类。
class Car
{
public:
//纯虚函数
virtual void Drive() = 0;
};
//类中有纯虚函数,该类就被称为抽象类
3.2特点
抽象类的特点,就是 不能实例化出对象,定义不出对象。但是 可以定义指针。 子类继承后 也不能实例化出对象,除非去 重写纯虚函数,子类才能实例化出对象。 纯虚函数规范了子类虚函数必须重写,另外纯虚函数更体现出了 接口继承。
class Car
{
public:
//纯虚函数
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
};
int main()
{
//Car c;
//不可行
Car* c1;
//可行
Benz b;
//子类也实例化不出对象
//可以认为子类也包含纯虚函数,因为它继承了父类
//所以子类也是抽象类
return 0;
}
重写虚函数后,子类就可以实例化对象了。
class Car
{
public:
//纯虚函数
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
//除非把这个纯虚函数进行重写
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
int main()
{
Benz b;
//此时子类就可以实例化对象了
return 0;
}
这也展现了多态的另一种形态
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
void func(Car* ptr)
{
ptr->Drive();
}
int main()
{
func(new Benz);
func(new BMW);
return 0;
}
3.3总结
关于纯虚函数
一、纯虚函数,或者说抽象类,在某种程度上,间接强制了子类去重写虚函数
因为如果子类不重写虚函数,就实例化不出对象,这样的话就没什么价值了二、纯虚函数描述的是抽象类,抽象类不能实例化出对象
什么时候把类设计成抽象类比较好呢?
当某个类指的是现实世界中一些抽象的表示,但是它又不对应某些具体的实体,只是公共特征抽象出来的表达时,可以认为它就是抽象类。比如说人这个类就可以定义为抽象类,在定义各种职业类时去继承人这个类
比如,医生、老师、程序员这些类去继承人这个类
人不是一个具体的职业,人也不需要去实例化出对象又比如说动物类,动物没有具体的对象,牛、马、羊才是具体的动物
如果给一个类加上纯虚函数,定义为抽象类,就说明了,这个类不能实例化出对象,它可能在现实生活中不对应具体的实体
抽象类的另一层意义是,它拥有多个子类,这里实现的多态,是想在多个子类之间实现。
虚函数的意义就是实现重写、实现多态,在这里就是多个子类去重写虚函数。
3.4补充:接口继承和实现继承
普通函数的继承是一种实现继承,子类继承了父类函数,可以使用函数,因为 继承的是函数的实 现, 是一种复用。 而 虚函数的继承是一种接口继承,子类继承的是 父类虚函数的接口,目的是为了重写函数的实现,达成多态,继承的是接口,所以子类不加virtual也是可以的。 所以 如果不实现多态,就不要把函数定义成虚函数。
4.多态的原理
4.1虚函数表
这里以一道常考的笔试题来引入。
//常考的一道笔试题:sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
int main()
{
cout << sizeof(Base) << endl;
// 32位下是8
// 64位下是16
return 0;
}
可以看到,除了_b成员,还多一个__vfptr放在对象的前面(有些平台可能会放到对象的最后面,这个跟平台有关), 对象中的这个指针我们叫做 虚函数表指针(v代表virtual,f代表function)。 一个含有虚函数的类中都至少 有一个虚函数表指针,因为 虚函数的地址要被放到虚函数表中,虚函数表也简称为虚表。为什么是这样的?
这里就涉及到一个原理:C++中会把虚函数的地址,存储在一个叫做虚函数表的地方。
只要有虚函数就会有虚表。
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
char ch;
};
//此时sizeof(Base)的值
//32位下是12,依旧遵循类对齐规则
//64位下是16
再次观察
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
virtual void Func2()
{
cout << "Func2()" << endl;
}
virtual void Func3()
{
cout << "Func3()" << endl;
}
private:
int _b = 1;
char _ch;
};
int main()
{
cout << sizeof( & (Base::Func1)) << endl;//32位是4
Base b;
return 0;
}
虚表实际就是一个虚函数指针数组
虚函数编译后,存储在内存中的哪个位置?
答案:代码段//编译好之后,是一串指令
//最开始是建立栈帧,然后执行中间的动作,最后销毁栈帧//只是说虚函数又单独做了一个动作,它的地址被拿出来放在了虚函数表中
//函数编译完是一串指令,表中不可能说去把所有的指令全部存进去(太多了),只是存储了虚函数的地址
4.2探究多态的原理
class Person
{
public:
virtual void BuyTicket() { cout << "普通人买票-全价" << endl; }
virtual void func() {}
private:
int _a=0;
};
class Student : public Person
{
public:
virtual void BuyTicket() { cout << "学生买票-半价" << endl; }
private:
int _b=1;
};
void Func(Person* ptr)
{
ptr->BuyTicket();
//是运行起来后,去对应的虚表中去找虚函数
}
int main()
{
Person p;
Student s;
Func(&p);
Func(&s);
return 0;
}
形象的的说法,就是从语法的角度这样说更好理解。
比如说成员函数的this指针,我们不能显式写,但实参、形参会把this指针加上,但实际上编译器并不是先把this这个实参、形参加上,它会直接编译成汇编指令,this通过ecx直接就传递了。
再比如引用,表面说引用就是别名,没有开空间,但底层上,它就是指针,也开了空间
由此也可以看出,赋值兼容规则,实际上也是在为多态做准备
4.2.1汇编代码
4.2.2为什么对象调用不构成多态?
这个函数在传参时,如果传的对象是子类对象,会把子类对象中父类的那一部分的成员拷贝给ptr对象,调用拷贝构造。
但是!此时不会把虚函数表指针拷贝过去,所以ptr中没有虚表指针。
4.2.3为什么不允许拷贝虚函数表指针呢?
如果允许拷贝虚表指针,看似可以构成多态了,但实际上会导致许多的连锁反应。
综上,在拷贝时,不能把虚表指针拷贝过去,可以认为这是个特例。
4.3补充
如果子类不重写虚函数,父类和子类的虚表是否一样?
但是,同类对象的虚表指针是一样的。
同一个类对象,共用一张虚表。不同的类,不会共用虚表。
练习题
1.
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();//打印B->1
p->func();//打印B->0
注意:这里不构成多态,所以就是正常的函数调用
这也说明了,只有构成多态时,调用的子类虚函数,才会是继承父类的接口来重写的
是接口继承,使用的是父类的接口、声明,重写的是函数的实现
return 0;
}
通过这道题我们也可以看出,多态这里设计的过于复杂,
比如子类可以不加virtual这个用法,如果可以报警告,也许是更好的处理方法
警告不需要处理,但是要评估下它有没有什么影响
标签:函数,子类,多态,virtual,class,C++,重写,public From: https://blog.csdn.net/2301_80342122/article/details/143193723