C++程式開發技巧

来源:https://www.cnblogs.com/BlickWinkel/archive/2023/06/04/17453337.html
-Advertisement-
Play Games

## 引言 > 類(class)的使用分為兩種——基於對象(object Based)和麵向對象(object oriented) > > 基於對象是指,程式設計中單一的類,和其他類沒有任何關係 > > 單一的類又分為:不帶指針的類(class without pointer members)和帶指 ...


引言

類(class)的使用分為兩種——基於對象(object Based)和麵向對象(object oriented)

基於對象是指,程式設計中單一的類,和其他類沒有任何關係

單一的類又分為:不帶指針的類(class without pointer members)和帶指針的類(class with pointer members)

面向對象則是類(class)中涉及了類之間的關係:複合(composition)、委托(delegation)、繼承(inheritance)

1.頭文件的防禦式聲明

#ifndef xxx
#define xxx
...
#endif

在編寫頭文件時應該有這樣的一種習慣

目的是避免多次重覆包含同一個頭文件,否則會引起變數及類的重覆定義

2.使用初始化列表的好處

  • 只有構造函數這類函數具有“初始化列表”這一特性
  • 從結果上來看,構造函數時使用初始化列表和在類內賦值是一樣的,但我們都知道一個變數必須先初始化然後才被賦值,而初始化列表顧名思義是只執行初始化這一步,在類內賦值時就要先初始化再被賦值,所以從執行效率上講,使用初始化列表會更快,也更簡潔

3.設計模式:singleton(單例類)

  • demo:
class A {
public:
    static A & getInstance();
    setup() {...}
private:
    A();
    A(const A & rhs);
    ...
}

A & A::getInstance()
{
    static A a;
    return a;
}

...
//外部介面
A::getInstance().setup();
  • 原理:將構造函數設置為私有屬性,同時設置一個靜態函數介面返回一個該類對象

  • 作用:保證每一個類僅有一個實例,併為它提供一個全局訪問點

  • 單例模式(Singleton)的主要特點不是根據用戶程式調用生成一個新的實例,而是控制某個類型的實例唯一性。它擁有一個私有構造函數,這確保用戶無法通過new直接實例它。除此之外,該模式中包含一個靜態私有成員變數instance與靜態公有方法Instance()。Instance()方法負責檢驗並實例化自己,然後存儲在靜態成員變數中,以確保只有一個實例被創建。

    這種模式主要有以下特征或條件:

    1. 有一個私有的無參構造函數,這可以防止其他類實例化它,而且單例類也不應該被繼承,如果單例類允許繼承那麼每個子類都可以創建實例,這就違背了Singleton模式“唯一實例”的初衷。
    2. 單例類被定義為sealed,就像前面提到的該類不應該被繼承,所以為了保險起見可以把該類定義成不允許派生,但沒有要求一定要這樣定義。
    3. 一個靜態的變數用來保存單實例的引用。
    4. 一個公有的靜態方法用來獲取單實例的引用,如果實例為null即創建一個。
  • 參考:

    設計模式詳解:Singleton(單例類)_singleton類_p_帽子戲法的博客-CSDN博客

    單例模式(Singleton)的6種實現 - JK_Rush - 博客園 (cnblogs.com)

4.常成員函數的重要性

  • 如果一個成員函數不改變類的數據成員時,就把它聲明為常函數,這是一個好的習慣

  • 當實例化一個常對象時,常對象要求不能改變數據成員,如果成員函數不加const,將無法調用此成員函數,編譯器不會通過,即使此函數確實沒有改變數據成員;同時,即使成員函數被聲明為了常函數,實例化一個普通對象時依然可以調用。

  • 簡單來說,不聲明為常成員函數可能不會有問題,但聲明為常成員函數能確保一定不出問題

5.如何解釋成員函數接收同類對象參數...

  • 問:如何解釋一個類的成員函數在接收同類對象的參數(比如拷貝構造函數)可以直接調用該對象的任何成員,明明既不是友元也不是嵌套?

  • 答:相同class的各個objects互為friends(友元)

