目录
继承
继承的意义
访问限定符、继承方式
赋值兼容规则(切片)
子类的默认成员函数
多继承
继承is a和组合has a
多态
什么是多态
形成多态的条件
函数重载,隐藏,重写的区别
override和final
多态原理
继承
继承的意义
继承本质就是类之间代码的复用,通过子类继承父类的方式,让子类对象中具有父类对象的成员。
访问限定符、继承方式
通过访问限定符,可以限制子类可以继承哪些变量,不能继承哪些变量,在实际开发过程中通常使用公有public继承。父类中想要让子类继承的成员就用protected或public修饰,不想让子类看到就用private修饰。
赋值兼容规则(切片)
在子类对象,指针,引用分别赋值给父类对象,指针,引用时,会遵循赋值兼容规则(切片)。即把子类对象中父类对象的可见范围给到父类对象、指针、引用。
子类的默认成员函数
默认成员函数主要包括构造函数,拷贝构造,赋值运算符重载,析构函数。那么在子类中他们是如何调用的呢?(考虑到要初始化子类对象中的父类对象)
子类的初始化包括对子类成员的初始化和对继承的父类成员的初始化。
由于父类的默认成员函数在子类内是可见的,所以在初始化父类成员时,只需要调用父类的默认成员函数即可。子类成员再另当初始化。
例如,基类为Person,子类为Student的代码:
class Person { public:Person(string name,int age,string gender):_name(name),_age(age),_gender(gender){cout << "Person()" << endl;}Person(const Person& p){cout << "Person(const Person& p)" << endl;}Person& operator=(const Person& p){cout << "Person.operator=()" << endl;return *this;}~Person(){cout << "~Person()" << endl;} protected:string _name;//姓名int _age;//年龄string _gender;//性别 }; class Student:public Person { private:string _id;//学号string _college;//所属学院 public:Student(string name, int age, string gender,string id,string college):Person(name,age,gender)//调用父类构造,_id(id),_college(college){//初始化子类成员cout << "Student()" << endl;}Student(const Student& s):Person(s)//调用父类拷贝构造{//拷贝子类成员cout << "Student(const Student& s)" << endl;}Student& operator=(const Student& s){Person::operator=(s);//调用父类的operator=cout << "Student.operator=()" << endl;//赋值子类成员return *this;}~Student(){//默认自动先调用子类析构,再调用父类析构cout << "~Student()" << endl;} }; int main() {Student s("cx", 20, "男", "2022112102601", "计算机与信息工程学院");//构造Student s2 = s;//拷贝构造Student s3("xxx", 18, "女", "2022112102728", "经济管理学院");//构造s3 = s2;//赋值return 0; }
代码输出结果:
多继承
多继承:多继承就是一个类可以继承多个父类。
多继承会导致的问题:菱形继承,图示如下
为什么说菱形继承会有问题?
由于继承的本质是代码的复用,而菱形继承会导致A类对象在D类对象中存在两份,导致数据冗余和二义性。
如何解决:虚继承
虚继承就是多个类 在继承重复类时,都使用虚继承。这样在上图中D对象内就只会有一份A对象,消除了对象的冗余。
继承is a和组合has a
继承和组合本质都是代码的复用,使得一个对象存在于另一个对象之中。
继承:
class B { protected:int _b; }; class A :public B { //A中可以直接使用父类的protected和public成员 protected:int _a; };
组合:
class B { protected:int _b; }; class A { protected:B _b;//B类对象作为成员,只能使用B类对象的public成员int _a; };
由于组合只能使用另一个对象的公有成员,这使得组合而成的类之间耦合度更低,因此在组合和继承都可以达到目的时,组合方案更优。
多态
什么是多态
多态:多态就是一个接口多种实现,多种状态,是面向对象编程语言的一重要特征。当使用父类指针或引用接收子类对象指针或引用,并调用子类对象中重写的虚函数时,就会体现出多态性。即指向父类调父类,指向子类调子类。
class Animal //基类 { public:virtual void say()//虚函数{cout << "animal say " << endl;} }; class Cat:public Animal //子类,重写基类的虚函数 { public:virtual void say(){cout << "cat say miao~miao~" << endl;} }; class Dog :public Animal //子类,重写基类的虚函数 { public:virtual void say(){cout << "dog say wang~wang~" << endl;} }; void func(Animal* n)//基类指针/引用接收 {n->say();//多态调用 } int main() {Animal a;Cat c ;Dog d;func(&a);func(&c);func(&d);return 0; }
上面的代码基类为Animal,子类有Cat和Dog,当使用基类指针或引用接收对象的指针或引用时,指向哪个类调用哪个类的虚函数。
形成多态的条件
1.具有继承关系
2.虚函数重写
3.父类指针或引用调用虚函数
函数重载,隐藏,重写的区别
重载:同一作用域内,函数名相同,参数不同。
隐藏(重定义):子类函数和父类函数名字相同。父类函数就被隐藏了。
重写(覆盖):子类函数和父类函数名字相同,参数相同,返回类型相同,且父类函数是虚函数。
override和final
子类重写父类虚函数时,用override修饰子类重写的函数,用于检查是否成功重写(三同或者协变)。如果不构成重写在编译时就会报错。
用final修饰的类,不允许有子类继承
用final修饰的虚成员函数,在子类中不可被重写
多态原理
首先看一下这段代码
class A
{
public:void func(){;}
};
int main()
{A a;cout << sizeof(a) << endl;return 0;
}
因为类的成员函数存放在代码段,并不会在每个对象中单独存放一份,所以这里是空类,但是打印的是1,是因为要用至少一个字节来标识不同的对象。
class A
{
public:virtual void func(){;}
};
int main()
{A a;cout << sizeof(a) << endl;return 0;
}
这个类的大小是多少呢?
这又是为什么呢?为什么函数由普通函数变为虚函数,对象的大小就变了呢?
这是因为类中的每个虚函数地址要放到一个专门的位置,即虚函数表中。
对象中会多存放一个虚函数表指针,虚函数表中存放着一个类中所有的虚函数指针。
指针的大小在32位计算机上为4字节,64位计算机为8字节,所以类的大小变为8字节。
这和多态有什么关系呢?
子类继承父类时,会把父类对象中的虚函数表也继承下来。
重写虚函数前:
子类虚函数表中的虚函数地址和父类虚函数表中的虚函数地址是相同的。
重写之后:子类虚函数表中的虚函数地址会被重写的虚函数地址"覆盖"。
虚函数指针被覆盖后,此后在多态的条件下,基类指针指向父类对象时,编译器会在父类对象的虚函数表中查找虚函数,指向子类对象时,编译器会在子类对象的虚函数表中查找虚函数。进而体现出了指向谁,调用谁的多态性。