C語言學習筆記(五)——結構,位操作,預處理器

C語言學習筆記(一 ~ 五)整理完了,這裏都是整理了一些理論上的東西,方便理解!雖然說編程是動手敲代碼爲主,但是不理解來龍去脈往往很難寫出好的程序,也不容易深入研究。有了理論基礎,就可以大膽的去實操了。

筆記來源於《C Primer Plus 第6版》

目錄

1 結構

1)結構聲明與定義     2)初始化      3)訪問成員        4)函數中的結構       5)保存文件

6)聯合結構

7)枚舉類型

8)typedef

9)複雜的聲明

2 位操作

3 預處理


1 結構

1)結構聲明與定義

結構聲明(structure  declaration)描述了一個結構的組織布局。該聲明並未創建實際的數據對象,只描述了該對象由什麼組成。首先是關鍵字 struct,它表明跟在其後的是一個結構,後面是一個可選的標記,稍後程序中可以使用該標記引用該結構。

struct book {
char title[MAXTITL];
char author[MAXAUTL];
float value;
};

如果把結構聲明置於一個函數的內部,它的標記就只限於該函數內部使用。如果把結構聲明置於函數的外部,那麼該聲明之後的所有函數都能使用它的標記。

先聲明後定義:

struct book library; #這把library聲明爲一個使用book結構佈局的結構變量。

編譯器執行這行代碼便創建了一個結構變量library。編譯器使用book模板爲該變量分配空間:一個內含MAXTITL個元素的char數組、一個內含MAXAUTL個元素的char數組和一個float類型的變量。這些存儲空間都與一個名稱library結合在一起:

聲明結構的過程和定義結構變量的過程可以組合成一個步驟:

struct { /* 無結構標記 */
char title[MAXTITL];
char author[MAXAUTL];
float value;
} library;

聲明結構數組:

聲明結構數組和聲明其他類型的數組類似:struct book library[MAXBKS];

以上代碼把library聲明爲一個內含MAXBKS個元素的數組。數組的每個元素都是一個book類型的數組。

聲明嵌套結構:

在一個結構中包含另一個結構,在結構聲明中創建嵌套結構。和聲明int類型變量一樣,進行簡單的聲明:

struct guy {          // 第2個結構
struct names handle;   // 嵌套結構
char favfood[LEN];
char job[LEN];
float income;
};

聲明結構指針:
struct guy * him;

該聲明並未創建一個新的結構,但是指針him現在可以指向任意現有的guy類型的結構。例如,如果barney是一個guy類型的結構,可以這樣寫:him = &barney;
和數組不同的是,結構名並不是結構的地址,因此要在結構名前面加上&運算符。

2)初始化

初始化一個結構變量與初始化數組的語法類似:

struct book library = {
"The Pious Pirate and the Devious Damsel",
"Renee Vivotte",
1.95
};

初始化器:

C99和C11爲結構提供了指定初始化器,結構的指定初始化器使用點運算符和成員名標識特定的元素。

struct book surprise = { .value = 10.99};

注意:對特定成員的最後一次賦值纔是它實際獲得的值。

struct book gift= {.value = 18.90,
.author = "Philionna Pestle",
0.25};

賦給value的值是0.25,因爲它在結構聲明中緊跟在author成員之後。

3)訪問成員

結構類似於一個“超級數組”,這個超級數組中,使用結構成員運算符——點(.)訪問結構中的成員。例如,library.value即訪問library的value部分。

結構數組成員:

library[0].value /* 第1個數組元素與value 相關聯 */
library[4].title /* 第5個數組元素與title 相關聯 */

library[2].title[4]
這是library數組第3個結構變量(library[2]部分)中書名的第5個字符(title[4]部分)。

嵌套成員:

如何訪問嵌套結構的成員,這需要使用兩次點運算符: fellow.handle.first

從左往右解釋fellow.handle.first:(fellow.handle).first

指針訪問:

fellow 是一個結構數組,這意味着 fellow[0]是一個結構:him = &fellow[0];

指針him指向結構變量fellow[0],如何通過him獲得fellow[0]的成員的值:使用->運算符

如果him == &barney,那麼him->income 即是 barney.income
如果him == &fellow[0],那麼him->income 即是 fellow[0].income

順序指定結構成員的值:如果him ==&fellow[0],那麼*him == fellow[0],即fellow[0].income == (*him).income
(必須要使用圓括號,因爲.運算符比*運算符的優先級高。)

