C++|继承(菱形+虚拟)

news2024/11/20 20:20:49

目录

一、继承的概念及定义

1.1概念

1.2定义

1.2.1定义格式

1.2.2继承关系和访问限定符

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

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

三、继承中的作用域 

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

五、继承与友元、静态成员

六、菱形继承与虚拟继承

6.1单继承与多继承

6.2菱形继承(继承缺陷)

6.3虚拟继承(继承缺陷的解决)

6.3.1虚拟继承使用

 6.3.2虚拟继承原理

七、继承的反思


一、继承的概念及定义

1.1概念

继承是面向对象中的一个重要概念,它由一个类(子类又叫派生类)继承另一个类(父类又叫基类)的属性和方法。通过继承,子类可以复用父类的代码,并且可以添加自己特定的功能或行为。

样例: 

#include <iostream>
using namespace std;

//父类/基类
class Person
{
public:

	void fun()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	const char* _name = "张三";
	int _age = 18;
	
};

//字类/派生类
class Student : public Person//子类继承父类
{
	//虽然子类中没有自己的方法,但是子类继承了父类,那么子类拥有父类中的方法fun
    //只不过这个方法在子类中并没有显示出来,可以通过调试进行观察
    //这个方法是属于自己的,对该方法进行修改,不会影响父类中的方法,如果要修改,        
    //那么就要在子类中显示写出该函数进行修改,但是又有其他规则限定,待后续一步一步讲解。
protected:
	int _id;
};

int main()
{

	Student stu;

	stu.fun();//子类拥有父类中的方法,父类方法中的成员给了初始值,在这里可以直接调用,若没给,需要自己重新实现方法,否则会报错
	
	return 0;
}

调试结果(通过调试可以验证,子类中拥有父类的成员): 

 输出结果:

 根据样例,有了对继承概念的初步了解,但是继承的规则远不止这么简单,有许多细节要注意,接下来进一步深究。

1.2定义

1.2.1定义格式

该定义格式的形式如上图所示,但继承根据继承方式存在一定的限定。 

1.2.2继承关系和访问限定符

继承方式有三种: 

访问限定符有三种:

访问权限的大小正如上图排序所示,public>protected>private,在没学继承之前,经常用public和private,并且呢private和protected的作用可以认为一致, 但是在继承中因为继承方式的变化,public、private、protected之间的组合会产生不一样的化学反应,究竟是火花四溅,还是一缕如平,且品下回。

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

父类成员\子类继承方式public继承protected继承private继承
父类的public成员继承下来的子类中拥有的父类成员也为public成员继承下来的子类中拥有的父类成员为protected成员继承下来的子类中拥有的父类成员为private成员
父类的protected成员继承下来的子类中拥有的父类成员也为protected成员继承下来的子类中拥有的父类成员也为protected成员继承下来的子类中拥有的父类成员也为private成员
父类的private成员父类的private成员,子类不可继承,即在子类中不可见父类的private成员,子类不可继承,即在子类中不可见父类的private成员,子类不可继承,即在子类中不可见

根据上述表的统计,可以归纳几点:

父类的私有成员,子类不能继承且都不可直接访问

继承下来的子类中拥有的父类成员的访问权限是根据父类中成员的访问权限与子类继承方式中取权限小的那一个,权限相等就取权限相等的那一个,权限大小排行public>protected>private。

例如:父类成员为protected,子类继承方式为public,取权限小的那个为protected

那么根据上述描述,protected和private到底有什么区别呢?

①不是继承关系的外界依然不能访问protected成员

②如果不是私有继承,子类可以访问父类的protected成员

所以protected的作用基本上只能在继承中才能体现。


除了有上述的组合产生不同的权限访问问题,还有几点也要了解:

①继承方式可以省略不写,若没写,父类如果是class类,其继承方式默认为私有继承,父类如果是struct类,其继承方式默认为公有继承

