=
运算符重载入手,总结默认的
成员赋值(浅拷贝)与深拷贝,最后探究程序中常见的对象赋值和拷贝。如有指正或补充,请不吝指出。
还是以一个例子辅以说明,这里使用一个自定义带编号字符串类MyString
,每一个字符串对象都由其编号和字符串内容唯一确定。
定义MyString
类后,看如下语句:
MyString str1("C++"); //使用构造器
//先声明,再赋值
MyString str2;
str2=str1;
//声明并初始化
MyString str3=str1;
上面str2
和str3
的两种赋值方式是否有区别?首先,从结果来看,它们是等效的,都是创建了一个字符串,并把str赋给它。但二者实现过程却具有本质的不同。前者是先使用无参构造器创建对象,再通过赋值运算把str1赋给该对象;后者是直接从复制构造器创建一个对象。因此,C++中的"初始化"特指声明的同时赋值。
编译器会在类没有重载=
运算符的时候提供默认的赋值运算操作,默认赋值只进行简单的成员赋值。如,在MyString
中,类这样:
/*MyString对象具有属性成员: id,len,str(char*)*/
MyString& MyString::operator=(const MyString& s){
id=s.id;
len=s.len;
str=s.str; //直接把指针赋值
}
所以,即使没有重载=
,类对象也能进行赋值,但这种赋值不一定安全。
复制构造器(copy constructor)是编译器默认为类添加的构造器。是的,编译器除了在没有构造器的时候提供默认无参构造器,还会在你没有提供复制构造器的时候提供默认的复制构造器。它的签名为:
Class Class(const Class&);//参数为const的对象引用
你也可以使用显式调用复制构造器
MyString str1;
MyString str2(str1); //使用复制构造器
MyString str3=MyString(str1); //同上
默认的复制构造器与默认的操作一样,只进行简单的成员赋值。
无论是默认的赋值操作还是复制构造器,都是进行对象之间的成员赋值。在只有基本类型的时候,这种赋值往往是可行的,但是如果具有指针成员的时候,就不安全了。赋值:str1=str2
,只是把str2.str
地址赋给str1.str
,导致两个指针指向同一个地址,这样一来,str1
字符串的修改就会影响到str2
的字符串,最后还会对同一个地址释放两次。总之,指针成员的复制会使两个对象共享一个指向目标,而不是独立的相同目标,因此,这种复制被称为浅拷贝。
为了避免这个问题,我们需要编写赋值运算函数与复制构造器来实现深拷贝。
重载具有指针成员的类的赋值运算符时,需要记住几点:
重载的一个好处是可以自定义操作内容,比如MyString
希望复制的时候不要改变原有的id,所以没有对id进行赋值,而是保留了原有。
根据以上两点,以及MyString
的设计逻辑,可以编写如下运算符函数:
MyString& MyString::operator=(const MyString& s){
if(this==&s) //防止自身赋值
return *this;
len=s.len;
delete[] str; //释放原有空间
str=new char[len+1]; //重新分配空间
strcpy(str,s.str); //复制字符串
}
复制构造器需要完成构造器的初始化任务,比赋值函数简单一些:
MyString::MyString(const MyString& s){
id=++count;
len=s.len;
str=new char[len+1];
strcpy(str,s.str);
}
这里使用一个例子演示程序中有一些常见的拷贝(包括赋值和构造器拷贝)。看看具体都使用了什么函数,同时也复习一下构造器与析构函数。为了捕捉哪些函数在什么时候被调用了,在各个函数中都使用了打印消息。
//mystring.h
#ifndef MYSTRING_H
#define MYSTRING_H
#include
class MyString{
private:
static int count; //静态成员记录对象数量
char * str;
int id;
int len;
public:
friend std::ostream& operator<<(std::ostream&, const MyString&);
MyString(const char* ="");
MyString(const MyString&);
~MyString();
MyString& operator=(const MyString&);
};
#endif
//mystring.cpp
#include
#include "mystring.h"
#include
int MyString::count=0;
MyString::MyString(const char* s){
id=++count;
len=strlen(s);
str=new char[len+1];
strcpy(str,s);
std::cout<<"(Create "<<*this<<")"<
//main.cpp
#include
#include "mystring.h"
using namespace std;
void fun(MyString);
MyString createString();
int main(){
cout<<"(Enter main)"<
输出如下:
你可以试着自己去用调用逻辑去解释一下输出。前面初始化与赋值已经讲过。
直接看把对象按值传递的时候。输出Copy 3 to 4
说明按值传递是使用复制构造器进行副本拷贝。
对象作为返回值的时候,没有发生拷贝行为(看输出)。说明被返回对象并不是传递了副本之后在函数本地被摧毁,而是实实在在传递了在本地声明的那个对象。它的声明周期与调用动作有关,如果返回后没有使用它,就被立刻摧毁。
(完。有一些是我基于测试之后的合理推断,如果有错误,望指正)