考研:C語言複習筆記 [Hex Note]

畏新發表於2020-12-09

C語言複習筆記

C語言編譯環境與發展史

C語言是一門編譯型的程式語言,原始碼要經過編譯器編譯成彙編程式碼,彙編程式碼經過彙編成機器碼之後才能被機器執行。編譯器(如下圖gcc)編譯包括四步:1.預處理(Preprocessing), 2.編譯(Compilation), 3.彙編(Assemble), 4.連結(Linking),下面這張圖已經夠詳細了。

在這裡插入圖片描述

C語言在不同環境下的編譯方式有很大不同,一大堆名詞可能會看得雲裡霧裡,所以對下列名詞進行解釋。在類Unix系統(Linux發行版、macOS)上主流的兩個編譯器,一個是gcc,一個是Clang。gcc是屬於GNU(免費UNIX軟體集合)專案的編譯器,Clang是LLVM專案的編譯器。在Windows上,微軟的整合開發環境(Visual Studio)上自帶了C語言的編譯器;或者選擇在Widnows上模擬Linux的編譯環境,可以選擇MinGW或Cygwin。

MinGW下載地址:https://sourceforge.net/projects/mingw/

根據CPU的指令集不同,可以分為複雜指令集和簡單指令集,複雜指令集的代表是Intel,簡單指令集的代表是ARM。如果要在Intel CPU的機器上編譯能夠執行在ARM架構CPU的機器上,還需要用到交叉編譯工具鏈。做嵌入式的C語言開發離不開這個。

# 使用gcc編譯器編譯hello.c原始碼
# 編譯完成後會生成一個hello的可執行檔案
gcc hello.c -o hello

