【C++】面向?qū)ο笾瓹++對C的擴展-002

第三章 C++對C的擴展

3.1 ::作用域運算符

???????通常情況下,如果有兩個同名變量,一個是全局變量,另一個是局部變量,那么局部變量在其作用域內(nèi)具有較高的優(yōu)先權(quán),它將屏蔽全局變量。

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

程序的輸出結(jié)果是a:20
test函數(shù)的輸出語句中,使用的變量atest函數(shù)內(nèi)定義的局部變量,因此輸出的結(jié)果為局部變量a的值。

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

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

???????這個例子可以看出,作用域運算符可以用來解決局部變量與全局變量的重名問題,即在局部變量的作用域內(nèi),可用 :: 對被屏蔽的同名的全局變量進行訪問。

3.2 名字控制

???????創(chuàng)建名字是程序設(shè)計過程中一項最基本的活動,當(dāng)一個項目很大時,它會不可避免地包含大量名字。c++ 允許我們對名字的產(chǎn)生和名字的可見性進行控制。

???????我們之前在學(xué)習(xí)c語言可以通過static關(guān)鍵字來使得名字只得在本編譯單元內(nèi)可見,在c++ 中我們將通過一種通過命名空間來控制對名字的訪問。

3.2.1 C++命名空間(namespace)

???????在c++ 中,名稱(name)可以是符號常量、變量、函數(shù)、結(jié)構(gòu)、枚舉、類和對象等等。工程越大,名稱互相沖突性的可能性越大。另外使用多個廠商的類庫時,也可能導(dǎo)致名稱沖突。為了避免,在大規(guī)模程序的設(shè)計中,以及在程序員使用各種各樣的C++ 庫時,這些標(biāo)識符的命名發(fā)生沖突,標(biāo)準(zhǔn)C++ 引入關(guān)鍵字namespace(命名空間/名字空間/名稱空間),可以更好地控制標(biāo)識符的作用域。

3.2.2 命名空間使用語法

  • 創(chuàng)建一個命名空間:
namespace A{
    int a = 10;
}
namespace B{
    int a = 20;
}
void test(){
    cout << "A::a : " << A::a << endl;
    cout << "B::a : " << B::a << endl;
}
  • 命名空間只能命名空全局范圍內(nèi)定義(以下錯誤寫法)
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();
}
  • 聲明和實現(xiàn)可分離
#pragma once

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

void MySpace::func1(){
    cout << "MySpace::func1" << endl;
}

void MySpace::func2(int param){
    cout << "MySpace::func2 : " << param << endl;
}
  • 無名命名空間,意味著命名空間中的標(biāo)識符只能在本文件內(nèi)訪問,相當(dāng)于給這個標(biāo)識符加上了static,使得其可以作為內(nèi)部連接
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聲明可使得指定的標(biāo)識符可用。

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聲明碰到函數(shù)重載

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);
}

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

3.2.4 using編譯指令

???????using編譯指令使整個命名空間標(biāo)識符可用.

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();

    //不會產(chǎn)生二義性
    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;
    //二義性產(chǎn)生,不知道調(diào)用A還是B的paramA
    //cout << paramA << endl;
}

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

3.2.5 命名空間使用

???????需要記住的關(guān)鍵問題是當(dāng)引入一個全局的using編譯指令時,就為該文件打開了該命名空間,它不會影響任何其他的文件,所以可以在每一個實現(xiàn)文件中調(diào)整對命名空間的控制。比如,如果發(fā)現(xiàn)某一個實現(xiàn)文件中有太多的using指令而產(chǎn)生的命名沖突,就要對該文件做個簡單的改變,通過明確的限定或者using聲明來消除名字沖突,這樣不需要修改其他的實現(xiàn)文件。

3.3 全局變量檢測增強

c語言代碼:

int a = 10; //賦值,當(dāng)做定義
int a; //沒有賦值,當(dāng)做聲明

int main(){
    printf("a:%d\n",a);
    return EXIT_SUCCESS;
}

此代碼在c++下編譯失敗,在c下編譯通過.

3.4 C++中所有的變量和函數(shù)都必須有類型

c語言代碼:

