C++:类和对象(中)


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)”: 未调用原型函数(是否是有意用变量定义的?)

报错

扫描二维码关注公众号,回复: 14698722 查看本文章
  • 不能用对象去调用构造函数,如 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,15
	return 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++中类和对象相关的一些学习记录总结,如有错误,希望大家帮忙指正,也欢迎大家给予建议和讨论,谢谢!

猜你喜欢

转载自blog.csdn.net/qq_67216978/article/details/128881728