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

目录

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

2、构造函数

        构造函数概念

        构造函数特性

3、析构函数

        析构函数概念

        析构函数特性

4、拷贝构造函数

        拷贝构造函数概念

        拷贝构造函数特性

5、赋值运算符重载

        运算符重载

        赋值运算符重载

6、const成员

        const修饰类的成员函数

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


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

在我们前面学习的类中,我们会定义成员变量和成员函数,这些我们自己定义的函数都是普通的成员函数,但是如若我们定义的类里什么也没有呢?是真的里面啥也没吗?如下:

class Date {};

如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成6个默认成员函数。

【 C++ 】类和对象(中)—— 类的6个默认成员函数_第1张图片


2、构造函数

构造函数概念

如下的日期类:

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, 5, 17);
	d1.Print();
	return 0;
}

正常情况下,我们写的这个日期类,首先初始化,其次打印。但如果说你突然忘记初始化了,直接就开始访问会怎么样呢?

【 C++ 】类和对象(中)—— 类的6个默认成员函数_第2张图片

从运行结果上看,没初始化直接访问输出的是随机值。 忘记初始化其实是一件很正常的事情,C++大佬在这一方面为了填补C语言的坑(必须得手动初始化)。因而就设计出了构造函数

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。其目的就是为了方便我们不需要再初始化。

构造函数特性

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

其特征如下:

  1. 函数名和类名相同
  2. 无返回值
  3. 对象实例化时编译器自动调用对应的构造函数
  4. 构造函数可以重载
  5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
  6. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。

如下即为构造函数:

Date()
{
	_year = 1;
	_month = 1;
	_day = 1;
}
  • 解释特性3:对象实例化时编译器自动调用对应的构造函数

也就是说我们在实例化一个对象后,它会自动调用这个构造函数,自动就初始化了,我们可以通过调试看看:

【 C++ 】类和对象(中)—— 类的6个默认成员函数_第3张图片

  • 解释特性4:构造函数支持重载,

如下的函数:

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

像这个重载函数是明确了我们要传参的,所以我们在实例化对象后就必须把参数写上去(虽然看着奇奇怪怪,但是没有办法,毕竟我们普通的调用,参数都是在函数名后面,而这个参数在实例化对象后面):

Date d2(2022, 5, 17);

来输出和我们先前的构造函数对比看看:

【 C++ 】类和对象(中)—— 类的6个默认成员函数_第4张图片

  • 注意:没有参数时我在调用的时候不能加上括号(),切忌!!构造函数尤为特殊
  • 如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明

无参的情况下必须要像我们刚开始实例化的d1那样:

Date d1;
d1.Print();
  •  构造函数的重载我们推荐写成全缺省的样子:
//普通的构造函数
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}
//全缺省的构造函数
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

首先,普通的构造函数和全缺省的构造函数在不调用的情况下可以同时存在,编译也没有错误。但是在实际调用的过程中,会存在歧义。如下的调用:

class Date
{
public:
//普通的构造函数
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}
//全缺省的构造函数
	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;
	d1.Print();
}

此时我实例化的d1到底是调用普通的构造函数?还是调用全缺省的构造函数?并且此段代码编译出现错误。何况我在没有调用函数的情况下编译是没错的。

由此可见:它们俩在语法上可以同时存在,但是使用上不能同时存在,因为会存在调用的歧义,不知道调用的是谁,所以一般情况下,我们更推荐直接写个全缺省版的构造函数,因为是否传参数可由你决定。传参数数量也是由你决定。

  • 解释特性5:如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
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 d;
	d.Print();
}

不是说好我不自己写构造函数,编译器会默认生成吗?为什么到这又是随机值了?这难道也算初始化?别急,搞清楚这个得先明白默认构造函数

注意:C++把变量分成两种

  • 内置类型/基本类型:int、char、double、指针……
  • 自定义类型:class、struct去定义的类型对象

