程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 【Deep C (and C++)】深入理解C/C++(3)

【Deep C (and C++)】深入理解C/C++(3)

編輯:C++入門知識

譯自Deep C (and C++) by Olve Maudal and Jon Jagger,本身半桶水不到,如果哪位網友發現有錯,留言指出吧:)

 

第二位候選者表現不錯,那麼,相比大多數程序員,他還有什麼潛力沒有被挖掘呢?

可以從以下幾個角度去考察:

有關平台的問題—32位與64位的編程經驗;

內存對齊;

CPU以及內存優化;

C語言的精髓;

 

接下來,主要分享一下以下相關內容:

內存模型;

優化;

C語言之精髓;

 

 

內存模型:

靜態存儲區(static storage):如果一個對象的標識符被聲明為具有內部鏈接或是外部鏈接,或是存儲類型說明符是static,那麼這個對象具有靜態生存期。這個對象的生命周期是整個程序的運行周期。

PS:內部鏈接,也就是編譯單元內可見,是需要使用static來修飾的,連接程序不可見;外部鏈接,是指別的編譯單元可見,也就是鏈接程序可見。我這裡還不太清楚為什麼需要三種情況來說明。


int* immortal(void) 

    static int storage = 42; 
    return &storage; 

 

自動存儲區(automatic storage):如果一個對象沒有被指明是內部鏈接還是外部鏈接,並且也沒有static修飾,那麼,這個對象具有自動生存期,也稱之為本地生存期。一般使用auto說明符來修飾,只在塊內的變量聲明中允許使用,這樣是默認的情況,因此,很少看到auto說明符。簡單地說,自動存儲區的變量,在一對{}之間有效。


int* zombie(void) 

    auto int storage = 42; 
    return &storage; 

分配的存儲區域(allocated storage):調用calloc函數,malloc函數,realloc函數分配的內存,稱之為分配的存儲區域。他們的作用域(生命周期會是更好的術語嗎?)在分配和釋放之間。

 

int* finite(void) 

    int* ptr = malloc(sizeof(int*)); 
    *ptr = 42; 
    return ptr; 

 

優化相關:

一般來說,編譯的時候,你都應該打開優化選項。強制編譯器更努力的去發現更多的潛在的問題。


上面,同樣地代碼,打開優化選項的編譯器得到了警告信息:a 沒有初始化。

 \

C語言的精髓:

C語言的精髓體現在很多方面,但其本質在於一種社區情感(communitysentiment),這種社區情感建立在C語言的基本原則之上。

C語言原理簡介:

1、  相信程序員;

2、  保持語言簡單精煉;

3、  對每一種操作,僅提供一種方法;(譯者注:?)

4、  盡可能的快,但不保證兼容性;

5、  保持概念上的簡單;

6、  不阻止程序員做他們需要做的事。

 

 

現在來考察一下我們的候選者關於C++的知識:)

 

你:1到10分,你覺得你對C++的理解可以打幾分?

第一個候選者:我覺得我可以打8到9分。

第二個候選者:4分,最多也就5分了。我還需要多加學習C++。

這時,C++之父Bjarne Stroustrup在遠方傳來聲音:我覺得我可以打7分。(OH,MY GOD!!)

 

那麼,下面的代碼段,會輸出什麼?


#include <iostream> 
 
struct X 

    int a; 
    char b; 
    int c; 
}; 
 
int main(void) 

    std::cout << sizeof(X) << std::endl; 

 

第二個候選者:這個結構體是一個樸素的結構體(POD:plain old data),C++標准保證在使用POD的時候,和C語言沒有任何區別。因此,在你的機器上(64位機器,運行在32位兼容模式下),我覺得會輸出12.

順便說一下,使用func(void)而不是用func()顯得有點詭異,因為C++中,void是默認情況,這個相對於C語言的默認是任意多的參數,是不一樣的。這個規則同樣適用於main函數。當然,這不會帶來什麼傷害。但這樣的代碼,看起來就像是頑固的C程序員在痛苦的學習C++的時候所寫的。下面的代碼,看起來更像C++:


#include <iostream> 
 
struct X 

    int a; 
    char b; 
    int c; 
}; 
 
int main() 

    std::cout << sizeof(X) << std::endl; 

 

第一個候選者:這個程序會打印12.

你:好。如果我添加一個成員函數,會怎麼樣?比如:


#include <iostream> 
 
struct X 

    int a; 
    char b; 
    int c; 
 
    void set_value(int v) { a = v; } 
}; 
 
int main() 

    std::cout << sizeof(X) << std::endl; 


第一個候選者:啊?C++中可以這樣做嗎?我覺得你應該使用類(class)。

你:C++中,class和struct有什麼區別?

候選者:在一個class中,你可以有成員函數,但是我不認為在struct中可以擁有成員函數。莫非可以?難道是默認的訪問權限不同?(Is it the default visibility that is different?)

不管怎樣,現在程序會輸出16.因為,會有一個指針指向這個成員函數。

你:真的?如果我多增加兩個函數呢?比如:


#include <iostream> 
 
