從零開始學C++(1 變量和基本類型)

  接下來的幾篇文章介紹C++的基礎知識點。c++

 

  C++是一種靜態數據類型語言,它的類型檢查發生在編譯時。所以,編譯器必須知道程序中每個變量對應的數據類型。express

  數據類型是程序的基礎:它告訴咱們數據的意義以及咱們能在數據上執行的操做。數組

   好比:i = i + j;   這條語句的具體含義要取決於i、j的類型安全

  

  void也是一種類型,即空類型。函數

  算術類型:整型(integral type,包括字符和布爾類型)、浮點型佈局

  算術類型所佔的位(bit)數,在不一樣機器上有所差異。C++標準規定了類型的最小尺寸,好比int最小佔16位,char最小佔8位,long最小佔32位,long long最小佔64位,float最小6位有效數字,double最小10位有效數字等等。this

  布爾類型(bool)的取值是真(true)或者假(false)。spa

  基本的字符類型是char,一個char的空間應確保能夠存放機器基本字符集中任意字符對應的數字值。其餘字符類型用於擴展字符集,如wchar_t、char16_t、char32_t。wchar_t類型用於確保能夠存放機器最大擴展字符集中的任意一個字符,char16_t和char32_t則爲Unicode字符集服務(Unicode是用於表示全部天然語言中字符的標準)。指針

  

   簡單介紹一下內置類型的機器實現:日誌

    計算機是以二進制形式存儲數據的,也就是每一個位非0即1。通常計算機都以2的整數次冪個bit做爲塊來處理內存,大多數以 8 bits 構成一個最小的可尋址內存塊,稱之爲一個字節(byte)。

    而計算機將內存中的每一個字節與一個數字(也就是地址)關聯起來,例如:

    

    圖中,左側的數字是該字節的地址,右側是 8 bits的具體內容。

    使用某個地址來表示從這個地址開始的大小不一樣的比特串,例如,地址1000的那個字,地址1003的那個字節。因而可知,爲了明確某個地址的含義,必須首先知道存儲該地址的數據的類型,也就是須要知道數據所佔的位數以及該如何解釋這些bit的內容。

 

  帶符號類型(signed)和無符號類型(unsigned)

    類型int、short、long和long long都是帶符號的,類型名前添加unsigned獲得無符號類型。

    字符型被分爲了三種:char、signed char和unsigned char。特別須要注意的是:類型char和類型signed char並不同。類型char實際上會表現爲帶符號的和無符號的兩種形式中的一種,具備是哪一種由編譯器決定。

 

  類型轉換(convert):將對象從一種給定的類型轉換爲另外一種相關類型。

  若是表達式裏既有帶符號類型又有無符號類型,當帶符號類型取值爲負時會出現異常結果:

int val1 = 10; unsigned val2 = -20; std::cout << val1 + val2 << std::endl;

 

  這段代碼獲得的結果就不是咱們想要的,由於結果會自動轉換爲無符號數。

 

  字面值常量

    字面值常量的形式和值決定了它的數據類型。例如: 20 /*十進制整型字面值*/  024 /*八進制*/  0x14 /*十六進制*/

    十進制字面值的類型是int、long和long long 中尺寸最小的那個,前提是這種類型能容納下當前的值。

    short類型沒有對應的字面值。

    若是咱們使用了一個形如 -20 的負十進制字面值,那個負號並不在字面值以內,它的做用僅僅是對字面值取負值而已。

    'a'  // char型字面值(字符字面值)

    "hello c++"  // 字符串字面值

    字符串字面值的類型其實是由常量字符構成的數組,編譯器在每一個字符串的結尾處添加一個空字符('\0'),所以,字符串字面值的實際長度要比它的內容多1。

 

  轉義序列:以反斜線開始,好比 \n(換行符)  \r(回車符)

  在程序中,轉義序列被看成一個字符使用。

 

  變量

    變量提供一個具名的、可供程序操做的存儲空間。C++中的每一個變量都有其數據類型,數據類型決定着變量所佔內存空間的大小和佈局方式、該空間能存儲的值的範圍,以及變量能參與的運算。

    初始化(initialized):對象被建立時得到一個特定的值。

    列表初始化:

      int value = {0};

      int value2{0};

      當用於內置類型的變量時,列表初始化形式有一個重要特色:若是初始值存在丟失信息的風險,編譯器將報錯:long double ld = 3.14;  int a{ld}, b = {ld};  // 錯誤,存在丟失信息的風險

  

  聲明和定義:聲明(declaration)使得名字可被程序知道,一個文件若是想使用別處定義的名字則必須包含對該名字的聲明。定義(definition)負責建立與名字關聯的實體。

    extern int i;  // 聲明i

    int j;  // 聲明並定義j

    extern double pi = 3.1416;  // 定義,包含了顯示初始化

 

  做用域(scope):同一個名字在不一樣的做用域中可能指向不一樣的實體。

  做用域能彼此包含,被包含(或者說被嵌套)的做用域稱爲內層做用域(inner scope),包含着別的做用域的做用域稱爲外層做用域(outer scope)。

  

  複合類型(compound type)是指基於其餘類型定義的類型。好比:引用和指針。

  一條聲明語句由一個基本數據類型(base type)和緊隨其後的一個聲明符(declarator)列表組成。每一個聲明符命名了一個變量並指定該變量爲與基本數據類型有關的某種類型。

  引用(reference)爲對象起了另外一個名字:

    int ival = 1024;

    int &ref_val = ival;

    int &ref_val2;  // 錯誤,引用必須被初始化

    int &ref_val3 = 10;  // 錯誤,初始值必須是一個對象

    double dval = 3.14;

    int &ref_val4 = dval; // 錯誤,初始值類型必須匹配

  指針(pointer)是「指向(point to)」另一種類型的複合類型。指針自己是一個對象,而引用自己不是一個對象,它只是一個別名。

  空指針(null pointer)不指向任何對象,能夠用字面值nullptr來指定。nullptr是一種特殊類型的字面值,它能夠被轉換成任意其餘的指針類型。

  void*是一種特殊的指針類型,可用於存聽任意對象的地址。可是,不能直接操做void*指針所值的對象,由於並不知道這個對象究竟是什麼類型,也就沒法肯定能在這個對象上作哪些操做。

// 指向指針的指針:
int
ival = 1024; int *pi = &ival; int **ppi = &pi;


