C++中的继承

一、继承的概念和定义

1.1 继承的概念

继承(inheritance)机制是面向对象程序设计是代码可以复用的最重要的手段 ,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称为派生类。继承呈现了面向对象程序设计的层次结构 ,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用

下面是一个继承的实例:

c++ 复制代码
#include<iostream>
#include<vector>

using namespace std;

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}

protected:
	string _name = "son";
	int _age = 18;
};

//继承父类Person
class Student : public Person
{
protected:
	int _stuid;//学号
};

class Teacher : public Person
{
protected:
	int _jobid;//工号
};

int main()
{
	Person p;
	Student s; 
	Teacher t;
	t.Print();

	return 0;
}

继承后父类Person的成员(成员函数+成员变量)都会成为子类的一部分,但是是一种类似拷贝的方式但又不是拷贝,改变子类中继承父类的成员,真正的父类对象中的成员不会改变。

1.2 继承的定义

1.2.1 定义格式

c++ 复制代码
class Student : public Person
{
protected:
	int _stuid;//学号
};

上面的形式就是继承的定义格式 :顺序是 class 子类(派生类) : 继承方式(访问限定符) 父类

上面的Person是父类 也叫做基类 。Student是子类 ,也称作派生类

注意:继承父类的成员变量相当于拷贝过来的,继承父类的函数是共用的一个的

父类和子类的构造函数是各自的,不是共用的,但子类可以使用父类的构造来处理子类中父类的成员

1.2.2 继承关系和访问限定符

###1.2.3 继承基类成员访问方式的变化

类成员/继承方式 public继承 protected继承 private继承
基类的public成员 派生类的public成员 派生类的的protected成员 派生类的private成员
基类的protected成员 派生类的protected成员 派生类的protected成员 派生类的private成员
基类的private成员 在派生类中不可见 在派生类中不可见 在派生类中不可见
  1. 基类(父类)private成员在派生类中无论以什么方式继承都是不可见的。不可见指的是基类的私有成员被继承到了派生类对象中,但是语法上限制派生类对象不管在类内还是类外都不能去访问它。
  2. 基类private成员在派生类中是不能被访问的,如果基类成员不想在类外被直接访问,但是需要在派生类中能访问,就将基类成员定义为protected。保护成员限定符(protected)就是因为继承才出现的。
  3. 基类成员在派生类中的访问方式 == Min(成员在基类中的访问限定符, 继承方式), 比较标准:public > protected > private。
  4. 使用关键字class时默认的继承方式是private, 使用struct时的默认继承方式是public, 最好显式的写出继承方式。
  5. 在实际运用中一般使用都是public继承,很少使用protected/private继承

二、基类和派生类对象赋值转换

  • 派生类对象 可以赋值给基类对象/基类对象的指针/基类对象的引用 ,这个赋值的过程叫做切片或者切割 ,就是把子类中父类的那部分赋值给父类。
  • 基类对象不能赋值给派生类对象。

效果图如下:

下面是我们的代码实例:

c++ 复制代码
class Person
{
protected:
	string _name; 
	string _sex;
	int _age;
};

//继承父类Person
class Student : public Person
{
public:
	int _no;
};

void test()
{
	Student s;
	//子类对象可以给父类对象赋值
	Person per = s;
	Person* pper = &s;
	Person& rper = s;

	//父类对象不能给子类对象赋值
	//s = per;

}

三、 继承中的作用域

  1. 在继承体系中基类派生类 都有独立的作用域。
  2. 子类和父类中有同名成员, 子类成员将屏蔽父类对同名成员的之间访问, 这种情况叫做隐藏 ,也叫做重定义。 (在子类成员函数中,可以使用 父类 ::父类成员 的方式进行显式访问。
  3. 如果是成员函数的隐藏, 只需要函数名相同就构成隐藏。
  4. 在实际中在继承体系 中最好不要定义同名的成员。

实例1(成员变量之间构成隐藏):

c++ 复制代码
class Person
{
protected:
	string _name = "son";
	int _id = 111;
};

class Student : public Person
{
public:
	void Print()
	{
		cout << "name: " << _name << endl;
		cout << "id: " << Person::_id << endl;
		cout << "id: " << _id << endl;
	}

protected:
	int _id = 999;
};

void test2()
{
	Student s1; 
	s1.Print();
}

实例2(成员函数之间构成隐藏):

c++ 复制代码
class A
{
public:
	void fun()
	{
		cout << "fun()" << endl;
	}
};

class B : public A
{
public: 
	void fun(int i)
	{
		cout << "fun(int i)" << i << endl;
	}
};

void test3()
{
	B b;
	b.fun(109);
}

实例2中的fun()和fun(int i) 之间是不构成重载的因为它们不在同一作用域,B中的fun和A中的fun构成隐藏,成员函数只要满足函数名相同就构成隐藏。

四、派生类中的默认成员函数

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认构造函数,则必须在派生类构造函数的初始化列表阶段显式调用。
  2. 派生类的拷贝构造函数必须要调用基类的拷贝构造完成基类的拷贝初始化。
  3. 派生类的operator=必须要调用基类的operator=来完成基类的复制。
  4. 派生类的析构函数会在被调用后自动调用基类的析构函数来清理基类成员。因为这样才能保证派生类对象先清理派生类成员在清理基类成员的顺序。
  5. 派生类对象初始化先调用基类构造再调用派生类构造。
  6. 派生类对象析构清理先调用派生类析构在调用基类的析构。

注意:

  1. 由于多态,析构函数的名字会被同一处理成destructor(), 析构函数全构成隐藏,所以要指定类域,但是父类的析构不能显示调用,调用子类的析构后会自动调用。
  2. 构造初始化不能先子后父,因为子类构造初始化可能会用到父类成员,没有初始化父,父类成员就是随机值。
  3. 初始化列表的顺序是按照声明的顺序来的。
  4. 析构的时候必须是先子后父,不能先父后子,因为子类析构可能用到父类成员的,先父后子就可能出问题。
  5. 父类的析构不是自己显式调用的,它会在子类析构结束之后自动调用。

五、 继承与友元

友元关系是不能继承的,也就是说基类友元不能访问子类私有和保护成员。即基类的友元其派生类不是友元。

代码实例:

c++ 复制代码
#include<iostream>

using namespace std;

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 p1;
	Student s1;
	Display(p1, s1);
	return 0;
}