//i沒有寫類型,可以是任意類型
int fun1(i){
    printf("%d\n", i);
    return 0;
}
//i沒有寫類型,可以是任意類型
int fun2(i){
    printf("%s\n", i);
    return 0;
}
//沒有寫參數(shù),代表可以傳任何類型的實參
int fun3(){ 
    printf("fun33333333333333333\n");
    return 0;
}

//C語言,如果函數(shù)沒有參數(shù),建議寫void,代表沒有參數(shù)
int fun4(void){
    printf("fun4444444444444\n");
    return 0;
}

g(){
    return 10;
}

int main(){
    fun1(10);
    fun2("abc");
    fun3(1, 2, "abc");
    printf("g = %d\n", g());

    return 0;
}

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

  • C語言中,int fun() 表示返回值為int,接受任意參數(shù)的函數(shù),int fun(void) 表示返回值為int的無參函數(shù)。
  • C++ 中,int fun()int fun(void) 具有相同的意義,都表示返回值為int的無參函數(shù)。

3.5 更嚴(yán)格的類型轉(zhuǎn)換

C++,不同類型的變量一般是不能直接賦值的,需要相應(yīng)的強轉(zhuǎn)。
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++ 編譯器無法編譯通過。

3.6 struct類型加強

  • c中定義結(jié)構(gòu)體變量需要加上struct關(guān)鍵字,c++ 不需要。
  • c中的結(jié)構(gòu)體只能定義成員變量,不能定義成員函數(shù)。c++ 即可以定義成員變量,也可以定義成員函數(shù)。
//1. 結(jié)構(gòu)體中即可以定義成員變量,也可以定義成員函數(shù)
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++中定義結(jié)構(gòu)體變量不需要加struct關(guān)鍵字
void test01(){
    Student student;
    student.setName("John");
    student.setAge(20);
    student.showStudent();
}

3.7 新增bool類型關(guān)鍵字

???????標(biāo)準(zhǔn)c++bool類型有兩種內(nèi)建的常量true(轉(zhuǎn)換為整數(shù)1)和false(轉(zhuǎn)換為整數(shù)0)表示狀態(tài)。這三個名字都是關(guān)鍵字。

  • bool類型只有兩個值,true(1值),false(0值)
  • bool類型占1個字節(jié)大小
  • bool類型賦值時,非0值會自動轉(zhuǎn)換為true(1),0值會自動轉(zhuǎn)換false(0)
void test()
{   cout << sizeof(false) << endl; //為1,//bool類型占一個字節(jié)大小
    bool flag = true; // c語言中沒有這種類型
    flag = 100; //給bool類型賦值時,非0值會自動轉(zhuǎn)換為true(1),0值會自動轉(zhuǎn)換false(0)
}

c語言中的bool類型:

???????c語言中也有bool類型,在c99標(biāo)準(zhǔn)之前是沒有bool關(guān)鍵字,c99標(biāo)準(zhǔn)已經(jīng)有bool類型,包含頭文件stdbool.h,就可以使用和c++ 一樣的bool類型。

3.8 三目運算符功能增強

  • c語言三目運算表達式返回值為數(shù)據(jù)值,為右值,不能賦值。
    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;

左值和右值概念:
c++ 中可以放在賦值操作符左邊的是左值,可以放到賦值操作符右面的是右值。
有些變量即可以當(dāng)左值,也可以當(dāng)右值。
左值為Lvalue,L代表Location,表示內(nèi)存可以尋址,可以賦值。
右值為RvalueR代表Read,就是可以知道它的值。
比如:int temp = 10; temp在內(nèi)存中有地址,10沒有,但是可以Read到它的值。

3.9 C/C++中的const

3.9.1 const概述

???????const單詞字面意思為常數(shù),不變的。它是c/c++ 中的一個關(guān)鍵字,是一個限定符,它用來限定一個變量不允許改變,它將一個對象轉(zhuǎn)換成一個常量。

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

3.9.2 C/C++中const的區(qū)別

3.9.2.1 C中的const

???????常量的引進是在c++ 早期版本中,當(dāng)時標(biāo)準(zhǔn)C規(guī)范正在制定。那時,盡管C委員會決定在C中引入const,但是,他們c中的const理解為”一個不能改變的普通變量”,也就是認為const應(yīng)該是一個只讀變量,既然是變量那么就會給const分配內(nèi)存,并且在cconst是一個全局只讀變量,c語言中const修飾的只讀變量是外部連接的。
???????如果這么寫:

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

