《c++入門經典》筆記13

来源:https://www.cnblogs.com/adapter-chen/archive/2020/07/19/13340177.html
-Advertisement-
Play Games

第十三章 高級引用和指針 13.1按引用傳遞以提高效率 ​ 每次將值按對象傳入函數是,都將創建該對象的一個備份。每次按值從函數返回一個對象時,也將創建其備份。 ​ 對於用戶創建的大型對象,備份的代價很高。這將增加程式占用的記憶體量,而程式的運行速度將更慢。 ​ 在棧中,用戶創建的對象的大小為其成員變數 ...


第十三章 高級引用和指針

13.1按引用傳遞以提高效率

​ 每次將值按對象傳入函數是,都將創建該對象的一個備份。每次按值從函數返回一個對象時,也將創建其備份。

​ 對於用戶創建的大型對象,備份的代價很高。這將增加程式占用的記憶體量,而程式的運行速度將更慢。

​ 在棧中,用戶創建的對象的大小為其成員變數的大小之和,而每個成員變數本身也可能是用戶創建的對象。從性能和記憶體耗用方面說,將如此大的結構傳入棧中的代價非常高。

​ 當然,還有其它代價。對於您創建的類,每次創建備份時,編譯器都將調用一個特殊的構造函數:複製構造函數(拷貝構造函數)。

​ 對於大型對象,調用這些構造函數和析構函數在速度和記憶體耗用方面的代價非常高。

程式清單13.1 ObjectRef.cpp

#include <iostream>

class SimpleCat {
public:
  SimpleCat();            //預設構造函數
  SimpleCat(SimpleCat &); //拷貝構造函數
  ~SimpleCat();
};

SimpleCat::SimpleCat() {
  std::cout << "Simple Cat Constructor ..." << std::endl;
}

SimpleCat::SimpleCat(SimpleCat &) {
  std::cout << "Simple Cat Copy Constructor ..." << std::endl;
}

SimpleCat::~SimpleCat() {
  std::cout << "Simple Cat Destructor ..." << std::endl;
}

SimpleCat FunctionOne(SimpleCat theCat);
SimpleCat *FunctionTwo(SimpleCat *theCat);

int main() {
  std::cout << "Making a Cat ..." << std::endl;
  SimpleCat simpleCat;
  std::cout << "Calling FunctionOne..." << std::endl;
  FunctionOne(simpleCat);
  std::cout << "Calling FunctionTwo..." << std::endl;
  FunctionTwo(&simpleCat);
  return 0;
}

SimpleCat FunctionOne(SimpleCat theCat) {
  std::cout << "Function One. Returning ..." << std::endl;
  return theCat;
}
SimpleCat *FunctionTwo(SimpleCat *theCat) {
  std::cout << "Function Two. Returning ..." << std::endl;
  return theCat;
}

​ 可以看到程式中FunctionOne按值傳遞,FunctionTwo按址傳遞,FunctionOne的調用會產生一次拷貝構造,返回值是SimpleCat類型,所以也會產生一次拷貝構造,然後FunctionOne結束時,兩個拷貝出來的對象(一個是調用時產生,一個是返回時產生)就會被析構函數進行析構。

​ 調用FunctionTwo,因為參數是按引用傳遞,所以不會進行複製備份,也就不會調用拷貝構造函數,所以也就不會有輸出。所以結果中,FunctionTwo所觸發的輸出語句僅有Function Two. Returning ...一句,且並未調用拷貝構造和析構函數。

13.2傳遞const指針

這部分其實前面十一章的筆記有提到。

​ 雖然將指針傳遞給函數效率更高(比如上面的程式),但這也使得對象有被修改的風險。按值傳遞雖然效率較低,但是只是將複製品傳遞,所以並不會影響到原品,也就較按址傳遞多了一層保護。

