文章目錄
- 1. 二級指針
- 2. 指針數組
- 3. 字符指針變量
- 4. 數組指針變量
- 5. 二維數組傳參的本質
- 6. 函數指針變量
- 7. 函數指針數組
- 8. 轉移表
- 9. 回調函數
- 10. qsort函數的使用與模擬實現
1. 二級指針
我們知道,指針變量也是變量,它也有自己的地址,使用什么來存放它的地址呢?答案是:二級指針。
int main()
{int a = 10;int* p = &a;int** pp = &p; //二級指針變量ppreturn 0;
}
關于二級指針的運算
- *pp先解引用,對pp中的地址進行訪問,訪問的就是p
- **pp, 先通過*pp找到p,再對p進行解引用,訪問的就是a
2. 指針數組
指針數組,顧名思義,它應該是一個數組,是用來存放指針的。
指針數組中的每一個元素又是一個地址,可以指向另一個區域。
int main()
{int arr1[] = { 1,2,3,4,5 };int arr2[] = { 2,3,4,5,6 };int arr3[] = { 3,4,5,6,7 };//數組名是數組首元素的地址,類型是int*,可以存放在數組指針arr中int* arr[3] = { arr1, arr2, arr3 };return 0;
}
- 使用指針數組模擬二維數組
int main()
{int arr1[] = { 1,2,3,4,5 };int arr2[] = { 2,3,4,5,6 };int arr3[] = { 3,4,5,6,7 };//數組名是數組首元素的地址,類型是int*,可以存放在數組指針arr中int* arr[3] = { arr1, arr2, arr3 };int i = 0;for (i = 0; i < 3; i++){int j = 0;for (j = 0; j < 5; j++){printf("%d ", arr[i][j]);//也可以寫成下面這種形式//printf("%d ", *(*(arr + i) + j));}printf("\n");}return 0;
}
3. 字符指針變量
在指針的類型中,我們知道有一種指針類型叫字符指針。
一般使用:
int main()
{char ch = 'c';char* pc = &ch;*pc = 'a';printf("%c\n", ch);return 0;
}
還有一種使用方式:
int main()
{char* pc = "abcdef";printf("%s\n", pc);return 0;
}
- 可以把字符串想象為一個字符數組,但是這個數組是不能修改的,因此為了避免出錯,常常加上const
const char* pc = "abcdef";
- 當常量字符串出現在表達式中的時候,他的值是第一個字符的地址。當我們知道存放的是第一個字符的地址的時候,我們就可以這樣玩
int main()
{char* pc = "abcdef";printf("%c\n", "abcdef"[3]); //dprintf("%c\n", pc[3]); //dreturn 0;
}
下面來看一道題目:
str1 不等于 str2 這個很好理解;str3 等于str4怎么理解呢?
由于str3 與 str4中存放的都是常量字符串,C/C++會把常量字符串存儲到單獨的?個內存區域,當?個指針指向同?個字符串的時候,他們實際會指向同?塊內存。(內容相等的常量字符串僅保存一份)
但是?相同的常量字符串去初始化不同的數組的時候就會開辟出不同的內存塊
。所以str1和str2不同,str3和str4相同。
4. 數組指針變量
數組指針變量是數組還是指針呢?答案是:指針。
我們已經熟悉:
- 整形指針變量: int * pint; 存放的是整形變量的地址,能夠指向整形數據的指針。
- 浮點型指針變量: float * pf; 存放浮點型變量的地址,能夠指向浮點型數據的指針。
那數組指針變量應該是:存放的應該是數組的地址,能夠指向數組的指針變量。
int main()
{int arr[10] = { 0 };int(*parr)[10] = &arr; //數組指針變量parrreturn 0;
}
5. 二維數組傳參的本質
我們知道一維數組傳參的本質是:傳的是數組首元素的地址。
那二維數組呢?
過去我們使用二維數組時,是這樣的:
void func(int arr[][5], int row, int col)
{int i = 0;for (i = 0; i < row; i++){int j = 0;for (j = 0; j < col; j++){printf("%d ", arr[i][j]);}printf("\n");}
}
int main()
{int arr[3][5] = { {1,2,3,4,5}, {2,3,4,5,6}, {3,4,5,6,7} };func(arr, 3, 5);return 0;
}
這里實參是?維數組,形參也寫成?維數組的形式,那還有什么其他的寫法嗎?
首先,我們應該知道?維數組其實可以看做是每個元素是?維數組的數組
,也就是?維數組的每個元素是?個?維數組。那么?維數組的首元素就是第一行,是個?維數組。
所以,根據數組名是數組首元素的地址這個規則,?維數組的數組名表示的就是第??的地址,是?維數組的地址。根據上?的例?,第?行的?維數組的類型就是 int [5] ,所以第?行的地址的類型就是數組指針類型 int(*)[5] 。那就意味著?維數組傳參本質上也是傳遞了地址,傳遞的是第?行這個?維數組的地址
,那么形參也是可以寫成指針形式的。如下:
void func(int (*arr)[5], int row, int col)
{int i = 0;for (i = 0; i < row; i++){int j = 0;for (j = 0; j < col; j++){printf("%d ", *(*(arr + i) + j));}printf("\n");}
}
6. 函數指針變量
函數指針變量應該是用來存放函數地址的,未來通過地址能夠調用函數,那函數是否真的有地址呢?
確實打印出來了地址,所以函數是有地址的。
- 函數名就是函數的地址
- &函數名 也是函數的地址。
這里和數組相比還是有區別的
- 數組名是數組首元素的地址;
- &數組名是整個數組的地址
函數指針類型解析:
函數指針變量的使用
int add(int x, int y)
{return x + y;
}
int main()
{int (*p1)(int, int) = &add;int ret1 = add(3, 5);printf("%d\n", ret1); //8int ret2 = (*p1)(3, 5);printf("%d\n", ret2); //8int (*p2)(int, int) = add;int ret3 = (*p2)(4, 6);printf("%d\n", ret3); //10int ret4 = p2(4, 6);printf("%d\n", ret4); //10return 0;
}
因為函數名就是地址,我們調用函數的時候沒有使用解引用,所以函數指針也可以不解引用, 如ret4。
兩段有趣的代碼:
(*(void (*)())0)();
- 上述代碼是一次函數調用
- 將0強制類型轉換成一個函數指針,這個函數沒有參數,返回類型是void
- (*0)()調用0地址處的函數
void (* signal(int , void(*)(int)) )(int);
- signal是一個函數名,這個函數有兩個參數,一個是整型int,一個是函數指針類型的 void (*)(int),這個函數的參數是int,返回值是void
- void (*)(int) 去掉函數名和函數參數,剩下的就是函數的返回類型。該signal函數的返回類型是 void ( * )(int)的函數指針。
這樣寫是不是挺不好理解的,我們可以使用typedef將復雜的類型簡單化。
比如,將 int* 重命名為 int_p ,這樣寫
typedef int* int_p;
但是對于數組指針和函數指針稍微有點區別:新的名字必須在*的旁邊
比如我們有數組指針類型 int(*)[5] ,需要重命名為 parr_t ,那可以這樣寫:
typedef int (*parr_t)[5];
將 void(*)(int) 類型重命名為 pf_t ,就可以這樣寫:
typedef void (*pf_t)();
pf_t signal(int, pf_t); //signal函數就可以被這樣簡化
7. 函數指針數組
數組是?個存放相同類型數據的存儲空間,而且我們已經學習了函數指針。
int Add(int x, int y)
{return x + y;
}int main()
{int (*p)(int, int) = Add; //函數指針return 0;
}
那么如果要把多個函數(函數參數的類型、返回值的類型都應相同
)的地址存放起來,那函數指針數組應該如何定義呢?
int Add(int x, int y)
{return x + y;
}int Sub(int x, int y)
{return x - y;
}int Mul(int x, int y)
{return x * y;
}int Div(int x, int y)
{return x / y;
}int main()
{int (*p)(int, int) = Add;//函數指針int (*pArr[])(int, int) = { Add, Sub, Mul, Div }; //函數指針數組return 0;
}
我們都知道,一個數組,去掉數組名和 [ ] 剩下的就是數組元素的類型。例如 int arr[ ], int 就是數組元素的類型。
因此 int (* pArr[ ] )(int, int),去掉數組名和 [],剩下的int (*)(int ,int)就是這個數組元素的類型,很顯然,這是數組元素的類型是函數指針類型。
函數指針數組的使用:
8. 轉移表
當我們學習完函數指針數組以后,你是否也有過這樣的疑問:我直接通過函數名調用函數不是更簡單嗎,干嘛還要放進數組中,然后再調用函數呢?請看下面的代碼:
假設我們要實現一個計算器,一般寫法是不是這樣呢?
void menu()
{printf("****************************\n");printf("******1.Add 2.Sub*****\n");printf("******3.Mul 4.Div*****\n");printf("******0.exit *****\n");printf("****************************\n");
}int main()
{int input = 0;int x = 0;int y = 0;int ret = 0;do{menu();printf("請選擇:\n");scanf("%d", &input);switch (input){case 1:printf("請輸入兩個操作數:");scanf("%d %d", &x, &y);ret = Add(x, y);printf("%d\n", ret);break;case 2:printf("請輸入兩個操作數:");scanf("%d %d", &x, &y);ret = Sub(x, y);printf("%d\n", ret);break;case 3:printf("請輸入兩個操作數:");scanf("%d %d", &x, &y);ret = Mul(x, y);printf("%d\n", ret);break;case 4:printf("請輸入兩個操作數:");scanf("%d %d", &x, &y);ret = Div(x, y);printf("%d\n", ret);break;case 0:printf("退出計算器\n");break;default:printf("選擇錯誤,請重新選擇!\n");}} while (input);return 0;
}
因此,我們就可以利用轉移表來解決代碼的冗余問題,首先,我們要知道什么是轉移表?
轉移表就是用一個函數指針數組存儲每一個自定義的函數指針,在調用自定義函數的時候,就可以通過數組下標訪問 ----總結于《C和指針》
利用轉移表解決問題:
int main()
{int input = 0;int x = 0;int y = 0;int ret = 0;int (*pArr[])(int, int) = { NULL, Add, Sub, Mul ,Div };// 0 1 2 3 4 使用NULL巧妙地與選擇相對應do{menu();printf("請選擇:\n");scanf("%d", &input);if (input == 0){printf("退出計算器\n");}else if (input >= 1 && input <= 4){printf("請輸入兩個操作數:");scanf("%d %d", &x, &y);ret = pArr[input](x, y);printf("%d\n", ret);}else{printf("選擇錯誤,請重新選擇!\n");}} while (input);return 0;
}
這樣是不是就很好地解決了代碼地冗余問題,同時也方便了以后再增加新的功能,只需向函數指針數組中添加函數的地址即,改變以下判斷的范圍即可,無需再寫一大串的case了。
9. 回調函數
- 回調函數是什么呢?
回調函數就是?個通過函數指針調用的函數。
如果你把函數的指針(地址)作為參數傳遞給另?個函數,當這個指針被用來調用其所指向的函數時,被調用的函數就是回調函數。回調函數不是由該函數的實現方直接調用,而是在特定的事件或條件發生時由另外的?方調用的,用于對該事件或條件進行響應
Calculate函數的參數是函數指針類型的
void Calculate(int (*pfunc)(int, int))
{int x = 0;int y = 0;int ret = 0;printf("請輸入兩個操作數:");scanf("%d %d", &x, &y);ret = pfunc(x, y); //通過函數指針調用函數printf("%d\n", ret);
}int main()
{int input = 0;do{menu();printf("請選擇:\n");scanf("%d", &input);switch (input){case 1:Calculate(Add);break;case 2:Calculate(Sub);break;case 3:Calculate(Mul);break;case 4:Calculate(Div);break;case 0:printf("退出計算器\n");break;default:printf("選擇錯誤,請重新選擇!\n");}} while (input);return 0;
}
我們可以發現,當Calculate函數的參數是函數指針類型時,只要你給Calculate函數傳遞一個函數指針類型的變量,它都可以調用,這樣看它的功能是不是強大了不少。
10. qsort函數的使用與模擬實現
- qsort函數是什么?
qsort函數是可以排序任何類型數據的一個函數。 - qsort是如何設計的?
void qsort (void* base, size_t num, size_t size,int (*compar)(const void*,const void*));
這個函數有四個參數,各參數的說明如下:
第四個參數有點特別,它是一個函數指針,指針指向的函數的功能是比較兩個元素的大小,這個函數需要qsort函數的使用者自己實現,并且函數的返回值要符合qsort函數的要求
那么我們先來使用以下qsort函數吧:
struct Stu
{int age;char name[20];
};
//使用者自己實現兩個元素的比較函數
int cmp(const void* p1, const void* p2)
{return *((int*)p1) - *((int*)p2);//此處是將p1、p2變量強制轉換為 整型指針變量 然后解引用
}int cmp_struct_name(const void* p1, const void* p2)
{return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}void print1(int arr[], int sz)
{int i = 0;for (i = 0; i < sz; i++){printf("%d ", arr[i]);}printf("\n");
}void print2(struct Stu arr[],int sz)
{int i = 0;for (i = 0; i < sz; i++){printf("%d %s\n", arr[i].age, arr[i].name);}printf("\n");
}int main()
{int arr1[] = { 2,1,8,5,6,3,4,9,7,0 };int sz1 = sizeof(arr1) / sizeof(arr1[0]);print1(arr1, sz1);qsort(arr1, sz1, sizeof(arr1[0]), cmp);print1(arr1, sz1);struct Stu arr2[] = { {18, "zhangsan"}, {38, "lisi"}, {25, "wangwu"} };int sz2 = sizeof(arr2) / sizeof(arr2[0]);print2(arr2, sz2);qsort(arr2, sz2, sizeof(arr2[0]), cmp_struct_name);print2(arr2, sz2);return 0;
}
接下來,我們就來模擬實現一個qsort函數,由于qsort的實現使用的是快速排序,我們在此就使用冒泡排序
//使用者自己實現兩個元素的比較函數
int cmp(const void* p1, const void* p2)
{return *((int*)p1) - *((int*)p2);//此處是將p1、p2變量強制轉換為 整型指針變量 然后解引用
}int cmp_struct_name(const void* p1, const void* p2)
{return strcmp(((struct Stu*)p1)->name, ((struct Stu*)p2)->name);
}int cmp_struct_age(const void* p1, const void* p2)
{return (*(struct Stu*)p1).age - (*(struct Stu*)p2).age;
}//由于被交換的數據的類型不是固定的,但是數據類型的大小是知道的
//因此我們可以交換數據的每一個字節的數據
void _Swap(const void* p1, const void* p2, int sz)
{int i = 0;//交換數據的每一個字節for (i = 0; i < sz; i++){char tmp = *((char*)p1 + i);*((char*)p1 + i) = *((char*)p2 + i);*((char*)p2 + i) = tmp;}
}void my_qsort(void* base, int num, int size, int (*compar)(const void*, const void*))
{int i = 0;for (i = 0; i < num - 1; i++){int j = 0;for (j = 0; j < num - 1 - i; j++){
//此處應該是傳給compar()兩個參數arr[j]與arr[j+1],讓其進行比較
//但是怎么拿到要傳的數呢?
//qsort函數只有這個數組首元素的地址,和數組元素類型的大小
//因此我可以讓base指針加上 j個類型的大小 找到某個元素的首地址,具體比較多大內容的數據,看比較什么類型的數據,由使用者決定
//但是我得到的數組首元素的地址也是void* 類型的,所以我們可以將base轉換位char*類型的指針,一次訪問 j*size 大小if (compar((char*)base + j * size, (char*)base + (j + 1) * size) > 0){//交換_Swap((char*)base + j * size, (char*)base + (j + 1) * size, size);}}}
}
int main()
{//比較整型的數據int arr1[] = { 2,1,8,5,6,3,4,9,7,0 };int sz1 = sizeof(arr1) / sizeof(arr1[0]);printf("排序整型\n");print1(arr1, sz1);my_qsort(arr1, sz1, sizeof(arr1[0]), cmp);print1(arr1, sz1); //比較結構體類型的數據struct Stu arr2[] = { {38, "lisi"}, {18, "zhangsan"}, {25, "wangwu"} };int sz2 = sizeof(arr2) / sizeof(arr2[0]);printf("排序結構體型-按姓名\n");print2(arr2, sz2);my_qsort(arr2, sz2, sizeof(arr2[0]), cmp_struct_name);print2(arr2, sz2);printf("排序結構體型-按年齡\n");print2(arr2, sz2);my_qsort(arr2, sz2, sizeof(arr2[0]), cmp_struct_age);print2(arr2, sz2);return 0;
}
本次的分享就到這里啦~