目录
七、C++11 中的 final 关键字 和 override 关键字
一、多态的定义
多态就是完成某种行为时,当不同的对象去做就会产生出不同的状态。
比如买票:普通成年人 和 学生 进行买票时,学生有优惠,而 普通成年人 没有。
二、形成多态的条件
多态示例:
class Person
{
public:
virtual void BuyTecket()
{
cout << "买票 -- 全价\n";
}
};
class Student : public Person
{
public:
virtual void BuyTecket()
{
cout << "买票 -- 半价\n";
}
};
void func(Person& p)
{
p.BuyTecket();
}
int main()
{
Person p;
Student s;
func(p);
func(s);
return 0;
}
多态的条件:
1、虚函数的重写
重写要求:父子类中的两个虚函数要:三同(函数名、参数、返回值)
2、父类的指针或引用调用虚函数
往 func() 中传的是父类就调用父类的虚函数,传的是子类就调用子类的虚函数
三、协变
虚函数的重写要求有特例,协变也满足虚函数重写条件。
协变:虚函数的返回值可以不同,但必须是父子类关系的指针或引用
父类中是父类,子类中是子类
class A{}; // 子类
class B : public A{}; // 父类
class Person
{
public:
virtual A* BuyTecket()
{
cout << "买票 -- 全价\n";
// Person只有返回 A 的 指针/引用 才构成协变,返回 B 的 指针/引用 会报错
return new A;
}
};
class Student : public Person
{
public:
virtual B* BuyTecket()
{
cout << "买票 -- 半价\n";
// 同理,Student只能返回 B 的 指针/引用
return new B;
}
};
void func(Person& p)
{
p.BuyTecket();
}
int main()
{
Person p;
Student s;
func(p);
func(s);
return 0;
}
若 Person 中返回 B 的指针或引用,而 Student 返回 A 的指针或引用,则不构成协变,会报错!
class A{}; // 子类
class B : public A{}; // 父类
class Person
{
public:
virtual B* BuyTecket()
{
cout << "买票 -- 全价\n";
// Person只有返回 A 的 指针/引用 才构成协变,返回 B 的 指针/引用 会报错
return new B;
}
};
class Student : public Person
{
public:
virtual A* BuyTecket()
{
cout << "买票 -- 半价\n";
// 同理,Student只能返回 B 的 指针/引用
return new A;
}
};
四、析构函数的重写
析构函数的重写是指:只要基类的析构函数加上了 virtual ,那么无论派生类的析构函数加不加 virtual ,它们都构成重写。
虽然父子类的类名不同,看似违反了重写规则中的 函数名相同,但其实编译器会在底层把析构函数的名字都改为 destruction()。
析构函数的重写可以防止将 new 出来的子类对象赋值给父类时,在进行 delete 时只调用父类的析构函数 。
如:
class Person
{
public:
~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
~Student()
{
cout << "~Student()" << endl;
}
};
int main()
{
Person* p = new Person;
//将 new 的子类对象 切片 赋值给父类
Person* s = new Student;
delete p;
delete s;
return 0;
}
上述父类析构函数没加 virtual ,导致在 delete s; 时,只调用了 Person 的析构。
这就会导致内存泄漏。
加上 virtual 后,就可以正常析构了。
因为此时析构函数构成多态,用父类的指针 s 调用析构函数就会调用 ~Student()。
五、其他特例
子类可以不加 virtual
因为子类是把父类的接口继承下来,重写实现
class Person
{
public:
virtual void BuyTecket()
{
cout << "买票 -- 全价\n";
}
};
class Student : public Person
{
public:
void BuyTecket()
{
cout << "买票 -- 半价\n";
}
};
void func(Person& p)
{
p.BuyTecket();
}
int main()
{
Person p;
Student s;
func(p);
func(s);
return 0;
}
上述代码中,子类重写虚函数没加 virtual ,但依旧构成多态。
虽然子类重写虚函数可以不加 virtual ,但还是建议加上,这样代码的可读性更高。
六、重载、重写、隐藏的总结
七、C++11 中的 final 关键字 和 override 关键字
1、final
final 关键字修饰的类为最终类,不能被继承
final 关键字修饰的函数不能被重写
在上述代码中,加入 final 就会报错
①、修饰类
②、修饰虚函数
2、override
作用:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
八、抽象类
抽象类的定义:在虚函数的后面加上 =0 就称为纯虚函数,包含纯虚函数的类就称为抽象类.
抽象类无法实例化出对象
派生类继承后也只有重写该纯虚函数才能实例化出对象。 这体现了接口继承。
// 抽象类
class Person
{
public:
// 纯虚函数
virtual void func1() = 0;
int _p = 0;
};
class Student : public Person
{
public:
int _s = 1;
};
int main()
{
Person p;
return 0;
}
上述代码Person 为纯虚函数,因此实例化Person,会报错
实例化 未重写纯虚函数的Student ,也会报错
重写后:
纯虚函数体现出了接口继承。
九、多态的原理
多态的原理
子类和父类都有一个虚函数表指针,指向的是虚函数的函数指针数组
可以理解为:子类的虚表指针是先把父类的虚函数表拷贝一份,再判断是否重写,重写了就覆盖父类的虚函数,因此,重写也叫做覆盖。
class Person
{
public:
virtual void func1()
{}
virtual void func2()
{}
virtual void func3()
{}
int _p = 0;
};
class Student : public Person
{
public:
virtual void func2()
{}
int _s = 1;
};
int main()
{
Person p;
Student s;
return 0;
}
在用父类的指针或引用调用重写的虚函数时,父类就是在虚函数表中查找;而子类被切片后也可以看做父类,和父类一样在虚函数表中查找,确定函数地址。
因此,父类调用的就是父类的虚函数,子类调用的就是子类的虚函数。
多态调用,是在运行时,从指向对象虚表中找到虚函数地址。
普通调用,是在编译链接时,从符号表中找,确定函数地址。
虚函数表的存放区域
class Person
{
public:
virtual void func1()
{}
virtual void func2()
{}
int _p = 0;
};
class Student : public Person
{
public:
virtual void func1()
{
cout << "重写" << endl;
}
virtual void func3()
{}
int _s = 1;
};
int ga = 1;
int main()
{
Person p;
Student s;
int sa = 0; // 栈
int* ha = new int; // 堆
static int ta = 1;// 静态区
const char* ca = "aaaaaaaaa"; // 常量区
Student* ps = &s;
Person* pp = &p;
printf("栈: %p\n", &sa);
printf("堆: %p\n", ha);
printf("静态区: %p\n", &ta);
printf("全局数据区: %p\n", &ga);
printf("常量区: %p\n", ca);
// 取 s 的头四个字节(x86环境)
printf("虚函数指针: %08x\n", *(int*)ps);
return 0;
}
由上述代码,我们可以判断出,虚函数指针是存在常量区的。
虚表指针是在编译时生成的,在初始化列表的第一个。
打印虚函数表
我们取出对象的头 4/8 个字节就是虚函数表指针,再根据指定虚函数个数,就可以把全部虚函数打印出来。
class Person
{
public:
virtual void func1()
{
cout << "Person::func1()" << endl;
}
virtual void func2()
{
cout << "Person::func2()" << endl;
}
int _p = 0;
};
class Student : public Person
{
public:
virtual void func1()
{
cout << "Student::func1()" << endl;
}
virtual void func3()
{
cout << "Student::func3()" << endl;
}
int _s = 1;
};
// 返回值和参数都为空的函数指针数组
typedef void(*VF_PTR)();
// 打印指定个数的虚函数
void PrintVFT(VF_PTR* vft, int n)
{
for(int i = 0; i < n; ++i)
{
printf("[%d]:[%p]->", i, vft[i]);
vft[i]();
}
}
int main()
{
Student s;
// 将 s 的地址强转为 void** ,解引用步长就为一个指针的长度
// 再将这个值强转为 虚函数表指针的类型,就可以传参了
PrintVFT((VF_PTR*)(*(void**)(&s)), 3);
return 0;
}