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

【C++学习】类和对象(中)一招带你彻底了解六大默认成员函数

前言:在之前,我们对类和对象的上篇进行了讲解,今天我们我将给大家带来的是类和对象中篇的学习,继续深入探讨【C++】中类和对象的相关知识!!!


目录

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

2. 构造函数

2.1概念介绍

2.2 特性介绍

3. 析构函数

3.1 概念介绍

3.2 特性介绍

4. 拷贝构造函数

4.1 概念介绍

4.2 特征介绍

5. 赋值运算符重载

5.1概念引出

5.2 运算符重载

5.3 赋值运算符重载

6. const成员函数

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


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

首先,我们直接给出类中有哪六类默认1. 类的6个默认成员函数

 此时,我们可以会想到,为什么要有这些默认成员函数?这些默认成员函数会带来什么作用呢?

要弄清楚这个问题,我们先来引入一个“空类”的概念。

👉空类的定义:如果一个类中什么成员都没有,即一个类中没有成员变量,也没有成员函数,简称为空类。定义形式如下:

class Date 
{};

通过如上代码发现,空类中什么都没有放,此时请大家认真思考一下,👉空类中难道真的什么都没有吗

  • 答案其实是否定的,对于任何一个类来说,它们都有六个默认成员函数,即使是空类。经过编译器处理之后,类【Date】便不在为空,它会自动的生成六个默认的成员函数,即使这六个成员函数什么也不做。

因此,这就给我们解答了为什么要引入这六个默认成员函数,具体大家可以这样理解

  • 当我们定义一个类时,在初始化之前就调用了打印函数,这样会导致输出的是一个随机值,为了避免这种情况,所以c++给了六种默认成员函数
  • 而且当我们定义的类为空类时,都会自动生成六个默认成员函数。

具体还可以像如下这样分,大家可以直观的感受各个函数的区别与功能:

 至此,这个六个默认成员函数的作用与由来便给大家将清楚了,接下来我们逐个去认识!!!


2. 构造函数

2.1概念介绍

首先,在正式的给出【构造函数】具体的概念前,我们通过代码的方式来为大家做个前情铺垫,这样大家可以直观的感受,通过之前类和对象(上)的学习,我相信大家都能写出一个如下日期类,如果对其还有疑惑的话,可以参考【类和对象(上)】

class Date
{
public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;
};int main()
{Date d1;d1.Init(2023, 3, 15);d1.Print();Date d2;d2.Init(2023, 2, 14);d2.Print();return 0;
}

对于上述这样一个日期类,当我们构造出来之后一般都会先对其进行“初始化”的操作:

 但是有时候我们可能会忘记进行初始化操作,直接对对象进行操作,这时当我们不初始化就直接用可能就会出现问题:

 当我们进行调试时也可以直观的看到:

 因此,为了解决当我们构造出函数之后,未进行初始化就直接对对象进行操作的情况,【C++】就给出了今天我们将要学习的知识——构造函数。有了构造函数,当我们每创建完一个对象,就不用再去手动的调用【Init】函数,因为在创建对象时编译器会自动去调用构造函数对对象进行初始化。

有了上述的认知之后,在这里我给出构造函数的具体概念:

  • 构造函数 ,是一种特殊的方法。主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与【new】运算符一起使用在创建对象的语句中。特别的一个类可以有多个构造函数 ,可根据其参数个数的不同或参数类型的不同来区分它们 即构造函数的重载。

2.2 特性介绍

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

其特征如下:

1. 函数名与类名相同。

  • 这个意思很简单,当我们定义好一个类后,此时它的构造函数的函数名就确定好了,跟当前类的类名是相同的。

2. 无返回值,不能指定返回类型,即使是void也不行。

3. 对象实例化时编译器自动调用对应的构造函数。虽然在一般情况下,构造函数不被显式调用,而是在创建对象时自动被调用。但是并不是不能被显式调用。

4. 构造函数可以重载。

我们通过代码进行举例说明,此时我们已经创建出了一个【Date】类:

class Date
{
public:// 1.无参构造函数Date(){}// 2.带参构造函数Date(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1;            // 调用无参构造函数//d1.Init(2023, 3, 15);d1.Print();Date d2(2023, 3, 15);// 调用带参的构造函数//d2.Init(2023, 2, 14);d2.Print();Date d3();d3.Print(); return 0;
}

解析:

此时当我们运行【Date.d1】和【Date.d2】时,集合上面说到的我们可以发现运行结果如下,传参就调用有参数的构造函数,不传参就调用不传参的构造函数

 而当我们此时运行【Date.d3】时,我们会发现程序出现了报错的情况:

 👉此时就需要注意一点,如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明 ,声明了【d3】函数,该函数无参,返回一个日期类型的对象

到这大家思考一下,当我们写出这两个代码时,是否可以进行合并为一个代码呢?

class Date
{
public:Date(){_year = 1;_month = 2;_day = 3;}Date(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}private:int _year;int _month;int _day;
};

答案当然是可以的,那么我们要怎么做呢?这就需要用到之前学习的缺省参数的知识:

 Date(int year=1 , int month=2 , int day=3 ){_year = year;_month = month;_day = day;}

此时,当我们运行它时,如果开始我们不传参,就用默认的,如果传了,就用我们传的。

 这跟之前那个比较就显得很“高级”,不仅如此,这个功能相对比上面那种写法还更多,因此这里支持缺省参数,例如:

 因此,这里就给大家说明一个点:一个类从大部分场景来说,当能提供构造函数的情况下尽可能提供全缺省或者至少是半缺省,就会显得十分好用。

紧接着就是一点小细节的问题,大家注意以下这两个函数可以同时存在吗?

我们浅浅的分析一波:

  •  首先这里的两个函数构成我们之前讲过的重载吗?不知道大家是否还知道重载的基本知识:【函数名相同,参数不同】,大家从语法上看着可能觉得“确实像那么回事”,但是真的可以吗?我们直接运行代码:

  •  那么到底为什么呢?大家可以试着想想,当我们传参数的时候既可以调无参,又可以调有参数的函数,那么这样到底应该调用谁呢?编译器就不知道该调那个了,调用时存在歧义,因此就报错了。

5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

class Date
{
public:void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1;d1.Print();return 0;
}

在这时,我们将构造函数删除掉了,当我们去进编译时,我们会发现可以编译通过。为什么呢?因为之前说过默认会生成一个构造函数。

那么此时大家是否会有这样的想法,既然编译器自己就有默认的,那么是不是我们就不需要在去构造了呢?事实真的是这样的吗?当我们的代码运行起来时,大家可以看到下图:

 为什么这是随机值呢?

  • 这个问题,大家可以认为是我们的祖师爷设计的不好的一个地方,可能当时没有“想明白”。C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类 型,如:int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型。对编译器自动生成的构造函数不会对内置类型进行处理,然而对于自定义类型,则是去调用该自定义类型对应的默认构造函数。因此,上面代码类中的成员变量都是整形【int】,是内置类型,所以是随值

接下来我们还是通过下图代码来进行相关的理解:

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;int _month;int _day;// 自定义类型Time _t;
};int main()
{Date d1;return 0;
}

👉解析:

  • 上述代码我们不难发现,既有内置类型,也有自定义类型的,其中【Date】类中我们并没有对其写相应的构造函数,此时当我们在创建一个对象的时候,根据前面讲的内置类型不做处理,这时自然而然就会去调用编译器自动生成的构造函数。而对于这里的自定义类型【Time _t】,因为为自定义类型,因此编译器会自动去调用它对应的默认构造函数。此时当我们故意在【Time 】类的默认构造函数里面增加打印看是否进行了调用。

我们运行程序,结果如下:

此时,我们就会注意到自定义类型不写构造函数就没法初始化,这不是一个妥妥的【bug】吗?

因此,祖师爷呢在后来也发现了这个问题,并在C++11中针对内置类型不初始化的缺陷打了一个补丁,即:内置类型成员变量在 类中声明时可以给默认值。

什么意思呢?意思就是如果你不写构造函数,那么就默认用这个缺省值;如果你写构造函数了就不会用这个缺省值 。注意,这里不是初始化(千万要分辨清楚,这里没开空间哟!!!)

6. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。

注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为 是默认构造函数。


3. 析构函数

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

3.1 概念介绍

这里我们就需要引入有关于虚构函数的知识:

  • 析构函数 与构造函数相反,当对象结束其生命周期 ,如对象所在的函数已调用完毕时,系统自动执行析构函数。 析构函数往往用来做“清理善后” 的工作(例如在建立对象时用new开辟了一片内存空间,delete会自动调用析构函数后释放 内存 )。

3.2 特性介绍

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

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

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

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

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

typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 3){_array = (DataType*)malloc(sizeof(DataType) * capacity);if (NULL == _array){perror("malloc申请空间失败!!!");return;}_capacity = capacity;_size = 0;}void Push(DataType data){// CheckCapacity();_array[_size] = data;_size++;}// 其他方法...~Stack(){if (_array){free(_array);_array = NULL;_capacity = 0;_size = 0;}}
private:DataType* _array;int _capacity;int _size;
};
void TestStack()
{Stack s;s.Push(1);s.Push(2);
}

我们通过以上代码来做分析:

  • 上述我们定义了一个栈类,并且已经写好了构造函数。我的问题是这里的【s】需要我们亲自动手去进行清理工作吗?很显然是不需要的,因为【s】是定义在栈区上的局部变量,一旦整个程序运行结束,就会随着【main】函数的栈帧自动销毁。

5. 关于编译器自动生成的析构函数,是否会完成一些事情呢?

下面的程序我们会看到,编译器生成的默认析构函数,对自定类型成员调用它的析构函数。

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】类的析构函数?

  1. 【main】方法中创建了【Date】对象d,而d中包含4个成员变量,其中_year, _month, _day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;
  2. 而_t是【Time】类对 象,所以在d销毁时,要将其内部包含的【Time】类的_t对象销毁,所以要调用【Time】类的析构函数。
  3. 但是【main】函数中不能直接调用【Time】类的析构函数,实际要释放的是【Date】类对象,所以编译器会调用【Date 】类的析构函数,而【Date】没有显式提供,则编译器会给【Date】类生成一个默认的析构函数,目的是在其内部 调用【Time】类的析构函数,即当【Date】对象销毁时,要保证其内部每个自定义对象都可以正确销毁
  4. 【main】函数中并没有直接调用【Time】类析构函数,而是显式调用编译器为【Date】类生成的默认析构函数

 👉注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数

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


4. 拷贝构造函数

4.1 概念介绍

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

那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?因此这就引出了拷贝构造函数的概念:

  • 复制构造函数是构造函数的一种,也称拷贝构造函数,它只有一个参数,参数类型是本类的引用。
  • 复制构造函数的参数可以是 const 引用,也可以是非 const 引用。 一般使用前者这样既能以常量对象(初始化后值不能改变的对象)作为参数,也能以非常量对象作为参数去初始化其他对象。一个类中写两个复制构造函数,一个的参数是 const 引用,另一个的参数是非 const 引用,也是可以的。

此时,我们在通过代码来进行直观的理解:

class Date
{
public:Date(int year = 2023, int month = 3, int day = 15){_year = year;_month = month;_day = day;}void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1(2008,2,5);Date d2(d1);d2.Print();return 0;
}

解析:

  • 这里给出了初始化 【d2】的参数,即 【d1】。只有编译器自动生成的那个默认复制构造函数的参数才能和【d1】匹配,因此,【d2】就是以 【d1】 为参数,调用默认复制构造函数进行初始化的。初始化的结果是 【d2】 成为【d1】 的复制品,即 【d2】 和 【d1】 每个成员变量的值都相等。

4.2 特征介绍

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

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

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

根据上述的基本知识,在这里我们知道可以通过如下去写:

 Date(Date d){_year = d._year;_month = d._month;_day = d._day;}

但是这样呢就会发生报错的情况,具体情况如下,如果语法强制这里编译不会通过,通过了这里它也会发生无穷递归的情况,就是不能使用传值传参,这里需要使用引用

 要理解上述问题我们需要了解这里为什么不能使用传值传参:

我们先来理解下面这几行代码的意思:

// 传值传参
void Func1(Date d)
{}// 传引用传参
void Func2(Date& d)
{}int main()
{Func1(d1);Func2(d1);return 0;
}

👉解析:

这里的【func1】是传值传参,是一个拷贝,即理解为新开一片空间,把【d1】拷贝给【d】,传引用传参即【d】是【d1】的别名,形参是实参的拷贝。内置类型,编译器可以直接拷贝;自定义类型的拷贝,需要调用拷贝构造。

 解析:

  • 对于【Date】这样的类,编译器可以自己去进行拷贝,而对于栈这样的类来说,编译器则是不能自己擅自去进行拷贝的(如果编译器这样的工作都能干的话,程序员可能真的就要失业了)。对于上述【Date】类里面只有简单的年月日等,可以按照类似于【memcpy】的方式一字节一字节的进行拷贝,俗称为浅拷贝。
  • 而对于栈这样的类来说,假设【stl1】和【stl2】同时以字节去进行拷贝,把【_a】拷贝到一块新空间,这时就会出现问题。不难看出此时两个对象指向同一块空间,当两个对象此时指向同一块时,假设过一会儿就会调用析构函数,当【stl1】先析构,过了一会儿【stl2】又会继续析构,同一块空间析构了两次,这是不允许的,同时也还会引起其他的问题。因此,基于这样的原因自定义类型需要调用拷贝构造,栈上的内容需要进行相应的深拷贝构造(具体的我们后面会讲,这里先给出这样的概念)

到这里我们在解释上述提到的为什么要是无穷递归:

 大家想想,那是一个对象实例化,对象实例化就需要用到构造函数,对应的构造函数又是拷贝构造,调拷贝构造之前需要先传参,传值传参又是一个拷贝构造,拷贝构造又需要传参,这样的不断循环,最终就是无穷递归,因此编译器构造的不能是传值传参!!!

因此这里可以怎么做呢?答案是在这里我们可以使用引用的基本方法去解决这个问题,具体如下:

class Date
{
public:Date(int year = 2023, int month = 3, int day = 16){_year = year;_month = month;_day = day;}Date(const Date& d)   {_year = d._year;_month = d._month;_day = d._day;}void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}
private:int _year;int _month;int _day;
};int main()
{Date d1;Date d2(d1);d1.Print();d2.Print();return 0;
}

加引用之后要怎么理解呢?

  • 我们可以这样进行理解。这里的【d】加了引用,因此我们可以看做【d】是【d1】的别名,【d】就是【d1】,指针【this】就是【d2】(我们这里并没有有把【*this】写出来),此时【d1】就传给了【d2】

 同时还有一种写法也是拷贝构造,编译器也可以允许像如下这样去写:

int main()
{Date d1;Date d2(d1);Date d3 = d1;d1.Print();d2.Print();d3.Print();return 0;
}

程序运行结果如下:

 👉注意:

这里有个小细节的地方问问大家,上面代码中,可以发现我们加入了【const】,大家知不知道为什么要加上这个【const】的?

对于为什么要加入【const】,我们还是以代码为例进行直观的了解,当我们不小心写反的时候,如果不加其中的【const】会出现什么情况呢?具体如下:

    Date( Date & d){d._year = _year;d._month= _month;d._day= _day;}

当我们去编译这个程序时,却不会出现报错的情况。但是当我们一运行这个程序,结果就会出现报错的情况。

 此时我们浅浅的分析一波:

  • 我们可以发现代码【d2】本来是拷贝【d1】,结果【d2】非但没能拷贝【d1】,还把【d1】改为了随机值。原因就是因为写反,【d】是【d1】的别名,【*this】的【d2】,本来是【d1】赋值给【d2】,但是现在你变成了【d2】赋值给【d1】的情况,因此就出现上述运行出错。

因此如果要确保实参的值不会改变,又希望避免复制构造函数带来的开销,解决办法就是将形参声明为对象的 const 引用加上【const】,传递过来的不管是不是加了【const】都可以进行接收,但是如果不加【const】就会引起权限放大的问题,编译器是不允许这种情况出现的。出现任何有可能导致 【d】的值被修改的语句,都会引发编译错误。

3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按 字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝这上面已经说过。

  • 注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定 义类型是调用其拷贝构造函数完成拷贝的。

4. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗? 当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?

我们通过下列栈类的进行举例:

typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 10){_array = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _array){perror("malloc申请空间失败");return;}_size = 0;_capacity = capacity;}void Push(const DataType& data){// CheckCapacity();_array[_size] = data;_size++;}~Stack(){if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}
private:DataType* _array;size_t _size;size_t _capacity;
};int main()
{Stack s1;s1.Push(1);s1.Push(2);s1.Push(3);s1.Push(4);Stack s2(s1);return 0;
}

上述代码当我们去运行时, 这里会发现下面的程序会崩溃掉。什么原因呢?这里就需要我们理解深拷贝去解决。

 在上面我们已经浅浅的谈到过这个问题,会发生析构两次的问题,那么到底谁先析构呢?在这里我们仔细分析一下。

 👉分析如下:

  • 首先我们给出答案,这里是【st2】先析构,我们知道【st1】和【st2】都是在栈上的建立的,而之前我们学习数据结构的时候知道,栈的特点是“先进后出”,因此遵循这样的原则,【st1】比【st2】先进入栈区中,这就会导致【st2】先析构,申请的这块空间就被释放了。但是紧接着当【st2】析构完了,【st1】也会进行它的析构,而此时虽然【st1】还保留了这块空间的地址,但是这块空间刚才已经被释放,所以这就会导致【st1】变成野指针,而编译器对野指针进行释放就会导致我们看到的崩溃现象。

注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 时,则拷贝构造函数是一定要写的,否则就是浅拷贝。 

而对于深拷贝我们后面会具体去讲,这里我们先浅浅的谈一下:

对于上述的【Stack】进行浅拷贝就会导致两个栈对象指向了同一块空间了,所以才会出现崩溃的情况,那么深拷贝是怎么做的呢?其实深拷贝解决这个问题的原理就是让这两个对象各自拥有独立的空间这样做对两个对象之间就不会互相影响了。

 用代码浅浅的实现一下:

	Stack(const Stack& st){cout << "Stack(const Stack& st)" << endl;_array = (DataType*)malloc(sizeof(DataType)*st._capacity);if (nullptr == _array){perror("malloc fail");exit(-1);}memcpy(_array, st._array, sizeof(DataType)*st._size);_size = st._size;_capacity = st._capacity;}

