最全C語言筆記篇(下)

2020-08-12 19:46:46

BACK:最全C語言筆記篇(中)

Eleventh Week 結構型別


1. 列舉

常數符號化:

  • 用符號而不是具體的數位來表示程式中的數位
#include <stdio.h>

const int red = 0;
const int yellow =1;
const int green = 2;

int main(int argc, char const *argv[]) {
	int color = -1;
    char *colorName = NULL;
    
    printf("輸入你喜歡的顏色的代號:");
    scanf("%d", &color);
    switch (color) {
        case red: colorName = "red"; break;
        case yellow: colorName = "yellow"; break;
        case green: colorName = "green"; break;
        default: colorName = "unknown"; break;
    }
    printf("你喜歡的顏色是%s\n", colorName);
    
    return 0;
}

列舉:

  • 用列舉而不是定義獨立的const int變數
#include <stdio.h>

enum COLOR {RED, YELLOW, GREEN};

int main(int argc, char const *argv[]) {
	int color = -1;
	char *colorName = NULL;c
	
	printf("輸入你喜歡的顏色的代號:");
    scanf("%d", &color);
    switch (color) {
        case RED: colorName = "red"; break;
        case YELLOW: colorName = "yellow"; break;
        case GREEN: colorName = "green"; break;
        default: colorName = "unknown"; break;
    }
    printf("你喜歡的顏色是%s\n", colorName);
    
    return 0;
}

列舉:

  • 列舉是一種使用者定義的數據型別,它用關鍵字enum以如下語法來宣告:

    enum 列舉型別名字 {名字0, ..., 名字n};
    

  • 列舉型別名字通常並不真的使用,要用的是在大括號裡的名字,因爲它們就是常數符號,它們的型別是int,值則依次從0到n。如:

    enum color {red, yellow, green};
    
  • 就建立了三個常數,red的值是0,yellow是1,而green是2。

  • 當需要一些可以排列起來的常數值時,定義列舉的意義就是給了這些常數值名字。

  • 列舉可以作爲值
  • 列舉型別可以跟上enum作爲型別
  • 但是實際上是以整數來做內部計算和外部輸入輸出的
#include <stdio.h>

enum color {red, yellow, green};

void f(enum color c);

int main(void) {
    enum color t = red;
    
    scanf("%d", &t);
    f(t);
    
    return 0;
}

void f(enum color c) {
    printf("%d\n", c);
}

套路:自動計數的列舉

  • 這樣需要遍歷所有的列舉量或者需要建立一個用列舉量做下標的陣列的時候就很方便了
#include <stdio.h>

enum COLOR {RED, YELLOW, GREEN, NumCOLORS};

int main(int argc, char const *argv[]) {
    int color = -1;
    char *ColorNames[NumCOLORS] = {
        "red", "yellow", "green",
    };
    char *colorName = NULL;
    
    printf("輸入你喜歡的顏色的程式碼:");
    scanf("%d", &color);
    if (color>=0 && color<NumCOLORS) {
        colorName = ColorName[color];
    } else {
        colorName = "unknown";
    }
    printf("你喜歡的顏色是%s\n", colorName);
    
    return 0;
}

列舉量:

  • 宣告列舉量的時候可以指定值

    • enum COLOR {RED=1, YELLOW, GREEN=5};
      
#include <stdio.h>

enum COLOR {RED=1, YELLOW, GREEN=5, NumCOLORS};

int main(int argc, char const *argv[]) {
    printf("code of GREEN is %d\n", GREEN);
    
    return 0;
}

列舉只是int:

  • 即使給列舉型別的變數賦不存在的整數值也沒有任何warning和error
#include <stdio.h>

enum COLOR {RED=1, YELLOW, GREEN=5, NumCOLORS};

int main(int argc, char const *argv[]) {
    enum COLOR color = 0;  // 本應該用符號量賦值,如:RED等。但未進行列舉型別轉換,而直接用數位,是因爲編譯器在這方面的放鬆,主要是這個不常用
    
    printf("code of GREEN is %d\n", GREEN);
    printf("and color is %d\n", color);
    
    return 0;
}

列舉:

  • 雖然列舉型別可以當作型別使用,但是實際上很少有,不好用
  • 如果有意義上排比的名字,用列舉比const int方便
  • 列舉比宏(macro)好,因爲列舉有int型別

主要作用:定義符號量,而不是當作型別使用。C的列舉不太成功!主要是爲了定義符號量。

2. 結構

宣告結構型別:

#include <stdio.h>

