程序設計與算法 | (15) 指針

本專欄主要基於北大郭煒老師的程序設計與算法系列課程進行整理,包括課程筆記和OJ作業。該系列課程有三部分: (一) C語言程序設計;(二) 算法基礎;(三) C++面向對象程序設計

(一) C語言程序設計 課程鏈接

1. 指針的概念

基本概念

  • 每個變量都被存放在從某個內存地址(以字節爲單位)開始的若干個字節中(如int類型4個字節,char類型1個字節等)
  • “指針”,也稱作“指針變量”,大小爲4個字節(或8個字節)的變量, 其內容代表一個內存地址(首地址)。
  • 通過指針,能夠對該指針指向的內存區域(首地址開始的若干個字節)進行讀寫。
  • 如果把內存的每個字節都想像成賓館的一個房間,那麼內存地址相當於就是房間號,而指針裏存放的,就是房間號。

定義

類型名 * 指針變量名

int * p; // p 是一個指針,變量 p的類型是 int 
char * pc; // pc 是一個指針, 變量 pc 的類型是 char *
float *pf; // pf 是一個指針,變量 pf 的類型是 float *

內容

int * p = (int *)40000  //4000是int類型,強制類型轉換爲 int * 類型

p是int *類型的指針。
其內容存儲的是一個地址:
十進制 40000
十六進制 0x9C40
二進制每個比特 0000 0000 0000 0000 1001 1100 0100 0000

p指向地址40000(首地址),地址p就是地址40000
*p 就代表地址40000開始處的若干個字節(p是int *的話,就是4個字節)的內容

通過指針訪問其指向的內存空間

int * p = ( int * ) 40000; //p是int*類型 其存儲地址40000,即p指向地址40000(首地址)
*p = 5000; //往地址40000處起始的若干個字節的內存空間裏寫入 5000
int n = *p; //將地址40000處起始的若干字節的內容賦值給 n

“若干” = sizeof(int)=4,因爲p是 int * 類型;
在這裏插入圖片描述

指針定義總結

T * p ; //T可以是任何類型的名字,比如int,double,char等等。

p 的類型: T *
*p 的類型: T

通過表達式 * p,可以讀寫從地址p開始的 sizeof(T)個字節

*p 等價於存放在地址p處的一個 T 類型的變量

間接引用運算符
sizeof(T
) 4字節(64位計算機上可能8字節) (char、double、int類型變量所佔的字節是不同的,但是char*,double*,int*這些指針類型變量所佔的字節是相同的)

2. 指針的用法

用法

char ch1 = 'A';
char * pc = &ch1; //使得pc指向變量ch1 (pc存儲/指向變量ch1的首地址)

& : 取地址運算符
&x : 變量x的地址(即指向x的指針)
對於類型爲 T 的變量 x,&x 表示變量 x 的地址(即指向x的指針) ,&x 的類型是 T *。

之前我們把int類型變量40000強制類型轉換爲int*,並賦值給一個int*類型的指針變量p,這只是理論上的用法,實際這樣使用很可能會報錯,因爲地址40000未必能訪問,一般像上面這麼做。

char ch1 = 'A';
char * pc = &ch1;  //使得pc 指向變量ch1(的首地址)
* pc = 'B';        // 使得ch1 = 'B'
char ch2 = * pc;  // 使得ch2 = ch1
pc = & ch2;      // 使得pc 指向變量ch2
* pc = 'D';      // 使得ch2 = 'D'

上面這段代碼,完全可以不用指針就能操作,用指針感覺有點畫蛇添足,那麼指針的作用是什麼呢?

指針的作用

有了指針,就有了自由訪問內存空間的手段

  • 不需要通過變量,就能對內存直接進行操作。通過指針,程序能訪 問的內存區域就不僅限於變量所佔據的數據區域
  • 在C++中,用指針p指向a的地址,然後對p進行加減操作,p就能指 向a後面或前面的內存區域,通過p也就能訪問這些內存區域(可以寫一些病毒或反病毒程序等)

指針的互相賦值

