C/C++中指針和引用之相關問題深入研究



一、基本知識
指針和引用的聲明方式:
聲明指針: char* pc;
聲明引用: char c = 'A'
   char& rc = c;

它們的區別:
①從現象上看,指針在運行時可以改變其所指向的值,而引用一旦和某個對象綁定後就不再改變。這句話可以理解爲:指針可以被重新賦值以指向另一個不同的對象。但是引用則總是指向在初始化時被指定的對象,以後不能改變,但是指定的對象其內容可以改變。

②從內存分配上看,程序爲指針變量分配內存區域,而不爲引用分配內存區域,因爲引用聲明時必須初始化,從而指向一個已經存在的對象。引用不能指向空值。

③從編譯上看,程序在編譯時分別將指針和引用添加到符號表上,符號表上記錄的是變量名及變量所對應地址。指針變量在符號表上對應的地址值爲指針變量的地址值,而引用在符號表上對應的地址值爲引用對象的地址值。符號表生成後就不會再改,因此指針可以改變指向的對象(指針變量中的值可以改),而引用對象不能改。這是使用指針不安全而使用引用安全的主要原因。從某種意義上來說引用可以被認爲是不能改變的指針。

④不存在指向空值的引用這個事實意味着使用引用的代碼效率比使用指針的要高。因爲在使用引用之前不需要測試它的合法性。相反,指針則應該總是被測試,防止其爲空。

⑤理論上,對於指針的級數沒有限制,但是引用只能是一級。

如下:

複製代碼 代碼如下:

  int** p1; // 合法。指向指針的指針
  int*& p2; // 合法。指向指針的引用
  int&* p3; // 非法。指向引用的指針是非法的
  int&& p4; // 非法。指向引用的引用是非法的

注意上述讀法是從左到右。

程序1:
複製代碼 代碼如下:

#include "stdio.h"
int main(void)
{
  // 聲明一個char型指針pc,且讓它指向空值
  char* pc = 0;
  char a = 'a';
  // 聲明一個引用rc,且讓它引用變量a
  char& rc = a;
  printf("%d, %c\n", pc, rc);

  char *pc2;
  // 聲明一個指針,但可以不初始化
  pc2 = pc;

  // char& rc2;
  // 上面語句編譯時,會產生如下錯誤:
  // error C2530: 'rc2' : references must be initialized
  // 即,應用必須初始化
  // rc = *pc;
  // 上面語句編譯不會有問題,但運行時,會報如下錯誤:
  // "0x00401057"指令引用的"0x00000000"內存。該內存不能爲"read"
  // 說明引用在任何情況下,都不能指向空值

  return 0;
}

程序2:
複製代碼 代碼如下:

#include <iostream>
#include <string>
using namespace std;
int main(void)
{
  string s1("Hello");
  string s2("World");
  // printf("%s\n", s1); 不能用printf輸出s1,而應該用cout

  cout << "s1的地址 = "<< &s1 << endl;// &s1 = 0012FF64
  cout << "s2的地址 = "<< &s2 << endl;// &s2 = 0012FF54

  string& rs = s1;   // 1. 定義一個引用rs,rs引用s1
  cout << "引用rs的地址 = " << &rs << endl;  // &rs = 0012FF64

  string* ps = &s1; //定義一個指針ps, ps指向s1
  cout << "指針ps的地址 = " << ps << endl;// ps = 0012FF64

  cout << rs << ", " << *ps << endl;  // Hello, Hello
  // 如果沒有#include <string>,上面的語句在編譯的時候,會出現如下錯誤:
  // error C2679: binary '<<' : no operator defined which takes a right-
  // hand operand of type 'class std::basic_string<char,struct
  // std::char_traits<char>,class std::allocator<char> >'
  // (or there is no acceptable  conversion)

  rs = s2;  // 2. rs仍舊引用s1, 但是s1現在的值是"World"
  ps = &s2;   // ps現在指向s2

  cout << "引用rs的地址 = " << &rs << endl;  // &rs = 0012FF64 未改變
  cout << "引用rs的值 = " << rs << endl;   // rs = "World" 已改變

  cout << "指針ps的地址 = " << ps << endl;// ps = 0012FF54  已改變
  cout << "指針ps所指地址的內容 = " << *ps << endl;  // *ps = World已改變

  cout << "s1的地址 = "<< &s1 << endl;// 3. &s1 = 0012FF64 未改變
  cout << "s1的值 = " << s1 << endl; // 4. s1 = World  已改變

  return 0;
}

