【C++初阶】类和对象(中)

文章目录

一.默认成员函数

默认成员函数就是⽤⼾没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。但是一旦显示定义,编译器就不会自动生成了。

此处讲解地类的默认成员函数有:构造函数、析构函数、拷贝构造函数、赋值重载函数和取地址运算符重载函数。

二.构造函数

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,⽽是对象实例化时初始化对象,类似于Stack中的Init函数。

(1)构造函数的特点

  1. 构造函数函数名与类名相同;
  2. ⽆返回值(返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此);
  3. 对象实例化时系统会⾃动调⽤对应的构造函数;
  4. 构造函数可以重载;
  5. 如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数(这个无参构造函数函数体为空,什么都不做),但是⼀旦⽤⼾显式定义构造函数,那么编译器将不再⽣成这个无参的默认构造函数。
  6. ⽆参构造函数、全缺省构造函数、我们不写显示定义构造函数时编译器默认⽣成的构造函数,都叫做默认构造函数。但是这三个函数有且只有⼀个存在,不能同时存在 。⽆参构造函数和全缺省构造函数虽然构成函数重载,但是调⽤时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认⽣成那个叫默认构造,实际上⽆参构造函数、全缺省构造函数也是默认构造,总结⼀下就是**不传实参就可以调⽤**的构造就叫默认构造;
  7. 我们不显示定义,编译器默认⽣成的无参构造函数对类中的内置类型成员变量的初始化没有要求,也就是说是否初始化是不确定的,看编译器。对于类中的⾃定义类型成员变量 ,要求调⽤这个成员变量的默认构造函数 (包括无参、全缺省和不显示定义下的默认生成)对其进行初始化(无论是不是编译器自动生成的类的构造函数,类中自定义类型的成员变量都会调⽤它自个儿的构造函数)。如果这个成员变量,没有默认构造函数,那么就会报错。此时如果我们要初始化这个成员变量,就需要⽤初始化列表才能解决,初始化列表,我以后再细细讲解。

说明:C++把类型分成内置类型(基本类型)和⾃定义类型。

内置类型就是语⾔提供的原⽣数据类型,如:int/char/double/指针等。

⾃定义类型就是我们使⽤class/struct等关键字⾃⼰定义的类型。

(2)代码呈现

cpp 复制代码
#include<iostream>
//工具集合

using namespace std;
//准许使用书

//日期类
class Date
{
public:
	void Print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}

	//构造函数

	//1.无参
	Date()
	{
		_year = 2026;
		_month = 8;
		_day = 17;
	}

	//2.有参
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//3.全缺省
	//Date(int year = 2026, int month = 8, int day = 17)
	//{
	//	_year = year;
	//	_month = month;
	//	_day = day;
	//}

	//三个构造函数构成了重载,但是第一个函数和第三个函数不能共存,会造成调用歧义

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	//实例化------此时自动调用无参构造函数
	Date d2(2026, 8, 17);
	//实例化------此时自动调用含参构造

	d1.Print();
	d2.Print();

	return 0;
}

注意:不准将无参构造和全缺省构造函数的调用写成Date d1( ); 因为编译器无法区分这到底是实例化对象还是函数的声明。

大部分情况下,都需要我们自个儿写构造函数。

三.析构函数

析构函数与构造函数功能相反。

析构函数不是完成对对象本⾝的销毁,像局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管。析构函数是来完成对象中资源的清理释放工作的。

C++规定对象在销毁时会⾃动调⽤析构函数,完成对象中资源的清理释放⼯作。类似于Stack栈中的Destroy函数。

