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

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 日期类的实现
  • 7 const成员
  • 8 取地址及const取地址操作符重载


1 类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类(如:class Date{};)。那么空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。 默认成员函数:用户没有显示实现,编译器会生成的成员函数称为默认成员函数。

默认成员函数


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(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1;d1.Init(2022, 7, 5);d1.Print();Date d2;d2.Init(2022, 7, 6);d2.Print();return 0;
}

我们可以通过Init公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,而且有时还可能因为忘记使用该方法进行对象信息初始化设置而直接使用对象造成未预期的后果(如:我们实现一个栈,如果忘记初始化而直接使用,就可能造成越界访问等问题)。

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

2.2 特性

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

其特征包括:

  1. 函数名与类名相同。
  2. 无返回值(这里无返回值并不是指返回类型为void,事实上,构造函数不需要加返回类型,直接类名即可)。
  3. 对象实例化时编译器 自动调用 对应的构造函数。
  4. 构造函数可以重载。 表示一个类中可以有多个构造函数,或者说可以有多种初始化方式。

    示例代码:
class Date
{
public:// 1.无参构造函数Date(){}// 2.带参构造函数Date(int year, int month, int day){_year = year;_month = month;_day = day;}private:int _year;int _month;int _day;
};void TestDate()
{Date d1; // 调用无参构造函数Date d2(2023, 1, 1); // 调用带参的构造函数//Date d3();//不是创建对象,而是函数声明//d4.Date();//?
}int main() {TestDate();return 0;
}

注意:

  • 如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明,如对于代码 Date d3(); ,其表示的是声明了 d3 函数,该函数无参,返回一个日期类型的对象,如果运行该代码,则会报出警告如下:warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)

报错

  • 不能用对象去调用构造函数,如 d4.Date() ,且不说在调用之前 d4 对象是否已经创建了出来(没有创建的话就无法调用成员函数),就算 d4 是一个已经创建好的对象,如果再用对象去调用构造函数是否是多此一举了,也违背了对于优化C语言中需要自己调用初始化函数问题的初衷。

  1. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义了构造函数后,编译器将不再生成默认构造函数。
