【C++第十五章】继承
定义🧐
继承方式和访问限定符🧐
继承方式和访问限定符都有三种,public、protected以及private,其中public和private在之前就介绍过,如果是protected那么只有子类能访问,外界无法访问。子类继承方式表示继承的类是什么属性,子类继承方式不写默认为私有。
父类private成员在子类中无论以什么方式继承都是不可见的,其含义是私有成员还是被继承到子类中,但是在语法上了派生类对象不管在类里面还是类外面都无法访问。
继承方式大多以public为主,并且不提倡使用protected和private继承,因为继承下来的成员都只能在子类的类里面使用,实际中维护性不强。
父子类赋值兼容规则🧐
公有继承被看做相近类型,可以进行隐式类型转换。
隐式类型转换会生成临时变量,但是在公有继承下,父类与子类是is-a(你就是我)的关系,子类对象赋值给父类对象/父类指针/父类引用,我们认为是天然的,中间不产生临时对象,称为父子类赋值兼容规则(也叫切割/切片)。
父子成员同名🧐
当父类和子类都有同一个成员时,会优先使用自己的成员,如果我们要使用父类成员可以加上父类的域。由此得知,继承中同名函数会构成隐藏,不管参数和返回值,所以尽量不要使用同名函数。
子类的默认成员函数🧐
我们以下面代码为例:
class Person
{
public:
Person(const char* name, int age)
:_name(name)
,_age(age)
{
cout << "Person构造函数" << endl;
}
Person(const Person& p)
:_name(p._name)
,_age(p._age)
{
cout << "Person拷贝构造" << endl;
}
Person& operator=(const Person& p)
{
cout << "Person赋值重载" << endl;
if (&p != this)
{
_name = p._name;
_age = p._age;
}
return *this;
}
void Print()
{
cout << _age << " " << _name << endl;
}
~Person()
{
cout << "Person析构函数" << endl;
}
protected:
int _age = 18;
string _name = "Mick";
};
class Teacher : public Person
{
public:
void Print()
{
cout << Person::_age << endl;
}
protected:
int _age = 10;
int _jobid = 0;
};
子类不写默认构造函数,那么会去调用父类的默认构造函数。
如果要显示初始化子类,且要用父类成员时,需要把父类当成完整对象,复用父类成员完成初始化。
拷贝构造需要在子类中取到父类成员,我们对子类切片即可。
同理,赋值重载也是这种方法。
子类析构要特殊一点,因为多态的存在,析构函数名会被统一处理成destructor,所以父子类析构函数会构成隐藏,当我们调用父类析构时需要加上域。并且,构造时是先构造父类再构造子类,析构则是先子后父,原因在于父类先析构了,但子类依然能够访问父类,那么就会存在风险,所以要先释放子类,父类访问不了子类,则不存在该风险,编译器为了确保安全,所以只需要析构子类,父类会自动帮我们析构。
完整代码如下,可以自己调试学习:
#include<iostream>
using namespace std;
class Person
{
public:
Person(const char* name, int age)
:_name(name)
,_age(age)
{
cout << "Person构造函数" << endl;
}
Person(const Person& p)
:_name(p._name)
,_age(p._age)
{
cout << "Person拷贝构造" << endl;
}
Person& operator=(const Person& p)
{
cout << "Person赋值重载" << endl;
if (&p != this)
{
_name = p._name;
_age = p._age;
}
return *this;
}
void Print()
{
cout << _age << " " << _name << endl;
}
~Person()
{
cout << "Person析构函数" << endl;
}
protected:
int _age = 18;
string _name = "Mick";
};
class Teacher : public Person
{
public:
Teacher(const char* name,int age,int id)
:Person(name,age)
,_age(age)
,_jobid(id)
{
cout << name << " " << _age << " " << _jobid << endl;
}
Teacher(const Teacher& t)
:Person(t) //切片
,_age(t._age)
,_jobid(t._jobid)
{
cout << "Teacher拷贝构造" << endl;
}
Teacher& operator=(const Teacher& s)
{
if (&s != this)
{
Person::operator=(s); //这里要指定,不然会发生隐藏
_jobid = s._jobid;
_age = s._age;
}
return *this;
}
//由于多态原因,析构函数统一会被处理成destructor
//父子类的析构函数构成隐藏
//为了保证析构安全,先子后父
//父类析构函数不需要显示调用,子类析构函数会自动调用父类
~Teacher()
{
// Person::~Person();
cout << "Teacher析构" << endl;
}
void Print()
{
cout << _name << " " << _age << " " << _jobid << endl;
}
private:
protected:
int _age = 10;
int _jobid = 0;
};
int main()
{
Teacher t("张三",18,20023030);
Teacher t1("李四", 10, 20043030);
t = t1;
t.Print();
return 0;
}
关键字final🧐
在C++11之前,我们想要一个类不能被继承,可以将构造函数私有化,而在C++11后,我们可以在父类加上final来禁止继承。
静态成员继承🧐
静态成员继承的是使用权,它存在静态区中,属于整个类。
多继承🧐
多继承格式如下:
class student
{
//成员
};
class Teacher
{
//成员
};
//多继承格式
class Assistant : public student, public Teacher
{
//子类成员
}
但是在C++中可能会出现菱形继承,assistant会拥有两份person成员。
菱形继承不仅会造成代码冗余,并且会出现二义性,编译器无法识别我们想调用哪个类的成员,必须要加上类域才能使用。
解决方法是在冗余的继承类加上virtual(虚继承)
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;
}
我们在内存窗口中看一下对象d的存储情况,发现了数据冗余,A出现了两次。
组合与继承🧐
如下代码,B类和D类大小一样,但一个是继承一个是组合,区别在于继承权限更大,组合只能使用公有的成员,且在类外不能直接调用成员函数,从可维护性来看,组合更好,因为组合依赖关系不强,耦合度低,有助于保持每个类被封装,但从便利角度来看,继承更好用。
#include<iostream>
using namespace std;
class A
{
public:
void func()
{
}
protected:
int _a;
};
class B : public A
{
public:
void f()
{
func(); //直接调用
_a = 1; //可以访问_
}
protected:
int _b;
};
class C
{
public:
void func()
{
}
protected:
int _c;
};
class D
{
public:
void f()
{
_cc.func(); //间接调用
_c = 1; //不可以访问
}
protected:
int _d;
C _cc;
};
int main()
{
B bb;
D dd;
bb.func(); //可以调用
dd.func(); //无法调用
return 0;
}
小试牛刀🧐
题目一:
题目二:
答案:D的构造函数会先走初始化列表,而继承顺序决定声明顺序,并且这里是虚继承(如果没有虚继承A会调用两次,A自己本身不需要走初始化列表),A只调用一次,所以选A
总结🧐
结尾👍
以上便是继承的全部内容,如果有疑问或者建议都可以私信笔者交流,大家互相学习,互相进步!🌹