???????看似是一件合理的編碼,但是這將得出一個錯誤。 因為arrSize占用某塊內(nèi)存,所以C編譯器不知道它在編譯時的值是多少?

3.9.2.2 C++中的const

???????在c++ 中,一個const不必創(chuàng)建內(nèi)存空間,而在c中,一個const總是需要一塊內(nèi)存空間。在c++ 中,是否為const常量分配內(nèi)存空間依賴于如何使用。一般說來,如果一個const僅僅用來把一個名字用一個值代替(就像使用 #define 一樣),那么該存儲局空間就不必創(chuàng)建。

???????如果存儲空間沒有分配內(nèi)存的話,在進行完數(shù)據(jù)類型檢查后,為了代碼更加有效,值也許會折疊到代碼中。

???????不過,取一個const地址, 或者把它定義為extern,則會為該const創(chuàng)建內(nèi)存空間。

??????? 在c++ 中,出現(xiàn)在所有函數(shù)之外的const作用于整個文件(也就是說它在該文件外不可見),默認為內(nèi)部連接,c++ 中其他的標(biāo)識符一般默認為外部連接。

3.9.2.3 C/C++中const異同總結(jié)

  • c語言全局const會被存儲到只讀數(shù)據(jù)段。c++ 中全局const當(dāng)聲明extern或者對變量取地址時,編譯器會分配存儲地址,變量存儲在只讀數(shù)據(jù)段。兩個都受到了只讀數(shù)據(jù)段的保護,不可修改。
      const int constA = 10;
      int main(){
           int* p = (int*)&constA;
           *p = 200;
     }

??????? 以上代碼在c/c++ 中編譯通過,在運行期,修改constA的值時,發(fā)生寫入錯誤。原因是修改只讀數(shù)據(jù)段的數(shù)據(jù)。

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

運行結(jié)果:

constA:300
*p:300

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

c++中對于局部的const變量要區(qū)別對待:
???????1. 對于基礎(chǔ)數(shù)據(jù)類型,也就是const int a = 10這種,編譯器會把它放到符號表中,不分配內(nèi)存,當(dāng)對其取地址時,會分配內(nèi)存。

    const int constA = 10;
    int* p = (int*)&constA;
    *p = 300;
    cout << "constA:" << constA << endl;
    cout << "*p:" << *p << endl;

運行結(jié)果:

constA:10
*p:300

constA在符號表中,當(dāng)我們對constA取地址,這個時候為constA分配了新的空間,*p操作的是分配的空間,而 constA 是從符號表獲得的值。

???????2. 對于基礎(chǔ)數(shù)據(jù)類型,如果用一個變量初始化const變量,如果const int a = b,那么也是會給a分配內(nèi)存。

    int b = 10;
    const int constA = b;
    int* p = (int*)&constA;
    *p = 300;
    cout << "constA:" << constA << endl;
    cout << "*p:" << *p << endl;

運行結(jié)果:

constA:300
*p:300

???????constA 分配了內(nèi)存,所以我們可以修改constA內(nèi)存中的值。
???????3. 對于自定數(shù)據(jù)類型,比如類對象,那么也會分配內(nèi)存。

    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;

運行結(jié)果:

pPerson->age:100
pPerson->age:200

???????為person分配了內(nèi)存,所以我們可以通過指針的間接賦值修改person對象。

  • cconst默認為外部連接,c++const默認為內(nèi)部連接.當(dāng)c語言兩個文件中都有const int a的時候,編譯器會報重定義的錯誤。而在c++ 中,則不會,因為c++ 中的const默認是內(nèi)部連接的。如果想讓c++ 中的const具有外部連接,必須顯示聲明為: extern const int a = 10;

???????constc++ 采用,并加進標(biāo)準(zhǔn)c中,盡管他們很不一樣。在c中,編譯器對待const如同對待變量一樣,只不過帶有一個特殊的標(biāo)記,意思是”你不能改變我”。在c++ 中定義const時,編譯器為它創(chuàng)建空間,所以如果在兩個不同文件定義多個同名的const,鏈接器將發(fā)生鏈接錯誤。簡而言之,constc++ 中用的更好。

了解: 能否用變量定義數(shù)組:
在支持c99標(biāo)準(zhǔn)的編譯器中,可以使用變量定義數(shù)組。

  1. 微軟官方描述vs2013編譯器不支持c99.:
Microsoft C conforms to the standard for the C language as set forth in the 9899:1990 edition of the ANSI C standard. 
  1. 以下代碼在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.9.3 盡量以const替換#define

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

???????#define MAX 1024;

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

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

???????const int max= 1024;

const和#define區(qū)別總結(jié):

1.const有類型,可進行編譯器類型安全檢查。#define無類型,不可進行類型檢查.
2.const有作用域,而#define 不重視作用域,默認定義處到文件結(jié)尾.如果定義在指定作用域下有效的常量,那么 #define 就不能用。

  1. 宏常量沒有類型,所以調(diào)用了int類型重載的函數(shù)。const有類型,所以調(diào)用希望的short類型函數(shù)?
#define PARAM 128
const short param = 128;

void func(short a){
    cout << "short!" << endl;
}
void func(int a){
    cout << "int" << endl;
}
  1. 宏常量不重視作用域.
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作用域從定義到文件結(jié)束或者到#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.10 引用(reference)

3.10.1 引用基本用法

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

  • 變量名實質(zhì)上是一段連續(xù)內(nèi)存空間的別名,是一個標(biāo)號(門牌號)
  • 程序中通過變量來申請并命名內(nèi)存空間
  • 通過變量的名字可以使用存儲空間

???????對一段連續(xù)的內(nèi)存空間只能取一個別名嗎?
???????c++ 中新增了引用的概念,引用可以作為一個已定義變量的別名。

???????基本語法:

Type& ref = val;

???????注意事項:

  • &在此不是求地址運算,而是起標(biāo)識作用。
  • 類型標(biāo)識符是指目標(biāo)變量的類型
  • 必須在聲明引用變量時進行初始化。
  • 引用初始化之后不能改變。
  • 不能有NULL引用。必須確保引用是和一塊合法的存儲單元關(guān)聯(lián)。
  • 可以建立對數(shù)組的引用。
//1. 認識引用
void test01(){

    int a = 10;
    //給變量a取一個別名b
    int& b = a;
    cout << "a:" << a << endl;
    cout << "b:" << b << endl;
    cout << "------------" << endl;
    //操作b就相當(dāng)于操作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; //不能改變引用
    //3) 不能對數(shù)組建立引用
    int arr[10];
    //int& ref3[10] = arr;
}

    //1. 建立數(shù)組引用方法一
    typedef int ArrRef[10];
    int arr[10];
    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. 建立數(shù)組引用方法二
    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.10.2 函數(shù)中的引用

???????最常見看見引用的地方是在函數(shù)參數(shù)和返回值中。當(dāng)引用被用作函數(shù)參數(shù)的時,在函數(shù)內(nèi)對任何引用的修改,將對還函數(shù)外的參數(shù)產(chǎn)生改變。當(dāng)然,可以通過傳遞一個指針來做相同的事情,但引用具有更清晰的語法。

???????如果從函數(shù)中返回一個引用,必須像從函數(shù)中返回一個指針一樣對待。當(dāng)函數(shù)返回值時,引用關(guān)聯(lián)的內(nèi)存一定要存在。

//值傳遞
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;
}