struct X 

    int a; 
    char b; 
    int c; 
 
    void set_value(int v) { a = v; } 
    int get_value() { return a; } 
    void increase_value() { a++; } 
}; 
 
int main() 

    std::cout << sizeof(X) << std::endl; 

第一個候選者:我覺得對打印24,多了兩個指針?

你:在我的機器上,打印的值比24小。

候選者:啊!對了,當然,這個struct有一個函數指針的表,因此他僅僅需要一個指向這個表的指針!我確實對此有一個很深的理解,我差點忘記了,呵呵。

你:事實上,在我的機器上,這段代碼輸出了12.

候選者心裡犯嘀咕:哦?可能是某些詭異的優化措施在搗鬼,可能是因為這些函數永遠不會被調用。

 

你對第二個候選者說:你怎麼想的?

第二個候選者:在你的機器上?我覺得還是12?

你:好,為什麼?

候選者:因為以這種方式來增加成員函數,不會增加struct的所占內存的大小。對象對他的函數一無所知,反過來,是函數知道他具體屬於哪一個對象。如果你把這寫成C語言的形式,就會變得明朗起來了。

你:你是指這樣的?


struct X 

    int a; 
    char b; 
    int c; 
}; 
 
void set_value(struct X* this, int v) { this->a = v; } 
int get_value(struct X* this) { return this->a; } 
void increase_value(struct X* this) { this->a++; } 

第二個候選者:恩。就想這樣的。現在很明顯很看出,類似這樣的函數是不會增加類型和對象的內存大小的。

 

你:那麼現在呢?


#include <iostream> 
 
struct X 

    int a; 
    char b; 
    int c; 
 
    virtual void set_value(int v) { a = v; } 
    int get_value() { return a; } 
    void increase_value() { a++; } 
}; 
 
int main() 

    std::cout << sizeof(X) << std::endl; 

//注意改變:第一個成員函數變成了虛函數。

第二個候選者:類型所占用的內存大小很有可能會增加。C++標准沒有詳細說明虛類(virtual class)和重載(overriding)具體如何實現。但是一般都是維護一個虛函數表,因此你需要一個指針指向這個虛函數表。所以,這種情況下會增加8字節。這個程序是輸出20嗎?

你:我運行這段程序的時候,得到了24.

候選者:別擔心。極有可能是某些額外的填充,以便對齊指針類型(之前說的內存對齊問題)。

你:不錯。再改一下代碼。


#include <iostream> 
 
struct X 

    int a; 
    char b; 
    int c; 
 
    virtual void set_value(int v) { a = v; } 
    virtual int get_value() { return a; } 
    virtual void increase_value() { a++; } 
}; 
 
int main() 

    std::cout << sizeof(X) << std::endl; 

現在會發生什麼?

第二個候選者:依舊打印24.每一個類,只有一個虛函數表指針的。

你:恩。什麼是虛函數表?

候選者:在C++中,一般使用虛函數表技術來支持多態性。它基本上就是函數調用的跳轉表(jump table),依靠虛函數表,在繼承體系中,你可以實現函數的重載。

 

讓我們來看看另一段代碼:


#include "B.hpp" 
 
class A { 
    public: 
      A(int sz) { sz_ = sz; v = new B[sz_]; } 
      ~A() { delete v; } 
      //... 
    private: 
      //... 
      B* v; 
      int sz_; 
}; 

看看這段代碼。假設我是一名資深的C++程序員,現在要加入你的團隊。我向你提交了這麼個代碼段。請從學術的層面,盡可能詳細輕柔的給我講解這段代碼可能存在的陷阱,盡可能的跟我說說一些C++的處理事情的方式。

第一個候選者:這是一段比較差的代碼。這是你的代碼?首先,不要使用兩個空格來表示縮進。還有class A後面的大括號要另起一行。sz_?我從來沒見過如此命名的。你應該參照GoF標准_sz或且微軟標准m_sz來命名。(GoF標准?)

你:還有呢?

候選者:恩?你是不是覺得在釋放一個數組對象的時候,應該使用delete []來取代delete?說真的,我的經驗告訴我,沒必要。現代的編譯器可以很好的處理這個事情。

你:好?有考慮過C++的“rule of three“原則嗎?你需要支持或是不允許復制這一類對象嗎?

PS:

(來自維奇百科http://en.wikipedia.org/wiki/Rule_of_three_(C%2B%2B_programming))

The rule of three (also known asthe Law of The Big Three or The Big Three) is a rule of thumb in C++ that claimsthat if a class defines one of the following itshould probably explicitly define all three:

§  destructor

§  copy constructor

§  assignment operator

也就是說,在C++中,如果需要顯式定義析構函數、拷貝構造函數、賦值操作符中的一個,那麼通常也會需要顯式定義余下的兩個。

 

第一個候選者:恩。無所謂了。聽都沒聽說過tree-rule。當然,如果用戶要拷貝這一類對象的話,會出現問題。但是,這也許就是C++的本質,給程序員無窮盡的噩夢。

順便說一下,我想你應該知道哎C++中所有的析構函數都應該定義為virtual函數。我在一些書上看到過這個原則,這主要是為了防止在析構子類對象時候出現內存洩露。

你心裡嘀咕:或是類似的玩意。Another ice cream perhaps?(我還是沒搞明白這到底哪門情感)

 

令人愉悅的第二個候選者登場了:)

 

