現代C++學習指南-類型系統

来源:https://www.cnblogs.com/honguilee/archive/2023/06/13/17478638.html
-Advertisement-
Play Games

> 在前一篇,我們提供了一個方向性的指南,但是學什麼,怎麼學卻沒有詳細展開。本篇將在前文的基礎上,著重介紹下怎樣學習C++的類型系統。 ### 寫在前面 在進入類型系統之前,我們應該先達成一項共識——儘可能使用C++的現代語法。眾所周知,出於相容性的考慮,C++中很多語法都是合法的。但是隨著新版本的 ...


在前一篇,我們提供了一個方向性的指南,但是學什麼,怎麼學卻沒有詳細展開。本篇將在前文的基礎上,著重介紹下怎樣學習C++的類型系統。

寫在前面

在進入類型系統之前,我們應該先達成一項共識——儘可能使用C++的現代語法。眾所周知,出於相容性的考慮,C++中很多語法都是合法的。但是隨著新版本的推出,有些語法可能是不推薦或者是需要避免使用的。所以本篇也儘可能採用推薦的語法形式(基於C++11或以上版本),這也是現代C++標題的含義。

採用現代語法有兩點好處。其一,現代語法可以編譯出更快更健壯的代碼。編譯器也是隨著語言的發展而發展的,現代語法可以在一定程度上幫助編譯器做更好的優化。其二,現代語法通常更簡潔,更直觀,也更統一,有助於增強可讀性和可維護性。
明確了這點後,讓我們一起踏入現代C++的大門吧。

類型系統

程式是一種計算工具,根據輸入,和預定義的計算方法,產生計算結果。當程式運行起來後,這三者都需要在記憶體中表示成合適的值才能讓程式正常工作,負責解釋的這套工具就是類型系統。數字,字元串,鍵盤滑鼠事件等都是數據,而且在記憶體中實際存在的形式也是一樣的,但是按我們人類的眼光來看的話,對它們的處理是不一樣的。數字能進行加減乘除等算術運算,但是對字元串進行算術運算就沒有意義,而鍵盤滑鼠的值通常只是讀取,不進行計算的。正是由於這些差異,編程語言的第一個任務就是需要定義一套類型系統,告訴電腦怎樣處理記憶體中的數據。
為了讓編程語言儘可能簡單,編程語言一般把類型系統分為兩步實現,一部分是編譯器,另一部分是類型。編譯器那部分負責將開發者的代碼解釋成合適的形式,以便可以高效,準確在記憶體中表示。類型部分則定義一些編譯器能處理的類型,以便開發者可以找到合適的數據來完成輸入輸出的表示和計算方法的描述。這兩者相輔相成,相互成就。
類型作為類型系統的重要表現形式,在編程語言中的重要性也就不言而喻了。如果把寫程式看成是搭積木的話,那麼程式的積木就是類型系統。類型系統是開發者能操作的最小單位,它限制了開發者的操作規則,但是提供了無限的可能。C++有著比積木更靈活的類型系統。

類型

類型是編程語言的最小單位,任何一句代碼都是一種記憶體使用形式。
而談到C++的類型也就不得不談到它的三種類型表現形式——普通類型,指針,引用。它們是三種不同的記憶體使用和解釋形式,也是C++的最基礎的形式。和大部分編程語言不同,C++對內置類型沒有做特權處理,只要開發者願意,所有的類型都可以有一致的語法形式(通過運算符重載),所以下麵關於類型的舉例適合所有的類型。
普通類型就是沒有修飾的類型,如int,long,double等。它們是按值傳遞的,也就是賦值和函數傳參是拷貝一份值,對拷貝後的值進行操作,不會再影響到老值。

int a=1; //老值,存在地址1
int b=a; //新值,存在地址2
b=2; //改變新值,改變地址2
//此時a還是1,b變成了2

那假如我們需要修改老值呢,有兩種途徑,一種是指針,另一種則是引用。
指針是C/C++裡面的魔法,一切皆可指針。指針包含兩個方面,一方面它是指一塊記憶體,另一方面它可以指允許對這塊記憶體進行的操作。指針的值是一塊記憶體地址,操作指針,操作的是它指向的那塊地址。

int a=1; //老值,存在地址1
int* b=&a; //&代表取地址,從右往左讀,取a的地址——地址1,存在地址2
*b=2; //*是解引用,意思是把存在地址2(b)的值取出來,並把那個地址(地址1)的值改成2
//此時a,*b變成了2


