欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 健康 > 美食 > C++继承

C++继承

2025/2/23 14:31:18 来源:https://blog.csdn.net/Small_entreprene/article/details/141546591  浏览:    关键词:C++继承

概念: 

继承(inheritance)机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有类特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称⼦类(派生类)。继承呈现了⾯向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复⽤,继承是类设计层次的复⽤。
下⾯我们看到没有继承之前我们设计了两个类Student和Teacher,Student和Teacher都有姓名/地址/电话/年龄等成员变量,都有identity⾝份认证的成员函数,设计到两个类⾥⾯就是冗余的。当然他们也有⼀些不同的成员变量和函数,⽐如⽼师独有成员变量是职称,学⽣的独有成员变量是学号;学⽣的独有成员函数是学习,⽼师的独有成员函数是授课:
#include<iostream>
#include<string>
using namespace std;
class Student
{
public:// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证void identity(){// ...}// 学习void study(){// ...}
protected:string _name = "peter"; // 姓名string _address; // 地址string _tel; // 电话int _age = 18; // 年龄int _stuid; // 学号
};
class Teacher
{
public:// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证void identity(){// ...}// 授课void teaching(){//...}
protected:string _name = "张三"; // 姓名int _age = 18; // 年龄string _address; // 地址string _tel; // 电话string _title; // 职称
};
int main()
{return 0;
}

学生和老师的信息就很多重复,很冗余,下⾯我们公共的成员都放到Person类中,Student和teacher都继承Person,就可以复⽤这些成员,就不需要重复定义了,省去了很多⿇烦:(Person作为父类(基类),student/teacher作为子类(派生类))(将公共信息提取出来到父类)

定义格式:

下⾯我们看到Person是⽗类,也称作基类。Student是⼦类,也称作派⽣类。(因为翻译的原因,所以既叫⽗类/⼦类,也叫基类/派生类)

 

其实继承方式和访问限定符有着紧密关系:

两两对应的效果:
 

类成员/继承方式public继承protected继承private继承
父类的public成员子类的public成员子类的protected成员子类的private成员
父类的protected成员子类的protected成员子类的protected成员子类的private成员
父类的private成员在子类中不可见在子类中不可见在子类中不可见

  1. ⽗类private成员在⼦类中⽆论以什么⽅式继承都是不可⻅的。这⾥的不可⻅是指⽗类的私有成员还是被继承到了⼦类对象中,但是语法上限制⼦类对象不管在类⾥⾯还是类外⾯都不能去访问它;
  2. ⽗类private成员在⼦类中是不能被访问,如果⽗类成员不想在类外直接被访问,但需要在⼦类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的;
  3. 实际上⾯的表格我们进⾏⼀下总结会发现,⽗类的私有成员在⼦类都是不可⻅。⽗类的其他成员在⼦类的访问⽅式 == Min(成员在⽗类的访问限定符,继承⽅式),public > protected > private;
  4. 使⽤关键字class时默认的继承⽅式是private,使⽤struct时默认的继承⽅式是public,不过最好显⽰的写出继承⽅式;
  5. 在实际运⽤中⼀般使⽤都是public继承,⼏乎很少使⽤protetced/private继承,也不提倡使⽤protetced/private继承,因为protetced/private继承下来的成员都只能在⼦类的类⾥⾯使⽤,实际中扩展维护性不强;

简单来说:
  • 权限大小就是取两者的权限最小的状态;
  • 都继承下来到子类了,只是权限访问的问题;
  • 父类的private成员,在子类的类里类外都不可使用,但是protect可以在子类中使用(权限受继承方式的影响);
class Student : Person
//不写继承方式,class默认为private继承
struct Student : Person
//不写继承方式,struct默认为public继承

因此,我们可以继承类模板来实现类似一个容器的适配,只不过是以继承的方式:

#include<iostream>
#include<vector>
using namespace std;namespace rose
{//template<class T>//class vector//{};// stack和vector的关系,既符合is-a,也符合has-atemplate<class T>class stack : public std::vector<T>{public:void push(const T& x){// ⽗类是类模板时,需要指定⼀下类域,// 否则编译报错:error C3861: “push_back”: 找不到标识符// 因为stack<int>实例化时,也实例化vector<int>了// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到vector<T>::push_back(x);//push_back(x);}void pop(){vector<T>::pop_back();}const T& top(){return vector<T>::back();}bool empty(){return vector<T>::empty();}};
}
int main()
{rose::stack<int> st;st.push(1);st.push(2);st.push(3);while (!st.empty()){cout << st.top() << " ";st.pop();}return 0;
}

