【C++】继承(详解)

avatar
作者
筋斗云
阅读量:4

前言:今天我们正式的步入C++进阶内容的学习了,当然了既然是进阶意味着学习难度的不断提升,各位一起努力呐。

💖 博主CSDN主页:卫卫卫的个人主页 💞
👉 专栏分类:高质量C++学习 👈
💯代码仓库:卫卫周大胖的学习日记💫
💪关注博主和博主一起学习!一起努力!
在这里插入图片描述


什么是继承

C++中的继承是一种面向对象编程的特性,它允许一个类(子类)继承另一个类(父类)的属性和方法,并且可以添加自己的属性和方法。通过继承,子类可以重用父类的代码,减少重复编写代码的工作量。在C++中,使用关键字"extends"可以声明一个类继承另一个类。子类将自动继承父类的非私有成员和方法,并可以通过重写(override)父类的方法或添加新方法来实现自己的行为。(光看文字大家肯定还是觉得晦涩难懂的,我们直接看代码)


继承的使用格式

在C++中,使用继承的格式如下:

class 子类名 : 访问权限 基类名 {     // 子类的成员和函数声明 }; 

其中,子类名表示要定义的子类的名称,访问权限可以是public、protected或private,用于指定从基类继承成员的访问权限,基类名表示要继承的基类的名称。

例如,定义一个基类(父类)father和一个派生类(子类)son:

class father//父类 { public: 	string name = "张三"; 	int age = 20; };  class son: public father//public继承子类可以直接继承父类的公有对象 { public : 	void print() 	{ 		cout << name << endl << age << endl; 	} }; int main() { 	son s1; 	s1.print(); 	return 0; } 

在上述例子中son是parent的子类,使用public权限继承了parent中name和age,然后用子类中的成员函数去调用print函数从而调用了父类中的成员。

结果如下:
在这里插入图片描述

如果还是不太能理解父类和子类的关系,看下图,通俗的理解就是儿子继承了父亲的遗产,并且儿子的财产依然存在。
在这里插入图片描述


继承后子类(派生类)的访问权限

在C++中,继承后的访问权限可以分为三种:public、protected和private。

公有继承(public inheritance):当通过公有继承派生一个子类时,基类的公有成员和保护成员在子类中仍然是公有的,可以直接访问。基类的私有成员在子类中是不可访问的。

保护继承(protected inheritance):当通过保护继承派生一个子类时,基类的公有成员和保护成员在子类中都变成了保护成员,它们通过子类只能被子类自身和子类的派生类访问,外部无法访问。基类的私有成员在子类中是不可访问的.。

私有继承(private inheritance):当通过私有继承派生一个子类时,基类的公有成员和保护成员在子类中都变成了私有成员,只能在子类内部访问,外部无法访问。基类的私有成员在子类中是不可访问的。

需要注意的是,继承的访问权限仅影响继承后的成员的访问权限,不会改变基类中成员本身的访问权限。
在这里插入图片描述

实例演示:

class Base { public:     int publicMember; protected:     int protectedMember; private:     int privateMember; };  class PublicDerived : public Base { public:     void example() {         publicMember = 10; // 可以直接访问公有成员         protectedMember = 20; // 可以直接访问保护成员         // privateMember = 30; 无法访问私有成员     } };  class ProtectedDerived : protected Base { public:     void example() {         publicMember = 10; // 可以直接访问公有成员         protectedMember = 20; // 可以直接访问保护成员         // privateMember = 30; 无法访问私有成员     } };  class PrivateDerived : private Base { public:     void example() {         publicMember = 10; // 可以直接访问公有成员         protectedMember = 20; // 可以直接访问保护成员         // privateMember = 30; 无法访问私有成员     } };  

总结:

  1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
  2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
  3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他
    成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
  4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
  5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
  6. 很多人分不清保护继承和私有继承,其实区别就是保护继承他的儿子的儿子也能访问他的保护成员和公有成员,私有继承就是只有他儿子能访问。

子类与父类

子类与父类的相互赋值转换

