目录
1. 继承
1.1 继承的概念
1.2 继承的定义
1.2.1 定义格式
1.2.2 继承关系和访问限定符
1.2.3 继承基类成员访问方式的变化
2. 基类和派生类对象赋值转换
3. 继承中的作用域
4. 派生类的默认成员函数
5. 继承与友元
6. 继承与静态成员
7. 复杂的菱形继承及菱形虚拟继承
8. 多态
8.1 多态的概念
8.2 多态的构成条件
8.3 虚函数
8.4 虚函数的重写
8.5 C++11 override和final
8.6 重载、重写(覆盖)、重定义(隐藏)
9. 抽象类
9.1 抽象类的概念
9.2 接口继承和实现继承
10. 多态的原理
10.1 虚函数表
10.2 多态的原理
11. 单继承和多继承关系的虚函数表
11.1 单继承中的虚函数表
11.2 多继承中的虚函数表
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "peter";//姓名
int _age = 18;//年龄
};
//继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分
class Student : public Person
{
protected:
int _stuid;//学号
};
class Teacher : public Person
{
protected:
int _jobid;//工号
};
int main()
{
Student s;
Teacher t;
s.Print();
t.Print();
return 0;
}
调用监视窗口查看Student和Teacher对象,可以看到成员变量的复用。
通过运行结果可以看到成员函数的复用。
public继承
protected继承
private继承
public访问
protected访问
private访问
class Person
{
protected:
string _name = "peter";//姓名
int _age = 18;//年龄
};
class Student : public Person
{
public:
int _stuid;//学号
};
int main()
{
Student s;
//子类对象可以赋值给父类对象/指针/引用
Person p = s;
Person* pp = &s;
Person& rp = s;
//父类对象不能赋值给子类对象
s = p;//err
//父类的指针或引用可以通过强制类型转换赋值给子类的指针
pp = &s;
Student* ps1 = (Student*)pp;//ok
ps1->_stuid = 000111;
pp = &p;
Student* ps2 = (Student*)pp;//这种情况转换时虽然可以,但是会存在越界访问的问题
ps2->_stuid = 000222;
return 0;
}
//Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
class Person
{
protected:
string _name = "小李子";//姓名
int _num = 111;//身份证号
};
class Student : public Person
{
public:
void Print()
{
cout << " 姓名:" << _name << endl;
cout << " 身份证号:" << Person::_num << endl;
cout << " 学号:" << _num << endl;
}
protected:
int _num = 999;//学号
};
void Test()
{
Student s;
s.Print();
};
//B中的fun和A中的fun不是构成重载,因为不是在同一作用域
//B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
A::fun();
cout << "func(int i)->" << i << endl;
}
};
void Test()
{
B b;
b.fun(10);
};
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。
class Person
{
public:
Person() { ++_count; }
protected:
string _name;//姓名
public:
static int _count;//统计人的个数
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuNum;//学号
};
class Graduate : public Student
{
protected:
string _seminarCourse;//研究科目
};
void TestPerson()
{
Student s1;
Student s2;
Student s3;
Graduate s4;
cout << " 人数 :" << Person::_count << endl;
Student::_count = 0;
cout << " 人数 :" << Person::_count << endl;
}
int main()
{
TestPerson();
return 0;
}
单继承:一个子类只有一个直接父类。
多继承:一个子类有两个或以上直接父类。
菱形继承:菱形继承是多继承的一种特殊情况。
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。在Assistant的对象中Person成员会有两份。
class Person
{
public:
string _name;//姓名
};
class Student : public Person
{
protected:
int _num;//学号
};
class Teacher : public Person
{
protected:
int _id;//职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse;//主修课程
};
void Test()
{
//这样会有二义性无法明确知道访问的是哪一个
Assistant a;
a._name = "peter";//err
//需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
}
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。
class Person
{
public:
string _name;//姓名
};
class Student : virtual public Person
{
protected:
int _num;//学号
};
class Teacher : virtual public Person
{
protected:
int _id;//职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse;//主修课程
};
void Test()
{
Assistant a;
a._name = "peter";//ok
}
为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成员的模型。
A
↗↖
B C
↖↗
B
菱形继承的内存对象成员模型:
class A
{
public:
int _a;
};
class B : public A
{
public:
int _b;
};
class C : public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
可以看出菱形继承数据冗余。
菱形虚拟继承的内存对象成员模型:
class A
{
public:
int _a;
};
class B : virtual public A
{
public:
int _b;
};
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
这里可以分析出D对象中将A放到的了对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。
多态:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。
那么在继承中要构成多态还有两个条件:
被virtual修饰的类成员函数称为虚函数。
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "买票-半价" << endl;
}
};
void Func(Person& people)
{
people.BuyTicket();
}
void Test()
{
Person p;
Func(p);
Student s;
Func(s);
}
虚函数重写的两个例外:
1. 协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。协变是一种重写。
class A {};
class B : public A {};
class Person
{
public:
virtual A* f() { return new A; }
};
class Student : public Person
{
public:
virtual B* f() { return new B; }
};
2. 析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
class Person
{
public:
virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person
{
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
//只有派生类Student的析构函数重写了Person的析构函数
//下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。
1. final:修饰虚函数,表示该虚函数不能再被重写
2. override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
重载:
重写(覆盖):
重定义(隐藏):
在虚函数的后面写上 = 0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
int main()
{
cout << sizeof(Base) << endl;//8
Base b;
return 0;
}
一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
针对上面的代码我们做出以下改造:
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
通过观察和测试,我们发现了以下几点问题:
看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。
静态绑定:又称前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
动态绑定:又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
class Base
{
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
int a;
};
class Derive :public Base
{
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
virtual void func4() { cout << "Derive::func4" << endl; }
private:
int b;
};
//打印虚表中的函数
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
//依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Base b;
Derive d;
//思路:取出b、d对象的头4bytes,就是虚表的指针,
//虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
//1. 先取b的地址,强转成一个int*的指针
//2. 再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
//3. 再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
//4. 虚表指针传递给PrintVTable进行打印虚表
//5. 需要说明的是这个打印虚表的代码经常会崩溃,
//因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。
//我们只需要点目录栏的 - 生成 - 清理解决方案,再编译就好了。
VFPTR* vTableb = (VFPTR*)(*(int*)&b);
PrintVTable(vTableb);
VFPTR* vTabled = (VFPTR*)(*(int*)&d);
PrintVTable(vTabled);
return 0;
}
监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。我们可以通过打印出虚表中的函数来观察。
可以看出,Derive::func1覆盖了Base::func1。
class Base1
{
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2
{
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
class Derive : public Base1, public Base2
{
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Derive d;
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb1);
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
PrintVTable(vTableb2);
return 0;
}
可以看出,多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。