继承的本质是复用,继承是类设计层次的复用
基类/父类(大家都有的信息提取到一个类中)
子类/派生类
定义格式 class Student(派生类) : public(继承方式) Person(基类)
基类private成员在派生类中无论什么方法继承都是不可见的,语法上限制派生类对象在类里和类外都不能访问它
保护和私有在父类中没有区别
在子类中,private成员不可见,protected是可见的
实际中一般都是使用public继承,几乎很少使用private和protected继承,也不提倡使用
常见的使用:
父类成员:公有和保护
子类继承方式:公有继承
class 中如果不写默认限定符,默认是private
struct中如果不写默认限定符,默认是public
继承方式上,如果不写的话,class是私有继承
赋值兼容规则 – 也叫切割/切片
父类=子类 赋值兼容->切割/切片,但是仅限于公有继承
子类 = 父类 可以传引用或指针,但是有越界的风险
基类和派生类都有自己独立的作用域
不同的域可以有相同的成员名,局部优先原则(就近原则)
如果基类和派生类都有共同的成员变量_num,想要访问基类中的_num要指定作用域
上面这种子父类出现同名成员,我们把它叫做隐藏/重定义,上面这种情况下,子类会隐藏父类的_num
既然隐藏了,就不能直接调用基类的函数或成员变量,除非指定作用域
函数重载要求在同一作用域
成员函数如果函数名相同就会构成隐藏(参数无所谓,不影响)
#include#include #include using namespace std; class Person { protected: string _name = "小李子"; // 姓名 int _num = 111; // 身份证号 }; class Student : public Person { public: void Print() { cout << "姓名" << _name << endl; cout << "身份号码" << _num << endl; } protected: int _num = 120; }; class A { public: void fun() { cout << "func()" << endl; } }; class B : public A { public: void fun(int i) { cout << "func(int i)->" << i << endl; } }; int main() { Person p; Student s; s.Print(); A a; B b; a.fun(); b.fun(1); b.A::fun(); return 0; }
如果不写,C++会默认生成8个默认构造函数(构造,析构,拷贝构造,赋值, 移动构造,移动赋值…还有两个不用管)
我们不写默认生成的派生的构造和析构?
a . 父类继承下来的(调用父类的默认构造和析构处理)b .自己的(内置类型和自定义类型成员)(跟普通类一样处理)
我们不写默认生成的拷贝构造和operator=?
a . 父类继承下来的(调用父类的默认构造和析构处理)b .自己的(内置类型和自定义类型成员)(跟普通类一样,默认浅拷贝,要深拷贝的地方自己写)
总结:原则,继承下来的调用父类处理,自己的按普通类的规则
什么情况下要我们自己写?
1 .父类没有默认构造,需要我们自己显示地写构造
2 .如果子类有资源需要释放,需要自己显示地写析构
3 .如果子类存在浅拷贝问题,需要自己写拷贝构造和赋值解决浅拷贝问题
如果让我们自己写,如何写?
父类成员调用父类的对应构造,拷贝构造,operator=和析构处理
自己成员按普通类处理
传切片 ,赋值(自己给自己赋值特殊处理,记得调用operator=要指定域),假设有资源要清理,在析构函数里自己写比如delete[] …,调父类的析构要指定作用域
才能调(析构函数名字会被统一处理成destructor,那么子类的析构函数跟父类的析构函数构成隐藏)
子类析构函数结束时,会自动调用父类的析构函数,这样可以保证先析构子,再析构父
class Person {
public:
Person(const char* name = "哈哈")
:_name(name)
{
cout << "Person()" << endl;
}
Person(const Person& person)
:_name(person._name)
{
cout << "Person(const Person & person)" << endl;
}
Person& operator=(const Person& p)
{
if (this != &p)
{
_name = p._name;
}
return *this;
}
~Person()
{
cout << "~Person" << endl;
}
protected:
string _name;
};
class Student:public Person
{
public:
Student(const char* name = "嘻嘻", const int num = 1001)
:Person(name)
, _num(num)
{
cout << "Student()" << endl;
}
Student(const Student& s)
:_name(s._name)
,_num(s._num)
{
}
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s);
_num = s._num;
}
return *this;
}
~Student(){
cout << "~Student"<
Person p;
Student s("haha");
Student s1(s);
return 0;
}
有元关系不能继承
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
friend void Display(const Person& p, const Student& s);
protected:
int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
int main()
{
Person p;
Student s;
Display(p, s);
}
继承和静态成员
无论继承了多少份,都只有一个
因为每次都会调用父类的构造函数,构造函数里写个++静态,就可以统计有多少个对象(反正这个静态只有一份),能够继承但只有一份
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; // 研究科目
};
int main()
{
Person p;
Student s;
Graduate g;
cout << Person::_count << endl;
cout << Student::_count << endl;
cout << Graduate::_count << endl;
cout << &Person::_count << endl;
cout << &Student::_count << endl;
cout << &Graduate::_count << endl;
}
4 .多继承
菱形继承
单继承:一个子类只有一个直接父类时称这个关系叫做单继承
多继承:一个子类有两个或两个以上的直接父类时称这个关系叫做多继承,写的时候两个public用逗号隔开
菱形继承是多继承的一种特殊情况
存在数据冗余和二义性的问题
多继承就是一个坑,Java没有多继承
学习知识“学其然,学其所以然”
可以适用多继承,但是不要搞成菱形继承
指定作用域可以勉强解决二义性的问题
虚继承解决数据冗余和二义性
在菱形继承腰部的位置进行虚继承
加入关键词virtual, _name赋值可以指定作用域,但是访问的都是同一个,加了virtual后就不用固定加作用域了,没有数据冗余
内存窗口输入&d ,1 3就是b ,2 4 就是c , 先继承在前,后继承在后
//虚继承解决数据冗余和二义性
class Person
{
public:
string _name; // 姓名
int _a[10000];
};
class Student : virtual public Person
{
public:
int _num; //学号
};
class Teacher : virtual public Person
{
public:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
int main()
{
// 二义性、数据冗余
Assistant a;
a._id = 1;
a._num = 2;
a.Student::_name = "小张";
a.Teacher::_name = "张老师";
a._name = "张三";
cout << sizeof(a) << endl;
return 0;
}
int main()
{
return 0;
}
A一般叫虚基类
有类 A B C D 在D里面,A放到一个公共位置,那么有时B需要找A,C需要找A,就要在虚基表中的偏移量进行计算
继承的总结和反思:
有了多继承,就有了菱形继承,有了菱形继承,就有了菱形虚拟继承,底层就会很复杂,是C++的一个缺陷之一。
还有一个缺陷就是不像java有垃圾回收机制,需要我们手动去进行回收
public继承是一种is-a的关系,每个派生类有都基类的对象
组合是一种has-a的关系,假设B组合了A,每个B对象中有A
可以想头和眼睛的关系,车子和车轮的关系,都是组合
继承是白箱复用,组合是黑箱复用
白箱测试更难,因为内部的细节都知道,需要测试者能够看懂,才可以设计出能够覆盖全部功能的测试代码
“高内聚,低耦合”:类和类之间的关系越少越高,类内部的关联越强越好,如果不是和这个类强相关的成员不要设计到这个类里面去
类和类之间最好的就是高内聚,低耦合,这样方便维护
大白话就是说,需要修改一个地方的时候,不需要去其他地方进行大范围的修改
如果一个类完美契合继承,那就适用继承(is-a)
如果一个类完美契合组合,那就适用组合(has-a)
如果两者都可以的情况下,我们选用组合
多态:多种形态
又分为静态多态和动态多态
静态的多态:函数重载,看起来调用一个函数会有不同的行为
静态:原理是编译时实现
动态的多态:一个父类的指针或引用去调用同一个函数,传递不同的对象,会调用不同的函数
动态:原理是运行时实现
本质:不同的人去做同一件事情,结果不同
#include2 .多态的满足条件#include using namespace std; class Person { public: virtual void Buy_ticket() { cout << "Person::全价票" << endl; } }; class Student :public Person { public: void Buy_ticket() { cout << "Student::半价票" << endl; } }; void Buy_ticket(Person& p) { p.Buy_ticket(); } int main() { Person p; Student s; Buy_ticket(p); Buy_ticket(s); return 0; }
①继承的关系
②必须通过基类(父类)的指针或者引用调用虚函数
③被调用的函数一定是虚函数,且派生类要对基类的虚函数进行重写
虚函数重写的四个条件:
子类满足三同(函数名,参数列表,返回值)虚函数(也就是说要加上virtual,也可不加,后面会说),叫做重写(覆盖)
重写的返回值有个例外:协变 – 要求返回的值是父子关系的指针或者引用
class Person {
public:
//virtual A* BuyTicket() { cout << "买票-全价" << endl; return nullptr; }
virtual Person* BuyTicket() { cout << "买票-全价" << endl; return nullptr; }
};
class Student : public Person {
public:
// 子类中满足三同(函数名、参数、返回值)虚函数,叫做重写(覆盖)
//virtual B* BuyTicket() { cout << "买票-半价" << endl; return nullptr; }
virtual Student* BuyTicket() { cout << "买票-半价" << endl; return nullptr; }
};
// 构成多态,跟p的类型没有关系,传的哪个类型的对象,调用的就是这个类型的虚函数 -- 跟对象有关
// 不构成多态,调用就是p类型的函数 -- 跟类型有关
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
//int i = 1;
//double d = 2.22;
//cout << i; // cout.operator<<(int)
//cout << d; // cout.operator<<(double)
Person ps;
Student st;
Func(ps);
Func(st);
//st.BuyTicket();
//st.Person::BuyTicket();
return 0;
}
构成多态,跟p的类型没有关系,传的是什么类型的对象,调用的就是这个类型的虚函数–跟对象有关
不构成多态,调用的就是p类型的函数 – 跟类型有关
析构函数+ virtual 不然不可以实现多态
析构函数为什么要设计成虚函数
情况一:Person* p1 = new Person
情况二:Person* p2 = new Student
情况一不需要但是情况二是需要的
动态申请的父子对象,如果想要正确管理,那么析构函数要用到虚函数实现正确的调用
关于为什么虚函数要调virtual,重点要注意这种场景
其他的场景,析构函数是不是虚函数,都可以正确调用析构函数
//我们在完成重写的过程中,析构函数的名字不同
//一个是~Person,一个是~Student,可以正确调用吗?
//当然是可以的,这个没有关系,最后他们都会变成destrction
//但是如果想要实现多态,还是要加上virtual的
class Person {
public:
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student :public Person
{
public:
virtual ~Student()
{
cout << "~Student()" << endl;
}
};
int main()
{
//这样调当然是没有问题的
//Person p;
//Student s;
//如果没有加virtual,这边调出来是两个父类的析构函数
//万一我们子类有自己的资源呢?那么就需要我们调子类自己的析构函数
Person* p1 = new Person;
Person* s2 = new Student;
delete p1;
delete s2;
return 0;
}
3 .虚函数的重写
虚函数的重写,允许父子类都是虚函数或者父类是虚函数,再满足三同,就构成重写
建议是都写上virtual
虽然子类没写virtual ,但是他先继承了父类的虚函数的属性,再完成重写,那么他也算是虚函数
哪怕子类重写的方法是private的,仍然是可以被调用的(C++设计的小缺陷)
既然这样,子类的虚函数在这种情况下也是可以不写的
父类的析构函数加上了virtual,那么就不存在不构成多态,没调用子类的析构函数,发生内存泄漏
所以还是建议都加上virtual
// 虚函数的重写允许,两个都是虚函数或者父类是虚函数,再满足三同,就构成重写。
// 其实这个是C++不是很规范的地方,当然我们建议两个都写上virtual
// 本质上,子类重写的虚函数,可以不加virtual是因为析构函数
// 父类析构函数加上virtual,那么就不存在不构成多态,没调用子类析构函数,内存泄漏场景
// 建议,我们自己写的时候,都加上virtual,肯定没毛病
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
// 虽然子类没写virtual,但是他是先继承了父类的虚函数的属性,再完成重写。那么他也算是虚函数
void BuyTicket() { cout << "买票-半价" << endl; }
public:
~Student() { cout << "~Student()" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
// 普通对象,析构函数是否虚函数,是否完成重写,都正确调用了
// Person p;
// Student s;
// 动态申请的对象,如果给了父类指针管理,那么需要析构函数是虚函数
Person* p1 = new Person; // operator new + 构造函数
Person* p2 = new Student;
// 析构函数 + operator delete
// p1->destructor()
delete p1;
delete p2;
// p2->destructor()
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
4 .IO流的遗留问题
单独用ofs是可以的
看懂IO的继承体系
实现了ostream,下面的三个子类就也都支持了
自定义类型我们才去重载一下
复习:
什么是多态?不同的对象,去完成同一件事情,结果不同
–“看人下菜”
多态的条件为什么一定是基类的指针/引用?
这样既可以传子类也可以传父类
final
如何设计一个不能被继承的类?
方法一(C++98的方式):
间接限制,把父类的构造函数设计成私有的,这样子类的构造函数无法调用父类的构造函数初始化成员,没办法实例化对象
加个static可以打破先有鸡还是先有蛋(要有实例才能调用成员函数)的循环,否则不加就没有对象
#include#include #include using namespace std; //C++98的方式 class A { private: A(int a = 1) { _a = a; } public: //不能实例化是不能够调用到成员方法的,这就是一个先有鸡和先有蛋的问题 //解决的办法就是加上一个static static A CreatA(int a = 0) { return A(a); } protected: int _a; }; class B :public A { }; int main() { A a = A::CreatA(10); return 0; }
方法二(C++11final直接限制):
直接在class A 后面加final
一个类加上final也就被叫做最终类,不可以被继承
不想让一个成员方法被重写,也可以在方法的后面加上final ,void func( ) final
C++ final 可以修饰虚函数,限制他不能被子类中的虚函数重写(了解了解)
//class A final
//{
//
//};
//
//class B :public A
//{
//
//};
class C
{
public:
virtual void func() final
{
cout << "Here is C";
}
};
class D :public C
{
public:
virtual void func()
{
cout << "Here is D";
}
};
int main()
{
//B b;
return 0;
}
关键词override
override放在子类重写的虚函数的后面,检查是否完成了重写,没有完成重写的话,会报错(强制检查)
class bike
{
public:
virtual void drive()
{
cout << "bike working" << endl;
}
};
class JiAnt:public bike
{
public:
virtual void drive()override
{
cout << "JiAnt working" << endl;
}
};
int main()
{
bike b;
b.drive();
JiAnt j;
j.drive();
bike* p = new JiAnt;
p->drive();
return 0;
}
重载,重写(覆盖),重定义(隐藏)三个的对比
重载:
①两个函数在同一作用域
②函数名/参数不同,返回值没有要求
重写:
①两个函数分别在基类和派生类当中
②“三同”:函数名/参数/返回值都要相同(协变例外)
③ 两个函数都是虚函数(不考虑说子类可以不写virtual的情况,其实也是完成重写了)
重定义(隐藏):
①两个函数分别在基类和派生类当中
②函数名相同
③两个基类和派生类的同名函数如果不构成重写就是重定义
抽象类
包含纯虚函数的类就叫抽象类(纯虚函数就是在虚函数的后面加上一个 =0)
特点:不能够实例化出对象
纯虚函数一般只声明,不实现(实现是没有价值的)
包含纯虚函数的类是不能够实例化出对象的,但是指针是可以的
但是如果调用,会发生错误
原理放在后面虚函数表中去讲
抽象类的派生类也是抽象类,如果想要实例化出对象,一定要重写纯虚函数
哪些类会设计成抽象类呢?
抽象 – 一般指在现实世界中没有对应的实物
一个类型,如果一般在现实世界中,没有对应的实物就定义成抽象类比较好
抽象类实际上强制了子类去完成重写,override只是在语法上检查是否完成了重写
class bike
{
public:
//纯虚函数,一般只定义,不实现
virtual void drive() = 0;
};
class JiAnt :public bike
{
public:
virtual void drive()
{
cout << "JiAnt working" << endl;
}
};
int main()
{
//抽象类不可以实例化对象,但是可以调用其指针
bike* p = new JiAnt;
p->drive();
return 0;
}
一道笔试题
有虚函数就会多四个字节(32位下的虚函数表指针_vfptr,简称虚表指针)
_vfptr实际上是一个函数指针数组
//这里常考一道笔试题:sizeof(Base)是多少?-- 12
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
virtual void Func2()
{
cout << "Func2()" << endl;
}
private:
int _b = 1;
char _ch = 'A';
};
int main()
{
cout << sizeof(Base) << endl;
Base bb;
return 0;
}
6 .为什么传的是指针和引用?
普通的函数是直接确定地址
传入对象(Mike)那就去Mike的虚函数表指针里面找虚函数
如果传子类,也去对应的虚函数表指针里找子类的虚函数
多态的原理:基类的指针/引用,指向谁,就去谁的虚函数表里找到对应位置的虚函数进行调用
class Person
{
public:
virtual void BuyTicket()
{
cout << "Person - 普通票" << endl;
}
void method()
{
cout << "——method" << endl;
}
};
class Student :public Person
{
public:
virtual void BuyTicket()
{
cout << "Student - 学生票" << endl;
}
};
void _Func(Person& p)
{
p.BuyTicket();
p.method();
}
int main()
{
Person p;
Student s;
_Func(p);
_Func(s);
return 0;
}
为什么多态只能传引用或指针(为什么调对象不行呢)?对象也可以是切片
拿调引用和调对象做比较:
引用里的_vptr虚函数表指针和父类中的是一个(因为是切片后的别名)
对象的切片,会把值赋过去,虚表指针不会拷贝过去,父类的虚表指针可能指向子类(当然不行),父类就是父类虚表,子类就是子类的虚表,所以只能是指针或引用
对象的话不会把虚表传过去,那就不会实现出多态了
父类对象一定是父类的虚表,子类对象一定是子类的虚表
深层次原因:对象是不能实现出多态的
要实现多态就要拷虚表,但拷虚表就乱了
不是多态,编译时确定地址
符合多态的条件,才会到虚函数表中去找,再调用
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;
}
virtual void Func4()
{
cout << "Derive::Func4()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
Base* p1 = &b;
p1->Func1();
p1 = &d;
p1->Func1();
return 0;
}
同类型的虚表指针是一样的,同类型的对象,虚表指针是相同的,指向同一张虚表(看图)
普通函数和虚函数存的位置一样吗?
一样的,都在公共代码段。只是虚函数要把地址存一份到虚表,方便实现多态
普通函数直接找到地址(更快),虚函数符合多态的情况下还要去找
为什么子类对象私有地方的方法也可以调到?
去虚表里去找这个私有就会不起作用了;
虚函数的地址才会被放进虚表
总结一下派生类的虚表生成:
a .先将基类的虚表拷贝一份到派生类的虚表当中
b .如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c .派生类自己的虚函数按其在派生类的声明次序增加到虚表的最后
虚函数表是存在哪的?(不是虚函数表指针)
如何取一个对象的头四个字节呢?取地址强转int* 再解引用*((int*)&b)变成一个int
发现最接近常量区
多继承的情况下,子类存的有所有父类的附表吗?
不会,只会存第一个父类的虚表
发现地址不同,只是jump操作不同,跳到的其实是同一个地方
反汇编查看代码
多继承虚表有兴趣看看
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;
};
int main()
{
Derive d;
Base1* p1 = &d;
p1->func1();
Base2* p2 = &d;
p2->func1();
return 0;
}
复习
构成多态,运行时决议,运行时决定调用谁
不构成多态,编译时决议,编译时决定调用谁
虚表的指针实在构造函数的初始化列表初始化的
_vfptr 全程virtual function pointer
打印虚函数表(了解)
虚函数表:简称虚表
本质从类型出发:函数指针数组
函数指针的typedef有点不一样
函数指针的地址 就是 指向函数指针的指针
x86 就是32位 windows下叫win32 ,Linux 下叫X86
条件编译实现32位和64位
void**写法是通用的
#include#include using namespace std; typedef void(*VF_PTR)(); //void PrintVFTable(VF_PTR table[]) // 打印虚函数表中内容 void PrintVFTable(VF_PTR* table) { for (int i = 0; table[i] != nullptr; ++i) { printf("vft[%d]:%p->", i, table[i]); VF_PTR f = table[i]; f(); } cout << endl << endl; } / // 单继承 class Base { public: Base() { a = 0; } //private: virtual void func1() { cout << "base::func1" < cout << "base::func2" << endl; } private: int a = -1; }; class derive :public Base { public: virtual void func1() { cout << "derive::func1" << endl; } virtual void func3() { cout << "derive::func3" << endl; } void func4() { cout << "derive::func4" << endl; } private: int b; }; int main() { Base b; //cout << &b << endl; //b.func1(); #ifdef _win64 PrintVFTable((VF_PTR*)(*(long long*)&b)); #else PrintVFTable((VF_PTR*)(*(int*)&b)); #endif // _win64 // PrintVFTable((VF_PTR*)(*(void**)&b)); //printvftable((vf_ptr*)(*(char**)&b)); // printvftable((vf_ptr*)(b)); // cout << sizeof(long long) << endl; derive d; return 0; }
意义相关类型,才可以转换
多继承的虚函数表
Derive有两份虚表
打印第二份虚表要跳过Base1
切片的时候指针会偏移
先继承的会放在前面
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;
};
class A
{
public:
virtual void f1()
{}
public:
int _a;
};
// class B : public A
class B : virtual public A
{
public:
virtual void f1()
{}
virtual void f2()
{}
public:
int _b;
};
// class C : public A
class C : virtual public A
{
public:
virtual void f1()
{}
virtual void f2()
{}
public:
int _c;
};
class D : public B, public C
{
public:
virtual void f1()
{}
public:
int _d;
};
int main()
{
//Base1 b1;
//Base2 b2;
//Derive d;
//PrintVFTable((VF_PTR*)*((void**)&d));
PrintVFTable((VF_PTR*)*((void**)((char*)&d+sizeof(Base1))));
//Base1* p1 = &d;
//Base2* p2 = &d;
//PrintVFTable((VF_PTR*)*((void**)(p2)));
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
菱形继承有两篇相应的文章,有兴趣的可以看看
7 .几道经典题目一道选择题
虚继承 A只有一份
编译器经过处理以后A B C三个初始化最后会变成一份
首先s4的打印一定是最后的
先继承的是B,那么B就先打印
如果先继承C,再继承B的话
答案就是ACBD了
初始化列表的顺序跟初始化的顺序无关,跟声明的顺序有关,先继承的在前面
虚继承A只有一份,A既不属于B,也不属于C,如果是虚继承只有公共的一份
#include#include #include using namespace std; class A{ public: A(const char *s) { cout << s << endl; } ~A(){} }; class B : virtual public A { public: B(const char *s1, const char*s2) :A(s1) { cout << s2 << endl; } }; class C : virtual public A { public: C(const char *s1, const char*s2) :A(s1) { cout << s2 << endl; } }; class D :public B, public C { public: D(const char *s1, const char *s2, const char *s3, const char *s4) :B(s1, s2), C(s1, s3), A(s1) { cout << s4 << endl; } }; int main() { D *p = new D("class A", "class B", "class C", "class D"); delete p; return 0; }
第二题,指针偏移问题
画图,会发生切片行为,p1,p2看一部分,p3可以看全体
对于指针的位置我们发现p1和p3是在一起的,但是意义确是不一样的
第三题,经典题
B->1
先用的是继承
隐形的多态,调用的是子类的虚函数
小坑:声明缺省参数用的是父类的,所以这里val是1,子类的func()参数怎么写用的都是父类的
C++语法的一个坑
问答题
1 .内联函数可以是虚函数吗?
严格来说不可以,前面可以加virtual,不过编译器会忽略掉inline内联这个属性,因为虚函数是要被放到虚表的
符合的内敛函数不需要地址,直接被替换掉
多态的调用一定要到虚表里面去找
标准回答:
可以。调用时,如果不构成多态,这个函数保持inline属性
如果构成多态,这个函数就没有内敛属性,因为调用是到对象的虚函数表中找到虚函数的地址,实现调用无法使用内联属性
实践出真知,找官方文档 / 自己找环境测,网上的不一定是最真实的,能被自己眼睛验证的才是最真实的
2 .静态成员函数可以是虚函数吗?
不可以,virtual 和 static 不能一起使用。
虚函数的价值是重写以后实现多态,静态成员函数没有this指针,无法访问虚表(静态成员函数可以直接类型调用A::())
static成员函数定义成虚函数没有价值
实际上我们定义静态函数就是为了直接通过类型去调
3 .构造函数可以是虚函数吗?
不能,构造函数成为虚函数没有价值
虚函数的意义是构成多态调用,那么多态调用要去虚函数表中查找虚表指针,
但是对象中的虚函数表指针是在初始化列表阶段才初始化的
4 .析构函数可以是虚函数吗
可以,最好讲出场景【(有自己的资源需要释放),virtual不用写。。。】
5 .对象访问普通函数快还是虚函数更快?
(虚函数的调用一定是去虚表中找的吗?)
如果不构成多态,都是编译时确定调用函数的地址,那么他们一样快
如果构成多态,那么虚函数调用是运行时, 去虚函数表中确定函数地址,普通函数编译时直接确定地址,那么普通函数更快



