IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 面向对象三个特性之------继承(C++) -> 正文阅读

[C++知识库]面向对象三个特性之------继承(C++)

目录

1.继承的概念和定义

1.1继承的概念

1.2继承的定义

1.2.1定义格式

1.2.1不同继承方式下父类成员访问方式的变化

2.父类和子类对象赋值转换

3.继承中的作用域

3.1成员变量

?3.2成员函数

4.子类的默认成员函数

?4.1子类的默认构造函数

?4.2子类的默认析构函数

4.3子类的默认拷贝构造函数

4.4子类默认operator=

5.继承与友元

5.1友元函数

5.2继承中的友元函数

6.继承与静态成员

7.菱形继承及菱形虚拟继承

7.1单继承、多继承

7.1.1单继承:一个类只有一个直接父类。

?7.1.2多继承:一个类有两个或两个以上的直接父类。

?7.1.3菱形继承(多继承的一种特殊情况)

7.2菱形继承存在的问题?

7.3菱形继承的二义性

7.4虚拟继承

8.继承和组合的对比


1.继承的概念和定义

1.1继承的概念

继承:代码复用的重要的手段,使得子类具有父类的属性、重新定义、追加属性和函数等;可以在保持原有类特性的基础上进行扩展,增加功能。

继承的概念并不是固定的,只要能够通过自己的语言组织起来,再结合一些实例能够解释就可以了。

1.2继承的定义

既然提到继承的定义,那么至少要有两个类才能够完成,我们可以先定义一个Person类:

class Person
{
	void show()
	{
		cout << "name:" << name << " " << "age:" << age << endl;
	}
	string name;
	int age;
};

1.2.1定义格式

以学生(Student)类为例,继承Person类:

class Student : public Person
{};

1.Person类是父类,也称为基类;Student类是子类,也称为派生类。

2.继承方式和访问限定符一样,也有三种(public、protected、private),但是和访问限定符表示的有所差别。

1.2.1不同继承方式下父类成员访问方式的变化

下面来演示一下,不同限定符的父类成员在子类中的变化:

1.父类中的public成员,公有继承

class Person
{
public:
	void show()
	{
		cout << "name:" << name << " " << "age:" << age << endl;
	}
	string name;
	int age;
};
class Student : public Person
{};
int main()
{
	Student s1;
	s1.name = "阿飞";
	s1.age = 19;
	s1.show();
	return 0;
}

Student中没有任何成员,只有从Person类中继承下来的name和age。

2.父类的protected成员,公有继承

同样使用1中的Person类,只是把成员变量name和age改为了protected:

class Person
{
public:
	void show()
	{
		cout << "name:" << name << " " << "age:" << age << endl;
	}
protected:
	string name;
	int age;
};

子类继承之后成员属性为protected,不能在类外进行访问。

?protected属性的成员在类内是可以访问的:

class Student : public Person
{
public:
	void Set(string m_name, int m_age)
	{
		name = m_name;
		age = m_age;
	}
};
int main()
{
	Student s1;
	s1.Set("阿飞", 19);
	return 0;
}

类外无法访问类内的protected/private成员,但是可以设置公有的接口对类内的protected/private成员进行访问。?

3.父类的private成员,公有继承

上面提到,父类的private成员在子类中是不可见的,那么这个不可见是什么含义呢?

class Person
{
public:
	void show()
	{
		cout << "name:" << name << " " << "age:" << age << endl;
	}
private:
	string name;
	int age;
};

在子类Student中设置公有的属性去访问父类中的private是否可行?

不可见: 子类对象在类内和类外都无法进行访问。

一般的话,我们不会设置父类的成员为private,除非不行被子类继承的成员。

关于继承方式和访问限定符就演示这三种情况,剩下的几种情况大家感兴趣的话可以自己去演示一下。

下面进行一下总结:

  1. 父类中的private成员在派生类中无论以什么方式继承都是不可见的。(语法上限制子类对象不管是在类内还是类外都不能访问)
  2. 如果子类成员不想在类外被访问,但需要在类内访问的,就可以定义为protected。
  3. 父类的私有成员子类不可见,其他成员在子类中的访问方式 为继承方式和访问限定符中权限小的一个。(public > protected > private)
  4. 使用class定义类时默认的继承方式是private,使用struct默认继承方式为public,不过最好显示写出继承方式。
  5. 实际运用中一般使用public继承,很少用到protected和private继承。

? ? ?

2.父类和子类对象赋值转换

Student类公有继承Person类:

class Person
{
public:
	string name;
	int age;
};
class Student : public Person
{
public:
	int id;//学号
};

定义一个子类的对象,那么能不能赋值给父类?如果能是否发生了类型的转换?

int main()
{
	Student s;
	Person p = s;
	return 0;
}

通过编译,可以得出结论:子类对象可以赋值给父类对象。

在子类对象赋值给父类对象的时候,实际上发生了切片,把子类对象中继承父类的成员切割赋值给了父类对象。?

下面对以上结论进行扩展,既然子类对象可以赋值给父类,那么子类对象的地址能不能赋值给父类对象的指针?子类对象能不能赋值给父类对象的引用?

