C++赋值运算符的“生死线“:为什么必须返回引用?this指针的幕后操控

目录

赋值运算符函数

赋值运算符函数的形式

this指针

this指针的本质

this指针存在哪儿

this指针的生命周期

理解以下问题:

赋值运算符函数的定义

思考:

总结步骤—— 四步走(重点):

赋值运算符函数的形式探究*

赋值运算符函数的返回必须是一个引用吗?

2. 赋值操作符函数的返回类型可以是void吗?

3. 赋值操作符函数的参数一定要是引用吗?

 4. 赋值操作符函数的参数必须是一个const引用吗?


赋值运算符函数

赋值运算同样是一种很常见的运算,比如:

int x = 1, y = 2;
x = y;

自定义类型当然也需要这种运算,比如:

Point pt1(1, 2), pt2(3, 4);pt1 = pt2;//赋值操作

我们想上面的操作不符合构造函数(因为没有创建新的对象),也不符合拷贝构造的三种出现方式,所以是一个赋值操作。

在执行 pt1 = pt2; 该语句时, pt1 与 pt2 都存在,所以不存在对象的构造,这要与 Point pt2 =pt1; 语句区分开,这是不同的。

赋值运算符函数的形式

在上述例子中,当 = 作用于对象时,其实是把它当成一个函数来看待的。在执行 pt1 = pt2; 该语句时,需要调用的是赋值运算符函数。其形式如下:

类名& operator=(const 类名 &)

对Point类进行测试时,会发现我们不需要显式给出赋值运算符函数,就可以执行测试。这是因为如果类中没有显式定义赋值运算符函数时,编译器会自动提供一个赋值运算符函数。对于 Point 类而言,其实现如下:

Point & Point::operator=(const Point & rhs)
{
	_ix = rhs._ix;
	_iy = rhs._iy;
}

手动写出赋值运算符,再加上函数调用的提示语句。执行发现语句被输出,也就是说,当对象已经创建时,将另一个对象的内容复制给这个对象,会调用赋值运算符函数

那么现在又产生了问题

首先,赋值号是一个双目运算符,如果把它视为一个函数,那么应该有两个参数。但是从赋值运算符函数的形式上看只接收了一个参数,为什么?

其次,赋值运算符函数返回类型是Point&,那么它的返回值是什么?

这也是上面代码报警告的原因。

这两个问题引出了一个重要的知识点——this指针

image-20240308101159658

因为是指针常量不能指向其他对象,下面的方式是不允许的。

this指针
  • this指针的本质

this指针的本质是一个指针常量 Type* const pointer; 它储存了调用它的对象的地址,不可被修改。这样成员函数才知道自己修改的成员变量是哪个对象的。

this是一个隐藏的指针,可以在类的成员函数中使用,它可以用来指向调用对象。当一个对象的成员函数被调用时,编译器会隐式地传递该对象的地址作为 this 指针。

this指针指向本对象

  • this指针存在哪儿

编译器在生成程序时加入了获取对象首地址的相关代码,将获取的首地址存放在了寄存器中,这就是this指针。

对this取地址也是不允许的因为他是存储在寄存器中的。

  • this指针的生命周期

this 指针的生命周期开始于成员函数的执行开始。当一个非静态成员函数被调用时,this 指针被自动设置为指向调用该函数的对象实例。在成员函数执行期间,this 指针一直有效。它可以被用来访问调用对象的成员变量和成员函数。this指针的生命周期结束于成员函数的执行结束。当成员函数返回时,this指针的作用范围就结束了。

要注意,this指针的生命周期与它所指向的对象的生命周期虽然并不完全相同,但是是相关的。

this指针本身只在成员函数执行期间存在,但它指向的对象可能在成员函数执行前就已经存在,并且在成员函数执行后继续存在。

如果成员函数是通过一个已经销毁或未初始化的对象调用的,this指针将是悬挂的,它的使用将会是未定义行为。

image-20240308101909443

理解以下问题:
  1. 对象调用函数时,是如何找到自己本对象的数据成员的? —— 通过this指针

  2. this指针代表的是什么? —— 指向本对象

  3. this指针在参数列表中的什么位置? —— 参数列表的第一位(默认自动加入)

  4. this指针的形式是什么? —— 类名 * const this (指针常量)

Point & operator=(const Point & rhs){
    this->_ix = rhs._ix;
    this->_iy = rhs._iy;
    cout << "Point & operator=(const Point &)" << endl;
    return *this;
}

