C++面向对象三大特性之——继承

server/2024/12/22 18:18:11/

C++面向对象三大特性之——继承

  • 一.继承的概念及定义
    • 1.1概念
    • 1.2 继承的定义
    • 1.3继承基类成员访问方式的变化
  • 二.基类和派生类对象赋值转换
  • 三.继承中的作用域
  • 四. 派生类的默认成员函数
    • 4.1.派生类构造函数
    • 4.2派生类拷贝构造函数
    • 4.3派生类的赋值重载函数(operator=)
    • 4.4派生类中的析构函数
  • 五.继承与友元
  • 六. 继承与静态成员
  • 七.复杂的菱形继承及菱形虚拟继承
  • 八.继承和组合

一.继承的概念及定义

1.1概念

继承是面向对象编程中的一个核心概念,它指的是一种类与类之间的关系,其中一个类可以继承另一个类的属性和方法通过继承,子类可以重用父类的代码,并且可以在此基础上进行扩展或修改

1.2 继承的定义

在这里插入图片描述

例如:

class Person
{
public:void Print(){cout << _name << endl;cout << _age << endl;}
protected:string _name = "xiaowang";int _age = 0;
};class Student : public Person
{
public:
protected://年级int _grade = 1;
};
int main()
{Person p1;//s1中也拥有_name 和 _age 成员变量Student s1;return 0;
}

1.3继承基类成员访问方式的变化

在这里插入图片描述

在这里插入图片描述

  1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它
  2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
  3. 基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private
  4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,最好是显示继承
  5. **在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,**也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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

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

class Person
{
public:void Print(){cout << _name << endl;cout << _age << endl;}
protected:string _name = "xiaowang";int _age = 0;
};class Student : public Person
{
public:Student(const string& name){_name = name;}
protected://年级int _grade = 1;
};int main()
{Student s1("小王");Person 	p1 = s1;p1.Print();Person* p2 = &s1;p2->Print();Person& p3 = s1;p2->Print();return 0;
}

在这里插入图片描述

基类对象不能赋值给派生类对象。
在这里插入图片描述

下图中的这一行为叫做切片
在这里插入图片描述

三.继承中的作用域

之前学过的作用域
在这里插入图片描述

  1. 在继承体系中基类和派生类都有独立的作用域
  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
  3. 如果是成员函数的隐藏只需要函数名相同就构成隐藏。
  4. 注意在实际中在继承体系里面最好不要定义同名的成员。
class Person
{
public:void Print(){cout << _name << endl;cout << _age << endl;}string _name = "xiaowang";int _age = 0;
};class Student : public Person
{
public:Student(const string& name,int age,int grade){_name = name;_age = age;_grade = grade;}	string _name = "";int _grade = 1;
};int main()
{Student s1("小李", 20, 1);cout << s1._name << endl;cout << s1._age << endl << endl;cout << s1.Person::_name << endl;cout << s1.Person::_age << endl;return 0;
}

在这里插入图片描述

总结:
基类和派生类中都有name成员变量,优先访问派生类中的成员变量
想要访问基类中的成员变量需要指定基类的类域
他们两的关系可以理解为全局变量与局部变量的关系
在这里插入图片描述

基类与派生类的同名函数不构成重载!!!

  1. 它们的关系是隐藏(重定义),只有函数名相同
  2. 在没有指定类域的时候,编译器只会调用派生类中的函数
  3. 如果调用函数时传递的参数与派生类中的参数不符合,程序会直接报错,不回去基类中匹配基类的函数
    解释如下:

有如下代码:

class Person
{
public:void fun(int i){cout << "void fun(int i)" << endl;}
};class Student : public Person
{
public:void fun(){cout << "void fun()" << endl;}
};int main()
{Student s1;s1.fun(1);return 0;
}

这段代码会直接报错,他会先在派生类(子类)中查找fun函数,如果找到了就会调用它,调用时发现参数不匹配,直接报错。
报错信息如下:
在这里插入图片描述

在这里插入图片描述

如果将派生类中的fun注释掉,就会会调用父类的函数,所以说派生类与基类中的同名函数关系为隐藏(也可以说是重定义)。

