本文将介绍对类使用new和delete 以及如何处理由于 使用动态内存而引起的一些微妙问题,它将影响构造函数的析构函数的设计和运算符的重载。

注意:不要在类声明文件(.h文件)中初始化类静态成员。在包含类方法的文件(.cpp文件)中初始化。但是如果静态成员是整型或枚举类型const,则可以在类声明中初始化。(c因为onst 定义的常量属于局部作用域,不是静态变量所属的全局区)

下面代码在每个构造函数都有num++。使得每创建一个str对象,静态变量的值就+1记录对象总数。完整代码实现部分如下:

 

#ifndef STR_H
#define STR_H


class str
{
    public:
        str();
        str(const char *s);
        virtual ~str();
        friend std::ostream & operator<<(std ::ostream & os,const str & st);
        str(const str& st);
    protected:

    private:
        char * str;
        int len;
        static int num;
};

#endif // STR_H

  

#include "str.h"

str::str()
{
    //ctor
     int len=4;
    str =new char[4];
    std::strcpy(str,"C++");
    num++;
}
str::str(const char *s){
    len = std::strlen(s);
    str = new char[len+1];
    std::strcpy(str,s);
    num++;
}
str::~str()
{
    //dtor
    --num;
    delete[] str;
}
std::ostream & operator<<(std::ostream & os,const str &st){   //重载一下<<输出str对象
    os<<st.str;
    return os;
}
str & str::operator=( const str& st){
    if(this == &st)
        return *this;  //判断一下赋值的对象同一个则返回自身
        delete[] str;   //如果不同,把旧的字符串指的内存释放,如果此时不释放,
        //上面的字符串一直在内存中,因为str不指向它了,该内存浪费了,稍后把一个新的字符串地址赋给str,
    len = st.len;
    str = new char[len+1];
    std::strcpy(str,st.str);
    return *this;  //此次对象赋值会把旧的内存delete,而使str指向一个新new的字符串,不会创建新的对象因此num不会变化

}
int str::num=0;
str::str(const str& st){
    num++;
    len = st.len;
    str =new char [len +1];
    std::strcpy(str,st.str);
}
int main(){
    str sport("dsdsss");
}

  

分析: 类的成员str是一个指针,指向字符串。所以构造函数必须提供内存来存字符串,初始化对象时,给构造函数传一个字符串指针,(字符串可变)

Str(“String”);

 

构造函数必须分配足够的内存来存储字符串,然后把字符串复制到内存中

 

首先,用strlen()函数计算字符串的大小,并对成员len初始化,接着,用new分配足够的空间来保存字符串,然后新的内存地址(new函数返回值就是地址char  *)给str 成员

strlen()返回字符串长度,不包括末尾的空字符\0,所以得+1,使得分配的内存能存储包含了空字符的字符串。

 

构造函数用strcpy()将传递的字符串复制到新的内存,并更新对象计数num

 

必须知道字符串并不保存在对象中,单独保存在堆内存中(new的动态分配的内存都在堆区),对象中str成员指针仅保存了指出到哪里查找字符串的地址

 

默认构造函数,虽然无形参,但实现中strcpy 提供了一个默认字符串“C++”,复制到新建的内存中。

 

该析构函数成员str指向new分配的内存,Str对象过期了,指针也过期若无delete

但str指向new分配的内存仍被分配。删除对象可以释放对象本身占的内存,但并不能自动释放属于对象成员指针指向的内存。

 

所以必须用析构函数里确保对象过期了,由构造函数new分配的内存也被释放。)(两件事,不能用默认的析构函数)

 

总结:str成员是指向一块新建内存的指针。该内存长度运行时候确定,是传入字符串strlen()+1,构造函数中new一个内存,再用std空间的成员方法strcpy把形参的复制到新建内存中

若用默认析构函数只会使str对象消亡,不会使得成员str指向的堆内存的字符串释放空间

 

复制构造函数引出:

int main 中有如下语句:

    str sport("dsdsss");

 str  sailor=sports;                    //句子1

上面是把一个对象赋值给另一个对象是赋值运算符的重载吗 (不对)

句子1 不是调用默认也不是调用有参构造函数。

