动态内存和类
如果有这样的strbad类:
#include<iosream>
#ifdef STRBAD_H_
#define SREBAD_H_
class strbad{
private:
char * str;
int len;
static int num;
public:
strbad(const char *s);
strbad();
~strbad();
friend std::ostream & operator<<(std::ostream &os,const strbad &st);
}
#endif
#include<cstring>
#include"stringbad.h"
using std::cout;
int strbad::num=0;//静态成员变量不能在声明中初始化
//除非静态变量是const或者枚举型
strbad::strbad(const char *s){
len=std::strlen(s);
str=new char[len+1];//str是指针,因此必须提供内存
std::strcopy(str,s);//不能用str=s,这样的话没有创建副本,而只是复制了指针
num++;
}
strbad::strbad(){
len=4;
str=new char[4];
std::strcopy(str,"C++");
num++;
}
strbad::~strbad(){
num--;
delete[] str;
//new对应delete,new[]对应delete[]
}
std::ostream & operator<<(std::ostream &os,const strbad &st){
os<<st.str;
return os;
}
看似设计没有什么问题,但下面的语句证明它其实大有问题:
{
strbad a=srebad("a");
call(a);
cout<<a;//事实证明在这里a已经被析构了
strbad b=a;
}
cout<<strbad::num;//发现num为负数
void call(strbad s);//这里是值传递
所有的原因在于忽略了类的复制构造函数。
strbad(const strbad &);
编译器自动生成的复制构造函数不知道如何处理num,导致num被弄乱了。
下面一些新建对象的情况会导致调用复制构造函数:
strbad ditto(motto);
strbad metto=motto;
strbad also=strbad(motto);
strbad * p=new strbad(motto);
当程序生成了对象副本时,会调用复制构造函数。具体比如:当函数按值传递对象(call方法)或函数返回对象时,都将使用复制构造函数。因此我们应该尽可能按引用传递对象。
默认复制构造函数逐个复制非静态成员,复制的是成员的值。
因此我们可以发现,在调用call方法时,s被a赋值,但由于类中存储的值为指针类型,因此使用在调用s的析构函数时会把s的指针(同样是a的指针)指向的动态内存释放,因此我们会发现a的动态内存已经被delete了。
我们可以定义一个显式复制构造函数:
strbad:strbad(const strbad & st){
num++;
len=st.len;
str=new char[len+1];
std::strcpy(str,st.str);
}
有些类成员是使用new初始化的、指向数据的指针,而不是数据本身,因此我们需要深度复制(改写默认复制构造函数)。
当然我们也要重载赋值运算符。
strbad b;
b=a;//不会触发复制构造函数,需要重载=
有以下注意点:
(1)目标对象可能引用了以前分配的数据,函数应使用delete来释放它们。
(2)函数应当避免将对象赋给自身,否则可能会导致在赋值前删除了自身的内容。
(3)函数返回一个指向调用对象的引用。
strbad & strbad::operator=(const strbad& st){
if (this==&st) return *this;
delete[] str;
len=st.len;
str=new char[len+1];
std::strcpy(str,st.str);
return *this;
}
如果赋值为str=0,这说明str为空指针,此时使用delete[] str是可行的。
可以将成员函数声明为静态的,这样就不能通过对象调用静态成员函数,甚至不能用this,静态成员函数也不与特定的对象相关联,因此只能使用静态数据成员。
构造函数中new的注意事项
有如下注意点:
(1)如果在构造函数中用new,应该在析构函数中用delete。
(2)new和delete必须兼容,new对应delete,new[]对应delete[]
(3)如果有多个构造函数,则必须用相同的方法new,要么都带中括号,要么都不带,因为只有一个析构函数。delete(无论有没有中括号)都可以用于空指针(0,NULL)
(4)应定义一个复制构造函数。
(5)应重载一个赋值运算符。
对于定位new运算符,有如下注意点:
pc1=new(buffer) strbad;
pc2=new(buffer) strbad;//no
pc2=new(buffer+sizeof(strbad)) strbad("aaa");//yes
再执行buffer的delete操作前,需要显式地调用析构函数:
pc1->~strbad();
pc2->~strbad();
delete[] buffer;//这样才算完全销毁
有关返回对象的说明
1.返回指向const对象的引用
如果函数返回传递给它的对象,可以通过返回引用来提高效率,因为返回引用不会调用复制构造函数。而且,引用指向的对象应该在调用函数执行时存在。除此以外,返回的类型必须为const才能匹配。
2.返回指向非const对象的引用
两种常见的情形是,重载赋值运算符和cout一起使用的<<。前者是提高效率,后者是必须那么做。
3.返回对象
如果被返回的对象是被调用函数中的局部变量,则不应按引用方式返回它。此时将使用复制构造函数来生成返回的对象。
4.返回const对象
net=force1+force2;
force1+force2=net;//返回const对象将不允许那么做
使用const能够防止那样奇怪的错误,当然返回的对象将会是const的。