类和对象(中)【类的6个默认成员函数】 【零散知识点】 (万字)

类和对象(中)

1.类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类。

空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。

默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

image-20240526225447180

2.构造函数

2.1概念

对于以下Date类:

# include<iostream>
using namespace std;class Date
{
public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}void Print() // void Print(Date* this)  编译后{cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1,d2;d1.Init(2024, 5, 26);// d1.Init(&d1, 2024, 5, 26); // 编译后d2.Init(2077, 5, 26);// d2.Init(&d2, 2024, 5, 26);d1.Print(); // 2024-5-26 // d1.Print(&d1)d2.Print(); // 2077-5-26 // d2.Print(&d2);return 0;
}

对于Date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置

信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次

2.2特性

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象

其特征如下:

  1. 函数名与类名相同。

  2. 无返回值。

  3. 对象实例化时编译器自动调用对应的构造函数。

  4. 构造函数可以重载。

我们来看例子:

# include<iostream>
using namespace std;class Date
{
public:// 构造函数——在对象构建时调用的函数,这个函数完成初始化工作// [要注意这个函数只完成实例对象的初始化,不参与对象的构造,那是编译器的工作]Date(int year, int month, int day) // 构造函数的名和类名相同{_year = year;_month = month;_day = day;}// 构造函数不要返回值// 构造函数也支持重载Date() // 无参构造函数{_year = 0;_month = 0;_day = 0;}// 这个函数按需使用void Init(int year, int month, int day)// void Init(Date* this, int year, int month, int day){_year = year; // 如果我们设置的成员变量名称不加_或者m 这里就不好理解_month = month;_day = day;}void Print() // void Print(Date* this)  编译后{cout << _year << "-" << _month << "-" << _day << endl;// cout << this->_year << "-" << this->_month << "-" << this->_day << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1(2024, 5, 26); // 直接给实例对象传参,由自动调用的构造函数完成初始化。Date d2(2077, 5, 26);Date d3; // 通过函数重载实现,调用无参构造函数// 注意这里不能Date d3();d1.Print(); // 2024-5-26 // d1.Print(&d1);d2.Print(); // 2077-5-26 // d2.Print(&d2);// 是通过隐含的this指针实现的d3.Print(); // 0 - 0 - 0return 0;
}
  1. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
// 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
# include<iostream>
using namespace std;class Time
{
public:Time(){_hour = 0;_min = 0;_second = 0;cout << "Time()" << endl;}private:int _hour;int _min;int _second;
};class Date
{
public:// 这里我们没有显示定义构造函数,编译器会生成无参默认构造函数// 一旦用户定义了显式构造函数。那么编译器将不会生成无参构造函数// 即使只定义了带参构造函数,编译器也不会生成无参构造函数// 想要无参构造函数,要自己再定义一个//Date(int year, int month, int day) //{//	_year = year;//	_month = month;//	_day = day;//}void Init(int year, int month, int day){_year = year; _month = month;_day = day;}void Print() // void Print(Date* this)  编译后{cout << _year << "-" << _month << "-" << _day << endl;// cout << this->_year << "-" << this->_month << "-" << this->_day << endl;}private:int _year;int _month;int _day;Time _t;
};int main()
{Date d1; // 调用编译器生成的无参默认构造函数,d1.Print();// -858993460--858993460--858993460// 我们发现,即使d1调用了编译器的默认构造函数,打印出来还是随机值,看起来这个构造函数什么都没做一样// 但是实际上,这个无参默认构造函数是有做事情的// 我们给Date一个自定义的成员变量,_t// d1.Print();的结果如下://Time()//-858993460--858993460--858993460// 这个说明了一个现象// 默认生成的无参构造函数(语法坑:双标)// 1. 针对内置类型的成员变量不会做处理// 2. 针对自定义类型的成员变量,调用它的构造函数初始化return 0;
}
  1. 关于编译器生成的默认成员函数,很多人会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生的默认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??

解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数。

总结:

默认生成的无参构造函数(语法坑:双标

  • 针对内置类型的成员变量不会做处理

  • 针对自定义类型的成员变量,调用它的构造函数初始化

  • 一旦用户定义了显式构造函数。那么编译器将不会生成无参构造函数
    即使只定义了带参构造函数,编译器也不会生成无参构造函数
    想要无参构造函数,要自己再定义一个

注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值

class Time
{
public:Time(){cout << "Time()" << endl;_hour = 0;_minute = 0;_second = 0;}
private:int _hour;int _minute;int _second;
};
class Date
{
private:// 基本类型(内置类型)int _year = 1970;int _month = 1;int _day = 1;// 自定义类型Time _t;
};
int main()
{Date d;return 0;
}

但是这样也不够好,我们可以用之前学习的缺省参数——全缺省参数

  1. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数
# include<iostream>
using namespace std;
class Date
{
public: 带参构造函数//Date(int year, int month, int day) //{//	_year = year;//	_month = month;//	_day = day;//}// 构造函数不能有返回值 构造函数也支持重载//Date() // 无参构造函数//{//	_year = 0;//	_month = 0;//	_day = 0;//}// 更好的方式// 构造函数——全缺省Date(int year = 0, int month = 0, int day = 0){_year = year;_month = month;_day = day;} 注意,有了全缺省的构造函数,不能再有无参构造函数,编译的时候会产生歧义//Date() // 无参构造函数//{//	_year = 0;//	_month = 0;//	_day = 0;//}void Init(int year, int month, int day){_year = year;_month = month;_day = day;}void Print() // void Print(Date* this)  编译后{cout << _year << "-" << _month << "-" << _day << endl;// cout << this->_year << "-" << this->_month << "-" << this->_day << endl;}private:int _year;int _month;int _day;};int main()
{Date d1; // 调用默认构造函数// 1. 自己实现的无参构造函数// 2. 自己实现的全缺省构造函数// 3. 编译器自动生成的默认构造函数// 这三个的特点都是无参,不传参数// 因此这三个只能存在一个Date d2(2024, 5, 27);d1.Print();// 0-0-0d2.Print();// 2024-5-27return 0;
}

总结:
默认构造函数一共有三种:

  • 自己实现的无参构造函数
  • 自己实现的全缺省构造函数
  • 编译器自动生成的默认构造函数

这三种默认构造函数只能存在一种

3.析构函数

3.1 概念

通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

3.2 特性

析构函数是特殊的成员函数,其特征如下:

  1. 析构函数名是在类名前加上字符 ~。

  2. 无参数无返回值类型。

  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载

  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数

来看一段代码:

// 析构函数——在实例对象的生命周期结束的时候,会自动调用析构函数
# include<iostream>
using namespace std;
class Date
{
public:// 构造函数——全缺省  [一种默认构造函数]Date(int year = 0, int month = 0, int day = 0){_year = year;_month = month;_day = day;}void Print() // void Print(Date* this)  编译后{cout << _year << "-" << _month << "-" << _day << endl;}~Date(){cout << "析构函数" << endl;}private:int _year;int _month;int _day;};int main()
{// 析构函数,会在这两个对象生命周期结束之后自动调用,完成清理工作,不是完成对其的销毁Date d1; // 调用默认构造函数Date d2(2024, 5, 27);// 执行代码,会执行两次析构函数//析构函数//析构函数//注意这个析构,先析构d2 在析构d1,因为是在栈区的变量return 0;
}

在上述代码中要注意:析构,先析构d2 在析构d1,因为是在栈区的变量

而有了析构函数和构造函数的存在,我们来感受一下它们的使用。

这里我们来写一个Stack类,这个Stack类,不在需要初始化和销毁的接口,由构造函数和析构函数代替了。

// 实现Stack类——使用构造和析构函数
# include<iostream>
using namespace std;class Stack
{
public:// 构造函数Stack(int n = 10){_a = (int*)malloc(sizeof(int) * n);if (_a == NULL){perror("Stack()malloc()");exit(-1);}cout << "malloc:" << _a << endl; _size = 0;_capacity = n;}// ....... 栈的接口// 析构函数~Stack(){free(_a);cout << "free:" << _a << endl;_a = NULL;_size = _capacity = 0;}private:int* _a;int _size;int _capacity;
};int main()
{Stack s1; Stack s2; // 注意先析构s2 在析构s1。因为两个局部变量在栈区上 ,后进先出//malloc:000001FC0C62BEF0//malloc : 000001FC0C636160//free : 000001FC0C636160//free : 000001FC0C62BEF0return 0;
}
  1. 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对自定类型成员调用它的析构函数。
# include<iostream>
using namespace std;class Time
{
public:~Time(){cout << "~Time()" << endl;}
private:int _hour;int _minute;int _second;
};class Date
{
private:// 基本类型(内置类型)int _year = 1970;int _month = 1;int _day = 1;// 自定义类型Time _t;
};
int main()
{Date d;return 0;
}
// 程序运行结束后输出:~Time()
// 在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
// 因为:main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month, _day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t是Time类对象,所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是:
// main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁// main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数
// 注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数

总结:

默认生成的析构函数(语法坑:双标

  • 针对内置类型的成员变量不会做处理

  • 针对自定义类型的成员变量,调用它的析构函数

  • 一旦用户定义了显式析构函数。那么编译器将不会生成默认析构函数

  1. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。

4.拷贝构造函数

4.1概念

在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎

那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?

拷贝构造函数只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用

4.2特征

拷贝构造函数也是特殊的成员函数,其特征如下:

  1. 拷贝构造函数是构造函数的一个重载形式

  2. 拷贝构造函数的参数只有一个必须是类 类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。

我们来看一个例子:

// 拷贝构造函数
# include<iostream>
using namespace std;class Date
{
public:// 构造函数Date(int year = 0, int month = 0, int day = 0){_year = year;_month = month;_day = day;}// 这样无法编译通过,因为这里会造成递归拷贝,会无穷递归下去Date(Date d) // 构造函数的重载函数{_year = d._year;_month = d._month;_day = d._day;}// 析构函数~Date(){cout << "析构函数" << endl;}private:int _year;int _month;int _day;};int main()
{Date d1(2024, 5, 27);// 如果我们想创建一个跟d1一模一样的对象,我们可以怎么做呢?//Date d2(2024, 5, 27);// 这样可以,但是肯定不好,因为d1一变,我们还要手动更改d2// 这个时候我们就可以用到拷贝构造Date d2(d1); // 传d1进去,// 这里会报错return 0;
}

为什么会造成无穷递归呢?

	// 这样无法编译通过,因为这里会造成递归拷贝,会无穷递归下去Date(Date d) // 构造函数的重载函数{_year = d._year;_month = d._month;_day = d._day;}
int main()
{// Date d2(d1);
}

因为我们这里是传值给拷贝构造函数,我们知道,传值调用的形参就是实参的一个临时拷贝,既然是拷贝,我们就要创建一个跟实参一个类型的值,那实参是什么类型的值呢,是Date,相当于要把实参d1 拷贝给 形参d,那d的构造又需要调用拷贝构造函数,那又要传参,又要拷贝实参,又要构造形参,又要调用拷贝构造函数。无限循环下去。

如图所示:

image-20240527152533769

解决方法是什么呢?

其实就是让形参是实参的别名就好了。

image-20240527162520756

// 拷贝构造函数
# include<iostream>
using namespace std;class Date
{
public:// 构造函数Date(int year = 0, int month = 0, int day = 0){_year = year;_month = month;_day = day;} 这样无法编译通过,因为这里会造成递归拷贝,会无穷递归下去//Date(Date d)//{//	_year = d._year;//	_month = d._month;//	_day = d._day;//}// 我们使用别名来解决这个问题Date(const Date& d) // 传参的过程相当于 Date& d = d1;{_year = d._year;_month = d._month;_day = d._day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}// 析构函数~Date(){cout << "析构函数" << endl;}private:int _year;int _month;int _day;};int main()
{Date d1(2024, 5, 27);// 如果我们想创建一个跟d1一模一样的对象,我们可以怎么做呢?//Date d2(2024, 5, 27);// 这样可以,但是肯定不好,因为d1一变,我们还要手动更改d2// 这个时候我们就可以用到拷贝构造Date d2(d1); // 传d1进去Date d3 = d1; // 这个也是拷贝构造。d2.Print();d3.Print();return 0;
}

上面这个代码的运行结果

image-20240527164231057

  1. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
class Time
{
public:Time(){_hour = 1;_minute = 1;_second = 1;}Time(const Time& t){_hour = t._hour;_minute = t._minute;_second = t._second;cout << "Time::Time(const Time&)" << endl;}
private:int _hour;int _minute;int _second;
};
class Date
{
private:// 基本类型(内置类型)int _year = 2024;int _month = 5;int _day = 28;// 自定义类型Time _t;
};int main()
{Date d1;// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数Date d2(d1);return 0;
}
  1. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?
// 涉及内存资源管理的Stack的拷贝构造
# include<iostream>
using namespace std;class Stack
{
public:// 构造函数Stack(int n = 10){_a = (int*)malloc(sizeof(int) * n);if (_a == NULL){perror("Stack()malloc()");exit(-1);}cout << "malloc:" << _a << endl;_size = 0;_capacity = n;}// ....... 栈的接口// 析构函数~Stack(){free(_a);cout << "free:" << _a << endl;_a = NULL;_size = _capacity = 0;}private:int* _a;int _size;int _capacity;
};int main()
{// 浅拷贝问题Stack s1;Stack s2(s1);return 0;
}

上面我们的Stack类中,没有显式实现的拷贝构造,因此我们在main函数中的拷贝构造 的使用,用的都是编译器生成的默认的函数。这里用的是浅拷贝。因此造成了c++中比较经典的一个问题——浅拷贝问题

我们来分析一下代码为何会崩溃。

  1. 首先就是我们在调用拷贝构造的时候,我们是将s1对象一个一个字节拷贝到s2中的,这就意味着我们两个对象所存储的数组指针_a是一样的,也就是说指向的数组都是同一个空间的数组
  2. 这个时候我们的代码还不会崩溃,并且也确实成功完成了拷贝的任务

image-20240529004507107

  1. 但是问题会出现在析构函数上,我们知道在析构函数中,我们要完成对栈这个类对象的资源清理工作,我们要在析构函数中释放掉数组的空间,由于s1 s2存储在栈区,我们先释放掉s2的数组空间,但是紧接着我们要释放掉s1的数组空间,但是s1和s2指向的数组是同一个数组,这就会造成会同一个空间的重复释放,这就会导致代码崩溃

image-20240529005943051

image-20240529005527934

因此对于这种涉及到内存资源管理的类,我们需要自己实现深拷贝的拷贝构造

但是这里有人会说,那我不自己实现析构函数不就不会报错了吗?

因为我们的析构函数中存在对数组空间的释放,但是不释放就会造成内存泄漏的问题,释放了就会造成浅拷贝问题。

  1. 拷贝构造函数典型调用场景:
  • 使用已存在对象创建新对象
  • 函数参数类型为类类型对象
  • 函数返回值类型为类类型对象
class Date
{
public:Date(int year, int minute, int day){cout << "Date(int,int,int):" << this << endl;}Date(const Date& d){cout << "Date(const Date& d):" << this << endl;}~Date(){cout << "~Date():" << this << endl;}
private:int _year;int _month;int _day;
};
Date Test(Date d) // 调用拷贝构造函数
{Date temp(d);return temp; // 调用拷贝构造函数
}
int main()
{Date d1(2022, 1, 13); // 调用拷贝构造函数Test(d1);return 0;
}

5.赋值运算符重载

5.1 运算符重载

为什么要搞运算符重载呢?

其实很简单,就是因为内置类型,我们在使用运算符的时候。编译器知道怎么去比较,但是如果我们自己定义了一个类,在用内置的运算符,编译器就不知道怎么比较了,因此,为了能够方便的比较我们自己定义的类对象,我们需要对运算符重载

在之前我们是通过自定义函数,来解决这个问题的,但是这个解决方式也没有运算符重载好。因为可读性不是很好。

C++为了增强代码的可读性引入了运算符重载运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数名字为:关键字operator后面接需要重载的运算符符号

函数原型:返回值类型 operator操作符(参数列表)

注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@

  • 重载操作符必须有一个类类型参数

  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义

  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this指针形参

  • .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。注意是 **.**不能重载,不是 (解引用)

我们来看一段代码:

// 运算符重载
# include<iostream>
using namespace std;class Date
{
public:// 默认构造函数Date(int year = 0, int month = 0, int day = 0){_year = year;_month = month;_day = day;}Date(const Date& d) // 拷贝构造函数{_year = d._year;_month = d._month;_day = d._day;}//private: // 我们的成员变量不能是私有的,不然运算符重载函数内无法访问int _year;int _month;int _day;};// 运算符有几个操作数, operator重载的函数就有几个参数
bool operator==(const Date& d1, const Date& d2)
{return d1._year == d2._year&& d1._month == d2._month&& d1._day == d2._day;
}int main()
{Date d1(2024, 5, 27);Date d2(d1);// 如果我们相对Date类的 对象 d1 d2 进行比较// 我们可以通过定义函数实现//IsDateEqual(d1, d2); // 可读性不好// 为了让可读性更好,c++推出了运算符重载d1 == d2;// 编译过后会变成, operator==(d1, d2)// 如果不对== 进行重载,那么这里就会编译出错operator==(d1, d2); // 等价d1 == d2 ,但是不推荐这样写,这样可读性又下降了if (d1 == d2)cout << "相等" << endl;return 0;
}

上述的写法不好,因为我们为了实现运算符重载,我们牺牲了Date类中成员变量的私有性。把成员变量变成公有的,才能让运算符重载函数访问到其成员变量。

要如何解决这个问题呢?

这里其实可以用友元解决,这个后面学习

我们还可以通过把运算符重载函数写进类中来解决

# include<iostream>
using namespace std;class Date
{
public:// 默认构造函数Date(int year = 0, int month = 0, int day = 0){_year = year;_month = month;_day = day;}Date(const Date& d) // 拷贝构造函数{_year = d._year;_month = d._month;_day = d._day;}// 运算符重载// 我们知道成员函数会自带this指针形参,因此这里要修改函数的形参个数。// 我们知道谁调用这个运算符重载函数,那么this指针就指向谁// d1 == d2;// d1.operator==(d2);bool operator==(const Date& d) // bool operator==(Date* this, const Date& d){return _year == d._year&& _month == d._month&& _day == d._day;}// d1 > d2bool operator>(const Date& d){if (_year > d._year){return true;}else if (_year == d._year && _month > d._month){return true;}else if (_year == d._year &&  _month == d._month && _day > d._day){return true;}elsereturn false;}private:int _year;int _month;int _day;};int main()
{Date d1(2024, 5, 27);Date d2(d1);d1 == d2; // 编译后变成  d1.operator==(d2)if (d1 > d2)cout << "大于" << endl;elsecout << "其他" << endl;return 0;
}

5.2赋值运算符重载

  1. 赋值运算符重载格式
  • 参数类型:const T&,传递引用可以提高传参效率
  • 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
  • 检测是否自己给自己赋值
  • 返回*this :要复合连续赋值的含义

来看代码:

// 赋值运算符重载
# include<iostream>
using namespace std;class Date
{
public:// 默认构造函数Date(int year = 0, int month = 0, int day = 0){_year = year;_month = month;_day = day;}// 拷贝构造函数Date(const Date& d) {_year = d._year;_month = d._month;_day = d._day;}// 运算符重载// d1 == d2 -> d1.operator==(d2);bool operator==(const Date& d) // bool operator==(Date* this, const Date& d){return _year == d._year&& _month == d._month&& _day == d._day;}// 赋值运算符重载//d2 = d1  -> d2.operator(&d2, d1)Date& operator=(const Date& d) // 其实这里不用引用传参也不会报错,但是使用引用传参效率更高// void operator(Date* this, const Date d){// 自己给自己赋值是没有意义的if (this == &d) // d是引用,d的地址和this相同就说明是自己给自己赋值return *this; _year = d._year;_month = d._month;_day = d._day;return *this;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;};int main()
{Date d1(2024, 5, 27);Date d2 = d1; //注意这里这个=不是赋值运算符,而是拷贝构造Date d3;d3 = d1;d3.Print();// 2024-5-27d3 = d3; // 自己赋值给自己//我们的=除了要实现单个的赋值,还要能实现连续的赋值d2 = d3 = d1;// 先执行d3 = d1,也就是d3.operator=(d1), 其返回值得是d3 才可以d2.Print();return 0;
}

在来看看拷贝构造和 赋值运算符重载的区别:

image-20240528215910346

  1. 赋值运算符只能重载成类的成员函数不能重载成全局函数
class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}int _year;int _month;int _day;
};// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{if (&left != &right){left._year = right._year;left._month = right._month;left._day = right._day;}return left;
}// 编译失败:
// error C2801: “operator =”必须是非静态成员

编译出错的原因是:

赋值运算符如果不显式实现,编译器会生成默认的赋值运算符重载。此时我们类外边在定义一个赋值运算符重载,就会和编译器的产生冲突。因此,赋值运算符重载只能是类的成员函数

  1. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
// 如果没有显式实现 拷贝构造 和 赋值运算符重载,编译器会生成默认的
# include<iostream>
using namespace std;class Date
{
public:// 默认构造函数Date(int year = 0, int month = 0, int day = 0){_year = year;_month = month;_day = day;} 拷贝构造函数//Date(const Date& d)//{//	_year = d._year;//	_month = d._month;//	_day = d._day;//} 赋值运算符重载d2 = d1  -> d2.operator(&d2, d1)//Date& operator=(const Date& d) // 其实这里不用引用传参也不会报错,但是使用引用传参效率更高//	// void operator(Date* this, const Date d)//{//	// 自己给自己赋值是没有意义的//	if (this == &d) // d是引用,d的地址和this相同就说明是自己给自己赋值//		return *this;//	_year = d._year;//	_month = d._month;//	_day = d._day;//	return *this;//}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1(2024, 5, 10);Date d2(2024, 5, 28);d1 = d2; // 即使我们类中屏蔽了赋值运算符重载,这里不会报错// 因为我们不实现的时候,编译器会生成拷贝构造和 operator= // 会完成按字节的值拷贝(浅拷贝)。// 也就是说有些类,比如这个日期类,我们不需要去实现拷贝构造和赋值运算符重载。// 但是涉及到 内存管理 的类 我们就需要去实现拷贝构造和赋值运算符重载d1.Print();// 2024-5-28d2.Print();// 2024-5-28Date d3(d1); // 屏蔽了拷贝构造函数,也不会报错,这里调用了编译器生成的默认拷贝构造函数Date d4 = d3;d3.Print();// 2024-5-28d4.Print();// 2024-5-28return 0;
}
  • 值拷贝(浅拷贝):将对象按照一个字节一个字节的传过去。

注意: 只有六个成员函数我们不显式实现,编译器会自动生成。

拷贝构造函数和 赋值运算符函数就是其中之二,其他运算符编译器在没有显式实现的时候不会默认生成

但是这样就产生了一个问题:

  • 我们还需要去自己实现拷贝构造函数和赋值运算符重载吗?编译器不是已经帮我们实现了吗?

答案当时是需要的,因为除了我们的日期类,还有许多类我们会涉及到**(内存的资源管理)**。

我们来看一段代码来感受一下:

// 涉及内存资源管理的Stack的拷贝构造和赋值运算符重载
# include<iostream>
using namespace std;class Stack
{
public:// 构造函数Stack(int n = 10){_a = (int*)malloc(sizeof(int) * n);if (_a == NULL){perror("Stack()malloc()");exit(-1);}cout << "malloc:" << _a << endl;_size = 0;_capacity = n;}// ....... 栈的接口// 析构函数~Stack(){free(_a);cout << "free:" << _a << endl;_a = NULL;_size = _capacity = 0;}private:int* _a;int _size;int _capacity;
};int main()
{// 浅拷贝问题Stack s1;Stack s3(30);s1 = s3;return 0;
}

上面我们的Stack类中,没有显式实现的赋值运算符重载,因此我们再main函数中的赋值运算符的使用,用的都是编译器生成的默认的函数。这里用的是浅拷贝。造成了浅拷贝问题

前面我们已经知道了拷贝构造函数造成浅拷贝的分析和原理了

对于赋值运算符也是同样的道理,我们将s3赋值给s1用的是浅拷贝,s1和s3的_a数组指针,指向的都是同一个数组空间,在调用析构函数的时候,也会造成对同一个空间的重复释放,代码崩溃.

image-20240529004537914

我们来看图片来更好的理解一下:

image-20240529003926332

image-20240529005154244

因此对于这种涉及到内存资源管理的类,我们需要自己实现深拷贝的赋值运算符重载

但是这里有人会说,那我不自己实现析构函数不就不会报错了吗?

因为我们的析构函数中存在对数组空间的释放,但是不释放就会造成内存泄漏的问题,释放了就会造成浅拷贝问题。

5.3前置++和后置++的重载

直接来看代码:

class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// 前置++:返回+1之后的结果// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率Date& operator++(){_day += 1;return *this;}// 后置++:// 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载// C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this + 1// 而temp是临时对象,因此只能以值的方式返回,不能返回引用Date operator++(int){Date temp(*this);_day += 1;return temp;}private:int _year;int _month;int _day;
};int main()
{Date d;Date d1(2022, 1, 13);d = d1++;    // d: 2022,1,13   d1:2022,1,14d = ++d1;    // d: 2022,1,15   d1:2022,1,15return 0;
}

6.日期类的实现

实际上完整的完善的日期类,我们应该让声明和定义分离,并且还要优化代码。这里为了方便,我们就不让声明和定义分离了。

// 实现一个完善的日期类
# include<iostream>
using namespace std;class Date
{
public:int GetMonthDay(int year, int month){// 给13 的原因是为了刚好让下标对上月份static int monthday[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };// 给static的原因是 每次访问的数组都不变并且都是这个数组,那每次访问都要开辟,不如直接放静态区去// 闰年的2月份 是 29天if (month == 2 && (year % 4 == 0 && year % 100 != 0) || year % 400 == 0)monthday[2] = 29;elsemonthday[2] = 28;int day = monthday[month];return day;}// 默认构造函数Date(int year = 0, int month = 0, int day = 0){// 对传进来的年 月 日 进行判断,是否合法,合法才构造if (year >= 0 && month > 0 && month < 13 && day <= GetMonthDay(year, month)){_year = year;_month = month;_day = day;}else{cout << "非法日期" << endl;}}// 拷贝构造函数 (其实日期类这种不涉及内存资源管理的类,不需要我们显式实现拷贝构造,但是实现了也没问题)// Date(Date* this, const Date& d) Date(const Date& d) // 一定要有别名 & ,不然会无限递归{_year = d._year;_month = d._month;_day = d._day;}// 析构函数~Date() // 其实类似日期类这种类,是不需要我们去编写显式析构函数的{cout << " 析构函数" << endl;}// 运算符重载//d1 < d2  编译器处理后 d1.operator<(&d1, d2)bool operator<(const Date& d)// bool operator(Date* this, const Date& d){if (_year < d._year)return true;else if (_year == d._year && _month < d._month)return true;else if (_year == d._year && _month == d._month && _day < d._day)return true;elsereturn false;}// d1 == d2 编译器处理后  d1.operator==(&d1, d2)bool operator==(const Date& d)// bool operator==(Date* this, const Date& d){return _year == d._year&& _month == d._month&& _day == d._day;}// d1 <= d2  编译器处理  d1.operator<=(&d1, d2)bool operator<=(const Date& d) // bool operator<=(Date* this, const Date& d){// 这里我们采用复用上面的代码来实现,以减少代码重复return *this < d || *this == d; // *this 解引用就是 d1// (*this).operator<(&(*this), d) || (*this).operator==(&(*this), d);}// d1 > d2 编译器处理后 d1.operator>(&d1, d2)bool operator>(const Date& d){// 不再采取之前那种代码,代码重复性太强,并且一旦成员变量改变,代码也要跟着大量修改// 我们采取函数复用return !(*this <= d); // 只要*this 不<= d  那就是> d// 编译器处理后  !((*this).operator<=(&(*this), d))}// d1 >= d2 编译器处理 d1.operator>=(&d1, d2)bool operator>=(const Date& d)// bool operator>=(Date* this, const Date& d){// 也采用函数复用。复用前面已经实现的 >  和 == 的运算符重载函数return *this > d || *this == d;}// d1 != d2 ->  d1.operator!=(&d1, d2)bool operator!=(const Date& d)// bool operator!=(Date* this, const Date& d){// 函数复用return !(*this == d);} d1 + 10(天数) -> d1.operator+(&d1, 10)//Date operator+(int day) // Date operator+(Date* this, int day) 这里不能使用引用返回,因为拷贝构造的ret在函数结束之后就会销毁//{//	Date ret = *this; // 拷贝构造一个d1 ,这里等价于 ret(*this)//	ret._day += day;//	// 对day进行判断是否合法,合法就输出,不合法要进位//	while (ret._day > GetMonthDay(ret._year, ret._month))// 有可能不止进一次位,所以给一个循环//	{//		ret._day -= GetMonthDay(ret._year, ret._month);//		ret._month++;//		// 要注意月份是否合法//		if (ret._month == 13)//		{//			ret._year++;//			ret._month = 1;//		}//	}//	return ret;//}//d1 += 10 -> d1.operator+=(&d1, 10)Date& operator+=(int day)// 这里可以使用引用返回,因为this指向的本来就是外面的d1,这个函数结束之后,d1不会销毁{// 如果day是负数。要处理if (day < 0){return *this -= -day; // 加负数 相当于 减正数}// += 和 + 的区别是, + 不改变d1本身,但是+=要改变d1本身_day += day;while (_day > GetMonthDay(_year, _month)){_day -= GetMonthDay(_year, _month);_month++;// 判断月份是否合法if (_month == 13){_year++;_month = 1;}}return *this; // 返回的是d1本身}// 上面我们对+的重载和 对+=的重载的代码重复性很高,那我们就可以考虑采用复用// d1 + 10Date operator+(int day){Date ret(*this); // 拷贝构造d1ret += day; // 复用我们实现的+=的重载// ret.operator+=(day)return ret;} d1 - 10(天数) -> d1.operator-(&d1, 10)//Date operator-(int day)//{//	Date ret = *this; // 拷贝构造一个d1//	ret._day -= day;//	// 判断-=day之后的 day是否合法,合法就返回,不合法就要退位//	while (ret._day <= 0)//	{//		ret._month--; // 先让月份退到上一个月//		// 判断月份是否合法//		if (ret._month == 0)//		{//			ret._month = 12; //			ret._year--;//		}//		ret._day += GetMonthDay(ret._year, ret._month); // + 上一个月份的天数,看看是否_day是否合法//	}//	return ret;//}// d1 -= 10 -> d1.operator(&d1, 10)Date& operator-=(int day) // Date& operator(Date* this, int day){// 判断day是否是负数if (day < 0){return *this += -day; // 减负数 相当于 加正数}// -= 改变的是d1自己,也就是this指针指向的对象_day -= day;// 判断_day是否合法,不合法要退位,直至合法while (_day <= 0){// 先让月份退一位_month--;// 判断月份是否合法if (_month == 0){_month = 12;_year--;}_day += GetMonthDay(_year, _month); // += 上一个月份的天数,看看是否_day是否合法}return *this; // 返回自身}// 对-的重载采用代码复用,减少对代码的重复性,提升维护性// d1 - 10Date operator-(int day){Date ret(*this); // 拷贝构造d1ret -= day; // 函数复用return ret; // 返回ret的时候要创建一个临时变量, 会调用一次拷贝构造}// ++d1 -> d1.operator++(&d1)Date& operator++() // Date& operator(Date* this){// ++也可以像之前一样,每次调用+1天,+完之后判断day是否需要进位,月是否需要进位,年是否需要进位// 但是为了提升类的维护性,和减少代码重复度,我们采取复用*this += 1;// ++d1就是让自己去 + 1return *this;}// d1++ -> d1.operator++(&d1, 0)Date operator++(int) // 加int是为了代表是后置++,如果不加就是默认前置++{// 前置++和后置++的区别就是 前置++返回+之后的  后置++返回+之前的Date tmp = *this;*this += 1;return tmp; // 返回+之前的}//--d1Date& operator--(){*this -= 1; // 函数复用return *this;}//d1--Date operator--(int){// 前置-- 要返回--之后的   后置-- 要返回-之前的Date tmp(*this);*this -= 1;return tmp;}// 赋值运算符重载 operator=// d1 = d2 -> d1.operator(&d1, d2)Date& operator=(const Date& d){// 防止自己给自己赋值if (this != &d) // this指向的地址  d是别名,d的地址和this指针相等就说明是自己给自己赋值{_year = d._year;_month = d._month;_day = d._day;}return *this;}// 日期 - 日期   d1 - d2// 我们让小的日期++  直至 == 大的日期, + 了多少次,就差了多少天int operator-(const Date& d){int flag = 1;// 默认大的是 this指针指向的  小的是 dDate max = *this; // 拷贝构造Date min = d; // 拷贝构造if (max < min){// 走到这里说明  默认的情况是错的min = *this;max = d;flag = -1; // 说明是小的 - 大的 最后应该是个负数}int n = 0;while (max > min) // 让小日期一直加 直至 == 大日期{++min;++n;}// 加了多少次,n就是多少,n就是相差的天数return n * flag;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1(2024, 5, 28);Date d2 = d1; // 等价于 d2(d1)cout << (d1 < d2) << endl; // 0cout << (d1 == d2) << endl;// 1cout << (d1 > d2) << endl;// 0cout << (d1 != d2) << endl;// 0cout << (d1 <= d2) << endl;// 1cout << (d1 >= d2) << endl;// 1// 是否要重载一个运算符,看的是这个运算符是否对该类的对象有意义// 比如日期 + 日期没有意义, 但是日期 - 日期有意义,是两日期相隔的天数// 比如 日期 + 天数 有意义,是多少天之后, 日期 - 天数有意义 是多少天之前// 而有+ 就有 += , 有-就有-=  , 并且日期 * 日期 和 日期 / 日期是没有意义的Date d3 = d1 + 10; // 将d1 + 10后的日期 拷贝构造到d3对象d3.Print();// 2024-6-7Date d4(d1 + 100);d4.Print();// 2024-9-5Date d5(d1 + 1000);d5.Print();// 2027-2-22Date d6(d1 - 1000);d6.Print();// 2021-9-1Date d7(d1 -= 1000);d7.Print();// 2021-9-1Date d8(d1++);d8.Print(); // 2021-9-1Date d9(++d1);d9.Print();//2021-9-3Date d10(d1--);d10.Print();// 2021-9-3Date d11(--d1);d11.Print();//2021-9-1cout << d3 - d1 << endl; // 1010  cout << d1 - d3 << endl;// -1010return 0;
}

image-20240529153501815

中间两次调用析构函数是因为,在日期 - 日期函数中,我们创建了两个Date类的局部变量,函数栈帧结束后要销毁变量。调用了析构函数

7.const成员

将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改

image-20240529212234519

我们来看一段代码:

// const成员
# include<iostream>
using namespace std;class Date
{
public:// 默认构造函数Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// 拷贝构造函数Date(const Date& d) {_year = d._year;_month = d._month;_day = d._day;}// 由于this指针是隐含的 所以不能这样加  void Print(const Date* this)// 因此我们使用const修饰成员函数, 实际上修饰的是 *this, 也就是this指针指向的对象void Print() const // 编译后 -> void Print(const Date* this){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;};void f(const Date& d)
{d.Print();// 编译无法通过 编译后 -> d.Print(&d)// 因为这里的&d 是const Date*类型, 但是Print函数的this形参是 Date* 类型// 涉及到了权限放大的问题,就无法编译通过
}int main()
{Date d1;f(d1);return 0;
}

这里可以在回忆一下const修饰的用法

  1. const Date p1;*

  2. Date const p2;*

  3. Date const p3;*

第一第二种const都在*的左边,修饰的都是指针指向的对象,也就是不能修改指针指向的对象。

第三种 const在* 的右边,修饰的是指针本身,也就是不能让修改指针本身指向其他地方

上面代码属于是对象调用成员函数,那我们再来看看成员函数之间的相互调用,是否也有const之间的关系

来看代码:

// 成员函数之间调用
# include<iostream>
using namespace std;class Date
{
public:// 默认构造函数Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// 拷贝构造函数Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}void f1()// void f1(Date* this){f2(); // 编译后 this->f2(this);// 这里的*this是可读可写的,传进去的*this要变成const Date类型,只能读// 属于权限缩小,不会报错}void f2() const // void f2(const Date* this){}void f3()// void f3(Date* this){}void f4() const // void f4(const Date* this){f3(); // this->f3(this)  会报错// 这里的*this 只能读不能改, 但是传给f3之后会变成可读可写的// 属于权限放大,因此会报错}void Print() const // 编译后 -> void Print(const Date* this){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;};int main()
{return 0;
}

看了上面两段代码之后,请思考下面的几个问题:

  1. const对象可以调用非const成员函数吗?【不可以】

  2. 非const对象可以调用const成员函数吗?【可以】

  3. const成员函数内可以调用其它的非const成员函数吗?【不可以】

  4. 非const成员函数内可以调用其它的const成员函数吗?【可以】

注意:

成员函数之间调用,实际上就是this指针指向的对象在调用。

总结:

只要成员函数中不需要修改成员变量,最好都加上const修饰成const成员函数

不然有const类型的对象传进来就会报错,因为权限缩小了。上面我们也有讲。

如图所示:

image-20240530002352630

8.取地址及const取地址操作符重载

这两个默认成员函数一般不用重新定义 ,编译器默认会生成。

//取地址及const取地址操作符重载
# include<iostream>
using namespace std;class Date
{
public:// 默认构造函数Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// 取地址操作符重载Date* operator&(){cout << "取地址操作符重载" << endl;return this;}// 取地址操作符重载const Date* operator&() const{cout << "const取地址操作符重载" << endl;return this;}void Print() const // 编译后 -> void Print(const Date* this){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;};int main()
{Date d1, d2;const Date d3, d4;cout << &d1 << endl;//取地址操作符重载//00000008B33CF528cout << &d2 << endl;//取地址操作符重载//00000008B33CF558//cout << &d3 << endl; // 这里不会调用我们实现的&重载函数,因为d3是const Date类 权限放大了。 仍然获得了d3的地址是因为调用了编译器默认生成的const取地址操作符重载函数000000A4BA3AF568// const取地址操作符重载函数我们也可以自己实现cout << &d4 << endl; // 这里调用的就是我们自己实现的//const取地址操作符重载//00000008B33CF5B8return 0;
}

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!

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

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

相关文章

【Python】如何使用 Python 自动发送每日电子邮件报告

人不走空 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌赋&#xff1a;斯是陋室&#xff0c;惟吾德馨 目录 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌…

整合Spring Boot 框架集成Knife4j

本次示例使用Spring Boot作为脚手架来快速集成Knife4j,Spring Boot版本2.3.5.RELEASE ,Knife4j版本2.0.7 POM.XML完整文件代码如下&#xff1a; <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0…

智能合约引领:探索Web3的商业革新之路

随着区块链技术的迅速发展&#xff0c;智能合约作为其重要应用之一&#xff0c;正在逐步改变着商业世界的格局。Web3作为下一代互联网的代表&#xff0c;正引领着智能合约在商业领域的广泛应用和创新。本文将深入探讨智能合约在Web3中的作用&#xff0c;以及智能合约如何引领着…

【正在线上召开】2024机器智能与数字化应用国际会议(MIDA2024),免费参会

【ACM出版】2024机器智能与数字化应用国际会议&#xff08;MIDA2024&#xff09; 2024 International Conference on Machine Intelligence and Digital Applications 【支持单位】 宁波财经学院 法国上阿尔萨斯大学 【大会主席】 Ljiljana Trajkovic 加拿大西蒙菲莎大…

使用画图工具修改图片文字

方法思路&#xff1a; 使用背景色将需要修改的文字覆盖&#xff0c;然后在原来的地方加入修改后的字。 第一步&#xff1a; 选中图片后右键&#xff0c;选择“编辑”&#xff08;默认会使用画图工具打开&#xff09; 第二步&#xff1a; 选取颜色选取器&#xff0c;如下图 使…

【Text2SQL 论文】DIN-SQL:分解任务 + 自我纠正 + in-context 让 LLM 完成 Text2SQL

论文&#xff1a;DIN-SQL: Decomposed In-Context Learning of Text-to-SQL with Self-Correction ⭐⭐⭐⭐ NeurIPS 2023, arXiv:2304.11015 Code: Few-shot-NL2SQL-with-prompting | GitHub 文章目录 一、论文速读1.1 Schema Linking Module1.2 Classification & Decompo…

【每日刷题】Day52

【每日刷题】Day52 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f34d; &#x1f33c;文章目录&#x1f33c; 1. 2965. 找出缺失和重复的数字 - 力扣&#xff08;LeetCode&#xff09; 2. 350. 两个数组的交集 II …

kaggle竞赛系列基于图像对水稻分类代码案例

目录 依赖环境 代码 导入依赖包 定义数据集路径&#xff1a; 创建训练集、验证集和测试集的文件夹&#xff1a; 代码的作用&#xff1a; 设置新的数据集路径与类别名称 代码的作用&#xff1a; 定义数据预处理和增强变换&#xff1a; 代码的作用&#xff1a; 定义数…

【源码】多语言H5聊天室/thinkphp多国语言即时通讯/H5聊天室源码/在线聊天/全开源

多语言聊天室系统&#xff0c;可当即时通讯用&#xff0c;系统默认无需注册即可进入群聊天&#xff0c;全开源 【海外聊天室】多语言H5聊天室/thinkphp多国语言即时通讯/H5聊天室源码/在线聊天/全开源 - 吾爱资源网

每日5题Day13 - LeetCode 61 - 65

每一步向前都是向自己的梦想更近一步&#xff0c;坚持不懈&#xff0c;勇往直前&#xff01; 第一题&#xff1a;61. 旋转链表 - 力扣&#xff08;LeetCode&#xff09; /*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;…

STM32学习和实践笔记(32):电容触摸按键实验

1.电容触摸按键原理介绍 触摸按键与传统的机械按键相比&#xff0c;不仅美观而且耐用、寿命长&#xff0c;它颠覆了传统意义上的机械按键控制&#xff0c;只要轻轻触摸&#xff0c;就可以实现按键开关的控制、量化调节甚至方向控制。触摸按键已广泛应用于手机、DVD、洗衣机等消…

宿舍管理系统带文档java项目基于springboot+vue的宿舍管理系统

文章目录 宿舍管理系统一、项目演示二、项目介绍三、八千字项目文档四、部分功能截图五、部分代码展示六、底部获取项目源码和8000字论文参考&#xff08;9.9&#xffe5;带走&#xff09; 宿舍管理系统 一、项目演示 宿舍管理系统 二、项目介绍 基于springbootvue的前后端分…

MVC、MVP 和 MVVM 架构总结

MVC、MVP 和 MVVM 是常见的软件架构模式&#xff0c;主要用于组织应用程序的结构&#xff0c;特别是在用户界面和业务逻辑之间进行分离。以下是对它们的详细解释&#xff0c;包括它们的差异、优缺点。 MVC&#xff08;Model-View-Controller&#xff09; 结构 Model&#xf…

在长窗口时代,RAG技术是否仍然必要?

自从谷歌推出 Gemini 1.5 Pro&#xff0c;行业内部对于 RAG 的讨论就不绝于耳。 Gemini 1.5 Pro 的性能确实令人瞩目。根据谷歌公布的技术文档&#xff0c;该系统能够稳定处理长达 100 token 的内容&#xff0c;相当于一小时的视频、十一小时的音频、超过三万行的代码或七十万…

【VTKExamples::Utilities】第十七期 ZBuffer

很高兴在雪易的CSDN遇见你 VTK技术爱好者 QQ:870202403 公众号:VTK忠粉 前言 本文分享VTK样例ZBuffer,并解析接口vtkWindowToImageFilter,希望对各位小伙伴有所帮助! 感谢各位小伙伴的点赞+关注,小易会继续努力分享,一起进步! 你的点赞就是我的动力(^U^)ノ…

C语言文件操作:打开关闭,读写

程序文件 源程序文件&#xff08;后缀为.c&#xff09; 目标文件&#xff08;Windows环境后缀为.obj&#xff09; 可执行文件&#xff08;Windows环境后缀为.exe&#xff09; fputc FILE* pf fopen("test.txt","w");if (pf NULL){printf("%s\n"…

深入理解Qt计算器应用的构建过程

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、数字按钮的信号与槽函数连接 二、运算符按钮的信号与槽函数连接 三、特殊按钮的信号与…

红外超声波雷达测距(water)

文章目录 一 RS-232二 RS485三 Modbus四 stm32多路超声波测距4.1 设计方案4.2 代码 参考资料总结 实验要求 一. 采用stm32F103和HC-SR04超声波模块&#xff0c; 使用标准库或HAL库 定时器中断&#xff0c;完成1或2路的超声波障碍物测距功能。 1&#xff09;测试数据包含噪声&am…

【Linux】线程ID

大致草稿—————————— 思维导图 学习目标 一、线程ID的理解 1.1 引出对tid的理解 我们先来创建一个线程复习一下线程的函数&#xff1a; pthread_t tid; // 创建一个线程 pthread_create(&tid, nullptr, threadrun, (void*)"thread-1"); // 打印出…

怎么花草识别?方法有三种!

怎么花草识别&#xff1f;在这个五彩斑斓的世界里&#xff0c;花草是我们生活中不可或缺的一部分。它们点缀着我们的环境&#xff0c;为我们带来无尽的美丽与惊喜。然而&#xff0c;面对众多的花草种类&#xff0c;你是否曾感到困惑和迷茫&#xff0c;不知道如何识别它们&#…