不同類型的指針,如果不經過強制類型轉換,不能直接互相賦值。

 int * pn, char * pc, char c = 0x65;
 pn = pc; //類型不匹配,編譯出錯
 pn = & c; //類型不匹配,編譯出錯
 pn = (int * ) & c; 
 int n = * pn;  //n值不確定
 * pn = 0x12345678;//編譯能過但運行可能出錯

pn指向變量c的首地址,而pn是int*類型,pn表示從首地址開始的sizeof(int)=4個字節的內存區域中的內容,而原始的變量c是char類型,只佔一個字節,其餘三個字節未知,所以n的值是不確定的。 如果對pn表示的這塊內存區域賦值的話,運行可能出錯,因爲其餘三個字節的地址可能不能訪問。

3. 指針的運算

1)兩個同類型的指針變量,可以比較大小

地址p1<地址p2,等價於 p1< p2 值爲真。 (地址p1,就是指針p1中存儲的首地址或指針p1指向的首地址)
地址p1=地址p2,等價於 p1== p2 值爲真
地址p1>地址p2,等價於 p1 > p2 值爲真

2)兩個同類型的指針變量,可以相減

兩個T * 類型的指針 p1和p2
p1 – p2 = ( 地址p1 – 地址 p2 ) / sizeof(T) (p1、p2之間可存放多少個T類型的變量)

例:int * p1, * p2;
若 p1 指向地址 1000,p2 指向地址 600, 則
p1 – p2 = (1000 – 600)/sizeof(int) = (1000 – 600)/4 = 100

3)指針變量加減一個整數的結果是指針
p: T*類型的指針
n : 整數類型的變量或常量
p+n : T * 類型的指針,指向地址: 地址p + n × sizeof(T)
n+p, p-n , *(p+n), *(p-n) 含義自明

4)指針變量可以自增、自減

T* 類型的指針p指向地址n
p++, ++p : p指向 n + sizeof(T)
p–, --p : p指向 n - sizeof(T)

5)指針可以用下標運算符“[ ]”進行運算
p 是一個 T * 類型的指針,
n 是整數類型的變量或常量

p[n] 等價於 *(p+n)

通過指針實現自由內存訪問

如何訪問int型變量 a 前面的那一個字節?

int a;
char * p = (char * ) &a; //&a是int*類型 不能直接用int*來做,--p指向的是上一個int變量(4個字節)的首地址,不是上一個字節的地址。所以要強制類型轉換爲char*
--p; //--p指向的是上一個char變量的首地址,而char類型是一個字節,所以也就是上一個字節的地址
printf("%c", * p); //可能導致運行錯誤 
* p = 'A'; //可能導致運行錯誤

指針運算示例

#include <iostream>
using namespace std;
int main() 
{
	int * p1, * p2; int n=4;
	char * pc1, * pc2;
	p1 = (int *) 100; //地址p1爲100 (p1中存儲地址100,p1指向地址100)
	p2 = (int *) 200; //地址p1爲200
	cout<< "1) " << p2 - p1 << endl; //(地址p2-地址p1)/sizeof(int)
	//輸出 1) 25, 因(200-100)/sizeof(int) = 100/4 = 25 p1、p2間存儲了多少個int型變量
	pc1 = (char * ) p1; //地址pc1爲100
	pc2 = (char * ) p2; //地址pc2爲200
	cout<< "2) " << pc1 - pc2 << endl; //-100 (地址pc1-地址pc2)/sizeof(char)
	//輸出 2) -100,因爲(100-200)/sizeof(char) = -100
	cout<< "3) " << (p2 + n) - p1 << endl; //輸出 3) 29 
	//((地址p2+n*sizeof(int))-地址p1)/sizeof(int) = 29 即p2+n、p1之間存儲了多少個int型變量
	int * p3 = p2 + n; // p2 + n 是一個指針,可以用它給 p3賦值
	cout<< "4) " << p3 - p1 << endl; //輸出 4) 29
	cout<< "5) " << (pc2 - 10) - pc1 << endl; //輸出 5) 90
	return 0;
}