父类和子类对象的复制兼容转换:

  • public继承的⼦类对象可以赋值给⽗类的对象 / ⽗类的指针 / ⽗类的引⽤。这⾥有个形象的说法叫切⽚或者切割。寓意把⼦类中⽗类那部分切来赋值过去。

注意:一定是在公有继承下才有切片这一说法:

class Person
{
protected :
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public :
int _No ; // 学号
};
int main()
{
Student sobj ;
// 1.⼦类对象可以赋值给⽗类对象/指针/引⽤
Person pobj = sobj ;
Person* pp = &sobj;
Person& rp = sobj;
//2.⽗类对象不能赋值给⼦类对象,这⾥会编译报错
sobj = pobj;
return 0;
}

需要注意的是:不同对象间的赋值是隐私类型转换(如果是支持的情况下)但是赋值兼容转换是可以看作是不走产生临时变量(隐私类型转换)的,我们可以通过以下代码知晓结果:

Person& rp = sobj;//并没有像临时变量带来的常性的报错,相当于从子类切出来对应的父类的引用int i = 1;
double d = i;
double& rd = i;//临时变量具有常性,相当于被const修饰,相当于权限放大,报错
const double& rd = i;//这才是正确的写法

赋值兼容转换是一种特殊的转换(并没有像临时变量带来的常性的报错,相当于从子类切出来对应的父类的引用),我们可以看成是C++规定的;

  • ⽗类对象不能赋值给⼦类对象
//父类对象不能赋值给子类对象,这里会编译报错
sobj = (Student)pobj;
  • ⽗类的指针或者引⽤可以通过强制类型转换赋值给⼦类的指针或者引⽤。但是必须是⽗类的指针是指向⼦类对象时才是安全的。这⾥⽗类如果是多态类型,可以使⽤RTTI(Run-Time Type Information)的dynamic_cast 来进⾏识别后进⾏安全转换。
//pp作为父类的指针,有可能指向父类对象,有可能本身是指向子类对象,子类对象的话,转为子类对象很正常,但是编译器本身又不知道是指向父类还是子类,所以要放开这个(指针·引用)口子
Student* ps = (Student*)pp;
//dynamic_cast<>转换是安全的,指向的是子类就转换成功,是指向父类就转换失败
Student* ps = dynamic_cast<Student*>pp;//对于此Person,还要要求Person是多态类型
  1. pp作为父类的指针,有可能指向父类对象,有可能本身是指向子类对象,子类对象的话,转为子类对象很正常,但是编译器本身又不知道是指向父类还是子类,所以要放开这个(指针·引用)口子
  2. dynamic_cast<>转换是安全的,指向的是子类就转换成功,是指向父类就转换失败
  3. 对于此Person,还要要求Person是多态类型

继承中的作用域:

  • 在继承体系中⽗类和⼦类都有独⽴的作⽤域。
  • ⼦类和⽗类中有同名成员,⼦类成员将屏蔽⽗类对同名成员的直接访问,这种情况叫隐藏。(在⼦类成员函数中,可以使⽤ ⽗类::⽗类成员 显⽰访问)
  • 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。(区分:函数重载要求在同一个作用域)
  • 注意在实际中在继承体系⾥⾯最好不要定义同名的成员。
// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是⾮常容易混淆
class Person
{
protected:string _name = "小李子"; // 姓名int _num = 111; // ⾝份证号
};
class Student : public Person
{
public:void Print(){cout << "姓名:" << _name << endl;cout << "学号:" << _num << endl;//999cout << "学号:" << Person::_num << endl;//111}
protected:int _num = 999; // 学号//由于父类中也有 int _num,导致在派生类这个体系中有两个同名变量
};
int main()
{Student s1;s1.Print();return 0;
};

子类的默认成员函数:

默认生成的构造函数的行为

  1. 内置类型->不确定
  2. 自定义类型->调用默认构造
  3. 继承的父类成员看做是一个整体对象,要求调用父类的默认构造

以下是一个比较完整的体系,用于展现下列解决方案:

#include<iostream>
#include<string>
using namespace std;class Person
{
public:Person(const char* name = "peter"): _name(name){cout << "Person()" << endl;}Person(const Person& p): _name(p._name){cout << "Person(const Person& p)" << endl;}Person& operator=(const Person& p){cout << "Person operator=(const Person& p)" << endl;if (this != &p)_name = p._name;return *this;}~Person(){cout << "~Person()" << endl;}
protected:string _name = "rose"; // 姓名
};
class Student : public Person
{
public://默认生成的构造函数的行为//1.内置类型->不确定//2.自定义类型->调用默认构造//3.继承的父类成员看做是一个整体对象,要求调用父类的默认构造
protected:int _num = 100; //学号string _address="玫瑰花海"; //地址
};
int main()
{Student s;return 0;
}