class Date
{
public:/*// 如果用户显式定义了构造函数,编译器将不再生成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类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数// 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成// 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用Date d1;return 0;
}

  1. 关于编译器生成的默认成员函数,或许有人会疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象的成员变量_year/_month/_day依旧是随机值。也就是说这里编译器生成的默认构造函数并没有什么用?那究竟这个编译器生成的默认构造函数中实现了什么?

    解释: C++把类型分成了内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char…;自定义类型就是我们使用class/struct/union等自己定义的类型。我们看下面的程序,运行后可以发现编译器生成的默认构造函数会调用自定义类型成员 _t 的默认构造函数。
class Time
{
public:Time(){cout << "Time()" << endl;_hour = 0;_minute = 0;_second = 0;}private:int _hour;int _minute;int _second;
};class Date
{
public:void ShowInfo() {cout << _year << "-" << _month << "-" << _day << endl;}private:// 基本类型(内置类型)int _year;int _month;int _day;// 自定义类型Time _t;
};int main()
{Date d;d.ShowInfo();return 0;
}

程序运行结果:

结果


既然编译器默认生成的构造函数都对自定义类型进行处理了,那为什么不对内置类型做处理呢?关于这点,可以说这是C++的一个缺陷,基于此,C++11中针对编译器生成的默认构造器不处理(或者说不初始化)内置类型成员的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。 此时,如果在对象实例化时调用的构造函数(无论是编译器默认生成的构造函数还是自己重载的构造函数)中没有对成员变量进行初始化,则就以默认值进行初始化。

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

程序运行结果:
结果


  1. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数 最多只有一个 。注意:无参构造函数、全缺省构造函数、我们没写而编译器默认生成的构造函数,都可以认为是默认构造函数。

① 类中没有默认构造函数情况:

class Date
{
public://构造函数//由于我们自己定义了构造函数,因此编译器不会生成默认的无参构造函数,也就是说类中没有默认构造函数Date(int year, int month, int day){_year = year;_month = month;_day = day;}private:int _year;int _month;int _day;
};// 以下测试函数能通过编译吗?
void Test()
{Date d1;
}

程序运行结果:

结果


② 类中有多个默认构造函数情况:

class Date
{
public://无参构造函数Date(){_year = 1900;_month = 1;_day = 1;}//全缺省构造函数Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}private:int _year;int _month;int _day;
};// 以下测试函数能通过编译吗?
void Test()
{Date d1;
}

程序运行结果:

结果

说明: 上述程序运行中断,编译器表示对重载函数的调用不明确,即不知道该调用哪一个构造函数(从语法上来说,无参构造函数和全缺省构造函数是构成函数重载的,如果我们没有调用函数,程序是可以编译通过的)。这是因为对于函数调用来说,无参构造函数和全缺省构造函数都不需要传入参数,这就意味着无论我们本意是调用哪个构造函数,由于调用时参数列表都是无参,所以经过修饰后的函数名是相同的,此时就会导致链接器在链接查找函数时不明确到底是要找哪个函数。


3 析构函数

3.1 概念

通过前面对构造函数的学习,可以知道一个对象是怎么来的,如何初始化的,那如果要销毁一个对象呢?就好比我们定义一个栈,进行栈的初始化,然后使用,最后还需要将栈销毁,对应也就是栈的销毁函数。那既然编译器都可以帮我们自动调用构造函数完成对象初始化了,那是不是也可以帮我们在使用完对象后自动进行销毁呢?没错,除了构造函数,C++中还引入了析构函数的概念。

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁(对象本身是存储在栈区的,出了作用域会自动销毁),局部对象销毁工作是由编译器完成的(就好比我们调用栈的销毁函数并不是销毁栈本身,而是释放栈中开辟的空间,同时将栈容量等信息归置)。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。


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申请空间失败!!!");exit(-1);}_capacity = capacity;_size = 0;}void Push(DataType data){// CheckCapacity();_array[_size] = data;_size++;}// 其他方法...~Stack(){cout << "~Stack()" << endl;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);
}int main() {TestStack();return 0;
}

程序运行结果:

结果


  1. 那编译器自动生成的析构函数是否会完成一些事情呢?从下面的程序我们可以看到,与编译器默认生成的构造函数类似,编译器生成的默认析构函数不会处理内置类型成员,而对自定义类型成员会调用它的析构函数。**
class Time
{
public:~Time(){cout << "~Time()" << endl;}private:int _hour;int _minute;int _second;
};class Date
{
private:// 基本类型(内置类型)int _year = 2023;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 概念

在现实生活中,可能存在两个一模一样的人,我们称之为双胞胎。

双胞胎

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

基于此,C++中引入了拷贝构造函数的概念。

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


4.2 特性

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

  1. 拷贝构造函数是构造函数的一个重载形式(函数名与类名相同,无返回值类型)。
  2. 拷贝构造函数的参数只有一个,且必须是类类型对象的引用(规定),使用传值方式编译器直接报错(语法不通过),否则若是语法同意使用传值方式,将会引发无穷递归调用。此外,拷贝构造函数除了可以通过 目标对象(拷贝原型对象) 的方式调用,还可以直接通过 目标对象 = 拷贝原型对象 的方式调用。
class Date
{
public:Date(int year = 2023, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// Date(const Date& d)  //正确写法Date(Date d)  //错误写法:编译报错,会引发无穷递归{_year = d._year;_month = d._month;_day = d._day;}private:int _year;int _month;int _day;
};int main()
{Date d1;Date d2(d1);return 0;
}

程序运行结果:

报错


解释:函数的形参是实参的一份临时拷贝,如果采用传值方式传参,如果参数是内置类型,那这函数声明并没有什么问题,可以正常运行;但如果参数是自定义类型对象,有些细节就需要注意:C++规定,函数采用传值传参时,对于内置类型,编译器可以直接拷贝;而对于自定义类型,需要调用其拷贝构造函数。 此时如果拷贝构造函数本身采用的是传值传参(参数是自定义类类型),那就意味着在真正调用到拷贝构造函数前,还需要先调用实参对象的拷贝构造函数得到形参对象,如此就陷入了无穷递归调用拷贝构造函数的困境。而如果是采用传引用方式传参,那形参对象就是实参对象的一个别名,不需要再调用一次拷贝构造函数。那可不可以用传指针传参呢(如:Date(const Date* pd))?这当然是可以的,传指针传参也不会有无穷递归的问题,但是采用传指针传参的方式,还需要对指针进行一次拷贝,显然还是传引用传参的方式效率更高。事实上如果采用传指针传参的方式,此时函数相当于是重载的构造函数,并不能称为拷贝构造函数。

无穷递归调用

  1. 若未显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数按对象存储字节大小完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
class Time
{
public://全缺省构造函数Time(int hour = 0, int minute = 0, int second = 0){_hour = hour;_minute = minute;_second = second;}//拷贝构造函数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
{
public://全缺省构造函数Date(int year = 2023, int month = 1, int day = 1) {_year = year;_month = month;_day = day;}void ShowInfo() {cout << _year << "-" << _month << "-" << _day << endl;}private:// 基本类型(内置类型)int _year;int _month;int _day;// 自定义类型Time _t;
};int main()
{Date d1(2023, 2, 3);Date d2(d1);cout << "d1:";d1.ShowInfo();cout << "d2:";d2.ShowInfo();return 0;
}

程序运行结果:

结果

说明: 在上述代码中,我们使用已存在的日期类对象 d1 拷贝构造 d2 ,此处会调用 Date 类的拷贝构造函数,但 Date 类中并没有显式定义拷贝构造函数,则编译器会给 Date 类生成一个默认的拷贝构造函数。

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


  1. 既然编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,那还需要自己显式实现吗?当然像日期类这样的类是没必要自己显式实现,那么下面的栈类呢?
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;
}

程序运行结果:

浅拷贝

程序运行崩溃

结果说明: 对于类中涉及资源申请的对象,调用编译器默认生成的拷贝构造函数进行值拷贝将造成一块空间被多个同类类对象使用的情况,这样对象之间对空间数据的修改会相互影响,且在最后会造成一块空间被多次释放的问题。

注意:对象本身作为局部变量也是存储在栈空间中的,其析构的顺序为:先构造的后析构,后构造的先析构

浅拷贝与深拷贝

结论: 类中如果没有涉及资源申请时,拷贝构造函数是否自己显式实现都可以;一旦涉及到资源申请时,拷贝构造函数必须自己显式实现,否则就是浅拷贝。

那如果是深拷贝要如何实现呢?还是以上面的栈类为例,如下自己编写可实现深拷贝的拷贝构造函数:

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;}//拷贝构造函数Stack(const Stack& st) {_array = (int*)malloc(_capacity * sizeof(DataType));if (nullptr == _array){perror("malloc申请空间失败");return;}memcpy(_array, st._array, _capacity * sizeof(DataType));_size = st._size;_capacity = st._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;
}

程序运行结果:

深拷贝


  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 个运算符不能重载。

示例1(全局实现):

// 全局的operator==
class Date
{
public:Date(int year = 2023, int month = 1, int day = 1){_year = year;_month = month;_day = day;}//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);
}void Test()
{Date d1(2022, 9, 26);Date d2(2022, 9, 27);cout << (d1 == d2) << endl; //注意要记得加括号,因为流插入运算符 << 的优先级高于 ==//cout << operator(d1, d2) << endl; //也可采用函数调用的方式
}int main() {Test();return 0;
}//结果输出:0

示例2(作为成员函数实现):

class Date
{
public:Date(int year = 2023, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// bool operator==(Date* this, const Date& d2)// 这里需要注意的是,左操作数是this,指向调用函数的对象bool operator==(const Date& d2){return (_year == d2._year) && (_month == d2._month) && (_day == d2._day);}private:int _year;int _month;int _day;
};void Test()
{Date d1(2022, 9, 26);Date d2(2022, 9, 27);cout << (d1 == d2) << endl; //注意要记得加括号,因为流插入运算符 << 的优先级高于 ==//cout << d1.operator(d2) << endl; //也可采用函数调用的方式
}int main() {Test();return 0;
}
//结果输出:0

5.2 赋值运算符重载

1 赋值运算符重载格式及注意事项

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

    示例:
class Date
{
public:Date(int year = 2023, 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;}//赋值运算符重载Date& operator=(const Date& d){//检测是否给自己赋值,如果是给自己赋值,直接返回即可,避免多余操作if (this != &d){_year = d._year;_month = d._month;_day = d._day;}return *this;}private:int _year;int _month;int _day;
};

2 赋值运算符只能重载成类的成员函数,不能重载成全局函数

class Date
{
public:Date(int year = 2023, 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 =”必须是非静态成员

原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。

赋值运算符重载


3 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。

//时间类
class Time
{
public:Time(){_hour = 1;_minute = 1;_second = 1;}//赋值运算符重载Time& operator=(const Time& t){if (this != &t){_hour = t._hour;_minute = t._minute;_second = t._second;}cout << "Time::operator=()" << endl;return *this;}private:int _hour;int _minute;int _second;
};//日期类
class Date
{
public:Date(int year = 2023, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void ShowInfo() {cout << _year << "-" << _month << "-" << _day << endl;}private:// 基本类型(内置类型)int _year;int _month;int _day;// 自定义类型Time _t;
};int main()
{Date d1(2022, 12, 25);Date d2(2022, 1, 1);cout << "赋值前:" << endl;d1.ShowInfo();d2.ShowInfo();cout << endl;d1 = d2;cout << endl;cout << "赋值后:" << endl;d1.ShowInfo();d2.ShowInfo();return 0;
}

程序运行结果:

结果

既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,那还需要自己实现吗?与编译器生成的默认拷贝构造函数的问题类似,或许像日期类这样的类没有必要自己实现赋值运算符重载,那下面的栈类呢?

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

程序运行结果:程序运行中断

结果

结果说明:

赋值运算符重载浅拷贝问题

总结:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要自己实现。

以下实现栈类的赋值运算符重载(深拷贝):

	//赋值运算符重载Stack& operator=(const Stack& st) {memcpy(_array, st._array, st._size * sizeof(DataType));_size = st._size;_capacity = st._capacity;return *this;}

将上述赋值运算符重载添加到栈类中,重新运行程序,结果如下:

结果


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


注意:由于前置++和后置++都是一元运算符,为了让前置++与后置++能正确重载,C++规定,后置++重载时多增加一个 int 类型的参数,但调用函数时该参数不用传递,由编译器自动传递。


以日期类为例:

class Date
{
public:Date(int year = 2023, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// 前置++:返回+1之后的结果// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率Date& operator++(){_day += 1;return *this;}// 后置++:// 注意:后置++是先使用后+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.operator++(0); //后置++由编译器自动传入一个整型值d = d1++;  // d: 2022,1,13   d1:2022,1,14// d = d1.operator++();d = ++d1;  // d: 2022,1,15   d1:2022,1,15return 0;
}

可以看到:相比于前置的 ++ 与 - - ,后置的 ++ 与 - - 的实现要多一次拷贝构造,因此建议,在不影响最终结果的情况下,尽量使用前置的 ++ 与 - - 。


6 日期类的实现

Date.h

#pragma once
#include <iostream>using namespace std;class Date {
public://获取某年某月的天数int GetMonthDay(int year, int month) {static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };int day = days[month];if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) {day++;}return day;}//全缺省构造函数Date(int year = 2023, int month = 1, int day = 1);//打印信息void Print();拷贝构造函数//Date(const Date& date);赋值运算符重载//Date& operator=(const Date& date);析构函数//~Date();//日期+=天数Date& operator+=(int day);//日期+天数Date operator+(int day);//日期-天数Date operator-(int day);//日期-=天数Date& operator-=(int day);//前置++Date& operator++();//后置++Date operator++(int);//后置--Date& operator--(int);//前置--Date& operator--();//>运算符重载bool operator>(const Date& date);//==运算符重载bool operator==(const Date& date);//>=运算符重载bool operator>=(const Date& date);//<运算符重载bool operator<(const Date& date);//<=运算符重载bool operator<=(const Date& date);//!=运算符重载bool operator!=(const Date& date);//日期-日期 返回天数int operator-(const Date& date);private:int _year;int _month;int _day;
};

Date.cpp

#include "Date.h"//全缺省构造函数
Date::Date(int year, int month, int day) {if (month > 0 && month < 13 && day > 0 && day <= GetMonthDay(year, month)) {_year = year;_month = month;_day = day;}else {cout << "日期非法!" << endl;}
}void Date::Print() {cout << _year << "-" << _month << "-" << _day << endl;
}拷贝构造函数
//Date::Date(const Date& date) {
//	_year = date._year;
//	_month = date._month;
//	_day = date._day;
//}
//
赋值运算符重载
//Date& Date::operator=(const Date& date) {
//	if (this != &date) {
//		_year = date._year;
//		_month = date._month;
//		_day = date._day;
//	}
//	return *this;
//}
//
析构函数
//Date::~Date() {
//	_year = 2023;
//	_month = 1;
//	_day = 1;
//}//日期+=天数
Date& Date::operator+=(int day) {if (day < 0) {return *this -= -day;}_day += day;while (_day > GetMonthDay(_year, _month)) {_day -= GetMonthDay(_year, _month);_month++;if (_month > 12) {_year++;_month = 1;}}return *this;
}//日期+天数
Date Date::operator+(int day) {if (day < 0) {return *this - (-day);}Date temp(*this);temp += day;return temp;
}//日期-天数
Date Date::operator-(int day) {if (day < 0) {return *this + (-day);}Date temp(*this);temp -= day;return temp;
}//日期-=天数
Date& Date::operator-=(int day) {if (day < 0) {return *this += -day;}_day -= day;while (_day <= 0) {_month--;if (_month == 0) {_month = 12;_year--;}_day += GetMonthDay(_year, _month);}return *this;
}//前置++
Date& Date::operator++() {*this += 1;return *this;
}//后置++
Date Date::operator++(int) {Date temp(*this);*this += 1;return temp;
}//后置--
Date& Date::operator--(int) {Date temp(*this);*this -= 1;return temp;
}//前置--
Date& Date::operator--() {*this -= 1;return *this;
}//>运算符重载
bool Date::operator>(const Date& date) {if (_year > date._year) {return true;}else if (_year == date._year && _month > date._month) {return true;}else if (_year == date._year && _month == date._month && _day > date._day) {return true;}else {return false;}
}//==运算符重载
bool Date::operator==(const Date& date) {return _year == date._year && _month == date._month && _day == date._day;
}//>=运算符重载
bool Date::operator>=(const Date& date) {return *this > date || *this == date;
}//<运算符重载
bool Date::operator<(const Date& date) {return !(*this >= date);
}//<=运算符重载
bool Date::operator<=(const Date& date) {return !(*this > date);
}//!=运算符重载
bool Date::operator!=(const Date& date) {return _year != date._year || _month != date._month || _day != date._day;
}//日期-日期 返回天数
int Date::operator-(const Date& date) {Date max = *this;Date min = date;int flag = 1;if (max < min) {max = date;min = *this;flag = -1;}int count = 0;while (min != max) {++min;++count;}return count * flag;
}

7 const成员

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

编译器对const成员函数的处理

我们再看看下面的代码:

class Date {
public:Date(int year = 2023, int month = 1, int day = 1) {_year = year;_month = month;_day = day;}void Print() {cout << "Print()" << endl;cout << "_year:" << _year << endl;cout << "_month:" << _month << endl;cout << "_day:" << _day << endl;}void Print()const {cout << "Print()const" << endl;cout << "_year:" << _year << endl;cout << "_month:" << _month << endl;cout << "_day:" << _day << endl;}private:int _year;int _month;int _day;
};void Test() {Date d1(2022, 1, 13);cout << "d1:" << endl;d1.Print();cout << endl;const Date d2(2022, 2, 5);cout << "d2(const):" << endl;d2.Print();
}int main() {Test();return 0;
}

运行程序可以发现: 虽然调用函数一样,但const 对象会调用对应的 const 成员函数,而非 const 对象则调用普通成员函数。当我们将const成员函数注释掉后,此时类中只有普通成员函数,再运行程序,编译器报错:error C2662: “void Date::Print(void)”: 不能将“this”指针从“const Date”转换为“Date &”,这是因为,对于编译器自动传递的隐含形参 this指针 ,其在普通成员函数参数列表中的完整声明为:类型* const this , 以日期类为例就是 Date* const this , 表示指针指向不能改变,但是并没有约束指向的日期对象中的内容不能改变,而用 cosnt 修饰的对象的成员是不能被改变的,如果再用其去调用普通成员函数的话,相当于权限被放大了,所以无法通过编译。基于 this指针 作为隐含参数是不能被更改的,即我们无法再直接用 const 去显示的修饰 *this,因此为了使 const 修饰的对象也能够调用相应的函数,引入了 const 成员函数的概念,在函数最后用 const 修饰,实际上是用 const 修饰了 *this (const Date* const this)。此外,当我们将普通成员函数注释掉,只保留对应的 const 成员函数时,非 const 修饰的对象就会去调用 const 成员函数,这里传参时则是权限缩小了,因此可以正常运行,也就是说,在有普通成员函数时,非 const 对象会优先调用非 const 成员函数,如果没有,再去调用 const 成员函数。

结果


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

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

示例:

class Date
{
public:Date* operator&(){return this;}const Date* operator&()const{return this;}private:int _year; // 年int _month; // 月int _day; // 日
};

以上是我对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 日期类的实现7 const成员8 取地址及const取地址操作符重载1 类的6个默认成员函…...

53. 最大子数组和

文章目录题目描述暴力法动态规划法分治法参考文献题目描述 给你一个整数数组 nums &#xff0c;请你找出一个具有最大和的连续子数组&#xff08;子数组最少包含一个元素&#xff09;&#xff0c;返回其最大和。 子数组 是数组中的一个连续部分。 示例 1&#xff1a; 输入&…...

基于Java+SpringBoot+SpringCloud+Vue前后端分离医院管理系统设计与实现

博主介绍&#xff1a;✌全网粉丝3W&#xff0c;全栈开发工程师&#xff0c;从事多年软件开发&#xff0c;在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建、毕业项目实战、项目定制✌ 博主作品&#xff1a;《微服务实战》专栏是本人的实战经验总结&#xff0c;《S…...

QT基础入门【环境配置篇】linux桌面QT开发环境的构建以及问题解决

目录 一、下载QT的安装包 二、安装 1.执行以下命令开始安装 2.选择配置 三、启动...

Linux系统之部署企业内部静态导航页

Linux系统之部署企业内部静态导航页 一、本次实践目的二、检查本地系统环境1.检查系统版本2.检查内核版本三、下载静态导航页资源包1.创建下载目录2.下载资源包四、安装apache服务1.安装httpd2.复制网页文件3.重启httpd服务4.检查httpd服务状态五、访问导航页六、修改导航页网站…...

2023备战金三银四,Python自动化软件测试面试宝典合集(四)

接上篇&#xff1a;11、点击塞钱进红包&#xff0c;选择使用新卡付款&#xff0c;按照流程添加新卡&#xff0c;此时同样需要考虑金额>新卡余额&#xff0c;金额<新卡余额&#xff0c;金额新卡余额三种情况12、使用指纹确认付款(正确的/不正确的指纹)13、使用密码确认付款…...

算法训练营 day43 动态规划 不同路径 不同路径 II

算法训练营 day43 动态规划 不同路径 不同路径 II 不同路径 62. 不同路径 - 力扣&#xff08;LeetCode&#xff09; 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右移动一步。机器人试图达…...

关联查询的SQL有几种情况

1、内连接&#xff1a;inner join … on 结果&#xff1a;A表 ∩ B表 2、左连接&#xff1a;A left join B on &#xff08;2&#xff09;A表全部 &#xff08;3&#xff09;A表- A∩B 3、右连接&#xff1a;A right join B on &#xff08;4&#xff09;B表全部 &#…...

查缺补漏三:事务隔离级别

什么是事务&#xff1f; 事务就是一组操作的集合&#xff0c;事务将整组操作作为一个整体&#xff0c;共同提交或者共同撤销 这些操作只能同时成功或者同时失败&#xff0c;成功即可提交事务&#xff0c;失败就执行事务回滚 MySQL的事务默认是自动提交的&#xff0c;一条语句执…...

没有她的通讯录(C语言实现)

&#x1f680;write in front&#x1f680; &#x1f4dd;个人主页&#xff1a;认真写博客的夏目浅石. &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐️ 留言&#x1f4dd; &#x1f4e3;系列专栏&#xff1a;夏目的C语言宝藏 &#x1f4ac;总结&#xff1a;希望你看完之…...

Spring Security 从入门到精通

前言 Spring Security 是 Spring 家族中的一个安全管理框架。相比与另外一个安全框架Shiro&#xff0c;它提供了更丰富的功能&#xff0c;社区资源也比Shiro丰富。 一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多&#xff0c;因为相比与Spr…...

微信小程序Springboot vue停车场车位管理系统

系统分为用户和管理员两个角色 用户的主要功能有&#xff1a; 1.用户注册和登陆系统 2.用户查看系统的公告信息 3.用户查看车位信息&#xff0c;在线预约车位 4.用户交流论坛&#xff0c;发布交流信息&#xff0c;在线评论 5.用户查看地图信息&#xff0c;在线导航 6.用户查看个…...

看完这篇 教你玩转渗透测试靶机vulnhub——Hack Me Please: 1

Vulnhub靶机Hack Me Please: 1渗透测试详解Vulnhub靶机介绍&#xff1a;Vulnhub靶机下载&#xff1a;Vulnhub靶机安装&#xff1a;Vulnhub靶机漏洞详解&#xff1a;①&#xff1a;信息收集&#xff1a;②&#xff1a;漏洞利用③&#xff1a;获取反弹shell&#xff1a;④&#x…...

nodejs+vue地铁站自动售票系统-火车票售票系统vscode

地铁站自动售票系统主要包括个人中心、地铁线路管理、站点管理、购票信息管理、乘坐管理、用户信息管理等多个模块。它使用的是前端技术&#xff1a;nodejsvueelementui 前后端通讯一般都是采取标准的JSON格式来交互。前端技术&#xff1a;nodejsvueelementui,视图层其实质就是…...

Spring Security in Action 第十二章 OAuth 2是如何工作的?

本专栏将从基础开始&#xff0c;循序渐进&#xff0c;以实战为线索&#xff0c;逐步深入SpringSecurity相关知识相关知识&#xff0c;打造完整的SpringSecurity学习步骤&#xff0c;提升工程化编码能力和思维能力&#xff0c;写出高质量代码。希望大家都能够从中有所收获&#…...

天工开物 #5 我的 Linux 开发机

首先说一下结论&#xff1a;最终我选择了基于 Arch Linux[1] 的 Garuda Linux[2] 发行版作为基础来搭建自己的 Linux 开发机。Neofetch 时刻发行版的选择在上周末的这次折腾里&#xff0c;我一共尝试了 Garuda Linux 发行版&#xff0c;原教旨的 Arch Linux 发行版&#xff0c;…...

【沁恒WCH CH32V307V-R1开发板输出DAC实验】

【沁恒WCH CH32V307V-R1开发板输出DAC实验】1. 前言2. 软件配置2.1 安装MounRiver Studio3. DAC项目测试3.1 打开DAC工程3.2 编译项目4. 下载验证4.1 接线4.2 演示效果5. 小结1. 前言 数字/模拟转换模块&#xff08;DAC&#xff09;&#xff0c;包含 2 个可配置 8/12 位数字输入…...

Linux进程控制详解

目录前言一、进程创建1.1 fork函数初识1.2 写时拷贝1.3 fork常规用法1.4 fork调用失败的原因二、进程终止2.1 进程终止时&#xff0c;操作系统做了什么&#xff1f;&#xff1f;2.2 进程终止的常见方式有哪些&#xff1f;&#xff1f;2.3 如何用代码终止一个进程三、进程等待3.…...

C语言深度剖析之程序环境和预处理

1.程序的翻译环境和执行环境 第一种是翻译环境&#xff0c;在这个环境中源代码被转换为可执行的机器指令 第二种是执行环境&#xff0c;它用于实际执行代码 2.翻译环境 分为四个阶段 预编译阶段 &#xff0c;编译&#xff0c;汇编&#xff0c;链接 程序编译过程&#xff1a;多个…...

【Spark分布式内存计算框架——Spark Core】9. Spark 内核调度(上)

第八章 Spark 内核调度 Spark的核心是根据RDD来实现的&#xff0c;Spark Scheduler则为Spark核心实现的重要一环&#xff0c;其作用就是任务调度。Spark的任务调度就是如何组织任务去处理RDD中每个分区的数据&#xff0c;根据RDD的依赖关系构建DAG&#xff0c;基于DAG划分Stag…...

Vulkan教程(15): Graphics pipeline之Render passes(渲染通道)

Vulkan官方英文原文&#xff1a; https://vulkan-tutorial.com/Drawing_a_triangle/Graphics_pipeline_basics/Render_passes对应的Vulkan技术规格说明书版本&#xff1a; Vulkan 1.3.2Setup设置Before we can finish creating the pipeline, we need to tell Vulkan about the…...

乐观锁、雪花算法、MyBatis-Plus多数据源

乐观锁、雪花算法、MyBatis-Plus多数据源e>雪花算法2、乐观锁a>场景b>乐观锁与悲观锁c>模拟修改冲突d>乐观锁实现流程e>Mybatis-Plus实现乐观锁七、通用枚举a>数据库表添加字段sexb>创建通用枚举类型c>配置扫描通用枚举d>测试九、多数据源1、创建…...

详解Redisson分布式限流的实现原理

我们目前在工作中遇到一个性能问题&#xff0c;我们有个定时任务需要处理大量的数据&#xff0c;为了提升吞吐量&#xff0c;所以部署了很多台机器&#xff0c;但这个任务在运行前需要从别的服务那拉取大量的数据&#xff0c;随着数据量的增大&#xff0c;如果同时多台机器并发…...

[python入门㊹] - python测试类

目录 ❤ 断言方法 assertEqual 和 assertNotEqual assertTrue 和 assertFalse assertIsNone 和 assertIsNotNone ❤ 一个要测试的类 ❤ 测试AnonymousSurvey类 ❤ setUp() 和 teardown() 方法 ❤ 断言方法 常用的断言方法: 方法 用途 assertEqual(a, b) 核实a …...

Web 框架 Flask 快速入门(二)表单

课程地址&#xff1a;Python Web 框架 Flask 快速入门 文章目录&#x1f334; 表单1、表单介绍2、表单的简单实现1. 代码2. 代码的执行逻辑3、使用wtf扩展实现4、bug记录&#xff1a;表单验证总是失败&#x1f334; 表单 1、表单介绍 当我们在网页上填写账号密码进行登录的时…...

C++基础(5) - 复合类型(上)

文章目录数组1、什么是数组2、数组的声明3、数组的初始化4、数组的访问5、二维数组6、memset —— 给数组中每一个元素赋同样的值字符串&#xff08;字符数组&#xff09;1、string.h 头文件1.1 strlen()1.2 strcmp()1.3 strcpy()1.4 strcat()string 类简介1、C11 字符串初始化…...

java重写(@Override)介绍及实例说明

1.概述方法的重写&#xff08;override&#xff09;是封装的特性之一。在子类中可以根据需要对基类中继承来的方法进行重写。重载和重写没有任何关系。作用&#xff1a;通过重写&#xff0c;子类既可以继承父类的东西&#xff0c;又可以灵活的扩充。1.override注解是告诉编译器…...

基于STM32的虚拟示波器

仓库地址 https://github.com/shuai132/ScopeMCU ScopeMCU Oscilloscope for MCU MCU: STM32F103C8Tx 需配合ScopeGUI使用 截图说明见wiki 最新版Releases Introduction 用最少的硬件成本&#xff0c;做一个实用的虚拟示波器。 这是硬件部分&#xff0c;基于STM32最小…...

搭建云端vscode-server,使用web ide进行远程开发

使用乌班图系统&#xff0c;搭建自己的网页vs code开发环境github地址&#xff1a;GitHub - coder/code-server: VS Code in the browser安装脚本curl -fsSL https://code-server.dev/install.sh | sh出现deb package has been installed.表示已经正确安装。测试启动2.1修改配置…...

Linux clock子系统及驱动实例

文章目录基本概念CLK子系统时钟API的使用clock驱动实例1、时钟树2、设备树3、驱动实现fixed_clk固定时钟实现factor_clk分频时钟实现gate_clk门控时钟实现基本概念 晶振&#xff1a;晶源振荡器 PLL&#xff1a;Phase lock loop&#xff0c;锁相环。用于提升频率 OSC&#xff1a…...

的网站建设公司哪家好/站外推广方式

AHP是最基础的评价类算法&#xff1a; 解决评价类问题&#xff0c;大家首先要想到以下三个问题&#xff1a; &#xff08;1&#xff09; 我们评价的目标是什么&#xff1f;答&#xff1a;为小明同学选择最佳的旅游景点。 &#xff08;2&#xff09; 我们为了达到这个目标有哪几…...

哪个网站做阿里首页模板/云计算培训

1、给出了一个序列&#xff0c;你需要处理如下两种询问。 "C a b c"表示给[a, b]区间中的值全部增加c (-10000 ≤ c ≤ 10000)。 "Q a b" 询问[a, b]区间中所有值的和。 2、线段树单点更新太费时&#xff0c;所以使用区间更新 3、 #include <cstdio>…...

潍坊企业宣传片制作公司/杭州seo网站建设靠谱

Bootstrap Method:在统计学中&#xff0c;Bootstrap从原始数据中抽取子集&#xff0c;然后分别求取各个子集的统计特征&#xff0c;最终将统计特征合并。例如求取某国人民的平均身高&#xff0c;不可能测量每一个人的身高&#xff0c;但却可以在10个省市&#xff0c;分别招募10…...

美女教师做爰网站/多地优化完善疫情防控措施

引言 前面Android开发之旅&#xff1a;环境搭建及HelloWorld&#xff0c;我们介绍了如何搭建Android开发环境及简单地建立一个HelloWorld项目&#xff0c;本篇将通过HelloWorld项目来介绍Android项目的目录结构。本文的主要主题如下&#xff1a; 1、HelloWorld项目的目录结构 1…...

wordpress 首页定制/球队积分排名

前言阿里巴巴一直是很多Java程序最想去的公司之一&#xff0c;今天我就给大家分享一个阿里Java程序员面经&#xff1a;阿里面试流程面试一般是四到五面&#xff0c;以电话面试为主。最后一轮面试时HR面试&#xff0c;所以只要挺过前面的技术面试一般就OK了。第一轮是考察基础&a…...

网站建设目标论文/电脑培训学校在哪里

刚开始接触网络的时候&#xff0c;都必须得学习 OSI 模型&#xff0c;很多人不以为然&#xff01; 但是该模型&#xff0c;对于项目中的技术实施与故障定位与排查&#xff0c;具有极其重要的意义&#xff01; 少年&#xff0c;慢慢体会~~ 附件&#xff1a;http://down.51cto.co…...