回 帖 发 新 帖 刷新版面

主题:[讨论]C++之拷贝构造函数问题

最近在看C++的构造函数,析构函数,拷贝构造函数和赋值函数的相关知识,写了一个简单的string类
(1)、先请大家看看程序有什么不妥的地方没有?
(2)、程序中有些地方不明白,已经在程序中注释了,大家帮忙看看,主要是拷贝构造函数的问题?
(3)、关于拷贝构造函数,大家都是怎么理解的?

谢谢大家···

忘了,环境是 XP sp3 + VC2005

代码如下:
[code=c]
#include <iostream>
using namespace std;

class CSString
{
public:
    //构造函数
    CSString():str(NULL)  //a
    {
        str = new char[1];
        str[0] = '\0';
    }
    CSString(const char *strr):str(NULL)  //b
    {
        if (strr == NULL)
        {
            str = new char[1];
            str[0] = '\0';
        }
        else
        {
            size_t len = strlen(strr);
            str = new char[len+1];
            strcpy_s(str,len+1,strr);
        }
    }

    //拷贝构造函数
    CSString(const CSString &src)  //c
    {
        size_t len = strlen(src.str);
        str = new char[len+1];
        strcpy_s(str,len+1,src.str);
    }

    //赋值函数
    CSString &operator =(const CSString &src)
    {
        if (this == &src)
        {
            return *this;
        }

        delete []str;

        size_t len = strlen(src.str);
        str = new char[len+1];
        strcpy_s(str,len+1,src.str);

        return *this;
    }

    //析构函数
    ~CSString()
    {
        delete []str;
        str = NULL;
    }

    //重载 + 运算符
    CSString operator +(const CSString ss) const
    {
        CSString tmp;
        delete []tmp.str;

        size_t len = strlen(str) + strlen(ss.str) + 1;
        tmp.str = new char[len];

        strcpy(tmp.str,str);
        strcat(tmp.str,ss.str); 

        return tmp; //调试到这里的时候调用了拷贝构造函数,这里不太明白???
    }

private:
    char *str;
};


int main()
{
    CSString str0("西安");  //调用构造函数b
    CSString str1("好地方"); //调用构造函数b
    CSString str2;  //调用构造函数a
    str2 = str0 + str1;  //先调用拷贝构造函数c(值是“好地方”,这里也不太明白??),再调用重载+,然后调用赋值函数

    //这里调用了析构函数,析构赋值函数中间变量

    return 0;//析构str2,str1,str0
}
[/code]

回复列表 (共11个回复)

沙发

代码基本能工作,但不好的地方有很多

先调用拷贝构造函数c(值是“好地方”,这里也不太明白??)
--- 因为你写的是 CSString operator +(const CSString ss) const
而不是 CSString operator +(const CSString& ss) const

板凳

[code=c]
#include <iostream> // CSString用不到<iostream>,因此#include <iostream>不应该出现在CSString之前
using namespace std;

class CSString
{
public:
    CSString() : str(0) // 将str(NULL)改为str(0),记住你现在用的是C++
    {
        //str = new char[1]; // 既然不能保存数据就别开辟空间
        //str[0] = '\0';
    }
    CSString( const char* strr) // 将 const char *strr 改为 const char* strr,虽然含义一样。
                                // 在C中,TYPE *strr意图表现*strr为TYPE;在C++中,TYPE* strr意图表现strr为TYPE*
    {
        // if (strr == NULL) // 永远不要考虑strr为NULL的情况,如果你看strcpy的原型就能明白,C/C++契约就规定其应当为一个合法的字符串
        // {                 // 因为你是无法健壮到支持任何错误的,比如你要不要检查strr指向的不是一个字符串?所以你只能约定好调用者给一个合法的字符串
        //    str = new char[1];
        //    str[0] = '\0';
        //}
        //else
        //{
            size_t len = strlen(strr);
            str = new char[len+1];
            //strcpy_s(str,len+1,strr); // strcpy_s是什么?str为len+1,那strcpy可能会出现错误吗?strcpy_s是M$的一个笑话。
            strcpy(str,strr);
        //}
    }

    //拷贝构造函数
    CSString( const CSString& src ) // const CSString &src 改为 const CSString& src
    {
        if( !src.str )
            str = 0;
        else
        {
            size_t len = strlen(src.str); // 有太多类似的操作了
            str = new char[len+1];        // ,因此可以考虑将它们写到一个私有函数中
            strcpy(str,src.str);          //
        }
    }

