Hello大家好! 很高兴与大家见面! 给生活添点快乐,开始今天的编程之路。
我的博客:<但愿.
我的专栏:C语言、题目精讲、算法与数据结构、C++
欢迎点赞,关注
目录
一继承的概念及定义
1.1继承的概念
1.2继承的定义
1.2.1定义格式
1.2.2类继承基类方式改变对应成员访问⽅式的变化
1.2.3继承类模板【类继承类似】
二基类和派⽣类间的转换
2.1不同的转换方式
2.1.1会产生临时变量
2.1.2不会产生临时变量(基类和派⽣类间的转换)
2.1.2.1不会产生临时变量(基类和派⽣类间的转换)概念
2.1.2.2不会产生临时变量(基类和派⽣类间的转换)规则
三继承中的作⽤域
3.1隐藏规则
3.2考察继承作⽤域相关选择题
四派⽣类的默认成员函数
五实现⼀个不能被继承的类
5.1方法(两种)
六继承与友元、继承与静态成员
6.1继承与友元
6.2继承与静态成员
七多继承、菱形继承、虚继承
7.1多继承、菱形继承、虚继承的关系
7.2 继承模型
7.2.1单继承
7.2.2多继承
7.2.3菱形继承
7.3菱形继承问题(数据冗余和二义性)的解决办法
7.3.1二义性解决办法(1) —— 指定类域显示访问
7.3.2二义性解决办法(2)—— 虚拟继承
7.4多继承中指针偏移问题
7.5总结
八继承和组合
一继承的概念及定义
1.1继承的概念
//普通方式-设计一个学生类和一个老师类型 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; // 职称 }; //我们看到没有继承之前我们设计了两个类Student和Teacher,Student和Teacher都有姓名 / 地址 / //电话 / 年龄等成员变量,都有identity⾝份认证的成员函数,设计到两个类⾥⾯就是冗余的。 //继承方式把两个类中的公有成员提取出来,设计成一个类,在继承这个类 class Person { public: // 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证 void identity() { // ... } protected: string _name = "peter"; // 姓名 string _address; // 地址 string _tel; // 电话 int _age = 18; // 年龄 }; class Student :public Person { public: // 学习 void study() { // ... } protected: int _stuid; // 学号 }; class Teacher :public Person { public: // 授课 void teaching() { //... } protected: string _title; // 职称 };1.2继承的定义
1.2.1定义格式
1.2.2类继承基类方式改变对应成员访问⽅式的变化
继承和访问限定符分类如下:
继承基类的方式不同不然会导致,访问基类成员的方式不同,具体如下:
规则
1.基类private成员在派⽣类中⽆论以什么⽅式继承都是不可⻅的。这⾥的不可⻅是指基类的私有成员还是被继承到了派⽣类对象中,但是语法上限制派⽣类对象不管在类⾥⾯还是类外⾯都不能去访问它。(即无法在派生类中显示调用,可以通过在基类中设计一个公有函数进行访问)2.基类private成员在派⽣类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派⽣类中能访问,就定义为protected。保护成员限定符是因继承才出现的。3.基类的私有成员无论任何生成派生类在派⽣类都是不可⻅。基类的其他成员在派⽣类的访问⽅式 == Min(成员在基类的访问限定符,继承⽅式),public > protected > private。4.使⽤关键字class时默认的继承⽅式是private,使⽤struct时默认的继承⽅式是public,不过最好显⽰的写出继承⽅式。注意:在实际运⽤中⼀般使⽤都是public继承,⼏乎很少使⽤protetced/private继承,也不提倡使⽤protetced/private继承,因为protetced/private继承下来的成员都只能在派⽣类的类⾥⾯使⽤,实际中扩展维护性不强。
【示例】
//1基类中的private(私有成员)不管与什么继承方式都在派生类中不可见, // 但是不一定是不能使用,可以在基类中设计一个公有函数进行访问。例如父亲的私房钱虽然不可见,但是可以通过简介的方式得到(全部告诉妈妈)。 //2基类中的其他类型成员在派生类中的限制是,基类中成员变量的访问限定符和派生类的继承方式两种访问限定符种更小的一个 class Person { public: // 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证 void identity() { // ... } //为了满足基类中的私有成员可以在派生类中可见,在基类中设计一个公有函数进行访问。 void func() { ++_age; } protected: string _name = "peter"; // 姓名 string _address; // 地址 string _tel; // 电话 private: int _age = 18; // 年龄 }; class Student :public Person { public: // 学习 void study() { // ... _name = "张三"; } protected: int _stuid; // 学号 }; class Teacher :public Person { public: // 授课 void teaching() { //... //_age++;基类中的(private)私有成员,不管与哪种方式继承,都是在派生类中不可见 } protected: string _title; // 职称 }; int main() { Student s; Teacher t; //s._name;基类中的(protected)保护成员,通过tublic(公有继承)是派生类中的保护成员,在类外不可见,在类中可将 s.identity();//基类中的公有成员,通过tublic(公有继承)是派生类中的公有成员; s.func();//对基类中的私有成员进行访问; t.identity(); return 0; }1.2.3继承类模板【类继承类似】
【示例】实现栈以前是使用组合(一个类中有一个类,后面会讲)这里通过继承类模板来实现
//原来实现一个栈是一个组合(类中有一个类)后面会讲 template<class T,class Container> class Stack { private: Container _con; }; //继承-模板继承 //私有模板继承私有一个栈 namespace wzy { // template<class T> class stack : public std::vector<T>//如果展开了std就不用指定 { 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() { wzy::stack<int> st; st.push(1); st.push(2); st.push(3); while (!st.empty()) { cout << st.top() << " "; st.pop(); } return 0; }二基类和派⽣类间的转换
2.1不同的转换方式
2.1.1会产生临时变量
前面不同类型变量自己支持类型转换,并且之间会产生临时对象(由于临时对象具有常性所以一般用const来修饰转换的结果),例如int和double类型之间的转换是支持的,只是可能由于精度的原因值可能改变。
【实例】
int main() { //为什么i加引用,不加const会报错. //原因在于赋值转化时,首先会将变量d转化为int类型并将结果存在一个临时变量里 //所以这里引用绑定的对象实际是这个临时变量,所以必须加const,否则会编译报错。 double d = 1.2; //int& i = d; //error const int& i = d; return 0; }2.1.2不会产生临时变量(基类和派⽣类间的转换)
2.1.2.1不会产生临时变量(基类和派⽣类间的转换)概念
在基类和派生类对象之间的赋值转换并不会产生临时变量。派生类赋值给基类对象的指针或者基类对象的引用,我们认为这是天然的,中间不产生临时变量,这个叫做赋值兼容规则(或切割、切片)。
【验证】
class Person { protected: string _name; // 姓名 string _sex; // 性别 int _age; // 年龄 }; class Student : public Person { public: int _No; // 学号 }; int main() { Student sobj; //派⽣类对象可以赋值给基类的指针/引⽤,并且中间不会产生临时变量 //将派生类对象赋值给 基类的指针 / 基类的引⽤,从而可以得到派⽣类中基类(两者共有)那部分叫切⽚或者切割。 Person* pp = &sobj; Person& rp = sobj; return 0; }我们运行这代码发现可以正常通过,也从侧面说明基类和派生类对象之间的赋值转换不会产生临时变量。通过观察类pp和rp的成员变量也从侧面说明将派生类对象赋值给 基类的指针 / 基类的引⽤,从而可以得到派⽣类中基类那部分叫切⽚或者切割(但要注意在基类和派⽣类间的转换-建立在公有(public)继承的方式条件下)
2.1.2.2不会产生临时变量(基类和派⽣类间的转换)规则
•public继承(前提条件不是public继承没有这个之说)的派⽣类对象 可以赋值给 基类的指针 / 基类的引⽤。这⾥有个形象的说法叫切⽚或者切割。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分。•基类对象不能赋值给派⽣类对象。•基类的指针或者引⽤可以通过强制类型转换赋值给派⽣类的指针或者引⽤。但是必须是基类的指针是指向派⽣类对象时才是安全的。这⾥基类如果是多态类型,可以使⽤RTTI(Run-Time TypeInformation)的dynamic_cast 来进⾏识别后进⾏安全转换。(ps:这个我们后⾯类型转换章节再单独专⻔讲解,这⾥先提⼀下)
【示例】
//基类和派⽣类间的转换-建立在公有(public)继承的方式条件下 //将派生类对象赋值给 基类的指针 / 基类的引⽤,从而可以得到派⽣类中基类那部分叫切⽚或者切割。 class Person { protected: string _name; // 姓名 string _sex; // 性别 int _age; // 年龄 }; class Student : public Person { public: int _No; // 学号 }; int main() { Student sobj; // 1.派⽣类对象可以赋值给基类的指针/引⽤ Person* pp = &sobj; Person& rp = sobj; // 派⽣类对象可以赋值给基类的对象是通过调⽤后⾯会讲解的基类的拷⻉构造完成的 Person pobj = sobj; //2.基类对象不能赋值给派⽣类对象,这⾥会编译报错 //sobj = pobj; //像类型转化之间会产生临时对象,所以要用const修饰; //而将派生类对象赋值给基类的指针/基类的引⽤,虽然它们之间类型比特但是它们之间进行转换是不会产生临时对象 int i = 1; const double& d = i; return 0; }三继承中的作⽤域
继承中的作用域主要注意基类和派生类的成员变量中间可能会产生隐藏。
3.1隐藏规则
1.在继承体系中基类和派⽣类都有独⽴的作⽤域。2.派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。(在派⽣类成员函数中,可以使⽤ 基类::基类成员 显⽰访问)3.需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。 [注意和函数重载进行区分,函数重载要建立在同一作用域的前提下(不在同一作用域没有函数重载之说,基类和派生类形成成员函数隐藏,由于不同类属于不同作用域,所以函数隐藏是在不同作用域中),函数名相同而函数参数不同(而基类和派生类形成成员函数隐藏只需要函数名相同即可),才形成函数重载】注意:在实际中在继承体系⾥⾯最好不要定义同名的成员。
【实例】
//1成员变量形成隐藏,怎么访问基类中的成员变量-通过指定命名空间 class Person { protected: string _name = "wdefg"; // 姓名 int _num = 111; // ⾝份证号 }; class Student : public Person { public: void Print() { cout << "姓名:" << _name << endl; cout << "身份证号:" << Person ::_num<< endl;//要想访问基类中的_num成员变量,由于和派生类中的变量_num形成隐藏,所以要指定命名空间访问 cout << "学号:" <<_num<< endl;//就像我们前面讲的全局/和局部中有同名变量一样,先从局部中找局部没有才会到全局中找, //只有指定命名空间才会在其他地方查找(就近原则),这里也是默认在派生类中找,只有指定在基类才会在基类中查找 } protected: int _num = 999; // 学号 }; int main() { Student s1; s1.Print(); return 0; }; //成员函数形成隐藏,怎么访问基类中的成员函数-通过指定命名空间 // 注意基类和派生类之间成员函数之间形成隐藏与函数重载之间的区分 // 函数重载要建立在同一个作用域中(不在同一个作用域不用函数重载之说),并且保证函数参数不同; // 而基类和派生类之间成员函数之间形成隐,只需要保证函数名相同即可 class A { public: void fun() { cout << "func()" << endl; } }; class B :public A { public: void fun(int i) { cout << "func(int i)" << endl; } }; int main() { B b; b.fun(1);//访问派生类中的成员函数 b.A::fun();//访问基类中的成员函数 }3.2考察继承作⽤域相关选择题
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() { B b; b.fun(10); b.fun(); return 0; };3.2.1A和B类中的两个func构成什么关系()A. 重载 B. 隐藏 C.没关系//这里和函数重载进行区分即可:函数重载要建立在同一作用域的前提下(不在同一作用域没有函数重载之说,基类和派生类形成成员函数隐藏,由于不同类属于不同作用域,所以函数隐藏是在不同作用域中),函数名相同而函数参数不同(而基类和派生类形成成员函数隐藏只需要函数名相同即可),才形成函数重载。所以是B3.2.2上⾯程序的编译运⾏结果是什么()A. 编译报错 B. 运⾏报错 C. 正常运⾏//由于基类和派生类中的成员函数fun形成了隐藏,所以要访问基类中的成员函数要指定空间,而这里没有指定。所以是A
四派⽣类的默认成员函数
1.派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤。2.派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化3.派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。需要注意的是派⽣类的operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域4.派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。因为这样才能保证派⽣类对象先清理派⽣类成员再清理基类成员的顺序。5.派⽣类对象初始化先调⽤基类构造再调派⽣类构造。6.派⽣类对象析构清理先调⽤派⽣类析构再调基类的析构。7.因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同(这个我们多态章节会讲解)。那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系。总结:这里我们只有记住两点即可
1记住前面默认成员函数的规则
2把基类成员当成一个整体规则和以前的自定义类型规则一样
【示例】:(下面主要演示4个主流的默认成员函数,至于其他2个基本没用,这里主要讲自己手动实现)
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; // 姓名 }; class Student : public Person { //对于4大默认成员函数,其中默认构造函数一般都是自己手动写的, // 编译器自动生成的一般不能满足需求,当然你不想写,只要简单的实现也可以在声明的地方给缺省值 //而对于拷贝构造,赋值重载,析构函数,一般都是同时出现的,只有深拷贝是才要手动的写(有资源管理) //这里我们主要学习怎么写派生类中的4大默认成员函数,这里我们只需要把基类当成以前的自定义类型一样处理,规则和前面讲的一样 public: //1默认构造 //如果基类中有默认构造函数,和以前的自定义类型一样,就会自动调用对应的默认构造函数 //那如果要自己手动写,此时就要调用基类中的默认构造函数,那怎么调用呢?通过基类名(参数)调用 Student(const char* name, const char* address, int num) :Person(name)//自己手动写,此时就要调用基类中的默认构造函数,那怎么调用呢?通过基类名(参数)调 ,_address(address) ,_num(num) { } //拷贝构造 //如果基类中有拷贝构造函数,和以前的自定义类型一样,就会自动调用对应的构造函数 //那如果要自己手动写,此时就要调用基类中的拷贝构造函数,那怎么调用呢?通过基类名(参数)调用 //此时就有一个问题怎么传参了(怎么通过派生类得到基类中的成员),由于基类中的拷贝构造函数是一个引用,所以这里可以使用前面的切⽚ //将派生类对象赋值给 基类的指针 / 基类的引⽤,从而可以得到派⽣类中基类那部分。这也是为什么拷贝构造参数有引用的原因之一。 Student(const Student& s) :Person(s)//显⽰调⽤基类的拷贝构造函数,可以通过切⽚进行传参 ,_address(s._address) ,_num(s._num) {} //赋值重载 //如果基类中有拷贝函数,和以前的自定义类型一样,就会自动调用对应的赋值函数 //那如果要自己手动写,此时就要调用基类中的默认构造函数,那怎么调用呢? //由于基类和派生类中的赋值重载函数和基类中的赋值重载函数形成隐藏,所以显⽰调⽤基类的operator=,需要指定基类作⽤域 Student& operator=(const Student& s) { if (this != &s) { Person::operator=(s);//显⽰调⽤基类的operator=,需要指定基类作⽤域 _address = s._address; _num = s._num; } return *this; } ~Student() { //Person::~Person();//这里不显示调用基类的析构函数,由于编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以派⽣类析构函数和基类析构函数构成隐藏关系 //所以要调用基类的析构函数需要指定空间。而这里不显示调用基类的析构函数的原因是为了保证先子后父(由于基类中的成员可能在子类中会被用到,如果先析构父的就会出错)的析构顺序, // 编译器会在结束后在后面自动call基类的析构函数, 如果此时我们还显示调用就调用了两次就会出问题【核心还是编译器不放心程序员】 //delete[] _ptr; } protected: string _address = "李四"; int _num = 1; //学号 //资源管理 //int* _ptr = new int[10]; };五实现⼀个不能被继承的类
5.1方法(两种)
⽅法1:基类的构造函数私有,派⽣类的构成必须调⽤基类的构造函数,但是基类的构成函数私有化以后,派⽣类看不⻅就不能调⽤了,那么派⽣类就⽆法实例化出对象。⽅法2:C++11新增了⼀个final关键字,final修改基类,派⽣类就不能继承了。(现在一般用这种)
【实例】
// C++11的⽅法 class Base final { public: void func5() { cout << "Base::func5" << endl; } protected: int a = 1; private: // C++98的⽅法 /*Base() {}*/ };六继承与友元、继承与静态成员
6.1继承与友元
【实例】一个类外函数想同时访问基类和派生类种的成员变量。
// 前置声明 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; // 学号 }; //问题一如果将友元函数定义在基类中,此时找不到派生类;如果将派生类定义在基类之前此时派生类有找不到基类(这就是相互依赖关系)。 // 此时可以在基类前加一个派生类的声明,此时解决了问题1 //那怎么解决问题2怎么访问派生类中的私有成员,由于友元函数不能继承,所以只能在派生类中在定义成友元函数 void Display(const Person& p, const Student& s) { cout << p._name << endl;//问题1 //cout << s._stuNum << endl;//问题2 } int main() { Person p; Student s; // 编译报错:error C2248: “Student::_stuNum”: 无法访问 protected 成员 // 解决方案:Display也变成Student 的友元即可 Display(p, s); return 0; }6.2继承与静态成员
【实例】:(统计定义了多少个对象,都是继承关系)
【分析】:我们可以定义一个静态变量,由于是继承关系,所有对象的默认构造是都需要调用子类的默认构造来初始化对象中子类的成员。所以我们可以在最开始的类中显示的写默认构造函数 + 定义静态变量,同时每运行一次说明创建了一个对象,将静态变量++即可。
【代码】
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; Student s1; Person p1; cout << " 人数 :" << Person::_count << endl; Student s2; Student s3; cout << " 人数 :" << Person::_count << endl; return 0; }七多继承、菱形继承、虚继承
7.1多继承、菱形继承、虚继承的关系
继承分为单继承和多继承。而菱形继承是多继承的一直特殊情况,虚拟继承则是为了解决其中的一些问题孕育而生。
7.2 继承模型
7.2.1单继承:⼀个派⽣类只有⼀个直接基类时称这个继承关系为单继承
7.2.2多继承:
7.2.3菱形继承:
7.3菱形继承问题(数据冗余和二义性)的解决办法
【示例】
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 { protected: string _majorCourse; // 主修课程 };几个类之间的关系:
7.3.1二义性解决办法(1) —— 指定类域显示访问
虽然显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决。
a.Student::_name = "wzy"; a.Teacher::_name = "wzz";7.3.2二义性解决办法(2)—— 虚拟继承
在上面的继承关系中,在Student和Teacher的继承Person时使用虚拟继承(在继承方式前加virtual关键字),即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。
【实例】:
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 a; a._name = "peter"; }7.4多继承中指针偏移问题
下⾯说法正确的是( )
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; }7.5总结
八 继承和组合
【实例】
// 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; }本篇文章就到此结束,欢迎大家订阅我的专栏,欢迎大家指正,希望有所能帮到读者更好理解C++相关知识 ,觉得有帮助的还请三联支持一下~后续会不断更新C/C++相关知识,我们下期再见。