C++11:Default和Delete

目录

 

引子

默认构造函数

析构函数

默认拷贝构造函数

默认拷贝赋值运算

默认移动构造函数

默认移动赋值函数

default

delete

C++11之前

C++11实现


引子

default和delete是C++11新添加的关键字,依靠这两个关键字C++编译器可以控制函数的默认生成和删除,这是对C++98标准的很大升级。这儿需要说明的是default仅仅可以控制类的特殊成员函数的默认生成,而delete可应用于任何函数。

为了可更好的说明C++11新引入的default和delete特性,我们需要对类的特殊成员函数加以描述和说明,所谓类的特殊成员函数就是编译器隐式为类自动生成的成员函数。C++98标准类的特殊成员函数4个,他们分别是:默认构造函数,默认拷贝构造函数,默认拷贝赋值运算,析构函数。而C++11标准类的特殊成员函数在这4个基础上又扩展了2个,他们是移动构造函数,移动赋值运算。

默认构造函数

如果你定义了一个类没有实现任何构造函数,那么编译器会默认帮你添加一个构造函数,此构造函数就是类的默认构造函数;默认构造函数不接受任何输入参数,也无任何返回值。

默认构造函数并不是一个空函数,默认构造函数会完成类成员变量的默认初始化工作,但是类成员变量默认初始化为何值,除C++标准中有明确说明默认初始实现限制的成员变量以外,其他成员变量C++标准没有特定说明,不同的编译器有不同的实现。所以类的默认构造函数有时会给我们未定义行为。如果一个类包含一个指针成员变量,那么在默认构造时此指针的默认值就是一个未定义行为,编译器厂商可以默认初始此指针为null指针或一个无意义的野指针。

假设定义一个类CUser,那么编译器生成的默认构造函数可以用下面的代码描述:

class CUser 
{
public:
    CUser () 
    {
        // 1. m_pszName指针的初始化,可能会null,也有可能是其他也指针
        // 2. m_nSex 默认初始化,一般情况下m_nSex会初始为0
    }
private:
    char* m_pszName;  // 名称
    int   m_nSex;     // 性别
};

析构函数

析构函数与类的构造函数相对应,当一个对象释放时,类的析构函数一定会被调用。所以如果我们实现了一个类类型,没有实现此类类型析构函数,那么编译器同样会默认帮我们添加一个默认析构函数,否则在类对象释放时,就会报出析构函数不存在错误了。

同默认构造函数一样,默认析构函数同样不是一个空函数,它会实现类成员放操作。成员对象的释放操作按照成员的类型可分为指针成员和非指针成员。对于指针成员,默认析构仅仅释放指针所占用的内存开销;而对于非指针成员,析构函数会调用其析构函数完成对象析构。

假设定义一个类CUser,那么编译器生成的默认析构函数可以用下面的代码描述:

class CUser 
{
    public:~ CUser () 
    {
        // 1. 指针成员,释放指针所占用的内存开销    
        // 2. 成员对象,调用其析构函数完成对象释放
    }
    
    private:
        char* m_pszName;  // 名称
        int   m_nSex;     // 性别
};

默认拷贝构造函数

拷贝构造函数实现由一个存在的对象采用内存拷贝方式构建一个不存在的对象,请牢记这一点构建的对象原本是不存在的;这个很重要,因为它是默认构造和默认赋值的唯一区别,也决定了两个默认函数何时会被调用。

由上面的描述,我们可得出这样的一个结论:默认拷贝构造函数存在输入参数,而且输出是一个已经存在的同类型对象,而默认拷贝构造函数的功能则是用“老对象”的数据成员对“新对象”的数据成员进行一对一赋值,即所谓的浅拷贝(关于浅拷贝后续会有专门主题详细论述)。

假设定义一个CUser类型,那么编译默认生成的拷贝构造函数,可用下面代码描述:

class CUser
{
public:
     // 缺省的支持浅拷贝的拷贝构造函数
     CUser (const CUser & other) : m_pszName (other.m_pszName) , m_nSex(other. m_nSex) {}
 
private:
    char* m_pszName;  // 名称
    int   m_nSex;     // 性别
};
 
