C語言:第20天筆記
內容提要
- 構造類型
- 枚舉類型
- typedef
- 綜合案例:斗地主
- 預處理
構造類型:枚舉類型
使用建議
- 如果定義不相干的常量,使用宏定義(符號常量);
- 如果需要定義一組相關聯的常量(如月份011、星期06、方向03、男女01等),使用枚舉,進行統一管理。
- 正式開發中,switch的case后面訪問的通常是枚舉中的常量。
定義
- 一般情況下,定義常量使用宏定義(
#define 宏名稱 宏值
),宏定義適合無關聯關系的常量; - 當需要對一組有關聯關系的量(如月份011、星期06、方向0~3等)進行定義時,宏定義清晰度低、不便于統一管理且會增加代碼量,此時需使用枚舉;
- 枚舉的作用是將多個有關聯關系的常量組合到一起,提高代碼的可讀性。
說明
- 枚舉定義了一組常量,開發中可直接使用這些常量(常用);
- 枚舉類型也可類似于結構體定義變量等操作(不常用);
- 枚舉常量有默認值,從0開始依次+1;可在定義時指定默認值,若個別未賦值,可根據已賦值常量依次+1推導。
特點
- 定義了一組常量,類似于定義了多個符號常量(宏定義);
- 提高了代碼的可讀性。
語法
語法分類 | 格式 | 說明 |
---|---|---|
先定義類型后定義變量 | enum 枚舉類型名 變量列表; | 定義枚舉類型名后,再定義該枚舉類型的變量,枚舉的元素是符號常量 |
定義類型同時定義變量 | enum 枚舉類型名{枚舉元素列表} 變量列表; | 在定義枚舉類型的同時,定義該枚舉類型的變量 |
直接定義枚舉變量 | enum {枚舉元素列表} 變量列表; | 不單獨定義枚舉類型名,直接定義枚舉變量 |
案例(demo01.c)
#include <stdio.h>void test1()
{// 定義一個枚舉類型// 注意:枚舉類型名一般首字母大寫,主要是跟枚舉元素名區分enum Week{// 定義枚舉元素,元素本質上就是常量,在編譯期,會被替換為字面量// 枚舉元素的命名和符號常量命名一致,都是大寫+下劃線// 多個枚舉元素之間使用逗號分隔// SUN,MON,TUE,WED,THU,FRI,SAT // 此時,這7個常量的值依次為:0~6SUN = 10, MON, TUE, WED, THU, FRI, SAT // 此時,這7個常量的值依次為:10~16};// 1. 直接訪問枚舉元素,適合于switchprintf("%d,%d,%d\n", SUN, WED, SAT); // 10,13,16// 2. 定義枚舉類型的變量,適合于函數傳參enum Week week;// 初始化week = TUE; // 不能隨便賦值,賦值一定是這個枚舉中定義的元素printf("%d\n", week); // 12// 3. 定義枚舉類型變量的同時賦值enum Week week1 = THU;printf("%d\n", week1);// 14// 4. 可以定義多個枚舉變量enum THU{A, B, C} x, y;// 賦值x = B;y = C;printf("x=%d,y=%d\n", x, y);// 1,2
}void test2()
{// 定義枚舉類型enum CaiQuan{SHI_TOU, JIAN_DAO, BU};printf("請輸入0~2之間的整數:\n0-石頭,1-剪刀,2-布\n");int choice;scanf("%d", &choice);switch (choice) {case SHI_TOU:printf("石頭\n");break;case JIAN_DAO:printf("剪刀\n");break;case BU:printf("布\n");break;}
}int main(int argc, char *argv[])
{test1();test2();return 0;
}
typedef
說明
給類型重命名,不會影響到類型本身。
作用
給已有的類型起別名。
格式
typedef 已有類型名 重命后的類型名;
示例:typedef unsigned long size_t;
使用案例(demo02.c)
#include <stdio.h>int main(int argc, char *argv[])
{// 方式1:先定義數據類型,再重命名// 定義一個結構體struct Student{int id;char *name;char sex;int age;};// 類型重命名typedef struct Student Stu; // 將 struct Student 重命名為Stu// 使用新類型名// 定義結構體實例Stu stu = {1,"張三",'w',21};printf("%d,%s,%c,%d\n", stu.id, stu.name, stu.sex, stu.age);Stu *p = &stu;printf("%d,%s,%c,%d\n", p->id, p->name, p->sex, p->age);// 方式2:定義數據類型的同時重命名typedef struct PersonInfo{int a;double b;} Per;// 定義變量Per per = {2, 4.5};printf("%d,%.2f\n", per.a, per.b);// 定義指針Per *p1 = &per;printf("%d,%.2f\n", p1->a, p1->b);return 0;
}
應用場景
數據類型復雜(結構體、共用體、枚舉、結構體指針、無符號的長整型)時使用。
跨平臺兼容性案例
C語言標準提供:
typedef signed long int_int64_t;
typedef unsigned Long intuint64_t;
常見跨平臺類型重命名:
size_t
:typedef unsigned long size_t;
unit_16
:類型重命名后的數據類型。
進階案例(demo03.c)
#include <stdio.h>struct Student
{int age;char *name;double scores[3];
};typedef struct Student Stu_t; // 對類型重命名
typedef Stu_t* pStu_t; // 結構體指針重命名void test1()
{Stu_t s1 = {21, "zhangsan", {99, 98, 97}};printf("%d,%s,%.2lf,%.2lf,%.2lf\n", s1.age, s1.name, s1.scores[0], s1.scores[1], s1.scores[2]);Stu_t *p;p = &s1;printf("%d,%s,%.2lf,%.2lf,%.2lf\n", (*p).age, p->name, p->scores[0], p->scores[1], p->scores[2]);
}int main(int argc, char *argv[])
{test1();return 0;
}
綜合案例:斗地主
1. 程序概述
這是一個模擬斗地主游戲發牌過程的C語言程序,實現了撲克牌的初始化、洗牌和發牌功能。
2. 功能需求
2.1 撲克牌定義
使用結構體Card
表示一張牌,包含:
- 花色屬性
suit
(0-3表示普通花色????,4表示小王,5表示大王); - 點數屬性
rank
(0-12對應3-A、2,-1表示大小王)。
2.2 主要功能
- 初始化牌組
- 創建包含54張牌的牌組(52張普通牌+2張王牌);
- 普通牌按花色(?,?,?,?)和點數(3-2)排列。
- 洗牌功能
- 使用隨機數對牌組進行隨機排序;
- 確保每次運行洗牌結果不同(基于時間種子)。
- 發牌功能
- 將洗好的牌發給3個玩家;
- 每個玩家17張牌;
- 剩余3張作為底牌。
- 顯示功能
- 打印每個玩家的手牌;
- 打印底牌。
3. 數據結構
suits[]
:存儲4種花色符號的字符串數組;ranks[]
:存儲13個點數等級的字符串數組;jokers[]
:存儲大小王描述的字符串數組;Card
結構體:表示單張牌的數據結構;- 牌組數組:
deck[54]
; - 玩家手牌數組:
player1[17]
、player2[17]
、player3[17]
; - 底牌數組:
bottomCards[3]
。
4. 用戶交互
程序運行后自動完成以下流程:
- 初始化牌組;
- 洗牌;
- 發牌;
- 顯示發牌結果(3個玩家的手牌和底牌)。
5. 輸出格式
- 普通牌顯示格式:花色+點數(如"? 3");
- 王牌顯示格式:“小王"或"大王”;
- 玩家手牌按順序顯示,每張牌用空格分隔;
- 底牌同樣格式顯示。
6. 源碼
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>#define LEN 54// 定義撲克牌的花色和點數
const char *suits[] = {"?","?","?","?"};
const char *ranks[] =
{"3","4","5","6","7","8","9","10","J","Q","K","A","2"}; // 點數
const char *jokers[] = {"小王","大王"}; // 大小王typedef struct
{int suit; // 牌花色int rank; // 底數
} Card;/*** @brief 初始化撲克*/
void initCards(Card *deck);/*** @brief 洗牌*/
void shuffDeck(Card *deck);void dealCard(Card *deck, Card *bottomCards, Card players[3][17]);void printfCard(Card players[3][17]);int main(int argc, char *argv[])
{// 創建一個數組,存放一副牌(54張包括大小王)Card deck[LEN];// 創建三個玩家Card players[3][17];// 創建一個數組用來存放底牌Card bottomCards[3];// 初始化牌initCards(deck);// 洗牌shuffDeck(deck);// 發牌dealCard(deck,bottomCards,players);// 展示牌printfCard(players);return 0;
}/*** @brief 初始化撲克*/
void initCards(Card *deck)
{int index = 0;for(int i = 0; i < 4; i++){for(int j = 0;i < 13; j++){deck[index].rank = j;deck[index].suit = i;index++;}}// 初始化大小王deck[index].suit = 4; // 小王deck[index].rank = -1;index++;deck[index].suit = 5; // 大王deck[index].rank = -1;
}// 洗牌
void shuffDeck(Card *deck)
{srand((unsigned)time(NULL));// 洗牌for (int i = 0; i < LEN; i++){int j = rand() % LEN; // 0-53Card temp = deck[i];deck[i] = deck[j];deck[j] = temp;}
}void dealCard(Card *deck, Card *bottomCards, Card players[3][17])
{int index = 0;for(int i = 0;i < 17; i++){for(int j = 0;j < 3; j++){players[j][i] = deck[index++];}}// 底牌for(int i=0;i< 3; i++){bottomCards[i] = deck[index++]; }}// 打印
void printfCard(Card players[3][17])
{for(int i = 0;i < 3; i++){printf("第%d個玩家的牌是:",i+1);for(int j = 0; j < 17; j++){Card card = players[i][j]; if (card.suit == 4 || card.suit == 5){// 大小王printf("%s ",jokers[card.suit - 4]);}printf("%s %s ",suits[card.suit], ranks[card.rank]);}}}
運行結果
預處理
一、C語言的編譯步驟
C語言程序從源代碼到可執行文件,需經歷以下4個核心步驟,預處理是整個流程的第一步:
- 預處理:由預處理器處理源文件中的預處理指令(如
#define
、#include
等),生成預處理文件(.i
格式); - 編譯:編譯器對預處理文件進行詞法分析、語法分析、語義分析及優化,生成匯編代碼文件(
.s
格式); - 匯編:匯編器將匯編代碼轉換為機器可識別的二進制目標文件(
.o
或.obj
格式); - 鏈接:鏈接器將多個目標文件與標準庫(如
libc
)組合,生成最終可執行文件(如Windows下的.exe
、Linux下的無后綴可執行文件)。
編譯流程示意圖:
源文件(.c) → 預處理(cpp) → 預處理文件(.i) → 編譯(cc1) → 匯編文件(.s) → 匯編(as) → 目標文件(.o) → 鏈接(ld) → 可執行文件
二、什么是預處理
預處理是在源文件(.c
文件)編譯之前,由預處理器自動完成的預備操作。當編譯器對源文件進行編譯時,會先調用預處理器執行預處理操作,只有預處理解析完成后,才能進入后續的編譯過程。
查看預處理結果
通過gcc
編譯器的-E
選項可查看預處理后的結果,命令格式如下:
gcc 源文件 -E -o 預處理輸出文件名
例如,對demo01.c
進行預處理并輸出到demo01.i
,命令為:gcc demo01.c -E -o demo01.i
。
三、預處理核心功能:宏定義
宏定義是預處理階段最常用的功能之一,用于將一個標識符(宏名)定義為一個字符串(宏值),在預處理階段會將代碼中所有宏名替換為對應的宏值(即“宏展開”)。
1. 不帶參數的宏定義
語法
#define 宏名稱 宏值(替換文本)
- 預處理機制:僅做文本替換,不進行類型檢查;
- 內存特性:宏定義不占用內存空間,因為編譯前宏名已被替換為宏值;
- 宏展開:預處理階段將宏名替換為宏值的過程稱為“宏展開”。
案例(demo01.c)
#include <stdio.h>#define PI 3.1415926 // 定義宏PI,宏值為3.1415926int main(int argc, char *argv[])
{float l, s, r;printf("請輸入圓的半徑:\n");scanf("%f", &r);// 計算周長(宏PI會被替換為3.1415926)l = 2.0 * PI * r;// 計算面積(宏PI會被替換為3.1415926)s = PI * r * r;printf("l=%10.4f\ns=%10.4f\n", l, s);return 0;
}
宏展開后的效果
預處理后,代碼中PI
會被直接替換為3.1415926
,關鍵代碼展開如下:
l = 2.0 * 3.1415926 * r;
s = 3.1415926 * r * r;
2. 帶參數的宏定義
帶參數的宏定義允許宏名接收參數,替換時會將宏體中的參數占位符替換為實際傳入的參數,類似函數但無函數調用開銷(僅文本替換)。
語法
#define 宏名(參數列表) 替換表達式
注意事項(面試高頻考點)
帶參數的宏定義需注意括號的使用,若缺少括號可能導致運算優先級錯誤。例如:
- 錯誤寫法:
#define MULTI(a,b) a * b
當調用MULTI(7+2, 3)
時,展開為7+2*3
,結果為13(不符合預期); - 正確寫法:
#define MULTI(a,b) (a) * (b)
當調用MULTI(7+2, 3)
時,展開為(7+2)*(3)
,結果為27(符合預期)。
案例(demo02.c)
#include <stdio.h>// 帶參數的宏定義,宏名一般小寫(區分函數名)
#define MULTI_1(a,b) (a) * (b) // 正確:參數加括號
#define MULTI_2(a,b) a * b // 錯誤:參數無括號int main(int argc, char *argv[])
{// 調用MULTI_1:(7+2)*(3) = 27int result1 = MULTI_1(7+2, 3); printf("%d\n", result1);// 調用MULTI_2:7+2*3 = 13(結果不符合預期)int result2 = MULTI_2(7+2, 3); printf("%d\n", result2);return 0;
}
3. 宏定義的作用域
- 默認作用域:
#define
命令需寫在函數外部,宏名的有效范圍從定義處開始,到本源文件結束; - 終止作用域:可通過
#undef
命令主動終止宏定義的作用域,后續代碼中該宏名不再生效。
案例(demo04.c)
#include <stdio.h>#define PI 3.14 // PI的有效范圍:從定義處(第8行)到#undef處(第18行)
#define DAY 29 // DAY的有效范圍:從定義處(第10行)到本源文件結束void func1()
{float r = 4;float s = PI * r * r; // 預處理后:3.14 * r * r(PI有效)int day = DAY; // 預處理后:29(DAY有效)
}#undef PI // 終止PI的作用域,后續代碼中PI失效
#define PI 3.1415926 // 重新定義PI,作用域從第20行到本源文件結束void func2()
{float r = 4;float s = PI * r * r; // 預處理后:3.1415926 * r * r(新PI有效)int day = DAY; // 預處理后:29(DAY仍有效)
}int main(int argc, char *argv[])
{return 0;
}
4. 宏定義的嵌套引用
宏定義支持嵌套,即一個宏的宏值中可以引用其他已定義的宏名,預處理時會逐層展開所有宏。
案例(demo04.c)
#include <stdio.h>#define R 3.0 // 定義宏R(半徑)
#define PI 3.14 // 定義宏PI(圓周率)
#define L 2 * PI * R // 嵌套引用PI和R,計算周長
#define S PI * R * R // 嵌套引用PI和R,計算面積// 錯誤示例:宏定義中不應加等號(會導致替換錯誤)
#define P_WIDTH = 800
#define P_HEIGHT = 480
#define SIZE = P_WIDTH * P_HEIGHTint main(int argc, char *argv[])
{// 預處理后:L展開為2*3.14*3.0,S展開為3.14*3.0*3.0printf("L=%f\nS=%f\n", L, S);return 0;
}