当前位置: 首页 > news >正文

c++进阶——类与继承

文章目录

  • 继承
    • 继承的基本概念
      • 继承的基本定义
        • 继承方式
        • 继承的一些注意事项
      • 继承类模板
    • 基类和派生类之间的转换
    • 继承中的作用域
    • 派生类的默认成员函数
      • 默认构造函数
      • 拷贝构造
      • 赋值重载
      • 析构函数
      • 默认成员函数总结
    • 不能被继承的类
    • 继承和友元
    • 继承与静态成员
    • 多继承及其菱形继承问题
      • 继承模型
      • 多继承
      • 菱形继承
      • 菱形继承解决方案之——虚继承
      • 菱形继承的一个实例
      • 多继承中的指针偏移
    • 继承和组合

继承

本篇文章将进入c++学习地进阶部分。相比以往学的基础语法和基本概念会有所提升,且需要以往的概念掌握较为扎实。第一个部分就从继承开始讲起。

继承的基本概念

c++面向对象有三大特性:封装、继承、多态。我们今天要讲的正是三大特性之一——继承。

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称派⽣类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用,继承是类设计层次的复用。

我个人看来,可以这么理解:一个类继承一个类,就是将被继承的类的东西放在继承的类上,但是又可以在此基础上衍生新的成员变量和函数。这不就很像继承先辈遗产嘛?将先辈遗产继承过来,但是我们可能还有自己的财产。那加在一起才是我的总财产。

我们下面来看一段代码就能理解了:

比如我们想要设计两个类,叫老师和学生。那这两个类都会有基本的成员变量比如:年龄、姓名、电话等。可能针对于老师会有一些特殊的函数如教书,学生会有学习的函数:

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; // 职称
};

如果我们分别实现两个类,其实是很麻烦的。最主要的就是代码逻辑会有冗余,因为有些成员是重复的。在以前函数或者某些类在功能上重复,数据类型不同时,衍生了一个叫模板的概念,是一种效率比较高的代码复用手段。但是现在是内部的代码有些相同,有些不同,应当如何复用呢?答案是使用继承。

我们先来简单看看是怎么实现的,有一些概念我们后续会提及:

class Preson {
public:void identity() {cout << "identity: " << _id << endl;}protected:string _name = "peter"; // 姓名string _address; // 地址string _tel; // 电话string _id;//身份int _age = 18; // 年龄 
};class Student : public Preson {
public:void study(){}
protected:int _stuid;//学号
};class Teacher : public Preson {
public:void teach() {}
protected:string _title;//职称
};

我们先不管继承的方式和为什么使用protected限定符,我们现在只需看看逻辑是怎么走的即可。

我们发现,对于重复的信息(即相同的成员变量和成员函数),我们都放在了一个叫Person的类内。那对于StudentTeacher的定义,不正好是在Person类的基础上衍生而来的吗?那可以进行继承,就是把Person类中的内容继承下来,再配合自己需要使用的成员,这不构造好了吗?这里代码量并不大,但是已经能体现出代码复用的优势了。

我们来看看子类内部是个什么结构:
在这里插入图片描述
符合我们之前说的,内部就是包含了父类的内容。而且在监视窗口下,会把父类当成一个整体。我们也最好是这么理解的。具体原因后续来说。

然后现在就该来讲,继承是如何使用的。

继承的基本定义

对于很多刚刚出现的新的内容,我们现在来一一讲解。

继承是需要有父类(被继承类) 的,就和我们模板特化一样,需要有主模板。在有了父类的情况下,继承类的用法为:class 继承类名 : 继承方式 父类(被继承类)

被继承的类就是父类,那类比人类父子关系,继承类就是叫子类。当然有些教材或者书籍上可能会称父类为基类,子类为派生类。这个其实也很好理解。基类对应的就是基本的概念,就代表被继承。派生类就是在基础的方式上进行衍生。

所以我们最后得到继承类的定义:class 子类名 : 继承方式 父类(被继承类)
代码示例:

class Student : public Preson {
protected:int _stuid;//学号
};class Teacher : public Preson {
protected:string _title;//职称
};
继承方式

我们重点讲讲这里的继承方式:

我们发现,继承方式对应的那个空填的的是public,这是不是意味着这个空还能填
private或者protected这两个类作用域限定符呢?

很聪明,是可以,只不过一般情况下用的都是public而已。

那它们分别代表着什么意思呢?我们来看看下面这个表格:

父类中成员变量属性 / 继承方式public继承protected继承private继承
父类的public成员子类的public成员子类的protected成员子类的private成员
父类的protected成员子类的protected成员子类的protected成员子类的private成员
父类的private成员不可见不可见不可见

这个表格看着很多情况,其实很好记忆,我们将其分为两大类:

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

我们来看看是怎么个事:
在这里插入图片描述
至于在类外面使用就肯定是不可能的,因为有限定符private的限制。这是我们早已知道的。但是我们发现,继承过后,父类A中的私有成员在子类a中也是无法使用的。至于其他继承方式就不再演示了,也都是一样的效果。

我们现在来看看是怎么样继承的:
在这里插入图片描述
其实这个看不见并不是说不继承过来,而是语法规定,基类的私有成员继承到派生类也是无法在派生类中进行使用。那类外面就更不可能了。

  1. 对于基类中publicprotected成员来讲,继承到派生类的限定方式是Min(基类中成员的访问方式, 继承方式),其中:public > protected > private

这里我们就来讲讲限定符protected的作用是什么:
在之前刚进入类的学习的时候,我们并没有对这个限定符进行过多介绍,只是说知道那时候它和private的用法差不多就ok了。确实如此,因为protected正常情况下确实也是限制了类外不能使用其限定的成员。

但是我们倒回继承方式表格来看:
基类本身也是类。很可能有时候也会直接使用基类。如果基类中想控制在类中使用而不能在外界使用,用private来修饰确实可以。但是无论你以何种方式继承到派生类,都是 “看不见” 的,也就是不能在派生类中使用。

我们此时就来想,有没有这么一种场景,我需要控制只能在类中使用,但是又希望继承到派生类的时候在派生类里面也能使用呢?这个时候protected的起到这个作用了。所以可以看出保护成员限定符是因继承才出现的,我们也就很好的理解三个限定符的大小关系了。

所以对于这个表格记忆其实很简单,就这两个大点进行理解。

继承的一些注意事项

当然,我们还需要了解一些关于继承这个概念的注意事项。

其实对于继承方式的那个选项是可以不写的,甚至继承的基类可以是struct
只不过使用对于继承的是struct还是class是有区别的。

区别就是:继承class默认是private继承,而继承struct默认是public继承,我们验证一下class的即可:
在这里插入图片描述
很明显,使用私有继承,对于A类中的public成员 _b在报错界面显示的是无法访问在a中的private成员。如果默认是private继承,那么基类中什么成员继承下来都是private成员。所以很好理解这个报错。而至于变量_a则是因为本身就是在基类中的私有变量,所以继承下来是看不见的。