C++默认生成的构造函数对于内置类型成员变量不做处理,对于自定义类型的成员变量才会处理,这也就能很好的说明了为什么刚才没有对年月日进行处理(初始化),因为它们是内置类型(int类型的变量)

让我们来看看自定义类型是如何处理的。

class A
{
public:
	A()
	{
		cout << "A()" << endl;
		_a = 0;
	}
private:
	int _a;
};

首先,这是一个名为A的类,有成员变量_a,并且还有一个无参的构造函数,对_a初始化为0。接着:

class Date
{
public:
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
	A _aa;
};
int main()
{
	Date d;
	d.Print();
}

日期类里有三个内置类型,一个自定义类型(A_aa),我们编译运行看看结果:

【 C++ 】类和对象(中)—— 类的6个默认成员函数_第5张图片

通过运行结果以及调试,也正验证了默认构造函数对自定义类型才会处理。这也就告诉我们,当出现内置类型时,就需要我们自己写构造函数了。

什么时候使用默认构造函数会凸显出其价值呢?就比如我们之前写的括号匹配这道题:

class Stack
{
public:
	Stack()
	{
		_a = nullptr;
		_top = _capacity;
	}
private:
	int* _a;
	int _top;
	int _capacity;

};

class MyQueue 
{
public:
	//默认生成的构造函数就可以用了
	void push(int x)
	{}

	int pop() 
	{}
private:
	Stack _S1;
	Stack _s2;
};

此时我队列里自定义类型_s1和_s2就不需要单独写初始化了,直接用默认的。但是如果栈里没有写构造函数,那么其输出的还是随机的,因为栈里的也是内置类型。就是一层套一层,下一层生效的前提是上一层地基打稳了。

总结:

  1. 如果一个类中的成员全是自定义类型,我们就可以用默认生成的函数
  2. 如果有内置类型的成员,或者需要显示传参初始化,那么都要自己实现构造函数。
  • 解释特性6:无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。

特性6可以简单总结为不用传参就可以调用的即为默认构造函数

【 C++ 】类和对象(中)—— 类的6个默认成员函数_第6张图片

既然我默认构造函数只对自定义类型才会处理,那如果我不想自己再写构造函数也要对内置类型处理呢?我们可以这样做:(后续的博文会继续详细讲解这块)

【 C++ 】类和对象(中)—— 类的6个默认成员函数_第7张图片


3、析构函数

析构函数概念

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

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

析构函数特性

析构函数是特殊的成员函数。

其特征如下:

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

我们实际写一个析构函数看看:

~Date()
{
	cout << "~Date()" << endl;
}

带入示例再看看:

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()
	{
		cout << "~Date()" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	return 0;
}

首先,我实例化出的d1会调用它的默认构造函数进行初始化,其次,出了作用域后又调用其析构函数,这也就是为什么输出结果会是~Date()

析构的目的是为了完成资源清理,什么样的才能算是资源清理呢?像我这里定义的年月日变量就不需要资源清理,因为出了函数栈帧就销毁,真正需要清理的是malloc、new、fopen这些的,就比如清理栈里malloc出的

class Stack
{
public:
	//构造函数
	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		assert(_a);
		_top = 0;
		_capacity = capacity;
	}
	//析构函数
	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
int main()
{
	Stack st;
}

这里不难感慨C++的构造函数就像先前C语言常写的Init,而析构函数就像Destroy

  • 看如下的题目:
int main()
{
	Stack st1;
	Stack st2;
}

现在我用类实例化出st1和st2两个对象,首先,st1肯定先构造,st2肯定后构造,这点毋庸置疑,那关键是谁先析构呢?

答案:st2先析构,st1后析构

解析:这里st1和st2是在栈上的,建立栈帧,其性质和之前一样,后进先出,st2后压栈,那么它肯定是最先析构的。所以栈里面定义对象,析构顺序和构造顺序是反的。

  • 解释特性3:一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。

若自己没有定义析构函数,虽说系统会自动生成默认析构函数,不过也是有要求的,和构造函数一样,内置类型不处理,自定义类型会去调用它的析构函数,如下:

class Stack
{
public:
	//构造函数
	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		assert(_a);
		_top = 0;
		_capacity = capacity;
	}
	//析构函数
	~Stack()
	{
		cout << "~Stack():" << this << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;

};
class MyQueue
{
public:
	//默认生成的构造函数可以用
	//默认生成的析构函数也可以用
	void push(int x)
	{}

	int pop()
	{}
private:
	Stack _S1;
	Stack _s2;
};
int main()
{
	MyQueue q;
}

对于MyQueue而言,我们不需要写它的默认构造函数,因为编译器对于自定义类型成员(_S1和_S2)会去调用它的默认构造,Stack提供了默认构造,出了作用域,编译器会针对自定义类型的成员去默认调用它的析构函数,因为有两个自定义成员(_S1和_S2),自然析构函数也调了两次,所以会输出两次Stack()……


4、拷贝构造函数

拷贝构造函数概念

我们在创建对象时,可否创建一个与一个对象一某一样的新对象呢?

int main()
{
	Date d1(2022, 5, 18);
	Date d2(d1);
	return 0;
}

能否让d2的值跟d1一样呢?也就是说我拿d1去初始化d2,此时调用的函数就是拷贝构造函数。

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

拷贝构造函数特性

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

  1. 拷贝构造函数是构造函数的一个重载形式。
  2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
  3. 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。

如下即为拷贝构造函数:

Date(Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}
  • 解释特性2:拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。

为什么传值传参会引发无穷递归呢?

我们先举一个普通的func函数作为例子:

//传值传参
void Func(Date d)
{}
int main()
{
	Date d1(2022, 5, 18);
	Func(d1);
	return 0;
}

此函数调用传参是传值传参。在C语言中,把实参传给形参是把实参的值拷贝给形参,而我的实参d1是自定义类型的,需要调用拷贝构造,传值传参是要调用拷贝构造的,但是我如果不想调用拷贝构造呢?就需要引用传参,因为此时d就是d1的别名

void Func(Date& d) {}

此时再回到我们刚才的例子:如若我不传引用传参,就会疯狂的调用拷贝构造:

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

【 C++ 】类和对象(中)—— 类的6个默认成员函数_第8张图片

为了避免出现无限递归调用拷贝构造,所以要加上引用,加上引用后,d就是d1的别名,不存在拷贝构造了。同类型的传值传参是要调用拷贝构造的

Date(const Date& d) {} 
//最好加上const,对d形成保护
  • 解释特性3:若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。
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;
};
void Func(Date& d)
{
	d.Print();
}
int main()
{
	Date d1(2022, 5, 18);
	Date d2(d1);
	Func(d1);
	d2.Print();
}

为什么我这里没有写拷贝构造函数,它也会自动完成拷贝构造呢?由此我们要深思,拷贝构造与构造和析构是不一样的,构造和析构都是针对自定义类型才会处理而内置类型不会处理,而默认拷贝构造针对内置类型的成员会完成值拷贝,浅拷贝,也就是像把d1的内置类型成员按字节拷贝给d2。

由此得知,对于日期类这种内置类型的成员是不需要我们写拷贝构造的,那是不是所有的类都不需要我们写拷贝构造呢?来看下面的栈类。

class Stack
{
public:
	//构造函数
	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		assert(_a);
		_top = 0;
		_capacity = capacity;
	}