int main()
{
	Student s;
	Person p = s;
	Person* pp = &s;
	Person& ps = s;
	return 0;
}

这两种情况同样也是正确的,通过画图来加深一下理解:

??

上面我们遗留了一个问题:赋值的时候发生了切割,?为什么不是类型转换?

int main()
{
	int a = 10;
	const double& d = a;//a赋值给d的时候产生一个临时变量,临时变量具有常性,不加上const会报错
	return 0;
}

子类对象赋值给父类引用的时候,没有加const,而且没有报错,说明没有发生类型转换。

注意:子类对象可以赋值给父类对象,但是父类对象不能赋值给子类对象。

3.继承中的作用域

3.1成员变量

每一个变量都有其对应的作用域,类中也有属于自己的类域;而且不同的类有不同的类域。

父类和子类中的成员在不同的类域中。

class Person
{
public:
	string name;
	int age;
};
class Student : public Person
{
public:
	string name;//父类中有同名的name
	int id;//学号
};
int main()
{
	Student s1;
	s1.name = "阿飞";
	s1.age = 19;
	return 0;
}

通过s1访问name,首先访问的是Student类中的name,因为在这里存在一个就近原则;s1属于Student类,首先调用Student类域中的name。

?3.2成员函数

class Person
{
public:
	void func(int n)
	{
		cout << "func(int n)" << endl;
	}
	string name;
	int age;
};
class Student : public Person
{
public:
	void func()
	{
		cout << "func()" << endl;
	}
	int id;//学号
};
int main()
{
	Student s1;
	s1.func();
	return 0;
}

成员函数的调用同样满足就近原则:

那如果使用子类对象传参数调用func(),运行结果是什么?

答案是:编译报错。

这里来总结一下:

  1. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫作重定义。
  2. 在子类成员函数中,可以使用? 父类::父类成员 显示访问。
  3. 成员函数的隐藏,只需要函数名相同。?

显示访问一下父类中的func():

注意:父类和子类中的同名函数参数不同,并不能构成函数重载;因为函数重载要求函数必须要在相同的作用域。

4.子类的默认成员函数

提供一个Person类,类中提供了构造函数(有缺省值)、拷贝构造函数、operator=、析构函数。

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; // 姓名
};

?4.1子类的默认构造函数

  1. 子类成员,跟类和对象一样(内置类型不处理,自定义类型调用它的构造函数)。
  2. 继承的父类成员,必须调用父类的构造函数。
class Student : public Person//子类Student公有继承Person类
{
protected:
	int _id; //学号
};
Student(const char* name, int id)
	: Person(name)//调用父类的构造函数
	, _id(id)
{
	cout << "Student()" << endl;
}

注意:子类构造函数的调用顺序是父类先于子类。

?4.2子类的默认析构函数

  1. 子类成员,跟类和对象一样(内置类型不处理,自定义类型调用它的析构函数)。
  2. 继承的父类成员,必须调用父类的析构函数。
~Student()
{
	~Person();
	cout << "~Student()" << endl;
}

析构函数可以这样写吗?

由于多态的需要,父类和子类析构函数的名字会统一处理为destructor();

这也就造成了子类的构造和父类的构造构成了隐藏。

指定调用父类的析构:

~Student()
{
	Person::~Person();
	cout << "~Student()" << endl;
}

如果显式的调用析构会存在一个问题,创建一个子类对象,清理的时候会调用两次父类的析构。

注意:子类析构函数不需要显式调用父类的析构函数。

每个子类析构函数后面,会自动调用父类的析构函数,这样才能保证先析构子类,再析构父类(栈中先进后出)。

4.3子类的默认拷贝构造函数

  1. 子类的成员跟类和对象一样(内置类型值拷贝,自定义类型调用它的拷贝构造)
  2. 继承的父类成员,必须调用父类的拷贝构造。
Student(const Student& s)
	: Person(s)//子类传参给父类时发生切片
	, _id(s._id)
{
	cout << "Student(const Student& s)" << endl;
}

?

?拷贝构造函数调用之后,s2中的_name和_id都是相等的,显式写的子类拷贝构造完成。

4.4子类默认operator=

  1. 子类的成员跟类和对象一样(内置类型值拷贝,自定义类型调用它的operator=)
  2. 继承的父类成员,必须调用父类的operator=。

一种错误示例写法:

Student& operator = (const Student& s)
{
	cout << "Student& operator= (const Student& s)" << endl;
	if (this != &s)
	{
		operator =(s);
		_id = s._id;
	}
	return *this;
}

?父类中的operator=和子类中的函数名相同,构成了隐藏,如果不显式调用父类中的operator=,会不断的进行子类operator,最后导致栈溢出。?

正确写法:

Student& operator = (const Student& s)
{
	cout << "Student& operator= (const Student& s)" << endl;
	if (this != &s)
	{
		Person::operator =(s);
		_id = s._id;
	}
	return *this;
}

5.继承与友元

5.1友元函数

友元函数:某些虽然不是类中的成员却能够访问类的所有成员的函数,类授予它的友元特别的访问权。

