c++编程5——类与对象(3)拷贝构造函数

本文详细介绍了C++中的this指针及其在成员函数中的作用,区分了浅拷贝构造函数和深拷贝构造的区别,以及它们在对象初始化和拷贝时的应用。
摘要由CSDN通过智能技术生成

欢迎来到博主的专栏——c++编程
博主ID:代码小豪

this指针

其实this指针在类与对象(1)中就应该阐述,但是苦于没有使用场景,因此放置在类与对象(3)中讲解。

类其实是c++中的语法糖,是C语言无法封装对象的缺点的一个改良,在c++中的类能完成的事情,在C语言中也能做到,只是会麻烦多点。

还记得成员函数吧,在C语言中如果想要对结构体中的成员变量进行操作,那么函数当中就必须对结构体进行成员访问
例如栈的压栈操作:

void StackPush(stack* s,STDataType e)
{
	s->_stack[s->_top++] = e;
}

我们是不是必须先对结构体指针进行解引用操作来访问成员才能完成压栈操作?但是c++中的成员函数则不需要上传对象本体来进行对对象的压栈操作

void stack::Push(STDataType e)//入栈操作
{
	_stack[_top++] = e;//_stack和_top都是对象的成员变量,但是可以直接使用
}

这是因为c++在使用成员函数时,会有一个this指针来指向对象,这个this指针等同于对象的指针。

stack s;
stack*this=&s;

而成员函数会直接使用这个this指针作为参数接收对象的地址。那么c++中压栈操作等同在C语言中如下操作

void StackPush(stack* this,STDataType e)
{
	this->_stack[this->_top++] = e;
}

但是c++中会将这个this指针给隐藏起来,也不允许用户将this指针作为显示参数,只能由编译器将this指针作为隐式参数。

例如这个c++代码:

void stack::Push(stack* this,STDataType e)
{
	this->_stack[this->_top++] = e;
}

将this指针作为显示参数就导致编译出错
在这里插入图片描述
虽然this指针不能作为成员函数的显示参数,但是this指针可以在函数内部显示使用。

void stack::Push(STDataType e)//入栈操作
{
	this->_stack[this->_top++] = e;
}

那么这个this指针的作用是什么,关看前面的陈述感觉有点不知所云,抛去与C语言的差异,下面就来总结一下:
比如将stack类实例化出一个对象。

typedef int STDataType;
class stack {
public:
	stack(int capacity=4);//栈的初始化

	STDataType Top();//取栈顶元素

	bool Empty();//判断栈是否为空

	void Pop();//弹出栈顶

	void Push(STDataType e);//入栈操作

	~stack();

private:
	STDataType* _stack;
	int _top;
	int _capacity;
};

int main()
{
	stack s1;
	s1.Push(1);//调用成员函数
}
	return 0;
}

(1)调用成员函数时,由编译器生成一个this指针,这个this指针的生命周期在成员函数的调用开始到调用结束。在调试中能看到这个指针。
在这里插入图片描述
在调用成员函数之前,不存在this指针。
在这里插入图片描述
(2)进入成员函数后,this指针由编译创建,而且this指针指向的是对象本体的地址(&s1==this)。

当成员函数调用结束后,this指针被编译器销毁。
在这里插入图片描述
(3)this指针不能作为成员函数的参数,前面已经演示过了。

(4)this指针可以在函数体内使用,this指针是指向对象的指针(由s1调用的成员函数就是指向s1的this指针)。
在这里插入图片描述

上述的例子,无论有没有this函数,都能完成一样的操作,那么为什么要有这个this指针呢?他的作用会在重载赋值函数中讲到。

拷贝构造函数

构造函数分为三种:默认构造函数、拷贝构造函数、其他构造函数,其他两种构造函数都在前面的文章中讲过了、现在来说第三种构造函数:拷贝构造函数。

拷贝构造函数能从其他构造函数中分离出来、一定有点独特的特性。拷贝构造函数的声明规则如下
(1)函数名与类名相同
(2)无返回值
(3)参数必须是相同类的对象的引用或者const形式的对象的引用