​ 要同時獲得按值傳遞的安全性和按址傳遞的效率,解決的辦法是傳遞一個指向該類常量的指針(也可為常量的常量指針,即const 類型 *const 指針變數),這樣就不能修改所指向對象的值(如果是常量的常量指針,不僅不能修改所指向對象的值,也不能修改自身的值,即不能指向其它對象)

程式清單13.2 ConstPasser.cpp

#include <iostream>

class SimpleCat {
private:
  int itsAge;

public:
  SimpleCat();
  SimpleCat(SimpleCat &);
  ~SimpleCat();

  int getAge() const { return itsAge; }
  void setAge(int age) { itsAge = age; }
};

SimpleCat::SimpleCat() {
  std::cout << "Simple Cat Constructor ..." << std::endl;
  itsAge = 1;
}

SimpleCat::SimpleCat(SimpleCat &) {
  std::cout << "Simple Cat Copy Constructor ..." << std::endl;
}

SimpleCat::~SimpleCat() {
  std::cout << "Simple Cat Destructor ..." << std::endl;
}

const SimpleCat *const
FunctionTwo(const SimpleCat *const simpleCat); //指向常量的常量指針

int main() {
  std::cout << "Making a cat ..." << std::endl;
  SimpleCat simpleCat;
  std::cout << "simpleCat is now " << simpleCat.getAge() << " years old"
            << std::endl;
  int age = 5;
  simpleCat.setAge(age);
  std::cout << "simpleCat is now " << simpleCat.getAge() << " years old"
            << std::endl;
  std::cout << "Calling FunctionTwo..." << std::endl;
  FunctionTwo(&simpleCat);
  std::cout << "simpleCat is now " << simpleCat.getAge() << " years old"
            << std::endl;
  return 0;
}

const SimpleCat *const FunctionTwo(const SimpleCat *const simpleCat) {
  std::cout << "Function Two. Returning ..." << std::endl;
  std::cout << "simpleCat is now " << simpleCat->getAge() << " years old"
            << std::endl;
  // simpleCat->setAge(9);//報錯,無法對const類型對象進行修改
  return simpleCat;
}

​ 如果將註釋掉的代碼去掉註釋,程式則無法通過,所以可見將參數設置為常量的指針或者是常量的常量指針這種方式兼顧了址傳遞的高效與值傳遞的安全。

13.3作為指針替代品的引用

​ 將上一個程式重寫,使用引用而非指針。

程式清單13.3 RefPasser.cpp

#include <iostream>

class SimpleCat {
private:
  int itsAge;

public:
  SimpleCat();
  SimpleCat(SimpleCat &);
  ~SimpleCat();

  int getAge() const { return itsAge; }
  void setAge(int age) { itsAge = age; }
};

SimpleCat::SimpleCat() {
  std::cout << "Simple Cat Constructor ..." << std::endl;
  itsAge = 1;
}

SimpleCat::SimpleCat(SimpleCat &) {
  std::cout << "Simple Cat Copy Constructor ..." << std::endl;
}

SimpleCat::~SimpleCat() {
  std::cout << "Simple Cat Destructor ..." << std::endl;
}

const SimpleCat &FunctionTwo(const SimpleCat &simpleCat); //指向常量的常量指針

int main() {
  std::cout << "Making a cat ..." << std::endl;
  SimpleCat simpleCat;
  std::cout << "simpleCat is now " << simpleCat.getAge() << " years old"
            << std::endl;
  int age = 5;
  simpleCat.setAge(age);
  std::cout << "simpleCat is now " << simpleCat.getAge() << " years old"
            << std::endl;
  std::cout << "Calling FunctionTwo..." << std::endl;
  FunctionTwo(simpleCat);
  std::cout << "simpleCat is now " << simpleCat.getAge() << " years old"
            << std::endl;
  return 0;
}

const SimpleCat &FunctionTwo(const SimpleCat &simpleCat) {
  std::cout << "Function Two. Returning ..." << std::endl;
  std::cout << "simpleCat is now " << simpleCat.getAge() << " years old"
            << std::endl;
  // simpleCat.setAge(9);//報錯,無法對const類型對象進行修改
  return simpleCat;
}

