【C++】继承(上) 继承的基本概念 | 子类的默认成员函数

一、继承

概念

继承(inheritance)是一种面向对象编程的概念,它允许一个类(称为子类或派生类)继承另一个类(称为父类或基类)的特征和行为。子类可以获得父类的成员函数和变量,而不需要重新编写它们。子类还可以添加自己的成员函数和变量,以扩展其功能。

可以说,继承机制 是在面向对象的程序设计中,使代码可以复用的最重要的手段。以前我们接触的复用都是函数复用,而继承是类设计层次的复用。

我们通过下面的例子来说明什么是继承。假设要录入全校师生的信息,那要根据学校中的不同角色,涉及不同的类,如学生、老师、职工……

#include<iostream>
using namespace std;
class Student
{
    string name;
    int age;
    string tele;
    string StudentID;
};
class Teacher
{
    string name;
    int age;
    string tele;
    string TeacherID;
};
class Staff
{
    string name;
    int age;
    string tele;
    string StaffID;
};
int main() {
    
    return 0;
}

但实际上,我们会发现,这些类的基本成员(姓名、年龄、电话)是一致的,仅仅是ID不一样。那这样写,是否会造成代码的冗余呢?

Yes。为了避免这种冗余,C++中用“继承”的机制来复用类中的代码。

我们先定义出一个包含基本成员的Person类,然后复用这个Person类,让各个类都能继承Person类中的基本成员:

#include<iostream>
using namespace std;
class Person
{
    string name;
    int age;
    string tele;
};
class Student:public Person   //让Student继承Person类
{
    string StudentID;
};
class Teacher :public Person
{
    string TeacherID;
};
class Staff :public Person
{
    string StaffID;
};
int main() {
    Student s;
    return 0;
}

这就是继承,Student类继承了Person类中的所有成员。我们打开监视窗口看看s的当前成员:

可见,Perosn的成员已经被Student对象包含进来了。

格式

格式:class 子类名 : 继承方式 父类名

在下面的例子中,Person是父类,也称作基类。Student是子类,也称作派生类

Student和Person是“子承父业”的关系,子类在获得父类全部成员的基础上,又拓展了自己的属性,添加了“学号”“专业”这种 自己的成员变量。

访问限定符protected

之前学访问限定符时,就剩了个protected没提。因为protected是为继承而设计的。

三种访问方式:

public:公有,类外可直接访问

private:私有,仅类里能访问,类外不可访问

protected:保护。通常用于父类中,子类能访问父类的protected成员,而类外不能访问。

举个栗子,如果我们把父类成员设为private,那类外和子类都被拦在类域之外,子类无法访问父类成员:

#include<iostream>
using namespace std;
class Person       //把Person成员设为private
{
private:
    string name="zhangsan";
    int age=20;
    string tele="1234567";
};
​
class Student:public Person
{
public:
    void Print() {
        cout << name << endl;  //访问父类成员
        cout << age << endl;
        cout << tele << endl;
    }
    string StudentID;
};
​
int main() {
    Student s;
    s.Print();
    return 0;
}

这种情况下,Student是访问不了Person成员的:

如果我们设父类成员为protected,那就对子类开放了访问窗口(类外仍不可访问):

class Person
{
protected:
    string name="zhangsan";
    int age=20
    string tele="1234567";
};
class Student:public Person
{
    ……
};
int main() {
    Student s;
    s.Print();
    return 0;
}

这就是protected访问方式。

其实,public、protected、private不仅是三种访问方式,也可以是三种继承方式。

三种继承方式

继承方式,分public、protected、private这三种情况。

3种父类的访问方式,与3种子类的继承方式,结合一下就是9种情况:

这个表老师一般都会要求背诵,我们不要死记硬背,这张表其实是很有规律的:两个权限结合,我们取权限更小的那个。从权限来看,public>protected>private,如果是protected继承与基类的private成员,那继承到派生类中的权限就是private(小的那个)。

几点说明:

1.基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指:基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。

2.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。

3.在实际运用中,绝大部分情况都用public继承,很少使用protetced/private继承,也不提倡使用protetced/private继承,protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

4.如果不指定继承方式,那默认为private。

子类和父类对象的赋值转换

1.子类对象可以赋值给父类的对象 / 指针 / 引用。这里有个形象的说法叫切片or切割。寓意把子类中父类那部分切来赋值过去。