可以認爲:
引用就是變量的別名,在引用初始化的時候就已經確定,以後不能再改變。見程序2的粗體字語句。第1句,聲明瞭rs引用s1,s1的值爲”Hello”,從這以後,rs實際上就相當於變量s1了,或者從更本質的意義上來說,rs的地址就是初始化時s1的地址了,以後都不會再改變。這應該比較好理解,比如我們在程序中定義了一個變量a,不管我們如何給a賦值,但它的地址是不會改變的;

第2句,rs仍舊指向初始化時s1的地址,但此處的賦值就相當於重新給s1賦值,因此我們從第3句和第4句可以看到,s1的地址並沒有發生變化,但是其值已經發生了變化。

二、作爲參數傳遞
利用引用的這個特性,可以用它作爲函數的傳出參數。如程序3:

複製代碼 代碼如下:

#include <iostream>
#include <string>
using namespace std;
int newEvaluation(string& aStr)
{
  string bStr("Hello,");
  aStr = bStr + aStr;

  return 0;
}

int main(void)
{
  string aStr("Patrick!");
  newEvaluation(aStr);
  std::cout << aStr << endl; // 輸出結果:"Hello, Patrick!"

  return 0;
}

而一般變量,則不能從函數內部傳值出來,比如程序4:
複製代碼 代碼如下:

#include <iostream>
#include <string>
using namespace std;

int newEvaluation(string aStr)
{
  string bStr("Hello,");
  aStr = bStr + aStr;

  return 0;
}

int main(void)
{
  string aStr("Patrick!");
  newEvaluation(aStr);
  std::cout << aStr << endl; // 輸出結果:"Patrick!",aStr的值沒有變化

  return 0;
}

當然程序3引用傳遞的方式也可以寫成指針傳遞的方式,如程序5:
複製代碼 代碼如下:

#include <iostream>
#include <string>
using namespace std;

int newEvaluation(string* const aStr)
{
  string bStr("Hello,");
  *aStr = bStr + *aStr;

  return 0;
}

int main(void)
{
  string aStr("Patrick!");
  newEvaluation(&aStr);
  std::cout << aStr << endl; // 輸出結果:"Hello, Patrick!"

  return 0;
}

注意程序中的陷井,如程序6:
複製代碼 代碼如下:

#include <iostream.h>
int *pPointer;
void SomeFunction()
{
  int nNumber;
  nNumber = 25;
  //讓指針指向nNumber
  pPointer = &nNumber;
}

void main()
{
  SomeFunction();//爲pPointer賦值
  //爲什麼這裏失敗了?爲什麼沒有得到25
  cout << "Value of *pPointer: " << *pPointer << endl;
}

這段程序先調用了SomeFunction函數,創建了個叫nNumber的變量,接着讓指針pPointer指向了它。可是問題出在哪兒呢?當函數結束後,nNumber被刪掉了,因爲這一個局部變量。局部變量在定義它的函數執行完後都會被系統自動刪掉。也就是說當SomeFunction 函數返回主函數main()時,這個變量已經被刪掉,但pPointer還指着變量曾經用過的但現在已不屬於這個程序的區域。

儘管在SomeFunction中使用所謂的動態分配內存。程序7中也存在陷井:
複製代碼 代碼如下:

#include <iostream.h>
int *pPointer;

void SomeFunction()
{
int intNumber = 25;
// 讓指針指向一個新的整型
pPointer = new int;
pPointer = &intNumber;
}

void main()
{
SomeFunction();   // 爲pPointer賦值
cout<< "Value of *pPointer: " << *pPointer << endl;
delete pPointer;
}

原因也如上面所言,intNumber的作用範圍僅限於SomeFunction中,離開了SomeFunction,那麼intNumber就不存在了,那麼&intNumber即intNumber的地址就變得沒有意義了,因此,該地址所指向的值是不確定的。如果改爲下面的程序就不會有問題了。

程序8:

複製代碼 代碼如下:

#include <iostream.h>
int *pPointer;

void SomeFunction()
{
int intNumber = 25;
// 讓指針指向一個新的整型
pPointer = new int(intNumber);
}

void main()
{
SomeFunction();   // 爲pPointer賦值
cout<< "Value of *pPointer: " << *pPointer << endl;
delete pPointer;
}

三、指針的指針
前面說到,指針是沒有級數限制的。
程序9:
複製代碼 代碼如下:

#include<stdio.h>
#include<stdlib.h>