//不写拷贝构造,编译器调用默认拷贝构造
	/*
	Stack(const Stack& st)
	{
		_a = st._a;
		_top = st._top;
		_capacity = st._capacity;
	}
	*/
	//析构函数
	~Stack()
	{
		cout << "~Stack():" << this << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
int main()
{
	Stack st1(10);
	Stack st2(st1);
}

是这也写吗?

【 C++ 】类和对象(中)—— 类的6个默认成员函数_第9张图片

我们通过调试看到运行崩溃了,可见栈的拷贝构造函数不能像日期类一样不写而让编译器去调用默认拷贝构造(就是按照日期类的模式写了拷贝构造也会出错),因为此时的st1(指针)和st2(指针)指向的就是同一块空间,通过调试可以看出:

【 C++ 】类和对象(中)—— 类的6个默认成员函数_第10张图片

【 C++ 】类和对象(中)—— 类的6个默认成员函数_第11张图片

st1和st2指向同一块空间会引发一个巨大的问题:析构函数那出错,因为我st2会先析构,析构完后我st1再析构,不过我st1指向的空间已经被st2析构过了,因为它俩指向同一块空间,同一块空间我释放两次就会有问题。 出了析构存在问题,增删查改那也会有问题,这个后续会谈到。

其实刚才写的栈的拷贝构造就是浅拷贝,真正栈的拷贝构造应该用深拷贝来完成,此部分内容我等后续会专门出一篇博文详解,这里大家先简单接触下。

综上,我们可以得知,浅拷贝针对日期类这种是没有问题的,而类的成员若是指向一块空间的话就不能用浅拷贝了。

  • 如果是自定义类型呢?让编译器自己生成拷贝构造会怎么样呢?

自定义类型的成员,去调用这个成员的拷贝构造。

class Stack
{
public:
	//构造函数
	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		assert(_a);
		_top = 0;
		_capacity = capacity;
	}
//不写拷贝构造,编译器调用默认拷贝构造
	/*
    浅拷贝
	Stack(const Stack& st)
	{
		_a = st._a;
		_top = st._top;
		_capacity = st._capacity;
	}
	*/
private:
	int* _a;
	int _top;
	int _capacity;
};
class MyQueue
{
//默认拷贝构造针对内置类型的成员会完成值拷贝,浅拷贝
//自定义类型的成员,去调用这个成员的拷贝构造。
private:
	int _size = 0;
	Stack _S1;
	Stack _s2;
};
int main()
{
	MyQueue mq1;
	MyQueue mq2(mq1);
}

其实这里同样是发生了浅拷贝,归根结底在于我栈的深拷贝还没写。

【 C++ 】类和对象(中)—— 类的6个默认成员函数_第12张图片

仔细看我的调试,mq2调用了其成员栈的拷贝构造,而我mq1和mq2的两个栈又发生了浅拷贝,它们对应的_S1和_S2指向的地址都是一样的,这也就意味着析构时同一块空间又会析构两次,出错。这里套了两层。

  • 总结:一般的类,自己生成拷贝构造就够用了,只有像Stack这样自己直接管理资源的类,需要自己实现深拷贝。

补充:

void TestDate2()
{
	Date d1(2022, 5, 18);
	Date d3 = d1; //等价于 Date d3(d1);
}

Date d3 = d1 是拷贝构造,不是赋值,拿一个对象初始化另一个对象是拷贝构造。

5、赋值运算符重载

运算符重载

如下的日期类:

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;
};

我能否按照如下的方式对日期类的对象进行大小比较呢?

【 C++ 】类和对象(中)—— 类的6个默认成员函数_第13张图片

很明显是不可以的,从波浪线提示的警告就能看出。我们都清楚内置类型是可以直接进行比较的,但是自定义类型是不能直接通过上述的运算符进行比较的,为了能够让自定义类型使用各种运算符,于是就提出了运算符重载的规则。

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

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

注意:

  • 运算符重载函数的参数由运算符决定,比如运算符==应有两个参数,双操作数运算符就有两个参数,单操作数运算符(++或--)就有一个参数

就比如我现在写一个日期比较相等的运算符重载(传值传参会引发拷贝构造,所以要加上引用,最好再加上const):

bool operator==(const Date& d1, const Date& d2) //避免传值传参调用拷贝构造
{
	return d1._year == d2._year &&
		d1._month == d2._month &&
		d1._day == d2._day;
}

【 C++ 】类和对象(中)—— 类的6个默认成员函数_第14张图片

最好把运算符重载函数放到类里面(像图示的将成员变量变成公有也可以),因为类外不允许访问私有的成员变量(虽然可以通过友元来解决,不过现如今暂未学到)

