yuedx的个人空间 https://blog.eetop.cn/317611 [收藏] [复制] [分享] [RSS]

空间首页 动态 记录 日志 相册 主题 分享 留言板 个人资料

日志

类的构造函数、析构函数与赋值函数

已有 2049 次阅读| 2009-9-8 11:31 |个人分类:C/C++程序设计

类的构造函数、析构函数与赋值函数
1)对于任意一个类A,如果不想编写构造函数、析构函数与赋值函数,C++编译器将会自动为A产生四个缺省的函数,如:缺省的无参数构造函数A(void);缺省的拷贝构造函数A(const A &a);缺省的析构函数~A(void);缺省的赋值函数A& perate=(const A &a);
2)为什么还要程序员编写函数呢?原因如下:
    a)如果使用“缺省的无参数构造函数”和“缺省的析构函数”,等于放弃了自主“初始化”和“清除”的机会;
    b)“缺省的拷贝构造函数”和“缺省的赋值函数”均采用“位拷贝”而非“值拷贝”的方式来实现,倘若类中含有指针变量,这两个函数注定将出错。
3)“位拷贝”和“值拷贝”、深拷贝和浅拷贝:
    a)位拷贝,即bitwise assignment,是指将一个对象的内存映像按位元丰不懂地赋值给另一个对象,这两个对象会指向同一个内存区域,当一个对象被释放后,另一个对象的指针就会编程空指针。这时就应该编写operator=和copy constructor来实现拷贝。值拷贝,就是指将原对象的值复制一份给新对象。
    b)深拷贝和浅拷贝:如果一个类拥有资源(堆,或者是其它系统资源),当这个类的对象发生复制过程的时候,这个过程就可以叫做深拷贝,繁殖对象存在资源,但在复制过程中并未复制资源的情况视为浅拷贝。
    c)因此,位拷贝就是浅拷贝,值拷贝就是深拷贝
    d)如果没有资源,则位拷贝和值拷贝就没有区别了。
4)以类String的设计与实现为例,String的结构如下:
class String
{
public:
    String(const char* str = NULL); //普通构造函数
    String(const String& other); //拷贝构造函数
    ~String(void); //析构函数
    String& perator=(const String& other); //赋值函数

private:
    char* m_data; //用于保存字符串
};

//String的普通构造函数
String::String(const char* str)
{
    if(str == NULL)
    {
        m_data = new char[1];
        *m_data = '\0';
    }
    else
    {
        int length = strlen(str);
        m_data = new char[length + 1];
        strcpy(m_data, str);
    }
}

//String的析构函数
String::~String(void)
{
    delete[] m_data;
}

//拷贝构造函数
String::String(const String& other)
{
    //允许操作other的私有成员m_data
    int length = strlen(other, m_data);
    m_data = new char[length + 1];
    strcpy(m_data, other.m_data);
}

//赋值函数
String& String::operator=(const String& other)
{
    //(1)检查自赋值
    if(this == &other)
        return *this;

    //(2)释放原有的内存资源
    delete[] m_data;

    //(3)分配新的内存资源,并复制内容
    int length = strlen(other.m_data);
    m_data = new char[length + 1];
    strcpy(m_data, other.m_data);

    //(4)返回本对象的引用
    return *this;
}
5)构造函数与析构函数的起源:
    a)根据经验,不少哪一察觉的程序错误是由于变量没有被正确初始化或清除造成的,而初始化和清除工作很容易被人以往。所以,在设计C++语言时,把对象的初始化工作放在构造函数中,把清除工作放在析构函数中。当对象被创建时,构造函数被自动执行,当对象消亡时,析构函数被自动执行。这样,就不用担心忘了对象的初始化和清除工作。
    b)构造函数与析构函数的名字不能随便起,必须让编译器认得出才可以被自动执行,所以,让构造函数、析构函数与类同名,由于析构函数的目的与构造函数的相反,就在前面加一个~以示区别。
    c)除了名字以外,构造函数与析构函数的另一个特别之处就是没有返回值类型。因为构造函数与析构函数的使命非常明确,如果它们又返回值类型,那么编译器将不知所错。为了防止节外生枝,干脆设定没有返回值类型。