// 指向指針的引用:
int *&rpi = pi;
rpi = &ival;
*rpi = 0;

 

  const限定符

    const常量的特徵僅僅在執行改變該常量的操做時纔會發揮做用。

    當以編譯時初始化的方式定義一個const對象時,如:

      const int buff_size = 512;

    編譯器將在編譯過程當中把用到該變量的地方都替換成對應的值。爲了執行替換操做,編譯器必須知道變量的初始值。若是程序包含多個文件,則每一個用了const對象的文件都必須得能訪問到它的初始值才行。要作到這一點,就必須在每一個用到變量的文件中都有對它的定義。爲了支持這一用法,同時避免對同一變量的重複定義,默認狀況下,const對象被設定爲僅在文件內有效。當多個文件中出現了同名的const變量時,其實等同於在不一樣文件中分別定義了獨立的變量。

  若是要在文件間共享,只在一個文件中定義const,在其餘對各文件中聲明並使用它。解決方法是,對於const變量不論是聲明仍是定義都添加extern關鍵字:

    // file1.cpp定義並初始化

    extern const int buff_size = func();

    // file1.h頭文件

    extern const int buff_size;  // extern的做用是指明buff_size並不是本文件所獨有

 

  對常量的引用:

    const int ci = 1024;

    const int &r1 = ci;

    r1 = 5;  // 錯誤,r1是對常量的引用

    int &r2 = ci;  // 錯誤,試圖讓一個很是量引用指向一個常量對象

  初始化常量引用時,容許用任意表達式做爲初始值,只要該表達式的結果能轉換成引用的類型便可:

    int i = 5;

    const int &r1 = i;

    const int &r2 = 1024;

    const int &r3 = r1 * 2;

    int &r4 = r1 * 2;  // 錯誤,r4爲很是量引用,初始值必須類型匹配

 

  必定要注意區分指向常量的指針(pointer to const)和常量指針(const pointer)。

  前者表示指針所指向的對象是常量,後者表示指針自己是常量。

    const double pi = 3.14;

    double *ptr = &pi;  // 錯誤

    const double *cptr = &pi;  // 正確,cptr就是指向常量的指針

    int *const pi = &i;  // 常量指針,必須初始化

  這裏引出:頂層const(top-level const)表示指針自己是個常量,底層const(low-level const)表示指針所指的對象是一個常量。

    const int &r = ci;  // 用於聲明引用的const都是底層const

 

  常量表達式(const expression)是指值不會改變而且在編譯過程就能獲得計算結果的表達式。

  一個對象(或表達式)是否是常量表達式由它的數據類型和初始值共同決定,例如:

    const int max_files = 30;   // max_files是常量表達式

    int staff_size = 27;  // 不是常量表達式

    const int sz = GetSize();  // sz不是常量表達式,由於它的值直到運行時才能獲取到 

  將變量聲明爲constexpr類型,以便編譯器來驗證變量的值是不是一個常量表達式:

    constexpr int mf = 30;

    constexpr int sz = size();  // 只有當size是一個constexpr函數時纔是一條正確的聲明語句

    const int *p = nullptr;  // p是一個指向整型常量的指針

    constexpr int *q = nullptr; // q是一個指向整型的常量指針

 

  類型別名(type alias)是一個名字,它是某種類型的同義詞:

    typedef double wages;  // wages是double的同義詞

    using SI = SalesItem;  // SI是SalesItem的同義詞(C++11)

  

  從C++11開始,引入了auto類型說明符,用它就能讓編譯器替咱們去分析表達式所屬的類型。

    auto i = 0, *p = &i;  // 正確,數據類型一致

    auto sz = 0, pi = 3.14;  // 錯誤,sz和pi的類型不一致

  編譯器推斷出來的auto類型有時候和初始值的類型並不徹底同樣,編譯器會適當地改變結果類型使其更符合初始化規則。

    int i = 0, &r = i;

    auto a = r;  // a是一個整數

    const int ci = i, &cr = ci;

    auto b = ci;  // b是一個整數

    auto c = cr;  // c是一個整數(cr是ci的別名,ci自己是一個頂層const)

  若是但願推斷出的auto類型是一個頂層const,須要明確指出:

    const auto d = ci;  // d是const int

 

  decltype類型說明符,它的做用是選擇並返回操做數的數據類型。在此過程當中,編譯器分析表達式並獲得它的類型,卻不實際計算表達式的值:

    decltype(f()) sum = x;  // sum的類型就是函數f的返回類型

    const int ci = 0, &cj = ci;

    decltype(ci) x = 0;  // x的類型是const int

    decltype(cj) y = x;  // y的類型是const int&, y綁定到變量x

  若是表達式的內容是解引用操做,則decltype將獲得引用類型(由於解引用指針能夠獲得指針所指的對象,並且還能給這個對象賦值):

    int i = 42, *p = &i;

    decltype(*p) c;  // 錯誤,c的類型爲int&,必須初始化

  注意:對於decltype所用的表達式來講,若是變量名加上了一對括號,則獲得的類型與不加括號時會有不一樣。若是decltype使用的是一個不加括號的變量,則獲得的結果就是該變量的類型;若是加上了括號,編譯器就會把它當成一個表達式,獲得引用類型:

    decltype(i) d;

    decltype((i)) e;  // 錯誤,e的類型是int&,必須初始化

 

  在《Effective C++》中,有這樣兩條建議:儘可能以const,enum,inline替換#define(條款02)、儘量使用const(條款03)。

  對於第一條:

例子:#define ASPECT_RATIO 1.653  記號名稱ASPECT_RATIO也許從未被編譯器看見:在預處理階段就被替換掉了。
若是運用此常量但得到一個編譯錯誤,那可能會帶來困惑,由於錯誤信息也許會提到1.653而不是ASPECT_RATIO
解決辦法就是用常量替換宏:
    const double kAspectRatio = 1.653;

定義常量指針(指針自己是常量),常量一般定義在頭文件內(以便被不一樣的源碼含入),所以有必要將指針聲明爲const。
  const char* const kAuthorName = "Scott Meyers";
這裏,string對象更合適:
  const std::string kAuthorName("Scott Meyers");