C語言主要的三個標準:C89(C90)、C99、C11,語言標準的命名是標準的公佈年份。

  • C90是第一個C語言標準,其內容和ANSI C保持一致。
  • C99是第二個C語言標準。在C90的基礎上增加了:1.國際化程式設計;2.解決明顯的缺陷;3.適應科學和工程專案中的關鍵數值計算。
    • 增加了對編譯器的限制,比如源程式每行要求至少支援到 4095 位元組,變數名函式名的要求支援到 63 位元組(extern 要求支援到 31)。
    • 增強了預處理功能。例如:
    • 巨集支援取可變引數 #define Macro(…) __VA_ARGS__
    • 使用巨集的時候,允許省略引數,被省略的引數會被擴充套件成空串。
    • 支援 // 開頭的單行註釋(這個特性實際上在C89的很多編譯器上已經被支援了)
    • 增加了新關鍵字 restrict, inline, _Complex, _Imaginary, _Bool
    • 支援 long long, long double _Complex, float _Complex 等型別
    • 支援不定長的陣列,即陣列長度可以在執行時決定,比如利用變數作為陣列長度。宣告時使用 int a[var] 的形式。不過考慮到效率和實現,不定長陣列不能用在全域性,或 struct 與 union 裡。
    • 變數宣告不必放在語句塊的開頭,for 語句提倡寫成 for(int i=0;i<100;++i) 的形式,即i 只在 for 語句塊內部有效。
    • 允許採用(type_name){xx,xx,xx} 類似於 C++ 的建構函式的形式構造匿名的結構體。
    • 複合字面量:初始化結構的時候允許對特定的元素賦值,形式為:
    • struct test{int a[3],b;} foo[] = { [0].a = {1}, [1].a = 2 };
    • struct test{int a, b, c, d;} foo = { .a = 1, .c = 3, 4, .b = 5 }; // 3,4 是對 .c,.d 賦值的
    • 格式化字串中,利用 \u 支援 unicode 的字元。
    • 支援 16 進位制的浮點數的描述。
    • printf scanf 的格式化串增加了對 long long int 型別的支援。
    • 浮點數的內部資料描述支援了新標準,可以使用 #pragma 編譯器指令指定。
    • 除了已有的 __line__ __file__ 以外,增加了 __func__ 得到當前的函式名。
    • 允許編譯器化簡非常數的表示式。
    • 修改了 /% 處理負數時的定義,這樣可以給出明確的結果,例如在C89中-22 / 7 = -3, -22% 7 = -1,也可以-22 / 7= -4, -22% 7 = 6。 而C99中明確為 -22 / 7 = -3, -22% 7 = -1,只有一種結果。
    • 取消了函式返回型別預設為 int 的規定。
    • 允許 struct 定義的最後一個陣列不指定其長度,寫做 [](flexible array member)。
    • const const int i 將被當作 const int i 處理。
    • 增加和修改了一些標準標頭檔案,比如定義 bool 的 <stdbool.h> ,定義一些標準長度的 int 的 <inttypes.h> ,定義複數的 <complex.h> ,定義寬字元的 <wctype.h> ,類似於泛型的數學函式 <tgmath.h>, 浮點數相關的 <fenv.h>。 在<stdarg.h> 增加了 va_copy 用於複製 … 的引數。裡增加了 struct tmx ,對 struct tm 做了擴充套件。
    • 輸入輸出對寬字元以及長整數等做了相應的支援。
  • C11是最近的一次C語言標準。主要是提高了對C++的相容性。
    • 對齊處理(Alignment)的標準化(包括_Alignas標誌符,alignof運算子, aligned_alloc函式以及<stdalign.h>標頭檔案。
    • Noreturn 函式標記,類似於 gcc 的 __attribute_((noreturn))。
    • _Generic 關鍵字。
    • 多執行緒(Multithreading)支援,包括:
    • _Thread_local儲存型別識別符號,<threads.h>標頭檔案,裡面包含了執行緒的建立和管理函式。
    • _Atomic型別修飾符和<stdatomic.h>標頭檔案。
    • 增強的Unicode的支援。基於C Unicode技術報告ISO/IEC TR 19769:2004,增強了對Unicode的支援。包括為UTF-16/UTF-32編碼增加了char16_t和char32_t資料型別,提供了包含unicode字串轉換函式的標頭檔案<uchar.h>.
    • 刪除了 gets() 函式,使用一個新的更安全的函式gets_s()替代。
    • 增加了邊界檢查函式介面,定義了新的安全的函式,例如 fopen_s(),strcat_s() 等等。
    • 增加了更多浮點處理巨集。
    • 匿名結構體/聯合體支援。這個在gcc早已存在,C11將其引入標準。
    • 靜態斷言(static assertions),_Static_assert(),在解釋 #if 和 #error 之後被處理。
    • 新的 fopen() 模式,(“…x”)。類似 POSIX 中的 O_CREAT|O_EXCL,在檔案鎖中比較常用。
    • 新增 quick_exit() 函式作為第三種終止程式的方式。當 exit()失敗時可以做最少的清理工作。

資料型別

基本資料型別:short、int、long、char、float、double。

注:在不同字長的機器上,資料型別佔據的位元組長度以實際為準。比如在32位機器上int佔據的記憶體大小是4 個byte;double佔據的記憶體大小是8 個byte;

基本資料型別的轉換隻能從高精度到低精度:
在這裡插入圖片描述

另外,在<limit.h>中規定了整型的上下限值。

型別有符號最小值有符號最大值無符號最大值
字元SCHAR_MINSCHAR_MAXUCHAR_MAX
短整型SHRT_MINSHRT_MAXUSHRT_MAX
整型INT_MININT_MAXUINT_MAX
長整型LONG_MINLONG_MAXULONG_MAX

在<float.h>中規定了浮點數的上下限值。

型別最小值最大值
floatFLT_MINFLT_MAX
doubleDBL_MINDBL_MAX
long doubleLDBL_MINLDBL_MAX

列舉型別

//定義列舉型別
enum WEEKDAY {
    MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY, SATURDAY,
    SUNDAY
};
enum SHORT_WEEKDAY {
    MON = 1, TUE = 2, WED = 3,
    THU = 4, FRI = 5, SAT = 6,
    SUN = 0
};
printf("%d\n", MONDAY); //0
printf("%d\n", sizeof(MONDAY)); //4
enum WEEKDAY mon = MONDAY; //定義列舉變數

定義型別別名:typedef關鍵字

typedef int IntegerNumber;
IntegerNumber a;

控制流和函式

  • if-else
  • while
  • do-while
  • for
  • switch-case-default
  • break
  • continue
  • goto

switch

#include <stdio.h>
int main() {
    enum {
        SUN, MON, TUE, WED, THU, FRI, STA
    };
    int input;
    while (1) {
        printf("input=");
        scanf("%d", &input);
        switch (input) {
            case 0:
                printf("Today is SUN.\n");
                break;
            case 1:
                printf("Today is MON.\n");
                break;
            case 2:
                printf("Today is TUE.\n");
                break;
            case 3:
                printf("Today is WED.\n");
                break;
            case 4:
                printf("Today is THU.\n");
                break;
            case 5:
                printf("Today is FRI.\n");
                break;
            case 6:
                printf("Today is STA.\n");
                break;
            default:
                printf("WRONG INPUT\n");
                break;
        }
    }
    return 0;
}

goto

#include <stdio.h>
int main() {
    int number;
    outer:
    printf("input a number:");
    scanf("%d", &number);
    if (number < 0) {
        printf("the input number can't be negative\n");
        goto outer;
    }
    printf("input success:%d\n", number);
    return 0;
}

可變長引數列表

#include <stdio.h>
#include <stdarg.h>
int calc_sum_of_numbers(int n, ...) {
    int sum = 0;
    va_list var_arg;//宣告結構體物件
    va_start(var_arg, n);//初始化引數指標,分配記憶體
    for (int i = 0; i < n; ++i) {//指標往後移動多少位需要傳參
        sum += va_arg(var_arg, int);//通過指標間接訪問,並向後移動一位
    }
    va_end(var_arg);//銷燬指標,釋放記憶體
    return sum;
}
int main() {
    int res = calc_sum_of_numbers(3, 1, 2, 3);
    printf("res=%d\n", res);
    return 0;
}

操作符和表示式

  • 算術操作符:算數運算 + - * / %
  • 移位操作符:進行二進位制移位操作 << 左移直接丟棄 >> 右移分為邏輯右移(補0)和算數右移(符號不變)
  • 位操作符:進行二進位制位運算 AND(按位與,&,相同為1)、OR(按位或,|,有1為1)、XOR(按位異或,^,不同為1)
  • 賦值 = 從右到左 可以和其他操作符結合
  • 單目操作符:只作用於一個變數 ! ++ - & sizeof ~ – + * (型別)?)表示強轉;a++和++a的區別
  • 關係操作符:> >= < <= != ==
  • 邏輯操作符:&& ||
  • 條件操作符:表示式?表示式為真:表示式為假
  • 逗號操作符:用逗號分隔多個表示式,最後一個表示式的結果就是整體的值
  • 下標引用、函式呼叫、結構成員:array[下標]、*(array+下標);.用來訪問結構體的成員,->用指標操作符操作指標指向的物件(此物件非彼物件);
  • 布林值:0為假,非0為真
  • 左值和右值(L-Value、R-Value):左值只能是標識某個特定位置的值