​ 可見,改用引用效率和代價較指針沒變,但是使用起來較指針簡單。

13.4什麼情況下使用引用以及什麼情況下使用指針

​ 一般而言,c++程式員更喜歡使用引用而不是指針,因為他們更清晰,使用起來更容易。然而,引用不能重新賦值,如果需要依次指向不同的對象,就必須使用指針。引用不能為NULL,因此如果要指向的對象可能為NULL,就必須使用指針,而不能使用引用。如果要從堆中分配動態記憶體,就必須使用指針。

13.5指向對象的引用不在作用域內

​ c++程式員學會按引用傳遞後,常常會瘋狂地使用這種方式。然而,過猶不及,別忘了,引用始終是另一個對象的別名,如果將引用傳入或傳出函數,務必自問:它是哪個對象的別名?當我使用引用時,這個對象還存在嗎?

程式清單13.4 ReturnRef.cpp

#include <iostream>

class SimpleCat {
private:
  int itsAge;
  int itsWeight;

public:
  SimpleCat(int age, int weight);
  ~SimpleCat(){};
  int getAge() { return itsAge; }
  int getWeight() { return itsWeight; }
};

SimpleCat::SimpleCat(int age, int weight) : itsAge(age), itsWeight(weight) {}

SimpleCat &TheFunction();

int main() {
  SimpleCat &rCat = TheFunction();
  int age = rCat.getAge();
  std::cout << "rCat is " << age << " years old!" << std::endl;
  return 0;
}

SimpleCat &TheFunction() {
  SimpleCat simpleCat(5, 9);
  return simpleCat;
}

​ 這個程式一般編譯器是會報錯的,因為TheFunction()函數返回的對象引用是在TheFunction()函數內創建的,但是TheFunction()返回時,創建的這個對象是已經被銷毀了的,也就是返回的引用指向了一個不存在的對象(空引用被禁止),從而被編譯器禁止該程式運行。

13.6返回指向堆中對象的引用

​ 你是否認為:如果TheFunction()函數在堆中創建對象,這樣,返回的時候這個對象就依然存在,也就自然解決了上面的空引用問題。

​ 這種方法的問題在於,使用完該對象後,如何釋放為它分配的記憶體?

程式清單13.5 Leak.cpp

#include <iostream>

class SimpleCat {
private:
  int itsAge;
  int itsWeight;

public:
  SimpleCat(int age, int weight);
  ~SimpleCat(){};
  int getAge() { return itsAge; }
  int getWeight() { return itsWeight; }
};

SimpleCat::SimpleCat(int age, int weight) : itsAge(age), itsWeight(weight) {}

SimpleCat &TheFunction();

int main() {
  SimpleCat &rCat = TheFunction();
  int age = rCat.getAge();
  std::cout << "rCat is " << age << " years old!" << std::endl;
  std::cout << "&rCat is " << &rCat << std::endl;
  SimpleCat *pCat = &rCat;
  delete pCat; //原對象被釋放了,這時候rCat不就成了空引用了嗎?
  return 0;
}

SimpleCat &TheFunction() {
  SimpleCat *simpleCat = new SimpleCat(5, 9);
  std::cout << "SimpleCat: " << simpleCat << std::endl;
  return *simpleCat;
}

​ 不能對引用調用delete,一種聰明的解決方案是創建一個指針並將其指向該引用的對象的地址,這個時候調用delete也就能釋放分配的記憶體,但是往後引用名不就成了空引用嗎?雖然編譯器檢測不到且能正常運行,所以這就埋了一個雷,指不定什麼時候炸了。(正如之前指出,引用必須始終是一個實際存在的對象的別名。如果它指向的是空對象,那麼程式仍是非法的)

​ 對於這種問題,實際上有兩種解決方案。一種是返回一個指針,這樣可以在使用完該指針之後將其刪除。為此,需要將返回值類型聲明為指針而非引用,並返回該指針。