barney.income == (*him).income == him->income

4)函數中的結構

(1)假設要編寫一個與結構相關的函數,是用結構指針作爲參數,還是用結構作爲參數和返回值?

把指針作爲參數有兩個優點:無論是以前還是現在的C實現都能使用這種方法,而且執行起來很快,只需要傳遞一個地址。缺點是無法保護數據。被調函數中的某些操作可能會意外影響原來結構中的數據。不過,ANSI  C新增的const限定符解決了這個問題。

把結構作爲參數傳遞的優點是,函數處理的是原始數據的副本,這保護了原始數據。另外,代碼風格也更清楚。

(2)字符串

在結構中常使用字符數組來儲存字符串。可以使用指向 char 的指針來代替字符數組:

如果使用malloc()分配內存並使用指針儲存該地址,那麼在結構中使用指針處理字符串就比較合理。這種方法的優點是,可以請求malloc()爲字符串分配合適的存儲空間。

(3)複合字面量

C99  的複合字面量特性可用於結構和數組。如果只需要一個臨時結構值,複合字面量很好用。語法是把類型名放在圓括號中,後面緊跟一個用花括號括起來的初始化列表。

(struct book) {"The Idiot", "Fyodor Dostoyevsky", 6.99}

(4)匿名結構

匿名結構是一個沒有名稱的結構成員。用嵌套的匿名成員結構定義person:

struct person
{
int id;
struct {char first[20]; char last[20];}; // 匿名結構
};

初始化ted的方式相同:
struct person ted = {8483, {"Ted", "Grass"}}; 

5)保存文件

由於結構可以儲存不同類型的信息,所以它是構建數據庫的重要工具。

最沒效率的方法是用fprintf():

struct book {
char title[MAXTITL];
char author[MAXAUTL];
float value;
}primer;
fprintf(pbooks, "%s %s %.2f\n", primer.title,primer.author, primer.value);

更好的方案是使用fread()和fwrite()函數讀寫結構大小的單元:

fwrite(&primer, sizeof(struct book), 1, pbooks); #這兩個函數一次讀寫整個記錄,而不是一個字段。

缺點是,不同的系統可能使用不同的二進制表示法,所以數據文件可能不具可移植性。甚至同一個系統,不同編譯器設置也可能導致不同的二進制佈局。

6)聯合結構

聯合(union)是一種數據類型,它能在同一個內存空間中儲存不同的數據類型(不是同時儲存)。建聯合和創建結構的方式相同,需要一個聯合模板和聯合變量。可以用一個步驟定義聯合,也可以用聯合標記分兩步定義。

union hold {
int digit;
double bigfl;
char letter;
};

定義了3個與hold類型相關的變量:
union hold fit; // hold類型的聯合變量
union hold save[10]; // 內含10個聯合變量的數組
union hold * pu; // 指向hold類型聯合變量的指針

聯合只能儲存一個值,這與結構不同。有 3 種初始化的方法:把一個聯合初始化爲另一個同類型的聯合;初始化聯合的第1個元素;或者根據C99標準,使用指定初始化器: 

union hold valB = valA; // 用另一個聯合來初始化
union hold valC = {88}; // 初始化聯合的digit 成員
union hold valD = {.bigfl = 118.2}; // 指定初始化器

點運算符表示正在使用哪種數據類型:

fit.digit = 23; //把 23 儲存在 fit,佔2字節
fit.bigfl = 2.0; // 清除23,儲存 2.0,佔8字節

用指針訪問聯合時也要使用->運算符:

pu = &fit;
x = pu->digit; // 相當於 x = fit.digit

7)枚舉類型

枚舉類型(enumerated type)聲明符號名稱來表示整型常量。使用enum關鍵字,可以創建一個新“類型”並指定它可具有的值(實際上,enum常量是int類型,因此,只要能使用int類型的地方就可以使用枚舉類型)的語法與結構的語法相同。例如,可
以這樣聲明:

enum spectrum {red, orange, yellow, green, blue, violet};
enum spectrum color;

默認情況下,枚舉列表中的常量都被賦予0、1、2等。因此,下面的聲明中nina的值是3:
enum kids {nippy, slats, skippy, nina, liz};

在枚舉聲明中,可以爲枚舉常量指定整數值:
enum levels {low = 100, medium = 500, high = 2000};

8)typedef

