C++构造

C++ 构造函数

  1. 默认构造函数
  2. 普通构造函数
  3. 复制构造函数
  4. 移动构造函数
  5. 转换构造函数

1. 默认构造函数

  • 什么是默认构造函数?没有参数或者所有参数都有默认值的构造函数。
  • 如果类没有定义任何构造函数,编译器会提供一个默认构造函数。
  • 如果定义了非默认构造函数,而不显式定义默认构造函数,则编译器会自动禁用默认构造函数,此时创建未经初始化的对象(实例)的语句会引发编译器错误。
  • 通常情况下,不要显式定义默认构造函数,这种做法是禁止用户创建未初始化的对象
#include <iostream>
using std::cout;
using std::endl;

class tString
{
private:
    char *m_str;

public:
    /**
     * @brief  默认构造函数
     * @note   没有参数或者所有参数都有默认值的构造函数,称为默认构造函数
     *         作用: 
     *              定义了默认构造函数后,允许创建未经初始化的对象(实例),
     *              如 tString ts; // 如果没定义默认构造函数,这句代码会引发编译器报错
     */
    //tString();
};

//tString::tString() {}

int main()
{
    // 把所有构造函数注释掉等价于未定义任何构造函数,此时以下语句是合法的,因为编译器提供了默认构造函数
    tString ts;

    return 0;
}
class tString
{
private:
    char *m_str;

public:
    /**
     * @brief  默认构造函数
     * @note   没有参数或者所有参数都有默认值的构造函数,称为默认构造函数
     *         作用: 
     *              定义了默认构造函数后,允许创建未经初始化的对象(实例),
     *              如 tString ts; // 如果没定义默认构造函数,这句代码会引发编译器报错
     */
    // tString();

    /**
     * @brief  非默认构造函数
     * @note   定义非默认构造函数之后,如果不显式定义默认构造函数,不允许创建未经初始化的对象(实例)
     *         如 tString ts; // 触发编译器错误
     * 
     * @param  src              - 
     * @param  len              - 
     */
    tString(const char *src, int len);

};

// tString::tString() {}

tString::tString(const char *src, int len) : m_str(new char[len + 1]) {}

int main()
{
    // 此时该语句触发编译器错误,但如果将默认构造函数解注释,则可通过编译
    tString ts;

    return 0;
}

2. 普通构造函数

3. 复制构造函数

  • 复制构造函数/拷贝构造函数通常用于“左值复制”。
#include <iostream>
#include <string.h>    // C standard lib

using std::cout;
using std::endl;

class tString
{
private:
    char *m_str;
    int   m_len;

public:

    /**
     * @brief  非默认构造函数
     */
    tString(const char *src, int len) : m_len(len), m_str(new char[len + 1]) 
    {
        for (int i = 0; i < m_len; i++)
        {
            m_str[i] = src[i];
        }

        m_str[m_len] = '\0';
        cout << "general constructor." << endl;
    }

    /**
     * @brief       复制构造函数/拷贝构造函数
     * @note        C++默认的复制构造函数只进行“浅拷贝”,自己实现复制构造函数的目的
     *              通常都是将默认的浅拷贝转换为“深拷贝”
     * 
     * @attention   这里有个有趣的现象,复制构造函数可以访问不同对象的私有成员!!!
     * 
     * @param  _ts              - 
     */
    tString(const tString &_ts)
    {
        m_len = _ts.m_len;          // 直接访问_ts对象的私有成员变量
        m_str = new char[m_len + 1];

        char *tmp = _ts.m_str;      // 直接访问_ts对象的私有成员变量
        int     i = 0;
        while(*tmp)
        {
            m_str[i] = *tmp;
            i++;
            tmp++;
        }

        m_str[m_len] = '\0';

        cout << "copy constructor called." << endl;
    }

    ~tString() 
    {
        if (m_str)
        {
            delete[] m_str;
        }

        cout << "destructor called." << endl;
    }
    
    void showAddr()
    {
        cout << (void *)m_str << endl;
    }

};

tString testCopyTime(tString ts1)
{
    return ts1;
}