#include<iostream>
using namespace std;
class Person
{
public:
    string name="zhangsan";
    int age=20;
    string tele="1234567";
};
class Student:public Person
{
public:
    string StudentID;
};
int main() {
    Student s;
    Person p;
    p = s;   //子类对象赋值给父类对象
    return 0;
}

2.父类对象不能赋值给子类对象。

3.当父类指针指向子类对象时,如何避免切片现象?

刚刚我们说了,子类对象可以赋值给父类的指针。但当子类对象的大小大于父类对象的大小时,则会切片,使子类对象的部分信息丢失。

这种情况下,为了避免切片现象,处理方法是:将父类指针强制转换为子类指针。

示例:

class A
{
public:
    int _a=10;
};
class B:public A
{
public:
    int _b=20;
};
int main() {
    B b;
    A* pa = &b;
    B* pb = (B*)pa;  //强转
    return 0;
}

隐藏(重定义)

在继承体系中子类和父类都有独立的作用域。

如果子类和父类中有同名成员,子类成员将屏蔽 父类的同名成员 的直接访问,这种情况叫隐藏,也叫重定义

例:

#include<iostream>
using namespace std;
class Person
{
public:
    string name="zhangsan";
    int age=20;
    string tele="Person中的tele";
};
class Student:public Person
{
public:
    void Print() {
        cout << tele << endl;
    }
    string tele="Student中的tele";
};
int main() {
    Student s;
    s.Print();  //此时从Person中继承来的tele被隐藏
    return 0;
}

隐藏并不是删除,从父类那里继承来的同名成员 依然存在于子类中。在子类的成员函数中,可以使用 “父类::父类成员”的方式访问。我们来访问一下看看:

class Person
{
public:
    ……
    string tele="Person中的tele";
};
class Student:public Person
{
public:
    void Print() {
        cout << Person::tele << endl;
    }
    string tele="Student中的tele";
};
int main() {
    Student s;
    s.Print();  
    return 0;
}

如果是成员函数的隐藏,只需要函数名相同就构成隐藏。不过,在实际中,继承体系里面最好不要定义同名的成员。

区分隐藏与函数重载

小练习:A和B的func构成什么关系? A、重载 B、重定义 C、重写

class A {
public:
    void func() {
        cout << "func()" << endl;
    }
};
 
class B : public A {
public:
    void func(int i) {
        A::func();
        cout << "func(int i) -> " << i << endl;
    }
};
 
int main(void) {
    B b;
    b.func(10);
}

answer:B重定义,即隐藏。这里很容易误选A。函数重载的前提是,得在同一个作用域里。而子类和父类是两个不同的类域,所以这俩类域中相同的函数名构成隐藏,而非函数重载。

继承与友元

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

class Student;
class Person
{
    friend void Print(Person& p, Student& s);
protected:
    string _name;
};
class Student :public Person
{
protected:
    string _ID;
};
void Print(Person& p, Student& s) {
    cout << p._name << endl;     //可以访问
    cout << s._ID << endl;     //不可访问
}
int main() {
    Person p;
    Student s;
    Print(p, s);
    return 0;
}

基类与静态成员

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

#include<iostream>
using namespace std;
class Student;
class Person
{
public:
    static int count;
    Person()
        :_name("")
    {
        count++;
    }
protected:
    string _name;
};
int Person::count = 0;
​
class Student :public Person
{
protected:
    string _ID;
};
int main() {
    Person p1;
    Student s;
    cout << Person::count <<" "<< Student::count << endl;   //原本count为2
    Person::count = 0;             //将Person的count置空
    cout << Student::count << endl;   //发现Student的count也被清空了,说明这俩count就是同一个
    return 0;
}

静态成员始终是放在静态区的,而不是储存在对象的内存分配中。静态成员是一定不被包含在对象中的。

 判断正误

1.基类的所有成员变量都会被子类继承。

√,包括静态成员变量,都会被子类继承。

2.基类对象包含了所有的基类成员变量。

×,静态成员变量跟普通变量存放的位置不一样,前者存放在静态区,后者存放在对象的内存空间中。所以不包含。

3.子类对象中不仅包含了所有基类成员变量,也包含了所有子类成员变量。

×,静态成员变量就不被包含

