类和对象(中)

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

如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员
函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。


2. 构造函数

构造函数是一个特殊的函数,它没有返回值(不用写),函数名就是类名,它是用来初始化成员变量的;只在对象创建的时候自动调用一次;

在使用栈,顺序表的时候,我们每次都要先初始化成员变量;如果不初始化就去调用增删查改就会让程序崩溃掉,为了防止这种情况的发生,c++增加了一个新的函数构造函数;

以下是用构造函数完成栈的初始化:

typedef int DataType;
class Stack {
public:
	Stack(int Capacity = 4);//构造函数
private:
	DataType* _Arrey;
	int _top;
	int _Capacity;
};

Stack::Stack(int Capacity) {
	cout << "Stack(int Capacity)" << endl;
	DataType* array = (DataType*)malloc(sizeof(DataType) * Capacity);
	if (array == NULL) {
		perror("malloc fail");
		return;
	}
	_Arrey = array;
	_top = 0;
	_Capacity = Capacity;
}

构造函数的特性

1.构造函数的函数名就是类名

2.构造函数不需要返回值(不用写)

3.构造函数只在对象实例化的时候自动调用一次

4.构造函数支持重载

1.无参构造函数的调用
typedef int DataType;
class Stack {
public:
	Stack(int Capacity = 4);//构造函数
private:
	DataType* _Arrey;
	int _top;
	int _Capacity;
};

Stack::Stack(int Capacity) {
	cout << "Stack(int Capacity)" << endl;
	DataType* array = (DataType*)malloc(sizeof(DataType) * Capacity);
	if (array == NULL) {
		perror("malloc fail");
		return;
	}
	_Arrey = array;
	_top = 0;
	_Capacity = Capacity;
}

int main() {

	//对象的实例化
	Stack st1;//无参构造函数调用

	return 0;
}
2.有参构造函数的调用
typedef int DataType;
class Stack {
public:
	Stack(int Capacity);//构造函数
private:
	DataType* _Arrey;
	int _top;
	int _Capacity;
};

Stack::Stack(int Capacity) {
	cout << "Stack(int Capacity)" << endl;
	DataType* array = (DataType*)malloc(sizeof(DataType) * Capacity);
	if (array == NULL) {
		perror("malloc fail");
		return;
	}
	_Arrey = array;
	_top = 0;
	_Capacity = Capacity;
}

int main() {

	//对象的实例化
	Stack st1(4);//有参构造函数调用,对象+(参数列表)

	return 0;
}

实际上,更多使用的是全缺省构造函数,因为全缺省构造函数可以无参调用也可以传参数;

5.如果我们没有显示定义构造函数,C++编译器会自动生成无参数的构造函数;如果我们显示定义编译器就不会生成

6.编译器自动生成的构造函数不会处理内置类型,如果是自定义类型就会调用它的默认构造函数;

7.默认构造函数有三种,编译器生成的构造函数,无参构造函数,全缺省构造函数;不传参的构造函数就是默认构造函数

我们再创建一个Date类:

class Date {
public:
	Date(int year = 1, int month = 1, int day = 1);
private:
	int _year;
	int _month;
	int _day;
};
typedef int DataType;
class Stack {
public:
private:
	DataType* _Arrey;
	int _top;
	int _Capacity;
    Date d;//自定义类型成员变量
};

Date::Date(int year,int month,int day) {
	cout << "Date()" << endl;
	_year = year;
	_month = month;
	_day = day;
}

int main() {

	//对象的实例化
	Stack st1;//无参构造函数调用

	return 0;
}

最后只有d的成员变量初始化了

实际上也确实调用了Date的构造函数,但系统默认生成的构造函数也对内置类型进行了初始化,这个依赖于编译器的实现,标准没有规定必须要对内置类型进行初始化;

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

如果你没有显示定义构造函数,在调用编译器生成的构造函数就会将成员变量初始化为你给的默认值;

3. 析构函数

析构函数恰好和构造函数相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由
编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

下面是用析构函数来完成成员变量资源的清理:

typedef int DataType;
class Stack {
public:
	Stack(int Capacity = 4);//构造函数
    ~Stack();//析构函数
private:
	DataType* _Arrey;
	int _top;
	int _Capacity;
};

