找回密码
 立即注册
收起左侧

C++老鸟日记024 类的初始化与清除

4
回复
5548
查看
[复制链接]
累计签到:41 天
连续签到:1 天
来源: 原创 2018-9-17 18:27:13 显示全部楼层 |阅读模式

马上注册,查看详细内容!注册请先查看:注册须知

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
本帖最后由 baizy77 于 2018-10-1 20:56 编辑

版权声明
---------------------------------------------------------------------------------------------------------------------
该文章原创于Qter开源社区(www.qter.org
作者: 女儿叫老白 (白振勇)
转载请注明出处!
---------------------------------------------------------------------------------------------------------------------
本套课程属于:《C++跨平台开发干货》系列课程。
-----------------------------------------------------------------------------

引言:
----------------------------------------------------------------------------
       如果您用过C语言编程,那么您一定知道malloc()和free()。这两个接口用来申请和释放内存。C++中也支持这两个接口,但它提供了一种更好的手段来管理内存。我们今天就来讨论一下。

正文:
----------------------------------------------------------------------------
       在C++编码规范中,有一条非常重要的规则就是一定要为进行变量初始化。对于类对象来说,就是调用类的构造函数。如果我们没有提供类的构造函数,编译器会默认生成一个。当然,编译器不知道我们到底想要做哪些初始化,因此编译器生成的构造函数也许并不能满足我们的需求。所以,还是为类编写构造函数吧。      

  1. // user.h
  2. class CUser {
  3. public:
  4.     CUser(int id, const string& name);
复制代码


       从在上述代码可以看出,类的构造函数与类同名,没有返回值。类的构造函数负责对成员变量进行初始化,并做一些其他必要的工作。上述示例代码中,成员变量的初始化直接写在构造函数的函数名的那一行。这是非常常用的方法。当然,您也可以写成这样:
  1. // 构造函数写法2
  2. CUser:: CUser (int id, const string& name) {
  3.        m_nId = id;
  4.        m_strName = name;
  5. }
复制代码


       请注意:如果按照前一个写法,类的成员变量初始化的顺序必须与头文件中定义的顺序保持一致,否则在gcc编译(或者其他编译器)可能会报错(跟编译器版本有关)。
       在实际开发过程中,我们可能有不同的构造需求,比如我们可以提供另外一个构造函数2:
  1. // user.h
  2. class CUser {
  3. public:
  4.     CUser(int id, const string& name);      // 构造函数1
  5.     CUser(int id, const string& name, const string& addr, bool bMarried); //构造函数2

  6. private:
  7.     int m_nId;             // id号
  8.     string m_strName;// 姓名
  9.     string m_strAddress;// 住址
  10.    bool m_bMarried; // 是否已婚
  11. };

  12. // user.cpp
  13. #include “user.h”
  14. // 构造函数1
  15. CUser:: CUser (int id, const string& name):m_nId(id), m_strName(name),m_strAddress(“”),m_bMarried(false) {
  16. }

  17. // 构造函数2
  18. CUser:: CUser (int id, const string& name,const string& addr, bool bMarried):m_nId(id), m_strName(name),m_strAddress(addr),m_bMarried(bMarried) {
  19. }
复制代码
    如果我们定义了派生类,派生类的构造函数也需要调用父类的构造函数。
  1. // student.h
  2. #include user.h
  3. class CStudent : public CUser {
  4. public:
  5.     CStudent(const string& school, const string& pro);
  6. private:
  7.     string m_strSchool;       // 学校
  8.     string m_strPro;           // 专业
  9. };

  10. // student.cpp
  11. #include “student.h”
  12. // 构造函数
  13. CStudent:: CStudent (const string& school, const string& pro):CUser (0, “”):m_strSchool(school), m_strPro(pro) {
  14. }
复制代码
从上述代码可以看出,子类的构造函数中,可以调用父类的构造函数(当然,也可以不调用,这完全取决于您的需要,不过一般情况下都是需要调用的)。然后才是子类成员的初始化。
       从这里我们也可以得出结论,在C++中,变量的初始化是按照从父类到子类的顺序。否则父类还没有初始化完毕的情况下,如果子类的构造函数调用了父类的某个接口,就很有可能出问题。
       避免在类的构造函数中抛异常,因为类还没有初始化完毕,完全处于一种未知状态。此时抛出异常,可能程序仍然无法继续正确运行。

       下面,我们再看一下类的析构。每个类都有一个析构函数,显然,在析构函数中,我们要做一些释放内存、断开联系之类的工作,总之是扫尾工作。当超出对象的作用域时,编译器会自动调用类的析构函数。     
  1. // user.h
  2. class CUser
  3. {
  4. public:
  5.        virtual ~CUser(){}
  6. };
复制代码

       请注意,在上述代码中,我们在析构函数前使用了virtual关键字。它是做什么用的呢?稍后我们会为大家解释。现在请继续看下文。

  1. // student.h
  2. #include “user.h”
  3. class CStudent : public CUser {
  4. public:
  5.     ~CStudent(){}
  6. }

  7. // main.cpp
  8. CUser* pUser = new CStudent(“qinghua”, “actor”);
  9. ……
  10. if (NULL != ) {
  11.     delete pUser;
  12.     pUser = NULL;
  13. }
复制代码

现在我们来解答刚才的疑问。在上述代码中,我们定义了一个CUser的指针指向new出来的CStudent对象。这没有问题。在最后的代码中,我们把这个指针删除。请注意,是以CUser的身份删除该指针,因此调用的是CUser的析构函数。如果我们不把~CUser()前面加上virtual关键字,那么编译器的工作到此结束,它只负责析构CUser,不会调用~CStudent(),因此就可能导致CStudent中申请的内存泄漏或者~CStudent()中该做的工作没做。为父类CUser的析构函数加上了virtual关键字,就能保证编译器可以继续调用子类的析构函数。因此,程序可以把该做的工作做完。

结语:
----------------------------------------------------------------------------
       构造函数和析构函数负责类的初始化与清除工作能够正常进行,因此我们一定要注意按照规范编写构造函数与析构函数。
       1.一定要编写构造函数、析构函数。
       2.子类的构造函数中,先调用父类的构造函数,再对子类的成员变量初始化。
       3.父类的析构函数前要使用virtual关键字。

参考资料
----------------------------------------------------------------------------
《C++编程思想》两卷合订本中文版(6.1章节,6.2章节),(美) Bruce Eckel  Chuck Allison著
回复

使用道具 举报

累计签到:41 天
连续签到:1 天
2018-9-20 08:56:07 显示全部楼层
重新补充了“构造函数2”的代码。搞不懂,网页的编辑不太好用,经常把代码弄丢了。
回复 支持 反对

使用道具 举报

累计签到:50 天
连续签到:1 天
2018-9-21 08:17:31 显示全部楼层
构造函数和析够造函数,    析构函数的父类析构要加virtual, 既然要加virtual, 为什么编译器不加, 哪里地方可以不加呢?
回复 支持 反对

使用道具 举报

累计签到:41 天
连续签到:1 天
2018-9-21 08:41:28 显示全部楼层
tan 发表于 2018-9-21 08:17
构造函数和析够造函数,    析构函数的父类析构要加virtual, 既然要加virtual, 为什么编译器不加, 哪里 ...

父类的析构函数使用virutal关键字,就会形成动态绑定,这是编译器给我们的一个选择,因为只有需要的时候才使用,也就是,派生类的析构中需要释放内存或者有额外处理的时候。而动态绑定提高了代码的执行成本。因此并非默认的选项。
回复 支持 反对

使用道具 举报

累计签到:50 天
连续签到:1 天
2018-9-22 11:04:39 显示全部楼层
baizy77 发表于 2018-9-21 08:41
父类的析构函数使用virutal关键字,就会形成动态绑定,这是编译器给我们的一个选择,因为只有需要的时候 ...

OK       thanks.                 
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

公告
可以关注我们的微信公众号yafeilinux_friends获取最新动态,或者加入QQ会员群进行交流:190741849、186601429(已满) 我知道了