1. 程式人生 > 其它 >第三節 巨集、引用和行內函數

第三節 巨集、引用和行內函數

#include <iostream>
using namespace std;

/****************************************************************************************************
 * 11.在C++中儘量使用const替換巨集(#define):#define MAX 1024,替換為:const int max=1024
 *     ① const有型別,可進行編譯器型別安全檢查,使用時呼叫的是short型別的函式;
 *     ② #define無型別,不可進行型別檢查,使用時呼叫的是int型別過載的函式;
 *     ② const有作用域,可用於頂一個指定作用域下的有效常量,而#define不重視作用域,預設定義處到檔案結尾或到
 * #undef A(解除安裝巨集常量)處可用;
 *     ③ 巨集可以在名稱空間中定義,但是它不屬於該名稱空間,const屬於名稱空間中的元素。
 ****************************************************************************************************/
#define PARAM 128
const short param = 128;
void func(short a){
    cout << "short!" << endl;
}
void func(int a){    // 函式的過載,即對上面同一個函式的重新定義,增加其函式功能
    cout << "int" << endl;
}
void test06()
{
    func(PARAM);    // 輸出結果為:int,表示巨集呼叫的是int型別的過載函式
    func(param);    // 輸出結果為:short,表示巨集呼叫的是short型別的過載函式
}

/****************************************************************************************************
 * 12.引用(reference),C++中能用引用絕不用指標,它是C++給函式傳遞地址的途徑,可以簡單理解為給已有的變數取一個別名。
 *     ① &別名:表示引用,&在此不是求地址運算,而是起標識作用;
 *     ② 給某個變數取別名,就定義某個變數,即操作別名就相當於操作原;
 *     ③ 從上到下替換,之後操作別名就相當於操作變數本身;
 *     ④ 一個變數可以有多個引用,引用一旦初始化之後,就不能修改。
 *     ⑤ 不能給“陣列”取別名,但是有方法給陣列取別名:
 *         i> 用小括號提高優先順序取別名;
 *         ii> 用typedef給陣列型別取個別名;
 *     ⑥ 其實引用的本質是常量指標:Type& ref = val; // Type* const ref = &val;內部實現使用者不可見
 ****************************************************************************************************/
void test07(){
    int a = 10;
    int& b = a;    // 給變數 a 取一個別名 b
    // int *b = &a;    // 這是取地址,上面是取別名,即引用變數
    cout << "a:" << a << endl;    // 10
    cout << "b:" << b << endl;    // 10
    cout << "------------" << endl;
    //操作 b 就相當於操作 a 本身
    b = 100;
    cout << "a:" << a << endl;    // 100
    cout << "b:" << b << endl;    // 100
    cout << "------------" << endl;
    //一個變數可以有 n 個別名
    int& c = a;
    c = 200;
    cout << "a:" << a << endl;    // 200
    cout << "b:" << b << endl;    // 200
    cout << "c:" << c << endl;    // 200
    cout << "------------" << endl;
    //下面是取地址符,a,b,c的地址都是相同的,而變數之間的賦值,其地址是不同的。
    cout << "a:" << &a << endl;    // 0x61feb4
    cout << "b:" << &b << endl;    // 0x61feb4
    cout << "c:" << &c << endl << endl;    // 0x61feb4

    /*** 給陣列取別名 ***/
    int veryLongNameArray[5] = {11, 22, 33, 44, 55};

    // 方法i> 用小括號提高優先順序取別名;
    int (&sArr)[5] = veryLongNameArray;    // 因為&引用識別符號的優先順序<中括號,所以要加個小括號

    // 方法ii> 用typedef給陣列型別取個別名;
    typedef int TYPE_ARR[5];    // 先定義一個數組型別,(TYPE_ARR就是一張陣列型別,其中有5個int型別的元素)
    TYPE_ARR &new_arr = veryLongNameArray;    // 再用該資料型別繼承或者賦值veryLongNameArray的值

    for(int i=0;i<5;i++){
        cout << sArr[i] << " ";
        cout << new_arr[i] << "| ";
    }
    cout << endl;
}

