类和对象(中)

文章详细介绍了C++中的默认成员函数,包括构造函数(无参、有参、函数重载)、析构函数、拷贝构造函数(函数重载、参数、内置与自定义类型处理)、运算符重载(本质、成员函数特点、不能重载的运算符)以及赋值运算符(返回值、连续赋值、自赋值处理)。文章强调了默认构造函数、析构函数和拷贝构造函数在处理内置与自定义类型时的区别,并指出运算符重载和赋值运算符在提高代码效率和处理动态内存时的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

类中默认成员函数

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

请添加图片描述


1. 构造函数

1.1 背景

在C语言中,我们在创建栈,用到栈的变量是否都要先初始化,size == 0 ,capacity ,top之类都要初始化。
而在C++中,为了解决这种烦恼添加一个函数,叫构造函数


1.2 特性

构造函数是默认成员函数,是特殊的。它的主要工作是完成对对象的初始化,而不是构造(别被名字蒙蔽了)。

特性:

  1. 函数名与类名相同。
  2. 无返回值。
  3. 对象实例化时编译器自动调用对应的构造函数。
  4. 构造函数可以重载。

1.2.1 类中有无构造函数

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

这里就有个问题了

显示构造函数是自己定义的,知道怎么初始化。

但编译器自己生成的构造函数,初始化了什么?怎么初始化的呢?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fKXTQPtM-1684414831856)(image-20230510150407373.png)]

看上图,变量是随机值,说明编译器没有处理内置类型。

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

以上是内置类型的初始化,那对于自定义类型,编译器又是如何处理呢?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fslY4ei7-1684414831856)(image-20230510152015422.png)]

编译器打印了num1,说明编译器自动生成的构造函数在处理自定义类型的时候,会将调用自定义类型的默认的构造函数,实现初始化。

总结:

  • 编译器在有显式构造函数,会直接调用。
  • 编译器在没有显式构造函数,会自动生成构造函数,对于内置类型不会处理,对于自定义类型会调用该类型的默认构造函数。
  • 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。

1.2.2 类中构造函数的参数
1.2.2.1 无参数

构造函数在初始化手动赋值。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5DO2WgrQ-1684414831857)(image-20230510154551088.png)]


1.2.2.2 有参数

构造函数是支持带参数的,也支持缺省

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QWjhNJek-1684414831857)(image-20230510155750695.png)]


1.2.2.2.1 构造函数一些小特殊

在创建对象有参数传递,如d1直接在后面接参数。在无参数传递,如d2后面什么都带。
假如写成d3,会报错,会被编译器当做是函数声明,Data是返回值,d3是函数名,()是参数。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tD2VcSLA-1684414831858)(image-20230510160510196.png)]


1.2.3 构造函数的函数重载

函数名相同,参数列表不同

class Date
{
public:
	// 1.无参构造函数
	Date()
	{}
	// 2.带参构造函数
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
void Test()
{
	Date d1; // 调用无参构造函数
	Date d2(2015, 1, 1); // 调用带参的构造函数
}

2. 析构函数

2.1 背景

在C语言中,我们常常会遇到需要开辟新空间,开辟空间相对就要释放它,否则会造成内存泄漏。
在C++中,为了防止程序员忘记释放空间,增加了析构函数,对象在销毁时会自动调用析构函数,完成对象中资源的清理工作


2.2 特性

析构函数是特殊的成员函数,它的特征:

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3DUHIXFn-1684414831858)(image-20230512113158378.png)]

这里我们看反汇编,会更直观看到对象生命周期结束时,是否会调用析构函数。
他有一条是call Stack::~Stack,说明他调用了。(一般反汇编call 啥啥啥的,就是在调用该函数)

​	[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UVAShlBl-1684414831858)(image-20230512113427047.png)]


2.2.1 类中有析构函数

类中有析构函数,就是我们显式定义了析构函数,编译器就不会去调用默认的析构函数。
如果析构函数中,有自定义类型,编译器会如何执行呢?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6BJrleNX-1684414831859)(image-20230512120545540.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3U8CyEft-1684414831859)(image-20230512120649840.png)]

