程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> C++ 關鍵字淺談,關鍵字淺談

C++ 關鍵字淺談,關鍵字淺談

編輯:C++入門知識

C++ 關鍵字淺談,關鍵字淺談


  這裡有一個游戲:要求寫一個符合C++標准的程序,包含至少十個連續而且不同的關鍵字。連續是指不能被標識符、運算符、標點符號分割。注意這裡的“不同”要求,別想用

int main() { return sizeof sizeof sizeof sizeof sizeof sizeof sizeof sizeof (int); }

這個交卷,而且這個可以任意長。動動腦經,應該是可以想出來的。我們從很久很久以前(long long ago)開始吧,

unsigned long long int ago;
const volatile unsigned long long int ago;
extern const volatile unsigned long long int ago;
extern inline const volatile unsigned long long int f();
template extern inline const volatile unsigned long long int operator *(const Type&);
template extern inline const volatile unsigned long long int operator and(const Type& lhs, const Type& rhs); // 如果允許運算符&變成關鍵字and

我們強調了不同的關鍵字,所以只好用 long int 而不是 long long int。
typeid 也可以級聯,與 sizeof 不同的是,必須添加括號。如果參數是表達式而非數據類型,sizeof 可以不用加括號。new 則沒有這個限制。

#include <typeinfo>

int main()
{
    const std::type_info& info = typeid(typeid(typeid(typeid(int))));
    return 0;
}

C++規范裡還要求使用 typeid 關鍵字的時候必須 #include <typeinfo>,否則任何使用到的地方都是 ill-formed。new 有時候也需要 #include <new> 頭文件。

sizeof new const volatile signed long int();

限定不同的關鍵字,目前最好的答案是:

int main() { if(false); else do throw sizeof new const volatile signed long int(); while(false); return 0; } spoiler alert

 

  回到正題,C++ 誕生於1983年,現在有很多的關鍵字了,這裡有詳細的列表。


  C++ 的一些運算符和標點符號需要 ISO 646 的代碼集之外的字符:{, }, [, ], #, \, ^, |, ~。要能夠使用不存在這些字符編碼的字符集(如德國的 DIN 66003),C++定義了兩種替代方案:額外的關鍵字對應這些操作符,使用 ISO 646 兼容的字符構成的特殊的兩元組或三元組解釋成一個非 ISO 646 字符。

首選 && &= & | ~ ! != || |= ^ ^= 替代方案 and and_eq bit_and bitor compl not not_eq or or_eq xor xor_eq

使用兩元組和三元組可能會遇到一些奇怪的問題,而且也影響代碼閱讀。C++標准打算在C++17版本廢除掉三元組符號 ??< ??> ??( ??) ??= ??/ ??' ??! ??-。兩元組 <: 會被替換成 [ 符號,於是 std::vector<::std::string> 會被錯誤的當成 std::vector[:std::string> 對待。現在的鍵盤都有這些符號,所以不用用他們的替代符號。畢竟符號比字符串易懂,想想數學運算中的加減乘除符號如果用 add/subtract/multiply/devide 英文替換表示,讀起來就不舒服,這也是為什麼C++引入操作符重載,矩陣的運算可以直接寫 (A+B)/(C*D); 而不是 divide(add(A, B), multiply(C, D));

 

  基本數據類型 void bool char wchar_t short int long unsigned float double,布爾值 true false,以及 C++11 新增的 char16_t char32_t nullptr 關鍵字。
char、signed char、unsigned char 是不同的類型,這個需要注意一下。GCC 編譯的時候,可以用編譯選項 -fsigned-char 或 -funsigned-char,它們分別將 char 指定為 signed char 或 unsigned char。很多程序直接寫char,希望它是有符號的或者無符號的。程序員喜歡簡短,跟 int 關鍵字一樣,不寫就默認有符號的類型;但有時候處理二進制流,表示成[0, 256)區間的整數。
  看到這個表格,原來整形有這麼多。其實我更喜歡用簡短的 uint8_t/int8_t/.../int32_t 等。你會看到很多代碼工程都自己定義了一套整形數據,避免 16位整形的程序移植到32位系統、32位整形的程序移植到64位系統 出現問題。而且C++ 對關鍵字的修飾順序也沒有要求,這讓一些程序員甚是糾結。