候選者:哦,我該從何說起呢?先關注一些比較重要的東西吧。

首先是析構函數。如果你使用了操作符new[],那麼你就應該使用操作符delete[]進行析構。使用操作符delete[]的話,在數組中的每一個對象的析構函數被調用以後,所占用的內存會被釋放。例如,如果像上面的代碼那樣寫的話,B類的構造函數會被執行sz次,但是析構函數僅僅被調用1次。這個時候,如果B類的構造函數動態分配了內存,那麼就是造成內存洩漏。

接下類,會談到“rule of three”。如果你需要析構函數,那麼你可能要麼實現要麼顯式禁止拷貝構造函數和賦值操作符。由編譯器生成的這兩者中任何一個,很大可能不能正常工作。

還有一個小問題,但是也很重要。通常使用成員初始化列表來初始化一個對象。在上面的例子中,還體現不出來這樣做的重要性。但是當成員對象比較復雜的時候,相比讓對象隱式地使用默認值來初始化成員,然後在進行賦值操作來說,使用初始化列表顯式初始化成員更為合理。

先把代碼修改一下:)然後再進一步闡述問題。

你改善了一下代碼,如下:


#include "B.hpp" 
 
class A 

    public: 
      A(int sz) { sz_ = sz; v = new B[sz_]; } 
      ~A() { delete[] v; } 
      //... 
    private: 
      A(const A&); 
      A& operator=(const A&); 
      //... 
      B* v; 
      int sz_; 
}; 
這個時候,這位候選者(第二個)說:好多了。

你進一步改進,如下:


#include "B.hpp" 
 
class A 

    public: 
      A(int sz) { sz_ = sz; v = new B[sz_]; } 
      virtual ~A() { delete[] v; } 
      //... 
    private: 
      A(const A&); 
      A& operator=(const A&); 
      //... 
      B* v; 
      int sz_; 
}; 
 

第二位候選者忙說道:別著急,耐心點。

接著他說:在這樣的一個類中,定義一個virtual的析構函數,有什麼意義?這裡沒有虛函數,因此,如果以此作為基類,派生出一個類,有點不可理喻。我知道是有一些程序員把非虛類作為基類來設計繼承體系,但是我真的覺得他們誤解了面向對象技術的一個關鍵點。我建議你析構函數的virtual說明符去掉。virtual這個關鍵字,用在析構函數上的時候,他有這麼個作用:指示這個class是否被設計成一個基類。存在virtual,那麼表明這個class應該作為一個基類,那麼這個class應該是一個virtual class。

還是改一下初始化列表的問題吧:)

 

於是代碼被你修改為如下:


#include "B.hpp" 
 
class A 

    public: 
      A(int sz):sz_(sz), v(new B[sz_]) { } 
      ~A() { delete[] v; } 
      //... 
    private: 
      A(const A&); 
      A& operator=(const A&); 
      //... 
      B* v; 
      int sz_; 
}; 
 

第二個候選者說:恩,有了初始化列表。但是,你有沒有注意到由此有產生了新的問題?

你編譯的時候使用了-Wall選項嗎?你應該使用-Wextra、-pedantic還有-Weffc++選項。如果沒有警告出現,你可能沒有注意到這裡發生的錯誤。但是如果你提高了警告級別,你會發現問題不少。

一個不錯的經驗法則是:總是按照成員被定義的順序來書寫初始化列表,也就是說,成員按照自己被定義的順序來呗初始化。在這個例子中,當v(new B[sz_])執行的時候,sz_還沒有被定義。然後,sz_被初始化為sz。

事實上,C++代碼中,類似的事情太常見了。

 

你於是把代碼修改為:


#include "B.hpp" 
 
class A 

    public: 
      A(int sz):v(new B[sz]), sz_(sz) { } 
      ~A() { delete[] v; } 
      //... 
    private: 
      A(const A&); 
      A& operator=(const A&); 
      //... 
      B* v; 
      int sz_; 
}; 

第二個候選者:現在好多了。還有什麼需要改進的嗎?接下來我會提到一些小問題。。。

在C++代碼中,看到一個光禿禿的指針,不是一個好的跡象。很多好的C++程序員都會盡可能的避免這樣使用指針。當然,例子中的v看起來有點像STL中的vector,或且差不多類似於此的東西。

對於你的私有變量,你貌似使用了一些不同的命名約定。在此,我的看法是,只要這些變量是私有的,你愛怎麼命名就怎麼命名。你可以使得你的變量全部以_作為後綴,或且遵循微軟命名規范,m_作為前綴。但是,請你不要使用_作為前綴來命名你的變量,以免和C語言保留的命名規范、Posix以及編譯器的命名規則相混淆:)

 

 

摘自 Rockics的專欄

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved