今日分享C++ ---继承
😎【博主主页:晚云与城——csdn博客】😎
🤔【本文内容:C++ 继承😍 】 🤔
----------------------------------------- 感谢大家的观看,点赞 ,收藏-----------------------------------------------
1.继承的介绍:
2.继承的定义:
看上图,这里涉及到新的知识 —— 继承方式。之前我们学习类的时候,主要接触的是public
(公共)和private
(私有)访问修饰符,protected
(受保护)的存在感似乎不强。但在继承场景中,protected
就发挥出了很大的作用。
1.继承关系和访问限定符:
类成员 / 继承方式 | public 继承 | protected 继承 | private 继承 |
---|---|---|---|
基类的 public 成员 | 派生类的 public 成员 | 派生类的 protected 成员 | 派生类的 private 成员 |
基类的 protected 成员 | 派生类的 protected 成员 | 派生类的 protected 成员 | 派生类的 private 成员 |
基类的 private 成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
下面为大家总结一些关于类继承的关键要点与实用小技巧:
- 基类的
private
成员,派生类无论采用何种继承方式,都无法直接访问。不过要注意,这并非是没有继承,而是派生类 “不可见”,所以用不了。- 若希望成员能在派生类中被访问,却又不想在类外被访问,
protected
访问限定符就派上用场了。- 观察上面表格内容能发现这样的规律:基类的私有成员子类不可见,其余基类成员在子类中的访问权限,是取 “成员在基类的访问限定符” 与 “继承方式” 两者中的更严格者(
基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式))(权限优先级:public
>protected
>private
)。class
的默认访问限定符是private
,struct
的默认访问限定符是public
。即便知晓这一规则,为了代码的清晰性,最好还是显式写出继承方式。- 实际开发中,常用的是
public
继承,protected
和private
继承很少使用,也不推荐。因为通过这两种继承方式得到的成员,只能在派生类内部使用,不利于代码的扩展与维护。
1.public继承方式:
2.protected继承方式:
3.private继承方式:
3.基类和派生类对象复制转换:
- 派生类对象向基类的赋值
派生类对象能为基类的指针、引用、对象赋值。这一过程可形象地称为 “切片”—— 即把派生类里继承自基类的那部分 “切割” 出来,赋值给基类的指针、引用或对象。 - 基类对象向派生类对象的直接赋值
基类对象无法直接给派生类对象赋值。因为派生类包含了基类没有的成员,基类对象缺少这些额外信息,不能直接填充派生类对象的所有部分。 - 基类对象向派生类指针、引用的赋值(强制类型转换)
若要让基类对象给派生类的指针、引用赋值,可通过强制类型转换实现。但为了安全,最好确保基类指针原本指向的就是派生类对象。要是涉及多态场景下的强制转换,我们可以利用运行时类型识别(RTTI)中的dynamic_cast
,它能先识别对象的实际类型,再进行安全的转换,有效避免因类型不匹配而导致的错误。
class person
{
//public:
// void print()
// {
// cout << _name << endl;
// }
protected:string _name = "那笔小新";string _sex ="男";
//private:size_t _age = 3;
};
class student :public person
{
public:int _no;/*using person::print;void print1(){cout << _sex << endl;}
protected:size_t stuid;string school;*/
};
void test()
{student sobj;// 1.子类对象可以赋值给父类对象/指针/引用person pobj = sobj;person* pp = &sobj;person& rp = sobj;//2.基类对象不能赋值给派生类对象sobj = pobj;// 3.基类的指针可以通过强制类型转换赋值给派生类的指针pp = &sobj;student* ps1 = (student*)pp; // 这种情况转换时可以的。ps1->_no = 10;pp = &pobj;student* ps2 = (student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题ps2->_no = 10;
}
4.继承的作用域:
- 作用域独立性:基类和派生类拥有各自独立的作用域,继承关系并非意味着派生类会无差别继承基类的所有内容。
- 同名成员的隐藏:当基类与派生类存在同名成员时,派生类虽会继承基类成员,但会触发隐藏机制,直接访问基类同名成员的操作会被屏蔽。不过,在子类成员函数里,可通过
基类::基类成员
的方式显式访问基类同名成员。 - 同名成员函数的隐藏判定:在派生类中,只要成员函数与基类的成员函数名称相同,就会构成隐藏,无需考虑参数列表等其他因素。
- 使用建议:为避免因成员同名引发的隐藏问题,建议在设计类时,从一开始就尽量避免出现同名成员.
class person
{
public:void print(){cout << "person " << _num << endl;}
protected:string _name = "嘻嘻";int _num = 1;
private:
};class student:public person
{
public:void print(int _num){cout <<"student " << _num << endl;}
protected:int _num = 2;
private:
};
int main()
{student s;s.print();
}//运行结果
student 2
- 在通过
print
函数进行打印操作时,此处的_num
属于派生类内部的成员。 - 这里的
print
打印函数,并非函数重载。因为函数重载要求多个函数处于同一个命名作用域,而此处不满足该条件。 print
函数形成了函数隐藏。在类的成员函数中,只要函数名称相同,就会构成函数隐藏。
5.派生类的默认成员函数:
6个默认函数是指,我们没写,但编译器会自动给我们写的函数,但在派生类里面,默认函数是怎么回事,是会继承基类里面的默认函数还是会在派生类里面从新生成。
#include<iostream>
#include<string>
using namespace std;
class person
{
public:person(const string& name):_name(name){cout << "person()" << endl;cout <<_name<< endl;}person(const person& p):_name(p._name){cout <<" person(const person & p)" << endl;cout << 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;
private:size_t _age;string _sex;
};class student:public person
{
public:student(const string& name , const size_t& stuid ):_stuid(stuid), person(name) /*person(name)*/{cout << "student()" << endl;cout <<"_name:" << _name << "\tstuid:" << _stuid << endl;cout << 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;}~student(){cout << "~student()" << endl;}
protected:
private:size_t _stuid;
};int main()
{student s("pertr", 100);student s1(s);student s2("haha", 101);s2 = s;return 0;
}
1.派生类构造函数需调用基类构造函数来初始化基类成员。若基类无默认构造函数,就必须在派生类构造函数的初始化列表阶段显式调用基类构造函数。
2.派生类的拷贝构造函数要调用基类的拷贝构造函数,以此完成基类部分的拷贝初始化。
3.派生类的 operator=
必须调用基类的 operator=
,从而完成基类部分的复制操作。
4.派生类的析构函数在自身调用完成后,会自动调用基类的析构函数来清理基类成员。这样做是为了确保派生类对象先清理派生类自身成员,再清理基类成员的顺序。
5.派生类对象初始化时,会先调用基类构造函数,之后再调用派生类构造函数。
6.派生类对象析构清理时,会先调用派生类析构函数,接着再调用基类的析构函数。
7.后续有些场景中析构函数需要构成重写,而重写的条件之一是函数名相同(后续会讲解)。编译器会对析构函数名进行特殊处理,将其处理为 destructor()
。所以,在父类析构函数不加 virtual
的情况下,子类析构函数和父类析构函数构成隐藏关系。
6.继承与友元:
友元是不可以被继承的,也就是说基类友元无法访问派生类里面的私人和保护成员。
6. 继承与静态成员:
static
静态成员,那么在整个继承体系里,该静态成员只有唯一的一个实例。它类似全局变量,存储在静态存储区,无论基类派生出多少个子类,都共享这同一个 static
成员实例。
class person
{
public:person(){++_count;}static int _count;
protected: string _name;
};
int person::_count = 0;
class student:public person
{
protected:int stuid;
};
class graduate:public student
{
protected:string _seminarCourse;
};int main()
{student s0;student s1;student s2;student s3;student s4;graduate s5;cout << " 人数 :" << person::_count << endl;student::_count = 0;cout << " 人数 :" << person::_count << endl;}
//运行结果:
人数:6
人数:0
7.复杂的菱形继承及菱形虚拟继承:
1. 单继承
单继承指的是一个派生类仅拥有一个基类的继承方式。在这种继承关系里,派生类能从唯一的基类那里继承属性与方法,类的层次结构呈现出简单的树形分支特点,例如类 B
继承自类 A
,类 C
又继承自类 B
这样的链式继承。
2. 多继承
多继承是指一个派生类具备多个基类的继承形式。通过多继承,派生类可以整合多个不同基类的特性,像类 D
同时继承类 B
和类 C
,从而能拥有类 B
与类 C
各自的成员(属性和方法)。不过,多继承也容易引发一些问题,比如命名冲突等。
3. 菱形继承
菱形继承属于多继承里的特殊情形。其继承结构形似菱形,存在一个基类(如类 A
),有两个类(类 B
和类 C
)都继承自类 A
,之后又有一个类(类 D
)同时继承类 B
和类 C
。这种情况下,类 D
会间接包含类 A
的成员两次,极易产生二义性等问题,在很多编程语言中需要通过特定机制(如虚继承)来解决。
代码中出现了菱形继承问题,针对该问题,有两种常见的解决思路:
1.显式访问:通过明确指定要访问的父类成员,来消除二义性。
2.虚继承:在继承时采用虚继承的方式,让派生类只保留一份共同基类的成员,从而避免重复继承导致的问题。
虚继承就是在我们的继承方式前面加上virtual ,它类似于我们的静态变量,但不完全是。
1.未加virtual
class A
{
public:int _a = 10;
};
class B:/*virtual*/ public A
{
public:int _b;
};
class C:/*virtual */public A
{
public:int _c;
};
class D:public B,public C
{
public:/*void print(){cout << num << endl;}*/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
类对象中,因包含 B
、C
类,而 B
、C
类又都包含 A
类,导致 A
的成员存在两份,既引发访问时的二义性问题,又造成数据冗余。
2.加virtual
class A
{
public:int _a = 10;
};
class B:virtual public A
{
public:int _b;
};
class C:virtual public A
{
public:int _c;
};
class D:public B,public C
{
public:/*void print(){cout << num << endl;}*/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;
}
这里的0到D的最后面去了,并且只有一份。类中会包含一个虚基表指针(vbptr),指向对应的虚基表。虚基表内部记录着当前类到共同虚基类(如上述例子中的类 A)的偏移量等信息。借助虚基表,派生类(如类 D)能通过统一的偏移计算,精准且唯一地访问共同虚基类的成员,既消除了访问的二义性,又避免了数据的重复存储,高效处理了菱形继承的缺陷。
8.继承的总结:
在C++里面,继承最大的问题就是他的继承方式(大多数情况下,只会用常用的)以及多继承。多继承可以认为是C++的缺陷之一,很多后来的面对对象的语言都没有多继承,如Java。
继承与组合:
1.继承是一种代码复用方式,属于 “白箱复用”:派生类可基于基类的实现来定义自身逻辑,基类的内部细节对派生类可见。但这会一定程度破坏基类的封装性,基类的改动易对派生类产生较大影响,使得派生类与基类依赖强、耦合度高。
2.对象组合是另一种复用选择,属于 “黑箱复用”:通过组装或组合对象来实现更复杂功能,被组合对象只需有良好定义的接口,其内部细节不可见。组合的类之间依赖弱、耦合度低,能更好地保持类的封装性。
实际开发中,应优先使用对象组合,因其耦合度低,利于代码维护。不过继承也有适用场景,比如类间存在明确的继承关系,或者需要实现多态(多态的实现依赖继承)时,就适合用继承。当类之间既能用继承又能用组合时,优先选择组合。
#include <iostream>
#include <string>
using namespace std;
// Animal和Cat、Dog构成is - a的关系
class Animal {
protected:string _name = "动物"; // 名字int _age = 1; // 年龄
};
class Cat : public Animal {
public:void Action() { cout << "会捉老鼠" << endl; }
};
class Dog : public Animal {
public:void Action() { cout << "会看家" << endl; }
};
//“has - a” 关系(汽车与发动机)
class Engine {
protected:string _brand = "某品牌"; // 发动机品牌int _power = 150; // 发动机功率
};
class Car {
protected:string _color = "黑色"; // 汽车颜色string _model = "SUV"; // 汽车型号Engine _engine; // 汽车有发动机,体现has - a关系
public:void Start() { cout << "汽车启动,发动机品牌:" << _engine._brand << ",功率:" << _engine._power << endl; }
};
❤️总结
相信坚持下来的你一定有了满满的收获。那么也请老铁们多多支持一下,点点关注,收藏,点赞。❤️