C++ | 类和对象(中) (构造函数 | 析构函数 | 拷贝构造函数 | 赋值运算符重载 | 取地址 | const取地址)

目录

默认成员函数

构造函数

构造函数是什么

构造函数特征

什么是默认构造函数

注意事项

编译器自动生成的默认构造

缺省值

对象如何传值给构造函数

初始化列表

析构函数

析构函数的特征

编译器默认生成的析构函数

总结

拷贝构造函数

拷贝构造函数的使用场景

拷贝构造函数的特征

参数不为引用引发无穷递归讲解

编译器默认生成的拷贝构造

拷贝构造函数总结

赋值运算符重载

运算符重载

赋值运算符重载

前置++  与  后置++

其他运算符重载(+、-、+=、-=、++、--......)

const成员

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

结语


默认成员函数

我们写完了类之后,其实编译器里面会有六个自动生成的函数:

  • 构造函数
  • 析构函数
  • 拷贝构造函数
  • 赋值重载
  • 取地址(不重要)
  • const取地址(不重要)

为什么叫默认成员函数,这是因为:即使我们不显示写出这六个函数,编译器也能为我们自动生成

class Date
{};

如上是一个空类,但是这里面一定什么都没有吗?

当然不是,我们在上面说了,如果我们没有显示写出这6个函数的话,编译器会帮我们自动生成

所以,这个类里面有六个默认生成的函数

构造函数

构造函数是什么

首先,什么是构造函数?

这和我们在C语言中的的初始化很像

我们在数据结构相关学习时有学到栈相关的,我们如果用C语言写的话,就得单独写一个初始化函数,并且在main函数里调用该初始化函数,否则轻则随机值,重则程序崩溃

typedef struct Stack
{
	int* a;
	int capacity;
	int top;
}ST;

void StackInit(ST* plist)
{
	int* tmp = (int*)malloc(sizeof(int) * 4);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}
	plist->a = tmp;
	plist->capacity = 0;
	plist->top = 0;
}

int main()
{
	ST s1;
	StackInit(&s1);
	return 0;
}

如上是我们用C语言实现的栈(初始化)

试想一下:如果我们没有初始化就直接push数据进去,那么程序不就直接崩溃了

但是,C++中的构造函数不一样

如果我们没有显示写一个,编译器会帮我们写一个

如果我们显示写了一个,那么编译器就会自动调用我们写的默认构造函数

如下我们写一个C++版本的栈来看看:

class Stack
{
public:
	Stack()
	{
		_a = (int*)malloc(sizeof(int) * 4);
		_capacity = 0;
		_top = 0;
	}
private:
	int* _a;
	int _capacity;
	int _top;
};

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

两相比较之下,C++版本的明显会简单很多

构造函数特征

  1. 函数名与类名相同
  2. 无返回值
  3. 对象在main函数中被创建出来的时候,编译器自动调用默认构造函数
  4. 构造函数可以重载

我们一条一条讲起:

首先是函数名与类名相同,也就是说:我们创建的类的名字是什么,我们直接将其拿下来就可以作为构造函数的名字了

而且,无返回值,如下:

class Date
{
public:
	Date()//构造函数
	{
		;
	}
private:
	int _year;
};

如上,我们写出来一个无参的构造函数

我们再往下看构造函数的特征,说构造函数是支持重载的

也就是说,我们可以显示写多个构造函数,只要符合函数重载的规则即可

如下:

class Date
{
public:
	Date()
	{
		cout << "构成函数重载" << endl;
	}
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

我们写了一个无参,一个全缺省,这样子是能编译过的,因为构成了函数重载

另外,我们再main函数中如果将对象实例化了,那么如果你显示写的构造函数是默认构造的话,那么编译器就会自动调用你的构造函数

什么是默认构造函数

默认构造函数包括:

  • 编译器自动生成的构造函数
  • 全缺省的构造函数
  • 无参的构造函数

综上,我们可以得出一个结论:无需传参的就是默认构造函数

注意事项

注意,我们没有写构造函数的情况下,编译器会自动生成一个构造函数

damn是如果我们自己显示写了的话,那么编译器就不会生成默认构造函数

但是编译器就只会自动调用默认构造函数

如果我们没有将自己显示写出来的构造函数写成默认构造函数的话

那么编译器就不会自动调用我们写的构造函数

如下代码:

class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date s1;
	return 0;
}