引用則是指針的改進版,引用能避免無效引用,不過引用不能重設,比指針缺少一定的靈活性。

int a=1; //老值,存在地址1
int& b=a; //&出現在變數聲明的位置,代表該變數是引用變數,引用變數必須在聲明時初始化
b=2; //可以像普通變數一樣操作引用變數,同時,對它的操作也會反應到原始對象上
//此時a,b變成了2

變數定義

類型僅僅是一種語法定義,而要真正使用這種定義,我們需要用類型來定義變數,即變數定義。
C++變數定義是以下形式:

type name[{initial_value}]

這裡的關鍵在於typetype是類型和限定符的組合。看下麵的例子:

int a; //普通整型
int* b; //類型是int和*的組合,組成了整型指針
const int* c; //從右往左讀,*是指針,const int是常量整型,組成了指向常量整型的指針類型
int *const d; //也是從右往左讀,const是常量,後面是指針,說明這個指針是常量指針,指向最左邊的int,組成常量指針指向整型
int& e=a; //類型是int和&的組合,組成了整型引用
constexpr int f=a+e; //constexpr代表這個變數需要在編譯期求值,並且不再可變。

以上,基本就是變數定義的所有形式了,類型確定了變數的基本屬性,而限定符限定了變數的使用範圍。
定義變數也是按照這個步驟進行,首先確定我們需要什麼類型的變數,其次再進一步確定是否需要對這個變數添加限定,很多時候是需要的。可以按以下步驟來確定添加什麼樣的限定符:

  1. 是個大對象,可以考慮把變數聲明成引用類型。通常引用類型是比指針類型更優的選擇。
  2. 大對象可能需要被重置,可以考慮聲明為指針。
  3. 只想要個常量,添加constexpr
  4. 只想讀這個變數,添加const

變數初始化

變數定義往往伴隨著初始化,這對於局部變數來說很重要,因為局部變數的初值是不確定的,在沒有對變數進行有效初始化前就使用變數,會導致不可控的問題。所以嚴格來說,前面的變數定義是不完全正確的。
C++11推出了全新的,統一的初始化方式,即在變數名後面跟著大括弧,大括弧里包著初始化的值。這種方式可以用在任何變數上,稱之為統一初始化,如:

int a{9527}; //普通類型
string b={"abc"}; //另一種寫法,等價但是不推薦
Student c{"張三","20220226",18}; //大括弧中是構造函數參數

當然,除了用類型名來定義變數外,還可以將定義和初始化合二為一,變成下麵這種最簡潔的形式:

auto a={1}; //推導為整型
auto b=string{"abc"}; 
auto c=Student{"張三","20220226",18}

這裡auto是讓編譯器自己確定類型的意思。上面這種寫法是完全利用了C++的類型推導,這也是好多現代語言推薦的形式。不過需要註意的是,使用類型推導後,=就不能省略了。
有了初始化的變數後,我們就可以用它們完成各種計算任務了。C++為開發者實現了很多內置的計算支持。如數字的加減乘除運算,數組的索引,指針的操作等。還提供了分支ifswitch,迴圈whilefor等語句,為我們提供了更靈活的操作。

函數

變數是編程語言中的最小單位,隨著業務的複雜度增加,有些時候中間計算會分散業務的邏輯,增加複雜度。為了更好地組織代碼,類型系統增加了 函數來解決這個問題。
函數也是類型,是一種複合類型。它的類型由參數列表,返回值組合而成,也就是說兩個函數,假如參數列表和返回值一樣,那麼它們從編譯器的角度來看是等價的。當然光有它們還不夠,不然怎麼能出現兩個參數列表和返回值一樣的函數呢。一個完整的函數還需要有個函數體和函數名。所以函數一般是下麵這種形式:

//常規函數形式
[constexpr] 返回值 函數名(參數列表)[noexcept]{
    函數體
    }

//返回值後置形式
auto 函數名(參數列表)->返回值

當一個函數沒有函數體的時候,我們通常稱之為函數聲明。加上函數體就是一個函數定義。

void f(int); //函數聲明
void fun(int value){  //函數定義,因為有大括弧代表的函數體
    
}

以上就是函數的基本框架,接下來我們分別來看一看組成它的各部分。
先說最簡單的函數名,它其實是函數這種類型的一個變數,這個變數的值表示從記憶體地址的某個位置開始的一段代碼塊。前面也說過之所以能出現兩個參數列表和返回值都相同的函數,但是編譯器能識別,其主要功勞就在函數名上,所以函數名也和變數名一樣,是一種標識符。那假如反過來,函數名相同,但是參數列表或者返回值不同呢,這種情況有個專有名詞——函數重載。基於函數是複合類型的認識,它們中只要其中一種不同就算重載。另外,在C++11,還有一種沒有名字的函數,稱為lambda表達式。lambda表達式是一種類似於直接量的函數值,就像13,'c'這種,是一種不提前定義函數,直接在調用處定義並使用的函數形式。
參數列表是前面類型定義的升級款。所有前面說的關於變數定義的都適用於它,三種形式的變數定義,多個變數,變數初始化等。不過,它們都有了新名詞。參數列表的變數稱為形式參數,初始化稱為預設參數。同樣形參在實際使用的時候需要初始化,不過初始化來自調用方。形式參數沒有預設值就需要在調用的時候提供參數,有預設值的可以省略。

int plus(int a,int b=1){ //b是一個預設參數
    return a+b;
}

int main(void){
    int c=plus(1); //沒有提供b的值,所以b初始化為1,結果是2
    int d=plus(2,2); //a,b都初始化為2,結果是4
    //int f=plus(1,2,3); //plus只有兩個形參,也就是兩個變數,沒法保存三個值,所以編譯錯誤
    return 0;
}

和參數列表一樣,返回值也是一個變數,這個變數會通過return語句返回給調用者,所以從記憶體操作來看,它是一個賦值操作。

std::string msg(){
    std::string input;
    std::cin>>input;
    return input;
}

int main(void){
    auto a=msg();
    std::string b=msg();//msg返回的input複製到了b中
    return 0;
}

遺憾的是C++只支持單返回值,也就是一個函數調用最多只能返回一個值,假如有多個值就只能以形參形式返回了,這種方式對於函數調用就不是很友好,所以C++提出了新的解決思路。

隨著業務的複雜度再次增加,函數形參個數可能會增加,或者可能需要返回多個值,然後在多個不同的函數間傳遞。這樣會導致數據容易錯亂,並且增加使用者的學習成本。
為瞭解決這些問題,工程師們提出了面向對象——多個數據打包的技術。表現在語言層面上,就是用類把一組操作和完成這組操作需要的數據打包在一起。數據作為類的屬性,操作作為類的方法,使用者通過方法操作內部數據,數據不再需要使用者自己傳遞,管理。這對於開發者無疑是大大簡化了操作。我們稱之為面向對象編程,而在函數間傳遞數據的方式稱為面向過程編程。這兩種方式底層邏輯其實是一致的,該傳遞的參數和函數調用一樣都不少,但是面向對象的區別是這些繁瑣、容易出錯的工作交給編譯器來做,開發者只需要按照面向對象的規則做好設計工作就好了,剩下的交給編譯器。至此,我們的類型系統又向上提升了一級。類不僅是多個類型的聚合體,還是多個函數的聚合體,是比函數更高級的抽象。
可以看下麵面向過程編程和麵向對象編程的代碼對比

struct Computer{
    bool booted;
    friend std::ostream& operator<<(std::ostream& os,const Computer & c){
        os<<"Computing";
        return os;
    }
};

void boot(Computer& c){
    c.booted=true;
    std::cout<<"Booting...";
}

void compute(const Computer& c){
    if(c.booted){
       std::cout<<"Compute with "<<c;
    }
}

void shutdown(Computer& c){
    c.booted=false;
    std::cout<<"Shutdown...";
}

int main(void){
    auto c=Computer();
    boot(c);
    compute(c);
    shutdown(c);
    return 0;                                                                                                         
}

面向過程最主要的表現就是,開發者需要在函數間傳遞數據,並維護數據狀態,上面例子中的數據是c

struct Computer{
    bool booted;
    
    friend std::ostream& operator<<(std::ostream& os,const Computer & c){
        os<<"Computing";
        return os;
    }

    void boot(){
        booted=true;
        std::cout<<"Booting...";
    }

    void compute(){
        if(booted){
            std::cout<<"Compute with "<<this;
        }
    }