6.設計一個類要考慮什麼

  • 目的:高效、安全、簡潔、嚴密
  • 1.數據成員私有
  • 2.參數傳遞和返回值優先考慮用引用(傳遞的是地址值,這樣不管傳遞的數據記憶體占用多大,依然固定傳入四個位元組,即使當傳遞字元這樣小於四個位元組時用值傳遞確實比引用或指針傳遞更快一些,但不必考慮這些細枝末節)
  • 3.構造函數優先去使用初始化列表
  • 4.能聲明為常成員就聲明為常成員

7.返回值加不加引用

  • 取決於返回的值是否要改變、是否可以改變,前者由我們決定,後者由語法限制

兩個案例

class A
{
    int value;
    ...
};

...
    
A& fun1(A* x, const A& y)
{
    x.value += y.value; //第一參數會改變,第二參數不會改變
    return *x
}
class B
{
    int value;
    ...
};

...

B fun2(const B& x, const B& y)
{
	//第一參數和第二參數都不會改變
	return B(x.value + y.value);
}

8.運算符重載成員函數的思考

e.x.

inline complex&
_doapl(comlex* ths, const complex& r)
{
	...
	return *ths;
}

inline complex&
complex::operator += (const comlex& r)
{
	return _doapl(this, r);
}

...

comlex c1(2,1), c2(3), c3;
//c2 += c1;
//c3 += c2 += c1;

當重載一個二元運算符為成員函數時,我們知道重載函數除了右操作數是我們傳遞的,函數還會預設用一個this指針,來接收左操作數

那麼可能會有疑問,我們想要改變的是左操作數,而且由於傳遞的是指針,函數內也確實可以改變,那返回值又有什麼用呢,聲明為空不就行了。

當我們使用重載運算符時只是像被註釋的第一行代碼一樣,那麼返回值確實不重要,但是當我們使用的形式像被註釋的第二行代碼時,那麼返回值就很重要,因為c2.+=(c1) 這個函數的返回值就是 c3 += () 函數的參數

9.temp object(臨時對象)

  • 語法: typename ();
  • 生存期:僅聲明那一行
  • 返回值是臨時對象時不難return by reference

10.<<重載的一些註意點

  • 只能重載為非成員函數

  • 左操作數固定為系統定義的 ostream 類型,且為非常量引用

    • 非常量:向流寫入內容其實就是改變了它的狀態
    • 引用:無法複製一個 ostream 對象
    • 註:ostream 類與 istream 類一樣,它的的拷貝構造函數和賦值函數都是保護類型的,所以 ostream 是不允許拷貝或者賦值的,所以它也不能直接作為返回類型和參數傳遞,很多時候需要使用引用來進行傳遞。
  • 最好加返回值且為引用,原因前面已經說明,且我們對於連續調用<<的頻率要大得多

  • 連續調用時的調用順序

    complex c1, c2;
    cout << c1 << c2;
    //先執行 <<(cout, c1) 函數
    //返回的 ostream類型的cout的引用 又作為<<(ostream &, c2)的第一參數 
    
  • 在語法上我們當然也可以重載為成員函數,只要左操作數為自定義類型即可,但這樣並不符合我們通常的書寫習慣

11.Big Three(三位一體原則)

  • 三大件:拷貝構造、拷貝賦值、析構函數

  • 解釋:當一個類需要我們去主動設計析構函數時,那它很大概率也需要一個拷貝構造函數和賦值運算符重載成員函數

  • 應用:當一個類具有指針成員時(class with point member)或者說當我們設計了一個有動態記憶體管理的類時

  • 原因:

    • 析構函數角度:預設析構函數會僅刪除指向對象的指針,而刪除一個指針不會釋放指針指向對象占用的記憶體,最終會導致記憶體泄露

    • 拷貝構造角度:預設的構造函數是淺拷貝,複製的只是指針也就是地址值,這樣導致兩個對象共用一個記憶體空間,這是十分危險的,當其中一個對象被刪除後,析構函數將釋放那片共用的記憶體空間,接下來對這片已經釋放了記憶體的任何引用都將會導致不可遇見的後果。

    • 賦值運算角度:

      賦值相比於拷貝構造要考慮更多

      首先是自我賦值判斷,如果不判斷,當左右操作數指向的是同一個地址時,會造成將左操作數對象的元素刪除並釋放其占用的記憶體,同時由於左右操作數指向同一對象,導致右操作數同時被刪除,但接下來還要將右操作對象複製,這會造成不可預知的結果。這也被稱為證同測試。

      其次是進行三步必要操作:

      • 釋放已有記憶體
      • 開闢新的記憶體
      • 內容賦值
  • 代碼示例:

#ifndef __MYSTRING__
#define __MYSTRING__

class String
{
public:                                 
   String(const char* cstr=0);//構造函數
   String(const String& str);//拷貝構造函數           
   String& operator=(const String& str);//重載=運算符 
   ~String();//析構函數                               
   char* get_c_str() const { return m_data; }//成員函數,返回指向字元數組首地址的指針
private:
   char* m_data;//字元數組指針
};

#include <cstring>

//構造函數
inline
String::String(const char* cstr)
{
   //開闢記憶體、計算長度、內容拷貝
   if (cstr) {
      m_data = new char[strlen(cstr)+1];
      strcpy(m_data, cstr);
   }
   else {   
      m_data = new char[1];
      *m_data = '\0';
   }
}

//析構函數
inline
String::~String()
{
   delete[] m_data;//釋放指針指向空間
}

//重載=
inline
String& String::operator=(const String& str)
{
   //檢測自我賦值(self assignment)
   if (this == &str)
      return *this;

   delete[] m_data;
   m_data = new char[ strlen(str.m_data) + 1 ];
   strcpy(m_data, str.m_data);
   return *this;
}

//構造函數
inline
String::String(const String& str)
{
   m_data = new char[ strlen(str.m_data) + 1 ];
   strcpy(m_data, str.m_data);
}

#include <iostream>
using namespace std;

//重載<<
ostream& operator<<(ostream& os, const String& str)
{
   os << str.get_c_str();
   return os;
}

#endif

12.new和構造函數,delete和析構函數

  • 當我們使用new創建了一個指向類的對象的指針時

    這裡的new幹了三件事:

    • 調用 operator new 函數,這個函數內部又調用了malloc函數來分配記憶體
    • operator new 函數返回的是空指針,顯式轉換為類類型指針後賦值給我們創建的指針
    • 調用指針指向對象的構造函數

  • 當我們使用delete釋放一個指向對象的指針時
    • 首先調用對象的析構函數,這裡該類的析構函數又用delete釋放了類內動態分配的數組指針
    • 然後再釋放這個指向對象的指針
    • 結合本例來看,就是delete ps,先delete它指向的成員,再delete它自己

13.malloc()動態分配記憶體的結構

  • 紅色部分是 cookie ,記錄記憶體分配的總大小,就是圖中的41,其最低位用於表示是否已分配(1表示已分配,0表示已回收),之所以最低位可以變,是因為分配的記憶體總空間一定是16的倍數,其16進位表示時最低位一定為0,也就是說這個位置是空出來的,剛好用來表示記憶體狀態。每一個 new 的對象都會有上下兩個 cookie,來預先申請一塊記憶體池,然後供對象實例化。

  • 綠色部分是調用malloc()時向系統申請的記憶體,該函數返回時,也會返回這塊區域開頭的指針。

  • 綠色部分上下兩塊 gap 預先被填充為了0xfdfdfdfd,用來分隔客戶可以使用的記憶體區和不可使用的記憶體區,同時,當這塊記憶體被歸還時,編輯器也可以通過下gap的值區判斷當前記憶體塊是否被越界使用了

  • 從gap向上連續的7個記憶體空間共同組成了debug header,從上向下標號為1-7

    • 1、2兩塊空間保存了兩根指針,目的是使多個記憶體塊連接成鏈表。
    • 3空間保存了申請本記憶體塊的文件名
    • 4空間保存了申請本記憶體塊的代碼行數
    • 5空間記錄了本記憶體塊中實際可以被用戶使用的記憶體空間的大小
    • 6空間記錄了當前記憶體塊的流水號,即是鏈表中的第幾個,從1開始
    • 7空間記錄了當前記憶體塊被分配的形式
  • 填補區pad