typedef工具是一個高級數據特性,利用typedef可以爲某一類型自定義名稱。這方面與#define類似,但是兩者有3處不同:

  • 與#define不同,typedef創建的符號名只受限於類型,不能用於值。
  • typedef由編譯器解釋,不是預處理器。
  • 在其受限範圍內,typedef比#define更靈活。

9)複雜的聲明

1.數組名後面的[]和函數名後面的()具有相同的優先級。它們比*(解引用運算符)的優先級高。因此下面聲明的risk是一個指針數組,不是指向數組的指針:
int * risks[10];
2.[]和()的優先級相同,由於都是從左往右結合,所以下面的聲明中,在應用方括號之前,*先與rusks結合。因此rusks是一個指向數組的指針,該數組內含10個int類型的元素:
int (* rusks)[10];

2 位操作

C 提供按位邏輯運算符和移位運算符。

1)按位邏輯運算符

4個按位邏輯運算符都用於整型數據,包括char。之所以叫作按位(bitwise)運算,是因爲這些操作都是針對每一個位進行,不影響它左右兩邊的位。

二進制反碼或按位取反:~
一元運算符~把1變爲0,把0變爲1。

按位與:&
二元運算符&通過逐位比較兩個運算對象,生成一個新值。對於每個位,只有兩個運算對象中相應的位都爲1時,結果才爲1。

按位或:|
二元運算符|,通過逐位比較兩個運算對象,生成一個新值。對於每個位,如果兩個運算對象中相應的位爲1,結果就爲1。

按位異或:^
二元運算符^逐位比較兩個運算對象。對於每個位,如果兩個運算對象中相應的位一個爲1(但不是兩個爲1),結果爲1。

2)移位運算符

左移:<<
左移運算符(<<)將其左側運算對象每一位的值向左移動其右側運算對象指定的位數。左側運算對象移出左末端位的值丟失,用0填充空出的位置。

右移:>>
右移運算符(>>)將其左側運算對象每一位的值向右移動其右側運算對象指定的位數。左側運算對象移出右末端位的值丟。對於無符號類型,用0 填充空出的位置;對於有符號類型,其結果取決於機器。空出的位置可用0填充,或者用符號位(即,最左端的位)的副本填充。

3)位字段

操控位的第2種方法是位字段(bit field)。位字段是一個signed int或unsigned int類型變量中的一組相鄰的位。位字段通過一個結構聲明來建立,該結構聲明爲每個字段提供標籤,並確定該字段的寬度。

struct {
unsigned int autfd : 1;
unsigned int bldfc : 1;
unsigned int undln : 1;
unsigned int itals : 1;
} prnt;

由於每個字段恰好爲1位,所以只能爲其賦值1或0。 

3 預處理

1)宏定義

在預處理之前,編譯器必須對該程序進行一些翻譯處理。

  • 首先,編譯器把源代碼中出現的字符映射到源字符集。
  • 第二,編譯器定位每個反斜槓後面跟着換行符的實例,並刪除它們。
  • 第三,編譯器把文本劃分成預處理記號序列、空白序列和註釋序列,編譯器將用一個空格字符替換每一條註釋。

#define預處理器指令可以出現在源文件的任何地方,其定義從指令出現的地方到該文件末尾有效。

預處理器指令從#開始運行,到後面的第1個換行符爲止。

宏的名稱中不允許有空格,而且必須遵循C變量的命名規則:只能使用字符、數字和下劃線(_)字符,而且首字符不能是數字。

預處理器不做計算,不對表達式求值,它只進行替換。

(1)記號

可以把宏的替換體看作是記號(token)型字符串,而不是字符型字符串。

替換體中有多個空格時,字符型字符串和記號型字符串的處理方式不同。如:#define EIGHT 4 * 8

如果預處理器把該替換體解釋爲字符型字符串,將用4 * 8替換EIGHT。即,額外的空格是替換體的一部分。如果預處理器把該替換體解釋爲記號型字符串,則用3個的記號4 * 8(分別由單個空格分隔)來替換EIGHT。換而言之,解釋爲字符型字符串,把空格視爲替換體的一部分;解釋爲記號型字符串,把空格視爲替換體中各記號的分隔符。

(2)重定義常量

假設先把LIMIT定義爲20,稍後在該文件中又把它定義爲25。這個過程稱爲重定義常量。除非新定義與舊定義相同,否則有些實現會將其視爲錯誤。

(3)使用參數

在#define中使用參數可以創建外形和作用與函數類似的類函數宏。

#define SQUARE(X) X*X