(1)析构函数的特点

  1. 析构函数名是在类名前加上字符~(例如 ~Stack);
  2. ⽆参数⽆返回值;
  3. ⼀个类只能有⼀个析构函数(因为不能有参数,那就不会有重载)。若没有显式定义析构函数,那么系统会⾃动⽣成默认的析构函数,自动生成的析构函数函数体为空;
  4. 类实例化的对象⽣命周期结束时,系统会自动调⽤析构函数。无论该类中的成员变量是否有堆内存、文件关闭这类需要手动释放的资源,只要对象的生命周期走到尽头,就一定会触发析构函数的调用;
  5. 跟构造函数类似,我们不显示定义析构函数时编译器⾃动⽣成的无参析构函数对类中的内置类型成员不做处理,而类中的⾃定义类型成员变量会调⽤他的析构函数(无论是不是编译器自动生成的类的析构函数,类中自定义类型的成员变量都会调⽤他的析构函数);
  6. 还需要注意的是,我们显⽰写析构函数时,对于类中⾃定义类型成员变量,编译器也会调⽤该自定义类型成员变量的析构函数。也就是说,类中⾃定义类型成员变量⽆论什么情况都会⾃动调⽤他自个儿它的析构函数(后面会通过MyQueue和Stack举例);
  7. 如果类中没有申请资源,它的析构函数可以不写,直接使⽤编译器⽣成的默认析构函数就行。如Date类。注意:日期类虽然没有涉及资源清理的问题,但是也会生成默认析构函数,只是析构函数也不会做什么,因为全部都是内置类型变量;如果默认⽣成的析构就可以⽤,也就不需要显⽰写析构函数,如MyQueue,只要Stack有显示定义的析构函数就可以;但是有资源申请时(像堆内存和文件关闭),就一定要⾃⼰写析构函数,否则会造成资源泄漏,如Stack;
  8. ⼀个局部域的多个类实例化对象,C++规定后定义的先调用析构函数清理资源。

(2)代码呈现

cpp 复制代码
//Stack类
#include<iostream>
using namespace std;

typedef int STDataType;
class Stack
{
	
public:

	//构造函数
	Stack(int n = 4)
	{
		 _a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (_a == nullptr)
		{
			perror("malloc failed");
			return;
		}
		_top = 0;
		_capacity = n;
	}

	//析构函数
	~Stack()
	{
		cout << "~Stack()" << endl;

		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}


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

//两个Stack实现队列Queue

class MyQueue
{

public:

	//构造初始化函数
	//可以不写,编译器会默认生成
	//反正都会调用类中自定义类型Stack的构造函数
	MyQueue()
	{}

	//析构函数
	//可以不写,反正会调用Stack自己的析构函数
	~MyQueue()
	{}


private:
	Stack pushst;
	Stack popst;
};

int main()
{
  MyQueue mq;
  //构造和析构都是自动调用

  //对象在实例化时自动调用构造函数
  //对象在生命周期结束时默认调用析构函数
  return 0;
}

四.拷贝构造函数

如果⼀个构造函数的第⼀个参数是⾃⾝类类型的引⽤,且任何额外的参数都有默认值(缺省值),则此构造函数也叫做拷⻉构造函数。

拷贝构造函数和构造函数构成重载的关系。

(1)拷贝构造函数的特点

1.拷贝构造函数是构造函数的⼀个重载(函数名相同,但是参数不同);

  1. C++规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,所以这⾥⾃定义类型传值传参和传值返回都会调⽤拷⻉构造函数来完成;

  2. 拷贝构造函数的第⼀个参数必须是类类型对象的引⽤,如果使⽤传值⽅式传递类类型,编译器会直接报错,因为语法逻辑上会引发⽆穷递归调⽤;

C++中规定自定义类型传值传参时就会调用拷贝构造函数。在上图中,在调用Date的拷贝构造函数时,参数是d1,参数的自定义类型Date且是传值传参,会调用拷贝构造函数。而在调用拷贝构造函数的同时,由于此时的拷贝构造函数本身也是自定义类型传值传参的形式,那么就又会调用拷贝构造函数。按这个思路下去,一直涉及传值传参,一直调用拷贝构造函数,从而就会引发无穷递归调用的问题。