位運算

#include <stdio.h>
int main() {
    // 13 = 00001101
    // 21 = 00010101
    int a = 13, b = 21;
    printf("%d\n", a & b); // 5 = 00000101
    printf("%d\n", a | b); // 29 = 00011101
    printf("%d\n", a ^ b); // 24 = 00011000
    return 0;
}

移位運算

#include <stdio.h>
int main() {
    printf("sizeof(int)=%d\n", sizeof(int)); //4Byte = 32bit
    int num1 = 24;
    int num2 = -24;
    printf("24 = 00011000\n");
    printf("24 << 2 = %d = 01100000\n", num1 << 2);
    printf("24 >> 2 = %d = 00000110\n", num1 >> 2);
    printf("-24 << 2 = %d\n", num2 << 2);
    printf("-24 >> 2 = %d\n", num2 >> 2);
    return 0;
}

結果:從結果上看移位並沒有收到正負號的影響,僅僅是對數字的二進位制串進行移位操作。

sizeof(int)=4
24 = 00011000
24 << 2 = 96 = 01100000
24 >> 2 = 6 = 00000110
-24 << 2 = -96
-24 >> 2 = -6

操作符優先順序

在這裡插入圖片描述
在這裡插入圖片描述

基本輸入輸出

格式化輸出

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

  • flag
    • 不填或+ 輸出的內容右對齊
    • - 輸出的內容左對齊
  • width 控制輸出內容的寬度
  • prec 如果輸出的內容是浮點數,它用於控制輸出內容的精度(四捨五入)
  • type
    • %hd、%d、%ld 以十進位制、有符號的形式輸出 short、int、long 型別的整數。
    • %hu、%u、%lu 以十進位制、無符號的形式輸出 short、int、long 型別的整數
    • %c 輸出字元。
    • %lf 以普通方式輸出double(float棄用,long doube無用)。
    • %e 以科學計數法輸出double。
    • %s 輸出字串。

