==继承==机制详解[ C++ ]

C++的三大件:封装,继承,多态。这篇博客我们就来了解一下继承的概念

继承是面向对象编程(OOP)中的一个核心概念,它允许一个类(子类)继承另一个类(父类)的属性和方法。在C++中,继承机制允许代码的重用,提高了程序的可维护性和可扩展性。

继承的基本概念

基本概念

在C++中,继承通过关键字classpublicprotectedprivate来实现。继承可以是单继承或多继承,单继承是指一个子类只继承一个父类,而多继承是指一个子类可以继承多个父类。继承允许我们定义一个新类(派生类)来继承另一个类(基类)的属性和行为。这样,派生类不仅拥有基类的特性,还可以添加新的特性或者修改继承来的行为。继承的语法简单明了,通过使用冒号和访问修饰符来实现。

语法用法

class 派生类名 : 继承方式 基类名
{
    // 派生类新增的成员
};

其中,继承方式可以是以下三种之一:

  • public:公有继承
  • protected:保护继承
  • private:私有继承
    如果不显式指定继承方式,则默认为私有继承(private)。

例如,如果我们想定义一个名为 Student 的类,它公有继承自一个名为 Person 的类,语法如下:

#include <iostream>
#include <string>

using namespace std;

class Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "zhangsan";
	int _age = 18;
};

// 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。
//这里体现出了Student和Teacher复用了Person的成员。
class Student :public Person
{
protected:
	int _stuid;
};

class Teacher :public Person
{
protected:
	int _jobid;
};

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

上面的代码定义了三个类:PersonStudentTeacher

  • Person类是一个基础类,它有名字(_name)和年龄(_age)两个属性,以及一个打印这些信息的方法(Print)。
  • StudentTeacher类是基于Person类创建的,它们继承了Person类的属性和方法。这意味着它们也有名字和年龄,并且可以打印这些信息。
  • main函数中,创建了StudentTeacher类的两个对象st。然后调用它们的Print方法打印出名字和年龄。

继承的好处是,StudentTeacher类可以直接使用Person类中的代码,而不需要重新写。这减少了重复代码,使代码更加清晰和易于管理。

继承的类型

在这里插入图片描述

公有继承

公有继承(Public Inheritance)是C++中的一种继承方式,它具有以下特点:

  1. 访问权限保持不变:基类的公有成员(public)在派生类中仍然是公有的,保护成员(protected)保持保护状态,而私有成员(private)在派生类中是不可访问的。
  2. 接口继承:公有继承实现了接口的继承,即派生类继承了基类的公有接口,这意味着派生类的对象可以使用基类的公有成员函数。
  3. 子类型关系:公有继承建立了子类型关系,派生类对象可以被当作基类对象来使用,这被称为“IS-A”关系。例如,如果 Student 公有继承自 Person,则可以说“Student IS-A Person”
  4. 构造函数和析构函数:派生类会继承基类的构造函数和析构函数,但不会继承基类的拷贝构造函数、赋值运算符重载函数和析构函数。派生类需要定义自己的这些函数,如果需要的话。

私有继承

私有继承(Private Inheritance)是C++中的一种继承方式,它具有以下特点:

  1. 访问权限改变:基类的公有成员(public)和保护成员(protected)在派生类中变为私有成员(private),而基类的私有成员在派生类中是不可访问的。
  2. 不继承接口:私有继承不实现接口的继承,即派生类不继承基类的公有接口。这意味着派生类的对象不能使用基类的公有成员函数。
  3. 不是子类型关系:私有继承不建立子类型关系,派生类对象不能被当作基类对象来使用,因此不满足“IS-A”关系。
  4. 构造函数和析构函数:派生类会继承基类的构造函数和析构函数,但不会继承基类的拷贝构造函数、赋值运算符重载函数和析构函数。派生类需要定义自己的这些函数,如果需要的话。
  5. 成员访问:派生类的成员函数可以访问基类的公有和保护成员,因为这些成员在派生类中变成了私有成员。派生类的成员函数不能直接访问基类的私有成员。
  6. 内存布局:派生类对象包含基类的所有成员变量,以及派生类自己添加的任何成员变量。

私有继承通常用于实现类的“HAS-A”关系,而不是“IS-A”关系。它主要用于继承实现细节,而不是接口。通过私有继承,派生类可以重用基类的实现,而不暴露基类的接口给外部使用。这种继承方式在设计中用于封装和代码重用。

保护继承

保护继承(Protected Inheritance)是C++中的一种继承方式,它具有以下特点:

  1. 访问权限改变:基类的公有成员(public)和保护成员(protected)在派生类中变为保护成员(protected),而基类的私有成员在派生类中是不可访问的。
  2. 不继承接口:与私有继承类似,保护继承也不实现接口的继承,即派生类不继承基类的公有接口。这意味着派生类的对象不能使用基类的公有成员函数。
  3. 不是子类型关系:保护继承不建立子类型关系,派生类对象不能被当作基类对象来使用,因此不满足“IS-A”关系。
  4. 构造函数和析构函数:派生类会继承基类的构造函数和析构函数,但不会继承基类的拷贝构造函数、赋值运算符重载函数和析构函数。派生类需要定义自己的这些函数,如果需要的话。
  5. 成员访问:派生类的成员函数和派生类的派生类可以访问基类的公有和保护成员,因为这些成员在派生类中变成了保护成员。派生类的成员函数不能直接访问基类的私有成员。
  6. 内存布局:派生类对象包含基类的所有成员变量,以及派生类自己添加的任何成员变量。

