Chapter 1

来源:https://www.cnblogs.com/Lingh/archive/2022/08/24/16618434.html
-Advertisement-
Play Games

1 讓自己習慣C++ 條款 01 視 C++ 為一個語言聯邦 C : C++以C為基礎,block、語句、預處理器、內置數據類型、數組、指針都來自於C。當使用C++中的C成分工作時,沒有模板(Template)、沒有異常(Exceptions)、沒有重載(overloading)。 Object-O ...


1 讓自己習慣C++

條款 01 視 C++ 為一個語言聯邦

  • C : C++以C為基礎,block、語句、預處理器、內置數據類型、數組、指針都來自於C。當使用C++中的C成分工作時,沒有模板(Template)、沒有異常(Exceptions)、沒有重載(overloading)。

  • Object-Oriented C++ : 也就是 C with classes,classes(包括構造函數和析構函數)、封裝(encapsulation)、繼承(inheritance)、多態(polymorphism)、virtual函數(動態綁定)......等等。

  • Template C++ : C++的泛型編程部分。

  • STL(Standard Template Library) : 對容器、迭代器、演算法以及函數對象對的規約有極佳的緊密配合與協調。

*請記住 : *

1. 高效編程守則視狀況而變化,取決於你使用C++的哪一部分。

條款 02 儘量以 const,enum,inline 替代 #define

例 : `#define ASPECT_RATIO 1.653`預處理器會將程式中的`ASPET_RATIO`記號全部替換成數值`1.653`,也就是這個記號不會進入記號表內,這樣在程式出錯時獲取的錯誤信息會很難分析。

​ 當我們使用#define 實現巨集時,更要小心,必須為巨集中的所有實參加上小括弧。例 :

#define CALL_WITH_MAX(a,b) f((a) > (b) ? (a) : (b))//一定要加小括弧,這裡的例子還體現不出什麼,註意加括弧的位置就行

int a = 5,b = 0;
//當第一個參數(++a)大於第二個參數時,++a會被執行兩次,這顯然很容易出問題
CALL_WITH_MAX(++a,b);
CALL_WITH_MAX(++a,b+10);

解決方法 :

  1. 以一個常量替換上述巨集。

    const double Aspect_ratio = 1.653; 作為一個語言常量,自然會進入記號表,這樣在Debug的時候容易跟蹤,且生成的代碼量比較小,開支較小,因為巨集定義是盲目地替換欄位。

    以 const 代替 #define 時要註意兩點 :

    ​ (1) 定義常量指針時,要註意頂層 const 和底層 const 的區別。頂層 const 是將 const 放在 '*' 左邊,意思是指針指向的常量類型,所以該指針所指向的內容不能被修改;

    ​ 而底層 const 則是將 const 放在 '*' 右邊,意思是我這個指針是一個常量指針,只能指向在初始化時的值,不能指向其他變數。

    ​ (2)定義 class 專屬常量時,也就是類內 static 成員,例如

    class GamePlayer{
    private:
        static const int NumTurns = 5;
        int scores[NumTurns];
    };
    

    ​ 註意,上例中的的static const int NumTurns = 5; 是聲明式而不是定義式。C++要求要對使用的任何東西都要提供一個定義式。

    但如果它是個 class 專屬常量,又是 static 且為整數類型(integral type,例如int,char,bool),只要不取指針,則只需要提供聲明式,若有些編譯器不支持,則需要在實現文件中定義。(註意非整型必須定義,初值可以放在定義式中)

    //.h
    class CostEstimate{
    private:
        static const double FudgeFactor;
    };
    
    //.c
    const double CostEstimate::FudgeFactor = 1.35;
    

    ​ 另外,#define 不重視作用域(scope),在定義後編譯過程中都有效。

  2. 使用 enum hack

    這種做法比較像#define,同樣不能取地址,同樣會導致非必要的記憶體分配。

  3. 對於“實現巨集“,用template inline函數,同樣是代碼替換,但這種做法屬於函數操作,一切按函數操作就行了,不用操心參數問題,同時遵守作用域和訪問規則。

請記住:

1. 對於單純常量,最好以 const 對象或 enum 替換 #define

2. 對於形似函數的巨集(macros),最好改用 inline template 函數替代 #define

條款 03 儘可能使用const

​ 頂層 const : 表示 const 修飾的類型為常量,特別地,對於指針類型,const 在 ‘*’ 右邊時表示頂層指針,也就是該指針變數為常量, 與該指針變數指向的對象是否為常量無要求。

int i = 0;
int *const p1 = &i;//const 在‘*’右邊,表示p1只能指向i(頂層指針)

​ 底層 const : 與指針和引用類型有關,對於指針類型,當 const 在 ‘*’ 左邊(一般寫在類型左邊)時,表示該指針指向的對象為常量類型, 此時該指針可以指向別的對象,當不能通過指針解引用來更改所指對象的值。

​ 而對於引用類型,都是底層 const,也就是所引用的值為常量,但const引用可以綁定常量與非常量,這是一個特殊的例子。

