C++ 继承:面向对象编程的核心概念(一)

news2025/4/21 10:23:55

文章目录

  • 引言
  • 1. 继承的基本知识
    • 1.1 继承的关键词的区别
    • 1.2 继承类模版
  • 2. 基类和派生类间的转换
  • 3. 继承中的作用域
  • 4. 派生类的默认成员函数
    • 4.1 默认成员函数的规则
    • 4.2 自己实现成员函数
    • 4.3 实现一个不能被继承的基类(基本不用)

引言

在C++中,继承是面向对象编程的核心概念之一,它允许根据现有的类定义新类。这种机制不仅简化了应用程序的创建和维护,还实现了代码重用和执行效率的提升。当你创建一个新类时,无需重写数据成员和成员函数,只需指明新类继承了现有类的成员即可。现有的类称为基类,新创建的类称为派生类。继承与函数重载、模版相比,前者是类设计层次的复用,后者是函数层次的复用。

// 继承的实现意义
class Student
{
public:
	// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
	void identity()
	{
		// ...
	}
	// 学习
	void study()
	{
		// ...
	}
protected:
	string _name = "peter"; // 姓名
	string _address; // 地址
	string _tel; // 电话
	int _age = 18; // 年龄
	int _stuid; // 学号
};

class Teacher
{
public:
	// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
	void identity()
	{
		// ...
	}
	// 授课
	void teaching()
	{
		//...
	}
protected:
	string _name = "张三"; // 姓名
	int _age = 18; // 年龄
	string _address; // 地址
	string _tel; // 电话
	string _title; // 职称
};

int main()
{
	return 0;
}

在上面的代码中,我们看到没有继承的类Student 和 Teacher,Student 和 Teacher都有一部分相同的成员和函数(_name、_address、_tel、_age、identity),这部分函数分别实现在两个类里面是冗余的。但是这两个类又有不同的部分(学生:study、_stuid 和 老师:teaching、_title)。这就导致我们没办法彻底把两个类合并。

这个时候就可以使用继承,把公共的部分分别继承给两个类,提高程序的实现效率:

// 继承的使用
// 基类
class Person
{
public:
	// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
	void identity()
	{
		cout << "void identity()" << _name << endl;
	}

protected:
	string _name = "张三"; // 姓名
	string _address; // 地址
	string _tel; // 电话
	int _age = 18; // 年龄
};

// 派生类1
class Student : public Person
{
public:
	// 学习
	void study()
	{
		// ...
	}

protected:
	int _stuid; // 学号
};

// 派生类2
class Teacher : public Person
{
public:
	// 授课
	void teaching()
	{
		//...
	}

protected:
	string title; // 职称
};

int main()
{
	Student s;
	Teacher t;
	s.identity();
	t.identity();
	return 0;
}

1. 继承的基本知识

1.1 继承的关键词的区别

首先,我们需要知道继承根据不同的关键词,基类成员在派生类中的访问权限不同。

在这里插入图片描述

  • 基类private成员虽然成功继承在派生类中。但是派生类无论以什么方式都是不可访问private成员的。
  • 基类protected成员可以在派生类中被访问,但是无法在类外被访问。
  • 使用class定义的类,在不显示写继承方式时,默认的继承方式是private;使用struct定义的类,默认的继承方式是public。一般都需要显示写继承方式。
  • 一般继承的方式都是public继承,不建议使用其他继承。
// 实例演⽰三种继承关系下基类成员的各类型成员访问关系的变化
class Person
{
public:
	void Print()
	{
		cout << _name << endl;
	}
protected:
	string _name; // 姓名
private:
	int _age; // 年龄
};
//class Student : protected Person
//class Student : private Person
class Student : public Person
{
protected:
	int _stunum; // 学号
};

1.2 继承类模版

template<class T>
class stack : public std::vector<T>
{
public:
	void push(const T& x)
	{
		// 基类是类模板时,需要指定⼀下类域,std::
		// 否则编译报错:error C3861: “push_back”: 找不到标识符 
		// 因为stack<int>实例化时,也实例化vector<int>了
        // 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到
		vector<T>::push_back(x);
		// push_back(x);
	}

	void pop()
	{
		vector<T>::pop_back();
	}

	const T& top()
	{
		return vector<T>::back();
	}

	bool empty()
	{
		return vector<T>::empty();
	}
};

2. 基类和派生类间的转换

  • public继承的派生类对象,可以定义指向基类的指针。可以理解成切片:指针指向的不是原本的基类,而是从派生类中切片出的基类部分。
  • 基类对象不能定义指向派生类的指针。
  • 基类的指针或引用可以通过强制类型转换赋值给派生类的指针或引用。但是必须是基类的指针是指向派生类对象的时候才是安全的。
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;
}

3. 继承中的作用域

在继承中基类和派生类都有其独立的作用域。就算指明作用域也无法给相关成员赋值。派生类和基类中有同名成员时,派生类将屏蔽基类对派生类同名成员的访问,这种情况叫隐藏。
如果是成员函数的话,只需要同名就构成隐藏。所以在继承中注意尽量不定义同名的成员/函数。

// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是⾮常容易混淆
class Person
{
protected :
	string _name = "⼩李⼦"; // 姓名
	int _num = 111; // ⾝份证号
};

class Student : public Person
{
public :
	void Print()
	{
		cout << " 姓名:" << _name << endl;
		// 没有指明类域默认访问派生类中的_num
		cout << " ⾝份证号:" << Person::_num << endl;
		cout << " 学号:" << _num << endl;
	}

protected:
	int _num = 999; // 学号
};

int main()
{
	// 主函数中指明作用域也无法使用
	//Person::_name;
	//Student::_num;

	Student s1;
	s1.Print();

	return 0;
}

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

4.1 默认成员函数的规则

  • 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
  • 派生类的拷贝构造函数必须调用基类的拷贝构造函数完成基类的拷贝初始化。
  • 派生类的operator=必须要调用基类的operator=完成基类的复制。需要注意的是派生类的operator=隐藏了基类的operator=,所以显示调用基类的operator=,需要指定基类作用域。
  • 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
  • 派生类对象初始化先调用基类构造,再调用派生类构造。
  • 派生类对象析构先调用派生类析构,再调用基类析构。
  • 基类析构函数不加virtual的情况下,编译器会对析构函数名进行特殊处理,处理成destructor(),派生类析构函数也会在运行时重命名为destructor(),所以会和基类析构函数构成隐藏关系。
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);
	
	// operator=
	// 先调用派生类的=,再调用基类的=
	s1 = s3;

	// 析构函数先调用派生类的,再调用基类的
	return 0;
}

4.2 自己实现成员函数

当默认成员函数不能满足我们的需求时,可以自己实现成员函数。

// 自己实现成员函数的要点:子类的成员函数需要带上父类的成员函数
// 特点:子类中继承下来的父类成员当做一个整体对象
// 构造:子类成员 内置类型(有缺省值就用,没有不确定)和自定义类型(默认构造) + 父类成员(必须调用父类默认构造) 
//
// 拷贝构造:子类成员 内置类型(值拷贝)和自定义类型(这个类型拷贝构造) + 父类成员(必须调用父类拷贝构造)
// 
// 赋值重载:类似拷贝构造
// 
// 析构:子类成员 内置类型(不处理)和自定义类型(调用他的析构) + 父类成员(调用他的析构)
// 自己实现的话,注意不需要显示调用父类析构,子类析构函数结束后,会自动调用父类析构

class Person
{
public:
	// 基类构造函数
	Person(const char* name)
		: _name(name)
	{
		cout << "Person()" << endl;
	}

	// 基类const构造函数
	Person(const Person& p)
		: _name(p._name)
	{
		cout << "Person(const Person& p)" << endl;
	}

	// operator=重置
	Person& operator=(const Person& p)
	{
		cout << "Person operator=(const Person& p)" << endl;
		if (this != &p)
			_name = p._name;

		return *this;
	}

	// 编译时重写为destructor()
	~Person()
	{
		cout << "~Person()" << endl;
	}

protected:
	string _name; // 姓名
};

class Student : public Person
{
public:
	// 派生类构造函数
	Student(int num, const char* address, const char* name)
		:_num(num)
		,_address(address)
		,Person(name)
	{
		cout << "Student()" << endl;
	}

	// 派生类拷贝构造函数。初始化列表调用基类的拷贝构造函数
	Student(const Student& s)
		: Person(s)
		, _num(s._num)
		,_address(s._address)
	{
		cout << "Student(const Student& s)" << endl;
	}

	// operator=重置
	Student& operator = (const Student& s)
	{
		cout << "Student& operator= (const Student& s)" << endl;
		if (this != &s)
		{
			_num = s._num;
			_address = s._address;
			// 子类赋值重载调用父类赋值重载
			Person::operator=(s);
		}

		return *this;
	}

	// 编译器在编译的时候,会将析构函数的名字重写为destructor()。
	// 这样析构函数的名字就会重复
	~Student()
	{
		// 不需要写,子类析构函数结束后,会自动调用父类析构
		//Person::~Person();
		cout << "~Student()" << endl;
		// delete[] _ptr;
	}

protected:
	int _num; //学号
	string _address;

	//int* _ptr = new int[10];
};

int main()
{
	Student s1(18, "张三", "西安");
	Student s2(s1);

	Student s3(19, "张四", "西安");
	s1 = s3;

	return 0;
}

4.3 实现一个不能被继承的基类(基本不用)

  • 方法1:基类的构造函数私有,派生类的构成必须调用基类的构造函数,但是基类的构成函数私有化以后,派生类看不见就不能调用了,那么派生类就无法实例化出对象。
  • 方法2:C++11新增了⼀个final关键字,final修改基类,派生类就不能继承了。
// C++11的⽅法。方法2
class Base final
{
public :
	void func5() { cout << "Base::func5" << endl; }

protected:
	int a = 1;

private:
	// C++98的⽅法。方法1
	/*Base()
	{}*/
}

class Derive :public Base
{
	void func4() { cout << "Derive::func4" << endl; }
protected:
	int b = 2;
};

int main()
{
	Base b;
	Derive d;

	return 0;
}

未完待续。
剩下的内容可以跳转到:C++ 继承:面向对象编程的核心概念(二)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2322702.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

蓝桥杯 临时抱佛脚 之 二分答案法与相关题目

二分答案法&#xff08;利用二分法查找区间的左右端点&#xff09; &#xff08;1&#xff09;估计 最终答案可能得范围 是什么 &#xff08;2&#xff09;分析 问题的答案 和 给定条件 之间的单调性&#xff0c;大部分时候只需要用到 自然智慧 &#xff08;3&#xff09;建…

【算法day22】两数相除——给你两个整数,被除数 dividend 和除数 divisor。将两数相除,要求 不使用 乘法、除法和取余运算。

29. 两数相除 给你两个整数&#xff0c;被除数 dividend 和除数 divisor。将两数相除&#xff0c;要求 不使用 乘法、除法和取余运算。 整数除法应该向零截断&#xff0c;也就是截去&#xff08;truncate&#xff09;其小数部分。例如&#xff0c;8.345 将被截断为 8 &#x…

关于服务器只能访问localhost:8111地址,局域网不能访问的问题

一、问题来源&#xff1a; 服务器是使用的阿里云的服务器&#xff0c;服务器端的8111端口没有设置任何别的限制&#xff0c;但是在阿里云服务器端并没有设置相应的tcp连接8111端口。 二、解决办法&#xff1a; 1、使用阿里云初始化好的端口&#xff1b;2、配置新的阿里云端口…

基于ADMM无穷范数检测算法的MIMO通信系统信号检测MATLAB仿真,对比ML,MMSE,ZF以及LAMA

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 4.1 ADMM算法 4.2 最大似然ML检测算法 4.3 最小均方误差&#xff08;MMSE&#xff09;检测算法 4.4 迫零&#xff08;ZF&#xff09;检测算法 4.5 OCD_MMSE 检测算法 4.6 LAMA检测算法 …

Linux 配置时间服务器

一、同步阿里云服务器时间 服务端设置 1.检查chrony服务是否安装&#xff0c;设置chrony开机自启&#xff0c;查看chrony服务状态 [rootnode1-server ~]# rpm -q chrony # rpm -q 用于查看包是否安装 chrony-4.3-1.el9.x86_64 [rootnode1-server ~]# systemctl enable --n…

可视化web组态开发工具

BY组态是一款功能强大的基于Web的可视化组态编辑器&#xff0c;采用标准HTML5技术&#xff0c;基于B/S架构进行开发&#xff0c;支持WEB端呈现&#xff0c;支持在浏览器端完成便捷的人机交互&#xff0c;简单的拖拽即可完成可视化页面的设计。可快速构建和部署可扩展的SCADA、H…

C++笔记-模板初阶,string(上)

一.模板初阶 1.泛型编程 以往我们要交换不同类型的两个数据就要写不同类型的交换函数&#xff0c;这是使用函数重载虽然可以实现&#xff0c;但是有以下几个不好的地方&#xff1a; 1.重载的函数仅仅是类型不同&#xff0c;代码复用率比较低&#xff0c;只要有新类型出现时&a…

关于cmd中出现无法识别某某指令的问题

今天来解决以下这个比较常见的问题&#xff0c;安装各种软件都可能会发生&#xff0c;一般是安装时没勾选注册环境变量&#xff0c;导致cmd无法识别该指令。例如mysql&#xff0c;git等&#xff0c;一般初学者可能不太清楚。 解决这类问题最主要的是了解环境变量的概念&#x…

绿联NAS安装内网穿透实现无公网IP也能用手机平板远程访问经验分享

文章目录 前言1. 开启ssh服务2. ssh连接3. 安装cpolar内网穿透4. 配置绿联NAS公网地址 前言 大家好&#xff0c;今天给大家带来一个超级炫酷的技能——如何在绿联NAS上快速安装cpolar内网穿透工具。想象一下&#xff0c;即使没有公网IP&#xff0c;你也能随时随地远程访问自己…