②在实际运用中一般使用都是public继承,几乎很少使用protected/private继承,也不提倡使用,因为protected/private继承下来的成员都只能在派生类的类里面使用,实际中维护性并不高。

例子(展示省略继承方式): 

#include <iostream>
using namespace std;

//父类/基类
class Person
{
public:

	void fun()
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	const char* _name = "张三";
	int _age = 18;
	
};

//字类/派生类
class Student : Person//省略继承方式,父类是class类,其继承方式默认为私有继承
{
	//那么此时子类中拥有的fun成员是私有的
protected:
	int _id;
};

int main()
{

	Student stu;

	stu.fun();//fun是私有的,外界不可直接访问,此时就会报错
	
	return 0;
}

 struct类就不演示了。

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

既然存在了继承,那么对于父类对象和子类对象是否存在赋值关系?是的,但是他们之间的赋值存在着规则限定。

  •  子类对象可以赋值给父类对象/父类指针/父类引用。这种赋值还有另外一个说法叫做切片/切割,表示把子类中父类的那部分切割/赋值给父类对象。如图:
  •  父类对象不能直接赋值给子类对象
  • 父类的指针或者引用可以通过强制类型转换赋值给子类的指针或者引用。但是必须是父类的指针指向子类对象时才是安全的,否则可能发生越界访问。

例子:

#include <iostream>
using namespace std;

//父类/基类
class Person
{
public:

	void fun()
	{
		cout << "name:" << _name << endl;
		cout << "sex:" << _sex << endl;
		cout << "age:" << _age << endl;
	}
protected:
	string _name = "张三";
	string _sex = "男";
	int _age = 18;


};

//字类/派生类
class Student : public Person
{
public:
	int _id;
};

int main()
{

	Student stu;
	Person p = stu;//把子类中父类的那部分切割/赋值给父类对象
	Person* ptr = &stu;//父类指针ptr指向子类对象中父类的那一部分成员
	Person& pptr = stu;//父类引用pptr引用子类对象中父类的那一部分成员

	Student* s = (Student*)ptr;//ptr指向子类对象,进行强转后,s依然指向子类对象。注意强转并不能改变ptr所能访问的大小,
	//ptr所能访问的大小还是根据其本身的类型决定,ptr是父类指针,其所能访问的大小是sizeof(Person),进行强转后,依然是这么大,
	//强转不会对其原本的类型发生改变,ptr依然是Person*类型
	s->_id = 10;

	ptr = &p;//改变父类指针ptr指向为父类对象p
	Student* ss = (Student*)ptr;//此时ptr指向了父类对象p,进行强转赋值给了Student*对象,那么此时ss指向了父类对象
	//ss所能访问的大小为sizeof(Student)
	ss->_id = 20;//由于ss指针指向了父类,父类中并没有_id成员,此时就发生了越界访问。
	return 0;
}

运行越界错误,由于父类指针给了子类指针,即子类指针指向了父类,但又访问了不属于所指向类的成员: 

在了解完基类与派生类之间的赋值之后,有一个问题没有体现,那就是如果子类有与父类相同的成员,然后子类对象调用该成员究竟是调用子类的还是父类的?既然子类中有父类的成员,那子类又该如何访问那么接下来就来解答 

三、继承中的作用域 

了解作用域的划分,限定,方能明白如何去访问子类父类成员。那就有以下几条规则。

1.在继承体系中父类和子类都有独立的作用域,子类中拥有父类的成员是属于自己的,对其进行修改并不会影响原父类的成员

2.子类和父类有同名成员,即子类中显示的与父类有同名成员,子类成员将屏蔽父类对同名成员的直接访问(子类访问该同名成员时,访问的就不是属于父类作用域的同名成员,而是子类中的同名成员,但是子类中依然有父类中的成员),这种情况叫隐蔽,也叫重定义(如果要访问父类同名成员,就要显示进行访问,即 父类::父类成员)

3.对于成员函数的隐藏,只要子类的成员函数的名字与父类成员函数的名字相同就构成隐藏,与返回值,参数类型无关。 

