🏆个人主页:企鹅不叫的博客
🌈专栏
⭐️ 博主码云gitee链接:代码仓库地址
⚡若有帮助可以【关注+点赞+收藏】,大家一起进步!
💙系列文章💙
【初阶与进阶C++详解】第二篇:C&&C++互相调用(创建静态库)并保护加密源文件
【初阶与进阶C++详解】第三篇:类和对象上(类和this指针)
【初阶与进阶C++详解】第四篇:类和对象中(类的六个默认成员函数)
【初阶与进阶C++详解】第五篇:类和对象下(构造+static+友元+内部类
【初阶与进阶C++详解】第六篇:C&C++内存管理(动态内存分布+内存管理+new&delete)
【初阶与进阶C++详解】第七篇:模板初阶(泛型编程+函数模板+类模板+模板特化+模板分离编译)
【初阶与进阶C++详解】第八篇:string类(标准库string类+string类模拟实现)
【初阶与进阶C++详解】第十一篇:stack+queue+priority_queue
【初阶与进阶C++详解】第十二篇:模板进阶(函数模板特化+类模板特化+模板分离编译)
文章目录
💎一、继承的概念和定义
🏆1.继承概念
继承:是面向对象的特性之一,继承可以理解成是类级别的一个复用,允许我们在原有类的基础上进行扩展,增加新的功能。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程,继承是类设计层次的复用
//用法
class [派生类名] : [继承方式] [基类名]
//实例
class Person
{
public:
string _name;
int _age;
};
//class [派生类名] : [继承方式] [基类名]
class Student : public Person
{
protected:
string _stuNum;
};
🏆2.继承方式
继承方式有三种:public继承, protected继承, private继承
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 派生类中不可见 | 派生类中不可见 | 派生类中不可见 |
总结:
- 基类的private成员在派生类中都是不可见的,这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
- 基类成员在父类中的访问方式=min(成员在基类的访问限定符,继承方式),public>protected>private。
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
💎二、基类和派生类对象赋值转换
- 派生类对象会通过 切片的方式赋值给基类的对象、指针或引用,派生类赋值给基类会切掉多余的部分。
- 但是基类对象不能赋值给派生类对象。
- 基类的指针可以通过强制类型转换赋值给派生类的指针,但必须是基类的指针指向派生类的对象才是安全的。
class Person
{
public:
Person(const char* name = "")
:_name(name)
{}
void Print()
{
cout << "name:" << _name << " age:" << _age << endl;
}
protected:
string _name = "";
int _age = 1;
};
class Student : public Person
{
public:
Student()
:Person("xiaoming")
{}
void Print()
{
cout << "name:" << _name << " age:" << _age << " _stuid:" << _stuid << " _major:" << _major << endl;
}
private:
int _stuid = 0;// 学号
int _major = 0;// 专业
};
int main()
{
Student s;
// 子类对象可以赋值给父类的对象、指针和引用 反过来不行
//如果把父类对象赋值给子类引用,子类能调用父类方法,但父类方法可能是私有的
// Student对象通过 “切片” 的方式进行赋值
Person p1 = s;
Person* p2 = &s;
Person& p3 = s;
//s = p1; 基类对象不能赋值给派生类
// 基类的指针可以通过强制类型转换赋值给派生类的指针
Student* ps = (Student*)p2;
return 0;
}
派生类对象赋值给基类对象图示:剩下的_stuid就没了
派生类对象赋值给基类引用图示:剩下的_stuid就没了
派生类对象赋值给基类指针图示:指向学生
💎三、继承中的作用域
**隐藏:**也叫重定义,当基类和派生类中出现重名的成员时,派生类就会将基类的同名成员给隐藏起来,然后优先使用派生类的成员。
(但是隐藏并不意味着就无法访问,可以通过指明基类作用域来显式访问隐藏成员。)
class Person { public: Person(const char* name = "") :_name(name) {} void Print() { cout << "name:" << _name << " age:" << _age << endl; } protected: string _name = ""; int _age = 1; }; class Teacher : public Person { public: void Print() { cout << "name:" << _name << " age:" << _age << " jobid:" << _jobid << endl; } private: int _jobid = 0;// 工号 }; int main() { Teacher t; //第一个打印出来的是name : age:1 jobid:0 //优先访问派生类的成员 t.Print(); //第二个打印出来的是name : age:1 // 可以通过指定域作用限定符显示访问 //基类::基类成员 (显示访问) t.Person::Print(); return 0; }
在基类与派生类中,同名的方法并不能构成重载,因为处于不同的作用域中。而只要满足方法名相同,就会构成隐藏。
💎四、派生类的默认成员函数
在每一个类中,都会有6个默认的成员函数,这些函数即使我们自己不去实现,编译器也会帮我们实现。
class Person { public: //基类构造函数 Person(const char* name = "", int age = 1) :_name(name) ,_age(age) { cout << "Person()" << endl; } //基类拷贝构造 Person(const Person& p) :_name(p._name) , _age(p._age) { cout << "Person(const Person& p)" << endl; } //基类赋值 Person& operator=(const Person& p) { _name = p._name; _age = p._age; cout << "Person& operator=(const Person& p)" << endl; return *this; } void Print() { cout << "name:" << _name << " age:" << _age << endl; } //基类析构 ~Person() { cout << "~Person()" << endl; } protected: string _name; int _age; }; class Student : public Person { public: //派生类构造函数 Student(const char* name, int age, int stuid = 0) :Person(name, age)// 此处调用父类的构造函数来继承下来的成员进行初始化 , _stuid(stuid) { cout << "Student()" << endl; } //派生类拷贝构造 Student(const Student& s) :Person(s)// 子类对象可以传给父类的对象、指针或引用 ,_stuid(s._stuid) { cout << "Student(const Student& s)" << endl; } //派生类赋值 Student& operator=(const Student& s) { cout << "Student& operator=(const Student& s)" << endl; if (this != &s) { Person::operator=(s);// 先完成基类的赋值 _stuid = s._stuid; } return *this; } void Print() { cout << "name:" << _name << " age:" << _age << " _stuid:" << _stuid << endl; } //派生类析构 ~Student() { // 基类和派生类的析构函数的函数名都被编译器处理成了destruction,构成隐藏 //Person::~Person(); // 不需要显示调用 编译器会自动先调用派生类的析构函数,然后调用基类的析构函数 cout << "~Student()" << endl; } private: int _stuid;// 学号 }; void test1() { //构造函数优先基类 //拷贝构造优先基类 //赋值优先基类 //析构函数优先派生类 Student s("小明",18,10); Student s2(s1); }
总结:
- 子类的构造函数必须调用基类的构造函数初始化基类的那一部分成员,如果基类没有默认构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。Person(参数),Person::operator=(参数)
- 子类的拷贝构造必须代用父类的拷贝构造完成父类成员的拷贝。
- 子类的operator=必须调用基类的operator完成基类的赋值。
- 子类的析构函数会在被调用完成后自动调用基类的析构函数清理基类的成员。不需要显示调用。
- 子类对象会先调用父类的构造在调用子类的构造。
- 子类对象会先析构子类的析构再调用父类的析构。
如何设计一个不能被继承的类?
把该类的构造函数设为私有。如果基类的构造函数是私有,那么派生类不能调用基类的构造函数完成基类成员的初始化,则无法进行构造。所以这样设计的类不可以被继承。
💎五、继承的友元与静态成员函数
🏆1.友元
友元关系不能被继承。也就是说基类的友元不能够访问子类的私有和保护成员。
🏆2.静态成员函数
基类定义了static静态成员,无论继承了多少次,派生了多少子类,静态成员在这整个继承体系中有且只有一个。静态成员不再单独属于某一个类亦或者是某一个对象,而是属于这一整个继承体系。
下面函数输出结果是 3
class Person { public: Person() { ++_count; } // static成员存在于整个类 无论实例化出多少对象,都只有一个static成员实例 static int _count; }; int Person::_count = 0; class Student :public Person { public: int _stuid; }; int main() { Student s1; Student s2; Student s3; // Student()._count = 10; cout << "人数:" << Student()._count - 1 << endl; return 0; }
💎六、菱形继承及菱形虚拟继承
🏆1.单继承,多继承,虚拟继承
单继承: 一个子类只有一个直接父类时称这个继承关系为单继承。
多继承: 一个子类有两个或以上的直接父类时称这个继承关系为多继承。
菱形继承: 多继承的一种特殊情况,但是会带来数据冗余和二义性
🏆2.菱形虚拟继承
菱形虚拟继承可以解决菱形继承的二义性和数据冗余的问题 ,禁止信息通过中间件传递,在继承方式前加 virtual 的关键字即可。
class Person { public: string _name; }; // 不要在其他地方去使用。 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; // 主修课程 };
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 = 4; d._c = 5; d._d = 6; return 0; }
ABCD在内存中分布情况
没加virtual
加了virtual
A对象同时属于B和C,B和C中分别存放了一个指针,这个指针叫虚基表指针,分别指向的两张表,叫虚基表,虚基表中存的是相对于A的偏移量,B和C通过偏移量就可以找到公共空间(存放A对象的位置)。
🏆3.练习
上图是四个类的关系,abstr_emp是虚基类,manager和fink是虚基类,highfink是最后的虚子类,highfink 是 manager 和 fink 的子类,同时也间接是 abstr_emp 的子类
最开始的虚基类:包括无参构造,带参构造,析构函数,友元函数,其中析构,赋值重载,ShowAll和SetAll要变成虚函数class abstr_emp { private: string fname; // abstr_emp's first name string lname; // abstr_emp's last name string job; public: abstr_emp() :fname("none") ,lname("none") ,job("none") {} abstr_emp(const string & fn, const string & ln, const string & j) :fname(fn) ,lname(ln) ,job(j) {} virtual abstr_emp & operator=(const abstr_emp & e) { if (this != &e) { // 复制成员变量 fname = e.fname; lname = e.lname; job = e.job; } return *this; } virtual void ShowAll() const{ cout << fname << ", " << lname << ", " << job << endl; } // labels and shows all data virtual void SetAll(){ cout << "Enter fname: "; cin >> fname; cout << "Enter lname: "; cin >> lname; cout << "Enter job: "; cin >> job; } // prompts user for values friend ostream & operator<<(ostream & os, const abstr_emp & e){ os<< e.fname <<"-"<<e.lname<<"-"<<e.job; return os; } // just displays first and last name virtual ~abstr_emp(){} // virtual base class };
虚基类manager,注意对基类的构造和拷贝构造要在列表初始化完成,调用基类的赋值重载要指定作用域
class manager: virtual public abstr_emp { private: int inchargeof; // number of abstr_emps managed public: manager() :abstr_emp() ,inchargeof(0) {} manager(const std::string & fn, const std::string & ln, const std::string & j, int ico = 0) :abstr_emp(fn, ln, j) ,inchargeof(ico) {} manager(const abstr_emp & e, int ico) :abstr_emp(e) ,inchargeof(ico) {} manager(const manager & m) :abstr_emp(m) { inchargeof = m.inchargeof; } virtual ~manager(){} virtual manager & operator=(const manager & m) { if (this != &m) { // 调用基类的赋值运算符 abstr_emp::operator=(m); // 处理 manager 特有的成员 inchargeof = m.inchargeof; } return *this; } virtual void ShowAll() const{ abstr_emp::ShowAll(); cout << "In charge of: " << inchargeof << endl; } virtual void SetAll(){ abstr_emp::SetAll(); cout << "Enter inchargeof: "; cin >> inchargeof; } int InChargeOf() const { return inchargeof; } // output int & InChargeOf(){ return inchargeof; } // input };
第二个虚基类fink
class fink: virtual public abstr_emp { private: string reportsto; // to whom fink reports public: fink() :abstr_emp() ,reportsto("none") {} fink(const std::string & fn, const std::string & ln, const std::string & j, const std::string & rpo) :abstr_emp(fn, ln, j) ,reportsto(rpo) {} fink(const abstr_emp & e, const std::string & rpo) :abstr_emp(e) ,reportsto(rpo) {} fink(const fink & e) :abstr_emp(e) { reportsto = e.reportsto; } virtual ~fink(){} virtual fink & operator=(const fink & f) { if (this != &f) { // 调用基类的赋值运算符 abstr_emp::operator=(f); // 处理 fink 特有的成员 reportsto = m.reportsto; } return *this; } virtual void ShowAll() const{ abstr_emp::ShowAll(); cout << "reportsto: " << reportsto << endl; } virtual void SetAll(){ abstr_emp::SetAll(); cout << "Enter reportsto: "; cin >> reportsto; } const std::string ReportsTo() const { return reportsto; } std::string & ReportsTo(){ return reportsto; } };
两个虚基类的子类highfink,继承的两个虚基类在初始化时必须根据声明来,构造函数初始化是,要分别调用基类初始化和两个虚基类的初始化,在赋值运算符重载时为了避免重复赋值基类,所以不写基类赋值重载,调用基类函数必须加上定义域
class highfink: public manager, public fink // 根据声明顺序management必须在fink之前初始化 { public: highfink() :abstr_emp() ,manager() ,fink() {} highfink(const std::string & fn, const std::string & ln, const std::string & j, const std::string & rpo, int ico) :abstr_emp(fn, ln, j) , manager(fn, ln, j, ico) , fink(fn, ln, j, rpo) {} highfink(const abstr_emp & e, const std::string & rpo, int ico) :abstr_emp(e) ,manager(e, ico) ,fink(e, rpo) {} highfink(const fink & f, int ico) :abstr_emp(f) ,manager(f, ico) ,fink(f) {} highfink(const manager & m, const std::string & rpo) :abstr_emp(m) ,manager(m) ,fink(m,rpo) {} highfink(const highfink & h) :abstr_emp(h) , manager(h) , fink(h) {} virtual ~highfink() {} virtual highfink & operator=(const highfink & h) { if (this != &h) { //不写abstr_emp::operator=(h); //避免了重复赋值 abstr_emp 部分 manager::operator=(h); fink::operator=(h); // 如果 highfink 有自己的成员变量,在这里处理它们的赋值 // 例如: // member_variable = h.member_variable; } return *this; } virtual void ShowAll() const{ abstr_emp::ShowAll(); cout << "In charge of: " << InChargeOf() << endl; cout << "Reports to: " << ReportsTo() << endl; } virtual void SetAll(){ abstr_emp::SetAll(); cout << "Enter number in charge of: "; cin >> InChargeOf(); cout << "Enter reports to: "; cin >> ReportsTo(); } };
💎七、继承与组合
- 组合和继承都属于类层次的复用。
- public继承是一种is-a的关系。也就是说每个派生类都是基类分出来的一个子类。
- 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
- 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
- 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用,因为对象的内部细是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
- 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。