C++ 高级特性:右值与new

简介:对于modernC++,特别是11之后左右值的使用,右值引用等变得非常复杂,很多C++developer 甚至高级C++都没弄明白,所以这里就把之前总结理清的笔记放出来share~~

1、右值总结

在现代的C++中,左值表示一个可修改并具有持久身份的对象,而右值表示一个临时对象,其值可以用作初始化其他对象
左值和右值的区别:

  • 左值:表示对象的内存位置,可以在赋值操作符的左边,例如变量名、数组元素或解引用指针。
  • 右值:表示一个临时的、短暂存在的值,例如字面量或结果为临时对象的表达式。它们通常不能在赋值操作符的左边。

右值引用:
右值引用(通过 && 符号表示)是 C++11 引入的一种新类型的引用,右值引用主要用于捕获临时对象(右值),这些对象通常在表达式求值后就被销毁。它可以绑定到右值,从而允许我们在函数中捕获和修改右值而不影响原始数据。这在很多场景下都非常有用,例如实现移动语义和完美转发。

右值引用的好处:

  1. 对于移动语义:右值引用允许实现高效的资源管理。在某些情况下,如拷贝构造函数和赋值操作符,我们可以避免资源的昂贵拷贝,而是仅传递底层资源的指针。这提高了性能,尤其是在处理大型数据结构(如 std::vector 或 std::string)时。
  2. 支持完美转发:右值引用主要是针对模板和可变参数模板,可以实现将参数完全按原样传递给另一个函数。

**移动语义:**关键字std::move:此实用程序将其参数强制转换为右值引用,使得对象的资源可以被安全地移动。当我们明确地希望调用移动构造函数或移动赋值操作符时,使用 std::move。

T operator=(T &&other) {
      if (this != &other) {
          // 移动资源,而不是复制它们
          resource_ = std::move(other.resource_);
      }
      return *this;
  }

//避免拷贝或产生临时变量
std::vector<int> create_large_vector() {
    std::vector<int> v(1000000);
    // 初始化 v ...
    return std::move(v);  // 使用 move 语义来避免拷贝
}

移动构造函数或移动赋值:
std::move 用于将给定对象(左值或右值)强制转换为右值引用,以便触发移动操作。实质上,std::move 通过告诉编译器我们不再需要原始对象,可以把它当作临时对象来对待,从而允许进行移动操作。所
为什么添加了移动构造、赋值函数?(传统的拷贝构造函数和拷贝赋值操作符对于处理堆上分配的资源(如动态数组、智能指针等)并不高效。当一个临时对象(右值)被复制时,其资源会重新分配给新对象,随后临时对象会被销毁。这意味着需要进行额外的内存分配和释放,可能导致性能下降。)

class cppTest
{
public:
    cppTest(/* args */);
    ~cppTest();
    //移动构造函数
    cppTest(cppTest&& other){
        data = other.data;
        size = other.size;
        other.data = nullptr;   // 释放原始对象的资源
        other.size = 0;
    }

    //移动赋值
    cppTest& operator=(cppTest&& other){
        if (this != &other)
        {
            /* code */
            delete [] data; // 释放当前对象的资源


            data = other.data;
            size = other.size;


            other.data = nullptr;   // 释放原始对象的资源
            other.size = 0;
        }
        return *this;
    }
    
private:
    /* data */
    int *data;
    int size;
};

为什么要引入复杂的引用折叠与完美转发?
答:引用折叠和完美转发机制允许我们以统一的方式处理模版多种参数类型,包括左值和右值。

**完美转发:**关键字std::forward:当我们希望将传递给泛型函数的参数按原样传递给另一个函数时,使用 std::forward。它可以根据调用者传递的参数类型来自动选择应该转发为左值引用还是右值引用,从而保持参数的原始类型保留参数的左值/右值特性,从而实现完美转发。

template <typename F, typename T>
void wrapper(F&& f, T&& arg) {
    f(std::forward<T>(arg));  // 转发 arg 给 f
}

void func(const std::string& s) { /* 使用左值引用操作 */ }
void func(std::string&& s) { /* 使用右值引用操作 */ }

int main() {
    std::string s = "hello";
    wrapper(func, s);            // 调用左值引用版本的 func
    wrapper(func, "temporary");  // 调用右值引用版本的 func
}

引用折叠:
如果不使用引用折叠,将无法实现真正的完美转发。引用折叠规则允许开发者在模板中使用统一的模板参数(如 T&&),无论向其传递左值还是右值。这可以简化函数模板,并确保能够正确地转发参数。std::forward 配合引用折叠规则可以实现将参数正确地转发给目标函数,保留其原始类型。引用折叠规则将多个引用类型折叠到单个引用类型。这在处理模板和完美转发时尤为重要。
C++11后的规则是:“当一个引用类型被另一个引用类型初始化时,两个引用"折叠"为一个引用”
引用折叠规则如下:

1. 左值引用 && 左值引用 -> 左值引用
2. 左值引用 && 右值引用 -> 左值引用
3. 右值引用 && 右值引用 -> 右值引用
4. 右值引用 && 左值引用 -> 左值引用

引用折叠规则使得在完美转发场景中,我们可以使用统一的右值引用语法来处理实际上包括左值引用和右值引用的各种参数类型。在编译期间,引用折叠规则会确定实际的引用类型。

2、new与delete的重载

通过重载newdelete操作符为类定制内存分配和释放策略。这对于需要特殊内存管理策略的来说非常有用。当重载newdelete时,应确保它们成对出现,以便在分配和释放内存时保持一致。

  1. 使用std::size_tnew操作符重载中接受请求的内存大小参数的类型是std::size_t。使用此类型确保跨平台兼容性。
  2. 内存分配失败处理:当分配内存失败时(例如使用malloc函数返回nullptr时),应该抛出std::bad_alloc异常。这与C++默认的new操作符的行为一致,能够正确处理内存不足的情况。
  3. 避免死循环:在自定义newdelete操作符实现中,不要直接调用newdelete,因为这可能导致递归调用。而是使用std::mallocstd::free函数进行内存分配和释放。
  4. 调用构造函数和析构函数:重载newdelete操作符只负责管理内存分配和释放,不会自动调用对象的构造函数和析构函数。确保在使用定位new操作符创建对象时调用构造函数,并在释放对象前调用析构函数。
  5. 全局和类内重载:通常,我们重载类-特定的newdelete操作符以满足特定类的需求。但是,也可以全局重载这些操作符,从而影响整个程序的内存管理。谨慎使用全局重载,因为这可能导致意外的副作用
class MemoryPool {
public:
    MemoryPool(size_t blockSize, size_t blockCount)
        : m_blockSize(blockSize), m_blockCount(blockCount) {
        m_pool.reserve(m_blockCount);
        for (size_t i = 0; i < m_blockCount; ++i) {
            m_pool.push_back(new uint8_t[m_blockSize]);
        }
    }
    ~MemoryPool() {
        for (auto block : m_pool) {
            delete[] block;
        }
    }
    void* allocate() {
        if (m_freeBlocks.empty()) {
            return nullptr;
        } else {
            void* result = m_freeBlocks.back();
            m_freeBlocks.pop_back();
            return result;
        }
    }
    void deallocate(void* ptr) {
        m_freeBlocks.push_back(static_cast<uint8_t*>(ptr));
    }
private:
    size_t m_blockSize;
    size_t m_blockCount;
    std::vector<uint8_t*> m_pool;
    std::vector<uint8_t*> m_freeBlocks;
};
class MyClass {
public:
    static void* operator new(std::size_t size) {
        if (size != sizeof(MyClass)) {
            return ::operator new(size);
        }
        void* ptr = m_memoryPool.allocate();
        if (!ptr) {
            throw std::bad_alloc();
        }
        return ptr;
    }
    static void operator delete(void* ptr, std::size_t size) noexcept {
        if (!ptr || size != sizeof(MyClass)) {
            ::operator delete(ptr);
            return;
        }
        m_memoryPool.deallocate(ptr);
    }
    void someFunction() {
        std::cout << "MyClass::someFunction()" << std::endl;
    }
private:
    static MemoryPool m_memoryPool;
};

3、new、operator new、placement new总结

  1. operator new:这是一个全局函数,用于在堆上分配内存。所谓的全局意味着它不在任何类中(尽管每个类都可以定义自己的operator new),并且它可以通过重载来定制内存分配策略。operator new只负责分配内存,而不负责调用对象的构造函数。例如:
void* operator new(size_t size) {
    return malloc(size);
}
  1. new:这是一个C++关键字,用于动态创建对象。它首先调用operator new为对象分配内存,然后调用对象的构造函数。由于这种方法会自动处理内存分配和对象构造,因此在大多数场景中,我们都会直接使用new。例如:
MyClass* ptr = new MyClass();
  1. placement new:这是一种特殊的new表达式,它允许在已经分配的内存上显式调用对象的构造函数。它不会进行额外的内存分配,并要求提供一个已分配内存块的指针以便直接使用。这对于手动管理内存或需要在特定内存地址创建对象的情况非常有用。例如:
#include  // 必须包含此头文件才能使用placement new
void* memory = malloc(sizeof(MyClass));
MyClass* ptr = new(memory) MyClass(); // 使用placement new在已分配的内存上构造对象

总结及其使用范围:

  • operator new:用于在堆上分配内存,可重载以定制内存分配策略。适用于需要自定义内存分配行为的场景。
  • new:用于动态创建对象,负责同时调用operator new和对象的构造函数。适用于绝大多数常规的动态内存分配场景。
  • placement new:用于在已分配内存上显式调用对象的构造函数。适用于手动管理内存或需要在特定内存地址创建对象的场景。
    详细还可以看我之前的博客文章等:
    C++ 内存管理-- new, delete,new[],placement new 总结
    placement new理解

你可能感兴趣的:(C&C++,c++,性能优化)