int main(int argc, char const *argv[]) {
    struct date {
        int month;
        int day;
        int year;
    };  // 注意不要漏了這個分號!
    struct date today;
    
    today.month = 07;
    today.day = 31;
    today.year = 2014;
    
    printf("Today's date is %i-%i-%i.\n", today.year, today.month, today.day);
    
    return 0;
}

在函數內/外?

  • 和本地變數一樣,在函數內部宣告的結構型別只能在函數內部使用
  • 所以通常在函數外部宣告結構型別,這樣就可以被多個函數所使用了
#include <stdio.h>

struct date {
    int month;
    int day;
    int year;
};

int main(int argc, char const *argv[]) {
    struct date today;
    
    today.month = 07;
    today.day = 31;
    today.year = 2014;
    
    printf("Today's date is %i-%i-%i.\n", today.year, today.month, today.day);
    
    return 0;
}

宣告結構的形式:

  • 對於第一和第三種形式,都宣告瞭結構point。但是第二種形式沒有宣告point,只是定義了兩個變數。
struct point {
    int x;
    int y;
};
struct point p1, p2;  // p1和p2都是point裏面有x和y的值

struct {
    int x;
    int y;
} p1, p2;  // p1和p2都是一種無名結構,裏面有x和y

struct point {
    int x;
    int y;
} p1, p2;  // p1和p2都是point裏面有x和y的值

結構的初始化:

#include <stdio.h>

struct date {
	int month;
	int day;
	int year;
};

int main(int argc, char const *argv[]) {
	struct date today = {07, 31, 2014};
    struct date thismonth = {.month=7, .year=2014};  // 和陣列一樣沒給的值添零
    
    printf("Today's date is %i-%i-%i.\n", today.year, today.month, today.day);
    printf("This month is %i-%i-%i.\n", thismonth.year, thismonth.month, thismonth.day);
    
    return 0;
}

結構成員:

  • 結構和陣列有點像

  • 陣列用[]運算子和下標存取其成員

    • a[0] = 10;
      
  • 結構用.運算子和名字存取其成員

    • today.day
      
    • student.firstName
      
    • p1.x
      
    • p1.y
      

結構運算:

  • 要存取整個結構,直接用結構變數的名字

  • 對於整個結構,可以做賦值、取地址,也可以傳遞給函數參數

    • p1 = (struct point){5, 10};  // 相當於p1.x = 5; p1.y = 10;
      
    • p1 = p2;  // 相當於p1.x = p2.x; p1.y = p2.y;
      
  • 陣列無法做這兩種運算!

結構指針:

  • 和陣列不同,結構變數的名字並不是結構變數的地址,必須使用&運算子

  • struct date *pDate = &today;  // today本身無法列印?類似於int x
    

結構作爲函數參數:

int numberOfDays(struct date d)
  • 整個結構可以作爲參數的值傳入函數
  • 這時候是在函數內新建一個結構變數,並複製呼叫者的結構的值
  • 也可以返回一個結構
  • 這與陣列完全不同

輸入結構:

  • 沒有直接的方式可以一次scanf一個結構
  • 如果我們打算寫一個函數來讀入結構
  • 但是讀入的結構如何送回來呢?
  • 記住C在函數呼叫時是傳值的
    • 所以函數中的p與main中的y是不同的
    • 在函數讀入了p的數值之後,沒有任何東西回到main,所以y還是{0, 0}
#include <stdio.h>

struct point {
	int x;
	int y;
};

void getStruct(struct point);
void output(struct point);

void main() {
    struct point y = {0, 0};
    getStruct(y);
    output(y);
}

void getStruct(struct point p) {
    scanf("%d", &p.x);
    scanf("%d", &p.y);
    printf("%d,%d", p.x, p.y);
}

void output(struct point p) {
    printf("%d,%d", p.x, p.y);
}

解決的方案:

  • 之前的方案,把一個結構傳入了函數,然後在函數中操作,但是沒有返回回去
    • 問題在於傳入函數的是外面那個結構的克隆體,而不是指針
      • 傳入結構和傳入陣列是不同的
  • 在這個輸入函數中,完全可以建立一個臨時的結構變數,然後把這個結構返回給呼叫者
struct point inputPoint() {
	struct point temp;
	scanf("%d", &temp.x);
	scanf("%d", &temp.y);
	return temp;
}

void main() {
	struct point y = {0, 0};
	y = inputPoint();
	output(y);
}

指向結構的指針:

  • 用->表示指針所指的結構變數中的成員
struct date {
	int month;
	int day;
	int year;
} myday;

struct date *p = &myday;

(*p).month = 12;
p->month = 12;

  • ->用於結構體指針的
