【c++】类和对象(中)

个人主页: 主页
专栏:专栏
每篇一句:

  • 甲乙丙丁又如何,青春的阳光,照亮的是所有人。

在这里插入图片描述


类的6个默认成员函数

  • 默认构造函数
  • 默认析构函数
  • 默认拷贝构造函数
  • 默认赋值重载函数
  • 默认取地址重载函数
  • 默认const取地址重载函数

其中,前四个都是比较重要的,下面一一展开。

一.构造函数

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。

构造函数的作用是自动完成初始化工作,避免了频繁手动调用初始化函数的问题
注意:并不是开空间创建对象,而是初始化对象

下面就是一个构造函数。

class Stack
{
public:
	Stack()
	{
		cout << "Stack()" << endl;
	}
}

构造函数的特征如下:

  • 函数名与类名相同
  • 无返回值,(void都不需要写)
  • 对象实例化时编译器自动调用对应的构造函数
  • 构造函数可以重载,即一个类可以有多个构造函数,但默认构造函数只能有一个

构造函数的分类:

构造函数分为无参构造函数和有参构造函数

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

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

他们调用的方式是不同的

Date d1;//调用无参构造函数
Date d2(4);//调用有参构造函数

实际上,推荐使用全缺省的构造函数,就能实现对上述两个构造函数的合并

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

这样既可以无参调用,又可以有参调用,传1个,2个,3个参数都可以。

	Date d1;
	Date d2(2023, 2, 3);
	Date d3(2023);
	Date d4(2023, 2);

默认构造函数:

  1. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
    也就是说:全缺省的构造函数和无参构造函数同时存在会有歧义。
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}

	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
  1. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户实现了任意一个构造函数,编译器将不再生成。

  2. 有人可能会觉得:既然编译器会自动生成,那么看起来好像我们根本就不用写了。
    然而并不是这样。默认构造函数设计的有一个坑

C++把类型分成内置类型(基本类型)和自定义类型,内置类型就是语言提供的数据类型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型

  • 默认生成的构造函数对内置类型不处理
  • 对自定义类型的成员会调用他们的默认构造函数

调用了默认构造函数后,内置类型成了随机值。

在这里插入图片描述

为了解决这个问题,c++11新增了个补丁

新增补丁

在内置类型声明时,可以给他们指定一个缺省值,这样就会以这些缺省值来初始化成员变量。

在这里插入图片描述

二. 析构函数

析构函数的作用自动完成对象内资源的销毁,解决手动频繁销毁操作的问题

注意:析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的

析构函数是特殊的成员函数,其特征如下:

  1. 析构函数名是在类名前加上字符 ~
  2. 无参数无返回值类型
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

格式如下:

	~Stack()
	{
		_year = _month = _day = 0;
	}

析构函数也存在默认析构函数

和默认构造函数类似。
默认生成的析构函数对内置类型不处理,对自定义类型的成员会调用他们的默认析构函数。

📝注意一点:如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数即可;当一个类涉及到动态内存分配时,就需要我们自己手写一个析构函数,否则会造成资源泄漏

	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_size = _capacity = 0;
	}

三.拷贝构造函数

作用:在创建对象时,去创建一个与已存在对象一模一样的新对象

写法:

构造函数的实现:

class Date
{
public:
	Date(const Date& d)
	{
		//d 拷贝给 *this
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year = 2022;
	int _month = 6;
	int _day = 9;
};

规范化:
Date(const Date& d) 这里尽量加上const,防止权限放大。加上引用,防止无限递归循环
调用有两种方式:

Date d1;	
Date d2(d1);	//第一种

Date d3 = d1;	//第二种

特征:

拷贝构造函数也是特殊的成员函数,其特征如下:

  1. 拷贝构造函数是构造函数的一个重载形式。
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用使用传值方式编译器直接报错,因为会引发无穷递归调用。

为什么会发生无穷递归?

参数没有设为引用,进行传值传参
对于内置类型编译器可以可以直接拷贝
对于自定义类型的拷贝,需要调用拷贝构造

**因为调用拷贝构造需要去传值传参,自定义类型的传值传参就会去调用拷贝构造,调用拷贝构造就需要传值传参,而自定义类型的传值传参就会去调用拷贝构造… … …**如下:

	Date(Date d)
	{
		//d 拷贝给 *this
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	Date d2(d1);

因此应该这样写:

Date(const Date& d)
{
	//有效避免无穷递归问题
	//……
}

  1. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数是按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

默认拷贝构造函数

默认拷贝构造函数,既可以对内置类型进行处理,也可以对自定义类型进行处理
但需要注意深浅拷贝的问题。

  • 什么是浅拷贝什么是深拷贝?

对于浅拷贝,即按字节序完成拷贝
这就意味着,当涉及空间开辟时,若只是简单的进行浅拷贝,就会出现两个指针变量指向同一块空间的情况。因为只是简单的把地址进行了拷贝,并没有开辟新的空间。见下图:

在这里插入图片描述
当程序要退出时,s2和s1要销毁,s2先销毁,他指向的空间已经被释放,s1不知道,s1会再将他指向的空间释放一次。造成了一块空间的多次释放,引起程序崩溃。
此外,s2写入的数据会被s1写入覆盖。

因此对于拷贝构造,浅拷贝存在问题(数据被覆盖,free两次)

而深拷贝,会先开辟一块同样大的空间,再将空间中的数据拷贝过来

因此:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 时,则拷贝构造函数是一定要写的,否则就是浅拷贝

场景:

  • 使用已存在对象创建新对象
  • 函数参数类型为类类型对象
  • 函数返回值类型为类类型对象

注意: 默认拷贝构造函数与默认构造函数名相同,当我们只写拷贝而不写构造时,编译器就会报错,因为此时的拷贝会被误以为是默认构造函数

四.赋值运算符重载

运算符重载

我们知道,运算符默认只对内置类型有效,它无法作用于自定义类型。

为了能让自定义类型使用运算符,我们就需要进行运算符重载
运算符重载是具有特殊函数名的函数也具有其返回值类型,函数名字以及参数列表

写法:返回值类型 operator操作符(参数列表)

举例:定义两个日期类对象,对两个日期类对象的年月日进行比较

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

	int _year;
	int _month;
	int _day;
};

写一个运算符重载

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

这样就可以对我们的自定义类型进行比较

Date d1(2022, 9, 26);
Date d2(2023, 9, 27);

//两种调用方式,一般都会去用第二种
operator==(d1,d2);//第1种
d1 == d2;//第2种

注意:

  • 1.运算符的优先级问题
cout << d1 == d2 << endl;//<< 优先级大于 ==
cout << (d1 == d2) << endl;//需要括号括起来
    1. 运算符重载写在类外,如果没有自定义get等函数获取成员变量,就需要把成员变量定义为公有,这样不好。最好把运算符重载写在类里面。 但写法有所区别,因为作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
bool operator==(const Date& d)
{
    return _year == d._year;
      && _month == d._month
      && _day == d._day;
}

这样写在类中,调用也有所不同

d1.operator==(d2);//第一种
d1 == d2;//第二种
    1. 重载操作符必须有一个类型参数
    1. .* :: sizeof ?: . 注意以上5个运算符不能重载.

举例:其他运算符的重载

  1. 小于 <
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;
}
  1. 小于等于 <=
bool operator<=(const Date& d)
{
	return *this < d || *this == d;
}
  1. 大于 >
bool operator>(const Date& d)
{
	return !(*this <= d);
}
  1. 大于等于 >=
bool operator>=(const Date& d)
{
	return !(*this < d);
}
  1. 不等于 !=
bool operator(const Date& d)
{
	return !(*this == d);
}

这里需要注意去复用之前的运算符重载,减少代码量,更加方便。

赋值运算符重载

赋值重载的目的将 d1 对象赋值给 d2,非拷贝构造,d1、d2均已存在
有了上面的铺垫,写赋值运算符就比较容易

void operator=(const Date& d)		
//能用引用的地方,就用引用
//避免去走拷贝构造函数
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

但是这样写并不好。我们需要解决两个问题。

  1. 赋值运算符支持连续赋值例如,j = k = l;
    解决:最后return *this
Date& operator=(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;

	return *this;
}
  1. 自己给自己赋值,k = k;
    解决:加判断
Date& operator=(const Date& d)
{
	if(this != &d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	return *this;
}

因此,赋值运算符重载格式

  • 参数类型:const T&,传递引用可以提高传参效率
  • 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
  • 检测是否自己给自己赋值
  • 返回*this :要能够连续赋值

注意:

  • 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝即:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。

  • 赋值重载和拷贝构造都能用 = ,如何区别
    赋值重载针对两个已经定义好的对象,拷贝构造是用一个已经定义的去初始化另外一个未定义的。

Date d5 = d4;//是拷贝构造

默认生成的特性总结:

在这里插入图片描述

const修饰

const修饰this 指针

见下面代码:

class Date
{
public:
	//实现简单的打印函数
	void Print()
	{ 
		cout << _a << endl;
	}
	
private:
	int _a = 200;
};

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

上面代码会发生报错。
传入Printh函数的d的类型是const Date*,而Print的this指针类型为Date*。
这就造成了权限的放大,原本不可更改的变为了可更改的。

可以加一个const来修饰this指针

class Date
{
public:
	//实现简单的打印函数
	void Print() const//修饰this指针
	{ 
		cout << _a << endl;
	}
	
private:
	int _a = 200;
};

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

权限的缩小
下面代码即为权限的缩小,将可修改的类型变为了不可修改的类型

class Date
{
public:
	//实现简单的打印函数
	void Print() const//修饰this指针
	{ 
		cout << _a << endl;
	}
	
private:
	int _a = 200;
};

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

此外,
const 修饰可以提高程序的健壮性,常被用来修饰引用、指针

当被指向对象为常量或临时变量时,应该去使用 const 修饰,避免出现权限放大问题(常量不可更改,从不能改到能改,这就是权限放大了,此时是不行的)

//int* p = 1;	//错误,1 具有常性
const int* p = (const int*)1;	//正确

//int& a = 2;	//错误,2 具有常性
const int& a = 2;	//正确

五. 取地址重载函数

作用就是获取当前对象的地址

class Date
{
public:
	Date* operator&()
	{
		return this;
	}
	
private:
	int _a = 200;
};

但我们可以直接使用&符号获取地址,完全没有进行重写的必要。
不过可以进行一些另类的操作

	Date* operator&()
	{
		return nullptr;//不想让别人获取地址,直接返回空
	}
	
	Date* operator&()
	{
		return (Date*)0x01;//返回一个假的地址
	}

六. const取地址重载

获取 const 修饰对象的地址

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

取地址重载函数和const取地址重载函数使用编译器默认生成的就够了,基本不需要重写


  • 29
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jayce..

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值