类与对象[中]

类的6个默认成员函数

一个类中如果什么成员也没有,简称为空类,但是空类中什么也没有吗?


class Date
{

};

并不是的,任何一个类都会自动生成以下6个默认成员函数:

1、初始化和清理

构造函数主要完成初始化工作
析构函数主要完成清理工作

2、拷贝复制

拷贝构造是使用同类对象初始化创建对象
赋值重载主要是把一个对象赋值给另外一个对象

3、取地址重载

主要是普通对象const对象取地址,这两个很少会自己实现

一、构造函数

概念: 构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次。
构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
特征如下:

  1. 函数名与类名相同。
  2. 无返回值。
  3. 对象实例化时编译器自动调用对应的构造函数。
  4. 构造函数可以重载。
class Date
{
public:
	// 两个构造函数——完成初始化,完成函数重载
	Date()
	{
		_year = 0;
		_month = 1;
		_day = 1;
	}
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

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

int main()
{
	Date d1; // 构造函数会在对象实例化的时候自动调用
	Date d2(2022, 5, 26);

	return 0;
}

C++把类型分为自定义类型和内置类型两类
内置类型:int、char、double、指针、内置类型数组(int数组,char数组等)
自定义类型:struct/class定义的类型

  1. C++我们不写构造函数,编译器会默认生成构造函数
    对于内置类型不做初始化处理
    对于自定义类型会去调用他的默认构造函数(不用参数就可以调用的)去初始化,如果没有默认的构造函数,就会报错,比如我们写了构造函数,但是构造函数是带参数的,编译器就不会默认生成了,编译就不会通过了
class A
{
public:
	A() // 默认构造函数(不用参数就可以调用的)
	{
		cout << "A()" << endl;
		_a = 0;
	}

private:
	int _a;
};
class Date
{
public:

private:
	int _year; // 内置类型,不处理
	int _month;
	int _day;
	A _aa; // 自定义类型,处理
};
int main()
{
	Date d1;
	return 0;
}
class A
{
public:
	//A(int a) 
	//{
	//	cout << "A()" << endl;
	//	_a = 0;
	//}


	/* 自定义类型没有默认构造函数(就是无参的构造函数)就会报错 */

	A(int a) 
	{
		cout << "A()" << endl; 
		_a = 0;
	}

private:
	int _a;
};
class Date
{
public:

private:
	int _year; 
	int _month;
	int _day;
	A _aa; // 自定义类型,处理
};
int main()
{
	Date d1;
	return 0;
}
class A
{
public:
	//A(int a) 
	//{
	//	cout << "A()" << endl;
	//	_a = 0;
	//}
	//A(int a) 
	//{
	//	cout << "A()" << endl;
	//	_a = 0;
	//}

	/* 这样子也不会报错,因为编译器会自动生成默认的构造函数,但是不做任何处理 */

private:
	int _a;
};
class Date
{
public:

private:

	int _year; 
	int _month;
	int _day;
	A _aa; 
};
int main()
{
	Date d1;
	return 0;
}

任何一个类的默认构造函数有三个,不需要参数就可以调用——有三个,全缺省、无参、我们不写编译器默认生成的(也是无参数的)——如果写了构造函数就不会生成默认的了,并且写了有参数的构造函数了会报错(因为必须要有无参的或者全缺省的构造函数)

  1. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
class A
{
public:
	// 语法上,无参的构造函数与全缺省的构造函数可以同时存在,但是如果有对象定义去调用就会报错
	A() 
	{
		cout << "A()" << endl;
		_a = 0;
	}
	A(int a = 1) 
	{
		cout << "A()" << endl;
		_a = 0;
	}

private:
	int _a;
};
class Date
{
private:

	int _year; 
	int _month;
	int _day;
	A _aa; 
};
int main()
{
	// Date d1; // 没有定义对象,就不会报错,如果定义对象后,编译就会报错
	return 0;
}

总结:
对于日期类,我们就需要自己实现构造函数
对于Queue类,我们就不需要自己实现构造函数,默认生成的就可以

二、析构函数

概念: 析构函数:与构造函数功能相反,析构函数不是完成对象的销毁局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象的一些资源清理工作
特征:
1、析构函数名是在类名前加~
2、无参数无返回值
3、一个类有且只有一个析构函数(没有重载),若没有显示定义,系统会自动生成默认的析构函数(不做处理)
4、对象生命周期结束时,系统会自动调用析构函数

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	~Date()
	{
		// Date类没有资源需要清理,所以Date类不实现析构函数也可以
		cout << "~Date()" << endl;
	}


private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	return 0;
}

日期类是不需要实现析构函数的,栈类是需要析构的

class Stack
{
public:
	Stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			cout << "malloc fail\n" << endl;
			exit(-1);
		} 
		_top = 0;
		_capacity = capacity;
	}
	~Stack()
	{
		// 如果不清理就会内存泄漏
		// 堆上的资源不会主动释放
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

private:
	int* _a;
	size_t _top;
	size_t _capacity;
};
int main()
{
	Stack s1;
	Stack s2(10);

	return 0;
}

