【C++】——继承详解

news2024/11/13 10:16:26

目录

1、继承的概念与意义

2、继承的使用

2.1继承的定义及语法

2.2基类与派生类间的转换

2.3继承中的作用域

2.4派生类的默认成员函数

<1>构造函数

<2>拷贝构造函数

<3>赋值重载函数

<4析构函数

<5>总结

3、继承与友元

4、继承与静态变量

5、菱形继承及菱形虚拟继承

6、继承与组合


1、继承的概念与意义

什么是继承?

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称派生类。

继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用,继承是类设计层次的复用。

通过继承联系在一起的类构成了一种层次关系,在这种关系中有一个基类(base class),其他类则是直接或间接地从基类继承过来的,这些继承来的类可以称为派生类(drived class)。基类通常有着层次关系中所有类共同拥有维护的成员,而每个派生类也有着自己各自特定的成员。

一个简单的例子:一个学习管理系统,那么成员必定有学生,老师等等,这些是身份,归根到底是个人(基类)包含着名字、年龄、地址等基础信息。这些需要共同维护的就是基类的成员。

//共同维护的成员部分->基类
class Person
{
public:
	// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证 
	void identity()
	{
		cout << "void identity()" << _name << endl;
	}
protected:
	string _name = "qsy"; // 姓名 
	string _address; // 地址 
	string _tel; // 电话 
	int _age = 18; // 年龄 
};

class Student : public Person
{
public:
	// 学习 
	void study()
	{
		// ...
	}
protected:
	int _stuid; // 学号 
};
class Teacher : public Person
{
public:
	// 授课 
	void teaching()
	{
		//...
	}
protected:
	string title; // 职称 
};
int main()
{
	Student s;
	Teacher t;
	s.identity();
	t.identity();
	return 0;
}

可以看到派生类可以访问基类成员

如果没有继承这种结构关系的话 Student和Teacher 都有姓名/地址/ 电话/年龄等成员变量,都有identity身份认证的成员函数,设计到两个类里面就是冗余的。更好地体现了继承是类设计层次的复用。

2、继承的使用

2.1继承的定义及语法

这就是继承的语法格式

继承方式与访问限定符号一样有着三种,不同的继承方式与不同的类成员组合会是不同的情况

总结一下规律:

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

将年龄变为私有验证一下是否继承到了派生类对象

可以看到继承下来了但是不可以访问!!

<2>如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为 protected。可以看出保护成员限定符是因继承才出现的。

如果想要访问 private 成员可以在基类中成员函数访问,这样派生类可以间接访问到 private成员

<3>基类的私有成员在派生类都是不可见。基类的其他成员在派生类的访问方式 == Min(成员在基类的访问限定符,继承方式),public  > protected > private。

Tip:class默认继承方式是 private,struct默认继承方式是public。最好显示写出继承方式

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

2.2基类与派生类间的转换

基类与派生类之间是否有着类型的转换呢?

答案是可以的! public继承中有一个 is-a 概念:每个派生类都是一个特殊的基类对象

• public继承的派生类对象 可以赋值给 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中基类那部分切出来,基类指针或引用指向的是派生类中切出来的基类那部分。

基类对象不能赋值给派生类对象

基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type  Information)的dynamic_cast 来进行识别后进行安全转换。

2.3继承中的作用域

继承体现中也有各自的作用域规则并且引出来一个隐藏概念,隐藏影响的只是编译器查找规则

1. 在继承体系中基类和派生类都有独立的作用域。

2. 派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。 (在派生类成员函数中,可以使用 基类::基类成员显示访问)

3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。(区分重载)

4. 注意在实际中在继承体系里面最好不要定义同名的成员。

// Student的_num 和 Person的_num 构成隐藏关系,可以看出这样代码虽然能跑,但是⾮常容易混淆 
class Person
{
protected:
	string _name = "小徐"; // 姓名 
	int _num = 111; // ⾝份证号 
};
class Student : public Person
{
public:
	void Print()
	{
		cout << " 姓名:" << _name << endl;
		cout << " 身份证号:" << Person::_num << endl;
		cout << " 学号:" << _num << endl;
	}
protected:
	int _num = 999; // 学号 
};
int main()
{
	Student s1;
	s1.Print();
	return 0;
};

访问的是哪个 _num 呢?

可以看到派生类成员隐藏了基类的同名成员,直接访问了派生类的 _num 

同理,函数也有隐藏的现象

A和B类中的 fun 两个函数构成什么关系呢??根据前面的知识可以知道继承体系中函数名相同就构成隐藏关系

2.4派生类的默认成员函数

6个默认成员函数,默认的意思就是指我们不写,编译器会自动生成⼀个,那么在派生类中,这 几个成员函数是如何生成的呢?

四个常见默认成员函数:

<1>构造函数

派生类的构造函数必须调用基类的构造函数初始化基类的那⼀部分成员。

class Person
{
public:
	Person(const char* name="xxc") //全缺省函数,默认构造
		:_name(name)
	{
		cout << "Person()" << endl;
	}
protected:
	string _name;//姓名
};

class Student :public Person
{
public:
	//不显示实现默认构造,编译器生成的
	// 1. 内置类型->不确定
	// 2. 自定义类型->调用自定义类型的显示写的默认构造
	// 3. 基类成员看作一个整体,要求调用基类的默认构造
protected:
	int _num;//学号
	string _addrss;//地址
};

int main()
{
	Student s1;
	return 0;
}

如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用

写一个 Student的构造函数

还是报错!前面提到 需要把基类成员当成一个对象调用基类的构造函数

如何实现一个不能被继承的类呢?

方法1:基类的构造函数私有,派生类的构成必须调用基类的构造函数,但是基类的构成函数私有化以后,派生类看不见就不能调用了,那么派生类就无法实例化出对象。

方法2:C++11新增了⼀个final关键字,final 修改基类,派生类就不能继承了。

<2>拷贝构造函数

派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。

可以看到,没有资源申请的时候 Student 并不需要自己显示实现拷贝构造,因为编译器默认拷贝构造会调用基类的拷贝构造

那么怎么自己实现拷贝构造呢?(Tip:基类对象是最先声明(内存顺序)的,初始化列表中第一个初始化)

Person(const Person& p)
	: _name(p._name)
{
	cout << "Person(const Person& p)" << endl;
}

Student(const Student& s)
	:Person(s)
	,_num(s._num)
	,_addrss(s._addrss)
{
	//深拷贝
}

Person(s) 这个 s 是派生类对象的引用为什么可以传给基类呢? 涉及基类与派生类间的转换概念——切片 

如果显示写了拷贝构造但是不显示调用基类的拷贝构造的会,编译器会自动调用默认构造而非调用基类的拷贝构造

补充一下缺省值构成默认构造,运行一下发现调用的就是默认构造而非拷贝构造

<3>赋值重载函数

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

赋值重载与拷贝构造类似一般编译器默认生成的就已经够用了,如果有资源申请的话才需要显示实现

Student& operator=(const Student& s)
{
	if (this != &s)
	{
		operator=(s);//派生类切片基类成员
		_num = s._num;
		_addrss = s._addrss;
	}
	return *this;
}

栈溢出,无限递归调用,我们不是想要调用基类的赋值函数吗?为什么调用了派生类的呢?

需要注意的是派生类的 operator= 隐藏了基类的operator= ,所以显示调用基类的operator= ,需要指定基类作用域

Student& operator=(const Student& s)
{
	if (this != &s)
	{
		//基类和派生类的赋值构成了隐藏关系 需要指定作用域
		Person::operator=(s);//派生类切片基类成员
		_num = s._num;
		_addrss = s._addrss;
	}
	return *this;
}

<4析构函数

析构函数可以显示调用,那么可以在派生类显示调用基类的析构函数来清理基类成员

可是为什么调不动呢?这里派生类和基类的析构函数构成了隐藏关系