GNU 的 STL 庫也有這種順序的 long unsigned int
#ifndef __SIZE_TYPE__
#define __SIZE_TYPE__ long unsigned int
#endif

  true/false 的類型是 bool,那 nullptr 的類型是什麼?

typedef decltype(nullptr) nullptr_t;

這種類型定義妙不可言,也橫空推出新的關鍵字 decltype。如果兩個重載的函數 fun 可以接受不同的指針類型,比如 void fun(int*); void fun(float*); 那麼編譯 fun(NULL) 會有二義性錯誤。不妨再定義一個 std::nullptr_t 空指針類型的函數 void fun(std::nullptr_t nullp),這次 fun(NULL) 編譯失敗但是 fun(nullptr) 可以通過。

  wchar_t 用來表示一個 Unicode 字符集中的編碼,在 Windows 上是 UTF-16,類 Unix 系統上是 UTF-32。sizeof(wchar_t) 是 implementation defined,移植性差。一般在Windows上是2個字節,在類 Unix 系統上是4個字節。Windows 接受了寬字符並使之成為標准,可以看到許多Windows API 有兩個版本,functionNameA 和 functionNameW 分別對應ANSI版本和寬字符版本。這篇博客列舉了使用wchar_t可能犯的錯誤。

  C規范中並沒有寫明寬字符 wchar_t 的具體類型,與編譯器實現相關,可能8/16/32位,也可能是signed 或者 unsigned。關鍵在於選用的編碼字符集,注意字符集(Charset)與字符編碼(Character Encoding)的區別,ASCII/GBK/BIG5/GB18030 是字符集,Unicode 是字符集。當時各個國家都做了自己的編碼方案,中國有 GBK/GB18030/BIG5 等,這在國內沒有問題,但是網絡遍及世界各地,外面的人訪問就出現亂碼。於是 Unicode 應運而生。Unicode 字符集有 UTF-7/UTF-8/UTF-16/UTF-32 這幾種編碼。
推薦使用 Unicode 字符,不推薦使用 wchar_t,取而代之使用固定長度的 char16_t/char32_t 類型。

ISO/IEC 10646:2003 Unicode 標准 4.0 裡講到:
"The width of wchar_t is compiler-specific and can be as small as 8 bits. Consequently, programs that need to be portable across any C or C++ compiler should not use wchar_t for storing Unicode text. The wchar_t type is intended for storing compiler-defined wide characters, which may be Unicode characters in some compilers."

 

 

  C 語言關鍵字 int 是用的最多的,我經常覺得 short/long 的修飾讓 int 顯得多余。short int 與 short 語義一樣,long int 與 int 語義一樣。long 與 short 是相對來講的,於是又出現了 long long int 這一類型。long long 是1995年提議加沒加成,C++ 因為 C 沒有加所以也不肯加入。很多編譯器都自己實現了,10年之後,大家覺得有必要標准化了。(說不定將來某一天,整數的計算范圍需要擴充到 128 位,於是要喚出 long long long long int?好囧)個人覺得,程序語言誕生的時候,就應該盡量讓關鍵字語義正交,雖然標准規定了 sizeof(short) <= sizeof(int) <= sizeof(long),但是很多編譯器還是將 int 與 long int 等同看待。C 語言可移植好不由爭辯;Java 很聰明,直接規定了各自的長度。short/int/long 分別為 2/4/8 個字節。
  有一個頭文件 <stdint.h> 專門定了各種整形數 int8_t/int16_t/int32_t/int64_t 等,還有 unsigned 類型。其實 short/long/signed 都是形容詞,int 是名詞,所以聲明整形沒有以 int 結尾也是對的,與其寫 long long int,不如寫 long long,甚至 int64_t。我也想過如果 sizeof(int) == sizeof(long int),在 32 位機器上,要麼 long 多余,要麼 int 多余。然後我想到了 long double 類型,覺得還是 int 多余吧。

  選取關鍵字要盡量做到語法正交,盡量能表達所有的意思,小部分可以留給擴展。從 C++11 的改變可以看出,盡量不添加新的關鍵字,雙中括號擴展 [[]] 和右值符號 &&;不得已時添加新的關鍵字 alignas/alignof/thread_local 等。對差不多不用的關鍵字回收,auto 重新煥發光芒,template 能做的活,auto 現在也可以簡單完成。
  添加新的關鍵字的時候,需要考慮幾乎不會在歷史代碼中用到的。一些上下文相關的(contex-sensitive)關鍵字 final,override 的出現。它們不算關鍵字,但是放到函數末尾可以讓編譯器查錯。可想而知,如果像 final override 這樣常見的單詞選入關鍵字,會有多少歷史代碼需要修改。很顯然,final 和 override 是從 Java 語言中學過來的。Python 從 2.x 升到 3.x,各種不兼容,讓人對這個語言又愛又恨,雖然有幫助遷移代碼的文檔和工具。
  因為 final 與 override 是上下文相關的,所以你可以這麼寫也會編譯通過。