以日期类Date为例,其拷贝构造函数的声明如下:

	Date(Date& d);//类类型的引用

那么拷贝构造函数的作用是什么呢?既然它的名字叫作拷贝构造,那么肯定又从两个方面理解它的作用
(1)拷贝:
什么叫做拷贝?相信大家对快捷键ctrl+c,ctrl+v很了解了吧,将一个数据复制到另一个数据当中就是拷贝。那么拷贝构造函数的第一个作用就是让一个对象,与另一个对象具有相同的值。
(2)构造:
拷贝构造函数是一个构造函数,那么他一定是在创建对象时调用的函数。因此我们可以得出一个结论:拷贝构造函数是创建在一个对象时,调用的函数,目的是为了拷贝相同类的对象。

Date类的拷贝构造如下:

class Date {
public:
	Date(Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	
	int _year;
	int _month;
	int _day;
};

那么这个函数该如何调用呢?拷贝构造函数需要在对象初始化时使用。这该如何理解呢?

注意这句话的关键词,初始化。
相信大家对下面这条初始化指令不陌生。

	int i = 1;
	int j = i;//初始化拷贝
	i++;
	j=i;//赋值

i被初始化成1,j的初始化是把i拷贝给J,这叫做初始化时的拷贝。而后面j=i还是不是初始化时拷贝呢?当然不是了,这叫做赋值操作。

那么类的拷贝构造函数如何调用呢?
方法1:传参构造调用

class Date {
public:
	Date(int year = 2024, int month = 4, int day = 13)
	{
		_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;
};

    Date d1;
	Date d2(d1);

此时d2的值会和d1一模一样。完成拷贝
在这里插入图片描述
方法二:重载运算符调用。
c++中将拷贝构造函数与赋值运算符进行了重载。因此我们可以直接使用重载运算符调用拷贝构造函数(注意,只有在初始化时调用的才是拷贝构造函数)

    Date d1;
	Date d2=d1;//这与Date d2(d1);等同

在这里插入图片描述
推荐使用第二种方法,毕竟在作用相同的情况下,使用重载运算符(=)调用显然更有效率。

很显然,被引用的对象在拷贝的过程中应该保证数据不被修改,因此可以在参数类型加上const。

	Date(const Date& d);

默认拷贝构造函数(浅拷贝)

若是没有定义拷贝构造函数,编译器也会生成默认的拷贝构造函数。例如:

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

int main()
{
	Date d1;
	Date d2=d1;//这与Date d2(d1);等同
	return 0;
}

在这里插入图片描述

可以发现,此时编译器并不会报错,而且还能实现数值的拷贝,那么编译器生成的拷贝构造函数有什么作用呢:

默认拷贝构造函数会将对象中的值,按照内存存储的字节序拷贝过去,用简要的话来说,就是将对象内的所有数据ctrl c+ctrl v给另一个对象、这种方法称为值拷贝,也叫做浅拷贝

那么既然默认的拷贝构造函数就能完成值拷贝,那么为什么我们还要自定义一个拷贝构造函数呢?

首先我们要明白两件事,什么是拷贝,什么是浅拷贝。浅拷贝可以解决简单结构情况下的数值拷贝,若是数据结构复杂还能继续使用浅拷贝吗?

我们以栈stack类为例

typedef int STDataType;
class stack {
public:
     stack(int capacity=4)
     {
	     _stack = (STDataType*)malloc(sizeof(STDataType) * capacity);
	     _capacity = capacity;
	     _top = 0;
     }
	stack(const stack& stack)
	{
		_stack =stack._stack;
		_top = stack._top;
		_capacity = stack._capacity;
	};
    void Push(STDataType e)//入栈操作
    {
	_stack[this->_top++] = e;
    }

    ~stack()
    {
	    free(_stack);
	    _top = 0;
	    _stack = NULL;
	    _capacity = 0;
    }
private:
	STDataType* _stack;
	int _top;
	int _capacity;
};

int main()
{
	stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);