因为多态中⼀些场景析构函数需要构成重写,重写的条件之一是函数名相同,那么编译器会对析构函数名进行特殊处理,处理成destructor(),所以基类析构函数不加 virtual的情况下,派生类析构函数和基类析构函数构成隐藏关系。

想要调用就标明作用域:

Person::~Person()

但是像上述这样写,会有一个问题,基类的析构会调用两次!!!

其实,派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。所以我们不必在派生类的析构函数中进行调用基类的析构函数,不然就会重复释放同一块空间,导致报错!

可以怎么理解派生类析构自动调用基类的析构呢? 先子后父保证析构顺序!显示调用不一定保证先子后父的析构顺序

<5>总结

派生类和基类的层次关系逻辑基础还是类和对象

派生类的默认成员函数的注意事项:

<1>派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。


<2>派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。


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


<4>派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员(不需要显示和调用基类析构)。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。


<5>派生类对象初始化先调用基类构造再调派生类构造。派生类对象析构清理先调用派生类析构再调基类的析构。


<6>因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系

3、继承与友元

友元关系不能继承,也就是说基类友元不能访问派生类私有和保护成员。比如爸爸的朋友可以说是你的朋友吗?


class Student;//前置声明
class Person
{
public:
	friend void Display(const Person& p, const Student& s);//需要前置声明否则报错招不到 Student
protected:
	string _name; // 姓名 
};
class Student : public Person
{
protected:
	int _stuNum; // 学号 
};
void Display(const Person& p, const Student& s)
{
	cout << p._name << endl;
}
int main()
{
	Person p;
	return 0;
}

如果访问派生类的私有和保护成员呢?

可以看见是不可访问的 在派生类同样设置一个友元就可以解决这个问题了。

4、继承与静态变量

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

验证一下:

class A
{
public: 
	static int _a;
	int _aa;
};
class B :public A
{
public:
	int _b;
};
// static int _a = 1;报错
 int A::_a = 1;//注意定义的方式
int main()
{
	A  a;
	B b1;
	B b2;
	//这⾥的运行结果可以看到非静态成员_aa的地址是不⼀样的
	// 说明派生类继承下来了,⽗类派生类对象各有⼀份 
	cout << &a._aa << endl;
	cout << &b1._aa << endl;
	cout << endl;
	// 这⾥的运行结果可以看到静态成员 _a 的地址是⼀样的 
	//说明派生类和基类共用同⼀份静态成员 
	cout << &a._a << endl;
	cout << &b1._a << endl;
	cout << &b2._a << endl;
	cout << endl;
	//公有情况下 基类派生类都可以访问静态成员变量
	cout << a._a << endl;
	cout << b1._a << endl;
	cout << b2._a << endl;
	return 0;
}

也就说明他们共用一个_a变量,所以无论派生出多少个子类,都只有一个static成员实例

这个特性可以带来一种思路统计实例化类的数量个数,只需在构造函数中加入一个增加该静态变量的语句即可:

class Person
{
public:
	Person() { ++_count; }//子类的构造会调用父类构造
protected:
	string _name; // 姓名
public:
	static int _count; // 统计人的个数。
};
int Person::_count = 0;

class Student : public Person
{
protected:
	int _stuNum; // 学号
};
int main()
{
	Student s1;
	Student s2;
	Student s3;
	cout << " 人数 :" << Person::_count << endl;
	Student::_count = 0;
	cout << " 人数 :" << Person::_count << endl;
	return 0;
}

这样我们就可以知道该继承体系中实例化了多少个类了!!!

5、菱形继承及菱形虚拟继承

首先声明一下,由于C++的历史缘故,其一致行走在语言发展的前端,一直在尝试新的内容。在发展过程中,有些内容加入到C++的时候,还没有发现其弊端。而后来发现的时候,为了向上兼容,只能打补丁,所以不开避免的不会有一些弊端,会有复杂的语法和复杂的特性。总要有先驱者走前前面,而C++就是!!!

单继承:⼀个派生类只有⼀个直接基类时称这种继承关系为单继承

