【C語言深度解剖】:(11)函數指針、函數指針數組、指向函數指針數組的指針、回調函數

🤡博客主頁:醉竺

🥰本文專欄:《C語言深度解剖》《精通C指針》

😻歡迎關注:感謝大家的點贊評論+關注,祝您學有所成!


??💜💛想要學習更多C語言深度解剖點擊專欄鏈接查看💛💜???


目錄

1.函數指針

2.函數指針數組

3.指向函數指針數組的指針?

4.回調函數?

4.1 void* 的使用

4.2?使用回調函數,模擬實現qsort

4.3 使用qsort排序結構體?

5.指針和數組筆試題解析?

1.一維數組

2.字符數組

3. 字符串數組

4.字符串指針?

5.二維數組?

6.指針筆試題(難點)

7.對數組和指針的一些思考


1.函數指針

首先看一段代碼:?

輸出的是兩個地址,這兩個地址是 test 函數的地址。

對于函數加不加取地址符號” & “效果都一樣,調用函數加不加解引用符號” * “也都一樣。

那我們的函數的地址要想保存起來,怎么保存?(即函數指針的形式是怎樣的?)

下面我們看代碼:?

首先,能給存儲地址,就要求pfun1或者pfun2是指針,那哪個是指針?

答案是:?

pfun1可以存放。pfun1先和*結合,說明pfun1是指針,指針指向的是一個函數,指向的函數無參數,返回值類型為void。

閱讀兩段有趣的代碼:

分析上述代碼1和代碼2分別代表什么含義?

代碼1解釋:

代碼1是 一次函數調用

調用0地址處的一個函數

首先代碼中將0強制類型轉換成類型為?void (*) ( ) 的函數指針

然后去調用0地址處的函數

第一個” * “號,可無可有,上面已講過。?

代碼2解釋:

代碼2是 一次函數的聲明?

聲明的函數名字為 signal

signal函數的參數有2個,第一個參數是int型,第二個參數類型為函數指針型void (*) (int),該函數指針指向的類型是 返回值為void,其中一個參數是int型的函數

?signal函數的返回類型是一個函數指針,該函數指針指向的類型也是?返回值為void,其中一個參數是int型的函數

如果代碼按照下面這樣子寫,大家可能就更容易理解了,不過這種語法是錯誤的(其實很多復雜指針之所以難學,跟C語言語法風格的設計有很大關系,設計的不夠直觀):

代碼2太復雜,如何簡化:?

typedef void(*pfun_t)(int);pfun_t signal(int, pfun_t);

2.函數指針數組

數組是一個存放相同類型數據的存儲空間,那我們已經學習了指針數組,比如:

int* arr[10];
//數組的每個元素是int*

如果一個數組中存放的都是函數的地址,那這個數組就叫函數指針數組,那函數指針數組如何定義呢??

例子:(計算器)

1.打印菜單及實現函數功能

void menu()
{printf("*********1.Add   2.Sub*********\n");printf("*********3.Mul   2.Div*********\n");printf("*********0.Exit*********\n");
}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;
}

方法1:分支循環實現計算器