对于继承struct的情况我就不再演示了,感兴趣的读者可以自行前往尝试。

但是还需要注意的是:
在实际运用中⼀般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

所以如果实在记忆不下那么多知识点就专门记一下public继承的要点即可。当然充分理解上面说的每一点进行记忆还是不难的。

继承类模板

现在我们再来看一个情况,即继承的类是个类模板。
之所以要提及继承类是个类模板是因为编译器的按需实例化处理

在讲模板进阶的时候就已经说到过,编译器为了提高效率以及节省资源,对于类模板的处理都是按需实例化的。只会检查一下基本的语法是否有问题(如缺失分号、括号是否匹配,或者检查不依赖于模板参数的函数是否存在)。老一点的编译器可能在没有调用某个接口的情况下对于内部语法检查都不会做(如vs 2013)。这是模板的知识,我们先简单回顾一下做铺垫。

我们现在来看下面这样一个例子:
在这里插入图片描述

这里实现栈是使用继承来实现的。和适配器方式的区别后面再说。当前只需要知道,栈也可以使用继承方式来实现。

我们来看这个场景,发现报错了。正常来讲,这些接口在基类中是public 成员,按道理public继承下来在派生类中应该是可以使用的。为什么找不到标识符呢?

这时候就需要深刻理解按需实例化了。我们说过,编译器对于模板的处理是按需实例化。即调用了才会进行类型推演。此时我们定义一个stack< int > st,很多人会觉得,我不是把int传过去了吗,怎么还不能识别呢?

这是因为,定义模板类对象的时候传入的参数类型是给默认构造函数/构造函数使用的。我们并没有调用它的内部接口啊,所以那些接口都是没有实例化的。

我们可以尝试验证一下:
在这里插入图片描述
我们把默认构造写成private成员,编译器发现我们写了就不会自己写。但是我们在外界定义类对象的时候很明显报错了。无法访问默认构造函数。这就很好证明了,模板类定义的时候是只先实例化构造函数的。

基于此我们应该改进一下我们的代码,并且将功能完善一下:

#include<vector>
template<class T>
class stack : public  std::vector<T>{
public:void push(const T& val) {std::vector<T>::push_back(val);}void pop() {std::vector<T>::pop_back();}size_t size() const{return std::vector<T>::size();}bool empty() const {return std::vector<T>::empty();}T& top() {return std::vector<T>::back();}const T& top() const{return std::vector<T>::back();}protected:
};

在这里插入图片描述
我们发现是可以正常使用的。

所以基类为类模板的时候需要我们特别的注意。

基类和派生类之间的转换

我们直接来看看要点:

• public继承的派生类对象可以赋值给(基类的指针 / 基类的引⽤)。有个形象的说法叫切⽚或者切
割。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分。
• 基类对象不能赋值给派生类对象。
• 基类的指针或者引用可以通过强制类型转换赋值给派⽣类的指针或者引⽤。但是必须是基类的指针
是指向派⽣类对象时才是安全的。这⾥基类如果是多态类型,可以使⽤RTTI(Run-Time Type
Information)的dynamic_cast 来进⾏识别后进⾏安全转换。(ps:这个目前仅作了解)

我们来举个例子看看就明白了:

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;return 0;
}

我们进入监视窗口查看一下:
在这里插入图片描述
很明显能发现,确实是只将派生类切片分割出基类有的成员,并且将这一个部分转化成了基类对应的对象/指针/引用。

在这里插入图片描述
其实是很形象的。注意,继承后的派生类中,基类的成员是放在前面存储的。编译器当识别到是派生类对应的内容向基类转化时,会做特殊处理。也就是会进行自动切割出从基类继承出来的内容,然后再转化。

但是需要注意的是,基类对象不能转化为派生类对象:
在这里插入图片描述
一是编译器没有对这种情况重载,二是根本就不符合语法规定。

其实很好理解,要执行切片分割,基类和派生类都有的当然好办,但是派生类中可能会有基类中不存在的成员呢?这该怎么处理呢?所以对此c++直接明确规定了基类对象不能转化为派生类的对象。

但是c++又规定经过强制转换,基类的指针/引用可以转化为派生类的指针/引用:
在这里插入图片描述
这里发现编译是不会报错的。至于引用的我就不再展示了。效果类似。

至于最后一个对于dynamic_cast来判断指针转换类型的方法,由于其涉及到多态等未学到的概念,这就后续再说了。当前了解即可。

继承中的作用域

首先我们得知道,基类和派生类本质都是类。c++中有四大作用域,即局部域、全局域、命名空间域、类域。类域也是独立的域。

不同的类对象构成的都是自己的域,所以不同的类对象内的变量是需要通过类域作用限定符来访问的。基类和派生类本质都是类,所有都是有自己的独立的作用域的。

既然是不同的域,就可以在不同的域中写相同的函数名或者变量名。这一定是不会构成命名歧义的。但是对于继承来讲,是一个新的现象。如果基类和派生类有同名成员是怎么办呢?

我们先说结论,无论是在类内中使用或是类外调用,都是将基类的同名成员隐藏起来,默认访问的是派生类中的那个。如若要访问基类中的那个需要使用域作用限定符来指定访问。

class A {
public:void test() {cout << "class A: test()" << endl;}
protected:int _a = 0;int _b = 1;
};class AA : public A {
public:void test() {cout << "class AA: test()" << endl;}
protected:int _a = 2;int _b = 3;
};

我们以这一段代码为例:
我们发现基类A和派生类AA中的内容均是同名的。我们来看看在派生类使用是如何使用呢?
在这里插入图片描述
很明显,默认使用的就是派生类中的那个。因为从基类继承下来的那一部分被隐藏了。但并不是说派生类中就没有从基类继承下来的同名成员了。我们通过调试还是会发现被继承下来的。只不过默认调用的是派生类的,要用基类中的需要通过域作用限定符进行访问:
在这里插入图片描述
这是我们需要注意的一点。当然对于成员变量也是一样的。我在这里就不进行演示了。原理都是一样的。

那如果在类外面直接调用这个test函数呢?
在这里插入图片描述
默认使用的依然是派生类中的那一份。想要使用到基类中的也需要指定访问:
在这里插入图片描述

当然还是有细节要注意的,我们通过下面这个例子来看看:

1.A和B类中的两个func构成什么关系()
A. 重载 B. 隐藏 C.没关系2.下⾯程序的编译运⾏结果是什么()
A. 编译报错 B. 运⾏报错 C. 正常运⾏
class A{
public:void fun(){cout << "func()" << endl;}
};class B : public A{
public:void fun(int i){cout << "func(int i)" << i << endl;}
};
int main(){B b;b.fun(10);b.fun();return 0;
};

