一、指針
1、指針的概念:用來保存地址的“變量”叫做指針,可以理解成指針是地址的一個別名。
例:定義一個整形指針
2、“指針的內容”,“指針所指向的內容”,“指針變量的地址”
(1)、指針的內容:
指針變量p裏面存放的是a的地址,也就是0x0018ff44.
(2)、指針所指向的內容:
指針變量p裏面存放的地址(0x18ff44)這塊空間所對應的值,也就是10,我們通過*p(解引用)可以訪問到這個值。即:*p作爲右值時,*p==10,當*p作爲左值時代表的就是a這塊空間。
(3)、指針的地址:
指針p本身有一個地址,由編譯器分配的我們不知道,注意:不要講指針變量p本身的地址和p所保存的地址(0x0018ff44)相混淆。
3、"未初始化的指針"和"NULL指針"
例:定義兩個指針變量p1和p2:
int *p1; //未初始化的指針
int *p2=NULL; //空指針
*p1=10;
*p2=10;
這樣賦值顯然是錯誤的,爲什麼呢???
試想一下,p1,p2是一個指針變量,所以p1,p2裏面應該存放的應該是一個地址。而對於p1我們沒有往裏面放地址,這時p1是隨機指向的。如果此時解引用*p1,訪問到的是塊隨機的地址,再修改這個隨機地址裏面的值,假設這個隨機空間裏面的值非常重要,那你就再也找不回來了,所以通常定義一個指針就將他初始化爲NULL。
對於p2:雖然我們將它初始化爲NULL,但它指向的是一塊空地址啊!!!向一塊空地址裏面放東西,根本是不可能的。
4、指針常量:
例:*(int *)200=10;
相信很多人對這個表達是都會產生疑惑,其實很好理解的,200是一個常數,我們將它強制轉化爲 int *(也就是將常數200轉化成一個整形地址),再對這塊地址進行*引用,就訪問到一塊空間,可以對這塊空間進行賦值。
5、常量指針
例:int *p=&100;
讓指針指向一個常量,一般用不到。
二、高級指針
1、二級指針概念:
什麼是指針???存放地址的變量就叫做指針,所以二級指針就是存放一級指針地址的指針變量。
注意:跟一級指針一樣,二級指針變量p2裏面存放的是一級指針變量p1的地址,一級指針變量p1裏面存放的是a的地址。要想訪問一塊地址裏面的內容可以使用間接訪問符“*”,所以:
*p2==&p1, *p2就是訪問p2這塊空間裏面存放的地址所對應的內容。
**p2==10,因爲*p2得到的結果是p1的地址,在對p1的地址進行解引用*p1就訪問到了10.
例1:分析下面幾種情況下能不能作爲可修改的左值(可修改的左值必須代表一塊空間)
int a=10;
int *cp=&a;
(1)*cp=20 //可以作爲左值,當cp指向a時,*cp放到等號左邊代表a這塊空間,當*cp放到等號右邊代表a這塊空間的值。
(2)&a=20 //錯誤,&a不可以作爲左值,因爲他不能表示一塊特定的空間,&a得到的結果是a的地址,但並不代表a這塊空間,要想使用這塊空間,必須進行*引用,*&a=20正確。&a可以作爲右值,代表是a的地址這個數值。
(3)*cp+1 //不可以作爲左值,因爲*優先級高於+,所以*cp先結合,再加1相當於10+1,不代表一塊空間。
(4)*(cp+1) //可以作爲左值,cp+1先結合,指向a的下一塊空間,再對這塊空間進行*引用,放在等號左邊就代表這塊空間。
(5)++cp //不可以作爲左值,因爲++cp只是將cp裏面的內容加一,並沒有進行*引用
(6)cp++ //不可以作爲左值
(7)*cp++ //可以作爲左值,先對cp裏面的地址進行*引用。再讓cp=cp+1(也就是讓cp指向a的下一塊空間,因爲++優先級高於*)
(8)++*cp //不可以作爲左值,*cp代表cp所指向的內容,再讓其加一,相當於10+1
注意:C中++cp和--cp都不能做左值,C++中++cp可以作爲左值。
例2:const 修飾一級指針,修飾二級指針(const修飾的變量,還是一個變量,只不過只是可讀的 )
int const a=10;
int b=30;
1、a=20; //錯誤,const修飾a,所以不能改變a的值
2、int const *p; //const修飾的是*p,所以不能改變*p
p=&a; //正確
*p=20; //錯誤 不能通過*p改變a的值
3、const int *p; //const修飾的是*p
p=&a; //正確
*p=20; //錯誤
4、int *const p=&a; //const修飾的是p
p=&b; //錯誤 不能改變p
*p=20; //正確
5、int const * const p; //const修飾*p,也修飾p,所以*p,p都不能改變
p=&b; //錯誤
*p=20; //錯誤
注意:const修飾變量的原則是離誰近修飾誰。const int *p與int const *p完全一樣。
2、指針和數組的關係 ,指針數組,數組指針,指針的運算
2.1、指針和數組的關係:
很多人都分不清指針和數組之間的關係,嚴格的來講指針和數組之間沒關係,指針是指針,數組是數組。只不過他們兩個都可以通過“*”引用的方式和下標的方式來訪問元素而已。
例1:
int a[5]={1,2,3,4,5};
int *p=a;
a[5]佔20個字節的大小,而p只佔4個字節的大小,其次p本身有自己的地址,只不過他裏面存放的是數組首元素的地址。
要訪問3則有兩種方式:a[2]或者*(a+2).
其中*(a+2)就是*的形式訪問的,因爲a表示首元素的地址,加2表示向後偏移2個整形大小,找到3的地址,在通過*得到3.
在編譯器中a[2]會被先解析成*(a+2)訪問的。
例2:
所以必須保持定義和聲明的一致性,指針就是指針,數組就是數組。
3、指針數組,數組指針
注意:[]的優先級高於*,指針數組是一個數組,只不過裏面的元素全部都是指針。數組指針是一個指針,指向數組的指針,偏移的單位是整個數組。
例1:
int a[6]={1,2,3,4,5,6};
int (*p2)[6];
p2=a;
這是錯誤的,因爲指針p2的類型是int [6],所以應該是p2=&a;
int (*p2)[3];
這樣的話p2的類型是int [3],所以p2=(int(*) [3])&a; 要強制轉換成數組指針的類型。
注意:數組指針“所指向”的類型就是去掉指針變量之後所剩下的內容。
數組指針的類型就是去掉指針後剩下的內容。
例2:int (*p3)[5];
p3的類型是 int(*)[5];
p3所指向的類型是 int [5];
4、指針的運算:
1、指針相減得到的結果是兩指針之間元素的個數,而不是字節數。
2、指針的運算
例1:
struct test
{
int name;
char *pcname;
short data;
char c[2];
}*p;
假設p的值是0x100000,結構體的大小是12;
則:
1、 p+0x1=0x10000c //p指向結構體 則p+1表示的是p+sizeof(test)*1
2、(unsigned int)p+0x1=0x100001 //p已經被轉化爲一個無符號的數,加一就相當於兩個數相加
3、(int *)p+0x1=0x100004 //p被轉化爲int *,所以p+1就表示p+sizeof(int *)*1
例2:假設當前機器小端存儲
int a[4] = { 1, 2, 3, 4 };
int *p = (int *)(a + 1);
int *p1 = (int *)(&a + 1);
int *p2 = (int *)(( int)&a + 1);
printf( "*p=%d,*p1=%d,*p2=%d\n" , *p, p1[-1],*p2);
輸出結果:*p=2,*p1=4,*p2=2000000
解析:p = (int *)(a + 1); a代表首元素地址,加1指向第二個元素,所以是2.
p1 = (int *)(&a + 1); &a表示數組的地址,&a+1就相當於&a+sizeof(a),p1指向的就是a[3]後面的地址,p1[-1]被解析成*(p1-1),所以是4.
p2 = (int *)(( int)&a + 1); (int)&a是把數組的地址取出來再轉化爲一個int類型的數再加1,這時的加1就相當於兩個數相加(效果相當於讓&a向後偏移一個字節),相加的結果再轉化成(int *)地址,使p2指向這個地址。
當前數組的存儲:01 00 00 00 02 00 00 00 ........... 因爲&a指向的是01這個字節的位置,(int)&a+1的結果是指向了01的下一個字節,這時在強制類型轉換成int *,則p2這時指向的空間的內容就是 00 00 00 02,因爲是小端存儲,所以大端就是 02 00 00 00,輸出後就是2000000.
5、二維數組與二級指針參數
(1)、二維數組做參數:
二維數組做參數與一維數組做參數一樣,傳遞的都是首元素的地址,只不過二維數組的每個元素又是一個一維數組 。
例:int arr[5][10];
這是一個5行10列的整形數組,可以將它看成一個只有5個元素的一維數組,只不過每個元素又是一個大小爲10的一維數組。
我們來分析一下當arr作爲實參是,需要什麼類型的形參來接受它???
arr作爲參數時,代表首元素地址,要接受這個地址必須是一個指針或者是一個數組,但是他的每個元素的類型是int[10]。
所以我們可以用一個指向類型爲int [10]的數組指針來接受arr[5][10],即:int (*p)[10] 。
同樣也可以用一個數組來接受arr,即:int arr1[][10],這裏第二維的大小不能省略,在這裏int arr1[][10] 也可以被解析成int (*arr1)[10];
(2)、二級指針做參數:
例:char *p[5];
這是一個大小爲5,每個元素都是char *類型的數組,當他作爲實參時,需要什麼樣類型的形參來接受他呢???
分析:既然p是一個數組,那麼數組在作爲實參時,實際上傳遞過去的是首元素的地址,但是p的每一個元素都是一個char *類型,也是一個地址,所以形參的類型應該是char **。
總結:
數組名只有在sizeof()和取&時纔不發生降級,其餘地方都代表首元素地址。
6、函數指針
(1)函數指針: 是一個指向函數的指針
聲明:
例:void (*p)(); 首先p是一個指針,它指向一個返回值爲空,參數爲空的函數。
初始化:
要明確,函數指針本質還是一個指針,既然是指針,那麼就必須給他進行初始化才能使用它,下面來看一看函數指針的初始化,定義一個返回值爲空參數爲空的函數:void fun()。
p=fun;
p=&fun;
這兩種初始化的方式都是正確的。因爲函數名在被編譯後其實就是一個地址,所以這兩種方式本質上沒有什麼區別。
調用:
p();
(*p)();
這兩種方式都是正確的。p裏面存的fun的地址,而fun與&fun又是一樣的。
例:分析一下(*(void (*) () ) 0 ) ()是個什麼東東!!!
首先,void (*) ();是一個返回值爲空,參數爲空的函數指針類型。
void (*) () 0 ; 它的作用是把0強制類型轉換成一個返回值爲空,參數爲空的函數指針。
(*(void (*) () )0) ; 找到保存在0地址處的函數。
(*(void (*) () ) 0 ) (); 對這個函數進行調用。
用途:
1、回調函數:用戶將一個函數指針作爲參數傳遞給其他函數,後者再“回調”用戶的函數,這種方式稱爲“回調函數”,如果想要你編寫的函數在不同的時候執行不同的工作,這時就可以使用回調函數。回調函數也算是c語言裏面爲數不多的一個高大上的東西。
2、轉換表:轉換表本質上是一個函數指針數組,說白了就是一個數組,只不過數組裏面存放的全部都是函數指針。
例:實現一個計算器
要求:有“+、-、*、/”四個功能。用戶輸入操作數,得出結果。
#include<stdio.h> #include<stdlib.h> #include<assert.h> int Add(int a, int b) { return a + b; } int Sub(int a, int b) { return a - b; } int Mul(int a, int b) { return a *b; } int Div(int a, int b) { assert(b != 0); return a / b; } int operator(int (*fun)(int,int)) //回調函數 { int a = 0; int b = 0; printf( "請輸入操作數:" ); scanf( "%d%d", &a, &b); return (*fun )(a,b); } int main() { printf( "*************************************\n" ); printf( "*0.exit 1.Add****\n" ); printf( "*2.Sub 3.Mul****\n" ); printf( "*4.Div *********\n" ); int(*fun[5])(int ,int); //轉換表 fun[1] = Add; fun[2] = Sub; fun[3] = Mul; fun[4] = Div; int input = 1; while (input) { printf( "請選擇> " ); scanf( "%d", &input); if (input<0 || input>4) { printf( "選擇無效\n" ); } else if (input == 0) { break; } else { int ret = operator(fun[input]); printf( "%d\n", ret); } } system( "pause"); return 0; }
在這個計算器程序中,就使用到了轉換表fun[]。fun[]裏面存放了加減乘除四個函數,根據input的不同,fun[input]調用不同的函數。這種方式與switch() case的功能比較相似,不過轉換表比switch()case更簡單。
(2)、函數指針數組:
函數指針數組是一個指針數組,也就是一個數組,只不過裏面存放的全都是函數指針。
聲明:例: char* (*p[5])(int,int);
p與[5]先結合成一個數組,所以這是一個大小爲5的數組,數組元素的類型是一個函數指針,這個函數指針指向一個返回值爲char *,有兩個參數,且參數類型爲int,int的數組。
可以這樣理解:char * (*)(int,int) p[5]; 其中char*(*)(int int)是p[5]的類型。
(3)、函數指針數組的指針:
函數指針數組的指針是一個數組指針,本質上是一個指針,只不過指向的是一個存放函數指針的數組。
聲明:例:char *(*(*p)[5])(int,int);
*與p先結合成一個指針,所以p是一個指針,指針所指向的是一個大小爲5的數組,這個數組存放的是函數指針,這些函數指針所指向的類型是返回值爲char *,有兩個int型參數的函數。
可以這樣理解: char *(*[5])(int,int) *p; p是一個指針,類型是char*(*[5])(int,int).
總結:指針數組,是一個數組,裏面存放的是指針。
數組指針,是一個指針,指向一個數組的指針。
函數指針數組,是一個數組,裏面存放的是函數指針。
函數指針數組指針,是一個指針,指向一個存放函數指針的數組。
其實規律很簡單,強調的都是最後兩個字,最後兩個字是什麼他就是什麼,而前面的字就是他的類型。
三、求內存和長度的差異:
1、整形數組:
int a[] = { 1, 2, 3, 4 };
printf( "%p\n",a); //a代表首元素地址
printf( "%p\n",a+1); //a+1表示第二個元素的地址
printf( "%p\n",&a); //&a代表整個數組的首地址
printf( "%p\n",&a+1); //&a代表數組的地址,&a+1則跳過整個數組
printf( "%d\n", sizeof (a)); //a在sizeof()中代表整個數組, 16
printf( "%d\n", sizeof (a + 0)); //代表&a[0],32位下所有的地址大小都爲4 4
printf( "%d\n", sizeof (*a)); //表示a[0],int佔4個字節 4
printf( "%d\n", sizeof (a + 1)); //表示&a[1],32位下所有的地址大小都爲4 4
printf( "%d\n", sizeof (a[1])); //表示a[1],int佔4個字節 4
printf( "%d\n", sizeof (&a)); //代表整個數組的地址,32位下所有的地址大小都爲4 4
printf( "%d\n", sizeof (&a + 1)); //因爲&a代表整個數組地址,&a+1跳過整個數組,但還是一個地址
printf( "%d\n", sizeof (&a[0])); //表示a[0]的地址 4
printf( "%d\n", sizeof (&a[0] + 1)); //表示a[1]的地址 4
printf( "%d\n", sizeof (*&a)); //&a代表整個數組,再*引用訪問這塊地址,就相當於求取真個數組的大小 16
2、字符數組:
char name[] = "abcdef" ; //這時字符串不代表首元素地址,而是字符數組的一種初始化方式,並且字符串總是默認以’\0‘結尾
printf( "%d\n", sizeof (name[0])); //表示a,32位下char大小爲1 1
printf( "%d\n", sizeof (&name)); //表示整個數組的地址,32位下地址就是4 4
printf( "%d\n", sizeof (*name)); //表示 a 1
printf( "%d\n", sizeof (&name+1)); //表示指向整個數組之後的一塊空間,但還是一個地址 4
printf( "%d\n", sizeof (name+1)); //表示b的地址 4
printf( "%d\n", sizeof (name)); //代表整個數組,還要加上‘\0' 7
printf( "%d\n", strlen(name)); //求取字符串長度,不包括’\0‘ 6
printf( "%d\n", strlen(&name)); //代表整個數組的地址,&name==name,strlen遇到’\0‘停下 6
printf( "%d\n", strlen(&name + 1)); //是一個隨機值,表示指向整個數組之後的一個地址,從這個地址開始向後尋找'\0',因爲’\0‘位置不確定所以是隨機值
printf( "%d\n", strlen(name + 1)); //表示b的地址
3、字符指針:
char *name = "abcdef" ; //字符串放在等號右邊代表首元素地址
printf( "%d\n", sizeof (name[0])); //以下標形式訪問,代表 a 1
printf( "%d\n", sizeof (&name)); //表示字符指針name的地址 4
printf( "%d\n", sizeof (*name)); //通過*訪問,表示a 1
printf( "%d\n", sizeof (&name+1)); //表示指針name之後的一塊地址 4
printf( "%d\n", sizeof (name+1)); //表示b的地址 4
printf( "%d\n", sizeof (name)); //代表a的地址, 4
printf( "%d\n", strlen(name)); //代表字符串首元素地址 6
printf( "%d\n", strlen(&name)); //表示指針name本身地址,因爲從name開始向後’\0‘的位置不確定,所以是個隨機值
printf( "%d\n", strlen(&name + 1)); //表示指針name本身地址之後的地址,因爲’\0‘的位置不確定,所以是個隨機值
printf( "%d\n", strlen(name + 1)); //代表b的地址 5