int main()
{int ret = 0;int input = 0;do{menu();int x = 0, y = 0; // 參與計算的兩個數字int ret = 0; // 保存計算結果printf("請選擇計算方式->");scanf("%d", &input);switch (input){case 0:printf("退出游戲!\n");break;case 1:printf("請分別輸入兩個數字x和y:\n");scanf("%d%d", &x, &y);ret = Add(x, y);printf("運算結果為:%d\n", ret);break;case 2:printf("請分別輸入兩個數字x和y:\n");scanf("%d%d", &x, &y);ret = Sub(x, y);printf("運算結果為:%d\n", ret);break;case 3:printf("請分別輸入兩個數字x和y:\n");scanf("%d%d", &x, &y);ret = Mul(x, y);printf("運算結果為:%d\n", ret);break;case 4:printf("請分別輸入兩個數字x和y:\n");scanf("%d%d", &x, &y);ret = Div(x, y);printf("運算結果為:%d\n", ret);break;default:printf("輸入不符合條件請重新輸入!\n");break;}} while (input);return 0;
}

方法2:函數指針數組的應用->轉移表,改善方式一的冗余?

int main()
{int ret = 0;int input = 0;int (*pfArr[5])(int, int) = { 0,Add,Sub,Mul,Div }; // 轉移表do{menu();int x = 0, y = 0; // 參與計算的兩個數字int ret = 0; // 保存計算結果printf("請選擇計算方式->");scanf("%d", &input);if (input >= 1 && input <= 4){printf("請分別輸入兩個數字x和y:\n");scanf("%d%d", &x, &y);ret = pfArr[input](x, y);printf("運算結果為:%d\n", ret);}else if (input == 0){printf("退出游戲!\n");break;}else{printf("輸入不符合條件請重新輸入!\n");}} while (input);return 0;
}

方式3:回調函數->實現計算器(第4節會學習回調函數)

void Cal(int (*pfun)(int, int))
{int x = 0, y = 0;printf("請分別輸入兩個數字x和y:\n");scanf("%d%d", &x, &y);int ret = pfun(x, y);printf("運算結果為:%d\n", ret);
}int main()
{int ret = 0;int input = 0;do{menu();int x = 0, y = 0; // 參與計算的兩個數字int ret = 0; // 保存計算結果printf("請選擇計算方式->");scanf("%d", &input);switch (input){case 0:printf("退出游戲!\n");break;case 1:Cal(Add);break;case 2:Cal(Sub);break;case 3:Cal(Mul);break;case 4:Cal(Div);break;default:printf("輸入不符合條件請重新輸入!\n");break;}} while (input);return 0;
}

3.指向函數指針數組的指針?

指向函數指針數組的指針:首先是一個指針,該指針指向一個 數組,數組的元素都是 函數指針 ; 如何定義??

void test(const char* str)
{printf("%s\n", str);
}
int main()
{//函數指針pfunvoid (*pfun)(const char*) = test;//函數指針的數組pfunArrvoid (*pfunArr[5])(const char* str);pfunArr[0] = test;//指向函數指針數組pfunArr的指針ppfunArrvoid (*(*ppfunArr)[5])(const char*) = &pfunArr;return 0;
}

4.回調函數?

回調函數就是一個通過函數指針調用的函數。如果你把函數的指針(地址)作為參數傳遞給另一個函數,當這個指針被用來調用其所指向的函數時,我們就說這是回調函數。回調函數不是由該函數的實現方直接調用,而是在特定的事件或條件發生時由另外的一方調用的,用于對該事件或條件進 行響應。?

首先演示一下qsort函數的使用:

4.1 void* 的使用

在C語言中,void*?是一種特殊的指針類型,它表示一個指向未知類型的指針。這種指針可以指向任何類型的數據,但它不知道所指向數據的具體類型。void*?主要用于以下幾種情況:?

  1. 函數指針參數或返回值:當函數需要處理多種類型的數據,但具體類型不確定時,可以使用?void*?作為參數類型或返回類型。例如,標準庫函數?memcpy?就使用了?void*?作為參數。

  2. 動態內存分配malloc?函數返回?void*?類型的指針,因為它可以分配任何類型的內存。在使用時,通常需要將?void*?強制轉換為實際需要的類型。

  3. 類型無關的代碼:有時,我們希望編寫可以處理任何類型的代碼,例如通用數據結構或容器,這時可以使用?void*?來實現類型無關性。

?示例1:作為函數參數

#include <stdio.h>void print_value(void* ptr) {// 強制類型轉換int value = *(int*)ptr;printf("%d\n", value);
}int main() {int x = 10;print_value((void*)&x);return 0;
}

在這個例子中,print_value?函數接受一個?void*?類型的參數,并在函數內部將其強制轉換為?int*?類型,然后打印出該指針指向的整數值。?

示例2:動態內存分配

#include <stdio.h>
#include <stdlib.h>int main() {// 動態分配一個整型大小的內存void* ptr = malloc(sizeof(int));// 強制類型轉換后使用*(int*)ptr = 20;printf("%d\n", *(int*)ptr);// 釋放內存free(ptr);return 0;
}

在這個例子中,我們使用?malloc?分配內存,它返回一個?void*?類型的指針。我們將其強制轉換為?int*?類型,以便能夠存儲一個整數值。

注意事項:

  • 使用?void*?時,必須確保類型轉換是正確的,否則可能會導致未定義行為。
  • void*?指針不能直接進行算術操作,例如自增或自減,因為編譯器不知道指針指向的數據類型大小。
  • 在使用?void*?指針前,應確保它確實指向了正確的數據類型,否則在解除引用時可能會出現問題。?

4.2?使用回調函數,模擬實現qsort

(這里內部結構采用冒泡的方式)?

