• 15.含有指针成员的类的拷贝[ClassCopyConstructorWithPointerMember]


    【题目】

    下面是一个数组类的声明与实现。请分析这个类有什么问题,并针对存在的问题提出几种解决方案。

     C++ Code 
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
     

    template<typename T>
    class Array
    {
    public:
        Array(
    unsigned arraySize): data(0), size(arraySize)
        {
            
    if(size > 0)
                data = 
    new T[size];
        }

        ~Array()
        {
            
    if(data) delete[] data;
        }

        
    void setValue(unsigned index, const T &value)
        {
            
    if(index < size)
                data[index] = value;
        }

        T getValue(
    unsigned index) const
        {
            
    if(index < size)
                
    return data[index];
            
    else
                
    return T();
        }

    private:
        T *data;
        
    unsigned size;
    };

    类中有指针变量,即深浅拷贝问题。因为类中没有实现拷贝构造函数和赋值构造函数,因此在用到拷贝构造函数和赋值构造函数的时候只会将指针简单复制,这样二个对象的这个指针会指向同一块内存,在对象删除及调用时会造成程序崩溃。

    【解决方法】

    【方案1】

    将拷贝构造函数和赋值构造函数改成私有,这样程序调用时编译器会报错。

    分析:我们注意在类的内部封装了用来存储数组数据的指针。软件存在的大部分问题通常都可以归结指针的不正确处理。

    这个类只提供了一个构造函数,而没有定义构造拷贝函数和重载拷贝运算符函数。当这个类的用户按照下面的方式声明并实例化该类的一个实例

    Array A(10);

    Array B(A);

    或者按照下面的方式把该类的一个实例赋值给另外一个实例

    Array A(10);

    Array B(10);

    B=A;

    编译器将调用其自动生成的构造拷贝函数或者拷贝运算符的重载函数。在编译器生成的缺省的构造拷贝函数和拷贝运算符的重载函数,对指针实行的是按位拷贝,仅仅只是拷贝指针的地址,而不会拷贝指针的内容。因此在执行完前面的代码之后,A.data和B.data指向的同一地址。当A或者B中任意一个结束其生命周期调用析构函数时,会删除data。由于他们的data指向的是同一个地方,两个实例的data都被删除了。但另外一个实例并不知道它的data已经被删除了,当企图再次用它的data的时候,程序就会不可避免地崩溃。

    由于问题出现的根源是调用了编译器生成的缺省构造拷贝函数和拷贝运算符的重载函数。一个最简单的办法就是禁止使用这两个函数。于是我们可以把这两个函数声明为私有函数,如果类的用户企图调用这两个函数,将不能通过编译。实现的代码如下:

     C++ Code 
    1
    2
    3
     
    private:
    Array(
    const Array &copy);
    const Array &operator = (const Array &copy);

    【方案2】

    添加二函数。

    最初的代码存在问题是因为不同实例的data指向的同一地址,删除一个实例的data会把另外一个实例的data也同时删除。因此我们还可以让构造拷贝函数或者拷贝运算符的重载函数拷贝的不只是地址,而是数据。由于我们重新存储了一份数据,这样一个实例删除的时候,对另外一个实例没有影响。这种思路我们称之为深度拷贝。实现的代码如下:

     C++ Code 
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
     
    public:
    Array(
    const Array &copy): data(0), size(copy.size)
    {
        
    if(size > 0)
        {
            data = 
    new T[size];
            
    for(int i = 0; i < size; ++ i)
                setValue(i, copy.getValue(i));
        }
    }

    const Array &operator = (const Array &copy)
    {
        
    if(this == &copy)
            
    return *this;

        
    if(data != NULL)
        {
            
    delete []data;
            data = 
    NULL;
        }

        size = copy.size;
        
    if(size > 0)
        {
            data = 
    new T[size];
            
    for(int i = 0; i < size; ++ i)
                setValue(i, copy.getValue(i));
        }
    }

     【方案3】

    用引用计数器(指针)。

    即在类中新增一变量用来记录类中指针被拷贝的次数。当调用默认构造函数时将次数置1,在调用以上二函数时将计数器+1,在析构函数中将计数器-1,当计数器值=0时,表示没有对象使用它,这时才真的删除指针指向的内存(此行为可能发生在赋值构造函数及析构函数中)。

    为了防止有多个指针指向的数据被多次删除,我们还可以保存究竟有多少个指针指向该数据。只有当没有任何指针指向该数据的时候才可以被删除。这种思路通常被称之为引用计数技术。在构造函数中,引用计数初始化为1;每当把这个实例赋值给其他实例或者以参数传给其他实例的构造拷贝函数的时候,引用计数加1,因为这意味着又多了一个实例指向它的data;每次需要调用析构函数或者需要把data赋值为其他数据的时候,引用计数要减1,因为这意味着指向它的data的指针少了一个。当引用计数减少到0的时候,data已经没有任何实例指向它了,这个时候就可以安全地删除。实现的代码如下:

     C++ Code 
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
     

    public:
    Array(
    unsigned arraySize)
        : data(
    0), size(arraySize), count(new unsigned int)
    {
        *count = 
    1;
        
    if(size > 0)
            data = 
    new T[size];
    }

    Array(
    const Array &copy)
        : size(copy.size), data(copy.data), count(copy.count)
    {
        ++ (*count);
    }

    ~Array()
    {
        Release();
    }

    const Array &operator = (const Array &copy)
    {
        
    if(data == copy.data)
            
    return *this;

        Release();

        data = copy.data;
        size = copy.size;
        count = copy.count;
        ++(*count);
    }

    private:
    void Release()
    {
        --(*count);
        
    if(*count == 0)
        {
            
    if(data)
            {
                
    delete []data;
                data = 
    NULL;
            }

            
    delete count;
            count = 
    0;
        }
    }

    unsigned int *count;
    }

    【参考】

    http://zhedahht.blog.163.com/blog/static/25411174200722710364233/

    http://www.cnblogs.com/t427/archive/2012/08/10/2633133.html

    http://blog.csdn.net/gamecreating/article/details/5382902

    http://www.cppblog.com/xczhang/archive/2008/01/21/41569.html

    个人学习笔记,欢迎拍砖!---by hellogiser

    Author: hellogiser
    Warning: 本文版权归作者和博客园共有,欢迎转载,但请保留此段声明,且在文章页面明显位置给出原文连接。Thanks!
    Me: 如果觉得本文对你有帮助的话,那么【推荐】给大家吧,希望今后能够为大家带来更好的技术文章!敬请【关注】
  • 相关阅读:
    第三十八条:检查参数的有效性
    第二十九条:优先考虑类型安全的异构容器
    第二十八条:利用有限制通配符来提升API的灵活性
    C# 利用第三方SharpZipLib进行ZIP压缩
    无法解决 equal to 操作中 "Chinese_PRC_CI_AS_WS" 和 "Chinese_PRC_CI_AS" 之间的排序规则冲突
    使用EasyUI的treegrid犯的个低级错误
    Js千位分隔符
    Google Chrom浏览器默认只能显示字体大小大于等于12px
    Asp.Net2.0开发小问题汇总
    Oracle dbms_output.put_line长度限制问题
  • 原文地址:https://www.cnblogs.com/hellogiser/p/3738722.html
Copyright © 2020-2023  润新知