討論區快速選單
知識庫快速選單
傑米的攝影旅遊筆記 網路投保旅行平安險 政府補助!學嵌入式+物聯網
[ 回上頁 ] [ 討論區發言規則 ]
常見函式使用 void 指標傳遞參數的現象,以及一些看法
更改我的閱讀文章字型大小
作者 : ice_emissary(燃燒的大地) 貼文超過200則
[ 貼文 386 | 人氣 0 | 評價 1770 | 評價/貼文 4.59 | 送出評價 17 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2017/10/17 下午 12:37:01
# 常見函式使用 void 指標傳遞參數的現象,以及一些看法

## 觀察現象

我幾年來陸續在不同公司的不同人寫出來的不同程式碼裡見到一種使用 `void*` 傳遞參數的現象。
合理狀況下,我們在函式介面裡加上 `void*` 參數多半是因為不知道使用者欲傳入的資料型態為何?
例如標準函式庫裡的 memset、memcpy 等,但讓我看到的程式碼都不是這種!
下面我用一個簡單的範例來描述我所看到的狀況:

    // 這是函式的宣告,位在某個 h 檔。
    // 雖然我不喜歡匈牙利命名法,但看到這樣的程式碼通常都愛用匈牙利命名,不知兩者間是否有何種關聯?
    // 為了保持原程式的「味道」,所以我也很無聊的保留了匈牙利命名的變數。
    void* SomeFunction(void *pData);

    // 這是函式實體,位在某個 c 檔。
    void* SomeFunction(void *pData)
    {
     // 這裡有很多的變數宣告…
     SOME_STRUCT_TYPE *pstData = (SOME_STRUCT_TYPE *) pData;
     // 這裡還有另外很多從匈牙利來的變數們…

     // 以下是函式工作內容,可能有數百至千行!
     // 但可以肯定的是,再也見不到 pData 參數。
     // ...
    }

這個函式的參數並不存在前述「不知使用者會傳入何種參數」或「希望函式能支援任何種類參數」的需求,
事實上以函式內容來看,函式根本就預設它的參數是 `SOME_STRUCT_TYPE` 型態,這個函式根本就應該被直接宣告為:

    void SomeFunction(SOME_STRUCT_TYPE *Data);

雖然原來的寫法一樣可以編譯、甚至可以正確執行,但卻存在幾個大缺點:

1. 不直觀,光看函式宣告無法看出參數型態,在不知型態的情況下有些時候甚至不能直覺的知道參數的意義!
2. 無法依賴編譯器的型態檢查把關,若有一天因故需要修改參數型態,此時編譯器連吭都不會都不會吭一聲!
    只能祈禱可以手動找出所有關聯的地方並完成同步修改,然後希望它在執行時不會出錯!

## 分析解法對策

那麼為何這些函式要這樣宣告呢?
說來好玩,大部份的原作者已找不到人;而在作者還在的情況下,問他,他也答不出個所以然!
我對這種奇怪寫法是否具有某種歷史意義已經沒有多大的興趣,所以直接猜測會這樣宣告函式的可能原因:

1. 這是要傳入某個程式庫的回呼函式,
    因為程式庫 API 要求的是長這樣的回呼函式,所以只好把要傳入的函式定成這樣,
    不然編譯器會抱怨說函式參數不符!

2. 這函式就是個普通的函式,也沒有被回呼的需求。
    但因為程式的結構如同義大利麵條一樣、程式檔案的引用關係毫無順序章法,
    如果明確定義函式參數型態,則該型態必須在函式標頭處定義,或引用相關標頭;
    然而一旦這麼做,就會引發一些莫名其妙的編譯錯誤,如無限循環引用、或型態衝突等!
    因此最後只好這麼惡搞,好來逃避錯誤的發生!

以上原因不是在唬爛,都是在真正經手過的程式碼裡出現的狀況!
對於第二種原因我不再去探究,因為這種狀況根本就應該要對整坨程式重新分層、封裝、解偶、重構,
而不是去研究語法上有什麼可避免問題的技巧!

對於第一種原因,以下將闡述我的這種情況的分析和採行策略。
為了避免有些人說我寫的東西太理論,下面我就拿真正的程式庫函式來做範例,比方說 pthread。
當使用 pthread 欲建立一個執行緒時,我們會呼叫下面這個函式:

    int pthread_create(pthread_t *thread,
     const pthread_attr_t *attr,
     void *(*start_routine) (void *),
     void *arg);

由於這不是執行緒教學,就不說明函式的用法行為等細節,
這裡只探討函式的最後兩個參數,即 start_routine 與 arg。
start_routine 是一個回呼函式,會在執行緒建立起來後被呼叫;
而 pthread 基本上不會去處理 arg 參數,只會把它直接 pass 給 start_routine 函式。
假設我有一個想被新執行緒執行的函式(以及它的資料參數)定義如下:

    struct my_data
    {
     int value;
    };

    void* my_thread_proc(struct my_data *data)
    {
     printf("My thread has started!\n");
     printf("The argument value is: %d\n", data->value);
     return NULL;
    }

然後我在主程式執行下面程式碼來建立執行緒並執行我的函式:

    struct my_data mydata = { .value=7 };
    pthread_t thread
    pthread_create(&thread, NULL, my_thread_proc, &mydata);

這樣,程式可以被編譯並正常執行。
我的回呼函式會收到 data 參數,而這個參數就是我在主程式所傳入的 mydata 變數,
因此回呼函式會印出數字 7,沒什麼意外。
唯一美中不足的就是在編譯時,編譯器會發出警告,告訴我 pthread_create 的參數不匹配!
原因很明顯,因為我的回呼函式長相是 `void*(*)(struct my_data*)`,
而 pthread_create 要的卻是 `void*(*)(void*)`。

其實指標是相通的,不同型態的指標都一樣是記憶體位址,所以即使型態不匹配,執行結果仍然正確。
而 pthread_create 的回呼參數定義為 `void*` 型態原因也很合理,
因為它不知道使用者實際需要的參數型態為何,所以使用了萬用指標。
唯一讓人不開心的地方就是關於型態匹配的編譯警告!
除了直接關閉編譯警告這種做法外(極度不推薦),在某個地方強制轉型大概是免不了的了!
只是該在什麼地方執行怎麼樣的轉型?

當然這文章最初提到的處理方法是其中一種解法,好處壞處都分析過了,但極不建議使用!
使用這種方法來處理的話,我需要將回呼函式修改成下面這樣:

    void* my_thread_proc(void *data)
    {
     struct my_data *data = arg;
     // ...
    }

第二種方法就是不動我的函式宣告,然後在呼叫 pthread_create 的時候進行參數轉型。
那麼我的函式長相將維持最初定義的:

    void* my_thread_proc(struct my_data *data)

而在建立執行緒時就需要一些轉型修飾:

    pthread_create(&thread, NULL, (void*(*)(void*)) my_thread_proc, &mydata);

這種做法的好處是維持了自訂回呼函式的參數明確性,也解決了傳遞函式給別人時的參數不匹配問題;
缺點就是每次寫這個轉型敘述有點煩,佔版面又容易讓人眼花,特別是在有些回呼型態比較複雜的時候。

第三種做法是在別處使用 typedef 定義回呼函式的類型,這樣在做轉型的時候,畫面會比較好看,
但原理與第二種做法類似。
使用這種做法,要先定義回呼函式的型態:

    typedef void*(*pthread_start_routine_t)(void*);

那麼 pthread_create 的函式原型可能可以改成這樣:

    int pthread_create(pthread_t *thread,
     const pthread_attr_t *attr,
     pthread_start_routine_t start_routine,
     void *arg);

在建立執行緒時的呼叫就會變成:

    pthread_create(&thread, NULL, (pthread_start_routine_t) my_thread_proc, &mydata);

這種做法看似簡潔、畫面也不凌亂,但卻存在致命缺點:
當 pthread_create 的回呼介面、也就是 pthread_start_routine_t 做了更動時,
編譯器無法給予任何的檢查警告!

有時還會見到一種做法與第三種方法類似,但副作用更劇烈,就是把所有的回呼傳入通通轉型成 `void*`:

    pthread_create(&thread, NULL, (void*) my_thread_proc, &mydata);

或者它可能會長成這樣:

    #define PTHREAD_CALLBACK(f) ((void*)f)
    pthread_create(&thread, NULL, PTHREAD_CALLBACK(my_thread_proc), &mydata);

但不管哪一種,效果是一樣的,也都不推薦。

## 最優做法

繞了一大圈後,我覺得最好的方法,也是我通常再用的寫法是上面提的第二種寫法,
也就是每次不厭其煩重寫一次回呼介面轉型的寫法:

    pthread_create(&thread, NULL, (void*(*)(void*)) my_thread_proc, &mydata);

雖然寫起來比較繁複,但卻是最保險的寫法!
當 pthread_create 的回呼函式型態發生變化,比如說變成這樣時:

    int pthread_create(pthread_t *thread,
     const pthread_attr_t *attr,
     void*(*start_routine)(void*, int, float),
     void *arg);

此時在所有呼叫到 pthread_create 的地方都會發生函式參數不匹配的警告,提醒程式師來一一修改。
當然,你可以說會不會有人只修改了轉型宣告就了事:

    pthread_create(&thread, NULL, (void*(*)(void*,int,float)) my_thread_proc, &mydata);

    // 然而 my_thread_proc 仍然是原樣而無任何更動!
    void* my_thread_proc(struct my_data *data);

當然這是有可能的!
只能說我們已經盡力做好防止警告,若真有人這樣只改了轉型宣告後卻不知道要檢查修改後面的函式,
只能說是紀律問題!

還有另外一種狀況也會讓這種寫法無法產生警告:那就是當 my_thread_proc 介面變動的時候,
因為我們在呼叫 pthread_create 時都進行了強制轉型,所以編譯器不會檢查出任和異狀!
針對這種狀況,只能說盡力了!畢竟 C 語言有它的語法限制,這已經是在限制條件下想到的最佳做法,
若需要語法上完美的回呼機制的話,只能改用具有物件導向多型特性的其它高階程式語言了!
作者 : ma_hty(白老鼠(Gary))討論區板主 OpenGL卓越專家DirectX優秀好手C++頂尖高手貼文超過2000則人氣指數超過70000點
[ 貼文 2170 | 人氣 89850 | 評價 10100 | 評價/貼文 4.65 | 送出評價 79 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2017/10/18 下午 12:35:58
> 那麼為何這些函式要這樣宣告呢?
> ... 原因:
> 1. 這是要傳入某個程式庫的回呼函式,
> 因為程式庫 API 要求的是長這樣的回呼函式,所以只好把要傳入的函式定成這樣,
> 不然編譯器會抱怨說函式參數不符!
> 2. 這函式就是個普通的函式,也沒有被回呼的需求。
> 但因為程式的結構如同義大利麵條一樣、程式檔案的引用關係毫無順序章法,
> 如果明確定義函式參數型態,則該型態必須在函式標頭處定義,或引用相關標頭;
> 然而一旦這麼做,就會引發一些莫名其妙的編譯錯誤,如無限循環引用、或型態衝突等!
> 因此最後只好這麼惡搞,好來逃避錯誤的發生!

void* 作為變數類型, 就是傳入 pointer 的意思. 這個沒啥特別呀. 有好些情況, 函數的變數類型
是不確定的, 例如 多執行緒的工作函數 又或是 單純是想管理位址 又或是好些win32 API函數...
問根源的話... 好些記憶體管理的函數, 如 memset() realloc(), 變數都是void*的. 如果程式裡曾用上
這些函數的話, 這種用法就會自然地被承襲下來.

其實又何必拘泥於 "編譯器的型態檢查把關" 呢. 既然用上了 pointer, 靠封裝把關的前題早就不成立了.
當然, 不是說要不管不理, 最簡易的解決方案, 就是把包含 pointer 的東東, 都封裝起來, 令對外介面看不到
pointer. 如此, 對內你就可以活用 pointer靈活的好處. 是靈活 還是含糊曖昧, 是一體兩面的事.
有法可依, 好; 但過火了, 就是作法自斃啊.

說管理, 從來就是人的事, 靈活一點去權衡輕重吧.

作者 : ice_emissary(燃燒的大地) 貼文超過200則
[ 貼文 386 | 人氣 0 | 評價 1770 | 評價/貼文 4.59 | 送出評價 17 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2017/10/18 下午 04:54:35
>有好些情況, 函數的變數類型
>是不確定的, 例如 多執行緒的工作函數 又或是 單純是想管理位址 又或是好些win32 API函數...
>問根源的話... 好些記憶體管理的函數, 如 memset() realloc(), 變數都是void*的.
>...

你說的不錯,
問題是我從一開始就把這種狀況排除在外了!

>合理狀況下,我們在函式介面裡加上 `void*` 參數多半是因為不知道使用者欲傳入的資料型態為何?
>例如標準函式庫裡的 memset、memcpy 等,但讓我看到的程式碼都不是這種!
>...
>這個函式的參數並不存在前述「不知使用者會傳入何種參數」或「希望函式能支援任何種類參數」的需求,
>事實上以函式內容來看,函式根本就預設它的參數是 `SOME_STRUCT_TYPE` 型態,這個函式根本就應該被直接宣告為...
作者 : wallace_tsou(Wallace) 貼文超過200則
[ 貼文 253 | 人氣 314 | 評價 960 | 評價/貼文 3.79 | 送出評價 6 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2017/10/18 下午 07:25:35
MCU上還不得不這樣用,因為常常Data是從通信來的,完全沒有格式。都是先解命令後才能知道後面要套那一個資料結構體。
所以這種指標在MCU上多到不奇怪了。
也許PC上的除錯器真的很強很好,會自動將內容解開來。
MCU的工具就可憐很多了。
有時為了做程式更新,外面來的是二進制執行碼,或是將執行碼搬到RAM去執行。
所以指標這樣用,沒有什麼奇怪的。
作者 : ice_emissary(燃燒的大地) 貼文超過200則
[ 貼文 386 | 人氣 0 | 評價 1770 | 評價/貼文 4.59 | 送出評價 17 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2017/10/19 上午 08:54:06
>MCU上還不得不這樣用,因為常常Data是從通信來的......

啊?封包編解是另外的題目了!
通訊封包解析、和函式指標是兩回事,解析封包不一定需要用到函式指標,函式指標也非針對解析封包而設;
簡單的說,兩者沒有任何關係!
就算你說的是萬用指標,兩者也沒有必然的關聯!

>也許PC上的除錯器真的很強很好,會自動將內容解開來......

不知為什麼?但這誤會大了!
其實我也是寫嵌入式系統的,但是,
我這次所討論的主題都是在編譯時期完成的東西,基本上和執行環境、或除錯器 完~全~沒~有~任~何~關~係~

如果你堅持這些東西有重大相關的話,也許你可以提出一些範例來做探討。

這裡對不起大家!
我以為我寫的東西大家都看得懂!我自以為我寫得夠白話、也夠實務!
作者 : ma_hty(白老鼠(Gary))討論區板主 OpenGL卓越專家DirectX優秀好手C++頂尖高手貼文超過2000則人氣指數超過70000點
[ 貼文 2170 | 人氣 89850 | 評價 10100 | 評價/貼文 4.65 | 送出評價 79 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2017/10/19 上午 11:12:09
> 我以為我寫的東西大家都看得懂!我自以為我寫得夠白話、也夠實務!

奇怪... 我上面的回覆,說得這麼透徹,為啥還會被認為 "我看不懂你寫的東東" ?

雖說寫程式 就似寫文章 並無絶對的好,但是,宏觀地去說管理,可分高下。宏觀地說管理,你上面說的,就太僵化 太反人類文化了。


記得有一次,我去某政府部門辦手續,那裡有兩條布條,分出了兩行讓人排隊。當下,只有兩隊伍也無人,我就直接跑到櫃台前,說著 說著... 。

然後,工務員跟我說 :"請你繞到後面,由另一條隊伍再進來。"

那時我不以為然,繞了一下,再跑到相同的櫃台前。特然心生疑惑 問道 : "繞這一下,何以?"

工務員答道 : "這一隊 和 那一隊 是辦不同的手續的,你辦這手續,要由那一隊進入。"

*o*y--oO ,這麼冷的笑話啊... 一陣冷風吹過,眼眉毛都結霜了。
作者 : wallace_tsou(Wallace) 貼文超過200則
[ 貼文 253 | 人氣 314 | 評價 960 | 評價/貼文 3.79 | 送出評價 6 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2017/10/19 下午 02:16:48
狀況是這樣MCU內部容量不足,所以利用SPI Flash存程式,要執行時搬去特定地方的RAM內執行。
就會有搬入時是資料,進RAM後是程式。函式的參數也不一定。
以前有想過這種方式去執行,因為8位元MCU定址實在太小。
後來沒有機會了,升到32位元MCU後不再有此種狀況。
現在MCU連外部SPI Flash都可以直接映射到記憶體空間來執行,也沒有機會再用這種轉來轉去的指標。
作者 : wallace_tsou(Wallace) 貼文超過200則
[ 貼文 253 | 人氣 314 | 評價 960 | 評價/貼文 3.79 | 送出評價 6 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2017/10/19 下午 02:35:35
這種型式執行程式,算是舊技術了。後來直接寫個VM,將SPI Flash當成Machine Code在執行。
也找好了VM型式,不過時代前進的很快。技術準備好沒有機會上場,時代就過去了。
作者 : cxxlman(CxxlMan) C++優秀好手貼文超過1000則
[ 貼文 1034 | 人氣 3227 | 評價 1260 | 評價/貼文 1.22 | 送出評價 27 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2017/10/19 下午 04:39:38
會使用 void* 作為參數,那麼提供者和接收者都須知道那是什麼東西,因須經過一個中介者,採用 void* 是一個最簡單的方法,以下是我用過的例子,Arg 由實作自己去定義

// 在指定的 Group 索取一個元件
// 失敗回傳 NULL
CXXLPLUGIN_DLLEXPORT Smart_Ptr<cxxlObjectPlugin> cxxlFASTCALL
cxxlCM_GetElement(
  const UTF8_String &InterfaceID,
  const UTF8_String &Group,
  void *Arg = NULL
);
作者 : cxxlman(CxxlMan) C++優秀好手貼文超過1000則
[ 貼文 1034 | 人氣 3227 | 評價 1260 | 評價/貼文 1.22 | 送出評價 27 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2017/10/19 下午 04:45:21

>這種型式執行程式,算是舊技術了。後來直接寫個VM,將SPI Flash當成Machine Code在執行。
>也找好了VM型式,不過時代前進的很快。技術準備好沒有機會上場,時代就過去了。

void* 歷久不衰,從古早的c時代到未來都不會過時,請安心享用^^
作者 : ice_emissary(燃燒的大地) 貼文超過200則
[ 貼文 386 | 人氣 0 | 評價 1770 | 評價/貼文 4.59 | 送出評價 17 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2017/10/20 上午 09:21:48
我這樣說好了,我做了一個函式,可以簡單計算列印一個車子的一些基本資訊:

void PrintCarInfo(const Car *car)
{
    int age = GetCurrYear() - car->year_of_manufacture;

    printf("Type: %s\n", car->type);
    printf("Weight: %f kg\n", car->weight);
    printf("Fuel: %f %%\n", car->fuel);
    printf("Age: %d\n", age);
}

再附上 Car 結構的定義:

typedef struct Car
{
    char type[32];
    float weight;
    float fuel;
    int year_of_manufacture;
} Car;

好了,以上是基本環境條件的建立,現在來做些表達我問題的敘述。
現在,我就是要把 PrintCarInfo 函式改寫成下面這樣,不知各位如何看法?

void PrintCarInfo(void *pCar)
{
    Car *pstCar = (Car*) pCar;

    int iAge = GetCurrYear() - pstCar->year_of_manufacture;

    printf("Type: %s\n", pstCar->type);
    printf("Weight: %f kg\n", pstCar->weight);
    printf("Fuel: %f %%\n", pstCar->fuel);
    printf("Age: %d\n", iAge);
}
作者 : igbt(IGBT)
[ 貼文 7 | 人氣 0 | 評價 10 | 評價/貼文 1.43 | 送出評價 0 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2017/10/20 上午 11:14:50
版主也可以用qsort舉例
常碰到不會使用的人
作者 : cxxlman(CxxlMan) C++優秀好手貼文超過1000則
[ 貼文 1034 | 人氣 3227 | 評價 1260 | 評價/貼文 1.22 | 送出評價 27 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2017/10/20 下午 04:51:20
>void PrintCarInfo(void *pCar)
>{
> Car *pstCar = (Car*) pCar;
>

會這樣用直覺你一定落了什麼,比如

void PrintData(int ID,void *pData)
{
  switch(ID)
  {
  case 0:
    PrintCarInfo(pData);
    break;
  case 1:
    PrintMotorolaInfo(pData);
    break;
  ..........
  }
}
作者 : ma_hty(白老鼠(Gary))討論區板主 OpenGL卓越專家DirectX優秀好手C++頂尖高手貼文超過2000則人氣指數超過70000點
[ 貼文 2170 | 人氣 89850 | 評價 10100 | 評價/貼文 4.65 | 送出評價 79 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2017/10/20 下午 11:23:51
唉... 工具箱內有 十字螺絲批 釘 和 鐡鎚.
因此, 我可以斷定 十字螺絲批是無用之物,
因為它不能用來打釘, 打釘都用鐡鎚啦.

這論述 荒唐. 跟你的論述不遑多讓.

void* 作為參數的用法, 有它合適出場的場合的.
硬要把它用到不合適的場合作例子, 從而得出它是
無用之物的結論啊... 在你的眼中, 只怕世上己無
有用之物了. 感悟至此了, 何不出家為僧?
作者 : cxxlman(CxxlMan) C++優秀好手貼文超過1000則
[ 貼文 1034 | 人氣 3227 | 評價 1260 | 評價/貼文 1.22 | 送出評價 27 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2017/10/21 上午 11:12:35
>void PrintCarInfo(void *pCar)
>{
> Car *pstCar = (Car*) pCar;
>

這一看就不是作為直接呼叫用的,應該是去找出給誰用的就有解了
作者 : ccl0504(手動程式產生器) 貼文超過200則
[ 貼文 450 | 人氣 211 | 評價 930 | 評價/貼文 2.07 | 送出評價 2 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2017/10/21 下午 09:29:50

用匈牙利命名及指標參數轉某種資料型態在 Windows 的 message processing 上是還蠻常看到的,
因為 message handler 只能是固定的參數型態 (WPARAM wParam, LPARAM lParam)

https://www.codeproject.com/Articles/546/Message-Management

>void PrintCarInfo(void *pCar)
>{
> Car *pstCar = (Car*) pCar;
>
>

通常應該還會加上像是 IS_VALID(pPar, Car) 來檢查指標是否是為該型態,以免形態錯誤的記憶體存取,
檢查方式可以在資料前面加 ID header

typedef struct Car
{
    char ID[3];
    char type[32];
    float weight;
    float fuel;
    int year_of_manufacture;
} Car;

if( memcmp(pChar->ID, "CAR") == 0 )

作者 : ccl0504(手動程式產生器) 貼文超過200則
[ 貼文 450 | 人氣 211 | 評價 930 | 評價/貼文 2.07 | 送出評價 2 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
主題發起人ice_emissary註記此篇回應為很有道理 2017/10/21 下午 09:45:49

話說資料型態檢查, 個人是覺得 compiler 能檢查多少就算多少,

當寫成物件供其他使用, 或是 networking communication 等,
呼叫/被呼叫程式已經不是同時編譯,
多增加一些 runtime 的資料型態檢查可能會安全一些

作者 : cxxlman(CxxlMan) C++優秀好手貼文超過1000則
[ 貼文 1034 | 人氣 3227 | 評價 1260 | 評價/貼文 1.22 | 送出評價 27 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2017/10/22 下午 03:18:40
不用檢查啦,void* 都是配套設計好的,不是給人隨便亂叫的,若有問題就是設計錯誤沒套準
作者 : cxxlman(CxxlMan) C++優秀好手貼文超過1000則
[ 貼文 1034 | 人氣 3227 | 評價 1260 | 評價/貼文 1.22 | 送出評價 27 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
主題發起人ice_emissary註記此篇回應為很有道理 2017/10/23 下午 02:46:31
我試著說清楚一點

假設有一個包裝異質的容器,但還是會包裹這些異質的共同介面 class Base

void* 在精神上和 class Base 類似,處理 void* 的程序的工作就只是在 void* 這個層次上,因 void 已經沒有語意了,所以大概就是只能對 void* 所代表的物件搬來搬去,最後只要能搬到正確的接收者就可以了

void* 這種東西只能在遇到了不得不才會去用,一時我也想不出例子,給我個例子百分之九九都能找到替代方法,沒事搞個 void* 再做檢查自找罪受,不如用 class Base,至少在 class Base 中可設置檢查機制,若要用 void* 要用在絕對不會出錯的地方



作者 : ma_hty(白老鼠(Gary))討論區板主 OpenGL卓越專家DirectX優秀好手C++頂尖高手貼文超過2000則人氣指數超過70000點
[ 貼文 2170 | 人氣 89850 | 評價 10100 | 評價/貼文 4.65 | 送出評價 79 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2017/10/23 下午 03:26:09
> void* 這種東西只能在遇到了不得不才會去用,一時我也想不出例子,...

^^, 說例子, 上面的討論裡不是提及了很多了嗎?

worker thread 函數 的變數傳遞呀, window 系統函數的變數傳遞呀, 底層的記憶體管理函數 (如 memset memcpy ) 呀...

作者 : cxxlman(CxxlMan) C++優秀好手貼文超過1000則
[ 貼文 1034 | 人氣 3227 | 評價 1260 | 評價/貼文 1.22 | 送出評價 27 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
2017/10/23 下午 03:37:10

>> void* 這種東西只能在遇到了不得不才會去用,一時我也想不出例子,...
>
>^^, 說例子, 上面的討論裡不是提及了很多了嗎?
>
>worker thread 函數 的變數傳遞呀, window 系統函數的變數傳遞呀, 底層的記憶體管理函數 (如 memset memcpy ) 呀...
>

我說的是樓主提到的

void PrintCarInfo(void *pCar)
{
 Car *pstCar = (Car*) pCar;

void* 再轉回原形使用,memset 要的就是 void* 意義不同
作者 : ma_hty(白老鼠(Gary))討論區板主 OpenGL卓越專家DirectX優秀好手C++頂尖高手貼文超過2000則人氣指數超過70000點
[ 貼文 2170 | 人氣 89850 | 評價 10100 | 評價/貼文 4.65 | 送出評價 79 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
主題發起人ice_emissary註記此篇回應為很有道理 2017/10/23 下午 04:43:08
>我說的是樓主提到的
>
>void PrintCarInfo(void *pCar)
>{
> Car *pstCar = (Car*) pCar;
>
>由 void* 再轉回原形使用,memset 要的就是 void* 意義不同
>

這只是多型的其中一種演繹方式. 說多型, 用C++的話, 我們還可
以選擇繼承virtual函數. 使用得宜, 還是可以寫出易讀易懂的原碼的.
除了可能會太囉嗦之外, 沒啥不妥的.

什麼是太囉嗦嘛... 你可以試試把 memset memcpy memcmp 等等...
一系列函數, 以 繼承virtual函數+多型 的方式 重寫一遍, 去提供相同的功能.
沒有犯大錯的話, 除了多出非常多的複雜概念和一大堆自訂句式之外,
結果就和原來的沒分別了.

樓主最初貼的文, 就是嘗試以 簡陋的方式 去演繹 限制多一點的多型.
因為簡陋, 所以沒能嚴謹到句法自理; 又因為限制多一點, 所以不靈活.
兩面不討好囉.

句法 什麼的, 恰到好處就好了. 套一句 很久以前 在這裡 某大大說過
的話, 寫程式應該盡量用問題本身的邏輯. 之乎者也...


作者 : cxxlman(CxxlMan) C++優秀好手貼文超過1000則
[ 貼文 1034 | 人氣 3227 | 評價 1260 | 評價/貼文 1.22 | 送出評價 27 次 ] 
[ 給個讚 ]  [ 給個讚 ]  [ 回應本文 ]  [ 發表新文 ]  [ 回上頁 ] [ 回討論區列表 ] [ 回知識入口 ]
主題發起人ice_emissary註記此篇回應為很有道理 2017/10/23 下午 05:19:39
我只是關注在把 void* 當作是 root class 這點上,要這樣用的條件就是要精準,任何多餘的檢查動作本身就是一個錯誤,因不能排除檢查的項目會有意外發生,其餘的用法我就不在意了
 板主 : simula
 > C++ - 討論區
 - 最近熱門問答精華集
 - 全部歷史問答精華集
 - C++ - 知識庫
  ■ 全站最新Post列表
  ■ 我的文章收藏
  ■ 我最愛的作者
  ■ 全站文章收藏排行榜
  ■ 全站最愛作者排行榜
  ■  月熱門主題
  ■  季熱門主題
  ■  熱門主題Top 20
  ■  本區Post排行榜
  ■  本區評價排行榜
  ■  全站專家名人榜
  ■  全站Post排行榜
  ■  全站評價排行榜
  ■  全站人氣排行榜
 請輸入關鍵字 
  開始搜尋
 
Top 10
評價排行
C++
1 Raymond 13050 
2 青衫 4760 
3 simula 4690 
4 coco 4030 
5 白老鼠(Gary) 3670 
6 ozzy 2540 
7 Ben 2250 
8 Anderson 1960 
9 windblown 1650 
10 Kenny 1560 
C++
  專家等級 評價  
  一代宗師 10000  
  曠世奇才 5000  
  頂尖高手 3000  
  卓越專家 1500  
  優秀好手 750  
Microsoft Internet Explorer 6.0. Screen 1024x768 pixel. High Color (16 bit).
2000-2018 程式設計俱樂部 http://www.programmer-club.com.tw/
0.21875