第一节:继承:
1,相关概念
父类,基类。子类,派生类
(1)基类的私有成员,派生类不可访问
(2)基类中被保护的成员再子类中可以被访问,但是在类外不可以访问
(3)基类其他成员在子类中的访问方式为(成员在基类的访问限定符和继承方式,取小值)
(4)如果继承方式不写的话,class默认为私有继承,struck默认为共有继承
(5)继承的时候“类名:继承方式+父类名”
class person {};
class student :public person {};
(6)子类对象赋值给父类对象/父类指针/父类的引用。我们认为是天然的,中间不产生临时对象,这个叫做父子类赋值兼容规则(切割/切片)。也就是说,rp是子类研究对象中父类那一部分的别名。
student s;
person p = s;
person& rp = s;
父类和子类可以有同名成员,默认访问的是子类的,子类同名成员隐藏了父类的同名成员,如果想要访问父类的成员变量,就要在成员变量前面加类域(person1::_num )。
同名的成员函数就构成隐藏,参数和返回值都不重要!!
——————————————————————————————————
2,子类如何继承父类
class Person {
public:
Person(string str="zhangsan",int age=18) {
_name = str;
_age = age;
cout << "Person()" << endl;
}
Person(Person& p)
:_name(p._name)
{
cout << "Person(Person& p)" << endl;
}
Person& operator=(const Person& per) {
if (&per != this) {
_name = per._name;
_age = per._age;
}
cout << " Person& operator=(const Person& per)" << endl;
return *this;
}
~Person() {
cout << "~Person" << endl;
}
protected:
string _name;
int _age;
};
class Student :public Person {
public:
Student(const char* name,int id)
:Person(name)
{
cout << "Student(char* name,int id)" << endl;
}
Student(Student& s)
:Person(s)
,_id(s._id)
{
cout << "Student(Student& s)" << endl;
}
Student& operator=(const Student& stu) {
if (&stu != this) {
_id = stu._id;
Person::operator=(stu);
}
cout << "Student& operator=(const Student& stu)" << endl;
return *this;
}
~Student() {
cout << "~Student() " << endl;
}
protected:
int _id;
};这里需要注意观察,子类如何调用父类的相关函数
————————————————————————————————
Student s1("lihua", 1001)
Student s2(s1);
Student s1("lihua", 1001);
Student s3("zhangsan",2002);
s3 = s1;
由以上三个例子可知:构造函数,拷贝构造,运算符重载都是先调父类,再调子类,但是析构函数除外,时先调子类,再调父类(父子类析构函数会构成隐藏,由于多态的原因,析构函数统一被处理成destructor)
为了保证先子后父,子类析构函数完成后会自动调用父类的析构函数从而实现先子后父。原因:难免子类的析构函数会访问父类的成员变量,如果先析构了父类,就会越界。
——————————————————————————————
3,如何实现一个不能被继承的类?
(1)构造函数私有化
(2)c++11新增了一个关键字,final
——————————————————————————————
(1)友元关系不能被继承
(2)基类定义了静态成员变量,则整个继承关系中只有这样一个这个样的成员
(3)c++允许多继承,之间用逗号隔开,但是多继承回引发菱形继承
4,菱形继承
class A {
public:
int _a;
};
class B:public A {
public:
int _a;
};
class C :public A {
public:
int _c;
};
class D :public B, public C{
public:
int _d;
};
——————————————————————————
缺点:
(1)这样会有二义性(继承了两份的名字和年龄),无法明确知道访问的是哪一个,
(2)数据冗余
为了解决这个问题,引入了虚拟继承,用关键字virtual,加在继承同一个类的两个类前面。这样一来,将父类A的成员变量放到另一块空间里,B,C类里面会有一个指针,指向一个表,表里会存放父类A的成员变量的位置信息,这个表被称为“虚基表”,这样子类在实例化对象时就只有一份父类(A类)的成员变量了。
二,多态
1,多态的条件
(1)虚函数重写
(2)父类指针/引用,调用虚函数(引用没有开空间,只是子类中父类部分的别名)
void func(person& p) {p.print();}//引用
void func(person* p) { p->print();}//指针
2,分类:
(1)静态多态,函数重载
(2)动态多态,虚函数重写,父类指针/引用调用虚函数
3,虚函数
(1)虚函数和虚继承没有关系
(2)虚函数的重写:继承关系中父子的两个虚函数,三同(函数名/参数/返回)
(3)virtual只能修饰成员函数
(4)三同(函数名/参数/返回)返回值可以不同,但必须是父子类关系的指针或者引用,
(5)派生类重写虚函数可以不加virtual(建议都加上)
(6)普通函数的继承时实现继承,虚函数的继承,是一种接口继承,继承的是接口
4,调用方式
(1)普通调用:调用的函数类型是谁,就调用这个类型的函数
(2)多态调用:调用指针或者引用指向的对象,指向父类调用父类函数,指向子类调用子类函数
5,建议析构函数运用多态的原因
编译器把析构函数特殊处理成distructor,从而构成重名,完成虚函数重写。为什么要写成虚函数,清理派生类,防止内存泄漏。
6,小试牛刀:
会输出什么:
答:B->1
当调用test()时,调用的是父类的test函数,所以test()这里的隐形参数是A* this(父类指针),子类对象传给了父类指针,然后调用func(),发生多态,会调用子类的func(),多态调用是接口继承,派生类是重写父类的函数的内容,但用的还是父类的接口,所以子类的缺省参数还是1,所以输出B->1
7,重载,重写,隐藏区别:
重载:作用域同一作用域,参数不同
重写(覆盖)两个函数分别在基类和派生类(三同)虚函数
隐藏(重定义)两个函数分别在基类和派生类,函数名相同,不构成重写
8,其他关键字
(1)final可以修饰类,不能被继承,修饰虚函数,不能被重写
(2)override,修饰派生类的虚函数,检查是否完成重写
9,虚函数,纯虚函数区别:
虚函数:成员函数前添加 `virtual` 关键字,可以直接使用或被子类重载实现后以多态形式调用。
纯虚函数:在虚函数后添加 `=0`,必须被子类重载实现后才能以多态形式调用,基类中只有声明。
10,抽象类
(1)抽象类:无法实例化对象
(2)包括纯虚函数的类称为抽象类,如果父类包含纯虚函数,那么他的派生类也不能实例化对象
除非将纯虚函数进行重写
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;
}
};
——————————————————————————————
11,含有虚函数类的大小
class M {
private:
int _b = 1;
char _ch;
public:
virtual void func() {
cout << "func()" << endl;
}
cout << sizeof(M) << endl;在32位的计算机上占12字节
原因:会把虚函数地址存到虚函数表中,所以这个类会有一个指针,指向虚函数表(称为虚函数表指针)所以出了一个int和通过内存对齐的char型,还有一个4个字节的指针,所以一共12字节。
注意:编译好后,虚函数在代码段,虚函数表在代码段(常量区)
问:指针和引用,是指向子类中切割出来的父类那一部分,那么对象为什么不可以接收?
答:切割出子类中父类那一部分,成员拷贝给父类,但是不会拷贝虚函数表指针
12,多态的原理:(画图)
父类对象为Person,里面有两个成员函数,分别为print和func,均为虚函数。子类为student类,有成员变量_a,这里初始化为了1,里面有一个成员函数,为print,且print进行了重写。
我们查看父类对象和子类对象的内存情况,父类对象中含有一个指针(虚表),我们打开指针所指的空间,发现有两个指针分别对应print和func,子类对象中也含有一个指针(虚表),我们打开指针所指的空间,发现有两个指针分别对应print和func,func是继承的父类的函数。而且func没有进行重写,所以子类中指向func和父类指向func为同一地址(同一函数),但是print进行了重写,所以,所以子类中指向print和父类指向print为不同地址。所以子类传给父类的引用调用的是覆盖后的print函数。
注意:一个类可以有多个对象,但是只有一张虚表
13,打印虚表中的函数
(1)单继承
class A1 {
public:
virtual void func1() { cout << "A::func1" << endl;}
virtual void func2() { cout << "A::func2" << endl; }
};
class B1 :public A1 {
public:
virtual void func1() {cout << "B::func1" << endl; }
virtual void func3() { cout << "B::func3" << endl; }
virtual void func4() { cout << "B::func4" << endl; }
};
class C1 :public B1 {
public:
virtual void func3() { cout << "C::func3" << endl; }
};
————————————————————————
typedef void (*VFUNC)();函数指针的类型
void printVFT(VFUNC* a) {
for (size_t i = 0; a[i]!=0 ; i++)
{
printf("[%d]:%p->", i, a[i]);
VFUNC f = a[i];
f();//打印出函数地址对应的函数
}
printf("\n");
}
int main() {
A1 a;
printVFT((VFUNC*)(*((int*)&a)));
B1 b;
printVFT((VFUNC*)(*((int*)&b)));
C1 c;
printVFT((VFUNC*)(*((int*)&c)));将对象取地址,只取对象前四个字节的地址,因为前四个字节储存的为虚表的地址。所以转型为(int*)再解引用,再转为函数数组指针的类型。然后进行遍历
——————————————————————
(2)多继承
class A1 {
public:
virtual void func1() {cout << "A::func1" << endl;}
virtual void func2() { cout << "A::func2" << endl; }
};
class B1 {
public:
virtual void func1() { cout << "B::func1" << endl; }
virtual void func2() { cout << "B::func3" << endl; }
protected:
int _b;
};
class C1 :public A1,public B1 {
public:
virtual void func3() { cout << "C::func3" << endl; }
virtual void func1() { cout << "C::func1" << endl; }
protected:
int _c;
};
————————————————————————
typedef void (*VFUNC)();
void printVFT(VFUNC* a) {
for (size_t i = 0; a[i] != 0; i++)
{
printf("[%d]:%p->", i, a[i]);
VFUNC f = a[i];
f();
}
printf("\n");
}
C1 c;
printVFT((VFUNC*)(*((int*)&c)));
B1* p2 = &c;
printVFT((VFUNC*)(*(int*)p2));
我们可以清楚地看到,A1和B1的虚表存放着相同的func1,但是他们的地址却不一样,下面我们来分析原因:
如图所示:我们分别用p1和p2去调用func1,p2比p1多跳转了几步最终访问到了同一个func1,由此可以他们指向的是同一个函数,只不过走的路径不同
(3)菱形继承(虚拟继承)(这里简单说明)
他们的父类A,单独放到了下方的空间(只有一份),B和C里面都有两份指针,第一个是虚函数表,第二个是虚基表,第一个存放虚函数的地址,第二个存放父类A类中的成员变量距这里有多少个字节。