int main()
{
    char src[20] = "Hello, world.";

    tString ts1 = tString(src, strlen(src));
    
    tString ts2 = ts1;

    ts1.showAddr();
    ts2.showAddr();

    // 为了测试不适用左值引用作为形参,以及返回临时对象时,复制构造函数被调用的次数
    // 根据gdb调试,调用testCopyTime函数会调用两次复制构造函数,同一份数据会在一瞬间存在于三个对象中(三份拷贝)
    tString ts3 = testCopyTime(ts2);

    return 0;
}
# 打印结果
general constructor.      # ts1对象构建时调用一般构造函数
copy constructor called.  # ts2对象构建时调用复制构造函数               
0xeb6fd0                  # 使用自定义复制构造函数进行深拷贝,ts1和ts2内容一致,但是内部指针指向两个不同的内存块
0xeb6fe8                 
copy constructor called.  # 调用testCopyTime()创建形参变量时调用复制构造函数一次
copy constructor called.  # testCopyTime()赋值给返回值时,触发复制构造函数一次,调用一次testCopyTime触发两次复制构造函数!!
destructor called.                                                                                  destructor called.                                                                              destructor called.                                                                                 destructor called.

如果把自定义的复制构造函数注释掉,c++也会提供默认的复制构造函数。

class tString
{
private:
    char *m_str;
    int   m_len;

public:

    /**
     * @brief  非默认构造函数
     */
    tString(const char *src, int len) : m_len(len), m_str(new char[len + 1])
    {
        for (int i = 0; i < m_len; i++)
        {
            m_str[i] = src[i];
        }

        cout << "general constructor." << endl;
    }

    /**
     * @brief       复制构造函数/拷贝构造函数
     * @note        C++默认的复制构造函数只进行“浅拷贝”,自己实现复制构造函数的目的
     *              通常都是将默认的浅拷贝转换为“深拷贝”
     *
     * @attention   这里有个有趣的现象,复制构造函数可以访问不同对象的私有成员!!!
     *
     * @param  _ts              -
     */
#if 0
    tString(const tString &_ts)
    {
        m_len = _ts.m_len;          // 直接访问_ts对象的私有成员变量
        m_str = new char[m_len];

        char *tmp = _ts.m_str;      // 直接访问_ts对象的私有成员变量
        int     i = 0;
        while(*tmp)
        {
            m_str[i] = *tmp;
            i++;
            tmp++;
        }

        cout << "copy constructor called." << endl;
    }
#endif

    ~tString()
    {
        if (m_str)
        {
            delete[] m_str;
        }

        cout << "destructor called." << endl;
    }

    void showAddr()
    {
        cout << (void *)m_str << endl;
    }

};

tString testCopyTime(tString ts1)
{
    return ts1;
}

