C++类中指针成员的管理(值型类、智能指针)

来源:互联网 发布:unity3d 数据库 编辑:程序博客网 时间:2024/05/16 03:07

在使用C++类的时候免不了会遇到类中需要指针成员的时候,但类成员里面一出现指针就

很容易一不小心碰上各种各样的麻烦,尤其需要注意的是类对象的初始化和赋值,下面

总结了一些常见解决办法。

先来看看这样一个类:

#include <iostream>#ifndef DEMO_H_#define DEMO_H_using std::cout;using std::endl;class Demo {    private:        int val;        int *ptr;    public:        Demo(int v = 0, int *p = nullptr): val(v), ptr(p)        {            cout << "object with value " << val << " constructed" << endl;        }        ~Demo()        {            cout << "object with value " << val << " destroyed" << endl;            delete ptr;        }        void set_val(int v)         {            val = v;        }        void print()        {            cout << val << ", " << ptr << endl;        }};#endif

一个很简单的类,包含一个string对象和一个指向string对象的指针,然后定义了一个默

认构造函数和析构函数,set_val可用于改变val值,print用于输出对象内容。下面来用一用这个类:

#include <iostream>#include "demo.h"using std::cout;using std::endl;int main(){    int *p = new int(10)    {        Demo a(10, p);        a.print();        Demo b = a;        b.set_val(20);        b.print();        Demo c;        c = a;        c.set_val(30);        c.print();    }    cout << "Done!" << endl;    return 0;}

Clang++编译,Mac上运行时输出了如下结果:

object with value 10 constructed  //对象a的创建10, 0x7fff554a49b8  //对象a的内容20, 0x7fff554a49b8  //对象b的内容object with value 0 constructed //对象c的创建30, 0x7fff554a49b8  //对象c的内容object with value 30 destroyed  //调用对象c的析构函数a.out(15751,0x7fffc8d623c0) malloc: *** error for object 0x7fff554a49b8: pointer being freed was not allocated*** set a breakpoint in malloc_error_break to debug[1]    15751 abort      ./a.out

对照main函数可以看见大括号里面的代码都看起来运行得很正常,出大括号时局部对象

a,b,c要各自调用析构函数,因为自动存储对象被删除的顺序与创建顺序相反,所以可以看见最先调用的是c的析构函数,但是接下去却出问题了,没有对象a,b析构函数的调用,括号外的“Done!”也没有输出。

问题出在哪了呢?

我们仔细看看输出结果可以发现,三个对象的ptr成员的值都是相同的,也就是说,在初始化b和对c赋值的时候,直接将原来对象的指针值直接复制给了新对象,三个指针都指向了同一个对象,大括号结束的时候调用析构函数,第一次对c调用后就已经delete了ptr,之后再次调用b,a的析构函数delete的还是同一个指针,于是就出现了问题。

事实上,由于我们没有定义Demo类的复制构造函数,所以在初始化的时候编译器会为我们合成默认复制构造函数,这个默认构造函数直接将各个成员的值简单复制给新对象的成员,所以a,b对象的指针成员都指向了同一个对象。同样的,由于我们没有为这个类重载赋值运算符=,在遇到对象间的赋值操作时,编译器自动为我们重载了赋值运算符=,将=右边对象中的成员的值简单地复制到左边,导致b,c对象的指针成员值相同。这样的复制叫浅复制,对于这个问题,有几种解决方法:

1.避免使用对象对新对象进行初始化或对其他对象赋值

也就是说,在本例中我们直接创建对象b,c:

int *p1 = new int(10);int *p2 = new int(10);Demo b(20, p1);Demo c(30, p2);

严格来讲,这只是在避免出现问题,而不是解决问题的方法,但对于一些很小程序来说还是可以一用的。有时尽管我们极力避免使用这两种操作,但还是有可能一不注意使用了这样的操作,导致最终程序运行时出现严重的后果,而且很难意识到问题所在。为了避免这种疏漏,我们可以声明类的伪私有函数,禁止使用这两种操作,使用了这样的操作将通不过编译。

伪私有函数放在类的private部分,一来避免了编译器自动生成复制构造函数和赋值运算符的重载函数,二来因为函数是私有的,在用到函数时就会编译错误,提醒我们及时改正。

private:    int val;    int *ptr;    Demo(const Demo&);  //复制构造函数    Demo &operator=(const Demo &);  //复制运算符重载

2.定义值型类

所谓值型类,就是对象在复制时进行的是深度复制,的到一个新的副本,对于本例来说,就是将类对象成员指针所指向的值复制给新的对象,而不是简单地复制指针的值。

为了进行深度复制,我们必须定义自己的复制构造函数和赋值运算符重载函数:

public://other functionsDemo(const Demo &obj): val(obj.val), ptr(new int(*obj.ptr)) { }Demo &operator=(const Demo &obj): val(obj.val), ptr(new int(*obj.ptr)) { return *this; }

3.智能指针

智能指针的行为与普通指针一样,只是增加了部分功能,C++11提供了shared_ptr智能指针模板,可用于解决我们的问题。为使用智能指针,必须包含头文件memory,然后将ptr的声明改为:

std::shared_ptr<int> ptr;

最后将析构函数中的delete ptr;删去即可。利用C++11提供的shared_ptr智能指针,很容易问题就解决了,那如果没有shared_ptr呢?这时候就需要我们定义自己的智能指针了,这也是shared_ptr的实现思路:

为了定义我们自己的智能指针,需要引入使用计数(use count),又叫引用计数(reference count),就是在每次创建新的类对象时初始化指针并将使用计数置为1,当对象副本被创建时,复制构造函数复制指针值并增加使用计数的值。当对一个对象赋值时,赋值操作符左边对象的使用计数值减一(使用计数减至0,则删除对象),右边的对象使用计数值加一。在调用析构函数时,减少使用计数的值,如果计数减至0,则删除基础对象。

用人话说的话,就是所有通过原对象来初始化、通过赋值操作得到的原对象的副本对象中的指针成员都和原对象指向同一个对象,通过一个计数变量来计数指向这个对象的指针的个数,每次调用析构函数,都只是将计数变量的值减一,只有减到0时才删除这些对象中指针成员所指向的对象。

如何实现呢?首先,我们需要一个单独的类用来封装使用计数和相关的指针:

class Shared_ptr {    friend class Demo;    int *ptr;    size_t use;    Shared_ptr(int *p): ptr(p), use(1) { }    ~Shared_ptr() { delete ptr; }};

对于普通用户而言,只需要使用我们的Demo类提供的接口,并不需要关心为了辅助实现Demo类而定义的这个Shared_ptr类,所以这个类的全部成员均为private。注意这个类需要放在Demo类的前面,因为修改后的Demo类需要用到此类:

class Demo {    private:        int val;        Shared_ptr *sptr;    public:        Demo(int v = 0, int *p = nullptr): val(v), sptr(new Shared_ptr(p)){}        ~Demo() { if(--sptr->use == 0) delete sptr; }        Demo(const Demo &obj): val(obj.val), sptr(obj.sptr) { ++sptr->use; }        Demo &operator=(const Demo &obj)        {            ++obj.sptr->use;            if(--sptr->use == 0)                delete sptr;            val = obj.val;            sptr = obj.sptr;            return *this;        }};

为了突出变化,这里只列出了Demo类中关键的改变处。

我们将原来Demo类中的指针int *ptr移到了Shared_ptr类中,Shared_ptr类还有一个成员size_t use就是使用计数。而Demo类中的指针变成了指向Shared_ptr对象的指针sptr,Demo类的构造函数、析构函数、复制构造函数以及赋值运算符的重载也有相应的改变。

为了说清楚这个智能指针是如何工作的,我们还是回到前面的main函数:

首先:

Demo a(10, p); //调用 Demo(int v = 0, int *p = nullptr): val(v), sptr(new Shared_ptr(p)){}

这一行调用Demo类的构造函数创建了一个新对象a,传入的指针p又被用于构建一个Shared_ptr对象,调用Shared_ptr的构造函数,将ptr的值设为p,use的值设为1。Demo类的Shared_ptr指针指向这个new出来的Shared_ptr对象。

之后:

Demo b = a; //调用 Demo(const Demo &obj): val(obj.val), sptr(obj.sptr) { ++sptr->use; }

这一行用a来初始化新对象b,调用复制构造函数,复制构造函数直接将a中val和sptr的值复制给b,此时a,b中的指针成员指向同一个Shared_ptr对象,故将使用计数的值加一(++sptr->use;)

接下来:

Demo c;c = a;  /*调用     Demo &operator=(const Demo &obj)        {            ++obj.sptr->use;            if(--sptr->use == 0)                delete sptr;            val = obj.val;            sptr = obj.sptr;            return *this;        }*/

这一行创建了新对象c并对c赋值,先后调用Demo类的构造函数和赋值运算符重载函数,由于在赋值时,左操作数被右操作数覆盖,相当于与左操作数对象指向同一个Shared_ptr对象的Demo对象少了一个,故其使用计数要减一,如果使用计数减为0,则删除sptr指向的Shared_ptr对象,Shared_ptr对象调用其析构函数,删除ptr所指对象;而与右操作数指向同一个Shared_ptr对象的Demo对象多了一个,故其使用计数要加一。至此,a,b,c对象中的指针成员同时指向同一个Shared_ptr对象,使用计数use = 3.

接下来,大括号结束,自由存储区的对象按照与创建顺序相反的顺序开始依次调用析构函数,首先是c调用析构函数,使用计数use减一,变为2,不执行delete sptr操作;接着调用b的析构函数,use减为1,任然不执行delete sptr操作;最后,调用a的析构函数,use减为0,执行delete sptr操作,经一步调用sptr指向的Shared_ptr的析构函数,执行delete ptr操作,至此,a,b,c对象被删除,它们的指针成员共同指向的Shared_ptr对象也被删除,Shared_ptr对象中的ptr指向的对象也被删除。

智能指针通过封装普通指针和使用计数,避免了多次delete同一个指针而引发的错误,但自己实现起来还是稍复杂。前面两种方法,第一种只适合在一些小程序里面用来规避问题,稍复杂一点的程序可能就不适用了;第二种通过定义自己的复制构造函数和赋值运算符重载函数来实现深度复制,使每个对象都有一个指针成员所指值的副本,也避免了多次delete同一个指针的问题。

原创粉丝点击