6)构造函数的初始化表
    a)初始化表位于函数参数表之后,却在函数体{}之前。这说明该表里的初始化工作发生在函数体内的任何代码被执行之前。
    b)构造函数初始化表的使用规则:
        i)如果类存在继承关系,派生类必须在其初始化表里调用基类的构造函数。
        ii)类的const常量只能在初始化表里被初始化,因为它不能在函数体内用赋值的方式来初始化。
        iii)类的数据成员的初始化可以采用初始化表或者函数体内复制两种方式,这两种方式的效率不完全相同:非内部数据类型的成员对象应当采用第一种方式(初始化表)初始化,以获取更高的效率。
        iv)对于内部数据类型的数据成员而言,两种初始化方式的效率几乎没有区别,但是后者的程序版式更清晰些。
7)构造和析构的次序
    a)构造从类层次的最根处开始,在每一层中,首先调用基类的构造函数,然后调用成员对象的构造函数。析构次序相反。
    b)成员对象初始化的次序只由成员对象在类中声明的次序决定,而与它们在初始化表中的次序无关。这是因为类的声明是唯一的,而类的构造函数可以有多个,因此会有多个不同次序的初始化表。如果成员对象按照初始化表的次序进行构造,这将导致析构函数无法得到唯一的逆序。
8)不要轻视拷贝构造函数与复制函数
    a)如果不主动编写拷贝构造函数和复制函数,编译器将以“位拷贝”的方式自动生成缺省的函数。
    b)拷贝构造函数和赋值函数非常容易混淆,常导致错写、错用。拷贝构造函数是在对象被创建时调用的,而赋值函数只能被已经存在了的对象调用。
9)示例:类String的拷贝构造函数与赋值函数
    a)类String拷贝构造函数与普通构造函数的区别是:在函数入口处无需与NULL进行比较,这是因为“引用”不可能是NULL,而“指针”可以为NULL。
    b)类String的赋值函数比构造函数复杂得多,分四部实现:
        i)检查自赋值。因为间接的自赋值有可能出现。
        ii)用delete释放原有的内存资源。防止造成内存泄露。
        iii)分配新的内存资源,并复制字符串。注意:strcpy连'\0'一起复制。
        iv)返回本对象的引用。目的是为了实现像a=b=c这样的链式表达。

10)偷懒的办法处理拷贝构造函数与赋值函数:如果实在不想编写拷贝构造函数和赋值函数,又不允许别人使用编译器生成缺省函数,偷懒的办法是:只需将拷贝构造函数和赋值函数声明为私有函数,不用编写代码。
11)如何在派生类中实现类的基本函数?
    a)基类的构造函数、析构函数、赋值函数都不能被派生类继承。
    b)如果类之间存在继承关系,在编写上述基本函数时应注意以下事项:
        i)派生类的构造函数应在其初始化表里调用基类的构造函数。
        ii)基类与派生类的析构函数应该为虚(即加virtual关键字)。
        iii)在编写派生类的赋值函数时,注意不要忘记对基类的数据成员重新赋值。
class Base
{
public:
    //...
    Base& perator=(const Base& other);// 类Base的赋值函数
private:
    int m_i, m_j, m_k;
};

class Derived:public Base
{
public:
    //...
    Derived& perator=(const Derived* other); //类Derived的赋值函数
private:
    int m_x, m_y, m_z;
};

Derived& Derived::operate=(const Derived& other)
{
    //(1)检查自赋值
    if(this == &other)//注意不要错写成if(*this == other)
        return *this;

    //(2)对基类的数据成员重新赋值
    Base::operate=(other);//因为不能直接操作私有数据成员

    //(3)对派生类的数据成员赋值
    m_x = other.m_x;
    m_y = other.m_y;
    m_z = other.m_z;

    //(4)返回本对象的引用
    return *this;//不要错写成return this;
    //也不能写成return other;因为我们不知道参数other的生命期。有可能other是个临时对象,在赋值结束后它马上消失,那么return other返回的将是垃圾。
}


点赞

评论 (0 个评论)

facelist

您需要登录后才可以评论 登录 | 注册

  • 关注TA
  • 加好友
  • 联系TA
  • 0

    周排名
  • 0

    月排名
  • 0

    总排名
  • 0

    关注
  • 13

    粉丝
  • 1

    好友
  • 35

    获赞
  • 156

    评论
  • 4698

    访问数
关闭

站长推荐 上一条 /2 下一条

小黑屋| 关于我们| 联系我们| 在线咨询| 隐私声明| EETOP 创芯网
( 京ICP备:10050787号 京公网安备:11010502037710 )

GMT+8, 2024-4-19 15:22 , Processed in 0.016430 second(s), 7 queries , Gzip On, Redis On.

eetop公众号 创芯大讲堂 创芯人才网
返回顶部