句子1 与 str  sailor =str(sports);    //句子2  等效

句2把对象作为实参传入str中,是有参构造函数调用吗?(不对)

若是有参构造函数,形参应该是str &,因为实参是个对象

正解:句子2原型是str(const str &)

这就是复制构造函数

复制构造函数形参是该类的引用

用于将一个对象复制到新创建的对象中。也就是说,它用于初始化过程,而不是常规的赋值过程

它接收一个对象的常量引用作为参数

对复制构造函数需要知道什么时候调用,有啥作用。下面是会调用复制构造函数几种形式

  1.  str ditto(motto); //该实参是一个对象,在实参传给形参过程中调用了复制构造函数
  2.  str metoo =motto;
  3.  str also =Str(motto);   //该函数返回值是一个对象

    4    str* ptring =new Str(motto);  // new出了一个匿名对象,并把这个对象地址给了pting这个指针

 

作用:

每当程序产生了一个对象副本,编译器都将使用复制构造函数

具体就是:按值传递对象,或返回对象都将用复制构造函数。按值传递意味着创建原始变量的一个副本

 

介绍了复制构造函数,

再说如果用默认复制构造函数对str类复制

会有什么问题?

str类有个成员是指针类型。

这样  sailior.str = sports.str;是复制了指针还是字符串?

是指针,只是把内存地址复制了一遍。并没把指针指向的字符串内容复制下来。所有复制的对象副本str指针指向的仍然是原来的内存,并没有一个新的内存被他指向、所以当析构函数调用两次删除对象时,会两次delete同一片内存空间,而产生错误。

 

正确做法是:重新编写新的复制构造函数,不用默认的。使得str指向一片新的内存空间。

如下代码:

str::str(const str& st){

    num++;

    len = st.len;   //与有参构造函数这句有差别

    str =new char [len +1];

    std::strcpy(str,st.str);

}

 调用有参构造函数,会创建一个全新的对象。虽然看起来和复制构造函数形式有点类似(这体现引用的特点:指向一个对象,名称是该已有的对象的别名)。 

 有参构造函数

str::str(const char *s){

    len = std::strlen(s);

    str = new char[len+1];

    std::strcpy(str,s);

    num++;

}

  

实参给一个字符串,用指针指向它,

str析构函数使用默认的会出问题,也会出现在

赋值运算符重载 中

函数未定义的话,c++会自动添加重载的赋值运算符,

若未重载赋值运算符,把对象赋值给对象是不允许的。

 

重载的原型是

str & str::operator=(const str &)

该函数接收一个指向类对象的引用

 

什么时候用赋值运算符?

在把一个已有的对象赋值给一个对象时:

如 

str kont;(“sds”);

Str head;

Kont=head;

该处用赋值问题和用隐含的复制构造函数错误相同,成员复制问题。

=复制只会head.str与knot指向相同的地址。两次调用析构函数时,删除同一片内存出错;

初始化对象时,不一定会用上赋值运算符

str  sailor=sports;  //此处用复制构造函数,不是赋值重载

上面句sailor 是一个新创建的对象,被初始化为sport的值,所以用复制构造函数

 

实现时也可以使用复制构造函数创建一个临时对象,然后通过赋值将临时对象的值赋到新对象中去。

 

总结就是初始化一定会用上复制构造函数,用=也可能调用赋值运算符(不一定是初始化过程)

 

赋值运算符与复制构造函数过程 类似,赋值运算符也是对成员进行逐个复制

如果成员本身就是类对象,则程序将用为这个类定义的赋值运算符来赋值该成员

自己写个重载的赋值运算符可以避免出现上述问题。如下

str & str::operator=( const str& st){

    if(this == &st)

        return *this;  //判断一下赋值的对象同一个则返回自身

        delete[] str;   //如果不同,把旧的字符串指的内存释放,如果此时不释放,

        //上面的字符串一直在内存中,因为str不指向它了,该内存浪费了。稍后把一个新的字符串地址赋给str,

    len = st.len;

    str = new char[len+1];

    std::strcpy(str,st.str);

    return *this;  //此次对象赋值会把旧的内存delete,而使str指向一个新new的字符串,不会创建新的对象。因此num不会变化

}