五大板块(2)—— 指针
參考:五大板塊(2)—— 指針
作者:丶PURSUING
發布時間: 2021-03-18 16:01:22
網址:https://blog.csdn.net/weixin_44742824/article/details/114981482
本文為學習筆記,整合課程內容及文章如下:
在 為什么要使用指針 板塊:
參考:原文鏈接
作者:Cloudkip
目錄
- 一、地址的引入
- 概念
- 地址長啥樣?
- 二、指針變量的引入
- 三、指針分類型與指針偏移量
- 整型指針,字符指針
- 函數指針(重點)
- 無參無返的函數指針
- 有參有返的函數指針
- 結構體中的函數指針
- 規律總結
- 數組指針(少用)
- 指針數組(少用)
- 結構體指針(重點)
- 定義賦值調用與指針偏移
- 實際應用例子
- 野指針
- 是什么
- 野指針是怎樣生成的?
- 有什么危害
- 如何避免
- malloc與內存泄漏
- 指針類型小測試
- 四、指針也可以作為數組傳入的形式參數
- 五、為什么要使用指針?
- (1)節省內存
- (2)在子函數中修改被傳遞過來的對象
- (3)動態分配內存
- (4)在指定地址寫入數據(目前少用)
- (5)函數多個返回值
指針,你說他難吧,其實都是在學校學的時候給勸退了,其實很簡單,而且學會了之后老是想用,也很好用,以后看到指針就不會有心理障礙啦。
一、地址的引入
概念
地址是一個十六進制表示的整數,用來映射一塊空間,是系統用來查找數據位置的依據。地址標識了存儲單元空間,而字節就是最小的存儲單位。
按字節的編址方式:每一個字節都有一個唯一的地址。例如:一個int型的變量是4個字節,就會對應4個地址,我們只需要取到這個變量的首地址就能得到完整的int型數據。
地址長啥樣?
用一個例子感受變量存放的地址:
#include <stdio.h>int main() {int a=10;int b=11;int* p=&a;int* p2=&b;printf("a的地址是:%p\n",p);printf("b的地址是:%p\n",p2);return 0; }結果:可以發現兩者地址相差4個字節,說明int型變量用4個字節的空間存放
a的地址是:0x7ea5d1dc b的地址是:0x7ea5d1d8- 1
- 2
大概可以表示為:
二、指針變量的引入
什么是指針?從根本上看,指針是一個值為內存地址的變量。
正如char型變量存放字符,int型變量存放整型數一樣,指針變量存放的是地址,沒有什么難理解的。
給指針賦值就是讓其指向一個地址。
三、指針分類型與指針偏移量
用sizeof發現linux下所有指針類型的大小均為8字節。
平臺:樹莓派
首先明白:指針所占用的空間與指針指向的內容和內容的大小無關。
其次明白:在不同的操作系統及編譯環境下,指針類型所占用的字節數是不同的
例如:
編譯生成16位的代碼時,指針占2個字節
編譯生成32位的代碼時,指針占4個字節
編譯生成64位的代碼時,指針占8個字節
整型指針,字符指針
#include <stdio.h>int main() {int a = 5;char b = 'A';int *pa = &a;//存放整型數的指針叫整型指針char *pb = &b;//而這叫字符型指針//printf("int型指針 pa 的地址是%p,指針偏移(++pa)的地址是:%p\n",pa,++pa);//printf("char型指針 pb 的地址是%p,指針偏移(++pb)的地址是:%p\n",pb,++pb);printf("int 型指針pa的地址是%p\n",pa);printf("int 型指針偏移(++pa)后的地址是:%p\n\n",++pa);printf("char 型指針pb的地址是%p\n",pb);printf("char 型指針偏移(++pb)后的地址是:%p\n",++pb);return 0;}結果:可以看到指針類型不同,其每次偏移的地址量也不同。
pi@raspberrypi:~/Desktop $ ./a.out int 型指針pa的地址是0x7ead81ec int 型指針偏移(++pa)后的地址是:0x7ead81f0char 型指針pb的地址是0x7ead81eb char 型指針偏移(++pb)后的地址是:0x7ead81ec不知道你會不會思考,為什么我不使用代碼中被注釋的兩條語句,簡短明了,而要使用4條printf。
你盡管試試,打印出來的pa和++pa是一樣的,好似是地址沒有偏移,這其實關系到了printf的出棧入棧問題,放在六、其他小知識點:printf 里的 a++,++a,真的有鬼!! 中詳細展開。
函數指針(重點)
顧名思義,指向函數地址的指針。
無參無返的函數指針
這是函數指針最簡單的一種形式
#include <stdio.h>void print()//要被指向的函數 {printf("hello world\n"); }int main() {void (*pprint)() = NULL;//定義函數指針pprint = print; //函數指針賦值:指向函數的首地址(就是函數名)//如同數組的首地址,是數組名pprint(); //調用方法1(*pprint)(); //調用方法2printf("函數指針pprint的地址是%p\n",pprint);printf("函數指針偏移(++pprint)后的地址是:%p\n",++pprint);return 0; }結果:
hello world hello world 函數指針pprint的地址是0x1046c 函數指針偏移(++pprint)后的地址是:0x1046d- 1
- 2
- 3
- 4
有參有返的函數指針
稍微上升點難度
#include <stdio.h>int sum(int a,int b)//要被指向的函數 {int c = 0;c = a+b;return c; }int main() {int total1;int total2;int (*psum)(int a,int b) = NULL;//定義函數指針psum = sum; //函數指針賦值,指向函數的首地址(就是函數名)//如同數組的首地址,是數組名total1 = psum(5,6); //調用方法1total2 = (*psum)(6,9);//調用方法2printf("total1:%d\ntotal2:%d\n",total1,total2);printf("%p\n",psum);printf("%p\n",++psum);return 0; }結果:
total1:11 total2:15 0x10440 0x10441- 1
- 2
- 3
- 4
結構體中的函數指針
比較常見的還是和結構體的結合,這個容易看花眼。
#include <stdio.h> #include <string.h>struct student {int english;int japanese;int math;int chinese;char name[128];int (*pLanguage)(int english,int japanese);//順便復習函數指針怎么使用};int Language(int eng,int jap)//函數指針所指向的函數 {int total;total = eng + jap;return total; }int main() {int lanSum;struct student stu1 = {.japanese = 90,.english = 100,.math = 90,.name = "華天朱",.pLanguage = Language,};lanSum = stu1.pLanguage(stu1.english,stu1.japanese);printf("stu1的名字是%s,他的語言綜合分數是%d\n",stu1.name,lanSum);printf("%p\n",stu1.pLanguage);printf("%p\n",++stu1.pLanguage);return 0; }結果:
stu1的名字是華天朱,他的語言綜合分數是190 0x10470 0x10471- 1
- 2
- 3
規律總結
函數指針無非就三步走:
定義
類型 (*指針名)();
- 1
兩個括號很好記
賦值
指針名 = 函數名
- 1
調用
如有參數則調用時傳
- 1
- 2
數組指針(少用)
顧名思義,就是指向數組地址的指針。
目前還沒碰到數組指針的使用,涉及即更新
#include <stdio.h>int main() {int a[3] = {1,2,3};int (*p)[3] = NULL;p = a;printf("%p\n",p);printf("%p\n",++p);return 0; }結果:偏移了整個數組的大小12字節
0x7ede61e8 0x7ede61f4- 1
- 2
指針數組(少用)
存放一系列指針的數組,本質是數組。
#include <stdio.h>int main() {int a=1;int b=2;int c=3;int *parray[3] = {&a,&b,&c};int i;printf("指針數組的第一個元素是:%p,地址的內容是:%d\n",parray[0],*parray[0]);return 0; }結果:
指針數組的第一個元素是:0x7ed501f4,地址的內容是:1- 1
結構體指針(重點)
定義賦值調用與指針偏移
#include <stdio.h> #include <stdlib.h> #include <string.h>struct STU {int score;char name[128]; };int main() {struct STU stu1 = {.score = 99,.name = "果粒臣",};//malloc為結構體指針開辟一塊空間struct STU *pstu = (struct STU *)malloc(sizeof(struct STU));//結構體指針的賦值1:直接賦值,在此之前要開辟空間pstu->score = 100;strcpy(pstu->name,"華天朱");printf("%s:%d\n",pstu->name,pstu->score);free(pstu);//釋放指針,重新指向一段地址//結構體指針的賦值2:指向結構體變量的地址pstu = &stu1;printf("%s:%d\n",pstu->name,pstu->score);//指針偏移printf("%p\n",pstu);printf("%p\n",++pstu);return 0; }結果:結構體偏移了4+128個字節
華天朱:100 果粒臣:99 0x7e905170 0x7e9051f4- 1
- 2
- 3
- 4
實際應用例子
用一個結構體指針做一個最簡單的學生成績管理。
#include <stdio.h> #include <stdlib.h> #include <string.h>struct stud {char* name;int score; };int main() {int num;int i;printf("需要錄入幾個學生的成績?\n");scanf("%d",&num);//這里開辟了num個結構體所需要的空間,動態分配內存struct stud *pstu = (struct stud *)malloc(sizeof(struct stud)*num);for(i=0;i<num;i++){pstu->name = (char* )malloc(sizeof(char)*16);memset(pstu->name,0,sizeof(char)*16);printf("請輸入第%d個學生的名字\n",i+1);scanf("%s",pstu->name);printf("請輸入第%d個學生的成績\n",i+1);scanf("%d",&pstu->score);pstu++;}pstu -= num;//指針回頭for(i=0;i<num;i++){printf("%s:%d\n",pstu->name,pstu->score);pstu++;}return 0; }結果:
美羊羊:45 廢羊羊:100 喜羊羊:60 灰太狼:76- 1
- 2
- 3
- 4
野指針
是什么
野指針指向的地址是隨機(又稱為:"垃圾"內存)的,無法得知他的地址,操作系統自動對其進行初始化。
野指針是怎樣生成的?
(1)創建指針時沒有對指針進行初始化
(2)使用free釋放指針后沒有將其指向NULL
有什么危害
當一個指針成為野指針,指向是隨機的,當你使用它時,危害程度也是隨機而不可預測的。一般會造成內存泄漏也很容易遭到黑客攻擊,只要將病毒程序放入這塊內存中,當使用到這個指針時就開始執行。
如何避免
- 定義指針時進行初始化
如果沒有確定指向,就讓它指向NULL
NULL在宏定義中是#define NULL (void **) 0,代表的是零地址,零地址不能進行任何讀寫操作
- 要給指針指向的空間賦值時,先給指針分配空間,并且初始化空間
簡單示例:
//char型指針 char *p = (char *)malloc(sizeof(char)); memset(p,0,sizeof(char));//int型指針 //指針(指向地址)游標卡尺 開辟空間大小 int *p = (int *)malloc(sizeof(int)); memset(p,0,sizeof(int));//結構體指針 struct stu *p = (struct stu *)malloc(sizeof(struct stu)); memset(p,0,sizeof(struct stu));malloc動態內存分配,用于申請一塊連續的指定大小的內存塊區域以void*類型返回分配的內存區域地址。void *malloc(unsigned int size),因為返回值時void*,所以要進行強制轉換。
memset將某一塊內存中的內容全部設置為指定的值, 這個函數通常為新申請的內存做初始化工作,是對較大的結構體或數組進行清零操作的一種最快方法。void *memset(void *s, int ch, size_t n);
- 釋放指針同時記得指向NULL
- 1
- 2
malloc與內存泄漏
情景:
程序剛跑起來的時候沒問題,時間久了程序崩潰,大多為內存泄漏。
最常見的情況是在無限的循環中一直申請空間。用malloc申請的空間,程序不會主動釋放,只有當程序結束后,系統才回收空間。
避免在循環中一直申請空間,即使合理釋放(free,指向NULL)
指針類型小測試
搞幾個容易混淆的
int *p[4]; int (*p)[4]; int *p(); int(*p)();- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
四、指針也可以作為數組傳入的形式參數
在數組作為子函數的形式參數小節中,子函數的形式參數我們用 int array[ ]來定義。
學了指針之后,我們也可以用 int *array來定義.這是因為前者的本質上傳入的是數組的首地址,而指針也一樣,需要傳入數組的首地址。
如下:
#include <stdio.h>int arraySum(int *array,int num)//數組形參,僅僅傳遞數組的首地址,代表不了個數。 {int sum;int i;for(i=0;i<num;i++){sum+=array[i];}return sum;}int main() {int sum;int array[5]={0,1,2,3,4};// sum=arraySum(array,sizeof(array)/sizeof(array[0])); //傳遞數組名sum=arraySum(&array[0],sizeof(array)/sizeof(array[0]));//或者傳遞首元素的地址(&)//sizeof里面只能傳入數組名 printf("sum=%d\n",sum);return 0; }五、為什么要使用指針?
(1)節省內存
指針的使用使得不同區域的代碼可以輕易的共享內存數據,當然也可以通過數據的復制達到相同的效果,但是這樣往往效率不太好。
指針節省內存主要體現在參數傳遞上,比如傳遞一個結構體指針變量和傳遞一個結構體變量,結構體占用內存越大,傳遞指針變量越節省內存,也就是可以減少不必要的數據復制。
(2)在子函數中修改被傳遞過來的對象
C語言中的 一切函數調用中,值傳遞都是“按值傳遞” 的。如果要在函數中修改被傳遞過來的對象,就必須通過這個對象的指針來完成。
太抽象了不懂?舉個栗子:
#include <stdio.h>void add(int a) {a = a+1;printf("add:a的值為%d\n",a); }int main() {int a = 10;add(a);printf("main:a的值為%d\n",a);return 0; }結果:可以發現main函數中a的值并沒有真正發生改變。
add:a的值為11 main:a的值為10- 1
- 2
為什么沒有改變呢?
函數add調用時,才申請了一個內存空間,才有了這個變量a,同時把實際參數(main中的a)的值拷貝一份給形式參數(add中的a),函數執行結束后釋放空間,這個子函數中的變量自然也被釋放了。
其中這個a就是傳遞入子函數add的對象,如果想要在這個子函數中修改a的值,就要使用指針
#include <stdio.h>void add(int *a) {*a = *a+1;printf("add:a的值為%d\n",*a); }int main() {int a = 10;add(&a);printf("main:a的值為%d\n",a);return 0; }結果:
add:a的值為11 main:a的值為11- 1
- 2
傳入了main中a的地址,再在子函數中修改這個地址中的內容,當然能夠修改成功。
(3)動態分配內存
常??梢钥吹?#xff0c;程序使用的內存在一開始就進行分配(靜態內存分配)。這對于節省計算機內存是有幫助的,因為計算機可以提前為需要的變量分配內存。
但是大多應用場合中,可能一開始程序運行時不清楚到底需要多少內存,這時候可以使用指針,讓程序在運行時獲得新的內存空間(動態內存分配),并讓指針指向這一內存更為方便。
在結構體指針實際應用舉例中有涉及:
int num;printf("需要錄入幾個學生的成績?\n"); scanf("%d",&num);//這里開辟了num個結構體所需要的空間,動態分配內存 struct stud *pstu = (struct stud *)malloc(sizeof(struct stud)*num);(4)在指定地址寫入數據(目前少用)
了解就行 JAVA等其他編程語言實現不了這種操作
#include <stdio.h>int main() {volatile int *p = (volatile int *)0x7ead81eb;//強制轉化成整型地址*p = 10; //ARM架構 裸機編程 ARM驅動會使用printf("在地址%p中存放的數據是%d\n",p,*p);return 0; }結果:
在地址0x7ead81eb中存放的數據是10- 1
注意哈,這個操作運行很有可能
Segmentation fault- 1
這也正常,畢竟可能這地址放有東西或者不允許這樣操作。
volatile和編譯器的優化有關:
編譯器的優化
在本次線程內,當讀取一個變量時,為了提高讀取速度,編譯器進行優化時有時會先把變量讀取到一個寄存器中(寄存器比內存要快!);以后,再讀取變量值時,就直接從寄存器中讀取;當變量值在本線程里改變時,會同時把變量的新值copy到該寄存器中,以保持一致。
當變量因別的線程值發生改變,上面寄存器的值不會相應改變,從而造成應用程序讀取的值和實際的變量值不一致。
當寄存器因別的線程改變了值,原變量的值也不會改變,也會造成應用程序讀取的值和實際的變量值不一致。
(5)函數多個返回值
有時候我們總是希望一個功能子函數的返回值可以有很多個,但奈何用return只能返回一個。
碰到實際用上的例子再補充,不硬找例子。
總結
以上是生活随笔為你收集整理的五大板块(2)—— 指针的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 寻找子串
- 下一篇: 21天学通c语言总结(3)