多继承:⼀个派生类有两个或以上直接基类时称这种继承关系为多继承多继承对象在内存中的模型是,先继承的基类在前面,后面继承的基类在后面,派生类成员在放到最后面。

菱形继承:菱形继承是多继承的⼀种特殊情况。

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

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

那该如何解决数据冗余的问题呢??可以借用虚拟继承!!

虚拟继承(virtual)可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在StudentTeacher的继承Person时使用虚拟继承,即可解决问题。

这是什么原理呢?测试一下!

菱形继承不虚拟继承的情况

#include<iostream>
#include<string>

using namespace std;

class A
{
public:
	int _a;
};
 class B : public A
//class B : virtual public A
{
public:
	int _b;
};
 class C : public A
//class C : virtual public A
{
public:
	int _c;
};
class D : public B, public C
{
public:
	int _d;
};
int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	return 0;
}

调试一下:

通过这个逐语句调试的内存变化,我们可以确定大致的内存情况:

不使用虚拟继承就是这样的内存情况,也好理解为什么同名变量的两份是如何储存的了。
接下来我们来看虚拟继承下的菱形继承是怎么个情况:

内存分布:

a储存在最下面,而B,C部分的原有储存_a的位置现在是什么呢???
其实是个指针,那我们来看看指针指向的空间储存着什么吧:

???怎么对应位置是00 00 00 00为什么是零?往下看看:
分别储存着16进制数字14 0c转换为10进制数字20 12,然后对应B,C原本的指针位置(006FFB6C)加上这个值(偏移量),都会指向到A _a的空间!!!这个00 00 00 00到多态的部分再来进行讲解,知道原地址加上下面的值就是A _a的空间就可以了!!!

这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。
即原本B,C中_a的位置储存这一个指针,指针指向的位置有一个偏移量,原位置的地址加上偏移量就会指向A的空间!!!

那这样进行拷贝切片的时候是怎样的呢?一样是把D中B对象的部分切片,然后通过虚基表的方式来找到_a。但这样也带来了一些代价:(PS:内存中的储存顺序就是声明的顺序,先继承谁,谁就在前面)

多继承指针偏移问题(切片)

p1和p2指向哪里呢???

内存分布中,先继承的放前面!

因为切片的概念p2指向 base2开始但是只能看见 base2 那一部分

6、继承与组合

  • public继承是一种is-a(谁是什么)的关系。也就是说每个派生类对象都是一个基类对象。
  • 组合是一种has-a(谁有什么)的关系。假设B组合了A,每个B对象中都有一个A对象(也就是把A作为B的成员变量)
  • 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse 能看见,不安全,耦合度高)。术语 “白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。


  • 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse 不能能看见,安全,耦合度低),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。

有关继承的经典面试题
<1>C++有多继承,为什么java等语言没有?
历史原因!C++是先驱者(人的直觉认为多继承很合理,我感觉正常人都会想到多继承),并且c++中的多继承处理起来十分复杂,访问基类变量的过程就会很复杂!!!java等后来发展的语言见到c++中多继承的复杂,就干脆放弃了。

<2>什么是菱形继承?多继承的问题是什么?
菱形继承如字面意思(两个父类的父类是同一个类就会发生菱形继承),多继承本身没什么问题,真正的问题是有多继承就可能发生菱形继承。菱形继承就有问题了:变量的二义性和继承冗杂。解决办法很简单就是虚拟继承,但是这样就会大大降低效率。

<3>继承和组合的区别?什么时候用继承?什么时候用组合?
继承:通过扩展已有的类来获得新功能的代码复用方法
组合:新类由现有类的对象合并而成的类的构造方式

如果二者间存在一个“是”的关系,并且一个类要对另外一个类公开所有接口,那么继承是更好的选择
如果二者间存在一个“有”的关系,那么首选组合
!能用组合就用组合!!!能用组合就用组合!!!能用组合就用组合!!!

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

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

相关文章

在VC++6.0中创建一个C++项目

