最全C語言筆記篇 V

2020-08-13 19:43:56

BACK:最全C語言筆記篇 IV

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++的面試題