class專屬常量
爲了將常量的做用域(scope)限制於class內,必須讓它成爲class的一個成員(member)。而爲確保此常量至多隻有一份實體,必須讓它成爲一個static成員:
class GamePlayer {
private:
  static const int kNumTurns = 5;  // 常量聲明式
  int m_scores[kNumTurns];
};
上面的kNumTurns是聲明式而非定義式。必須另外提供定義式:
  const int GamePlayer::kNumTurns;    // kNumTurns的定義,應該放在實現文件中,聲明時已得到初值,所以定義時不能夠再設初值

若是不想讓別人得到一個pointer或reference指向某個整數常量,能夠用enum替換const。
class GamePlayer {
private:
  enum {EM_NUM_TURNS = 5};
  int m_scores[EM_NUM_TURNS];
};

#define誤用狀況:實現宏,宏看起來像函數,但不會招致函數調用帶來的額外開銷
  // 以a和b的較大值調用函數f
  #define CALL_WITH_MAX(a,b) f((a) > (b) ? (a) : (b))
帶來的問題:
  int a = 5, b = 0;
  CALL_WITH_MAX(++a, b);  // a被累加兩次
  CALL_WITH_MAX(++a, b + 10); // a被累加一次
取而代之的應該寫出template inline函數
  template<typename T>
  inline void CallWithMax(const T &a, const T &b) // 因爲不知道T是什麼,因此採用pass by reference-to-const
  {
    f(a > b ? a : b);
  }

  

  對於第二條:

STL迭代器系以指針爲根據塑造出來,因此迭代器的做用就像個T*指針。聲明迭代器爲const就像聲明指針爲const同樣(即聲明一個T* const指針)。若是但願迭代器所指的東西不可被改動(即但願STL模擬一個const T*指針),則使用const_iterator:
  std::vector<int> vec;
  const std::vector<int>::iterator iter = vec.begin();  // iter的做用像個T* const
  std::vector<int>::const_iterator kIter = vec.begin(); // kIter的做用像個const T*

類中不恰當的聲明const成員函數的例子:
class CTextBlock {
public:
  char& operator[](std::size_t position) const // bitwise const聲明,但其實不適當
  {
    return m_ptext[position];
  }
private:
  char *m_ptext;
};
重載的operator[]函數,被聲明爲const成員函數,可是卻返回一個reference指向對象內部值。
operator[]實現代碼並不更改m_ptext,因而編譯器很開心地爲operator[]產出目標碼。可是:
  const CTextBlock kctb("Hello");  // 聲明一個常量對象
  char *pc = &kctb[0];
  *pc = 'J';  // kctb如今的內容爲"Jello"

mutable(可變的)釋放掉non-static成員變量的bitwise constness約束

在const和non-const成員函數中避免重複
假設TextBlock內的operator[]不單只是返回一個reference指向某個字符,也執行邊界檢驗(bounds checking)等:
class TextBlock {
public:
  const char& operator[](std::size_t position) const
  {
    ... // 邊界檢驗
    ... // 日誌記錄數據訪問(log access data)
    ... // 檢驗數據完整性(verify data integrity)
    return m_text[position];
  }
  char& operator[](std::size_t position)
  {
    ... // 邊界檢驗
    ... // 日誌記錄數據訪問(log access data)
    ... // 檢驗數據完整性(verify data integrity)
    return m_text[position];
  }
private:
  std::string m_text;
};
上面的代碼中包含不少重複代碼,以及伴隨的編譯時間、維護、代碼膨脹等問題。就算將邊界檢驗...等代碼移到一個函數內,也會存在重複代碼:函數調用、兩次return語句等等。
真正應該作的是實現operator[]的機能一次並使用它兩次,也就是說,必須令其中一個調用另外一個。
將常量性轉除,這裏將返回值的const轉除是安全的:
class TextBlock {
public:
  const char& operator[](std::size_t position) const
  {
    ...
    ...
    ...
    return m_text[position];
  }
  char& operator[](std::size_t position)
  {
    return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
  }
};
若是不執行static_cast轉換,則會遞歸調用本身。
令const版本調用non-const版本以免重複————不該該這樣作。記住,const成員函數承諾毫不改變其對象的邏輯狀態(logical state),non-const成員函數卻沒有這般承諾。
相關文章
相關標籤/搜索