#include <stdio.h>
int int_cmp(const void* p1, const void* p2)
{return (*(int*)p1 - *(int*)p2);
}
void _swap(void* p1, void* p2, int size)
{int i = 0;for (i = 0; i < size; i++){char tmp = *((char*)p1 + i);*((char*)p1 + i) = *((char*)p2 + i);*((char*)p2 + i) = tmp;}
}
void bubble(void* base, int count, int size, int(*cmp)(void*, void*))
{int i = 0;int j = 0;for (i = 0; i < count - 1; i++){for (j = 0; j < count - i - 1; j++){if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0){_swap((char*)base + j * size, (char*)base + (j + 1) * size, size);}}}
}
int main()
{int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };//char *arr[] = {"aaaa","dddd","cccc","bbbb"};int i = 0;bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++){printf("%d ", arr[i]);}printf("\n");return 0;
}

4.3 使用qsort排序結構體?

5.指針和數組筆試題解析?

注意一下代碼測試環境為visual stdio 2022 x86環境下(地址/指針 大小為4字節)?

1.一維數組

2.字符數組

下面一些代碼存在一些問題:

  1. strlen(*arr):這里?*arr?是數組的第一個元素,是一個字符,不是字符串的地址,所以這是錯誤的。

  2. strlen(arr[1]):這里?arr[1]?是數組的第二個元素,是一個字符,不是字符串的地址,所以這是錯誤的。

  3. strlen(&arr):這里?&arr?是整個數組的地址,但是?strlen?需要一個以?\0?結尾的字符串的地址,所以這是錯誤的。

  4. strlen(&arr + 1):這里?&arr + 1?是數組后面的內存地址,這并不是一個有效的字符串地址,所以這是錯誤的。

  5. strlen(&arr[0] + 1):這里?&arr[0] + 1?是數組的第二個元素的地址,但是數組沒有以?\0?結尾,所以這也是錯誤的。

  • 使用?strlen?函數時,需要確保傳遞的參數是一個以?\0?結尾的字符串的地址。
  • strlen(*arr)strlen(arr[1])strlen(&arr)strlen(&arr + 1)?和?strlen(&arr[0] + 1)?都是錯誤的,因為它們傳遞的參數不是字符串的地址。
  • 由于數組?arr?沒有以?\0?結尾,所以?strlen(arr)?和?strlen(arr + 0)?也可能導致未定義行為。

為了避免這些問題,確保在使用?strlen?時傳遞的參數是一個以?\0?結尾的字符串的地址,并且在使用?sizeof?時理解你正在計算的是數組的大小還是指針的大小。

3. 字符串數組

下面一些代碼存在一些問題:?

  1. strlen(*arr):這是錯誤的,*arr?是數組第一個元素,即字符?'a',而不是一個字符串的地址。因此,strlen(*arr)?會導致未定義行為,因為?strlen?預期的是一個字符串的地址。

  2. strlen(arr[1]):這也是錯誤的,arr[1]?是數組第二個元素,即字符?'b',同樣不是一個字符串的地址。因此,strlen(arr[1])?也會導致未定義行為。

  3. strlen(&arr):這是錯誤的,&arr?是整個數組的地址,strlen?會嘗試計算從該地址開始直到遇到 null 字符的字符數。但是,因為?&arr?指向的是整個數組,而不是字符串的起始位置,所以這可能會導致計算出一個錯誤的結果,或者在某些情況下導致未定義行為。

  4. strlen(&arr + 1):這是錯誤的,&arr + 1?是數組后面的內存地址,它不指向任何有效的字符串。因此,strlen(&arr + 1)?會導致未定義行為。

4.字符串指針?

下面一些代碼存在一些問題:

  1. strlen(*p):這是錯誤的,*p?是字符串的第一個字符,不是一個字符串的地址,所以這是未定義行為。

  2. strlen(p[0]):這也是錯誤的,p[0]?是字符串的第一個字符,不是一個字符串的地址,所以這是未定義行為。

  3. strlen(&p):這是錯誤的,&p?是指針?p?的地址,不是一個字符串的地址,所以這是未定義行為。

  4. strlen(&p + 1):這也是錯誤的,&p + 1?是指針?p?后面的

5.二維數組?

sizeof(a[3]):這是錯誤的,因為?a?只有 3 行,a[3]?超出了數組的范圍,這將導致未定義行為。正確的做法是確保索引在數組的范圍內。

