原來C語言還可以這樣實現「泛型編程」!

2021-03-02 編程珠璣

在回答標題問題之前,先了解下什麼是泛型編程。

泛型編程(generic programming)是程序設計語言的一種風格或範式。泛型允許程式設計師在強類型程序設計語言中編寫代碼時使用一些以後才指定的類型,在實例化時作為參數指明這些類型。C++支持泛型編程,也就是模板,比如:

// 來源:公眾號【 編程珠璣】
// 作者:守望先生
#include <iostream>
template <class T>
T add(T a,T b){
  T ret = a + b;
  std::cout<< a << " + " << b <<" = " << ret << std::endl;
  return ret;
}
int main(){
  add(1,2);  // 整數相加
  add(1.2,2.3); // 浮點數相加
  return 0;
}

運行結果:

1 + 2 = 3
1.2 + 2.3 = 3.5

從上面的結果可以看到,對於調用add函數,如果傳入的是整型,則按照整型加法計算,如果是浮點數,則按照浮點數進行加法計算。也就是說,add函數沒有針對特定類型(泛型)。

你同樣可以使用重載實現上面的功能,但是存在大量重複代碼。

C語言支持泛型編程嗎?

很遺憾,C語言本身不支持真正意義上的泛型編程,但是卻在一定程度上可以「實現泛型編程」。

_Generic關鍵字

_Generic是C11的關鍵字,通過該關鍵字可以有一個泛型表達式:

_Generic((value). int:"int", float:"float",char*:"char*",default:"other type")

什麼意思呢?如果value是int類型,那麼表達式的值就是「int」,其他的以此類推。看起來是不是和switch語句有點類似呢?
根據這個示例,我們來實現一個功能,列印變量或常量到底是什麼類型:

// 來源:公眾號【編程珠璣】
// 作者:守望先生
#include <stdio.h>
#define TYPE(v) _Generic((v), \
    int:"int", \
    char:"char", \
    float:"float", \
    double:"double", \
    char*:"char*", \
    default:"other type")
int main(void)
{
    printf("1 + 2 type: %s\n",TYPE(1 + 2));
    printf("1/3 type: %s\n",TYPE(1/3));
    printf("2/3 type: %s\n",TYPE((float)2/3));
    printf("xxx type: %s\n",TYPE("xxx"));
    return 0;
}

這裡為了方便使用,我們通過define關鍵字,將泛型表達式簡化。

運行結果:

1 + 2 type: int
1/3 type: int
2/3 type: float                                                        
xxx type: char*

可以看到通過TYPE就可以獲得表達式的結果類型,這對初學者來說,可真是福音了

泛型算法

既然C語言有_Generic關鍵字了,那麼我們嘗試實現開頭C++示例代碼中的加法。看過上面的例子後,相信你已經會了:

// 來源:公眾號【編程珠璣】
// 作者:守望先生
#include <stdio.h>
// int類型加法
int addI(int a, int b)
{
    printf("%d + %d = %d\n",a,b, a + b );
    return (a + b);
}
// double類型加法
double addF(double a, double b)
{
    printf("%f + %f = %f\n",a,b, a + b );
    return (a + b);
}
void unsupport(int a,int b)
{
    printf("unsupport type\n");
}
#define ADD(a,b) _Generic((a), \
    int:addI(a,b),\
    double:addF(a,b), \
    default:unsupport(a,b))
int main(void)
{
    ADD(1 , 2);
    ADD(1.1,2.2);
    return 0;
}

觀察上面的代碼,我們注意到:

在這裡,我們需要定義兩種類型的加法(實際上,通過C++的模板,由編譯器幫我們完成了這件事),由於C語言中並不支持重載,因此兩個加法的函數名不一樣。

由於涉及參數有兩個,在做類型判斷時,如果兩個參數不一致,可能仍然存在編譯問題

調用者無需區分被加對象是什麼類型,都可以統一使用ADD

C99的tgmath.h

前面說到,_Generic關鍵字在C11中才有,那麼C99怎麼辦呢?實際上,tgmath.h中提供了一些泛型類型宏,如果math.h的函數中定義了float,double和long double版本,tgmath就會提供一個泛型類型宏。效果和前面的例子一樣,舉個例子:

// 來源:公眾號【編程珠璣】
// 作者:守望先生
#include <stdio.h>
#include <tgmath.h>
int main(void)
{
    float f = 4.0f;
    long double d = 1.44;
    printf("%f\n",sqrt(f)); // 實際上調用了sqrtf
    printf("%Lf\n",sqrt(d)); // 實際上調用了sqrtl
    return 0;
}

編譯運行結果:

2.000000
1.200000

但是不得不說,tgmath中提供的泛型宏也是有限的。

void *指針