保护继承通常用于实现类的“HAS-A”关系,而不是“IS-A”关系。它主要用于继承实现细节,而不是接口。通过保护继承,派生类可以重用基类的实现,并且在派生类的派生类中仍然可以访问这些继承来的成员。这种继承方式在设计中用于封装和代码重用,同时允许在派生类层次结构中共享一些实现细节。

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

总的来说我们可以用这样一个表格来进行总结:
在这里插入图片描述
总结:

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

示例

class Person
{
public:
	void Print()//基类的公有成员
	{
		cout << "name:" << _name << endl;
	}
	string _name = "zhangsan";
	int _age = 18;
};

class Student :public Person//公有继承
{
protected:
	int _stuid;
};

int main()
{
	Student s;
	s.Print();
	s._name = "lisi";
	s._age = 20;
    return 0;
}
class Person
{
public:
	void Print()//基类的公有成员
	{
		cout << "name:" << _name << endl;
	}
protected:
	string _name = "zhangsan";//基类的保护乘员
private:
	int _age = 18;//基类的私有成员
};

class Student :public Person//公有继承
{
protected:
	int _stuid;
};

在这里插入图片描述
…等

基类和派生类对象的赋值转化

  • 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
  • 基类对象不能赋值给派生类对象。
  • 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)的dynamic_cast 来进行识别后进行安全转换。(ps:这个我们后面再讲解,这里先了解一下)
class Person
{
protected :
 string _name; // 姓名
    string _sex;  // 性别
    int _age; // 年龄
};
class Student : public Person
{
public :
 int _No ; // 学号
};
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;
}

在这里插入图片描述

继承中的作用域

  1. 在继承体系中基类和派生类都有独立的作用域。
  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
    也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
  4. 注意在实际中在继承体系里面最好不要定义同名的成员。
// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
class Person
{
protected:
	string _name = "小李子"; // 姓名
	int _num = 111; // 身份证号
};
class Student : public Person
{
public:
	void Print()
	{
		cout << " 姓名:" << _name << endl;
		cout << " 身份证号:" << Person::_num << endl;
		cout << " 学号:" << _num << endl;
	}
protected:
	int _num = 999; // 学号
};
void Test()
{
	Student s1;
	s1.Print();
};
// B中的fun和A中的fun不是构成重载,因为不是在同一作用域
// B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。
class A
{
public:
	void fun()
	{
		cout << "func()" << endl;
	}
};
class B : public A
{
public:
	void fun(int i)
	{
		A::fun();
		cout << "func(int i)->" << i << endl;
	}
};

int main()
{
	B b;
	b.fun(10);
	return 0;
}

构造函数和析构函数

构造函数和析构函数的调用顺序

当创建一个派生类的对象时,构造函数的调用顺序如下:

  1. 基类的构造函数:首先调用基类的构造函数,按照继承列表中的顺序从左到右。
  2. 成员对象的构造函数:接下来调用派生类中成员对象的构造函数,按照它们在类定义中声明的顺序。
  3. 派生类的构造函数:最后调用派生类自己的构造函数。
    析构函数的调用顺序与构造函数相反:
  4. 派生类的析构函数:首先调用派生类自己的析构函数。
  5. 成员对象的析构函数:接下来调用派生类中成员对象的析构函数,按照它们在类定义中声明的逆序。
  6. 基类的析构函数:最后调用基类的析构函数,按照继承列表中的逆序从右到左。

初始化列表

初始化列表是在构造函数中用来初始化成员变量的特殊语法。它在构造函数体执行之前被调用,用于初始化成员变量的值。对于继承中的构造函数,初始化列表也遵循特定的顺序:

  1. 基类的初始化:首先初始化基类,按照继承列表中的顺序。
  2. 成员对象的初始化:接下来初始化派生类中成员对象,按照它们在类定义中声明的顺序。
  3. 派生类的成员变量初始化:最后初始化派生类自己的成员变量。
    在派生类的构造函数中,您可以使用初始化列表来初始化基类和成员变量。例如:
class Base {
public:
    Base(int x) : x(x) {}
protected:
    int x;
};
class Derived : public Base {
public:
    Derived(int x, int y) : Base(x), y(y) {}
private:
    int y;
};

在这个例子中,Derived 类的构造函数使用初始化列表来首先初始化基类 Basex 成员变量,然后初始化派生类自己的 y 成员变量。
正确地使用构造函数和析构函数的调用顺序以及初始化列表是确保对象正确构造和析构的关键。

点赞,关注,收藏

  • 26
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值