  1. 拷⻉构造函数也可以有多个参数(一般来说只有一个类类型的引用),但是第⼀个参数必须是类类型对象的引⽤,后⾯的参数必须有缺省值;
  2. 若未显式定义拷⻉构造,编译器会⾃动⽣成拷⻉构造函数。自动生成的拷贝构造函数是含参数的,参数是类类型的引用。⾃动⽣成的拷贝构造函数对类中的内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对类中的自定义类型成员变量会调⽤该自定义成员变量的拷⻉构造函数(拷贝构造函数对内置类型成员变量的处理和构造函数、析构函数不同);
  3. 传值返回会产⽣⼀个临时对象调⽤拷⻉构造。传值引⽤返回,返回的是返回对象的别名(引⽤),没有产⽣拷⻉。但如果返回对象是当前函数局部域的局部对象,函数结束就销毁了,那么使⽤引⽤返回是有问题的,这时的引⽤相当于⼀个野引⽤,类似⼀个野指针⼀样。传引⽤返回可以减少拷⻉,但是⼀定要确保返回对象在当前函数结束后还存在,才能⽤引⽤返回;
  4. 像Date这样成员变量全是内置类型且没有指向什么资源(堆内存、文件关闭等)的类,编译器⾃动⽣成的拷⻉构造函数(浅拷贝/值拷贝)就可以完成需要的拷⻉操作,不需要我们显⽰实现拷⻉构造函数;但是像Stack这样的类,虽然它的成员变量也都是内置类型,但是==_a指向了资源(堆内存),编译器⾃动⽣成的拷⻉构造函数完成的值拷⻉/浅拷⻉不符合我们的需求,所以需要我们⾃⼰实现拷贝构造函数来实现深拷⻉(对指向的资源也进⾏拷⻉);而像MyQueue==这样内部的成员变量主要是⾃定义类型Stack类的类型,编译器⾃动⽣成的MyQueue的拷⻉构造会调⽤Stack自个儿的拷⻉构造,也不需要我们显⽰实现MyQueue的拷⻉构造。这里还有⼀个小技巧:如果⼀个类显示实现了析构函数并释放资源,那么它就需要显示写拷贝构造,否则就不需要

为什么编译器自动生成的Stack类的拷贝构造函数完成的浅拷贝不符合需求?

但是如果是深拷贝,就是在堆中开辟一块和原先_a指向的空间大小一样的空间,这样就不用担心同一块内存空间回收释放两次造成程序崩溃和值覆盖的问题了。

这同样也告诉我们C语言在传值传参方面的缺陷。

(2-1) 代码呈现

①不显示定义深拷贝,还是用浅拷贝的拷贝构造函数:

cpp 复制代码
//Stack类
#include<iostream>
using namespace std;

typedef int STDataType;
class Stack
{
	
public:

	//构造函数
	Stack(int n = 4)
	{
		S _a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (_a == nullptr)
		{
			perror("malloc failed");
			return;
		}
		_top = 0;
		_capacity = n;
	}

	//析构函数
	~Stack()
	{
		cout << "~Stack()" << endl;

		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}


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

int main()
{
	Stack st1;
	//实例化对象同时完成初始化
	Stack st2(st1);
	//拷贝构造本质上也是构造函数,调用的写法自然有相同之处,都在定义的同时

	return 0;
}

②显示定义深拷贝的拷贝构造函数:

cpp 复制代码
//Stack类
#include<iostream>
using namespace std;

typedef int STDataType;
class Stack
{
	
public:

	//构造函数
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (_a == nullptr)
		{
			perror("malloc failed");
			return;
		}
		_top = 0;
		_capacity = n;
	}