此时,我们会看到两个地址空间不同,此时问题就解决了:

5. 拷贝构造函数典型调用场景:

  • 使用已存在对象创建新对象
  • 函数参数类型为类类型对象
  • 函数返回值类型为类类型对象

接下来我们一一进行分析,复制构造函数在以下三种情况下会被调用:
 

1) 当用一个对象去初始化同类的另一个对象时,会引发复制构造函数被调用。例如,下面的两条语句都会引发复制构造函数的调用,用以初始化 【d2】。

Date d2(d1);Date d2 = d1;

👉注意,上面说过这两条语句是等价的。第二条语句是初始化语句,不是赋值语句。赋值语句的等号左边是一个早已有定义的变量,赋值语句不会引发复制构造函数的调用。例如:

Date d1,d2; d1 = d2;
d1 = d2;

这条语句不会引发复制构造函数的调用,因为 【d1】早已生成,已经初始化过了。

2) 如果函数 【d1】的参数是类 【Date】的对象,那么当 【d1】被调用时,类 【Date】的复制构造函数将被调用。换句话说,作为形参的对象,是用复制构造函数初始化的,而且调用复制构造函数时的参数,就是调用函数时所给的实参。


class Date
{
public:Date(){};Date(const Date& d1){cout << "Time()" << endl;}
};void Func(Date d1) 
{ }int main() 
{Date d1;Func(d1);return 0;
}

输出结果为:

 这是因为 Func 函数的形参 【d1】在初始化时调用了拷贝构造函数。

前面说过,函数的形参的值等于函数调用时对应的实参,现在可以知道这不一定是正确的。如果形参是一个对象,那么形参的值是否等于实参,取决于该对象所属的类的复制构造函数是如何实现的。例如上面的例子,Func 函数的形参 【d1】 的值在进入函数时是随机的,未必等于实参,因为复制构造函数没有做复制的工作。

3) 如果函数的返冋值是类 【Date】的对象,则函数返冋时,类 【Date】的复制构造函数被调用。换言之,作为函数返回值的对象是用复制构造函数初始化 的,而调用复制构造函数时的实参,就是 return 语句所返回的对象。例如下面的程序:


class Date
{
public:Date(int year = 2023, int month = 3, int day = 16){_year = year;_month = month;_day = day;}Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;cout << "Date(const Date& d):" << this << endl;}void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}private:int _year;int _month;int _day;
};Date Test(Date d)
{Date temp(d);return temp;
}int main()
{Date d1(2008, 2, 15);Test(d1);d1.Print();return 0;
}

程序的输出结果是:

调用了 Test函数,其返回值是一个对象,该对象就是用复制构造函数初始化的, 而且调用复制构造函数时,实参就是return 语句所返回的 【temp】。复制构造函数在之前确实完成了复制的工作,所以函数的返回值为赋值的。

所以,为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用 尽量使用引用。


5. 赋值运算符重载

5.1概念引出

在讲解这个知识点之前我们先了解一个以及回顾一下之前的:

初始化和赋值的区别

  • 在定义的同时进行赋值叫做初始化(Initialization),定义完成以后再赋值(不管在定义的时候有没有赋值)就叫做赋值(Assignment)。
  • 初始化只能有一次,赋值可以有多次。
     

当以拷贝的方式初始化一个对象时,会调用拷贝构造函数;当给一个对象赋值时,会调用重载过的赋值运算符。
即使我们没有显式的重载赋值运算符,编译器也会以默认地方式重载它。默认重载的赋值运算符功能很简单,就是将原有对象的所有成员变量一一赋值给新对象,这和默认拷贝构造函数的功能类似。


对于简单的类,默认的赋值运算符一般就够用了,我们也没有必要再显式地重载它。但是当类持有其它资源时,例如动态分配的内存、打开的文件、指向其他数据的指针等,默认的赋值运算符就不能处理了,我们必须显式地重载它,这样才能将原有对象的所有数据都赋值给新对象。

基于上述情况,我们这里就引出了关于赋值运算符重载的概念。

5.2 运算符重载

在学习正式学习赋值运算符重载,我们先来学习运算符重载的基本知识,有了对这个理解,当我们讲解赋值运算符重载大家才会轻松上手,因为赋值运算符重载是属于运算符重载的。

为什么引入运算符重载:

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

还是以我们写过的日期类来举例子,在平常生活中我们是不是经常比较两个日期啊,想着多少天是几号,多少多少号跟今天相差几天这种情况。那么是否支持进行比较判别呢?答案当然是支持:,此时我们用日期类来实例化出两个对象,具体如下:

int main()
{Date d1(2008, 2, 15);Date d2(2023, 3, 16);return 0;
}

当我们实例化出两个对象d1,d2后,此时大家就会思考一个问题,现在我们想比较这两个对象是否相等,该怎么办呢?根据我们之前学过的知识,当然是用函数来封装它呀!写了功能函数就可以了。

bool Equal(const Date& x1, const Date& x2)
{//......
}

这时大家会有这样的想法,这样的方法可行肯定是可行的,但是有没有更加直观的呢?就像我们下面这样去写:

 d1 == d2;

为此,当C++引入了运算符重载之后,再去判断就直接像上面的代码这样去操作。但是我们要知道一点那就是自定义类型是不能直接作为这些操作符的操作数的,它不想内置类型一样可以直接进行操作。具体原因如下:

  1. 所谓的自定义类型,即为我们按照我们自己的想法或者为实现某个功能自己编写的程序,对于这样的程序,编译器是不知道该怎么做的。
  2. 其次就是我们自己编写的自定义类型其实并不是所有的运算都是有意义的,这个是由我们自己决定的,因此基于以上两点不难得出结论。

为了解决这个问题,就引入了运算符重载的概念!!!使得可以像【d1=d2】这样去进行操作。

  • 函数名字为:关键字operator后面接需要重载的运算符符号。
  • 函数原型:返回值类型 operator操作符(参数列表)

 对比的逻辑思路也很简单,只需比较各个成员变量是否相等。即如下代码:

bool operator==(const Date& d1, const Date& d2)
{return d1._year == d2._year&& d1._month == d2._month&& d1._day == d2._day;
}

但是此时当我们编译时,会出现报错的情况:

 咦....什么原因呢这是?我们浅浅的分析一波:

  • 👉我们不难发现这是定义在类外面的,然而类中的成员变量却是私有的,基于这个原因当我们去编译时就出出现报错的情况。

那么如何解决呢?在这里我给出几种解决方法:

1.第一种就是我们刚才已经把问题 分析出来了,我们就会想到既然你是私有的我无法访问,那我把你直接变为公有的不就可以了吗?因此第一种方法就是先全部都变为公有的,即屏蔽我们的【private】

class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;} //private:int _year;int _month;int _day;
};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(2008, 2, 15);Date d2(2023, 3, 16);operator ==(d1, d2);d1 == d2;          // 转换成去调用这个operator==(d1, d2);return 0;
}

此时在当我们去运行代码的时候,就可以正常的运行了

 当程序能够正常编译后,我们就会想着去运行,既然这个是函数,那我们可以打印一下这个结果,会有一个返回值,但是当我们运行时又会出现报错:

 cout << operator==(d1, d2)<< endl;cout << d1 == d2 << endl;

 这又是什么原因呢?答案很简单,是因为【<<的优先级比【==】高,因此为了限制这种情况我们需要加个括号:

 cout << operator==(d1, d2) << endl;cout << (d1 == d2) << endl; 

此时,当我们再次运行时,结果显示就为正确:

 从上可以看出这种办法可以解决这个问题,但是大家是否能够发现这样做存在的问题呢?上述方式,我们把全部都变为了公有,那么问题来了,封装性如何保证?

2.因此,基于以上方法存在的问题,我们给出了第二种方法。我们可以把这个函数重载到类里面,干脆重载成成员函数。

但是当我们放到类里面去后,我们再次运行代码,咦....怎么出错了呢?你不是说放到类里面去可以吗?别急,我们先看报错报的是什么?

👉它说我们的参数太多了,什么意思呢?

  • 我们这里重载的是【==】运算符,正常情况下只有两个操作数,所以只需要两个参数就够了。但是大家是否还记得我们默认的还有一个【*this】这个隐藏的参数呀!!!因此这里只需给一个参数就可以了。

bool operator==(const Date& d2){return _year == d2._year&& _month == d2._month&& _day == d2._day;}

此时,我们就需要这样去打印:

cout << d1.operator==(d2) << endl;cout << (d1 == d2) << endl;

这样我们再去运行程序,此时程序就正常运行的了。

👉注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐 藏的this
  • 【.*】 【 ::】 【 sizeof 】【 ?: 】【 .】 注意以上5个运算符不能重载。这个经常在笔试选择题中出 现。(【.*】只需记住即可

接下来,我们在多写几个来进行练习。

1.第一个先写一个日期类的【<】

bool 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;}else{return false;}
}

或者直接这样写:

return _year < d._year|| (_year == d._year && _month < d._month)|| (_year == d._year && _month == d._month && _day < d._day);

以上这两种都是可以的,那么到底是不是呢?我们直接运行程序,可以看到结果是正确的:

 如果我们还想要实现其他的话,是不是看着麻烦呀!这门一大堆的东西。其实根本没必要在像以上这样去写了,上面我们已经写好了【==】和【<】,在写其他的直接复用这个就可以了。

2.例如写个【!=】,我们可以这样去写:

bool operator!=(const Date& d)
{return !(*this == d);
}

3.对于【<=】

// d1 <= d2
bool operator<=(const Date& d)
{return *this < d || *this == d;
}

4.对于【>】

 // d1 > d2
bool operator>(const Date& d)
{return !(*this <= d);
}

5.对于【>=】

// d1 >= d2
bool operator>=(const Date& d)
{return !(*this < d);
}

当我们像这样做是不是就会很大程度上的减少我们的工作量呀!!!

5.3 赋值运算符重载

接下来我们正式的介绍关于赋值运算符重载的知识。

1. 赋值运算符重载格式

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

首先我们先来看看【=】即赋值怎么操作的把。通过上述的知识学习,我们不难写出这样的一个代码

//d1 = d2;
void operator=(const Date& d)
{_year = d._year;_month = d._month;_day = d._day;
}

当我们写出这样的代码时,接下来我们就需要去验证这个代码的正确性,紧接着我们直接运行程序,运行结果如下:

从上可以得出代码的运行结果是正确的,我们在调试去看看是否真的这样。

 

 上述我们也可以发现程序有去调对应的函数,同时记住一点,在转化的时候并不是让编译器把它给改了,而是编译的时候编译器识别,它看你有没有实现赋值,有实现赋值就转化为去【call】这个函数

 然后根绝我们之前的学习经历,赋值往往会有连续赋值这一说法,就像【i=j=k】这样,不断的去连续赋值,然而上述代码当我们去进行这样的操作的时候,我们会发现是编译不通过的。

 遇到困难不要害怕我们浅浅的分析一波:

 因此最终这里却是【d2 = d3】调用了重载函数,而我们上面实现的函数并没有返回值。因此这里就会出现报错的情况,那么怎么解决呢?很简单,我们只需要在这里添加个返回值即可,具体如下:

// 返回值为了支持连续赋值,保持运算符的特性
Date& operator=(const Date& d)
{if (this != &d){_year = d._year;_month = d._month;_day = d._day;}return *this;
}

最终的运行结果如下:

2. 赋值运算符只能重载成类的成员函数不能重载成全局函数,这个我们之前已经说过了,不再具体讲解。

3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。

  • 注 意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符 重载完成赋值。

既然如此,我们是不是可以理解为上述的那个日期类的赋值就不需要我们自己去写相应的程序,编译器自动生成的是不是就可以帮我们完成任务。那当我们屏蔽时,会不会正常运行呢?结果如下:

 可以是可以,但是这里的问题是不是就跟上面讲到的拷贝构造一样了,一样的,这里就不讲了。

接下来,我们在理解以下代码是什么意思:

	Date d5 = d1;// 拷贝构造Date d6(d1);// 拷贝构造

  • 如果我没有给出答案,大家会怎样理解上述代码呢?你认为它是拷贝构造还是赋值重载呀!!那就有人会问,这里不是有个【=】赋值符号,然而它却是拷贝构造呢?道理其实很简答,因为赋值重载是已经定义出来的对象。已经实例化好了,然而这里的【d5】没有实例化出来,只是用一个已经存在的实例化对象去初始化另外一个对象而已。

6. const成员函数

在类中,如果你不希望某些数据被修改,可以使用const关键字加以限定。const 可以用来修饰成员变量和成员函数。

const成员变量

  • const 成员变量的用法和普通 const 变量的用法相似,只需要在声明时加上 const 关键字。初始化 const 成员变量只有一种方法,就是通过构造函数的初始化列表,这点在前面已经讲到了。

const成员函数(常成员函数)

const 】成员函数可以使用类中的所有成员变量,但是不能修改它们的值,这种措施主要还是为了保护数据而设置的。【const 】成员函数也称为常成员函数。

还是通过代码来进行直观的举例说明,例如当我们运行下列代码时,程序时可以正常运行的,这个大家学到这了应该不陌生了:


class Date
{
public:Date(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << "Print()" << endl;cout << _year << "年" << _month << "月" << _day << "日" << endl;}private:int _year; // 年int _month; // 月int _day; // 日
};int main()
{Date d1(2023, 3, 17);d1.Print();return 0;
}

 然而当我们这样写的时候呢?即加入【const】时:

const Date d2(2008, 1, 13);d2.Print();

此时当我们再去编译的时候,我们发现程序就会出现报错的情况:

 还是浅浅的分析一下:

  • 其实是因为这里存在了一个权限放大的问题,即从可读 可读可写,【d2】是被【const】修饰的,说明对象本身不可被修改相当于【const Date*】的指针形式,但类成员函数中的【this】指针又是【Date* const this】,【const】修饰的是该地址指向的内容,即对象【d2】不能被修改。因此,当传给【this】,【this】可以修改其指向的内容即对象【d2】,因此权限放大了,所以发生了报错。 权限放大跟之前讲到的一个情况类似的

问题分析,如何解决呢?可以看到如果不想让权限放大,我们必须在【*】的前面加上【const 】由于【this】是隐形的,所以编译器规定在函数括号后面加【const】来表示此对象不可被修改。即如下表示方法:

void Print()const{cout << "Print()" << endl;cout << _year << "年" << _month << "月" << _day << "日" << endl;}

在当我们去运行代码时,就不会出现报错的情况了。

这里回答几个小问题:

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

  • 不允许,【const】成员函数调用非【const】成员函数,调用该【const】成员函数的对象已经被设置为【const】类型,只可以访问但是不能进行修改,在用该const成员函数访问其他非【const】成员函数可能会修改,因此【const】成员函数不能调用非const成员函数。

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

  • 可以,当一个类只有const成员函数的时候,非const对象也可以调用const成员函数

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

  • 不可以,若你把一个函数声明为const类型函数,那么就说明这个函数是只读的,不可修改,而非const成员函数是可读可写的。

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

  • 可以,外层函数类型【Date* const】:是可读可写的;
  • 而内层函数类型是【const Date* const】:只读外层可以修改也可以不修改,到底是否要修改,视情况而定。

最后再来区分一下 const 的位置:

  • 函数开头的 const 用来修饰函数的返回值,表示返回值是 const 类型,也就是不能被修改,
  • 函数头部的结尾加上 const 表示常成员函数,这种函数只能读取成员变量的值,而不能修改成员变量的值。

总结:

   到底要不要使用【const】去修饰成员函数,就看你函数中的变量需不需被修改,如果不希望被修改,则加上即可。


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

取地址成员函数也是''类的六大默认成员函数''之一。其分为两种,普通取地址操作符、【const】取地址操作符。

这两个默认成员函数一般不用重新定义 ,编译器默认会生成,用编译器默认生成的取地址的重载即可

class Date
{
public:Date* operator&(){return this;}const Date* operator&()const{return this;}private:int _year; int _month; int _day;
};int main()
{Date d1;cout << &d1 << endl;return 0;
}

小结:

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


总结:

  • 本期主要介绍的是【C++】中默认六大成员函数,大家对前四个一定要认真的学习,后两个只需知道,懂即可。

最后,如果本文对你有帮助的话,记得点赞三连哟!!!

在这里插入图片描述

相关文章:

【C++学习】类和对象(中)一招带你彻底了解六大默认成员函数

前言&#xff1a;在之前&#xff0c;我们对类和对象的上篇进行了讲解&#xff0c;今天我们我将给大家带来的是类和对象中篇的学习&#xff0c;继续深入探讨【C】中类和对象的相关知识&#xff01;&#xff01;&#xff01; 目录 1. 类的6个默认成员函数 2. 构造函数 2.1概念介…...

面试——Java基础

说一说你对Java访问权限的了解 在修饰成员变量/成员方法时&#xff0c;该成员的四种访问权限的含义如下&#xff1a; private&#xff1a;该成员可以被该类内部成员访问&#xff1b; default&#xff1a;该成员可以被该类内部成员访问&#xff0c;也可以被同一包下其他的类访…...

JavaWeb——Request(请求)和Response(响应)介绍

在写servlet时需要实现5个方法&#xff0c;在一个service方法里面有两个参数request和response。 浏览器向服务器发送请求会发送HTTP的请求数据——字符串&#xff0c;这些字符串会被Tomcat所解析&#xff0c;然后这些请求数据会被放到一个对象(request)里面保存。 相应的Tom…...

JMeter压测文件上传接口和中文乱码

一、压测文件上传接口 新建测试计划&#xff0c;然后添加需要的元件。 1、添加HTTP信息头管理器 可以在测试计划中添加&#xff0c;也可以在线程组里面添加。 我的接口使用到 token信息。这里在测试计划中添加。 2、添加线程组 上图解释&#xff1a;会在 2秒钟之内启动起来 5…...

CSRF漏洞复现

目录标题原理如何实现和xss区别危害CSRF实战&#xff08;pikachu&#xff09;dvwa靶场CSRF&#xff08;Cross Site Request Forgery&#xff09;。跨站请求伪造原理 攻击者会伪造一个请求&#xff08;一般是一个链接&#xff09;&#xff0c;然后让用户去点击&#xff0c;然后…...

Google Colab导入GitHub python项目进行运行