派生类与基类中有同名函数,还要调用基类中的函数的方法如下:
在这里插入图片描述

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

结论如下(下面代码介绍这些函数的显示调用):

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
  5. 派生类对象初始化先调用基类构造再调派生类构造。
  6. 派生类对象析构清理先调用派生类析构再调基类的析构。

解释:

4.1.派生类构造函数

派生类的构造函数会优先调用基类的构造函数,再初始化自己的成员
派生类显示调用构造的正确方法:

class Person
{
public:Person(const string& name, int age):_name(name),_age(age){}
protected:string _name = "";int _age = 0;
};class Student : public Person
{
public:Student(const string& name, int age, int grade):Person(name,age),_grade(grade){}void Print(){cout << _name << endl;cout << _age << endl;cout << _grade << endl;}
protected:int _grade = 0;
};int main()
{Student s1("小刘",20,2);s1.Print();return 0;
}

输出如下:
在这里插入图片描述

重点注意:

在这里插入图片描述
基类中的 _name 和 _age 必须要在初始化列表中初始化,要显示调用基类中的构造函数。

Student构造函数 错误写法如下:

Student(const string& name, int age, int grade):_name(name),_age(age),_grade(grade){}

在这里插入图片描述

在这里插入图片描述
初始化列表的顺序即使是打乱写,也会先调用Person(name,age)
因为初始化列表的初始化顺序是按照声明顺序初始化的。基类的成员变量是在派生类前面声明的。可以理解为:
在这里插入图片描述

4.2派生类拷贝构造函数

class Person
{
public:Person(const string& name, int age):_name(name),_age(age){}Person(const Person& p){_name = p._name;_age = p._age;}protected:string _name = "";int _age = 0;
};class Student : public Person
{
public://构造函数Student(const string& name, int age, int grade):Person(name, age),_grade(grade){}//拷贝构造Student(const Student& s):Person(s),_grade(s._grade){}void Print(){cout << _name << endl;cout << _age << endl;cout << _grade << endl;}
protected:int _grade = 0;
};int main()
{Student s1("小刘",20,2);s1.Print();Student s2(s1);s2.Print();return 0;
}

在这里插入图片描述

派生类的拷贝构造必须要在初始化列表调用基类的拷贝构造
在这里插入图片描述
派生类的 对象s 作为基类拷贝构造的参数,拷贝构造的参数是一个引用,所以这里的操作就是切片。把派生类的基类的那一部分参数切出来了。

4.3派生类的赋值重载函数(operator=)

派生类的operator=必须要调用基类的operator=完成基类的复制

class Person
{
public:Person(const string& name, int age):_name(name),_age(age){}Person(const Person& p):_name(p._name),_age(p._age){}Person& operator=(const Person& p){if (this != &p){_name = p._name;_age = p._age;}return *this;}protected:string _name = "";int _age = 0;
};class Student : public Person
{
public://构造函数Student(const string& name, int age, int grade):Person(name, age),_grade(grade){}//拷贝构造Student(const Student& s):Person(s),_grade(s._grade){}Student& operator=(const Student& s){if (this != &s){Person::operator=(s);_grade = s._grade;}return *this;}void Print(){cout << _name << endl;cout << _age << endl;cout << _grade << endl;}
protected:int _grade = 0;
};int main()
{Student s1("小刘",20,2);s1.Print();Student s2(s1);s2.Print();Student s3("小李", 22, 3);s1 = s3;s1.Print();return 0;
}

在这里插入图片描述

  1. 派生类的赋值拷贝函数在赋值基类的那部分时必须调用基类的赋值拷贝
  2. 这里必须指定基类的类域,因为派生类与基类的赋值拷贝是同名函数,它们构成隐藏,如果不指定类域程序会一直重复调用派生类中的赋值拷贝,进入死循环
  3. 这里也同样是切片,用基类的引用接收派生类。

虽然下面的下面代码也可以通过,但是如果有涉及复杂的资源管理或深拷贝等逻辑下面这种做法就是可能会导致内存泄漏,或者如果 _name 和 _ age 在基类中的权限时prevate,程序依然会报错。调用基类的赋值操作符,这样更加符合良好的编程习惯,特别是当基类发生变化时,能够确保赋值逻辑的一致性和正确性。