先来看第一题,很多人直接就不假思索地说是函数重载了。看到函数名相同,参数不同。这是错的。这是没有对函数重载的定义深刻理解。

函数重载是对一个函数在有不同的参数(个数,顺序,类型不同)可以在同一个作用域内进行编写同名函数。**我们需要注意,函数重载是在一个作用域内的!**不同作用域本来就可以写同名函数,甚至一模一样也是可以。因为在两个独立的作用域内。

现在这两个func函数是隶属于两个独立的类中的,都具有自己的独立的作用域。所以一定不是函数重载。实际上是构成隐藏关系。

所以引出了第三点:继承中只要函数名相同就构成隐藏关系,无论参数是否相同。

所以第二题来说,会直接编译报错。因为就没办法使用到那个不带参数的func,因为它在基类中,是需要通过类作用域限定符来访问:
在这里插入图片描述
而且报错的是因为不接受0个参数,说明默认调用的都是派生类中的那个,也就验证了在继承中函数名只要相同就构成隐藏关系。

但是还需要注意的是,在实际中在继承体系里面最好不要定义同名的成员。因为这样子使用起来会非常麻烦,对于同名的还需要去指定访问。所以一般建议是不要定义同名成员。

派生类的默认成员函数

现在我们再来一起回忆一下默认成员函数有哪些:
在这里插入图片描述
就是这六种,其中对于取地址重载的两个默认成员函数一般是不需要我们自己写的,编译器默认生成的就够用了,但是其他的四个我们是需要看情况来写的。

那对与派生类来讲,由于其继承了基类的成员,那对于派生类的几个默认成员函数是否需要写呢?是否需要对基类的部分进行构造呢?下面我们一起来探讨一下。

我们主要来看看前四个默认成员函数。

默认构造函数

#include<string>
class Person {
public://Person的默认构造Person():_name(""),_gender(""),_age(0){}
protected:string _name;string _gender;size_t _age;
};class Teacher : public Person {
protected:int _id;//工号string _title;//身份
};int main() {Teacher t1;return 0;
}

我们先来看在基类中有写构造函数,但派生类中不写:

在这里插入图片描述
很明显发现,这个派生类对象还是能够正常构造出来的,为什么?

因为对于一个类来讲,无论是否写了初始化列表,内部的成员变量都要走初始化列表。因为初始化列表可以看作是成员变量定义的地方(即开空间)。

我们需要分三个部分来看:

1.内置类型
2.自定义类型
3.基类(把基类当作一个整体对象存储在派生类中)

对于内置类型,有传参用传参,没传参用缺省,没有缺省值就是随机值。
对于自定义类型,编译器会自动调用其默认构造函数,如果没有就会报错:
在这里插入图片描述
只要我们任意写了一个构造函数,编译器将不在生成默认构造。那此时报错的是无法使用Teacher(void),其实就是Teacher的默认构造。

前面两个我们以前就讲过,应当需要清楚。我们现在最需要知道的是对于基类的部分是如何操作的。对于上面的情况,基类中有默认构造(不传参就可以构造),所以我们在派生类中不需要写也是可以的。因为编译器自动调用了基类的默认构造。对于派生类内部的成员,就按照以往认识的规律来走。内置类型最次也是随机值,而自定义类型会自动调用其默认构造。很显然对Teacher类来讲,string这个自定义类型肯定是有默认构造的。

其实绝大部分情况下确实不用写,但是如果当派生类中的成员变量有指向资源呢?那就需要写了。这种情况我就不再多说了,详细的参考一下string的实现即可。

但是如果基类中没有默认构造呢?比如下面这种情况:
在这里插入图片描述
发现又报错了。这个时候我们就必须自行为基类的成员变量进行构造,也就是我们需要写对Teacher类的默认构造。但是这个默认构造不是乱写的。

我们需要记住的是,当基类不提供默认构造函数的时候,那派生类中对基类的构造就必须通过派生类的初始化列表进行显示调用,具体操作如下:
在这里插入图片描述
对于派生类中自己的成员变量我并没有写到初始化列表去,因为就算不写编译器也会让它们走初始化列表那一套,且规律就是以往认知的那个。所以我就不写了。

对基类的构造函数显示调用是很有趣的,就是在初始化列表内显示调用,就好像再构造一个基类的匿名对象一样。注意:这个显示调用只能在初始化列表走。在这里插入图片描述
不走初始化列表就会报错。这点需要格外注意。

拷贝构造

直接看结论:

派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。
#include<string>
class Person {
public://Person的默认构造Person():_name(""),_gender(""),_age(0){}Person(string name, string gender, size_t age):_name(name),_gender(gender),_age(0){}Person(const Person& per) {_name = per._name;_gender = per._gender;_age = per._age;}protected:string _name;string _gender;size_t _age;
};class Student : public Person {
public:Student():Person("zhangsan", "male", 18),_pos("班长"),_stuid(2301){}
protected:string _pos;//班级职务int _stuid;//学号
};int main() {Student stuA; Student stuB = stuA; return 0;
}

我们来看看这个情形是否能够与正确使用:
在这里插入图片描述
我们并没有再派生类中写拷贝构造,所以用的就是默认生成的那个拷贝构造函数。

还是分为三类。基类部分的会去调用基类的拷贝构造。对于内置类型会使用值拷贝。对于自定义类型,如果没有写拷贝构造,那就是浅拷贝。如果写了,就自动调用其拷贝构造(如string)。

但是如果基类中不写拷贝构造呢?对于我上述举的例子也是可以的。为什么?
因为基类中的对象string是有拷贝构造函数的。那编译器生成Person类的拷贝构造的时候,势必要调用到string类的拷贝构造。

本质上来讲,还是依赖于基类中的拷贝构造。

但是再反过来想想,如果基类中有一个自定义类A,指向了资源,但是并没有A的拷贝构造。如果在基类中也不写拷贝构造,那么不就出问题了吗?因为A没有合适的拷贝构造,用A默认生成的那个是值拷贝,在有指向资源的时候肯定是不符合要求的:
在这里插入图片描述我们在基类Person中加多这么一个类A进行验证,确实如我们所说。所以尽量还是给基类写一下拷贝构造。这样子在派生类没有额外变量指向资源的情况下我们就可以不用给派生类写拷贝构造函数了。对于默认构造也是一样的,也是推荐基类写好。

当然,我们也是可以在派生类中的拷贝构造函数内显示调用基类的拷贝构造:
在这里插入图片描述
还是需要在初始化列表内显示调用,但是我们发现传的竟然是将stu作为参数传入。这就用到了前面部分讲的派生类向基类的转换,也就是切片分割。编译器会自动地将属于基类的切割出来赋值。

赋值重载

至于赋值重载,其实它的行为和拷贝构造很类似。只不过一个是针对对象定义的时候,一个是针对于两个变量都已经存在的赋值情况。

再讲完前面两个默认成员函数后,我们其实已经很熟悉了,对于赋值重载只简单带过一下。

