本文參考資料 C++ Primer, 5e; Coursera北大數據結構與算法課程。
1. 概論
指針在C\C++語言中是很重要的內容,並且和指針有關的內容一向令人頭大。針對初學者,我總結了一些關於指針和數組的用法(尤其是指針和二維數組)。初學者大部分關於指針和數組的問題應該可以再本文找到答案,高級用法我也沒有接觸到,就這樣吧。
2.指針基礎
指針是指向另外一種類型的復合類型。
指針本身就是一個對象,允許對指針進行賦值和拷貝;指針無需在定義時賦初值。
指針定義
"&"是取地址操作符。
int num=1;
int *p=# //(&是取地址操作符)
利用指針訪問對象
使用解引用操作符“*”。
cout<<*p<<endl;
輸出結果為1。
指針的狀態
- 指向一個對象
- 指向緊鄰對象所占空間的下一個位置
- 空指針 int *p=nullptr;
- 無效指針
指針作為條件判斷參數
例如:
if(p){}
只要指針p不是0,那么條件就為真。
另外值得注意的是,對於兩個類型相同的指針,可以用“==”或者“!=”來比較。若兩個指針存放的地址相同,則它們相等,否則不等。
3. 指針進階
指向指針的指針
由於指針是對象,所以指針也有自己的地址。因此,C++語言允許把一個指針指向另一個指針。 例子:
int i=9;
int *p1=&i;
int **p2=&p1;
cout<<i<<endl<<*p1<<endl<<**p2<<endl;
結果是打印3個9。
指針與const限定符
這里有2個初學者容易混淆的概念,即指向常量的指針和常量指針。根據其英文名字可能比較容易記住:
指向常量的指針(pointer to const)是說這個指針是一個普通的指針,它指向了一個常量,如果你願意,它也可以指向其他對象,並且可以令一個指向常量的指針指向另一個非常量;
const double pi=3.1415;
double *p1=π//error for p1 is a general pointer
const double *p2=π//correct; and p2 can poinnt to other objects
*p2=6.28;//error for pi is a const variable and p2 is const
常量指針(const pointer)是說這個指針本身就是一個常量對象,所以它不能指向其他對象,但是不意味着它不能改變所指向對象的值。
int num=9;
int *const p1=#//correct, but remember that p1 cannot point to other objects
*p1=18;//correct. You can use the const pointer to change the value of a unconst variable
const double e=2.71;
const double *const p2=&e;//p2 is a const pointer points to a const object
4. 一維數組的定義與初始化
定義
int arr[10];//含有10個整型的數組
int *arr2[3];//含有3個整型指針的數組
一般情況下,數組的元素被默認初始化。
顯示初始化
int arr[]={1,2,3};
int arr2[4]={1,2,3,4};
可以用字符串字面值初始化字符數組,但是需要記得字符串字面值結尾有一個空字符。
char arr[5]={'h','e','l','l','o'};//correct
char arr[5]="hello";//error, initilizer-string for the chars array is too long
訪問數組元素
使用下標訪問數組元素,注意數組的下標從0開始。
C++ 11標准增加了 range for語句可以遍歷數組元素:
for(auto i : arr)//auto用來自動確定類型
{
cout<<i<<endl;
}
使用range for的好處在於不用擔心數組越界。
5. 指針和數組
可以用一個指針指向數組元素:
int arr[]={1,2,3,4,5,6,7,8,9,0};
int *p=&arr[0];//此時p是一個指向數組首元素的指針
數組有一個特性,很多用到數組的地方,編譯器會自動把數組名替換為一個指向數組首元素的指針:
int arr[]={1,2,3,4,5,6,7,8,9,0};
int *p=&arr[0];//此時p是一個指向數組首元素的指針
cout<<p<<endl;//result 0x69fee4
cout<<arr<<endl;//result 0x69fee4
注意:由於數組名arr是一個常量,因此*arr++是沒有意義的,並且編譯器會報錯,因為arr++試圖修改arr的值。但是(arr+2)是有意義的,因為這並沒有試圖修改arr的值。同理,如果令p=&arr[0],那么我們也是可以使用p++的,因為p不是常量。
標准庫函數begin & end
盡管可以得到尾后指針,但這種做法極易出錯。C++11標准引入了兩個名為begin和end的函數:
int a[]={1,2,3,4,5,6};
int *beg=begin(a);//pointer to the first element
int *last=end(a);//pointer to the position next to the last element
//output the elements of the array
while(beg!=end)
{
cout<<*beg<<endl;
++beg;
}
6. 指針運算
給一個指針加上(減去)某個整數N,結果依然是指針。新指針與原來的指針相比前進或者后退了N個位置。
兩個指針相減的結果是它們之間的距離。
為了更好的理解這個問題,舉如下例子:
#include <iostream>
using namespace std;
int main()
{
int a[3][3]={{6,1,7},
{2,5,4},
{8,3,9}
};
<span class="hljs-built_in">cout</span><<a<<<span class="hljs-built_in">endl</span>;<span class="hljs-comment">//1</span>
<span class="hljs-built_in">cout</span><<a+<span class="hljs-number">1</span><<<span class="hljs-built_in">endl</span>;<span class="hljs-comment">//2</span>
<span class="hljs-built_in">cout</span><<&a+<span class="hljs-number">1</span><<<span class="hljs-built_in">endl</span>;<span class="hljs-comment">//3</span>
<span class="hljs-built_in">cout</span><<*a<<<span class="hljs-built_in">endl</span>;<span class="hljs-comment">//4</span>
<span class="hljs-built_in">cout</span><<*a+<span class="hljs-number">1</span><<<span class="hljs-built_in">endl</span>;<span class="hljs-comment">//5</span>
<span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
}
程序結果(你的結果可能會有所不同) 0x69fec0 0x69fecc 0x69fee4 0x69fec0 0x69fec4
第一個打印結果為0x69fec0,給a+1后,結果為0x69fecc,變大了12。為什么會變大12呢?要知道每個整數都是4個字節,為什么不是變大4呢?答案是:由於a是一個二維數組,所以a指向的第一個元素是一個含有3個整數的數組,因而加1后是指向下一個子數組,所以地址的值會變大12.
同理,&a是指向一個二維數組,因此加1后地址值會變大0x 24=36。
而a指向第一個子數組的第一個元素——這是一個整數,因此加1后地址值變大了4。同時你可以發現a和a的地址是一樣的,這是因為第一個子數組和第一個整型元素的起始地址是一樣的。
7. 多維數組和指針
嚴格來說,C++中是沒有多維數組的,通常所說的多維數組其實是數組的數組。
在本文第6部分的例子中展示了二維數組的初始化方法,更高維的數組初始化方法是類似的。這里再次詳細說明一下數組名和指針的關系。以本文第6部分的例子為例:
a是指向二維數組第一個元素即第一個子數組的指針,等價於&a[0];
a[0]是指向a[0][0]的指針,等價於*a;
a[0][0]指向第一個整型元素‘6’, 等價於**a;
&a指向整個二維數組;
總之,*會將指針降一級,&會把指針升一級。
下標訪問
多維數組同樣可以下標訪問,例如a[0][0]的值是6.
8. 指針形參
當使用指針作為函數參數的時候,執行的是指針拷貝的操作,拷貝的是指針的值。拷貝之后兩個指針是不同的指針,但是它們所指向的對象是一樣的,因此可以通過操作指針來改變指針所指向對象的值。
void change(int *p) {
*p=32;
}
限制指針的功能
很多情況下我們使用指針是為了避免拷貝對象,但是並不希望更改對象的值。這種情況下,使用const限定符限制指針的功能是一個不錯的選擇。
void test(const int *p) {
...
...
}
9. 數組形參
由於數組不允許拷貝,因此我們無法以傳值的方式傳遞一個數組;因為數組名相當於數組第一個元素的指針,因此可以通過傳遞指針的形式來在函數中操作數組。
以下3個聲明是等價的:
void print(const int *);
void print(const int[]);
void print(const int[5]);
由於數組是以指針形式傳遞給函數的,因此一開始的時候函數並不知道數組的維度。因此有時候有必要顯示傳遞一個維度參數。
當函數不需要對數組元素進行寫操作的時候,數組形參最好用const限定符限制指針功能,詳見本文第8部分示例。
傳遞多維數組
所謂多維數組其實是數組的數組。和一維數組一樣,我們實際傳遞的是數組的指針。下面2個聲明是等價的:
void print(int (*p)[3],int rowsize){...}
void print(int (p[][3],int rowsize){...}
這樣的例子可能沒有什么直觀的感受,下面我們用一個詳細的例子來說明。
#include <iostream>
using namespace std;
void print1(int (p)[3])//注意p兩邊的括號不可缺少。
{
cout<<p[1][1]<<endl;
}
void print2(int p[][3])
{
cout<<p[0][0]<<endl;
}
int main()
{
int a[2][3]={{1,2},{3,4}};
print1(a);
print2(a);
return 0;
}
注意,print1(int (p)[3])函數里面,形參p兩邊的括號必不可少。
p[3]表示3個指針構成的數組 (p)[3]表示指向含有3個整數數組的指針。
結果 4 1
你可能對這種方法並不是很滿意,為什么呢?因為使用這種方式必須顯示指定第二維的維度,而有些時候這個維度是無法獲得的。比如有以下題目:
摘抄自:北大poj 描述 在一個m×n的山地上,已知每個地塊的平均高程,請求出所有山頂所在的地塊(所謂山頂,就是其地塊平均高程不比其上下左右相鄰的四個地塊每個地塊的平均高程小的地方)。
輸入 第一行是兩個整數,表示山地的長m(5≤m≤20)和寬n(5≤n≤20)。 其后m行為一個m×n的整數矩陣,表示每個地塊的平均高程。每行的整數間用一個空格分隔。
輸出 輸出所有上頂所在地塊的位置。每行一個。按先m值從小到大,再n值從小到大的順序輸出。
樣例輸入 10 5 0 76 81 34 66 1 13 58 4 40 5 24 17 6 65 13 13 76 3 20 8 36 12 60 37 42 53 87 10 65 42 25 47 41 33 71 69 94 24 12 92 11 71 3 82 91 90 20 95 44
樣例輸出 0 2 0 4 2 1 2 4 3 0 3 2 4 3 5 2 5 4 7 2 8 0 8 4 9 3
如果題目要求你必須寫一個函數來處理,是不是感覺之前講解的參數傳遞方法就不實用了呢?因為你也不知道一開始第二個維度是多少啊!
其實萬變不離其宗,我們只要想辦法傳入一個地址,就可以通過這個地址訪問到這個數組的所有元素。這里最重要的就是要搞清楚本文第6部分和第7部分講解的關於指針運算的問題,這對於初學者可能會感覺有點亂,但是只要慢慢去想,你就會發現這一切都是那么的自然。
為了解決這個問題,首先看看如何訪問整個二維數組。
原則上只要傳入了第一個元素的指針,我們就可以通過對這個指針進行運算從而遍歷整個數組。
void print (int *a,int m,int n) {
for(int i=0;i!=m;++i)
{
for(int j=0;j!=n;++j)
cout<<*(a+i*n+j);
cout<<endl;
}
}
假設有一個55的二維數組a。這個函數,我們可以傳入print(a,5,5)。還記得嗎?在第7部分我們說過a就是指向a[0][0]的指針。這樣就可以打印整個數組了。我記得我剛學習這里的時候總是糾結於,我是不是在定義函數的時候形參是不是應該是print(intp, int m, int n)呢?其實是沒有必要的,因為二維數組名並不是指向指針的指針*,你需要的只是一個入口而已。
下面給出傻瓜式程序:
#include <iostream>
using namespace std;
static int m,n;
void hill(int **a) {
for(int i=0;i!=m;++i)
{
for(int j=0;j!=n;++j)
{
int num=*((int*)a+i*n+j);
if(i==0)
{
if(j==0)
{
if(num>=*((int*)a+i*n+j+1) && num>=*((int*)a+(i+1)*n+j))
{
cout<<i<<" "<<j<<endl;
}
}
else if(j==n-1)
{
if(num>=*((int*)a+i*n+j-1) && num>=*((int*)a+(i+1)*n+j))
{
cout<<i<<" "<<j<<endl;
}
}
else
{
if(num>=*((int*)a+i*n+j-1) && num>=*((int*)a+i*n+j+1) && num>=*((int*)a+(i+1)*n+j))
{
cout<<i<<" "<<j<<endl;
}
}
}
else if(i==m-1)
{
if(j==0)
{
if(num>=*((int*)a+(i-1)*n+j) && num>=*((int*)a+i*n+j+1))
{
cout<<i<<" "<<j<<endl;
}
}
else if(j==n-1)
{
if(num>=*((int*)a+(i-1)*n+j) && num>=*((int*)a+i*n+j-1))
{
cout<<i<<" "<<j<<endl;
}
}
else
{
if(num>=*((int*)a+(i-1)*n+j) && num>=*((int*)a+i*n+j-1) && num>=*((int*)a+i*n+j+1))
{
cout<<i<<" "<<j<<endl;
}
}
}
else
{
if(j==0)
{
if(num>=*((int*)a+(i-1)*n+j) && num>=*((int*)a+(i+1)*n+j) && num>=*((int*)a+i*n+j+1))
{
cout<<i<<" "<<j<<endl;
}
}
else if(j==n-1)
{
if(num>=*((int*)a+i*n+j-1) && num>=*((int*)a+(i-1)*n+j) && num>=*((int*)a+(i+1)*n+j))
cout<<i<<" "<<j<<endl;
}
else
{
if(num>=*((int*)a+i*n+j-1) && num>=*((int*)a+i*n+j+1) && num>=*((int*)a+(i-1)*n+j) && num>=*((int*)a+(i+1)*n+j))
cout<<i<<" "<<j<<endl;
}
}
}
}
}
int main()
{
<span class="hljs-built_in">cin</span>>>m>>n;
<span class="hljs-keyword">int</span> a[m][n];
<span class="hljs-keyword">for</span>(<span class="hljs-keyword">int</span> i=<span class="hljs-number">0</span>;i!=m;++i)
{
<span class="hljs-keyword">for</span>(<span class="hljs-keyword">int</span> j=<span class="hljs-number">0</span>;j!=n;++j)
{
<span class="hljs-built_in">cin</span>>>a[i][j];
}
}
<span class="hljs-comment">//int *p=*a;</span>
hill((<span class="hljs-keyword">int</span>**)a);
<span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
}
10. 返回指針和數組
返回指針
#include<iostream>
using namespace std;
int a[]={11,21,31,41};
int *f() {
return a;
}
int main()
{
cout<<*f()<<endl;//result is 11
}
上面的例子展示了如何返回一個指針。或許把函數定義寫成如下形式更好理解。
int* f(){... ...}
這種形式展示了函數 f 的返回類型是指針而不是讓人誤以為函數名是 *f。
永遠不要試圖返回局部對象的指針。因為局部變量(對象)的生命周期在函數調用結束后會消失,此時你返回的地址的內容可能已經發生了變化。
如果確實需要返回局部變量的指針,你需要把這個變量聲明為static
返回數組
由於指針不能拷貝,因此函數不能返回數組,但是可以返回數組的指針或者引用。其實上面的例子就是一個返回數組的例子,故不再敘述。
11. 結語
本文主要講述了指針和數組的用法。我覺得這對於初學者還是一個比較全面的教程,希望大家喜歡。
原文地址:https://blog.csdn.net/valada/article/details/79909749