博客 / 詳情

返回

每日一個C++知識點|對象資源傳遞機制

C++是一門對內存資源配置要求較高的語言,其中對象資源傳遞在C++開發中無處不在,下面我將在淺拷貝、深拷貝、左值右值、移動語義、完美轉發這5個方面層層遞進地講解C++對象資源傳遞機制,爭取做到知識串聯,深入淺出~

淺拷貝

我們從一個實際場景入手:寫一個Image類,存儲圖片的像素數據,代碼如下:

#include <iostream>
using namespace std;

// 圖片類:管理堆內存中的像素數據
class Image {
public:
    // 構造函數:分配堆內存(相當於“買快遞箱裝圖片數據”)
    Image(int w, int h) : width(w), height(h) {
        // 每個像素佔4字節(RGBA),分配一大塊堆內存
        pixels = new char[width * height * 4]; 
        cout << "構造函數:分配內存,圖片尺寸:" << width << "x" << height << endl;
    }

    // 析構函數:釋放堆內存(相當於“扔掉快遞箱”)
    ~Image() {
        if (pixels != nullptr) {
            delete[] pixels;
            cout << "析構函數:釋放了內存" << endl;
        }
    }

private:
    int width, height;
    char* pixels; // 指向像素數據的指針(核心資源)
};

int main() {
    Image img1(1000, 1000); // 創建1000x1000的圖片
    Image img2 = img1;      // 拷貝img1到img2
    return 0;
}

這段代碼看起來沒問題,卻會觸發內存錯誤。原因就是編譯器默認拷貝方式是
淺拷貝

那麼什麼是淺拷貝呢?淺拷貝只拷貝成員變量的值,不拷貝資源本身,會造成兩個對象共享同一塊堆內存,相當於兩個快遞單號指向同一個快遞箱。當多個對象共享資源,析構函數運行時就會崩潰。在上述代碼中只把img1pixels指針地址複製給img2,沒有把資源本身複製一份,導致程序結束後析構時雙重釋放img2先析構釋放內存,img1析構時又去釋放已經被釋放的內存,直接崩潰

深拷貝

那麼怎麼解決淺拷貝帶來的程序崩潰問題呢?一個簡單的方法是使用深拷貝。深拷貝不僅拷貝成員變量,還為新對象重新分配資源並複製數據,使每個對象擁有獨立資源,提升安全性。下面我們給Image類添加深拷貝構造函數(在原有代碼的基礎上直接添加,其他地方保持不變):

// 深拷貝構造函數:參數是const左值引用(const 類名&)
Image(const Image& other) {
    // 第一步:複製基礎屬性
    width = other.width;
    height = other.height;
    // 第二步:關鍵!重新分配新的堆內存(買新快遞箱)
    pixels = new char[width * height * 4]; 
    // (實際開發中會複製像素數據,這裏重點是“新分配內存”)
    cout << "深拷貝構造函數:新分配了內存" << endl;
}

以上拷貝構造函數會在編譯器中自動執行,因而無需在main函數添加。

此時再運行代碼,img1img2各自擁有獨立內存,程序正常結束。但是深拷貝的內存分配和數據複製會帶來巨大性能開銷,如果是為了處理臨時數據而產生這麼大的開銷,有點浪費資源。那麼我們可不可以在深拷貝完成之後對臨時數據進行刪除呢?

假設我們有一個函數,生成一張臨時的Image對象:

// 返回臨時Image對象(無名字,是“即將銷燬”的右值)
Image createWhiteImage(int w, int h) {
    Image temp(w, h);
    return temp;
}

因為Image temp(w, h)是在函數裏實現的,也就是在棧內實現的,所以對象在函數執行時可以自動創建,函數運行結束後自動釋放銷燬;

再用深拷貝接收這個臨時對象:

Image img3 = createWhiteImage(2000, 2000); // 深拷貝:耗時耗內存

