最全C語言筆記篇 IV

2020-08-13 19:38:41

BACK:最全C語言筆記篇 III

Tenth Week 字串


1. 字串

  • 以0(整數0)結尾的一串字元
    • 0或’\0’是一樣的,但是和’0’不同
  • 0標誌字串的結束,但它不是字串的一部分
    • 計算字串長度的時候不包含這個0
  • 字串以陣列的形式存在,以陣列或指針的形式存取
    • 更多的是以指針的形式
  • string.h裡有很多處理字串的函數

字串變數:

char *str = "Hello";
char word[] = "Hello";
char line[10] = "Hello";

字串常數1:

  • 「Hello」
  • "Hello"會被編譯器變成一個字元陣列放在某處,這個陣列的長度是6,結尾還有表示結束的0
  • 兩個相鄰的字串常數會被自動連線起來(可以用反斜槓\來分割字串爲上下兩行!但是第二行的兩個tab也會列印出來,所以第二行需要頂到最前面!)

字串:

  • C語言的字串是以字元陣列的形態存在的
    • 不能用運算子對字串做運算
    • 通過陣列的方式可以遍歷字串
  • 唯一特殊的地方是字串字面量""可以用來初始化字元陣列
  • 以及標準庫提供了一系列字串函數

字串常數2:

char *s = "Hello, world!";
  • s是一個指針,初始化爲指向一個字串常數
    • 由於這個常數所在的地方,所以實際上s是const char *s,但是由於歷史的原因,編譯器接受不帶const的寫法
    • 但是試圖對s所指的字串做寫入會導致嚴重的後果
  • 如果需要修改字串,應該用陣列:
char s[] = "Hello, world!";

指針還是陣列?

char *str = "Hello";
char word[] = "Hello";
  • 陣列:這個字串在這裏,在一個固定的位置

    • 作爲本地變數空間自動被回收
  • 指針:這個字串不知道在哪裏

    • 處理參數
    • 動態分配空間
  • 如果要構造一個字串 ——> 陣列方式

  • 如果要處理一個字串 ——> 指針方式

char*是字串?

  • 字串可以表達爲char*的形式
  • char*不一定是字串
    • 本意是指向字元的指針,可能指向的是字元的陣列(就像int*一樣)
    • 只有它所指的字元陣列有結尾的0,才能 纔能說它所指的是字串

字串輸入輸出:

  • char string[8];
    scanf("%s", string);
    printf("%s", string);
    
  • scanf讀入一個單詞(到空格、tab或回車爲止)

  • scanf是不安全的,因爲不知道要讀入的內容的長度

安全的輸入:

  • char string[8];
    scanf("%7s", string);
    
  • 在%和s之間的數位表示最多允許讀入的字元的數量,這個數位應該比陣列的大小小一

    • 下一次scanf從哪裏開始?(交給下一個%或者scanf去閱讀!)

常見錯誤:

  • char *string;
    scanf("%s", string);
    
  • 誤以爲char*是字串型別,定義了一個字串型別的變數string就可以直接使用了,

    • 其實由於沒有對string初始化爲0,可能指向的地址無害,則所以不一定每次執行都出錯

空字串:

  • char buffer[100] = "";
    
    • 這是一個空的字串,buffer[0] = ‘\0’
  • char buffer[] = "";
    
    • 這個陣列的長度只有1!

字串陣列:寫一個數組表達很多個字串

  • char **a;  // 非字串陣列
    
    • a是一個指針,指向另一個指針,那個指針指向一個字元(串)
  • char a[][10]; 
    char *a[];  // 字串陣列的兩種表達形式:第一種每個字串有固定的長度,用陣列來表示;而第二種沒有固定的長度,因爲每個字串是使用指針來表示的。
    

程式參數:

  • int main(int argc, char const *argv[])
    
  • argc是argv的個數,argv[0]是命令本身,argv[0]後面的是命令列輸入的參數

    • 當使用Unix的符號鏈接時,反應符號鏈接的名字
#include <stdio.h>

int main(int argc, char const *argv[]) {
	int i; 
	for (i=0; i<argc; i++) {
		printf("%d:%s\n", i, argv[i]);
	}
	
	return 0;
}

2. 字串函數

putchar:

  • int putcahr(int c);
    
  • 向標準輸出寫一個字元

  • 返回寫了幾個字元,EOF(-1)表示寫失敗

getchar:

  • int getchar(void);
    
  • 從標準輸入讀入一個字元

  • 返回型別是int是爲了返回EOF(-1)

    • Windows ——> Ctrl-Z
    • Unix ——> Ctrl-D
#include <stdio.h>

int main(int argc, char const *argv[]) {
	int ch;
	
	while ((ch=getcahr()) != EOF) {
		putchar(ch);
	}
	
	return 0;
}

Crtl+C強制結束

Crtl+Z返回EOF(windows)

string.h:

  • strlen:返回字串長度
  • strcmp:比較兩個字串
  • strcpy:字串拷貝
  • strcat:字串相接
  • strchr:字串中找字元
  • strstr:字串中找字串

strlen:

  • size_t strlen(cosnt char *s);
    
  • 返回s的字串長度(不包括結尾的0)

#include <stdio.h>
#include <string.h>

size_t mylen(const char *s) {
	int idx = 0;
	while (s[idx] != '\0') {
		idx++;
	}
	return idx;
}

int main(int argc, char const *argv[]) {
	char line[] = "Hello";
    printf("strlen=%lu\n", mylen(line));
    printf("sizeof=%lu\n", sizeof(line));
    
    return 0;
}

strcmp:

  • int strcmp(const char *s1, const char *s2);
    
  • 比較兩個字串,根據ascii碼從左到右依次比較,返回:

    • 0:s1 == s2
    • 1:s1 > s2
    • -1:s1 < s2
#include <stdio.h>
#include <string.h>

int mycmp(const char *s1, const char *s2) {
	// int idx = 0;
	// while (s1[idx]==s2[idx] && s1[idx]!='\0') {
    // 	 idx++;
	// }
	while (*s1==*s2 && s1!='\0') {
		s1++;
		s2++;
	}
	return *s1 - *s2
}

int main(int argc, char const *argv[]) {
	char s1[] = "abc";
	char s2[] = "abc";
	printf("%d\n", mycmp(s1, s2));
	printf("%d\n", 'a'-'A');
	
	return 0;
}

strcpy:

  • char* strcpy(char *restrict dst, const char *restrict src);
    
  • 把src的字串拷貝到dst

    • restrict表明src和dst不重疊,爲了多核效能(c99纔有)
  • 返回dst

    • 爲了能鏈起程式碼來

複製一個字串:

char *dst = (char*)malloc(strlen(src)+1);
strcpy(dst, src);
#include <stdio.h>
#include <string.h>

char* mycpy(char *dst, const cahr *src) {
    // int idx = 0;
    // while (src[idx]) {
    //     dst[idx] = src[idx];
    //     idx++;
    // }
    // dst[idx] = '\0';
    char *ret = dst;
    while (*dst++ == *src++);
    *dst = '\0';
    
    return 0;
}

int main(int argc, char const *argv[]) {
    char s1[] = "abc";
    char s1[] = "abc";
    strcpy(s1, s2);
    
    return 0;
}

strcat:

  • char* strcat(char *restrict s1, const char *restrict s2);
    
  • 把s2拷貝到s1的後面,接成一個長的字串,會把s1最後的’\0’給覆蓋掉

  • 返回s1

  • s1必須具有足夠的空間

安全版本:目的地沒有足夠的空間

  • char* strncpy(char *restrict dst, const char *restrict src, size_t n);
    
  • char* strncat(char *restrict s1, const char *restrict s2, size_t n);
    
  • int strncpm(const char *s1, const char *s2, size_t n);  // 此函數的作用不是爲了安全,是爲了決定比較前n個的字元
    

字串中找字元:

  • char* strchr(const char *s, int c);  // 返回找到的字元及其後面的字串
    
  • char* strrchr(const char *s, int c);  // 從右開始找
    
  • 返回NULL表示沒有找到

  • 如何尋找第二個?

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char const *argv[]) {
	char s[] = "hello";
    char *p = strchr(s, 'l');
    char c = *p;
    *p = '\0';  // 返回字元l之前的字串
    char *t = (char*)malloc(strlen(s)+1);
    strcpy(t, s);  // 把選定的字元的字串複製到另一個變數
    printf("%s\n", t);
    free(t);
    
    return 0;
}

字串中找字串:

  • char* strstr(const char *s1, const char *s2);
    
  • char* strcasestr(const char *s1, const char *s2);  // 忽略大小寫
    


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;
}

BACK:最全C語言筆記篇 V