其实派生类的赋值重载也是依赖于基类的赋值重载的。这点的理由已经在拷贝构造部分讲了,这里是类似的,就不再多说。

我们来看看下面一段代码:

class Person{public:Person(const char* name = "peter"): _name(name){}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;}
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;}
protected:int _num; //学号
};

我们会发现对于operator=这个函数在基类和派生类中是构成隐藏关系的,所以是需要指定访问的基类的赋值重载函数的。

析构函数

这里我们先讲结论,对于析构函数~类名,在底层都是会经过封装称函数destructor的,所以基类和派生类中的析构函数也是构成隐藏关系

还有就是编译器会自动调用派生类的析构函数再自动调用基类的析构函数。这就符合我们之前讲的类的构造和析构顺序了。因为基类都是比派生类先构造的。

class Person{public:Person(const char* name = "peter"): _name(name){}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; //学号
};

我们之前也讲过,对于自定义类型,就算我们的析构函数里面什么也没写,编译器也会自行调用其析构函数。所以里面是可以不用写的。内置类型是否析构其实是无所谓的。

在这里插入图片描述
我们发现确实是先析构了派生类再析构基类。注意我们不需要自行显示调用基类的析构函数。因为编译器会自己调用,就是为了保证后构造的先析构。如果我们显式调用了就没办法做到这样的保证了,这点需要格外注意。

默认成员函数总结

经过一大段的分析,相比我们对类的默认成员函数有了更深的理解了。

我们最后来总结一下:

1. 派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造
函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤。
2. 派⽣类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷⻉初始化。
3. 派⽣类的operator=必须调用基类的operator=完成基类的复制。需注意的是派⽣类的operator=
隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域
4. 派⽣类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派
⽣类对象先清理派生类成员再清理基类成员的顺序。
5. 派⽣类对象初始化先调用基类构造再调派⽣类构造。
6. 派⽣类对象析构清理先调用派⽣类析构再调基类的析构。
7. 因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同(这个我们多态章节会讲
解)。那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以基类析构函数不加
virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系。

不能被继承的类

在c++11以后,引入了一个新的关键字final,加在某个类的定义后就代表这个类是不能被继承的。

在了解这个知识前,我们来看看c++98是怎么做的:
c++98的方法就是将类的构造函数放在私有域,就能使得继承下来后的派生类无法调用基类的构造函数,这样子就没办法继承了。但是这个方法是不太好的,构造函数都不能直接使用了。

所以c++11引入了关键字final,使用这个关键字后,直接代表该类不能被继承:
在这里插入图片描述
这里就直接报错了,不能将final类作为基类。

继承和友元

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

这个其实很好的符合了以往讲的,友元是具有单向性的:
在这里插入图片描述
至于为什么前置要先写一个声名class Student这早已讲过。因为在person类中声名友元函数Display中用到了Student类,但是此时有还没定义。所以先进行声名(编译器只会向上查找),意思差不多是告诉编译器后面会对这个类进行定义。

那如果想要让函数Display也能够访问派生类中的私有变量呢?很简单,在派生类中派生类中再声名一次友元即可:
在这里插入图片描述

这样就可以了,说明友元是严格的单向性的。

继承与静态成员

我们之前也讲过,如果在类中放一个静态成员变量,需要在类外进行初始化。而且它存储在静态区,并不隶属于某个类对象。

也就是说,加入定义一个静态变量static int i,如果不是静态变量,是普通的变量,那么每个实例化后的对象都有一个独属于自己的变量i,虽然名字一样,但是属于不同的类对象的。如果是静态变量,那就是大家公用一份,这个值虽然也能通过对象访问,但是大家访问的是同一份在静态区上的。如果可以修改的话那所有的对象再访问就会被修改了。静态变量的使用就参考string中的static size_t npos

那对于继承是如何呢?
仍是一样的,继承下来给派生类,派生类去访问那个静态成员是和在基类中访问的那个是一样的,也就是地址是一样的,我们举个例子验证一下即可:

class Person
{
public:string _name;static int _count;
};
int Person::_count = 0;
class Student : public Person
{
protected:int _stuNum;
};
int main()
{Person p;Student s;// 这⾥的运⾏结果可以看到⾮静态成员_name的地址是不⼀样的// 说明派⽣类继承下来了,⽗派⽣类对象各有⼀份cout << &p._name << endl;cout << &s._name << endl;// 这⾥的运⾏结果可以看到静态成员_count的地址是⼀样的// 说明派⽣类和基类共⽤同⼀份静态成员cout << &p._count << endl;cout << &s._count << endl;// 公有的情况下,⽗派⽣类指定类域都可以访问静态成员cout << Person::_count << endl;cout << Student::_count << endl;return 0;
}

在这里插入图片描述
注意,如果成员变量定义为私有成员,就没办法在类外面指定访问了。

多继承及其菱形继承问题

这个部分我们将重点来将一下多继承

继承模型

单继承:⼀个派⽣类只有⼀个直接基类时称这个继承关系为单继承
多继承:⼀个派⽣类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型
是,先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯。
菱形继承:菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以
看出菱形继承有数据冗余和⼆义性的问题,在Assistant的对象中Person成员会有两份。⽀持多继承就
⼀定会有菱形继承,像Java就直接不⽀持多继承,规避掉了这⾥的问题,所以实践中我们也是不建议
设计出菱形继承这样的模型的。

单继承:意思就是一个类只有一个直接的基类:
在这里插入图片描述
如图所示,PostGraduate的直接基类是Student,Student的直接基类是Person,从图来看就是单向继承。

多继承:意思就是一个类其实是从多个类继承下来的,也就是说不止有一个基类(父类):
在这里插入图片描述
如图所示,Assistant的基类有两个,分别为Student和Teacher,这就是多继承。当然多继承可以是继承多个下来。

菱形继承:这个是特别需要注意的,只要有多继承的概念就一定会有菱形继承的出现:
在这里插入图片描述
也就是说,Assistant从Student和Teacher继承来,但是Student和Teacher又分别继承了Person。这就好像构成了一个菱形的关系。

在这里插入图片描述
菱形继承只是一个普适模型,并不是一定要规规整整的菱形才是菱形继承。如上面这个图也是,菱形继承的本质就是某个类的前继基类中有两个是从同一个类继承下来的,我们简单可以认为是封闭图形时即为菱形继承。

对于单继承就不需要讲太多了,前面都是以单继承作为例子讲解的。

多继承

对于多继承来说,还是很常用的。因为总有一些类会同时满足另外两个类的特性,举一个生活中的例子: 如水果黄瓜/小番茄,它们既是蔬菜,又是水果。所以我们可以认为它是由水果和蔬菜继承下来的。

class Student {
protected:int _stuid;string _name;
};class Teacher {
protected:int _id;string _name;
};class Assistant : public Student, public Teacher {
protected:int _num;
};