class Person
{
	friend void Display(const Person& p);//Display是Person类的友元函数
public:
	void SetName(const string& name)
	{
		_name = name;
	}
protected:
	string _name; // 姓名
};
void Display(const Person& p)
{
	cout << p._name << endl;
}
int main()
{
	Person p;
	p.SetName("阿飞");
	Display(p);
	return 0;
}

?

友元函数解决了类外不能访问类中protected/private的问题。

5.2继承中的友元函数

友元关系不能继承,也就是说父类的友元函数不能访问子类中的protected和private成员。

class Student;//声明子类
class Person
{
public:
	friend void Display(const Person& p, const Student& s);//父类的友元函数
protected:
	string _name = "阿飞"; // 姓名
};
class Student : public Person
{
protected:
	int _stuNum = 666; // 学号
};
void Display(const Person& p, const Student& s)
{
	cout << p._name << endl;
	//cout << s._stuNum << endl;
}
int main()
{
	Person p;
	Student s;
	Display(p, s);
	return 0;
}

函数Display()是父类Person的友元函数,可以访问父类中的protected/private成员;

但是友元关系不能继承,所以无法访问子类中的_stuNum。

?如果同时想要访问子类中的protected/private,可以把函数声明为子类的友元。

class Student : public Person
{
	friend void Display(const Person& p, const Student& s);
protected:
	int _stuNum; // 学号
};

6.继承与静态成员

一般成员在子类和父类中都是单独的一份,而静态成员在父类和子类中是同一份。

class Person
{
public:
	static int _count; 
};
int Person::_count = 0;//静态成员只能在类外进行初始化
class Student : public Person
{};
int main()
{
	Person p;
	Student s;
	cout << s._count << endl;
	Person::_count++;
	cout << s._count << endl;
    //打印一下父类和子类中静态成员的地址
	cout << &Person::_count << endl;
	cout << &Student::_count << endl;
	return 0;
}

??

子类对象和父类对象中的静态成员_count是同一份,改变父类对象中的_count,子类对象中的_count也会随之改变。?

7.菱形继承及菱形虚拟继承

7.1单继承、多继承

7.1.1单继承:一个类只有一个直接父类。

?7.1.2多继承:一个类有两个或两个以上的直接父类。

?7.1.3菱形继承(多继承的一种特殊情况)

7.2菱形继承存在的问题?

?从上图的对象模型,可以看出菱形继承有数据冗余和二义性的问题。(在Assistant对象中Person成员有两份)

7.3菱形继承的二义性

class Person
{
public:
	string _name; 
};
class Student : public Person
{
protected:
	int _num; 
};
class Teacher : public Person
{
protected:
	int _id; 
};
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse;
};
int main()
{
	Assistant a;
	a._name = "peter";
	return 0;
}

?菱形继承的二义性导致Assistant类中的_name访问不明确的问题,此问题可以显式指定类域来解决。

int main()
{
	Assistant a;
	a.Student::_name = "xxx";
	a.Teacher::_name = "yyy";
	return 0;
}

虽然可以指定类域来进行访问,但是这样无法从根本上解决菱形继承存在的问题。

7.4虚拟继承

虚拟继承可以解决菱形继承的二义性和数据冗余问题。

class Student : virtual public Person
{
protected:
	int _num; //学号
};
class Teacher : virtual public Person
{
protected:
	int _id; // 职工编号
};

下面来借助VS2019中的调试内存窗口来观察一下类对象成员分配:

1.菱形继承(非虚拟继承)

B中和C中都有一份A,导致数据冗余。

2.菱形虚拟继承

下图是菱形虚拟继承的内存对象成员模型:A同时属于B和C,那么B和C如何去找公共的A呢?

这里通过了B和C的两个指针,指向的一张表;这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的是偏移量,通过偏移量可以找到A。

使用到虚基表指针和虚基表的两种情况举例:

int main()
{
	//1.切片->需要找到A
	D d;
	B b = d;
	B* pb = &d;

	//2.通过父类B、C访问A中的成员->通过偏移量和地址找到_a
	pb->_a = 10;
	return 0;
}

8.继承和组合的对比

  • public继承是一种is-a的关系;例如 Student is-a Person
  • 组合是一种has-a的关系;例如 车has-a轮胎(a只是一个量词,实际不一定是一个)
  • 如果两个物体之间的关系既可以是is-a,也可以使has-a,那么优先使用has-a的组合

在编程中,我们追求的是一种“高内聚,低耦合”:继承一定程度上破坏了基类的封装,父类中的protected属性的成员在子类内是可以使用的,父类和子类之间的依赖关系强,耦合度高。

但如果使用的是组合,那么protected属性脱离了类内,在类外不能够使用,依赖关系较弱,耦合较低。

也有些关系适合使用继承,例如要实现多态就必须要使用继承。

面向对象三大特性之——继承到这里就结束了,喜欢这部分内容的铁汁们可以给博主一个三连支持,你们的支持是博主最大的动力,后续会继续更新面向对象三大特性中的多态,喜欢的铁汁们记得三连哈。

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-10-22 20:56:54  更:2022-10-22 20:57:52 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 2:49:31-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码