	//析构函数
	~Stack()
	{
		cout << "~Stack()" << endl;

		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

	//深拷贝

  //Stack (const Stack& this,const Stack& st)
	Stack(const Stack& st)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
		if (nullptr == _a)
		{
			perror("malloc failed");
			return;
		}
		//memcpy(this._a,st._a,sizeof(STDataType) * st._top);
		memcpy(_a, st._a, sizeof(STDataType) * st._top);
		//this._top = st._p;
		_top = st._top;
		//this._capacity = st._capacity
		_capacity = st._capacity;
	}

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

int main()
{
	Stack st1;
	//实例化对象同时完成初始化
	Stack st2(st1);
	//Stack st2 = st1; 这样写也是合法的
	//将st1的数据拷给st2

	return 0;
}

通过浅拷贝和显示定义深拷贝的拷贝构造函数,我们能知道,当自定义类型变量要传给函数是,不建议采用传值传参,因为在此过程中需要调用拷贝构造函数,不仅需要开辟另外的空间来存储形参,还可能出现问题,代价挺大。故而建议使用引用传参。当不需要修改实参时,还可以给引用加上const,以防在函数内部不小心修改了实参,却不知道哪里出了问题。

PS:

C语言中函数传参是可以传结构体的,但从C++的深浅拷贝角度就能看出,其实C语言中传结构体的值是很坑的。

8.传自定义对象的值返回,会调⽤拷⻉构造产⽣⼀个临时对象进行返回。传引⽤返回,返回的是返回对象的别名(引⽤),没有调用拷贝构造函数产生临时对象进行返回。但是传引用返回存在问题,如果返回的对象是当前函数局部域定义的局部对象,函数结束时该局部对象就销毁了,那么使⽤传引⽤返回是有问题的。函数结束后的引⽤相当于⼀个野引⽤,类似于⼀个野指针。所以虽然传引⽤返回可以减少拷⻉,但想⽤传引⽤返回,⼀定要先确保返回对象在当前函数结束后还存在。

(2-2) 代码呈现

cpp 复制代码
//Stack类
#include<iostream>
using namespace std;

typedef int STDataType;
class Stack
{
	
public:

	//构造函数
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (_a == nullptr)
		{
			perror("malloc failed");
			return;
		}
		_top = 0;
		_capacity = n;
	}