但我们需要注意的是,如果使用多继承的话,就得控制好一下逻辑,就比如此时Assist这个类继承Student和Teacher,对于_name这个变量很明显继承下来就重复了。所以最好的方式就是将_name这个变量放在派生类中。

如图所示:
在这里插入图片描述
这点我们需要特别注意一下。

菱形继承

当然在此我们先说一个结论,尽量不要玩菱形继承,会非常麻烦!

首先对于菱形继承来讲,最顶上的那个基类会把成员分别继承到它的直接派生类,如果有一个类又使用多继承来继承这两个类,那么就会导致最顶上那个类的信息复制了两份:

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()
{// 编译报错:error C2385: 对“_name”的访问不明确Assistant a;a._name = "peter";// 需要显⽰指定访问哪个基类的成员可以解决⼆义性问题,但是数据冗余问题⽆法解决a.Student::_name = "xxx";a.Teacher::_name = "yyy";return 0;
}

我们来看看这一段代码就知道了:
在这里插入图片描述
会报错,发现调用_name变量不明确,这是为什么?

我们分析一下就可以知道,这几个类之间的关系如下图所示:
在这里插入图片描述
先继承了Student,再继承了Teacher,所以对于Assistant这个派生类来讲,其内部结构大致是这样的。先继承的放前面。自己的变量放最后。然后对于Student和Teacher两个类来说,又是继承了Person,它们两个就有共同的继承下来的变量_name,这会导致歧义。调用的时候到底是哪个呢?这是无法说清楚的。

就算没有这个问题,更令人头大的问题是占用可空间的问题。因为Person的东西会有两份在菱形继承后的派生类中,所以这个派生类占用的空间将会变得特别大。

所以我们尽量还是不要玩菱形继承这一套。

菱形继承解决方案之——虚继承

但是菱形继承还是有解决办法的,就是使用虚继承。需要用到关键字virtual

使用方法直接记住即可,即在菱形继承中找到谁会在菱形继承后的派生类中有二义性,然后再这个类的直接派生类中加上关键字virtual即可。注意,不能加多。

这个看着很奇怪,我们直接举例子就知道了:
在这里插入图片描述
对于刚刚那个例子,直接在Student和Teacher类加关键字virtual即可。因为Person会有两份在Assistant中,所以在Person的直接继承(Student和Teacher)上直接加入关键字virtual,这样子编译器就会在最后继承的时候,自动识别其基类的内容合并为一份给最后的派生类,这样子就不会产生歧义了。

其背后的原理很复杂,在这里就不进行过多赘述了。

在这里插入图片描述
再举这个例子,我们的virtual应该加在哪里呢?答案是B和C。因为A会在E中产生二义性,所以要在A的直接继承处加关键字virtual

菱形继承的一个实例

一般是不建议玩菱形继承,但是在我们的程序中我们确实天天接触菱形继承,在这里就稍微做一下了解即可。

即我们常用的输入流和输出流,本质也是类:
在这里插入图片描述

template<class CharT, class Traits = std::char_traits<CharT>>
class basic_ostream : virtual public std::basic_ios<CharT, Traits>
{};
template<class CharT, class Traits = std::char_traits<CharT>>
class basic_istream : virtual public std::basic_ios<CharT, Traits>
{};

我们会发现中间的那个basic_iostream的直接基类是basic_ostream和basic_istream,这两个都是虚继承了ios_base。就是为了防止二义性的。

多继承中的指针偏移

然后我们现在来看一个指针偏移的问题:

class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main(){Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;
}		

现在要问的是:p1、p2、p3的关系是:

A:p1 == p2 == p3 
B:p1 <  p2 <  p3 
C:p1 == p3 != p2 
D:p1 != p2 != p3

我们画出Derive的结构就知道了:
在这里插入图片描述
注意,先继承的在前面。

将子类的指针转化给父类是可以的,会做切片分割。
p3指针没得说,当然指向整个空间的开头。p1也是,因为做了切割,base1的内容又是存在Derive的空间的最前面,所以p1和p3指向一样。

p2指向的是图中base2开始的位置,因为编译器将内容切割出来。

最后我们看这个指向:
在这里插入图片描述
所以最后的答案是C。

继承和组合

最后来讲讲继承和组合的方式。

在前面讲到,栈的实现方式可以用适配器模式,也可以像这篇文章写的继承模式。
这两种方式各有优势,前者通常称为has_a,后者是is_a。这很好理解。

因为:
public继承是⼀种is_a的关系。也就是说每个派生类对象都是⼀个基类对象。
组合是⼀种has_a的关系。假设B组合了A,每个B对象中都有⼀个A对象。

继承允许你根据基类的实现来定义派⽣类的实现。这种通过⽣成派⽣类的复⽤通常被称为⽩箱复⽤
(white-box reuse)。术语“⽩箱”是相对可视性⽽⾔:在继承⽅式中,基类的内部细节对派⽣类可
⻅ 。继承⼀定程度破坏了基类的封装,基类的改变,对派⽣类有很⼤的影响。派⽣类和基类间的依
赖关系很强,耦合度⾼。对象组合是类继承之外的另⼀种复⽤选择。新的更复杂的功能可以通过组装或组合对象来获得。对
象组合要求被组合的对象具有良好定义的接⼝。这种复⽤⻛格被称为⿊箱复⽤(black-box reuse),
因为对象的内部细节是不可⻅的。对象只以“⿊箱”的形式出现。 组合类之间没有很强的依赖关
系,耦合度低。优先使⽤对象组合有助于你保持每个类被封装。优先使⽤组合,⽽不是继承。实际尽量多去⽤组合,组合的耦合度低,代码维护性好。不过也不太
那么绝对,类之间的关系就适合继承(is-a)那就⽤继承,另外要实现多态,也必须要继承。类之间的
关系既适合⽤继承(is-a)也适合组合(has-a),就⽤组合。

黑箱复用是更简单的,因为不需要太关注其底层原理,且代码耦合度不会太高。一旦代码耦合度太高,就会导致修改涉及范围变大,这在工程上是很避讳的。

所以优先使用组合,因为生活中大多食物还是满足组合关系的,如车里面有轮胎。不可能说轮胎继车,那就是轮胎是车。这是不对的。

当然也不是说继承一无是处,既然有而且那么大篇幅来讲,肯定是有其重要意义的。特别是在后续讲到多态中。且有些场景确实只能用继承。

所以我们应当适当选择方法,只不过说在能用组合的情况下尽量用组合而已。

相关文章:

c++进阶——类与继承

文章目录 继承继承的基本概念继承的基本定义继承方式继承的一些注意事项 继承类模板 基类和派生类之间的转换继承中的作用域派生类的默认成员函数默认构造函数拷贝构造赋值重载析构函数默认成员函数总结 不能被继承的类继承和友元继承与静态成员多继承及其菱形继承问题继承模型…...