1、下载并安装VC6.0 暂时不介绍下载安装&#xff0c;后续可能会补充 2、打开VC6.0 初始界面如下图&#xff1a; 3、创建一个空工程 文件-新建 在新建弹框中选择&#xff1a;工程-win32 console Application-填写工程名、选择保存路劲-确定 在新的弹框中&#xff0c;选择&…

BIT小学期-电话号码问题

Output 输出包括两个部分&#xff0c;第一个部分是错误的电话号码&#xff0c;对于这些号码应当按照输入的顺序以原始的形式输出。在输出错误电话号码前输出Error:&#xff0c;随后输出这些号码&#xff0c;如果没有错误的电话号码&#xff0c;则输出Not found. 第二部分是重…

[C++进阶]AVL树

前面我们说了二叉搜索树在极端条件下时间复杂度为O(n),本篇我们将介绍一种对二叉搜索树进行改进的树——AVL树 一、AVL 树的概念 二叉搜索树虽可以缩短查找的效率&#xff0c;但如果数据有序或接近有序二叉搜索树将退化为单支树&#xff0c;查找效率低下。因此&#xff0c;两位…

6个Python小游戏项目源码【免费】

6个Python小游戏项目源码 源码下载地址&#xff1a; 6个Python小游戏项目源码 提取码: bfh3

深度学习Day-33:Semi-Supervised GAN理论与实战

&#x1f368; 本文为&#xff1a;[&#x1f517;365天深度学习训练营] 中的学习记录博客 &#x1f356; 原作者&#xff1a;[K同学啊 | 接辅导、项目定制] 一、 基础配置 语言环境&#xff1a;Python3.8编译器选择&#xff1a;Pycharm深度学习环境&#xff1a; torch1.12.1c…

cout无法正常显示中文

cout无法正常显示中文 虽然你使用了buf.length()来指定写入的字节数&#xff0c;但是在包含中文字符&#xff08;UTF-8编码下每个中文字符占用3个字节&#xff09;的情况下&#xff0c;直接使用length()可能不会正确反映实际的字节数&#xff0c;因为它给出的是字符数而非字节…

基于深度学习,通过病理切片直接预测HPV状态|文献速递·24-09-16

小罗碎碎念 有段时间没有写文献速递的推文了&#xff0c;搞得自己今天写还怪不适应的。 今天所有的推文&#xff0c;都是围绕一个系统的问题展开——既研究了HPV与EBV在头颈癌/鼻咽癌中的致病机制&#xff0c;也总结了如何结合病理组学直接由WSI预测HPV状态——没办法&#x…

变压器漏感对整流电路的影响

目录 1. 电压波形畸变 2. 输出电压波动 3. 电流纹波增加 4. 降低整流效率 5. 影响开关器件的性能 6. EMI&#xff08;电磁干扰&#xff09;增加 总结与应对措施 变压器漏感在整流电路中会产生一些影响&#xff0c;尤其在高频应用或电流变化较大的情况下&#xff0c;其影…

【GESP】C++一级练习BCQM3006,多行输出

多行输出练习题&#xff0c;使用cout或printf函数输出多行内容。 BCQM3006 题目要求 描述 在windows的控制台环境中所有的字符都是等宽的&#xff0c;默认情况下窗口中每行有 80 个字符&#xff0c;每个屏幕有 25 行&#xff0c;组成了一个字符矩阵。利用控制台的这个特点&a…

什么是 HTTP/3?下一代 Web 协议

毫无疑问&#xff0c;发展互联网底层的庞大协议基础设施是一项艰巨的任务。 HTTP 的下一个主要版本基于 QUIC 协议构建&#xff0c;并有望提供更好的性能和更高的安全性。 以下是 Web 应用程序开发人员需要了解的内容。 HTTP/3 的前景与风险 HTTP/3 致力于让互联网对每个人…

从登录到免登录:JSP与Servlet结合Cookie的基本实现

