C++ -- 类和对象【中】

一、类的默认成员函数

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

⼀个类,我们不写的情况下编译器会默认生成以下 6 个默认成员函数,需要注意的是这6个中最重要的是前 4 个,最后两个取地址重载不重要,我们稍微了解⼀下即可。其次就是 C++11 以后还会增加两个默认成员函数,移动构造和移动赋值,这个我们后⾯再讲解。默认成员函数很重要,也比较复杂,我们要从两个方面去学习:

  • 第⼀:我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求。
  • 第⼆:编译器默认生成的函数不满⾜我们的需求,我们需要自己实现,那么如何自己实现?

1、构造函数

1.1 定义

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

构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象 (我们常使用的局部对象是栈帧创建时,空间就开好了),而是对象实例化时初始化对象

其特点如下:

  1. 函数名与类名相同。
  2. 无返回值。 (返回值啥都不需要给,也不需要写 void,不要纠结,C++ 规定如此)
  3. 对象实例化时系统会自动调用对应的构造函数
  4. 构造函数可以重载。

说明:C++ 把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的原生数据类型,如:int/char/double/指针 等,自定义类型就是我们使用 class/struct 等关键字自己定义的类型。

下面是一个日期类的构造函数:

cpp 复制代码
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 d1(1,1,1);//自动调用
	d1.Print();
	return 0;
}
  • 构造函数的功能就相当于我们之前书写的初始化函数,但由于其自动调用的特性,大大提升了代码的容错率。

1.2 注意

  1. 如果类中没有显式定义构造函数,则 C++ 编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
cpp 复制代码
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;
};

​​​​​

  1. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
cpp 复制代码
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;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d;//引起歧义
	return 0;
}

当存在多个默认构造函数时,一旦我们对对象进行实例化,编译器不知道调用哪个构造函数,就会引起歧义。

  1. 编译器生成的默认构造函数只会对自定义类型(类)进行初始化,内置类型(int,double...)不会进行初始化,即调用自定义类型的构造函数。
cpp 复制代码
class Betty
{
public:
	Betty()
	{
		cout << "Betty" << endl;
	}
private:
	int _a;
};
class Date
{
public:
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	Betty b;
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d;
	d.Print();
	return 0;
}

从上述实例观察,编译器自动生成的默认构造函数的确只对自定义类型进行初始化。

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

cpp 复制代码
class Date
{
public:
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year = 1;//缺省值
	int _month = 1;//缺省值
	int _day = 1;//缺省值
};
int main()
{
	Date d;
	d.Print();
	return 0;
}

对于自定义类型,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错。如下代码所示:

cpp 复制代码
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	// ...
private:
	STDataType * _a;
	size_t _capacity;
	size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
	//编译器默认生成 MyQueue 的构造函数调用了Stack的构造,完成了两个成员的初始化
private:
	Stack pushst;
	Stack popst;
};
int main()
{
	MyQueue mq;
	return 0;
}

调试可以看到,mq 对象调用了 Stack 的构造函数。

这里我们给 MyQueue 加一个 size 成员。可以看到 size 被初始化为0,但是编译器对于内置类型是不确定的,也就是说这个 size 不同平台的实现的不同。也就是说这里的 size 就是个坑,因为有可能这里被初始化为 0,其他平台又是随机值。

再来看一下,我们把栈的构造改为带参的。编译器报错。

因为带参的构造不是默认构造。这样 MyQueue 初始化时调用栈的默认构造就找不到。就会报错说没有合适的默认构造。

1.3 初始化列表

之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值,构造函数初始化还有⼀种方式,就是初始化列表。

定义:初始化列表作用与构造函数类似,它是在构造函数中以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式。

下面我们还是以一个日期类来示范:

cpp 复制代码
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 d(2024,1,3);
	d.Print();
	return 0;
}

# 注意:

  1. 每个成员变量在初始化列表中只能出现一次 ( 初始化只能初始化一次 ) 。
  2. 类中包含以下成员,必须放在初始化列表位置进行初始化 :引用成员变量,const 成员变量,自定义类型成员(且该类没有默认构造函数时)。因为这些变量都需要在定义时初始化
cpp 复制代码
class A
{
public:
	A(int a)
		:_a(a)
	{}
private:
	int _a;
};
class B
{
public:
	B(int a, int ref)
		:_b(a)
		, _ref(ref)
		, _n(3)
	{}
private:
	A _b; // 没有默认构造函数
	int& _ref; // 引用
	const int _n; // const常量
};
  1. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