參考:

https://zhuanlan.zhihu.com/p/492161361

https://www.cnblogs.com/zyb993963526/p/15682014.html#_label2

https://blog.csdn.net/qq_61500888/article/details/122170203

14.array new 搭配 array delete

動態分配數組時要註意的:

  • 其記憶體區域相比上面所提到的多了一個記憶體塊用來記錄數組長度(分配對象數量)
  • 當申請記憶體後,返回的指針指向數據開始處,而使用 delete[] 釋放時,指針會指向它的上一塊,也就是記錄數組長度的那一塊,從而可以根據對象的數量調用相應次數的析構函數。如果使用 delete 釋放的話,它不會去獲取對象的長度,而是只調用指針指向的那一個對象的析構函數。
  • 如果對象的類型是內置類型或者是無自定義的析構函數的類類型,是可以使用 delete 來釋放 new[] 對象的。但是,如若不然,使用 delete 來釋放對象,對象所分配的記憶體空間雖然會照樣全部釋放,但是只會調用第一個對象的析構函數,這就導致記憶體泄漏。所以,養成良好的習慣,new [] 必 delete []

15.this指針

類的每一個非靜態成員函數(包括構造函數、拷貝構造等)都隱含著一個指針形參名為this,當對象調用成員函數時就會隱含傳遞該對象的地址給它,這也是為什麼一個類的成員函數雖然只有一份但也會根據接收的消息不同產生不同的行為,而靜態成員函數不隱含this指針,所以即使調用它的對象不同維護的依然是同一段代碼

16.namespace

三個案例:

  • 使用using引入一個命名空間的全部
  • 使用using引入一個命名空間的個體
  • 不用using,使用時手動引入


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

-Advertisement-
Play Games
更多相關文章
  • 在一些複雜的業務表中間查詢數據,有時候操作會比較複雜一些,不過基於SqlSugar的相關操作,處理的代碼會比較簡單一些,以前我在隨筆《基於SqlSugar的開發框架循序漸進介紹(2)-- 基於中間表的查詢處理》介紹過基於主表和中間表的聯合查詢,而往往實際會比這個會複雜一些。本篇隨筆介紹聯合多個表進行... ...
  • 一、功能變數名稱解析過程 DNS功能變數名稱完整解析過程 1、查詢本地 hosts文件 解析記錄 2、查詢客戶端本地DNS緩存記錄 3、訪問DNS轉發(緩存)伺服器本地緩存記錄 4、轉發到權威伺服器查詢本地緩存記錄 5、訪問權威伺服器解析記錄 6、權威伺服器迭代查詢 6.1、訪問子域權威伺服器查詢本地緩存記錄 6. ...
  • # ulimit Linux ulimit命令用於控制shell程式的資源。 ```shell [email protected] 10:41:17 [pwd:~]# ulimit --help ulimit: ulimit [-SHabcdefiklmnpqrstuvxPT ...
  • ## 01|修改Win用戶名 打開運行,輸入cmd,回車; 輸入control userpasswords2,回車; 點擊屬性,修改用戶名,點擊確定; 打開運行,輸入regedit,回車; 定位到HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\Cur ...
  • [TOC](快速上手kettle(三)壺中可以放些啥?) ### 序言 快速上手kettle開篇中,我們將kettle比作壺,並對這個壺做了簡單介紹。 而上一期中我們實現了①將csv文件通過kettle轉換成excel文件; ②將excel文件通過kettle寫入到MySQL資料庫表中 這兩個案例。 ...
  • # mysql編碼錯誤 ![image-20230604115322661](https://img2023.cnblogs.com/blog/2862884/202306/2862884-20230604115422584-1121547495.png) 問題出現在用django的admin組件向 ...
  • # **鎖** ## **概述** - **介紹** 鎖是電腦協調多個進程或線程併發訪問某一資源的機制。在資料庫中,除傳統的計算資源(CPU、RAM、I/O)的爭用以外,數據也是一種供許多用戶共用的資源。如何保證數據併發訪問的一致性、有效性是所有資料庫必須解決的一個問題,鎖衝突也是影響資料庫併發訪 ...
  • # 基於 Web 和 Deep Zoom 的高解析度大圖查看器的實踐 高解析度大圖像在 Web 中查看可以使用 Deep Zoom 技術,這是一種用於查看和瀏覽大型高解析度圖像的技術,它可以讓用戶以交互方式瀏覽高解析度大圖像,並且能夠在不影響圖像質量的情況下進行縮放和平移操作。 ## 技術點 ### ...