    //赋值函数
    CSString& operator=( const CSString& src )
    {
        //if (this == &src)
        //{
        //    return *this; // 在不影响阅读的情况下,最好一个return
        //}

        if (this != &src)
        {
            delete[] str; // 将 delete []str 改为 delete[] str。很明显[]和str是没关系的

            if( !src.str )
                str = 0;
            else
            {
                size_t len = strlen(src.str);
                str = new char[len+1];
                strcpy(str,src.str);
            }
        }

        return *this;
    }

    //析构函数
    ~CSString()
    {
        delete[] str;
        // str = NULL;  // 这个是没必要的,是一种陋习。一个对象析构了就不应当再去使用
                        // ,你这个str=NULL是不是想说你很鼓励对方访问已经析构了的对象?^_^
                        // 一个原则是:绝不对错误行为进行健壮性设计,因为这只会掩盖错误从而造成更大的错误。
    }

    //重载 + 运算符
    CSString operator+(const CSString ss) const // const CSString ss 改为 const CSString& ss
    {
        // 这下面的代码我就不改了
        CSString tmp;
        delete []tmp.str;

        size_t len = strlen(str) + strlen(ss.str) + 1;
        tmp.str = new char[len];

        strcpy(tmp.str,str);
        strcat(tmp.str,ss.str); // 从效率上不对,因为strcat需要对第一个参数做strlen(str),而你又已经做过了
                                // 差不多是 len1=strlen(str); len=len1+strlen(ss.str)+1; strcpy( temp.str+len1, ss.str );

        return tmp; //调试到这里的时候调用了拷贝构造函数,这里不太明白??? --- 可以有,也可以没有(因为具名返回值优化)
                    // 建议不要使用 tmp,因为匿名返回值优化大部分编译器都能优化得很好
    }

private:
    char *str; // 建议改为 char* str_,就是对于非public变量和函数,后面加个下划线。别人看到最后有下划线就知道不可以直接调用它。
};

再说说其他的吧
你允许 const char* 隐式转化为 CSString 吗?从常理上讲,应该是允许的。所以构造函数前不加 explicit
    ,但看你的 CSString operator+(const CSString ss) const,别人可以 CSString + "abc",但 "abc"+CSString 就不行了,别人会很奇怪,所以+应该是友元函数。
字符串长度在很多地方常用,有没有考虑加进去过
    如果加入长度,怎样保证……,这个这个太复杂了,不说了

int main()
{
    CSString str0("西安");
    CSString str1("好地方"); 
    CSString str2;
    str2 = str0 + str1;  // 为什么不写成 CSString str2 = str0 + str1;?

    return 0;
}

[/code]

3 楼

[quote][code=c]
#include <iostream> // CSString用不到<iostream>,因此#include <iostream>不应该出现在

CSString之前
using namespace std;

class CSString
{
public:
    CSString() : str(0) // 将str(NULL)改为str(0),记住你现在用的是C++
    {
        //str = new char[1]; // 既然不能保存数据就别开辟空间
        //str[0] = '\0';
    }
    CSString( const char* strr) // 将 const char *strr 改为 const char* strr,虽然含义一样。
                                // 在C中,TYPE *strr意图表现*strr为TYPE;在C++中,TYPE* strr意图表现

strr为TYPE*
    {
        // if (strr == NULL) // 永远不要考虑strr为NULL的情况,如果你看strcpy的原型就能明白,

C/C++契约就规定其应当为一个合法的字符串
        // {                 // 因为你是无法健壮到支持任何错误的,比如你要不要检查strr指向的不是一个

字符串?所以你只能约定好调用者给一个合法的字符串
        //    str = new char[1];
        //    str[0] = '\0';
        //}
        //else
        //{
            size_t len = strlen(strr);
            str = new char[len+1];
            //strcpy_s(str,len+1,strr); // strcpy_s是什么?str为len+1,那strcpy可能会出现错误吗?

strcpy_s是M$的一个笑话。
            strcpy(str,strr);
        //}
    }

    //拷贝构造函数
    CSString( const CSString& src ) // const CSString &src 改为 const CSString& src
    {
        if( !src.str )
            str = 0;
        else
        {
            size_t len = strlen(src.str); // 有太多类似的操作了
            str = new char[len+1];        // ,因此可以考虑将它们写到一个私有函数中
            strcpy(str,src.str);          //
        }
    }

    //赋值函数
    CSString& operator=( const CSString& src )
    {
        //if (this == &src)
        //{
        //    return *this; // 在不影响阅读的情况下,最好一个return
        //}

        if (this != &src)
        {
            delete[] str; // 将 delete []str 改为 delete[] str。很明显[]和str是没关系的

            if( !src.str )
                str = 0;
            else
            {
                size_t len = strlen(src.str);
                str = new char[len+1];
                strcpy(str,src.str);
            }
        }

        return *this;
    }