六、 继承与静态成员

基类定义了static静态成员后,整个继承体系中就只有一个这样的成员,也就是无论派生出多少个子类都只有一个static成员实例。

c++ 复制代码
class Person
{
public:
    Person()
    {
        ++_count;
    }
protected:
    string _name; // 姓名
public:
    static int _count; // 统计人数
};
int Person::_count = 0;
class Student : public Person
{
protected:
    int _stunum; // 学号
};

class Graduate : public Student
{
protected:
    string _seminarcourse; // 研究项目
};

void TestPerson()
{
    Student s1;
    Student s2;
    Student s3;
    Graduate s4;
    cout << "人数: " << Person::_count << endl;
    Student::_count = 0;
    cout << "人数 :" << Person::_count << endl;
}

七、 复杂的菱形继承以及菱形虚拟继承

单继承:一个子类只有一个直接父类时的关系就是单继承。

多继承:一个子类有两个或者两个以上的直接父类时的关系就是多继承。

菱形继承:菱形继承是多继承的一种特殊情况。

菱形继承存在的问题:菱形继承有数据冗余和二义性的问题,在Assistant的对象中Person成员会有两份。

下面是代码实例:

c++ 复制代码
class Person
{
public:
    string _name; //姓名
};

class Student : public Person
{
protected:
    int _num; //学号
};

class Teacher : public Person
{
    int _id; //编号
};

//继承上面两个类
class Assistant : public Student, public Teacher
{
protected:
    string _majorCourse; //主修课程
};

void Test()
{
    Assistant a;
    //这样会有二义性无法确定访问的是哪一个
    a._name = "rebenn";
    //可以显示的指定访问那个父类的成员来解决二义性问题,但是无法解决数据冗余的问题
    a.Student::_name = "peter";
    a.Teacher::_name = "woter";
}

虚拟继承可以解决菱形继承的二义性以及数据冗余的问题, 就像上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决二义性和数据冗余的问题。注意虚拟继承不要在其他地方去随意使用。

虚拟继承方式:

class 类名 :virtual public 父类

virtual是虚拟继承的关键字。

代码实例:

c++ 复制代码
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 = "rebenn";
}

注意:实践中,不建议使用菱形继承,可以使用多继承,但也要少用。

io流就是一个菱形继承, 菱形继承是特殊的多继承。

八、 继承和组合

  • public继承是一种is-a的关系,也就是每个派生类对象是一个特殊的基类对象。

  • 组合是一种has-a的关系,假设B组合了A, 就是每个B对象中都含有一个A对象。

  • 继承和组合都是复用的体现。

  • 推荐优先使用对象组合,而不是继承。

代码实例:

c++ 复制代码
//Car和BWM, Car和Benz之间是is-a关系,继承
class Car
{
protected:
    string _colour = "黑色"; //颜色
    string _num = "xxxxxx";//车牌号
};

class BMW : public Car
{
public:
    void Drive()
    {
        cout << "好操作" << endl;
    }
};

class Benz : public Car
{
public:
    void Drive()
    {
        cout << "车座舒适" << endl;
    }
};


//Tire和Car构成has-a的关系, 组合
class Tire
{
protected:
    string _brand = "xxxx"; //品牌
    size_t _size = 18; //尺寸
};

class Car
{
protected:
    string _colour = "xxx"; //颜色
    string _num = "xxxxx"; //车牌号
    Tire _t; //轮胎
};

class BMW : public Car

{

public:

void Drive()

{

cout << "好操作" << endl;

}

};

class Benz : public Car

{

public:

void Drive()

{

cout << "车座舒适" << endl;

}

};

//Tire和Car构成has-a的关系, 组合

class Tire

{

protected:

string _brand = "xxxx"; //品牌

size_t _size = 18; //尺寸

};

class Car

{

protected:

string _colour = "xxx"; //颜色

string _num = "xxxxx"; //车牌号

Tire _t; //轮胎

};

复制代码
相关推荐
林开落L12 分钟前
前缀和算法习题篇(上)
c++·算法·leetcode
远望清一色13 分钟前
基于MATLAB边缘检测博文
开发语言·算法·matlab
tyler_download15 分钟前
手撸 chatgpt 大模型:简述 LLM 的架构,算法和训练流程
算法·chatgpt
何曾参静谧21 分钟前
「Py」Python基础篇 之 Python都可以做哪些自动化?
开发语言·python·自动化
Prejudices25 分钟前
C++如何调用Python脚本
开发语言·c++·python
单音GG28 分钟前
推荐一个基于协程的C++(lua)游戏服务器
服务器·c++·游戏·lua
SoraLuna35 分钟前
「Mac玩转仓颉内测版7」入门篇7 - Cangjie控制结构(下)
算法·macos·动态规划·cangjie
我狠狠地刷刷刷刷刷38 分钟前
中文分词模拟器
开发语言·python·算法
鸽鸽程序猿39 分钟前
【算法】【优选算法】前缀和(上)
java·算法·前缀和
wyh要好好学习42 分钟前
C# WPF 记录DataGrid的表头顺序,下次打开界面时应用到表格中
开发语言·c#·wpf