4. 空指針

  • 地址0不能訪問。指向地址0的指針就是空指針
  • 可以用“NULL”關鍵字對任何類型的指針進行賦值。NULL實際上就是整數0,值爲NULL的指針就是空指針:
int * pn = NULL; char * pc = NULL; int * p2 = 0;
  • 指針可以作爲條件表達式使用。如果指針的值爲NULL,則相當於爲 假,值不爲NULL,就相當於爲真
    if§等價於if(p!=NULL) if(!p)等價於if( p==NULL )

指針作爲函數參數

#include <iostream>
using namespace std;
void Swap( int *p1, int * p2) 
{
	int tmp = *p1; //將p1指向的變量的值,賦給tmp
	*p1 = *p2; //將p2指向的變量的值,賦給p1指向的變量
	*p2 = tmp; //將tmp 的值賦給p2指向的變量的值。
}
int main() 
{
	int m = 3,n = 4;
	Swap( &m, &n); //&m是m的(首)地址,使得p1指向m(的首地址);p2指向n
	cout << m << " " << n << endl; //輸出 4 3 return 0;
}

之前我們遇到過這個例子,如果實參只傳遞m,n ,則不能完成交換,因爲形參只是實參的一個拷貝,只是形參交換了,實參並未交換。當時說數組和引用除外。實際上沒有例外,形參就是實參的一個拷貝,只不過這裏我們傳遞的實參是m,n的地址&m,&n,&m,&n分別指向m,n,形參是實參的一個拷貝,因此p1,p2也分別指向m,n,*p1,p2是p1,p2所指向變量的值,對p1,*p2操作也就是改變了變量的值,因此最終可以完成交換。

C++還可以用引用來實現交換,C語言中只能用指針,實際上後面我們會看到,數組名也是一個指針,數組名是數組的首地址,指針指向數組的首地址。

5. 指針和數組

  • 數組的名字是一個指針常量 指向數組的起始地址

T a[N];

  • a的類型是 T *
  • 可以用a給一個T * 類型的指針賦值 (能做右值)
  • a是編譯時其值就確定了的常量,不能夠對a進行賦值(不能做左值)
  • 作爲函數形參時, T *p 和 T p[ ] 等價
void Func( int * p) { cout << sizeof(p);} //指向數組,但不知道有多少個元素 sizeof(p)就是指針變量的大小
void Func( int p[]) { cout << sizeof(p);}
#include <iostream>
using namespace std;
int main() 
{
	int a[200];int * p ;
	p = a; // p指向數組a的起始地址,亦即p指向了a[0](的首地址)
	* p = 10; //a[0] = 10
	* (p+1) = 20; //a[1] = 20
	p[0] = 30; //p[i]等價於 *(p+i)   a[0]=30
	p[4] = 40;  //a[4]=40
	for( int i = 0;i < 10; ++i) //對數組a的前10個元素進行賦值
		*( p + i) = i;
	++p;  // p指向 a[1]
	cout << p[0] << endl; //輸出1 p[0]等效於*p, p[0]即是a[1]
	p = a + 6; // p指向a[6]
	cout << * p << endl;// 輸出 6  a[6]的值
	return 0
}	
#include <iostream>
using namespace std;
void Reverse(int * p,int size) { //顛倒一個數組
	for(int i = 0;i < size/2; ++i) {
    	int tmp = p[i];
    	p[i] = p[size-1-i];
    	p[size-1-i] = tmp;
    }
 }
int main() 
{
	int a[5] = {1,2,3,4,5};
    Reverse(a,sizeof(a)/sizeof(int));
    for(int i = 0;i < 5; ++i) {
		cout << *(a+i) << "," ; 
	}
    return 0;
} //5,4,3,2,1,

6. 指針和二維數組

如果定義二維數組:
T a[M][N];

  • ai是一個一維數組
  • a[i]的類型是 T *
  • sizeof(a[i]) = sizeof(T) * N
  • a[i]指向的地址: 數組a的起始地址 + i×N×sizeof(T)