#include <stdio.h>

struct point {
    int x;
    int y;
};

struct point* getStruct(struct point*);
void output(struct point);
void print(cosnt struct point *p);

int main(int argc, char const *argv[]) {
    struct point y = {0, 0};
    getStruct(&y);
    output(y);
    output(*getStruct(&y));
    print(getStruct(&y));
    getStruct(&y)->x = 0;  // 在賦值的左邊
    *getStruct(&y) = (struct point){1, 2};
    
    return 0;
}

struct pint* getStruct(struct point *p) {
    scanf("%d", &p->x);
    scanf("%d", &p->y);
    printf("%d, %d", p->x, p->y);
    return p;  // 再傳回去一個指針是爲了方便再運算
}

void output(struct point p) {
    printf("%d, %d", p.x, p.y);
}

void print(const struct point *p) {
    printf("%d, %d", p->x, p->y);
}

結構陣列:

struct date dates[100];
struct date dates[] = {
	{4, 5, 2005},
	{2, 4, 2005}
};

結構中的結構:

struct dateAndTime {
	struct date sdate;
	struct time stime;
};

巢狀的結構:

struct point {
	int x;
	int y;
};
struct rectangle {
	struct point pt1;
	struct point pt2;
};

// 如果有變數 struct rectangle r;
// 就可以有:r.pt1.x、r.pt1.y,r.pt2.x和r.pt2.y

// 如果有變數定義:struct rectangle r, *rp; rp = &r;
// 那麼下面 下麪的四種形式是等價的:
r.pt1.x
rp->pt1.x
(r.pt1).x
(rp->pt1).x
// 但是沒有rp->pt1->x(因爲pt1不是指針)

結構中的結構的陣列:

#include <stdio.h>

struct point {
	int x;
	int y;
};

struct rectangle {
	struct point p1;
	struct point p2;
};

void printRect(struct rectangel r) {
	printf("<%d,%d> to <%d,%d>\n", r.p1.x, r.p1.y, r.p2.x, r.p2.y);
}

int main(int argc, char const *argv[]) {
	int i;
    
	struct rectangle rects[] = {{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}};  // 2 rectangle
	for (i=0; i<2; i++) printRect(rects[i]);
    
    return 0;
}

struct的記憶體大小是成員個數n*成員裡佔據最大空間的成員佔據的空間,而地址是連續的,但是中間會有記憶體縫隙

3. 聯合

自定義數據型別:(typedef 方法看最後一個單詞是它的名字)

  • C語言提供了一個叫做 typedef 的功能來宣告一個已有的數據型別的新名字。比如:

    typedef int Length  // 使得Length成爲int型別的別名
    
  • 這樣,Length這個名字就可以代替int出現在變數定義和參數宣告的地方了:

    Length a, b, len;
    Length numbers[10];
    

Typedef:宣告新的型別的名字

  • 新的名字是某種型別的別名
  • 改善了程式的可讀性
typedef long int64_t;  // 過載已有的型別名字,新名字的含義更清晰,具有可移植性
typedef struct ADate {
	int month;
	int day;
	int year;
} Date;  // 簡化了複雜的名字

int64_t i = 100000000000;
Date d = {9, 1, 2005};

聯合:相似於struct

union AnElt {
	int i;
	char c;
} elt1, elt2;

elt1.i = 4;
elt2.c = 'a';
elt2.i = 0xDEADBEEF;
  • 成員是
    • 一個int i還是
    • 一個char c
  • sizeof(union …) = sizeof(每個成員)的最大值

  • 儲存
    • 所有的成員共用一個空間
    • 同一時間只有一個成員是有效的
    • union的大小是其最大的成員
  • 初始化
    • 對第一個成員做初始化

  • 作用:可以得到一個int、double、flaot等內部的各個位元組。

  • 檔案操作,把一個整數以二進制的形式寫到檔案裡的時候,做讀寫的中間媒介。

#include <stdio.h>

typedef union {
	int i;
	char ch[sizeof(int)];
} CHI;

int main(int argc, char const *argv[]){
	CHI chi;
	unsigned int i;
	chi.i = 1234;
	for (i=0; i<sizeof(int); i++) {
		printf("%02hhX", chi.ch[i]);
	}
	printf("\n");
    
	return 0;
}


Twelfth Week 程式結構


1. 全域性變數

  • 定義在函數外面的變數是全域性變數
  • 全域性變數具有全域性的生存期和作用域
    • 它們與任何函數都無關
    • 在任何函數內部都可以使用它們

__func__當前函數的名字

