1、指針的基本概念
1.1 什么是指針
1.1.1 指針的定義
????????指針是一種特殊的變量,與普通變量存儲具體數據不同,它存儲的是內存地址。在計算機程序運行時,數據都被存放在內存中,而指針就像是指向這些數據存放位置的 “路標”。通過指針,程序可以間接訪問和操作對應內存地址的數據。
1.1.2 指針與普通變量的區別
????????普通變量直接存儲數據值,比如 int num = 10; ,變量 num 中直接存放的是數值 10 。而指針變量存儲的是內存地址,例如 int num = 10; int *ptr = # ,指針 ptr 存儲的是變量 num 在內存中的地址,要獲取 num 的值,需要通過解引用操作 *ptr 。
????????此外,普通變量的運算基于其存儲的數據類型,如 int 型變量可進行加減乘除;指針變量的運算則圍繞內存地址偏移,比如 ptr++ 會根據 int 類型數據在內存中占用的字節數(通常 4 字節),將指針指向下一個 int 數據的地址。
1.2 內存與地址的關系
????????內存是計算機用于臨時存儲數據和程序的硬件設備,就像一個龐大的倉庫,被劃分成一個個連續的小格子,每個小格子都有唯一的編號,CPU 通過這些編號,找到相應的內存空間,這個編號就是內存地址。數據在存儲時,會被分配到特定的內存地址空間中。地址就如同倉庫格子的編號,程序通過地址來準確找到數據在內存中的存放位置,從而實現對數據的讀寫操作 。當定義一個變量時,系統會在內存中為其分配一定的空間,并賦予對應的內存地址,而指針變量存儲的就是這些地址,以此建立起對數據的間接訪問通道。
????????形象地來說,內存是一棟樓,一個內存單元是一戶人家,指針是門牌號,CPU是我們,我們通過門牌號可以找到那戶人家住的地方。
1.2.1 計算機內存結構簡介
????????計算機內存通常采用線性編址結構,從低地址到高地址連續排列。在邏輯上,內存可分為多個區域,如棧區、堆區、全局數據區、代碼區等。
- 棧區:主要用于存儲函數調用時的局部變量、函數參數等,遵循后進先出原則
- 堆區:用于動態內存分配,可通過 malloc (C 語言)或 new (C++ 語言)等函數在堆上申請內存
- 全局數據區:存放全局變量和靜態變量
- 代碼區:存儲程序的可執行代碼
????????指針在不同內存區域的數據操作中都發揮著關鍵作用,比如在堆區通過指針管理動態分配的內存,在棧區利用指針傳遞函數參數等。
1.2.2 地址的作用與表示
????????地址的核心作用是標識內存中數據的存儲位置,它使得程序能夠準確找到并操作數據。
????????在計算機中,地址通常以二進制形式存儲和處理,但在編程中,常以十六進制數表示,便于閱讀和理解。在 C 語言的調試過程中,打印指針變量的值,顯示的就是十六進制的內存地址。
2、指針的基本語法
2.1 指針的聲明與初始化
2.1.1 指針的聲明
????????語法格式:數據類型 *指針名;
注:“數據類型” 表明該指針所指向變量的類型,“指針變量名” 則是用戶為指針取的名字
int *ptr; //聲明一個指向int類型數據的指針ptr
float *fptr; //聲明一個指向float類型數據的指針fptr
2.1.2 指針的初始化
????????指針初始化就是在聲明指針的同時為其賦予一個合法的內存地址。
常見的初始化方式有:
- 初始化為 NULL
- 初始化為變量的地址
- 初始化為動態分配的內存地址
//初始化為 NULL
int *ptr = NULL;//初始化為變量的地址
int num = 10;
int *ptr = #//初始化為動態分配的內存地址
int *ptr = (int *)malloc(sizeof(int));
2.2 取地址操作符&
? ? ? ? 作用:用于獲取變量的內存地址。
#include <stdio.h>int main()
{int num = 10;int *ptr = # //使用取地址操作符&獲取變量num的地址并賦給指針ptrprintf("變量 num 的地址: %p\n", (void *)&num);printf("指針 ptr 存儲的地址: %p\n", (void *)ptr);return 0;
}
在使用 printf 函數輸出指針的值時,%p 格式說明符要求對應的參數是 void * 類型。這是由于 %p 用于以十六進制形式輸出指針所存儲的內存地址,而 void * 類型的指針可以存儲任意類型的地址,能確保輸出的是純粹的地址信息。
2.3 解引用操作符*
????????作用1:通過指針訪問所指向的值。
#include <stdio.h>int main()
{int num = 10;int *ptr = #int value = *ptr; //通過解引用操作符*獲取指針ptr所指向的值printf("指針 ptr 所指向的值: %d\n", value); //輸出為10return 0;
}
????????作用2:修改指針指向的值。
#include <stdio.h>int main()
{int num = 10;int *ptr = #*ptr = 20; //通過解引用操作符*修改指針ptr所指向的值printf("變量 num 的新值: %d\n", num); //輸出為20return 0;
}
2.4 空指針
????????空指針表示不指向任何有效內存地址的指針。?
- 在 C 語言中,通常用 NULL 來表示空指針
- 在 C++ 11 及以后的版本中,推薦使用 nullptr
int main()
{//C環境下int *ptr = NULL; //C語言中使用NULL初始化空指針printf("指針 ptr 的值: %p\n", (void *)ptr);//C++環境下int *ptr = nullptr; //C++中使用nullptr初始化空指針std::cout << "指針 ptr 的值: " << ptr << std::endl;return 0;
}
3、指針的核心應用
3.1 函數參數傳遞
3.1.1 值傳遞
????????在值傳遞中,函數調用時會將實參的值復制一份給形參。函數內部對形參的任何修改都只會影響形參本身,而不會影響到實參。
#include <stdio.h>void changeValue(int num)
{num = 20;printf("函數內部 num 的值: %d\n", num); //輸出為20
}int main()
{int num = 10;changeValue(num);printf("函數外部 num 的值: %d\n", num); //輸出依舊為10,值傳遞不影響實參return 0;
}
3.1.2 指針傳遞
????????指針傳遞是將實參的地址傳遞給形參,形參是一個指針,它指向實參所在的內存地址。因此,函數內部可以通過指針來修改實參的值。
#include <stdio.h>void changeValue(int *ptr)
{*ptr = 20;printf("函數內部指針指向的值: %d\n", *ptr); //輸出為20
}int main()
{int num = 10;changeValue(&num);printf("函數外部 num 的值: %d\n", num); //輸出為20,指針傳遞會改變實參return 0;
}
3.2 動態內存分配
- C 語言:malloc、calloc、realloc 和 free
- C++ 語言:new 和 delete
3.2.1 C 語言?
malloc
????????用于在堆上分配指定大小的內存塊,返回一個指向該內存塊起始地址的指針。如果分配失敗,返回 NULL。
#include <stdio.h>
#include <stdlib.h>int main()
{int *ptr = (int *)malloc(5 * sizeof(int));if (ptr == NULL){printf("內存分配失敗\n");return 1;}//初始化數組元素for (int i = 0; i < 5; i++) {ptr[i] = i;}//輸出數組元素for (int i = 0; i < 5; i++) {printf("%d ", ptr[i]);}printf("\n");//釋放內存free(ptr);ptr = NULL;return 0;
}
calloc
????????用于在堆上分配指定數量和大小的內存塊,并將其初始化為 0。返回一個指向該內存塊起始地址的指針。如果分配失敗,返回 NULL。
#include <stdio.h>
#include <stdlib.h>int main()
{int *ptr = (int *)calloc(5, sizeof(int));if (ptr == NULL) {printf("內存分配失敗\n");return 1;}//輸出數組元素,由于calloc會初始化為0,所以輸出全為0for (int i = 0; i < 5; i++) {printf("%d ", ptr[i]);}printf("\n");//釋放內存free(ptr);ptr = NULL;return 0;
}
realloc
????????用于調整已分配內存塊的大小。可以擴大或縮小內存塊。返回一個指向新內存塊起始地址的指針。如果分配失敗,返回 NULL,原內存塊不會被釋放。
#include <stdio.h>
#include <stdlib.h>int main()
{int *ptr = (int *)malloc(3 * sizeof(int));if (ptr == NULL) {printf("內存分配失敗\n");return 1;}//初始化數組元素for (int i = 0; i < 3; i++) {ptr[i] = i;}//擴大內存塊int *newPtr = (int *)realloc(ptr, 5 * sizeof(int));if (newPtr == NULL) {printf("內存重新分配失敗\n");free(ptr);ptr = NULL;return 1;}ptr = newPtr;//初始化新增的數組元素for (int i = 3; i < 5; i++) {ptr[i] = i;}//輸出數組元素for (int i = 0; i < 5; i++) {printf("%d ", ptr[i]);}printf("\n");//釋放內存free(ptr);ptr = NULL;return 0;
}
分析代碼里的內存分配情況:
- 借助 malloc 函數分配了 3 塊 int 類型的內存,并且把指向這塊內存的指針賦值給 ptr
- 利用 realloc 函數把之前分配的 3 塊 int 類型內存重新分配為 5 塊 int 類型的內存。要是重新分配成功,realloc 函數會返回一個指向新內存塊的指針,然后把這個指針賦值給 newPtr
- 把 newPtr 的值賦給 ptr,這樣 ptr 就指向了新分配的 5 塊 int 類型的內存
注意:先分配的 3 塊內存并沒有單獨釋放,而是在重新分配內存后,將其包含在新的內存塊中,最終一起釋放。
free
????????用于釋放之前通過 malloc、calloc 或 realloc 分配的內存塊。釋放后的內存可以被再次分配。
????????注意:釋放后的指針成為野指針,建議將其置為 NULL,避免誤操作。
3.2.2 C++ 語言
new
????????用于在堆上分配內存并構造對象。對于基本數據類型,直接分配內存;對于類類型,會調用構造函數。
#include <iostream>
using namespace std;int main()
{//分配一個int型內存,并初始化為10,也可以不初始化,直接new int就可以int *ptr = new int(10);cout << *ptr << endl;//分配一個int型數組,長度為5int *arr = new int[5];for (int i = 0; i < 5; i++) {arr[i] = i;}for (int i = 0; i < 5; i++) {cout << arr[i] << " ";}cout << endl;// 釋放內存delete ptr;delete[] arr; //釋放數組return 0;
}
delete
????????用于釋放通過 new 分配的內存。對于基本數據類型,直接釋放內存;對于類類型,會調用析構函數。
????????注意:釋放數組時需要使用 delete[ ],否則會導致內存泄漏。
3.2.3 結構體與指針的結合使用
????????結構體可以將不同類型的數據組合在一起,而指針可以方便地訪問和操作結構體。可以定義指向結構體的指針,通過指針來訪問結構體的成員。
#include <stdio.h>
#include <stdlib.h>//定義結構體
typedef struct
{int age;char name[20];
} Person;int main()
{//創建結構體變量Person p = {20, "John"};//創建指向結構體的指針Person *ptr = &p;//通過指針訪問結構體成員printf("Name: %s, Age: %d\n", ptr->name, ptr->age);return 0;
}
4、指針相關問題
4.1 野指針
4.1.1 產生原因
- 指針未初始化:定義指針變量后沒有給它賦一個合法的地址值,此時指針指向的位置是隨機的,變成野指針。
int* ptr; //ptr就是一個野指針
- 指針所指向的內存被釋放后未置空:當使用 free 或 delete 釋放了指針所指向的內存后,如果沒有將指針設置為 NULL,指針仍然保存著原來已釋放內存的地址,就變成了野指針。
int* ptr = (int*)malloc(sizeof(int));
free(ptr);
//此時ptr成為野指針,如果再次訪問*ptr就會有問題
- 指針越界操作:當指針進行了不恰當的運算,使其指向了不屬于原本所指向的內存區域,也會形成野指針。比如對數組指針進行越界的移動操作。
int main()
{//定義一個包含5個元素的整型數組int arr[5] = {1, 2, 3, 4, 5};//定義一個指針指向數組的首元素int *ptr = arr;//正常訪問數組元素for (int i = 0; i < 5; i++) {printf("arr[%d] = %d\n", i, *(ptr + i));}//越界操作:將指針移動到數組范圍之外ptr = ptr + 5;//嘗試訪問越界后的指針指向的內存printf("越界訪問的值: %d\n", *ptr);return 0;
}
4.1.2 解決方法
- 初始化指針:在定義指針變量時,將其初始化為 NULL 或者指向一個合法的內存地址。
//法一:初始化為 NULL
int* ptr = NULL;//法二:指向一個合法的內存地址
int num;
int* ptr = #
- 內存釋放后置空指針:在使用 free 或 delete 釋放內存后,立即將指針賦值為 NULL,這樣可以避免再次誤操作該指針。
int* ptr = (int*)malloc(sizeof(int));
free(ptr);
ptr = NULL; //釋放后置空
- 小心指針運算:進行指針運算時,要確保不超出所指向內存的范圍,對于數組指針,要根據數組的大小進行合理的指針移動。
4.2 內存泄漏
4.2.1 動態內存未釋放的后果
- 內存浪費:程序占用的內存會不斷增加,導致系統可用內存減少,影響其他程序的運行,甚至可能導致系統性能下降。
- 程序崩潰:當系統內存耗盡時,程序可能會因為無法分配到所需的內存而崩潰。
- 資源耗盡:在一些資源有限的環境中,內存泄漏可能會導致系統無法正常工作,因為沒有足夠的內存來執行其他必要的操作。
4.2.2 解決方法
- 及時釋放內存:在使用完動態分配的內存后,要及時使用 free 或 delete 釋放內存。
- 使用智能指針(C++):C++ 提供了智能指針(如 std::unique_ptr、std::shared_ptr)來自動管理內存,當智能指針超出作用域時,會自動釋放所指向的內存,從而避免內存泄漏。
4.3 指針運算
4.3.1 指針加減整數的運算
????????指針加減整數的運算結果是一個新的指針,其指向的位置會根據指針所指向的數據類型的大小進行移動。
#include <stdio.h>int main()
{int arr[5] = {1, 2, 3, 4, 5};int *ptr = arr; //ptr指向數組的第一個元素//指針加1int *next_ptr = ptr + 1;printf("ptr 指向的值: %d\n", *ptr); //輸出為1printf("ptr + 1 指向的值: %d\n", *next_ptr); //輸出為2//指針減1int *prev_ptr = next_ptr - 1;printf("next_ptr - 1 指向的值: %d\n", *prev_ptr); //輸出為1return 0;
}
4.3.2 指針間的減法運算
????????指針的減法運算通常用于計算兩個指針之間的距離,結果是一個整數,表示兩個指針之間相差的元素個數,前提是這兩個指針指向同一塊連續的內存區域(如數組)。
#include <stdio.h>int main()
{int arr[5] = {1, 2, 3, 4, 5};int *ptr1 = &arr[0]; //指向數組第一個元素int *ptr2 = &arr[3]; //指向數組第四個元素//計算兩個指針之間的距離int distance = ptr2 - ptr1;printf("ptr2 和 ptr1 之間相差的元素個數: %d\n", distance); //輸出為3,表示它們之間相差3個int型元素return 0;
}
4.3.3 指針間的比較
????????只有當兩個指針指向同一塊連續的內存區域(如數組)且有關聯時,進行比較才有意義。例如,比較數組中不同元素的指針,判斷它們的先后順序。
#include <stdio.h>int main()
{int arr[5] = {1, 2, 3, 4, 5};int *ptr1 = &arr[0]; //指向數組第一個元素int *ptr2 = &arr[3]; //指向數組第四個元素//比較指針if (ptr1 < ptr2) {printf("ptr1 指向的元素在 ptr2 指向的元素之前\n");} else {printf("ptr1 指向的元素在 ptr2 指向的元素之后或相同\n");}return 0;
}
4.4 二級指針、三級指針
4.4.1 二級指針
????????二級指針是指向指針的指針。也就是說,二級指針所存儲的地址是一個指針變量的地址,而這個指針變量又指向實際的數據。
4.4.2 三級指針
????????三級指針是指向二級指針的指針,即它存儲的地址是一個二級指針變量的地址。
int num = 10;
int* ptr1 = # //一級指針
int** ptr2 = &ptr1; //二級指針
int*** ptr3 = &ptr2; //三級指針
5、指針數組與數組指針
5.1 指針數組
- 定義:元素為指針的數組
- 語法格式:數據類型*? 數組名[數組長度]
#include <stdio.h>int main()
{//定義一個數組a,并給數組賦值int a[10];for (int i = 0; i < 10; i++){a[i] = i + 1;}//指針數組:數組中的每一個元素都是指針int* arr[10] = { a, a + 1, a + 2, a + 3, a + 4, a + 5, a + 6, a + 7, a + 8, a + 9 };//a + 1 == &a[0] + 1 == &a[1];//a[5] ? --> a數組中下標為5的元素是多少?//printf("%d %d", *arr[5], **(arr + 5));//**(arr + 5)的推導//arr == &arr[0];//arr + 5 == &arr[0] + 5 == &arr[5];//*(arr + 5) == *(&arr[5]) == arr[5];//**(arr + 5) == *arr[5] == a[5];return 0;
}
5.2 數組指針
- 定義:指向某個數組的指針
- 語法格式:數據類型? (*指針名)? [數組長度]
#include <stdio.h>int main()
{//定義一個數組a,并給數組賦值int a[10];for (int i = 0; i < 10; i++){a[i] = i + 1;}//數組指針:本身是一個指針,存的是元素類型是int、元素個數為10的數組的地址int (*arry)[10];arry = &a; //賦值printf("%d", *((int*)(arry + 1) - 1)); //輸出為10return 0;
}
輸出語句 printf("%d", *((int*)(arry + 1) - 1)); 的詳細解釋:
- arry + 1:由于 arry 是一個指向包含 10 個 int 元素數組的指針,arry + 1 會讓指針向后移動一個包含 10 個 int 元素數組的長度,也就是跳過整個數組 a 所占用的內存空間,指向數組 a 之后的內存位置。
- (int*):將 arry + 1 的結果強制轉換為 int* 類型的指針。這一步的作用是將原本指向整個數組的指針轉換為指向單個 int 元素的指針,以便后續進行以 int 為單位的指針運算。
- (int*)(arry + 1) - 1:在強制轉換為 int* 類型指針后,進行減 1 操作。因為現在是 int* 類型的指針,減 1 會讓指針向前移動一個 int 類型的長度,也就是回到數組 a 的最后一個元素的地址。
- *((int*)(arry + 1) - 1):對前面得到的指針進行解引用操作,獲取該指針所指向的內存位置存儲的值,也就是數組 a 的最后一個元素的值,即 10。
6、指針函數與函數指針
6.1 指針函數
- 定義:本身是一個函數,返回值是一個指針
- 語法格式:數據類型*? 函數名(參數列表)
#include <stdio.h>//指針函數:返回值為指針的函數
//注意:這里返回局部變量的地址會導致未定義行為,因為局部變量在函數結束后會被銷毀
//可以使用靜態局部變量來解決上述問題int* fun()
{static int a = 10; //使用靜態局部變量return &a;
}int main()
{int* ptr = fun();printf("訪問返回指針指向的值: %d\n", *ptr);return 0;
}
6.2 函數指針
- 定義:本身是一個指針,存的是函數的地址
- 語法格式:數據類型? (*指針名)? (參數列表)
?注:“數據類型”指的是函數的返回值類型,“參數列表”指的是函數的參數列表
#include <stdio.h>//函數的定義不允許嵌套
//函數的調用允許嵌套
//當函數體在主函數下方時,需要在函數上方進行函數聲明//聲明fff函數
int fff();int main()
{//函數指針:本身是一個指針,存的是函數的地址int (*f)() = fff; //fff等價于&fff//函數的調用方式fff();f(); //通過地址訪問,fff()等價于f()等價于(*f)()return 0;
}//定義fff函數
int fff()
{printf("asd");return 0;
}
- fff():這是普通的函數調用方式。fff 是函數名,直接在函數名后面加上括號并傳入相應的參數( fff 函數沒有參數),就可以調用這個函數。當執行 fff() 時,程序會跳轉到 fff 函數的代碼塊中執行其中的語句,即輸出字符串 "asd"。
- f():f 是一個函數指針,它存儲了函數 fff 的地址。當使用 f() 這種形式調用時,實際上是通過函數指針 f 來調用它所指向的函數(即 fff 函數)。因為 f 指向了 fff 函數的地址,所以 f() 的效果和直接調用 fff() 是一樣的,程序同樣會跳轉到 fff 函數的代碼塊中執行。
- (*f)():這種寫法也是通過函數指針 f 來調用函數。在 C 語言中,函數指針本質上是一個指向函數的地址的指針變量。*f 是對函數指針 f 進行解引用操作,從概念上來說,*f 就表示 fff 函數。所以 (*f)() 同樣是調用 f 所指向的函數,它和 f() 以及 fff() 的作用是等價的,最終都會執行 fff 函數中的代碼。