class Base
{
public:
    virtual int override(int )
    {
        std::cout<<"please override me"<<std::endl;
        return 0;
    }
};

class Fun: public Base
{
public:
    virtual int override(int ) override final
    {
        int override = 42;
        return override;
    }
};

int main()
{
    int me = 0x3e;
    Fun hey;
    hey.override(me);

    return 0;
}

 

  在 C++11 之前,C++ 標准交了很多東西給編譯器自行處理,比如是否自行產生構造函數,內存對齊,RVO 優化,enum 的類型等等,現在都可以顯式要求,避免各搞各的一套。enum 的類型是 implementation defined,很多編譯器都是用能容納的最小的整形范圍來表示。所以你會在微軟的很多代碼裡看到很多這樣的寫法:

typedef enum D3DTEXTUREADDRESS { 
    D3DTADDRESS_WRAP         = 1,
    D3DTADDRESS_MIRROR       = 2,
    D3DTADDRESS_CLAMP        = 3,
    D3DTADDRESS_BORDER       = 4,
    D3DTADDRESS_MIRRORONCE   = 5,
    D3DTADDRESS_FORCE_DWORD  = 0x7fffffff
} D3DTEXTUREADDRESS, *LPD3DTEXTUREADDRESS;

最後一個 enum 常量是  XXX_FORCE_DWORD  = 0x7fffffff,對其取值 sizeof(D3DTEXTUREADDRESS) 長度固定為 4。

  對齊本來是語言設計者想掩蓋的細節,不過在C++11編程方式越發復雜的情況下,提供給用戶更底層的手段往往是必不可少的。在一些情況下,用戶雖然不能保證總是寫出平台無關,或者說各平台習慣你能最優的代碼,但只需要改造 alignas 之後的對齊值參數就可以保證程序的移植性及性能良好,也不失為一種好的選擇。而 C++11 對對齊方式的支持從語法規則到庫,基本上考慮了各種情況,可以說是相當完備的。

template <typename T>
class alignas(sizeof(T)<<2) Color
{
    T r, g, b, a;
};

 

  讓關鍵字增加功能。以前 default 和 delete 只有一種用途:default 就是 switch 語句裡的默認分支,delete 就是釋放內存。現在都多了一種用法 = delete 表示刪除函數,= default 表示使用編譯期默認產生的。以前 using 只能與 namespace 為伴,而現在,typedef 所干的活,全部可以接手過來,而且在寫法上更直觀易懂。以前寫 typedef long long int int64; 現在你可以寫 using int64 = long long int; 怎麼樣?數據類型也可以用等號“賦值”了。
  C++11 出來後,你不用看編譯器的臉色行事了。這裡不需要定義構造函數,好讓編譯器自己生成;那裡需要定義構造函數,否則編譯器會做 shallow copying。現在你可以顯示指示編譯器怎麼做。

class noncopyable
{
private:
    noncopyable(const noncopyable& );            // not defined
    noncopyable& operator=(const noncopyable&);  // not defined
};

  之前,我們不想讓對象之間賦值,會講賦值構造函數和 operator = 限制為 private,並不提供定義。現在我們有更好的寫法:

class noncopyable
{
public:
    noncopyable(const noncopyable& ) = delete;
    noncopyable& operator=(const noncopyable&) = delete;
};

  聲明為 private 可以阻止被調用。故意不實現這些函數,意味著如果外部通過成員函數或者 friend 類想訪問它們,雖然編譯可以通過,但是鏈接的時候會因未定義的符號而報錯。在 C++11 裡,情況就不一樣了。使用 = delete 表示編譯期不會產生這個方法,所以這個可以扼殺在編譯期,不用推遲到鏈接的時候。雖然這裡的訪問權限對 C++11 不重要,但一般來說,標記 delete 的函數最好聲明 public,而不是 private。因為有的編譯器在檢查成員函數的調用時,只報訪問權限錯誤(錯誤如下)。很顯然,這裡 delete 才是重點,或者說錯誤的優先級高些。當然,並不是只有成員函數才能 delete,非成員函數和模版實例化的函數也是可以的。相比 C++98 而言,算是一項改進。

error: ‘pea::Log::Log(const pea::Log&)’ is private
Log(const Log& log) = delete;

 

  提醒一下,using namespace std; 不要寫在 .h 文件裡,要寫也是寫在 .cpp 文件裡。出於頭文件會被其他頭文件包含的可能,污染命名空間,但是 .cpp 文件不會。除非你故意想 #include "XXX.cpp" 這麼寫。在cocos2d-x 裡的 class HelloWorld 裡面添加方法 void onKeyReleased(EventKeyboard::KeyCode keyCode, Event* event); 會編譯不過,報錯如下:

1>classes\HelloWorldScene.h(17): error C2653: 'EventKeyboard' : is not a class or namespace name (..\Classes\AppDelegate.cpp)
1>classes\HelloWorldScene.h(17): error C2061: syntax error : identifier 'KeyCode' (..\Classes\AppDelegate.cpp)
1>classes\HelloWorldScene.h(17): error C2653: 'EventKeyboard' : is not a class or namespace name (..\Classes\HelloWorldScene.cpp)
1>classes\HelloWorldScene.h(17): error C2061: syntax error : identifier 'KeyCode' (..\Classes\HelloWorldScene.cpp)
1>classes\HelloWorldScene.cpp(72): error C2276: '&' : illegal operation on bound member function expression

其實修改很簡單,添加 EventKeyboard 和 KeyCode 所在的命名空間就好了。

void onKeyReleased(cocos2d::EventKeyboard::KeyCode keyCode, cocos2d::Event* event);

然而在 HelloWorldScene.cpp 裡不用添加,因為開頭有這麼一句 USING_NS_CC; 也就是 using namespace cocos2d; 引入了 cocos2d 所有的東西。

void HelloWorld::onKeyReleased(EventKeyboard::KeyCode keyCode, Event* event)
{
  //TODO
}

 

  virtual 關鍵字用來申明虛函數,在虛函數的末尾添加 =0 表示純虛函數。在函數名和參數可能動態變化的情況下,需要手動檢查基類與派生類的函數是否一致,是否派生類的成員函數覆蓋了基類的成員函數。這種枯燥的工作應該交給編譯器來做的,於是 C++11 引入了上下文相關的關鍵字 override,在需要覆蓋的地方沒有覆蓋,編譯器就會報錯。

  在 C++98,空指針用字面值 0 來表示, C++11 標准出來後,最好使用 nullptr,於是有人會問純虛函數的聲明 = 0 是不是應該更正為 = nullptr,嘗試了下是不行的。語法上規定必須是 0, 而不能是表達式(42 - 42),甚至 0L,0u 0x0 (clang 編譯器以前支持這樣的寫法)等。有些人選擇用宏 #define pure = 0 ,這個從聲明上似乎容易識別虛函數,這樣寫不是很好。C++ 之父 Bjarne Stroustrup 解釋了沒有添加 abstract/pure 關鍵字。(Java 裡有關鍵字 abstract)

  Bjarne Stroustrup 在他的書 The Design & Evolution of C++, section 13.2.3 中描述

The curious =0 syntax was chosen over the obvious alternative of introducing a new keyword pure or abstract because at the time I saw no chance of getting a new keyword accepted. Had I suggested pure, Release 2.0 would have shipped without abstract classes. Given a choice between a nicer syntax and abstract classes, I chose abstract classes. Rather than risking delay and incurring the certain fights over pure, I used the tradition C and C++ convention of using 0 to represent "not there." The =0 syntax fits with my view that a function body is the initializer for a function also with the (simplistic, but usually adequate) view of the set of virtual functions being implemented as a vector of function pointers. 

  = 0 似乎讓人迷惑,認為純虛函數就是未實現的函數,以為是將該函數指針置空(nullptr),而實際上,純虛函數也是可以實現的。不能在聲明時實現,而是必須在外部給出定義。C++11 引入了上下文相關的關鍵字概念,現在開來,引入 pure 作為上下文相關的關鍵字,將 = 0 替換成 pure 的寫法會更直觀。