Student& operator=(const Student& s)
{if (this != &s){_name = s._name;_age = s._age;_grade = s._grade;}return *this;
}

4.4派生类中的析构函数

class Person
{
public:~Person(){cout << "~Person()" << endl;}protected:string _name = "";int _age = 0;
};class Student : public Person
{
public:~Student(){Person::~Person();cout << "~Student()" << endl;}
protected:int _grade = 0;
};int main()
{Student s1;return 0;
}

先看输出结果
在这里插入图片描述

调用了两次基类的析构函数,这是为什么呢?最后为什么会再多调用一个析构函数?
实际上基类的析构函数会在派生类的析构结束后自动调用。
所以基类的析构不能显示调用
在这里插入图片描述
派生类对象析构清理先调用派生类析构再调基类的析构。

五.继承与友元

友元关系不能继承

下面代码注释部分展开程序会报错

class Person
{
public:friend void Print(Person p);//friend void Print(Student s);protected:int _age = 10;int _a = 20;};class Student : public Person
{
public:protected:int _grade = 0;
};void Print(Person p)
{cout << p._age << endl;cout << p._a << endl;
}//void Print(Student s)
//{
//	cout << s._age << endl;
//	cout << s._a << endl;
//}int main()
{Person p;Print(p);Student s;//Print(s);return 0;
}

在这里插入图片描述
这条语句应该写在 Student类中 就可以运行成功了
在这里插入图片描述

六. 继承与静态成员

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

class Person
{
public:Person(int a = 1):_a(a){}static int _age;
protected:int _a;};
int Person::_age = 1;class Student : public Person
{
public:protected:int _grade = 0;
};int main()
{Person p1;cout << p1._age << endl;p1._age++;Student s1;cout << s1._age << endl;return 0;
}

_age只有一份,因为它存在静态区
在这里插入图片描述

七.复杂的菱形继承及菱形虚拟继承

什么是菱形继承?

class Person
{
public:Person(int a = 1):_a(a){}int _a;};class Student : public Person
{
public:protected:int _grade = 0;
};class Teacher :public Person
{
public:protected:int _t = 1;
};class assistant : public Student, public Teacher
{
public:void Print(){cout << _a << endl;cout << _grade << endl;cout << _t << endl;cout << _ass << endl;}
private:int _ass;
};int main()
{Person p1;Student s1;Teacher t1;assistant a1;return 0;
}

在这里插入图片描述
这里的 _a 导致程序运行不了,出现了调用歧义
在这里插入图片描述
需要指明类域访问a,这样就可以访问到指定的类域

发生这种情况的原因:
StudentTeacher都继承了Person,它们当中都有**_a变量,
Assistent继承了
StudentTeacher**,Assistent中会出现两份_a变量,这就是菱形继承
它导致了数据冗余和二义性,调用时导致调用不明确.
当然也可以指定类域访问冗余的数据,但这并不是一个好的办法!
在这里插入图片描述

菱形虚拟继承

class Student :virtual public Person
{
public:
protected:int _grade = 0;
};class Teacher :virtual public Person
{
public:protected:int _t = 1;
};

在这里插入图片描述
在可能出现数据冗余和二义性的类继承时加上 virtual 关键字
这样就不会导致数据冗余和二义性了
在这里插入图片描述
虚拟继承的方法是使用了一个偏移量地址.

八.继承和组合