通過引用參數(shù)產(chǎn)生的效果同按地址傳遞是一樣的。引用的語法更清楚簡單:
???????1) 函數(shù)調(diào)用時傳遞的實參不必加“&”符
???????2) 在被調(diào)函數(shù)中不必在參數(shù)前加“ * ”符

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

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

    return EXIT_SUCCESS;
}
  • 不能返回局部變量的引用。
  • 函數(shù)當(dāng)左值,必須返回引用。

3.10.3 引用的本質(zhì)

???????引用的本質(zhì)在c++內(nèi)部實現(xiàn)是一個指針常量.

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

???????c++ 編譯器在編譯過程中使用常指針作為引用的內(nèi)部實現(xiàn),因此引用所占用的空間大小與指針相同,只是這個過程是編譯器內(nèi)部實現(xiàn),用戶不可見。

//發(fā)現(xiàn)是引用,轉(zhuǎn)換為 int* const ref = &a;
void testFunc(int& ref){
    ref = 100; // ref是引用,轉(zhuǎn)換為*ref = 100
}
int main(){
    int a = 10;
    int& aRef = a; //自動轉(zhuǎn)換為 int* const aRef = &a;這也能說明引用為什么必須初始化
    aRef = 20; //內(nèi)部發(fā)現(xiàn)aRef是引用,自動幫我們轉(zhuǎn)換為: *aRef = 20;
    cout << "a:" << a << endl;
    cout << "aRef:" << aRef << endl;
    testFunc(a);
    return EXIT_SUCCESS;
}