例子:

#include <iostream>
using namespace std;

//父类/基类
class Person
{
public:

	void fun()
	{
		cout << "name:" << _name << endl;
		cout << "sex:" << _sex << endl;
		cout << "age:" << _age << endl;
		cout << endl;
	}
protected:
	string _name = "张三";
	string _sex = "男";
	int _age = 18;


};

//字类/派生类
class Student : public Person
{
public:
	void fun()//与父类fun同名,构成隐藏
	{
		Person::fun();//由于fun构成隐藏要访问父类的fun,就得显示调用,如果不指定,调用的就是构成隐藏的fun,就会造成无限递归
		cout << "name:" << _name << endl;
		cout << "sex:" << _sex << endl;
		cout << "age:" << _age << endl;
		cout << endl;

		Person::fun();//第二次显示调用
	}
protected:
	string _name = "李四";//子类该成员与父类该成员同名,构成隐藏,并进行修改,不会影响父类的_name,因为这是属于自己的
	int _age = 20;//子类与父类同名,构成隐藏,并进行修改,不会影响父类的_age
	int _id = 10;
};

int main()
{

	Student stu;
	stu.fun();

	Person p = stu;//虽然子类出现了隐藏,但并不影响赋值/切割,子类中依然有父类中的成员并把这些成员赋值给父类对象
	p.fun();
	
	return 0;
}

调试结果:

输出结果:

有一点需注意的是,当在fun中修改成员时,不要认为是属于Student的同名成员:

class Student : public Person
{
public:
	void fun()//与父类fun同名,构成隐藏
	{

		Person::fun();//由于fun构成隐藏要访问父类的fun,就得显示调用,如果不指定,调用的就是构成隐藏的fun,就会造成无限递归

		_name = "李四";//虽然fun构成了隐藏,但是该fun中的成员还是属于父类类域,对其进行修改就是修改父类中的成员
		_age = 20;
		cout << "name:" << _name << endl;
		cout << "sex:" << _sex << endl;
		cout << "age:" << _age << endl;
		cout << endl;

		Person::fun();//第二次显示调用
	}
protected:
	int _id = 10;
};

将鼠标光标放于该成员上,也可看出其依然是属于Person类域,编译器会自动去识别。 

调试结果:

 输出结果:

 

研究完子类的访问情况,对于子类的默认成员函数又和之前学过的类的默认成员函数是否有什么不同,来进一步学习。

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

 同样的,是个类就会有6个默认成员函数,虽说我们不写,编译器会默认实现,但是子类对应的6个成员函数编译器具体是如何生成的呢?其符合以下规则。

1.子类的构造函数必须调用父类的构造函数来初始化父类的那一部分成员。也就相当于完成了子类中父类成员的初始化,如果父类没有默认构造函数,则必须在子类的构造函数的初始化列表阶段显示调用

2.子类的拷贝构造函数必须调用父类的拷贝构造完成父类的那一部分成员的拷贝初始化。也就相当于完成了子类中父类成员的拷贝初始化。

3.子类的operator=必须要调用父类的operator=完成父类的赋值。也就相当于完成了子类中父类成员的赋值

4.子类的析构函数会在被调用完成后自动调用父类的析构函数清理父类成员。这样保证了先清理子类对象的成员再清理子类对象中父类的那部分成员,也保证了在清理子类对象前可以去访问子类对象中父类的那部分成员。如果调用子类析构时,先完成了父类的析构,后续子类可能还会访问父类成员,而父类成员已经释放了,会导致野指针访问

 那么我们就来手动实现一下编译器具体是如何生成这些函数的:

#include <iostream>
using namespace std;

//父类/基类
class Person
{
public:
	Person(const string name = "张三")//默认构造函数
		:_name(name)
	{
		cout << "Person()" << endl;//通过打印字符标记是否调用过该默认构造
	}