  1. public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
  2. 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
  3. 优先使用对象组合,而不是类继承 。

优先使用组合的原因:

继承通常会导致子类对父类的高度依赖,继承关系一旦形成,子类与父类的耦合度就变得很高,修改父类可能会影响子类。
组合则是通过将多个对象的功能组合在一起,让它们协同工作。这样,功能之间的耦合度降低,系统更具弹性和扩展性。

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

继承和组合的区别
继承:是一种类之间的关系,子类从父类继承属性和方法,表示“是一个”(is-a)关系。子类可以重用父类的代码,也可以扩展或修改父类的行为。
组合:是通过在一个对象内部包含其他对象的方式来复用代码,表示“有一个”(has-a)关系。一个对象由多个组件对象组成,每个组件负责自己的功能。

什么时候使用继承,是么时候使用组合
在这里插入图片描述
例如:轮胎汽车的关系是 has-a有一个轮胎, 像这种有一个的就用组合
例如:植物水果的关系是 is-a 水果是一个植物,像这种是一个的就用继承
例如:listqueue 的关系即使 has-a 又是 is-a ,这种情况一般情况下都是是哦给你继承
使用继承:
当类之间有明确的“是一个”关系时。
当子类需要重用父类的代码并扩展其功能时。
当需要多态性时。
继承带来的紧耦合不会成为问题时。
使用组合:
当类之间有“有一个”关系时。
当需要灵活地改变、替换或扩展对象的功能时。
当继承导致紧耦合、层次结构复杂或行为不符合时。
需要实现高内聚、低耦合时。


http://www.ppmy.cn/server/152288.html

相关文章

使用ElasticSearch实现全文检索

文章目录 全文检索任务描述技术难点任务目标实现过程1. java读取Json文件&#xff0c;并导入MySQL数据库中2. 利用Logstah完成MySQL到ES的数据同步3. 开始编写功能接口3.1 全文检索接口3.2 查询详情 4. 前端调用 全文检索 任务描述 在获取到数据之后如何在ES中进行数据建模&a…

音视频入门基础:MPEG2-TS专题(19)——FFmpeg源码中,解析TS流中的PES流的实现

一、引言 FFmpeg源码在解析完PMT表后&#xff0c;会得到该节目包含的视频和音频信息&#xff0c;从而找到音视频流。TS流的音视频流包含在PES流中。FFmpeg源码通过调用函数指针tss->u.pes_filter.pes_cb指向的回调函数解析PES流的PES packet&#xff1a; /* handle one TS…

Kafka部署

部署命令 wget https://archive.apache.org/dist/kafka/2.6.0/kafka_2.13-2.6.0.tgz tar -xzvf kafka_2.13-2.6.0.tgz mv kafka_2.13-2.6.0 kafka useradd kafka yum install java mkdir /home/kafka/logs vi /home/kafka/kafka/config/server.properties /home/kafka/kafka…

java-5图片验证码

1 图片验证码意义 验证码可以防止恶意破解密码、刷票、论坛灌水&#xff0c;有效防止某个黑客对某一个特定注册用户用特定程序暴力破解方式进行不断的登录尝试。由于验证码技术具有随机性随机性较强、简单的特点&#xff0c;能够在一定程度上阻碍网络上恶意行为的访问&#xf…

C++ 面向对象编程:友元、

友元&#xff1a;让一个类或函数&#xff0c;能够访问另一个类的私有成员。友元关键字为friend。 友元有三种&#xff1a;第一种是全局函数作为友元&#xff0c;第二种是类作为友元&#xff0c;第三种是成员函数作为友元 第一种是全局函数作为友元&#xff0c;见以下代码&…

高并发架构设计:从 Java Callable 到 CompletableFuture 的进阶修炼

前言 在现代的分布式系统中&#xff0c;高并发性能是一个决定系统能否成功的关键因素。而 Java 作为主流的后端开发语言&#xff0c;也提供了许多强大的工具来帮助我们处理并发任务。今天&#xff0c;我们将通过两个关键工具来讲解高并发架构设计的技巧&#xff1a;Callable 和…

vue3中的ref与reactive

摘要 在vue3直接定义变量不是响应式数据 在vue3中定义变量更改变量页面是显示不出来的 在按钮中绑定一个函数&#xff0c;当我点击它的时候发生改变str值 点击前 点击后 点击后控制台显示更改数据&#xff0c;而页面中不显示数据&#xff0c;这就是vue3没触发响应式 使用re…

mac 安装graalvm

Download GraalVM 上面链接选择jdk的版本 以及系统的环境下载graalvm的tar包 解压tar包 tar -xzf graalvm-jdk-<version>_macos-<architecture>.tar.gz 移入java的文件夹目录 sudo mv graalvm-jdk-<version> /Library/Java/JavaVirtualMachines 设置环境变…