成员函数中可以加上this指针,展示本对象通过this指针找到本对象的数据成员。但是不要在参数列表中显式加上this指针,因为编译器一定会在参数列表的第一位加上this指针,如果显式再给一个,参数数量就不对了。

赋值运算符函数的定义

注意:如果对象的指针数据成员申请了堆空间,默认的赋值运算符函数就不够用了,以Computer类为例,默认的赋值运算符函数长这样

Computer & operator=(const Computer & rhs){
    this->_brand = rhs._brand;
    this->_price = rhs._price;
    return *this;
}

这里的指针成员_brand是进行的浅拷贝,会造成问题

image-20240308102452334

思考:

如果直接进行深拷贝,可行吗?

image-20240308102956579

会有内存泄露,需要回收掉pc2._brand原本申请的空间

如果用delete回收掉pc2._brand原本申请的空间,再进行深拷贝,是否可行?—— 还要考虑自复制

如果没有自复制的判断,发现我们已经将原来的内容回收了,不可以进行访问了,就会出现下面这种情况,虽然编译器也不会报错。

image-20240308111319344

先将原来指针指向的堆空间的内容回收,但是指针是存在的,然后利用这个指针重新申请可以空间用来存放copy的内容。

总结步骤—— 四步走(重点):
  1. 考虑自复制问题

  2. 回收左操作数原本申请的堆空间

  3. 深拷贝(以及其他的数据成员的复制)

  4. 返回*this

<span style="background-color:#f8f8f8"><span style="color:#333333"><span style="color:#000000">Computer</span> <span style="color:#981a1a">&</span> <span style="color:#770088">operator</span><span style="color:#981a1a">=</span>(<span style="color:#770088">const</span> <span style="color:#000000">Computer</span> <span style="color:#981a1a">&</span> <span style="color:#000000">rhs</span>){
    <span style="color:#770088">if</span>(<span style="color:#770088">this</span> <span style="color:#981a1a">!=</span> <span style="color:#981a1a">&</span><span style="color:#000000">rhs</span>){
        <span style="color:#770088">delete</span> [] <span style="color:#000000">_brand</span>;
        <span style="color:#000000">_brand</span> <span style="color:#981a1a">=</span> <span style="color:#770088">new</span> <span style="color:#008855">char</span>[<span style="color:#000000">strlen</span>(<span style="color:#000000">rhs</span>.<span style="color:#000000">_brand</span>)]();
        <span style="color:#000000">strcpy</span>(<span style="color:#000000">_brand</span>,<span style="color:#000000">rhs</span>.<span style="color:#000000">_brand</span>);
        <span style="color:#000000">_price</span> <span style="color:#981a1a">=</span> <span style="color:#000000">rhs</span>.<span style="color:#000000">_price</span>;
    }
    <span style="color:#770088">return</span> <span style="color:#981a1a">*</span><span style="color:#770088">this</span>;
}</span></span>

当没有return *this时,我们发现可以进行单次赋值

当我们在进行连续赋值时,是不可以的

但我们将上面的注释取消,就可以进行连续赋值,这是因为赋值运算是从右面开始的pt赋值给pt2,返回值为pt2的this指针指向pt2,然后再与pt3进行赋值运算。

赋值运算符函数的形式探究*

关于赋值运算符函数的形式探究也是面试中比较可能出现的问题,以下提出四个思考:

  1. 赋值运算符函数的返回必须是一个引用吗?
Computer operator=(const Computer & rhs)
{
    ……
    return *this;
}

—— 符合拷贝构造的调用时机会造成一次多余的拷贝,增加不必要的开销

2. 赋值操作符函数的返回类型可以是void吗?
void operator=(const Computer & rhs)
{
    ……
}

—— 无法处理连续赋值

3. 赋值操作符函数的参数一定要是引用吗?
Computer & operator=(const Computer rhs)   
{
	……
	return *this;
}

—— 会造成一次多余的拷贝,增加不必要的开销

注意:此时讨论的是赋值运算符函数的参数形式,前提是拷贝构造是正常的,如果拷贝构造没有加const就会出问题。

 4. 赋值操作符函数的参数必须是一个const引用吗?
Computer & operator=(Computer & rhs)   
{
	……
	return *this;
}

—— 无法避免在赋值运算符函数中修改右操作的内容,不合理

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值