跟濤哥一起學嵌入式 29:爲什麼很多人編程喜歡用 typedef?

1. typedef 的基本使用

 

1.1 typedef與結構體的結合使用

typedef 是 C 語言的一個關鍵字,用來給某個類型起個別名,也就是給C語言中已經存在的一個類型起一個新名字。大家在閱讀代碼的過程中,會經常見到 typedef 與結構體、聯合體、枚舉、函數指針聲明結合使用。比如下面結構體類型的聲明和使用:

struct student
{
    char name[20];
    int  age;
    float score;
};
struct student stu = {"wit", 20, 99};

在C語言中定義一個結構體變量,我們通常的寫法是:

struct 結構體名 變量名;
前面必須有一個struct關鍵字打前綴,編譯器纔會理解你要定義的對象是一個結構體變量。而在C++語言中,則不需要這麼做,直接使用:結構體名 變量名就可以了
struct student 
{
    char name[20];
    int age;
    float score; 
};
int main (void)
{
    student stu = {"wit", 20, 99};
    return 0;
}

如果我們使用typedef,就可以給student聲明一個別名student_t和一個結構體指針類型student_ptr,然後就可以直接使用student_t類型去定義一個結構體變量,不用再寫struct,這樣會顯得代碼更加簡潔。

#include <stdio.h>
typedef struct student
{
    char name[20];
    int  age;
    float score;
}student_t, *student_ptr;
​
int main (void)
{
    student_t   stu = {"wit", 20, 99};
    student_t  *p1 = &stu;
    student_ptr p2 = &stu;
    printf ("name: %s\n", p1->name);
    printf ("name: %s\n", p2->name); 
    return 0;
}
程序運行結果:
wit
wit

 

1. 2 typedef 與數組的結合使用

typedef除了與結構體結合使用外,還可以與數組結合使用。定義一個數組,通常我們使用int array[10];即可。我們也可以使用typedef先聲明一個數組類型,然後再使用這個類型去定義一個數組。

typedef int array_t[10]; 
array_t array;
int main (void)
{
    array[9] = 100;
    printf ("array[9] = %d\n", array[9]);
    return 0;
}

在上面的demo程序中,我們聲明瞭一個數組類型array_t,然後再使用該類型定義一個數組array,這個array效果其實就相當於:int array[10]。

 

1.3 typedef 與指針的結合使用

typedef char * PCHAR;
int main (void)
{
    //char * str = "學嵌入式,到宅學部落";
    PCHAR str = "學嵌入式,到宅學部落";
    printf ("str: %s\n", str);
    return 0;
}

在上面的demo程序中,PCHAR 的類型是 char *,我們使用PCHAR類型去定義一個變量str,其實就是一個char *類型的指針。

 

1.4 typedef與函數指針的結合使用

定義一個函數指針,我們通常採用下面的形式:

int (*func)(int a, int b);

我們同樣可以使用typedef聲明一個函數指針類型:func_t

typedef int (*func_t)(int a, int b);
func_t fp;  // 定義一個函數指針變量

寫個簡單的程序測試一下,運行OK:

typedef int (*func_t)(int a, int b);
int sum (int a, int b)
{
	return a + b;
} 
int main (void)
{
	func_t fp = sum;
	printf ("%d\n", fp(1,2));
	return 0;
}

爲了增加程序的可讀性,我們經常在代碼中看到下面的聲明形式:

typedef int (func_t)(int a, int b);
func_t *fp = sum;

函數都是有類型的,我們使用typedef給函數類型聲明一個新名稱:func_t。這樣聲明的好處是:即使你沒有看到func_t的定義,也能夠清楚地知道fp是一個函數指針,代碼的可讀性比上面的好。

 

1.5 typedef與枚舉的結合使用

typedef enum color
{
	red,
	white,
	black,
	green,
	color_num,
} color_t;

int main (void)
{
	enum color color1 = red;
	color_t    color2 = red;
	color_t color_number = color_num;
	printf ("color1: %d\n", color1);
	printf ("color2: %d\n", color2);
	printf ("color num: %d\n", color_number);
	return 0;
}

枚舉與typedef的結合使用方法跟結構體類似:可以使用typedef給枚舉類型color聲明一個新名稱color_t,然後使用這個類型就可以直接定義一個枚舉變量。

 

2. 使用typedef的優勢

不同的項目,有不同的代碼風格,也有不同的代碼“癖好”。看得代碼多了,你會發現:有的代碼喜歡用宏,有的代碼喜歡使用typedef。那麼,使用typedef到底有哪些好處呢?爲什麼很多人喜歡用它呢?

 

2.1 可以讓代碼更加清晰簡潔

typedef struct student
{
	char name[20];
	int  age;
	float score;
}student_t, *student_ptr;