全域性變數初始化:

  • 沒有做初始化的全域性變數會得到0值
    • 指針會得到NULL值
  • 只能用編譯時刻已知的值來初始化全域性變數
  • 它們的初始化發生在main函數之前

區域性變數不會初始化:

#include <stdio.h>

int f(void);

int gAll = f();  // 指明不是一個編譯時刻的常數,不可如此寫

int main(int argc, char const *argv[]) {
    printf("in %s gAll=%d\n", __func__, gAll);
    f();
    printf("agn in %s gAll=%d\n", __func__, gAll);
    
    return 0;
}
 int f(void) {
    printf("in %s gAll=%d\n", __func__, gAll);
    gAll += 2;
    printf("agn in %s gAll=%d\n", __func__, gAll);
    
    return gAll;
 }
#include <stdio.h>

int f(void);

const int gAll = 12;
int g2 = gAll;

int main(int argc, char const *argv[]) {
    printf("in %s gAll=%d\n", __func__, gAll);
    f();
    printf("agn in %s gAll=%d\n", __func__, gAll);
    
    return 0;
}
 int f(void) {
    printf("in %s gAll=%d\n", __func__, gAll);
    // gAll += 2;
    printf("agn in %s gAll=%d\n", __func__, gAll);
    
    return gAll;
 }
  • 不建議如此做

  • 相同的變數,原生的會覆蓋全域性的變數

2. 靜態本地變數

  • 在本地變數定義時加上static修飾符就成爲靜態本地變數
  • 當函數離開的時候,靜態本地變數會繼續存在並保持其值
  • 靜態本地變數的初始化只會在第一次進入這個函數時做,以後進入函數時會儲存上次離開時的值

  • 靜態本地變數實際上時特殊的全域性變數
  • 它們位於相同的記憶體區域
  • 靜態本地變數具有全域性的生存期,函數內的區域性作用域
    • static在這裏的意思是區域性作用域(本地可存取)

3. 全域性變數貼士

*返回指針的函數

  • 返回本地變數的地址是危險的
  • 返回全域性變數或靜態本地變數的地址是安全的
  • 返回在函數內malloc的記憶體是安全的,但是容易造成問題
  • 最好的做法是返回傳入的指針

tips:

  • 不要使用全域性變數來在函數間傳遞參數和結果
  • 儘量避免使用全域性變數
    • 豐田汽車的案子
  • *使用全域性變數和靜態本地變數的函數是執行緒不安全的

4. 編譯預處理和宏

編譯預處理指令:

  • #開頭的是編譯預處理指令
  • 它們不是C語言的成分,但是C語言程式離不開它們
  • #define用來定義一個宏

  • c檔案編譯預處理成i檔案再編譯器編譯成s彙編程式碼檔案,然後s彙編成一個目的碼檔案o,o再和其他的鏈接成一個可執行檔案out

#define:

  • #define <名字> <值>
  • 注意沒有結尾的分號,因爲不是C的語句
  • 名字必須是一個單詞,值可以是各種東西
  • 在C語言的編譯器開始編譯之前,編譯器預處理程式(cpp)會把程式中的名字換成值
    • 完全的文字替換
  • gcc --save-temps

宏:

  • 如果一個宏的值中有其他的宏的名字,也是會被替換的
  • 如果一個宏的值超過一行,最後一行之前的行末需要加\
  • 宏的值後面出現的註釋不會被當作宏的值的一部分

沒有值的宏:

  • #define _DEBUG

  • 這類宏是用於條件編譯的,後面有其他的編譯預處理指令來檢查這個宏是否已經被定義過了

  • 檢查是否存在來做條件編譯程式碼

預定義的宏:

  • _LINE_:原始碼檔案當前所在的行號
  • _FILE_:原始碼檔案的檔名
  • _DATE_:編譯時候的日期
  • _TIME_:編譯時候的時間
  • _STDC_:當要求程式嚴格遵循ANSIC標準時該識別符號被賦值爲1

5. 帶參數的宏

像函數的宏:

  • #define cube(x) ((x)*(x)*(x))
    
  • 宏可以帶參數,但是參數沒有型別

帶參數的宏的原則:

  • 一切都要括號

    • 整個值要括號
    • 參數出現的每個地方都要括號
  • #define RADTODEG(x) ((x)*57.29578)
    

帶參數的宏:

  • 可以帶多個參數

    • #define MIN(a, b) ((a)>(b)?(b):(a))
      
  • 也可以組合(巢狀)使用其他宏

  • 在大型程式的程式碼中使用非常普遍
  • 可以非常複雜,如「產生」函數
    • 在#和##這兩個運算子的幫助下
  • 存在中西方文化差異(國內用的較少,國外用的較多)
  • 部分宏會被inline函數替代(因爲inline可以相比宏做型別檢查)