	//Person(const string name)//构造函数
	//	:_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()
	{
		node = nullptr;
		_name = "";
		cout << "~Person()" << endl;//通过打印字符标记是否调用过该析构
	}
protected:
	string _name;
	struct p
	{
	public:
		int a = 1;
	};
	p a;
	p* node = &a;
};


class Student : public Person
{
public:
	//若父类有默认构造,则在调用子类构造初始化子类成员前会先去调用父类默认构造,
	//若父类没有默认构造,但有构造函数,则需在子类中显示调用父类构造
	//当然对编译器来说,我们啥都没写,会自己生成默认构造的
	Student(int id)//构造
		:Person("李四")//显示调用。注意:将该显示调用与_id初始化互换上下位置,并不会影响规则,编译器还是会先去调用父类构造
		,_id(id)
	{
		cout << "Student(int id)" << endl;//通过打印字符标记是否调用过该默认构造
	}


	
	//调用子类的拷贝构造时必须先去调用父类的拷贝构造完成父类的那一部分成员的拷贝初始化
	Student(const Student& s)//拷贝构造
		:Person(s)//显示调用父类拷贝构造。同理将该显示调用与_id初始化互换上下位置,并不会影响规则,编译器还是会先去调用父类拷贝构造
		,_id(s._id)
	{
		cout << "Student(const Student& s)" << endl;//通过打印字符标记是否调用过该拷贝构造
	}

	//调用子类的operator=必须必须先去调用父类的operator=完成父类的赋值
	Student& operator=(const Student& s)//赋值重载
	{
		if (this != &s)
		{
			Person::operator=(s);//显示调用父类的赋值重载
			_id = s._id;
		}
		return *this;
	}

	//子类的析构函数会在被调用完成后自动调用父类的析构函数清理父类成员
	~Student()
	{
		//Person::~Person();//如果在这里调用子类析构时,先完成父类的析构,
		//node->a = 3;//然后访问父类成员,而父类成员已经释放了,node为空指针,导致对空指针的解引用

		//node->a = 2;//先调用子类析构,父类还未析构,那么子类可以继续访问父类成员。
		cout << "~Student" << endl;//通过打印字符标记是否调用过该析构
	}


protected:
	int _id;
};

int main()
{

	Student stu(20);
	Student s(stu);
	Student ss = s;

	return 0;
}

 当调用子类析构时,先完成了父类的析构,然后访问父类成员,程序崩溃:

代码正确时的调试结果:

 总结:对于派生类这些成员函数规则,其实跟我们之前玩的类的规则类似,唯一不同的是,不管是构造/拷贝/析构,就是多了父类那一部分,那么区别的他们的原则是:父类那部分就调用父类对应的函数完成。

 艰难的完成了成员函数的原理实现,接着就是熟悉的友元和静态成员了

五、继承与友元、静态成员

  • 继承与友元:

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

#include <iostream>
using namespace std;

class Student;
class Person
{
	friend void Print(const Person& p, const Student& s);//在上面声明一下Student类,这样友元中的Student可以在这书写,因为Student类是在下面定义的
protected:
	string _name = "王五";
};

class Student : public Person
{
protected:
	int _num = 1;
};

void Print(const Person& p, const Student& s)
{
	
	cout << p._name << endl;
	cout << s._num << endl;//友元不能继承,所以在这s不能直接访问保护成员

}

int main()
{
	Person p;
	Student s;
	Print(p,s);
	return 0;
}

报错,也可看出友元不能继承: 

  •  继承与静态成员:

 父类中定义了静态成员,则整个继承体系中只有一个这样的成员。无论派生出多少个子类,都只有一个静态成员实例。因为静态成员不属于某个具体的类,而是存放在静态区


#include <iostream>
using namespace std;


class Person
{
public:
	Person()
	{
		_count++;
	}

public:
	static int _count;
};
int Person::_count = 0;//静态成员类外定义
class Student : public Person
{
protected:
	int _num;
};