#define PSQR(X) printf("The square of X is %d.\n", ((X)*(X)));

(4)粘合劑

##運算符可用於類函數宏的替換部分。而且,##還可用於對象宏的替換部分。##運算符把兩個記號組合成一個記號。

#define XNAME(n) x ## n                                                          然後,宏XNAME(4)將展開爲x4。

#define PRINT_XN(n) printf("x" #n " = %d\n", x ## n);             PRINT_XN(1); // 變成 printf("x1 = %d\n", x1);

(5)變參宏

一些函數(如 printf())接受數量可變的參數。stdvar.h 頭文件提供了工具,讓用戶自定義帶可變參數的函數。通過把宏參數列表中最後的參數寫成省略號(即,3個點...)來實現這一功能。這樣,預定義宏_ _VA_ARGS_ _可用在替換部分中,表明省略號代表什麼。

#define PR(...) printf(_ _VA_ARGS_ _)
假設稍後調用該宏:
PR("Howdy");
PR("weight = %d, shipping = $%.2f\n", wt, sp);
對於第1次調用,_ _VA_ARGS_ _展開爲1個參數:"Howdy"。
對於第2次調用,_  _VA_ARGS_  _展開爲3個參數:"weight  =  %d,shipping = $%.2f\n"、wt、sp。

2)文件包含

當預處理器發現#include 指令時,會查看後面的文件名並把文件的內容包含到當前文件中,即替換源文件中的#include指令。這相當於把被包含文件的全部內容輸入到源文件#include指令所在的位置。

#include <stdio.h> ←文件名在尖括號中
#include "mystuff.h" ←文件名在雙引號中

在 UNIX 系統中,尖括號告訴預處理器在標準系統目錄中查找該文件。雙引號告訴預處理器首先在當前目錄中(或文件名中指定的其他目錄)查找該文件,如果未找到再查找標準系統目錄。

實例:

names_st.h頭文件

#include <string.h>
#define SLEN 32

// 結構聲明
struct names_st
{
char first[SLEN];
char last[SLEN];
};

// 類型定義
typedef struct names_st names;

// 函數原型
void get_names(names *);
void show_names(const names *);
char * s_gets(char * st, int n);

name_st.c源文件

#include <stdio.h>
#include "names_st.h"  // 包含頭文件

// 函數定義
void get_names(names * pn)
{
printf("Please enter your first name: ");
s_gets(pn->first, SLEN);
printf("Please enter your last name: ");
s_gets(pn->last, SLEN);
}
。。。

 useheader.c程序

#include <stdio.h>
#include "names_st.h"

int main(void)
{
names candidate;
get_names(&candidate);
。。。
}

另外,還可以使用頭文件聲明外部變量供其他文件共享。 可以在包含這些函數聲明的源代碼文件定義一個文件作用域的外部鏈接變量:int status = 0; // 該變量具有文件作用域,在源代碼文件

然後,可以在與源代碼文件相關聯的頭文件中進行引用式聲明:extern int status; // 在頭文件中

3)其他指令

#undef指令用於“取消”已定義的#define指令。

創建條件編譯(conditinal compilation):

#ifdef、#else和#endif指令

#ifdef MAVIS
#include "horse.h"// 如果已經用#define定義了 MAVIS,則執行下面的指令
#define STABLES 5
#else
#include "cow.h"   //如果沒有用#define定義 MAVIS,則執行下面的指令
#define STABLES 15
#endif

#ifndef指令與#ifdef指令的用法類似,也可以和#else、#endif一起使用,但是它們的邏輯相反。#ifndef指令判斷後面的標識符是否是未定義的。

#if指令很像C語言中的if。#if後面跟整型常量表達式,如果表達式爲非零,則表達式爲真。

#if SYS == 1
#include "ibm.h"
#endif
#if SYS == 1
#include "ibmpc.h"
#elif SYS == 2
#include "vax.h"
#else
#include "general.h"
#endif

 4)內聯函數

函數調用都有一定的開銷,使用宏使代碼內聯,可以避免這樣的開銷。C99還提供另一種方法:內聯函數(inline function)。

標準規定具有內部鏈接的函數可以成爲內聯函數,還規定了內聯函數的定義與調用該函數的代碼必須在同一個文件中。因此,最簡單的方法是使用函數說明符  inline  和存儲類別說明符static。

inline static void eatline() // 內聯函數定義/原型
{
while (getchar() != '\n')
continue;
}

 

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