指針和引用區別,引用可以用常指針實現嗎?

講述一下指針和引用這兩個複合類型的使用,以及區別。然後再回答這一個問題,引用可以用常指針實現嗎?首先我們先了解一下什麼是指針,函數指針以及智能指針,管理資源對象的指針(shared_ptr,unique_ptr,weak_ptr,maked_ptr)。因篇幅有限,系列會持續講。其次我們了解一下引用包含的左值引用,右值引用,引用摺疊,常量引用以及引用的基本概念。

指針與引用 相愛相殺相似

C++語言中存放及使用內存地址是通過指針和引用完成的。而對象就是位於某個地址中。 * 什麼是指針?

a. 指針是一個對象,允許對指針進行賦值和拷貝,而且在指針的生命周期中,指針可以先後指向不同的對象。

首先我們來看一段代碼

int *ptr;
int val = 42;
ptr = &val;

這就是指針最基本的用法,反映了什麼呢?首先我們定義了一個指針ptr,它是一個對象,我們允許對其進行賦值,而這裡賦的值是val的地址,先說明一下&這個符號就是用來獲取某個對象的地址的,取名叫 取地址符

那麼我們皮一下,將一個常量賦值給ptr

int *ptr;
int val = 42;
ptr = val; // ?

這裡是會報錯的,為什麼呢?首先我們說明了指針是一個對象,那麼,這個對象必須要有一個類型,指針的類型就決定了我們能對指針所指的對象進行哪些操作。這裡的指針的類型就是int *,它是一個對象,它是一個指向int型的對象。

所以當我們給ptr賦值int類型的時候,就會報錯。那麼如果是一個對象,那需不需要給他賦初值呢?不需要,因為如果在其作用域中沒有賦予初值,將會有一個不確定的值。那麼,對一個指針最基本的操作,就是解引用,也是用*獲取指針所指的值。這個操作也叫做間接取值(indirection)(霸氣的英文,印地略順)。解引用獲取到相應類型的值說到類型,那麼我們繼續來看看void*這個指向未知類型。

void*

這個怎麼用呢?偶爾需要不知道對象確切類型的情況下,僅通過對象在內存中的地址存儲或傳遞對象。

int *ptr;
int val = 42;
void *pi;
void *pi1;
pi = ptr; // 這裡發生了隱式轉換

那我們能使用pi嗎?並不行。其實兩個void*指針能比較是不是相等,因為都是存了內存地址的,但是事實上,編譯器並不清楚void*所指的對象到底是什麼類型的,所以對它執行其他操作可能不太安全並且會引發編譯器錯誤。如果要使用還得把它顯式地轉換成某一特定類型的指針

int *piv = static_cast<int*>(pi);

這樣就可以使用piv的值。強制類型轉換將一個指針的類型轉換成一種與實際所指對象類型完全不同的新類型,則使用轉換後的指針是不安全的行為。

使用轉換後的指針是不安全的,因為例如

double *pd1 = static_cast<double*>(pi); // 可以但是不安全

可能int與double分配內存的方式不一樣,就會造成不安全的後果。

那麼我們平常見的的數組中的數組名可以看成是指向數組的首元素的指針是怎麼回事呢?

數組中的指針

首先,數組的空間申請,就是申請一塊連續的空間。那麼首元素就是指針,指針加上n*sizeof(data)就是數組的元素的位置。

char carr[] = {a,b,c};
char *p = carr; // char[] 類型到char *隱式轉換。

函數指針

函數指針和指向類成員的指針不能被賦為void*。

前面說過,指針是一個對象,用以存儲內存地址。那麼我們可以看到函數題生成的代碼也是置於某塊內存區域中,因此它也有自己的地址。既然指針可以指向一個對象,存放對象的內存地址,當然也可以讓指針指向函數。對於函數,我們只能調用它或者獲取它的地址來使用。

void errorf(string s){
cout << s << endl;
}
void (*efct)(string);
int main(){
efct = &errorf; // 通過函數名獲取地址
efct("runtime_error"); //
}

經典的用法! 編譯器發現efct是一個函數指針,而且參數類型聲明相同,函數類型精確匹配,然後就調用函數。

void (*f1)(string) = &errorf;// <=> =errorf
void (*f2)(string) = errorf; // <=> =&errorf
int main(){
f1("error");
(*f1)("error");// 上下表達式等價
}

所以講到函數指針之後,我們可以開始將一下關於引用的用法

什麼是引用?

講引用必須包括左值引用,右值引用,摺疊引用、常量引用以及普通引用的知識點其實指針的使用就是為了避免拷貝構造,拷貝賦值等默認操作這些高代價的操作,而傳遞大量的數據。其實引用也是為了降低代價來傳遞大量的數據的。它的作用就是作為對象的別名存放對象的機器地址。那麼引用和指針有什麼區別嗎? 指針是通過解引用(*) 來訪問對象本身的,而訪問引用跟訪問對象本身從語法上看是一樣一樣的。 指針可以在其生命周期指向不同的對象(但必須是同類型),而引用所引的永遠是一開始初始化那個對象。 指針可以為空 nullptr,而引用不能為空引用,引用一定會對應著某個對象。所以總結來說,引用實際上是對象的另外一個名字。

我們先看一段簡單的代碼,待會再看一段嚇人的。

int i = 42;
int &r(i);
++r;
int *ptr = &r; // ptr指向i

沒錯,就只有四行,很好,可以看出,引用(&)就是只是i的別名,操作r就等於操作i;i=43,這裡充分說明了,引用存放的是對象的機器地址,操作r就等於操作i

我們在這裡也可以看出,我們不能令某個指針指向引用,也不能定義引用的數組,所以,其實區別於指針,引用他不是一個對象。每次使用引用,就好像對該指針執行解引用操作。再來看一段複雜的

template<typename T>
class Vector{
T *elem;
public:
T& operator[](int){ return elem[i];} // 返回元素的引用
const T& operator[](int i)const { return elem[i];} // 返回常量元素的引用
void push_back(const T& a); // 通過引用傳入待添加的元素
};

void func(const Vector<double> &v){
double b1 = v[1]; // 把operator[](1)所引的double值拷貝給b1
...
}

很複雜吧!這裡面所有都是左值引用,說到左值引用,我們可以先看看以下定義 左值引用 引用我們希望改變值的對象 const引用 引用那些我們不希望改變值的對象 * 右值引用 所引對象的值在我們使用之後就無須保留了(比如臨時變數)。

上面說得前兩種都是左值引用,一般來說(左值持久,右值短暫)。上面持續用到T&,這個普通的T類型的引用,其實傳給這個T類型的引用是左值引用,因為它並不是傳了值,這個值就沒了,而是持續存在的,那麼請問const T&就可以不一定是個左值又是為什麼呢?先來看一段代碼

const double& cdr(1);

這段代碼是可以通過編譯並運行的,那為什麼這個1這個臨時變數可以在常量引用中存在,到底是為什麼呢? 1. 如果必要的話,先執行目標為T的隱式類型轉換 2. 所得的值置於一個T類型的臨時變數中 3. 把這個臨時變數作為初始值

所以這個臨時變數的生命周期從它創建開始,直到它的引用作用於結束為止。

所以我們經常會在函數中使用

void func(const T&,const T&);

這種傳參方式。

當然也可以返回一個引用類型

template<typename K,typename V>
class Map{
public:
V& operator[](const K&);
pair<K,V>* begin(){return &elem[0];}
pair<K,V>* end(){return &elem[0]+elem.size();}
private:
vector<pair<K,V> > elem;
};
template <class K,class V>
V& Map<K,V>::operator[](const K& k){
for(auto &x:elem)
if(k==x.first)
return x.second;
elem.push_back({K,V{}});
return elem.back().second; // 返回新元素的默認值
}

這裡的返回值類型是引用,因為用戶肯定是想改以下這個查找到的map值,而這個map的值就是一個引用而不是const引用,可以直接改動。

講完左,我們講以下右,這個短暫的引用。其實設計那麼多,就是為了支持對象的不同方法。* 右值引用對應一個臨時對象,用戶可以修改這個對象,並且認定這個對象以後不會被用到了。這裡推薦以下這篇文章,四行代碼講述右值引用。從四行代碼看右值引用所以讀完者篇文章,突然對右值引用又有了很深的一次了解。