void Reverse(int * p,int size)
{ //顛倒一個數組 
	for(int i = 0;i < size/2; ++i) {
    	int tmp = p[i];
        p[i] = p[size-1-i];
        p[size-1-i] = tmp;
	} 
}
int a[3][4] = { {1,2,3,4},{5,6,7,8},{9,10,11,12}};
Reverse(a[1],4); //=> { {1,2,3,4},{8,7,6,5},{9,10,11,12}};
Reverse(a[1],6); //=> { {1,2,3,4},{10,9,5,6},{7,8,11,12}};

因爲二維數組的元素在內存中是連續存放的,所以Reverse(a[1],6);是對從a[1]地址開始之後的六個元素進行顛倒。

7. 指向指針的指針

定義:
T ** p;
p是指向指針的指針,p指向的地方應該存放着一個類型爲 T * 的指針
*p 的類型是 T *

#include <iostream> 
using namespace std; 
int main()
{
	int **pp; //指向int*類型指針的指針 
	int * p;
	int n = 1234;
	p = &n; // p指向n 或 p指向n的首地址
	pp = & p; //pp指向p  或 pp指向p的首地址
	cout << *(*pp) << endl; // *pp是p, 所以*(*pp)就是n
	return 0;
}

在這裏插入圖片描述

8. 指針和字符串

  • 字符串常量的類型就是 char *
  • 字符數組名的類型也是 char *
#include <iostream> 
using namespace std; 
int main()
{
	char * p = "Please input your name:\n"; 
	cout << p ; // 若不用cout, printf(p) 亦可 
	char name[20];
	char * pName = name;
	cin >> pName;
	cout << "Your name is " << pName; 
	return 0;
}
  • 字符數組名的類型也是 char *,就是一個地址
	char name[20];
	int n;
	scanf("%d%s",&n, name); 
	cin >> n >> name;

字符串操作庫函數

在這裏插入圖片描述

  • char * strchr(const char * str,int c); 尋找字符c在字符串str中第一次出現的位置。如果找到,就返回指向該位置的char*指針;如果str中不包含字符c,則返回NULL
  • char * strstr(const char * str, const char * subStr); 尋找子串subStr在str中第一次出現的位置。如果找到,就返回指向該位置的指針;如果str不包含字符串subStr,則返回NULL
  • int stricmp(const char * s1,const char * s2); 大小寫無關的字符串比較。如果s1小於s2則返回負數;如果s1等於s2,返回0;s1大於s2,返回正數。不同編譯器編譯出來的程序,執行stricmp的結果就可能不同。
  • int strncmp(const char * s1,const char * s2,int n); 比較s1前n個字符組成的子串和s2前n個字符組成的子串的大小。若長度不足n,則取整個串作爲子串。返回值和strcmp類似。
  • char * strncpy(char * dest, const char * src,int n); 拷貝src的前n個字符到dest。如果src長度大於或等於n,該函數不會自動往dest中寫入‘\0’;若src長度不足n,則拷貝src的全部內容以及結尾的‘\0’到dest。
  • char * strtok(char * str, const char * delim); 連續調用該函數若干次,可以做到:從str中逐個抽取出被字符串delim中的字符分隔開的若干個子串。
  • int atoi(char *s); 將字符串s裏的內容轉換成一個整型數返回。比如,如果字符串s的內容是“1234”,那麼函數返回值就是1234。如果s格式不是一個整數,比如是"a12",那麼返回0。
  • double atof(char *s); 將字符串s中的內容轉換成實數返回。比如,"12.34"就會轉換成12.34。如果s的格式不是一個實數 ,則返回0。
  • char *itoa(int value, char *string, int radix);將整型值value以radix進製表示法寫入 string:
char szValue[20];
itoa( 27,szValue,10); //使得szValue的內容變爲 "27" 
itoa( 27,szValue,16); //使得szValue的內容變爲"1b"
#include <iostream>
#include <cstring>
using namespace std;
int main()
{
	char s1[100] = "12345";
	char s2[100] = "abcdefg";
	char s3[100] = "ABCDE";
	strncat(s1,s2,3); // s1 = "12345abc"
	cout << "1) " << s1 << endl; //輸出 1) 12345abc 
	strncpy(s1,s3,3); // s3的前三個字符拷貝到s1,s1="ABC45abc" 
	cout << "2) " << s1 << endl; //輸出 2) ABC45abc 
	strncpy(s2,s3,6); // s2 = "ABCDE" 6超過了s3的長度 此時\0也會被拷貝
	cout << "3) " << s2 << endl; //輸出 3) ABCDE
	cout << "4) " << strncmp(s1,s3,3) << endl; //比較s1和s3的前三個字符,比較結果是相等,輸出 4) 0
	char * p = strchr(s1,'B'); //在s1中查找 'B'第一次出現的位置 返回指向該位置的指針
	if( p ) // 等價於 if( p!= NULL)
		cout << "5) " << p - s1 <<"," << *p << endl; //輸出 5) 1,B
	else
    	cout << "5) Not Found" << endl;
    p = strstr( s1,"45a"); //在s1中查找子串 "45a"。s1="ABC45abc"
    if( p )
		cout << "6) " << p - s1 << "," << p << endl; //輸出 6) 3,45abc 
	else
    	cout << "6) Not Found" << endl;
    //以下演示strtok用法:
	cout << "strtok usage demo:" << endl;
	char str[] ="- This, a sample string, OK."; //下面要從str逐個抽取出被" ,.-"這幾個字符分隔的字串
	p = strtok (str," ,.-"); //請注意," ,.-"中的第一個字符是空格
	while ( p != NULL) { //只要p不爲NULL,就說明找到了一個子串
    	cout << p << endl; //打印子串
		p = strtok(NULL, " ,.-"); //後續調用,第一個參數必須是NULL 
	}
    return 0;
}

在這裏插入圖片描述

9. void指針

  • void指針:
    void * p;
  • 可以用任何類型的指針對 void 指針進行賦值或初始化:
double d = 1.54; 
void * p = & d;  //double *
void * p1;
p1 = & d;
  • 因 sizeof(void) 沒有定義,所以對於 void * 類型的指針p,
    *p 無定義
    ++p, --p, p += n, p+n,p-n 等均無定義

內存操作庫函數

  • memset
    頭文件cstring中聲明:
 void * memset(void * dest,int ch,int n);

將從dest開始的n個字節,都設置成ch。返回值是dest。ch只有最低的字節起作用(ch雖然是int類型(4個字節),但只有最低的字節起作用)。

例:將szName的前10個字符(一個字符佔一個字節),都設置成’a’:

char szName[200] = "";  //空串包含\0
memset( szName,'a',10); 
cout << szName << endl;

用memset函數將數組內容全部設置成0:

int a[100]; 
memset(a,0,sizeof(a));

則數組a的每個元素都變成0
memset的參數和返回值是void*,也就是他可以接收任意類型的數組或指針變量。逐字節初始化(不是逐元素)

  • memcpy
    頭文件cstring中聲明:
 void * memcpy(void * dest, void * src, int n);

將地址src開始的n個字節,拷貝到地址dest。返回值是dest。
將數組a1的內容拷貝到數組a2中去,結果是a2[0] = a1[0], a2[1] =a1[1]…a2[9] = a1[9] :

int a1[10];
int a2[10];
memcpy( a2, a1, 10*sizeof(int));

自己編寫memcpy:

void * MyMemcpy( void * dest , const void * src, int n)
{
	char * pDest = (char * )dest;  //逐字節拷貝 把void*轉換爲 char*
	char * pSrc = ( char * ) src; 
	for( int i = 0; i < n; ++i ) { //逐個字節拷貝源塊的內容到目的塊
		* (pDest + i) = * ( pSrc + i );
	}
	return dest;
}

有缺陷,在dest區間和src區間有重疊時可能出問題!!!

10. 函數指針

基本概念