int main()
{
    char src[20] = "Hello, world.";

    tString ts1 = tString(src, strlen(src));
	
    // 此语句触发的是“默认的复制构造函数”,而自定义的复制构造函数被注释掉了
    tString ts2 = ts1;

    ts1.showAddr();
    ts2.showAddr();

    // 为了测试不适用左值引用作为形参,以及返回临时对象时,复制构造函数被调用的次数
    // 根据gdb调试,调用testCopyTime函数会调用两次复制构造函数,同一份数据会在一瞬间存在于三个对象中(三份拷贝)
    //tString ts3 = testCopyTime(ts2);

    return 0;
# 打印结果
general constructor.                                                                                 0xee6fd0                                                                                             0xee6fd0                                                                                             destructor called.                                                                                   destructor called.

可以看到ts2的m_str指针和ts1的m_str指针指向同一内存处,这种情况下,当ts2和ts1析构时,就会造成***“double free”***的问题。

4. 移动构造函数

C++11引入移动语义。所谓移动,就是把原来对象A所拥有的数据转移给对象B,这种情况就像是有两颗二叉树,需要把二叉树A的一个结点移到二叉树B上,移动节点后,树A原有的结点可以置空、也可以释放。

初始状态
转移结点

在C++代码层面:

#include <iostream>
#include <string.h>    // C standard lib

using std::cout;
using std::endl;

class tString
{
private:
    char *m_str;
    int   m_len;

public:
    /**
     * @brief  默认构造函数
     */
    // tString() : m_str(nullptr), m_len(0) {}

    /**
     * @brief  非默认构造函数
     */
    tString(const char *src, int len) : m_len(len), m_str(new char[len + 1]) 
    {
		// ..
    }

    /**
     * @brief       复制构造函数/拷贝构造函数
     */
    tString(const tString &_ts)
    {
        // ...
    }

    /**
     * @brief  移动构造函数
     * @param  ts_              - 
     */
    tString(tString &&ts_) : m_len(ts_.m_len), m_str(ts_.m_str)
    {
        // 为了防止double free,将ts_源对象的值置空
        ts_.m_str = nullptr;
        ts_.m_len = 0;

        cout << "Move constructor called." << endl;
    }

    ~tString() 
    {
        if (m_str)
        {
            delete[] m_str;
        }

        cout << "destructor called." << endl;
    }
    
    void showAddr()
    {
        cout << (void *)m_str << endl;
    }

};

int main()
{
    char src[20] = "Hello, world.";

    tString ts3 = tString(src, strlen(src));

    // 触发移动构造函数:三种方式
    ts3.showAddr();

    // 方式1 - 强制类型转换
    tString ts4 = tString(static_cast<tString &&>(ts3));
    ts3.showAddr();
    ts4.showAddr();

    // 方式2 - std::move()
    tString ts5 = std::move(ts4);
    ts4.showAddr();
    ts5.showAddr();

    // 方式三,重载+/=等运算符,使其支持移动语义

    return 0;
}

移动构造函数中使用到**右值引用作为形参,要点是如果源对象有指针成员,转移后,需置空源对象指针,否则容易导致double free

5. 转换构造函数 - 单参构造函数

#include <iostream>
#include <string.h>    // C standard lib

using std::cout;
using std::endl;

class tString
{
private:
    char *m_str;
    int   m_len;

public:
    /**
     * @brief  默认构造函数
     */
    // tString() : m_str(nullptr), m_len(0) {}

    /**
     * @brief  非默认构造函数
     */
    tString(const char *src, int len) : m_len(len), m_str(new char[len + 1]) 
    {
		// ...
    }

    /**
     * @brief  转换构造函数/单参构造函数
     * @note   主要作用由两点:
     *              1. tString对象仅需初始化单个参数
     *              2. 作为类型转换,如将int转换为tString
     * @param  num              - 
     */
    explicit tString(int num) : m_str(nullptr), m_len(num)
    {
        // 这里并没有作类型转换,简单例子

        cout << "type convert constructor called." << endl;
    }

    /**
     * @brief       复制构造函数/拷贝构造函数
     */
    tString(const tString &_ts)
    {
		// ...
    }

    /**
     * @brief  移动构造函数
     * @param  ts_              - 
     */
    tString(tString &&ts_) : m_len(ts_.m_len), m_str(ts_.m_str)
    {
		// ...
    }

    ~tString() 
    {
        if (m_str)
        {
            delete[] m_str;
        }

        cout << "destructor called." << endl;
    }
    
    void showAddr()
    {
        cout << (void *)m_str << endl;
    }

};

tString testCopyTime(tString ts1)
{
    return ts1;
}

int main()
{
    tString ts6(100);

    return 0;
}

6.构造函数的显式(explicit)和隐式(implicit)

加了explicit关键字的构造函数,只能被显式调用,而且***一般只用于修饰单参构造函数***,可以禁止编译器执行非预期的类型隐式转换。

7. 构造函数小结

  • 复制构造函数应当保证每一个对象实例化时,都拥有自己独立的内存地址,不与其他对象共享内存地址,避免double free or multiple free。
  • 移动构造函数实际上是同一个内存地址空间换了个对象表示,但必须把源对象的指针/引用置空,也是为了防止double free。

C++深复制和浅复制

1. 什么是深拷贝、浅拷贝(什么是深复制、浅复制)?

深拷贝

浅拷贝

2. 深拷贝和浅拷贝的区别

  • 对于基本类型数据(非指针、非引用),深拷贝和浅拷贝效果基本一致
  • 但当对象中含有指针、引用时,如果使用浅拷贝,会导致多个对象中的指针指向同一块内存空间,当对象析构时,会对同一地址多次delete,造成***“double free or multiple free”***。
  • 如果使用深拷贝,每一个对象中的指针/引用都指向独立的内存空间,于是避免了"对同一地址多次delete"的问题。

完整地练习源码

#include <iostream>
#include <string.h>    // C standard lib

using std::cout;
using std::endl;

class tString
{
private:
    char *m_str;
    int   m_len;

public:
    /**
     * @brief  默认构造函数
     * @note   没有参数或者所有参数都有默认值的构造函数,称为默认构造函数
     *         作用: 
     *              定义了默认构造函数后,允许创建未经初始化的对象(实例),
     *              如 tString ts; // 如果没定义默认构造函数,这句代码会引发编译器报错
     */
    // tString() : m_str(nullptr), m_len(0) {}

    /**
     * @brief  非默认构造函数
     * @note   定义非默认构造函数之后,如果不显式定义默认构造函数,不允许创建未经初始化的对象(实例)
     *         如 tString ts; // 触发编译器错误
     * 
     * @param  src              - 
     * @param  len              - 
     */
    tString(const char *src, int len) : m_len(len), m_str(new char[len + 1]) 
    {
        for (int i = 0; i < m_len; i++)
        {
            m_str[i] = src[i];
        }

        m_str[m_len] = '\0';
        cout << "general constructor." << endl;
    }

    /**
     * @brief  转换构造函数/单参构造函数
     * @note   主要作用由两点:
     *              1. tString对象仅需初始化单个参数
     *              2. 作为类型转换,如将int转换为tString
     * @param  num              - 
     */
    explicit tString(int num) : m_str(nullptr), m_len(num)
    {
        // 这里并没有作类型转换,简单例子

        cout << "type convert constructor called." << endl;
    }

    /**
     * @brief       复制构造函数/拷贝构造函数
     * @note        C++默认的复制构造函数只进行“浅拷贝”,自己实现复制构造函数的目的
     *              通常都是将默认的浅拷贝转换为“深拷贝”
     * 
     * @attention   这里有个有趣的现象,复制构造函数可以访问不同对象的私有成员!!!
     * 
     * @param  _ts              - 
     */
    tString(const tString &_ts)
    {
        m_len = _ts.m_len;          // 直接访问_ts对象的私有成员变量
        m_str = new char[m_len + 1];

        char *tmp = _ts.m_str;      // 直接访问_ts对象的私有成员变量
        int     i = 0;
        while(*tmp)
        {
            m_str[i] = *tmp;
            i++;
            tmp++;
        }

        m_str[m_len] = '\0';

        cout << "copy constructor called." << endl;
    }

    /**
     * @brief  移动构造函数
     * @param  ts_              - 
     */
    tString(tString &&ts_) : m_len(ts_.m_len), m_str(ts_.m_str)
    {
        // 为了防止double free,将ts_源对象的值置空
        ts_.m_str = nullptr;
        ts_.m_len = 0;

        cout << "Move constructor called." << endl;
    }

    ~tString() 
    {
        if (m_str)
        {
            delete[] m_str;
        }

        cout << "destructor called." << endl;
    }
    
    /**
     * @brief  重载赋值运算符,支持深拷贝
     * @param  ts_              - 
     * @return tString&         - 
     */
    tString & operator=(const tString &ts_)
    {
        // 如果是将自己赋值给自己
        if (this == &ts_)
        {
            return *this;
        }

        delete[] m_str;

        m_len = ts_.m_len;

        m_str = new char[m_len + 1];

        int i = 0;
        while (i < m_len)
        {
            m_str[i] = ts_.m_str[i];
            i++;
        }

        m_str[m_len] = '\0';

        cout << "copy assigned function called." << endl;
    }

    /**
     * @brief  支持移动语义的赋值运算符重载
     * @param  ts_              - 
     * @return tString&         - 
     */
    tString & operator=(tString &&ts_)
    {
        if (this == &ts_)
        {
            return *this;
        }

        delete[] m_str;

        m_len = ts_.m_len;
        m_str = ts_.m_str;

        ts_.m_len = 0;
        ts_.m_str = nullptr;

        return *this;

        cout << "move assign function called." << endl;
    }

    void showAddr()
    {
        cout << (void *)m_str << endl;
    }

    void val()
    {
        cout << m_str << endl;
    }
};

tString testCopyTime(tString ts1)
{
    return ts1;
}

int main()
{
    char src[20] = "Hello, world.";

    tString ts1 = tString(src, strlen(src));
    ts1.val();

    tString ts2 = ts1;

    ts1.showAddr();
    ts2.showAddr();
    ts2.val();

    tString ts3 = testCopyTime(ts2);

    // 触发移动构造函数:三种方式
    ts3.showAddr();
    ts3.val();

    // 方式1 - 强制类型转换
    tString ts4 = tString(static_cast<tString &&>(ts3));
    ts3.showAddr();
    ts4.showAddr();

    // 方式2 - std::move()
    tString ts5 = std::move(ts4);
    ts4.showAddr();
    ts5.showAddr();

    // 方式三,重载+/=等运算符,使其支持移动语义


    tString ts6(100);

    ts6 = tString(src, strlen(src));
    ts6.showAddr();

    return 0;
}

不知不觉已经工作快一年了,好久没有写过博客,再次记录一下本周学习,偷得浮生半日闲,又摘桃花换酒钱,纯属个人笔记,如有错误,欢迎大佬讨论!!!

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值