但是在Visua Stdio運行中,并沒有報錯,結果反而是16為什么呢?

  • 在 C 語言中,sizeof?運算符返回的是操作數的大小,以字節為單位。當你嘗試計算?sizeof(a[3])?時,實際上你在嘗試獲取數組的第四行(記住數組索引是從 0 開始的)的大小。然而,由于你的數組?a?只有 3 行,a[3]?實際上是一個越界的訪問。
  • 在 Visual Studio 中,當你嘗試訪問越界的數組行時,你可能會得到一個看似合理的值(比如 16 字節),這是因為?sizeof?運算符不會實際訪問內存,它只是返回類型的大小。在這種情況下,a[3]?被當作一個指向?int[4](即一個有 4 個整數的數組)的指針,因此?sizeof(a[3])?返回的是?int[4]?的大小,即 4 個整數乘以每個整數的大小(通常在 32 位系統中是 4 字節,在 64 位系統中是 8 字節,取決于?int?的大小)。
  • 這就是為什么你得到了 16 字節的結果,因為它相當于?4 * sizeof(int)。然而,這并不意味著?a[3]?是一個有效的數組行,它只是?sizeof?運算符根據?a[3]?的類型推斷出的結果。實際上,訪問?a[3]?是未定義行為,可能會導致程序崩潰或其他意外結果。

下面對二維數組的進行一些拓展:

總結:?

6.指針筆試題(難點)

筆試題1:

筆試題2:

?筆試題3

筆試題4:?

筆試題5 :

筆試題6:

筆試題7:

筆試題8:


7.對數組和指針的一些思考

?想繼續深入學習指針,可以訂閱下方”精通C指針“專欄?哦~

《精通C指針》icon-default.png?t=N7T8http://t.csdnimg.cn/gbpQp

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/13296.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/13296.shtml
英文地址,請注明出處:http://en.pswp.cn/web/13296.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

AVDemo漏洞平臺黑盒測試

信息收集 說明一下&#xff1a; 因為是本地的環境&#xff0c;端口這些就不掃描了&#xff0c; 還有這個是某個dalao寫的平臺&#xff0c;也就檢測不到什么cms了&#xff0c; 信息收集&#xff0c;端口&#xff0c;cms這些是必做的&#xff0c; 首先&#xff0c;這里先簡單的…

web3 ETF軟件開發難點

開發一個涉及到 Web3 ETF&#xff08;Exchange-Traded Fund&#xff0c;交易所交易基金&#xff09;的軟件可能會面臨一些挑戰和難點&#xff0c;特別是在整合 Web3 技術和金融服務方面。以下是一些可能的難點。北京木奇移動技術有限公司&#xff0c;專業的軟件外包開發公司&am…

記一次:mysql統計的CAST函數與json字段中的某個字段

前言&#xff1a;因為需求的問題&#xff0c;會遇到將某個json存入到一個字段中&#xff0c;但在統計的時候&#xff0c;又需要將這個json中的某個字段作為條件來統計&#xff0c;所以整理了一下cast函數和json中某個字段的條件判斷 一、淺談mysql的json 1.1 上例子 SELECTli…

植物大戰僵尸雜交版(含下載方式)

最近時間&#xff0c;一款很火的植物大戰僵尸雜交版火爆出圈&#xff0c;在玩家之間瘋狂擴散。各種奇特的雜交組合讓游戲變得更加有趣。 游戲介紹 植物大戰僵尸雜交版是一款將《植物大戰僵尸》和植物雜交概念結合在一起的獨特塔防策略游戲。它將《植物大戰僵尸》中的植物與進行…

什么是析構函數?

在編程語言C中&#xff0c;析構函數是一個特別重要的組件&#xff0c;它主要負責在對象生命周期結束時釋放資源和執行清理任務。析構函數的正確實現對于資源管理尤為關鍵&#xff0c;尤其是在處理動態分配內存、文件句柄、網絡連接或其他系統資源時。本文將詳細介紹析構函數的基…

Minio 對象存儲 OSS概述

系列文章目錄 第五章 Minio 對象存儲 OSS概述 Minio 對象存儲 OSS概述 系列文章目錄對象存儲 OSS基本概念存儲空間&#xff08;Bucket&#xff09;對象&#xff08;Object&#xff09;ObjectKeyRegion&#xff08;地域&#xff09;Endpoint&#xff08;訪問域名&#xff09;Ac…

C#知識|上位機子窗體嵌入主窗體方法(實例)

哈嘍,你好啊,我是雷工! 上位機開發中,經常會需要將子窗體嵌入到主窗體, 本節練習C#中在主窗體的某個容器中打開子窗體的方法。 01 需求說明 本節練習將【賬號管理】子窗體在主窗體的panelMain容器中打開。 賬號管理子窗體如下: 主窗體的panelMain容器位置如圖: 02 實現…

一次JAVA接口優化記錄

目錄 一次接口優化記錄首先考慮&#xff0c;添加緩存緩存策略方案一&#xff1a;本地緩存方案二&#xff1a;Redis緩存 優化結果原因分析&#xff1a;原因驗證 接口數據分析將響應數據返回大小減少compression壓縮配置完美&#xff08;代指這里的小系統&#xff09; 一次接口優…

