目錄
一. 字符指針變量
二. 數組指針變量
三. 二維數組傳參
3.1 二維數組的本質
3.2 訪問方式與地址計算
3.3 二維數組的傳參方式
3.4 深入解析?*(*(arr+i)+j)?與?arr[i][j]?的等價性
四. 函數指針變量
4.1 函數指針變量的創建
4.2 函數指針變量的使用
4.3 兩段"有趣"代碼的講解
4.3.1 解析?(*(void(*)())0)();
4.3.2?解析void(* signal(int, void(*)(int)))(int);
4.4?typedef關鍵字講解
4.4.1 基本語法
4.4.2?常見用法
4.4.3?結合?typedef?解析?signal?函數
4.4.4?typedef?的優點
4.4.5?總結
五. 函數指針數組
5.1 基本概念
5.2 詳細用法
5.3 實際應用示例
六. 轉移表
一. 字符指針變量
什么是字符指針變量呢?
字符指針變量是指向字符類型數據的指針,在C/C++中通常用于處理字符和字符串和字符數組。
例如以下代碼? 處理字符
int main() {char ch = 'w';char* pc = &ch; ///字符指針變量return 0; }
pc存放的是字符w的地址
例如以下代碼? 處理字符串
int main() {const char* pc = "Hello, World!";//常量字符串 不可被修改printf(" %c\n", *pc);printf(" %s", pc); //注意: 打印字符串的時候 需要的參數是字符串的起始地址return 0; }
pc存放的是首字符H的地址? 打印驗證結果如下
注意: 打印字符串的時候 需要的參數是字符串的起始地址
注意:pc此時指向的是常量字符串 即*pc現在是一個左值 無法被修改
由于*pc現在是一個左值 無法被修改 使用我們使用const來修飾它
例如以下代碼? 處理字符數組
int main() {char arr[] = "ABCDE";char* pc = arr;printf(" %c\n", *pc);printf(" %s", pc); //注意: 打印字符串的時候 需要的參數是字符串的起始地址return 0; }
pc存放的是數組第一個元素a的地址 同理 運行結果如下
注意:pc此時指向的是數組? 與指向字符串的區別是*pc現在可以被修改
現在讓我們來認真閱讀以下代碼 運行結果會是什么呢?
int main()
{char str1[] = "Hello world";char str2[] = "Hello world";const char* str3 = "Hello world";const char* str4 = "Hello world";if (str1 == str2)printf("str1=str2\n");elseprintf("str1!=str2\n");if (str3 == str4)printf("str3=str4\n");elseprintf("str3!=str4\n");return 0;
}
運行結果如下
可以看出str1與str2不相等 而str3和str4卻相等 這是為什么呢??
原因分析:
str1
?和?str2
?的比較 (str1 == str2
)
str1
?和?str2
?是兩個獨立的字符數組,分別存儲?"Hello world"
。- 數組名在比較時會被轉換為指向數組首元素的指針(即?
&str1[0]
?和?&str2[0]
)。- 由于?
str1
?和?str2
?是兩個不同的數組,它們的地址不同,所以?str1 == str2
?為?false
,輸出?str1!=str2
。
str3
?和?str4
?的比較 (str3 == str4
)
str3
?和?str4
?是指向字符串常量的指針,且它們的值都是?"Hello world"
。- 編譯器會對相同的字符串常量進行優化(稱為?字符串池化,String Interning),即多個相同的字符串常量在內存中只存儲一份。
- 因此,
str3
?和?str4
?實際上指向同一個內存地址,所以?str3 == str4
?為?true
,輸出?str3=str4
。
關鍵區別:
- 字符數組 (
char[]
) 會分配獨立的內存空間,即使內容相同,地址也不同。- 字符串常量 (
const char*
) 可能被優化為共享同一內存,因此相同內容的字符串常量可能指向同一地址。
而如果想比較字符串的內容是否相同,應該使用?strcmp
?函數,而不是直接比較指針:
if (strcmp(str1, str2) == 0) // 比較內容是否相同printf("str1 和 str2 內容相同\n");
elseprintf("str1 和 str2 內容不同\n");
strcmp函數簡要功能如下?
具體了解請訪問strcmp - C++ Reference
二. 數組指針變量
在學習數組指針前 讓我們來回顧一下 字符指針 整型指針
字符指針: char* p? ?----指向字符的指針 存放的是字符的地址 char *p=&ch;
整形指針: int* p? ? ? ----指向整型的指針 存放的是整形的地址? int a=10;? p=&a;
數組指針:? ? ? ? ? ? ? ?----指向數組的指針? 存放的是數組的地址
即數組指針變量是指向數組的指針,且在C/C++中用于處理多維數組和動態數組操作。
而數組的地址我們也曾經遇見過 同學們不妨通過以下代碼回想一下
int main()
{int arr[10] = { 0 };arr; //首元素的地址&arr[0];&arr;//取出的是整個數組的地址---數組的地址return 0;
}
相信已經有同學分不清了? 現在讓我們來區別一下指針數組和數組指針
特性 | 數組指針?(int (*p)[N] ) | 指針數組?(int *p[N] ) |
---|---|---|
本質 | 一個指針,指向整個數組 | 一個數組,元素全是指針 |
聲明方式 | int (*p)[5]; | int *p[5]; |
內存占用 | 指針大小(通常8字節) | N個指針的大小(如5個指針=40字節) |
存儲內容 | 存儲數組的首地址 | 存儲多個指針(地址) |
典型用途 | 處理二維數組 | 存儲多個字符串/動態數組 |
sizeof結果 | sizeof(p) =指針大小 | sizeof(p) =N×指針大小 |
直觀理解:
數組指針?→?指向數組的指針
int arr[3][4];
int (*p)[4] = arr; // p指向arr的第一行(一個包含4個int的數組)
p+1
會跳過整個子數組(移動4*sizeof(int)
字節)
指針數組?→?存放指針的數組
char *strs[3] = {"Hello", "World", "!"};
strs[1]
返回第二個字符串的地址("World"
的首地址)
關鍵區別:?
操作 | 數組指針?(int (*p)[4] ) | 指針數組?(int *p[4] ) |
---|---|---|
定義 | 指向int[4] 的指針 | 包含4個int* 的數組 |
p+1的偏移量 | 16字節(假設int =4字節) | 8字節(指針大小) |
記憶口訣
“星號括起來是指針,星號不括是數組”
int (*p)[N]
?→ 星號被括號括住,強調是指針int *p[N]
?→ 星號沒被括,強調是數組
如何使用數組指針來打印數組里的值呢? 如下
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{int arr[] = {1,2,3,4,5,6,7,8,9};int (*p)[9] = arr;//(*p)得到arr的地址 [i]表示調用arr里的第幾個元素for (int i = 0;i < 9;i++){printf("%d ", (*p)[i]);}
}
但這種寫法似乎更加復雜了 并沒有什么優勢
但其實我們并不會在這種情況使用數組指針 下面讓我們繼續深入學習
三. 二維數組傳參
3.1 二維數組的本質
二維數組是?“數組的數組”,在內存中仍然是連續存儲的線性結構。例如:
int arr[3][4] = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12}
};
內存布局:
[1][2][3][4] [5][6][7][8] [9][10][11][12]
- 每行?
arr[i]
?是一個一維數組,類型是?int[4]
arr
?本身是?“指向int[4]
的指針”(即?int (*)[4]
)
3.2 訪問方式與地址計算
arr[i][j]
?的地址:&arr[0][0] + i * 4 + j
(假設?int
?占4字節,4是列數)- 行指針?
arr[i]
:等價于?*(arr + i)
- 元素?
arr[i][j]
:等價于?*(*(arr + i) + j)
3.3 二維數組的傳參方式
(1) 標準方式(必須指定列數)
void print(int arr[][4], int rows){for(int i=0; i<rows; i++){for(int j=0; j<4; j++){printf("%d ", arr[i][j]);}printf("\n");}
}
本質:arr[][4]
?會被編譯器轉換為?int (*)[4]
(2) 數組指針方式
void print(int (*arr)[4], int rows) {// 與上述代碼完全等價
}
關鍵點:
arr+1
?會跳過?16字節(4個int
)- 必須指定列數,否則無法計算步長
(3) 錯誤方式
void print(int **arr, int rows, int cols) { // 錯誤!靜態二維數組不是二級指針
}
下面讓我們具體運行來觀察一下二維數組傳參
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
text(int(*arr)[5], int r,int c)
{for (int i = 0;i < r;i++){for (int j = 0;j< c;j++){printf("%d ", *(*(arr + i)+j));}}
}
int main()
{int arr[3][5] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};text(arr, 3, 5);return 0;
}
注意:
*(arr+i) = arr[i] 因為二維數組的數組名表示第一行的地址? +i表示跳過 i 行? 并且*(*(arr+i)+j) = arr[ i ][ j?]??
二維數組傳參本質上也是傳遞了地址 傳遞的是第一行這個一維數組的地址?
3.4 深入解析?*(*(arr+i)+j)
?與?arr[i][j]
?的等價性
1. 關鍵概念拆解
表達式 | 類型 | 含義 |
---|---|---|
arr | int (*)[3] | 指向第一行({1,2,3} )的指針 |
arr + i | int (*)[3] | 指向第i行的指針 |
*(arr + i) | int * | 第i行的首元素地址(退化成一維) |
*(arr+i) + j | int * | 第i行第j個元素的地址 |
*(*(arr+i)+j) | int | 第i行第j個元素的值 |
2. 與下標訪問的對應關系
arr[i][j] ≡ *(*(arr + i) + j)
編譯器實際處理:
所有?arr[i][j]
?最終都會被轉換為指針運算形式。
3. 為什么需要列數?
arr + i
?的步長取決于列數(sizeof(int[N])
)- 若未指定列數(如?
int arr[][]
),編譯器無法計算?arr + i
?的偏移量
4. 典型考題示例
題目:以下代碼輸出什么?
int arr[2][3] = {{1,2,3}, {4,5,6}};
printf("%d\n", *(*(arr + 1) + 2));
答案:6
解析:
*(arr + 1)
?指向第二行?{4,5,6}
,*(arr + 1) + 2
?指向?6
,解引用后得到值?6
。
掌握這個核心等價關系,就能徹底理解二維數組的指針運算! 🎯
四. 函數指針變量
4.1 函數指針變量的創建
讓我們回憶一下之前的指針內容
字符指針: 存放的是字符的地址 指向的就是字符變量
整型指針: 存放的是整型的地址 指向的是整行變量
數組指針: 存放的是數組的地址 指向的是數組
函數指針: 存放的是函數的地址 指向的是函數
那函數的地址怎么得到的呢?
我們知道數組的地址是通過&+數組名得到的 那函數的地址是通過&+函數名得到的嗎?
答案是 是的 函數的地址就是通過&+函數名獲得
可以看到 地區打印了函數的地址
那函數和數組一樣嗎? 數組名代表了數組首元素的地址? 那函數名代表什么呢? 讓我們來試試
可以看到 打印出來的一模一樣 那函數名是首函數的地址嗎?? 顯然沒有這個說法?
他們倆所得到的都是函數的地址 并沒有什么區別?
那什么是函數指針變量呢?
函數指針變量是一個指向函數的指針,它存儲了函數的地址,可以通過該指針間接調用函數。函數指針的類型由函數的返回類型和參數列表決定。
1. 函數指針的聲明
函數指針的聲明語法如下:
返回類型 (*指針變量名)(參數類型1, 參數類型2, ...);
示例:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int Add(int x, int y)
{return x + y;
}
int main()
{int(*pf)(int,int) = &Add;//pf就是函數指針變量return 0;
}
那存放下面這個函數的函數指針變量該如何寫呢?
int* text(int n, char* p)
{}
通過類比 不難寫出
int* (*pf)(int, char*)=&text;//pf就是函數指針變量
4.2 函數指針變量的使用
我們知道通過解引用操作符 可以通過地址來找到存放的變量 那解引用函數指針變量是否就可以使用函數了呢?? 答案是 是的 例子如下
可以看到 我通過*pf 實現了對函數的調用 并且傳入參數( 3 , 5 )用r來接受 因為函數返回值是int類型 所以r也是int類型
其次 我們知道我們可以通過函數名來調用函數 即Add( 3 , 5 ) 那函數指針變量存放的又是函數名
那我們是否可以直接通過函數指針變量來使用函數呢?? 不妨讓我們試試
可以看到 無論是函數名調用 還是函數指針變量調用? 還是解引用函數指針變量調用 都可以實現對函數的使用
4.3 兩段"有趣"代碼的講解
那讓我們來思考思考下面這段代碼的含義是什么呢?
(*(void(*)())0)();
相信大家看了之后都會感覺 渾身不自在吧 現在讓我們一起來解讀一下這段代碼
4.3.1 解析?(*(void(*)())0)();
這個表達式看起來復雜,但它實際上是一個?函數指針強制轉換 + 調用?的典型例子。我們可以一步步拆解它的含義。
1. 表達式拆解
(*(void(*)())0)();
可以分解為:
(void(*)())0
:將?0
?強制轉換為一個?函數指針。*(void(*)())0
:解引用?這個函數指針,得到函數本身。(*(void(*)())0)();
:調用?這個函數。
2. 詳細分析
(1)?void(*)()
?是什么?
void(*)()
?是一個?函數指針類型,表示:- 返回類型:
void
(無返回值) - 參數列表:
()
(無參數)
- 返回類型:
- 所以,
void(*)()
?是一個?指向無參無返回值函數的指針。
(2)?(void(*)())0
:將?0
?強制轉換為函數指針
0
?是一個整數,代表?內存地址?0x0
(NULL 指針)。(void(*)())0
?表示?把?0
?強制轉換為一個函數指針,即:void (*func_ptr)() = (void(*)())0; // 現在 func_ptr 指向地址 0
(3)?*(void(*)())0
:解引用函數指針
*(void(*)())0
?相當于:void (*func_ptr)() = (void(*)())0; *func_ptr; // 解引用,得到函數本身
- 在 C 語言中,函數指針解引用后仍然是函數,所以?
*func_ptr
?和?func_ptr
?是等價的(見上一節分析)。
(4)?(*(void(*)())0)();
:調用這個函數
- 最終,
(*(void(*)())0)();
?相當于:void (*func_ptr)() = (void(*)())0; (*func_ptr)(); // 調用地址 0 處的函數
- 或者更簡單的寫法(因為?
func_ptr()
?和?(*func_ptr)()
?等價):((void(*)())0)(); // 直接調用
4.3.2?解析void(* signal(int, void(*)(int)))(int);
這個聲明?void(* signal(int, void(*)(int)))(int);
?是一個函數聲明,它定義了一個名為?signal
?的函數。為了理解這個聲明,我們可以逐步解析它:
-
最內層部分?
void(*)(int)
:-
這是一個函數指針類型,指向一個接受?
int
?參數并返回?void
?的函數。 -
例如,
void handler(int sig);
?這樣的函數可以匹配這個指針類型。
-
-
中間部分?
signal(int, void(*)(int))
:-
signal
?是一個函數,它接受兩個參數:-
第一個參數是?
int
?類型。 -
第二個參數是?
void(*)(int)
?類型(即上述的函數指針)。
-
- 因此,
signal
?的函數原型可以理解為:void (*signal(int sig, void (*handler)(int)))(int);
-
-
最外層部分?
void(* ... )(int)
:-
signal
?函數的返回值也是一個函數指針,類型為?void(*)(int)
。 -
也就是說,
signal
?函數返回一個指向“接受?int
?參數并返回?void
?的函數”的指針。
-
簡化理解:
signal
?是一個函數,它接受一個?int
?和一個函數指針,并返回一個同類型的函數指針。
總結:
void(* signal(int, void(*)(int)))(int);
?聲明了一個函數?signal
,它:
- 接受兩個參數:
int
?和?void(*)(int)
(函數指針)。 - 返回一個?
void(*)(int)
?類型的函數指針。
這種寫法在 C 語言中很常見,尤其是在處理回調函數或函數指針時。
4.4?typedef關鍵字講解
typedef
?關鍵字解釋
typedef
?是 C/C++ 中的一個關鍵字,用于為現有的數據類型(包括基本類型、結構體、聯合體、枚舉、函數指針等)定義一個新的別名,使代碼更易讀、更簡潔。
4.4.1 基本語法
typedef <原類型> <新別名>;
<原類型>
:可以是?int
、float
、char
、struct
、union
、enum
?或函數指針等。<新別名>
:你給這個類型取的新名字。
4.4.2?常見用法
(1) 為基本類型定義別名
typedef unsigned int uint; // 定義 uint 代替 unsigned int
typedef float real; // 定義 real 代替 floatuint age = 25; // 等同于 unsigned int age = 25;
real weight = 65.5f; // 等同于 float weight = 65.5f;
(2) 為結構體定義別名
傳統寫法(需要?struct
?關鍵字):
struct Point {int x;int y;
};struct Point p1; // 必須寫 struct Point
使用?typedef
?簡化:
typedef struct {int x;int y;
} Point; // 定義 Point 代替 struct { ... }Point p1; // 直接使用 Point,不需要寫 struct
(3) 為指針類型定義別名
typedef int* IntPtr; // IntPtr 是 int* 的別名int a = 10;
IntPtr p = &a; // 等同于 int* p = &a;
(4) 為函數指針定義別名
原始寫法(復雜):
void (*funcPtr)(int); // funcPtr 是一個指向 void(int) 函數的指針
使用?typedef
?簡化:
typedef void (*FuncPtr)(int); // FuncPtr 是 void(*)(int) 的別名void foo(int x) { printf("%d\n", x); }FuncPtr fp = foo; // 等同于 void (*fp)(int) = foo;
fp(10); // 調用 foo(10)
4.4.3?結合?typedef
?解析?signal
?函數
原聲明:
void (*signal(int, void(*)(int)))(int);
使用?typedef
?簡化:
typedef void (*SignalHandler)(int); // 定義 SignalHandler 代替 void(*)(int)SignalHandler signal(int sig, SignalHandler handler); // 更清晰的聲明
SignalHandler
?是一個函數指針類型,指向?void(int)
?函數。signal
?是一個函數,接受?int
?和?SignalHandler
,并返回?SignalHandler
。
4.4.4?typedef
?的優點
- 提高可讀性:復雜的類型(如函數指針)可以用更直觀的名字表示。
- 減少重復代碼:避免反復寫冗長的類型聲明。
- 便于維護:修改類型時只需改?
typedef
?定義,而不需要修改所有使用的地方。
4.4.5?總結
用途 | 示例 |
---|---|
基本類型別名 | typedef int Int32; |
結構體別名 | typedef struct { ... } Point; |
指針別名 | typedef int* IntPtr; |
函數指針別名 | typedef void (*FuncPtr)(int); |
typedef
?是 C/C++ 中非常重要的關鍵字,能顯著提升代碼的可讀性和可維護性,尤其是在處理復雜類型(如函數指針)時非常有用。
五. 函數指針數組
首先我們要明白什么是函數指針數組?
5.1 基本概念
1. 函數指針
函數指針是指向函數的指針變量。聲明一個函數指針需要指定它指向的函數的返回類型和參數類型。
// 函數原型
int add(int a, int b);
int subtract(int a, int b);// 函數指針聲明
int (*funcPtr)(int, int);// 指向add函數
funcPtr = add;
2. 函數指針數組
函數指針數組是存儲多個函數指針的數組。
// 聲明一個包含兩個函數指針的數組
int (*funcArray[2])(int, int);// 初始化數組
funcArray[0] = add;
funcArray[1] = subtract;
5.2 詳細用法
1. 聲明函數指針數組
// 返回類型 (*數組名[數組大小])(參數列表)
double (*operations[4])(double, double);
2. 初始化函數指針數組
double add(double a, double b) { return a + b; }
double sub(double a, double b) { return a - b; }
double mul(double a, double b) { return a * b; }
double div(double a, double b) { return a / b; }// 初始化數組
operations[0] = add;
operations[1] = sub;
operations[2] = mul;
operations[3] = div;
3. 使用函數指針數組
double result;
int choice = 2; // 假設用戶選擇乘法
double x = 5.0, y = 3.0;// 通過索引調用函數
result = operations[choice](x, y);
printf("結果: %.2f\n", result); // 輸出: 15.00
5.3 實際應用示例
1. 計算器實現
#include <stdio.h>// 定義運算函數
double add(double a, double b) { return a + b; }
double sub(double a, double b) { return a - b; }
double mul(double a, double b) { return a * b; }
double div(double a, double b) { return a / b; }int main() {// 聲明并初始化函數指針數組double (*ops[4])(double, double) = {add, sub, mul, div};int choice;double x, y;printf("選擇運算:\n0. 加\n1. 減\n2. 乘\n3. 除\n");scanf("%d", &choice);printf("輸入兩個數字: ");scanf("%lf %lf", &x, &y);// 調用選中的函數double result = ops[choice](x, y);printf("結果: %.2f\n", result);return 0;
}
運行結果如下:
六. 轉移表
轉移表(Jump Table)
轉移表(也稱為跳轉表)是一種使用函數指針數組來實現多路分支的技術,它比傳統的switch-case
語句更高效、更靈活。
基本概念
轉移表本質上是一個函數指針數組,通過數組索引來選擇和調用不同的函數,避免了冗長的條件判斷。
轉移表 vs switch-case
switch-case實現
void handleCommand(int cmd) {switch(cmd) {case 0: cmd0(); break;case 1: cmd1(); break;case 2: cmd2(); break;// ...default: defaultHandler();}
}
轉移表實現
// 定義命令處理函數
void cmd0() { /* ... */ }
void cmd1() { /* ... */ }
void cmd2() { /* ... */ }
void defaultHandler() { /* ... */ }// 創建轉移表
typedef void (*CommandHandler)(void);
CommandHandler jumpTable[] = {cmd0, cmd1, cmd2};void handleCommand(int cmd) {if (cmd >= 0 && cmd < sizeof(jumpTable)/sizeof(jumpTable[0])) {jumpTable[cmd]();} else {defaultHandler();}
}
轉移表優勢
-
效率更高:直接通過索引訪問,時間復雜度O(1),而switch-case可能需要多次比較
-
代碼更簡潔:特別是當分支很多時
-
更易維護:添加新功能只需擴展數組,不需要修改邏輯結構
-
動態性:可以在運行時修改函數指針
實際應用示例
1. 簡單計算器
#include <stdio.h>// 運算函數
double add(double a, double b) { return a + b; }
double sub(double a, double b) { return a - b; }
double mul(double a, double b) { return a * b; }
double div(double a, double b) { return a / b; }// 定義轉移表
typedef double (*Operation)(double, double);
Operation operations[] = {add, sub, mul, div};int main() {int choice;double x, y;printf("選擇運算(0-3): ");scanf("%d", &choice);printf("輸入兩個數字: ");scanf("%lf %lf", &x, &y);if (choice >= 0 && choice < sizeof(operations)/sizeof(operations[0])) {double result = operations[choice](x, y);printf("結果: %.2f\n", result);} else {printf("無效選擇\n");}return 0;
}
以上就是本篇內容 希望能對你有所幫助