[C++]牢记这7个要点,掌握继承和多态将变得非常简单
程序员文章站
2022-07-11 11:41:15
...
文章目录
一:c++中的继承
之前我妈所接触到的复用更多的都是函数复用,而继承则是对类层次的复用。它允许在保持原有类的特性基础上进行扩展,增加功能,产生新的类,称之为派生类(子类),原本的类则称之为基类(父类)。
1. 继承及访问权限
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
public:
int _id = 1;
// proteced: 类外不可见,类内部和子类的可见
protected:
string _name = "peter"; // 姓名
private:
int _age = 18; // 年龄
};
class Student : public Person
{
public:
void show()
{
cout << _name << "\t" << _id << endl;
//父类private成员在子类中不可见, 但是子类确实有父类的私有成员
//cout << _age << endl;
}
public:
int _num = 2020;
};
继承基类成员后访问方式的变化:
- 友元关系不能继承,也就是说父母的朋友不一定是我们的朋友,是不能够相互访问的。
- 基类定义了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; // 研究科目
test:
Person p;
Student s;
Graduate g;
p._count = 1;
s._count = 2;
g._count = 3;
Person::_count = 4;
Student::_count = 5;
Graduate::_count = 6;
cout << &p._count << endl;//6
cout << &s._count << endl;//6
cout << &g._count << endl;//6
};
- 优先使用对象组合,而不是类继承,类继承一定程度破坏了基类的封装,也被人们称之为白箱复用,而对象组合则是黑箱复用,组合类之间没有较强的依赖性,耦合度低,代码的维护性较好。
2. 切片操作
把派生类中父类那部分切割出来进行赋值。
3. 同名隐藏及默认成员函数
同名隐藏:
- 父类和子类中有同名的成员,子类只能够直接看到自己的成员
- 如果需要访问父类同名的成员,则需要加上父类的作用域
- 不同的作用域下,含有同名成员的话,当前作用域下的成员就会隐藏其他作用域下的同名成员,这不仅仅是继承体系独有的
成员变量隐藏:成员变量的名称相同
- 函数隐藏:只要函数名相同,就会构成函数隐藏,和参数无关–>这种情况一般是发生在不同作用域的父类和子类中(因此我们在继承体系中最好不要定义同名成员)
- 函数重载:在同一个作用域中,函数名相同,但参数不同的情况。
默认成员函数
- 派生类对象初始化先调用基类构造,再调派生类构造
- 派生类对象析构清理先调用派生类析构,再调用基类的析构
4. 菱形继承
- 单继承:一个子类只有一个父类
- 多继承:一个子类有两个或以上直接父类
- 菱形继承:多继承的一种特殊情况
菱形继承存在着冗余性和二义性
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 ass;
ass._name = "1";//无法知道它访问的是那个,或造成二义性
cout << sizeof(ass) << endl;
ass.Student::_name = "1";//因此即使是指定父类的话,但是却无法解决掉数据冗余的问题
ass.Teacher::_name = "2";
}
为了更好解决菱形继承的二义性和数据冗余问题,可以使用虚拟继承的方式来进行解决(虚拟继承不能够在其他地方使用)virtual
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;
};
二:多态
不同的对象执行同一种行为时所产生的不同状态,被人们称之为多态。(比如我们抢红包,同样的点击动作,出现的金额是不同的)
- 如果是非多态的话,则需要看它的类型
- 多态的话,则只需要看实际指向的那个实体
1. 多态的实现条件
- 继承
- 父类定义虚函数,子类重写了父类虚函数
- 调用虚函数的类型必须为指针/引用,一般都为父类指针/引用
必须同时满足以上条件,三者条件缺一不可
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
重写
- 重写: 派生类中有一个跟基类完全相同的虚函数(返回值类型,函数名,参数列表完全相同)当然其中也有列外的存在,就是协变函数重写和析构函数的重写
- 协变: 返回值类型可以不同,但是返回值类型必须是父子关系的指针/引用
- 析构函数的重写:虽然函数名不相同,看似违背重写规则,但在编译后的析构函数却是同一个,因此无论子类析构函数是否加virtual关键字,都与父类析构函数构成重写
虚函数的重写
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
virtual ~Student() { cout << "~Student()" << endl; }
}
协变函数重写
virtual B& BuyTicket() {
cout << "买票-半价" << endl;
return B();
}
析构函数的重写
- 派生类的虚函数即使是不加
virtual
关键字,也是可以构成重写,因为继承后基类的虚函数被继承下来了,不建议这样使用 - 如果父类函数加了virtual声明,则子类接口完全一致的函数,即使不加virtual的声明,也具有虚函数的属性,建议一般对于所有虚函数,都加上virtual。
2. 检测重写 抽象类
C++给我们提供了override和final
两个关键字,可以帮助我们检测是否重写
- final:修饰虚函数,表示改虚函数不能被继承
class Car
{
public:
virtual void Drive() final {}//使用final定义的函数不能被重写,体现实现继承
};
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒适" << endl;}
};
- override:检查派生类虚函数是否重写了基类某个虚函数,强制子类重写父类的某一个虚函数,如果没有则重写编译报错
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}//体现接口继承
};
重载,重写,隐藏的对比:
3. 抽象类(纯虚函数)
在虚函数的后面写上= 0,则整个函数为纯虚函数(纯虚函数没有函数体),包含纯虚函数的类也被称之为抽象类,不能实例化对象。
class Car
{
public:
virtual void Drive() = 0;
};
3. 多态的原理
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
};
sizeof(Base)=4;
这也就意味着在类中,存放着一个_vfptr
指针放在对象的前面,我们将这个指针叫做虚函数表指针。一个含有虚函数的类中都至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表(虚表)中。
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;
};
void test()
{
Base b;
Derive d;
Base& rb = b;
Base& rd = d;
rb.Func1();
rd.Func1();
rd.Func2();
d.Func1();
}
4. 单继承和多继承的虚函数表(重要)
- 单继承状态下的虚表
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;
};
2. 多继承中的虚函数表
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;
};
//定义 函数指针: void 函数()
typedef void(*vfPtr)();
void doVF(vfPtr* vftable)
{
cout << "虚表地址:" << vftable << endl;
//nullptr: 结束
for (int i = 0; vftable[i] != nullptr; ++i)
{
//获取当前虚表位置的函数指针
vfPtr func = vftable[i];
//执行指向的函数
func();
}
}
void test()
{
Base1 b;
Base2 b2;
Derive d;
cout << "Base1:" << &b << endl;
vfPtr* vftable = (vfPtr*)(*((int*)&b));
doVF(vftable);
cout << "Base2:" << &b2 << endl;
vftable = (vfPtr*)(*((int*)&b2));
doVF(vftable);
//访问Derive的第一个虚表
vftable = (vfPtr*)(*((int*)&d));
cout << "Derive first vftable:" << &d << endl;
doVF(vftable);
//访问Derive的第二个虚表: 地址偏移
vftable = (vfPtr*)(*((int*)((char*)&d + sizeof(Base1))));
cout << "Derive second vftable:" << (int*)((char*)&d + sizeof(Base1)) << endl;
doVF(vftable);
}
总结:
上一篇: Java面试通关要点汇总集和答案