二、子类的默认成员函数

构造函数

当实例化出子类对象时,会先调用父类的构造函数,再调用子类的构造函数。

示例:

#include<iostream>
using namespace std;
class Person
{
public:
    Person(string n="",int a=0,string tel="")
        :name(n)
        , age(a)
        , tele(tel)
    {
        cout << "调用了基类的构造" << endl;
    }
​
    string name;
    int age;
    string tele;
};
​
class Student :public Person
{
public:
    Student(string n="", int a=0, string tel="", string teleNum="")
        :tele(teleNum)     //这里没有显示调用父类的构造函数,但编译器会自觉调用父类的默认构造函数
    {
        cout << "调用了派生类的构造" << endl;
    }
    string tele;
};
​
int main() {
    Student s;
    return 0;
}

子类的构造函数必须调用 父类的构造函数 初始化父类的那一部分成员。那假如父类没有默认构造函数,要怎么办呢?

class Person
{
public:
    Person(string n,int a,string tel)  //父类的构造函数
        :name(n)
        , age(a)
        , tele(tel)
    {
        cout << "调用了基类的构造" << endl;
    }
    string name;
    int age;
    string tele;
};

这时,得在子类的构造函数中,显示调用父类的构造函数:

class Student:public Person
{
public:
    Student(string n, int a, string tel,string teleNum)
        :tele(teleNum)
        ,Person(n,a,tel)   //需显式调用
    {
        cout << "调用了派生类的构造" << endl;
    }
    string tele;
};

总之,父类的构造函数是一定要调用 并且是先调用的。假如你不想让某类被继承,那就把它的构造函数私有化。

析构函数

析构的顺序是固定的:先析构子类对象,再析构父类对象。为了保证一定是这个析构顺序,子类的析构函数在调用完成后,会自动调用父类的析构函数。

#include<iostream>
using namespace std;
class Person
{
public:
    ~Person() {
        cout << "析构父类" << endl;
    }
    string name;
    int age;
    string tele;
};
class Student:public Person
{
public:
    ~Student() {
        cout << "析构子类" << endl;
    }
    string tele;
};
int main() {
    Student s;
    return 0;
}

要注意,析构函数不要再显示调用了!来看下面这个错误示例:

class Person
{
public:
    ~Person() {
        cout << "析构父类" << endl;
    }
    ……
};
class Student:public Person
{
public:
    ~Student() {
        Person::~Person();   //在析构函数中显示调用基类的析构
        cout << "析构子类" << endl;
    }
    string tele;
};
int main() {
    Student s;
    return 0;
}

可见,这样会导致基类对象被析构两次。

因为父类的析构会被自动调用,所以不要再显示调用父类的析构函数了。不然如果有指针变量,析构两次就造成释放野指针的问题。

拷贝构造函数

子类的拷贝构造函数中,初始化 从父类那儿继承来的成员 时,必须得调用父类的拷贝构造函数。

#include<iostream>
using namespace std;
class Person
{
public:
    Person(string n="",int a=0,string tel="")
        :name(n)
        , age(a)
        , tele(tel)
    {
        cout << "调用了基类的构造" << endl;
    }
​
    //拷贝构造
    Person(const Person& p)
        :name(p.name)
        ,age(p.age)
        ,tele(p.tele)
    {
        cout << "调用了父类的拷贝构造" << endl;
    }
    string name;
    int age;
    string tele;
};
​
class Student :public Person
{
public:
    Student(string n = "", int a = 0, string tel = "",string teleNum ="")
        :tele(teleNum)
        ,Person(n,a,tel)
    {
        cout << "调用了派生类的构造" << endl;
    }
​
    //拷贝构造
    Student(const Student& s)
        :Person(s)   //s中,属于Person的那部分会“切片”,拷贝过去
        ,tele("")
    {
        cout << "调用了zi类的拷贝构造" << endl;
    }
    string tele;
};
  
int main() {
    Student s1("zhangsan",20,"111","");
    Student s2(s1);
    return 0;
}

赋值重载operator=

子类的operator=必须调用父类的operator=,完成父类成员的赋值。

