【C++】类和对象 --- 类中的6个默认成员函数

作者主页:lightqjx

本文专栏:C++

目录

一、构造函数

[1. 基础概念](#1. 基础概念)

[2. 使用特性](#2. 使用特性)

[3. 特性的解释](#3. 特性的解释)

二、析构函数

[1. 基本概念](#1. 基本概念)

[2. 使用特性](#2. 使用特性)

[3. 特性的解释](#3. 特性的解释)

三、拷贝构造函数

[1. 基本概念](#1. 基本概念)

[2. 使用特性](#2. 使用特性)

[3. 特性的解释](#3. 特性的解释)

四、赋值运算符重载

[1. 基本概念](#1. 基本概念)

[2. 使用特性](#2. 使用特性)

[3. 特性的解释](#3. 特性的解释)

五、取地址操作符重载

[1. const 成员](#1. const 成员)

[2. 取地址操作符重载和const取地址操作符重载](#2. 取地址操作符重载和const取地址操作符重载)


前言

如果一个类中什么成员都没有 ,简称为空类。但是空类在并不是什么都没有,任何类在什么都不时,编译器会自动生成以下6个默认成员函数。

默认成员函数如果用户没有显式实现,但编译器会自动生成的成员函数称为默认成员函数


一、构造函数

为什么要有构造函数?

如果我们仍然写初始化函数对于一个日期类:

cpp 复制代码
#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 d;//实例化
	d.Init(2025, 8, 24);
	d.Print();
	return 0;
}

如果仍保留C语言的思维,每次创建对象时都调用初始化函数(Init函数)来设置信息,就比较麻烦,如果忘记调用这个函数,此时的对象中的成员变量就是随机值了,这是非常不好的情况。

但是,若在对象创建时,就将信息设置进去,这样就比较简单了,因此就出现了构造函数构造函数 就可以实现**在对象创建时,就将信息设置进去。**不用再写像这样 "d.Init(2025, 8, 24);" 的语句了。

1. 基础概念

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

因此,构造函数的作用就是用来方便我们初始化的。

2. 使用特性

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

需要注意的特性如下:

  1. 函数名与类名相同。
  2. 构造函数没有返回值,也不需要写void 。
  3. 对象实例化时编译器会自动调用对应的构造函数。
  4. 构造函数可以重载。
  5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
  6. (1)无参构造函数;(2)全缺省构造函数;(3)我们没写编译器默认生成的构造函数 --- 都可以认为是默认构造函数。它们三个在类中只能存在一个(三选一)。

3. 特性的解释

对特性1、2、3的解释 --- 构造函数的基本定义

构造函数是怎么用的?来实操一下,我们将上面的那个Date类使用构造函数来实现初始化:

cpp 复制代码
#include <iostream>
using namespace std;
class Date
{
public:
	//构造函数
	Date()
	{
		_year = 2025;
		_month = 8;
		_day = 26;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d;//实例化时会自动调用构造函数来实现初始化,不用再另写函数
	d.Print();
	return 0;
}

编译运行后是可以完全通过的。

对特性4的解释 --- 支持函数重载

使用函数重载,进一步修改上述代码:

cpp 复制代码
#include <iostream>
using namespace std;
class Date
{
public:
	//构造函数1
	Date()
	{
		_year = 2025;
		_month = 8;
		_day = 26;
	}
	//构造函数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; // 调用无参构造函数
	Date d2(2025,8,27); // 调用有参构造函数

	//分别打印
	d1.Print();
	d2.Print();
	return 0;
}

这也是完全没有问题的,需要注意的只有参数的传递,即传递的数目是否正确的问题。

需要补充一下:

关于必须要传递参数的构造函数的使用方式是:在对象实例化时的后面加一个括号,然后括号中写要传递的参数(传递方式和普通函数调用一样)。但是对于无参的构造函数时不能怎么使用的,因为这样写,会跟函数声明有点冲突,编译器不好识别。

cpp 复制代码
Date d1(); // 可以理解为:返回对象为 Date类 的一个函数的声明(冲突)
//Date d1(); //        - 错误方式(冲突)
Date d2(2025,8,27); // - 正确方式

当然重载时,构造函数也是可以使用缺省参数的,其中全缺省的构造函数是默认构造函数(在特性6时会再讲)。

对特性5的解释 --- 若不显示定义,则会自动生成无参的默认构造函数

若正常显示定义构造函数

cpp 复制代码
#include <iostream>
using namespace std;
class Date
{
public:
	// 显示定义
	Date()
	{
		_year = 2025;
		_month = 8;
		_day = 26;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1; // 实例化  
	d1.Print();
	return 0;
}

运行后:

这是可以正常运行的。

不显示定义构造函数

cpp 复制代码
#include <iostream>
using namespace std;
class Date
{
public:
	// 不显示定义
	/*Date()
	{
		_year = 2025;
		_month = 8;
		_day = 26;
	}*/
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1; // 实例化  
	d1.Print();
	return 0;
}

运行后:

可以发现,这时就有问题了:打印出来的是随机数。为什么呢?不是说有自动生成的默认构造函数吗?难道编译器生成的默认构造函数并没有什么用?

其实,它是调用了默认构造函数的。要理解这个,就需要理解默认构造函数。在C++中,把类型分成内置类型(基本类型)和自定义类型:

  • 内置类型就是语言提供的数据类型,如:short、int、char、指针、 ...... 等等,
  • 定义类型就是我们使用class、struct、union等自己定义的类型,

而编译器自动生成的默认构造函数对于内置类型成员变量不做处理(有些编译器会做处理,但那是个性化处理,不是所有的编译器都会处理),对于自定义类型的成员变量才会处理(会去调用它的默认构造函数)。如以下代码所示:

cpp 复制代码
#include <iostream>
using namespace std;
class Time
{
public:
	Time()
	{
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
	void Print()
	{
		cout << _hour << "-" << _minute << "-" << _second << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
public:
	//不显示定义构造函数

	//打印
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
		_t.Print();
	}
private:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;

	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	d.Print();
	return 0;
}

运行后:

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

cpp 复制代码
#include <iostream>
using namespace std;
class Date
{
public:	
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	// C++11才支持,只是声明,不是初始化。
	// 给的缺省值是给默认生成的默认构造函数使用的
	int _year = 1; 
	int _month = 2;
	int _day = 3;
};
int main()
{
	Date d1; // 实例化  
	d1.Print();
	return 0;
}

可以正常运行。结果是打印出的就是缺省值。

注意:这里虽然给了缺省值,但是这里不是初始化,因为这里只是声明 ,这里给的是默认的缺省值,是给编译器生成的默认构造函数用的
对特性6的解释 --- (1)无参构造函数;(2)全缺省构造函数;(3)我们没写编译器默认生成的构造函数 --- 都可以认为是默认构造函数。它们三个在类中只能同时存在一个(三选一)。

为什么只能存在一个呢?首先,因为如果显示定义构造函数,则就没有编译器默认生成的构造函数(若有(1)、(2),则无(3));对于全缺省的构造函数和无参构造函数,则当实例化时若不传参,就会出现歧义((1)、(2)会冲突)。


二、析构函数

1. 基本概念

构造函数时当对象实例化时用来初始化的,而当程序运行结束后,如果也一定是要销毁的,就像C语言实现栈时,需要销毁开辟的空间。一般我们往往会忘记写这个销毁函数,虽然在程序运行结束时编译器会自动销毁,但是对于一些动态申请的空间,编译器是不会销毁的,这时就会发生内存泄漏的问题,是非常不好的,所以C++就出现了一个用来自动实现清理功能的函数。

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

2. 使用特性

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

  1. 析构函数名是在类名前加上字符 ~ 。
  2. 无参数无返回值类型。
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
  5. 一个局部域有多个对象时,后被定义的会调用析构函数。
  6. 如果类中没有显式定义析构函数,则会自动生成一个无参的默认析构函数。 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数;有资源申请时,一定要写,否则会造成资源泄漏。

3. 特性的解释

对特性1、2、3、4的解释 --- 析构函数的基本定义

我们来实现一个析构函数,下面是一个 Stack 类的部分代码:

cpp 复制代码
#include <iostream>
using namespace std;
class Stack
{
public:
	Stack(int n = 4)  // 构造函数
	{
		_a = (int*)malloc(sizeof(int) * n);
		if (_a == nullptr)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}

	~Stack()  // 析构函数的使用
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

private:
	int* _a;
	int _capacity;
	int _top;
};

int main()
{
	Stack st;
	return 0;
}

当运行到return时才会开始调用析构函数。

对特性5的解释 --- 一个局部域有多个对象时,后被定义的会先被析构

如图所示:

cpp 复制代码
#include <iostream>
using namespace std;
class Time
{
public:
	~Time()
	{
		cout << "~Time()" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
private:
	// 基本类型(内置类型)
	int _year = 2025;
	int _month = 8;
	int _day = 27;

	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

对特性6的解释 --- 默认析构函数

和构造函数一样,如果不显示定义,编译器会自动生成,并且

  • 对于内置类型成员变量不做处理(因为内置类型成员,销毁时不需要资源清理,最后系统会直接将其内存回收);
  • 对于自定义类型的成员变量才会处理(会去调用它的默认析构函数)。

如以下代码:

cpp 复制代码
#include <iostream>
using namespace std;
class Time
{
public:
	~Time()
	{
		cout << "~Time()" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
private:
	// 基本类型(内置类型)
	int _year = 2025;
	int _month = 8;
	int _day = 27;

	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

在上述代码中,main方法中创建了Date对象d,而d中包含4个成员变量,三个是内置类型,一个自定义类型。由于_t是Time类对象,所以在d 销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是:main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁。

结论:一般情况下,有动态申请资源时,就需要显示写析构函数来释放资源,没有动态申请资源就不需要析构函数。所以,需要释放资源的都是自定义类型,不需要写析构函数。


三、拷贝构造函数

1. 基本概念

拷贝构造函数是用来创建一个与已存在对象一某一样的新对象的。

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

2. 使用特性

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

  1. 拷贝构造函数是构造函数的一个重载形式。
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用 (最好使用const来修饰),使用传值方式编译器直接报错,因为会引发无穷递归调用
  3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
  4. 类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
  5. 拷贝构造函数的典型调用场景
    (1)使用已存在对象创建新对象
    (2)函数参数类型为类类型对象
    (3)函数返回值类型为类类型对象

C++规定:内置类型是可以直接拷贝的,而自定义类型必须调用拷贝构造函数进行拷贝。

3. 特性的解释

对特性 1、2 的解释 --- 拷贝构造函数的基本定义方式

因为是构造函数的重载,所以拷贝构造函数的名字也是和类名相同的,也没有返回值,函数参数只有一个,是类类型对象的引用(最好使用const来修饰,表示常性)。如以下代码所示:

cpp 复制代码
#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 Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2025, 8, 26);
	Date d2(d1);  // 拷贝构造函数

	//分别打印
	d1.Print();
	d2.Print();
	return 0;
}

接下来解答两个疑问:

  1. 为什么要用传引用的方式,而不用传值?
  2. 为什么最好要用const修饰参数?

问题 1 解答:

如果传值,那么在调用函数时就需要传递实参,而定义函数时的形参是实参的拷贝,在C++中如果是自定义类型,则必须要调用拷贝构造函数,而内置类型,则不会调用拷贝构造函数。所以如果拷贝构造函数改为传值调用。如下代码所示:

cpp 复制代码
//拷贝构造函数 --- 传值调用
Date(const Date d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

会出现如下错误:

当我们开始调用拷贝构造时,会进行形参的拷贝,这是又会调用拷贝构造,又会进行形参的拷贝,又会调用拷贝构造函数,如此无限重复下去......

问题 2 解答:

加 const 有助于保持引用的常性,防止拷贝原数据被修改。

对特性 3、4 的解释 --- 未显式定义,会生成默认的拷贝构造函数。

不写拷贝构造函数,编译器会生成默认拷贝构造函数,对于默认拷贝构造函数

  • 对于内置类型,则会完成浅拷贝
  • 对于自定义类型,则会调用它的拷贝构造函数

浅拷贝是让对象按内存储按字节序完成拷贝,换一种说法就是值拷贝,只对不用申请资源的情况有效。如果是申请了资源的情况,则要完成的就是深拷贝。

首先,来看浅拷贝的情况:

cpp 复制代码
#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1, 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;
};
int main()
{
	Date d1(2025, 8, 26);
	Date d2(d1);

	//分别打印
	d1.Print();
	d2.Print();
	return 0;
}

运行后:

可以发现,这里调用的是默认构造函数。

首先,来看深拷贝的情况:

深拷贝是对于一些申请了资源的情况的拷贝,比如栈类。如以下代码:

cpp 复制代码
#include <iostream>
using namespace std;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (int*)malloc(sizeof(int) * n);
		if (_a == nullptr)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}

	//没有写拷贝构造函数

	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

private:
	int* _a;
	int _capacity;
	int _top;
};

int main()
{
	Stack st1;
	Stack st2(st1);
	return 0;
}

如果直接运行这个代码,程序是会崩溃的,如果调试,就会可以出现以下情况:

可见栈是不能像日期类的那样直接调用默认构造函数的,也就是不能直接用浅拷贝。原因如下:

因为通过上面这个代码实例化出的两个栈对象其实是指向同一块空间的,因为默认生成的拷贝构造函数时值拷贝,传递的是指针,则拷贝也是指针故而你讲st1中的内容原封不动的拷贝到st2中去,指向的也就是同一块空间。而在结束时,调用了两次析构函数,也就是对同一块空间释放了两次空间,同一块空间释放两次就会有问题,这就是错误的原因。

所以为了不出现这种情况,就需要我们自己实现一个深拷贝,防止两个对象指向同一块空间。如以下代码所示:

cpp 复制代码
#include <iostream>
using namespace std;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (int*)malloc(sizeof(int) * n);
		if (_a == nullptr)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}

	//写一个拷贝构造函数 --- 实现深拷贝
	Stack(const Stack& st)
	{
		_a = (int*)malloc(sizeof(int) * st._capacity);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}

		memcpy(_a, st._a, sizeof(int) * st._top);
		_top = st._top;
		_capacity = st._capacity;
	}

	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _capacity;
	int _top;
};

int main()
{
	Stack st1;
	Stack st2(st1);
	return 0;
}

这样就不会出现问题了。

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

对特性 5 的解释 --- 拷贝构造的典型应用场景

(1)使用已存在对象创建新对象

cpp 复制代码
Stack st1;
Stack st2(st1);//创建新对象

(2)函数参数类型为类类型对象

cpp 复制代码
class Stack
{
public:
	Stack(int n = 4)
	{.......}

	//拷贝构造函数 -- 参数为类型
	Stack(const Stack& st)
	{
		_a = (int*)malloc(sizeof(int) * st._capacity);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}

		memcpy(_a, st._a, sizeof(int) * st._top);
		_top = st._top;
		_capacity = st._capacity;
	}

	~Stack()
	{.......}
private:
	int* _a;
	int _capacity;
	int _top;
};

(3)函数返回值类型为类类型对象

函数返回值有返回值,和返回引用的情况。由于传值返回会产生一个临时对象调用拷贝构造,传值引用返回的是返回对象的别名。如果返回的是一个局部变量的对象,则出局部域,对象就没有了,这时就不能使用引用返回了。这时就是一个野引用,和野指针一样。所以,如果要返回对象的引用,则一定要确保返回的在出函数局部域后仍然存在。


四、赋值运算符重载

1. 基本概念

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

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

因为对于类类型的对象,也是需要比较大小的,比如日期类比较日期大小等。由于类用普通运算符比较是不能比较的,所以我们就可以自己来实现类的比较。所以就可以使用运算符重载来实现。

赋值运算符重载 是用来实现两个已存在对象之间的复制拷贝。其函数定义如下:

cpp 复制代码
数据类型 operator=(类类型参数)
{      }

2. 使用特性

  1. 赋值运算符重载格式
    (1)参数类型:const T&,传递引用可以提高传参效率;
    (2)返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值;
    (3)检测是否自己给自己赋值;
    (4)返回 *this :要复合连续赋值的含义
  2. 赋值运算符只能重载成类的成员函数不能重载成全局函数
  3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。

3. 特性的解释

特性 1、2 的解释 --- 赋值运算符重载格式

根据是以上所有特性,可以得到赋值运算符重载的格式定义,来看以下代码:

cpp 复制代码
#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指针,实际传参:Date& operator=(Date* const this,const Date& x)
    // 即:operator=(d1,d2)
	Date& operator=(const Date& x)  
	{
		if (this != &x) // 判断是否给自己赋值
		{
			_year = x._year;
			_month = x._month;
			_day = x._day;
		}
		return *this;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2025, 8, 26);
	Date d2(2025, 9, 1);
	d1.Print();
	d2.Print();

	d1 = d2;
	//调用,相当于:operator=(d1,d2); // 将d2复制给d1

	d1.Print();
	d2.Print();
	return 0;
}

其中的赋值运算符重载的返回值也是类的类型的引用。因为

  • 这样可以支持连等赋值,如果是void类型,则只能赋值一个
  • 使用引用可以提高效率。

并且这种类的赋值运算符重载,一定要在类的里面定义,不能在全局定义。其原因如下:

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

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

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

如以下代码:

cpp 复制代码
#include <iostream>
using namespace std;
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;
		 }
		 return *this;
	 }
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d1;
	Date d2;

	// 默认赋值运算符重载
	// 1.内置类型完成值拷贝
	// 2.自定义类型定义它自己的赋值运算符重载
	d1 = d2;
	return 0;
}

深拷贝的问题

由于默认赋值函数重载只是浅拷贝。所以对于一些申请了资源的类型的拷贝就并不是很好了。比如上面我们所说的Stack类的拷贝构造。对于默认赋值函数重载也是一样。如以下代码:

cpp 复制代码
#include <iostream>
using namespace std;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (int*)malloc(sizeof(int) * n);
		if (_a == nullptr)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}

	// 不写赋值运算符重载,生成默认的赋值运算符重载

	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _capacity;
	int _top;
};

int main()
{
	Stack st1;
	Stack st2;
	st2 = st1;
	return 0;
}

调试后,可以发现这两个栈对象所指向的空间都是一样的。

所以运行也会崩溃。

因此,我们就需要实现深拷贝,来避免这种错误。

cpp 复制代码
#include <iostream>
using namespace std;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (int*)malloc(sizeof(int) * n);
		if (_a == nullptr)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}

	// 写一个赋值运算符重载 --- 实现深拷贝
	Stack& operator=(const Stack& st)
	{
		if (this != &st)
		{
			free(_a);
			_a = (int*)malloc(sizeof(int) * st._capacity);
			if (nullptr == _a)
			{
				perror("malloc申请空间失败");
				return *this;
			}
			memcpy(_a, st._a, sizeof(int) * st._top);
			_top = st._top;
			_capacity = st._capacity;
		}
		return *this;
	}

	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _capacity;
	int _top;
};

int main()
{
	Stack st1;
	Stack st2;
	st2 = st1;
	return 0;
}

五、取地址操作符重载

1. const 成员

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

也就是说被const修饰的成员函数中的this指针除了指针本身不能改变,还有它指向的内容也不能改变。

来看下面这个代码:

cpp 复制代码
#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1, 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; // 日
};

int main()
{
	Date d1(2025, 8, 26);
	d1.Print();

	const Date d2(2025, 8, 27);
	d2.Print();

	return 0;
}

这个代码是有错误的。d2.Print()是不能通过编译的。即:

因为这里出现了权限的放大。由于d2在实例化时是被const修饰了的,所以d2只是可以读的权限,不能改变,类型为const Date ,而Print()是普通成员函数,调用函数时会将对象传递给this指针,而this指针指向的对象是既可以读,也可以改的。所以它的权限放大了,从而发生了错误。

所以我们也需要将this指向的内容用const进行修饰,那要怎么修饰呢?this指针是看不见的嘛。而在这里C++规定了只要在函数后面加一个const就可以堆*this进行修饰了,即:

cpp 复制代码
void Print() const
{
	cout << _year << "-" << _month << "-" << _day << endl;
}

所以,我们在以后,如果不对*this的内容进行修改,则就可以在函数后面加上const

关于const,只需要直到权限只能平移或缩小,但是不能放大就行了。

2. 取地址操作符重载和const取地址操作符重载

最后两个默认构造函数:取地址操作符重载const取地址操作符重载 ,一般这两个函数编译器自己默认生成的就已经够用了,如以下代码:

cpp 复制代码
#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

int main()
{
	Date d1(2025, 8, 26);
	const Date d2(2025, 8, 27);
	cout << &d1 << endl;
	cout << &d2 << endl;
	return 0;
}

运行结果:

如果自己要实现,其内容大致如下:

cpp 复制代码
#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对象取地址  --  普通对象
	Date* operator&()
	{
		return this;
	}

	//对const Date对象取地址  --  const修饰的对象
	const Date* operator&()const
	{
		return this;
	}

private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

int main()
{
	Date d1(2025, 8, 26);
	const Date d2(2025, 8, 27);
	cout << &d1 << endl;
	cout << &d2 << endl;
	return 0;
}

所以,如果想让别人获取到指定的内容时,才会自己写这两种函数。比如,不需要别人取到当前对象的地址。如以下代码:

cpp 复制代码
#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&()
	{
		return nullptr; // 返回空地址,别人就取不到了
		//return this;
	}

	//const修饰的对象
	const Date* operator&()const
	{
		return nullptr; // 返回空地址,别人就取不到了
		//return this;
	}

private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

int main()
{
	Date d1(2025, 8, 26);
	const Date d2(2025, 8, 27);
	cout << &d1 << endl;
	cout << &d2 << endl;
	return 0;
}

感谢各位观看!希望能多多支持!

相关推荐
Tipriest_19 小时前
C++ 中 ::(作用域解析运算符)的用途
开发语言·c++·作用域解析
Swift社区20 小时前
Java 常见异常系列:ClassNotFoundException 类找不到
java·开发语言
Tipriest_20 小时前
求一个整数x的平方根到指定精度[C++][Python]
开发语言·c++·python
闻缺陷则喜何志丹21 小时前
【有序集合 有序映射 懒删除堆】 3510. 移除最小数对使数组有序 II|2608
c++·算法·力扣·有序集合·有序映射·懒删除堆
蓝倾97621 小时前
淘宝/天猫店铺商品搜索API(taobao.item_search_shop)返回值详解
android·大数据·开发语言·python·开放api接口·淘宝开放平台
John_ToDebug1 天前
从源码看浏览器弹窗消息机制:SetDefaultView 的创建、消息转发与本地/在线页通用实践
开发语言·c++·chrome
菌王1 天前
EXCEL 2 word 的一些案例。excel通过一些策略将内容写入word中。
开发语言·c#
励志不掉头发的内向程序员1 天前
STL库——list(类模拟实现)
开发语言·c++·学习
Swift社区1 天前
Swift 解法详解:LeetCode 367《有效的完全平方数》
开发语言·leetcode·swift
蒋星熠1 天前
Python API接口实战指南:从入门到精通
开发语言·分布式·python·设计模式·云原生·性能优化·云计算