	//析构函数
	~Stack()
	{
		cout << "~Stack()" << endl;

		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

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

//✅️
Stack func1()
{
	//函数局部域中定义局部变量并传值返回
	Stack st;
	//产生的临时对象不受func1生命周期的影响
	return st;
}

//❌️
Stack& func2()
{
	//函数局部域中定义局部变量并传引用返回
	Stack st;
	return st;
}

//✅️
Stack& func3()
{
	//全局变量并传引用返回
	static Stack st;
	return st;
}

//✅️
Stack& func4(Stack& st)
{
	//传值传参后传引用返回
	
	//......
	return st;
}

int mian()
{
	Stack ret1 = func1();
	Stack ret2 = func2();
	Stack ret3 = func3();

	Stack st;
	Stack ret4 = func4(st);

	return 0;
}

在类里面想看成员变量,可以直接输入一个this

五.赋值运算符重载函数

(1)运算符重载

  • 当运算符被⽤于类类型的对象时(操作数是类对象),C++允许我们通过运算符重载(自定义运算符重载函数,重新定义运算符的行为)的形式指定新的含义。C++规定类类型对象使⽤运算符时,必须转换成调⽤对应运算符重载的函数,若没有对应的运算符重载函数,则会编译报错(因为普通运算符只作用于内置类型);
  • 运算符重载函数是具有特殊名字的函数,它的名字由operator和后⾯要定义的运算符共同构成。和其他函数⼀样,它也具有返回值、参数列表以及函数体;
  • 重载运算符函数的参数个数和该运算符作⽤的运算对象数量⼀样多。比如:⼀元运算符有⼀个参数,⼆元运算符有两个参数。⼆元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第⼆个参数;
  • 如果将重载运算符函数定义在类内部(因为成员变量受private限定,不好操作时可以选择放在类里,成为成员函数),则成为成员函数,那么它的第⼀个运算对象(参数)默认传给隐式的this指针。因此运算符重载函数作为成员函数时,参数会⽐运算对象少⼀个;
  • 不能通过连接语法中没有的符号来创建新的操作符:⽐如operator@;
  • .* : / : / sizeof / ?: / . 以上5个运算符不能重载;
  • 运算符重载的函数的参数⾄少有⼀个是类类型参数,也不能通过运算符重载改变原生内置类型运算符的含义,比如说加号不能运算符重载成减号的含义;
  • ⼀个类需要重载哪些运算符,是看哪些运算符重载后有意义,⽐如Date类重载operator-就有意义,但是重载operator+就没有意义;
  • 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致;
  • 重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,⽆法很好的区分。C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,⽅便区分;
  • 重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第⼀个形参位置,第⼀个形参位置是左侧运算对象,调⽤时就变成了对象<<cout,不符合使⽤习惯和可读性。重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对象。

代码呈现

cpp 复制代码
#include<iostream>
using namespace std;
//Java中指令不受物理位置的影响
//像定义可以出现在调用之后(不存在声明),import无论写在哪都对代码起作用
//但C++不同
//函数调用要先看见声明或定义、#include只对下方代码生效

class Date
{
public:
	void Print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}

	//构造函数
	Date()
	{
		_year = 2026;
		_month = 8;
		_day = 17;
	}

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

private:
	int _year;
	int _month;
	int _day;
};

//全局函数
bool operator== (Date& d1, Date& d2)
{
	return d1._year - d2._year;
	//我个人设置成只比较年份大小
}

int main()
{
	Date d1(2026, 8, 20);
	Date d2;
	
  //运算符重载函数的调用
  //1.
  operator==(d1,d2);
  //2.
  d1 == d2;
  //虽然写成了d1 == d2,但是编译器在编译阶段也会转换成operator==(d1,d2);对待
  
	if (d1 == d2)
	{
		cout << "1" << endl;
	}
	else {
		cout << "0" << endl;
	}
	return 0;
}

代码写出来了,但是还存在问题:

解决私有成员变量类外被访问的问题

有四种⽅法可以解决:

1、成员变量放公有;'

2、Date提供getxxx函数;

3、友元函数(暂时不讲);

4、重载为成员函数(最推荐)。

方法一好实现,直接把private换成public就行;

方法二代码实现:

cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:

		//构造函数
	Date()
	{
		_year = 2026;
		_month = 8;
		_day = 17;
	}

	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	
  int GetYear()
  {
    return _year;
    //return this._year;

private:
	int _year;
	int _month;
	int _day;
};

bool operator==(const Date& d1,const Date& d2)
{
   return d1.GetYear() - d2.GetYear();
}

int main()
{
  Date d1(2026.8.20);
  Date d2;

  operator==(d1,d2);
  d1 == d2;
  return 0;
}

方法四代码实现:

cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	//构造函数
	Date()
	{
		_year = 2026;
		_month = 8;
		_day = 17;
	}

	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	
  bool operator==(const Date& d2)
  {
    return _year == d2._year;
  }

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
  Date d1(2026.8.20);
  Date d2;
  
  d1.operator==(d2);
  //调用方式略有差异
  d1 == d2;
  //编译器会转换成 d1.operator==(d2);
  return 0;
}

由于成员函数第一个参数默认为类类型参数 const Date& this;故而全局定义的运算符重载函数的第一个参数会传给this,所以此时运算符重载函数是类的成员函数时,参数只需要写const Date& d2就好。

PS:

1.如果不小心同时在类内和全局处都定义了运算符重载函数,在调用时优先调用类里的,再调用全局处的。作用域不同,不构成函数重载,但会存在二义性吗?

2.函数重载和运算符重载函数中虽然都有重载,但是二者的含义并不相同。运算符重载:是指重新定义运算符的行为。而函数重载则是指函数名相同但是参数不同的函数构成了函数重载。

(2)赋值运算符重载函数

赋值运算符重载函数是默认成员函数(跟构造、析构、拷贝构造函数属于一类),它用于完成两个已经存在的对象直接的拷⻉赋值。这⾥要注意跟拷贝构造区分开来,拷贝构造⽤于⼀个对象拷⻉初始化给另⼀个要创建的对象。

1.赋值运算符函数特点