class ID : public Student
{
protected:
	int _id;
};
int main()
{
	Student s1;//调用父类默认构造
	Student s2;//调用父类默认构造
	ID i;//调用Student的默认构造,Student又会调用Person默认构造
	cout << Person::_count << endl;
	Student::Person::_count = 0;//改变子类中的静态成员就是改变所有类的静态成员
	cout << Student::Person::_count << endl;
    cout << ID::Person::_count << endl;
	cout << Person::_count << endl;

	s2.Person::_count = 1;
	cout << Person::_count << endl;

	return 0;
}

输出结果: 

最后就是到了继承最复杂的部分,无疑是多继承,然而多继承又会存在一定的弊端。让我们拭目以待 

六、菱形继承与虚拟继承

6.1单继承与多继承

我们先来了解继承的单继承和多继承。

单继承:一个子类只有一个直接父类。

class Person
{};

class Student : public Person
{};

class ID : public Student
{};

每个子类只有一个直接父类,示意图: 

多继承:一个子类有两个或两个以上直接父类。

class Person
{};

class Student
{};

class ID : public Person,public Student
{};

 其中ID有两个父类,这就是一个多继承。示意图:

如上面,多继承似乎比单继承有一定的优势,多继承下来的子类可以同时拥有两个父类的成员,

确实,但多继承也存在着缺点,那就是菱形继承的存在。 

6.2菱形继承(继承缺陷)

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

 菱形继承有着一定的缺陷,它存在二义性和冗余性。如下:

#include <iostream>
using namespace std;


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

int main()
{
	Student s;
	s._name = "张三";//访问子类中父类的成员,该父类成员由Person提供
    
    Teacher t;
    t._name = "张坤";

	Assistant a;
	//a._name = "李四";//Assistant有两个父类,这两个父类继承了Person,都有_name成员,
	// 而Assistant继承了这两个父类,那么Assistant就有两份该成员,这就导致了数据的冗余
	//其次,访问Assistant子类中父类的成员_name究竟是由Student提供的还是由Teacher提供的,就存在二义性

	//那么只能显示指定访问哪个父类成员才可以解决二义性,但是数据的冗余性还没有解决。
	a.Student::_name = "李四";
	a.Teacher::_name = "王五";

	return 0;
}

 菱形继承示意图:

 

 Assistant子类示意图:

 虽然Student对象和Teacher对象中Person成员各自拥有一份,但Assistant对象中Person成员拥有两份,导致数据的冗余性,当Assistant对象访问其父类中的成员_name时,该成员不知道是Student中的还是Teacher中的,导致了数据的二义性。

调试结果: 

正如上述示意图所示。

 多继承可以认为是该语法的缺陷,可能祖师爷当时也并没有想到会出现这么一个幺蛾子,所以为了解决该两处问题,引入了虚拟继承。

6.3虚拟继承(继承缺陷的解决)

6.3.1虚拟继承使用

 虚拟继承需要使用一个的关键字virtual,其关键字加在所继承父类的前面。其符合以下继承规则:

虚拟继承的作用是使得重复的数据变成一份,存放在另一处,而当要访问该数据时,便到所存储的该数据位置进行查找,从而解决数据的冗余性导致访问时的二义性。

例如,将该菱形继承改成虚拟继承,Student和Teacher继承了最头部,那么在继承时,Person前面加关键字virtual:

#include <iostream>
using namespace std;


class Person
{
public:
	string _name;
};

class Student : virtual public Person//虚拟继承,在继承的父类前加关键字
{
protected:
	int _num;
};

class Teacher : virtual public Person//虚拟继承,在继承的父类前加关键字
{
protected:
	int _id;
};

class Assistant : public Student, public Teacher//由于其两个父类采用了虚拟继承,那么Assistant中的_name成员只有一份了
{
protected:
	string _course;
};

int main()
{
	Student s;
	s._name = "张三";//访问子类中父类的成员,该父类成员由Person提供
    Teacher t;
    t._name = "李四";

	Assistant a;
	a._name = "王五";//由于Assistant对象中的_name成员只有一份了,可以直接访问
	return 0;
}

 虚拟继承下Assistant子类示意图:

由于其两个父类采用了虚拟继承,那么Assistant中的_name成员只有一份了,被放在了最下面。

 

 调试结果:

通过调试结果,也可观察出,采用虚拟继承,重复的部分被放在了最下面,数据不在出现冗余,解决了二义性。但是,调试结果是为了方便观察这么一个现象,是一种形象表达,并不代表已经不是冗余的数据是直接存放在该子类对象中,所以就要去探究其到底有何玄学。那么接下来就来诠释其底层的奥秘。

 6.3.2虚拟继承原理

 为了更清楚的了解虚拟继承的原理,就要从内存的角度来观察,这里引入了两个新的名词概念,

虚基表指针和虚基表,虚基表指针指向虚基表,虚基表存放的是偏移量,代表虚基表指针的地址距离那只有一份成员地址的距离。

结合概念和代码来演示:

#include <iostream>
using namespace std;


class Person
{
public:
	string _name;
};

class Student : virtual public Person//虚拟继承,在继承的父类前加关键字
{
public:
	int _num;
};

class Teacher : virtual public Person//虚拟继承,在继承的父类前加关键字
{
public:
	int _id;
};

class Assistant : public Student, public Teacher//由于其两个父类采用了虚拟继承,那么Assistant中的_name成员只有一份了,那么它属于a对象中两个父类和自己共享
{
public:
	int _size;
};

int main()
{


	Assistant a;
	a._num = 1;
	a._id = 2;
	a._size = 3;
    
    //_name只有一份,它属于a对象中两个父类和自己共享
	a.Student::_name = "张三";
	a.Teacher::_name = "李四";
    a._name = "王五";

	return 0;
}

 调试监视窗口角度:

通过调试结果也可证实, _name只有一份,它属于a对象中两个父类和自己共享,修改_name就会影响全部。

内存角度:

 根据内存角度就可以更好的诠释虚拟继承的原理是如何体现的,图中有一处值得注意的是,a对象父类Student的成员的第一个为何是虚基表指针,而第二个是 _num成员,其实这样的分布顺序是根据成员声明的顺序来排布的,_name在Person中已经声明了,那么Person的子类中,_name就相当于是先声明的,其实在调试中也可观察出这一现象。

七、继承的反思

1. 毫无疑问的,继承充分体现了C++语法的复杂,从单继承->多继承->菱形继承->虚拟继承,这一路的变化,使得其体系无比炸裂,对于很多人难以招架,这也是C++的缺陷之一了,有了这样的教训,在后来许多的语言就没有多继承了,如java。所以我们在实践中,也不建议设计出多继承,尤其是菱形继承。

2.尽量为了避免继承,实践中会选择组合 

  • 继承是一种is-a的关系。每个派生类对象都是一个基类对象

       继承中的复用又称为“白箱”。“白箱”:其内部是可见的。在继承中,基类内部对子类而言是可见         的,那么基类的改变就会影响子类,一定程度上破坏了基类的封装,使得基类和派生类之间           的依赖关系耦合度高。

  • 组合是一种has-a的关系。B中组合了A,那么每个B对象都有一个A对象

       组合中的复用称为“黑箱”。“黑箱”:其内部不可见。B对象中的A对象对外是不可见的,在B被           使用时,外部不会对其内部造成影响,那么组合与组合之间的耦合度就低。

 所以在实践中,优先使用对象组合,耦合度低,代码维护性好。但并不代表继承毫无用处,在实现多态时,就需要用到继承。

那么本章节就到此结束了~

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

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

相关文章

Redis - Set 集合

目录 前言 命令 SADD 将一个或者多个元素添加到 set 中 语法 SMEMBERS 获取一个 set 中的所有元素 语法 SISMEMBER 判断⼀个元素在不在 set 中 语法 SCARD 获取 set 中的元素个数 语法 SPOP 从 set 中随机删除并返回⼀个或者多个元素 语法 SMOME 将⼀个元素从源 se…

