《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
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...