析构的顺序是与构造顺序相反的,因为s1先进栈,s2后进栈,清理资源的时候,s2先清理,s1后清理

总结
如果类对象需要资源清理,才需要自己实现析构函数

三、拷贝构造函数

概念: 只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰), 在用已存在的类类型对象创建新对象时由编译器自动调用,也叫复制构造
特征:
1、拷贝构造函数是构造函数的一个重载形式
2、拷贝构造函数的参数有且只有一个且必须使用引用传参,如果使用传值传参会引发无穷的递归调用

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(Date& d) // 必须要引用传参,且只有一个参数
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	~Date()
	{
		cout << "~Date()" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2022, 5, 27);

	// 拷贝复制
	Date d2(d1);
	return 0;
}

为什么必须要使用引用传参?
调用拷贝构造需要先传参,传值传参又是一个拷贝构造(下面有解释),…,一直递归下去
为什么加引用后就没有这个问题了?
Date就是d1的别名,就不会去调用拷贝构造了

在这里插入图片描述

为什么传值传参又是一个拷贝构造?

void f(Date d)
{

}
int main()
{
	Date d1(2022, 5, 27);
	f(d1);
	return 0;
}

用这个程序来说明,d1传给d,实参传给形参,就需要实参拷贝一份值传给形参,也就是用d1构造d(又是一个拷贝构造了——同类型的对象去初始化)
C++里面传值传参就是拷贝构造
一般来说,建议加上const,原因:防止修改了本身对象的属性

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;
	}
	~Date()
	{
		cout << "~Date()" << endl;
	}

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

int main()
{
	Date d1(2022, 5, 27);
	Date d2(d1);
	return 0;
}

3、 如果没有显示定义,系统会自动生成默认的拷贝构造函数
默认的拷贝构造函数对内置类型成员:会按照字节序的拷贝(浅拷贝)
对于自定义类型:会调用他的拷贝构造
下面这个代码,如果我们使用编译器默认生成的拷贝构造函数就会崩溃

class Stack
{
public:
	Stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			cout << "malloc fail\n" << endl;
			exit(-1);
		} 
		_top = 0;
		_capacity = capacity;
	}
	~Stack()
	{
		// 如果不清理就会内存泄漏
		// 堆上的资源不会主动释放
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

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

int main()
{
	Stack st1(10);
	Stack st2(st1);
	return 0;
}

在这里插入图片描述
可以查看到st1与st2是完成了字节序的拷贝的(浅拷贝)
到第二次析构的时候就会崩溃掉

在这里插入图片描述
问题1:st1与st2指向的是同一个空间,st2先析构,释放掉指向的空间后,st1在析构,再次释放这个空间,就出现了问题
问题2:st2插入数据1后,st1也看到了数据1,st1删掉数据3后,st2数据3也被删掉了,我们应该是想让他们互相不影响的——就需要实现深拷贝,后续讲解

写了拷贝构造函数,也必须有默认的构造函数,不然会报错

四、赋值运算符重载

4.1 运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号
函数原型:返回值类型 operator操作符(参数列表)
注意:

  1. 不能通过连接其他符号来创建新的操作符:比如operator@
  2. 重载操作符必须有一个类类型或者枚举类型的操作数
  3. 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  4. 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参
  5. .* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。

想让日期之间进行加减怎么办?——运算符重载

int main()
{
	Date d1(2022, 5, 27);
	Date d2(2022, 1, 31);
	Date d3(2023, 2, 26);
	// 计算d1 - d2
	// 默认情况下,C++是不支持自定义类型对象使用运算符的

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

// private:
	int _year;
	int _month;
	int _day;
};
// 返回值 operator操作符
// 返回类型:看操作符运算后返回值是什么
// 参数:操作符有几个操作数,他就有几个参数
// 不改变操作数就加const
bool operator>(const Date& d1, const Date& d2) 
// 传引用对自定义类型更好用
{
	// 这里访问不到私有的成员变量
	// 解决方法1:变为公有——破坏封装
	// 解决方法2:共有函数GetYear——麻烦
	// 解决方法3:友元——破坏封装
	// 解决方法4:把函数放在类里面
	if (d1._year > d2._year) // 私有变量访问不了
	{
		return true;
	}
	else if (d1._year == d2._year && d1._month > d2._month)
	{
		return true;
	}
	else if (d1._year == d2._year && d1._month == d2._month && d1._day > d2._day)
	{
		return true;
	}
	else
	{
		return false;
	}

}

int main()
{
	Date d1(2022, 5, 27);
	Date d2(2022, 1, 31);
	Date d3(2023, 2, 26);

	// 调用d1>d2的方法1:
	cout << (d1 > d2) << endl;
	// 调用d1>d2的方法2:当做一个函数去使用——一般不这么用,可读性差
	cout << operator>(d1, d2) << endl;

	return 0;
}

修改后:

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

	// 搞成成员函数后,这里要改为一个参数
	// 因为这里就有一个隐含的this指针,d1是this
	bool operator>(const Date& d) 
	{
		if (_year > d._year) 
		{
			return true;
		}
		else if (_year == d._year && _month > d._month)
		{
			return true;
		}
		else if (_year == d._year && _month == d._month && _day > d._day)
		{
			return true;
		}
		else
		{
			return false;
		}
	}

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

int main()
{
	Date d1(2022, 5, 27);
	Date d2(2022, 1, 31);
	Date d3(2023, 2, 26);
	
	d1 > d2; 
	// 先去全局看有没有这个重载,如果没有再去看类里面,然后就转换为下面一行的调用
	d1.operator>(d2);
	return 0;
}

4.2 赋值运算符重载

class Date
{
public:
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// d1 = d3; this就是d1,d就是d3的别名
	// 赋值运算符是有返回值的(左操作数),不是没有返回值
	// 如i = j = k = 10; 10赋给k,返回值就是k
	// 如果返回void,d2 = d1 = d3就出错了,因为d1=d3就返回void了,void不能够赋值给d2
	/*void operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}*/
	// 修改——返回左操作数
	// 传引用相比传值减少了一次拷贝
	Date(const Date& d)
	{
		cout << "Date(const Date& d)" << endl;
	}
	Date& operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		return *this; // 出了作用域,d1(*this)还在,就可以引用返回
	}

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

int main()
{
	Date d1(2022, 5, 27);
	Date d2(2022, 1, 31);
	Date d3(2023, 2, 26);
	
	// 用一个已经存在的对象拷贝去初始化一个马上创建实例化的对象
	//Date d4(d1); // 拷贝构造

	// 两个已经存在的对象之间,进行赋值拷贝
	d1 = d2 = d3;
	return 0;
}


为了防止写出自己给自己赋值,就优化一下:

Date& operator=(const Date& d)
{
	if (this != &d) // 判断两者地址是否相同
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		return *this; // 出了作用域,d1(*this)还在,就可以引用返回
	}
}