  1. 赋值运算符重载函数是运算符重载函数,规定必须重载为类的成员函数。赋值运算重载函数的参数建议写成const 当前类类型引⽤,传值传参会产生拷贝的代价**(赋值运算符重载函数可以传值传参,不会产生无穷递归)**;
  2. 赋值运算符函数有返回值,有返回值⽬的是为了⽀持连续赋值场景(语法上可以写void返回值,但是不推荐)。建议将返回值写成当前类类型引⽤,引⽤返回可以提⾼效率;
  3. 和构造、析构、拷贝构造函数一样,当没有显式实现赋值运算符重载函数时,编译器会⾃动⽣成⼀个默认赋值运算符重载。默认赋值运算符重载的⾏为跟默认拷⻉构造函数类似,对类中的内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对类中的⾃定义类型成员变量会调⽤它的赋值重载函数;
  4. 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的赋值运算符重载就可以完成需要的拷⻉,所以不需要我们显⽰实现赋值运算符重载。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器⾃动⽣成的赋值运算符重载完成的值拷⻉/浅拷⻉不符合我们的需求,所以需要我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。像MyQueue这样的类型内部主要是⾃定义类型Stack成员,编译器⾃动⽣成的赋值运算符重载会调⽤Stack的赋值运算符重载,也不需要我们显⽰实现MyQueue的赋值运算符重载。这⾥还有⼀个⼩技巧,如果⼀个类显⽰实现了析构并释放资源,那么他就需要显⽰写赋值运算符重载,否则就不需要。(和拷贝构造函数是一样的)。
a:不显示定义下的自定义类型中的自定义类型的函数调用:

构造函数:自定义类型中的自定义类型------>调用默认构造函数(自定义无参、全缺省或编译器自动生成的);

析构函数:自定义类型中的自定义类型------>调用默认析构函数(自定义或编译器自动生成的,析构函数没有参数);

拷贝构造函数:自定义类型中的自定义类型------>调用类内自定义类型自己的析构函数;

赋值运算符重载函数:自定义类型中的自定义类型------>调用类内自定义类型自己的赋值运算符重载函数。

说法各种各样,但是归根结底就是一条:不显示定义下的自定义类型中的自定义类型会主动调用当前参数列表相符的函数,如果没有参数相符的函数,就会报错。

b:为什么赋值重载可以传值传参,不会形成无穷递归?

定义一个传值传参的拷贝构造函数后,调用拷贝构造函数传自定义类型的值时会调用拷贝构造函数,而此时定义的拷贝构造函数本身就是传值传参的,所以又要调用拷贝构造函数......一直调用拷贝构造函数,这样下去就会产生无穷递归。

而定义传值传参的赋值运算符重载函数后,调用该传值传参赋值运算符重载函数传自定义类型的值时,会调用拷贝构造函数,由于此时的拷贝构造函数的参数是引用类型,故而不需要再次调用拷贝构造函数。拷贝构造函数结束后,会回到赋值运算符重载函数中。不会产生无穷递归。

2.代码呈现

cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	void Print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}

	//构造函数
	Date()
	{
		_year = 2026;
		_month = 8;
		_day = 17;
	}

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

	//赋值运算符重载函数
	Date& operator=(const Date& d1)
	{
		_year = d1._year;
		_month = d1._month;
		_day = d1._day;

		return *this;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2026, 8, 20);
	Date d2;
	//d2 = d1;
	//简化写法

  //连续赋值
  Date d3(2026,8,24);
  d1 = d2 = d3;

	cout << d1._year << "/" << d1._month << "/" << d1._day << endl;
	cout << d2._year << "/" << d2._month << "/" << d2._day << endl;

	return 0;
}
c:拷贝构造函数和赋值运算符重载函数的区分:

二者的区别在于:赋值运算符重载函数是将一个已经初始化好的对象的值赋给一个刚刚被创建的对象,而拷贝构造函数是将一个已经初始化好的对象的值赋给另一个已经初始化好的对象。

cpp 复制代码
int main()
{

  //赋值运算符重载函数
	Date d1(2026, 8, 20);
	Date d2;
	d2.operator=(d1);
	//d2 = d1;

  //拷贝构造函数
	Date d3(2007, 7, 31);
	Date d4(d3);
	//Date d4 = d3;

	cout << d1._year << "/" << d1._month << "/" << d1._day << endl;
	cout << d2._year << "/" << d2._month << "/" << d2._day << endl;

	cout << d3._year << "/" << d3._month << "/" << d3._day << endl;
	cout << d4._year << "/" << d4._month << "/" << d4._day << endl;


	return 0;
}
d:不妨再来思考一下:指针能够彻底代替引用吗?

拿赋值运算符重载函数的连续赋值和拷贝构造函数来看:

①赋值运算符重载函数连续赋值:
cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	void Print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}

	//构造函数

	Date()
	{
		_year = 2026;
		_month = 8;
		_day = 17;
	}

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

	////赋值运算符重载函数:返回值是引用
	//Date& operator=(const Date& d1)
	//{
	//	_year = d1._year;
	//	_month = d1._month;
	//	_day = d1._day;

	//	return *this;
	//}
	
	//赋值运算符重载函数:返回值是指针
	Date* operator=(const Date& d1)
	{
		_year = d1._year;
		_month = d1._month;
		_day = d1._day;

		return this;
		//返回值是指针,返回this
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2026, 8, 20);
	Date d2;
	Date d3(2007, 7, 31);

	d1 = *(d2 = d3);
	//赋值操作符的结合性是从右向左,d3是类实例化的对象,故而d2 = d3的返回值要解引用
	

	cout << d1._year << "/" << d1._month << "/" << d1._day << endl;
	cout << d2._year << "/" << d2._month << "/" << d2._day << endl;
	cout << d3._year << "/" << d3._month << "/" << d3._day << endl;

	return 0;
}
②拷贝构造函数:
cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	void Print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}

	//构造函数
	Date()
	{
		_year = 2026;
		_month = 8;
		_day = 17;
	}

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

	////拷贝构造函数:参数是引用
	//Date(const Date& d)
	//{
	//	_year = d._year;
	//	_month = d._month;
	//	_day = d._day;
	//}

   //拷贝构造函数:参数是指针
	Date(const Date* d)
	{
		_year = d->_year;
		_month = d->_month;
		_day = d->_day;
	}

//private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d3(2007, 7, 3);
	Date d4(&d3);
	//Date d4 = &d3;
	
	cout << d3._year << "/" << d3._month << "/" << d3._day << endl;
	cout << d4._year << "/" << d4._month << "/" << d4._day << endl;

	return 0;
}

单从解引用这一条就能看出传指针返回、传指针传参没有传引用返回、传指针传参方便。

需要注意:当拷贝构造函数传指针传参时,原本的拷贝构造函数就不是拷贝构造函数了,只是一个普通的构造函数。因为拷贝构造函数规定,第一个参数要是当前类类型的引用。

六.取地址运算符重载函数

(1)const成员函数

由const修饰的成员函数称之为const成员函数。const修饰成员函数放到成员函数参数列表的后

⾯。

const实际上修饰的是该成员函数隐含的this指针(类类型* const ),表明在该成员函数中不能对类中的任何成员变量进行修改。

①代码实现

cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:

  //const成员函数
	void Print() const
	{
		cout << _year << " " << _month << " " << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
}

②const成员函数存在的意义

先看一段代码:

cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:
	void Print() 
	{
		cout << _year << " " << _month << " " << _day << endl;
	}

	//构造函数
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	const Date d1(2026, 8, 25);
	d1.Print();

	Date d2(2007, 6, 18);
	d2.Print();

  return 0;
}