上图,我在Stack析构函数中创建Time类的对象,最后编译器输出了~Time()。说明Time类的析构函数被调用了。
也就可以理解为在析构函数中,自定义类型也会被创建,会在其生命周期结束时,调用该类型的析构函数。


2.2.2 类中无析构函数

没有析构函数,编译器会自动生成析构函数,这是它的特性之一。
默认的析构函数,会对自定义类做如何处理?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cd3iHhkx-1684414831860)(image-20230512122218468.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2cEibByJ-1684414831860)(image-20230512122233142.png)]

编译器输出了~Time(),说明Time析构函数被调用了。

问题这就来了,在main方法中,只创建Data对象,并没有创建Time对象,而Data对象也没有显式的析构函数,Time对象的析构函数如何被调取的呢?

还记得若无显式析构函数,编译器会自动生成默认析构函数吗,关键就在这里了。
这个默认析构函数,对于内置成员变量是不需要销毁的,生命周期结束,系统会自动回收。
对于自定义类型,在它的外部对象(Data)要销毁的时候,顺带要销毁它的里面的自定义对象(Time)。

所以类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁,main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数。


3. 拷贝构造函数

拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

3.1特性

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

  1. 拷贝构造函数是构造函数的一个重载形式(本质上是函数重载)。
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用

3.1.1 拷贝构造函数本质是函数重载

函数名相同,参数列表不同,符合函数重载

class Date
{
public:
	//构造函数
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//拷贝构造
	Date(Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

3.1.2 拷贝构造函数的参数只有一个且必须是类类型对象的引用

如果使用传值方式会引发无穷递归调用 。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gv44Y9iY-1684414831861)(image-20230518121957207.png)]


3.1.3 拷贝函数对于内置类型和自定义类型的处理
  • 若未显式定义,编译器会生成默认的拷贝构造函数。
    默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

  • 内置类型,生成默认拷贝是做处理的(不同于构造函数和析构函数),直接完成拷贝

  • 自定义类型,会去调用它的拷贝函数,若没有显式的,会生成默认拷贝做处理,完成拷贝任务。
    但是,会出现问题


3.1.4 默认拷贝构造处理自定义类型
class Date
{
public:
	//构造函数
	Date(int year = 1900, 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;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	Date d2(d1);
	return 0;
}

这边通过调试,发现值有正常的传输,没问题!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aANscnzr-1684414831861)(image-20230518123451374.png)]

真的是这样吗?来看接下来的代码

class Stack
{
public:
	//构造函数
	Stack(size_t capacity = 3)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (NULL == _a)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}
	//析构函数
	~Stack()
	{
		if (_a)
		{
			free(_a);
			_a = NULL;
			_capacity = 0;
			_size = 0;
		}

	}
private:
		int* _a;
		int _capacity;
		int _size;
};

int main()
{
	Stack s1;
	Stack s2(s1);
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5WqKnKjI-1684414831861)(image-20230518124200656.png)]

上面代码执行,编译器报错了,是挂在析构函数里面的。
OK,让我来一步步分析。

s1,s2都创建成功了,但是比较奇怪的是s1和s2里面的a为什么是指向同一个地址的?

指向同个地址的a,当对象销毁时,进入析构函数先将s2里面的a free掉了(对象是创建在栈里的,栈先进后出,所以会先析构后创建的对象),到现在都没问题。好了,当s1进入析构函数了,准备free掉a时,发现a已经被释放掉了,编译报错,就寄掉了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-etaLBX3Y-1684414831862)(image-20230518124851767.png)]

总结:

  1. 自定义类型会完成值拷贝(浅拷贝)。
  2. 自定义函数在处理这些动态开辟的空间时,是浅拷贝是没办法很好的处理的,就必须依靠显式的拷贝构造函数来处理,不然就会出现上面的情况。这种给申请的空间拷贝构造叫深拷贝

3.1.4 拷贝构造使用场景
  • 使用已存在对象创建新对象
  • 函数参数类型为类类型对象
  • 函数返回值类型为类类型对象

现实我们其实要减少拷贝构造的使用的,这样可以提高代码执行效率。

对于函数的返回值

  • 出了作用域,对象还在,用引用返回
  • 出了作用域,对象销毁了,就不能用引用返回了,要用拷贝构造。

4. 运算符重载

4.1 背景