一周排行
    -Advertisement-
    Play Games
  • 1. 說明 /* Performs operations on System.String instances that contain file or directory path information. These operations are performed in a cross-pla ...
  • 視頻地址:【WebApi+Vue3從0到1搭建《許可權管理系統》系列視頻:搭建JWT系統鑒權-嗶哩嗶哩】 https://b23.tv/R6cOcDO qq群:801913255 一、在appsettings.json中設置鑒權屬性 /*jwt鑒權*/ "JwtSetting": { "Issuer" ...
  • 引言 集成測試可在包含應用支持基礎結構(如資料庫、文件系統和網路)的級別上確保應用組件功能正常。 ASP.NET Core 通過將單元測試框架與測試 Web 主機和記憶體中測試伺服器結合使用來支持集成測試。 簡介 集成測試與單元測試相比,能夠在更廣泛的級別上評估應用的組件,確認多個組件一起工作以生成預 ...
  • 在.NET Emit編程中,我們探討了運算操作指令的重要性和應用。這些指令包括各種數學運算、位操作和比較操作,能夠在動態生成的代碼中實現對數據的處理和操作。通過這些指令,開發人員可以靈活地進行算術運算、邏輯運算和比較操作,從而實現各種複雜的演算法和邏輯......本篇之後,將進入第七部分:實戰項目 ...
  • 前言 多表頭表格是一個常見的業務需求,然而WPF中卻沒有預設實現這個功能,得益於WPF強大的控制項模板設計,我們可以通過修改控制項模板的方式自己實現它。 一、需求分析 下圖為一個典型的統計表格,統計1-12月的數據。 此時我們有一個需求,需要將月份按季度劃分,以便能夠直觀地看到季度統計數據,以下為該需求 ...
  • 如何將 ASP.NET Core MVC 項目的視圖分離到另一個項目 在當下這個年代 SPA 已是主流,人們早已忘記了 MVC 以及 Razor 的故事。但是在某些場景下 SSR 還是有意想不到效果。比如某些靜態頁面,比如追求首屏載入速度的時候。最近在項目中回歸傳統效果還是不錯。 有的時候我們希望將 ...
  • System.AggregateException: 發生一個或多個錯誤。 > Microsoft.WebTools.Shared.Exceptions.WebToolsException: 生成失敗。檢查輸出視窗瞭解更多詳細信息。 內部異常堆棧跟蹤的結尾 > (內部異常 #0) Microsoft ...
  • 引言 在上一章節我們實戰了在Asp.Net Core中的項目實戰,這一章節講解一下如何測試Asp.Net Core的中間件。 TestServer 還記得我們在集成測試中提供的TestServer嗎? TestServer 是由 Microsoft.AspNetCore.TestHost 包提供的。 ...
  • 在發現結果為真的WHEN子句時,CASE表達式的真假值判斷會終止,剩餘的WHEN子句會被忽略: CASE WHEN col_1 IN ('a', 'b') THEN '第一' WHEN col_1 IN ('a') THEN '第二' ELSE '其他' END 註意: 統一各分支返回的數據類型. ...
  • 在C#編程世界中,語法的精妙之處往往體現在那些看似微小卻極具影響力的符號與結構之中。其中,“_ =” 這一組合突然出現還真不知道什麼意思。本文將深入剖析“_ =” 的含義、工作原理及其在實際編程中的廣泛應用,揭示其作為C#語法奇兵的重要角色。 一、下劃線 _:神秘的棄元符號 下劃線 _ 在C#中並非 ...