class Base
{
public:
    virtual void fun() = 0;
};

void Base::fun()
{
    std::cout<<"pure virtual implementation\n";
}

class Derived: public Base
{
public:
    virtual void fun() { Base::fun(); }
};

  這裡引出另外兩個話題:析構函數可以是純虛函數,但必須給出定義,否則會出現鏈接錯誤,因為繼承的類在析構的時候會調用基類的析構函數,找不到符號。純虛函數要求非抽象子類給出實現,所以基類的構造函數最好給 protected 權限。




  C++11 增加的 operator "" 的重載,用戶自定義語義(user literal)限制在以下幾種類型,

( const char * )
( unsigned long long int )
( long double )
( char )
( wchar_t )
( char16_t )
( char32_t )
( const char * , std::size_t )
( const wchar_t * , std::size_t )
( const char16_t * , std::size_t )
( const char32_t * , std::size_t )

  發現沒有,比如浮點類型只有 long double,卻沒有 float 和 double 類型,為什麼呢?假設我們有一個角度制和弧度制轉換的函數語義。

constexpr long double operator"" _deg ( long double deg )
{
    return deg*M_PI/180;
}

我們要表達 M_PI 弧度時候,可以寫成 180_deg ,注意 180 和 _deg 是一個整體,之間不能有空格。(就像 C/C++ 語言內的後綴語義 long int degree = 180L; float pi = 3.14159265f 一樣)。如果要區分 float、double、long double 數據類型,我們的定義應該是這樣的:

float radian1 = 180f_deg;
double radian2 = 180_deg;
long double radian3 = 180L_deg;
很顯然,編譯器會把沒有空格的合法字符當成一個單詞(token)解析,於是 f_deg 變成了一個單詞,前面也說了 f 與 _deg 之間不能有空格,所以我們無法滿足不同浮點類型的重載,也包括整數類型。為了不截斷或窄收,於是采取最大表示范圍的類型,整形用 unsigned long long int,浮點類型用 long double。整形是 long long int 而不是 unsigned long long int 是因為有符號和無符號數相加減,會擴充到無符號類型。這裡似乎暗示了 unsigned long long int 是最大范圍的整形,所以應該不會出現 128 位的整形。

 

  盡量讓計算推前到編譯器而不是運行期,在 C++11 增加了新的關鍵字 static_cast、constexpr。boost 庫裡面有模擬 static_cast 的功能。
之前我們用宏定義 FOURCC

#define MAKE_FOURCC(ch0, ch1, ch2, ch3) \
    ((uint32_t)(uint8_t)(ch0) |         \
    ((uint32_t)(uint8_t)(ch1) << 8) |   \
    ((uint32_t)(uint8_t)(ch2) << 16) |  \
    ((uint32_t)(uint8_t)(ch3) << 24))   \

現在我們可以直接用 constexpr 函數來寫,而不是類型不安全的宏了。因為是編譯器計算出值,所以算出來的整形值可以直接用於 switch case 語句中而不會出錯。

constexpr uint32_t makeFourCC(uint8_t ch0, uint8_t ch1, uint8_t ch2, uint8_t ch3)
{
    return static_cast<uint32_t>(ch0 | (ch1<<8) | (ch2<<16) | (ch3<<24));
}

constexpr uint32_t RIFF = makeFourCC('R', 'I', 'F', 'F');
constexpr uint32_t WAVE = makeFourCC('W', 'A', 'V', 'E');
constexpr uint32_t FMT_ = makeFourCC('F', 'M', 'T', ' ');
constexpr uint32_t DATA = makeFourCC('D', 'A', 'T', 'A');

 


  C++ 十幾年的時間沒有動靜,造就了 boost 優秀庫。又從較自己晚出身的語言身上也學到了很多,比如 Java 類的成員在定義的時候給定默認值,委托構造函數(delegate constructor)、Python 的原生字符串常量,也算方便了程序員對 C++ 字符串的學習和使用。

 

未完待續。。

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