仔细观察我的截图:毕竟我都写好了运算符重载,可是我调用运算符重载的方式怎么还能跟调用普通函数一样呢?与其这样还取名运算符重载又有何意义,所以真正的调用应该如下:

【 C++ 】类和对象(中)—— 类的6个默认成员函数_第15张图片

调用的时候直接和内置类型进行运算符操作那样,编译器会自动处理成调用运算符重载的样子 

  • 注意:上述的运算符重载就算完成了吗?当然不是,按理说我们要把运算符重载函数放成类里的成员函数。

并且,这里的参数也不能像如上的方式写:

【 C++ 】类和对象(中)—— 类的6个默认成员函数_第16张图片

如若直接把运算符重载函数放到类里,编译器会报错(运算符函数的参数太多)。报错的原因就在于成员函数存在隐含的this指针。 这也就意味着实际的参数有3个,因此我们要少写一个参数:

bool operator==(const Date& d)//编译器会处理成 bool operator(Date* const this, const Date& d)
{
	return _year == d._year &&
		_month == d._month &&
		_day == d._day;
}

并且我在调用成员函数的时候也要做出改变:

if (d1.operator==(d2))
{
	cout << "==" << endl;
}

和刚才一样,为了凸显出运算符重载的意义,我们调用的时候可以直接像内置类型一样操作运算符,因为编译器会帮我们处理:

if (d1 == d2)//编译器会处理成对应重载运算符调用if (d1.operator==(d2))或者if (d1.operator==(&d1, d2))
{
	cout << "==" << endl;
}

如下:

【 C++ 】类和对象(中)—— 类的6个默认成员函数_第17张图片

  • 现在,我们来写一个日期类的比较大小来练练手:
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//日期比较大小
	bool operator<(const Date& d)
	{
		if (_year > d._year ||
			_year == d._year && _month > d._month ||
			_year == d._year && _month == d._month && _day > d._day)
			return false;
		else
			return true;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2022, 5, 17);
	Date d2(2022, 5, 20);
	if (d1 < d2)
		cout << "<" << endl;
}

 

接下来,再来总结下运算符重载的注意点:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型(对自定义类型成员才可运算符重载)或者枚举类型的操作数
  • 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义
  • 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参
  • .*::sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。

赋值运算符重载

前面我们已经学习了拷贝构造,是拿同类型的对象去初始化另一个对象,那如果我不想用拷贝构造呢?

int main()
{
	Date d1(2022, 5, 17);
	Date d2(2022, 5, 20);
	Date d3(d1);//拷贝构造 -- 一个存在的对象去初始化另一个要创建的对象
	d2 = d1; //赋值重载/复制拷贝 -- 两个已经存在的对象之间赋值
}

可不可以直接拿d1去赋值给d2呢?这就是我们要谈的赋值运算符重载。赋值运算符重载和上文的运算符重载是有点相似的。有了运算符重载的基础,写一个赋值重载还是很简单的。

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

但是这里的赋值重载是存在一定问题的,我们C语言的赋值是支持连等赋值的,如下:

int i = 0, j, k;
k = j = i;

我们把i赋值给j,随后把j作为返回值再赋值给k。要知道C++是建立在C的基础上的,刚刚我们写的赋值重载支持连等吗?

【 C++ 】类和对象(中)—— 类的6个默认成员函数_第18张图片

很显然是不支持的,原因就是当我把d1赋值给d2后,没有一个返回值来赋给d3,这就导致出错。改正如下: 

Date operator=(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
	return *this;
}

【 C++ 】类和对象(中)—— 类的6个默认成员函数_第19张图片

此外:这里的赋值重载还可进一步改进。

  • 改进1:刚才我们写的赋值重载是传值返回,传值返回会生成一个拷贝,会调用拷贝构造。如果出了作用域要让其对象还在我们就可以用传引用返回:
  • 改进2:有可能会存在这样的情况:d1=d1,像这样自己给自己赋值的情况还要再调用赋值重载函数属实没必要,所以我们还可以加个if条件判断。