【CODEMATE】进制转换(transform) 粤港澳青少年信息学创新大赛 C/C++/Python 解题思路

目录 问题描述做题思路&#xff0c;解决过程思路&#xff1a;踩过的坑&#xff1a;核心代码C 语言 / C 切片&#xff1a;C 语言 / C 判断 ‘A’ 数量&#xff1a;Python 切片&#xff1a;Python 判断 ‘A’ 数量&#xff1a; 完整代码C 语言 完整代码C 完整代码Python 完整代码…...

window和ubuntu自签证书

window下 以管理员身份 运行 Windows PowerShell # CN192.168.0.100 (换成自己的IP或者域名) # O(组织) OU(组织单位) # Cert:\LocalMachine\My&#xff1a;证书存储位置 # test_10&#xff1a;自定义证书名称 .AddYears(10): 证书过期时间 10 年 $cert New-SelfSi…...

ES历史版本下载

下载地址 Past Releases of Elastic Stack Software | Elastic 安装步骤参考 windows 安装 Elasticsearch_windows安装elasticsearch-CSDN博客...

技术面试一面标准流程

0. 自我介绍 ...... 1. 拷打项目 项目干了啥&#xff1f; 难点是啥&#xff1f; 问项目中用到的东西&#xff1f; 扩展&#xff1f; ...... 2. 基础知识 数据结构、C基础、设计模式 数据结构&#xff1a; 堆&#xff1f; unordered_map 和 布隆过滤器 都是用于查找…...

第14篇:Linux设备驱动程序入门<一>

Q&#xff1a;如何简单的理解DE1-SoC-UP Linux系统的设备驱动程序&#xff1f; A&#xff1a;设备驱动程序&#xff08;Device Driver&#xff09;&#xff0c;简称驱动程序&#xff08;Driver&#xff09;。DE1-SoC-UP Linux系统中的设备驱动程序允许系统软件与DE1-SoC开发板…...

软件设计模式与体系结构:基于Java实现管道-过滤器架构

软件设计模式与体系结构&#xff1a;基于Java实现管道-过滤器架构 前言 在软件架构中&#xff0c;数据流风格是一种常见的架构模式&#xff0c;特别适用于需要对数据进行一系列处理的场景。管道-过滤器&#xff08;Pipe and Filter&#xff09;*架构是数据流风格的典型代表&a…...

Node.js 包管理工具介绍

Node.js 包管理工具介绍 Node.js 是一个基于 Chrome V8 JavaScript 引擎的服务器端运行环境&#xff0c;它允许开发者使用 JavaScript 进行后端开发。为了方便管理和维护项目中使用的第三方库和模块&#xff0c;Node.js 提供了多种包管理工具。本文将详细介绍几种常用的 Node.…...

Node.js 应用场景

Node.js 应用场景 引言 Node.js 是一个基于 Chrome V8 JavaScript 引擎的开源、跨平台 JavaScript 运行环境。它主要用于服务器端开发&#xff0c;通过非阻塞 I/O 模型实现了高并发处理能力。本文将详细介绍 Node.js 的应用场景&#xff0c;帮助你了解其在实际项目中的应用。…...

C/C++线程详解

一、C语言线程创建&#xff08;POSIX线程&#xff09; 1. 基本创建方法 POSIX线程&#xff08;pthread&#xff09;是C语言中创建线程的标准API&#xff1a; #include <pthread.h> #include <stdio.h>void* thread_func(void* arg) {printf("Thread runnin…...

动态ip与静态ip的概念、区别、应用场景

动态ip与静态ip的区别 前言 一、IP地址的概念和作用 1.1、IP地址的定义 1.2、IP地址的作用 二、动态IP和静态IP的区别 2.1、动态IP和静态IP的定义 2.2、动态IP和静态IP的特点 2.3、动态IP和静态IP的优缺点比较 三、动态IP和静态IP的应用场景 3.1. 动态IP的应用场景 3.2. 静态IP…...

P12167 [蓝桥杯 2025 省 C/Python A] 倒水

P12167 [蓝桥杯 2025 省 C/Python A] 倒水 题目描述 小蓝有 n n n 个装了水的瓶子&#xff0c;从左到右摆放&#xff0c;第 i i i 个瓶子里装有 a i a_i ai​ 单位的水。为了美观&#xff0c;小蓝将水循环染成了 k k k 种颜色&#xff0c;也就是说&#xff0c;第 i i i …...

Appium自动化开发环境搭建

自动化 文章目录 自动化前言 前言 Appium是一款开源工具&#xff0c;用于自动化iOS、Android和Windows桌面平台上的本地、移动web和混合应用程序。原生应用是指那些使用iOS、Android或Windows sdk编写的应用。移动网页应用是通过移动浏览器访问的网页应用(appum支持iOS和Chrom…...

【金仓数据库征文】金仓数据库:国产化浪潮下的技术突破与行业实践

目录 前言 技术突破&#xff1a;从追赶国际到引领创新 行业深耕&#xff1a;从医疗到航空航天的多领域落地 事务管理与ACID特性 事务管理概述 索引优化与性能调优 安全性与备份恢复策略 Json构造函数 总结 前言 在数字化转型的全球趋势下&#xff0c;数据库作为信息系…...

计算机操作系统

1. T0 时刻是否为安全状态&#xff1f; 步骤 1: 计算当前可用资源 总资源数量&#xff1a; A: 17B: 5C: 20 已分配资源&#xff1a; P1: (2, 1, 2)P2: (4, 0, 2)P3: (4, 0, 5)P4: (2, 0, 4)P5: (3, 1, 4) 当前可用资源&#xff1a; A: 17 - (2 4 4 2 3) 2B: 5 - (1 0 …...

linux系统问题杂谈

1.配置好anaconda之后&#xff0c;在一个终端中编辑好环境变量之后能够正常使用conda命令&#xff0c;但是新打开一个中断使用conda命令报错"无法识别conda"。 原因&#xff1a;使用“export PATH"/home/username/anaconda3/bin:$PATH"命令&#xff0c;临…...

百度打响第一枪!通用超级智能体时代,真的来了

Create2025百度AI开发者大会在武汉举行&#xff0c;K哥受邀参加&#xff0c;看到了许多有趣的创新技术和产品。其中最令我印象深刻的是一款全新发布的通用超级智能体——心响App。 这款App通过多智能体复杂组合、协作&#xff0c;满足用户能够「一站式」解决复杂问题的使用诉求…...

FWFT_FIFO和Standard_FIFO对比仿真

在FPGA中使用FIFO时&#xff0c;如果使用FPGA厂商提供的FIFO IP&#xff0c;一般都会有First Word Fall Through FIFO和Standard FIFO类型选项&#xff0c;那么这两种FIFO有什么差异么。两种FIFO的端口是一样的&#xff0c;看不出区别&#xff0c;只有通过仿真&#xff0c;才能…...

