【說在前面的話】
2022年了,想必已經(jīng)不會(huì)有人對(duì)嵌入式開發(fā)中“數(shù)據(jù)結(jié)構(gòu)(Data )”的作用產(chǎn)生疑問了吧?無論你是否心存疑惑,本文都將給你一個(gè)完全不同的視角。
每每說起數(shù)據(jù)結(jié)構(gòu),很多人腦海里復(fù)現(xiàn)的一定是以下的內(nèi)容:
數(shù)據(jù)結(jié)構(gòu)其實(shí)不是一個(gè)高大上的名詞,它意外的非常樸實(shí)——你也許每天都在用。作為一個(gè)新坑,我將在【非常C結(jié)構(gòu)】系列文章中為大家分享很多嵌入式開發(fā)中很多“非常”而又“好用”的數(shù)據(jù)結(jié)構(gòu)。
【人人都可以學(xué)會(huì)的“表格”】
你不必學(xué)過所謂的“關(guān)系數(shù)據(jù)庫”也可以理解“表格(Table)”這種數(shù)據(jù)結(jié)構(gòu)的本質(zhì)含義。
在C語言環(huán)境中,表格的本質(zhì)就是結(jié)構(gòu)體數(shù)組,即:由結(jié)構(gòu)體組成的數(shù)組。這里:
在嵌入式系統(tǒng)中,表格具有以下特點(diǎn):
如果一個(gè)需求能夠1)接受上述的特點(diǎn);或者2)本身就具有上述特點(diǎn);或者3)部分內(nèi)容經(jīng)過改造后可以接受上述特點(diǎn)——那么,就可以使用表格來保存數(shù)據(jù)了。
一個(gè)典型的例子就是:交互菜單。
很容易看到,每一級(jí)菜單本質(zhì)上都“可以”是一個(gè)表格。
雖然在很多UI設(shè)計(jì)工具中(比如LVGL),菜單的內(nèi)容是在運(yùn)行時(shí)刻動(dòng)態(tài)生成的(用鏈表來實(shí)現(xiàn)),但在嵌入式系統(tǒng)中,動(dòng)態(tài)生成表格本身并不是一個(gè)“必須使用”的特性,相反,由于產(chǎn)品很多時(shí)候功能固定——菜單的內(nèi)容也是固定的,因此完全沒有必要在運(yùn)行時(shí)刻進(jìn)行動(dòng)態(tài)生成——這就滿足了表格的“在編譯時(shí)刻初始化”的要求。
采用表格的形式來保存菜單,就獲得了在ROM中保存數(shù)據(jù)、減少RAM消耗的的優(yōu)勢(shì)。同時(shí),數(shù)組的訪問形式又進(jìn)一步簡(jiǎn)化了用戶代碼。
另外一個(gè)常見用到表格的例子是消息地圖(Message Map),它在通信協(xié)議棧解析類的應(yīng)用中非常常見,在很多結(jié)構(gòu)緊湊功能復(fù)雜的中也充當(dāng)著重要的角色。
如果你較真起來,菜單也不過消息地圖的一種。表格不是實(shí)現(xiàn)消息地圖的唯一方式,但卻是最簡(jiǎn)單、最常用、數(shù)據(jù)存儲(chǔ)密度最高的形式。在后續(xù)的例子中,我們就以“消息地圖”為例,深入聊聊表格的使用和優(yōu)化。
【表格的定義】
一般來說,表格由兩部分構(gòu)成:
因此,表格的定義也分為兩個(gè)部分:
記錄的定義一般格式如下:
typedef?struct?<表格名稱>_item_t <表格名稱>_item_t;
struct?<表格名稱>_item_t {
????//?每條記錄中的內(nèi)容
};
這里,第一行的typedef所在行的作用是“前置聲明”;struct所在行的作用是定義結(jié)構(gòu)體的實(shí)際內(nèi)容。雖然我們完全可以將“前置聲明”和“結(jié)構(gòu)體定義”合二為一,寫作:
typedef?struct <表格名稱>_item_t {
// 每條記錄中的內(nèi)容
} <表格名稱>_item_t;
但基于以下原因,我們還是推薦大家堅(jiān)持第一種寫法:
以消息地圖為例,一個(gè)常見的記錄結(jié)構(gòu)體定義如下:
typedef?struct?msg_item_t msg_item_t;
struct?msg_item_t {
uint8_t chID; //!< 指令
????uint8_t?chAccess;?????????????//!
????uint16_t hwValidDataSize; //!< 數(shù)據(jù)長(zhǎng)度要求
????bool?(*fnHandler)(msg_item_t?*ptMSG,???
???? void?*pData,?
??????????????????????uint_fast16_t?hwSize);
};
在這個(gè)例子中,我們腦補(bǔ)了一個(gè)通信指令系統(tǒng),當(dāng)我們通過通信前端進(jìn)行數(shù)據(jù)幀解析后,獲得了以下的內(nèi)容:
為了方便指令解析,我們也需要有針對(duì)性的來設(shè)計(jì)每一條指令的內(nèi)容,因此,我們加入了chID來存儲(chǔ)指令碼;并加入了函數(shù)指針來為當(dāng)前指令綁定一個(gè)處理函數(shù);考慮到每條指令所需的最小有效數(shù)據(jù)長(zhǎng)度是已知的,因此,我們通過來記錄這一信息,以便進(jìn)行信息檢索時(shí)快速的做出判斷。具體如何使用,我們后面再說。
對(duì)表格來說,容器是所有記錄的容身之所,可以簡(jiǎn)單,但不可以缺席。最簡(jiǎn)單的容器就是數(shù)組,例如:
const?msg_item_t?c_tMSGTable[20];
這里,類型的數(shù)組就是表格的容器,而且我們手動(dòng)規(guī)定了數(shù)組中元素的個(gè)數(shù)。實(shí)踐中,我們通常不會(huì)像這樣手動(dòng)的“限定”表格中元素的個(gè)數(shù),而是直接“偷懶”——埋頭初始化數(shù)組,然后讓編譯器替我們?nèi)?shù)數(shù)——根據(jù)我們初始化元素的個(gè)數(shù)來確定數(shù)組的元素?cái)?shù)量,例如:
const msg_item_t c_tMSGTable[] = {
[0] = {
????????.chID?=?0,
????????.fnHandler = NULL,
},
[1] = {
...
},
...
};
上述寫法是C99語法,不熟悉的小伙伴可以再去翻翻語法書哦。說句題外話,2022年了,連頑固不化的Linux都擁抱C11了,不要再抱著C89規(guī)范不放了,起碼用個(gè)C99沒問題的。
上面寫法的好處主要是方便我們偷懶,減少不必要的“數(shù)數(shù)”過程。那么,我們要如何知道一個(gè)表格中數(shù)組究竟有多少個(gè)元素呢?別慌,我們有sizeof():
# dimof
#???dimof(__array) (sizeof(__array)/sizeof(__array[0]))
#
這個(gè)語法糖dimof()可不是我發(fā)明的,不信你問Linux。它的原理很簡(jiǎn)單,當(dāng)我們把數(shù)組名稱傳給dimof()時(shí),它會(huì):
通過 sizeof() 來獲取整個(gè)目標(biāo)數(shù)組的字節(jié)尺寸;
通過 sizeof([0]) 來獲取數(shù)組第一個(gè)元素的字節(jié)尺寸——也就是數(shù)組元素的尺寸;
通過除法獲取數(shù)組中元素的個(gè)數(shù)。
【表格的訪問(遍歷)】
由于表格的本質(zhì)是結(jié)構(gòu)體數(shù)組,因此,針對(duì)表格最常見的操作就是遍歷(搜索)了。還以前面消息地圖為例子:
static?volatile?uint8_t?s_chCurrentAccessPermission;
/*!?\brief?搜索消息地圖,并執(zhí)行對(duì)應(yīng)的處理程序
?*!?\retval?false??消息不存在或者消息處理函數(shù)覺得內(nèi)容無效
?*! \retval true 消息得到了正確的處理
?*/
bool?search_msgmap(uint_fast8_t?chID,
?????????????????? void *pData,
?????????????????? uint_fast16_t hwSize)
{
????for?(int?n?=?0;?n?
????????msg_item_t *ptItem = &c_tMSGTable[n];
????????if?(chID?!=?ptItem->chID) {
???????? continue;
????????}
????????if?(!(ptItem->chAccess & s_chCurrentAccessPermission)) {
????????????continue;??//!
????????}
????????if?(hwSize < ptItem->hwSize) {
????????????continue; //!< 數(shù)據(jù)太小了
????????}
????????if (NULL == ptItem->fnHandler) {
????????????continue;??//!
????????}
????????
????????//! 調(diào)用消息處理函數(shù)
????????return?ptItem->fnHandler(ptItem,?pData,?hwSize);
????}
????
????return?false;???//!
}
別看這個(gè)函數(shù)“很有料”的樣子,其本質(zhì)其實(shí)特別簡(jiǎn)單:
其實(shí)上述代碼隱藏了一個(gè)特性:就是這個(gè)例子中的消息地圖中允許出現(xiàn)chID相同的消息的——這里的技巧是:對(duì)同一個(gè)chID值的消息,我們可以針對(duì)不同的訪問權(quán)限(值)來提供不同的處理函數(shù)。比如,通信系統(tǒng)中,我們可以設(shè)計(jì)多種權(quán)限和模式,比如:只讀模式、只寫模式、安全模式等等。不同模式對(duì)應(yīng)不同的值。這樣,對(duì)哪怕同樣的指令,我們也可以根據(jù)當(dāng)前模式的不同提供不同的處理函數(shù)——這只是一種思路,供大家參考。
【由多實(shí)例引入的問題】
前面的例子為我們展示表格使用的大體細(xì)節(jié),對(duì)很多嵌入式應(yīng)用場(chǎng)景來說,已經(jīng)完全夠用了。但愛思考的小伙伴一定已經(jīng)發(fā)現(xiàn)了問題:
如果我的系統(tǒng)中有多個(gè)消息地圖(每個(gè)消息地圖中消息數(shù)量是不同的),我改怎么復(fù)用代碼呢?
為了照顧還一臉懵逼的小伙伴,我把這個(gè)問題給大家翻譯翻譯:
簡(jiǎn)而言之,()現(xiàn)在跟某一個(gè)消息地圖(數(shù)組)綁定死了,如果要讓它支持其它的消息地圖(其它數(shù)組),就必須想辦法將其與特定的數(shù)組解耦,換句話說,在使用()的時(shí)候,要提供目標(biāo)的消息地圖的指針,以及消息地圖中元素的個(gè)數(shù)。
一個(gè)頭疼醫(yī)頭腳疼醫(yī)腳的修改方案呼之欲出:
bool?search_msgmap(msg_item_t?*ptMSGTable,
uint_fast16_t hwCount,
uint_fast8_t chID,
void *pData,
uint_fast16_t hwSize)
{
for (int n = 0; n < hwCount; n++) {
msg_item_t *ptItem = &ptMSGTable[n];
if (chID != ptItem->chID) {
continue;
}
...
//! 調(diào)用消息處理函數(shù)
return ptItem->fnHandler(ptItem, pData, hwSize);
}
return false; //!< 沒找到對(duì)應(yīng)的消息
}
假設(shè)我們有多個(gè)消息地圖,對(duì)應(yīng)不同的工作模式:
const?msg_item_t?c_tMSGTableUserMode[] = {
...
};
const msg_item_t c_tMSGTableSetupMode[] = {
...
};
const msg_item_t c_tMSGTableDebugMode[] = {
...
};
const msg_item_t c_tMSGTableFactoryMode[] = {
...
};
在使用的時(shí)候,可以這樣:
typedef enum?{
????USER_MODE?=?0,????//!
????SETUP_MODE,???????//!
????DEBUG_MODE,???????//!
????FACTORY_MODE,?????//!
}?comm_mode_t;
bool?frame_process_backend(comm_mode_t tWorkMode,
?????????????????????????? uint_fast8_t chID,
?????????????????????????? void *pData,
?????????????????????????? uint_fast16_t hwSize)
{
????bool?bHandled = false;
switch (tWorkMode) {
case USER_MODE:
????????????bHandled = search_msgmap(
???????????? c_tMSGTableUserMode,
???????????? dimof(c_tMSGTableUserMode),
???????????? chID,
???????????? pData,
??????????????????????????hwSize);
????????????break;
???????? case SETUP_MODE:
bHandled = search_msgmap(
c_tMSGTableSetupMode,
dimof(c_tMSGTableUserMode),
chID,
pData,
hwSize);
break;
?????????...
}
return bHandled;
}
看起來很不錯(cuò),對(duì)吧?非也非也!早得很呢。
【表格定義的完全體】
前面我們說過,表格的定義分兩個(gè)部分:
其中,關(guān)于容器的定義,我們說過,數(shù)組是容器的最簡(jiǎn)單形式。那么容器定義的完全體是怎樣的呢?
“還是結(jié)構(gòu)體”!
是的,表格條目的本質(zhì)是結(jié)構(gòu)體,表格容器的本質(zhì)也是一個(gè)結(jié)構(gòu)體:
typedef struct <表格名稱>_item_t <表格名稱>_item_t;
struct <表格名稱>_item_t {
// 每條記錄中的內(nèi)容
};
typedef struct <表格名稱>_t <表格名稱>_t;
struct <表格名稱>_t {
????uint16_t?hwItemSize;
????uint16_t?hwCount;
????<表格名稱>_item_t *ptItems;
};
容易發(fā)現(xiàn),這里表格容器被定義成了一個(gè)叫做 _t 的結(jié)構(gòu)體,其中包含了三個(gè)至關(guān)重要的元素:
這個(gè)其實(shí)是來湊數(shù)的,因?yàn)?2位系統(tǒng)中指針4字節(jié)對(duì)齊的緣故,2字節(jié)的hwCount橫豎會(huì)產(chǎn)生2字節(jié)的氣泡。不理解這一點(diǎn)的小伙伴,可以參考文章《》
還是以前面消息地圖為例,我們來看看新的容器應(yīng)該如何定義和使用:
typedef?struct?msg_item_t?msg_item_t;
struct msg_item_t {
uint8_t chID; //!< 指令
uint8_t chAccess; //!< 訪問權(quán)限檢測(cè)
uint16_t hwValidDataSize; //!< 數(shù)據(jù)長(zhǎng)度要求
bool (*fnHandler)(msg_item_t *ptMSG,
void *pData,
uint_fast16_t hwSize);
};
typedef?struct?msgmap_t?msgmap_t;
struct msgmap_t {
uint16_t hwItemSize;
uint16_t hwCount;
????msg_item_t *ptItems;
};
const msg_item_t c_tMSGTableUserMode[] = {
...
};
const?msgmap_t?c_tMSGMapUserMode = {
????.hwItemSize?=?sizeof(msg_item_t),
????.hwCount?=?dimof(c_tMSGTableUserMode),
????.ptItems = c_tMSGTableUserMode,
};
既然有了定義,()也要做相應(yīng)的更新:
bool?search_msgmap(msgmap_t?*ptMSGMap,
uint_fast8_t chID,
void *pData,
uint_fast16_t hwSize)
{
for (int n = 0; n < ptMSGMap->hwCount; n++) {
msg_item_t *ptItem = &(ptMSGMap->ptItems[n]);
if (chID != ptItem->chID) {
continue;
}
...
//! 調(diào)用消息處理函數(shù)
return ptItem->fnHandler(ptItem, pData, hwSize);
}
return false; //!< 沒找到對(duì)應(yīng)的消息
}
看到這里,相信很多小伙伴內(nèi)心是毫無波瀾的……
“是的……是稍微優(yōu)雅一點(diǎn)……然后呢?”
“就這?。烤瓦@?!”
別急,下面才是見證奇跡的時(shí)刻。
【要優(yōu)雅……】
在前面的例子中,我們注意到表格的初始化是分兩部分進(jìn)行的:
const msg_item_t c_tMSGTableUserMode[] = {
[0] = {
.chID = 0,
.fnHandler = NULL,
},
[1] = {
...
},
...
};
const?msgmap_t?c_tMSGMapUserMode = {
????.hwItemSize?=?sizeof(msg_item_t),
????.hwCount?=?dimof(c_tMSGTableUserMode),
????.ptItems = c_tMSGTableUserMode,
};
那么,我們可不可以把它們合二為一呢?這樣:
要做到這一點(diǎn),我們可以使用一個(gè)類似“匿名數(shù)組”的功能:
我們想象中的樣子:
const msgmap_t c_tMSGMapUserMode = {
.hwItemSize = sizeof(msg_item_t),
.hwCount = dimof(c_tMSGTableUserMode),
.ptItems = const msg_item_t c_tMSGTableUserMode[] = {
[0] = {
.chID = 0,
.fnHandler = NULL,
},
[1] = {
...
},
...
},
};
使用“匿名數(shù)組”后的樣子(也就是刪除數(shù)組名稱后的樣子):
const msgmap_t c_tMSGMapUserMode = {
.hwItemSize = sizeof(msg_item_t),
.hwCount = dimof(c_tMSGTableUserMode),
????.ptItems?=?(msg_item_t?[]){
[0] = {
.chID = 0,
.fnHandler = NULL,
},
[1] = {
...
},
...
},
};
其實(shí),這不是什么“黑魔法”,而是一個(gè)廣為使用的GNU擴(kuò)展,被稱為“復(fù)合式描述( literal)”,本質(zhì)上就是一種以“省略”數(shù)組或結(jié)構(gòu)體名稱的方式來初始化數(shù)組或結(jié)構(gòu)體的語法結(jié)構(gòu)。具體語法介紹,小伙伴們可以參考這篇文章《》。
眼尖的小伙伴也許已經(jīng)發(fā)現(xiàn)了問題:既然我們省略了變量名,那么如何通過 dimof() 來獲取數(shù)組元素的個(gè)數(shù)呢?
少俠好眼力!
解決方法不僅有,而且簡(jiǎn)單粗暴:
const msgmap_t c_tMSGMapUserMode = {
.hwItemSize = sizeof(msg_item_t),
.hwCount = dimof((msg_item_t []){
[0] = {
.chID = 0,
.fnHandler = NULL,
},
[1] = {
...
},
...
??????}),
????.ptItems?=?(msg_item_t?[]){
[0] = {
.chID = 0,
.fnHandler = NULL,
},
[1] = {
...
},
...
},
};
所以說?……
為了優(yōu)雅的初始化……
我們要把同樣的內(nèi)容寫兩次?!!
手寫的確挺愚蠢,但宏可以?。?/p>
# __impl_table(__item_type, ...) \
????.ptItems?=?(__item_type?[])?{??????????????????????? \
__VA_ARGS__ \
}, \
????.hwCount?=?sizeof((__item_type?[])?{?__VA_ARGS__?})??\
/ sizeof(__item_type), \
.hwItemSize = sizeof(__item_type)
# ?impl_table(__item_type,?...)???? \
__impl_table(__item_type,?__VA_ARGS__)
借助上面的語法糖,我們可以輕松的將整個(gè)表格的初始化變得簡(jiǎn)單優(yōu)雅:
const msgmap_t c_tMSGMapUserMode = {
impl_table(msg_item_t,
[0] = {
.chID = 0,
.fnHandler = NULL,
},
[1] = {
...
},
...
),
};
這下舒服了吧?
【禁止套娃……】
還記得前面多實(shí)例的例子吧?
const msg_item_t c_tMSGTableUserMode[] = {
...
};
const msg_item_t c_tMSGTableSetupMode[] = {
...
};
const msg_item_t c_tMSGTableDebugMode[] = {
...
};
const msg_item_t c_tMSGTableFactoryMode[] = {
...
};
現(xiàn)在當(dāng)然就要改為如下的形式了:
const msgmap_t c_tMSGMapUserMode = {
impl_table(msg_item_t,
...
????),
};
const msgmap_t c_tMSGMapSetupMode = {
impl_table(msg_item_t,
...
),
};
const msgmap_t c_tMSGMapDebugMode = {
impl_table(msg_item_t,
...
),
};
const msgmap_t c_tMSGMapFactoryMode = {
impl_table(msg_item_t,
...
),
};
但……它們不都是類型的么?為啥不做一個(gè)數(shù)組呢?
typedef enum {
USER_MODE = 0, //!< 普通的用戶模式
SETUP_MODE, //!< 出廠后的安裝模式
DEBUG_MODE, //!< 工程師專用的調(diào)試模式
FACTORY_MODE, //!< 最高權(quán)限的工廠模式
} comm_mode_t;
const?msgmap_t?c_tMSGMap[] = {
[USER_MODE] = {
impl_table(msg_item_t,
...
),
},
[SETUP_MODE] = {
impl_table(msg_item_t,
...
),
},
[DEBUG_MODE] = {
impl_table(msg_item_t,
...
),
},
[FACTORY_MODE] = {
impl_table(msg_item_t,
...
),
},
};
是不是有點(diǎn)意思了?再進(jìn)一步,我們完全可以做一個(gè)新的表格,表格的元素就是 呀?
typedef?struct?cmd_modes_t?cmd_modes_t;
struct cmd_modes_t {
uint16_t hwItemSize;
uint16_t hwCount;
msgmap_t *ptItems;
};
然后就可以開始套娃咯:
const?cmd_modes_t?c_tCMDModes?=?{
????impl_table(msgmap_t,
???? [USER_MODE] = {
impl_table(msg_item_t,
[0] = {
.chID = 0,
.fnHandler = NULL,
},
[1] = {
...
},
...
),
},
[SETUP_MODE] = {
impl_table(msg_item_t,
...
),
},
[DEBUG_MODE] = {
impl_table(msg_item_t,
...
),
},
[FACTORY_MODE] = {
impl_table(msg_item_t,
...
),
},
????),
};
【差異化……】
在前面的例子中,我們可以根據(jù)新的定義方式更新函數(shù)d()函數(shù):
extern const cmd_modes_t c_tCMDModes;
bool frame_process_backend(comm_mode_t tWorkMode,
uint_fast8_t chID,
void *pData,
uint_fast16_t hwSize)
{
bool bHandled = false;
????if?(tWorkMode?> FACTORY_MODE) {
???? return false;
????}
????
????return search_msgmap( &(c_tCMDModes.ptItems[tWorkMode]),?
chID,
pData,
??????????????????????????hwSize);
}
是不是特別優(yōu)雅?
把容器定義成結(jié)構(gòu)體還有一個(gè)好處,就是可以給表格更多的差異化,這意味著,除了條目數(shù)組相關(guān)的內(nèi)容外,我們還可以放入其它東西,比如:
現(xiàn)有的d()為每一個(gè)消息地圖()都使用相同的處理函數(shù)(),這顯然缺乏差異化的可能性。如果每個(gè)消息地圖都有可能有自己的特殊處理函數(shù)怎么辦呢?
為了實(shí)現(xiàn)這一功能,我們可以對(duì) 進(jìn)行擴(kuò)展:
typedef struct msgmap_t msgmap_t;
struct msgmap_t {
uint16_t hwItemSize;
uint16_t hwCount;
msg_item_t *ptItems;
????bool?(*fnHandler)(msgmap_t?*ptMSGMap,
uint_fast8_t chID,
void *pData,
??????????????????????uint_fast16_t?hwSize);
};
則初始化的時(shí)候,我們就可以給每個(gè)消息地圖指定一個(gè)不同的處理函數(shù):
extern
bool?msgmap_user_mode_handler(msgmap_t?*ptMSGMap,
uint_fast8_t chID,
void *pData,
uint_fast16_t hwSize);
extern
bool msgmap_debug_mode_handler(msgmap_t *ptMSGMap,
uint_fast8_t chID,
void *pData,
uint_fast16_t hwSize);
const cmd_modes_t c_tCMDModes = {
impl_table(msgmap_t,
[USER_MODE] = {
impl_table(msg_item_t,
????????????????...
),
.fnHandler = &msgmap_user_mode_handler,
},
[SETUP_MODE] = {
impl_table(msg_item_t,
...
),
????????????.fnHandler?=?NULL;?//!
},
[DEBUG_MODE] = {
impl_table(msg_item_t,
...
),
.fnHandler = &msgmap_debug_mode_handler,
},
[FACTORY_MODE] = {
impl_table(msg_item_t,
...
),
????????????//.fnHandler = NULL 什么都不寫,就是NULL(0)
},
),
};
此時(shí),我們?cè)俑耫()函數(shù),讓上述差異化功能成為可能:
bool frame_process_backend(comm_mode_t tWorkMode,
uint_fast8_t chID,
void *pData,
uint_fast16_t hwSize)
{
bool bHandled = false;
????msgmap_t?*ptMSGMap?= c_tCMDModes.ptItems[tWorkMode];
????if?(tWorkMode?> FACTORY_MODE) {
???? return false;
????}
????
????//!?調(diào)用每個(gè)消息地圖自己的處理程序
????if (NULL != ptMSGMap->fnHandler) {
?????????return?ptMSGMap->fnHandler(ptMSGMap,?
chID,
pData,
hwSize);
????}
????//!?默認(rèn)的消息地圖處理程序
????return search_msgmap( ptMSGMap,
chID,
pData,
hwSize);
}
【說在后面的話】
啥都不說了……你們看著辦吧。我們下期再見。
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。