本文介绍包含 ipynb后缀文件的github项目&#xff0c;导入到GitHub上进行运行的方法。 导入项目 Colab是需要梯子的。 访问网址&#xff1a;https://colab.research.google.com 输入github网之后回车&#xff0c;下面的内容是从github上自动获取的。 选择项目要打开的ipynb文…...

Qss样式表语法

QSS样式表语法 更多精彩内容&#x1f449;个人内容分类汇总 &#x1f448;&#x1f449;QSS样式学习 &#x1f448;文章目录QSS样式表语法[toc]概述一、样式规则二、选择器类型三、子控件四、伪状态五、样式表冲突解决六、级联七、继承八、命名空间中的控件概述 Qt样式表的概念…...

「Python 基础」异步 I/O 编程

I/O 密集型应用程序大大提升系统多任务处理能力&#xff1b; 异步 I/O 模型 一个消息循环&#xff0c;主线程在消息循环中不断重复 读取消息-处理消息&#xff1b; # 获取线程池 loop get_event_loop() while True:# 接收事件消息event loop.get_event()# 处理事件消息pro…...

通配符的匹配很全面, 但无法找到元素 ‘tx:advice‘ 的声明

&#x1f497;wei_shuo的个人主页 &#x1f4ab;wei_shuo的学习社区 &#x1f310;Hello World &#xff01; 通配符的匹配很全面, 但无法找到元素 ‘tx:advice’ 的声明 错误原因&#xff1a; xmlns和xsi:schemaLocation未书写约束或者书写错误 正确书写 <beans xmlns:tx&q…...

响应式编程详解,带你熟悉Reactor响应式编程

文章目录一、什么是响应式编程1、Java的流和响应式流2、Java中响应式的使用3、Reactor中响应式流的基本接口4、Reactor中响应式接口的基本使用二、初始Reactor1、Flux和Mono的基本介绍2、引入Reactor依赖3、响应式类型的创建4、响应式类型的组合&#xff08;1&#xff09;使用m…...

踩坑篇之WebSocket实现类中无法使用@Autowired注入对象

大家好&#xff0c;我是小简&#xff0c;今天我又大意了&#xff0c;在WebSocket这个类上踩坑了。 接下来我讲讲我踩坑的经历吧&#xff01; package cn.donglifeng.shop.socket.endpoin;import cn.donglifeng.shop.common.context.SpringBeanContext; import cn.donglifeng.s…...

QT CTK插件框架 (一 下载编译)

CTK 为支持生物医学图像计算的公共开发包&#xff0c;其全称为 Common Toolkit。为医学成像提供一组统一的基本功能&#xff1b;促进代码和数据的交互及结合&#xff1b;避免重复开发&#xff1b;在工具包&#xff08;医学成像&#xff09;范围内不断扩展到新任务&#xff0c;而…...

【Java版oj】day10 井字棋、密码强度等级

目录 一、井字棋 &#xff08;1&#xff09;原题再现 &#xff08;2&#xff09;问题分析 &#xff08;3&#xff09;完整代码 二、密码强度等级 &#xff08;1&#xff09;原题再现 &#xff08;2&#xff09;问题分析 &#xff08;3&#xff09;完整代码 一、井字棋 &a…...

JavaScript的事件传播机制

你在学习和编写JavaScript时可能听说过事件冒泡&#xff08;event bubbling&#xff09;。它会发生在多个元素存在嵌套关系&#xff0c;并且这些元素都注册了同一事件(例如click)的监听器时。 但是事件冒泡只是事件机制的一部分。它经常与事件捕获(event capturing)和事件传播…...

队列的定义及基本操作实现(链式)

个人主页&#xff1a;【&#x1f60a;个人主页】 系列专栏&#xff1a;【❤️数据结构与算法】 学习名言&#xff1a;天子重英豪&#xff0c;文章教儿曹。万般皆下品&#xff0c;惟有读书高 系列文章目录 第一章 ❤️ 学前知识 第二章 ❤️ 单向链表 第三章 ❤️ 递归 文章目录…...

集成方法!

目录 关注降低variance,选择bias较小的基学习器 Bagging Stacking Random Forest 关注降低bias,选择variance较小的基学习器 Adaboost Boosting 关注降低variance,选择bias较小的基学习器 Bagging 给定m个样本的数据集&#xff0c;利用有放回的随机采样法&#xff0c;得…...

20年程序员生涯,读了200多本技术书,挑了几本精华好书分享给大家

不知不觉已经又走过了20个年头了&#xff0c;今年已经44了&#xff0c;虽然我已经退休在家&#xff0c;但一直都保持着读书的习惯&#xff0c;我每年平均要读10本技术书籍&#xff0c;保持不让自己的技术落伍。 这些年读的技术书不下200本&#xff0c;很多好书我都会保存在家&a…...

C++ 手写一个WebServer

文章目录 前言一、WebServer的原理刨析二、HTTP协议基础三、C++代码实战四、运行测试前言 本文由:我不会画饼呀 提供建议 大家如果有什么想看的文章(想了解的知识点),都可以在本专栏文章底部评论,或者私信我,在有能力的前提下,我都会尽量给大家写出来,供大家学习参考 …...

Elasticsearch 简介与安装

简介 Elasticsearch 是一个开源的搜索引擎&#xff0c;建立在一个全文搜索引擎库 Apache Lucene™ 基础之上。 Lucene 可以说是当下最先进、高性能、全功能的搜索引擎库—​无论是开源还是私有。 但是 Lucene 仅仅只是一个库。为了充分发挥其功能&#xff0c;你需要使用 Java…...

Qt5.12实战之QByteArray与字符指针及字符串转换

示例源码:#include <QCoreApplication> #include <QDebug> #include <QTextStream> static QTextStream cout (stdout,QIODevice::WriteOnly); #include <iostream> #include <QtGlobal> #include <QByteArray>void test() {qDebug() <…...

二、ElasticSearch基础语法