修正如下:

	//d2 = d1;  -> d2.operator=(&d2, d1);
	Date& operator=(const Date& d)
	{
		if (this != &d) //不推荐写成if (*this != d) ,怕的是万一没有重载!=呢?,因为这里是对象的比较
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}
  • 注意:

operator赋值也是默认成员函数,我们不写赋值重载,编译器也会默认生成,不过编译器完成的依旧是值拷贝或浅拷贝,像这个日期类就可以不写赋值重载:

【 C++ 】类和对象(中)—— 类的6个默认成员函数_第20张图片

赋值重载和拷贝构造一样,我们不写,它会对内置类型完成值拷贝,而像栈这样的就不能不写了,因为我们要写一个深拷贝的赋值重载才可以,理由和拷贝构造类似。 具体实现等真正谈到深拷贝再来。

  •  补充:
void TestDate2()
{
	Date d1(2022, 5, 18);
	Date d3 = d1; //等价于 Date d3(d1);
}

Date d3 = d1 是拷贝构造,不是赋值,拿一个对象初始化另一个对象是拷贝构造。如下d2 =d1才是赋值:

void TestDate2()
{
	Date d1(2022, 5, 18);
    Date d2(2022, 5, 20);
	Date d3 = d1; //等价于 Date d3(d1); 是拷贝构造
    d2 = d1; //两个已经存在的对象才是赋值
}

6、const成员

const修饰类的成员函数

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

假如我现在有一个日期类,并且有如下的Func函数即调用情况:

【 C++ 】类和对象(中)—— 类的6个默认成员函数_第21张图片

很明显,这里Func函数d的调用Print()出错了,而d1调用Print()却没出错,为何呢?

这里涉及到权限问题。我们先把实际调用过程中,隐含的this指针写出来:

【 C++ 】类和对象(中)—— 类的6个默认成员函数_第22张图片

Print()函数里的const修饰this本身,this不能修改,但是this可以初始化,接着我们要搞清楚&d1和&d的类型分别是啥:

  • &d1:Date*
  • &d:const Date*
  1. Date*传给Date* const没有问题,都是可读也可修改,所以d1调用Print()不会出错
  2. 而const Date* 指向的内容不能被修改,可是当它传给Date*时就出错了,因为Date*是可以修改的,这里传过去会导致权限放大。所以当然d调用Print()函数报错。

解决办法:

加上const去保护this指向的内容,也就是在Date*的前面加上const:

void Print(const Date* const this)
{
	cout << _year << "-" << _month << "-" << _day << endl;
}

但是这里又不能之间加上const,因为this指针是隐含的,你不能显示的将const写出来。因此,C++为了解决此问题,允许在函数后面加上const以达到刚才的效果:

void Print() const// 编译器默认处理成:void Print(const Date* const this)
{
	cout << _year << "-" << _month << "-" << _day << endl;
}

此时我const Date*传给const Date*就是权限不变,自然不会出错了,同样我Date*传给const Date*就是权限缩小也不会有问题。因为权限不能放大,只能缩小或不变。

  • 注意:

建议成员函数中不修改成员变量的成员函数,都加上const,此时普通对象和const对象都可以调用。

补充:请思考下面几个问题:

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

答案:不能,const调用非const是权限放大

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

答案:可以,此时权限缩小

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

答案:不可以,依旧是权限放大

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

答案:可以,权限缩小

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

class Date
{
public:
    //取地址&重载
	Date* operator&()
	{
		return this;
	}
    //const取地址&重载
	const Date* operator&()const
	{
		return this;
	}
private:
	int _year;
	int _month;
	int _day;
};

当然,如果我们自己不写&重载,编译器也会默认生成,可以通过打印来看看:

【 C++ 】类和对象(中)—— 类的6个默认成员函数_第23张图片

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

你可能感兴趣的:(C,plus,plus,c++,开发语言,类和对象)