3.10.4 指針引用

???????在c語言中如果想改變一個指針的指向而不是它所指向的內(nèi)容,函數(shù)聲明可能這樣:

void fun(int**);

???????給指針變量取一個別名。

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

???????Type pointer = NULL; Type& = 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(){
    //創(chuàng)建Teacher
    Teacher* teacher = NULL;
    //指針間接賦值
    AllocateAndInitByPointer(&teacher);
    cout << "AllocateAndInitByPointer:" << teacher->mAge << endl;
    //引用賦值,將teacher本身傳到ChangeAgeByReference函數(shù)中
    AllocateAndInitByReference(teacher);
    cout << "AllocateAndInitByReference:" << teacher->mAge << endl;
    free(teacher);
}

對于c++ 中的定義那個,語法清晰多了。函數(shù)參數(shù)變成指針的引用,用不著取得指針的地址。

3.10.5 常量引用

???????常量引用的定義格式:

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;
}

const引用使用場景:
???????常量引用主要用在函數(shù)的形參,尤其是類的拷貝/復(fù)制構(gòu)造函數(shù)。

???????將函數(shù)的形參定義為常量引用的好處:

  • 引用不產(chǎn)生新的變量,減少形參與實參傳遞時的開銷。

  • 由于引用可能導(dǎo)致實參隨形參改變而改變,將其定義為常量引用可以消除這種副作用。

    如果希望實參隨著形參的改變而改變,那么使用一般的引用,如果不希望實參隨著形參改變,那么使用常引用。

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

3.11 練習(xí)作業(yè)

  1. 設(shè)計一個類,求圓的周長。
  2. 設(shè)計一個學(xué)生類,屬性有姓名和學(xué)號,可以給姓名和學(xué)號賦值,可以顯示學(xué)生的姓 名和學(xué)號

3.12 內(nèi)聯(lián)函數(shù)(inline function)

3.12.1 內(nèi)聯(lián)函數(shù)的引出

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

???????在c中我們經(jīng)常把一些短并且執(zhí)行頻繁的計算寫成宏,而不是函數(shù),這樣做的理由是為了執(zhí)行效率,宏可以避免函數(shù)調(diào)用的開銷,這些都由預(yù)處理來完成。

???????但是在c++ 出現(xiàn)之后,使用預(yù)處理宏會出現(xiàn)兩個問題:

  • 第一個在c中也會出現(xiàn),宏看起來像一個函數(shù)調(diào)用,但是會有隱藏一些難以發(fā)現(xiàn)的錯誤。
  • 第二個問題是c++ 特有的,預(yù)處理器不允許訪問類的成員,也就是說預(yù)處理器宏不能用作類類的成員函數(shù)。

???????為了保持預(yù)處理宏的效率又增加安全性,而且還能像一般成員函數(shù)那樣可以在類里訪問自如,c++ 引入了內(nèi)聯(lián)函數(shù)(inline function).

???????內(nèi)聯(lián)函數(shù)為了繼承宏函數(shù)的效率,沒有函數(shù)調(diào)用時開銷,然后又可以像普通函數(shù)那樣,可以進行參數(shù),返回值類型的安全檢查,又可以作為成員函數(shù)。

3.12.2 預(yù)處理宏的缺陷

???????預(yù)處理器宏存在問題的關(guān)鍵是我們可能認為預(yù)處理器的行為和編譯器的行為是一樣的。當(dāng)然也是由于宏函數(shù)調(diào)用和函數(shù)調(diào)用在外表看起來是一樣的,因為也容易被混淆。但是其中也會有一些微妙的問題出現(xiàn):

問題一:

#define ADD(x,y) x+y
inline int Add(int x,int y){
    return x + y;
}
void test(){
    int ret1 = ADD(10, 20) * 10; //希望的結(jié)果是300
    int ret2 = Add(10, 20) * 10; //希望結(jié)果也是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
}

問題三:
???????預(yù)定義宏函數(shù)沒有作用域概念,無法作為一個類的成員函數(shù),也就是說預(yù)定義宏沒有辦法表示類的范圍。

3.12.3 內(nèi)聯(lián)函數(shù)

3.12.3.1 內(nèi)聯(lián)函數(shù)基本概念

???????在c++中,預(yù)定義宏的概念是用內(nèi)聯(lián)函數(shù)來實現(xiàn)的,而內(nèi)聯(lián)函數(shù)本身也是一個真正的函數(shù)。內(nèi)聯(lián)函數(shù)具有普通函數(shù)的所有行為。唯一不同之處在于內(nèi)聯(lián)函數(shù)會在適當(dāng)?shù)牡胤较耦A(yù)定義宏一樣展開,所以不需要函數(shù)調(diào)用的開銷。因此應(yīng)該不使用宏,使用內(nèi)聯(lián)函數(shù)。

  • 在普通函數(shù)(非成員函數(shù))函數(shù)前面加上inline關(guān)鍵字使之成為內(nèi)聯(lián)函數(shù)。但是必須注意必須函數(shù)體和聲明結(jié)合在一起,否則編譯器將它作為普通函數(shù)來對待。
inline void func(int a);

???????以上寫法沒有任何效果,僅僅是聲明函數(shù),應(yīng)該如下方式來做:

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

???????注意: 編譯器將會檢查函數(shù)參數(shù)列表使用是否正確,并返回值(進行必要的轉(zhuǎn)換)。這些事預(yù)處理器無法完成的。

???????內(nèi)聯(lián)函數(shù)的確占用空間,但是內(nèi)聯(lián)函數(shù)相對于普通函數(shù)的優(yōu)勢只是省去了函數(shù)調(diào)用時候的壓棧,跳轉(zhuǎn),返回的開銷。我們可以理解為內(nèi)聯(lián)函數(shù)是以空間換時間。

3.12.3.2 類內(nèi)部的內(nèi)聯(lián)函數(shù)

???????為了定義內(nèi)聯(lián)函數(shù),通常必須在函數(shù)定義前面放一個inline關(guān)鍵字。但是在類內(nèi)部定義內(nèi)聯(lián)函數(shù)時并不是必須的。任何在類內(nèi)部定義的函數(shù)自動成為內(nèi)聯(lián)函數(shù)。

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

???????構(gòu)造函數(shù)Person,成員函數(shù)PrintPerson在類的內(nèi)部定義,自動成為內(nèi)聯(lián)函數(shù)。

3.12.3.3 內(nèi)聯(lián)函數(shù)和編譯器

???????內(nèi)聯(lián)函數(shù)并不是何時何地都有效,為了理解內(nèi)聯(lián)函數(shù)何時有效,應(yīng)該要知道編譯器碰到內(nèi)聯(lián)函數(shù)會怎么處理?

???????對于任何類型的函數(shù),編譯器會將函數(shù)類型(包括函數(shù)名字,參數(shù)類型,返回值類型)放入到符號表中。同樣,當(dāng)編譯器看到內(nèi)聯(lián)函數(shù),并且對內(nèi)聯(lián)函數(shù)體進行分析沒有發(fā)現(xiàn)錯誤時,也會將內(nèi)聯(lián)函數(shù)放入符號表。
當(dāng)調(diào)用一個內(nèi)聯(lián)函數(shù)的時候,編譯器首先確保傳入?yún)?shù)類型是正確匹配的,或者如果類型不正完全匹配,但是可以將其轉(zhuǎn)換為正確類型,并且返回值在目標(biāo)表達式里匹配正確類型,或者可以轉(zhuǎn)換為目標(biāo)類型,內(nèi)聯(lián)函數(shù)就會直接替換函數(shù)調(diào)用,這就消除了函數(shù)調(diào)用的開銷。假如內(nèi)聯(lián)函數(shù)是成員函數(shù),對象this指針也會被放入合適位置。