cpp 复制代码
class A
{
public:
	A(int a = 1)//默认构造
		:_a(a)
	{
		cout << "A(int a = 1)" << endl;
	}
private:
	int _a;
};
class B
{
public:
	B(int a)
		:_m(a)
	{}
private:
	int _m;
	A _b; 
};
int main()
{
	B b(2);
	return 0;
}
  1. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
cpp 复制代码
class A
{
public:
	A(int a)
		:_a1(a)
		, _a2(_a1)
	{}
	void Print() 
	{
		cout << _a1 << endl;
		cout << _a2 << endl;
	}
private:
	int _a2;
	int _a1;
};
int main()
{
	A aa(1);
	aa.Print();
}//输出??

如果是以初始化列表的顺序,那应该输出 1 和 1 。如果以声明顺序,那应该是 1 与随机值。

那初始化列表和函数体内赋值可以混着用吗?可以,因为有些场景必须混着用。

cpp 复制代码
Date(int& x,int year = 1, int month = 1, int day = 1)
	:_year(year)
	, _month(month)
	, _day(day)
	,a(1)
	,_ref(x)
	,_ptr((int*)malloc(size(int)//成员定义
{
	if (_ptr == nullptr)
	{
		perror(malloc fail!);
	}
}

例如有个指针,我们 malloc 以后需要检查是否失败。那就必须在函数体内检查。

2、析构函数

2.1 定义

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

  1. 析构函数名是在类名前加上字符 ~ 。
  2. 无参数无返回值类型。
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
  4. 对象生命周期结束时,C++ 编译系统系统自动调用析构函数。

下面是一个日期类的析构函数:

cpp 复制代码
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;
	}
	//析构函数
	~Date()
	{
		_year = _month = _day = 0;
	}
private:
	int _year;
	int _month;
	int _day;
};

析构函数就相当于 C 语言中的销毁函数,但由于其自动调用的特性,大大提升了代码的容错率。

析构函数负责对象指向资源的清理,如果对象没有指向资源则不用写析构函数。

2.2 注意

  1. 如果类中没有显式定义析构函数,则 C++ 编译器会自动生成一个析构函数,一旦用户显式定义编译器将不再生成。
cpp 复制代码
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;
	}
	//析构函数
	/*~Date()
	{
		_year = _month = _day = 0;
	}*/
    //编译器会自动生成一个析构函数
private:
	int _year;
	int _month;
	int _day;
};
  1. 编译器生成的析构函数对内置类型(int,double...)不会进行处理,对于自定义类型一定会调用其析构函数。
cpp 复制代码
class Betty
{
public:
	~Betty()
	{
		cout << "~Betty" << endl;
	}
private:
	int _a;
};
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:
	Betty b;
	int _year;
	int _month;
	int _day;
};

我们再来看 Stack 实现的 MyQueue:

cpp 复制代码
class MyQueue
{
public:
	//编译器默认⽣成MyQueue的析构函数调⽤了Stack的析构,释放的Stack内部的资源
	// 显⽰写析构,也会⾃动调⽤Stack的析构
	~MyQueue()
	{
		cout << "~MyQueue()" << endl;
	}
private:
	Stack pushst;
	Stack popst;
};

例如现在我们显示写了析构,但是没有清理栈的资源。

按理说我们自己写了析构就不会生成默认析构,但是编译器还是会跳转到栈的析构,所以自定义类型不论写不写都会调用他的析构。

  1. 因为指针类型也属于内置类型,所以默认成员在动态内存开辟内存后,必须显式写成析构函数。不能靠编译器默认生成。
