C++:继承

目录

一:继承的概念

[1.1 继承的定义](#1.1 继承的定义)

[1.2 继承方式](#1.2 继承方式)

[1.3 可见性区别](#1.3 可见性区别)

公有方式

私有方式

保护方式

[1.4 一般规则](#1.4 一般规则)

二、继承中的隐藏规则

三、基类和派生类间的转换

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

实现一个不能被继承的类

继承与友元

五、继承与静态成员

六、多继承及其菱形继承问题

虚拟继承解决菱形继承问题

虚拟继承的底层机制

[1. 虚基类指针(vbptr)](#1. 虚基类指针(vbptr))

2.内存布局实例

[3. 虚拟继承的代价](#3. 虚拟继承的代价)

内存开销

访问性能

复杂性

七、继承和组合


一:继承的概念

在C++中,继承是一种面向对象编程的特性,它允许我们定义一个类(称为子类或派生类)继承另一个类(称为基类或父类)的属性和方法。通过继承,我们可以复用代码,实现代码的共享和重用,同时还可以扩展基类的功能。

通过继承机制,可以利用已有的数据类型来定义新的数据类型。所定义的新的数据类型不仅拥有新定义的成员,而且还同时拥有旧的成员。我们称已存在的用来派生新类的类为基类,又称为父类。由已存在的类派生出的新类称为派生类,又称为子类。

1.1 继承的定义

在C++语言中,一个派生类可以从一个基类派生,也可以从多个基类派生。从一个基类派生的继承称为单继承;从多个基类派生的继承称为多继承。

单继承的定义格式如下:

cpp 复制代码
class<派生类名>:<继承方式><基类名>
{
    <派生类新定义成员>
};

其中,class是关键词,<派生类名>是新定义的一个类的名字,它是从<基类名>中派生的,并且按指定的<继承方式>派生的。

多继承的定义格式如下:

cpp 复制代码
class<派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,...
{
    <派生类新定义成员>
};

可见,多继承与单继承的区别从定义格式上看,主要是多继承的基类多于一个。

如果省略继承方式,对'class'将采用私有继承,对'struct'将采用公有继承。

cpp 复制代码
class Base1
{
	//
};
struct Base2
{
	//
};
class Base3 : Base1, Base2
{
	//
};

那么,Base3类将私有继承Base1,公有继承Base2,相当于:

cpp 复制代码
class Base3 : private Base1, public Base2
{
	//
};

1.2 继承方式

公有继承、私有继承、保护继承是常用的三种继承方式。

公有继承

公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的,不能被这个派生类的子类所访问。

私有继承

私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。

保护继承

保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员或友元访问,基类的私有成员仍然是私有的。

|------|-----------|-----------|---------|
|   | public | protected | private |
| 公有继承 | public | protected | 不可见 |
| 私有继承 | private | private | 不可见 |
| 保护继承 | protected | protected | 不可见 |

1.3 可见性区别

公有方式

(1) 基类成员对其对象的可见性:公有成员可见,其他不可见。这里保护成员同于私有成员。

(2) 基类成员对派生类的可见性:公有成员和保护成员可见,而私有成员不可见。这里保护成员同于公有成员。

(3) 基类成员对派生类对象的可见性:公有成员可见,其他成员不可见。

所以,在公有继承时,派生类的对象可以访问基类中的公有成员;派生类的成员函数可以访问基类中的公有成员和保护成员。这里,一定要区分清楚派生类的对象和派生类中的成员函数对基类的访问是不同的。

私有方式

(1) 基类成员对其对象的可见性:公有成员可见,其他成员不可见。

(2) 基类成员对派生类的可见性:公有成员和保护成员是可见的,而私有成员是不可见的。

(3) 基类成员对派生类对象的可见性:所有成员都是不可见的。

所以,在私有继承时,基类的成员只能由派生类中的成员函数访问,而且无法再往下继承。

保护方式

这种继承方式与私有继承方式的情况相同。两者的区别仅在于对派生类的成员而言,对基类成员有不同的可见性。

上述所说的可见性也就是可访问性。关于可访问性还有另的一种说法。这种规则中,称派生类的对象对基类访问为水平访问,称派生类的派生类对基类的访问为垂直访问。

1.4 一般规则

公有继承时,水平访问和垂直访问对基类中的公有成员不受限制;

私有继承时,水平访问和垂直访问对基类中的公有成员也不能访问;

保护继承时,对于垂直访问同于公有继承,对于水平访问同于私有继承。

对于基类中的私有成员,只能被基类中的成员函数和友元函数所访问,不能被其他的函数访问。

基类与派生类的关系

任何一个类都可以派生出一个新类,派生类也可以再派生出新类,因此,基类和派生类是相对而言的。

二、继承中的隐藏规则

在继承体系中,基类和派生类都有独立的作用域。派生类和基类中有同名成员,派生类成员将屏蔽基类对同名函数的直接访问,这种情况叫隐藏。(在派生类成员函数中,可以使用 基类::基类成员 显示访问)

需要注意的是 如果是成员函数的隐藏,只需要函数名相同就构成隐藏。

cpp 复制代码
class Person
{
protected:
	int _num = 111;
};

class Student : public Person
{
public:
	void Print()
	{
		//隐藏了基类对成员的直接访问
		cout << "学号:" << _num << endl;
		//显示调用
		cout << "学号:" << Person::_num << endl;
	}
protected:
	int _num = 99;
};

int main()
{
	Student s1;
	s1.Print();
	return 0;
}

我们可以看到只有使用了 基类::基类成员 的方法显示访问了以后才能输出我们想要的数据。明明继承了基类,但因为成员重名,所以隐藏了对基类成员的直接访问。

再来看下面的例子:

cpp 复制代码
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;
};

这里是调用了B类中的fun成员函数。 然后我们放出 b.fun(); 本意是让它调用基类的成员函数,结果出现了报错。

这里也发生了隐藏,基类中的成员函数被隐藏。

这里基类成员变量和成员函数被隐藏实际上就是局部优先规则,在子类中有的话就用子类的,子类没有才去父类中寻找。

三、基类和派生类间的转换

public继承的派⽣类对象可以赋值给基类的指针/基类的引⽤。这⾥有个形象的说法叫切⽚或者切 割。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分。

基类对象不能赋值给派⽣类对象。类的指针或者引⽤可以通过强制类型转换赋值给派⽣类的指针或者引⽤。但是必须是基类的指针 是指向派⽣类对象时才是安全的。

cpp 复制代码
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;
	return 0;
}

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

派生类的构造函数必须调用基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造 函数,则必须在派⽣类构造函数的初始化列表阶段显示调用。

派生类的拷贝构造函数必须调⽤基类的拷贝构造完成基类的拷贝初始化。

派生类的operator=必须要调用基类的operator=完成基类的复制。需要注意的是派生类的operator=隐藏了基类的operator=,所以显示调用基类的operator=,需要指定基类作用域。

派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派 生类对象先清理派生类成员再清理基类成员的顺序。

派生类对象初始化先调⽤基类构造再调派生类构造。

派生类对象析构清理先调用派生类析构再调基类的析构。

cpp 复制代码
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
{
public:
    Student(const char* name, int num)
        : Person(name)
        , _num(num)
    {
        cout << "Student()构造函数" << endl;
    }

    Student(const Student& s)
        : Person(s)
        , _num(s._num)
    {
        cout << "Student(const Student& s)拷贝构造函数" << endl;
    }

    Student& operator = (const Student& s)
    {
        cout << "Student& operator= (const Student& s)" << endl;
        if (this != &s)
        {
            //构成隐藏,所以需要显⽰调⽤
            Person::operator =(s);
            _num = s._num;
        }
        return *this;
    }

    ~Student()
    {
        cout << "~Student()析构函数" << endl;
    }
protected:
    int _num; //    学号

};
int main()
{
    Student s1("jack", 18);
    Student s2(s1);
    Student s3("rose", 17);
    s1 = s3;

    return 0;
}

实现一个不能被继承的类

方法1:基类的构造函数私有,派生类的构成必须调用基类的构造函数,但是基类的构成函数私有化以 后,派生类看不见就不能调用了,那么派生类就无法实例化出对象。

方法2:C++11新增了⼀个final关键字,final修改基类,派生类就不能继承了。

继承与友元

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

五、继承与静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个派生类,都只有一个static成员实例。

cpp 复制代码
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;

	// 公有的情况下,⽗派⽣类指定类域都可以访问静态成员
	cout << Person::_count << endl;
	cout << Student::_count << endl;
	return 0;
}

六、多继承及其菱形继承问题

单继承:⼀个派生类只有⼀个直接基类时称这个继承关系为单继承

多继承:⼀个派生类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前面,后面继承的基类在后面,派生类成员在放到最后⾯。

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

假设有下面的继承关系

cpp 复制代码
class Person
{
public:
	int age;
};
class Student1 : public Person
{ };
class Student2 : public Person
{ };
class Bat : public Student1, public Student2 //虚继承
{ };

此时,Bat对象将包含两个Person子对象。 这会导致 数据冗余 Bat中有两份age 二义性:直接访问age需要明确路径

虚拟继承解决菱形继承问题

通过virtual 关键字声明虚拟继承,使共享基类(Animal)仅保留一个实例:

cpp 复制代码
class Animal{};

class Mammal : public virtual Animal{}; // 虚拟继承
class Bird : public virtual Animal{}; //虚拟继承

class Bat : public Mammal, public Bird{};

此时,Bat对象中仅存在一个Animal对象,所有通过Mammal 或Bird的访问均指向该共享实例。

虚拟继承的底层机制

1. 虚基类指针(vbptr)

每个虚拟继承的派生类(如Mammal 和Bird)会包含一个虚基类指针(vbptr)。

vbptr指向一个虚基类表(vbtable),表中存储虚基类子对象相当于当前对象的偏移量。

2.内存布局实例

对于Bat对象:

Mammal和Bird的vbptr均指向各自的虚基类表,表中记录如何找到共享的Animal子对象。

构造顺序优先级:虚基类→非虚基类→成员变量→派生类自身

析构顺序与构造顺序严格相反:派生类自身→成员变量→非虚基类→虚基类。

3. 虚拟继承的代价
内存开销

每个虚拟继承的类需额外存储vbptr,增加对象大小

虚基类表占用额外内存

访问性能

访问虚基类成员需通过vbptr间接寻址,比直接访问多一步指针跳转。

复杂性

构造顺序需显示管理,尤其是存在多个虚基类时。

调试困难,内存布局更复杂。

七、继承和组合

(1)public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。

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

(3)优先使用对象组合,而不是类继承 。

(4)继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语"白箱"是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。

(5)对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以"黑箱"的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。

(6)实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。

cpp 复制代码
class A
{
	//....
};
class B
{
	//...
protected:
	A _a;
};

像上面的这种定义形式就是组合,即在B类中会有A类的对象,并且会使用A类内的部分成员函数或成员变量。

相关推荐
硬匠的博客12 分钟前
C++IO流
c++
眠修13 分钟前
Python 简介与入门
开发语言·python
Ai 编码助手18 分钟前
用Go语言&&正则,如何爬取数据
开发语言·后端·golang
C137的本贾尼21 分钟前
Java多线程编程初阶指南
java·开发语言
Bunury1 小时前
element-plus添加暗黑模式
开发语言·前端·javascript
XiaoyaoCarter1 小时前
每日两道leetcode
c++·算法·leetcode·职场和发展·贪心算法
LIU_Skill1 小时前
SystemV-消息队列与责任链模式
linux·数据结构·c++·责任链模式
矛取矛求2 小时前
STL C++详解——priority_queue的使用和模拟实现 堆的使用
开发语言·c++
Non importa2 小时前
【C++】新手入门指南(下)
java·开发语言·c++·算法·学习方法
pp-周子晗(努力赶上课程进度版)3 小时前
【C++】特殊类的设计、单例模式以及Cpp类型转换
开发语言·c++