【网络原理】TCP提升效率机制(二):流量控制和拥塞控制

目录 一. 前言 二. 流量控制 三. 拥塞控制 一. 前言 TCP的可靠传输依靠确认应答机制&#xff0c;超时重传机制是对确认应答的一种补充&#xff0c;解决了丢包问题 为了提高传输效率&#xff0c;避免大量的时间都浪费在等待应答的过程&#xff0c;故引入了滑动窗口机制&…...

DeepSeek+Cline:开启自动化编程新纪元

目录 一、引言&#xff1a;AI 编程时代的曙光二、认识 DeepSeek 和 Cline2.1 DeepSeek 是什么2.2 Cline 详解2.3 两者结合的魅力 三、DeepSeek Cline 安装与配置全流程3.1 安装 VS Code3.2 安装 Cline 插件3.3 获取 DeepSeek API Key3.4 配置 Cline 与 DeepSeek 连接 四、实战演…...

【RedisLockRegistry】分布式锁

RedisLockRegistry分布式锁 介绍 RedisLockRegistry‌是Spring框架提供的一种分布式锁机制&#xff0c;它基于Redis来实现对共享资源的保护&#xff0c;防止多个进程同时对同一资源进行修改&#xff0c;从而避免数据不一致或其他问题‌ 基本原理 RedisLockRegistry通过Redi…...

脚本批量启动Node服务器

创建文件start-projects.ps1 定义项目路径&#xff08;使用PowerShell中更可靠的路径表示方式&#xff09; $变量A “E:/XXXX文件根目录” $变量B “E:/XXXX” $变量C “E:/XXXX” 打开变量A并执行npm run dev Start-Process powershell -ArgumentList “-NoExit”, “-Com…...

使用命令行加密混淆C#程序

C#作为托管语言编译生成的IL中间代码极易被反编译工具还原源码。据统计&#xff0c;超过83%的商业软件曾遭遇过代码逆向风险&#xff0c;导致核心算法泄露、授权被跳过. 因此对于C#语言开发的程序来说, 在发布前进行混淆和加密非常有必要. 本文主要介绍如何使用恒盾C#混淆加密…...

零基础上手Python数据分析 (23):NumPy 数值计算基础 - 数据分析的加速“引擎”

写在前面 —— 超越原生 Python 列表,解锁高性能数值计算,深入理解 Pandas 的底层依赖 在前面一系列关于 Pandas 的学习中,我们已经领略了其在数据处理和分析方面的强大威力。我们学会了使用 DataFrame 和 Series 来高效地操作表格数据。但是,你是否好奇,Pandas 为何能够…...

深度学习实战106-大模型LLM+股票MCP Server的股票分析和投资建议应用场景