程序運行期間,每個函數都會佔用一段連續的內存空間。而函數名就是該函數所佔內存區域的起始地址(也稱“入口地址”)。我們可以將函數的入口地址賦給一個指針變量 ,使該指針變量指向該函數(的首地址)。然後通過指針變量就可以調用這個函數。這種指向函數的指針變量稱爲“函數指針”。
在這裏插入圖片描述

定義形式

類型名 (* 指針變量名)(參數類型1, 參數類型2,…);
例如:

int (*pf)(int ,char);

表示pf是一個函數指針,它所指向的函數,返回值類型應是int,該函數應有兩個參數,第一個是int 類型,第二個是char類型。

使用方法

可以用一個原型匹配的函數的名字給一個函數指針賦值。
要通過函數指針調用它所指向的函數,寫法爲:
函數指針名(實參表);

#include <stdio.h>
void PrintMin(int a,int b) 
{
	if( a<b ) 
		printf("%d",a);
	else 
		printf("%d",b);
}
int main() {
	void (* pf)(int ,int); //定義一個函數指針
	int x = 4, y = 5;
	pf = PrintMin;  //用函數名字給函數指針賦值
	pf(x,y); //用函數指針調用他所指向的函數
	return 0; 
}

在這裏插入圖片描述
直接用函數名調用函數不更好嗎,上面的代碼爲什麼多此一舉,用函數指針呢?在上面的例子中確實冗餘了,其實函數指針有大用處,如下面的例子。

函數指針和qsort庫函數

C語言快速排序庫函數:
在這裏插入圖片描述
可以對任意類型的數組進行排序。其最後一個函數參數,就是一個函數指針(指向一個函數)。
在這裏插入圖片描述
對數組排序,需要知道:
1)數組起始地址(數組名或指向首地址的指針)
2) 數組元素的個數
3)每個元素的大小(由此可以算出每個元素的地址)
4)元素誰在前誰在後的規則 (最簡單有升序、降序,還有一些其他自定義的規則,比如對整數按個位、或十位數字的大小進行排序等)

在這裏插入圖片描述
base: 待排序數組的起始地址,
nelem: 待排序數組的元素個數,
width: 待排序數組的每個元素的大小(以字節爲單位)
pfCompare :比較函數的地址(把比較函數的名字賦給函數指針,指向比較函數)

在這裏插入圖片描述
比較函數是程序員自己編寫的(自定義比較規則)。

排序就是一個不斷比較並交換位置的過程。
qsort函數在執行期間,會通過pfCompare指針調用 “比較函數”,調用時將要比較的兩個元素的地址傳給“比較函數”,然後根據“比較函數”返回值判斷兩個元素哪個更應該排在前面。

在這裏插入圖片描述
比較函數編寫規則:

  1. 如果 * elem1應該排在 * elem2前面,則函數返回值是負整數
  2. 如果 * elem1和* elem2哪個排在前面都行,那麼函數返回0
  3. 如果 * elem1應該排在 * elem2後面,則函數返回值是正整數

實例

下面的程序,功能是調用qsort庫函數,將一個unsigned int數組按照個位數從小到大進行排序。比如 8,23,15三個數,按個位數從小到 大排序,就應該是 23,15,8

 
#include <cstdio>
#include <cstdlib>
using namespace std;
int MyCompare( const void * elem1, const void * elem2 ) 
{
	unsigned int * p1, * p2; //比較的是unsign int類型,所以定義兩個該類型的指針
	p1 = (unsigned int *) elem1; // “* elem1” 非法  因爲elem1是void*類型 強制類型轉換
	p2 = (unsigned int *) elem2; // “* elem2” 非法 
	return (* p1 % 10) - (* p2 % 10 );
}
#define NUM 5 
int main()
{
	unsigned int an[NUM] = { 8,123,11,10,4 }; 
	qsort(an,NUM,sizeof(unsigned int),MyCompare); 
	for( int i = 0;i < NUM; i ++ )
		printf("%d ",an[i]); 
	return 0;
} //10,11,123,4,8
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章