  1. 符号赋值“=”

注:这里我们先暂时知道赋值的规则就是切片赋值,就是把子类中的父类那部分,赋值过去即可。

在C++中,子类对象可以被隐式地转换为父类对象,但是父类对象不能被隐式地转换为子类对象。这是因为子类对象继承了父类对象的成员和方法,但父类对象并不包含子类特有的成员和方法。

以下是一个示例说明子类对象转换为父类对象的情况:

class Parent { public:     void print() {         std::cout << "Parent class" << std::endl;         std::cout << age << std::endl;     }     int age = 30; };  class Child : public Parent { public:     void print() {         std::cout << "Child class" << std::endl;     } };  int main() {     Child child;     child.age = 50;     Parent parent = child;  // 子类对象隐式转换为父类对象     parent.print();  // 调用父类的print()方法     return 0; } 
  1. 引用
Child s1;//子类 	Parent& t1=st;//父类,通过引用对其进行赋值 
  1. 指针
Child s1;//子类 	Parent* t1 = &st;//父类,通过指针进行赋值 

总结来说,C++中可以将子类对象隐式地转换为父类对象,但是父类对象无法隐式地转换为子类对象。如果需要使用子类特有的成员和方法,需要进行类型转换.


同名成员变量

在C++中,当子类继承了父类时,如果子类中出现了与父类同名的成员变量,那么子类的同名成员变量会隐藏父类的同名成员变量。这称为"隐藏"(hiding)。

具体来说,如果子类定义了与父类同名的成员变量,那么子类中的同名成员变量会隐藏父类中的同名成员变量。这意味着当通过子类对象访问同名成员变量时,会优先访问子类中的成员变量,而无法直接访问到父类中的同名成员变量。

#include <iostream> class Parent { public:     int num = 10; };  class Child : public Parent { public:     int num = 20; };  int main() {     Child child;     std::cout << "Child num: " << child.num << std::endl;        // 编译器还是就近原则,优先访问子类中的num,输出: Child num: 20     std::cout << "Parent num: " << child.Parent::num << std::endl;      //想要访问父类中的num,需要指定类域,输出: Parent num: 10     return 0; }  

在这里插入图片描述

在上面的示例中,Parent类中定义了一个名为num的成员变量,初始值为10。Child类继承了Parent类,并定义了一个同名成员变量num,初始值为20。

在main函数中,我们创建了Child类的对象child。当我们通过子类对象child访问num时,会优先访问子类的同名成员变量,因此输出结果为20。如果我们需要访问父类中的同名成员变量,可以使用作用域解析符"::"来指定父类,如child.Parent::num


同名成员函数

在C++中,当子类继承了父类时,如果子类中出现了与父类同名的成员函数,那么子类的同名成员函数将隐藏父类的同名成员函数。

具体来说,如果子类定义了与父类同名的成员函数,那么子类中的同名成员函数将隐藏父类中的同名成员函数。这意味着当通过子类对象调用同名成员函数时,会优先调用子类中的成员函数,而无法直接调用到父类中的同名成员函数。

然而,与成员变量的隐藏不同的是,对于成员函数的隐藏,如果需要在子类中使用父类的同名成员函数,可以使用作用域解析符"::"来指定父类.

class Parent { public:     void print() {         std::cout << "Parent print" << std::endl;     } };  class Child : public Parent { public:     void print() {         std::cout << "Child print" << std::endl;         Parent::print();//可以通过指定域来调用     } };  int main() {     Child child;     child.print();                // 输出: Child print     child.Parent::print();        // 输出: Parent print      return 0; }   

子类中的默认成员函数

构造函数

在C++中,子类可以通过构造函数来初始化继承的父类成员。子类的构造函数可以调用父类的构造函数来初始化父类的成员变量。子类的构造函数在构造子类对象时被调用,而且会在子类构造函数的初始化列表中调用父类的构造函数.

注: 编译器会默认先调用父类的构造函数,再调用子类的构造函数.

class Parent { public: 	Parent(string name = "Dad", int age = 30) 		:_name(name) 		,_age(age) 	{ 		cout << "这是父类:" << _name << " " << age << endl; 	}  protected: 	string _name; 	int _age; };  class Child: public Parent { public: 	Child(string son) 		:_son(son) 	{ 		cout << "这是子类:" << _son << endl; 	}  protected: 	string _son; };  int main() { 	Child s1("son");  	return 0; } 

在这里插入图片描述
这里我们可以明显的知道,编译器先调用父类的默认构造,在调用子类的构造,所以一定要保证这里的父类的构造是有效的,以防止构造失效.


析构函数

在C++中,子类可以定义自己的析构函数,用来释放子类对象在内存中分配的资源。子类的析构函数与基类的析构函数类似,通过在类的声明中使用特殊的名称~类名()来定义。

子类的析构函数会自动调用基类的析构函数,以确保基类对象中分配的资源被正确释放。

注: 析构函数和构造函数相反,编译器默认先调用子类的析构函数,再调用父类的析构函数.

实例演示:

class Parent { public: 	~Parent() 	{ 		cout << "这是父类的析构" << endl; 	} protected: 	string _name; 	int _age; };  class Child: public Parent { public: 	~Child() 	{ 		cout << "这是子类的析构" << endl; 	} protected: 	string _son; };  int main() { 	Child s1; 	return 0; } 

在这里插入图片描述


拷贝构造

在C++中,子类可以定义自己的拷贝构造函数,用于创建一个新的子类对象,该对象与已存在的子类对象具有相同的值。

注: 子类的拷贝构造函数会自动调用基类的拷贝构造函数,以确保基类对象的成员变量得到正确的复制.

class Parent { public: 	Parent(string name = "Dad", int age = 30) 		:_name(name) 		, _age(age) 	{ 		cout << "这是父类:" << _name << " " << age << endl; 	}  protected: 	string _name; 	int _age; };  class Child : public Parent { public: 	Child(string son) 		:_son(son) 	{ 		cout << "这是子类:" << _son << endl; 	} 	Child(Child& child) 		:Parent(child)//通过切片的方式直接对父类进行了初始化 		,_son(child._son) 	{ 		cout << "拷贝构造:" << _name << " " << _age << " " << _son << endl; 	} protected: 	string _son; };  int main() { 	Child s1("son"); 	Child s2(s1); 	return 0; } 

在这里插入图片描述


赋值运算符重载

子类的赋值运算符重载函数可以用来实现对象之间的赋值操作,包括赋值基类对象的成员变量和子类对象的成员变量。在赋值过程中,可以先调用基类的赋值运算符重载函数来赋值基类对象的成员变量,然后再进行子类对象的成员变量的赋值操作.

class Parent { public: 	Parent(string name = "Dad", int age = 30) 		:_name(name) 		, _age(age) 	{ 		cout << "这是父类:" << _name << " " << age << endl; 	} 	Parent& operator=(const Parent& s1) 	{ 		if (this != &s1) 		{ 			cout << "调用父类" << endl; 			_name = s1._name; 			_age = s1._age; 		} 	}  protected: 	string _name; 	int _age; };  class Child : public Parent { public: 	Child(string son) 		:_son(son) 	{ 		cout << "这是子类:" << _son << endl; 	} 	Child(Child& child) 		:Parent(child) 		,_son(child._son) 	{ 		cout << "拷贝构造:" << _name << " " << _age << " " << _son << endl; 	} 	Child& operator=(const Child& s1) 	{ 		if (this != &s1) 		{ 			Parent::operator=(s1);//调用父类的运算符重载,以免调用自身造成栈溢出 			_son = s1._son;  		} 	} protected: 	string _son; };  int main() { 	Child s1("son"); 	Child s2(s1); 	return 0; } 

继承中的单继承与多继承

在C++中,单继承和多继承是两种不同的继承方式。