int i = 0;
const int j = 1;
const int &ref_i = i;//可以綁定非常量
const int &ref_j = j;//可以綁定常量類型
const int &ref = 10;//會創建臨時變數,然後綁定

​ const可以和函數的返回值、參數、函數自身產生關聯,儘可能地使用它,可以讓編譯器幫你更快的發現錯誤!

const成員函數

​ 將 const 作用在成員函數上,可以區分出常量對象和非常量對象所使用的不同版本的成員函數。const成員函數可以保證不修改類的成員變數。

//.h
class TextBlock{
public:
   // ...
    const char& operator[](std::size_t position) const//const對象調用此函數
    { return text[position]; }
    char& operator[](std::size_t position)//非const對象調用此函數
    { return text[position]; }
private:
    std::string text;
};

//.cpp
TextBlock tb("Hello");
std::cout << tb[0];//調用 char& operator[](std::size_t position)

const TextBlock ctb("Hello");
std::cout << ctb[0];//調用 const char& operator[](std::size_t position) const

​ 在C++中,不修改類的成員變數指的是編譯器強制實施的“bitwise constness”,也就是保證每一個bit都不允許修改,但現實中,我們可能會希望一部分成員不被修改,一部分成員被修改。

​ 此時,我們可以將能在const成員函數中修改的成員變數聲明為mutable

//.h
class CTextBlock{
public:
    //...
    std::size_t length() const;
private:
    char* pText;
    mutable std::size_t textLength;
    mutable bool lengthIsVaild;
};

//.cpp
std::size_t length() const
{
    if(!lengthIsVaild)
    {
        pText = "Hello";//錯誤,非mutable變數在const成員函數中禁止修改
        textLength = std::strlen(pText);//允許修改
        lengthIsvaild = true;//允許修改
    }
    return textLength;
}

在const和non-const成員函數中避免重覆

​ 很多時候,const和non-const函數知識返回值不同,函數定義會出現冗餘。

class TextBlock{
public;
    //...
    const char& operator[](std::size_t position) const
	{
		//...	//邊界檢查
    	//...	//日誌數據訪問
    	//...	//檢驗數據完整性
    	return text[position];
	}
    
/*
	//冗餘版本
	char& operator[](std::size_t position)
	{
		//...	//邊界檢查
    	//...	//日誌數據訪問
    	//...	//檢驗數據完整性
    	return text[position];
	}
*/
    char& operator[](std::size_t position)
    {
        return const_cast<char&>(//去除返回值的const屬性
        		static_cast<const TextBlock>(*this)[position];//static_cast安全轉型為const對象調用const版本,返回值為const char*
        );
    }
private:
    std::string text;
};

解決冗餘的原則是,在非常量的版本中對常量版本進行轉型,因為我們永遠不對常量版本的東西作任何修改,只單純調用,這完全符合const的設計 : const成員函數保證不修改成員。這避免了不必要的風險。

請記住:

1. 將某些聲明為 const 可幫助編譯器偵測出錯誤用法。 const 可被施加於任何作用域內的對象、函數參數、函數返回類型、成員函數 本體。

2. 編譯器強制實施 bitwise constness,但你編寫程式時應該使用“概念上的常量性(conceptul constness)。”

3. 當 const 和 non-const 成員函數有著實質等價的實現時,領 non-const 版本調用 const 版本可避免代碼重覆。

條款 04 確定對象被使用前已先被初始化

​ 有些類型(stl)保證內容能被預設初始化,而有些卻不行。

​ 因此我們強制規定 : 永遠在使用對象之前先將它初始化。

int x = 0;
const char* text = "hello world";
double d;
std::cin >> d;

​ 在C++中,除了內置類型外,初始化往往由構造函數完成。

​ 因此我們強制規定 : 確保每一個構造函數都將對象中的每一個成員初始化。

​ 註意不要混淆初始化和賦值這兩種概念。

//.h
class PhoneNumber {...};
class ABEntry{
public:
    ABEntry(const std::string& name, const std::string& address,
           	const std::list<PhoneNumber>& phones);
private:
    std::string theName;
    std::string theAddress;
    std::list<PhoneNumber> thePhones;
    int numTimesConsulted;
};

//.cpp
ABEntry::ABEntry(const std::string& name, const std::string& address,
           	const std::list<PhoneNumber>& phones)
{
    //賦值
    theName = name;
    theAddress = address;
    thePhones = phones;
    numTimesConsulted = 0;
}

​ 該構造函數定義中的行為是賦值,因為在進入構造函數定義也就是大括弧部分之前所有的成員變數已經被預設構造函數構造了。此時又 重新執行一遍賦值函數,一共兩次動作,開銷太大。

​ 因此

ABEntry::ABEntry(const std::string& name, const std::string& address,
           	const std::list<PhoneNumber>& phones)
	:theName(name),
	  theAddress(address),
	  thePhones(phones),
	  numTimesConsulted(0)
{ }

​ 此時對於各個成員,只執行依次構造函數即可完成初始化。

