如果类中包含了使用new初始化的指针成员,应当定义复制构造函数和重载赋值运算符,以复制的数据,而不是让多个对象的指针成员指向同一片空间(被称为浅复制)。浅复制会导致指针指向已被释放的空间,出现悬空指针。
复制构造函数用于初始化过程,重载的赋值运算符用于常规赋值语句中。
下面举一个实例来做有关说明,该实例声明并定义一个stringbad类(表示有问题的string)。
//stringbad.h
#include<iostream>
#ifndef STRNGBAD_H
#define STRNGBAD_H
class stringbad{
private:
char*str;
int len;
static int num_strings; //静态类成员,用于计数
public:
stringbad(const char*s);
stringbad();
~stringbad();
friend std::ostream & operator<<(std::ostream&os,const stringbad&st);
};
#endif
声明中静态类成员不可在声明中初始化(除非使用const限定),静态类成员归属于类,而不属于具体的对象。下面是成员函数定义
//string.cpp
#include<cstring>
#include"stringbad.h"
using std::cout;
int stringbad::num_strings=0; //外部初始化静态成员为0
stringbad::stringbad(const char*s){ //构造函数
len=std::strlen(s);
str=new char[len+1];
strcpy(str,s);
num_strings++;
cout<<num_strings<<":\""<<str<<"\"对象被创建\n";
}
stringbad::stringbad(){ //默认构造函数
len=4;
str=new char[len+1];
strcpy(str,"C++");
num_strings++;
cout<<num_strings<<":\""<<str<<"\"对象创建失败\n";
}
stringbad::~stringbad(){
cout<<"\""<<str<<"\"被删除";
--num_strings;
cout<<num_strings<<"left\n";
delete [] str; //释放new分配的空间
}
std::ostream&operator<<(std::ostream & os,const stringbad&st){ //重载运算符便于直接用<<打印对象存储的字符串
os<<st.str;
return os;
}
下面是测试函数
//vegnews.cpp
#include<iostream>
#include<cstdlib>
using std::cout;
#include"stringbad.h"
void callme1(stringbad &); //按引用传递
void callme2(stringbad); //按值传递
int main(){
using std::endl;
{ //析构函数在作用域结束时自动调用,若无该括号分隔,等main函数结束时看不见结果输出
cout<<"首先开始初始化三个对象"<<endl;
stringbad h1("CSM");
stringbad h2("LP");
stringbad sp("SLBD");
cout<<"h1:"<<h1<<endl;
cout<<"h2:"<<h2<<endl;
cout<<"sp:"<<sp<<endl;
//接下来调用callme函数,比较两种参数传递方式的不同
callme1(h1);
cout<<"h1:"<<h1<<endl;
callme2(h2);
cout<<"h2:"<<h2<<endl;
cout<<"通过赋值初始化对象"<<endl;
stringbad sla=sp;
cout<<"sla:"<<sla<<endl;
cout<<"把对象赋给对象(非初始化)"<<endl;
stringbad kn;
kn=h1;
cout<<"kn:"<<kn<<endl;
cout<<"退出代码块"<<endl;
}
cout<<"main函数结束"<<endl;
getchar();
return 0;
}
void callme1(stringbad& rsb){
cout<<"stringbad对象按引用传递:\n";
cout<<" \""<<rsb<<"\"\n";
}
void callme2(stringbad sb){
cout<<"stringbad对象按值传递\n";
cout<<" \""<<sb<<"\"\n";
}
下面是运行结果
异常1
这里string对象按值传递,这里复制的并非字符串,而是指向字符串的指针。所以当h2按值传递给sb时,sb对象char*指针也指向h2对象指针指向的字符串。callme2函数结束,自动调用析构函数删除sb对象,指向字符串空间被释放,h2的char*指针变为悬空指针。
与此相区别的是按引用传递,函数操作的是对象本身,而非副本。所以在callme1函数运行结束时不会调用析构函数。
异常2
这里我们把h1赋给kn,两个char*指针指向同一片new分配的空间。最后退出代码块时,用new分配了四次空间,却调用一共六次析构函数(我的运行结果有异常,最后main函数结束的语句未出现,实际最后应有5次调用),num_strings值为-2,也有空间被反复释放,可能导致异常退出。
问题解决
解决该异常的方法是进行深度复制,即复制应把字符串副本赋给对象而非指针。
复制构造函数
stringbad::stringbad(const stringbad&st){
num_strings++;
len=st.len;
str=new char[len+1];
std::strcpy(str,st.str);
cout<<num_strings<<":\""<<str<<"\"对象被创建\n";
}
赋值运算符重载
stringbad&stringbad::operator=(const stringbad&st){
if(this==&st)
return *this;//避免自己给自己赋值导致new多余空间
delete [] str; //删除原本分配空间
len=st.len;
str=new char[len+1];
std::strcpy(str,st.str);
return *this;
}
修改后运行结果正确,如下
因为对象在栈中存储,满足先入后出,所以一次释放kn,sla,sp,h2,h1.