/****************************************************************************************************
 * 13.引用(reference)在函式中的使用,引用作為函式的引數使用:
 *     ① 函式內部修改函式外部的值要麼使用地址,要麼使用引用(推薦);
 *     ② 通過引用引數產生的效果同按地址傳遞是一樣的。
 *     ③ 引用的語法更清楚簡單:
 *         1) 函式呼叫時傳遞的實參不必加“&”符
 *         2) 在被呼叫函式中不必在引數前加“*”符。
 *     ④ 引用作為其它變數的別名而存在,因此在一些場合可以代替指標。
 ****************************************************************************************************/
void mySwap1(int a, int b)    // 一個簡單的交換函式,但是這種交換隻在該方法內起作用,對外不起作用
{
    int temp = a;
    a = b;
    b = temp;
}
void mySwap2(int *a, int *b)    // a=&data,b=&data2,因為交換的是地址裡的內容,所以對外起作用
{
    int temp = *a;
    *a = *b;
    *b = temp;
}
void mySwap3(int &a, int &b)    // 給引數取別名
{
    int temp = a;
    a = b;
    b = temp;
}
void test08()
{
    int data1 = 10, data2 = 20;
    cout << "原始資料:data1 = " << data1 << ", data2 = " << data2 << endl;    // 原始資料
//    mySwap1(data1, data2);
//    cout << "第一種交換:data1 = " << data1 << ", data2 = " << data2 << endl;    // 第一種值傳遞,交換失敗
//    mySwap2(&data1, &data2);
//    cout << "第二種交換:data1 = " << data1 << ", data2 = " << data2 << endl;    // 第二種地址傳遞:交換成功
    mySwap3(data1, data2);
    cout << "第三種交換:data1 = " << data1 << ", data2 = " << data2 << endl;    // 第三種引用傳遞:交換成功
}

/****************************************************************************************************
 * 14.引用作為函式的返回值型別使用:
 *     ① 不能返回區域性變數的引用,因為區域性變數在函式結束時,記憶體內容將被釋放,引用無效,所以返回空,但是當不使用引用時
 * 區域性變數的值可以被返回;
 *     ② 可以返回靜態變數的引用,因為靜態變數的生命週期較長,所以引用有效。但是靜態變數只會作用1次,所以第二次呼叫靜
 * 態變數時,就不起作用了;
 *     ③ 函式返回,優先返回該變數的值,但是當函式作為左值被賦值時,返回的是引用(當然,如果函式返回值型別不是引用,那
 * 麼函式定義不合法,報錯)而不是值;所以作為右值,返回的自然就是值。
 ****************************************************************************************************/
//返回區域性變數引用
int& func01(){
    int a = 10; //區域性變數
    return a;
}
//返回靜態變數引用
int& func02(){
    static int a = 20;
    cout << "static int a : " << a << endl;
    return a;
}
void test09()
{
//    //不能返回區域性變數的引用
//    int& ret01 = func01();    // 現在ret01是函式返回值a的別名
//    cout << "func01: ret01 = " << ret01 << endl;    // 函式結束區域性變數被釋放,所以返回空

    cout << func02() << endl;    // 列印輸出20,函式內的cout,和該行的cout輸出結果一樣
    // 如果函式做左值,那麼必須返回引用,如下
    func02() = 100;    //列印輸出20,為函式內的cout輸出;此句返回的是a,即此句就相當於a=100
    cout << func02() << endl;     // 列印輸出100 100,因為第二次呼叫靜態變數的定義不起作用,所以函式內列印輸出為100,此句列印輸出也是100
}

/****************************************************************************************************
 * 15.常左值引用,即給左值取一個別名:左值一般為變數名a=2,a即為左值,2即為右值。
 *     ① 定義格式:const Type& ref = val;
 *     ② 字面量不能賦給引用,但是可以賦給 const 引用 const 修飾的引用,不能修改;
 *     ③ 將函式的形參定義為常量引用的好處: 引用不產生新的變數,減少形參與實參傳遞時的開銷。
 *     ④ 由於引用可能導致實參隨形參改變而改變,將其定義為常量引可以消除這種副作用。如果希望實參隨著形參的改變而改變,
 * 那麼使用一般的引用,如果不希望實參隨著形參改變,那麼使用常引用。
 * 16.常右值引用,即給右值取一個別名:好處是該引用將不能被修改。
 ****************************************************************************************************/
typedef struct {
    int num;
    char name[16];
} STU;
void printSTU01(STU tmp)    // 普通結構體變數作為形參,開銷太大,使用引用減小開銷
{
    cout << "姓名: " << tmp.name << ", 年齡: " << tmp.num <<"; 佔用空間大小:"<< sizeof (tmp) << endl;
    // 姓名: Lucy, 年齡: 16; 佔用空間大小:20,其中20位元組=name16位元組+num4位元組,此時形參tmp開闢了儲存空間。

}
void printSTU02(STU &tmp)    // 使用引用減小開銷,此時傳入的引數是變數lucy,即“STU &tmp = lucy”,此時tmp只是lucy的別名
{
    tmp.num = 80;    // 內容可被修改
    cout << "姓名: " << tmp.name << ", 年齡: " << tmp.num <<"; 佔用空間大小:"<< sizeof (tmp) << endl;
    // 姓名: Lucy, 年齡: 80; 佔用空間大小:20,其中20位元組=name16位元組+num4位元組,但是此時別名tmp並沒有開闢儲存空間。
}
void printSTU03(const STU &tmp)    // 常引用,使其傳入內容不可修改
{
    // tmp.num = 80;    // 此句報錯,說明不可被修改
    cout << "姓名: " << tmp.name << ", 年齡: " << tmp.num <<"; 佔用空間大小:"<< sizeof (tmp) << endl;
    // 姓名: Lucy, 年齡: 16; 佔用空間大小:20,其中20位元組=name16位元組+num4位元組,但是此時別名tmp並沒有開闢儲存空間。
}
void test10()
{
    STU lucy = {16, "Lucy"};
    printSTU01(lucy);
    printSTU02(lucy);
    printSTU03(lucy);

    const int &num = 10;    // 按照C++的語法,此處的10的型別為const int,而非int,所以此句簽名使用的是const int
    cout << "num = " << num << endl;
}

/****************************************************************************************************
 * 17.指標引用:
 *     ① 好處是傳參時,不需要取地址,方便操作;
 ****************************************************************************************************/
#include<stdlib.h>
#include<string.h>
// 在C中使用指標的的實現方法
void myStr1(char **p_str)    // 形參p_str == &str,*p_str == *&str ==str
{
    *p_str = (char *)calloc(1, 32);    // 申請空間用於寫入內容
    strcpy(*p_str, "Hello World!");    // 寫入內容
}
//在C++中使用引用實現該方法,指標的引用
void myStr2(char* &r_str)    // char* &r_str == str,即r_str == str
{
    r_str = (char *)calloc(1, 32);
    strcpy(r_str, "Hello World!");
}
void test11()
{
    // 需求:封裝一個函式,從堆區給str申請一個空間並賦值為“Hello World!”
    char *str = NULL;
//    myStr1(&str);    // 在C中使用指標就傳入地址,並往其地址內寫入內容
    myStr2(str);    // 在C++中使用引用就直接傳入變數名即可,
    cout << "str = " << str << endl;
    free(str);    // 釋放堆區
}