眾所周知,C語言中void *指針是一種無類型指針,從這點看,也可以算是泛型指針了。而它的使用在C語言中是非常常見的,舉例來說,在《高級指針話題-函數指針》中,我們介紹了快速排序接口的使用,它的函數聲明是這樣的:

#include <stdlib.h>
void qsort(void *base, size_t nmemb, size_t size,
                  int (*compar)(const void *, const void *));

庫函數qsort實際上就是泛型排序算法了,它可以針對任何類型的數據進行排序。當然有一個前提,就是你需要按照它的協議,實現一個compar函數,用於比較大小。

像這樣類似的例子,C語言中還有很多,不過相比於其他語言,如C++中的模板,這種所謂的泛型,確實有些小巫見大巫了。

總結

C語言語法上本身基本不支持泛型編程,但是藉助_Generic關鍵字和一些手段,可以實現泛型編程。

相關焦點

  • C語言編程核心要點
    原文標題C語言編程核心要點,男人看了沉默,女人看了本文轉載自【微信公眾號:碼磚雜役,ID:whatis9527want】引言筆者有十餘年的C++開發經驗,相比而言,我的C經驗只有一兩年,C比較簡單,簡單到《The C Programming Language》(C程序設計語言)只有區區的200多頁,相比上千頁的C++大部頭,不得不說真的很人性化了
  • 多範式程式語言-以Swift為例
    Swift 即支持面向對象編程範式,也支持函數式編程範式,同時還支持泛型編程。Swift 支持多種編程範式是由它的目標決定的。Swift 創造的初衷就是提供一門實用的工業語言。不同於 Haskell 這類出自大學和研究機構的學術性質的程式語言。
  • 在S7-1200/1500中實現泛型
    泛型是高級程式語言的一種特性。
  • 泛型:泛型的定義(類、接口、對象)、使用、繼承
    如果不小心往集合裡加了不相同類型的元素可能會導致類型異常(進行equals、compare比較的時候尤為明顯);             c. 由於沒有類型就需要在很多地方進行強制類型轉換,但是這樣做增加了編程的複雜度,並且代碼也不美觀(臃腫),維護起來也更加困難;    2) 泛型的概念定義:         i.
  • 如何實現Go泛型的討論 | Gopher Daily (2020.09.19) ʕ◔ϖ◔ʔ
    •Go專欄:《改善Go語言編程質量的50個有效實踐》在慕課網上線 - https://www.imooc.com/read/87 目前處於早鳥優惠階段,歡迎訂閱!•有意想學習容器或Kubernets的童鞋可以了解一下我的慕課網實戰課:k8s實戰 - https://coding.imooc.com/class/284.html1.Go泛型如何實現的討論 - https://groups.google.com/g/golang-dev/c/OcW0ATRS4oM 以及Keith Randall提出的兩個實現方案
  • 基於泛型編程的序列化實現方法
    對於類MyFilter和MyVideo則使用相同的方式,即分別增加一個友元模板函數serialize的實現即可,至於std::list模板類,boost已經幫我們實現了。這時我們發現,對於每一個定義的類,我們需要做的僅僅是在類內部聲明一個友元模板函數,同時類外部實現這個模板函數即可,對於後期類的成員變量的修改,如增加、刪除或者重命名成員變量,也僅僅是修改一個函數即可。
  • 想替代C的Zig語言成立了基金會
    今天,我自豪地宣布 Zig Software Foundation,這是一家 501(c)(3) 非營利性公司,致力於促進、保護和推進 Zig 程式語言,支持並促進多元化和國際化的 Zig 開發者社區的發展,並向學生提供教育和指導,教導下一代程式設計師要有能力、有道德並互相遵循高標準。
  • Java泛型的特點與優缺點,泛型擦除是怎麼回事?
    Java 是如何支持泛型的?底層實現?是否了解 Java 泛型的特點,優缺點?是否了解泛型擦除?考察的知識點Java 是如何支持泛型的?底層實現機制是什麼?首先,Java 語言中的泛型不能接受基本類型作為類型參數-它只能接受引用類型。這意味著可以定 List<Integer>,但是不可以定義 List<int>。其次,在 C++ 模板中,編譯器使用提供的類型參數來生成不同代碼。而 Java 中的泛型,編譯器僅僅對這些類型參數進行擦除和替換。
  • Swift 不是多範式函數式程式語言
    一個無法在O(1)時間內簡單地將一個列表折分出「第一個」和「不是第一個」元素的「函數式」語言是一種非常奇怪的函數式語言。Swift特有reduce和map函數,它還具有第一類函數和模式匹配。並且,它還具有在其它函數式語言中也很常見的一些特性。
  • Zig 0.7.1 發布,想要替換 C 的程式語言
    從 release notes 可以看到,此版本修復的問題集中在編譯器上,這不難理解,因為上個版本發布時,團隊指出 0.7.0 的主要目標之一正是實現自託管編譯器。Zig 是一門通用程式語言,專為穩定性、可維護性和性能而設計,追求替代 C 語言在系統編程上的最佳地位。
  • 三天學會C語言編程 | 中篇
    本文是《三天學會C語言編程》的第二篇文章,承接上一篇《變量的名稱就好像郵箱的編號一樣,這樣我們在編程的時候就可以通過這個名字方便的訪問(讀或者寫)變量。在C語言中變量的名稱可以是英文字符、下劃線和數字,但只能以英文字符和下劃線開頭,不能以數字開頭。另外,C語言中變量名稱是區分大小寫的,也就是var_name和var_Name是兩個不同的變量。
  • 單片機C語言編程心得
    寫這個8*8按鍵程序的過程中,不管是在自己寫還是參考別人程序的過程中,發現自己對C語言有些基本知識點和編程規範有很多不懂的地方,有些是自己以前的編程習慣不好,有些就是基礎知識不紮實的表現,所以總結出來。
  • [譯]聊聊C#中的泛型的使用
    類型參數使得設計某些類和方法成為可能,例如,通過使用泛型類型參數T,可以大大簡化類型之間的強制轉換或裝箱操作的過程(裝箱、拆箱問題)。說白了,泛型就是通過參數化類型來實現在同一份代碼上操作多種數據類型,利用「參數化類型」將類型抽象化,從而實現靈活的復用。每個集合的詳細規範可以在System.Collection.Generic名稱空間下找到。
  • Java總結篇系列:Java泛型
    顧名思義,就是將類型由原來的具體的類型參數化,類似於方法中的變量參數,此時類型也定義成參數形式(可以稱之為類型形參),然後在使用/調用時傳入具體的類型(類型實參)。看著好像有點複雜,首先我們看下上面那個例子採用泛型的寫法。
  • 哪種程式語言好?大神為你分析 Go、Java、C、C++ 等主流程式語言
    在生產力方面,其語言特性和生態系統還未成熟,版本還在快速迭代中,相比動態語言和 Java,並不具有優勢,目前階段是這些語言在某些場景下的可選角色。長期看,在 Google 的鼎力支持下,新特性和庫的應用能力還會不斷加入,是一門欣欣向榮的程式語言,但目前階段,建議必須控制好程序的規模和複雜度,語言和生態還未提供健全的支撐,同時還必須留意它的不成熟和版本快速迭代帶來的風險。
  • c語言編程軟體哪個好?c語言編程軟體下載地址
    c語言編程軟體哪個好?c語言編程軟體下載地址 2019年1月14日 HuangJiang來源:網際網路 繁體
  • 如何快速實現C語言上手編程,福利在這裡,C語言編程的入門教程
    C語言編程如何快速實現在我們初次學習C語言的時候,總想著快速的實現編譯過程。那麼C語言編程究竟是如何實現的呢,當然是要通過自己喜歡的編譯器來編譯完成,那麼今天就為大家介紹C語言編程是如何快速的實現。然後在選擇C++語言,C++語言是C語言的升級版,然後再次點擊「Next」,進入下一步。5. 選擇創建的名稱,可以創建一個醒目的名稱。然後便是文件放置位置,然後點擊「Next」。6. 點擊最左側欄「main.cpp」會出現編程書寫界面,然後便可以實現我們的代碼編程。7.
  • Zig 0.6.0 發布,想要替換 C 的程式語言
    Zig 0.6.0 已發布,這是一門通用程式語言,專為穩定性、可維護性和性能而設計,追求替代 C 語言在系統編程上的最佳地位。
  • R 的面向對象編程系統(S3、S4系統介紹)
    如果你熟悉 Java、Python、C++、C# 等程式語言,應該非常熟悉面向對象編程的風格。當我們談論編程時,我們想到的是什麼?我們想到的首先不是編程本身,而是利用編程工具來解決實際問題。想要解決實際問題,就要具體去了解該問題,並用程式語言對該問題進行建模,以藉助程序代碼抽象出該問題的實質,從而完成實際問題與模型之間的轉換。藉助面向對象的編程風格,我們通過定義對象的類以描述對象,通過定義對象的方法以抽象完成實際中的某種具體實現,以這樣的方式,我們可以模仿對象的重要特性。
  • 跟我學Java編程—理解Java泛型概念
    泛型是Java中一個非常重要的概念,在Java集合類框架中被廣泛應用。在介紹泛型之前先看一個例子。在泛型出現之前,這種現象在編程中會經常發生,因為有時程式設計師在獲取集合元素時,並不能夠完全明確集合中存儲的是屬於什麼類型的元素。那麼有什麼辦法可以讓裝入集合容器的數據保存自己的類型,而不被轉化為Object對象呢?這就需要用到JDK 5.0後支持的一項新功能——Java泛型。