其他編譯預處理指令:

  • 條件編譯
  • error

6. 大程式結構

多個.c檔案:

  • main()裡的程式碼太長了適合分成幾個函數
  • 一個原始碼檔案太長了適合分成幾個檔案
  • 兩個獨立的原始碼檔案不能編譯形成可執行的程式

專案:

  • 在Dev C++中新建一個專案,然後把幾個原始碼檔案加入進去
  • 對於專案,Dev C++的編譯會把一個專案中所有的原始碼檔案都編譯後,鏈接起來
  • 有的IDE有分開的編譯和構建兩個按鈕,前者是對單個原始碼檔案編譯,後者是對整個專案做鏈接

編譯單元:

  • 一個.c檔案是一個編譯單元
  • 編譯器每次編譯只處理一個編譯單元

7. 標頭檔案

  • 把函數原型放到一個頭檔案(以.h結尾)中,在需要呼叫整個函數的原始碼檔案(.c檔案)中#include這個標頭檔案,就能讓編譯器在編譯的時候知道函數的原型

  • 其他c檔案使用這個c檔案的時候,呼叫它的h檔案,是爲了告訴其他c檔案原來它的函數原型是這樣的

  • 這個c檔案呼叫自己的h檔案是爲了檢查函數型別是否一致

  • 標頭檔案是橋樑,是合同

#include:

  • #include是一個編譯預處理指令,和宏一樣,在編譯之前就處理了
  • 它把那個檔案的全部文字內容原封不動地插入到它所在的地方
    • 所以也不是一定要在.c檔案的最前面#include

""還是<>:

  • #include有兩種形式來指出要插入的檔案
    • ""要求編譯器首先在當前目錄(.c檔案所在的目錄)尋找這個檔案,如果沒有,到編譯器指定的目錄去找
    • <>讓編譯器只在指定的目錄去找
  • 編譯器自己知道自己的標準庫的標頭檔案在哪裏
  • 環境變數和編譯器命令列參數也可以指定尋找標頭檔案的目錄

#include的誤區

  • #include不是用來引入庫的
  • stdio.h裡只有printf的原型,printf的程式碼在另外的地方,某個.lib(Windows)或.a(Unix)中
  • 現在的C語言編譯器預設會引入所有的標準庫
  • #include <stdio.h>只是爲了讓編譯器知道printf函數的原型,保證你呼叫時給出的參數值是正確的型別

標頭檔案:

  • 在使用和定義這個函數的地方都應該知道#include這個標頭檔案
  • 一般的做法就是任何.c都有對應的同名的.h,把所有對外公開的函數的原型和全域性變數的宣告都放進去

不對外公開的函數:

  • 在函數前面加上static就使得它成爲只能在所在的編譯單元中被使用的函數
  • 在全域性變數前面加上static就使得它成爲只能在所在的編譯單元中被使用的全域性變數

變數的宣告:

  • int i;是變數的定義
  • extern int i;是變數的宣告,宣告不初始化

宣告和定義:

  • 宣告是不產生程式碼的東西

    • 函數原型
    • 變數宣告
    • 結構宣告
    • 宏宣告
    • 列舉宣告
    • 型別宣告
    • inline宣告
  • 定義是產生程式碼的東西

  • 以上的都是宣告,因爲他不產生程式碼,只是告訴你有這個東西

標頭檔案:

  • 只有宣告可以被放在標頭檔案中
    • 是規則不是法律
  • 否則會造成一個專案中多個編譯單元裡有重名的實體
    • *某些編譯器允許幾個編譯單元中存在同名的函數,或者用weak修飾符來強調這種存在

重複宣告:

  • 同一個編譯單元裡,同名的結構不能被重複宣告
  • 如果你的標頭檔案裡有結構的宣告,很難這個標頭檔案不會在一個編譯單元裡被#include多次
  • 所以需要」標準標頭檔案結構「

標準標頭檔案結構:

  • 運用條件編譯和宏,保證這個標頭檔案在一個編譯單元中只會被#include一次
  • #progma once也能起到相同的作用,但是不是所以的編譯器都支援
#ifndef __LIST_HEAD__
#define __LIST_HEAD__

#include "node.h"

typedef struct _list {
	Node* head;
	Node* tail;
} List;

#endif


Thirteenth Week 檔案


1. 格式化的輸入輸出

  • printf

    • %[flags][width][.prec][hIL]type
      

