文章目錄
- 一、引用
- 引用的概念和定義
- 引用的功能
- 引用的特性
- const引用
- const用法回顧
- 權限的放大縮小
- const引用的功能
- 指針和引用的關系
- 二、內聯函數
- 三、nullptr
- 補充
- 結構體指針變量類型重定義
一、引用
引用的概念和定義
C++祖師爺為了優化在部分場景中使用指針會出現的效率較低和比較復雜的情況,引入了一個新概念——引用。
引用不是新定義一個變量,而是為已經存在的變量取別名,編譯器不會為引用變量開辟空間,它和它引用的變量共用一塊空間,通俗來說就是為一個變量取別名,雖然叫法不同,變量的不同的別名還是指代這個變量本身,也就是你的正式名字和你的外號都是你本身,引用用法如下:
類型& 引用別名 = 引用的對象;
#include <iostream>
using namespace std;
int main()
{int a = 0;//引用:b和c是a的別名int& b = a;int& c = a;//也可以為別名b取別名,d相當于還是a的別名int& d = b;//這里我們可以看到abcd的地址都是一樣的cout << &a << endl;cout << &b << endl;cout << &c << endl;cout << &d << endl;return 0;
}
下面代碼不是讓d變成x的別名,只是賦值修改d指向的這塊空間的值。
引用的功能
一、
其實引用在功能上和指針是有部分重疊的,至于怎么個重疊法呢,跟隨小編的腳步一起來看看吧。
在C語言階段我們想要實現一個交換變量值的函數時是需要傳變量的地址的,這樣形參的改變才會影響實參,現在我們了解了引用后其實就可以把它用上了,如果我們傳引用調用函數,那么函數的形參名就是實參的別名,形參發生了什么變化實參也會跟著變。
補充:變量引用的生命周期一定和變量本身一樣或者比變量本身小,比如下面x是a的引用,x出了swap這個函數后就銷毀了,但是a還存在。
void swap(int* x, int* y)
{int tmp = *x;*x = *y;*y = tmp;
}void swap(int& x, int& y)
{int tmp = x;x = y;y = tmp;
}int main()
{int a = 10;int b = 20;swap(&a, &b);cout << a << " " << b << endl;swap(a, b);cout << a << " " << b << endl;return 0;
}
這里我們可以總結出引用的第一個功能:
引用做函數形參,修改形參影響實參
二、
在調用函數傳參的時候當參數很大時,比如下面這個結構體變量A,如果傳參數本身會把這個參數拷貝過去(因為形參是實參的臨時拷貝),這樣空間開銷就太大了,所以我們通常會傳它的指針,指針最大也就8個字節。我們想想,引用在這里依舊可以實現同樣的功能,前面介紹到引用是不會為變量開辟空間的,所以這里也可以采用傳引用的方式。
struct A
{int a[100];int b;
};void Func(struct A* a)
{ }void Func(struct A& aa)
{ }int main()
{struct A a;Func(&a);Func(a);
}
引用做函數形參,減少拷貝,提高效率
三、
引用不僅可以作為函數形參,還可作為函數返回值返回,在介紹引用做函數返回值有哪些功能之前,還請允許小編科普一些有關函數返回值的知識點。
上面小編實現了一個簡單的傳值返回,其中變量ret在出了Func這個函數作用域后就銷毀了,所以我們可以確定x接受的返回值一定不會是ret本身。這里返回的其實是ret的一份臨時拷貝變量,并且語法規定了這個臨時變量是具有常性的,這里可以簡單理解成它被const修飾過,并且語法規定就算返回值出了作用域還存在它返回的也是臨時變量,因為編譯器不會去識別返回值出作用域是否銷毀。
有了上面的鋪墊過后接下來小編就要介紹今天的主角了。
typedef struct SeqList
{int* arr;int size;int capacity;
}SL;void SLInit(SL& sl, int n = 4)
{sl.arr = (int*)malloc(n * sizeof(SL));sl.size = 0;sl.capacity = n;
}void SLPushBack(SL& sl, int x)
{sl.arr[sl.size] = x;sl.size++;
}//該函數表示返回順序表第i個位置的數據
int& SLAt(SL& sl, int i)
{//斷言檢查i是否越界assert(i < sl.size);return sl.arr[i];
}int main()
{SL s;SLInit(s);SLPushBack(s, 1);SLPushBack(s, 2);SLPushBack(s, 3);SLPushBack(s, 4);for (int i = 0; i < s.size; i++){SLAt(s, i) += 1;}for (int i = 0; i < s.size; i++){cout << SLAt(s, i) << endl;}return 0;
}
上面是小編實現的一個簡單順序表,我們可以看到順序表尾插了四個數字1234,SLAt函數實現的是返回順序表第i個位置的數據,如果我們想要在主函數中修改該函數的返回值該怎么做呢?讀者朋友應該可以發現SLAt的返回類型是int&,這不就是才介紹的引用嗎?沒錯,這就是傳引用返回。
上圖我們可以直觀看到傳值返回和傳引用返回的區別,傳值返回產生了臨時變量,所以修改臨時變量不會改變返回值,直觀來講就是順序表的值不會發生改變(并且這種操作會編譯報錯,因為臨時變量具有常性,具有常性的變量無法被修改),但是傳引用返回就不一樣了,它返回的是返回值的別名,所以不會產生臨時變量,編譯就不會報錯,我們可以簡單理解別名就是它本身,所以修改返回值的別名就是修改它自己,所以我們可以看到順序表的值確實被修改了。
注意:傳引用返回只適用于返回對象出了函數作用域還存在的情況,例如上面這個例子返回值對象是在堆上申請的。
這里我們可以類比引用的前兩個功能,總結出另外兩個功能:
引用做函數返回類型,修改返回對象
引用做函數返回類型,減少拷貝,提高效率
引用的特性
- 引用在定義時必須初始化,變量和指針都可以先定義后賦值。
//變量可以先初始化再賦值int x;x = 10;//引用不能先初始化再賦值int& r;r = x;//錯誤用法
- 一個變量可以有多個引用
//變量x可以有多個引用int x = 10;int& a = x;int& b = x;int& c = x;
- 引用一但引用一個實體,就不能引用其他實體。
就是平時所說的引用不能改變指向。(可以形象理解引用是非常忠貞的,一個別名只能對應一個對象)
所以引用是不能完全替代指針的,有些情況比如數據結構中的鏈式結構或者樹形結構我們有修改指向的需求,所以這類情況只能使用指針,指針可以修改指向。
const引用
const用法回顧
在介紹const引用之前,我們先回顧一下C語言階段我們掌握的有關const的用法:
- const修飾變量:
- const修飾的變量無法直接修改,但是可以通過它的指針繞過他,使用它的地址修改,雖然這樣做打破了語法的規則。
//const修飾變量const int n = 10;n = 20; //無法實現int* pn = &n;*pn = 20; //可以實現,但是破壞了語法規則
- const修飾指針變量:
1、const在*左邊,修飾指針指向的對象,保證指針指向的對象本身無法通過指針修改,但是指針變量本身可變。
//const在*左邊,修飾指針指向的內容,const int* p = &n;*p = m; //無法實現p = &m; //可以實現
2、const在*右邊,修飾指針變量本身,保證指針變量本身無法被修改,但是指針指向的對象可以通過指針改變。
//const在*左邊,修飾指針變量本身int* const p = &n;*p = m; //可以實現p = &m; //無法實現
權限的放大縮小
const int a = 10;//權限不能放大int& r1 = a;//權限可以平移const int& r2 = a;int b = 20;//權限可以縮小const int& r3 = b;
上面我們定義了一個const修飾的int類型變量a,它具有常性無法被修改。下面我們用沒被const修飾的r1做a的別名,編譯器會報錯,因為a本身不能被修改,r1為a的別名卻沒有任何限制,可以隨意修改,這里就會涉及到權限的放大。但是權限是可以平移和縮小的,下面兩種引用編譯器都不會報錯。
注意:指針和引用才會涉及權限的放大和縮小,因為指針和引用對象的改變會影響原對象。
const引用的功能
int a = 10;double d = 1.1;//以下兩種情況都是引用的臨時對象const int& r1 = d; //1const int& r2 = a * 10; //2
我們先看第一個引用,double類型的d被const修飾的int類型的r1引用,這里會發生隱式類型轉換(C語言規定相關類型可以進行隱式轉換),這里C++語法還規定類型轉換會產生臨時對象,這里r1引用的其實是那個臨時對象,臨時對象是是編譯器需要一個空間來暫存表達式的求值結果或者隱式轉化時產生的中間對象時臨時創建的一個未命名對象,它的特點是具有常性,不能修改。
所以這里r1需要加const修飾,防止權限的放大。
有了上面臨時對象的概念后第二個引用為什么要加const就很好理解了,直接上圖。
總結:C++引入const引用的目的就是方便傳參,后續函數形參部分若用const引用來接受,即可適配大部分傳參的情況。
指針和引用的關系
在C++中,引用和指針功能有重疊性,它們各自有各自的特點,在實踐中相輔相成,互相不可替代,所以C++從某種程度上會比C語言更便捷,能實現更多功能。
1、指針存儲一個變量地址需要開空間,引用是為一個變量取別名不用開空間。
2、引用在定義時必須初始化,指針可以先定義再初始化。
3、指針可以更改指向的對象,引用一旦引用一個實體后就不能再引用其他對象。
4、引用可以直接訪問指向對象,而指針必須解引用后訪問指向對象。
5、sizeof中含義不同,引用結果為引用類型大小,而指針始終是地址空間所占字節數(32位為4字節,64位為8字節)。
6、指針容易出現空指針和野指針的問題,引用很少出現,所以引用更安全一些。
7、在指令匯編角度,引用也是用指針實現的,但是一般我們都以語法層面為準。
二、內聯函數
1、我們在C語言階段學習的宏函數會在預處理時替換展開,但是宏函數是很容易出錯的,比如要注意運算符優先級,并且不能調試,但是函數會自動處理運算符優先級所以C++設計內聯函數就是為了替代C語言的宏函數。
2、內聯函數的關鍵字是inline,編譯時C++編譯器會在調用的地方展開內聯函數,這樣調用內聯函數就不會創建棧幀了,提高了時間效率(創建棧幀消耗時間),這里我給大家演示一下普通函數和內聯函數在匯編指令層面的區別:
inline int Add(int x, int y)
{int ret = x + y;return ret;
}
int main()
{int ret = Add(1, 2);cout << ret << endl;return 0;
}
不是內聯:
是內聯:
3、既然內聯函數有這么多優點,那么我們就可以無腦用它了嗎?肯定不行,因為內聯函數調用過多會造成程序變大,會占用很多空間,比如內聯函數轉化為指令有50行,調用10000次就是500000行,我們不內聯的話函數就一直待在一塊公共空間,每一次調用的時候會call指令跳轉到函數所在位置,那么就是程序指令一共就是10000+50行。世界上沒有什么是完美的,內聯函數可以簡單理解成是用空間換時間。
4、inline對于編譯器而言只是一個建議,也就是說就算你加了inline編譯器也可以在調用的地方不展開,比如說遞歸函數或者代碼相對多一點的函數加了inline也會被編譯器忽略,所以短小并且需要頻繁調用的函數適合用內聯。5、內聯函數不能聲明和定義分離,因為內聯函數會直接展開,所以在編譯階段不會進符號表,那么在另一個文件調用內聯函數時,因為文件里只有內聯函數的聲明,(聲明只有無效地址,只有在函數定義才會分配確切的地址)所以在編譯階段找不到函數的地址,那么編譯器只有寄希望于鏈接的時候合并符號表時得到函數的地址,但是內聯函數不會進符號表,所以編譯器就會報鏈接錯誤。
所以內聯函數的定義和聲明只能在一起,簡單理解就是內聯函數只能在有它的定義的文件里展開,若內聯函數定義在頭文件里,其他文件包了這個頭文件,調用這個內聯函數就可以展開。6、普通函數必須聲明和定義分離,如果普通函數定義在頭文件里,那么如果有兩個文件都包含了這個頭文件,最后兩個文件編譯后生成的目標文件都有這個函數的確切地址,后面鏈接這兩個目標文件時兩個文件的符號表都有這個函數的確切地址,就會鏈接報錯。
與前面知識的聯系:前面我們知道有static關鍵字,它修飾全局變量和函數后使得它們之只能在當前源文件里使用,之前我們的解釋是static讓它的外部鏈接屬性變成了內部鏈接屬性,其實本質就是讓他不進符號表,和inline類似。
三、nullptr
NULL實際上是一個宏,在傳統C頭文件中,可以看到如下代碼:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
C++中NULL被定義為字面常量0,C中被定義為無類型指針(void*)的常量,不論采用何種定義,在使用空值的指針時,都不可避免會遇到一些麻煩,比如下面:
本來想通過f(NULL)調用指針版本的Func(int*)函數,但是由于NULL被定義成0,調用了f(int x),因此與程序初衷相悖。
所以C++11中引?nullptr,nullptr是?個特殊的關鍵字,nullptr是?種特殊類型的字面量,它可以轉換成任意其他類型的指針類型。使?nullptr定義空指針可以避免類型轉換的問題,因為為nullptr只能被隱式地轉換為指針類型,而不能被轉換為整數類型。
補充
結構體指針變量類型重定義
下面代碼上面的寫法等價于下面
typedef struct ListNode
{int val;struct ListNode* next;
}ListNode, *pnode;typedef struct ListNode ListNode;
typedef struct ListNode* *pnode;
以上就是小編分享的全部內容了,如果覺得不錯還請留下免費的贊和收藏
如果有建議歡迎通過評論區或私信留言,感謝您的大力支持。
一鍵三連好運連連哦~~