/****************************************************************************************************
 * 18.巨集函式:C/C++的重要特徵是效率,而C++的效率>C。在C中常把一些短並且執行頻繁的計算寫成巨集,而不是函式,理由是為了執
 * 行效率,巨集可以避免函式呼叫的開銷,這些都由預處理來完成。但是在C++出現之後,使用預處理巨集函式會出現兩個問題:
 *     ① 巨集看起來像一個函式呼叫,但是會有隱藏一些難以發現的錯誤,C中也會出現的問題。
 *     ② 前處理器不允許訪問類的成員,即前處理器巨集不能用作類的成員函式,C++中特有的問題。
 * 與行內函數相比巨集函式還存在以下問題:
 *     ① 不具備完整性;
 *     ② 當傳入引數中有其他運算時,巨集函式的執行,會夾帶著呼叫函式中的運算如++運算進行執行;
 *     ③ 沒有作用域的概念,無法作為一個類的成員函式,也就是說預定義巨集沒有辦法表示類的範圍。
 * 19.行內函數(inline function):為了保持預處理巨集的效率又增加安全性,而且還能像一般成員函式那樣可以在類裡訪問自如,
 * C++引入了行內函數。它繼承巨集函式的效率(既沒有函式呼叫時開銷,又可以像普通函式那樣,進行引數、返回值型別的安全檢查,
 * 還可以作為成員函式。)
 *     ① 在C++中,預定義巨集的概念是用行內函數來實現的,而行內函數本身也是一個真正的函式。行內函數具有普通函式的所有行為;
 *     ② 行內函數和預定義巨集函式的唯一不同之處在於行內函數會在適當的地方像預定義巨集一樣展開,所以不需要函式呼叫的開銷,所
 * 以要儘量使用行內函數而非巨集;
 *     ③ 在普通函式(非成員函式)函式前面加上 inline 關鍵字即為行內函數。注意:函式體和宣告必須結合在一起,否則編譯器會
 * 將它作為普通函式來對待:
 *         inline void func(int a);    // 以上寫法沒有任何效果,僅僅是宣告函式
 *         inline int func(int a){return ++;}    // 行內函數的正確寫法
 *     ④ 在類內部定義行內函數時,inline修飾並不是必須的,因為任何在類內部定義的函式自動成為行內函數。
 *     注意: 編譯器將會檢查函式引數列表使用是否正確,並返回值(進行必要的轉換)。這些事前處理器無法完成的。行內函數的確佔
 * 用空間,但是行內函數相對於普通函式的優勢只是省去了函式呼叫時候的壓棧,跳轉,返回的開銷。我們可以理解為行內函數是以空間
 * 換時間。但是C++內聯編譯會有一些限制,以下情況編譯器可能考慮不會將函式進行內聯編譯:
 *     ① 不能存在任何形式的迴圈語句;
 *     ② 不能存在過多的條件判斷語句;
 *     ③ 函式體不能過於龐大;
 *     ④ 不能對函式進行取址操作。
 ****************************************************************************************************/
// 巨集函式的缺陷一:不具備完整性
#define ADD1(x, y) x+y    // 巨集函式,實現兩個變數的相加
#define ADD2(x, y) (x+y)    // 巨集函式,實現兩個變數的相加,巨集函式不具完整性的解決辦法
inline int add(int x, int y){return x+y;}    // 行內函數實現兩個變數的相加
void test12()
{
    int ret1 = ADD1(10, 20) * 10; //希望結果是 300
    int ret2 = ADD2(10, 20) * 10; //希望結果是 300
    int ret3 = add(10, 20) * 10; //希望結果是 300
    cout << "ret1:" << ret1 << endl; //但是巨集函式結果為210,錯誤,10+20*10=210
    cout << "ret2:" << ret2 << endl; //巨集函式不具完整性的解決辦法:結果為300,正確,(10+20)*10=300
    cout << "ret3:" << ret3 << endl; //行內函數結果為300,正確,(10+20)*10=300
}
// 巨集函式的缺陷二:用小括號是為了保證完整性,但是又出現了巨集函式中還執行了呼叫函式中的運算如++運算
#define COMPARE(x, y)((x)<(y)?(x):(y))    // 巨集函式,實現兩個變數的比較大小,並返回較小的值
inline int compare1(int x, int y){return x<y?x:y;}    // 行內函數實現兩個變數的比較大小,並返回較小的值
int compare2(int x, int y){return x<y?x:y;}    // 普通函式實現兩個變數的比較大小,並返回較小的值
void test13()
{
    int a = 1;
    int b = 3;
    // 比較++a和b的大小,取較小的值,我們想得到的結果是2
//    cout << "COMPARE(++a, b): " << COMPARE(++a, b) << endl;
    // 3×,因為傳入值為(++1,3),巨集函式中(++1=2)<(y)?(++2=3):(y)所以返回值為3
//    cout << "compare1(++a, b): " << compare1(++a, b) << endl;
    // 2√,只在傳參前執行++,所以直接傳入(2, 3)進行比較
    cout << "compare2(++a, b): " << compare2(++a, b) << endl;
    // 2√,只在傳參前執行++,所以直接傳入(2, 3)進行比較
}

/****************************************************************************************************
 * 20.函式的預設引數: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){cout << "a + b = " << a + b << endl;}    // 函式的定義

void test14(){
    //1. 沒有傳引數,那麼使用預設引數
    TestFunc01();
    //2. 傳一個引數,那麼第二個引數使用預設引數
    TestFunc01(100);
    //3. 傳入兩個引數,那麼兩個引數都使用我們傳入的引數
    TestFunc01(100, 200);
}

int main()
{
    // test06();
    // test07();
    // test08();
    // test09();
    // test10();
    // test11();
    // test12();
    // test13();
    test14();
    return 0;
}