void main(void)
{
int i, j;
int a[10], b[3][4], *p1, *p2, **p3;  
for(i = 0; i < 10; i++)
   scanf("%d", &a[i]);  

for(i = 0; i < 3; i++)
   for(j = 0; j < 4; j++)
   scanf("%d", &b[i][j]);

p1 = a;
p3 = &p1;
for(i = 0; i < 10; i++)
   printf("%4d", *(*p3+i));
printf("\n");

for(p1 = a; p1 - a < 10; p1++)
{
   p3 = &p1;
   printf("%4d", **p3);
}
printf("\n");

for(i = 0; i < 3; i++)
{
   p2 = b[i];
   p3 = &p2;
   for(j = 0; j < 4; j++)
   printf("%4d",*(*p3+j));
   printf("\n");
}

for(i = 0; i < 3; i++)
{
   p2 = b[i];
   for(p2 = b[i]; p2-b[i] < 4; p2++)
   {
   p3 = &p2;
   printf("%4d", **p3);
   }
   printf("\n");
}
}

輸出的結果:
1   2   3   4   5   6   7   8   9   10
1   2   3   4   5   6   7   8   9   10
11  12  13  14
15  16  17  18
19  20  21  22
11  12  13  14
15  16  17  18
19  20  21  22

四、函數指針和函數引用
函數指針是C++最大的優點之一。和使用普通指針相比,高級程序員只要有可能都更願意使用引用,因爲引用更容易處理一些。然而,當處理函數時,函數引用對比函數指針就未必有這個優勢了。現有的代碼很少使用函數引用。下面將向介紹如何函數指針、如何使用函數引用以及分別在什麼情況下使用它們。

① 函數指針的例子

複製代碼 代碼如下:

#include <iostream>
void print(int i)
{
std::cout << i << std::endl;
}

void multiply(int& nDest, int nBy)
{
nDest *= nBy;
}

void print_something()
{
std::cout << "something" << std::endl;
}

int sayHello()
{
std::cout << "Hello, World!" << std::endl;
return 10;
}

int main()
{
void (*pFunction_1)(int);
pFunction_1 = &print;
pFunction_1(1);
// 輸出結果爲1

void (*pFunction_2)(int&, int) = &multiply;
int i = 1;
pFunction_2(i, 10);
std::cout << "i = " << i << std::endl;
// 輸出結果爲10

void (*pFunction_3)();
pFunction_3 = &print_something;
pFunction_3();
// 輸出結果爲something

int (*pFunction_4)();
pFunction_4 = &sayHello;
int a = pFunction_4();
// 輸出結果爲Hello, World!
std::cout << a << std::endl;
// 輸出結果爲10

return 0;
}

② 函數引用的例子
複製代碼 代碼如下:

#include <iostream>
void print(int i)
{
std::cout << i << std::endl;
}

void print2(int i)
{
std::cout << i << std::endl;
}

void multiply(int& nDest, int nBy)
{
nDest *= nBy;
}

void print_something()
{
std::cout << "something" << std::endl;
}

int sayHello()
{
std::cout << "Hello, World!" << std::endl;
return 10;
}

 
int main()

// void (&rFunction_1)(int);
// 錯誤:未初始化引用!引用必須初始化

void (&rFunction_2)(int) = print;
rFunction_2(1);
// 輸出1

rFunction_2 = print2;
rFunction_2(2);
// 輸出2

void (&rFunction_3)(int&, int) = multiply;
int i = 1;
rFunction_3(i, 10);
std::cout << i << std::endl;
// 輸出10

void (&rFunction_4)() = print_something;
rFunction_4();
// 輸出something

int (&rFunction_5)();
rFunction_5 = sayHello;
int a = rFunction_5();   // 輸出Hello, World!
std::cout << a << std::endl;
// 輸出10

return 0;
}

③ 函數指針和函數引用作爲函數參數
複製代碼 代碼如下:

#include <iostream>

void print(int i)
{
std::cout << i << std::endl;
}

void print2(int i)
{
std::cout << i * 2 << std::endl;
}

void printSomething()
{
std::cout << "Something" << std::endl;
}

void sayHello()
{
std::cout << "Hello, World!" << std::endl;
}

void call_p_func(void (*func)(int))
{
func(1);
func(2);
func(3);
}

void call_r_func(void (&func)(int))
{
func(1);
func(2);
func(3);
}

void call_p_function(void (*func)())
{
func();
}

int main()
{
std::cout << "函數指針作爲參數" << std::endl;
call_p_func(&print);
call_p_func(&print2);
call_p_function(&printSomething);
call_p_function(&sayHello);
call_p_function(sayHello);
// 上面兩句對於某些編譯器來說是一樣的,但是推薦使用前者的寫法,
// 這樣可以是程序的可讀性更好一些

std::cout << "函數引用作爲參數" << std::endl;
call_r_func(print);
call_r_func(print2);

return 0;
}

總結:
函數指針的聲明使用方式:
<想要指向的函數之返回類型>(*函數指針的名稱)<想要指向的函數之參數類型…>
如要想聲明一個函數指針指向以下函數:
複製代碼 代碼如下:

void print(int i)
{
std::cout << i << std::endl;
}

那麼就可以如下操作:
void (*pFunction)(int);
然後如下用函數的地址給pFunction賦值:
pFunction = &print;
在然後,pFunction就可以和函數print一樣使用了,比如,
pFunction(1);
等等。

函數引用的聲明和使用方式:
<欲引用的函數之返回類型>(&函數引用的名稱)<欲引用的函數之參數類型…>=<欲引用的函數的名稱>,至所以如此,是引用在聲明的時候必須初始化,引用不能指向空值。
如要想聲明一個函數引用指向以下函數:
複製代碼 代碼如下:

void print(int i)
{
std::cout << i << std::endl;
}

那麼就可以如下操作:
void (&rFunction)(int)=print;
在然後,rFunction就可以和函數print一樣使用了,比如,
rFunction(1);
等等。

五、const修飾指針和引用
大致而言,const修飾指針和引用分三種情況,即const修飾指針、const修飾引用和const修飾指針的引用。下面分別討論之。

① const修飾指針
const修飾指針又分爲三種情況,即const修飾指針本身、const修飾指針所指的變量(或對象)以及const修飾指針本身和指針所指的變量(或對象)。

a. const修飾指針本身
在這種情況下,指針本身是常量,不能改變,任何修改指針本身的行爲都是非法的,例如:
double pi = 3.1416;
double* const PI = π

double alpha = 3.14;
PI = α   // 錯誤。因爲指針PI是常量,不能再被改變。
*PI = alpha;   // OK。雖然指針PI不能被改變,但指針所指的變量或者對象可變。

b. const修飾指針指向的變量(或對象)
在這種情況下,指針本身可以改變,但const所修飾的指針所指向的對象不能被改變,例如:
double pi = 3.1416;
const double* PI = π

double alpha = 3.14;
*PI = alpha;// 錯誤。因爲PI所指向的內容是常量,因此*PI不能被改變。
PI = α// OK。雖然指針所指的內容不能被改變,但指針PI本身可改變。從而通過這種方式改變*PI。

c. const修飾指針本身和指針所指向的變量(或對象)
在這種情況下,指針本身和指針指向的變量(或對象)均不能被改變,例如:
double pi = 3.146;
const double* const PI = π
//double const* const PI = π
cout << "PI = " << PI << endl;
cout << "*PI = " << *PI << endl;

double alpha = 3.14;
//*PI = alpha; // 錯誤。因爲PI所指向的內容是常量,因此*PI不能被改變。
//PI = α // 錯誤。因爲指針PI是常量,不能再被改變。

② const修飾引用
const修飾引用沒有指針修飾指針那麼複雜,只有一種形式。引用本身不能被改變,但所指向的對象是可以被改變的,見上面“一、基本知識”。
double pi = 3.1416;
//const double& PI = pi;
double const& PI = pi;  //和上面一句是等價的
//double& const PI = pi;//有問題。很多編譯器會產生warning
cout << PI << endl;

③ const修飾指針引用
我們用例子來說明。
double pi = 3.14;
const double* pPI = π
//const double*& rPI = π //錯誤。不能將double* 轉換成const double *&
const double*& rPI = pPI;   //OK。聲明指針引用的正確方法

說明:const double*& rPI = π 爲什麼會出現錯誤呢?我們知道,引用是被引用對象的別名,正因爲如此,由於rPI是pPI的別名,因此rPI和pPI的類型必須完全一致。從上面的代碼段我們可以看到,rPI的類型是const double*,而&pi的類型是double*,因此這句程序是錯誤的。

下面這段代碼和 ① 中的b中的情形對應(即內容不可變,指針可變):
double pi = 3.1416;
double api = 3.14;
const double* pPI = π
const double* pAPI = &api;
const double*& rPI = pPI;
const double*& rAPI = pPI;

*rAPI = api; // 錯誤。指針所指向的值不能被直接改變
rAPI = pAPI;   // OK。指針本身可以被改變

指針引用的用法還有其它的情形,由於罕用,故此不談及。

發佈了2 篇原創文章 · 獲贊 20 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章