我们在类里面显示写了一个构造函数,所以编译器就不会自动生成一个默认构造函数

但是我们自己显示写的又不是默认构造函数,所以编译器就不会自动调用我们写的构造

另外,我们再来看一种情况:

class Date
{
public:
	Date()
	{
		;//空类
	}
	Date(int year=2024, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

这也是一个老生常谈的问题了

两个默认构造函数,一个无参,一个全缺省,两个函数在语法上形成函数重载

但是这只是语法上,如果我们编译一下的话,那么就会报错

因为两个都是无参,编译器不知道要调用哪一个,所以就会报错

编译器自动生成的默认构造

我们上文一直在说:如果我们不显示写构造函数的话,那么编译器就会自动生成一个,默认构造函数

但是,编译器生成的这个默认构造函数会干什么呢?我们来试试看

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

int main()
{
	Date s1;
	s1.Print();
	return 0;
}

我们会看到,编译器自动生成的默认构造

什么都没干

这就很迷,如果上面都没干的话,要你何用?

根据C++的规矩,编译器自动生成的默认构造函数会将类里面的类型分为内置类型自定义类型

面对类里面的内置类型(int、char等),默认构造函数不做处理

面对类里面的自定义类型(类里面包着一个类),默认构造函数会调用自定义类型自己定义的默认构造函数

缺省值

由于编译器自动生成的默认构造函数,对内置类型直接是不做处理,为此本贾尼博士也是挨了不少骂,所以呢在C++11专门为这个东西打了一个补丁,这个补丁就是叫做——缺省值

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

我们加完了缺省值了之后,相当于先给内置类型设置了一个默认值,之后如果有显示写了构造并传参的话,就会覆盖缺省值

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 = 1;
	int _month = 1;
	int _day = 1;
};

int main()
{
	Date s1(2024, 1, 16);
	s1.Print();
	return 0;
}

我们能看到,是先走到缺省参数部分进行赋值

最后再将传过来的参数覆盖在上面

对象如何传值给构造函数

在C++的学习中,我们在main函数中将对象实例化了之后,我们应该如何传参呢?如下:

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 s1(2024, 1, 16);
	s1.Print();
	return 0;
}

如上我们会看到,我们是            类名  对象(参数);

对于刚接触的人来说,确实很奇怪

如果我们不传参呢?按上面的程序来的话,我们就是Date s1();

我们再定睛一看,发现这和函数声明长得好像,如果我是返回值为Date,函数名为s1,无参呢?

那就分不清楚了呀,所以如果不传参的话,那就括号也不写了直接

而我们这样子写,其实也有隐含的this指针在里面,this指针指向的是s1的地址

所以上面的Print也可以写成:

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

初始化列表

初始化列表是构造函数的一部分,初始化列表如下:

class Date
{
public:
	Date(int year = 2024, int month = 1, int day = 30)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
	void Print()
	{
		cout << _year << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

以冒号开始,以逗号分割,内置类型后面跟个括号,括号里面放表达式或值

为什么会有初始化列表的出现呢?

这是因为我们会遇到一个类里面包含着另一个类,但是这个被包含在内的类并没有默认构造函数,这时候,我们就必须要用初始化列表对其进行初始化

class Time
{
public:
	Time(int b)
	{
		_hour = b;
	}
private:
	int _hour;
};

class Date
{
public:
	Date(int year = 2024, int month = 1, int day = 30)
		:_year(year)
		,_month(month)
		,_day(day)
		,a(1231)
	{}
	void Print()
	{
		cout << _year << endl;
	}
private:
	int _year;
	int _month;
	int _day;
	Time a;
};

int main()
{
	Date s1;
	return 0;
}

如上,我们Date类里面包含了一个Time类,但是Time没有默认构造函数,所以我们就在初始化列表里面将其初始化

另外,初始化列表支持很多构造

你可以将这里理解成是成员定义的地方

  • private可以当成声明的地方,初始化列表可以当成是定义的地方
  • const、引用、没有默认构造的函数都可以在初始化列表处初始化
  • 以后尽量使用初始化列表
  • 初始化列表的初始化顺序是声明顺序

前面三点是概念,需要记住,无需讲解

但是最后一个可以讲一讲,我们来看一段代码:

class Date
{
public:
	Date()
		:_month(20)
		,_year(_month)
	{}
private:
	int _year;
	int _month;
};

int main()
{
	Date s1;
	return 0;
}

很多人会想,这段代码是先将_month初始化,随后将_month的值传给_year

但是我们会看到,_year并没有被初始化,这是因为初始化列表上的顺序是不作数的,初始化顺序是根据private里面定义变量的顺序来的

所以我们先初始化的_year,但是没能初始化到,后初始化_month时由于是数字,所以初始化到了

析构函数

析构函数的特征

在C++中,析构函数和构造函数是配对的,一个负责初始化,一个负责资源清理

为什么说是资源清理呢?因为析构函数并不会销毁对象

同时,我们的析构函数会在对象生命周期结束的时候自动调用

与构造函数类似,析构函数也有相似的规则:

  • 析构函数的函数名是在类名前加上~
  • 无参无返回值
  • 没有显示写的时候编译器会自动生成一个默认的析构函数,不支持重载
  • 自动调用

我们来写一个析构函数看看:

class stack
{
public:
	stack(int* a = nullptr)
		:_a((int*)malloc(sizeof(int)*4))
		,_capacity(0)
		,_top(0)
	{
		cout << "stack" << endl;
	}

	~stack()
	{
		free(_a);
		_a = nullptr;
		cout << "~stack" << endl;
	}

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

int main()
{
	stack s1;
	return 0;
}

我们写了一个栈,然后分别写了栈的构造与析构函数,并且我们各自在构造和析构里面做了打印处理,如果我们对其进行了调用的话,那我们就能在黑框框上看到打印的结果

这个栈我们使用了malloc在堆上开辟了空间,所以我们在析构函数上就相应地写上了free

编译器默认生成的析构函数

既然编译器会默认生成析构函数,那么这个析构函数会干什么呢?

这个析构函数和构造函数一样:

  • 对内置类型不做处理
  • 对自定义类型会自动调用他的析构函数

我们来看一段代码:

class Time
{
public:
	Time()
		:c(0)
	{}
	~Time()
	{
		cout << "Time" << endl;
	}
private:
	int c;
};

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

int main()
{
	Date s1;
	return 0;
}

可以看到,我们创建了两个类:Time和Date

我们并没有显式调用析构函数,但是我们创建了Date对象s1,当其生命周期结束时会自动调用析构函数,而我们在Date类里面并没有显示写析构,所以对内置类型不做处理,对自定义类型会去调用其析构函数,所以我们会调用Time类的析构

然后我们在Time的内部只做了打印处理,所以如果我们上述属实的话,那么屏幕上会打印出一个Time

总结

我们的内置类型其实也并不需要析构处理,因为当程序结束的时候,内置类型会自动销毁(存在栈上)

所以只有一种情况需要显示写析构:当有资源需要清理的时候,如stack、Queue等比如在堆上开了空间

如下两种情况是不需要显示写析构的:

  1. 只有内置类型的类,如:Date(由于全是内置类型,所以在程序结束的时候会自动销毁)、
  2. 类中无资源需要清理,其余类成员都有自己的析构函数

拷贝构造函数

拷贝构造函数的使用场景

当我们实例化了一个对象之后,如果此时我们需要再创建一个一摸一样的变量的话,那么我们再去做一遍一样的初始化就会显得比较冗余,因此就有了拷贝构造函数

举个例子:

class Date
{
public:
	Date(int year=1,int month=1,int day=1)
		:_year(year)
		,_month(month)
		,_day(day)
	{}

	Date(const Date& d1)//拷贝构造函数
	{
		_year = d1._year;
		_month = d1._month;
		_day = d1._day;
	}
	void Print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date s1(2024, 1, 16);
	Date s2 (s1);//拷贝构造使用场景
	s1.Print();
	s2.Print();
	return 0;
}

我们会看到,在一个Date的类里面我们创建了一个构造函数和一个拷贝构造函数(后面细讲,此处举个例子而已)

而在main函数里面我们先实例化了一个对象s1,而后我们又想要实例化一个一摸一样的对象s2,于是我们就使用了拷贝构造函数

拷贝构造函数的特征

同为默认构造函数,拷贝构造和我们上面讲过的构造函数和析构函数有诸多相似之处:

  • 拷贝构造函数是构造函数的一个重载形式
  • 拷贝构造函数有且仅有一个参数,且该参数必须为引用,否则会引发无穷递归导致程序崩溃
  • 未显式定义时,编译器会自动生成一个拷贝构造函数,该拷贝构造函数会对对象进行浅拷贝(值拷贝)
  • 默认生成的拷贝构造函数会对内置类型进行一个字节一个字节地拷贝,对自定义类型会调用他们的拷贝构造函数

首先我们来讲一讲为什么拷贝构造函数的参数必须加上const

加上了const意味着被修饰的值不可修改,试想一下,我们将作业借给别人抄,但是第二天的时候老师把你叫到办公室问你为什么全是错的

你一看,发现找你借作业的那个人不仅抄了你的作业,还把你的给改了

所以,为了不被更改,我们需要将拷贝构造函数的参数加上const

Date s2(s1);

各位且看,我们在main函数中显式调用拷贝构造函数的时候是这样子调用的

但是我们拷贝构造函数的参数就只有一个,这意味着这里面会有一个隐含的this指针

我们再在拷贝构造函数里面显示写一下this指针:

Date(const Date& d1)//拷贝构造函数
{
	this->_year = d1._year;
	this->_month = d1._month;
	this->_day = d1._day;
}

Date(const Date& d1)//拷贝构造函数
{
	_year = d1._year;
	_month = d1._month;
	_day = d1._day;
}

如上代码,不难看出来这个this指针储存的是s2的地址,而我们传过去的是对象s1,然后拿引用接收

由上述特征可知,拷贝构造函数是构造函数的一种重载显示,这就意味着拷贝构造函数也是和构造函数一样,类名作为参数名的

参数不为引用引发无穷递归讲解

在讲解之前,我们需要知道什么情况下会调用拷贝构造函数——自定义类型传值传参的时候

比如我们之前在C语言阶段学过的函数传址与传值:

int Swap(int a, int b)
{
	int tmp = a;
	a = b;
	b = tmp;
}

int Swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

如上,作为值直接传给函数即为传值

而我们在C++中如果直接将对象作为值的话,那么就会引发拷贝构造

Date(int year = 1,int month = 2)
	:_year(year)
	,_month(month)
{}

上面这种情况就不会引发拷贝构造,因为我们并没有将对象作为参数传过去

class Date
{
public:
	Date(int year = 1,int month = 2)
		:_year(year)
		,_month(month)
	{}
	void Print(const Date& d1)
	{
		;
	}
private:
	int _year;
	int _month;
};

int main()
{
	Date s1;
	Date s2;
	s1.Print(s2);
	return 0;
}

但是如果是如上代码,我们现在实例化了两个对象s1和s2,这时我们将s2作为参数传过去的时候,就会引发拷贝构造

综上,我们来看一看拷贝构造函数参数不为引用的情况:

Date(const Date s1)
{
	_year = s1._year;
	_month = s1._month;
}

为了避免这两种情况,我们有两种方法可以防止拷贝构造:

  • 指针
  • 引用(传的不是值,而是对象的地址)

虽然两种都可以,但是为了防止拷贝构造而写指针,又要取地址,又要这个那个,而且还面临私有的问题,而引用的话在拷贝构造函数的位置加上即可

两相比较之下,我们的引用自然更优

编译器默认生成的拷贝构造

学习了前两个默认构造函数之后,我们发现前两个默认构造(构造与析构)对内置类型都不做处理,对自定义类型会调用那个自定义类型自己的构造或析构函数

但是拷贝构造函数却不一样

我们来看一段代码:

class Date
{
public:
	Date(int year = 1, int month = 1)
		:_year(year)
		,_month(month)
	{}
	void Print()
	{
		cout << _year << " " << _month << " " << endl;
	}
private:
	int _year;
	int _month;
};

int main()
{
	Date s1(2024, 1);
	Date s2(s1);
	s1.Print();
	s2.Print();
	return 0;
}

可以看到,我们在上述代码中并没有显示实现拷贝构造函数,所以编译器会自动生成一个拷贝构造函数

而当我们打印结果时:

我们会发现,编译器默认生成的拷贝构造函数是干事的

但其实,编译器完成的是值拷贝,也叫浅拷贝

为什么叫做浅拷贝呢?这是因为编译器默认生成的拷贝构造函数是通过字节一个一个拷贝的

我们再来看一段代码:

class stack
{
public:
	stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			return;
		}
		_size = 0;
		_capacity = capacity;
	}
	void Push(const int& data)
	{
		_a[_size] = data;
		_size++;
	}
	~stack()
	{
		if (_a)
		{
			free(_a);
			_a = nullptr;
			_size = 0;
			_capacity = 0;
		}
	}
private:
	int* _a;
	int _size;
	int _capacity;
};

int main()
{
	stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	stack s2(s1);
	return 0;
}

如上代码,我们建立了一个栈的类,然后在main函数中将对象实例化后Push了4个数据进去

我们并没有写拷贝构造函数,但是我们却在main函数中使用了拷贝  stack s2(s1);

这时候程序就会崩溃:

这是由于我们的栈是在堆上开辟了数据,而当我们使用了编译器默认生成的拷贝构造函数的时候,由于是浅拷贝,所以并不会在堆上自主开辟出一块空间

我们栈这个类有三个元素,一个指向堆空间的指针,两个整形变量

两个整形变量编译器默认生成的拷贝构造函数能完成拷贝,这没问题

但是指针变量呢?他是指向堆空间的,如果直接拷贝的话,那就相当于是两个指针一起指向同一块空间

程序接着往下走会怎么样

当两个对象的生命周期都结束了的时候,假设s1先结束了,然后s1就会调用一次析构函数

第一次析构用了free,释放了堆空间,那第二次呢?

当第二个类生命周期结束的时候,又调用了一次析构函数,这时堆空间已经被释放掉了,所以程序就会崩溃

综上,编译器默认生成的拷贝构造函数并不是全能的,他只能值拷贝,面对没有开辟空间如Date这种类却是可以用,但是如果是像stack,Queue这种,就只能自己手写了

接下来给大家写一个stack的拷贝构造函数:

stack(const stack& s1)
{
	_a = (int*)malloc(sizeof(int) * s1._capacity);
	if (_a == nullptr)
	{
		perror("malloc fail");
		return;
	}
	memcpy(_a, s1._a, sizeof(int) * s1._size);
	_capacity = s1._capacity;
	_size = s1._size;
}

我们能看到,是能拷贝成功的

拷贝构造函数总结