对于内置类型的运算符,编译器是知道按照什么方法计算,然后有相应返回值。

对于自定义类型,编译器是不知道我们按照什么方法计算,所以就会用到运算符重载。


4.2特性
  • 函数名字为:关键字operator后面接需要重载的运算符符号。
    函数原型:返回值类型 operator操作符(参数列表)

  • 不能通过连接其他符号来创建新的操作符:比如operator@

  • 重载操作符必须有一个类类型参数

  • 用于内置类型的运算符,其含义不能改变。(+,就是加,不管加什么)

  • 作为类成员函数重载时,其形参看起来比操作数数目少一个,因为成员函数的第一个参数为隐藏的this指针

  • 注意有5个运算符不能重载。

    .*     ::    sizeof     ?:   .
    

4.2.1 运算符重载的本质是转化成调用operator
class Fun
{
public:
	//构造函数
	Fun(int num = 0)
	{
		_num = num;
	}
	
	//运算符重载 >
	bool operator> (const Fun& f)
	{
		if (_num > f._num)
		{
			return true;
		}
		else
		{
			return false;
		}
	}

private:
	int _num;
};
int main()
{
	Fun f1(100);
	Fun f2(20);

	cout << (f1 > f2) << endl;

	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lCwGCCSG-1684414831862)(image-20230518195000885.png)]

编译器调用了opeartor>


4.2.2 作为类成员函数重载时,其形参看起来比操作数数目少一个,因为成员函数的第一个参数为隐藏的this指针

由于this指针的存在,所以在我们使用运算符重载的时候,参数传递的顺序是很重要的,如 f1 > f2,f1的地址会是this指针,不要搞反了。


4.3 赋值运算符

4.3.1 背景

赋值运算符是运算符重载中比较特殊的,它的特殊在于它的返回值。在一段不断赋值的指令,a1 = a2 = a3 (a1,a2,a3都是内置类型)可以看出,a2 = a3赋值完后,将它的返回值再赋值给a1。这样的特性,我们在设计赋值运算符时也得考虑。


4.3.2 特性

赋值运算符是对已经存在的两个对象之间的拷贝(区别于构造函数)。

  • 参数类型:const T&,传递引用可以提高传参效率

  • 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值

  • 检测是否自己给自己赋值

  • 返回*this :要复合连续赋值的含义

  • 赋值运算符只能重载成类的成员函数不能重载成全局函数

  • 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。


4.3.2.1 默认赋值运算符处理内置类型和自定义类型
  • 内置类型,编译器会以值传递进行拷贝
  • 自定义类型,编译器会调用它的赋值运算符。若没有显式,会用默认的,进行值拷贝。
    简单的值拷贝(浅拷贝)会出现与运算符重载相同的问题,无法处理好申请的空间。
    此时就要手动显式定义赋值运算符。

4.3.2.2 前置++,后置++

前置++与后置++,在定义的时候,注意后置++的参数是有个int的,这个int是没有实际意义的,仅仅作为占位。

它们的返回值也各不同:

  • 前置++,要符合先++的原则,然后把加完后的结果返回,返回*this也就是本身,返回值是引用(返回值本来是值返回的(Fun),但考虑到值返回(Fun)会调用拷贝构造,这里优化成引用,提高效率)。
  • 后置++,要符合后++的原则,利用临时变量存储++前的数据,同样返回的也是这个临时变量(对于返回的是临时变量,就无法使用引用返回,必须是值返回,调用拷贝构造)
class Fun
{
public:
	//构造函数
	Fun(int num = 0)
	{
		_num = num;
	}
	
	//前置++
	Fun& operator++()
	{
		_num += 1;
		return *this;
	}
	// 后置++:
	Fun operator++(int)
	{
		Fun temp(*this);
		_num += 1;
		return temp;
	}

private:
	int _num;
};

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

这两个默认成员函数一般不用重新定义,编译器会默认生成。

class Fun
{
public :
    Date* operator&()
    {
    return this ;
    }
    const Date* operator&()const
    {
    return this ;
    }
};

5.1 const成员

上面代码有个const写在外面了,这个const的作用就是用来修饰this指针的。

对于这个const修饰,就要考虑到权限的问题。记住,权限可以平移,可以缩小,但是不能放大。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值