如果没有显示定义赋值运算符重载,编译器就会默认生成一个,做的事情与拷贝构造做的事情相似
1、内置类型:完成值拷贝(浅拷贝)
2、自定义类型成员变量:调用他的operator=,默认是浅拷贝,深拷贝需要自己实现

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

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

int main()
{
	Date d1(2022, 5, 27);
	Date d2(2022, 1, 31);
	Date d3(2023, 2, 26);
	d1 = d2 = d3; // 自定义类型会自动调用他的operator=
	return 0;
}

对于内置类型,可能需要自己实现拷贝构造与赋值运算符重载(日期类不需要,栈类需要),90%的开辟空间都需要实现深拷贝,只有智能指针与迭代器不需要实现深拷贝——就看析构是不是析构两次,共用一块空间
对于自定义类型,不需要实现,如myqueue类
总结 :默认生成的四个默认成员函数,构造与析构处理机制是基本相似的,拷贝构造与赋值重载处理是基本类似的

int main()
{
	Date d1(2022, 5, 27);
	Date d2(2022, 1, 31);
	Date d3(2023, 2, 26);
	Date d5 = d1; // 是拷贝构造——用一个已有的对象去初始化一个马上创建实例化的对象
	return 0;
}

重载前置++与后置++如何区分?
后置++增加一个占位参数

// ++d1
Date& operator++();
// d1++;后置++为了与前置++进行区分,增加一个占位参数
Date operator++(int);

五、const成员

5.1 const修饰类的成员函数

为什么const Date d2不能调用Print函数?

int main()
{
	Date d1;
	d1.Print(); 
	const Date d2; 
	d2.Print();  // 报错
	return 0;
}

this指针是Date* const 类型:Date* const this
先不看const,方便理解,也就是说明this指针是Date*类型
const Date d2; &d2传给this,也就是const Date*传给了thisDate*类型,权限的放大,因此不能这样子做

int main()
{
	Date d1;
	d1.Print(); // &d1 -- Date*

	// this指针是Date* const this类型 
	
	// const在*右边,修饰的是this本身不能被改
	const Date d2;
	d2.Print(); // &d2 -- const Date* 传过去就是权限的放大
	// const在*左边,修饰的是this指向的内容不能被更改
	// d2是不能修改指向的内容,this可以,因此就是权限的放大了
	return 0;
}

为了解决问题,让this指针变为const Date* const this

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

这样子就防止了权限的放大与缩小

下面这个代码涉及权限的放大与缩小吗?

int a = 10;
const int x = a;

不涉及,x的改变不会影响a
const Date* const this中第二个const是为了防止this不能被改变,第一个const是修饰*this

总结
成员函数加const是有利的,建议能加上const的都加上const,这样子普通对象与const对象都能调用他;但是如果要修改成员变量的成员函数是不能加的,比如日期类中+=、++等等实现

5.2 取地址及其const取地址操作符重载

class A
{
	// 这两个不需要写,编译器也会自动默认生成
	A* operator&()
	{
		return this;
	}
	const A* operator&() const
	{
		return this;
	}
};

int main()
{
	A aa;
	cout << &aa << endl;
	const A aa2;
	cout << &aa2 << endl;
	return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值