這樣就實現了在深拷貝完成之後對臨時數據進行刪除,但是這就像 “把快遞裏的東西複製一份,再把原快遞箱扔掉”,完全沒必要,這時候移動語義就該登場了。

左值和右值

在瞭解移動語義之前,我們需要了解一個重要的概念——左值右值

左值通常在等號左邊,右值通常在等號右邊,但是左值並非是在等號左邊的對象,右值也並非是在等號右邊的對象

左值是有名字、能取地址的對象,是持久存在 的對象。

右值是無名字、不能取地址的臨時對象,是即將銷燬的對象。

在上述代碼中,img1是左值,int a = 10;中的a是左值;createWhiteImage()的返回值是右值。瞭解左值和右值的基本概念後,我們就能在移動語義中使用它們了~

移動語義

移動語義本質是轉移右值的資源所有權,而非執行資源拷貝,所以可以達到減少資源浪費的效果

移動構造函數

要實現移動語義,需要給Image類添加移動構造函數:

// 移動構造函數:參數是右值引用(類名&&),通常加noexcept
Image(Image&& other) noexcept {
    // 第一步:“偷”走源對象的資源(僅複製指針地址,無內存分配)
    width = other.width;
    height = other.height;
    pixels = other.pixels;

    // 第二步:關鍵!將源對象置為空(作廢原快遞單號,避免析構衝突)
    other.pixels = nullptr;
    other.width = 0;
    other.height = 0;

    cout << "移動構造函數:直接轉移資源,無內存分配!" << endl;
}

此時再接收臨時對象:

Image img3 = createWhiteImage(2000, 2000); // 觸發移動構造,瞬間完成

這就像 “直接把快遞箱的地址改成自己的,不用複製裏面的東西”,性能直接拉滿~

std::move

std::move是把左值 “偽裝” 成右值的小工具,如果想把左值的資源轉移給其他對象,可以用std::move

Image img4(1500, 1500); // 左值
Image img5 = std::move(img4); // 觸發移動構造,img4變為空

注意:它只是強制轉換類型,不會真的移動數據

完美轉發

移動語義解決了臨時對象的拷貝問題,但在模板函數中,會遇到新問題:參數的左值和右值屬性會丟失。

如下代碼所示:

template <typename T>
void wrapper(T x) {
    Image img = x; // 無論x是左值還是右值,都觸發深拷貝
}

// 調用:傳入右值,卻還是深拷貝
wrapper(createWhiteImage(1000, 1000));

由於模板參數x是拷貝後的對象,已經變成了左值,丟失了原來的右值屬性

這時候需要用到完美轉發來解決上述問題,完美轉發在模板中保留參數的左值 / 右值屬性,它需要兩個核心要素:萬能引用std::forward

萬能引用

T&&是萬能引用符號,僅在模板中使用,能綁定左值或右值

std::forward

std::forward:根據參數的原始類型,轉發為左值或右值

用代碼舉例如下:

template <typename T>
void wrapper(T&& x) { // 萬能引用
    Image img = std::forward<T>(x); // 完美轉發:保留屬性
}

// 測試:屬性保留
Image img6(800, 800);
wrapper(img6); // 傳入左值,觸發深拷貝(符合預期)
wrapper(createWhiteImage(800, 800)); // 傳入右值,觸發移動構造

完美轉發就像 “快遞包裝不拆,直接原封不動轉發”,確保參數的屬性不丟失。

總結

以上便是C++對象資源傳遞機制的主要內容,從淺拷貝、深拷貝、左值右值、移動語義、完美轉發層層遞進,如下圖所示:

  1. 淺拷貝:絕對禁用(除非類無動態資源),會導致內存崩潰。
  2. 深拷貝:解決淺拷貝的內存崩潰,但需要更多內存開銷。
  3. 移動語義:處理右值的性能方案,用資源轉移代替拷貝,std::move可把左值轉為右值。
  4. 完美轉發:在模板中使用,保障移動語義在參數傳遞中生效。

如果這篇文章文章對你有用的話, 歡迎點贊收藏加關注哦~

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.