  • ⼦类的构造函数必须调⽤⽗类的构造函数初始化⽗类的那⼀部分成员。如果⽗类没有默认的构造函数,则必须在⼦类构造函数的初始化列表阶段显⽰调⽤
Student(const char* name, int num, const char* address): Person(name), _num(num), _address(address)
{cout << "Student()" << endl;
}

  • ⼦类的拷⻉构造函数必须调⽤⽗类的拷⻉构造完成⽗类的拷⻉初始化。

1.对于内置类型,拷贝构造完成的是直拷贝

2.对于自定义类型,调用本身的拷贝构造

3.对于父类,调用父类的拷贝构造

所以说对于Student,默认的拷贝构造已经足够了,换种方向:Student的拷贝构造不需要我们自行实现,默认生成的就够用了。如果在有需要深拷贝的资源,我们才需要自己实现:(也就是将父类看成是自定义类型的成员)

也可以写出来:

Student(const Student& s): Person(s)//显示调用父类的拷贝构造,需要传父类对象,可是没有父类对象,这时候赋值兼容规则就体现出来了://s是子类对象的引用,将父类对象拿出来,把子类对象传给父类的引用,父类的引用,引用的是切出来的子类的一部分, _num(s._num), _address(s._address)
{cout << "Student(const Student& s)" << endl;//深拷贝//...
}
  • ⼦类的operator=必须要调⽤⽗类的operator=完成⽗类的复制。需要注意的是⼦类的operator=隐藏了⽗类的operator=,所以显⽰调⽤⽗类的operator=,需要指定⽗类作⽤域

与拷贝构造一样,对于Student严格来说赋值重载默认生成的就够用了,不需要自己写。如果有需要深拷贝的资源,才需要自己实现。

也可以写出来:

Student& operator = (const Student& s)
{cout << "Student& operator= (const Student& s)" << endl;if (this != &s){// 构成隐藏,所以需要显⽰调⽤,不然会一直调用子类的operator=(),导致栈溢出Person::operator =(s);_num = s._num;_address = s._address;}return *this;
}

  • ⼦类的析构函数会在被调⽤完成后⾃动调⽤⽗类的析构函数清理⽗类成员。因为这样才能保证⼦类对象先清理⼦类成员再清理⽗类成员的顺序。

对于Student这个子类的析构也是不需要我们自行实现,因为没有资源的释放。

也可以写出来:

~Student()
{//有资源要释放cout << "~student()" << endl;
}
  • ⼦类对象初始化先调⽤⽗类构造再调⼦类构造。
  • ⼦类对象析构清理先调⽤⼦类析构再调⽗类的析构。
  • 因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同,那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以⽗类析构函数不加virtual的情况下,⼦类析构函数和⽗类析构函数构成隐藏关系。(析构函数都会被处理成destructor(),注意:是所有的析构函数,不管是不是继承关系
~Student()
{~Student()
{//析构是可以显式调用的//~Person();//调不动,因为子类的析构和父类的析构构成隐藏//所以要指定类域Person::~Person();
}

但是,这里不需要显式调用,子类析构函数之后,会自动调用父类析构,上面的代码会重复析构,如果有资源的析构,就会导致资源的多次析构,造成类似野指针的解引用错误,其他的对象(string·int)是显式调用,对于继承关系的子类就会自动调用父类的析构,这里可以看成是一个规定。(保证先子后父,如果显式调用,就取决于实现者,不能保证先子后父)

final关键字:

对于要实现一个不能被继承的类,我们有两种方法

  • ⽗类的构造函数私有,⼦类的构成必须调⽤⽗类的构造函数,但是⽗类的构成函数私有化以后,⼦类看不⻅就不能调⽤了,那么⼦类就⽆法实例化出对象;

子类的构造函数不管是我们自动生成,还是自己实现,都需要调用父类的成员,但是父类的私有成员在子类中是不可见的,不足的地方是不够明显,在不定义的时候不会报错:

class Base 
{
public:void func5() { cout << "Base::func5" << endl; }
protected:int a = 1;
private:// C++98的⽅法Base(){}
};
class Derive :public Base
{void func4() { cout << "Derive::func4" << endl; }
protected:
};
int main()
{//不足之处:不定义不报错//Derive d;return 0;
}
  • C++11新增了⼀个final关键字,final修改⽗类,⼦类就不能继承了;
// C++11的⽅法
class Base final
{
public:void func5() { cout << "Base::func5" << endl; }
protected:int a = 1;
private:
};
class Derive :public Base//直接报错
{void func4() { cout << "Derive::func4" << endl; }
protected:
};
int main()
{Derive d;return 0;
}

继承与友元:

友元关系不能继承,也就是说⽗类友元不能访问⼦类私有和保护成员 。(形象一点的就是对子类说:我是你爹的朋友,又不是你的朋友)

class Student;
class Person
{
public:friend void Display(const Person& p, const Student& s);
protected:string _name; // 姓名
};
class Student : public Person
{
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;// 编译报错:error C2248: “Student::_stuNum”: ⽆法访问 protected 成员// 解决⽅案:Display也变成Student 的友元即可Display(p, s);return 0;
}

继承与静态成员:

⽗类定义了static静态成员,则整个继承体系⾥⾯只有⼀个这样的成员。⽆论派⽣出多少个⼦类,都只有⼀个static成员实例。(也就是说继承下来后,在子类还是原本的static成员实例,并不是产生新的static成员)
class Person
{
public:string _name;static int _count;
};
int Person::_count = 0;
class Student : public Person
{
protected:int _stuNum;
};
int main()
{Person p;Student s;// 这⾥的运⾏结果可以看到⾮静态成员_name的地址是不⼀样的// 说明⼦类继承下来了,⽗⼦类对象各有⼀份cout << &p._name << endl;cout << &s._name << endl;// 这⾥的运⾏结果可以看到静态成员_count的地址是⼀样的// 说明⼦类和⽗类共⽤同⼀份静态成员cout << &p._count << endl;cout << &s._count << endl;// 公有的情况下,⽗⼦类指定类域都可以访问静态成员cout << Person::_count << endl;cout << Student::_count << endl;return 0;
}

在具体实例中也就是: Person对象有一个_name,Student继承下来也有一个_name,但是这两个_name不是同一个_name(普通成员继承下来是不同的),而static的成员继承下来是相同的;

继承方式:

  • 单继承:⼀个⼦类只有⼀个直接⽗类时称这个继承关系为单继承:

  • 多继承:⼀个⼦类有两个或以上直接⽗类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的⽗类在前⾯,后⾯继承的⽗类在后⾯,⼦类成员在放到最后⾯:

  • 菱形继承:菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以看出菱形继承有数据冗余和⼆义性的问题,在Assistant的对象中Person成员会有两份。⽀持多继承就⼀定会有菱形继承,像Java就直接不⽀持多继承,规避掉了这⾥的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的

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
{//Student有一份Person的信息,Teacher也有一份Person的信息,意味着Assistant存在俩份Person得信息,这也就造成了空间的浪费
protected:string _majorCourse; // 主修课程
};int main()
{// 编译报错:error C2385: 对“_name”的访问不明确,二义性,分不清楚到底是哪个的_nameAssistant a;a._name = "peter";// 需要显⽰指定访问哪个⽗类的成员可以解决⼆义性问题,但是数据冗余问题⽆法解决a.Student::_name = "玫瑰花茶";a.Teacher::_name = "茉莉花茶";//存两份造成的数据冗余cout << sizeof(a) << endl;return 0;
}
  • Student有一份Person的信息,Teacher也有一份Person的信息,意味着Assistant存在俩份Person得信息,这也就造成了空间的浪费;
  • 对a的_name访问不明确,二义性,分不清楚到底是哪个的_name;
  • 需要显⽰指定访问哪个⽗类的成员可以解决⼆义性问题,但是数据冗余问题⽆法解决;

虚继承:

我们可以参考库里面的IO流的实现:

其实在io流的实现中是使用到了菱形继承,那当让也就有了对菱形继承带来的问题的解决方式:

虚继承的格式就是在继承导致数据冗余和二义性的(注意位置)父类前加上一个关键字:virtual

使用虚继承可以解决数据冗余和二义性:

class Person
{
public:string _name; // 姓名
};
// 使⽤虚继承Person类
class Student : virtual public Person
{
protected:int _num; //学号
};
// 使⽤虚继承Person类
class Teacher : virtual public Person
{
protected:int _id; // 职⼯编号
};
// 教授助理
class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修课程
};
int main()
{// 使⽤虚继承,可以解决数据冗余和⼆义性Assistant a;a._name = "peter";return 0;
}

注意点:在没有进行虚继承(还没有virtual修饰时):子类与父类间的关系是包含的:

这里就涉及到了切片的原理,带来了偏移的指向:

class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main()
{Derive d;Base1* p1 = &d;Base2* p2 = &d;	Derive* p3 = &d;
return 0;
}

如果程序进行了虚继承(virtual修饰): Person不包含在Student/Teacher里面,将其放在公共区域:

所以在对Assistant的实现构造函数时,显示调用的Student与Teacher的构造函数不行,需要显示调用Person的构造函数:(假设这三个类中都显示写了构造函数)

class Person
{
public:Person(const char* name):_name(name){}string _name; // 姓名
};
// 使⽤虚继承Person类
class Student : virtual public Person
{
public:Student(const char* name, int num = 0):Person(name){}
protected:int _num; //学号
};
// 使⽤虚继承Person类
class Teacher : virtual public Person
{
public:Teacher(const char* name, int id = 0):Person(name){}
protected:int _id; // 职⼯编号
};
// 教授助理
class Assistant : public Student, public Teacher
{Assistant(const char* name1, const char* name2, const char* name3, const char* _majorCourse = "math"): Student(name1), Teacher(name2), Person(name3)//可以编译看看,Student/Teacher的Person(name)是不走的,最终的是name3,也就是Person直接的初始化{}
protected:string _majorCourse; // 主修课程
};

在io流中也是使用了此关键字达到解决数据冗余和二义性的问题(库中的原码)

template<class CharT, class Traits = std::char_traits<CharT>>
class basic_ostream : virtual public std::basic_ios<CharT, Traits>
{};
template<class CharT, class Traits = std::char_traits<CharT>>
class basic_istream : virtual public std::basic_ios<CharT, Traits>
{};

继承和组合:

  • public继承是⼀种is-a的关系。也就是说每个⼦类对象都是⼀个⽗类对象。
  • 组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象。
  • 继承允许你根据⽗类的实现来定义⼦类的实现。这种通过⽣成⼦类的复⽤通常被称为⽩箱复用(white-box reuse)。术语“⽩箱”是相对可视性⽽⾔:在继承⽅式中,⽗类的内部细节对⼦类可⻅。继承⼀定程度破坏了⽗类的封装,⽗类的改变,对⼦类有很⼤的影响。⼦类和⽗类间的依赖关系很强,耦合度⾼。
  • 对象组合是类继承之外的另⼀种复⽤选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接⼝。这种复⽤⻛格被称为⿊箱复⽤(black-box reuse),因为对象的内部细节是不可⻅的。对象只以“⿊箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使⽤对象组合有助于你保持每个类被封装。
  • 优先使⽤组合,⽽不是继承。实际尽量多去⽤组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承(is-a)那就⽤继承,另外要实现多态,也必须要继承。类之间的关系既适合⽤继承(is-a)也适合组合(has-a),就⽤组合。
  • 很多⼈说C++语法复杂,其实多继承就是⼀个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂,性能也会有⼀些损失,所以最好不要设计出菱形继承。多继承可以认为是C++的缺陷之⼀,后来的⼀些编程语⾔都没有多继承,如Java。
// Tire(轮胎)和Car(⻋)更符合has-a的关系
class Tire {
protected:string _brand = "Michelin"; // 品牌size_t _size = 17; // 尺⼨
};
class Car {
protected:string _colour = "⽩⾊"; // 颜⾊string _num = "陕ABIT00"; // ⻋牌号Tire _t1; // 轮胎Tire _t2; // 轮胎Tire _t3; // 轮胎Tire _t4; // 轮胎
};
class BMW : public Car {
public:void Drive() { cout << "好开-操控" << endl; }
};
// Car和BMW/Benz更符合is-a的关系
class Benz : public Car {
public:void Drive() { cout << "好坐-舒适" << endl; }
};
template<class T>
class vector
{};
// stack和vector的关系,既符合is-a,也符合has-a
template<class T>
class stack : public vector<T>
{};
template < class T>
class stack
{
public:vector<T> _v;
};int main()
{return 0;
}

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词