  1. 如果类中全是内置类型,没有比如在堆上开空间什么的,那么用默认生成的拷贝构造函数即可
  2. 如果类中全是自定义成员,也无需显示写拷贝构造函数,因为默认生成的默认构造会去调用自定义成员自己的拷贝构造
  3. 一般情况下,不需要显示写析构函数的,就不需要显示写拷贝构造函数

赋值运算符重载

运算符重载

我们来想一个问题,假如你现在有女朋友了,有一天,她突然问你:

今天,是我们在一起的第多少天?

这是有可能会出现的情况,前提是你有女朋友了

那么如果这时有人想用一下变成来看一下的话,那就跑不了要写一个函数,然后实现一下日期相减

我们先来一个简单一点的,就判断一下日期大小

bool Compare(const Date& s1, const Date& s2)
{
	if (s1._year > s2._year)
		return true;
	else if (s1._year == s2._year)
	{
		if (s1._month > s2._month)
			return true;
		else if (s1._month == s2._month)
		{
			if (s1._day > s2._day)
				return true;
		}
	}
	return false;
}

能看到,我们是依次比较的年、月、日,最终得出结果的

但是我现在大于写完了,那我现在像写一个小于呢?

那我是不是应该将Compare这个名字换一换啊,可能换成Compare1,然后小于是Compare2

如果还有一个等于的话,就再起一个Compare3的名字,很挫

再者,我们如果要比较的话,我们还得调用函数,得出结果之后放在另一个变量上,然后才去比较的大小

我们就不能像内置类型一样直接比较吗?

cout<<(2>1)<<endl;

Date s1;
Date s2;
cout<<(s1>s2)<<endl;

在C++里面,有这种东西,其名赋值运算符重载,这里面又分了运算符重载和赋值运算符重载

C++中引进了一个关键字operator

当我们在关键字后面加上运算符号的时候,我们再将里面的逻辑实现一下,这样就能做到自定义类型像内置类型一样直接比较、加减......

而赋值运算符重载有如下几个特征:

  1. 不能连接其他奇奇怪怪的符号形成新的符号,如operator@
  2. 重载类型中必须有一个为类的类型的参数
  3. 定义的运算符不能曲解含义,如你写了一个加,但在里面实现的是减法的逻辑
  4. .*      ::       sizeof       ?:       .     这几个运算符不能重载
  5. 作为类成员函数的时候,参数会少一个,因为有this指针的存在

我们来重点讲一下最后一个:

我们如果将其显示调用的话,是这样的:

operator>(s1, s2);

只不过我们一般情况下都不喜欢这样子写,我们都是直接像内置类型一样去比较的

s1 > s2;

但是我们在类里面显示实现的时候,会发现我们只有一个参数,我们拿一个大于来举例子:

bool operator<(const Date& d)
{
	if (_year < d._year)
		return true;
	else if (_year == d._year)
	{
		if (_month < d._month)
			return true;
		else if (_month == d._month)
		{
			if (_day < d._day)
				return true;
		}
	}
	
	return false;
}

你会发现,我们只传了一个参数过去,这是因为有this指针的存在,所以我们只需要传一个参数即可

如上,假如我们现在在main函数内部调用的是:  s1 > s2

那么我们this指针指向的就是s1

所以我们只需要传一个参数即可

而我们如果要实现>,==......究其本质,都是运算符重载,不同的只是其中的内核而已,这里就做个小小的演示

上面我们已经实现了<,接下来,>先不着急,我们先把==实现一下:

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

既然我们有了小于和等于,那么大于是不是既不是小于也不是等于的情况啊,我们可以这么写:

bool operator>(const Date& d)
{
	return !(*this < d && *this == d);
}

其他的都大体是这个思路,这里我们就不再过多演示了

赋值运算符重载

赋值运算符重载的使用场景是:当我们有了一个自定类型的对象的时候,我们可以将另一个赋值给他,如下:

int main()
{
	Date s1(2024, 1, 16);
	Date s2(2023, 11, 18);
	Date s3 = s1;
	s3 = s2;
	return 0;
}

我们可以看到,此时s3是已经存在的,然后我们在其实例化出来了之后,我们再将s2的值赋给s3

赋值运算符重载的格式如下:

  1. 参数类型为const 类型&,因为我们的参数如果没有传引用的话,那么自定义类型传值传参会调用拷贝构造,白白多拷贝一次会影响效率
  2. 返回值类型为&,若为void则无法支持连续赋值,如不为&则会白白调用拷贝构造函数,影响效率
  3. 返回值为*this

对于第一点,我们先来看一段代码:

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;
		cout << "const Date& d" << endl;
	}

    

	void operator=(const Date d)//参数不为引用
	{//返回类型为void而非Date、Date&
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}


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

int main()
{
	Date s1(2024, 1, 16);
	Date s2(2023, 11, 18);
	Date s3 = s1;//此处会调用一次拷贝构造
	s3 = s1;
	return 0;
}

当我们将参数中的类型改为Date&时:

void operator=(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

但是我们如上这种写法,只能支持我们进行一次赋值

s1 = s2 = s3;

像如上这种连续赋值,如果无返回值的话,就不支持

假如我们现在的返回值是Date类型的话,我们再来跑一遍程序:

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

你会发现又调用了拷贝构造,效率会受到影响,所以我们需要将返回值改为引用:

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

这样子的时候,就能最大程度上提升效率

至于返回值为何为this指针,试想一下,如果是内置类型连续赋值:

int n = 3;
int m, i;
i = m = n;

如上,我们将整形n给定义了出来,然后我们又进行了一次连续赋值

而我们应该是先将n赋值给m,然后将赋值好的m返回,场上就相当于只剩下i = m

这时再将m赋给i

而我们在赋值运算符重载中,我们看似只传了一个参数,其实还有一个隐含的this指针,这时我们的this指针指向的那个值就是我们要返回的值

另外还有一点,即赋值运算符重载不能重载成全局的,如果重载成全局的,那么就需要两个参数,因为没有this指针

另外,还会面临私有和公有的问题

最后就是,如果全局和类里面同时定义了的话,那么我们实例化出来的对象只会调用类里面的那个,全局的那个写了也是白写

如果我们没有显示写赋值运算符重载,那么编译器就会自动生成一个

编译器默认生成的赋值运算符重载,和拷贝构造是一样的,只会进行浅拷贝,如果有在堆上开辟空间的话,那么就会有大问题

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 s1(2024, 10, 28);
	Date s2;
	s2 = s1;
	s1.Print();
	s2.Print();
	return 0;
}

前置++  与  后置++

前置++与后置++都是一元操作符,我们只需要对其进行一个++即可

想法很美好,但是我们写出来那?

operator++