student_t   stu = {"wit", 20, 99};
student_t  *p1 = &stu;
student_ptr p2 = &stu;

如前面的示例代碼所示,使用typedef,我們可以在定義一個結構體、聯合、枚舉變量時,省去關鍵字struct,讓代碼更加簡潔。

 

2.2 增加代碼的可移植性

C語言的int類型,我們知道,在不同的編譯器和平臺下,所分配的存儲字長不一樣:可能是2個字節,可能是4個字節,也有可能是8個字節。如果我們在代碼中想定義一個固定長度的數據類型,此時使用int,在不同的平臺環境下運行可能會出現問題。爲了應付各種不同“脾氣”的編譯器,最好的辦法就是使用自定義數據類型,而不是使用C語言的內置類型。

#ifdef PIC_16
typedef  unsigned long U32
#else
typedef unsigned int U32  
#endif

在16位的 PIC 單片機中,int一般佔2個字節,long佔4個字節,而在32位的ARM環境下,int和long一般都是佔4個字節。如果我們在代碼中想使用一個32位的固定長度的無符號類型,可以使用上面方式聲明一個U32的數據類型,在代碼中你可以放心大膽地使用U32。將代碼移植到不同的平臺時,直接修改這個聲明就可以了。

在Linux內核、驅動、BSP 等跟底層架構平臺密切相關的源碼中,我們會經常看到這樣的數據類型,如size_t、U8、U16、U32。在一些網絡協議、網卡驅動等對字節寬度、大小端比較關注的地方,也會經常看到typedef使用得很頻繁。

 

2.3 比宏定義更好用

C語言的預處理指令#define用來定義一個宏,而typedef則用來聲明一種類型的別名。typedef跟宏相比,不僅僅是簡單的字符串替換,可以使用該類型同時定義多個同類型對象。

typedef char* PCHAR1;
#define PCHAR2 char *

int main (void)
{
	PCHAR1 pch1, pch2;
	PCHAR2 pch3, pch4;
	printf ("sizeof pch1: %d\n", sizeof(pch1));
	printf ("sizeof pch2: %d\n", sizeof(pch2));
	printf ("sizeof pch3: %d\n", sizeof(pch3));
	printf ("sizeof pch4: %d\n", sizeof(pch4));
	return 0;
}

在上面的示例代碼中,我們想定義4個指向char類型的指針變量,然而運行結果卻是:

sizeof pch1: 4
sizeof pch2: 4
sizeof pch3: 4
sizeof pch4: 1

本來我們想定義4個指向char類型的指針,但是 pch4 經過預處理宏展開後,就變成成了一個字符型變量,而不是一個指針變量。而 PCHAR1 作爲一種數據類型,在語法上其實就等價於相同類型的類型說明符關鍵字,因此可以在一行代碼中同時定義多個變量。上面的代碼其實就等價於:

char *pch1, *pch2;
char *pch3, pch4;

 

2.4 讓複雜的指針聲明更加簡潔

一些複雜的指針聲明,如:函數指針、數組指針、指針數組的聲明,往往很複雜,可讀性差。比如下面函數指針數組的定義:

int *(*array[10])(int *p, int len, char name[]);

上面的指針數組定義,很多人一瞅估計就懵逼了。我們可以使用typedef優化一下:先聲明一個函數指針類型func_ptr_t,接着再定義一個數組,就會更加清晰簡潔,可讀性就增加了不少:

typedef int *(*func_ptr_t)(int *p, int len, char name[]);
func_ptr_t array[10];

 

3. 使用typedef需要注意的地方

通過上面的示例代碼,我們可以看到,使用typedef可以讓我們的代碼更加簡潔、可讀性更強一些。但是typedef也有很多坑,稍微不注意就可能翻車。下面分享一些使用typedef需要注意的一些細節。

 

3.1 typedef在語法上等價於關鍵字

我們使用typedef給已知的類型聲明一個別名,其在語法上其實就等價於該類型的類型說明符關鍵字,而不是像宏一樣,僅僅是簡單的字符串替換。舉一個例子大家就明白了,比如const和類型的混合使用:當const和常見的類型(如:int、char) 一同修飾一個變量時,const和類型的位置可以互換。但是如果類型爲指針,則const和指針類型不能互換,否則其修飾的變量類型就發生了變化,如常見的指針常量和常量指針:

char b = 10;
char c = 20;
int main (void)
{	
	char const *p1 = &b; //常量指針:*p1不可變,p1可變
	char *const p2 = &b; //指針常量:*p2可變,p2不可變	
	p1  = &c; //編譯正常 
	*p1 = 20; //error: assignment of read-only location	
	p2  = &c; //error: assignment of read-only variable`p2'
	*p2 = 20; //編譯正常
	return 0;
}