目录一、简单了解ik分词器(分词效果)1.standard(单字分词器&#xff0c;es默认分词器)2.ik_smart分词(粗粒度的拆分)3.ik_max_word分词器&#xff08;最细粒度拆分&#xff09;二、指定默认分词器1.为索引指定默认分词器三、ES操作数据1.概述2.创建索引3.查询索引4.删除索引5.添…...

Yolov8详解与实战

文章目录摘要模型详解C2F模块Losshead部分模型实战训练COCO数据集下载数据集COCO转yolo格式数据集&#xff08;适用V4&#xff0c;V5&#xff0c;V6&#xff0c;V7&#xff0c;V8&#xff09;配置yolov8环境训练测试训练自定义数据集Labelme数据集摘要 YOLOv8 是 ultralytics …...

多线程案例——阻塞队列

目录 一、阻塞队列 1. 生产者消费者模型 &#xff08;1&#xff09;解耦合 &#xff08;2&#xff09;“削峰填谷” 2. 标准库中的阻塞队列 3. 自己实现一个阻塞队列&#xff08;代码&#xff09; 4. 自己实现生产者消费者模型&#xff08;代码&#xff09; 一、阻塞队列…...

学习优秀博文(【国产MCU移植】手把手教你使用RT-Thread制作GD32系列BSP)有感 | 文末赠书5本

学习优秀博文&#xff08;【guo产MCU移植】手把手教你使用RT-Thread制作GD32系列BSP&#xff09;有感 一篇优秀的博文是什么样的&#xff1f;它有什么规律可循吗&#xff1f;优秀的guo产32位单片机处理器是否真的能成功替换掉stm32的垄断地位&#xff1f; 本文博主以亲身经历聊…...

写用例写的焦头烂额?看看摸鱼5年的老点工是怎么写的...

给你个需求&#xff0c;你要怎么转变成最终的用例&#xff1f; 直接把需求文档翻译一下就完事了。 老点工拿到需求后的标准操作&#xff1a; 第一步&#xff1a;解析需求 先解析需求-找出所有需求中的动词&#xff0c;再列出所有测试点。测试点过程不断发散&#xff0c;对于…...

基于深度学习的鸟类检测识别系统(含UI界面,Python代码)

摘要&#xff1a;鸟类识别是深度学习和机器视觉领域的一个热门应用&#xff0c;本文详细介绍基于YOLOv5的鸟类检测识别系统&#xff0c;在介绍算法原理的同时&#xff0c;给出Python的实现代码以及PyQt的UI界面。在界面中可以选择各种鸟类图片、视频以及开启摄像头进行检测识别…...

零基础搭建Tomcat集群(超详细)

&#x1f497;推荐阅读文章&#x1f497; &#x1f338;JavaSE系列&#x1f338;&#x1f449;1️⃣《JavaSE系列教程》&#x1f33a;MySQL系列&#x1f33a;&#x1f449;2️⃣《MySQL系列教程》&#x1f340;JavaWeb系列&#x1f340;&#x1f449;3️⃣《JavaWeb系列教程》…...

机器学习自学笔记——聚类

聚类的基本概念 聚类&#xff0c;顾名思义&#xff0c;就是将一个数据集中各个样本点聚集成不同的“类”。每个类中的样本点都有某些相似的特征。比如图书馆中&#xff0c;会把成百上千的书分成不同的类别&#xff1a;科普书、漫画书、科幻书等等&#xff0c;方便人们查找。每…...

注意下C语言整形提升

C语言整形提升 C语言整形提升是指在表达式中使用多种类型的数据时&#xff0c;编译器会自动将较小的类型转换为较大的类型&#xff0c;以便进行运算。在C语言中&#xff0c;整型提升规则如下&#xff1a; 如果表达式中存在short类型&#xff0c;则将其自动转换为int类型。 如…...

Go panic的学习

一、前言 我们的应用程序常常会出现异常&#xff0c;包括由运行时检测到的异常或者应用开发者自己抛出的异常。 异常在一些其他语言中&#xff0c;如c、java&#xff0c;被叫做Exception&#xff0c;主要由抛出异常和捕获异常两部分组成。异常在go语言中&#xff0c;叫做pani…...

内蒙古网站建设流程/关键词有哪些关联词

Paul Jaquays是一个多面手 Paul Jaquays是一个多面手&#xff0c;他在id Software里为QuakeⅡ设计关卡&#xff0c;并为QuakeⅢ做游戏和关卡设计。现在&#xff0c;还有谁不愿用他的东西呢&#xff1f;当Jaquays了解到Saqes的秘决后&#xff0c;他想出版一个关卡设计规则方面的…...

哪个网站是做包装材料珍珠棉包管/免费直链平台

问题描述&#xff1a;本人的项目是用Maven管理&#xff0c;而且用到了servlet3.0的技术&#xff0c;但是项目中用到servlet3.0的地方&#xff0c;总提示找不到类中的方法。很奇怪&#xff0c;在网上找到好多解决办法&#xff0c;综合一下终于解决了。现将经验分享给大家。 前提…...

网站开发的最后五个阶段/什么是优化师

Git中删除文件可不太一样哦&#xff0c;我们一起来学习一下在Git中如果你想要删除文件&#xff0c;比平时我们删除文件要麻烦那么一点点&#xff0c;毕竟Git有两个地方&#xff1a;工作区、版本库&#xff0c;平时我们删除文件的方式&#xff0c;只是把工作区的文件给删除了&am…...

广州做网站哪里好/重庆seo网络推广优化

内容&#xff1a;软件项目与过程管理课程内容总结 经过八周时间的学习&#xff0c;软件项目与过程管理课程已经逐渐接近了尾声。通过这八周的学习&#xff0c;我对软件项目与过程管理课程有了更深的理解。 一、关于团队项目。 团队项目是本次软件项目与过程管理课程中最重要的一…...

vs做网站怎么添加子页/网络推广网站程序

本章要讲的是PHP的全局变量。 这里讲个小故事: 很多年前&#xff0c;一个很聪明的小偷&#xff0c;想去偷一户人家的钱。可是他偷不到主人的钥匙&#xff0c;怎么办呢&#xff1f; 他想到了一个办法&#xff0c;去之前嚼了一块口香糖&#xff0c;口香糖的牌子是“大大泡泡糖”。…...

贵州 跨境电商网站建设/最近新闻头条最新消息

1、在项目src文件夹下&#xff0c;创建一个pages文件夹&#xff0c;用来新建多页面的各个入口文件 &#xff08;1&#xff09;配置index页面&#xff0c;文件写在src/pages/index文件夹下&#xff0c;在浏览器中访问地址http://localhost:8090/index.html#/&#xff1a; src/…...