由于规定了符号必须在关键字operator的后面,所以我们只能写出如上的形式

但是我们没有办法区分前置与后置,祖师爷本贾尼博士也察觉到了这个问题,所以:

前置++就正常写,如下:

//前置++
Date& operator++()
{
    _day++;
    return *this;
}

而后置++就在传参数的地方多写上一个int,如下:

//后置++
Date operator++(int)
{
    Date tmp(*this);
    _day++;
    return tmp;
}

这里面的int只起一个标识作用,你在里面放的值为多少都没有用

另外,这里不传参的原因是隐含的this指针,所以我们直接对_day++即可(这里偷了个懒,没有对日期和年月进行判满,这里只作演示)

前置与后置最大的区别就在于:

一个先++后使用,一个先使用后++

为此我们面对前置++还好,我们直接将*this传回去即可,但是面对后置++,我们就需要先拿拷贝构造函数构造出一个一摸一样的函数,然后将这个函数返回,只对*this进行++操作

但是为什么我上面,前置的返回值是Date&,但是后置却是Date

这是因为我们前置++时,返回的是this指针,并不是临时创建的

但是我们后置++时,创建了一个临时的类,这个类在除了作用域之后生命周期就结束然后自动销毁了

这就好比C语言指针章节中的野指针,那个临时变量已经被销毁了,但是我们还存着他的地址

所以我们不能使用引用,只能让程序调用拷贝构造,然后将拷贝后的结果再赋值给我们赋值的变量

其他运算符重载(+、-、+=、-=、++、--......)

这个不是本章节最重点的内容,这些知识点会放到下一篇博客中去详细讲解

如果有需要的,可以看一看链接里面的代码,里面是关于日期类实现的全代码

日期类实现-gitee

const成员

如上我们在讲解operator<中,我们只是将参数的类型设置为const,这代表着参数不可被修改

但是可以这么理解,我们相当于是传了两个对象上去,只不过其中过一个为隐含的this指针而已

所以同样的,this指针指向的内容在大多数时候都不可被修改

但是this指针是隐含的,所以我们也没办法显示地将其用const修饰的

老本也是察觉到了这个问题啊,所以就想了这么个办法:

在函数后面加const,相当于是给this指针加const

我们来看一看下面这一段代码就知道了:

bool Date::operator<(const Date& d) const
{
	if (_year < d._year)
		return true;
	else if (_year == d._year)
	{
		if (_month < d._month)
			return true;
		else if (_month == d._month)
		{
			if (_day < d._day)
				return true;
		}
	}
	return false;
}

但是这只是针对this指向的对象不能修改的情况,如果this指向的对象需要修改如前置,那么我们就不能使用const修饰this指针

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

取地址和const取地址就是如下这两个哥:

A* operator&()
{
	return this;
}

const A* operator&()const
{
	return this;
}

虽然说运算符重载大多需要自己实现,因为编译器并不会默认生成

但是这两个哥不一样,这俩是默认成员函数,也就是即使你不写,编译器也会默认生成

我们来看一下我们实现的取地址与const取地址:

class A
{
public:
	A(int a = 1)
		:_a(a)
	{}
	
	A* operator&()
	{
		cout << "A* operator&()" << endl;
		return this;
	}

	const A* operator&()const
	{
		cout << "const A* operator&()const" << endl;
		return this;
	}
private:
	int _a;
};

int main()
{
	A s1;
	const A s2;
	cout << &s1 << endl;
	cout << &s2 << endl;
	return 0;
}

事实上,这两个默认成员函数并不需要我们自己实现,编译器默认生成的已经够用了,如下:

class A
{
public:
	A(int a = 1)
		:_a(a)
	{}
private:
	int _a;
};

int main()
{
	A s1;
	const A s2;
	cout << &s1 << endl;
	cout << &s2 << endl;
	return 0;
}

结语

今天这篇博客讲的是类和对象中的 6 个默认成员函数

构造、析构、拷贝构造、赋值运算符重载、取地址、const取地址

至于其他运算符重载如+、-、>=、<=、!= ......这些我们会放到下一篇博客中,用讲解日期类的方式对相关知识点详细讲解

另外,我在上面运算符重载的开头部分提到了一个问题:

今天,是我们在一起的第多少天?

不知道你会不会看这篇博客的结语,这是我们在一起的第 206 天

不出意外的话,以后也会是 206 天,

  • 22
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值