​ 另外,初始化的次序應該對應聲明的次序。可以避免一些隱晦的錯誤。

現在我們關註“不同編譯單元內定義的non-local static對象”的初始化次序 :

  • 編譯單元 : 產出單一目標問價的源碼。基本上是 單一源碼文件 + 其所加入到的頭文件。註意!編譯器編譯不同單元的次序無法確定!
  • static對象 : 壽命從被構造出來直到程式結束,程式結束時自動調用其析構函數。
  • non-local static對象 : 不在函數內的static對象。
  • local static對象 : 在函數內的static對象。

我們設想一種這樣的情況 :

​ 在一個編譯單元記憶體在一個對象

class FileSystem{
public:
    ...
    std::size_t numDisks() const;
    ...
};
extern FileSystem tfs;//聲明外部變數,預備給其他文件(編譯單元)使用

​ 在另一個編譯單元記憶體在另一個對象

class Directroy{
public:
    Directory( params );
    ...       
};

Directory::Directory( params )
{
    ...
    std::size_t disks = tfs.numDisks();    
}

Directory temp( params );

因為不同編譯單元編譯的次序是不確定的,我們無法保證Directory對象定義之前,類FileSystem已經被編譯,這樣會出現錯誤。

解決方法 :

Singleton模式

因為C++保證 : 函數內的local static對象會在“該函數被調用期間”“首次遇上該對象的定義式”時被初始化。

//編譯單元。
class FileSystem {...};
FileSystem& tfs()
{
	static FileSystem fs;
	return fs;
}

//另一個編譯單元
class Directory {...};
Directory::Directory( params )
{
	...
	std::size_t disks = tfs().numDisks();
	...
}

Directory& tempDir()
{
	static Directory td;
	return td;
}

因此可以保證調用tfs()時,只有FileSystem的定義式被編譯器遇到時才給裡面的fs對象初始化並返回。

但內含static對象在多線程系統中線程不安全。

*請記住 : *

1. 為內置型對象進行手工初始化,因為C++不保證初始化它們。

2. 構造函數最好使用成員初始化列表,而不要在構造函數本體內使用賦值操作,但如果有多個構造函數會產生冗餘代碼時,可以將具 有確定的預設初始化值的變數賦值封裝進一個函數,其餘還是使用列表初始化。同時,列表初始化的次序也應該與聲明次序相同。

3. 為免除"跨編譯單元的初始化次序"問題,請以 local static 對象替換 non-local static 對象。


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

-Advertisement-
Play Games
更多相關文章
  • DevCraft適用於所有.NET和JavaScript框架的功能豐富的UI組件。通過專業設計的組件和主題,構建更加美觀且現代的應用程式。 ...
  • 雖說是一個任務管理系統,但簡單地講,其實就是任務的增刪改查(CRUD)。 其中最重要的又當屬增,即創建任務,此為數據之源,刪改查都依賴於它所產生的數據。接下來就從交互設計到前端,服務端,資料庫一步步去實現任務的創建。 ...
  • 本文,將向大家介紹一種將多個 CSS 技巧運用到極致的技巧,利用純 CSS 實現拼圖游戲。 本技巧源自於 Temani Afif 的 CodePen CSS Only Puzzle game。一款完全由 CSS 實現的拼圖游戲。 我們要做的,就是將散落的圖片碎塊,複原成一幅完整的圖,像是這樣: 註意 ...
  • Vue中的插槽相信使用過Vue的小伙伴或多或少的都用過,但是你是否瞭解它全部用法呢?本篇文章就為大家帶來Vue3中插槽的全部用法來幫助大家查漏補缺。 什麼是插槽 簡單來說就是子組件中的提供給父組件使用的一個坑位,用<slot></slot> 表示,父組件可以在這個坑位中填充任何模板代碼然後子組件中< ...
  • 內聯模板 點擊打開視頻講解更加詳細 當 inline-template 這個特殊的 attribute 出現在一個子組件上時,這個組件將會使用其裡面的內容作為模板,而不是將其作為被分發的內容。這使得模板的撰寫工作更加靈活。 <my-component inline-template> <div> < ...
  • 1. MyBatis數據輸入 1.1 Mybatis總體機制概括 1.2 概念說明 註意:這裡的簡單類型不是指的基本數據類型。 1.3 單個簡單類型參數 1.3.1 Mapper介面中的抽象方法 public interface EmpMapper { /** * 通過這個方法對應Mapper配置文 ...
  • 服務一個人的系統,和服務一億人的系統,複雜度有著天壤之別。本文從工程師文化、組織戰略、公司內部協作等角度來分析軟體複雜度形成的原因,並提出了一些切實可落地的解法。 ...
  • 一、問題的描述 在實際的系統應用開發中我經常會遇到這樣的一類需求,相信大家在工作中也會經常遇到: 同一個系統在多個省份部署。 一個業務在北京是一種實現方式,是基於北京用戶的需求。 同樣的業務在上海是另外一種實現方式,與北京的實現方式大同小異 遇到這樣的需求,我們通常會定義一個業務實現的介面,比如: ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...