天天看點

C++基礎(一)—— C++概述、C++對C的擴充(作用域、struct類型、引用、内聯函數、函數預設參數、函數占位參數、函數重載)1. C++概述2. C++初識3. C++對C的擴充

1. C++概述

1.1 c++簡介

“c++”中的++來自于c語言中的遞增運算符++,該運算符将變量加1。c++起初也叫”c withclsss”.通過名稱表明,c++是對C的擴充,是以c++是c語言的超集,這意味着任何有效的c程式都是有效的c++程式。c++程式可以使用已有的c程式庫。

為什麼c++不叫++c呢?

因為它雖然對c進行了改進、擴充,但是很多人還是把它當做c來使用。c++後置++,先對c進行擴充,但傳回的還是c.

庫是程式設計子產品的集合,可以在程式中調用它們。庫對很多常見的程式設計問題提供了可靠的解決方法,是以可以節省程式員大量的時間和工作量。

c++語言在c語言的基礎上添加了面向對象程式設計和泛型程式設計的支援。c++繼承了c語言高效,簡潔,快速和可移植的傳統。

c++融合了3種不同的程式設計方式:

  • c語言代表的過程性語言.
  • c++在c語言基礎上添加的類代表的面向對象語言.
  • c++模闆支援的泛型程式設計。

c語言和c++語言的關系:

c++語言是在C語言的基礎上,添加了面向對象、模闆等現代程式設計語言的特性而發展起來的。兩者無論是從文法規則上,還是從運算符的數量和使用上,都非常相似,是以我們常常将這兩門語言統稱為“C/C++”。

C語言和C++并不是對立的競争關系:

  • C++是C語言的加強,是一種更好的C語言
  • C++是以C語言為基礎的,并且完全相容C語言的特性。

C語言和C++語言的學習是可以互相促進。學好C語言,可以為我們将來進一步地學習C++語言打好基礎,而C++語言的學習,也會促進我們對于C語言的了解,進而更好地運用C語言。

1.2 c++起源

與c語言一樣,c++也是在貝爾實驗室誕生的,Bjarne Stroustrup(本賈尼·斯特勞斯特盧普)在20世紀80年代在這裡開發了這種語言。

Stroustrup關心的是讓c++更有用,而不是實施特定的程式設計原理或風格。在确定語言特性方面,真正的程式設計比純粹的原理更重要。Stroustrup之是以在c的基礎上建立c++,是因為c語言簡潔、适合系統程式設計、使用廣泛且與UNIX作業系統聯系緊密。

1.3 可移植性和标準

假設為運作windows 2000的老式奔騰pc編寫了一個很好用的c++程式,而管理者決定使用不同作業系統(比如說Mac OS 或 Linux)和處理器的計算機替換它。該程式是否可在新平台運作呢?當然,但是必須使用為新平台設計的c++編譯器重新編譯。但是是否需要修改寫好的代碼?如果不需要修改代碼的情況下,重新編譯程式後,程式依然運作良好,該程式是可移植的。

程式是否可移植性有兩個問題需要解決。第一是硬體,針對特定硬體程式設計的程式是不可移植的。第二,語言的實作,windows xp c++ 和 Redhat Linux 或 Mac OS X對c++的實作不一定相同。雖然我們希望c++版本與其他版本相容,但是如果沒有一個公開的标準,很難做到。是以,美國國家标準局(AmericanNational Standards Institute,ANSI)在1990年設立一個委員會專門負責制定c++标準(ANSI制定了c語言的标準)。國際标準化組織(International Organization forStandardization,ISO)很快通過自己的委員會加入到這個行列,建立了聯合組織ANSI/ISO,制定c++标準。

經過多年的努力,制定出了一個國際标準ISO/IEC14882:1998 ,并于1998年獲得了ISO、IEC(International ElectrotechnicalCommittee,國際電工技術委員會)和ANSI的準許。這個标準就是我們經常所說的c++98。它不僅描述了已有的c++特性,還對語言進行了擴充,添加了異常、運作階段類型識别(RTTI)、模闆和标準模闆庫(STL).

2003年,釋出了c++标準第二版(IOS/IEC 14882:2003),這一版本對第一版修訂了一些錯誤,但并沒有改變語言特性,是以c++98表示c++98/c++2003.

c++不斷發展。IOS标準委員會于2011年8月準許了新标準ISO/IEC 14882:2011,該标準被稱為c++11,與c++98一樣c++11也新增了許多特性。

ISO c++标準還吸收了ANSI c語言标準,c++盡量做到是c的超集。意味着在理想情況下,任何有效的c程式都應該是有效的c++程式。

ANSI不僅定義了c語言,還 定義了一個ANSI c必須實作的标準c庫。c++也在使用這個庫,另外ANSI/ISOc++标準還提供了一個c++标準類庫。、

1.4 為什麼C++會成功

c++最初的目的是将c語言轉變為OOP語言,但是c++後來并沒有這麼做,而是立足于程式的實際。因為在c語言方面大量投入的程式員使其完全丢掉c語言那種程式設計的思考方式,轉而去接受一種新的語言,新的思維,那麼将會導緻這些程式員中大部分人在短時間内可能毫無成果,使其生産率降低。但是如果讓這些c程式員在已有知識的基礎上,再去學習c++語言,了解運用OOP,那麼也隻是在其已有思維的基礎上進行擴充而已,這樣可以保持其更好的生産率。

簡而言之,強迫程式員放棄c語言和c語言的思考方式,而去轉到OOP上是需要代價的,但是從c語言轉到c++所花費的代價就會小很多。是以也可以了解為c++的出現并不是去替代c,而是對c的擴充,是以在c++中既可以使用c++新特性,并且可以使用c的過程式思維來編寫程式。

對于傳統的結構化語言,我們向來沒有太多的疑惑,函數調用那麼自然而明顯,隻是從程式的某一個地點調到另一個地點去執行。但是對于面向對象(OO)語言,我們疑惑就會很多。其原因就是c++編譯器為我們程式員做了太多隐藏的工作:構造函數,析構函數、虛函數、繼承、多态....有時候它為我們合成出一些額外的函數,有時候它又偷偷在我們寫的函數裡,放進更多的操作。有時候也會給我們的對象裡放進一些奇妙的東西,使得我們sizeof的時候結果和我們預期不一樣。

