C++继承

C++继承

一.继承的概念和定义

1.继承的概念

可以把一些类的共性提取出来单独封装成一个类,让这个类作为基类

其他类继承这个类作为派生类

比方说下面这个Student类,Teacher类和Person类:

继承之前:

继承之后:

可见继承的确就是类设计层次的复用

2.继承的基本语法

了解了继承的概念之后,下面我们来学习一下继承的基本语法

比方说父类是A,子类是B

那么子类就可以通过在类名后面加上: 继承方式来继承父类

cpp 复制代码
class A
{...}
class B : public A
{...}

这里的继承方式分为public(公有继承),protected(保护继承),private(私有继承)

继承的父类的成员的属性也分为三种:public(公有成员),protected(保护成员),private(私有成员)

因此经过排列组合就可以组合出这9种情况:

3.继承的代码演示

下面我们来演示一下,就拿Student,Teacher,Person类为例

继承成功

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


下面我们来演示一下,看看到底能不能赋值转换呢?

可见,完全可以完成赋值转换

我们都知道,临时变量具有常性,因此对临时变量进行引用的话必须要加const.

而且发生类型转化时就会产生临时变量

就像是这样

cpp 复制代码
double d=1.1;
int& ri=d;//此时没有加const就会报错,因为d是double类型,转换为int类型时会产生临时变量

而我们刚才的时候

cpp 复制代码
//引用赋值
Person& rp = s;

在这里s对象是Student类型,引用赋值给rp对象时并没有任何报错或者报警,说明的确没有产生任何临时变量

也就是说基类和派生类对象进行赋值转换时是不会发生类型转换的

三.继承中的作用域

1.概念

不过不建议在子类中设计跟父类同名的成员,否则会让代码变得复杂

2.演示

下面我们来演示一下

3.经典题目

cpp 复制代码
class A
{
public:
	void fun()
	{
		cout << "func()" << endl;
	}
};
class B : public A
{
public:
	void fun(int i)
	{
		cout << "func(int i)->" << i << endl;
	}
};

请问这两个fun函数是什么关系?

A:两个fun构成函数重载

B:两个fun构成隐藏关系

C:编译时报错

D:运行时报错

注意:函数重载要求在同一作用域

而且只要这两个函数的函数名相同,就会构成隐藏关系

因此答案是B,而不是A

下面这两种调用方法正确吗?

cpp 复制代码
B b;
b.fun();
b.fun(1);

答案是:b.fun();不正确,因为子类的fun和父类的fun构成了隐藏关系

因此按照就近原则会先匹配子类的fun函数,而子类的fun函数要求传入一个参数,因此这种调用方法不正确,

而下面这种调用方法是正确的b.fun(1);

那么我就是想要调用父类的fun函数呢?

指定类域

cpp 复制代码
b.A::fun();

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


下面来演示一下

1.编译器默认生成的成员函数

编译器默认生成的成员函数会去调用父类相应的默认成员函数

cpp 复制代码
//基类
class Person
{
public:
	//构造
	Person(const char* name="root")
		:_name(name)
	{
		cout << "Person的构造函数调用" << endl;
	}
	//拷贝构造
	Person(const Person& p)
		:_name(p._name)
	{
		cout << "Person的拷贝构造函数调用" << endl;
	}
	//赋值运算符重载
	Person& operator=(const Person& p)
	{
		cout << "Person的赋值运算符重载函数调用" << endl;
		if (this != &p)
		{
			_name = p._name;
		}
		return *this;
	}
	//析构
	~Person()
	{
		cout << "Person的析构函数调用" << endl;
	}
protected:
	string _name;
};
//派生类,公有继承
class Student : public Person
{
public:
protected:
	int _StudentId;
};
int main()
{
	Student s;
	Student s2(s);
	s = s2;
	return 0;
}

2.构造函数

如果父类没有默认的构造函数,那么我们就要去给子类提供构造函数

但是父类成员不能在子类构造函数的初始化列表当中一个一个地进行初始化

而应该调用父类的构造函数去初始化父类成员:

cpp 复制代码
Student(const char* name,int StudentId)
	:Person(name)//显式调用父类的构造函数去初始化父类成员
	,_StudentId(StudentId)
{}

关于这个规定,我们可以理解为父类成员必须调用父类的成员函数

子类成员必须调用子类的成员函数

子类成员的初始化顺序:先初始化父类成员,然后才会初始化子类特有的成员

也就是说子类构造函数的初始化列表中先声明的是父类成员,其次才是子类特有的成员

父类成员默认调用父类的构造函数进行初始化,有些类似于自定义对象会默认调用自定义对象的默认构造函数