???????類型檢查和類型轉(zhuǎn)換、包括在合適位置放入對象this指針這些都是預(yù)處理器不能完成的。

???????但是c++ 內(nèi)聯(lián)編譯會有一些限制,以下情況編譯器可能考慮不會將函數(shù)進行內(nèi)聯(lián)編譯:

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

內(nèi)聯(lián)僅僅只是給編譯器一個建議,編譯器不一定會接受這種建議,如果你沒有將函數(shù)聲明為內(nèi)聯(lián)函數(shù),那么編譯器也可能將此函數(shù)做內(nèi)聯(lián)編譯。一個好的編譯器將會內(nèi)聯(lián)小的、簡單的函數(shù)。

3.13 函數(shù)的默認參數(shù)

???????c++ 在聲明函數(shù)原型的時可為一個或者多個參數(shù)指定默認(缺省)的參數(shù)值,當(dāng)函數(shù)調(diào)用的時候如果沒有指定這個值,編譯器會自動用默認值代替。

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

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

    return EXIT_SUCCESS;
}

注意點:

  • 函數(shù)的默認參數(shù)從左向右,如果一個參數(shù)設(shè)置了默認參數(shù),那么這個參數(shù)之后的參數(shù)都必須設(shè)置默認參數(shù)。
  • 如果函數(shù)聲明和函數(shù)定義分開寫,函數(shù)聲明和函數(shù)定義不能同時設(shè)置默認參數(shù)。

3.14 函數(shù)的占位參數(shù)

???????c++ 在聲明函數(shù)時,可以設(shè)置占位參數(shù)。占位參數(shù)只有參數(shù)類型聲明,而沒有參數(shù)名聲明。一般情況下,在函數(shù)體內(nèi)部無法使用占位參數(shù)。

void TestFunc01(int a,int b,int){
    //函數(shù)內(nèi)部無法使用占位參數(shù)
    cout << "a + b = " << a + b << endl;
}
//占位參數(shù)也可以設(shè)置默認值
void TestFunc02(int a, int b, int = 20){
    //函數(shù)內(nèi)部依舊無法使用占位參數(shù)
    cout << "a + b = " << a + b << endl;
}
int main(){

    //錯誤調(diào)用,占位參數(shù)也是參數(shù),必須傳參數(shù)
    //TestFunc01(10,20); 
    //正確調(diào)用
    TestFunc01(10,20,30);
    //正確調(diào)用
    TestFunc02(10,20);
    //正確調(diào)用
    TestFunc02(10, 20, 30);

    return EXIT_SUCCESS;
}

什么時候用,在后面的操作符重載的后置++要用到這個.

3.15 函數(shù)重載(overload)

3.15.1 函數(shù)重載概述

能使名字方便使用,是任何程序設(shè)計語言的一個重要特征!

???????我們現(xiàn)實生活中經(jīng)常會碰到一些字在不同的場景下具有不同的意思,比如漢語中的多音字“重”。

???????當(dāng)我們說: “他好重啊,我都背不動!”我們根據(jù)上下文意思,知道“重”在此時此地表示重量的意思。

???????如果我們說“你怎么寫了那么多重復(fù)的代碼? 維護性太差了!”這個地方我們知道,“重”表示重復(fù)的意思。

???????同樣一個字在不同的場景下具有不同的含義。那么在c++ 中也有一種類似的現(xiàn)象出現(xiàn),同一個函數(shù)名在不同場景下可以具有不同的含義。

???????在傳統(tǒng)c語言中,函數(shù)名必須是唯一的,程序中不允許出現(xiàn)同名的函數(shù)。在c++ 中是允許出現(xiàn)同名的函數(shù),這種現(xiàn)象稱為函數(shù)重載。

???????函數(shù)重載的目的就是為了方便的使用函數(shù)名。

???????函數(shù)重載并不復(fù)雜,等大家學(xué)完就會明白什么時候需要用到他們,以及是如何編譯,鏈接的。

3.15.2 函數(shù)重載

3.15.2.1 函數(shù)重載基本語法