2. C++初識

2.1 簡單的c++程式

2.1.1 c++ hello world

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
//using namespace std;

int main(){
    cout << "hello world" << endl;
    system("pause");
    return EXIT_SUCCESS;
}
           

分析:

  • #include<iostream>;預編譯指令,引入頭檔案iostream.
  • using namespace std; 使用标準命名空間
  • cout << “helloworld”<< endl; 和printf功能一樣,輸出字元串”hello wrold”

問題1:c++頭檔案為什麼沒有.h?

在c語言中頭檔案使用擴充名.h,将其作為一種通過名稱辨別檔案類型的簡單方式。但是c++得用法改變了,c++頭檔案沒有擴充名。但是有些c語言的頭檔案被轉換為c++的頭檔案,這些檔案被重新命名,丢掉了擴充名.h(使之成為c++風格頭檔案),并在檔案名稱前面加上字首c(表明來自c語言)。例如c++版本的math.h為cmath.

由于C使用不同的擴充名來表示不同檔案類型,是以用一些特殊的擴充名(如hpp或hxx)表示c++的頭檔案也是可以的,ANSI/IOS标準委員會也認為是可以的,但是關鍵問題是用哪個比較好,最後一緻同意不适用任何擴充名。

頭檔案類型 約定 示例 說明
c++舊式風格 以.h結尾 iostream.h c++程式可用
c舊式風格 以.h結尾 math.h c/c++程式可用
c++新式風格 無擴充名 iostream c++程式可用,使用namespace std
轉換後的c 加上字首c,無擴充名 cmath c++程式可用,可使用非c特性,如namespace std

問題2:using namespace std 是什麼?

namespace是名額識符的各種可見範圍。命名空間用關鍵字namespace 來定義。命名空間是C++的一種機制,用來把單個辨別符下的大量有邏輯聯系的程式實體組合到一起。此辨別符作為此組群的名字。

問題3:cout 、endl 是什麼?

cout是c++中的标準輸出流,endl是輸出換行并重新整理緩沖區。

2.1.2 面向對象三大特性

  • 封裝

把客觀事物封裝成抽象的類,并且類可以把自己的資料和方法隻讓可信的類或者對象操作,對不可信的進行資訊隐藏。

類将成員變量和成員函數封裝在類的内部,根據需要設定通路權限,通過成員函數管理内部狀态。

  • 繼承

繼承所表達的是類之間相關的關系,這種關系使得對象可以繼承另外一類對象的特征和能力。

繼承的作用:避免公用代碼的重複開發,減少代碼和資料備援。

  • 多态

多态性可以簡單地概括為“一個接口,多種方法”,字面意思為多種形态。程式在運作時才決定調用的函數,它是面向對象程式設計領域的核心概念。

3. C++對C的擴充

3.1 ::作用域運算符

通常情況下,如果有兩個同名變量,一個是全局變量,另一個是局部變量,那麼局部變量在其作用域内具有較高的優先權,它将屏蔽全局變量。

//全局變量
int a = 10;
void test(){
    //局部變量
    int a = 20;
    //全局a被隐藏
    cout << "a:" << a << endl;
}
           

程式的輸出結果是a:20。在test函數的輸出語句中,使用的變量a是test函數内定義的局部變量,是以輸出的結果為局部變量a的值。

作用域運算符可以用來解決局部變量與全局變量的重名問題

//全局變量
int a = 10;
//1. 局部變量和全局變量同名
void test(){
    int a = 20;
    //列印局部變量a
    cout << "局部變量a:" << a << endl;
    //列印全局變量a
    cout << "全局變量a:" << ::a << endl;
}
           

這個例子可以看出,作用域運算符可以用來解決局部變量與全局變量的重名問題,即在局部變量的作用域内,可用::對被屏蔽的同名的全局變量進行通路。

3.2 名字控制

建立名字是程式設計過程中一項最基本的活動,當一個項目很大時,它會不可避免地包含大量名字。c++允許我們對名字的産生和名字的可見性進行控制。

我們之前在學習c語言可以通過static關鍵字來使得名字隻得在本編譯單元内可見,在c++中我們将通過一種通過命名空間來控制對名字的通路。

3.2.1 C++命名空間(namespace)

在c++中,名稱(name)可以是符号常量、變量、函數、結構、枚舉、類和對象等等。工程越大,名稱互相沖突性的可能性越大。另外使用多個廠商的類庫時,也可能導緻名稱沖突。為了避免,在大規模程式的設計中,以及在程式員使用各種各樣的C++庫時,這些辨別符的命名發生沖突,标準C++引入關鍵字namespace(命名空間/名字空間/名稱空間),可以更好地控制辨別符的作用域。

3.2.2命名空間使用文法

  • 建立一個命名空間:
namespace A{
    int a = 10;
}
namespace B{
    int a = 20;
}
void test(){
    cout << "A::a : " << A::a << endl;
    cout << "B::a : " << B::a << endl;
}
           
  • 命名空間隻能全局範圍内定義(以下錯誤寫法):
void test(){
    namespace A{
        int a = 10;
    }
    namespace B{
        int a = 20;
    }
    cout << "A::a : " << A::a << endl;
    cout << "B::a : " << B::a << endl;
}
           
  • 命名空間可嵌套命名空間:
namespace A{
    int a = 10;
    namespace B{
        int a = 20;
    }
}
void test(){
    cout << "A::a : " << A::a << endl;
    cout << "A::B::a : " << A::B::a << endl;
}
           
  • 命名空間是開放的,即可以随時把新的成員加入已有的命名空間中:
namespace A{
    int a = 10;
}

namespace A{
    void func(){
        cout << "hello namespace!" << endl;
    }
}

void test(){
    cout << "A::a : " << A::a << endl;
    A::func();
}
           
  • 聲明和實作可分離:

檔案:test.h

#pragma once

namespace MySpace{
    void func1();
    void func2(int param);
}
           

檔案:test.cpp

void MySpace::func1(){
    cout << "MySpace::func1" << endl;
}
void MySpace::func2(int param){
    cout << "MySpace::func2 : " << param << endl;
}
           
  • 無名命名空間,意味着命名空間中的辨別符隻能在本檔案内通路,相當于給這個辨別符加上了static,使得其可以作為内部連接配接:
namespace{
    
    int a = 10;
    void func(){ cout << "hello namespace" << endl; }
}
void test(){
    cout << "a : " << a << endl;
    func();
}
           
  • 命名空間别名:
namespace veryLongName{
    int a = 10;
    void func(){ cout << "hello namespace" << endl; }
}

void test(){
    namespace shortName = veryLongName;
    cout << "veryLongName::a : " << shortName::a << endl;
    veryLongName::func();
    shortName::func();
}
           

3.2.3 using聲明

using聲明可使得指定的辨別符可用。

namespace A{
    int paramA = 20;
    int paramB = 30;
    void funcA(){ cout << "hello funcA" << endl; }
    void funcB(){ cout << "hello funcA" << endl; }
}

void test(){
    //1. 通過命名空間域運算符
    cout << A::paramA << endl;
    A::funcA();
    //2. using聲明
    using A::paramA;
    using A::funcA;
    cout << paramA << endl;
    //cout << paramB << endl; //不可直接通路
    funcA();
    //3. 同名沖突
    //int paramA = 20; //相同作用域注意同名沖突
           

using聲明碰到函數重載

namespace A{
    void func(){}
    void func(int x){}
    int  func(int x,int y){}
}
void test(){
    using A::func;
    func();
    func(10);
    func(10, 20);
}
           

如果命名空間包含一組用相同名字重載的函數,using聲明就聲明了這個重載函數的所有集合。

3.2.4 using編譯指令

using編譯指令使整個命名空間辨別符可用。

namespace A{
    int paramA = 20;
    int paramB = 30;
    void funcA(){ cout << "hello funcA" << endl; }
    void funcB(){ cout << "hello funcB" << endl; }
}

void test01(){
    using namespace A;
    cout << paramA << endl;
    cout << paramB << endl;
    funcA();
    funcB();

    //不會産生二義性
    int paramA = 30;
    cout << paramA << endl;
}

namespace B{
    int paramA = 20;
    int paramB = 30;
    void funcA(){ cout << "hello funcA" << endl; }
    void funcB(){ cout << "hello funcB" << endl; }
}

void test02(){
    using namespace A;
    using namespace B;
    //二義性産生,不知道調用A還是B的paramA
    //cout << paramA << endl;
}
           

注意:使用using聲明或using編譯指令會增加命名沖突的可能性。也就是說,如果有名稱空間,并在代碼中使用作用域解析運算符,則不會出現二義性。

3.2.5 命名空間使用

需要記住的關鍵問題是當引入一個全局的using編譯指令時,就為該檔案打開了該命名空間,它不會影響任何其他的檔案,是以可以在每一個實作檔案中調整對命名空間的控制。比如,如果發現某一個實作檔案中有太多的using指令而産生的命名沖突,就要對該檔案做個簡單的改變,通過明确的限定或者using聲明來消除名字沖突,這樣不需要修改其他的實作檔案。

3.3 struct類型加強

  • C中定義結構體變量需要加上struct關鍵字,c++不需要。
  • C中的結構體隻能定義成員變量,不能定義成員函數。c++即可以定義成員變量,也可以定義成員函數。
//1. 結構體中即可以定義成員變量,也可以定義成員函數
struct Student{
    string mName;
    int mAge;
    void setName(string name){ mName = name; }
    void setAge(int age){ mAge = age; }
    void showStudent(){
        cout << "Name:" << mName << " Age:" << mAge << endl;
    }
};

//2. c++中定義結構體變量不需要加struct關鍵字
void test01(){
    Student student;
    student.setName("John");
    student.setAge(20);
    student.showStudent();
}
           

3.4 更嚴格的類型轉換

在C++,不同類型的變量一般是不能直接指派的,需要相應的強轉。

c語言代碼:

typedef enum COLOR{ GREEN, RED, YELLOW } color;

int main(){

    color mycolor = GREEN;
    mycolor = 10;
    printf("mycolor:%d\n", mycolor);
    char* p = malloc(10);
    return EXIT_SUCCESS;
}
           

以上c代碼c編譯器編譯可通過,c++編譯器無法編譯通過。C++代碼如下:

char* p = (char*)malloc(10);
           

3.5 三目運算符功能增強

左值和右值概念

在c++中可以放在指派操作符左邊的是左值,可以放到指派操作符右面的是右值。

有些變量即可以當左值,也可以當右值。

左值為Lvalue,L代表Location,表示記憶體可以尋址,可以指派。

右值為Rvalue,R代表Read,就是可以知道它的值。

比如:int temp = 10; temp在記憶體中有位址,10沒有,但是可以Read到它的值。

  • c語言三目運算表達式傳回值為資料值,為右值,不能指派。
    int a = 10;
    int b = 20;
    printf("ret:%d\n", a > b ? a : b);
    //思考一個問題,(a > b ? a : b) 三目運算表達式傳回的是什麼?
    
    //(a > b ? a : b) = 100;
    //傳回的是右值
           
  • c++語言三目運算表達式傳回值為變量本身(引用),為左值,可以指派。
int a = 10;
    int b = 20;
    printf("ret:%d\n", a > b ? a : b);
    //思考一個問題,(a > b ? a : b) 三目運算表達式傳回的是什麼?

    cout << "b:" << b << endl;
    //傳回的是左值,變量的引用
    (a > b ? a : b) = 100;//傳回的是左值,變量的引用
    cout << "b:" << b << endl;
           

3.6 C/C++中的const

3.6.1 const概述

const單詞字面意思為常數,不變的。它是c/c++中的一個關鍵字,是一個限定符,它用來限定一個變量不允許改變,它将一個對象轉換成一個常量。

const int a = 10;
A = 100; //編譯錯誤,const是一個常量,不可修改
           

3.6.2 C/C++中const的差別

3.6.2.1 C中的const

常量的引進是在c++早期版本中,當時标準C規範正在制定。那時,盡管C委員會決定在C中引入const,但是,他們c中的const了解為”一個不能改變的普通變量”,也就是認為const應該是一個隻讀變量,既然是變量那麼就會給const配置設定記憶體,并且在c中const是一個全局隻讀變量,c語言中const修飾的隻讀變量是外部連接配接的。

const int arrSize = 10;
int arr[arrSize];
           

看似是一件合理的編碼,但是這将得出一個錯誤。因為arrSize占用某塊記憶體,是以C編譯器不知道它在編譯時的值是多少?

3.6.2.2 C++中的const

在c++中,一個const不必建立記憶體空間,而在c中,一個const總是需要一塊記憶體空間。在c++中,是否為const常量配置設定記憶體空間依賴于如何使用。一般說來,如果一個const僅僅用來把一個名字用一個值代替(就像使用#define一樣),那麼該存儲局空間就不必建立。

如果存儲空間沒有配置設定記憶體的話,在進行完資料類型檢查後,為了代碼更加有效,值也許會折疊到代碼中。

不過,取一個const位址, 或者把它定義為extern,則會為該const建立記憶體空間。const前加volatile關鍵字 禁止優化,也會建立記憶體空間。

在c++中,出現在所有函數之外的const作用于整個檔案(也就是說它在該檔案外不可見),預設為内部連接配接,c++中其他的辨別符一般預設為外部連接配接。

3.6.2.3 C/C++中const異同總結

  • c語言全局const會被存儲到隻讀資料段。c++中全局const當聲明extern或者對變量取位址時,編譯器會配置設定存儲位址,變量存儲在隻讀資料段。兩個都受到了隻讀資料段的保護,不可修改。
const int constA = 10;
int main(){
    int* p = (int*)&constA;
    *p = 200;
}
           

以上代碼在c/c++中編譯通過,在運作期,修改constA的值時,發生寫入錯誤。原因是修改隻讀資料段的資料。

  • c語言中局部const存儲在堆棧區,隻是不能通過變量直接修改const隻讀變量的值,但是可以跳過編譯器的檢查,通過指針間接修改const值。
const int constA = 10;
    int* p = (int*)&constA;
    *p = 300;
    printf("constA:%d\n",constA);
    printf("*p:%d\n", *p);
           

運作結果:

C++基礎(一)—— C++概述、C++對C的擴充(作用域、struct類型、引用、内聯函數、函數預設參數、函數占位參數、函數重載)1. C++概述2. C++初識3. C++對C的擴充

c語言中,通過指針間接指派修改了constA的值。

c++中對于局部的const變量要差別對待:

  1. 對于基礎資料類型,也就是const int a = 10這種,編譯器會進行優化,将值替換到通路的位置。
const int constA = 10;
    int* p = (int*)&constA;
    *p = 300;
    cout << "constA:" << constA << endl;
    cout << "*p:" << *p << endl;
           

運作結果:

C++基礎(一)—— C++概述、C++對C的擴充(作用域、struct類型、引用、内聯函數、函數預設參數、函數占位參數、函數重載)1. C++概述2. C++初識3. C++對C的擴充
  1. 對于基礎資料類型,如果用一個變量初始化const變量,如果const int a = b,那麼也是會給a配置設定記憶體。
int b = 10;
    const int constA = b;
    int* p = (int*)&constA;
    *p = 300;
    cout << "constA:" << constA << endl;
    cout << "*p:" << *p << endl;
           

運作結果:

C++基礎(一)—— C++概述、C++對C的擴充(作用域、struct類型、引用、内聯函數、函數預設參數、函數占位參數、函數重載)1. C++概述2. C++初識3. C++對C的擴充

constA 配置設定了記憶體,是以我們可以修改constA記憶體中的值。

  1. 對于自定資料類型,比如類對象,那麼也會配置設定記憶體。
   const Person person; //未初始化age
    //person.age = 50; //不可修改
    Person* pPerson = (Person*)&person;
    //指針間接修改
    pPerson->age = 100;
    cout << "pPerson->age:" << pPerson->age << endl;
    pPerson->age = 200;
    cout << "pPerson->age:" << pPerson->age << endl;
           

運作結果:

C++基礎(一)—— C++概述、C++對C的擴充(作用域、struct類型、引用、内聯函數、函數預設參數、函數占位參數、函數重載)1. C++概述2. C++初識3. C++對C的擴充

為person配置設定了記憶體,是以我們可以通過指針的間接指派修改person對象。

  • c中const預設為外部連接配接,c++中const預設為内部連接配接.當c語言兩個檔案中都有constint a的時候,編譯器會報重定義的錯誤。而在c++中,則不會,因為c++中的const預設是内部連接配接的。如果想讓c++中的const具有外部連接配接,必須顯示聲明為: extern const int a = 10;

const由c++采用,并加進标準c中,盡管他們很不一樣。在c中,編譯器對待const如同對待變量一樣,隻不過帶有一個特殊的标記,意思是”你不能改變我”。在c中定義const時,編譯器為它建立空間,是以如果在兩個不同檔案定義多個同名的const,連結器将發生連結錯誤。簡而言之,const在c++中用的更好。

了解: 能否用變量定義數組:

在支援c99标準的編譯器中,可以使用變量定義數組。

1. 微軟官方描述vs2013編譯器不支援c99.:

MicrosoftC conforms to the standard for the C language as set forth in the 9899:1990 editionof the ANSI C standard.

2. 以下代碼在Linux GCC支援c99編譯器編譯通過

int a = 10;
int arr[a];
int i = 0;
for(;i<10;i++)
    arr[i] = i;
i = 0;
for(;i<10;i++)
 printf("%d\n",arr[i]);
           

3.6.3 盡量以const替換#define

在舊版本C中,如果想建立一個常量,必須使用預處理器”

#define MAX 1024;// const int max = 1024
           

我們定義的宏MAX從未被編譯器看到過,因為在預處理階段,所有的MAX已經被替換為了1024,于是MAX并沒有将其加入到符号表中。但我們使用這個常量獲得一個編譯錯誤資訊時,可能會帶來一些困惑,因為這個資訊可能會提到1024,但是并沒有提到MAX.如果MAX被定義在一個不是你寫的頭檔案中,你可能并不知道1024代表什麼,也許解決這個問題要花費很長時間。

解決辦法就是用一個常量替換上面的宏。

const int max= 1024;
           

const和#define差別總結:

  1. const有類型,可進行編譯器類型安全檢查。#define無類型,不可進行類型檢查.
  1. const有作用域,而#define不重視作用域,預設定義處到檔案結尾.如果定義在指定作用域下有效的常量,那麼#define就不能用。

1. 宏常量沒有類型,是以調用了int類型重載的函數。const有類型,是以調用希望的short類型函數

#define PARAM 128
const short param = 128;

void func(short a){
    cout << "short!" << endl;
}
void func(int a){
    cout << "int" << endl;
}
           

2. 宏常量不重視作用域.

void func1(){
    const int a = 10;
    #define A 20 
    //#undef A  //解除安裝宏常量A
}
void func2(){
    //cout << "a:" << a << endl; //不可通路,超出了const int a作用域
    cout << "A:" << A << endl; //#define作用域從定義到檔案結束或者到#undef,可通路
}
int main(){
    func2();
    return EXIT_SUCCESS;
}
           

問題: 宏常量可以有命名空間嗎?

namespace MySpace{
    #define num 1024
}
void test(){
    //cout << MySpace::NUM << endl; //錯誤
    //int num = 100; //命名沖突
    cout << num << endl;
}
           

3.7 引用(reference)

3.7.1 引用基本用法

引用是c++對c的重要擴充。在c/c++中指針的作用基本都是一樣的,但是c++增加了另外一種給函數傳遞位址的途徑,這就是按引用傳遞(pass-by-reference),它也存在于其他一些程式設計語言中,并不是c++的發明。

  • 變量名實質上是一段連續記憶體空間的别名,是一個标号(門牌号)
  • 程式中通過變量來申請并命名記憶體空間
  • 通過變量的名字可以使用存儲空間

對一段連續的記憶體空間隻能取一個别名嗎?

c++中新增了引用的概念,引用可以 作為一個已定義變量的别名。

基本文法:

Type& ref = val;
           

注意事項:

  • &在此不是求位址運算,而是起辨別作用。
  • 類型辨別符是指目标變量的類型
  • 必須在聲明引用變量時進行初始化。
  • 引用初始化之後不能改變。
  • 不能有NULL引用。必須確定引用是和一塊合法的存儲單元關聯。
  • 建立對數組的引用。
//1. 認識引用
void test01(){

    int a = 10;
    //給變量a取一個别名b
    int& b = a;
    cout << "a:" << a << endl;
    cout << "b:" << b << endl;
    cout << "------------" << endl;
    //操作b就相當于操作a本身
    b = 100;
    cout << "a:" << a << endl;
    cout << "b:" << b << endl;
    cout << "------------" << endl;
    //一個變量可以有n個别名
    int& c = a;
    c = 200;
    cout << "a:" << a << endl;
    cout << "b:" << b << endl;
    cout << "c:" << c << endl;
    cout << "------------" << endl;
    //a,b,c的位址都是相同的
    cout << "a:" << &a << endl;
    cout << "b:" << &b << endl;
    cout << "c:" << &c << endl;
}
//2. 使用引用注意事項
void test02(){
    //1) 引用必須初始化
    //int& ref; //報錯:必須初始化引用
    //2) 引用一旦初始化,不能改變引用
    int a = 10;
    int b = 20;
    int& ref = a;
    ref = b; //不能改變引用
}
           

建立數組引用方法:

//1. 建立數組引用方法一
//1、定義數組類型
typedef int (ArrRef)[10];
int arr[10];
//2、建立引用
ArrRef& aRef = arr;
for (int i = 0; i < 10;i ++){
    aRef[i] = i+1;
}
for (int i = 0; i < 10;i++){
    cout << arr[i] << " ";
}
cout << endl;

//2. 建立數組引用方法二 
int(&f)[10] = arr; //直接定義引用
for (int i = 0; i < 10; i++){
    f[i] = i+10;
}
for (int i = 0; i < 10; i++){
    cout << arr[i] << " ";
}
cout << endl;

//3. 建立數組引用方法三
typedef int(&ArrRef1)[10];//定義引用數組類型
ArrRef1 aRef1 = arr;
for (int i = 0; i < 10; i++){
    aRef1[i] = i+10;
}
for (int i = 0; i < 10; i++){
    cout << arr[i] << " ";
}
cout << endl;
           

3.7.2 引用的本質

引用的本質在c++内部實作是一個常指針.

Type& ref = val; // Type* const ref = &val;
           

c++編譯器在編譯過程中使用常指針作為引用的内部實作,是以引用所占用的空間大小與指針相同,隻是這個過程是編譯器内部實作,使用者不可見。

//發現是引用,轉換為 int* const ref = &a;
void testFunc(int& ref){
    ref = 100; // ref是引用,轉換為*ref = 100
}
int main(){
    int a = 10;
    int& aRef = a; //自動轉換為int* const aRef = &a;這也能說明引用為什麼必須初始化
    aRef = 20; //内部發現aRef是引用,自動幫我們轉換為: *aRef = 20;
    cout << "a:" << a << endl;
    cout << "aRef:" << aRef << endl;
    testFunc(a);
    system("pause");
    return EXIT_SUCCESS;
}
           

3.7.3 指針引用

在c語言中如果想改變一個指針的指向而不是它所指向的内容,函數聲明可能這樣:

void fun(int**);
           

給指針變量取一個别名:

Type* pointer = NULL;  
Type*& ref = pointer;
           

指針間接修改與引用修改對比如下:

struct Teacher{
    int mAge;
};
//指針間接修改teacher的年齡
void AllocateAndInitByPointer(Teacher** teacher){
    *teacher = (Teacher*)malloc(sizeof(Teacher));
    (*teacher)->mAge = 200;  
}
//引用修改teacher年齡
void AllocateAndInitByReference(Teacher*& teacher){
    teacher->mAge = 300;
}
void test(){
    //建立Teacher
    Teacher* teacher = NULL;
    //指針間接指派
    AllocateAndInitByPointer(&teacher);
    cout << "AllocateAndInitByPointer:" << teacher->mAge << endl;
    //引用指派,将teacher本身傳到ChangeAgeByReference函數中
    AllocateAndInitByReference(teacher);
    cout << "AllocateAndInitByReference:" << teacher->mAge << endl;
    free(teacher);
}
           

對于c++中的定義那個,文法清晰多了。函數參數變成指針的引用,用不着取得指針的位址。

3.7.4 常量引用

常量引用的定義格式:

const Type& ref = val;
           

常量引用注意:

  • 字面量不能賦給引用,但是可以賦給const引用
  • const修飾的引用,不能修改。
void test01(){
    int a = 100;
    const int& aRef = a; //此時aRef就是a
    //aRef = 200; 不能通過aRef的值
    a = 100; //OK
    cout << "a:" << a << endl;
    cout << "aRef:" << aRef << endl;
}
void test02(){
    //不能把一個字面量賦給引用
    //int& ref = 100;
    //但是可以把一個字面量賦給常引用
    const int& ref = 100; //int temp = 200; const int& ret = temp;
}
           

3.7.5 引用使用場景

常量引用主要用在函數的形參,尤其是類的拷貝/複制構造函數。

将函數的形參定義為常量引用的好處:

  • 引用不産生新的變量,減少形參與實參傳遞時的開銷。
  • 由于引用可能導緻實參随形參改變而改變,将其定義為常量引用可以消除這種副作用。
//1.引用作為函數參數
void func(int &a, int &b)
{
    int sum = a + b;
    cout << "sum=" << sum << endl;
}

void test01()
{
    int a = 10;
    int b = 20;
    func(a, b);
}
           

如果希望實參随着形參的改變而改變,那麼使用一般的引用,如果不希望實參随着形參改變,那麼使用常引用。

//const int& param防止函數中意外修改資料
void ShowVal(const int& param){
    cout << "param:" << param << endl;
}
           

引用作為函數的傳回值:

//2.引用作為函數的傳回值
int& func2()
{
    int b = 10;//注意1:不要傳回局部變量的引用
    int &p = b;
    return p;
}
int &func3()
{
    static int b = 10;
    return b;
}
void test02()
{
    int &q = func2();
    q = 100;
    cout << q << endl;

    func2() = 200;
    cout << q << endl;
---------上面的代碼是錯誤,隻是編譯器沒有檢測出來
    cout << "func2="<<func2() << endl;

    func3() = 100;//注意2:如果要函數當左值,那麼該函數必須傳回引用
    cout << "func3()=" << func3() << endl;
}
           

3.7.5.1 引用使用中注意點

最常見看見引用的地方是在函數參數和傳回值中。當引用被用作函數參數的時,在函數内對任何引用的修改,将對還函數外的參數産生改變。當然,可以通過傳遞一個指針來做相同的事情,但引用具有更清晰的文法。

如果從函數中傳回一個引用,必須像從函數中傳回一個指針一樣對待。當函數傳回值時,引用關聯的記憶體一定要存在。

//值傳遞
void ValueSwap(int m,int n){
    int temp = m;
    m = n;
    n = temp;
}
//位址傳遞
void PointerSwap(int* m,int* n){
    int temp = *m;
    *m = *n;
    *n = temp;
}
//引用傳遞
void ReferenceSwap(int& m,int& n){
    int temp = m;
    m = n;
    n = temp;
}
void test(){
    int a = 10;
    int b = 20;
    //值傳遞
    ValueSwap(a, b);
    cout << "a:" << a << " b:" << b << endl;
    //位址傳遞
    PointerSwap(&a, &b);
    cout << "a:" << a << " b:" << b << endl;
    //引用傳遞
    ReferenceSwap(a, b);
    cout << "a:" << a << " b:" << b << endl;
}
           

通過引用參數産生的效果同按位址傳遞是一樣的。引用的文法更清楚簡單:

  1. 函數調用時傳遞的實參不必加“&”符
  1. 在被調函數中不必在參數前加“*”符

引用作為其它變量的别名而存在,是以在一些場合可以代替指針。C++主張用引用傳遞取代位址傳遞的方式,因為引用文法容易且不易出錯。

//傳回局部變量引用
int& TestFun01(){
    int a = 10; //局部變量
    return a;
}
//傳回靜态變量引用
int& TestFunc02(){    
    static int a = 20;
    cout << "static int a : " << a << endl;
    return a;
}
int main(){
    //不能傳回局部變量的引用
    int& ret01 = TestFun01();
    //如果函數做左值,那麼必須傳回引用
    TestFunc02();
    TestFunc02() = 100;
    TestFunc02();

    return EXIT_SUCCESS;
}
           
  • 不能傳回局部變量的引用。
  • 函數當左值,必須傳回引用。

3.8 C和C++的差別

  1. C語言的結構體不能寫函數,C++可以
  1. 結構體定義變量時,C++不需要加struct關鍵字
  1. 更加嚴格的類型檢查
  1. const修飾的變量,C++有時沒有記憶體,C語言的都有記憶體
  1. 三目運算符傳回的值不一樣
  1. 引用和C語言的指針功能一樣

3.9 内聯函數(inline function)

3.9.1 内聯函數的引出

c++從c中繼承的一個重要特征就是效率。假如c++的效率明顯低于c的效率,那麼就會有很大的一批程式員不去使用c++了。

在c中我們經常把一些短并且執行頻繁的計算寫成宏,而不是函數,這樣做的理由是為了執行效率,宏可以避免函數調用的開銷,這些都由預處理來完成。

但是在c++出現之後,使用預處理宏會出現兩個問題:

  • 第一個在c中也會出現,宏看起來像一個函數調用,但是會有隐藏一些難以發現的錯誤。
  • 第二個問題是c++特有的,預處理器不允許通路類的成員,也就是說預處理器宏不能用作類類的成員函數。

為了保持預處理宏的效率又增加安全性,而且還能像一般成員函數那樣可以在類裡通路自如,c++引入了内聯函數(inline function).

内聯函數為了繼承宏函數的效率,沒有函數調用時開銷,然後又可以像普通函數那樣,可以進行參數,傳回值類型的安全檢查,又可以作為成員函數。

3.9.2 預處理宏的缺陷

預處理器宏存在問題的關鍵是我們可能認為預處理器的行為和編譯器的行為是一樣的。當然也是由于宏函數調用和函數調用在外表看起來是一樣的,因為也容易被混淆。但是其中也會有一些微妙的問題出現:

問題一:

#define ADD(x,y) x+y
inline int Add(int x,int y){
    return x + y;
}
void test(){
    int ret1 = ADD(10, 20) * 10; //希望的結果是300
    int ret2 = Add(10, 20) * 10; //希望結果也是300
    cout << "ret1:" << ret1 << endl; //210
    cout << "ret2:" << ret2 << endl; //300
}
           

問題二:

#define COMPARE(x,y) ((x) < (y) ? (x) : (y))
int Compare(int x,int y){
    return x < y ? x : y;
}
void test02(){
    int a = 1;
    int b = 3;
    //cout << "COMPARE(++a, b):" << COMPARE(++a, b) << endl; // 3
    cout << "Compare(int x,int y):" << Compare(++a, b) << endl; //2
}
           

問題三:

預定義宏函數沒有作用域概念,無法作為一個類的成員函數,也就是說預定義宏沒有辦法表示類的範圍。

3.9.3 内聯函數

3.9.3.1 内聯函數基本概念

在c++中,預定義宏的概念是用内聯函數來實作的,而内聯函數本身也是一個真正的函數。内聯函數具有普通函數的所有行為。唯一不同之處在于内聯函數會在适當的地方像預定義宏一樣展開,是以不需要函數調用的開銷。是以應該不使用宏,使用内聯函數。

在普通函數(非成員函數)函數前面加上inline關鍵字使之成為内聯函數。但是必須注意必須函數體和聲明結合在一起,否則編譯器将它作為普通函數來對待。

inline void func(int a);
           

以上寫法沒有任何效果,僅僅是聲明函數,應該如下方式來做:

inline int func(int a){return ++;}
           

注意: 編譯器将會檢查函數參數清單使用是否正确,并傳回值(進行必要的轉換)。這些事預處理器無法完成的。

内聯函數的确占用空間,但是内聯函數相對于普通函數的優勢隻是省去了函數調用時候的壓棧,跳轉,傳回的開銷。我們可以了解為内聯函數是以空間換時間。

3.9.3.2 類内部的内聯函數

為了定義内聯函數,通常必須在函數定義前面放一個inline關鍵字。但是在類内部定義内聯函數時并不是必須的。任何在類内部定義的函數自動成為内聯函數。

class Person{
public:
    Person(){ cout << "構造函數!" << endl; }
    void PrintPerson(){ cout << "輸出Person!" << endl; }
}
           

構造函數Person,成員函數PrintPerson在類的内部定義,自動成為内聯函數。

3.9.3.3 内聯函數和編譯器

内聯函數并不是何時何地都有效,為了了解内聯函數何時有效,應該要知道編譯器碰到内聯函數會怎麼處理?

對于任何類型的函數,編譯器會将函數類型(包括函數名字,參數類型,傳回值類型)放入到符号表中。同樣,當編譯器看到内聯函數,并且對内聯函數體進行分析沒有發現錯誤時,也會将内聯函數放入符号表。

當調用一個内聯函數的時候,編譯器首先確定傳入參數類型是正确比對的,或者如果類型不正完全比對,但是可以将其轉換為正确類型,并且傳回值在目标表達式裡比對正确類型,或者可以轉換為目标類型,内聯函數就會直接替換函數調用,這就消除了函數調用的開銷。假如内聯函數是成員函數,對象this指針也會被放入合适位置。

類型檢查和類型轉換、包括在合适位置放入對象this指針這些都是預處理器不能完成的。

但是c++内聯編譯會有一些限制,以下情況編譯器可能考慮不會将函數進行内聯編譯:

  • 不能存在任何形式的循環語句
  • 不能存在過多的條件判斷語句
  • 函數體不能過于龐大
  • 不能對函數進行取址操作

内聯僅僅隻是給編譯器一個建議,編譯器不一定會接受這種建議,如果你沒有将函數聲明為内聯函數,那麼編譯器也可能将此函數做内聯編譯。一個好的編譯器将會内聯小的、簡單的函數。

3.10 函數的預設參數

c++在聲明函數原型的時可為一個或者多個參數指定預設(預設)的參數值,當函數調用的時候如果沒有指定這個值,編譯器會自動用預設值代替。

void TestFunc01(int a = 10, int b = 20){
    cout << "a + b  = " << a + b << endl;
}
//注意點:
//1. 形參b設定預設參數值,那麼後面位置的形參c也需要設定預設參數
void TestFunc02(int a,int b = 10,int c = 10){}
//2. 如果函數聲明和函數定義分開,函數聲明設定了預設參數,函數定義不能再設定預設參數
void TestFunc03(int a = 0,int b = 0);
void TestFunc03(int a, int b){}

int main(){
    //1.如果沒有傳參數,那麼使用預設參數
    TestFunc01();
    //2. 如果傳一個參數,那麼第二個參數使用預設參數
    TestFunc01(100);
    //3. 如果傳入兩個參數,那麼兩個參數都使用我們傳入的參數
    TestFunc01(100, 200);

    return EXIT_SUCCESS;
}
           

注意點:

  • 函數的預設參數從左向右,如果一個參數設定了預設參數,那麼這個參數之後的參數都必須設定預設參數。
  • 如果函數聲明和函數定義分開寫,函數聲明和函數定義不能同時設定預設參數。

3.11 函數的占位參數

c++在聲明函數時,可以設定占位參數。占位參數隻有參數類型聲明,而沒有參數名聲明。一般情況下,在函數體内部無法使用占位參數。

void TestFunc01(int a,int b,int){
    //函數内部無法使用占位參數
    cout << "a + b = " << a + b << endl;
}
//占位參數也可以設定預設值
void TestFunc02(int a, int b, int = 20){
    //函數内部依舊無法使用占位參數
    cout << "a + b = " << a + b << endl;
}
int main(){

    //錯誤調用,占位參數也是參數,必須傳參數
    //TestFunc01(10,20); 
    //正确調用
    TestFunc01(10,20,30);
    //正确調用
    TestFunc02(10,20);
    //正确調用
    TestFunc02(10, 20, 30);

    return EXIT_SUCCESS;
}
           

什麼時候用,在後面要講的操作符重載的區分前置++或 後置++要用到這個.

3.12 函數重載(overload)

3.12.1 函數重載概述

在傳統c語言中,函數名必須是唯一的,程式中不允許出現同名的函數。在c++中是允許出現同名的函數,這種現象稱為函數重載。函數重載的目的就是為了友善的使用函數名。

3.12.2 基本文法

實作函數重載的條件:

  • 同一個作用域
  • 參數個數不同
  • 參數類型不同
  • 參數順序不同
//1. 函數重載條件
namespace A{
    void MyFunc(){ cout << "無參數!" << endl; }
    void MyFunc(int a){ cout << "a: " << a << endl; }
    void MyFunc(string b){ cout << "b: " << b << endl; }
    void MyFunc(int a, string b){ cout << "a: " << a << " b:" << b << endl;}
    void MyFunc(string b, int a){cout << "a: " << a << " b:" << b << endl;}
}
//2.傳回值不作為函數重載依據
namespace B{
    void MyFunc(string b, int a){}
    //int MyFunc(string b, int a){} //無法重載僅按傳回值區分的函數
}
           

注意: 函數重載和預設參數一起使用,需要額外注意二義性問題的産生。

void MyFunc(string b){
    cout << "b: " << b << endl;
}
//函數重載碰上預設參數
void MyFunc(string b, int a = 10){
    cout << "a: " << a << " b:" << b << endl;
}
int main(){
    MyFunc("hello"); //這時,兩個函數都能比對調用,産生二義性
    return 0;
}
           

思考: 為什麼函數傳回值不作為重載條件呢?

當編譯器能從上下文中确定唯一的函數的時,如int ret =func(),這個當然是沒有問題的。然而,我們在編寫程式過程中可以忽略他的傳回值。那麼這個時候,假如一個函數為

void func(int x);另一個為int func(int x); 當我們直接調用func(10),這個時候 編譯器就不确定調用那個函數。是以在 c++中禁止使用傳回值作為重載的條件。

3.12.3 函數重載實作原理

編譯器為了實作函數重載,也是預設為我們做了一些幕後的工作,編譯器用不同的參數類型來修飾不同的函數名,比如void func(); 編譯器可能會将函數名修飾成_func,當編譯器碰到void func(int x),編譯器可能将函數名修飾為func_int,當編譯器碰到void func(int x,char c),編譯器可能會将函數名修飾為_func_int_char我這裡使用”可能”這個字眼是因為編譯器如何修飾重載的函數名稱并沒有一個統一的标準,是以不同的編譯器可能會産生不同的内部名。

void func(){}
void func(int x){}
void func(int x,char y){}
// 以上三個函數在linux下生成的編譯之後的函數名為:
_Z4funcv //v 代表void,無參數
_Z4funci //i 代表參數為int類型
_Z4funcic //i 代表第一個參數為int類型,第二個參數為char類型
           

3.12.4 extern “C”淺析

以下在Linux下測試:

c函數: void MyFunc(){} ,被編譯成函數: MyFunc

c++函數: voidMyFunc(){},被編譯成函數: _Z6Myfuncv

通過這個測試,由于c++中需要支援函數重載,是以c和c++中對同一個函數經過編譯後生成的函數名是不相同的,這就導緻了一個問題,如果在c++中調用一個使用c語言編寫子產品中的某個函數,那麼c++是根據c++的名稱修飾方式來查找并連結這個函數,那麼就會發生連結錯誤,以上例,c++中調用MyFunc函數,在連結階段會去找Z6Myfuncv,結果是沒有找到的,因為這個MyFunc函數是c語言編寫的,生成的符号是MyFunc。

那麼如果我想在c++調用c的函數怎麼辦?

extern"C"的主要作用就是為了實作c++代碼能夠調用其他c語言代碼。加上extern "C"後,這部分代碼編譯器按c語言的方式進行編譯和連結,而不是按c++的方式。

MyModule.h

#ifndef MYMODULE_H
#define MYMODULE_H

#include<stdio.h>

//這是告訴C++編譯器,找下面的函數,要以C語言的方式去尋找
#ifdef __cplusplus
extern "C"{
#endif
    //C語言的函數聲明
    void func1();
    int func2(int a,int b);

#ifdef __cplusplus
}
#endif

#endif
           

MyModule.c

#include"MyModule.h"

void func1(){
    printf("hello world!");
}
int func2(int a, int b){
    return a + b;
}
           

TestExternC.cpp

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

#if 0

    #ifdef __cplusplus
    extern "C" {
        #if 0
            void func1();
            int func2(int a, int b);
        #else
            #include"MyModule.h"
        #endif
    }

    #endif

#else

    extern "C" void func1();
    extern "C" int func2(int a, int b);

#endif

int main(){
    func1();
    cout << func2(10, 20) << endl;
    return EXIT_SUCCESS;
}
           

繼續閱讀