C++11:Default和Delete

本文详细介绍了C++11中的default和delete关键字,它们用于控制类的特殊成员函数的默认生成和删除。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”特性可以作用于非成员函数
<think> 这个错误是因为我们试图使用`std::vector`的`push_back`方法,而该方法在默认情况下会尝试复制元素。但由于我们在`CodeTrie`类中删除了拷贝构造函数拷贝赋值运算符,所以编译器无法生成默认的拷贝操作。当尝试将`CodeTrie`对象添加到`vector`时,就会触发这个错误。 ### 解决方案: 1. **允许移动语义**:我们已经有了移动构造函数,所以可以使用`std::move`将对象移动到`vector`中。 2. **使用`vector`的`emplace_back`方法**:该方法允许在容器中直接构造元素,避免拷贝或移动。 3. **或者使用`vector`存储`unique_ptr`**:如果确实需要存储多个`CodeTrie`对象,我们可以存储指向`CodeTrie`的智能指针。 ### 修改后的代码示例: 假设我们有一个场景需要存储多个字典树(例如,不同的编码表),我们可以这样修改: ```cpp #include <iostream> #include <vector> #include <array> #include <string> #include <memory> class TrieNode { public: int codeID = -1; std::array<std::unique_ptr<TrieNode>, 26> children; TrieNode() { children.fill(nullptr); } }; class CodeTrie { private: std::unique_ptr<TrieNode> root; public: CodeTrie() : root(std::make_unique<TrieNode>()) {} // 禁止拷贝 CodeTrie(const CodeTrie&) = delete; CodeTrie& operator=(const CodeTrie&) = delete; // 允许移动 CodeTrie(CodeTrie&& other) noexcept = default; CodeTrie& operator=(CodeTrie&& other) noexcept = default; void insert(const std::string& code, int id) { TrieNode* node = root.get(); for (char c : code) { int idx = c - 'a'; if (idx < 0 || idx >= 26) continue; if (!node->children[idx]) { node->children[idx] = std::make_unique<TrieNode>(); } node = node->children[idx].get(); } node->codeID = id; } int findLongestMatch(const std::string& input) const { if (!root) return -1; const TrieNode* node = root.get(); int lastMatchID = -1; for (size_t i = 0; i < 6 && i < input.length(); i++) { int idx = input[i] - 'a'; if (idx < 0 || idx >= 26) break; if (!node->children[idx]) break; node = node->children[idx].get(); if (node->codeID != -1) { lastMatchID = node->codeID; } } return lastMatchID; } }; int main() { // 构建一个字典树集合 std::vector<CodeTrie> tries; tries.reserve(2); // 预分配空间 // 创建第一个字典树 CodeTrie trie1; trie1.insert("a", 1); trie1.insert("aa", 2); // 使用移动语义添加到vector tries.push_back(std::move(trie1)); // 显式移动 // 第二种方式:直接在vector中构造(避免移动) tries.emplace_back(); // 在vector中构造一个空的CodeTrie tries.back().insert("b", 5); // 添加一些数据 // 测试第二个字典树 std::cout << tries[1].findLongestMatch("bcd") << std::endl; // 输出5 return 0; } ``` ### 关键修改点: 1. **移动赋值运算符**: ```cpp CodeTrie& operator=(CodeTrie&& other) noexcept = default; ``` - 显式声明移动赋值运算符,允许编译器生成默认实现 2. **移动操作**: ```cpp tries.push_back(std::move(trie1)); ``` - 使用`std::move`转移所有权,避免拷贝 3. **原地构造**: ```cpp tries.emplace_back(); ``` - 在vector内部直接构造`CodeTrie`对象,无需拷贝或移动 ### 错误原因详细说明: - `vector::push_back`有两个重载版本: ```cpp void push_back(const T& value); // 拷贝版本 void push_back(T&& value); // 移动版本 ``` - 当传递一个左值(如`trie1`)时,编译器会尝试调用拷贝版本,但拷贝操作已被删除 - 解决方案:使用`std::move`将其转换为右值引用,从而触发移动版本 ### 性能优化建议: 1. **预分配空间**: ```cpp tries.reserve(2); ``` - 减少动态内存分配次数 2. **优先使用`emplace_back`**: - 避免额外的移动操作 3. **移动操作标记为`noexcept`**: - 确保标准库容器在重新分配内存时使用移动而非拷贝(移动操作声明为`noexcept`时) §§相关问题§§: 1. 如何在C++中实现一个不可移动但可拷贝的类? 2. 当类包含多个`unique_ptr`成员时,如何正确实现移动语义? 3. 如何设计一个同时支持拷贝移动的字典树结构? 4. `std::vector`的`push_back``emplace_back`在性能上有何差异? </think> ### 错误分析 这个编译错误是由于尝试复制 `std::unique_ptr` 造成的。错误信息表明:"尝试引用已删除的函数",这是因为 `unique_ptr` 删除了拷贝构造函数拷贝赋值运算符,只支持移动语义。 ### 问题定位 在您的代码中,可能是在以下情况下触发了这个问题: 1. 尝试拷贝整个 `CodeTrie` 对象 2. 尝试在容器中存储 `CodeTrie` 对象时使用了拷贝而非移动语义 3. 函数返回或参数传递时使用了拷贝而非移动 ### 完整解决方案 ```cpp #include <iostream> #include <vector> #include <array> #include <string> #include <memory> #include <utility> // for std::move class TrieNode { public: int codeID = -1; std::array<std::unique_ptr<TrieNode>, 26> children; TrieNode() { children.fill(nullptr); } }; class CodeTrie { private: std::unique_ptr<TrieNode> root; public: CodeTrie() : root(std::make_unique<TrieNode>()) {} // 禁止拷贝构造赋值 CodeTrie(const CodeTrie&) = delete; CodeTrie& operator=(const CodeTrie&) = delete; // 显式定义移动构造移动赋值 CodeTrie(CodeTrie&& other) noexcept : root(std::move(other.root)) {} CodeTrie& operator=(CodeTrie&& other) noexcept { if (this != &other) { root = std::move(other.root); } return *this; } void insert(const std::string& code, int id) { TrieNode* node = root.get(); for (char c : code) { int idx = c - 'a'; if (idx < 0 || idx >= 26) continue; if (!node->children[idx]) { node->children[idx] = std::make_unique<TrieNode>(); } node = node->children[idx].get(); } node->codeID = id; } int findLongestMatch(const std::string& input) const { if (!root) return -1; const TrieNode* node = root.get(); int lastMatchID = -1; for (size_t i = 0; i < 6 && i < input.length(); i++) { int idx = input[i] - 'a'; if (idx < 0 || idx >= 26) break; if (!node->children[idx]) break; node = node->children[idx].get(); if (node->codeID != -1) { lastMatchID = node->codeID; } } return lastMatchID; } }; // 正确使用移动语义的函数 CodeTrie buildCodeTrie(const std::vector<std::pair<std::string, int>>& codeMap) { CodeTrie trie; for (const auto& entry : codeMap) { trie.insert(entry.first, entry.second); } return trie; // 依赖移动语义(RVO或移动构造) } // 正确处理集合中的字典树 void processTries() { std::vector<CodeTrie> tries; // 正确用法1:直接构造在vector中 tries.emplace_back(); // 在vector中直接构造 tries.back().insert("test", 100); // 正确用法2:使用移动语义添加 CodeTrie tempTrie; tempTrie.insert("temp", 200); tries.push_back(std::move(tempTrie)); // 显式移动 // 正确用法3:放置新元素并操作 tries.emplace_back(); tries.back().insert("new", 300); } int main() { // 构建编码表 std::vector<std::pair<std::string, int>> codeMap = { {"a", 1}, {"aa", 2}, {"aab", 3}, {"aba", 4}, {"b", 5}, {"xyzabc", 19800}, {"zab", 19801}, {"zz", 19802} }; // 正确构造字典树(使用移动语义) CodeTrie codeTrie = buildCodeTrie(codeMap); // 测试用例 std::cout << codeTrie.findLongestMatch("aiabcdef") << std::endl; // 1 std::cout << codeTrie.findLongestMatch("aabchjhk") << std::endl; // 3 // 处理多个字典树的集合 processTries(); return 0; } ``` ### 关键修复点 1. **显式定义移动操作**: ```cpp CodeTrie(CodeTrie&& other) noexcept : root(std::move(other.root)) {} CodeTrie& operator=(CodeTrie&& other) noexcept { if (this != &other) { root = std::move(other.root); } return *this; } ``` - 移动构造函数将资源所有权从源对象转移到新对象 - 移动赋值运算符确保安全地转移资源 2. **禁止拷贝操作**: ```cpp CodeTrie(const CodeTrie&) = delete; CodeTrie& operator=(const CodeTrie&) = delete; ``` - 明确禁用拷贝操作,防止意外使用 3. **正确使用容器**: ```cpp // 正确方法1:原地构造 tries.emplace_back(); // 正确方法2:显式移动 tries.push_back(std::move(tempTrie)); ``` 4. **正确处理函数返回**: ```cpp CodeTrie buildCodeTrie(...) { CodeTrie trie; // ... return trie; // 依赖编译器优化(RVO或移动构造) } ``` ### unique_ptr 的正确使用规则 | 操作 | 是否允许 | 说明 | |------|----------|------| | 拷贝构造 | ❌ 禁止 | 会尝试复制独占资源 | | 拷贝赋值 | ❌ 禁止 | 同上 | | 移动构造 | ✅ 允许 | 使用 `std::move` 转移所有权 | | 移动赋值 | ✅ 允许 | 同上 | | 容器存储 | ✅ 允许 | 需使用 `emplace_back` 或 `push_back(std::move(obj))` | | 函数返回 | ✅ 允许 | 编译器优化或移动语义 | ### 移动语义性能优势 1. **零额外开销**: - 移动操作只涉及指针交换,不复制实际节点数据 - 10,000个节点的树移动成本 = 1个指针赋值 2. **容器操作高效**: - `push_back(std::move(obj))`:恒定时间 O(1) - `emplace_back()`:原地构造,无需移动 3. **异常安全**: - `noexcept` 保证移动操作不会抛出异常 - 确保操作失败时的强异常安全保证
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值