3.拷贝构造

下面我们来完成子类的拷贝构造,注意:父类成员依然是要调用父类的拷贝构造

而且此时我们之前提到的基类和派生类的对象赋值转换就派上用场啦

cpp 复制代码
Student(const Student& stu)
	:Person(stu)//利用父子之间的对象赋值转换规则  显式调用父类的拷贝构造函数
	,_StudentId(stu._StudentId)
{}

4.赋值运算符重载

对于赋值运算符重载来说,因为子类和父类的赋值运算符重载的函数名都是operator=,所以构成隐藏关系,所以需要指定父类的作用域才能调用父类的赋值运算符重载函数

cpp 复制代码
Student& operator=(const Student& stu)
{
	if (this != &stu)
	{
		Person::operator=(stu);
		_StudentId = stu._StudentId;
	}
	return *this;
}

5.析构函数

对于析构函数,

1.为了保证先析构子类成员,再析构父类成员

所以编译器说:父类析构函数不需要我们显式调用,子类析构函数结束时会自动调用父类的析构函数

2.析构函数的名字会被编译器统一特殊处理为destructor(),因此需要显式指定父类的作用域来调用父类的析构函数

五.继承与友元

友元关系不能继承,也就是说父类的友元不能访问子类的私有和保护成员,

可以理解为父类的朋友不一定是子类的朋友

六.继承与静态成员

下面我们来演示一下

cpp 复制代码
//基类
class Person
{
public:
	static int _num;
protected:
	string _name = "root";
};

//静态成员变量:类内声明,类外定义
int Person::_num = 10;

//派生类,公有继承
class Student : public Person
{
public:
protected:
	int _StudentId = 123;
};

int main()
{
	Person p;
	Student s;
	p._num++;
	s._num++;
	cout << p._num << endl;
	cout << s._num << endl;
	cout << &p._num << endl;
	cout << &s._num << endl;
	return 0;
}

可见,对于静态成员,子类继承到的只是静态成员的使用权

并没有拷贝一份副本给自己,而是跟父类共用同一个静态成员

七.菱形继承和菱形虚拟继承

1.单继承,多继承,菱形继承的概念

单继承:一个子类只有一个直接父类时称这个继承关系为单继承

多继承:一个子类有两个或两个以上直接父类时称这个继承关系为多继承

多继承时,每个父类之间用逗号分割,父类成员初始化的顺序就是继承的顺序

多继承当中有一种特殊情况:菱形继承

菱形继承:多继承的情况下,两个直接父类有一个共同的祖先

注意:菱形继承也可以这个样子,不是说必须要是菱形的继承关系才叫做菱形继承

2.菱形继承的问题

下面我们来证明一下菱形继承的问题

cpp 复制代码
class Person
{
public:
	string _name;
};

class Student : public Person
{
protected:
	int _id;
};

class Teacher : public Person
{
protected:
	int _wage;
};
//多继承时,每个父类之间用逗号分割,父类成员初始化的顺序就是继承的顺序
class Assistant : public Student,public Teacher
{
protected:
	int _course;
};

int main()
{
	Assistant a;
	//a._name = "wzs";//编译报错,Assistant::_name不明确  因为菱形继承导致了二义性
	a.Student::_name = "张三";//指定类域
	a.Teacher::_name = "李四";//指定类域
	//尽管通过指定类域可以解决二义性
	//但是代码冗余依旧无法解决
	cout << a.Student::_name << endl;
	cout << a.Teacher::_name << endl;
	return 0;
}

二义性的问题:

数据冗余的问题:

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

1.菱形虚拟继承的概念

只需要在继承的时候加上virtual关键字即可:

注意:要在继承Person的类上加virtual关键字,进行虚继承

Assistant类不需要加virtual关键字

因此,对于这种菱形继承,应该这么修改为菱形虚拟继承

2.验证菱形虚拟继承

下面我们来验证一下到底有没有很好地解决这个菱形继承的问题呢?

cpp 复制代码
class Person
{
public:
	string _name;
};
//菱形虚拟继承
class Student :virtual public Person
{
protected:
	int _id;
};
//菱形虚拟继承
class Teacher :virtual public Person
{
protected:
	int _wage;
};
//多继承时,每个父类之间用逗号分割,父类成员初始化的顺序就是继承的顺序
class Assistant : public Student,public Teacher
{
protected:
	int _course;
};