當typedef 和 const一起去修飾一個指針類型時,與宏定義的指針類型進行比較:

typedef char* PCHAR2;
#define PCHAR1 char * 
char b = 10;
char c = 20;
int main (void)
{	
	const PCHAR1 p1 = &b;
	const PCHAR2 p2 = &b;
	p1  = &c; //編譯正常 
	*p1 = 20; //error: assignment of read-only location	
	p2  = &c; //error: assignment of read-only variable`p2'
	*p2 = 20; //編譯正常
	return 0;
}

運行程序,你會發現跟上面的示例代碼遇到相同的編譯錯誤,原因在於宏展開僅僅是簡單的字符串替換:

const PCHAR1 p1 = &b; //宏展開後是一個常量指針
const char * p1 = &b; //其中const與類型char的位置可以互換

而在使用PCHAR2定義的變量p2中,PCHAR2作爲一個類型,位置可與const互換,const修飾的是指針變量p2的值,p2的值不能改變,是一個指針常量,但是*p2的值可以改變。

const PCHAR2 p2 = &b; //PCHAR2此時作爲一個類型,與const可互換位置
PCHAR2 const p2 = &b; //該語句等價於上條語句
char * const p2 = &b; //const和PCHAR2一同修飾變量p2,const修飾的是p2!

 

3.2 typedef是一個存儲類關鍵字

沒想到吧,typedef在語法上是一個存儲類關鍵字!跟常見的存儲類關鍵字(如:auto、register、static、extern)一樣,在修飾一個變量時,不能同時使用一個以上的存儲類關鍵字,否則編譯會報錯:

typedef static char * PCHAR;
//error: multiple storage classes in declaration of `PCHAR'

 

3.3 typedef 的作用域

跟宏的全局性相比,typedef作爲一個存儲類關鍵字,是有作用域的。使用typedef聲明的類型跟普通變量一樣遵循作用域規則:包括代碼塊作用域、文件作用域等。

typedef char CHAR;

void func (void)
{
	#define PI 3.14
	typedef short CHAR;
	printf("sizeof CHAR in func: %d\n",sizeof(CHAR)); 
}

int main (void)
{	
	printf("sizeof CHAR in main: %d\n",sizeof(CHAR));
	func();
	typedef int CHAR;
	printf("sizeof CHAR in main: %d\n",sizeof(CHAR));
	printf("PI:%f\n", PI);	
	return 0;
}

宏定義在預處理階段就已經替換完畢,是全局性的,只要保證引用它的地方在定義之後就可以了。而使用typedef聲明的類型則跟普通變量一樣遵循作用域規則。上面代碼的運行結果爲:

sizeof CHAR in main: 1
sizeof CHAR in func: 2
sizeof CHAR in main: 4
PI:3.140000

 

4 如何避免typedef的濫用?

通過上面的學習我們可以看到:使用typedef可以讓我們的代碼更加簡潔、可讀性更好。在實際的編程中,越來越多的人也開始嘗試使用typedef,甚至到了“過猶不及”的濫用地步:但凡遇到結構體、聯合、枚舉都要用個typedef封裝一下,不用就顯得你low、你菜、你的代碼沒水平。

其實typedef也有副作用,不一定非得處處都用它。比如上面我們封裝的STUDENT類型,當你定義一個變量時:

STUDENT stu;

不看STUDENT的聲明,你知道stu的含義嗎?未必吧。而如果我們直接使用struct定義一個變量,則會更加清晰,讓你一下子就知道stu是個結構體類型的變量:

struct  student stu;

一般來講,當遇到以下情形時,使用typedef可能會有用,否則可能會適得其反:

  • 創建一個新的數據類型

  • 跨平臺的指定長度的類型:如U32/U16/U8

  • 跟操作系統、BSP、網絡字寬相關的數據類型:如size_t、pid_t等

  • 不透明的數據類型:需要隱藏結構體細節,只能通過函數接口訪問的數據類型

在閱讀Linux內核源碼過程中,你會發現大量使用了typedef,哪怕是簡單的int、long都使用了typedef。這是因爲:Linux內核源碼發展到今天,已經支持了太多的平臺和CPU架構,爲了保證數據的跨平臺性和可移植性,所以很多時候不得已使用了typedef,對一些數據指定固定長度:如U8/U16/U32等。但是內核也不是到處到濫用,什麼時候該用,什麼不該用,也是有一定的規則要遵循的,具體大家可以看kernel Document中的 CodingStyle 中關於typedef的使用建議。

專注嵌入式、Linux、C語言精品教程。

請關注公衆號:宅學部落,嵌入式技術QQ羣:宅學部落

加作者微信:brotau

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章