Flags 含義
- 左對齊
+ 在前面放+或-
(space) 正數留空
0 0填充
width或.prec(width指整個輸出佔的位數) 含義(用於格式靈活性)
number 最小字元數
* 下一個參數是字元數
.number 小數點後的位數
.* 下一個參數是小數點後的位數
#include <stdio.h>

int main(int argcm char const *argv[]) {
	printf("%*d\n", 6, 123);  //    123
	printf("%9.2f\n", 123.0);  //    123.00
	
	return 0;
}

型別修飾(hIL,修飾type) 含義
hh 單個位元組
h short
l long
ll long long
L long double
type 用於 type 用於
i或d int g float
u unsigned int G float
o 八進制 a或A 十六進制浮點
x 十六進制 c char
X 字母大寫的十六進制 s 字串
f或F float,6 p 指針
e或E 指數 n 讀入/寫出的個數
#include <stdio.h>

int main(int argc, char const *argv[]) {
	int num;
	printf("%dty%n\n", 12345, &num);  // 到%n之前已經輸出了多少個字元;12345ty
	printf("%d\n", num);  // 7
	
	return 0;
})

  • scanf

    • %[flag]type
      
flag 含義 flag 含義
* 跳過 l long,double
數位 最大字元數 ll long long
hh char L long double
h short
type 用於 type 用於
d int s 字串(單詞)
i 整數,可能爲十六進制或八進制 […] 所允許的字元
u unsigned int p 指針
o 八進制
x 十六進制
a,e,f,g float
c char

[^.]:到逗號之前的所有東西

//$GPRMC,004319.00,A,3016.98468,N,12006.39211,E,0.047,,130909,,,D*79
// 上方是GPS模組會產生的1083協定的數據

scanf("%*[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,]", sTime, sAV, sLati, &sNW, sLong, &sEW, sSpeed, sAngle, sDate);  // 讀取上面的數據,第一個是逗號之前的所有東西都跳過不要

printf和scanf的返回值:

  • 讀入的專案數,讀入的變數
  • 輸出的字元數
  • 在要求嚴格的程式中,應該判斷每次呼叫scanf或printf的返回值,從而瞭解程式執行中是否存在問題

2. 檔案的輸入輸出

用>和<做重定向

FILE:

  • FILE* fopen(const char *restrict path, const char *restrict mode);
    
    int fclose(FILE *stream);
    
    fscanf(FILE*, ...);
    
    fprintf(FILE*, ...);
    

開啓檔案的標準程式碼:

FILE *fp = fopen("file", "r");
if (fp) {
    fscanf(fp, ...);
    fclose(fp);
} else {
    ...
}
#include <stdio.h>

int main(int argc, char const *argv[]) {
    FILE *fp = fopen("12.in", "r");
    if (fp) {
        int num;
        fscanf(fp, "%d", &num);
        printf("%d\n", num);
        fclose(fp);
    } else {
        printf("無法開啓檔案!\n");
    }
    
    return 0;
}

fopen:

mode 功能
r 開啓只讀
r+ 開啓讀寫,從檔案頭開始
w 開啓只寫。如果不存在則新建,如果存在則清空
w+ 開啓讀寫。如果不存在則新建,如果存在則清空
a 開啓追加。如果不存在則新建,如果存在則從檔案尾開始
…x 只新建,如果檔案已存在則不能開啓
  • …x表示在後面可以再加x,比如wx,ax

3. 二進制檔案

  • 其實所有的檔案最終都是二進制的
  • 文字檔案無非是用最簡單的方式可以讀寫的檔案
    • more、tail
    • cat
    • vi
  • 而二進制檔案是需要專門的程式來讀寫的檔案
  • 文字檔案的輸入輸出是格式化,可能經過轉碼

文字vs二進制:

  • Unix喜歡用文字檔案來做數據儲存和程式設定
    • 互動式終端的出現使得人們喜歡用文字和計算機「talk」
    • Unix的shell提供了一些讀寫文字的小程式
  • Windows喜歡用二進制檔案
    • DOS是草根文化,並不繼承和熟悉Unix文化
    • PC剛開始的時候能力有限,DOS的能力更有限二進制更接近底層

  • 文字的優勢是方便人類讀寫,而且跨平臺
  • 文字的缺點是程式輸入輸出要經過格式化,開銷大
  • 二進制的缺點是人類讀寫困難,而且不跨平臺
    • int的大小不一致,大小端的問題
  • 二進制的優點是程式讀寫塊