    void shutdown(){
        booted=false;
        std::cout<<"Shutdown...";
    }
};

int main(void){
    auto c=Computer();
    c.boot();
    c.compute();
    c.shutdown();
    return 0;
}

可以看出面向對象的代碼最主要的變化是,方法的參數變少了,但是可以在方法裡面直接訪問到類定義的數據。另一個變化發生在調用端。調用端是用數據調用方法,而不是往方法裡面傳遞數據。這也是面向對象的本質——以數據為中心。
當然,類的封裝功能只是類功能的一小部分,後面我們會涉及到更多的類知識。作為初學者,我們瞭解到這一步就能讀懂大部分代碼了。

總結

類型系統是一門語言的基本構成部分,它支撐著整個系統的高級功能,很多高級特性都是在類型系統的基礎上演化而來的。所以學習語言的類型系統有個從低到高,又從高到低的過程,從最基礎的類型開始,學習如何從低級類型構築出高級類型,然後站在高級類型的高度上,審視高級類型是怎樣由低級類型構築的。這一上一下,一高一低基本上就能把語言的大部分特性瞭解清楚了。
低級類型更偏向於讓編譯器更好地工作,高級類型偏向於讓開發者更好地工作,C++從普通類型,函數,類提供了各個層級的支持,讓開發者有更多自由的選擇,當然也就增加了開發者的學習難度。但是開發者並不是都需要所有選擇的,所以我覺得正確的學習應該是以項目規模為指導的。一些項目,完全用不到面向對象,就可以把精力放在打造好用的函數集上。而有的項目,面向對象是很好的選擇,就需要在類上花費時間。回到開頭的積木例子,選用什麼積木完全看我們想搭什麼模型,要是沒有合適的積木,我們可以自己創造。這就是C++的迷人之處。


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

-Advertisement-
Play Games
更多相關文章
  • 生活中我們經常談及 “架構”,那麼到底什麼是 “架構”,Robert C.Martin《架構整潔之道》中的定義:軟體架構是指設計軟體的人為軟體賦予的形狀,這個形狀是指系統如何被劃分為組件 (Components),各個組件如何排列(Arrangement),組件之間如何溝通(Communicatio... ...
  • # 下載 https://www.wxwidgets.org/downloads/ 下載壓縮包即可 ![image](https://img2023.cnblogs.com/blog/916065/202306/916065-20230614040303993-2082032985.png) # 編 ...
  • # 問題背景 nexus3 的 admin 賬號密碼忘記了,需要重置。 # 環境說明 ``` nexus 基於 docker-compose 部署,版本 nexus3.26 docker 鏡像 sonatype/nexus3:3.26.1 ``` # 操作步驟 參考: https://support ...
  • ## 1、求解器 ### 1.1 複製源碼 本案例以icoFoam為例,複製【openFOAM/OpenFOAM-9/applications/solvers/incompressible/icoFoam】文件夾至run文件夾下(我的是【openFOAM/mtl-9/run/solvers/inco ...
  • 某日二師兄參加XXX科技公司的C++工程師開發崗位第13面: > 面試官:什麼是`RAII`? > > 二師兄:`RAII`是`Resource Acquisition Is Initialization`的縮寫。翻譯成中文是資源獲取即初始化。 > > 面試官:`RAII`有什麼特點和優勢? > > ...
  • 在springBoot的啟動類中,提供了一個mai函數的程式入口,來啟動載入SpringBoot程式,那麼註解@SpringBootApplication,通過源碼可以看到,它相當於@ComponentScan + @EnableAutoConfiguration + @SpringBootConf ...
  • C++是一門有著四十年曆史的語言,先後經歷過四次版本大升級(誕生、98、11、17(20),14算小升級)。每次升級都是很多問題和解決方案的取捨。瞭解這些歷史,能更好地幫助我們理清語言的發展脈絡。所以接下來我將借它的發展歷程,談一談我對它的理解,最後給出我認為比較合理的學習路線指南。 ### C++ ...
  • > 本文首發於公眾號:Hunter後端 > 原文鏈接:[celery筆記三之task和task的調用](https://mp.weixin.qq.com/s/AIobDZVDWV3r_XauvmkVKA) 這一篇筆記介紹 task 和 task 的調用。 以下是本篇筆記目錄: 1. 基礎的 task ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...