輸入/輸出

  • scanf():控制格式的輸入
  • getchar():輸入單個字元
  • gets():獲取一行內容,作為字串處理
  • putchar():輸出單個字元
  • puts():輸出一行內容

示例:

#include <stdio.h>
int main() {
    int n;
    printf("input a number:");
    scanf("%d", &n);
    printf("n=%d\n", n);
    fflush(stdin); //這裡一定要重新整理緩衝區,不然回車就會被視作第二個輸入

    printf("input a char:");
    char ch = getchar();
    printf("ch=%c\n", ch);
    fflush(stdin);

    printf("input a string:");
    char *s;
    gets(s);
    printf("s=%s\n", s);
    fflush(stdin);

    printf("output a char:");
    putchar('A');
    printf("\n");

    printf("output a string:");
    puts("Hello world!\n");
    return 0;
}

緩衝區

緩衝區的意義:緩衝主要是為了解決IO裝置和CPU之間速度不匹配的問題。為了避免頻繁佔用CPU,先把資料讀取到緩衝,IO從緩衝去取資料。

在這裡插入圖片描述

以下四種情況會重新整理緩衝區,進行真正的IO操作。

  • 緩衝區滿時;
  • 執行flush語句;
  • 執行endl語句;
  • 關閉檔案。

附上一張ASCII碼錶:
在這裡插入圖片描述

指標與陣列

指標

  1. * 取指標的內容
  2. & 取變數的地址
  3. 要確保指標被初始化,然後再使用。至少要賦個NULL,不至於出現野指標
  4. NULL在C語言裡面是0,對空指標進行間接訪問,會報錯(msvc)

常量指標、指標常量、指向常量的指標常量

#include <stdio.h>
int main() {
    /*
    常量指標:指向一個常量的指標
    int const *p;
    const int *q;
    */
    int a = 1, b = 2, c = 3, d = 4, e = 5;
    int const *pta = &a;
    int *ptb = &b;
    printf("*pta=%d\t*ptb=%d\ta=%d\tb=%d\n", *pta, *ptb, a, b);

    /* 報錯:常量指標指向的記憶體不允許通過指標修改 */
    // *pta = c;
    *ptb = c;
    printf("*pta=%d\t*ptb=%d\ta=%d\tb=%d\n", *pta, *ptb, a, b);

    /* 可以修改指標的指向 */
    pta = &d;
    ptb = &d;
    printf("*pta=%d\t*ptb=%d\ta=%d\tb=%d\n", *pta, *ptb, a, b);

    /* 可以通過變數自身修改值,常量指標只是限制了通過指標修改 */
    a = e;
    b = e;
    printf("*pta=%d\t*ptb=%d\ta=%d\tb=%d\n", *pta, *ptb, a, b);
    return 0;
}
#include <stdio.h>
int main() {
    /*
    指標常量:本質是一個常量,常量的值是一個指標
    int * const pt;
    */

    int a = 1, b = 2, c = 3;
    int *const pt = &a;
    printf("*pt=%d\ta=%d\tb=%d\n", *pt, a, b);

    /* 報錯:常量不能被賦值 */
    // pt = &b;
    printf("*pt=%d\ta=%d\tb=%d\n", *pt, a, b);

    /* 可以通過指標修改 */
    *pt = c;
    printf("*pt=%d\ta=%d\tb=%d\n", *pt, a, b);
    return 0;
}
// 綜合二者的特點,指標指向不可修改,指標指向的內容也不可修改
const int * const pt = &a;

const關鍵字和#define的區別:#define可以用在所有允許字面值常量的地方,const只能用於允許變數的地方。

函式指標、指標函式、回撥函式

#include <stdio.h>
int function(int a, int b){
    printf("a=%d,b=%d\n",a,b);
}
// 函式指標:指向函式的指標
int (*p)(int,int);
int main(){
    p = function;
    p(1,2);
    (*p)(1,2);
    return 0;
}
// 指標函式:返回值是指標的函式
List *addNode(List list);
// 回撥函式
#include <stdio.h>
void handle(void (*callback)(int, int), int n1, int n2) {
    callback(n1, n2);
}
void add(int n1, int n2) {
    printf("%d+%d=%d\n", n1, n2, n1 + n2);
}
void minus(int n1, int n2) {
    printf("%d-%d=%d\n", n1, n2, n1 - n2);
}
int main() {
    handle(add, 1, 2);
    handle(minus, 3, 4);
    return 0;
}