程式爲什麼要檔案:

  • 設定
    • Unix用文字,Windows用註冊表
  • 數據
    • 稍微有點量的數據都放數據庫了
  • 媒體
    • 這個只能是二進制的
  • 現實是,程式通過第三方庫來讀寫檔案,很少直接讀寫二進制檔案了

二進制讀寫:

  • size_t fread(void *restrict ptr, size_t size, size_t nitems, FILE *restrict stream);
    
    size_t fwrite(const void *restrict ptr, size_t size, size_t nitems, FILE *restrict stream);
    
  • 注意FILE指針是最後一個參數

  • 返回的是成功讀寫的位元組數

爲什麼nitem?

  • 因爲二進制檔案的讀寫一般都是通過對一個結構變數的操作來進行的
  • 於是nitem就是用來說明這次讀寫幾個結構變數!
// student.h
#ifndef _STUDENT_H_
#define _STUDENT_H_

const int STR_LEN = 20;

typedef struct _student {
    char name[STR_LEN];
    int gender;
    int age;
} Student;

#endif
#include <stdio.h>
#include "student.h"

void getList(Student aStu[], int number);
int save(Student aStu[]. int number);

int main(int argc, char const *argv[]) {
    int number = 0;
    printf("輸入學生數量:");
    scanf("%d", &number);
    Student aStu[number];
    
    getList(aStu, number);
    if (save(aStu, number)) {
        printf("儲存成功\n");
    } else {
        printf("儲存失敗\n");
    }
    
    return 0;
}

void getList(Student aStu[], int number) {
    char format[STR_LEN];
    sprintf(format, "%%%ds", STR_LEN - 1);  // 產生格式字串,%%指%
    // "%19s", 20-1是因爲字串陣列最後留一個"\0"
    
    int i;
    for (i=0; i<number; i++) {
        printf("第%d個學生:\n", i);
        printf("\t姓名:");
        scanf(format, aStu[i].name);
        printf("\t性別(0-男,1-女,2-其他):");
        scanf("%d", &aStu[i].gender);
        printf("\t年齡:");
        scanf("%d", &aStu[i].age);
    }
}

int save(Student aStu[], int number) {
    int ret = -1;
    FILE *fp = fopen("Student.data", "w");
    if (fp) {
        ret = fwrite(aStu, sizeof(Student), number, fp);
        fclose(fp);
    }
    
    return ret == number;
}

在檔案中定位:

  • long ftell(FILE *stream);
    
  • int fseek(FILE *stream, long offset, int whence);
    // whence的參數:
    // SEEK_SET:從頭開始
    // SEEK_CUR:從當前位置開始
    // SEEK_END:從尾開始(倒過來)
    
#include <stdio.h>
#include "student.h"

void read(FILE *fp, int index);

int main(int argc, char const *argv[]) {
    FILE *fp = fopen("student.data", "r");
    if (fp) {
        fseek(fp, 0L, SEEK_END);
        long size = ftell(fp);  // 返回當前的位置,其實在這裏就是指檔案的大小
        int number = size / sizeof(Student);
        int index = 0;
        printf("有%d個數據, 你要看第幾個:", number);
        scanf("%d", &index);
        read(fp, index - 1);
        fclose(fp);
    }
    
    return 0;
}

void read(FILE *fp, int index) {
    fseek(fp, index*sizeof(Student), SEEK_SET);
    Student stu;
    if (fread(&stu, sizeof(Student), 1, fp) == 1) {
        printf("第%d個學生:\n", index + 1);
        printf("\t姓名:%s\n", stu.name);
        printf("\t性別:");
        switch (stu.gender) {
            case 0: printf("男\n"); break;
            case 1: printf("女\n"); break;
            case 2: printf("其他\n"); break;
        }
        printf("\t年齡:%d\n", stu.age);
    }
}

可移植性:

  • 這樣的二進制檔案不具有可移植性
    • 在int爲32位元的機器上寫成的數據檔案無法直接在int爲64位元的機器上正確讀出
  • 解決方案之一是放棄使用int,而是typedef具有明確大小的型別
  • 更好的方案是用文字

4. 位運算

按位元運算:

  • c有這些按位元運算的運算子:
&  // 按位元的與
|  // 按位元的或
~  // 按位元取反
^  // 按位元的互斥或
<<  // 左移
>>  // 右移

按位元與&:

  • 如果(x)i==1(x)_i==1並且(y)i==1(y)_i==1,那麼(x & y)i=1(x\ \&\ y)_i=1
  • 否則的話(x&y)i=0(x\&y)_i=0
  • 按位元與常用於兩種應用:
    • 讓某一位或某些位爲0:x & 0xFE
    • 取一個數中的一段:x & 0xFF

