类和对象(中)

时间:2024-11-27 07:04:52

文章目录

  • 目录
    • 1. 类的6个默认成员函数
    • 2. 构造函数
    • 3. 析构函数
    • 4. 拷贝构造函数
    • 5. 赋值运算符重载
      • 5.1 运算符重载
      • 5.2 赋值运算符重载
      • 5.3 日期类实现
    • 6. const成员函数
    • 7. 取地址及const取地址操作符重载

目录

  • 类的6个默认成员函数
  • 构造函数
  • 析构函数
  • 拷贝构造函数
  • 赋值运算符重载
  • const成员函数
  • 取地址及const取地址操作符重载

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

默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

如果一个类中什么成员都没有,简称为空类。

空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。

6个默认成员函数

2. 构造函数

在写C语言代码时,大家可能会出现以下问题:

#include <iostream>
#include "Stack.h"

using namespace std;

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()
{
	//忘记调用Init,就使用
	Date d1;
	Date d2;

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

	Stack st1;
	st1.Push(1);
	st1.Push(2);
	st1.Push(3);

	return 0;
}

因此,C++中用构造函数来解决这个问题。

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象(我们常使用的局部对象是栈帧创建时,空间就开好了),而是对象实例化时初始化对象。构造函数的本质是要替代我们以前 Stack 和 Date 类中写的Init函数的功能,构造函数自动调用的特点就完美的替代了Init。

构造函数的特点:

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

#include <stdlib.h>

class Stack
{
public:
	Stack();
	void Push(int x);

private:
	int* _a;
	int _top;
	int _capacity;
};
//Stack.cpp

#include "Stack.h"

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

void Stack::Push(int x)
{
	//...
	_a[_top++] = x;
}
//Test.cpp

#include <iostream>
#include "Stack.h"

using namespace std;

class Date
{
public:
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}

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

private:
	int _year;//年
	int _month;//月
	int _day;//日
};

int main()
{
	Date d1;
	Date d2;

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

	Stack st1;
	st1.Push(1);
	st1.Push(2);
	st1.Push(3);

	return 0;
}
  1. 构造函数可以重载。(多个构造函数,有多种初始化方式)
#include <iostream>
#include "Stack.h"

using namespace std;

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();//err 无法跟函数声明区分开
	Date d1;
	d1.Print();

	Date d2(2024, 4, 2);
	d2.Print();

	Date d3(2024);
	d3.Print();

	Date d4(2024, 4);
	d4.Print();

	return 0;
}
  1. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
//Stack.h

#include <stdlib.h>

class Stack
{
public:
	Stack(int n = 4);
	void Push(int x);

private:
	int* _a;
	int _top;
	int _capacity;
};
//Stack.cpp

#include "Stack.h"

Stack::Stack(int n)
{
	_a = (int*)malloc(sizeof(int) * n);
	_top = 0;
	_capacity = n;
}
//Test.cpp

#include <iostream>
#include "Stack.h"

using namespace std;

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

private:
	int _a;
};

class Date
{
public:
	//我们没写,有没有构造函数?有->编译器自动生成
	//内置类型/基本类型 int/char/double.../指针
	//自定义类型        class/struct...
	//编译器自动生成的构造函数,对于内置类型成员变量,没有规定要不要做处理!(有些编译器会处理)
	//                          对于自定义类型成员变量才会调用这个成员变量所属的自定义类型的不传参就可以调用的那个构造(也就是无参数或者全缺省的构造函数)(如果你自己写了一个有参数的构造函数,会编译报错;如果我们没有自己写构造函数,就用自动生成的无参构造;写了无参/全缺省构造函数,就用自己写的,然后再用上面的规则判断要不要做处理)
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	int _year;//年
	int _month;//月
	int _day;//日

	A _aa;
};

//自动生成的构造函数意义何在?
//两个栈实现一个队列
class MyQueue
{
private:
	Stack _pushst;
	Stack _popst;
};

int main()
{
	Date d1;
	d1.Print();

	MyQueue q;

	return 0;
}
  1. 无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数,都叫做默认构造函数。但是这三个函数有且只有一个存在,不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认生成的那个叫默认构造,实际上无参构造函数、全缺省构造函数也是默认构造,总结一下就是不传实参就可以调用的构造就叫默认构造
  2. 我们不写,编译器默认生成的构造对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化(自定义成员变量不管是在编译器自动生成的构造函数,还是自己写的构造函数的情况下,都会调用它自己的默认构造函数)。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要用初始化列表才能解决,初始化列表,我们下个章节再细细讲解。

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


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