int main()
{
	Assistant a;
	a._name = "wzs";//解决了二义性
	cout << a._name << " " << a.Student::_name << " " << a.Teacher::_name << endl;

	a.Student::_name = "张三";//也可以指定类域访问
	cout << a._name << " " << a.Student::_name << " " << a.Teacher::_name << endl;

	a.Teacher::_name = "李四";//也可以指定类域访问
	cout << a._name << " " << a.Student::_name << " " << a.Teacher::_name << endl;

	return 0;
}

无论指定那个类域来修改Person成员,都是只修改那一个变量

说明Assistant类当中只有一份Person成员

成功解决了二义性和数据冗余问题

4.菱形虚拟继承解决菱形继承问题的原理

1.说明

为了更好地说明菱形虚拟继承的原理

我们给出一个简化的菱形虚拟继承体系,并且通过调试中的内存窗口来观察对象成员

cpp 复制代码
class A
{
public:
	int _a;
};
// class B : public A
class B : virtual public A
{
public:
	int _b;
};
// class C : public A
class C : virtual public A
{
public:
	int _c;
};
class D : public B, public C
{
public:
	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;
}

2.验证

我们可以看出

1:

在d对象刚被创建之后就更新了两个值

2:类D中的4种成员:

类A的成员存到一个单独的位置

类B和类C成员位置有一个"奇怪"的值

类D自己的成员单独存放

下面我们来看一下那个奇怪的数字到底是什么?

这两个指针叫做虚基表指针

这两个表叫做虚基表,

虚基表当中存放的是偏移量

因此我们就可以得出一个重大结论:

3.几个问题

1.为什么类B和类C要存储这个偏移量呢?

如果发生了D的对象赋值转换为B对象或者C对象时

需要找到D对象当中的B/C成员中的A成员才能赋值

cpp 复制代码
D d;
B& rb=d;
C& rc=d;

切片的时候就需要通过偏移量去计算位置,也就需要找到A成员也就是_a

2.为什么要搞到一个虚基表当中,为什么不能直接记录类A成员的地址呢?

验证类D实例化出的每一个对象都共用同一张虚基表

3.类B的对象模型是什么样的呢?

一起来看一下:

八.继承与组合

继承和组合都是一种复用,最大的区别是访问方式有所不同

九.小拓展

1:如何实现一个不能被继承的类?

1.C++11之前的实现方案

我们知道子类的构造函数当中需要调用父类的构造函数去对父类的成员进行初始化

因此我们可以把父类的构造函数私有化,这样子类就调用不到父类的构造函数了,这样的话继承之后也没有意义了,因为此时子类不能定义对象

cpp 复制代码
class A
{
private:
	A() {}
};
class B :public A
{};
int main()
{
	B b;
	return 0;
}

2.C++11新增关键字final

关键字final:用来修饰父类,让父类不能被继承

2:如何统计一个父类以及其子类一共实例化出多少个对象?

我们知道子类的构造函数当中需要调用父类的构造函数去对父类的成员进行初始化

因此我们可以在父类当中定义一个静态成员变量_count

并且在父类的构造函数当中++这个_count

_count当中的值就是实例化出对象的个数

cpp 复制代码
class A
{
public:
	A() { _count++; }
	static int _count;
};
int A::_count = 0;
class B : public A
{};
class C : public A
{};
class D : public A
{};
void test()
{
	A a;
	B b;
	C c;
	D d;
	cout << A::_count << endl;
}

以上就是C++继承的全部内容,希望能对大家有所帮助!

相关推荐
Bucai_不才18 分钟前
【C++】初识C++之C语言加入光荣的进化(上)
c语言·c++·面向对象
木向20 分钟前
leetcode22:括号问题
开发语言·c++·leetcode
筑基.28 分钟前
basic_ios及其衍生库(附 GCC libstdc++源代码)
开发语言·c++
yuyanjingtao40 分钟前
CCF-GESP 等级考试 2023年12月认证C++三级真题解析
c++·青少年编程·gesp·csp-j/s·编程等级考试
王老师青少年编程2 小时前
gesp(二级)(12)洛谷:B3955:[GESP202403 二级] 小杨的日字矩阵
c++·算法·矩阵·gesp·csp·信奥赛
OTWOL3 小时前
两道数组有关的OJ练习题
c语言·开发语言·数据结构·c++·算法
QQ同步助手3 小时前
C++ 指针进阶:动态内存与复杂应用
开发语言·c++
qq_433554544 小时前
C++ 面向对象编程:递增重载
开发语言·c++·算法
易码智能4 小时前
【EtherCATBasics】- KRTS C++示例精讲(2)
开发语言·c++·kithara·windows 实时套件·krts
ཌ斌赋ད4 小时前
FFTW基本概念与安装使用
c++