电脑教程1

一、介绍几个桌面上面的软件 1、火绒&#xff1a;主要用于电脑的安全防护和广告拦截 1.1 广告拦截 1.打开火绒软件点击安全工具 点击弹窗拦截 点击截图拦截 拦截具体的小广告 2、向日葵远程控制&#xff1a;可以通过这个软件进行远程协助 可以自己去了解下 这个软件不要…

每日算法之两两交换链表中的节点

题目描述 给你一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题&#xff08;即&#xff0c;只能进行节点交换&#xff09;。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4] 输出&…

Apollo 7周年大会自动驾驶生态利剑出鞘

前言 4月22日&#xff0c;百度Apollo在北京车展前夕举办了以“破晓•拥抱智变时刻”为主题的智能汽车产品发布会&#xff0c;围绕汽车智能化&#xff0c;发布了智驾、智舱、智图等全新升级的“驾舱图”系列产品。 1、7周年大会 自2013年百度开始布局自动驾驶&#xff0c;201…

Kotlin基础​​

数据类型 定义变量 var表示定义变量&#xff0c;可以自动推导变量类型&#xff0c;所以Int可以不用写。 定义常量 条件语句 if表达式可以返回值&#xff0c;该值一般写在if里的最后一行 类似switch的用法 区间 循环 a是标签&#xff0c;可以直接break到标签的位置&#xf…

Docker 的数据管理 端口映射 容器互联 镜像创建

一 Docker 的数据管理 1 管理 Docker 容器中数据主要有两种方式&#xff1a; 数据卷&#xff08;Data Volumes&#xff09; 数据卷容器&#xff08;DataVolumes Containers&#xff09;。 1.1 数据卷 数据卷是一个供容器使用的特殊目录&#xff0c;位于容器中。可将宿主机…

【PLC学习十四】TIA.V18无法启动仿真的问题

【PLC学习十四】TIA.V18无法启动仿真的问题 文章目录 【PLC学习十四】TIA.V18无法启动仿真的问题前言一、程序仿真出现的问题二、解决方法1.无法仿真的问题2.因安全问题&#xff0c;无法编译的问题3、在TIA V18内部设置完成PG/PC接口后&#xff0c;下一次打开仍然不能仿真&…

【Vue3+Tres 三维开发】01-HelloWord

预览 什么是TRESJS 简单的说,就是基于THREEJS封装的能在vue3中使用的一个组件,可以像使用组件的方式去创建场景和模型。优势就是可以快速创建场景和要素的添加,并且能很明确知道创景中的要素构成和结构。 项目创建 npx create-vite@latest # 选择 vue typescript安装依赖…

【Linux 进程间通信】管道

文章目录 1.为什么操作系统需要向用户提供进程间通信方式&#xff1f;2.进程间通信的种类3.管道3.1管道的作用3.2管道的本质3.3管道的通信原理3.4管道的分类 1.为什么操作系统需要向用户提供进程间通信方式&#xff1f; ①&#x1f34e;资源共享&#xff1a;有的时候两个进程需…

QT——简易计算器(从0开始)

目录 一、题目描述&#xff1a; 二、创建工程&#xff1a; 1. ​编辑 2. 3. 4. 默认 5. 6. 7. 8. 默认 9. 创建完成 三、UI界面设计&#xff1a; 1. 添加按钮 1. 2. 按钮界面 3. 按钮绑定快捷键 2. 文本框添加 1. 文本框字体 2. 默认文本 3. 文本对齐方式…

英智数字孪生机器人解决方案,赋能仓库物流模式全面升级

工业机械臂、仓储机器人、物流机器人等模式的机器人系统在现代产业中扮演着愈发重要的角色&#xff0c;他们的发展推动了自动化和智能化水平的提高&#xff0c;有助于为制造业、物流业、医疗保健业和服务业等行业创造新效率并提升人们的生活质量。 行业面临的挑战 机器人开发、…