    //析构函数
    ~CSString()
    {
        delete[] str;
        // str = NULL;  // 这个是没必要的,是一种陋习。一个对象析构了就不应当再去使用
                        // ,你这个str=NULL是不是想说你很鼓励对方访问已经析构了的对象?^_^
                        // 一个原则是:绝不对错误行为进行健壮性设计,因为这只会掩盖错误从而造成更

大的错误。
    }

    //重载 + 运算符
    CSString operator+(const CSString ss) const // const CSString ss 改为 const CSString& ss
    {
        // 这下面的代码我就不改了
        CSString tmp;
        delete []tmp.str;

        size_t len = strlen(str) + strlen(ss.str) + 1;
        tmp.str = new char[len];

        strcpy(tmp.str,str);
        strcat(tmp.str,ss.str); // 从效率上不对,因为strcat需要对第一个参数做strlen(str),而你又已

经做过了
                                // 差不多是 len1=strlen(str); len=len1+strlen(ss.str)+1; strcpy( 

temp.str+len1, ss.str );

        return tmp; //调试到这里的时候调用了拷贝构造函数,这里不太明白??? --- 可以有,也可

以没有(因为具名返回值优化)
                    // 建议不要使用 tmp,因为匿名返回值优化大部分编译器都能优化得很好
    }

private:
    char *str; // 建议改为 char* str_,就是对于非public变量和函数,后面加个下划线。别人看到最

后有下划线就知道不可以直接调用它。
};

再说说其他的吧
你允许 const char* 隐式转化为 CSString 吗?从常理上讲,应该是允许的。所以构造函数前不加 

explicit
    ,但看你的 CSString operator+(const CSString ss) const,别人可以 CSString + "abc",但 

"abc"+CSString 就不行了,别人会很奇怪,所以+应该是友元函数。
字符串长度在很多地方常用,有没有考虑加进去过
    如果加入长度,怎样保证……,这个这个太复杂了,不说了

int main()
{
    CSString str0("西安");
    CSString str1("好地方"); 
    CSString str2;
    str2 = str0 + str1;  // 为什么不写成 CSString str2 = str0 + str1;?

    return 0;
}
[/code][/quote]

恩,学习了,太感谢了···

友元函数还没有学到。

先解决部分问题吧,有下列疑问:
(1)、 NULL == 0 ---- 但用NULL表示指针不是更明确吗?
(2)、 strcpy_s是M$的一个笑话 ---- 是不是因为不是标准?
(3)、 关于析构函数的想法是这样的 ----
    [code=c]//析构函数
    ~CSString()
    {
        delete []str;
        str = NULL;
    }[/code]
             首先释放内存,再防止野指针。
(4)、 对于类似 + 的二元运算符重载,重载函数的变量使用一个好一些还是用两个好一些?书上

的解释不一样?

谢谢了!

4 楼

路过,学习了。

5 楼

1. 在C中规定:void* 可以隐式转化为任何 TYPE*;在C++中规定0可以隐式转化为任何 TYPE*。
   所以在C++的NULL定义和C中是不一样的,C++中保留NULL这个宏只是为了兼容原有的C代码。
   C++中推荐使用0,而不是NULL,因为NULL不但丑陋,而且不统一。
“用NULL表示指针不是更明确吗?”--- 如果为了明确的话,那么应该有 charNULL、intNULL 等无限多的NULL。这样一来,你一定会觉得麻烦,因为你只需要给其赋上零值,而不关心其类型。不需要关心类型也就是类型不依赖,这是优点。而C++规定0可以隐式转化为任何TYPE*,也就是出于这个目的,它不需要依赖对象类型是 TYPE 和 TYPE*。

当然,有的时候也需要明确类型,比如如下代码:
void foo( int )
{
    puts( "int" );
}
void foo( int* )
{
    puts( "int*" );
}
int main()
{
    foo( NULL );
}
将输出 int 而不是 int*,这导致在C++0x中可以能会引入类似nullptr之类的东西。只是顺便提一下,和本问题无关。

2。strcpy_s 解决不了任何问题,不出错的用了strcpy也不会出错,该出错的用了strcpy_s还是一样出错。
使用strcpy时,有一个长度在调用者心中,而使用strcpy_s时将这个长度告诉函数,问题在于函数也不能确保你这个长度是正确的呀,如果错,则strcpy_s也错;如果不错,则strcpy也不错。
另外一点最严重的是,strcpy_s会掩盖错误,而成使得错误升级。前面说过,任何意图掩盖错误的行为都必然会带来更大的错误。假设目的区长度不足以容纳源串,那么目的串就和原串不一样,在银行就会将"张宝宝"的钱给了"张宝",这比"张宝宝"取不出钱更糟糕。如果目的区足以容纳源串,那么使用strcpy_s又是画蛇添足,多做了一件无意义的检查。