Stack::Stack(int Capacity) {
	cout << "Stack(int Capacity)" << endl;
	DataType* array = (DataType*)malloc(sizeof(DataType) * Capacity);
	if (array == NULL) {
		perror("malloc fail");
		return;
	}
	_Arrey = array;
	_top = 0;
	_Capacity = Capacity;
}

Stack::~Stack() {
	cout << "~Stack()" << endl;
	free(_Arrey);
	_Arrey = nullptr;
	_top = 0;
	_Capacity = 0;
}

int main() {

	//对象的实例化
	Stack st1;//无参构造函数调用

析构函数其实就相当于完成我们栈的Destroy的接口工作,并且不需要我们自己去调用方便了很多;

析构函数的特性

1.析构函数的函数名就是类名,在类名前面再加个~

2.析构函数没有参数和返回值(都不用写)

3.析构函数在对象销毁时会自动调用

4.析构函数不支持重载(原因和c不支持重载同理)

5.和构造函数同理,如果不显示定义析构函数,编译器会自动生成析构函数

6.编译器生成的析构函数同样不会对内置类型进行处理,如果是自定义类型就会去调用它的析构函数

同样以Date类来验证:

class Date {
public:
	Date(int year = 1, int month = 1, int day = 1);
    ~Date();
private:
	int _year;
	int _month;
	int _day;
};
typedef int DataType;
class Stack {
public:
private:
	DataType* _Arrey;
	int _top;
	int _Capacity;
    Date d;//自定义类型成员变量
};

Date::Date(int year,int month,int day) {
	cout << "Date()" << endl;
	_year = year;
	_month = month;
	_day = day;
}

Date::~Date() {
	cout << "~Date()" << endl;
	_year = 0;
	_month = 0;
	_day = 0;
}

int main() {

	//对象的实例化
	Stack st1;//无参构造函数调用

	return 0;
}

当然,如果成员变量都是内置类型的话,也就没必要去写析构函数了;只有当我们动态开辟这种需要我们去正确销毁对象的时候才需要写析构函数,否则会造成内存泄露事故;

注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数

4. 拷贝构造函数

拷贝构造函数是构造函数的一种分支,本身他就是一种特殊的构造函数;

拷贝构造函数的特性

1.拷贝构造函数的函数名是类名(这一点也可以看出来其实就构造函数,可以认为他是构造函数的重载)

2.拷贝构造函数的参数只有一个,必须是引用类型(一般常用const修饰,和普通构造函数的区别);

为什么拷贝构造函数的参数只能是引用类型?
如果拷贝构造的参数是类对象呢?

首先,只要是自定义类型的拷贝,在进行拷贝的时候都要调用自己的拷贝构造函数,下面我们用一个函数来观察一下这个过程:

typedef int DataType;
class Stack {
public:
	Stack(int Capacity = 4);//构造函数
    Stack(Stack& st3);//拷贝构造
    ~Stack();//析构函数
private:
	DataType* _Arrey;
	int _top;
	int _Capacity;
};

Stack::Stack(int Capacity) {
	cout << "Stack(int Capacity)" << endl;
	DataType* array = (DataType*)malloc(sizeof(DataType) * Capacity);
	if (array == NULL) {
		perror("malloc fail");
		return;
	}
	_Arrey = array;
	_top = 0;
	_Capacity = Capacity;
}

void fun(Stack st3) {
    //...函数体
}

Stack::Stack(Stack& d) {
	cout << "拷贝构造" << endl;
	DataType* array = (DataType*)malloc(sizeof(DataType) * d._Capacity);
	if (array == nullptr) {
		perror("malloc fail");
		return;
	}
	_Arrey = array;
	memcpy(_Arrey, d._Arrey, sizeof(DataType) * d._Capacity);
	_top = d._top;
	_Capacity = d._Capacity;
}

Stack::~Stack() {
	cout << "~Stack()" << endl;
	free(_Arrey);
	_Arrey = nullptr;
	_top = 0;
	_Capacity = 0;
}

int main() {

	//对象的实例化
	Stack st1;//无参构造函数调用
    fun(st1);
    return 0;
}

如果是传值的话:

每次调用构造函数都要先传值,然后再调用拷贝函数;传值又会引发新的拷贝,又得先拷贝再调用;这样就会造成无限递归,最后栈溢出程序崩溃;

如果是引用的话就不需要进行拷贝,直接使用引用的对象;

3.如果我们没有显示定义拷贝构造函数,编译器会自动生成拷贝构造函数;

4.编译器生成的拷贝构造函数会按照字节一个一个的进行拷贝,也叫浅拷贝(值拷贝);

浅拷贝:只拷贝值;

深拷贝:开同样大的空间,同时将数据全部拷贝过来;

5.如果类中没有动态开辟的空间,就可以不用写拷贝构造,如果有的话就必须写拷贝构造;

6. 拷贝构造函数典型调用场景

1.使用已存在对象创建新对象
2.函数参数类型为类类型对象
3.函数返回值类型为类类型对象

5. 赋值运算符重载

在进入赋值运算符之前,我们先来了解一下运算符重载;

我们用一个Date类来进行逐步分析:

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

如果是自定义类型进行比较大小的话,直接比较即可,但是自定义类型是不支持的,因为内置类型的比较方法已经被编译器实现好了;如果我们要比较两个自定义类型的大小就得用函数来写,但是同时可读性也会变差;

c++里面引出了一个运算符重载关键字,operator; operator后面跟要重载的运算符,就是函数名;

同时要符合运算符重载要满足几个条件:

1.重载函数参数必须有1个或以上的类类型参数;

2.操作符的操作数有多少个,参数就必须有多少个,成员函数就是n-1个参数,因为有隐含的     this指针;

3.(.* , sizeof , ? : , :: , .)这五个操作符不支持重载

运算符是不支持自定义类型直接使用的;但是写了运算符重载自定义类型也可以直接使用操作符;

既然我们知道了函数重载的大概思路,那么他们的返回值又该是怎么样的?

传值返回和传引用返回

我们知道,如果我们函数有返回值的话,其实并不是直接返回那个变了,因为函数结束就代表开辟的栈帧空间销毁了,那么局部变量也就销毁了;所以其实是将返回的值先进行拷贝到一个临时变量或者寄存器中带回,那么如果是返回值的话则那么拷贝就会影响程序的运行效率;

传引用返回,我们知道引用就是别称,它和引用的变量共用同一块内存空间,那么返回引用其实就不需要进行拷贝,我们直接用引用接收,直接可以使用那块空间;

那么我们知道了两种返回的优劣,那什么情况呀要返回值什么情况要返回引用呢?

如果我们要返回局部变量,就只能返回值;如果要返回到变量和当前变量不在同一块栈帧空间上,则返回引用;

我们通过具体的图分析:

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

Date& func() {
	Date d2;
	return d2;
}

int main() {

	Date d1;
	func();
	return 0;
}

d2是func函数的局部变量,func给d2分配的空间,会在函数销毁的时候也跟着销毁;按照底层的逻辑来讲,其实就是把d2的地址传给了它的引用,其实就是野指针了;返回这块空间的引用代表我能直接使用这块空间,但是此时的空间已经不属于我了我们还能直接使用吗?

或者说此时返回引用会造成值的不确定;如果我在这个函数销毁马上又调用别的函数呢?

空间是可以复用的,可能Print函数开辟的栈空间刚好在那块空间上,或者编译器在调用什么数,把那块空间的值给变了,而我们恰好又能看见那块空间,就会导致返回到结果得不到我们想要的;所以只能进行值返回;

那么什么叫不在同一个栈上呢?

依旧用上面的那个图来理解:

 假设,我将d1引用传递给func函数,并且对d1进行相应的操作;此时我们应该返回值还是引用?

这里就可以使用引用了,d1是在main函数中定义的,func函数销毁回收空间了,关我main函数什么事呢?d1并不在调用func函数的栈上,所以返回引用也不会对d1产生影响;

那么我们继续研究运算符重载;

要用运算符重载,肯定得满足运算符的特性;比如i += j,这个表达式的最后返回值是i;i++返回的是++之前的值,等;

那么我们就来实操一个时间类的比大小和时间类的加等:

//比大小
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) {
			return _day > d._day;
		}
	}
	return false;
}

int GetMonthDay(int year, int month) {
	int MonthDayArrey[] = { -1,31,28,31,30,31,30,31,31,30,31,30,31 };
	if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) {
		return MonthDayArrey[month] + 1;
	}
	return MonthDayArrey[month];
}

Date& operator+=(int day) {
	//进位
	_day += day;
	if (_day > GetMonthDay(_year, _month)) {
		_day -= GetMonthDay(_year, _month);
		_month++;
		if (_month == 13) {
			_month = 1;
			_year++;
		}
	}
	return *this;
}
cout << d1.operator>(d2) << endl;

因为运算符重载一般都是成员函数,所以如果要调用的话就对象.成员名;但是这样就体现不出关键字的优势;一般都是这么写:

cout << (d1 > d2) << endl;

编译器会默认转换调用成员函数;

正式进入赋值重载

赋值运算符重载格式
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this :要复合连续赋值的含义

写法其实已经跟操作符号重载一样了,我们重点看赋值重载的特性:

1.赋值重载也是6大默认成员函数,那么肯定是只能在类里面;

2.赋值重载如果我们不显示定义,编译器就会自动生成,按值赋值(跟拷贝构造一样);

我们来实现一下日期类型的赋值重载:

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

注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必
须要实现。

前置++重载和后置++重载

因为两个操作数的个数一样,但表现形式不一样;所以为了让两个运算符构成重载,规定给后置++强行给一个int参数,这个参数不会使用,只是为了构成重载;

代码实现:

int GetMonthDay(int year, int month) {
	int MonthDayArrey[] = { -1,31,28,31,30,31,30,31,31,30,31,30,31 };
	if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) {
		return MonthDayArrey[month] + 1;
	}
	return MonthDayArrey[month];
}

Date& operator+=(int day) {
	//进位
	_day += day;
	if (_day > GetMonthDay(_year, _month)) {
		_day -= GetMonthDay(_year, _month);
		_month++;
		if (_month == 13) {
			_month = 1;
			_year++;
		}
	}
	return *this;
}

Date& operator++() {
	*this += 1;
	return *this;
}

Date operator++(int) {
	Date d3(*this);
	*this += 1;
	return d3;
}//直接复用加等即可

6. const成员函数

有时候我们会在对象前面加上const;

const Date d1;

此时去调用函数的话,就会报错; 本质原因其实是权限的放大:

那么就只能改变this指针,只用在函数后面加上const即可:

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

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

	//比大小
	bool 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) {
				return _day > d._day;
			}
		}
		return false;
	}

	int GetMonthDay(int year, int month) {
		int MonthDayArrey[] = { -1,31,28,31,30,31,30,31,31,30,31,30,31 };
		if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) {
			return MonthDayArrey[month] + 1;
		}
		return MonthDayArrey[month];
	}

	Date& operator+=(int day) {
		//进位
		_day += day;
		if (_day > GetMonthDay(_year, _month)) {
			_day -= GetMonthDay(_year, _month);
			_month++;
			if (_month == 13) {
				_month = 1;
				_year++;
			}
		}
		return *this;
	}

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

	Date& operator++() {
		*this += 1;
		return *this;
	}

	Date operator++(int) {
		Date d3(*this);
		*this += 1;
		return d3;
	}

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

int main() {

	const Date d1;
	Date d2(d1);
	cout << d1.operator>(d2) << endl;
	cout << (d1 > d2) << endl;
	
	return 0;
}

一句话,权限可以缩小但是不能放大,加了const的函数什么对象都可以使用,但是加了const对象的只能使用const成员函数;

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

Date* operator&() {
	return this;
}

不写编译器会自动生成,其实写了也是编译器取的地址;

8.构造函数的最后一块拼图---初始化列表

我们先理解一个点,初始化列表就是成员变量定义的地方;在执行构造函数时先走初始化列表;

我们之前看构造函数可能有很多不明白的地方:

1.为什么我们不显示定义构造函数,编译器默认的构造函数会去调用自定义类型的默认构造?

2.为什么我在成员变量声明的时候给缺省值会将成员初始化为缺省值?

了解完初始化列表一切都通了;

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括 号中的初始值或表达式。

代码:

class Date {
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)//初始化列表
	{}

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

	//比大小
	bool 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) {
				return _day > d._day;
			}
		}
		return false;
	}

	int GetMonthDay(int year, int month) {
		int MonthDayArrey[] = { -1,31,28,31,30,31,30,31,31,30,31,30,31 };
		if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) {
			return MonthDayArrey[month] + 1;
		}
		return MonthDayArrey[month];
	}

	Date& operator+=(int day) {
		//进位
		_day += day;
		if (_day > GetMonthDay(_year, _month)) {
			_day -= GetMonthDay(_year, _month);
			_month++;
			if (_month == 13) {
				_month = 1;
				_year++;
			}
		}
		return *this;
	}

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

	Date& operator++() {
		*this += 1;
		return *this;
	}

	Date operator++(int) {
		Date d3(*this);
		*this += 1;
		return d3;
	}

	Date* operator&() {
		return this;
	}

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

int main() {

	const Date d1;
	Date d2(d1);
	cout << d1.operator>(d2) << endl;
	cout << (d1 > d2) << endl;
	cout << &d1 << endl;
	return 0;
}

初始化列表不管写没写编译器都会默认生成,包括编译器自动生成的构造函数;如果没有初始化列表,并且构造函数也没有写,对于内置类型,编译器会先去找缺省值,如果没有缺省值就是随机值,这个我们可以验证:

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

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

private:
	int _year = 2024;
	int _month = 10;
	int _day = 25;
};

int main() {

	const Date d1;
	Date d2(d1);
	return 0;
}

直接就跳到了缺省值这;

并且全部都初始化成缺省值的内容了;

而对于自定义类型为什么编译器会调用自定义类型的默认构造呢?

class A {

public:
	A() {
		_a = 10;
		_b = 20;
		_c = 30;
	}

private:
	int _a;
	int _b;
	int _c;
};

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

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

private:
	int _year = 2024;
	int _month = 10;
	int _day = 25;
    A a1;
};

int main() {

	const Date d1;
	Date d2(d1);
	return 0;
}

这里我并没有去写a1的初始化,但是a1最后还是调用了自己的默认构造,为什么?

因为编译器其实已经写好了,只不过我们看不见:

Date的构造方法实际上是什么样的呢?

Date(int year = 1, int month = 1, int day = 1)
	:_year(2024)
	,_month(10)
	,_day(25)
	,a1()
{}

为什么说初始化列表是成员变量定义的地方,构造方法不可以显示调用,只能实例化的时候调用,那么为什么a1可以调用,因为初始化列表是成员变量定义的地方;

用一个MyQueen类来实现一下调用的逻辑:

class Stack {
public:

	Stack(int Capacity) {
			cout << "Stack(int Capacity)" << endl;
			int* array = (int*)malloc(sizeof(int) * Capacity);
			if (array == NULL) {
				perror("malloc fail");
				return;
			}
			_Arrey = array;
			_top = 0;
			_Capacity = Capacity;
	}

private:
	int* _Arrey;
	int _top;
	int _Capacity;
};


class MyQueen {
public:
    //没有提供默认构造
private:
	Stack _st1;
	Stack _st2;
};

这个代码最后会报错,原因是_st1和_st2没有默认构造;那么我们MyQueen就必须进行初始化:

参数列表的真正有用的地方:

通过初始化列表我们可以调用任意参数的构造方法;

class Stack {
public:

	Stack(int Capacity) {
			cout << "Stack(int Capacity)" << endl;
			int* array = (int*)malloc(sizeof(int) * Capacity);
			if (array == NULL) {
				perror("malloc fail");
				return;
			}
			_Arrey = array;
			_top = 0;
			_Capacity = Capacity;
	}

private:
	int* _Arrey;
	int _top;
	int _Capacity;
};


class MyQueen {
public:
	MyQueen()
		:_st1(4)//调用单参数默认构造
		,_st2(4)
	{}
private:
	Stack _st1;
	Stack _st2;
};

这也可以让我们知道为什么初始化列表可以理解为成员变量定义的地方;

尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。

每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)

初始化列表初始化的顺序是成员变量声明的顺序,一般初始化顺序要跟声明一致;

类中包含一下成员必须在初始化列表初始化:

1.引用类型成员(引用类型必须在定义时就指向一个内容,不能空引用)

2.const修饰的成员(const修饰的成员只有一次给值的机会就是定义的时候)

3.没有默认构造的自定义类型成员(调用带参数的构造)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值