  1. 单继承:
    单继承指的是一个派生类只能继承自一个基类(一个孩子一个父亲)。在单继承中,一个派生类可以继承基类的所有成员(包括成员函数和成员变量),并且可以通过派生类对象访问这些成员。单继承的语法如下:

    class Base {   // base class members };  class Derived : public Base {   // derived class members }; 

    在这个例子中,Derived类从Base类单继承,Derived类可以访问Base类中的所有公有成员。

  2. 多继承:
    多继承指的是一个派生类能够从多个基类继承。在多继承中,一个派生类可以继承多个基类的成员,这些成员可以通过派生类对象访问(一个孩子有多个父亲)。多继承的语法如下:

    class Base1 {   // base class 1 members };  class Base2 {   // base class 2 members };  class Derived : public Base1, public Base2 {   // derived class members }; 

    在这个例子中,Derived类从Base1类和Base2类多继承,Derived类可以访问Base1Base2类中的所有公有成员。

需要注意的是,多继承在设计上可能会引入复杂性和冲突,需要谨慎使用。在多继承中,如果多个基类具有相同的成员函数或成员变量,派生类必须显式指明使用哪个基类的成员。另外,多继承还可能导致菱形继承问题,即继承图中出现多个路径指向同一个基类。为了解决这个问题,可以使用虚继承或其他技术。

总结起来,单继承和多继承是C++中的两种继承方式。单继承指的是派生类只能继承自一个基类,而多继承则允许派生类从多个基类继承。使用继承时需要考虑设计的简洁性和灵活性,以及可能引入的复杂性和冲突。


菱形继承

C++中的菱形继承(diamond inheritance)是多继承的一个特殊情况。当一个派生类通过两个不同的路径继承自同一个基类时,就会形成菱形继承结构。这种继承结构的名称源于它的图形表示类似于菱形。

考虑以下示例代码:

class A { public:     void funcA() {         cout << "A::funcA()" << endl;     } };  class B : public A { public:     void funcB() {         cout << "B::funcB()" << endl;     } };  class C : public A { public:     void funcC() {         cout << "C::funcC()" << endl;     } };  class D : public B, public C { public:     void funcD() {         cout << "D::funcD()" << endl;     } }; 

在这里插入图片描述

在这个例子中,类D通过BC两个路径分别继承了类A,形成了菱形继承结构。这意味着类D会有两份来自A类的成员函数和成员变量。例如,D类实例可以调用funcA()方法两次,一次是通过B的路径,另一次是通过C的路径。

菱形继承可能导致以下问题:

  1. 虚函数二义性(Virtual Function Ambiguity):如果在菱形继承结构中,派生类中存在同名的虚函数,那么在派生类中访问这个虚函数时将产生二义性,编译器不知道应该使用哪个父类的虚函数实现。

  2. 数据冗余(Data Redundancy):由于菱形继承结构中派生类从两个不同的路径继承了同一个基类,导致派生类中会存在两份相同的成员变量。

为了解决菱形继承带来的问题,可以使用以下方法:

  1. 虚拟继承(Virtual Inheritance)是C++中一种特殊的继承方式,用于解决多继承中的菱形继承问题。通过使用虚拟继承,可以确保在派生类中只包含一个共享的基类子对象,避免了数据冗余和虚函数二义性的问题。

虚拟继承的特点有:

虚拟继承只发生在最顶层的派生类中,而不会在派生类的派生类中发生。也就是说,只有直接派生自基类的派生类才能使用虚拟继承。

虚拟继承会在内存布局中引入一个额外的指针(虚表指针)来维护虚拟继承的关系。这个指针指向了虚拟基类的子对象,用于访问虚基类的成员。

如果虚拟基类有自己的派生类,那么虚拟继承会优先选择这个派生类中的虚基类子对象作为共享对象

  1. 重写和调用具体的父类函数:通过在派生类中重写同名的虚函数,并在派生类中显式调用具体的父类函数,可以解决虚函数二义性的问题。

综上所述,菱形继承是C++多继承的一个特殊情况,可能会引发虚函数二义性和数据冗余的问题,可以通过虚继承和重写父类函数的方式来解决这些问题。


好啦,今天的内容就到这里啦,下期内容预告C++中的多态.


结语:进阶的内容有点繁杂,大家一起加油呐!。


🌏🗺️ 这里祝各位接下来的每一天好运连连 💞💞

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!