按位元或|:

  • 如果(x)i==1(x)_i==1(y)i==1(y)_i==1,那麼(x  y)i=1(x\ |\ y)_i=1
  • 否則的話,(x  y)i==0(x\ |\ y)_i==0
  • 按位元或常用於兩種應用:
    • 使得一位或幾個位爲1:x | 0x01
    • 把兩個數拼起來:0x00FF | 0xFF00

按位元取反~:

  • ( x)i=1(x)i(~x)_i=1-(x)_i
  • 把1位變0,0位變1
  • 想得到全部位爲1的數:~0
  • 7的二進制是0111,x | 7使得低3位爲1,而
  • x & ~7,就使得低3位爲0

邏輯運算vs按位元運算:

  • 對於邏輯運算,它只看到兩個值:0和1
  • 可以認爲邏輯運算相當於把所有非0值都變成1,然後做按位元運算
    • 5 & 4 ——> 4 而 5 && 4 ——> 1 & 1 ——> 1
    • 5 | 4 ——> 5 而 5||4 ——> 1 | 1 ——> 1
    • ~4 ——> 3 而 !4 ——> !1 ——> 0

按位元互斥或^

  • 如果(x)i==(y)i(x)_i==(y)_i,那麼(x  y)i=0(x\ \land\ y)_i=0
  • 否則的話,(x  y)i=1(x\ \land\ y)_i=1
  • 如果兩個位相等,那麼結果爲0;不相等,結果爲1
  • 如果x和y相等,那麼x ^ y的結果爲0
  • 對一個變數用同一個值互斥或兩次,等於什麼也沒做
    • x ^ y ^ y ——> x

左移<<:

  • i<<ji<<j
  • i 中所有的位向左移動 j 個位置,而右邊填入0
  • 所有小於int的型別,移位以int的方式來做,結果是int
  • x<<=1x<<=1 等價於 x = 2x\ *=\ 2
  • x<<=nx<<=n 等價於 x = 2nx\ *=\ 2^n

  • 這裏的<<=<<=是指左移後給自己賦值,本身<<<<左移只是產生一個新值,而不改變原值
  • 往左移動多少位取決於你的int有多大

右移>>:

  • i>>ji>>j
  • i 中所有的位向右移 j 位
  • 所有小於int的型別,移位以int的方式來做,結果是int
  • 對於unsigned的型別,左邊填入0
  • 對於signed的型別,左邊填入原來的最高位(保持符號不變)
  • x>>=1x>>=1 等價於 x /= 2x\ /=\ 2
  • x>>=nx>>=n 等價於 x /= 2nx\ /=\ 2^n

  • 左移不管符號位

  • 移位的位移不要用負數,這是沒有定義的事情

  • C語言的負數

5. 位運算的應用

輸出一個數的二進制:

#include <stdio.h>

int main(int argc, char const argv[]) {
	int number;
	scanf("%d", &number);
	unsigned mask = 1u<<31;
	for (; mask; mask>>=1) {
		printf("%d", number & mask? 1 : 0);
	}
	printf("\n");
	
	return 0;
}

位元欄:控制多個bite

  • 把一個int的若幹位組合成一個結構
struct {
    unsigned int leading: 3;  // 冒號後面的數表示這個成員佔幾個bite
    unsigned int FLAG1: 1;
    unsigned int FLAG2: 1;
    int trailing: 11;
};
#include <stdio.h>

void prtBin(unsigned int number);

struct U0 {
    unsigned int leading: 3;
    unsigned int FLAG1: 1;
    unsigned int FLAG2: 1;
    int trailing: 27;
};

int main(int argc, char const *argv[]) {
    struct U0 uu;
    uu.leading = 2;
    uu.FLAG1 = 0;
    uu.FLAG2 = 1;
    uu.trailing = 0;
    printf("sizeof(uu)=%lu\n", sizeof(uu));
    prtBin(*(int*)&uu);  // 取得uu的地址然後把struct U0型別的指針強制轉換爲int型別指針,並取得地址所指向的值
    
    return 0;
}

void prtBin(unsigned int number) {
	unsigned mask = 1u<<31;
	for (; mask; mask>>=1) {
		printf("%d", number & mask? 1 : 0);
	}
	printf("\n");
}

位元欄:

  • 可以直接用位元欄的成員名稱來存取
    • 比移位、與、或還方便
  • 編譯器會安排其中的位的排列,不具有可移植性
  • 當所需的位超過一個int時會採用多個int

推薦:68道C/C++的面試題