	stack s2 = s1;
	s1.Push(4);
	return 0;
}



这个类中的拷贝构造就是浅拷贝。乍一看这个拷贝函数没有问题,编译也能通过,但是一旦运行,就会导致很多问题出现。

问题1:
先来看看s2关于s1的拷贝有没有问题,我们对程序进行调试,令程序执行到s2的初始化之后。
在这里插入图片描述
打开调试窗口,可以发现s2对于s1的拷贝没有问题。s2的栈初始化与s1的结构无异。
在这里插入图片描述
接着执行下一个指令,下一个指令是向s1的栈顶压入一个4.
在这里插入图片描述
通过调试代码,可以发现对s1的栈的操作居然影响了栈s2,这对不对啊?当然不对了,拷贝是将对象生成一个副本。对对象的操作不应该会影响副本。这才是真正的拷贝。

就好比我将某个txt文件拷贝一份,然后在对原文件进行修改,原文件的修改不应该会影响拷贝的文件。
在这里插入图片描述

在这里插入图片描述
ok这是第一个问题,我们接着运行代码。

问题2:
在这里插入图片描述
我们发现这个程序不单单不满足需求,甚至导致了程序的崩溃,这是什么原因呢?

在这里插入图片描述
由于设计的拷贝构造函数是浅拷贝(也就是单纯的值拷贝),s1与s2的栈(_stack)的地址竟然是同一个,也就是说对象s1和对象s2竟然共用一个栈,而类stack的析构函数会将成员_stack进行释放,那么当s1和s2离开生命周期后,会分别将_stack内的资源进行释放,而s1和s2中的栈的地址是一致的,也就是将这个地址的动态内存释放两次,一个动态内存释放一次之后就不再属于动态内存空间了,于是对这块区域进行再次释放的行为会导致程序崩溃的原因。

既然浅拷贝不行,那么就来谈谈深拷贝是什么

深拷贝

通过分析,stack类的拷贝构造函数不能是浅拷贝的原因是由于对象会共用一个栈导致的,那么如果我们能将上述的s2对于s1的拷贝方式是创建一个栈,将s1的栈内数据拷贝过来,就能完美解决这个问题。

	stack(const stack& stack)
	{
		_stack = (STDataType*)malloc(sizeof(STDataType) * stack._capacity);
		//创建一个与拷贝对象相同大小的栈。
		_capacity = stack._capacity;
		_top = stack._top;

		memcpy(_stack, stack._stack, sizeof(STDataType) * stack._capacity);
		//将拷贝对象的栈的内容拷贝过来
	}

这个拷贝构造与浅拷贝的不同之处在于,将拷贝对象的栈复制了一个一模一样的栈,但是这个栈由于是malloc函数开辟出来的,他们的结构虽然一致,但是地址不同。因此不会出现共用一个栈的情况。

我们打开调试
在这里插入图片描述
这时候两个栈的结构一致,但是栈的地址不相同,因此对s1的操作不会影响s2,深拷贝可以完成栈的拷贝构造操作。
在这里插入图片描述

拷贝构造函数的其他应用

并不是只有创建对象时才能使用拷贝构造函数,在一下场景中也会使用拷贝构造函数:

(1)当参数为类的对象时
(2)当返回值为类的对象时

我们还是以Date类为例,写一个函数:

Date func(Date d1)
{
	//省略……
	return d1;
}

调用这个函数

d.func(d);

如何知道这个参数和返回值会调用拷贝构造函数呢?我们将拷贝构造函数修改一下:

	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		cout << "调用拷贝构造函数一次" << endl;
	}

那么,当调用func函数时,若是参数会调用一次拷贝构造函数,就会在终端显示调用一次,当返回值调用一次拷贝构造函数时,也会在终端显示调用一次。函数的调用结果如下:
在这里插入图片描述
由此可得,若是函数的参数是实参的临时拷贝时,会调用拷贝构造函数,将实参对象d拷贝给临时对象d1,当d1作为返回值时,也会生成一个拷贝对象作为函数的返回值,因此又调用一次拷贝构造函数将拷贝对象传递。

