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

news2024/12/29 2:27:59

目录

默认成员函数

构造函数

构造函数是什么

构造函数特征

什么是默认构造函数

注意事项

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

缺省值

对象如何传值给构造函数

初始化列表

析构函数

析构函数的特征

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

总结

拷贝构造函数

拷贝构造函数的使用场景

拷贝构造函数的特征

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

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

拷贝构造函数总结

赋值运算符重载

运算符重载

赋值运算符重载

前置++  与  后置++

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

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 天,

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1638550.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL数据库练习(13)

schooldb库——utf8字符集——utf8_general_ci排序规则 61. DDL CREATE TABLE settlements (settlementId int(11) NOT NULL AUTO_INCREMENT COMMENT 自增ID,settlementNo varchar(20) NOT NULL COMMENT 结算单号,settlementType tinyint(4) NOT NULL DEFAULT 0 COMMENT 结算…

金融案例:统一查询方案助力数据治理与分析应用更高效、更安全

随着企业数据规模的增长和业务多元化发展&#xff0c;海量数据实时、多维地灵活查询变成业务常见诉求。同时多套数据库系统成为常态&#xff0c;这既带来了数据管理的复杂性&#xff0c;又加大了数据使用的难度&#xff0c;面对日益复杂的数据环境和严格的数据安全要求&#xf…

Centos7 安装Git、使用

Centos7 安装Git 一、安装步骤1.1 查看版本1.2 卸载1.3 安装 二、创建仓库2.1 新增仓库2.2 新增配置项 三、管理文件3.1 文件创建3.2 文件修改、add、commit3.3 tree结构探索 四、分支4.1 创建分支&#xff1a;4.2 查看分支4.3 切换分支4.4 删除分支4.5 合并冲突 一、安装步骤 …

【蓝桥杯嵌入式】第七届省赛 - 模拟液位检测告警系统

代码开源&#xff0c;Gitee自取 代码开源&#xff0c;Gitee自取 代码开源&#xff0c;Gitee自取 目录 0 前言 1 展示 1.1 源码 1.2 演示视频 1.3 题目展示 2 工程配置 3 资源配置&代码实现 3.1 定时器 3.2 液位检测 3.3 液位阈值设定 3.4 液位阈值设定 3.5 串…

使用unreal engine5.3.2创建c++第一人称游戏

UE5系列文章目录 文章目录 UE5系列文章目录前言一、NuGet 简介二、解决方法&#xff1a; 前言 为了使用unreal engine5.3.2创建c第一人称游戏&#xff0c;今天安装了Visual Studio 2022专业版。在ue5中创建c工程&#xff0c;结果编译器报错&#xff1a; 严重性 代码 说明 项目…

UDP和TCP(传输层)

这里写目录标题 UDPUDP的基本特点UDP协议报文格式 TCPTCP协议报文格式TCP特点可靠传输实现机制确认应答超时重传数据丢了应答报文丢了 小结 UDP UDP的基本特点 无连接不可靠传输面向数据报全双工 UDP协议报文格式 2个字节有效范围(无符号): 0 ~ 65535(2^16 - 1). 2个字节有效范…

Web开发:<div>作用

< div >元素作用 解释用途1. 布局&#xff1a;2. 样式化&#xff1a;3. 结构化&#xff1a;4. JavaScript操作&#xff1a;5. 响应式设计&#xff1a;6. 模块化和重用&#xff1a;7. 包裹其他元素&#xff1a;8. 清除浮动&#xff1a; 总结举例示例结果分析 解释 在Web开…

全栈开发之路——前端篇(1)介绍、框架确定、ide设置与项目创建

文章目录 前言一、前端框架React和VueReactVue 二、编译器选择和配置1.传统的下载和使用2.你可能遇到的问题1.ERR&#xff01; syscall mkdir2.vue : File C:\nodejs\node_global\vue.ps1 cannot be loaded because running scripts is disabled on3.出现乱码问题 3.运行Vue 三…

模型智能体开发之metagpt-单智能体实践

需求分析 根据诉求完成函数代码的编写&#xff0c;并实现测试case&#xff0c;输出代码 代码实现 定义写代码的action action是动作的逻辑抽象&#xff0c;通过将预设的prompt传入llm&#xff0c;来获取输出&#xff0c;并对输出进行格式化 具体的实现如下 定义prompt模版 …

python算法题

需求 代码 class Solution:def searchInsert(self, nums: List[int], target: int) -> int:if max(nums) >target:for i in range(len(nums)-1):if nums[i1] > target and nums[i] <target:return i1if max(nums) <target:return len(nums)if min(nums) > …

解析transformer中的各模块结构

transformer是一种编解码&#xff08;encoder-decoer&#xff09;结构&#xff0c;用于自然语言处理、计算机视觉等领域&#xff0c;编解码结构是当前大模型必包含的部分。 文章目录 1. 词嵌入模块 2.位置编码模块 3. 多头注意力机制模块 3.1 自注意力机制模块 3.2 多头注…

Golang图片验证码的使用

一、背景 最近在使用到Golang进行原生开发&#xff0c;注册和登录页面都涉及到图片验证码的功能。找了下第三方库的一些实现&#xff0c;发现了这个库用得还是蛮多的。并且支持很多类型的验证方式&#xff0c;例如支持数字类型、字母类型、音频验证码、中文验证码等等。 项目地…

操作系统(2)——进程线程

目录 小程一言专栏链接: [link](http://t.csdnimg.cn/8MJA9)基础概念线程详解进程详解进程间通信调度常用调度算法 重要问题哲学家进餐问题问题的描述策略 读者-写者问题问题的描述两种情况策略 总结进程线程一句话 小程一言 本操作系统专栏&#xff0c;是小程在学操作系统的过…

Dockerfile实战(SSH、Systemctl、Nginx、Tomcat)

目录 一、构建SSH镜像 1.1 dockerfile文件内容 1.2 生成镜像 1.3 启动容器并修改root密码 二、构建Systemctl镜像 2.1 编辑dockerfile文件 ​编辑2.2 生成镜像 2.3 启动容器&#xff0c;并挂载宿主机目录挂载到容器中&#xff0c;然后进行初始化 2.4 进入容器验证 三、…

考研管理类联考(专业代码199)数学基础【2】整式与分式

一、整式及其运算 1.常用乘法公式&#xff08;逆运算就是因式分解&#xff09; 公式扩展① 公式扩展② 公式扩展③ 2.整式除法定理 若整式 F(x) 除以x-a的余式为r(x)&#xff0c;则 F(x) (x -a) g(x) r(x) &#xff0c;故r(a)F(a)成立 二、指数和对数的运算性质 1.指数运算…

【电路笔记】-石英晶体振荡器

石英晶体振荡器 文章目录 石英晶体振荡器1、概述2、石英晶体等效模型3、石英晶体振荡器示例14、Colpitts 石英晶体振荡器5、Pierce振荡器6、CMOS晶体振荡器7、微处理器水晶石英钟8、石英晶体振荡器示例21、概述 任何振荡器最重要的特性之一是其频率稳定性,或者换句话说,其在…

Linux migrate_type初步探索

1、基础知识 我们都知道Linux内存组织管理结构架构&#xff0c;顶层是struct pglist_data&#xff0c;然后再到struct zone&#xff0c;最后是struct page。大概的管理结构是这样的&#xff1a; 根据物理内存的地址范围可划分不同的zone&#xff0c;每个zone里的内存由buddy…

审计师能力与专长数据集(2014-2022年)

01、数据介绍 审计师是专门从事审计工作的人员&#xff0c;他们对企业、政府机关、金融机构等组织进行独立的、客观的、合法的审计&#xff0c;以评估这些组织的财务状况、经营绩效和风险水平。审计师通过收集和评估证据&#xff0c;以确定被审计单位的财务报表是否公允、合法…

【第3章】spring-mvc请求参数处理

文章目录 前言一、准备1. 增加mavan配置 二、简单参数1.JSP2.Controller 三、复杂参数1.JSP2.Controller 三、扩展1.JSP2.header3.cookie4.session 总结 前言 在上一章的基础上&#xff0c;我们来学习对于请求参数的解析&#xff0c;前后端分离已经是大势所趋&#xff0c;JSP相…

IOS上线操作

1、拥有苹果开发者账号 2、配置证书&#xff0c;进入苹果开发者官网&#xff08;https://developer.apple.com/&#xff09; 3、点击账户&#xff08;account&#xff09;&#xff0c;然后创建一个唯一的标识符 4、点击"Identifiers"&#xff0c;然后点击"&qu…