#include <iostream>

using namespace std;

class Time
{
public:

private:
	int _hour = 1;
	int _minute;
	int _second;
};

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

	//自定义类型调用默认构造函数
	Time _t;
};

int main()
{
	//Date d1(2024, 4, 9);
	//d1.Print();

	Date d2;
	d2.Print();

	return 0;
}
#include <iostream>

using namespace std;

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 d2(2024, 4, 9);
	d2.Print();//2024-4-9

	return 0;
}
#include <iostream>

using namespace std;

class Date
{
public:
	Date(int year, int month, int day)
	{

	}

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

private:
	//给缺省值
	int _year = 1;
	int _month = 1;
	int _day = 1;
};

int main()
{
	Date d2(2024, 4, 9);
	d2.Print();//1-1-1

	return 0;
}

总结:

  1. 一般情况下,构造函数都需要我们自己显示的去实现
  2. 只有少数情况下可以让编译器自动生成构造函数(类似MyQueue,成员全是自定义类型)

3. 析构函数

和上面一样,在写C语言代码时,可能会忘记销毁,因此,C++中用析构函数来解决这个问题。

析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的,函数结束栈帧销毁,它就释放了,不需要我们管,C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。析构函数的功能类比我们之前 Stack 实现的 Destroy 功能,而像 Date 没有Destroy,其实就是没有资源需要释放,所以严格说 Date 是不需要析构函数的。

析构函数的特点:

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数(这就说明析构函数不能重载)无返回值。 (这里跟构造类似,也不需要加void)
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
  4. 对象生命周期结束时,系统会自动调用析构函数。
#include <iostream>

using namespace std;

typedef int DataType;

class Stack
{
public:
	Stack(size_t capacity = 3)
	{
		cout << "Stack(size_t capacity = 3)" << endl;
		
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			
			return;
		}

		_capacity = capacity;
		_size = 0;
	}

	void Push(DataType data)
	{
		//CheckCapacity();
		_array[_size] = data;
		_size++;
	}

	//其他方法...

	~Stack()
	{
		cout << "~Stack()" << endl;

		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}

private:
	DataType* _array;
	int _capacity;
	int _size;
};

int main()
{
	Stack st;

	//析构可以显示调用,这样就类似于 Destroy 了两次
	st.~Stack();

	return 0;
}
  1. 跟构造函数类似,我们不写编译器自动生成的析构函数内置类型成员不做处理自定义类型成员会调用他的析构函数
  2. 还需要注意的是我们显示写析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数
  3. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,如Date;如果默认生成的析构就可以用,也就不需要显示写析构,如MyQueue;但是有资源申请时,一定要自己写析构,否则会造成资源泄漏,如Stack。
#include <iostream>

using namespace std;

typedef int DataType;

class Stack
{
public:
	Stack(size_t capacity = 3)
	{
		cout << "Stack(size_t capacity = 3)" << endl;
		
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			
			return;
		}

		_capacity = capacity;
		_size = 0;
	}

	void Push(DataType data)
	{
		//CheckCapacity();
		_array[_size] = data;
		_size++;
	}

	//其他方法...

	~Stack()
	{
		cout << "~Stack()" << endl;

		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}

private:
	DataType* _array;
	int _capacity;
	int _size;
};

class MyQueue
{
private:
	Stack _st1;
	Stack _st2;
	int _size = 0;
};

int main()
{
	MyQueue q;

	return 0;
}
  1. 一个局部域的多个对象,C++规定后定义的先析构

4. 拷贝构造函数

如果一个构造函数的第一个参数自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是一个特殊的构造函数。

拷贝构造的特点:

  1. 拷贝构造函数是构造函数的一个重载。
  2. C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参传值返回都会调用拷贝构造完成。
#include <iostream>

using namespace std;

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

	Date(Date& d)
	{
		cout << "Date(Date& d)" << endl;

		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

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

private:
	//给缺省值
	int _year = 1;
	int _month = 1;
	int _day = 1;
};

void func(Date d)
{
	d.Print();
}

int main()
{
	Date d2(2024, 4, 9);
	func(d2);

	return 0;
}
  1. 拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。 拷贝构造函数也可以多个参数,但是第一个参数必须是类类型对象的引用,后面的参数必须有缺省值
    自定义类型传值传参
#include <iostream>

using namespace std;

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

	Date(const Date& d)
	{
		//this->_year = d._year;
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

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

private:
	//给缺省值
	int _year = 1;
	int _month = 1;
	int _day = 1;
};

int main()