指針
指針是c語言裡面最抽象的、最重要的、最常用的。
指針的概念:
指針變量也是一個變量,
指針存放的內容是一個地址,該地址指向一塊內存空間,
指針是一種數據類型(指針類型)。
--------------------------------------
“我是一名從事了10年開發的老程序員,最近我花了一些時間整理關於C語言、C++,自己有做的材料的整合,一個完整的學習C語言、C++的路線,學習材料和工具。進企鵝裙學習(
計算機內存的最小單位是什麼?BYTE(字節)
對於內存,每個BYTE(字節)都有一個唯一不同的編號,這個編號就是內存地址。
操作系統就給內存的每一個字節編了一個號,所以說:一個編號對應的是一個BYTE(字節)的空間大小。
打比方:
1 -> BYTE
2 -> BYTE
3 -> BYTE
4 -> BYTE
對應於
--------------------------------------
一個int多大?答:4個BYTE(字節),所以一個int佔用了了4個編號(即4個不同的內存地址)。
地址的編號:在32位系統下是一個4個字節的無符號整數;在64位系統下是一個8個字節的無符號整數。
(因為地址不可能是負的,又因為無符號數可以表達一個更大的地址,有符號數表示的最大地址會變小)
-----------------------------------------------------------------------------
指針變量的定義:
可以定義一個指向一個變量的指針變量。
-----------------------------------------------------------------------------
取地址運算符 &
& 可以取得一個變量在內存當中的地址。(取地址取的是內存地址)
register int a;//寄存器變量,這種變量不在內存裡面,而在cpu裡面,所以是沒有地址的,
所以寄存器變量不能使用&來得到地址。
-----------------------------------------------------------------------------
無類型指針
定義一個指針變量,但不指定它指向具體哪種數據類型。可以通過強制轉化將 void * 轉化為其他類型指針,
也可以用 (void *) 將其他類型強制轉化為void類型指針。
void *p; 指針之間賦值需要類型相同,但任何類型的指針都可以賦值給 void * 。
-----------------------------------------------------------------------------
linux下示例代碼如下:
int main()
{
int*p;//定義了一個可以指向int類型地址的指針變量,指針變量的名字叫p。*不是指針變量名字的一部分。
//int * 是一種數據類型。
inta;//定義了一個int類型的變量,int變量的名字叫a。
a =1;//int * 和 int是兩種不同的數據類型。
p = &a;//把a的內存地址賦值給指針變量p。
printf("%p\\n", p);//0x7fff5b2faedc 輸出的是a的首地址的編號,不會把四個編號都輸出的。
//而且注意:每一次執行該代碼後,輸出的編號都會發生變化!
*p =10;//通過指針變量間接的訪問a的值,*p代表指針指向變量的值,p代表指向變量的地址。
printf("a = %d\\n", a);//a = 10; 通過上面的方法把a的值改變了。
a =100;19printf("%d\\n", *p);//100 通過指針變量間接的訪問a的值。
intb =2;
p = &b;//又把b的內存地址賦值給p。
*p =20;
printf("b = %d\\n", b);//20
//char *p1 = &a; //相當於 char *p1; p1 = &a;//兩個類型不相同的地址。即指針類型不兼容。那麼我們強轉試試!
char*p1 = (char*)&a;
a =123456;
*p1 =0;
printf("a = %d\\n", a);//a = 123392 就算強轉後也會出現問題,所以要避免指針類型不兼容問題。
void*p2;//可以指向任何類型的地址,void代表無類型。
return0;35}
-----------------------------------------------------------------------------
指針佔用內存的說明
在同一個系統下,不管指針指向什麼樣類型的變量,地址的大小(或叫編號的大小)總是一樣的。
linux下示例代碼如下:
int main()
{
char*p1;
int*p2;
longlong*p3;
printf("%lu, %lu, %lu\\n",sizeof(p1),sizeof(p2),sizeof(p3));//實質是:編號的大小是多少?
return0;//輸出的是 8, 8, 8
//地址的編號:在32位系統下是一個4個字節的無符號整數;在64位系統下是一個8個字節的無符號整數。
//指針變量的名字叫p1、p2、p3。指針變量的大小是多大呢?因為指針變量對應的是某某的首地址的編號,
//即指針變量對應的是編號,而編號就是內存地址。即編號在64位系統下是一個8個字節的無符號整數。
//所以指針變量的大小就是編號的大小,而編號在64位系統下用8個字節的無符號整數表示。
//舉例子說明下:同一個酒店,房間的編號的長度都是一樣的。
}
--------------------------------------
再比如:
linux下示例代碼如下:
#include
int main()
{
int*p1;
inta =0;
p1 = &a;
*p1 =10;
//p1 = 10;
int*p2;
p2 = &a;
//*p2是什麼?不管是*p1還是*p2都代表變量a的值,但p1和p2確實是兩個不同的指針變量。
return0;
}
畫圖說明如下:
=============================================================================
野指針 與 空指針
野指針:沒有指向任何有效地址的指針變量,所以在代碼中避免出現野指針,
如果一個指針不能確定指向任何一個變量地址,那麼就將這個指針變成空指針。
linux下示例代碼如下:
#include
int main()
{
int*p;
*p =100;//不能這樣寫,沒有初始化過值的指針,這種指針叫野指針。
return0;//因為地址編號所佔用的內存不是你程序要調用的內存。對於操作系統而言,不是你的內存你就不能改!
//如果你非要改的話,操作系統就會發現你在做非法操作,會直接把你清理出去了。即程序出錯。
}
編譯上段程序沒有錯誤,運行上段程序會出現一個錯誤:Segmentation fault(段錯誤,也即分段故障)
-----------------------------------------------------------------------------
空指針:就是指向了NULL的指針變量。
linux下示例代碼如下:
#include
int main()
{
int*p;//兩句代碼相當於一句:int *p = NULL;
p = NULL;//如果一個指針變量沒有明確的指向一塊內存,那麼就把這個指針變量指向NULL。
//這個指針就是空指針,空指針是合法的。
//實際上NULL並不是c語言的關鍵字,NULL在c語言中的定義是:#define NULL 0
//NULL在c語言裡面就是一個宏常量,值是0。那麼我們為什麼不直接寫0呢?
//NULL代表的是空指針,而不是一個整數零,這樣看的會舒服些。(這只是粗淺易懂的解釋)
return0;
}
程序中不要出現野指針,但可以出現空指針。
--------------------------------------
空指針理解的擴展:
注意:
inta =0;
int*p = &a;//相當於 int *p; p = &a;
int*node = NULL;//相當於:int *node; node = NULL;
NULL就是系統定義特殊的0,把你初始化的指針指向它,可以防止“野指針”的惡果。
NULL是個好東西,給一出生的指針一個安分的家。
--------------------------------------
用C語言編程不能不說指針,說道指針又不能不提NULL,那麼NULL究竟是個什麼東西呢? C語言中又定義,定義如下:
#undefNULL
#ifdefined(__cplusplus)
#defineNULL 0
#else
#defineNULL ((void *)0)
#endif
所以我覺得,如果一個指針被賦予NULL,應該就相當於這個指針執行了0x0000這個邏輯地址,
但是C語言中0x0000這個邏輯地址用戶是不能使用的,
(有些人說是因為0x0000沒有映射到物理地址,也有人說是因為0x0000映射到的地址是操作系統用於判斷野指針的,我也不太懂,總之就是用戶不能使用啦)
所以當你試圖取一個指向了NULL的指針的內容(或者叫值)時,就會提示段錯誤,聽著有點繞,看程序:
1int*node = NULL;2inta =0;3a = *node;//*node的意思是:取指針變量node的值。然後賦值給a。45printf("%d\\n", a);
*node的意思是:取指針變量node的值,也就是邏輯地址0x0000,而這個地址是不能被訪問的(即不能被取出來的),
c語言語法上沒有問題,所以編譯器編譯沒有問題,但是編譯器編譯後運行會出現段錯誤。
linux下示例代碼如下:
#include
int main() {
int*p = NULL;//相當於 int *p = 0; 但一般不這麼寫啊!
inta =0;
a = *p;
printf("%d\\n", a);
return0;
}
root@iZ2zeeailqvwws5dcuivdbZ:~/1/01/指針# gcc -o p6 p6.c
root@iZ2zeeailqvwws5dcuivdbZ:~/1/01/指針# p6
Segmentation fault(段錯誤,也即分段故障)
root@iZ2zeeailqvwws5dcuivdbZ:~/1/01/指針#
=============================================================================
指針的兼容性(即指針類型之間一定要匹配)
指針之間賦值比普通數據類型賦值檢查更為嚴格,例如:不可以把一個 double * 賦值給int。
#include
int main()
{
int*p;
charb =1;
p = &b;//指針類型之間一定要匹配,不然會有警告,強行運行的話,結果不可控!
return0;10}
警告如下:
warning: assignment from incompatible pointer type [-Wincompatible-pointer-types]
警告:不兼容的指針類型分配[-Wincompatible-pointer-types]
=============================================================================
我們不要把指針想象的特別神秘!其實指針變量也是一個變量。
它裡面放的就是一個地址的編號,地址的編號就是一個8個字節的無符號的整數(64位系統下)。
區別是:這個整數不能直接賦值,而是來自於對另外一個變量的取地址操作而得到!
=============================================================================
不同的數據類型在內存中佔用的地址
我們先看幾個現象:
每一次編譯後執行,輸出的地址會發生變化,但是相鄰地址間的間隔不變。
--------------------------------------
再比如:
每一次編譯後執行,輸出的地址會發生變化,但是相鄰地址間的間隔不變。
其餘的類型就不一一舉例啦!
=============================================================================
指向常量的指針 和 指針常量
const int *p; //定義一個指向常量的指針。
int *const p; //定義一個指針常量,一旦指向某一變量的地址後,不可再指向其他變量的地址。(注意:指針常量也叫常量指針)
二者區別:
const int *p;//p是一個變量,但指向一個常量。(即p可以指向任何地址,但是隻能通過*p來讀這塊地址的內容,不能通過*p來寫這塊地址的內容)
int *const p;//p是一個常量,但指向一個變量或者常量。(即如果一旦p指向了任何一個有效的地址後,就不可再指向其他變量的地址,但可以通過*p來讀寫這塊地址的內容)
--------------------------------------
linux下示例代碼如下:
=============================================================================
指針與數組的關係
--------------------------------------
一級指針畫圖小說明如下:
=============================================================================
指針運算
指針變量可以進行計算,如果是 int * 類型每加一,變化4個整數;
如果是 char * 類型每加一,變化1個整數。其他類型以此類推。
linux下示例代碼如下:
#include
int main()
{
inta =0;
int*p = &a;
printf("%p, %p, %p\\n", p, p +1, p +2);//0x7fff5c2a518c, 0x7fff5c2a5190, 0x7fff5c2a5194
return0;
}
-----------------------------------------------------------------------------
指針運算小例子:
linux下示例代碼如下:
#include
int main()
{
inta[10] = {0 };
int*p = a;
p +=5; 9*p =1;
p -=2;12*p =3;
int i;
for(i =0; i <10; i++)
{
printf("a[%d] = %d\\n", i, a[i]);
}
return0;
}
輸出的是:
a[0] =0
a[1] =0
a[2] =0
a[3] =3
a[4] =0
a[5] =1
a[6] =0
a[7] =0
a[8] =0
a[9] =0
-----------------------------------------------------------------------------
通過指針使用數組元素
linux下示例代碼如下:
#include
int main()
{
inta[10] = {1,2,3,4,5,6,7,8,9,10 };
int*p = a;//指針p指向a的首地址。
p[3] =100;//等價於 *(p + 3) = 100; 一般寫成左邊那樣。
printf("a[%d] = %d\\n", i, a[i]);
int i;
for(i =0; i <10; i++)
{
}
return0;
}
=============================================================================
不同類型的指針的區別以及與數組的關係
極端例子如下:
linux下示例代碼如下:
#include
int main()
{
inta =0x12345678;
char*p = (char*)&a;
printf("%x, %x, %x, %x, %x\\n", *p, p[0], p[1], p[2], p[3]);//%x的意思是按照十六進制的有符號整數輸出(小寫)
printf("--------------------\\n");
*p =0;
p[3] =0;
printf("%08x\\n", a);
printf("--------------------\\n");
charb[20] = {0 };
int*p1 = (int*)&b;
p1[3] =0x12345678;
int i;
for(i =0; i <20; i++)
{
printf("b[%d] = %x\\n", i, b[i]);
}
printf("--------------------\\n");
return0;
}
輸出的結果是:
78,78,56,34,12
--------------------
0034560033
--------------------
b[0] =0
b[1] =0
b[2] =0
b[3] =0
b[4] =0
b[5] =0
b[6] =0
b[7] =0
b[8] =0
b[9] =0
b[10] =0
b[11] =0
b[12] =78
b[13] =56
b[14] =34
b[15] =12
b[16] =0
b[17] =0
b[18] =0
b[19] =0
--------------------
說明:小端對齊輸出。
小結:c語言中所有的數據類型都可以理解為一個char的數組。
-----------------------------------------------------------------------------
小案例:int類型與ip地址的對應關係。
實際上的ip地址是一個無符號的整數構成的。1個int,4個字節。
1、把整數轉換為ip地址
ip地址的格式:
0.0.0.0 ~ 255.255.255.255
linux下示例代碼如下:
-----------------------------------------------------------------------------
2、把ip地址轉換為整數
輸入一個ip地址
char a[100] = "192.168.2.5"
把這個ip轉化為unsigned int類型的整數。
linux下示例代碼如下:
-----------------------------------------------------------------------------
3、使用指針給二維數組排序
linux下示例代碼如下:
#include
int main()
{
chara[2][5] = { {4,3,5,9,78}, {52,21,5,6,4 } };
char*p = (char*)a;
int i, j;
for(i =0; i <10; i++)
{
for(j =1; j <10- i; j++)
{
if(p[j] < p[j -1])
{
chartmp = p[j];
p[j] = p[j -1];
p[j -1] = tmp;
}
}
}
for(i =0; i <2; i++)
{
for(j =0; j <5; j++)
{
printf("%d\\n", a[i][j]);
}
}
return0;
}
輸出的結果是:
3
4
4
5
5
6
9
21
52
78
=============================================================================
指針數組
linux下示例代碼如下:
-----------------------------------------------------------------------------
linux下示例代碼如下:
#include
int main()
{
int*a[10] = { NULL };//定義了一個指針數組,指針數組的名字叫a,每 個成員是int *類型的,一共有10個成員。
intb, c, d;//對於指針數組來說,要先有指針的性質,再有數組的性質, 即先得獲得地址,然後對數組進行操作。
a[0] = &b;//a是指針數組的名字。
a[1] = &c;//a[0]是指針變量,
a[2] = &d;
*a[0] =10;//*a[0]是一個數組的成員之一。
printf("%d\\n", b);
return0;17}
=============================================================================
二級指針(指向指針的指針)
指針是一個變量,既然是變量就也存在內存地址,所以可以定義一個指向指針的指針。
linux下示例代碼如下:
二級指針說明圖如下:
三級指針及其以上指針
linux下示例代碼如下:
linux下示例代碼如下圖所示:
特別注意:
能用一級指針解決的問題不要用二級指針,能用二級指針解決的不用三級指針,指針級數過多會導致程序很複雜。
工作中大量使用的是一級指針,二級指針也很常用,三級指針就很罕見了,四級指針幾乎沒有。但筆試會考你哦!可以畫圖解決!
=============================================================================
函數的參數為指針變量(指針變量作為函數的參數)
實際上指針更多的時候用在函數的參數上。
函數的參數可以使是指針類型。它的作用是將一個變量的地址編號傳送給另一個函數。
void test(int *p); //定義一個函數,形參是int *類型。
c語言中如果想通過在一個函數的內部修改外部實參的值,那麼就需要給函數的參數傳遞這個實參的地址。
-----------------------------------------------------------------------------
linux下示例代碼如下:
#include
voidswap(inta,intb)//swap是交換的意思。
{
inttmp = a;
a = b;
b = tmp;
printf("a = %d, b = %d\\n", a, b);//a = 2, b = 1 形參中的值發生變化了。
}
int main()
{
inta =1;
intb =2;
swap(a, b);
printf("a = %d, b = %d\\n", a, b);//a = 1, b = 2 c語言中實參的值會傳給形參,而形參值的改變並不會影響到實參。
return0;
}
----------------------------------------------------------------------------
那麼現在我就想在一個函數的內部修改外部實參的值,那麼就需要給函數的參數傳遞這個實參的地址。代碼如下:
#include
voidswap(int*a,int*b)
{
inttmp = *a;
*a = *b;
*b = tmp;
printf("a = %d, b = %d\\n", a, b);//a = 2, b = 1 形參中的值發生變化了。
}
int main()
{
inta =1;
intb =2;
swap(&a, &b);//那麼現在我就想在一個函數的內部修改外部實參的值,那麼就需要給函數的參數傳遞這個實參的地址。
printf("a = %d, b = %d\\n", a, b);//a = 2, b = 1 實參中的值發生變化了。return0;
}
函數參數是指針變量的畫圖說明如下:
即:c語言想通過函數內部來修改實參的值,只能給函數傳遞實參的地址來間接的修改實參的值。
例如scanf函數:
int a;
scanf("%d", &a);//scanf是一個函數,現在要通過函數內部來修改實參a的值,只能用傳遞a的地址的方式修改a的值
=============================================================================
函數的參數為數組名時(即數組名作為函數的參數)
當一個數組名作為函數的形參的時候,c語言將數組名解釋為指針變量,其實是一個指針變量名。
如果數組名作為函數的參數,那麼這個就不是數組名了,而是一個指針變量名。
當把一個數組名作為函數的參數時,修改形參的值的時候,同時也影響實參的數組成員的值。
如果把一個數組名作為函數的參數,那麼在函數內部就不知道這個數組的元素個數了,需要再增加一個參數來標明這個數組的大小。
如果將一個數組作為函數的形參進行傳遞,那麼數組的內容可以在被調用函數的內部進行修改,
有時候不希望這樣的事情發生,所以要對形參採用const進行修飾。
-----------------------------------------------------------------------------
linux下示例代碼如下:
=============================================================================
函數的返回值為指針時(即指針作為函數的返回值)
int*test()//函數的返回值類型是指針類型(具體的講解在下一節:內存管理)
{
return NULL;
}
=============================================================================
幾個c語言的庫函數:memset、memcpy、memmove函數,使用的時候需要包含頭文件 #include <string.h>
這三個函數分別實現內存設置、內存複製、內存移動功能。
--------------------------------------
memset的功能是:將指定區域的內存置空(設置為0)。
void *memset(void *s, int c, size_t n);
第一個參數是:指定要置空的內存的首地址;
第二個參數是:要設置的值,一般寫0;
第三個參數是:這塊內存的大小,單位:字節。
linux下示例代碼如下:
-----------------------------------------------------------------------------
memcpy功能是:兩塊內存之間拷貝數據。
使用memcpy時,首先一定要確保內存沒有重疊區域。
void *memcpy(void *dest, const void *src, size_t n);
第一個參數是:目標地址(目標內存首地址);
第二個參數是:源地址(源內存首地址);
第三個參數是:拷貝多少內容,單位字節。
linux下示例代碼如下:
--------------------------------------
linux下示例代碼如下:
內存拷貝說明畫圖如下:
-----------------------------------------------------------------------------
memmove功能是:內存移動,參數與memcpy一致。
void *memmove(void *dest, const void *src, size_t n);
第一個參數是:目標地址(目標內存首地址);
第二個參數是:源地址(源內存首地址);
第三個參數是:拷貝多少內容,單位字節。
內存重疊區域說明如下圖所示:
=============================================================================
指針小結
定義 說明
int i; 定義個一個int類型的變量
int *p; 定義一個指向int類型的指針變量
int a[10]; 定義一個int類型的數組
int *p[10]; 定義一個指針數組,其中每一個數組元素指向一個int類型變量的地址
int func(); 定義一個函數,返回值類型為int類型
int *func(); 定義一個函數,返回值類型為int*類型
int **p; 定義一個指向int類型的指針的指針,二級指針
=============================================================================
字符指針 與 字符串
在c語言中,大多數的字符串操作其實就是指針操作。
--------------------------------------
1、通過指針訪問字符串數組
linux下示例代碼如下:
-----------------------------------------------------------------------------
通過指針使得字符串逆置
法一:使用一個指針
linux下示例代碼如下:
--------------------------------------
法二:使用二個指針
linux下示例代碼如下:
=============================================================================
函數的參數為char *(即char *作為函數的參數)
linux下示例代碼如下:
如果將一個數組作為函數的形參進行傳遞,那麼數組的內容可以在被調用函數的內部進行修改,
有時候不希望這樣的事情發生,所以要對形參採用const進行修飾。代碼如下:
voidtest(constchar*a)
{
printf("%s\\n", a);
//a[3] = '4';
}
-----------------------------------------------------------------------------
自定義函數實現求字符串長度和字符串拷貝
linux下示例代碼如下:
-----------------------------------------------------------------------------
如果一個數組作為函數的參數,那麼數組的成員數量在函數內部是不可見的。
解決方法:在傳遞一個數組的時候,需要同時提供另外一個參數,標明這個數組有幾個成員變量。
例外:如果函數的參數是一個字符串時,那麼並不需要再傳遞一個參數說明這個字符串有多長。
linux下示例代碼如下:
=============================================================================
指針數組作為main函數的形參
先來看一個指針數組作為函數的參數(此時把指針數組解釋為二級指針)
linux下示例代碼如下:
輸出結果是:
32,8
hello
h
abc
a
world
w
haha
h
-----------------------------------------------------------------------------
linux下示例代碼如下:
args是命令行參數的字符串數組,argc代表命令行參數的數量,程序名字本身就算一個參數。
main函數是由系統調用的,所以main函數的參數功能是:得到命令行的參數。
-----------------------------------------------------------------------------
舉個小例子:用到main函數的參數,實現計算兩個數的和
例如:
程序名 數1 數2
一回車結果就出來了。
a 15 45
60
linux下示例代碼如下:
-----------------------------------------------------------------------------
課後作業
寫一個程序,需要用到main函數的參數
例如:
程序名 整數1 運算符 整數2,程序運行的結果是計算結果。
a 5 + 6注意:中間的加號是字符串。
11
a 5 * 6
30
......
+ - * /都要實現
linux下示例代碼如下:
閱讀更多 虎牙來了 的文章