CentOS 的常見命令

CentOS 是一種廣泛使用的 Linux 發行版&#xff0c;特別在服務器環境中。本文將詳細介紹 CentOS 中常見的命令&#xff0c;以便幫助用戶在操作系統中有效地進行各種操作。下面介紹一下文件和目錄操作、用戶和權限管理、系統信息查看、軟件包管理以及網絡配置等方面的命令。 一…

應用層協議【HTTP和HTTPS】

1.概念 1.1 協議 協議是指在計算機通信和網絡通信中&#xff0c;為了實現數據交換而建立的一套規則、約定或者標準。它定義了通信雙方之間的通信格式、傳輸方式、數據的含義、錯誤處理等細節&#xff0c;從而確保通信的可靠性、有效性和安全性。 >1在計算機網絡中&#x…

Python簡易圖書管理系統重構

在本篇課文中&#xff0c;我們將使用Python語言結合MySQL數據庫&#xff0c;從零開始構建一個簡單的圖書管理系統。該系統旨在幫助圖書館管理員輕松管理圖書的借閱、歸還以及查詢圖書信息等日常操作。我們將分步介紹需求分析、數據庫設計、環境搭建、功能實現等關鍵環節&#x…

注冊講堂 | 體外診斷試劑分類目錄的變化

5月11日&#xff0c;千呼萬喚的《體外診斷試劑分類目錄》&#xff08;2024年第58號&#xff09;終于發布&#xff01; 前世今生 2013年&#xff1a;《6840 體外診斷試劑分類子目錄&#xff08;2013版&#xff09;》&#xff08;以下簡稱2013版目錄&#xff09; 2017年&#xff…

蘋果永久版安裝PD虛擬機:Parallels Desktop 19 一鍵激活版

Parallels Desktop 19是一款功能強大的虛擬機軟件&#xff0c;專為Mac用戶設計&#xff0c;允許用戶在同一臺Mac電腦上同時運行Windows、Linux等多個操作系統&#xff0c;而無需額外的硬件設備。 下載地址&#xff1a;https://www.macz.com/mac/9581.html?idOTI2NjQ5Jl8mMjcuM…

Kubernetes入門:核心概念

集群架構與組件 一個kubernetes集群主要是由控制節點(master)、工作節點(node)構成&#xff0c;每個節點上都會安裝不同的組件。 master&#xff1a;集群的控制平面&#xff0c;負責集群的決策 ( 管理 ) api-server : 資源操作的唯一入口&#xff0c;接收用戶輸入的命令&…

vue3 項目中 前端實現下載模板 csv文件

做項目時遇到讓前端實現模板下載功能&#xff0c;第一次碰到這種需求&#xff0c;記錄一下。 下載csv 模板&#xff1a; <el-button type"primary" click"download(data/CSVXX.csv)">下載模板</el-button> const download (url) > {con…

文本控件Text Control示例: 將圖像插入 TX 的各種方法

TX Text Control 是一款功能類似于 MS Word 的文字處理控件&#xff0c;包括文檔創建、編輯、打印、郵件合并、格式轉換、拆分合并、導入導出、批量生成等功能。廣泛應用于企業文檔管理&#xff0c;網站內容發布&#xff0c;電子病歷中病案模板創建、病歷書寫、修改歷史、連續打…

在Linux上面部署ELK

注明&#xff1a;一下的軟件需要自己準備 一、準備環境&#xff1a; 1.兩臺elasticsearch主機4G內存 2.兩臺elasticsearch配置主機名node1和node2(可以省略) #vim /etc/hostname #reboot 3. 兩臺elasticsearch配置hosts文件 #vim /etc/hosts 192.168.1.1 node1 192…

RTMP低延遲推流

人總是需要壓力才能進步, 最近有個項目, 需要我在RK3568上, 推流到公網, 最大程度的降低延遲. 廢話不多說, 先直接看效果: 數據經過WiFi發送到Inenter的SRS服務器, 再通過網頁拉流的. 因為是打金任務, 所以逼了自己一把, 把RTMP推流好好捋一遍. 先說說任務目標, 首先是MPP編碼…

【Altium】AD-檢查原理圖中元器件未連接的Passive Pin

1、 文檔目標 如何讓原理圖編譯時找出元器件上未連接的Passive Pin 2、 問題場景 當引腳屬性&#xff08;Pin type&#xff09;為passive時&#xff0c;原理圖編譯的默認規則是不會去檢查它們是否有連接的。在實際設計過程中&#xff0c;經常會有導線虛連&#xff0c;漏連的事…