C++常用特性原理解析

在我的早期印象中,C++這門語言是軟件工程發展過程中,出于對面向對象語言級支持不可或缺的情況下,一群曾經信誓旦旦想要用C統治宇宙的極客們妥協出來的一個高性能怪咖。

它駁雜萬分,但引人入勝,出于多(mian)種(shi)原因,我把它拿出來進行一次重新的學習。
這篇筆記從G++編譯出的匯編代碼出發,對部分C++的常用面向對象特性進行原理性解釋和總結,其中包括 引用類(成員函數,構造函數)多態(編譯時,運行時)模板與泛型

Here we go!


引用

這是一個老生常談的話題了,C++ primer中文譯本上說引用是對象的一個別名,別名是什么鬼?
上碼:

int invoke(int a) {return ++a;
}int main(int argc, char **argv) {int a = 123;             //       movl   $123,-20(%rbp)int *pa = &a;            //       leaq    -20(%rbp),%rax//       movq    %rax,-16(%rbp)int &ra = a;             //       leaq    -20(%rbp),%rax//       movq    %rax,-8(%rbp)invoke(a);               //       movl    -20(%rbp),%eax//       movl    %eax,%edi//       call    _Z6invokeiinvoke(*pa);             //       movq    -16(%rbp),%rax//       movl    (%rax),%eax//       movl    %eax,%edi//       call    _Z6invokeiinvoke(ra);              //       movq    -8(%rbp),%rax//       movl    (%rax),%eax//       movl    %eax,%edi//       call    _Z6invokei
}

簡單明了,pa是一個指向a的指針,ra是一個a的引用,可以看到編譯器對pa和ra的的定義以及參數傳遞做的工作幾乎是一模一樣,它們都在棧里有自己的空間且都存了一個a的地址,因此可以十分肯定的說引用是用指針實現的。
引用是對指針的一個語言級別的封裝,其出現的意義大概是為了提升程序的可讀性,通常都是用來進行參數傳遞。
關于引用的好處和使用技巧,有待進一步學習。//TODO

類(成員函數,構造函數)

貼代碼之前,有必要回顧一下標號這個概念,在匯編語言里,每條指令的前面都可以擁有一個標號,以代表和指示該指令地址的匯編地址,因為畢竟由我們自己來計算和跟蹤每條指令所在的匯編地址是極其困難的。

在匯編翻譯成機器碼的過程中,這些標號會被轉換成標號所在行的具體偏移地址,多數情況下用來標記指令塊入口地址,就是進行所謂函數的跳轉。忘記的同學可以先行度娘。

接下來的代碼,會在每個函數后的注釋中標出該函數編譯后的標號名。

int invoke(int a) {                   //        _Z6invokeireturn ++a;
}class Animal {
public:int age;int weight;Animal(): age(0), weight(0.0) {}    //        _ZN6AnimalC2Evvoid run() { }                      //        _ZN6Animal3runEv
};class Human {
public:Human() {}                          //        _ZN5HumanC2Ev
};int main(int argc, char **argv) {Animal cat;                         //        leaq  -16(%rbp), %rax//        movq  %rax, %rdi//        call  _ZN6AnimalC1Evcat.age = 5;                        //        movl  $5, -16(%rbp) cat.weight = 2;                     //        movl  $2, -12(%rbp)cat.run();                          //        leaq  -16(%rbp), %rax//        movq  %rax, %rdi//        call  _ZN6Animal3runEv
}

相比上一個例子,這波代碼里,增加了一個Animal類和一個Human類。

我們從main函數開始

  • 對象初始化
    首先語句Animal cat;初始化了一個Animal的對象cat,從右邊的匯編代碼可以看到,cat作為一個復合類型被存入新擴展的棧幀的第16個字節的偏移處-16(%rbp),然后將cat的地址存入rdi,顯而易見,這就是C++在調用類的成員函數時傳遞的隱式參數this指針,接著跳轉到標號名為_ZN6AnimalC1Ev的地方繼續執行,在Animal類里可以看到,對應該標號名的函數就是Animal類的構造函數。

  • 類成員賦值
    這沒什么好談的,跟C里結構體成員的賦值一樣。

  • 成員函數調用
    對成員函數run()的調用,編譯器的處理方式與對構造函數的調用一模一樣。

對比G++編譯過程中對不同的函數的標號命名:
Animal 類
普通函數: invoke() _Z6invokei
普通成員函數:run() _ZN6Animal3runEv
構造函數: Animal() _ZN6AnimalC2Ev
Human 類:
構造函數: Human() _ZN5HumanC2Ev

在語法層面上,C++規定了不同函數的定義和調用方式,編譯器會對不同函數使用不同的處理方式,比如調用成員函數會隱式傳遞this指針,比如直接調用成員函數會導致編譯出錯,在成功編譯后,所有函數都不外乎是以一個特定標號標志的指令序列。
從標號的命名上可以看出C++確保其唯一的方式。

因此,狹義上講,所謂類,其實就是一個復合類型,所謂成員函數,其實就是一個默認會傳遞調用對象本身指針的普通函數,所謂構造函數,其實就是一個在對象初始化的時候會自動調用的普通函數,這些額外的特性都是在編譯階段實現的。

多態(編譯時,運行時)

  • 重載
    從匯編的角度看,重載的多個函數也不過是對應多個不同的標號名而已:
class Animal {
public:void run() {}                     //      _ZN6Animal3runEv  void run(int a) {}                //      _ZN6Animal3runEivoid run(char b) {}               //      _ZN6Animal3runEcvoid run(int a, Human p) {}       //      _ZN6Animal3runEi5Human
};

G++正是通過重載的多個函數的不同形參列表來對標號進行唯一的命名,也是所謂的編譯時多態。

  • 繼承
    簡單的繼承是很容易實現的,第一點,編譯器在分配空間的時候會分配子類自有成員變量和其父類成員變量的總大小,第二點,編譯時會在子類構造函數的中調用父類的構造函數。
    這里就不給例子了,主要篇幅放在下面的運行時多態上。

  • 運行時多態

class Animal {
public:virtual void run() {}              //     _ZN6Animal3runEv
};class Cat : public Animal {
public:void run() {}                      //     _ZN3Cat3runEv
};int main(int argc, char **argv) {Animal *tom = new Cat();           //     _ZN3CatC2Ev://         _ZN6AnimalC2Ev://         movq  $_ZTV6Animal+16, (%rax)//     movq  $_ZTV3Cat+16, (%rax)tom.run();                         //     movq  %rbx, -24(%rbp)//     movq  -24(%rbp), %rax //     movq  (%rax), %rax //     movq  (%rax), %rax//     movq  -24(%rbp), %rdx//     movq  %rdx, %rdi//     call  *%rax
}

這里,我們把new Cat()要調用的2個構造函數按照執行順序進行選擇性展開,可以看到兩條關鍵的匯編代碼,其中(%rax)表示tom對象在堆中的起始位置,于是,唯一有效的最后一條代碼movq $_ZTV3Cat+16, (%rax)將Cat類的_虛函數表_指針存入了cat對象的起始位置。

再看tom.run()的匯編,追蹤發現,最后一條代碼call *%rax正好調用了Cat類的虛函數表的第一個函數。

這就是所謂的運行時多態的調用邏輯,為什么說是所謂的呢?因為這個邏輯在編譯的時候就可以實現了,有些聰明的編譯器會在你將tom指針指向Cat對象的時候就確定了tom到底對哪個run進行調用,它會將tom.run()直接優化編譯成call _ZN3Cat3runEv

那么,什么樣的運行時多態是在編譯階段做不了的呢?看下面代碼:

int main(int argc, char **argv) {Animal *tom;if (argc == 0)  tom = new Animal();else tom = new Cat();tom->run();
}

這時,編譯tom->run()的時候是不可能知道該調哪個run的,所以,根據上一段代碼我們展開的構造函數可以知道,在運行時,哪一個構造函數被調用,tom所指向的對象里就存了哪個類的虛函數表指針,這才是真正意義上的運行時多態。

模板與泛型

class Cat {};
class Mouse {}; template <typename T>
class Cave {
public:void capture(T& a) {}; 
};int main(int argc, char **argv) {Cat tom;Mouse jerry;Cave<Cat> catsCave;                    catsCave.capture(tom);               //       call  _ZN4CaveI3CatE7captureERS0_       Cave<Mouse> miceCave;miceCave.capture(jerry);             //       call  _ZN4CaveI5MouseE7captureERS0_
}

有了之前對函數和標號的認識,理解模板與泛型的實現就是信手拈來了。
編譯器會識別一個模板類有幾種指定了不同類型的聲明,然后會為每一種類型生成對應的唯一的函數標號和不同的函數實現。
就這個簡單的例子來說,編譯器會為抓貓的籠子和抓老鼠的籠子編譯出不同捕捉函數。

傳統的實現方式是為不同的籠子聲明不同的類和函數,這所產生的匯編代碼與使用模板與泛型產生的匯編代碼在功能上是一模一樣的,甚至在代碼細節上都是差不多的,不同的只是標號名罷了。

模板與泛型在語言級別上提供了這種簡便且擴展性極佳的編程方式,這種設計思維是C++所推薦的。


希望這寫篇筆記能夠為C++初學者提供些許指引,同時為我即將開始的求職之路提供一些幫助。

附上《C++程序設計語言》上的一句話:C++是一個可以伴隨你成長的語言。

歡迎批評和討論。

轉載于:https://www.cnblogs.com/JaSonS-toy/p/5347759.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/396432.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/396432.shtml
英文地址,請注明出處:http://en.pswp.cn/news/396432.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

容器created狀態_docker容器狀態的轉換實現

一 docker容器狀態轉換圖二 實戰[rootlocalhost ~]# docker infoContainers: 0Running: 0Paused: 0Stopped: 0Images: 3Server Version: 17.09.0-ceStorage Driver: overlayBacking Filesystem: xfsSupports d_type: falseLogging Driver: json-fileCgroup Driver: cgroupfsPlu…

nodejs命令行執行程序_在NodeJS中編寫命令行應用程序

nodejs命令行執行程序by Peter Benjamin彼得本杰明(Peter Benjamin) 在NodeJS中編寫命令行應用程序 (Writing Command-Line Applications in NodeJS) With the right packages, writing command-line apps in NodeJS is a breeze.有了合適的軟件包&#xff0c;用NodeJS編寫命令…

python re findall 效率_python re模塊findall()詳解

今天寫代碼&#xff0c;在寫到鄭澤的時候遇到了一個坑&#xff0c;這個坑是re模塊下的findall()函數。下面我將結合代碼&#xff0c;記錄一下importrestring"abcdefg acbdgef abcdgfe cadbgfe"#帶括號與不帶括號的區別#不帶括號regexre.compile("((\w)\s\w)&quo…

ubuntu16.04配置sonarqube+MySQL

環境&#xff1a;rootubuntu:~# uname -a Linux ubuntu 4.4.0-21-generic #37-Ubuntu SMP Mon Apr 18 18:33:37 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux rootubuntu:~# rootubuntu:~# cat /etc/issue Ubuntu 16.04 LTS \n \lrootubuntu:~#安裝配置mysql&#xff1a;1、更新源…

mysql 多表混全_mysql--淺談多表查詢1

這是對自己學習燕十八老師mysql教程的總結&#xff0c;非常感謝燕十八老師。依賴軟件&#xff1a;mysql5.6系統環境&#xff1a;win連接查詢在談連接查詢之前我們需要對數學上的笛卡爾積有一定的了解現在有兩個集合m和nm (m1,m2,.....mx)n (n1,n2,.....ny)m*n得到的笛卡爾積有…

鼠標固定在屏幕中間_無線電競黑科技,雷柏VT950Q游戲鼠標評測

雷柏作為目前小有聲譽的PC外設品牌&#xff0c;其定位高性能游戲領域的VT系列產品&#xff0c;想必大家也比較熟悉了。VT系列的產品除了有超強的性能以及出色的設計感&#xff0c;同時還都是性價比非常高的產品&#xff0c;即便是采用了旗艦級傳感器&#xff0c;定位最為高端的…

談論源碼_5,000名開發人員談論他們的薪水

談論源碼Let’s dive into the most interesting results from the O’Reilly 2016 Salary Survey of 5,000 developers (which excluded managers and students).讓我們來看看OReilly 2016年薪金調查對5,000名開發人員(其中不包括經理和學生)最有趣的結果。 性別工資差距是真…

WebSnapshotsHelper(HTML轉換為圖片)

1 /// <summary>2 /// WebBrowser Url生成圖片3 /// HTML轉圖片4 /// </summary>5 public class WebSnapshotsHelper6 {7 Bitmap m_Bitmap;8 string m_Url;9 int m_BrowserWidth, m_BrowserHeight, m_ThumbnailWidth,…

兩個多項式相乘求解系數數組算法

題目描述&#xff1a; 給出兩個多項式&#xff0c;最高次冪分別為n和m&#xff0c;求解這兩個系數相乘得到的系數數組。 分析&#xff1a; 最高次冪如果是m和n&#xff0c;那么他們相乘得到的系數數組的最高次冪一定是nm&#xff0c;對于其他的系數&#xff0c;不妨設a[],b[]是…

synchronized 和 reentrantlock 區別是什么_JUC源碼系列之ReentrantLock源碼解析

目錄ReentrantLock 簡介ReentrantLock 使用示例ReentrantLock 與 synchronized 的區別ReentrantLock 實現原理ReentrantLock 源碼解析ReentrantLock 簡介ReentrantLock 是 JDK 提供的一個可重入的獨占鎖&#xff0c;獨占鎖&#xff1a;同一時間只有一個線程可以持有鎖可重入&am…

gulp 和npm_為什么我離開Gulp和Grunt去看npm腳本

gulp 和npmI know what you’re thinking. WAT?! Didn’t Gulp just kill Grunt? Why can’t we just be content for a few minutes here in JavaScript land? I hear ya, but…我知道你在想什么 WAT &#xff1f;&#xff01; 古爾普不是殺死了咕unt嗎&#xff1f; 為什么…

mysql8.0遞歸_mysql8.0版本遞歸查詢

1.先在mysql數據庫添加數據DROP TABLE IF EXISTS dept;CREATE TABLE dept (id int(11) NOT NULL,pid int(11) DEFAULT NULL,name varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,date datetime(0) DEFAULT NULL,PRIMARY KEY (id) USING BTREE) ENGINE…

js 輪播插件

flexslider pc插件 個人用過flickerplate 移動端插件 個人用過個人覺得比較好的移動端插件swiper http://www.swiper.com.cn/ 用過個人覺得比較好的pc端插件待定

計算機中的字符編碼

字符編碼 什么是計算機編碼 計算機只能處理二進制的數據&#xff0c;其它的數據都要進行轉換&#xff0c;但轉換必須要有一套字符編碼(是字符與二進制的一個對應關系)。常用的字符&#xff1a;a-z、0-9、其它的符號等&#xff0c;計算機也不能直接處理。 &#xff08;字符編碼類…

致力微商_致力于自己。 致力于公益組織。

致力微商by freeCodeCamp通過freeCodeCamp 致力于自己。 致力于公益組織。 (Commit to Yourself. Commit to a Nonprofit.) In case you missed it, our October Summit was jam-packed with several big announcements about our open source community.如果您錯過了它&#…

應急照明市電檢測_應急照明如何供電? 如何接線? 圖文分析!

對于大部分剛接觸建筑電氣設計的工作者來說&#xff0c;應急照明的強啟原理一直都是很頭疼的問題。由于不知道應急照明的強啟原理&#xff0c;所以&#xff0c;應急燈具應該用多少根線&#xff0c;其實也就無從談起。下面以文字和圖片結合的方式來說明應急燈怎么接線的&#xf…

win10網速慢

升級到win10之后發現網速特別慢&#xff0c;搜了下&#xff0c;網上的解決辦法果然好使&#xff0c;按照如下操作即可。 返回桌面&#xff0c;按WINR鍵組合&#xff0c;運行gpedit.msc 打開組策略 依次展開管理模板-》網絡-》QoS數據計劃程序-》限制可保留寬帶&#xff0c;雙擊…

ubuntu安裝nodejs

下載nodejs https://nodejs.org/dist/v4.6.0/node-v4.6.0-linux-x64.tar.gz 解壓 tar -zxvf node-v4.6.0-linux-x64.tar.gz 移動到/opt/下 mv node-v4.6.0-linux-x64 /opt/ 創建鏈接 ln -s /opt/node-v4.6.0-linux-x64/bin/node /usr/local/bin/node 轉載于:https://www.cnblog…

android實用代碼

Android實用代碼七段&#xff08;一&#xff09; 前言 這里積累了一些不常見確又很實用的代碼&#xff0c;每收集7條更新一次&#xff0c;希望能對大家有用。 聲明 歡迎轉載&#xff0c;但請保留文章原始出處:)     博客園&#xff1a;http://www.cnblogs.com 農民伯伯&…

mysql 全文本檢索的列_排序數據列以檢索MySQL中的最大文本值

為此&#xff0c;您可以將ORDER BY與一些聚合函數right()一起使用。讓我們首先創建一個表-create table DemoTable1487-> (-> StudentCode text-> );使用插入命令在表中插入一些記錄-insert into DemoTable1487 values(560);insert into DemoTable1487 values(789);in…