d9-326

目录 一、添加逗号 二、爬楼梯 三、扑克牌顺子 添加逗号_牛客题霸_牛客网 (nowcoder.com) 一、添加逗号 没啥注意读题就是 注意逗号是从后往前加&#xff0c;第一位如果是3的倍数不需要加逗号&#xff0c;备注里面才是需要看的 count计数 是三的倍数就加逗号&#xff0c…

Win11+VS2022+CGAL5.6配置

1. CGAL库简介 CGAL&#xff08;Computational Geometry Algorithms Library&#xff09;是一个开源的计算几何算法库&#xff0c;主要用于处理几何问题和相关算法的实现。它提供了丰富的几何数据结构和高效算法&#xff0c;覆盖点、线、多边形、曲面等基本几何对象的表示与操…

【Linux】MAC帧

目录 一、MAC帧 &#xff08;一&#xff09;IP地址和MAC地址 &#xff08;二&#xff09;MAC帧格式 &#xff08;三&#xff09;MTU对IP协议的影响、 &#xff08;四&#xff09;MTU对UDP协议的影响 &#xff08;五&#xff09;MTU对TCP协议的影响 二、以太网协议 &…

Codeforces Round 1013 (Div. 3)(A-F)

题目链接&#xff1a;Dashboard - Codeforces Round 1013 (Div. 3) - Codeforces A. Olympiad Date 思路 找到第一个位置能凑齐01032025的位置 代码 void solve(){int n;cin>>n;vi a(n10);int id0;map<int,int> mp;for(int i1;i<n;i){cin>>a[i];mp[a…

Vite 与 Nuxt 深度对比分析

一、核心定位差异 二、核心功能对比 渲染能力 Vite&#xff1a;默认仅支持客户端渲染&#xff08;CSR&#xff09;&#xff0c;需通过插件&#xff08;如vite-plugin-ssr&#xff09;实现 SSR/SSG&#xff0c;但配置灵活 Nuxt&#xff1a;原生支持 SSR&#xff08;服务端渲…

通过一个led点灯的demo来熟悉openharmony驱动编写的过程(附带hdf详细调用过程)

概述 本应用程序(led_rgb)是在上实现直接通过消息机制与内核驱动进行交互&#xff0c;设置RGB三色灯的亮灯行为。我从网上随便找了个demo测试了一下&#xff0c;坑了三天…&#xff0c;整个状态如下图&#xff0c;同时也迫使我深度梳理了一下整个流程框架。直到绝望的时候&…

pycharm2024.1.1版本_jihuo

目录 前置&#xff1a; 步骤&#xff1a; step one 下载软件 step two 卸载旧版本 1 卸载软件 2 清除残余 step three 下载补丁 step four 安装2024.1.1版本软件 step five 安装补丁 1 找位置放补丁 2 自动设置环境变量 step six 输入jihuo码 前置&#xff1a; 之…

目标检测20年(四)——最终章

欢迎各位读者尽情阅读前三篇文献解读。这一篇将会介绍文献的第五部分&#xff1a;目标检测近些年的新技术发展以及第六部分&#xff1a;总结与未来展望。这也是本篇论文解读的最后一篇文章。 目录 五、目标检测最新进展 5.1 不采用滑动窗口的检测 5.2 旋转和尺度变化的鲁棒性…

【Linux】POSIX信号量与基于环形队列的生产消费者模型

目录 一、POSIX信号量&#xff1a; 接口&#xff1a; 二、基于环形队列的生产消费者模型 环形队列&#xff1a; 单生产单消费实现代码&#xff1a; RingQueue.hpp&#xff1a; main.cc&#xff1a; 多生产多消费实现代码&#xff1a; RingQueue.hpp&#xff1a; main.…

Spring Boot 连接 MySQL 配置参数详解

Spring Boot 连接 MySQL 配置参数详解 前言参数及含义常用参数及讲解和示例useUnicode 参数说明&#xff1a; 完整配置示例注意事项 前言 在 Spring Boot 中使用 Druid 连接池配置 MySQL 数据库连接时&#xff0c;URL 中 ? 后面的参数用于指定连接的各种属性。以下是常见参数…

[linux] linux基本指令 + shell + 文件权限

目录 1. Linux的认识 1.1. Linux的应用场景 1.2. Linux的版本问题 1.3. 操作系统的认识 1.4. 常用快捷键 2. 常用指令介绍 2.1. ADD 2.1.1. touch [file] 2.1.1.1. 文件的属性信息 2.1.2. mkdir [directory] 2.1.3. cp [file/directory] 2.1.4. echo [file] 2.1.4.…