#include<iostream>
using namespace std;
class Person
{
public:
    Person(const char* name="")
        :_name(name)
    {}
protected:
    string _name;
};
class Student :public Person
{
public:
    Student(const char* name="",string ID = "")
        :_ID(ID)
        ,Person(name)
    {}
    Student& operator=(const Student& s) {
        if (this != &s) {
            Person::operator=(s);   //调用父类的operator=
            _ID = s._ID;
        }
        return *this;
    }
​
protected:
    string _ID;
};
int main() {
    Student s1("111","000");
    Student s2;
    s2 = s1;
    return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【GraphQL】什么是Prisma?

本页提供了Prisma及其工作原理的高级概述。 什么是Prisma&#xff1f; Prisma是一个开源的下一代ORM。它由以下部分组成&#xff1a; Prisma客户端&#xff1a;Node.js和TypeScript的自动生成和类型安全查询生成器Prisma迁移&#xff1a;迁移系统Prisma Studio:GUI&#xff0…

柯桥学英语,商务外贸英语,BEC中级写作冲刺干货

think of… as 把……认为 eager to… 渴望 look forward to Ving 期待/盼望…… accept…as 接受……为 be certain of 对……确信 in contact with 与……接触 in accordance with 与……相符/一致 remind…of 提醒……关于 be advantageous to 有利于…… assure…of使……放…

mysql8报sql_mode=only_full_group_by(存储过程一直报)

1&#xff1a;修改数据库配置(重启失效) select global.sql_mode;会打印如下信息 ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION里面包含 ONLY_FULL_GROUP_BY&#xff0c;那么就重新设置&#xff0c;在数据库中输入以下代码&#xff0c;去掉ONLY_FULL_GROU…

WordPress 外链跳转插件

WordPress 外链跳转插件是本站开发的一款WordPress插件&#xff0c;能对文中外链添加一层过滤&#xff0c;有效防止追踪&#xff0c;以及提醒用户。 类似于知乎、CSDN打开其他链接的提示。 后台可以设置白名单 学习资料源代码&#xff1a;百度网盘 密码&#xff1a;123

电子学会C/C++编程等级考试2022年09月(三级)真题解析

C/C++等级考试(1~8级)全部真题・点这里 第1题:课程冲突 小 A 修了 n 门课程, 第 i 门课程是从第 ai 天一直上到第 bi 天。 定义两门课程的冲突程度为 : 有几天是这两门课程都要上的。 例如 a1=1,b1=3,a2=2,b2=4 时, 这两门课的冲突程度为 2。 现在你需要求的是这 n 门课…

Adobe Illustrator绘图解决卡顿问题

最近在用AI做矢量图&#xff0c;但是遇到了一个很难搞的问题&#xff0c;当我们需要分辨率较高的图片的时候&#xff0c;Python用Matplotlib生成的pdf时dpi参数会设置为600及以上&#xff0c;但是样子的话就造成了pdf文件过大以及AI卡顿&#xff0c;比如&#xff0c;下午生成的…

多文件夹图片预处理:清除空值、重置大小、分割训练集

→ 清理空值 防止出现cannot identify image file 参考Python数据清洗----删除读取失败图片__简单版_python用pil读取图片出错删除掉-CSDN博客 import os import shutil import warnings import cv2 import iofrom PIL import Image warnings.filterwarnings("error&qu…

UE 事件分发机制(二) day10

自定义事件分发机制 自建事件分发机制与结构 Unreal推荐的游戏逻辑开发流程 基于 Unreal推荐的游戏逻辑开发流程&#xff0c;一般我们的整体规划也就是这样 大致结构类图 创建接口类与管理类以及所需函数 新建一个Unreal接口类作为接口 然后创建一个蓝图函数库的基类 Ev…

Python基础:推导式(Comprehensions)详解

1. 推导式概念 Python推导式&#xff08;comprehensions&#xff09;是一种简洁而强大的语法&#xff0c;用于从已存在的数据&#xff08;列表、元组、集合、字典等&#xff09;中创建新的数据结构。推导式包括&#xff1a; 列表推导式元组推导式字典推导式集合推导式 2. 列表…

mybatis参数输入 #{}和${}

1、建库建表 CREATE DATABASE mybatis-example;USE mybatis-example;CREATE TABLE t_emp(emp_id INT AUTO_INCREMENT,emp_name CHAR(100),emp_salary DOUBLE(10,5),PRIMARY KEY(emp_id) );INSERT INTO t_emp(emp_name,emp_salary) VALUES("tom",200.33); INSERT INTO…

基于ssm亚盛汽车配件销售业绩管理系统

摘 要 如今的信息时代&#xff0c;对信息的共享性&#xff0c;信息的流通性有着较高要求&#xff0c;因此传统管理方式就不适合。为了让亚盛汽车配件销售信息的管理模式进行升级&#xff0c;也为了更好的维护亚盛汽车配件销售信息&#xff0c;亚盛汽车配件销售业绩管理系统的开…

快速操控鼠标行为!Vue鼠标按键修饰符让你事半功倍

&#x1f3ac; 江城开朗的豌豆&#xff1a;个人主页 &#x1f525; 个人专栏 :《 VUE 》 《 javaScript 》 &#x1f4dd; 个人网站 :《 江城开朗的豌豆&#x1fadb; 》 ⛺️ 生活的理想&#xff0c;就是为了理想的生活 ! ⭐ 专栏简介 欢迎来到前端入门之旅&#xff01;这个…

网络入门---网络编程预备知识

目录标题 ifconfigip地址和mac地址的区别端口号pid和端口号UDP和TCP的初步了解网络字节序socket套接字 ifconfig 通过指令ifconfig便可以查看到两个网络接口&#xff1a; 我们当前使用的是一个linux服务器并是一个终端设备&#xff0c;所以他只需要一个接口用来入网即可&…

甘草书店记:2023年10月15日 星期日 「等待也是人生的大事」

我常说&#xff0c;最好的人生是刚刚好。 财富不可少&#xff0c;也不必多&#xff0c;够用就好。爱情不要晚&#xff0c;也不要早&#xff0c;恰好就好。 可是人生活在社会中、自然中&#xff0c;不会万事由己。所以&#xff0c;等待是人生的必修课。 书店的装修设计和LOGO…

Tomcat及JDK下载安装(Linux系统)

前言 Tomcat是一个开源的Web应用服务器&#xff0c;由Apache软件基金会管理和维护。它的主要功能是处理来自客户端的HTTP请求&#xff0c;生成并返回响应结果。Tomcat不仅可以实现Java Servlet和JavaServer Pages&#xff08;JSP&#xff09;等Web编程模型的支持&#xff0c;也…

STM32开发学习(地址映射)

LED灯代码&#xff1a; #define PERIPH_BASE ((unsigned int)0x40000000)#define AHB1PERIPH_BASE (PERIPH_BASE 0x00020000)#define GPIOF_BASE (AHB1PERIPH_BASE 0x1400)#define GPIOF_MODER *(unsigned int*)(GPIOF_BASE0x00) #define GPIOF_BSRR *(uns…

使用自动化测试获取手机短信验证码

目前在职测试开发,,写一些脚本,个人认为这职业不科学不应该有的职业,测试就是测试,开发就是开发,运维还是老鸟,这行业总能折腾些莫名其妙的东西出来,刚做这行时学的第一门语言是bash shell, 去新去单位上班直接写了个一键搭建测试环境的测试脚本,本来不想干测试了,好好做微信小…

idea不需安装插件,自动生成mybatis-plus对应的实体类entity,带注解@TableName、@TableId、@TableField

目录 1、修改Generate poJOs.groovy文件 2、idea中连接数据库 3、生成entity代码 4、查看生成的实体类 1、修改Generate poJOs.groovy文件 在项目下方点击Scratches and Consoles→ Extensions→ Database Tools and SQL箭头→schema→ Generate POJOs.groovy 替换为以下文…

tomcat调优配置

一. 设置账户进入管理页面 通过浏览器进入Tomcat7的管理模块页面&#xff1a;http://localhost:8080/manager/status 按照提示&#xff0c;在Tomcat7服务器指定的位置修改配置文件&#xff08;conf/tomcat-users.xml&#xff09;&#xff0c;增加相应的用户和角色配置标签 <…

10个火爆的设计素材网站推荐

所谓聪明的女人没有米饭很难做饭&#xff0c;设计师也是如此。如何找到优秀的设计材料是每个设计师的痛点&#xff0c;国内材料网站收费&#xff0c;但也限制使用范围和期限&#xff0c;大多数外国设计网站不能打开或需要特殊互联网使用&#xff0c;有一定的安全风险。 作为一…