大家好,我是微学AI,今天给大家介绍一下深度学习实战106-大模型LLM+股票MCP Server的股票分析和投资建议应用场景。 文章目录 一、项目背景(一)大型语言模型(LLM)在金融领域的应用趋势(二)模型上下文协议(MCP)的兴起(三)大模型LLM+股票MCP服务的需求二、开发流程(…...

IDEA配置将Servlet真正布署到Tomcat

刚开始只能IDEA运行完Servlet web application 并保持IDEA运行才能通过浏览器访问到我的Servlet&#xff0c;跟想象中的不一样&#xff0c;不应该是IDEA运行完项目以后只要打开Tomcat就能访问吗&#xff1f;事实时运行完项目只要关掉IDEA就不能再访问到应用了&#xff0c;而且T…...

交叉编译paho.mqtt.c和paho.mqtt.cpp(MQTT客户端)

一、参考资料 【MQTT】paho.mqtt.cpp 库的 介绍、下载、交叉编译、MQTT客户端例子源码-CSDN博客 【MQTT】paho.mqtt.c 库的“介绍、下载、交叉编译” 详解&#xff0c;以及编写MQTT客户端例子源码-CSDN博客 二、准备工作 1. 重要说明 paho.mqtt.cpp与paho.mqtt.c&#xff…...

Prometheus中部署Alertmanager

部署Alertmanager 是 Prometheus 生态系统中的一个重要步骤&#xff0c;用于管理和处理 Prometheus生成的告警。Alertmanager和Prometheus Server一样均采用Golang实现&#xff0c;并且没有第三方依赖。一般来说我们可以通过以下几种方式来部署Alertmanager&#xff1a;二进制包…...

van-field组件设置为textarea属性被软键盘遮挡问题

在移动端van-field 输入框当type为text时&#xff0c;调出软键盘输入框会被顶上去&#xff0c;但type为textarea时不会被顶上去&#xff0c;可以用下面方法来实现&#xff1a; 1. 来2个van-field type为text的输入框z-index: 1 type为textarea的输入框z-index: 9999&#x…...

websheet之 编辑器

一、默认编辑器 该单元格编辑器是控件自带的编辑器&#xff0c;用户不需要指定。 二、下拉选择 该单元格编辑器是控件自带的编辑器的一种。该控件需要你指定下拉的数据源。在下面的例子中&#xff0c;我们给C3和C6单元格指定了币种的下拉选择编辑器。参数见&#xff1a;六、 参…...

氢气泄漏应急预案应包括哪些内容?

氢气泄漏应急预案是科研实验室中应对氢气泄漏事故的重要文件&#xff0c;其内容需要全面覆盖预防、检测、响应和善后处理等环节&#xff0c;确保在紧急情况下能够快速、有序地采取措施&#xff0c;最大限度地减少事故风险和损失。以下是氢气泄漏应急预案应包括的主要内容&#…...

【每天一个知识点】IPv4(互联网协议版本4)和IPv6(互联网协议版本6)

IPv4&#xff08;互联网协议版本4&#xff09;和IPv6&#xff08;互联网协议版本6&#xff09;是用于在互联网上标识和定位设备的两种主要协议。它们的主要区别在于地址空间、结构、以及一些附加功能。以下是两者的对比&#xff1a; 1. 地址长度 IPv4: 地址长度为32位&#xf…...

【高频考点精讲】前端构建工具对比:Webpack、Vite、Rollup和Parcel

前端构建工具大乱斗:Webpack、Vite、Rollup和Parcel谁是你的菜? 【初级】前端开发工程师面试100题(一) 【初级】前端开发工程师面试100题(二) 【初级】前端开发工程师的面试100题(速记版) 最近在后台收到不少同学提问:“老李啊,现在前端构建工具这么多,我该选哪个?…...

牛客小白月赛115-B题:签到题

题目传送门牛客网竞赛题目 一、题目描述 给定n道题目&#xff0c;每道题难度为aᵢ。要从中选出m道题组成比赛&#xff0c;使得难度最低的题目&#xff08;签到题&#xff09;数量尽可能多。求签到题的最大可能数量。 输入&#xff1a; 第一行两个整数n,m(1≤m≤n≤210⁵)第…...

【QQMusic项目复习笔记——音乐管理模块详解】第四章

&#x1f339; 作者: 云小逸 &#x1f91f; 个人主页: 云小逸的主页 &#x1f91f; motto: 要敢于一个人默默的面对自己&#xff0c;强大自己才是核心。不要等到什么都没有了&#xff0c;才下定决心去做。种一颗树&#xff0c;最好的时间是十年前&#xff0c;其次就是现在&…...

IPv6 技术细节 | 源 IP 地址选择 / Anycast / 地址自动配置 / 地址聚类分配

注&#xff1a;本文为 “IPv6 技术细节” 相关文章合集。 部分文章中提到的其他文章&#xff0c;一并引入。 略作重排&#xff0c;未整理去重。 如有内容异常&#xff0c;请看原文。 闲谈 IPv6 - 典型特征的一些技术细节 iteye_21199 于 2012-11-10 20:54:00 发布 0. 巨大的…...

代码随想录算法训练营day11(二叉树)

华子目录 翻转二叉树思路 对称二叉树思路 二叉树的最大深度思路 翻转二叉树 https://leetcode.cn/problems/invert-binary-tree/description/ 思路 采用递归的思路可以前序遍历和后序遍历&#xff0c;不能使用中序遍历 # Definition for a binary tree node. # class TreeNo…...

A Comprehensive Survey of Spoken Language Models

语音大语言模型&#xff08;Spoken Language Model, SLM&#xff09;正在引领人工智能领域的新一轮革新浪潮。正如文本自然语言处理从任务特定模型迈向通用大语言模型的演进&#xff0c;语音领域也正在经历类似转型。 为填补该领域系统性综述的空白&#xff0c;芝加哥大学、卡…...

深入解析 SMB 相关命令:smbmap、smbclient、netexec 等工具的使用指南

Server Message Block&#xff08;SMB&#xff09;协议是广泛应用于文件共享、打印机共享和进程间通信的网络协议&#xff0c;尤其在 Windows 环境中常见。渗透测试和网络安全审计中&#xff0c;SMB 是一个重要的攻击面&#xff0c;相关工具如 smbmap、smbclient 和 netexec 提…...

伊克罗德信息亮相亚马逊云科技合作伙伴峰会,以ECRobot 智能云迁移助手在GenAI Tech Game比赛勇夺金牌!

十年同行&#xff0c;共赴盛会&#xff1a;伊克罗德信息亮相2025亚马逊云科技Partner Summit 2025亚马逊云科技合作伙伴峰会&#xff08;AWS Partner Summit&#xff09;于乌镇盛大启幕&#xff0c;这场全球云计算领域的顶级盛会汇聚了亚马逊云科技全球核心合作伙伴、行业领袖与…...

【蓝桥杯】P12165 [蓝桥杯 2025 省 C/Java A] 最短距离

最短距离 题目描述 在一条一维的直线上&#xff0c;存在着 n n n 台显示器和 n n n 个电源插座。老师给小蓝布置了个任务&#xff1a;负责将每台显示器通过电源线与一个插座相连接&#xff08;每个插座最多只能给一台显示器供电&#xff09;&#xff1b;同时&#xff0c;老…...

深入浅出Sentinel:分布式系统的流量防卫兵

引言 在当今的微服务架构和分布式系统中&#xff0c;服务间的依赖关系错综复杂&#xff0c;一个服务的故障可能会像多米诺骨牌一样引发整个系统的崩溃。如何有效地保护系统免受突发流量、不稳定依赖服务的影响&#xff0c;成为每个架构师和开发者必须面对的挑战。今天&#xf…...

vite+vue2+elementui构建之 vite.config.js

webpack版本太低&#xff0c;构建依赖太多&#xff0c;头大。 各种查阅资料&#xff0c;弄了一份直通构建vite构建elementUi核心文件&#xff0c; 构建基于开源若依vue2vue3版本改造&#xff0c;感谢开源&#xff0c;感谢若依。 package.json 地址 vitevue2elementui构建之…...

【Pandas】pandas DataFrame radd

Pandas2.2 DataFrame Binary operator functions 方法描述DataFrame.add(other)用于执行 DataFrame 与另一个对象&#xff08;如 DataFrame、Series 或标量&#xff09;的逐元素加法操作DataFrame.add(other[, axis, level, fill_value])用于执行 DataFrame 与另一个对象&…...

Java 后端开发环境安装

Java环境安装 1. 安装程序 Java1.8下载&#xff0c;由于官网下载需要登录&#xff0c;比较麻烦&#xff0c;所以我将安装文件放到了我的资源中&#xff0c;大家通过资源直接下载即可 jdk-8u351-windows-x64.exe 大家根据自己的电脑的配置选择适当的版本 然后一路下一步 这里…...

Azure Data Factory ETL设计与调度最佳实践

一、引言 在Azure Data Factory (ADF) 中&#xff0c;调度和设计ETL&#xff08;抽取、转换、加载&#xff09;过程需要综合考量多方面因素&#xff0c;以确保数据处理高效、可扩展、可靠且易于维护。以下将详细介绍相关关键考虑因素、最佳实践&#xff0c;并辅以具体示例说明…...

【Mybatis】MyBatisPlus的saveBatch真的是批量插入吗?深度解析与性能优化

前言 在使用MyBatis-Plus进行批量数据插入时&#xff0c;许多开发者会发现&#xff1a;即使调用saveBatch方法&#xff0c;数据库仍会产生大量INSERT语句。本文将深入源码揭示背后的真相&#xff0c;并提供3种性能优化方案&#xff0c;让你的批量插入速度提升10倍&#xff01;…...

图像预处理-图像亮度变换

一.亮度变换 首先有两个关联的说法&#xff1a; 亮度调整&#xff1a;像素强度整体变高或者变低。 对比度调整&#xff1a;暗处像素强度变低&#xff0c;亮处像素强度变高&#xff0c;从而拉大中间某个区域范围的显示精度。 opencv中操作这两种变换的公式为&#xff1a; 对比…...

基于AI应用创业IDEA:使用百度搜索开放平台的MCP广场智能推荐MCPServices服务

基于AI应用创业IDEA&#xff1a;使用百度搜索开放平台的MCP广场智能推荐MCPServices服务 在当今快速发展的技术时代&#xff0c;人工智能&#xff08;AI&#xff09;已经成为推动各行各业创新的关键力量。特别是在创业领域&#xff0c;AI技术不仅能够帮助提升产品性能&#xf…...

URP-利用矩阵在Shader中实现物体的平移和缩放

一、平移 方法一&#xff1a; v.positionOS.xyz _Translate.xyz; 方法二&#xff1a; 利用矩阵实现平移&#xff1a; 二、缩放 方法一&#xff1a; v.positionOS.xyz * _Scale.xyz*_Scale.w; _Scale.w实现全局缩放 方法二&#xff1a; Shader"unity/Translation"…...