​ 另一種更好的解決方案是:在發出調用的函數中聲明對象,然後將其按引用傳遞給TheFunction()。這種方法的優點是,分配記憶體的函數(發出調用的函數)也負責釋放記憶體。

13.7誰擁有指針

​ 程式在堆中分配的記憶體時將返回一個指針。必須一直讓某個指針指向這塊記憶體,因為如果指針丟失(也就是沒有指針指向它了),便無法釋放該記憶體,進而導致記憶體泄露。

​ 在函數之間傳遞記憶體塊時,其中一個函數“擁有”指針。通常,使用引用傳遞記憶體塊中的值,而分配記憶體塊的函數將負責釋放它,但這是一個大致規則,並非不可打破。

​ 然而,讓一個函數分配記憶體,而另一個函數釋放很危險。在誰擁有指針方面不明確可能會導致兩個問題:忘記刪除指針或重覆刪除。無論哪種情況,都會給程式帶來嚴重的問題。編寫函數時,讓其負責釋放自己分配的記憶體是更安全的做法。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 網上關於Spring迴圈依賴的博客太多了,有很多都分析的很深入,寫的很用心,甚至還畫了時序圖、流程圖幫助讀者理解,我看了後,感覺自己是懂了,但是閉上眼睛,總覺得還沒有完全理解,總覺得還有一兩個坎過不去,對我這種有點笨的人來說,真的好難。當時,我就在想,如果哪一天,我理解了Spring迴圈依賴,一定要 ...
  • 關於編程語言中的註釋,其重要性基本上已為大家所共識。 然而關於註釋的規範,這個話題就像我們之前聊過的縮進、終止符和命名方式一樣,眾口難調。 註釋符通常可分為兩種,即行註釋與塊註釋(inline/block),它們在不同的編程語言中的符號可謂讓人眼花繚亂。 比如行註釋符,它至少有以下的 17 種之多( ...
  • 第十四章 高級函數 14.1重載成員函數 ​ 函數可以進行重載,成員函數(成員方法)實質上也是一種函數,所以成員函數也可以進行重載。 程式清單14.1 Rectangle.cpp #include <iostream> class Rectangle { private: int width; in ...
  • 在寫這篇文章時,我是滿懷感激與賞識之情的。 來誇一個人,講一個道理,寫給大家,也是寫給自己。 來自讀者的反饋 先說說事情的經過。 新書出版之後,昨天第一次看到(抱歉看到的比較晚)讀者的反饋。所謂反饋就是在書中留了GitHub的地址,如果書中有錯誤的地方,讀者可以通過該鏈接提交Issues(問題),來 ...
  • 之前只是很模糊的知道其意思:在request scope中,每個request創建一個新的bean;在session scope中,同一session中的bean都是一樣的。但是不知道怎麼用代碼去驗證它,今天可找到驗證它的代碼了。 ...
  • 隊列實質就是一種存儲數據的結構 通常用鏈表或者數組實現 一般而言隊列具備FIFO先進先出的特性,當然也有雙端隊列(Deque)優先順序隊列 主要操作:入隊(EnQueue)與出隊(Dequeue) BlockingQueue 隊列實質就是一種存儲數據的結構 通常用鏈表或者數組實現 一般而言隊列具備FI ...
  • 1 以前在github發開源項目,都因為懶,從來不構建到中央倉庫。最近因為其他人要用,聯繫我。希望可以發到中央倉庫。我想,不就是mvn deploy嘛,開搞。一圈弄下來,發現真沒那麼簡單。當中遇到了無數的坑,讓我每一次都心裡默默念到,發個項目,為何如此痛苦。 現將痛苦的過程詳細記錄下來。希望可以幫助 ...
  • JDBC中 execute與executeUpdate的區別 execute與executeUpdate的區別 步驟 1 : 相同點 execute與executeUpdate的相同點:都可以執行增加,刪除,修改 package jdbc; import java.sql.Connection; i ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...