3。这其实和2是同样的问题。你只应该保证正确的行为能得到正确的结果,对于错误的行为,其结果无论是正确还是错误,你都不应该去管。在这里“正确的行为”就是指内存释放后就不应该再去访问它。

4。一种简单的做法是将 成员函数CSString operator+(const CSString& ss) const
改为友元 CSString operator+( const CSString& lhs, const CSString& rhs );
这就能保证 const char* + const CSString 可行,能保证 const CSString + const char* 可行
能保证 const CSString + const CSString 可行,同时能阻止 const char* + const char*。
这利用了 const char* 可以隐式转化为 CSString 的特性。

当然,一般为了避免const char*转化为CSString带来的效率影响,可以使用三个重载:
CSString operator+( const CSString& lhs, const CSString& rhs );
CSString operator+( const char* lhs, const CSString& rhs );
CSString operator+( const CSString& lhs, const char* rhs );

6 楼


[code=c]
//赋值函数
    CSString& operator=( const CSString& src )
    {
        //if (this == &src)
        //{
        //    return *this; // 在不影响阅读的情况下,最好一个return
        //}

        if (this != &src)
        {
            delete[] str; // 将 delete []str 改为 delete[] str。很明显[]和str是没关系的

            if( !src.str )
                str = 0;
            else
            {
                size_t len = strlen(src.str);
                str = new char[len+1];
                strcpy(str,src.str);
            }
        }

        return *this;
    }

[/code]
前面的语句是为了防止出现自赋值现象,改掉不合适吧?

7 楼

[quote]前面的语句是为了防止出现自赋值现象,改掉不合适吧?[/quote]

看别人的代码要用心——见这里:

[quote]       if (this != &src)[/quote]

已经排除了自我赋值的情况。

说实话,要写好一个类是挺难的事情。

上面的这个版本,还是有问题——应该先分配新的,然后再释放原来的。

——如果你先就把原来的给释放掉了;若在此时,内存不够新内存分配——那么完蛋了,旧的值已经被破坏了。

正常的情况,发生这样的情况时,应用程序应该提示用户内存不足,是否需要保存当前的操作到硬盘——程序应当一切以用户数据的安全为首要考虑。

当然,如果自己本身内存够的话,还可以考虑直接利用之前的内存,而无须另行分配。

8 楼


[em8][em8]

真是不好意思,我照着原来的一步一步的改,后面的没有细看

9 楼

std::string还是有点技术含量的,模拟写一个作为练习很不错。楼上bruceteen和sarrow真有耐心。赞一个!!

10 楼

最新版本正在修改中,下面是部分代码,有些问题:

[code=c]
#include <string.h>

class CSString
{
public:
    //构造函数
    CSString():str_(0)
    {}

    CSString(const char *strr)
    {
        CScopy(strr);
    }

    //拷贝构造函数
    CSString(const CSString& src)
    {
        if (!src.str_)
        {
            str_ = 0;
        }
        else
        {
            CScopy(src);
        }
    }

    //赋值函数
    CSString& operator=(const CSString&);
    CSString& operator=(const char*);

    //析构函数
    ~CSString()
    {
        delete[] str_;
    }
    
private:
    char *str_;
    void CScopy(const CSString&);
    void CScopy(const char*);
};

void CSString::CScopy(const CSString& src)
{
    str_ = new char[strlen(src.str_)+1];
    strcpy(str_,src.str_);
}

void CSString::CScopy(const char* strr)
{
    str_ = new char[strlen(strr)+1];
    strcpy(str_,strr);
}

CSString& CSString ::operator =(const CSString &src)
{
    if (this != &src)
    {
        delete[] str_;
        if (!src.str_)
        {
            str_ = 0;
        }
        else
        {
            CScopy(src);
        }
    }
    return *this;
}

CSString& CSString::operator=(const char* strr)
{
    if (this->str_ != strr)
    {
        delete[] str_;
        if (!strr)
        {
            str_ = 0;
        }
        else
        {
            CScopy(strr);
        }
    }
    return *this;
}



int main()
{
    CSString str0("西安");  //普通构造函数
    CSString str1(str0);  //拷贝构造函数

    // = 重载测试
    CSString str2,str3;
    str2 = str1;
    str3 = "";

    return 0;
}
[/code]

上面还只是有重载 = 的部分。

问题:调试str3,为什么析构后反而像是给分配了内存??

环境:xp sp2 + VC2005

我来回复

您尚未登录,请登录后再回复。点此登录或注册