深入理解C++11 核心編程(一)

簡介

C++98/03的設(shè)計目標:
一、比C語言更適合系統(tǒng)編程(且與C語言兼容)。
二、支持數(shù)據(jù)抽象。
三、支持面向?qū)ο缶幊獭?br> 四、支持泛型編程。
(C++模板使得C++近乎成為了一種函數(shù)式編程語言,而且使得C++程序員擁有了模板元編程的能力。)

相比之下,C++11的整體設(shè)計目標如下:
一、使得C++成為更好的適用于系統(tǒng)開發(fā)及庫開發(fā)的語言。
二、使得C++成為更易于教學的語言(語法更加一致化和簡單化)。
三、保證語言的穩(wěn)定性,以及和C++03及C語言的兼容性。

(如果說C++11只是對C++語言做了大幅度的改進,那么就很有可能錯過了C++11精彩的地方。就是要做到,讀完這本書之后,只需要看一眼,就可以說出代碼是C++98/03的,還是C++11的)。

我想要的,就是C++11為程序員創(chuàng)造了很多更有效、更便捷的代碼編寫方式,程序員可以用簡短的代碼來完成C++98/03中同樣的功能,簡單到你會說"竟然這么簡單"。比起C++98/03,C++11大大縮短了代碼編寫量,最多可以將代碼縮短30%~80%。

C++11相對C++98/03有哪些顯著的增強呢?包括以下幾點:
一、通過內(nèi)存模型、線程、原子操作等來支持本地并行編程(Native Concurrency)。
二、通過統(tǒng)一初始化表達式、auto、declytype、移動語義等來統(tǒng)一對泛型編程的支持。
三、通過constexpr、POD(概念)等更好地支持系統(tǒng)編程。
四、通過內(nèi)聯(lián)命名空間、繼承構(gòu)造函數(shù)和右值引用等,以更好地支持庫的構(gòu)建。

(C++11像是個恐怖的“編程語言范型聯(lián)盟”。利用它不僅僅可以寫出面向?qū)ο蟮拇a,也可以寫出過程式編程語言代碼、泛型編程語言代碼、函數(shù)式編程語言代碼、元編程編程語言代碼,或者其他。多范型的支持使得C++11語言的“硬實力”幾乎在編程語言中“無出其右”)

程序員需要抽象出的不僅僅是對象,還有一些其他的概念,比如類型、類型的類型、算法、甚至是資源的生命周期,這些實際上都是C++語言可以描述的。在C++11中,這些抽象概念常常被實現(xiàn)在庫中,其使用將比在C++98/03中更加方便,更加好用。

(C++11是一種所謂的“輕量級抽象編程語言”)總的來說,靈活的靜態(tài)類型小的抽象概念絕佳的時間與空間運行性能,以及 與硬件緊密結(jié)合工作的能力 都是C++11突出的亮點。

WG21更專注的特性(我比較關(guān)注的)
1、更傾向于使用庫而不是擴展語言來實現(xiàn)特性。
2、更傾向于通用的而不是特殊的手段來實現(xiàn)特性。
3、增強代碼執(zhí)行性能和操作硬件的能力。
4、開發(fā)能夠改變?nèi)藗兯季S方式的特性
5、融入編程現(xiàn)實

Mayers根據(jù)C++11的使用者是類的使用者,還是庫的使用者,或者特性是廣泛使用的,還是庫的增強的來區(qū)分各個特性。具體地,可以把特性分為以下幾種:
A 類作者需要的(class writer, 簡稱為 “類作者”)
B 庫作者需要的(library writer, 簡稱為 “庫作者”)
C 所有人需要的(everyone, 簡稱為 “所有人”)
D 部分人需要的(everyone else, 簡稱為 “部分人”)

保證穩(wěn)定性和兼容性

保持與C99兼容

(這里有些庫不是特別懂,我們慢慢來)

#c++11 測試代碼
#include <iostream>
#include <thread>

using namespace std;

void my_thread()
{
    puts("hello, world");
}

int main(int argc, char *argv[])
{
    std::thread t(my_thread);
    t.join();

    system("pause");
    return 0;
}

thread是C++11的線程類,頭文件include <thread>。此程序能正常輸出,說明配置C++11成功。
c++11 對C99宏的一些支持:
STDC_HOSTED: 如果編譯器的目標系統(tǒng)環(huán)境中包含完整的標準C庫,那么這個宏就定義為1,否則宏的值為0
STDC: C編譯器通常用這個宏的值來表示編譯器的實現(xiàn)是否和C標準一致。C++11標準中這個宏是否定義以及定義成什么值由編譯器來決定。
(其他兩個值,好像在我的Dev-C++ 5.9.2中不太支持)

#include <iostream>
using namespace std;
int main(){
    cout<<"Standard Clib:"<<__STDC_HOSTED__<<endl;//Standard Clib:1
    cout<<"Standard C:"<<__STDC__<<endl;//Stand C:1
    // cout<<"C Stardard version:"<<__STDC_VERSION__<<endl;
    // cout<<"ISO/IEC"<<__STDC_ISO_10646__<<endl;//ISO/IEC 200009
}
//編譯選項:g++ -std=c++11 2-1-1.cpp 

預定義宏對于多目標平臺代碼的編寫通常具有重大意義。通過以上的宏,程序員通過使用#ifdef/#endif等預處理命令,就可使得平臺相關(guān)代碼只適合于當前平臺的代碼上編譯,從而在同一套代碼中完成對多平臺的支持。(通過這個預處理命名,就可以讓一些頭文件中的代碼在只適合當前平臺的情況下進行編譯。)
相關(guān)博客:
[C++中#if #ifdef的作用][1]
例如:

#include <iostream> 
using namespace std; 
int main(int argc,char** argv) 
{ 
    cout << "__FILE__ = " << __FILE__ << endl; 
    cout << "__DATE__ = " << __DATE__ << endl; 
    cout << "__TIME__ = " << __TIME__ << endl; 
    cout << "__LINE__ = " << __LINE__ << endl; 
    #ifdef __cplusplus
        cout<<"在此環(huán)境中可以編緝和調(diào)試標準C++程序。"<<endl; 
    #endif 
    #ifdef __STDC__
        cout<<"在此環(huán)境中可以編緝和調(diào)試標準C程序。"<<endl; 
    #endif 
    return EXIT_SUCCESS; 
}

值得注意的是,與所有的預定義宏相同的,如果用戶重定義(#define)或#undef了預定義的宏,那么后果是“未定義”的。因此在代碼編寫中,程序員應該注意避免自定義宏與預定義宏同名的情況。


(那么問題是:什么是預定義宏,這個預定義宏有什么用)
答案:
預定義宏,就是事先已經(jīng)定義好的宏。一般分為兩類:標準預定義宏,編譯器預定義宏。有兩個特性:

  • 無需提供他們的定義,就可以直接使用。
  • 預定義宏沒有參數(shù),且不可被重定義。

A:標準預定義宏(Standard Predefined Macros)
標準預定義宏由相關(guān)語言標準指定。因此所有該標準的編譯器都可以使用這些宏。ANSI C指定了以下預定義宏:

  • _FILE_ 在源文件中插入當前源文件名;
  • _LINE_ 在源代碼中插入當前源代碼行號;
  • _DATE_ 在源文件中插入當前的編譯日期;
  • _STDC_ 當要求程序嚴格遵循ANSI C標準時該標識被賦值為1,表明是標準的C程序;
  • _TIME_ 在源文件中插入當前編譯時間;
  • _TIMESTAMP_ The date and time of the last modification of the current source file, expressed as a string literal in the form Ddd Mmm Date hh:mm:ss yyyy, where Ddd is the abbreviated day of the week and Date is an integer from 1 to 31.
#include <iostream> 
using namespace std; 
int main(int argc,char** argv) 
{ 
    cout << "__FILE__ = " << __FILE__ << endl; 
    cout << "__DATE__ = " << __DATE__ << endl; 
    cout << "__TIME__ = " << __TIME__ << endl; 
    cout << "__LINE__ = " << __LINE__ << endl; 
    #if defined(__cplusplus) 
        cout<<"在此環(huán)境中可以編緝和調(diào)試標準C++程序。"<<endl; 
    #endif 
    #if defined(__STDC__) 
        cout<<"在此環(huán)境中可以編緝和調(diào)試標準C程序。"<<endl; 
    #endif 
    return EXIT_SUCCESS; 
}

![QQ截圖20151107172832.jpg-72.1kB][2]

(C99在_FILE、_LINE的之外,引入了_func_與之配合。注意func并不是宏)
具體參考如下官方文檔:
[http://gcc.gnu.org/onlinedocs/cpp/Standard-Predefined-Macros.html][3]
[http://gcc.gnu.org/onlinedocs/gcc/Function-Names.html#Function-Names][4]

B:編譯器預定義宏(GNU-, Microsoft-Specific Predefined Macros)
這部分的宏是由編譯器指定的,是對標準預定義宏的拓展。如GCC和VC++都有自己預定于的宏。
具體參考如下官方文檔:
[http://gcc.gnu.org/onlinedocs/cpp/Common-Predefined-Macros.html#Common-Predefined-Macros][5]
[http://msdn.microsoft.com/en-us/library/b0084kay][6]
ps: 編譯器預定義宏,也有部分實際上不是真正的宏。拿VC++來說,可以驗證的,_MSC_VER是宏,FUNCDNAME不是宏。
(預定義宏是很有用的,比如你要輸出日期,時間,文件名,函數(shù)名等等,都要用到它。預定義宏的使用比較簡單,網(wǎng)上可以找到很多介紹文章,特別是標準預定義宏。)

個人理解,就像我寫PHP一樣,會有些可以直接調(diào)用的方法,這些方法基本上是PHP已經(jīng)定義過的。


很多現(xiàn)實的編譯器都支持C99標準中的_func_預定義標識符功能,其基本功能就是返回所在函數(shù)的名字。

#include <string>
#include <iostream>
using namespace std;
const char *hello(){return __func__;}
const char *world(){return __func__;}
int main(){
    cout<<hello()<<","<<world()<<endl;//hello,world
}
//編譯選項:g++ -std=c++11 2-1-1.cpp 

事實上,按照標準定義,編譯器會隱式地在函數(shù)的定義之后定義_func_標識符。比如上述例子中的hello函數(shù),其實際的定義等同于如下代碼:

const char*hello(){
static const char*__func__="hello";//定義了一個靜態(tài)的常量。
return __func__;

_func_ 預定義標識符對于輕量級的調(diào)試代碼具有十分重要的作用。而在C++11中,標準甚至允許其使用在類或者結(jié)構(gòu)體中。(問題是:_func_怎樣去調(diào)試代碼)

#include <string>
#include <iostream>
using namespace std;
struct TestStruct{
    TestStruct(): name(__func__){// name屬性用__func__初始化,構(gòu)造函數(shù)體是空,推薦這種寫法。
           //name = __func__; 也可以寫在函數(shù)體里面。
    }
    const char*name;
};
//const char *hello(){return __func__;}
//const char *world(){return __func__;}
int main(){
    TestStruct ts;
    cout<<ts.name<<endl;//Teststruct
}
//編譯選項:g++ -std=c++11 2-1-1.cpp 

在結(jié)構(gòu)體的構(gòu)造函數(shù)中,初始化成員列表使用func預定義標識符是可行的,其效果跟在函數(shù)中使用一樣。不過將fun標識符作為函數(shù)參數(shù)的默認值是不允許的,如下例所示:

void FuncFail(string func_name=__func__){}; //無法通過編譯

(這是由于在參數(shù)聲明時,func還未被定義)


問題:在結(jié)構(gòu)體中是否可以聲明函數(shù)?
答案:
C++中結(jié)構(gòu)體可以定義一個函數(shù)
  C中的結(jié)構(gòu)體和C++中結(jié)構(gòu)體的不同之處:在C中的結(jié)構(gòu)體只能自定義數(shù)據(jù)類型,結(jié)構(gòu)體中不允許有函數(shù),而C++中的結(jié)構(gòu)體可以加入成員函數(shù)。
C++中的結(jié)構(gòu)體和類的異同:
  一、相同之處:結(jié)構(gòu)體中可以包含函數(shù);也可以定義public、private、protected數(shù)據(jù)成員;定義了結(jié)構(gòu)體之后,可以用結(jié)構(gòu)體名來創(chuàng)建對象。但C中的結(jié)構(gòu)體不允許有函數(shù);也就是說在C++當中,結(jié)構(gòu)體中可以有成員變量,可以有成員函數(shù),可以從別的類繼承,也可以被別的類繼承,可以有虛函數(shù)。
  二、不同之處:結(jié)構(gòu)體定義中默認情況下的成員是public,而類定義中的默認情況下的成員是private的。類中的非static成員函數(shù)有this指針,類的關(guān)鍵字class能作為template模板的關(guān)鍵字 即template<class T> class A{}; 而struct不可以。
  實際上,C中的結(jié)構(gòu)體只涉及到數(shù)據(jù)結(jié)構(gòu),而不涉及到算法,也就是說在C中數(shù)據(jù)結(jié)構(gòu)和算法是分離的,而到C++中一類或者一個結(jié)構(gòu)體可以包含函數(shù)(這個函數(shù)在C++我們通常中稱為成員函數(shù)),C++中的結(jié)構(gòu)體和類體現(xiàn)了數(shù)據(jù)結(jié)構(gòu)和算法的結(jié)合。

擴展:C++結(jié)構(gòu)體相關(guān)知識
C++結(jié)構(gòu)體提供了比C結(jié)構(gòu)體更多的功能,如默認構(gòu)造函數(shù),復制構(gòu)造函數(shù),運算符重載,這些功能使得結(jié)構(gòu)體對象能夠方便的傳值。

#include <iostream>
#include <vector>
using namespace std;
struct ST
{
    int a;
    int b;
    ST()  //默認構(gòu)造函數(shù)
    {
        a = 0;
        b = 0;
    }
    
    void set(ST* s1,ST* s2)//賦值函數(shù)
    {
        s1->a = s2->a;
        s1->b = s2->b;
    }
    ST& operator=(const ST& s)//重載運算符
    {
        set(this,(ST*)&s);
    }
    ST(const ST& s)//復制構(gòu)造函數(shù)
    {
        *this = s;  
    }
};
int main()
{
    ST a ;  //調(diào)用默認構(gòu)造函數(shù)
    vector<ST> v;
    v.push_back(a);  //調(diào)用復制構(gòu)造函數(shù)
    ST s = v.at(0);  //調(diào)用=函數(shù)
    cout << s.a <<"  " << s.b << endl;
    cin >> a.a;
    return 0;
}

_Pragma 操作符
在C/C++標準中,#pragma是一條預處理的指令。簡單地說,#pragma是用來向編譯器傳達語言標準以外的一些信息。

#pragma once 指示編譯器,該頭文件應該只被編譯一次。這與使用如下代碼來定義頭文件所達到的效果是一樣的。

#ifndef THIS_HEADER
#define THIS_HEADER
//一些頭文件的定義
#endif

在C++11中,標準定義了與預處理指令#pragma 功能相同的操作符_Pragma, 因為是操作符,所以,可以用于宏中. _Pragma操作符的格式如下所示:

_Pragma(字符串字面量)
//例如: _Pragma("once");

變長參數(shù)的宏定義以及VA_ARGS
變長參數(shù)的宏定義是指在宏定義中參數(shù)列表的最后一個參數(shù)為省略號,而預定義宏VA_ARGS則可以在宏定義的實現(xiàn)部分替換省略號所代表的字符串:

#include <stdio.h>
#define LOG(...){\
fprintf(stderr,"%s:Line%d:\t",__FILE__,__LINE__);\
fprintf(stderr,__VA_ARGS__);\
fprintf(stderr,"\n");\
}
int main(){
    int x=3;
    LOG("X=%d",x);//main.cpp:Line9: X=3 
}
//編譯選項:g++ -std=c++11 2-1-1.cpp 

關(guān)于 stdout、stderr
stderr -- 標準錯誤輸出設(shè)備
stdout -- 標準輸出設(shè)備 (printf("..")) 同 stdout。
如果輸入到文件中,stderr 是不顯示的。只有stdout和print才會顯示。上面代碼將stderr 改為 stdout 也是可以的,一樣能輸出。

程序員可以根據(jù)stderr產(chǎn)生的日志追溯到代碼中產(chǎn)生這些記錄的位置。引入這樣的特性,對于輕量級調(diào)試,簡單的錯誤輸出都是具有積極意義的。


在之前的C++標準中,將窄字符串(char)轉(zhuǎn)換成寬字符串(wchar_t)是未定義的行為。而在C++11標準中,在將窄字符串和寬字符串進行連接時,支持C++11標準的編譯器會將窄字符串轉(zhuǎn)換為寬字符串,然后再與寬字符串進行連接


long long 整型

C++11整型的最大改變就是多了 long long。分為兩種:long long 和unsigned long long。在C++11中,標準要求long long 整型可以在不同平臺上有不同的長度,但至少有64位。我們在寫常數(shù)字面量時,可以使用LL后綴(或是ll)標識一個long long 類型的字面量,而ULL (或ull、Ull、uLL) 表示一個unsigned long long 類型的字面量。比如:

long long int lli=-900000000000000LL; // 有符號的long long 變量lli
unsigned long long int ulli=-900000000000ULL; // 無符號的 unsigned long long 變量ulli。

對于有符號的,下面的類型是等價的:long long、signed long long、long long int、signed long long int; 而unsigned long long 和 unsigned long long int 也是等價的。要了解平臺上long long大小的方法就是查看<climits>(或<limits.h>中的宏)。與 long long 整型相關(guān)的一共有3個:LLONG_MIN、LLONG_MAX 和ULLONG_MAX, 它們分別代表了平臺上最小的long long 值、最大的long long 值,以及最大的unsigned long long 值。

#include <climits>
#include <cstdio>>
using namespace std; 
//#define LOG(...){\
//fprintf(stderr,"%s:Line%d:\t",__FILE__,__LINE__);\
//fprintf(stderr,__VA_ARGS__);\
//fprintf(stderr,"\n");\
//}
int main(){
    long long ll_min=LLONG_MIN;
    long long ll_max=LLONG_MAX;
    unsigned long long ull_max=ULLONG_MAX;
    printf("min of long long:%lld\n",ll_min);//min of long long:-9223372036854775808
    printf("max of long long:%lld\n",ll_max);//max of long long:9223372036854775807
    printf("max of unsigned long long:%llu\n",ull_max);//min of unsigned long long:18446744073709551615
//  LOG("X=%d",x);//main.cpp:Line9: X=3 
}
//編譯選項:g++ -std=c++11 2-1-1.cpp 

18446744073709551615 用16進制表示是0xFFFFFFFFFFFFFFFF(16個F),所以,在我們的實驗機上,long long 是一個64位的類型。

擴展的整型

有些整型的名字如:UINT、__int16、u64、int64_t, 等等。這些類型有的源自編譯器的自行擴展,有的則來自某些編程環(huán)境(比如工作在Linux內(nèi)核代碼中)。事實上,在C++11中一共只定義了以下5種標準的有符號整型:

  • signed char
  • short int
  • int
  • long int
  • long long int

標準同時規(guī)定,每一種有符號整型都有一種對應的無符號整數(shù)版本,且有符號整型與其對應的無符號整型具有相同的存儲空間大小。

在實際的編程中,由于這5種基本的整型適用性有限,所以有時編譯器出于需要,也會自行擴展一些整型。在C++11中,標準對這樣的擴展做出了一些規(guī)定。具體地講,除了標準整型之外,C++11標準允許編譯器擴展自有的所謂擴展整型。這些擴展整型的長度(占用內(nèi)存的位數(shù))可以比最長的標準整型(long long int, 通常是一個64位長度的數(shù)據(jù))還長,也可以介于兩個標準整數(shù)的位數(shù)之間。

C++11規(guī)定,擴展的整型必須和標準類型一樣,有符號類型和無符號類型占用同樣大小的內(nèi)存空間。而由于C/C++是一種弱類型語言,當運算、傳參等類型不匹配的時候,整型間會發(fā)生隱式的轉(zhuǎn)換,這種過程通常被稱為整型的提升,比如:

int(a) + (long long)b

通常就會導致變量(int)a被提升為long long類型后才與(long long)b 進行運算。而無論是擴展的整型還是標準的整型,其轉(zhuǎn)化的規(guī)則會由它們的"等級"決定。通常情況下:有如下原則:

  • 長度越大的整型等級越高,比如long long int的等級會高于int。
  • 長度相同的情況下,標準整型的等級高于擴展類型,比如long long int和_int64如果都是64位長度,則longlong int類型的等級更高。
  • 相同大小的有符號類型和無符號類型的等級相同,long long int 和unsigned long long int的等級就相同。

而在進行隱式的整型轉(zhuǎn)換的時候,一般是按照低等級整型轉(zhuǎn)換為高等級整型,有符號的轉(zhuǎn)換為無符號。這種規(guī)則跟C++98的整型轉(zhuǎn)換規(guī)則是一致的。

如果編譯器定義一些自有的整型,即使這樣自定義的整型由于名稱并沒有被標準收入,因而可移植性并不能得到保證,但至少編譯器開發(fā)者和程序員不用擔心自定義的擴展整型與標準整型間在使用規(guī)則上(尤其是整型提升)存在不同的認識了。

宏__cplusplus

在C和C++混合編寫的代碼中可以看到:

#ifdef__cplusplus
extern "C"{
#endif
//一些代碼
#ifdef__cplusplus
}
#endif

這種類型的頭文件可以被#include到C文件中進行編譯,也可以被#include到C++文件中進行編譯。由于extern "C"可以抑制C++對函數(shù)名、變量名等符號(symbol)進行名稱重整(name mangling), 因此編譯出的C目標文件和C++目標文件中的變量、函數(shù)名稱等符號都是相同的(否則不相同),鏈接器可以可靠地對兩種類型的目標文件進行連接。這樣該做法成為了C與C++混用頭文件的典型做法。

事實上,__cplusplus這個宏通常被定義為一個整型值。而隨著標準變化,__cplusplus宏一般會是一個比以往標準中更大的值。在C++11標準中,宏__cplusplus被預定義為201103L。比如程序員在想確定代碼是使用支持C++11編譯器進行編譯時,那么可以按下面的方法進行檢測:

#include <iostream> 
using namespace std; 
int main(int argc,char** argv) 
{ 
 #if __cplusplus<201103L
    #error"should use C++11 implementation"
 #endif 

}

預處理指令#error,使得不支持C++11的代碼編譯立即報錯并終止編譯。


extern "C"用法解析
extern "C"的主要作用就是為了能夠正確實現(xiàn)C++代碼調(diào)用其他C語言代碼。加上 extern "C"后,會指示編譯器這部分代碼按C語言的進行編譯,而不是C++的。由于C++支持函數(shù)重載,因此編譯器編譯函數(shù)的過程中會將函數(shù)的參數(shù)類型也加到編譯后的代碼中,而不僅僅是函數(shù)名;而C語言并不支持函數(shù)重載,因此編譯C語言代碼的函數(shù)時不會帶上函數(shù)的參數(shù)類型,一般之包括函數(shù)名。
標準頭文件

#ifndef __INCvxWorksh  /*防止該頭文件被重復引用*/
#define __INCvxWorksh

#ifdef __cplusplus    //__cplusplus是cpp中自定義的一個宏
extern "C" {          //告訴編譯器,這部分代碼按C語言的格式進行編譯,而不是C++的
#endif

    /**** some declaration or so *****/  

#ifdef __cplusplus
}
\#endif

\#endif /* __INCvxWorksh */

1、extern關(guān)鍵字
extern是C/C++語言中表明函數(shù)和全局變量作用范圍(可見性)的關(guān)鍵字,該關(guān)鍵字告訴編譯器,其聲明的函數(shù)和變量可以在本模塊或其它模塊中使用。
通常,在模塊的頭文件中對本模塊提供給其它模塊引用的函數(shù)和全局變量以關(guān)鍵字extern聲明。例如,如果模塊B欲引用該模塊A中定義的全局變量和函數(shù)時只需包含模塊A的頭文件即可。這樣,模塊B中調(diào)用模塊A中的函數(shù)時,在編譯階段,模塊B雖然找不到該函數(shù),但是并不會報錯;它會在鏈接階段中從模塊A編譯生成的目標代碼中找到此函數(shù)。
與extern對應的關(guān)鍵字是static,被它修飾的全局變量和函數(shù)只能在本模塊中使用。因此,一個函數(shù)或變量只可能被本模塊使用時,其不可能被extern “C”修飾。

2、被extern "C"修飾的變量和函數(shù)是按照C語言方式編譯和鏈接的
首先看看C++中對類似C的函數(shù)是怎樣編譯的。
作為一種面向?qū)ο蟮恼Z言,C++支持函數(shù)重載,而過程式語言C則不支持。函數(shù)被C++編譯后在符號庫中的名字與C語言的不同。例如,假設(shè)某個函數(shù)的原型為:
void foo( int x, int y );
該函數(shù)被C編譯器編譯后在符號庫中的名字為_foo,而C++編譯器則會產(chǎn)生像_foo_int_int之類的名字(不同的編譯器可能生成的名字不同,但是都采用了相同的機制,生成的新名字稱為“mangled name”)。
_foo_int_int這樣的名字包含了函數(shù)名、函數(shù)參數(shù)數(shù)量及類型信息,C++就是靠這種機制來實現(xiàn)函數(shù)重載的。 例如,在C++中,函數(shù)void foo( int x, int y )與void foo( int x, float y )編譯生成的符號是不相同的,后者為_foo_int_float。
同樣地,C++中的變量除支持局部變量外,還支持類成員變量和全局變量。用戶所編寫程序的類成員變量可能與全局變量同名,我們以"."來區(qū)分。而本質(zhì)上,編譯器在進行編譯時,與函數(shù)的處理相似,也為類中的變量取了一個獨一無二的名字,這個名字與用戶程序中同名的全局變量名字不同。

extern “C”這個聲明的真實目的是為了 實現(xiàn)C++與C及其它語言的混合編程。
應用場合:

  • C++代碼調(diào)用C語言代碼,在C++的頭文件中使用 (而在C語言的頭文件中,對其外部函數(shù)只能指定為extern類型,C語言中不支持extern "C"聲明,在.c文件中包含了extern "C"時會出現(xiàn)編譯語法錯誤。)
/* c語言頭文件:cExample.h */
\#ifndef C_EXAMPLE_H
\#define C_EXAMPLE_H
extern int add(int x,int y);     //注:寫成extern "C" int add(int , int ); 也可以
\#endif

/* c語言實現(xiàn)文件:cExample.c */
\#include "cExample.h"
int add( int x, int y )
{
 return x + y;
}

// c++實現(xiàn)文件,調(diào)用add:cppFile.cpp
extern "C"
{
 #include "cExample.h"        //注:此處不妥,如果這樣編譯通不過,換成 extern "C" int add(int , int ); 可以通過
}

int main(int argc, char* argv[])
{
 add(2,3);
 return 0;
}

如果C++調(diào)用一個C語言編寫的.DLL時,當包括.DLL的頭文件或聲明接口函數(shù)時,應加extern "C"{}。

  • 在C中引用C++語言中的函數(shù)和變量時,C++的頭文件需添加extern "C",但是在C語言中不能直接引用聲明了extern "C"的該頭文件,應該僅將C文件中將C++中定義的extern "C"函數(shù)聲明為extern類型
//C++頭文件 cppExample.h
#ifndef CPP_EXAMPLE_H
#define CPP_EXAMPLE_H
extern "C" int add( int x, int y );
#endif

//C++實現(xiàn)文件 cppExample.cpp
#include "cppExample.h"
int add( int x, int y )
{
 return x + y;
}

/* C實現(xiàn)文件 cFile.c
/* 這樣會編譯出錯:#include "cExample.h" */
extern int add( int x, int y );
int main( int argc, char* argv[] )
{
 add( 2, 3 );
 return 0;
}

靜態(tài)斷言

斷言:運行時與預處理時

斷言就是將一個返回值總是需要為真的判別式放在語句中,用于排除在設(shè)計的邏輯上不應該產(chǎn)生的情況。比如一個函數(shù)總需要輸入在一定的范圍內(nèi)的參數(shù),那么就可以對該參數(shù)使用斷言,以迫使在該參數(shù)發(fā)生異常的時候程序退出,從而避免程序陷入邏輯的混亂。

在C++中,標準在<cassert>或<assert.h>頭文件中為程序員提供了assert宏,用于在運行時進行斷言。
(宏:C++ 宏定義將一個標識符定義為一個字符串,源程序中的該標識符均以指定的字符串來代替。前面已經(jīng)說過,預處理命令不同于一般C++語句。因此預處理命令后通常不加分號。這并不是說所有的預處理命令后都不能有分號出現(xiàn)。由于宏定義只是用宏名對一個字符串進行簡單的替換,因此如果在宏定義命令后加了分號,將會連同分號一起進行置換。)

#include <cassert>
using namespace std;
//一個簡單的堆內(nèi)存數(shù)組分配函數(shù)(問題,如何區(qū)分是在堆還是在棧進行內(nèi)存分配)
char* ArrayAlloc(int n){
assert(n>0); //斷言,n必須大于0
return new char[n];
}
int main(){
char* a=ArrayAlloc(0);
}

接著,可以定義宏NDEBUG來禁用assert宏。這對發(fā)布程序來說還是必要的。assert宏在<cassert>中的實現(xiàn)方式類似于下列形式:

#ifdef NDEBUG
#define assert(expr)(static_cast<void>(0))
#else
...
#endif

一旦定義了NDEBUG宏,assert宏將被展開為一條無意義的C語句(通常會被編譯器優(yōu)化掉)。

通過預處理指令#if和#error的配合,也可以讓程序員在預處理階段進行斷言。比如GNU的cmathcalls.h頭文件中,我們會看到如下代碼:

#ifndef _COMPLEX_H
#error "Never use<bits/cmathcalls.h>direcctly;include<complex.h>instead."
#endif

如果程序員直接包含頭文件<bits/cmathcalls.h>并進行編譯,就會引發(fā)錯誤。#error指令會將后面的語句輸出,從而提醒用戶不要直接使用這個頭文件,而應該包含頭文件<complex.h>.這樣一來,通過預處理時的斷言,庫發(fā)布者就可以避免一些頭文件的引用問題。

(斷言assert宏只有在程序運行時才能起作用。而#error只在編譯器預處理時才能起作用。)有的時候,我們希望在編譯時能做一些斷言。

#include<cassert>
using namespace std;
//枚舉編譯器對各種特性的支持,每個枚舉值占一位
enum FeatureSupports{
    C99=0x0001,
    ExtInt=0x0002,
    SAssert=0x0004,
    NoExcept=0x0008,
    SMAX=0X0010,
}; 
//一個編譯器類型,包括名稱、特性支持等
struct Compiler{
    const char*name;
    int spp;//使用FeatureSupports枚舉 
}; 
int main(){
//檢查枚舉值是否完備
assert((SMAX-1)==(C99|ExtInt|SAssert|NoExcept));
Compiler a={"abc",(C99|SAssert)};
//...
if(a.spp&C99) {
//一些代碼... 
}
} 
//編譯選項:g++2-5-2.cpp

我們編寫了一個枚舉類型FeatureSupports,用于列舉編譯器對各種特性的支持。而結(jié)構(gòu)體Compiler則包含了一個int類型spp。由于各種特性都具有"支持"和"不支持"兩種狀態(tài),所以,為了節(jié)省空間,我們讓每個FeatureSupport的枚舉值占據(jù)一個特定的比特位置,并在使用時通過"或"運算壓縮地存儲在Compiler的spp成員中(即bitset的概念)。在使用時則可以通過檢查spp的某位來判斷編譯器對特性是否支持。

這樣的枚舉值非常多,而且還會在代碼維護中不斷增加。那么代碼編寫者必須想出辦法來對這些枚舉進行校驗,比如查驗是否有重位等。(在本例中程序員的做法是使用一個"最大枚舉" SMAX,并通過比較SMAX-1與其他枚舉的或運算值來驗證是否有枚舉值重位)。

assert是一個運行時的斷言,意味著不運行程序我們將無法得知是否有枚舉重位。在一些情況下,這是不可接受的,因為可能單次運行代碼并不會調(diào)用到assert相關(guān)的代碼路徑。因此這樣的校驗最好是在編譯時期就能完成。

#include<cassert>
#include<cstring>
using namespace std;

template<typename T,typename U>int bit_copy(T&a,U&b){
    assert(sizeof(b)==sizeof(a));
    memcpy(&a,&b,sizeof(b));
};
int main(){
    int a=0x2468;
    double b;
    bit_copy(a,b);
}

assert是要保證a和b兩種類型的長度一致,這樣bit_copy才能夠保證復制操作不會遇到越界等問題。我們還是使用assert的這樣的運行時斷言,但如果bit_copy不被調(diào)用,我們將無法觸發(fā)該斷言。實際上,正確產(chǎn)生斷言的時機應該是模板實例化時,即編譯時期。

他們的解決方法就是進行編譯時期的斷言,即所謂的"靜態(tài)斷言"。事實上,利用語言規(guī)則實現(xiàn)靜態(tài)斷言的討論非常多,比如典型的實現(xiàn)是開源庫Boost內(nèi)置的BOOST_STATIC_ASSERT斷言機制(利用sizeof操作符)。我們可以利用"除0"會導致編譯器報錯這個特性來實現(xiàn)靜態(tài)斷言。

#define assert_static(e)\
do{\
enum{assert_static__=1/(e);\
}while(0)
#include<cstring>
using namespace std;
#define assert_static(e)\
do{\
enum{assert_static__=1/(e)};\
}while(0)
template<typename T,typename U>int bit_copy(T&a,U&b){
    assert_static(sizeof(b)==sizeof(a));
    memcpy(&a,&b,sizeof(b));
};
int main(){
    int a=0x2468;
    double b;
    bit_copy(a,b);
}

無論哪種方式的靜態(tài)斷言,其缺陷都是很明顯的:診斷信息不夠充分,不熟悉該靜態(tài)斷言實現(xiàn)的時候,可能一時無法將錯誤對應到斷言錯誤上,從而難以準確定位錯誤的根源。

在C++11標準中,引入了static_assert斷言來解決這個問題。static_assert使用起來非常簡單,接收兩個參數(shù),一個是斷言表達式,這個表達式通常需要返回一個bool值;一個則是警告信息,通過也就是一段字符串。我們可以用static_assert進行替換。

template<typename t,typename u>int bit_copy(t&a,u&b){
    static_assert(sizeof(b)==sizeof(a),"the parameters of bit_copy must have same width.");
    memcpy(&a,&b,sizeof(b));
};

總代碼:

#include<cstring>
using namespace std;
template<typename t,typename u>int bit_copy(t&a,u&b){
    static_assert(sizeof(b)==sizeof(a),"the parameters of bit_copy must have same width.");
    memcpy(&a,&b,sizeof(b));
};
int main(){
    int a=0x2468;
    double b;
    bit_copy(a,b);
}

[Error] static assertion failed: the parameters of bit_copy must have same width.

這種錯誤非常清楚,也有利于程序員排錯。而由于static_assert是編譯時候時期的斷言,其使用范圍不像assert一樣受到限制。在通常情況下,static_assert可以用于任何名字空間。

static_assert(sizeof(int)==8,"This 64-bit machine should follow this!");
int main(){
   return 0;
  }
#include<cstring>
using namespace std;
#define assert_static(e)\
do{\
enum{assert_static__=1/(e)};\
}while(0)
//template<typename t,typename u>int bit_copy(t&a,u&b){
//  static_assert(sizeof(b)==sizeof(a),"the parameters of bit_copy must have same width.");
//  memcpy(&a,&b,sizeof(b));
//};
static_assert(sizeof(int)==8,"This 64-bit machine should follow this!");
int main(){
    return 0;
}

將static_assert寫在函數(shù)體外通常是較好的選擇,讓代碼閱讀者比較容易發(fā)現(xiàn)static_assert為斷言而非用戶定義的函數(shù)。反過來講,必須注意的是,static_assert的斷言表達式的結(jié)果必須是在編譯時期可以計算的表達式,即必須是常量表達式。

而如果有變量存在,且只需要運行時的檢查,那么還是應該使用assert宏。

noexcept修飾符與noexcept操作符

相比于斷言適應于排除邏輯上不可能存在的狀態(tài),異常通常是用于邏輯上可能發(fā)生的錯誤。在C++98中,我們看到了一整套完整的不同于C的異常處理系統(tǒng)。

void excpt_func() throw(int,double){...}

在excpt_func函數(shù)聲明之后,我們定義了一個動態(tài)異常聲明throw(int,douuble),該聲明指出了excpt_func可能拋出的異常的類型。但是該函數(shù)被棄用了。而表示函數(shù)不會拋出異常的動態(tài)異常聲明throw() 也被新的noexcept異常聲明所取代。

noexcept表示其修飾的函數(shù)不會拋出異常。不過與throw()動態(tài)異常不同的是,在C++11中如果noexcept修飾的函數(shù)拋出了異常,編譯器可以選擇直接調(diào)用std:: terminate() 函數(shù)來終止程序的運行,比基于異常機制的throw()在效率上高一些。

void excpt_func() noexcept;
void excpt_func() noexcept(常量表達式);

常量表達式的結(jié)果會被轉(zhuǎn)換成一個Bool類型的值。該值為true,表示函數(shù)不會拋出異常,反之,則有可能拋出異常。這里不帶常量表達式的noexcept相當于聲明了noexcept(true)
在通常情況下,在C++11中使用noexcept可以有效地阻止異常的傳播與擴散。

#include<iostream>
using namespace std;
void Throw(){throw 1;}
void NoBlockThrow(){Throw();}
void BlockThrow() noexcept{Throw();}
int main(){
    try{
        Throw();
    }
    catch(...){
        cout<<"Found throw."<<endl; //Found throw.
    }
    try{
        NoBlockThrow();
    }
    catch(...){
        cout<<"Throw is not blocked."<<endl;//Throw is not blocked.
    }
    try{
        BlockThrow();//terminate called after throwing an instance of 'int'  
    }
    catch(...){
        
        cout<<"Found throw 1."<<endl;
    }
}

結(jié)果:

![jietu2.jpg-69.7kB][7]

我們定義了Throw函數(shù),該函數(shù)的唯一作用是拋出一個異常。而NoBlockThrow是一個調(diào)用Throw的普通函數(shù),BlockThrow則是一個noexcept修飾的函數(shù)。從main的運行中我們可以看到,NoBlockThrow會讓Throw函數(shù)拋出的異常繼續(xù)拋出,直到mian中的catch語句將其捕捉。而BlockThrow則會直接調(diào)用std::terminate中斷程序的執(zhí)行,從而阻止了異常的繼續(xù)傳播。

而noexcept作為一個操作符時,通常可以用于模板。比如:

template<class T>
void fun() noexcept(noexcept(T())){}

這里,fun函數(shù)是否是一個noexcept的函數(shù),將由T() 表達式是否會拋出異常所決定。這里的第二個noexcept就是一個noexcept操作符。當其參數(shù)是一個有可能拋出異常的表達式的時候,其返回值為false,反之為true。這樣一來,我們就可以使模板函數(shù)根據(jù)條件實現(xiàn)noexcept修飾的版本或無noexcept修飾的版本。從泛型編程的角度看來,這樣的設(shè)計保證了關(guān)于"函數(shù)是否拋出異常" 這樣的問題可以通過表達式進行推導。
[1]: http://blog.sina.com.cn/s/blog_6e1827e10100x0dr.html
[2]: http://static.zybuluo.com/liuchenwei/09s187tdzetd5j3t64936ugj/QQ%E6%88%AA%E5%9B%BE20151107172832.jpg
[3]: http://gcc.gnu.org/onlinedocs/cpp/Standard-Predefined-Macros.html
[4]: http://gcc.gnu.org/onlinedocs/gcc/Function-Names.html#Function-Names
[5]: http://gcc.gnu.org/onlinedocs/cpp/Common-Predefined-Macros.html#Common-Predefined-Macros
[6]: http://msdn.microsoft.com/en-us/library/b0084kay
[7]: http://static.zybuluo.com/liuchenwei/rjns85bv6hrb88wcgmou4hqf/jietu2.jpg
[8]: http://static.zybuluo.com/liuchenwei/01ist2uvxei02ykyiu6rvcka/result.jpg
[9]: http://static.zybuluo.com/liuchenwei/8sv6n9qqb9hz6rq0c5j2x3rz/p.jpg
[10]: http://static.zybuluo.com/liuchenwei/xxrcsuz09sxtd6shm8nqebix/p.jpg

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容