實現(xiàn)函數(shù)重載的條件:

  • 同一個作用域
  • 參數(shù)個數(shù)不同
  • 參數(shù)類型不同
  • 參數(shù)順序不同
//1. 函數(shù)重載條件
namespace A{
    void MyFunc(){ cout << "無參數(shù)!" << 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.返回值不作為函數(shù)重載依據(jù)
namespace B{
    void MyFunc(string b, int a){}
    //int MyFunc(string b, int a){} //無法重載僅按返回值區(qū)分的函數(shù)
}

注意: 函數(shù)重載和默認參數(shù)一起使用,需要額外注意二義性問題的產(chǎn)生。

void MyFunc(string b){
    cout << "b: " << b << endl;
}
//函數(shù)重載碰上默認參數(shù)
void MyFunc(string b, int a = 10){
    cout << "a: " << a << " b:" << b << endl;
}
int main(){
    MyFunc("hello"); //這時,兩個函數(shù)都能匹配調(diào)用,產(chǎn)生二義性
    return 0;
}

思考:為什么函數(shù)返回值不作為重載條件呢?

??????? 當(dāng)編譯器能從上下文中確定唯一的函數(shù)的時,如int ret = func(),這個當(dāng)然是沒有問題的。然而,我們在編寫程序過程中可以忽略他的返回值。那么這個時候,一個函數(shù)為void func(int x);另一個為int func(int x); 當(dāng)我們直接調(diào)用func(10),這個時候編譯器就不確定調(diào)用那個函數(shù)。所以在c++ 中禁止使用返回值作為重載的條件。

3.15.2.2 函數(shù)重載實現(xiàn)原理

???????編譯器為了實現(xiàn)函數(shù)重載,也是默認為我們做了一些幕后的工作,編譯器用不同的參數(shù)類型來修飾不同的函數(shù)名,比如void func(); 編譯器可能會將函數(shù)名修飾成_func,當(dāng)編譯器碰到void func(int x),編譯器可能將函數(shù)名修飾為_func_int,當(dāng)編譯器碰到void func(int x,char c),編譯器可能會將函數(shù)名修飾為_func_int_char我這里使用”可能”這個字眼是因為編譯器如何修飾重載的函數(shù)名稱并沒有一個統(tǒng)一的標(biāo)準(zhǔn),所以不同的編譯器可能會產(chǎn)生不同的內(nèi)部名。

void func(){}
void func(int x){}
void func(int x,char y){}

以上三個函數(shù)在linux下生成的編譯之后的函數(shù)名為:

_Z4funcv //v 代表void,無參數(shù)
_Z4funci //i 代表參數(shù)為int類型
_Z4funcic //i 代表第一個參數(shù)為int類型,第二個參數(shù)為char類型

3.15.3 extern “C”淺析

以下在Linux下測試:
c函數(shù): void MyFunc(){} ,被編譯成函數(shù): MyFunc
c++函數(shù): void MyFunc(){},被編譯成函數(shù): _Z6Myfuncv

???????通過這個測試,由于c++ 中需要支持函數(shù)重載,所以cc++ 中對同一個函數(shù)經(jīng)過編譯后生成的函數(shù)名是不相同的,這就導(dǎo)致了一個問題,如果在c++ 中調(diào)用一個使用c語言編寫模塊中的某個函數(shù),那么c++ 是根據(jù)c++ 的名稱修飾方式來查找并鏈接這個函數(shù),那么就會發(fā)生鏈接錯誤,以上例,c++ 中調(diào)用MyFunc函數(shù),在鏈接階段會去找Z6Myfuncv,結(jié)果是沒有找到的,因為這個MyFunc函數(shù)是c語言編寫的,生成的符號是MyFunc。

那么如果我想在c++ 調(diào)用c的函數(shù)怎么辦?

extern "C"的主要作用就是為了實現(xiàn)c++ 代碼能夠調(diào)用其他c語言代碼。加上extern "C"后,這部分代碼編譯器按c語言的方式進行編譯和鏈接,而不是按c++ 的方式。

MyModule.h
#ifndef MYMODULE_H
#define MYMODULE_H

#include<stdio.h>

#if __cplusplus
extern "C"{
#endif

    void func1();
    int func2(int a,int b);

#if __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;
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容