CUser  liming;   // 调用默认构造函数
CUser  lining = liming;  // 调用拷贝构造函数

默认拷贝赋值运算

拷贝赋值运算实现用一个已经存在的对象通过内存拷贝的方式去修改另外一个已经存在的同类型对象。拷贝赋值由赋值运算符“=”触发,当用 = 运算符定义变量时调用拷贝构造函数,而对于已经存在的变量使用 = 则是拷贝赋值运算,这是需要和拷贝构造函数需要区分的。

由上面的描述,我们可得出这样的一个结论:默认拷贝赋值函数存在输入参数,而且输出是一个已经存在的同类型对象,而默认拷贝赋值函数的功能则是用赋值对象的数据成员对被赋值对象的数据成员进行一对一赋值,而不会对被赋值对象成员进行特殊释放操作,也是一种浅拷贝操作。

假设定义一个CUser类型,那么编译默认生成的拷贝赋值函数,可用下面代码描述,默认拷贝构造的输入参数为const类型左值引用。

class CUser
{
public:
    // 缺省的支持浅拷贝的拷贝赋值函数
    CUser&  operator=  (const CUser & other) 
    {
        m_pszName  = other.m_pszName; 
        m_nSex = other. m_nSex;
    }
 
private:
    char* m_pszName;  // 名称
    int   m_nSex;     // 性别
};
 
CUser  liming;    // 调用默认构造函数
CUser  lining;    // 调用默认构造函数
lining = liming;    // 调用拷贝赋值函数

默认移动构造函数

默认构造函数实现由一个已存在的左值对象复制并创建另外一个左值对象,而移动构造函数在不同,移动构造不会发生复制操作,而是通过交换成员的拥有者实现一个左值对象构建。但不论默认构造还是移动构造,他们的目标却是一样的,同是创建一个左值对象。构造函数拷贝构造函数输入参数为const类型左值引用,所以触发拷贝构造函数时,输入必须为一个左值类型的对象。那么默认移动构造函数呢?默认移动构造函数输入参数为右值引用,而接受的输入参数必须为一个右值或一个将亡值。

为了更好的说明移动构造的作用,我们先思考一下下面这个例子发生了什么

class CUser
{
public:
    CUser() {}
 
private:
   char* m_pszName;  // 名称
    int   m_nSex;     // 性别
};
 
CUser  CreateUser() 
{
   CUser user;
    return user
}
 
CUser  lining = CreateUser();

有过C++开发经验的同学应该都会明白,上面这段代码第14行函数返回时编译器将产生一个临时对象,并调用拷贝构造函数将user对象复制到临时变量包括user对象中可能存在大内存资源对象。所以可以看出简简单单的一个函数返回竟然产生了这么多内存开销,那么有没有改进实现呢?移动构造函数是C++11新引入的标准,目的是为了解决此临时对象赋值操作时的内存开销(关于移动构造后续会有专门主题详细论述)。

那么针对于CUser类类型,C++11 编译器默认生成的默认拷贝构造函数,可以用下面的代码展示,输入参数为一个非const类型的右值引用。

class CUser 
{
public:
CUser (CUser&&  other) 
{
    m_nSex = other. m_nSex;
    m_pszName = other. m_pszName;
    other. m_pszName = nullptr;
    other. m_nSex = 0;
}
private:
    char* m_pszName;  // 名称
    int   m_nSex;     // 性别
};
 
CUser user = CUser(); // 匿名对象/临时对象优先调用右值引用

移动构造和拷贝构造对比

  1. 移动构造和拷贝构造都是对象的构造函数,都可以完成对象的创建工作
  2. 移动构造将一个将右值或亡值的资源直接接管了过来,无需再去申请新的内存。尤其是对象中所含堆上资源比较大的情况下,在效率上的体现是非常高的。
  3. 拷贝构造将一个左值的资源通过拷贝的方式复制,需要重新申请内存,尤其是对象中所含堆上资源比较大的情况下,在效率上的体现是很低的。

默认移动赋值函数

移动赋值实现将一个将亡值的资源直接接管,而省去昂贵的内存开销,移动赋值的输入参数和默认移动构造一样是一个右值引用。一般情况下,如果用一个即将构造的对象给一个已存在的对象赋值,会触发移动赋值函数的调用。例如:

CUser user();
user = CUser();

移动构造函数的声明格式如下:CUser&  operator=  (const CUser && other)

移动赋值和拷贝赋值对比

  1. 移动赋值将一个将右值或亡值的资源直接接管了过来,无需再去申请新的内存。尤其是对象中所含堆上资源比较大的情况下,在效率上是非常高的。
  2. 拷贝赋值将一个左值的资源通过拷贝的方式复制,需要重新申请内存,尤其是对象中所含堆上资源比较大的情况下,在效率上是很低的。

default

C++11引入的default关键字,功能是指示编译器完成类特殊成员函数的默认生成工作,程序员只需在类特殊成员函数后面加上“=default;”,即可将该函数声明为 "=default"函数,编译器将为显式声明 "=default"函数自动生成默认函数体。除此之外,"=default"还具有如下两个优势:(1) 可以获取更高的执行效率,(2) 避免了程序员定义函数的繁重工作。

default函数可在类体内inline定义,也可以在类体外out-line定义,但是需要特别说明的是:default只可应用于类的6个特殊成员函数,其他成员函数不能使用=default,例如:

class CUser {
public:
    CUser () = default;    // inline定义
    CUser (const CUser &); 
    CUser (CUser &&) = default; // inline定义
    CUser & operator = (const CUser&);
    CUser & operator = (CUser&&) = default; // inline定义
    virtual ~ CUser () = default;  // inline 定义
    int get() = default; // default 不能作用一般函数
};
 
CUser:: CUser (const CUser &) = default;  // out of line定义
CUser & CUser::operator = (const CUser &) = default; // out of line定义

C++98标准虽然没有default关键字,但是一般情况下编译器会默认帮程序员添加类的默认构造,析构函数,拷贝构造函数以及拷贝赋值函数。C++11的default只是把这个默认实现变的更直观而已。

delete

delete关键字主要是控制类的6个特殊函数的使用工作,声明为“=default”的函数在其他地方无法使用,即这个函数不存在,禁止定义。这是C++11为了程序员的使用便利,而添加的新特性,当然,在C++11之前程序员也可以禁止4个特殊函数定义,控制函数的使用,只是实现方式有所不同。

所以本节将会分为两个小节,第一小节笔者将说明C++11之前的实现方式,而后第二小节笔者将详细说明C++11 delete关键字的使用。

C++11之前

在C++11标准之前,程序员想控制一个函数不允许被定义,一般通过两个手段达到目标:(1)声明此函数为私有成员函数;(2)此函数只有函数声明,没有函数定义。例如:

class CUser {
public:
    CUser (const CUser &); // 声明类的拷贝构造函数
    CUser & operator = (const CUser&);// 声明拷贝赋值函数
    virtual ~ CUser () {}
private:
    CUser ();    // 声明类的默认构造函数,没有函数定义体
};
 
CUser::CUser(const CUser &) {
}
 
CUser & CUser::operator = (const CUser&) {
}
 
CUser user; // 编译器会报错

C++11实现

程序员只需在函数声明后加上“=delete;”,就可将该函数声明为 "=delete"函数,声明为"=delete"的函数,编译器将明确禁止此函数的定义。需要特别说明的是delete可以用于任何函数不限制于类的特殊成员函数。例如下例中GetUser函数和ReleaseAt函数。

class CUserMgr {
public:
    CUserMgr (const CUserMgr &) = delete; // 禁止拷贝构造函数 
    CUserMgr (CUserMgr &&) = delete; 
    CUserMgr & operator = (const CUserMgr &) = delete;
    CUserMgr & operator = (CUserMgr &&) = delete; 
    virtual ~ CUserMgr () = default;
    void ReleaseAt(nIndex) = delete; //“=delete”特性可以作用于非特殊成员函数
    static CUserMgr& Instance();
private:
    CUserMgr () = default;
};
CUserMgr& CUserMgr::Instance() {
    static CUserMgr userMgr;
    return userMgr
}
 
CUser*  GetUser(int nIndex) = delete;  // “=delete”特性可以作用于非成员函数
  • 2
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值