字串常量

其實就是在堆上開闢了一片記憶體,在棧上宣告一個指標,指向堆上記憶體。

#include <stdio.h>
int main(int argc, char **argv) {
    printf("%c\n",*("abcdefg"+2));
    printf("%c\n","abcdefg"[2]);
    return 0;
}

陣列

// 陣列的宣告
int odd_number_array[5] = {1, 3, 5, 7, 9};
int even_number_array[5] = {2, 4, 5};
int nature_number_array[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
char char_array[5] = {'h', 'e', 'l', 'l', 'o'};
char string[] = "hello world!";//字元陣列
char *s = "Hex Studio.";//字串常量
int matrix[2][3] = {{1,2,3},{4,5,6}};
#include <stdio.h>
int main() {
    int array[5] = {1, 3, 5, 7, 9};
    //因為陣列的第一個元素的地址即陣列的地址,所以可以通過指標運算元組中的元素
    printf("res=%d\n", *(array + 2));
    return 0;
}

陣列儲存的首地址雖然看上去和指標差不多,也可以首地址直接做類似指標的表示式計算,但是陣列仍然不是指標。指標在分配記憶體的時候,只會為指標本身分配記憶體,而陣列要分配一大塊記憶體,陣列名是一個常量,儲存了這塊記憶體的起始地址。

假設a是陣列名(常量),b是指標變數。*a是合法的,等於取陣列第一個元素的值,*b就不知道是什麼東西了。b++可以通過編譯,a++卻不行,因為b是指標變數,b++等於是指標往後移動一位,a是常量,常量不能自增。(a++可以理解為a = a+1,這裡a是常量,a就不能做左值,所以賦值不成立)

#include <stdio.h>
int main() {
    int array[10] = {100, 103, 105, 107, 109, 111, 113, 115, 117, 119};
    int *ap = array + 2;//指標指向105
    printf("res=%d\n", *(array + 2));//因為陣列的第一個元素的地址即陣列的地址,所以可以通過指標運算元組中的元素
    printf("ap=%d\n", ap);//105的地址
    printf("*ap=%d\n", *ap);//105
    printf("ap[0]=%d\n", ap[0]);//105
    printf("ap+6=%d\n", ap + 6);//117的地址
    printf("*ap+6=%d\n", *ap + 6);//105+6=111
    printf("*(ap+6)=%d\n", *(ap + 6));//117
    printf("ap[6]=%d\n", ap[6]);//117
    printf("&ap=%d\n", &ap);//ap的地址
    printf("ap[-1]=%d\n", ap[-1]);//103
    printf("ap[9]=%d\n", ap[9]);//?
    return 0;
}

從下面這個例子可以知道,變數是值傳遞,陣列是地址傳遞。
(把陣列名作為引數傳遞給函式,實際上傳遞的是指標。可以用指標來運算元組,如果想要限制原陣列的修改,可以在形參宣告加const關鍵字限制)

#include <stdio.h>
void init_array(int arg_arr[], int len) {
    printf("address[arg_arr]=%d\taddress[len]=%d\n", arg_arr, &len);
    for (int i = 0; i < len; ++i) {
        arg_arr[i] = 100;
    }
}
void print_array(int arg_arr[], int len) {
    printf("address[arg_arr]=%d\taddress[len]=%d\n", arg_arr, &len);
    for (int i = 0; i < len; ++i) {
        printf("arg_arr[%d]=%d\n", i, arg_arr[i]);
    }
}
int main() {
    int length = 10;
    int arrar[10];
    printf("address[array]=%d\taddress[length]=%d\n", arrar, &length);
    init_array(arrar, length);
    print_array(arrar, length);
    return 0;
}

字串

字串的結束符NUL,但是NUL不算進字串的長度。所謂的NUL就是\0。

字串相關函式原型:

  • 字串函式
    • size_t strlen(char const *string);返回的是一個無符號整數,無符號整數永遠大於0,不注意這個可能會出現非預期效果
    • char *strcpy(char *dst,char const *src);從一個字串複製到另一個字串
    • char *strcat(char *dst,char const *src);連線兩個字串
    • int strcmp(char const *s1,char const *s2);比較兩個字串,s1<s2,返回小於0的值,s1>s2,返回大於0的值
    • char *strncpy(char *dst,char const *src,size_t len);向dst寫入len個字元,如果src的長度大於len,可能拷貝過去的就沒有結束符,就會出現非預期效果
    • char *strncat(char *dst,char const *src,size_t len);
    • int strncmp(char const *s1,char const *s2,size_t len);
    • char *strchr(char const *str,int ch);查詢字元ch第一次出現的位置,不存在就返回NULL指標
    • char *strrchr(char const *str,int ch);查詢字元ch最後一次出現的位置,不存在就返回NULL指標
    • char *strpbrk(char const *str,char const *group);返回str中第一個匹配group中任意一個字元的指標,不存在就返回NULL指標
    • cahr *strstr(char const *s1,char const *s2);查詢s2在s1中第一次出現的位置,不存在就返回NULL指標,s2是空字串就返回s1
    • size_t strspn(char const *str,char const *group);返回str起始部分匹配group中任意字元的字元數
    • size_t strcspn(char const *str,char const *group);返回str起始部分不與group中任意字元的字元數
    • char *strtok(char *str,char const *sep);用分割不sep分割字串str,返回被分割出的片段的指標
    • char *strerror(int error_number);返回描述錯誤的字串。錯誤碼儲存在error.h中,建議把該檔案包含進來
  • 字元分類函式
    • iscntrl();任何控制字元
    • isspace();空白字元:空格’’、換頁’\f’、換行’\n’、回車’\r’、製表符’\t’、垂直製表符’\v’
    • isdigit();十進位制數字0~9
    • isxdigit();十六進位制數字,包括所有十進位制數字,小寫字母af,大寫字母AF
    • islower();小寫字母a~z
    • isupper();大寫字母A~Z
    • isalpha();字母az,AZ
    • isalnum();字母或數字,az,AZ,0~9
    • ispunct();標點符號,任何不屬於數字或字母的圖形字元
    • isgraph();任何圖形字元
    • isprint();任何可列印字元,包括圖形字元和空白字元
  • 字元轉換
    • int tolower(int ch);
    • int toupper(int ch);
  • 記憶體操作
    • void *memcpy(void *dst,void const *src,size_t length);從src的起始位置複製length個位元組到dst的記憶體起始位置
    • void *memmove(void *dst,void const *src,size_t length);memcpy複製時不允許源地址塊和目標地址塊有重疊,memmove允許,因為它先複製到臨時位置
    • void *memcmp(void const *a,void const *b,size_t length);對兩段記憶體的內容進行比較
    • void *memchr(void const *a,int ch,size_t length);從a的起始位置開始查詢字元ch第1次出現的位置,並返回一個指向該位置的指標
    • void *memset(void *a,int ch,size_t length);從a開始length個位元組都設定為字元ch
#include <stdio.h>
#include <string.h>

int main() {
    char *s1 = "Hello world!";
    char *s2 = "Hello world!\n";
    printf("len(s1)=%d\n", strlen(s1));
    printf("len(s2)=%d\n", strlen(s2));
    char s3[] = "Hello world!", s4[] = "Hex Studio.";
    char *s5 = "abcdefg", s6[10] = {0};
    printf("s4=%s\ts6=%s\n", s4, s6);
    strcpy(s4, s3);
    strcpy(s6, s5);
    //這裡要注意,不能把陣列複製給指標,因為指標沒有分配存放字串的記憶體
    //但是可以把指標複製給陣列,也就是上面的s5複製給s6,陣列是已經開闢了記憶體的
    //還有一點就是,複製的時候要保證接收者有足夠大的空間,不然資料還是會丟失,出現非預期效果
    printf("s4=%s\ts6=%s\n", s4, s6);
    char s7[10] = "He", *s8 = "Xin";
    //這裡和前面copy一樣,第一個引數不能傳指標,因為指標不能表示實際的一塊記憶體
    //而且拼接的時候要考慮第一個引數是否有足夠大的空間
    printf("name=%s\n", strcat(s7, s8));
    //strcpy和strcat返回的都是第一個引數的拷貝
    char *s9 = "He", *s10 = "Xin";
    printf("compare result:%d\n", strcmp(s9, s10));
    return 0;
}

結構體和聯合體

結構體

  1. .操作符用於 結構體物件操作結構體的變數
  2. ->操作符用於 結構體物件的指標操作結構體的變數
#include <stdio.h>
struct People {
    char *name;
    int age;
    bool gender;
} people = {"Hex", 24, true};
typedef struct Student {
    struct People people;
    char *student_number;
} Student;
int main() {
    // 這個例子裡面包括了結構體變數初始化、給結構體取別名等細節。
    Student student;
    student.people = people;
    student.student_number = "15190308";

    Student *ps = &student;
    printf("people.name=%s\n", people.name);
    printf("people.age=%d\n", people.age);
    printf("people.gender=%d\n", people.gender);
    printf("student.people=%d\n", student.people);
    printf("student.student_number=%s\n", student.student_number);
    printf("student.people=%d\n", student.people);
    printf("ps->people=%d\n", ps->people);
    printf("student.student_number=%d\n", student.student_number);
    printf("ps->student_number=%d\n", ps->student_number);
    return 0;
}

邊界對齊

為了提高資料存取的速度以及其他原因,存在記憶體對齊機制,即資料的首地址必須是k(通常是4或8)的整數倍。所以結構體的結構直接影響了結構體在記憶體上佔的大小。像下面這種情況,都是一個int,兩個char,變數定義的先後順序直接影響了結構體總的大小。圖1佔了8個位元組,圖2佔了12個位元組。

在這裡插入圖片描述

位段(不適用於需要移植的專案)

位段的成員只能是int、signed int、unsigned int。位段可以把奇數長度的資料包裝在一起,這樣能夠最大限度地利用儲存。

struct CHAR {
    unsigned ch :7;
    unsigned font:6;
    unsigned size :19;
};
struct CHAR ch1;

結構體CHAR的儲存如下圖所示,位段的記憶體分配不再依據記憶體對齊的原則。下圖黃色部分是變數ch,綠色部分是變數font,紅色部分是變數size。

在這裡插入圖片描述

位段的應用,在我有限的C語言開發經歷中沒遇到過。大多數軟體開發中已經不在乎這點記憶體的優化了,能做到記憶體對齊已經足矣。唯一一個我能想到的位段的應用就是計算機網路中的IP報文格式。(我覺得這麼設計的原因有兩點,一是設計網路協議的年代,記憶體吃緊;二是IP協議工作在網路層,大部分底層裝置本身的記憶體不大,只能將利用率最大化)。感覺知識都關聯起來了呢 ?

在這裡插入圖片描述

聯合體

聯合體:在不同時刻把不同的東西儲存到同一位置

//聯合體的宣告
union {
    float f;
    int i;
} fi;

//聯合體的初始化
union {
    int a;
    float b;
    char c[4];
} x = {5};

動態記憶體分配

函式原型:

void *malloc(size_t size);分配一整塊連續的記憶體
void free(void *pointer);釋放記憶體
void *calloc(size_t,num_elements,size_t element_size);返回記憶體的指標之前先初始化為0
void *realloc(void *ptr,size_t new size);修改已經分配的記憶體塊大小,擴大或縮小

free()函式做的只是釋放記憶體操作,使指標指向一個未知(隨機)的地方。記得要在free之後加上一步賦值NULL的操作,以免後面誤操作指標。(具體問題具體分析)

下面這個例子說明,只要有指標,就可以通過指標變數隨意操作(讀寫)。讀可能讀到隨機值,也就是所謂的“野指標”,其內容是無法預判的。寫就更離譜了,直接在某塊沒有規則的記憶體上寫值,可能會引起更大的問題,比如程式崩潰。一般情況下不建議隨意操作記憶體,使用指標時需要處處小心!!!

指標如此之靈活也不是完全沒有好處,在某些場景下需要往指定的記憶體上寫值,比如作業系統的入口。

#include <stdio.h>
#include <malloc.h>

int main() {
    int *a = (int *) malloc(sizeof(int));
    *a = 10;
    for (int i = 1; i <= 5; ++i) { //給指標後5個單位的記憶體賦值
        *(a + i) = *a + i;
    }
    printf("a=%d\n", *a);
    for (int i = 0; i < 20; i++) {
        printf("*(a+%d)=%d\n", i, *(a + i)); //列印從a往後20個單位的記憶體儲存的值
    }
    return 0;
}
/*

a=10
*(a+0)=10
*(a+1)=11
*(a+2)=12
*(a+3)=13
*(a+4)=14
*(a+5)=15
*(a+6)=-659373408
*(a+7)=8107
*(a+8)=1900880
*(a+9)=0
*(a+10)=1928272
*(a+11)=0
*(a+12)=1766677612
*(a+13)=1936683619
*(a+14)=1551132271
*(a+15)=1684957527
*(a+16)=1098086255
*(a+17)=997421168
*(a+18)=1547322171
*(a+19)=1919251285

*/

其他

常用庫函式

  • div_t div(int numerator,int denominator); 除法

  • int rand(void); 返回0~32767(RAND_MAX)的隨機數

  • int atoi(char const *string); 字串轉數字

  • long int atol(char const *string);

  • long int strtol(char const *string, char **unused, int base); 如果值太大且為負數,返回LONG_MIN。如果值太大且為正數,返回LONG_MAX

  • double atof(char const *string); 字元轉浮點數

  • double sin(double angle); 正弦

  • double cos(double angle); 餘弦

  • double tan(double angle); 正切

  • double exp(double x); 返回e的x次冪

  • double log(double x); 返回以e為底的對數

  • double log10(double x); 返回以10為底的對數

    • 在這裡插入圖片描述
  • double pow(double x,double y);

  • double sqrt(double x);

  • double floor(double x); 向下取整

  • double ceil(double x); 向上取整

  • int abs(int value); 返回絕對值

  • double fabs(double x); 絕對值

  • double fmod(double x,double y); 返回除法的餘數

  • check_t clock(void); 把這個值轉換為秒還要除以常量CLOCKS_PER_SEC

命令列引數

#include <stdio.h>
int main(int argc, char **argv) {
    // argc:命令列引數的個數
    // argv:類似一個指標陣列
    for (int i = 0; i < argc; ++i) {
        printf("%s\n", *(argv + i));
    }
    // 這兩種方式都可以,但是要注意第二種方式是移動指標,第一種是計算指標的位置
    while (*++argv != NULL) {
        printf("%s\n", *argv);
    }
    return 0;
}

預編譯

預定義符號

  • __FILE__ 進行編譯的原始檔名
  • __LINE__ 檔案當前行的行號
  • __DATE__ 檔案被編譯的日期
  • __TIME__ 檔案被編譯的時間
  • __STDC__ 如果編譯器遵循ANSI C,其值就為1,否則未定義
#include <stdio.h>
int main(int argc, char **argv) {
    printf("%s\n",__FILE__);
    printf("%d\n",__LINE__);
    printf("%s\n",__DATE__);
    printf("%s\n",__TIME__);
    printf("%d\n",__STDC__);
    return 0;
}

巨集

#define name stuff //巨集定義:字面值替換
#undef name //移除巨集定義

條件編譯

#if		//如果條件為真,則執行相應操作
#elif	//如果前面條件為假,而該條件為真,則執行相應操作
#else	//如果前面條件均為假,則執行相應操作
#endif	//結束相應的條件編譯指令
#ifdef	//如果該巨集已定義,則執行相應操作
#ifndef	//如果該巨集沒有定義,則執行相應操作
#include <stdio.h>
#define DEBUG 1
int main(int argc, char **argv) {
#if DEBUG
    printf("DEBUG ENV");
#else
    printf("DEVELOP ENV");
#endif
    return 0;
}

檔案包含

#include <filename> //庫函式檔案包含
#include "filename" //本地檔案包含

變數的儲存

變數根據宣告的位置和宣告識別符號分為:全域性變數、靜態全域性變數、區域性變數、靜態區域性變數、暫存器變數。全域性、區域性是根據變數宣告的位置區分的,register修飾的就是暫存器變數,static修飾的就是靜態變數。

記憶體上分為程式碼段(存放程式程式碼,不可修改)、資料段(.data段存放的是全域性變數和靜態變數;.bss段存放的未初始化的全域性變數和靜態變數)、堆疊段(堆是類似連結串列的線性結構;棧是後進先出結構,用於儲存區域性變數、函式形參、函式地址)。

連結屬性

  • external:全域性變數,非靜態函式
  • internal:靜態全域性變數,靜態函式
  • none:函式形參,函式內的區域性變數(也包括靜態區域性變數)

關於外部變數的一個示例:

// 源1.cpp
#include <stdio.h>
#include "another.h"
int main() {
    printf("var=%d\n",var);
    func();
    return 0;
}
// 源2.h
extern int var = 10;
extern void func();
// 源2.cpp
#include <stdio.h>
void func(){
	printf("Hello world!\n");
}

注:未被初始化的變數不能被引用!!!

相關文章