这段代码是正确的吗?

放进VS中编译运行,编译报错:

讲到这,不妨探讨一下:什么情况下涉及访问权限的变化?
cpp 复制代码
int main()
{
  //情况1:
  const int a1 = 10;
  int b1 = a1;
  
  //情况2:
  int a2 = 10;
  const int b2 = a2;

  //情况3:
  const int* p1 = (int*)malloc(8);
  int* pp1 = p1;
 
  //情况4:
  int* p2 = (int*)malloc(8);
  const int* pp2 = p2;
  
  //情况5:
  int* const s1 = (int*)malloc(8);
  int* ss1 = s1;

  //情况6:
  int* s2 = (int*)malloc(8);
  int* const ss2 = s2;

涉及访问权限放大缩小的前提是:整个过程中,都是同一变量空间。

只有当访问权限不变或者缩小时,编译才能通过。

想让实例化的const对象能够成功调用类中的成员函数,在成员函数的参数列表的括号外加上const就可以了(const本质上是在修饰this指针)。

正确代码:
cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
public:

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

	//构造函数
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	const Date d1(2026, 8, 25);
	d1.Print();

  //这⾥⾮const对象也可以调⽤const成员函数是⼀种权限的缩⼩
	Date d2(2007, 6, 18);
	d2.Print();
  
  return 0;
}

所以,const成员函数的意义是,使const对象可以正常调用类中的成员函数。

(2)取地址运算符重载函数

取地址运算符重载函数是类的默认成员函数,分为普通取地址运算符重载和const取地址运算符重载,⼀般这两个重载函数编译器⾃动成的就已经够我们⽤了,不需要去显⽰实现。除⾮⼀些很特殊的场景,⽐如我们不想让别⼈取到当前类对象的地址,就可以⾃⼰实现⼀份,胡乱返回⼀个地址。

除赋值运算符重载函数operator= 、普通取地址运算符重载函数 operator& 、const取地址运算符重载函数 operator& const 这三个,编译器会给默认版本。其余运算符( + 、 - 、 == 、 < 、 \[\] 、 () 等)编译器不会自动生成,想要使用就必须自己手写重载。

代码呈现:

cpp 复制代码
class Date
{

public :

  //普通对象取地址,指针类型:Date*
  Date* operator&()
 {
   return this;
 }

  //const对象取地址,指针类型:const Date*
  const Date* operator&()const
 {
   return this;
 }
 
private :
  int _year ;
  int _month ; 
  int _day ; 
};

不想让其他人获得对象的地址,就可以胡乱返回:

cpp 复制代码
class Date
{

public :

  //普通对象取地址,指针类型:Date*
  Date* operator&()
 {
   return nullptr;
   //return (Date*)0x12345678
 }

  //const对象取地址,指针类型:const Date*
  const Date* operator&()const
 {
   return nullptr;
 }
 
private :
  int _year ;
  int _month ; 
  int _day ; 
};

------end------

相关推荐
qz5zwangzihan11 小时前
AtCoder高频英文词汇短语
c++·atcoder·高频词
码匠许师傅2 小时前
【C++ 面试真题】聊聊 C++ 的类型特征( type_traits)
java·c++·面试
小小龙学IT2 小时前
C++ 性能优化:CPU 缓存与数据布局优化深度解析
c++·缓存·性能优化
玉树临风ives2 小时前
atcoder ABC 472 题解
数据结构·c++·算法
张张的快乐时光呀!2 小时前
MFC添加类向导并实现响应
c++·mfc
程与留3 小时前
09_Qt 布局管理系统——QHBoxLayout、QVBoxLayout、QGridLayout
c++·qt
神仙别闹3 小时前
基于QT(C++)实现旅游线路规划系统
c++·qt·旅游
郝学胜_神的一滴3 小时前
C++11 工程级应用 02:decltype与返回类型后置,让泛型代码告别类型玄学
c++·编程语言
tudousisi2224 小时前
8.25第二题训练
c++