指針 定義 指針是一個變數,存儲另一個變數的記憶體地址,它允許直接訪問和操作記憶體中的數據,使得程式能夠以更靈活和高效的方式處理數據和記憶體。 獲取變數地址:使用取地址符 &。 訪問地址上的數據:使用解引用符 *。 例子1 指針是存儲另一個變數地址的變數。通過使用取地址符 & 和解引用符 *,我們可以靈活 ...
指針
定義
指針是一個變數,存儲另一個變數的記憶體地址,它允許直接訪問和操作記憶體中的數據,使得程式能夠以更靈活和高效的方式處理數據和記憶體。
獲取變數地址:使用取地址符 &
。
訪問地址上的數據:使用解引用符 *
。
例子1
指針是存儲另一個變數地址的變數。通過使用取地址符 &
和解引用符 *
,我們可以靈活地訪問和操作記憶體中的數據 。
#include <stdio.h>
int main() {
int var = 10; // 定義一個整數變數
int *p = &var; // 定義一個指向整數的指針,並將其初始化為變數 var 的地址
printf("Address of var: %p\n", &var); // 輸出變數 var 的地址
printf("Address stored in pointer p: %p\n", p); // 輸出指針 p 中存儲的地址
printf("Value of var using pointer: %d\n", *p); // 通過指針 p 解引用獲取 var 的值
// 修改 var 的值,通過指針 p
*p = 20;
printf("New value of var: %d\n", var); // 輸出修改後的 var 的值
return 0;
}
例子2
指針類型決定了它指向的變數類型,以及通過指針可以訪問的數據大小。不同類型的指針在操作時會有不同的步長,比如:
int *p = (int *)a;
:將 char
類型數組的首地址強制轉換為 int
指針類型。由於 int
類型通常占用 4 個位元組,因此通過 p
訪問數據時,每次會讀取 4 個位元組的數據。
char *q = a;
:將 char
類型數組的首地址賦值給 char
指針類型。char
類型占用 1 個位元組,因此通過 q
訪問數據時,每次只會讀取 1 個位元組的數據。
#include <stdio.h>
int main() {
char a[12] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C};
int *p = (int *)a; // 將 char 數組的地址賦值給 int 指針
char *q = a; // 將 char 數組的地址賦值給 char 指針
// 使用 int 指針訪問數據
printf("Using int pointer:\n");
for (int i = 0; i < 3; i++) {
printf("p[%d]: 0x%08x\n", i, *(p + i));
}
// 使用 char 指針訪問數據
printf("Using char pointer:\n");
for (int i = 0; i < 12; i++) {
printf("q[%d]: 0x%02x\n", i, *(q + i));
}
return 0;
}
/*
輸出
Using int pointer:
p[0]: 0x04030201
p[1]: 0x08070605
p[2]: 0x0c0b0a09
Using char pointer:
q[0]: 0x01
q[1]: 0x02
q[2]: 0x03
q[3]: 0x04
q[4]: 0x05
q[5]: 0x06
q[6]: 0x07
q[7]: 0x08
q[8]: 0x09
q[9]: 0x0a
q[10]: 0x0b
q[11]: 0x0c
*/
一級指針和二級指針
一級地址(一級指針)
定義
一級地址指的是指向普通變數的指針,也就是直接存儲變數的地址的指針。在C中,通常我們操作的是一級地址,例如指向整數、浮點數或其他基本數據類型的指針。
例子
int *ptr; // ptr 是一個指向整數的指針,是一級地址
float *ptr_float; // ptr_float 是一個指向浮點數的指針,也是一級地址
二級地址(二級指針)
定義
二級地址是指向指針的指針,也就是存儲另一個指針的地址的指針。在C中,可以通過二級指針來操作指向指針的指針,用來間接修改指針指向的值或者傳遞指針的引用。
例子
int x = 10;
int *ptr1 = &x; // ptr1 是一個指向 x 的指針,是一級地址
int **ptr2 = &ptr1; // ptr2 是一個指向 ptr1 的指針,是二級地址
例子
分析表達式 *(*(&arr + 1) - 1)
的值:
int arr[5] = {1, 2, 3, 4, 5};
arr[5] = {1, 2, 3, 4, 5}
:這定義了一個包含 5 個整數的數組arr
,其元素分別是{1, 2, 3, 4, 5}
。&arr
:這是數組arr
的地址。需要註意的是,&arr
的類型是int (*)[5]
,即指向一個包含 5 個整數的數組的指針。&arr + 1
:這是&arr
指針加 1。在這個上下文中,&arr
被看作是一個指向整個數組的指針,因此&arr + 1
將指向緊隨arr
之後的記憶體位置。也就是說,它指向arr
後面的記憶體地址,而不是數組中的下一個元素。&arr + 1
的類型仍然是int (*)[5]
。*(&arr + 1)
:這是對&arr + 1
解引用。&arr + 1
是一個指向數組的指針,對其解引用後,得到的仍然是一個指向數組末尾之後的指針。它的類型是int *
,即指向數組末尾之後的指針。*(&arr + 1) - 1
:這將剛纔得到的指針減去 1。由於指針的減法是以元素為單位的,這個操作將指針向後移動一個int
的大小。因為*(&arr + 1)
指向的是數組arr
末尾之後的位置,減去 1 後,這個指針將指向數組arr
的最後一個元素。*(*(&arr + 1) - 1)
:最後,對指針*(&arr + 1) - 1
解引用,即獲取這個指針所指向的值。這個指針現在指向arr
的最後一個元素,所以這個表達式的值就是arr
的最後一個元素的值。
因此,*(*(&arr + 1) - 1)
的值是 5
,即數組 arr
的最後一個元素。
指針的自增運算
*++p
這個表達式是先對指針 p
進行自增操作,然後對新的指針進行解引用,得到新的指針所指向的值。
步驟:
++p
:指針p
先自增,指向下一個元素( 自增的是指針 )。*
:對自增後的指針解引用,得到新指針所指向的值。
++*p
這個表達式是對指針 p
所指向的值進行解引用,然後對解引用得到的值進行自增操作。
步驟:
*p
:對指針p
進行解引用,得到指向的值。++
:對解引用得到的值進行自增操作( 自增的是指針指向的值 )。
函數地址與數組地址
函數地址
函數名就是函數的入口地址,因此可以直接把函數名賦給函數指針,也可以加上取地址符號 &
。取地址符號 &
是可選的,它只是顯式地說明瞭編譯器隱式執行的任務。獲取函數的地址時,加不加取地址符號 &
都可以。
#include <stdio.h>
int test(int i) {
return i;
}
int main(void) {
int (*p1)(int) = test;
int (*p2)(int) = &test;
printf("%p, %p\n", (void *)p1, (void *)p2);
return 0;
}
數組地址
數組名本身是數組第一個元素的地址。例如,對於 int
類型的數組來說,數組名 arr
的類型是 int*
,它是一個整型指針,不是數組指針。如果要取整個數組的地址,必須在數組名前面加上取地址符號 &
,這樣才能賦值給數組指針。獲取數組的地址時,必須加上取地址符號 &
。
在下麵的例子中, p1
是一個整型指針,指向數組的第一個元素,而 p2
是一個數組指針,指向整個數組。對 p1
和 p2
分別進行加 1 操作,p1
會加 4 個位元組(假設 int
為 4 位元組),而 p2
會加 20 個位元組(5 個 int
)。
#include <stdio.h>
int main(void) {
int array[5] = {1, 2, 3, 4, 5};
int* p1 = array; // 指向數組第一個元素
int(*p2)[5] = &array; // 指向整個數組
printf("p1 %p\n", (void*)p1);
printf("p2 %p\n", (void*)p2);
printf("p1+1 %p\n", (void*)(p1+1)); // p1 加 1,增加 4 個位元組(假設 int 為 4 位元組)
printf("p2+1 %p\n", (void*)(p2+1)); // p2 加 1,增加 20 個位元組(5 個 int)
return 0;
}
指針和數組
不需要嚴格區分的情況
訪問數組元素
對於數組和指針,都可以使用下標形式或指針形式來訪問元素。
#include <stdio.h>
int main(void) {
char array[] = "hello world!";
char* p = array;
array[1] = 'x'; // 使用下標訪問數組
*(array + 1) = 'x'; // 使用指針形式訪問數組
p[1] = 'y'; // 使用下標訪問指針
*(p + 1) = 'y'; // 使用指針形式訪問指針
printf("%s\n", array); // 輸出: hyllo world!
printf("%s\n", p); // 輸出: hyllo world!
return 0;
}
作為函數參數傳遞
實參傳遞數組時,形參可以是數組或指針。實參傳遞指針時,形參也可以是數組或指針。編譯器會將數組參數退化為指針。
#include <stdio.h>
void printArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main(void) {
int array[] = {1, 2, 3, 4, 5};
printArray(array, 5); // 傳遞數組
return 0;
}
需要嚴格區分的情況
使用 sizeof
計算長度
sizeof(array)
返回數組的位元組數。sizeof(pointer)
返回指針的大小(在64位系統中為8位元組,在32位系統中為4位元組)。
#include <stdio.h>
int main(void) {
char array[] = "hello world!";
char* p = array;
printf("sizeof(array) = %lu\n", sizeof(array)); // 輸出: 13
printf("sizeof(p) = %lu\n", sizeof(p)); // 輸出: 8 (在64位系統中)
return 0;
}
計算數組長度
當數組作為參數傳遞時,在函數內部不能使用 sizeof
計算數組長度,因為數組會退化為指針。
#include <stdio.h>
void test(int arg[]) {
printf("sizeof(arg) = %lu\n", sizeof(arg)); // 輸出指針大小,例如在64位系統中為8
}
int main(void) {
int array[10];
test(array); // 傳遞數組
return 0;
}
聲明外部變數
在一個文件中定義指針 p
,在另一個文件中不能聲明為數組。
//file1.c:
int* p;
// file2.c
extern int* p; // 正確:聲明為指針
// extern int p[]; // 錯誤:不能聲明為數組
指針常量和常量指針
指針常量
定義
不能改變指向的指針。也就是說,一旦初始化後,就不能再改變指向其它地址,但可以修改指針所指向地址上的內容。
例子
比如指針 p
,因為它被 const
修飾,所以 p
不能被修改,它只能指向 str
。如果強行對 p
進行 p++
操作,編譯時就會報錯。我們稱指針 p
為指針常量。儘管 p
本身不能被修改,但可以通過 p
來修改 str
的內容,例如 *p = 'H';
。
char str[] = "hello world!";
char *const p = str;
常量指針
定義
不能改變所指向的值的指針。也就是說,通過這個指針不能修改它所指向的值,但可以修改指針的指向。
例子
p
是一個指向 char
類型常量的指針。可以修改 p
的指向,例如 p = another_str;
,但不能通過 p
修改其指向的內容,例如 *p = 'H';
是不允許的。
const char *p = "hello world!";
char const *p = "hello world!";
指針數組和數組指針
指針數組
定義
本質是一個數組,數組的每個元素都是指針。
例子1
int *p[4];
這一聲明中,p
先與中括弧結合,表示 p
是一個有四個元素的數組,每個元素的類型都是 int *
(指向整型的指針)。因此,我們稱 p
為指針數組。
int *p[4];
例子2
int a = 1, b = 2, c = 3, d = 4;
int *p[4] = { &a, &b, &c, &d };
for (int i = 0; i < 4; i++) {
printf("%d ", *p[i]);
}
// 輸出:1 2 3 4
數組指針
定義
本質是一個指針,指向一個數組。
例子1
int (*p)[4];
這一聲明中,p
先與 *
結合,表示 p
是一個指針,然後與中括弧結合,表示 p
指向一個有四個元素的數組,每個元素的類型都是 int
。因此,我們稱 p
為數組指針。
int (*p)[4];
例子2
int arr[4] = {1, 2, 3, 4};
int (*p)[4] = &arr;
for (int i = 0; i < 4; i++) {
printf("%d ", (*p)[i]);
}
// 輸出:1 2 3 4
函數指針和指針函數
函數指針
定義
本質是一個指針,指向一個函數。每個函數在記憶體中都有一個地址,函數調用就是跳轉到這個地址開始執行,函數指針記錄了這個地址的變數。
例子
p
是一個指針,指向一個函數,該函數有一個 int
類型的參數,返回值是 int
。可以通過函數名 test(1)
來調用函數,也可以通過指針 p
來調用 p(1)
或者(*p)(1)
。
#include <stdio.h>
int test(int i) {
return i;
}
int main(void) {
int res = 0;
int (*p)(int) = test;
res = test(1);
printf("%d\n", res);
res = p(1);
printf("%d\n", res);
res = (*p)(1);
printf("%d\n", res);
return 0;
}
應用
往結構體中放入函數
C語言的結構體不支持成員函數,但可以通過函數指針實現類似的功能。結構體中可以定義一個函數指針,用來保存函數地址。函數名可以直接賦值給函數指針。C語言的結構體在使用時必須加上 struct
關鍵字,而 C++ 可以省略。
#include <stdio.h>
#include <stdlib.h>
// 定義一個列印函數
void print(int a, int b) {
printf("%d %d\n", a, b);
}
// 定義一個結構體,其中包含一個函數指針
struct Test {
void (*p)(int, int);
};
int main(void) {
// 動態分配結構體記憶體
struct Test* t = (struct Test *)malloc(sizeof(struct Test));
if (t == NULL) {
fprintf(stderr, "Memory allocation failed\n");
return 1;
}
// 將函數指針指向列印函數
t->p = print;
// 調用函數指針
t->p(3, 4); // 輸出: 3 4
// 釋放動態分配的記憶體
free(t);
return 0;
}
回調函數
是一種通過函數指針實現的技術,允許在一個函數中調用另一個函數。通常用於事件驅動編程或處理非同步操作。
#include <stdio.h>
// 定義一個回調函數類型
typedef void (*Callback)(int);
// 定義一個回調函數
void myCallback(int value) {
printf("Callback called with value: %d\n", value);
}
// 定義一個執行操作並調用回調函數的函數
void performOperation(int x, Callback callback) {
printf("Performing operation with value: %d\n", x);
// 調用回調函數
callback(x);
}
int main(void) {
// 使用回調函數
performOperation(5, myCallback);
return 0;
}
動態函數調用
在需要根據某些條件動態選擇和調用不同函數時,函數指針非常有用。例如,在一個計算器程式中可以動態選擇不同的操作函數。
#include <stdio.h>
// 定義操作函數
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }
int divide(int a, int b) { return a / b; }
int main(void) {
// 定義一個函數指針數組
int (*operations[4])(int, int) = { add, subtract, multiply, divide };
int a = 10, b = 5;
char op = '+';
// 根據操作符選擇對應的函數
int (*operation)(int, int) = NULL;
switch (op) {
case '+': operation = operations[0]; break;
case '-': operation = operations[1]; break;
case '*': operation = operations[2]; break;
case '/': operation = operations[3]; break;
}
if (operation != NULL) {
printf("Result: %d\n", operation(a, b));
} else {
printf("Invalid operation\n");
}
return 0;
}
實現多態行為
通過函數指針數組,可以在C語言中實現類似C++的多態行為。這種技術廣泛應用於設計模式和框架中。
#include <stdio.h>
// 定義一個基類結構體
struct Shape {
void (*draw)(struct Shape*);
};
// 定義一個派生類結構體
struct Circle {
struct Shape base; // 基類
int radius;
};
// 定義一個繪製函數
void drawCircle(struct Shape* shape) {
struct Circle* circle = (struct Circle*)shape;
printf("Drawing a circle with radius: %d\n", circle->radius);
}
int main(void) {
// 創建一個 Circle 對象
struct Circle c;
c.base.draw = drawCircle;
c.radius = 5;
// 調用繪製函數
c.base.draw((struct Shape*)&c);
return 0;
}
實現狀態機
是一種在不同狀態之間轉換的編程模式。可以通過函數指針實現狀態之間的動態切換。
#include <stdio.h>
// 定義狀態函數類型
typedef void (*StateFunction)();
// 定義狀態函數
void stateA() {
printf("State A\n");
}
void stateB() {
printf("State B\n");
}
void stateC() {
printf("State C\n");
}
int main(void) {
// 定義一個狀態函數指針數組
StateFunction states[3] = { stateA, stateB, stateC };
int currentState = 0;
for (int i = 0; i < 5; i++) {
// 調用當前狀態函數
states[currentState]();
// 切換到下一個狀態
currentState = (currentState + 1) % 3;
}
return 0;
}
代碼跳轉到指定位置執行
比如((void(*)())0)();
代表讓程式跳轉到地址為0的地方去運行。
解析步驟
(void (*)())
:這是一個類型轉換,表示一個指向返回類型為void
、無參數的函數的指針。具體來說,void (*)()
是一種函數指針類型,它指向的函數沒有參數並返回void
。0
:這是一個整數常量0
。在C中,可以將整數常量0
作為空指針常量。(void (*)())0
:這將整數0
轉換為類型為void (*)()
的函數指針。換句話說,這是將0
解釋為一個指向無參數、返回類型為void
的函數的指針。((void (*)())0)
:這隻是對前面步驟的類型轉換進行一次包裹,結果仍然是一個類型為void (*)()
的函數指針,指向0
地址。((void (*)())0)()
:這表示對0
地址的函數指針進行調用。具體來說,這是試圖調用位於地址0
處的函數。
指針函數
定義
本質是一個函數,其返回值是一個指針。
例子
錯誤示例
下麵這個例子是一個典型的錯誤,因為不能返回一個局部變數的地址。函數調用完畢後,局部變數的記憶體會被釋放,即使返回了這個地址也不能使用。
int* test() {
int array[5] = {0}; // 局部變數
return array; // 錯誤:返回局部變數的地址
}
int main(void) {
int* p = test(); // 不安全
return 0;
}
返回堆空間地址
#include <stdio.h>
#include <stdlib.h>
int* test() {
int* array = (int*)malloc(sizeof(int) * 5); // 動態分配堆空間
if (array != NULL) {
for (int i = 0; i < 5; ++i) {
array[i] = i; // 初始化數組
}
}
return array; // 返回堆空間地址
}
int main(void) {
int* p = test();
if (p != NULL) {
for (int i = 0; i < 5; ++i) {
printf("%d ", p[i]); // 輸出:0 1 2 3 4
}
free(p); // 釋放動態分配的記憶體
}
return 0;
}
返回全局變數地址
#include <stdio.h>
int array[5]; // 全局變數
int* test() {
for (int i = 0; i < 5; ++i) {
array[i] = i; // 初始化數組
}
return array; // 返回全局變數地址
}
int main(void) {
int* p = test();
for (int i = 0; i < 5; ++i) {
printf("%d ", p[i]); // 輸出:0 1 2 3 4
}
return 0;
}
返回靜態變數地址
#include <stdio.h>
int* test() {
static int array[5]; // 靜態變數
for (int i = 0; i < 5; ++i) {
array[i] = i; // 初始化數組
}
return array; // 返回靜態變數地址
}
int main(void) {
int* p = test();
for (int i = 0; i < 5; ++i) {
printf("%d ", p[i]); // 輸出:0 1 2 3 4
}
return 0;
}
指針函數指針
定義
是一個指向返回指針的函數的指針。它不僅是一個指向函數的指針,而且該函數返回的也是一個指針。
例子
#include <stdio.h>
#include <stdlib.h>
// 定義一個返回整數指針的函數
int* allocateArray(int size) {
int* array = (int*)malloc(size * sizeof(int)); // 動態分配記憶體
if (array != NULL) {
for (int i = 0; i < size; ++i) {
array[i] = i; // 初始化數組
}
}
return array; // 返回堆空間地址
}
// 定義一個指向返回整數指針的函數的指針類型
typedef int* (*ArrayAllocator)(int);
int main(void) {
// 定義一個指向返回整數指針的函數的指針
ArrayAllocator allocator = allocateArray;
int size = 5;
// 使用指針函數指針調用函數
int* array = allocator(size);
if (array != NULL) {
for (int i = 0; i < size; ++i) {
printf("%d ", array[i]); // 輸出:0 1 2 3 4
}
printf("\n");
free(array); // 釋放動態分配的記憶體
}
return 0;
}
懸掛指針和野指針
懸掛指針
定義
懸掛指針是指向已經釋放(通過 free
函數釋放)或者已經超出作用域的記憶體的指針。當我們試圖通過這樣的指針訪問或操作記憶體時,可能會導致未定義行為,因為那塊記憶體可能已經被操作系統重新分配給其它程式使用了。
#include <stdio.h>
#include <stdlib.h>
void dangling_pointer_example() {
int *p = (int *)malloc(sizeof(int));
*p = 42;
printf("Value: %d\n", *p); // 輸出42
free(p); // 釋放記憶體
// p現在是懸掛指針,訪問*p將導致未定義行為
// printf("Value: %d\n", *p); // 不安全,可能導致崩潰或未定義行為
}
int main() {
dangling_pointer_example();
return 0;
}
解決方法
立即將指針設為NULL:在釋放記憶體後,將指針設置為NULL。
free(p);
p = NULL;
避免返回局部變數的指針:不要返回局部變數的指針,因為它們在函數返回後將超出作用域。
int* incorrect_function() {
int x = 42;
return &x; // 返回局部變數的指針,錯誤
}
野指針
定義
野指針是一個未初始化的指針,它的值是未知的,可能指向任意記憶體地址。當我們試圖通過這樣的指針訪問或操作記憶體時,可能會導致未定義行為。
#include <stdio.h>
void wild_pointer_example() {
int *p; // 未初始化的指針
// *p = 42; // 不安全,可能導致崩潰或未定義行為
}
int main() {
wild_pointer_example();
return 0;
}
解決方法
在聲明指針時初始化:聲明指針時將其初始化為NULL或有效地址。
int *p = NULL;
確保在使用前分配記憶體:在使用指針之前,確保它已經被正確初始化和分配記憶體。
int *p = (int *)malloc(sizeof(int));
if (p != NULL) {
*p = 42;
}