为何要与云产商进行云端防护合作,上云企业如何保障云端安全

随着大数据、云计算等信息技术的迅猛发展&#xff0c;企业为了降低成本、提高效率&#xff0c;纷纷将业务迁移至云端。 随着大数据、云计算等信息技术的迅猛发展&#xff0c;企业为了降低成本、提高效率&#xff0c;纷纷将业务迁移至云端。这一全面的上云浪潮对传统的安全企业格…

YOLOv8+PyQt5野外火焰检测系统(可以从图像、视频和摄像头三种路径检测)

1.效果视频&#xff1a;https://www.bilibili.com/video/BV1Tm421s7te/?spm_id_from333.999.0.0 2.资源包含可视化的野外火焰检测系统&#xff0c;可用于火灾预警或火灾救援&#xff0c;该系统可自动检测和识别图片或视频当中出现的火焰&#xff0c;以及自动开启摄像头&#…

使用Windows GDI进行绘图

使用Windows GDI绘图&#xff0c;可以使用MFC&#xff0c;也可以直接使用Windows API绘图&#xff0c;两者其实都一样。MFC也是封装了Windows API。 下面以MFC为例&#xff0c;进行说明。因为MFC帮我们做好了一些底层&#xff0c;可以直接使用Windows GDI的函数。 在MFC中使用…

如此建立网络根文件系统 Mount NFS RootFS

安静NFS系统服务 sudo apt-get install nfs-kernel-server 创建目录 sudo mkdir /rootfsLee 将buildroot编译的根文件系统解压缩到 sudo tar xvf rootfs.tar -C /rootfsLee/ 添加文件NFS访问路径 sudo vi /etc/exports sudo /etc/exports文件&#xff0c;添加如下一行 …

SecureCRT中添加命令显示为空如何处理?(原因添加了空行)

相关背景信息 配置相关路径:~/Library/Application\ Support/VanDyke/SecureCRT/Config包括的配置信息 按钮、命令、全局配置、色彩、以及license都在$ ls ButtonBarV4.ini Commands Global.ini SSH2.ini Button…

STM32单片机通过ST-Link 烧录和调试

系列文章目录 STM32单片机系列专栏 C语言术语和结构总结专栏 文章目录 1. ST-LINK V2 2. 操作步骤 2.1 连接方式 2.2 驱动安装常规步骤 2.3 Keil中的设置 3. 调式仿真 4. 常见问题排查 1. ST-LINK V2 ST LINK v2下载器用于STM32单片机&#xff0c;可以下载程序、调试…

代码随想录第49天|121. 买卖股票的最佳时机 122.买卖股票的最佳时机II

121. 买卖股票的最佳时机 121. 买卖股票的最佳时机 - 力扣&#xff08;LeetCode&#xff09; 代码随想录 (programmercarl.com) 动态规划之 LeetCode&#xff1a;121.买卖股票的最佳时机1_哔哩哔哩_bilibili 给定一个数组 prices &#xff0c;它的第 i 个元素 prices[i] 表示一…

为什么常用氢化物

知识星球&#xff08;星球名&#xff1a;芯片制造与封测社区&#xff09;里的学员问&#xff1a;diffusion工序&#xff0c;所需要的气体种类有哪些&#xff1f; Diffusion是什么工序&#xff1f; "Diffusion"工序是通过热能将掺杂剂原子扩散到硅片中&#xff0c;以形…

AD高速板设计--RJ45(笔记)

瑞芯微VS全志科技&#xff08;处理器芯片王者&#xff09; - 知乎 (zhihu.com) RTL8211E应用&#xff08;二&#xff09;之信号输入、输出接口_rtl8211eg中文资料-CSDN博客 Raspberry Pi 硬件 - Raspberry Pi 文档 RJ45接口的PCB设计布局布线注意事项 - 知乎 (zhihu.com) 以…