为什么参数和返回值会产生拷贝呢?首先我们要清楚函数的形式参数和返回值的作用原理。

(1)函数的形参、是实参的一份临时拷贝,当调用函数时,函数会将形参进行初始化、初始化的值是实参的拷贝、我们注意这两个关键词:初始化、拷贝,在前文说明拷贝构造函数的作用是在对象初始化时进行拷贝,那么若是对象时函数的形参,在函数开始时将形参进行初始化并拷贝实参的过程、实际上就是一个对象的初始化拷贝的过程、于是编译器会调用拷贝构造函数来初始化形参。

(2)那么函数的返回值又为什么要拷贝一个对象呢?在我们大多数学习返回值时,我们对返回值的认识大多停留在返回值就是将值进行传递给调用者的一个过程。但是我们要注意一个规则:

函数的形参的声明周期只在函数内部、当函数结束并将返回值传递时、已经离开了函数、也就是形参已经离开了生命周期,那么编译器就会将这个形参销毁,那么形参已经被销毁了,他又该如何返回这个形参的值呢?

当函数在返回值之前,编译器会将这个返回值拷贝一份、然后将这个拷贝的值给调用者。这样就不会因为返回的值超出了变量的生命周期而销毁。那么再注意关键词:拷贝。那么编译器又要将这个返回值进行初始化并拷贝的过程。于是调用了一次拷贝构造函数。

杂谈

关于这部分的内容博主实在想不出标题名了,那么就当做杂谈吧。如果读者有C语言编程基础的话,那么应该能想到另外两种实现对象初始化时拷贝的函数。

一种是值传递的拷贝构造函数

	Date(const Date d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

这在c++中是不可行的,它可以是一个拷贝函数、但不能成为一个拷贝构造函数。这是为什么呢?在前文中提到了,值传递的形参、是实参的一份临时拷贝。它会在生成时调用拷贝构造函数。

有没有人反应过来问题所在之处,拷贝构造函数要调用拷贝构造函数。这就是不可行的原因了。拷贝构造函数调用拷贝构造函数、拷贝构造函数调用的拷贝构造函数调用拷贝构造函数……。这已经无限套娃了啊,如此反复递归调用。最后会因为栈溢出而程序崩溃。所以c++也直接禁止了这种行为。若是拷贝构造函数出现值传递,会导致编译错误。
在这里插入图片描述
(2)指针传递的拷贝构造函数

    Date(const Date* d)
	{
		_year = d->_year;
		_month = d->_month;
		_day = d->_day;
	}

这个函数时可以实现拷贝构造的作用的,但是它不是拷贝构造函数,它是一个其他构造函数。为什么这么说呢?我们先来想想拷贝构造函数的两大调用方式

Date func(Date d1)
{
	//省略……
	return d1;
}

int main()
{
	Date d1;
	Date d2(&d1);//方法可行
	Date d3 = &d2;//方法也可行
	d1.func(d1);//方法不可行
}

d2拷贝d1可行这是因为满足了构造函数的调用规则,d3拷贝d2也可行,但是可读性非常差,将对象的地址赋值给对象?这看起来就不合理(但是编译能过哈哈)。而为什么说它不是一个拷贝构造函数呢?来看调用func函数的结果。

前文说了,调用func函数时、编译器会在拷贝实参,以及返回函数返回值时调用两次拷贝构造函数,但是实际测试过后可以发现并没有调用该函数(方法参考前文)。这就说明编译器没有将传址调用的拷贝函数当成拷贝构造函数。

由此得出以下结论,拷贝构造函数的形式只有一个。那就是类类型的引用作为唯一参数的构造函数,才是拷贝构造函数

类名(类类型的引用)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

代码小豪

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

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

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

打赏作者

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

抵扣说明:

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

余额充值