前言 JSP中应用Cookie解析&#xff1a; 用户登录成功后&#xff0c;将用户信息保存到Cookie中&#xff0c;在页面读取Cookie并显示&#xff0c;不需要再次登录可以直接进入页面 第一步&#xff1a;创建JavaWeb项目&#xff0c;配置pom.xml文件 创建maven项目&#xff0c;项目名…

背包问题 总结详解

就是感觉之前 dp 的 blog 太乱了整理一下。 0-1 背包 例题:P1048 朴素算法 思路 对于一个物品&#xff0c;我们可以选&#xff0c;也可以不选。 我们用表示第 i 件物品的重量&#xff0c;表示第 i 件物品的价值。 考虑表示前 i 件物品放入容量为j的背包中的最大价值。 如…

时间复杂度计算 递归(solve2 后续)

原帖 最近校内比较忙&#xff0c;更新缓慢&#xff0c;致歉。 这里函数每次都需要遍历 h h h 和 m m m 之间的数&#xff08;复杂度 O ( n ) O(n) O(n)&#xff09;&#xff0c;所以和 solve1 略有不同。仍然假设 T ⁡ ( n ) \operatorname{T}(n) T(n) 表示 m − h 1 n…

【C++二叉树】606.根据二叉树创建字符串

606. 根据二叉树创建字符串 - 力扣&#xff08;LeetCode&#xff09; 图文分析&#xff1a; 代码实现&#xff1a; 代码说明&#xff1a; 1、前序遍历方式&#xff1a;根-左子树-右子树。 2、题目要求将二叉树转换为字符串输出&#xff0c;所以定义了一个string对象str。 3…

MySQL —— 视图

概念 视图是一张虚拟的表&#xff0c;它是基于一个或多个基本表或其他视图的查询结果集。 视图本身不存储数据&#xff0c;而是通过执行查询来动态生成数据&#xff0c;用户可以像操作普通表一样使用视图来进行查询更新与管理等操作。 视图本身也不占用物理存储空间&#xf…

网络安全学习(五)Burpsuite

经过测试&#xff0c;发现BP需要指定的JAVA才能安装。 需要的软件已经放在我的阿里云盘。 &#xff08;一&#xff09;需要下载Java SE 17.0.12(LTS) Java Downloads | Oracle 1.2023版Burp Suite 完美的运行脚本的环境是Java17 2.Java8不支持 看一下是否安装成功&#xff0c…

开源AI应用安全指导框架 — OWASP AI Exchange

在当今信息化迅猛发展的时代&#xff0c;网络专业人士正竞相提升人工智能&#xff08;AI&#xff09;安全领域的专业技能。随着这一趋势的推进&#xff0c;他们的企业也在快速地引入各类AI工具、平台、应用程序和服务&#xff0c;业界也相应涌现出众多资源&#xff0c;以协助从…

电梯电动车检测-目标检测数据集(包括VOC格式、YOLO格式)

电梯电动车检测-目标检测数据集&#xff08;包括VOC格式、YOLO格式&#xff09; 数据集&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1qRMdF08Jinx_5CRa3al24A?pwd3twc 提取码&#xff1a;3twc 数据集信息介绍&#xff1a; 共有 5347 张图像和一一对应的标注文件 …

web基础—dvwa靶场(五)File Upload

File Upload(文件上传) 上传的文件对 web 应用程序来说是一个巨大的风险&#xff0c;许多攻击的第一步是上传攻击代码到被攻击的系统上&#xff0c;然后攻击者只需要找到方法来执行代码即可完成攻击。也就是是说&#xff0c;文件上传是攻击者需要完成的第一步。 不受限制的文件…

c#中给winform定义快捷键的几种方式

快捷键的使用在日常的开发中频率比较高&#xff0c;这里总结了最常见的各种快捷键的设置方式&#xff0c;需要的时候大家直接照抄就可以了&#xff0c;不用再去查询如何实现了。 文章目录 一、按钮快捷键二、菜单快捷键三、窗体快捷键四、全局快捷键1、重写ProcessCmdKey2、使…