cpp 复制代码
typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 2)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (nullptr == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}
	void Push(DataType data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	~Stack()
	{
		if (_array!=nullptr)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};

比如说上述代码,默认生成的析构函数并不会释放其内存,就可能造成内存泄漏。

3、拷贝构造函数

3.1 定义

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

  1. 拷贝构造函数是构造函数的⼀个重载。

  2. C++ 规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。

cpp 复制代码
class Date
{
public:
	Date(int year = 1900, 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;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2024,2,2);
	Date d2(d1);//拷贝构造
	Date d3 = d1;//拷贝构造
    d1.Print();
	d2.Print();
	d3.Print();
	return 0;
}

3.2 注意

  1. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
cpp 复制代码
Date(const Date d) // error
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

int main()
{
	Date d(2024, 9, 8);
	Date d1(d);

	return 0;
}

因为 C++ 规定传值传参会调用拷贝构造,所以不用引用就会引发无穷递归。

  1. 若未显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
cpp 复制代码
class Date
{
public:
	Date(int year = 1900, 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(2024, 2, 2);
	Date d2(d1);//拷贝构造
	Date d3 = d1;//拷贝构造
	d1.Print();
	d2.Print();
	d3.Print();
	return 0;
}
  1. 因为编译器默认生成的拷贝构造函数是值拷贝,在某些场景下就会出错。比如说以下场景:
cpp 复制代码
class Stack
{
public:
    Stack(int n = 4)
    {
        _a = (STDataType*)malloc(sizeof(STDataType) * n);
        if (nullptr == _a)
        {
            perror("malloc申请空间失败");
                return;
        }
        _capacity = n;
        _top = 0;
    }
    void Push(STDataType x)
    {
        if (_top == _capacity)
        {
            int newcapacity = _capacity * 2;
            STDataType* tmp = (STDataType*)realloc(_a, newcapacity *
                sizeof(STDataType));
            if (tmp == NULL)
            {
                perror("realloc fail");
                return;
            }
            _a = tmp;
            _capacity = newcapacity;
        }
        _a[_top++] = x;
    }
    ~Stack()
    {
        cout << "~Stack()" << endl;
        free(_a);
        _a = nullptr;
        _top = _capacity = 0;
    }
 private:
     STDataType* _a;
     size_t _capacity;
     size_t _top;
 };
int main()
{
    Stack s;
    s.Push(1);
    Stack s1(s);
}

为什么会出现这种情况呢?因为我们没写栈的拷贝构造,而默认生成的拷贝构造只是进行值拷贝,对于 size,capacity 的拷贝并不会出现问题,但是当 s1 的 _array 拷贝给 s2 的 _array 时,就会让 s1 与 s2 的同时指向同一片空间。而我们知道当对象的作用域结束时,会自动调用析构函数,同时对同一片空间析构两次 ,就会报错。而且 s1 修改也会影响 s 。

这里我们加上拷贝构造两个栈就会指向不同的空间。

所以当类中需要资源申请时,都需要手动写拷贝构造。

  1. 拷贝构造的应用场景有很多,能用引用尽量用引用,减少拷贝,提高程序效率。
  • 传值返回会产生⼀个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名 ( 引用 ),没有产生拷贝。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于⼀个野引用,类似⼀个野指针⼀样。传引用返回可以减少拷贝,但是⼀定要确保返回对象,在当前函数结束后还在,才能用引用返回。
cpp 复制代码
Stack fun()
{
    Stack s;
    return s;
}
int main()
{
    Stack s;
    s = fun();
    return 0;
}

所以要注意使用引用返回时注意不要返回局部变量。

4、赋值运算符重载

4.1 运算符重载

4.1.1 定义

C++ 为了增强代码的可读性引入了运算符重载,运算符重载 是具由运算符 operator 定义的一个有特殊函数名的函数,也具有其返回值类型、函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。该函数能让我们自定义类型像内置类型一样使用 + , - , * , / 等运算符。

下面实现了简单判断日期是否相当的运算符重载:

cpp 复制代码
class Date
{
public:
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
	{
	 //...
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	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(2024,1,1);
	Date d2(2024, 1, 1);
	if (d1 == d2)//也可以显示调用operator==(d1,d2);
	{
		cout << "日期相等" << endl;
	}
	else
	{
		cout << "日期不相等" << endl;
	}
	return 0;
}

重载运算符函数的参数个数和该运算符作用的运算对象数量⼀样多。⼀元运算符有⼀个参数,⼆元运算符有两个参数,⼆元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第⼆个参数。

当然我们也可以将运算符重载声明在类中。如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的 this 指针,因此运算符重载作为成员函数时,参数比运算对象少⼀个。

所以此时成员函数的写法就只用传一个参数。通过 this 指针调用第一个参数

cpp 复制代码
bool operator==(const Date& d)
{
	return _year == d._year
		&& _month == d._month
		&& _day == d._day;
}
4.1.2 注意
  1. 不能通过连接其他符号来创建新的操作符:比如operator@重载操作符必须有一个类类型参数
  2. 重载操作符至少有⼀个类类型参数, 用于内置类型的运算符,其含义不能改变,例如:内置的整型 + ,不 能改变其含义
  3. 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  4. .* sizeof ? : :: .注意以上5个运算符不能重载。

对于5个不能重载的运算符,可能大家对第一个 .* 不太熟悉,这里就给大家浅浅介绍一下

我们先使用 typedef 声明一个成员函数指针,然后再给函数指针赋值,一般情况,直接把函数名赋值给函数指针就可以,但是 C++ 规定成员函数要加&才能取到函数指针,所以下面编译会报错。

一般函数指针回调只需要(*pf)();,但是成员函数有隐含的 this 指针,所以我们也要隐式传地址,那么就需要通过类对象来隐式传,这个时候就会出现 .* 运算符。如下图:

  • 重载 ++ 运算符时,有前置 ++ 和后置 ++,运算符重载函数名都是 operator++,无法很好的区分。因为后置会增加拷贝所以后置做出改变。C++ 规定,后置 ++ 重载时,增加⼀个 int 形参,形参可以不用名字。因为实际上并不接收。只是为了跟前置 ++ 构成函数重载,方便区分。
cpp 复制代码
Date operator++(int);//后置
Date& operator++();//前置
Date operator--(int);//后置
Date& operator--();//前置
  • 重载 << 和 >> 时,需要重载为全局函数,因为重载为成员函数,this 指针默认抢占了第⼀个形参位置,第⼀个形参位置是左侧运算对象,调用时就变成了 对象 << cout,不符合使用习惯和可读性。重载为全局函数把 ostream / istream 放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对象。

需要注意的是全局和类里面都同时存在运算符重载时,优先调用全局的。

  • 如果重载为全局函数,会面临访问私有成员变量的问题,如下编译会报错:

那有啥办法呢?

  1. 成员放公有
  2. Date 提供 getxxx 函数(在类中实现一个获取成员变量的函数,然后在类外可以调用函数来访问成员变量)
  3. 友元函数(下文讲解)
  4. 重载为成员函数

这里建议重载为成员函数,这样更方便。如下图所示:重载运算符 == 为成员函数,在调用时有两种方式可以调用。

4.2 赋值运算符重载

4.2.1 定义

赋值运算符重载是将运算符 = 进行运算符重载。但是它相较于其他运算符重载有着自己独特的特点。

赋值运算符重载是⼀个默认成员函数 ,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于⼀个对象拷贝初始化给另⼀个要创建的对象。

cpp 复制代码
int main()
{
	Date d1(2024, 7, 5);
	Date d2(d1);
	Date d3(2024, 7, 6);

	//赋值重载
	d1 = d3;
	// 需要注意这⾥是拷⻉构造,不是赋值重载
	// 请牢牢记住赋值重载完成两个已经存在的对象直接的拷⻉赋值

	// ⽽拷⻉构造⽤于⼀个对象拷⻉初始化给另⼀个要创建的对象
	//拷贝构造
	Date d4 = d1;

	return 0;
}

赋值运算符重载的特点

  • 参数类型:const T& ,传递引用可以提高传参效率。
  • 返回值类型:T& ,返回引用可以提高返回的效率,支持连续赋值。
  • 检测是否自己给自己赋值。
  • 返回值 * this :要复合连续赋值的值。
cpp 复制代码
Date& operator=(const Date& d)
{
	_year = d.year;
	_month = d.month;
	_day = d.day;

	return *this;
}
4.2.2 注意
  1. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
cpp 复制代码
class Time
{
public:
	Time()
	{
		_hour = 1;
		_minute = 1;
		_second = 1;
	}
	Time& operator=(const Time& t)
	{
		cout << "Time& operator=(const Time& t)" << endl;
		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 = 2024;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d1;
	Date d2;
	d1 = d2;
	return 0;
}
  1. 因为编译器默认生成默认赋值运算符重载的是值拷贝,在某些场景下就会出错。具体实例参考拷贝构造函数。
  2. 赋值运算符只能重载成类的成员函数不能重载成全局函数。
cpp 复制代码
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

    Date& operator=(Date& left, const Date& right) // bingo
    {
	    if (&left != &right)
	    {
		    left._year = right._year;
		    left._month = right._month;
		    left._day = right._day;
	    }
	    return left;
    }

	int _year;
	int _month;
	int _day;
};

Date& operator=(Date& left, const Date& right) //error
{
	if (&left != &right)
	{
		left._year = right._year;
		left._month = right._month;
		left._day = right._day;
	}
	return left;
}

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

像 Date 这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显示实现赋值运算符重载。

像 Stack 这样的类,虽然也都是内置类型,但是 _a 指向了资源,编译器自动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。

像 MyQueue 这样的类型内部主要是自定义类型 Stack 成员,编译器自动生成的赋值运算符重载会调用 Stack 的赋值运算符重载,也不需要我们显示实现 MyQueue 的赋值运算符重载。

这里还有⼀个小技巧,如果⼀个类显示实现了析构并释放资源,那么他就需要显示写赋值运算符重载,否则就不需要。

**5、**取地址运算符重载

5.1 const修饰函数

首先我们得知道一个规则就是:const 修饰的常变量不能赋值给普通变量,因为这样造成 const 权限的放大,但是普通变量可以赋值给 const 修饰的常变量

所以让我们来看看这段代码:

cpp 复制代码
class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << "Print()" << endl;
		cout << "year:" << _year << endl;
		cout << "month:" << _month << endl;
		cout << "day:" << _day << endl << endl;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

int main()
{
	const Date d2(2022, 1, 13);
	d2.Print();//error
	return 0;
}

这段代码会出错,因为 d2 进行函数传参是将 const Date* 传过去,而函数接受参数的类型为 Date*,这样就会造成权限的放大。为了解决这个问题,就需要使用 const 修饰原函数。

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

并且与原函数构成重载,可以同时存在,同时非 const 成员也可以调用 const 成员函数,因为权限可以缩小。所以加上 const 可以防止我们的程序不小心篡改(原本不应该修改却不小心修改会报错),同时也让我们的的成员函数传参更宽泛,const 成员也可以调用,所以能加尽加

5.2 取地址及const取地址运算符重载

取地址运算符重载分为普通取地址运算符重载和 const 取地址运算符重载,⼀般这两个函数编译器自动生成的就可以够我们⽤了,不需要去显示实现。除非⼀些很特殊的场景,比如我们不想让别⼈取到当前类对象的地址,就可以自己实现⼀份,胡乱返回⼀个地址。

cpp 复制代码
class Date
{
public:
	Date* operator&()
	{
		return this;
		// return nullptr;
	}
	const Date* operator&()const
	{
		return this;
		// return nullptr;
	}
private:
	int _year; // 年
	int _month; // ⽉
	int _day; // ⽇
};

一般这两个成员函数都不需要我们显示的写,因为他们两个是默认成员函数。

按理说我们只写 const 成员函数即可。因为不论是 const 成员还是非 const 成员都可以调用。

但是 const 成员调用返回的是 const this*。所以还需要写两份。并且编译器调用时会调用最匹配的。

相关推荐
Elias不吃糖7 小时前
LeetCode每日一练(209, 167)
数据结构·c++·算法·leetcode
Want5957 小时前
C/C++跳动的爱心②
c语言·开发语言·c++
初晴や7 小时前
指针函数:从入门到精通
开发语言·c++
铁手飞鹰7 小时前
单链表(C语言,手撕)
数据结构·c++·算法·c·单链表
无限进步_8 小时前
C语言动态内存管理:掌握malloc、calloc、realloc和free的实战应用
c语言·开发语言·c++·git·算法·github·visual studio
渡我白衣8 小时前
五种IO模型与非阻塞IO
运维·服务器·网络·c++·网络协议·tcp/ip·信息与通信
豐儀麟阁贵8 小时前
7.2内部类
java·开发语言·c++
FLPGYH8 小时前
从头开始c++ day4
开发语言·c++
dvlinker9 小时前
Windows中获取用户鼠标键盘闲置时间,以自动设置用户的离开状态以及处理离开状态下的自动消息回复(以QQ为例进行讲解,附源码)
c++·用户键盘闲置时间·lastinputinfo·sendinput·自动设置用户离开状态·自动回复消息
重启的码农9 小时前
enet源码解析(7): 跨平台套接字调用抽象层
c++·网络协议