在這裡我挑一段小的來講

Test& operator=(Test&& test){
cout << "move function"<< endl;
if(this==&test)
return *this;
//delete m_ptr;
m_ptr = test.m_ptr;
test.m_ptr = nullptr;
return *this;
}

這是移動賦值運算符。使用std::move就可以調用它

#include <iostream>

using namespace std;

/***
* 所以右值引用是獨立於左值和右值
* 以下是一個好例子
* 放入一個左值,那麼T&& t 就是一個左值
* 放入一個右值,那麼T&& t 就是一個右值
* 這裡發生了自動類型推斷
* 是發生在函數中的
* 類型中
* 並沒有類型的自動推斷
* 所以當我們使用移動構造函數的時候,需要使用的移動運算符std::move
*
* **/
void processValue(int &a){cout <<"lvalue"<<endl;}
void processValue(int &&a){cout << "rvalue"<<endl;}
template<typename T> void func(T&& val){
processValue(std::forward(val)); // FIXME:
}
int && i = 0;

class Test{
public:
Test():m_ptr(new int[0]){
cout << "construct" << endl;

}
Test(const Test& test):m_ptr(new int(*test.m_ptr)){
cout << "deep copy" <<endl;
}
Test(Test&& test):m_ptr(test.m_ptr){ // 這裡就一定是右值引用了
cout << "right move reference" << endl;
// delete test.m_ptr; ? 這裡不允許刪除一個右值
test.m_ptr = nullptr;
}
~Test(){
delete m_ptr;
cout << "delete"<<endl;
}
Test& operator=(const Test& test){ // 其實這裡使用了深拷貝
if(this==&test)
return *this;
delete m_ptr;
Test *temptest;
temptest->m_ptr = test.m_ptr;
m_ptr = temptest->m_ptr;
delete test.m_ptr;
cout << "assign to other" <<endl;
return *this;
}
Test& operator=(Test&& test){
cout << "move function"<< endl;
if(this==&test)
return *this;
//delete m_ptr;
m_ptr = test.m_ptr;
test.m_ptr = nullptr;
return *this;
}
private:
int *m_ptr;
};
Test getA(){
Test test;
return test; // 這是一個臨時量
}
int main(){
// cout << i << endl;
// int inum = 100;
// const int&& inumref = 100;
// func(inumref);
Test test;
Test movTest = std::move(getA()); // 調用移動構造函數 所以這裡不需要再構造一次,直接用移動構造
// test = movTest;
test = std::move(getA()); // 這裡
int i = 10;
func(i);
func(0);
return 0;
}

move並不執行什麼移動操作,他只是無條件地把左值轉換為右值,而forward也不做什麼轉發工作,只是有條件地將左值轉換為右值,或者將左值保留為左值。

那麼知道了move,我們來看看以下這段代碼,完美的swap

template<class T>
void swap(T& a,T& b){
T tmp(move(a)); // a中移出值
a = move(b); // b中移出值
b = move(tmp); // tmp中移出值
}

確實很完美,根本沒有浪費成本執行拷貝操作。

基本上講了大概的引用與指針,也說明了這兩個的區別,那我們來回答一下能不能使用常量指針來實現引用呢?

int *const p;// ? 必須要有初始化的對象
int i;
int &r = i;
int ii;
r = ii;

  • 常量指針,一開始必須要有初始化的對象,後面也不允許改變這個指針存放的內存值,也就是不能重新賦值。而引用卻是可以的,因為它不是一個對象,他只是一個別名,它的更改是直接更改於原來的對象的
  • 而且常量指針根本沒辦法改變,沒辦法改變自己的值,自己的內存地址
  • 傳參也不一樣,引用穿得參是可以改的,但是常量指針傳的就不能改,只能改變這個常量指針原來保存的值才能改。 通過這樣的區別,所以說,其實引用是不能用常量指針來實現的。問題是回答了,但是還是需要講一下智能指針的使用,weak_ptr和unique_ptr的使用。

未完待續...

推薦閱讀:

相关文章