一.赋值函数是什么?
1.1 运算符的重载
运算符的重载实际是一种特殊的函数重载,必须定义一个函数,并告诉C++编译器,当遇到该重载的运算符时调用此函数。这个函数叫做运算符重载函数,它通常为类的成员函数。
定义运算符重载函数的一般格式如下:
返回值类型 类名::operator重载的运算符(参数表)
{
……
}
operator是关键字,它与重载的运算符一起构成函数名。因函数名的特殊性,C++编译器可以将这类函数识别出来。
1.2 赋值函数
赋值函数是一个赋值运算符重载函数,其格式如下:
返回值类型 类名::operator=(参数表)
{
……
}
二.赋值函数如何实现?
2.1 缺省赋值函数
2.1.1 格式
如果类中没有给出定义,系统会自动提供缺省赋值函数。缺省的赋值操作格式对所有类是固定的。其格式如下:
A& operator=(const A&);
2.1.2 C++代码示例
#include <iostream>
using namespace std;
class CStudent{
public:
CStudent(int age = 0,int score = 0);
~CStudent(void);
CStudent(const CStudent &stu);
void print_info(void);
private:
int age;
int score;
};
CStudent::CStudent(int age,int score)
{
cout<<"Constructor!"<<endl;
this->age = age;
this->score = score;
}
CStudent::~CStudent(void)
{
cout<<"Desconstructor!"<<endl;
}
CStudent::CStudent(const CStudent &stu)
{
cout<<"Copy constructor!"<<endl;
age = stu.age;
score = stu.score;
}
void CStudent::print_info(void)
{
cout<<"age: "<<age<<endl;
cout<<"score: "<<score<<endl;
}
int main(int argc, char** argv)
{
CStudent stu(10,80);
CStudent stu1;
stu1 = stu;
stu1.print_info();
return 0;
}
2.1.3 运行结果
如下图所示。
由下图可知:用已存在的对象stu赋值给已存在的对象stu1时,stu的数据成员值拷贝给了stu1对应的数据成员。
2.1.4 汇编代码分析
2.1.4.1 构造函数的汇编代码
构造函数如下:
CStudent::CStudent(int, int)
它对应的汇编代码如下:
<+0>: push %rbp
<+1>: mov %rsp,%rbp
<+4>: sub $0x20,%rsp
<+8>: mov %rcx,0x10(%rbp)//存储对象stu/stu1的地址到栈中
<+12>: mov %edx,0x18(%rbp)//存储实参age的值到栈中
<+15>: mov %r8d,0x20(%rbp)//存储实参score的值到栈中
<+19>: lea 0x86ab6(%rip),%rdx # 0x488000
<+26>: mov 0x8b17f(%rip),%rcx # 0x48c6d0 <.refptr._ZSt4cout>
<+33>: callq 0x46edd0 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc>
<+38>: mov 0x8b183(%rip),%rdx # 0x48c6e0 <.refptr._ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_>
<+45>: mov %rax,%rcx
<+48>: callq 0x44d4c0 <_ZNSolsEPFRSoS_E>
<+53>: mov 0x10(%rbp),%rax//从栈中取出对象stu/stu1的地址
<+57>: mov 0x18(%rbp),%edx//从栈中取出实参age的值
<+60>: mov %edx,(%rax)//对应this->age = age;
<+62>: mov 0x10(%rbp),%rax//从栈中取出对象stu/stu1的地址
<+66>: mov 0x20(%rbp),%edx//从栈中取出实参score的值
<+69>: mov %edx,0x4(%rax)//对应this->score = score;
<+72>: add $0x20,%rsp
<+76>: pop %rbp
<+77>: retq
分析如下:
(1)下面几行代码将对象地址和实参值压入了堆栈中:
<+8>: mov %rcx,0x10(%rbp)//存储对象stu/stu1的地址到栈中
<+12>: mov %edx,0x18(%rbp)//存储实参age的值到栈中
<+15>: mov %r8d,0x20(%rbp)//存储实参score的值到栈中
(2)“this->age = age”语句由下面几行代码实现:
<+53>: mov 0x10(%rbp),%rax//从栈中取出对象stu/stu1的地址
<+57>: mov 0x18(%rbp),%edx//从栈中取出实参age的值
<+60>: mov %edx,(%rax)//对应this->age = age;
(3)“this->score = score”语句由下面几行代码实现:
<+62>: mov 0x10(%rbp),%rax//从栈中取出对象stu/stu1的地址
<+66>: mov 0x20(%rbp),%edx//从栈中取出实参score的值
<+69>: mov %edx,0x4(%rax)//对应this->score = score;
PS:其实,这里的rax寄存器已经保存了对象的地址,不需要从栈中重新取地址,<+62>的语句可不要。这也表明了编译器的局限性。
2.1.4.2 main函数的汇编代码
main函数对应的汇编代码如下:
<+0>: push %rbp
<+1>: push %rbx
<+2>: sub $0x48,%rsp
<+6>: lea 0x80(%rsp),%rbp
<+14>: mov %ecx,-0x20(%rbp)
<+17>: mov %rdx,-0x18(%rbp)
<+21>: callq 0x40e910 <__main>
<+26>: lea -0x50(%rbp),%rax//取栈偏移50的地址传送给rax,实质是对象stu的地址
<+30>: mov $0x50,%r8d //将80传送给r8d
<+36>: mov $0xa,%edx//将10传送给edx
<+41>: mov %rax,%rcx//将对象stu的地址保存到rcx中,传给构造函数,实质就是this指针
<+44>: callq 0x401530 <CStudent::CStudent(int, int)>
<+49>: lea -0x60(%rbp),%rax//取栈偏移60的地址传送给rax,实质是对象stu1的地址
<+53>: mov $0x0,%r8d//将0传送给r8d
<+59>: mov $0x0,%edx//将0传送给edx
<+64>: mov %rax,%rcx//将对象stu1的地址保存到rcx中,传给构造函数,实质就是this指针
<+67>: callq 0x401530 <CStudent::CStudent(int, int)>
<+72>: mov -0x50(%rbp),%rax//取出stu.age和stu.score的值,传送给rax
<+76>: mov %rax,-0x60(%rbp)//相当于stu1.age = stu.age,stu1.score = stu.score
<+80>: lea -0x60(%rbp),%rax
<+84>: mov %rax,%rcx
<+87>: callq 0x401606 <CStudent::print_info()>
<+92>: mov $0x0,%ebx
<+97>: lea -0x60(%rbp),%rax
<+101>: mov %rax,%rcx
<+104>: callq 0x40157e <CStudent::~CStudent()>
<+109>: lea -0x50(%rbp),%rax
<+113>: mov %rax,%rcx
<+116>: callq 0x40157e <CStudent::~CStudent()>
<+121>: mov %ebx,%eax
<+123>: jmp 0x40172d <main(int, char**)+168>
<+125>: mov %rax,%rbx
<+128>: lea -0x60(%rbp),%rax
<+132>: mov %rax,%rcx
<+135>: callq 0x40157e <CStudent::~CStudent()>
<+140>: jmp 0x401716 <main(int, char**)+145>
<+142>: mov %rax,%rbx
<+145>: lea -0x50(%rbp),%rax
<+149>: mov %rax,%rcx
<+152>: callq 0x40157e <CStudent::~CStudent()>
<+157>: mov %rbx,%rax
<+160>: mov %rax,%rcx
<+163>: callq 0x40f630 <_Unwind_Resume>
<+168>: add $0x48,%rsp
<+172>: pop %rbx
<+173>: pop %rbp
<+174>: retq
分析如下:
(1)如下几行代码实现“CStudent stu”语句:
<+26>: lea -0x50(%rbp),%rax//取栈偏移50的地址传送给rax,实质是对象stu的地址
<+30>: mov $0x50,%r8d //将80传送给r8d
<+36>: mov $0xa,%edx//将10传送给edx
<+41>: mov %rax,%rcx//将对象stu的地址保存到rcx中,传给构造函数,实质就是this指针
<+44>: callq 0x401530 <CStudent::CStudent(int, int)>
(2)如下几行代码实现“CStudent stu1”语句:
<+49>: lea -0x60(%rbp),%rax//取栈偏移60的地址传送给rax,实质是对象stu1的地址
<+53>: mov $0x0,%r8d//将0传送给r8d
<+59>: mov $0x0,%edx//将0传送给edx
<+64>: mov %rax,%rcx//将对象stu1的地址保存到rcx中,传给构造函数,实质就是this指针
<+67>: callq 0x401530 <CStudent::CStudent(int, int)>
(3)如下几行代码实现“stu1 = stu”语句:
<+72>: mov -0x50(%rbp),%rax//取出stu.age和stu.score的值,传送给rax
<+76>: mov %rax,-0x60(%rbp)//相当于stu1.age = stu.age,stu1.score = stu.score
<+80>: lea -0x60(%rbp),%rax
注意:rax是64位寄存器,即8字节,而age、score各占4字节空间。所以,<+72>行的代码是同时将age和score的值传送给rax。
2.2 自定义赋值函数
在某些情况下,缺省赋值函数对类与对象的安全性和处理的正确性还不够,这时就要求类的设计者提供自定义的赋值函数。
2.2.1 格式
赋值函数的一般形式如下:
class CStudent{
public:
CStudent& operator=(const CStudent& stu);
private:
int age;
int score;
};
CStudent& CStudent::operator=(const CStudent& stu)
{
...
}
2.2.2 C++代码示例
#include <iostream>
using namespace std;
class CStudent{
public:
CStudent(int age = 0,int score = 0);
~CStudent(void);
CStudent(const CStudent &stu);
//赋值函数
CStudent& operator=(const CStudent& stu);
void print_info(void);
private:
int age;
int score;
};
CStudent::CStudent(int age,int score)
{
cout<<"Constructor!"<<endl;
this->age = age;
this->score = score;
}
CStudent::~CStudent(void)
{
cout<<"Desconstructor!"<<endl;
}
CStudent::CStudent(const CStudent &stu)
{
cout<<"Copy constructor!"<<endl;
age = stu.age;
score = stu.score;
}
CStudent& CStudent::operator=(const CStudent& stu)
{
cout<<"Assignment!"<<endl;
if(this != &stu)
{
age = stu.age;
score = stu.score;
}
return *this;
}
void CStudent::print_info(void)
{
cout<<"age: "<<age<<endl;
cout<<"score: "<<score<<endl;
}
int main(int argc, char** argv)
{
CStudent stu(10,80);
CStudent stu1;
stu1 = stu;
stu1.print_info();
return 0;
}
2.2.3 运行结果
如下图所示。
执行"stu1 = stu"时,调用了一次赋值函数。
2.2.4 汇编代码分析
2.2.4.1 main函数汇编代码对比
下图是使用缺省赋值函数和自定义赋值函数时,main函数的汇编代码对比。
差异部分代码在红框中,即是“stu1 = stu”语句的汇编实现有区别,其余实现相同。
差异部分代码如下:
<+72>: lea -0x50(%rbp),%rdx//对象stu的地址传送给rdx
<+76>: lea -0x60(%rbp),%rax//对象stu1的地址传送给rax
<+80>: mov %rax,%rcx
<+83>: callq 0x401606 <CStudent::operator=(CStudent const&)>
下面重点分析赋值函数。
2.2.4.2 自定义赋值函数
赋值函数“CStudent::operator=(CStudent const&)”的汇编代码如下:
<+0>: push %rbp
<+1>: mov %rsp,%rbp
<+4>: sub $0x20,%rsp
<+8>: mov %rcx,0x10(%rbp) //将对象stu1的地址压入栈中
<+12>: mov %rdx,0x18(%rbp)//将对象stu的地址压入栈中
<+16>: lea 0x86a12(%rip),%rdx # 0x48802f
<+23>: mov 0x8b0ac(%rip),%rcx # 0x48c6d0 <.refptr._ZSt4cout>
<+30>: callq 0x46ee40 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc>
<+35>: mov 0x8b0b0(%rip),%rdx # 0x48c6e0 <.refptr._ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_>
<+42>: mov %rax,%rcx
<+45>: callq 0x44d530 <_ZNSolsEPFRSoS_E>
<+50>: mov 0x10(%rbp),%rax//从栈中取出对象stu1的地址,传送给rax
<+54>: cmp 0x18(%rbp),%rax//if(this != &stu)的实现
<+58>: je 0x40165c <CStudent::operator=(CStudent const&)+86>
<+60>: mov 0x18(%rbp),%rax//从栈中取出对象stu的地址,传送给rax
<+64>: mov (%rax),%edx//stu.age的值传送给edx
<+66>: mov 0x10(%rbp),%rax//从栈中取出对象stu1的地址,传送给rax
<+70>: mov %edx,(%rax)//stu1.age = stu.age(edx是4字节)
<+72>: mov 0x18(%rbp),%rax//从栈中取出对象stu的地址,传送给rax
<+76>: mov 0x4(%rax),%edx//stu.score的值传送给edx
<+79>: mov 0x10(%rbp),%rax//从栈中取出对象stu1的地址,传送给rax
<+83>: mov %edx,0x4(%rax)//stu1.score = stu.score(edx是4字节)
<+86>: mov 0x10(%rbp),%rax//从栈中取出对象stu1的地址,传送给rax,作为函数返回值
<+90>: add $0x20,%rsp
<+94>: pop %rbp
<+95>: retq
分析如下:
(1)赋值函数传入了stu和stu1两个对象的地址:
<+8>: mov %rcx,0x10(%rbp) //将对象stu1的地址压入栈中
<+12>: mov %rdx,0x18(%rbp)//将对象stu的地址压入栈中
由此可知:
stu1 = stu;
等价于:
stu1.operator=(stu);
进一步等价于:
operator=(&stu1,stu);
所以,“operator=”实质就是一个函数名。
(2)返回值由rax传送,返回对象引用的汇编代码如下:
<+86>: mov 0x10(%rbp),%rax//从栈中取出对象stu1的地址,传送给rax,作为函数返回值
三.为什么赋值函数要返回对象的引用?
自定义赋值函数也可以返回void类型,如下:
class CStudent{
public:
void operator=(const CStudent& stu);
private:
int age;
int score;
};
void CStudent::operator=(const CStudent& stu)
{
...
}
以引用返回是为了实现连续赋值,如:
a = b = c;
它等价于:
a.operator=(b.operator=(c));
如果赋值函数返回void类型,则变成:
a.operator=(void);
编译器会报错。
四.赋值函数何时调用?
赋值函数只能被已经存在的对象调用。