鍍金池/ 教程/ C/ 第一部分: 語言
第四部分: 工具
第二部分: 高級
第三部分: 系統(tǒng)
第一部分: 語言

第一部分: 語言

示例基于 GCC 32bit...

1. 數(shù)據(jù)類型

1.1 整數(shù)

以下是基本整數(shù)關鍵詞:

  • char: 有符號8位整數(shù)。
  • short: 有符號16位整數(shù)。
  • int: 有符號32位整數(shù)。
  • long: 在32位系統(tǒng)是32整數(shù) (long int),在64位系統(tǒng)則是64位整數(shù)。
  • long long: 有符號64位整數(shù) (long long int)。
  • bool: _Bool 類型,8位整數(shù),在 stdbool.h 中定義了 bool / true / false 宏便于使用。

由于在不同系統(tǒng)上 char 可能代表有符號或無符號8位整數(shù),因此建議使用 unsigned char /signed char 來表示具體的類型。

在 stdint.h 中定義了一些看上去更明確的整數(shù)類型。

typedef signed char int8_t;
typedef short int int16_t;
typedef int int32_t;

typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
typedef unsigned int uint32_t;

#if __WORDSIZE == 64
    typedef long int int64_t;
    typedef unsigned long int uint64_t;
#else
    __extension__
    typedef long long int int64_t;
    typedef unsigned long long int uint64_t;
#endif

還有各種整數(shù)類型的大小限制。

# define INT8_MIN (-128)
# define INT16_MIN (-32767-1)
# define INT32_MIN (-2147483647-1)
# define INT64_MIN (-__INT64_C(9223372036854775807)-1)

# define INT8_MAX (127)
# define INT16_MAX (32767)
# define INT32_MAX (2147483647)
# define INT64_MAX (__INT64_C(9223372036854775807))

# define UINT8_MAX (255)
# define UINT16_MAX (65535)
# define UINT32_MAX (4294967295U)
# define UINT64_MAX (__UINT64_C(18446744073709551615))

字符常量默認是一個 int 整數(shù),但編譯器可以自行決定將其解釋為 char 或 int。

char c = 'a';
printf("%c, size(char)=%d, size('a')=%d;\n", c, sizeof(c), sizeof('a'));

輸出:

a, size(char)=1, size('a')=4;

指針是個有特殊用途的整數(shù),在 stdint.h 中同樣給出了其類型定義。

/* Types for `void *' pointers. */
#if __WORDSIZE == 64
    typedef unsigned long int uintptr_t;
#else
    typedef unsigned int uintptr_t;
#endif

不過在代碼中我們通常用 sizeof(char*) 這樣的用法,省得去處理32位和64位的區(qū)別。

我們可以用不同的后綴來表示整數(shù)常量類型。

printf("int size=%d;\n", sizeof(1));
printf("unsigned int size=%d;\n", sizeof(1U));
printf("long size=%d;\n", sizeof(1L));
printf("unsigned long size=%d;\n", sizeof(1UL));
printf("long long size=%d;\n", sizeof(1LL));
printf("unsigned long long size=%d;\n", sizeof(1ULL));

輸出:

int size=4;
unsigned int size=4;
long size=4;
unsigned long size=4;
long long size=8;
unsigned long long size=8;

stdint.h 中定義了一些輔助宏。

# if __WORDSIZE == 64
#   define __INT64_C(c) c ## L
#   define __UINT64_C(c) c ## UL
# else
#   define __INT64_C(c) c ## LL
#   define __UINT64_C(c) c ## ULL
# endif

注: 宏定義中的 "##" 運算符表示把左和右結合在一起,作為一個符號。

1.2 浮點數(shù)

C 提供了不同精度的浮點。

  • float: 32位4字節(jié)浮點數(shù),精確度6。
  • double: 64位8字節(jié)浮點數(shù),精確度15。
  • long double: 80位10字節(jié)浮點數(shù),精確度19位。

浮點數(shù)默認類型是 double,可以添加后綴 F 來表示 float,L 表示 long double,可以局部省略。

printf("float %f size=%d\n", 1.F, sizeof(1.F));
printf("double %f size=%d\n", .123, sizeof(.123));
printf("long double %Lf size=%d\n", 1.234L, sizeof(1.234L));

輸出:

float 1.000000 size=4
double 0.123000 size=8
long double 1.234000 size=12 # 對齊

C99 提供了復數(shù)支持,用兩個相同類型的浮點數(shù)分別表示復數(shù)的實部和虛部。直接在 float、double、long double 后添加 _Complex 即可表示復數(shù),在 complex.h 中定義了complex 宏使得顯示更統(tǒng)一美觀。

#include <complex.h>

printf("float complex size=%d\n", sizeof((float complex)1.0));
printf("double complex size=%d\n", sizeof((double complex)1.0));
printf("long double complex size=%d\n", sizeof((long double complex)1.0));

輸出:

float complex size=8
double complex size=16
long double complex size=24

1.3 枚舉

和 C# 中我們熟悉的規(guī)則類似。

enum color { black, red = 5, green, yellow };

enum color b = black;
printf("black = %d\n", b);

enum color r = red;
printf("red = %d\n", r);

enum color g = green;
printf("green = %d\n", g);

enum color y = yellow;
printf("yellow = %d\n", y);

輸出:

black = 0
red = 5
green = 6
yellow = 7

枚舉成員的值可以相同。

enum color { black = 1, red, green = 1, yellow };

輸出:

black = 1
red = 2
green = 1
yellow = 2

通常省略枚舉小標簽用來代替宏定義常量。

enum { BLACK = 1, RED, GREEN = 1, YELLOW };

printf("black = %d\n", BLACK);
printf("red = %d\n", RED);
printf("green = %d\n", GREEN);
printf("yellow = %d\n", YELLOW);

2. 字面值

字面值 (literal) 是源代碼中用來描述固定值的記號 (token),可能是整數(shù)、浮點數(shù)、字符、字符串。

2.1 整數(shù)常量

除了常見的十進制整數(shù)外,還可以?用八進制 (0開頭) 或十六進制 (0x/0X)表示法。

int x = 010;
int y = 0x0A;
printf("x = %d, y = %d\n", x, y);

輸出:

x = 8, y = 10

常量類型很重要,可以通過后綴來區(qū)分類型。

0x200 -> int
200U -> unsigned int

0L -> long
0xf0f0UL -> unsigned long

0777LL -> long long
0xFFULL -> unsigned long long

2.2 浮點常量

可以用十進制或十六進制表示浮點數(shù)常量。

10.0 -> 10
10. -> 10
.123 -> 0.123

2.34E5 -> 2.34 * (10 ** 5)
67e-12 -> 67.0 * (10 ** -12)

默認浮點常量是 double,可以用 F 后綴表示 float,用 L 后綴表示 long double 類型。

2.3 字符常量

字符常量默認是 int 類型,除非用前置 L 表示 wchar_t 寬字符類型。

char c = 0x61;
char c2 = 'a';
char c3 = '\x61';
printf("%c, %c, %c\n", c, c2, c3);

輸出:

a, a, a

在 Linux 系統(tǒng)中,默認字符集是 UTF-8,可以用 wctomb 等函數(shù)進行轉換。wchar_t 默認是4字節(jié)長度,足以容納所有 UCS-4 Unicode 字符。

setlocale(LC_CTYPE, "en_US.UTF-8");

wchar_t wc = L'中';
char buf[100] = {};

int len = wctomb(buf, wc);
printf("%d\n", len);

for (int i = 0; i < len; i++)
{
    printf("0x%02X ", (unsigned char)buf[i]);
}

輸出:

3
0xE4 0xB8 0xAD

2.4 字符串常量

C 語言中的字符串是一個以 NULL (也就是 \0) 結尾的 char 數(shù)組??兆址趦却嬷姓加靡粋€字節(jié),包含一個 NULL 字符,也就是說要表示一個長度為1的字符串最少需要2個字節(jié) (strlen 和 sizeof 表示的含義不同)。

char s[] = "Hello, World!";
char* s2 = "Hello, C!";

同樣可以使用 L 前綴聲明一個寬字符串。

setlocale(LC_CTYPE, "en_US.UTF-8");

wchar_t* ws = L"中國人";
printf("%ls\n", ws);

char buf[255] = {};
size_t len = wcstombs(buf, ws, 255);

for (int i = 0; i < len; i++)
{
    printf("0x%02X ", (unsigned char)buf[i]);
}

輸出:

中國人
0xE4 0xB8 0xAD 0xE5 0x9B 0xBD 0xE4 0xBA";

和 char 字符串類型類似,wchar_t 字符串以一個4字節(jié)的 NULL 結束。

wchar_t ws[] = L"中國人";
printf("len %d, size %d\n", wcslen(ws), sizeof(ws));

unsigned char* b = (unsigned char*)ws;
int len = sizeof(ws);

for (int i = 0; i < len; i++)
{
    printf("%02X ", b[i]);
}

輸出:

len 3, size 16
2D 4E 00 00 FD 56 00 00 BA 4E 00 00 00 00 00 00

編譯器會自動連接相鄰的字符串,這也便于我們在宏或者代碼中更好地處理字符串。

#define WORLD "world!"
char* s = "Hello" " " WORLD "\n";

對于源代碼中超長的字符串,除了使用相鄰字符串外,還可以用 "\" 在行尾換行。

char* s1 = "Hello"
    " World!";
char* s2 = "Hello \
World!";

注意:"\" 換行后左側的空格會被當做字符串的一部分。

3. 類型轉換

當運算符的幾個操作數(shù)類型不同時,就需要進行類型轉換。通常編譯器會做某些自動的隱式轉換操作,在不丟失信息的前提下,將位寬 "窄" 的操作數(shù)轉換為 "寬" 類型。

3.1 算術類型轉換

編譯器默認的隱式轉換等級:

long double > doulbe > float > long long > long > int > char > _Bool

浮點數(shù)的等級比任何類型的整數(shù)等級都高;有符號整數(shù)和其等價的無符號類型等級相同。在表達式中,可能會將 char、short 當做默認 int (unsigned int) 類型操作數(shù),但 float 并不會自動轉換為默認的 double 類型。

char a = 'a';
char c = 'c';

printf("%d\n", sizeof(c - a));
printf("%d\n", sizeof(1.5F - 1));

輸出:

4
4

當包含無符號操作數(shù)時,需要注意提升后類型是否能容納無符號類型的所有值。

long a = -1L;
unsigned int b = 100;
printf("%ld\n", a > b ? a : b);

輸出:

-1

輸出結果讓人費解。盡管 long 等級比 unsigned int 高,但在32位系統(tǒng)中,它們都是32位整數(shù),且 long 并不足以容納 unsigned int 的所有值,因此編譯器會將這兩個操作數(shù)都轉換為 unsigned long,也就是高等級的無符號版本,如此 (unsigned long)a 的結果就變成了一個很大的整數(shù)。

long a = -1L;
unsigned int b = 100;

printf("%lu\n", (unsigned long)a);
printf("%ld\n", a > b ? a : b);

輸出:

4294967295
-1

其他隱式轉換還包括:

  • 賦值和初始化時,右操作數(shù)總是被轉換成左操作數(shù)類型。
  • 函數(shù)調用時,總是將實參轉換為形參類型。
  • 將 return 表達式結果轉換為函數(shù)返回類型。
  • 任何類型0值和 NULL 指針都視為 _Bool false,反之為 true。

將寬類型轉換為窄類型時,編譯器會嘗試丟棄高位或者四舍五入等手段返回一個 "近似值"。

3.2 非算術類型轉換

(1) 數(shù)組名或表達式通常被當做指向第一個元素的指針,除非是以下情況:

  • 被當做 sizeof 操作數(shù)。
  • 使用 & 運算符返回 "數(shù)組指針"。
  • 字符串常量用于初始化 char/wchar_t 數(shù)組。

(2) 可以顯式將指針轉換成任何其他類型指針。

int x = 123, *p = &x;
char* c = (char*)x;

(3) 任何指針都可以隱式轉換為 void 指針,反之亦然。

(4) 任何指針都可以隱式轉換為類型更明確的指針 (包含 const、volatile、restrict 等限定符)。

int x = 123, *p = &x;
const int* p2 = p;

(5) NULL 可以被隱式轉換為任何類型指針。

(6) 可以顯式將指針轉換為整數(shù),反向轉換亦可。

int x = 123, *p = &x;
int px = (int)p;

printf("%p, %x, %d\n", p, px, *(int*)px);

輸出:

0xbfc1389c, bfc1389c, 123

4. 運算符

基本的表達式和運算符用法無需多言,僅記錄一些特殊的地方。

4.1 復合字面值

C99 新增的內容,我們可以直接用該語法聲明一個結構或數(shù)組指針。

(類型名稱){ 初始化列表 }

演示:

int* i = &(int){ 123 }; ! // 整型變量, 指針
int* x = (int[]){ 1, 2, 3, 4 }; ! // 數(shù)組, 指針
struct data_t* data = &(struct data_t){ .x = 123 }; ! // 結構, 指針
func(123, &(struct data_t){ .x = 123 }); ! // 函數(shù)參數(shù), 結構指針參數(shù)

如果是靜態(tài)或全局變量,那么初始化列表必須是編譯期常量。

4.2 sizeof

返回操作數(shù)占用內存空間大小,單位字節(jié) (byte)。sizeof 返回值是 size_t 類型,操作數(shù)可以是類型和變量。

size_t size;
int x = 1;

size = sizeof(int);

size = sizeof(x);
size = sizeof x;

size = sizeof(&x);
size = sizeof &x;

附: 不要用 int 代替 size_t,因為在32位和64位平臺 size_t 長度不同。

4.3 逗號運算符

逗號是一個二元運算符,確保操作數(shù)從左到右被順序處理,并返回右操作數(shù)的值和類型。

int i = 1;
long long x = (i++, (long long)i);
printf("%lld\n", x);

4.4 優(yōu)先級

C 語言的優(yōu)先級是個?大?麻煩,不要吝嗇使用 "()" 。

優(yōu)先級列表 (從高到低):

類型 符號 結合律
后置運算符 []、func()、.、->、(type){init} 從左到右
一元運算符 ++、--、!、~、+、-、*、&、sizeof 從右到左
v轉換運算符 (type name) 從右到左
乘除運算符 *、/、% 從左到右
加減運算符 +、- 從左到右
位移運算符 <<、>> 從左到右
關系運算符 <、<=、>、>= 從左到右
相等運算符 ==、!= 從左到右
位運算符 & 從左到右
位運算符 ^ 從左到右
位運算符 / 從左到右
邏輯運算符 && 從左到右
邏輯運算符 // 從左到右
條件運算符 ?: 從右到左
賦值運算符 =、+=、-=、*=、/=、%=、&=、^=、/=、<<=、>>= 從右到左
逗號運算符 , 從左到右

如果表達式中多個操作符具有相同優(yōu)先級,那么結合律決定了組合方式是從左還是從右開始。如 "a = b = c",兩個 "=" 優(yōu)先級相同,依結合律順序 "從右到左",分解成 "a = (b = c)"。

下面是一些容易引起誤解的運算符優(yōu)先級:

(1) "." 優(yōu)先級高于 "*"。

原型: *p.f
誤判: (*p).f
實際: *(p.f)。

(2) "[]" 高于 "*"。

原型: int *ap[]
誤判: int (*ap)[]
實際: int *(ap[])

(3) "==" 和 "!=" 高于位操作符。

原型: val & mask != 0
誤判: (val & mask) != 0
實際: val & (mask != 0)

(4) "==" 和 "!=" 高于賦值符。

原型: c = getchar() != EOF
誤判: (c = getchar()) != EOF
實際: c = (getchar() != EOF)

(5) 算術運算符高于位移運算符。

原型: msb << 4 + lsb
誤判: (msb << 4) + lsb
實際: msb << (4 + lsb)

(6) 逗號運算符在所有運算符中優(yōu)先級最低。

原型: i = 1, 2
誤判: i = (1, 2)
實際: (i = 1), 2

5. 語句

5.1 語句塊

語句塊代表了一個作用域,在語句塊內聲明的自動變量超出范圍后立即被釋放。除了用 "{...}" 表示一個常規(guī)語句塊外,還可以直接用于復雜的賦值操作,這在宏中經常使用。

int i = ({ char a = 'a'; a++; a; });
printf("%d\n", i);

最后一個表達式被當做語句塊的返回值。相對應的宏版本如下。

#define test() ({ \
    char _a = 'a'; \
    _a++; \
    _a; })

int i = test();
printf("%d\n", i);

在宏里使用變量通常會添加下劃線前綴,以避免展開后跟上層語句塊的同名變量沖突。

5.2 循環(huán)語句

C 支持 while、for、do...while 幾種循環(huán)語句。注意下面的例子,循環(huán)會導致 get_len 函數(shù)被多次調用。

size_t get_len(const char* s)
{
    printf("%s\n", __func__);
    return strlen(s);
}

int main(int argc, char* argv[])
{
    char *s = "abcde";
    for (int i = 0; i < get_len(s); i++)
    {
        printf("%c\n", s[i]);
    }

    printf("\n");

    return EXIT_SUCCESS;
}

5.3 選擇語句

除了 if...else if...else... 和 switch { case ... } 還有誰呢。GCC 支持 switch 范圍擴展。

int x = 1;
switch (x)
{
    case 0 ... 9: printf("0..9\n"); break;
    case 10 ... 99: printf("10..99\n"); break;
    default: printf("default\n"); break;
}

char c = 'C';
switch (c)
{
    case 'a' ... 'z': printf("a..z\n"); break;
    case 'A' ... 'Z': printf("A..Z\n"); break;
    case '0' ... '9': printf("0..9\n"); break;
    default: printf("default\n"); break;
}

5.4 無條件跳轉

無條件跳轉: break, continue, goto, return。goto 僅在函數(shù)內跳轉,常用于跳出嵌套循環(huán)。如果在函數(shù)外跳轉,可使用 longjmp。

5.4.1 longjmp

setjmp 將當前位置的相關信息 (堆棧幀、寄存器等) 保存到 jmp_buf 結構中,并返回0。當后續(xù)代碼執(zhí)行 longjmp 跳轉時,需要提供一個狀態(tài)碼。代碼執(zhí)行緒將返回 setjmp 處,并返回 longjmp 所提供的狀態(tài)碼。

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <setjmp.h>

void test(jmp_buf *env)
{
    printf("1....\n");
    longjmp(*env, 10);
}

int main(int argc, char* argv[])
{
    jmp_buf env;
    int ret = setjmp(env); ! // 執(zhí)?行 longjmp 將返回該位置,ret 等于 longjmp 所提供的狀態(tài)碼。

    if (ret == 0)
    {
        test(&env);
    }
    else
    {
        printf("2....(%d)\n", ret);
    }

    return EXIT_SUCCESS;
}

輸出:

1....
2....(10)

6. 函數(shù)

函數(shù)只能被定義一次,但可以被多次 "聲明" 和 "調用"。

6.1 嵌套

gcc 支持嵌套函數(shù)擴展。

typedef void(*func_t)();

func_t test()
{
    void func1()
    {
        printf("%s\n", __func__);
    };

    return func1;
}

int main(int argc, char* argv[])
{
    test()();
    return EXIT_SUCCESS;
}

內層函數(shù)可以 "讀寫" 外層函數(shù)的參數(shù)和變量,外層變量必須在內嵌函數(shù)之前定義。

#define pp() ({ \
    printf("%s: x = %d(%p), y = %d(%p), s = %s(%p);\n", __func__, x, &x, y, &y, s, s); \
})

void test2(int x, char *s)
{
    int y = 88;
    pp();

    void func1()
    {
        y++;
        x++;
        pp();
    }

    func1();

    x++;
    func1();
    pp();
}

int main (int argc, char * argv[])
{
    test2(1234, "abc");
    return EXIT_SUCCESS;
}

輸出:

test2: x = 1234(0xbffff7d4), y = 88(0xbffff7d8), s = abc(0x4ad3);
func1: x = 1235(0xbffff7d4), y = 89(0xbffff7d8), s = abc(0x4ad3);
func1: x = 1237(0xbffff7d4), y = 90(0xbffff7d8), s = abc(0x4ad3);
test2: x = 1237(0xbffff7d4), y = 90(0xbffff7d8), s = abc(0x4ad3);

6.2 類型

注意區(qū)分定義 "函數(shù)類型" 和 "函數(shù)指針 類型"的區(qū)別。函數(shù)名是一個指向當前函數(shù)的指針。

typedef void(func_t)(); // 函數(shù)類型
typedef void(*func_ptr_t)(); // 函數(shù)指針類型

void test()
{
    printf("%s\n", __func__);
}

int main(int argc, char* argv[])
{
    func_t* func = test; // 聲明一個指針
    func_ptr_t func2 = test; // 已經是指針類型

    void (*func3)(); // 聲明一個包含函數(shù)原型的函數(shù)指針變量
    func3 = test;

    func();
    func2();
    func3();

    return EXIT_SUCCESS;
}

6.3 調用

C 函數(shù)默認采用 cdecl 調用約定,參數(shù)從右往左入棧,且由調用者負責參數(shù)入棧和清理。

int main(int argc, char* argv[])
{
    int a()
    {
        printf("a\n");
        return 1;
    }

    char* s()
    {
        printf("s\n");
        return "abc";
    }

    printf("call: %d, %s\n", a(), s());
    return EXIT_SUCCESS;
}

輸出:

s
a
call: 1, abc

C 語言中所有對象,包括指針本身都是 "復制傳值" 傳遞,我們可以通過傳遞 "指針的指針" 來實現(xiàn)傳出參數(shù)。

void test(int** x)
{
    int* p = malloc(sizeof(int));
    *p = 123;
    *x = p;
}

int main(int argc, char* argv[])
{
    int* p;
    test(&p);
    printf("%d\n", *p);
    free(p);

    return EXIT_SUCCESS;
}

注意: 別返回 test 中的棧變量。

6.4 修飾符

C99 修飾符:

  • extern: 默認修飾符,用于函數(shù)表示 "具有外部鏈接的標識符",這類函數(shù)可用于任何程序文件。用于變量聲明表示該變量在其他單元中定義。
  • static: 使用該修飾符的函數(shù)僅在其所在編譯單元 (源碼文件) 中可用。還可以表示函數(shù)類的靜態(tài)變量。
  • inline: 修飾符 inline 建議編譯器將函數(shù)代碼內聯(lián)到調用處,但編譯器可自主決定是否完成。通常包含循環(huán)或遞歸函數(shù)不能被定義為 inline 函數(shù)。

GNU inline 相關說明:

  • static inline: 內鏈接函數(shù),在當前編譯單元內內聯(lián)。不過 -O0 時依然是 call。
  • inline: 外連接函數(shù),當前單元內聯(lián),外部單元為普通外連接函數(shù) (頭文件中不能添加 inline 關鍵字)。

附:inline 關鍵字只能用在函數(shù)定義處。

6.5 可選性自變量

使用可選性自變量實現(xiàn)變參。

  • va_start: 通過可選自變量前一個參數(shù)位置來初始化 va_list 自變量類型指針。
  • va_arg: 獲取當前可選自變量值,并將指針移到下一個可選自變量。
  • va_end: 當不再需要自變量指針時調用。
  • va_copy: 用現(xiàn)存的自變量指針 (va_list) 來初始化另一指針。
#include <stdarg.h>

/* 指定自變量數(shù)量 */
void test(int count, ...)
{
    va_list args;
    va_start(args, count);

    for (int i = 0; i < count; i++)
    {
        int value = va_arg(args, int);
        printf("%d\n", value);
    }

    va_end(args);
}

/* 以 NULL 為結束標記 */
void test2(const char* s, ...)
{
    printf("%s\n", s);

    va_list args;
    va_start(args, s);

    char* value;
    do
    {
        value = va_arg(args, char*);
        if (value) printf("%s\n", value);
    }
    while (value != NULL);
    va_end(args);
}

/* 直接將 va_list 傳遞個其他可選自變量函數(shù) */
void test3(const char* format, ...)
{
    va_list args;
    va_start(args, format);

    vprintf(format, args);

    va_end(args);
}

int main(int argc, char* argv[])
{
    test(3, 11, 22, 33);
    test2("hello", "aa", "bb", "cc", "dd", NULL);
    test3("%s, %d\n", "hello, world!", 1234);

    return EXIT_SUCCESS;
}

7. 數(shù)組

7.1 可變長度數(shù)組

如果數(shù)組具有自動生存周期,且沒有 static 修飾符,那么可以用非常量表達式來定義數(shù)組。

void test(int n)
{
    int x[n];
    for (int i = 0; i < n; i++)
    {
        x[i] = i;
    }

    struct data { int x[n]; } d;
    printf("%d\n", sizeof(d));
}

int main(int argc, char* argv[])
{
    int x[] = { 1, 2, 3, 4 };
    printf("%d\n", sizeof(x));

    test(2);
    return EXIT_SUCCESS;
}

7.2 下標存儲

x[i] 相當于 *(x + i),數(shù)組名默認為指向第一元素的指針。

int x[] = { 1, 2, 3, 4 };

x[1] = 10;
printf("%d\n", *(x + 1));

*(x + 2) = 20;
printf("%d\n", x[2]);

C 不會對數(shù)組下標索引進行范圍檢查,編碼時需要注意過界檢查。數(shù)組名默認是指向第一元素指針的常量,而 &x[i] 則返回 int* 類型指針,指向目標序號元素。

7.3 初始化

除了使用下標初始化外,還可以直接用初始化器。

int x[] = { 1, 2, 3 };
int y[5] = { 1, 2 };
int a[3] = {};

int z[][2] =
{
    { 1, 1 },
    { 2, 1 },
    { 3, 1 },
};

初始化規(guī)則:

  • 如果數(shù)組為靜態(tài)生存周期,那么初始化器必須是常量表達式。
  • 如果提供初始化器,那么可以不提供數(shù)組長度,由初始化器的最后一個元素決定。
  • 如果同時提供長度和初始化器,那么沒有提供初始值的元素都被初始化為0或 NULL。

我們還可以在初始化器中初始化特定的元素。

int x[] = { 1, 2, [6] = 10, 11 };
int len = sizeof(x) / sizeof(int);

for (int i = 0; i < len; i++)
{
    printf("x[%d] = %d\n", i, x[i]);
}

輸出:

x[0] = 1
x[1] = 2
x[2] = 0
x[3] = 0
x[4] = 0
x[5] = 0
x[6] = 10
x[7] = 11

7.4 字符串

字符串是以 '\0' 結尾的 char 數(shù)組。

char s[10] = "abc";
char x[] = "abc";

printf("s, size=%d, len=%d\n", sizeof(s), strlen(s));
printf("x, size=%d, len=%d\n", sizeof(x), strlen(x));

輸出:

s, size=10, len=3
x, size=4, len=3

7.5 多維數(shù)組

實際上就是 "元素為數(shù)組" 的數(shù)組,注意元素是數(shù)組,并不是數(shù)組指針。多維數(shù)組的第一個維度下標可以不指定。

int x[][2] =
{
    { 1, 11 },
    { 2, 22 },
    { 3, 33 }
};

int col = 2, row = sizeof(x) / sizeof(int) / col;

for (int r = 0; r < row; r++)
{
    for (int c = 0; c < col; c++)
    {
        printf("x[%d][%d] = %d\n", r, c, x[r][c]);
    }
}

輸出:

x[0][0] = 1
x[0][1] = 11
x[1][0] = 2
x[1][1] = 22
x[2][0] = 3
x[2][1] = 33

二維數(shù)組通常也被稱為 "矩陣 (matrix)",相當于一個 row * column 的表格。比如 x[3][2] 相當于三行二列表格。多維數(shù)組的元素是連續(xù)排列的,這也是區(qū)別指針數(shù)組的一個重要特征。

int x[][2] =
{
    { 1, 11 },
    { 2, 22 },
    { 3, 33 }
};

int len = sizeof(x) / sizeof(int);
int* p = (int*)x;

for (int i = 0; i < len; i++)
{
    printf("x[%d] = %d\n", i, p[i]);
}

輸出:

x[0] = 1
x[1] = 11
x[2] = 2
x[3] = 22
x[4] = 3
x[5] = 33

同樣,我們可以初始化特定的元素。

int x[][2] =
{
    { 1, 11 },
    { 2, 22 },
    { 3, 33 },
    [4][1] = 100,
    { 6, 66 },
    [7] = { 9, 99 }
};

int col = 2, row = sizeof(x) / sizeof(int) / col;

for (int r = 0; r < row; r++)
{
    for (int c = 0; c < col; c++)
    {
        printf("x[%d][%d] = %d\n", r, c, x[r][c]);
    }
}

輸出:

x[0][0] = 1
x[0][1] = 11
x[1][0] = 2
x[1][1] = 22
x[2][0] = 0
x[2][1] = 0
x[3][0] = 0
x[3][1] = 0
x[4][0] = 0
x[4][1] = 100
x[5][0] = 6
x[5][1] = 66
x[6][0] = 0
x[6][1] = 0
x[7][0] = 9
x[7][1] = 99

7.6 數(shù)組參數(shù)

當數(shù)組作為函數(shù)參數(shù)時,總是被隱式轉換為指向數(shù)組第一元素的指針,也就是說我們再也無法用 sizeof 獲得數(shù)組的實際長度了。

void test(int x[])
{
    printf("%d\n", sizeof(x));
}

void test2(int* x)
{
    printf("%d\n", sizeof(x));
}

int main(int argc, char* argv[])
{
    int x[] = { 1, 2, 3 };
    printf("%d\n", sizeof(x));

    test(x);
    test2(x);

    return EXIT_SUCCESS;
}

輸出:

12
4
4

test 和 test2 中的 sizeof(x) 實際效果是 sizeof(int*)。我們需要顯式傳遞數(shù)組長度,或者是一個以特定標記結尾的數(shù)組 (NULL)。C99 支持長度可變數(shù)組作為函數(shù)函數(shù)。當我們傳遞數(shù)組參數(shù)時,可能的寫法包括:

/* 數(shù)組名默認指向第一元素指針,和 test2 一個意思 */
void test1(int len, int x[])
{
    int i;
    for (i = 0; i < len; i++)
    {
        printf("x[%d] = %d; ", i, x[i]);
    }

    printf("\n");
}

/* 直接傳入數(shù)組第一元素指針 */
void test2(int len, int* x)
{
    for (int i = 0; i < len; i++)
    {
        printf("x[%d] = %d; ", i, *(x + i));
    }

    printf("\n");
}

/* 數(shù)組指針: 數(shù)組名默認指向第一元素指針,&array 則是獲得整個數(shù)組指針 */
void test3(int len, int(*x)[len])
{
    for (int i = 0; i < len; i++)
    {
    printf("x[%d] = %d; ", i, (*x)[i]);
    }

    printf("\n");
}

/* 多維數(shù)組: 數(shù)組名默認指向第一元素指針,也即是 int(*)[] */
void test4(int r, int c, int y[][c])
{
    for (int a = 0; a < r; a++)
    {
        for (int b = 0; b < c; b++)
        {
            printf("y[%d][%d] = %d; ", a, b, y[a][b]);
        }
    }

    printf("\n");
}

/* 多維數(shù)組: 傳遞第一個元素的指針 */
void test5(int r, int c, int (*y)[c])
{
    for (int a = 0; a < r; a++)
    {
        for (int b = 0; b < c; b++)
        {
            printf("y[%d][%d] = %d; ", a, b, (*y)[b]);
        }

        y++;
    }

    printf("\n");
}

/* 多維數(shù)組 */
void test6(int r, int c, int (*y)[][c])
{
    for (int a = 0; a < r; a++)
    {
        for (int b = 0; b < c; b++)
        {
            printf("y[%d][%d] = %d; ", a, b, (*y)[a][b]);
        }
    }

    printf("\n");
}

/* 元素為指針的指針數(shù)組,相當于 test8 */
void test7(int count, char** s)
{
    for (int i = 0; i < count; i++)
    {
        printf("%s; ", *(s++));
    }

    printf("\n");
}

void test8(int count, char* s[count])
{
    for (int i = 0; i < count; i++)
    {
        printf("%s; ", s[i]);
    }

    printf("\n");
}

/* 以 NULL 結尾的指針數(shù)組 */
void test9(int** x)
{
    int* p;
    while ((p = *x) != NULL)
    {
        printf("%d; ", *p);
        x++;
    }

    printf("\n");
}

int main(int argc, char* argv[])
{
    int x[] = { 1, 2, 3 };

    int len = sizeof(x) / sizeof(int);
    test1(len, x);
    test2(len, x);
    test3(len, &x);

    int y[][2] =
    {
        {10, 11},
        {20, 21},
        {30, 31}
    };

    int a = sizeof(y) / (sizeof(int) * 2);
    int b = 2;
    test4(a, b, y);
    test5(a, b, y);
    test6(a, b, &y);

    char* s[] = { "aaa", "bbb", "ccc" };
    test7(sizeof(s) / sizeof(char*), s);
    test8(sizeof(s) / sizeof(char*), s);

    int* xx[] = { &(int){111}, &(int){222}, &(int){333}, NULL };
    test9(xx);

    return EXIT_SUCCESS;
}

8. 指針

8.1 void 指針

void 又被稱為萬能指針,可以代表任何對象的地址,但沒有該對象的類型。也就是說必須轉型后才能進行對象操作。void 指針可以與其他任何類型指針進行隱式轉換。

void test(void* p, size_t len)
{
    unsigned char* cp = p;

    for (int i = 0; i < len; i++)
    {
        printf("%02x ", *(cp + i));
    }

    printf("\n");
}

int main(int argc, char* argv[])
{
    int x = 0x00112233;
    test(&x, sizeof(x));

    return EXIT_SUCCESS;
}

輸出:

33 22 11 00

8.2 初始化指針

可以用初始化器初始化指針。

  • 空指針常量 NULL。
  • 相同類型的指針,或者指向限定符較少的相同類型指針。
  • void 指針。

非自動周期指針變量或靜態(tài)生存期指針變量必須用編譯期常量表達式初始化,比如函數(shù)名稱等。

char s[] = "abc";
char* sp = s;

int x = 5;
int* xp = &x;

void test() {}
typedef void(*test_t)();
int main(int argc, char* argv[])
{
    static int* sx = &x;
    static test_t t = test;

    return EXIT_SUCCESS;
}

8.3 指針運算

(1) 對指針進行相等或不等運算來判斷是否指向同一對象。

int x = 1;

int *a, *b;
a = &x;
b = &x;

printf("%d\n", a == b);

(2) 對指針進行加法運算獲取數(shù)組第 n 個元素指針。

int x[] = { 1, 2, 3 };
int* p = x;

printf("%d, %d\n", x[1], *(p + 1));

(3) 對指針進行減法運算,以獲取指針所在元素的數(shù)組索引序號。

int x[] = { 1, 2, 3 };

int* p = x;
p++; p++;

int index = p - x;

printf("x[%d] = %d\n", index, x[index]);

輸出:

x[2] = 3;

(4) 對指針進行大小比較運算,相當于判斷數(shù)組索引序號大小。

int x[] = { 1, 2, 3 };
int* p1 = x;
int* p2 = x;
p1++; p2++; p2++;

printf("p1 < p2? %s\n", p1 < p2 ? "Y" : "N");

輸出:

p1 < p2? Y

(5) 我們可以直接用 &x[i] 獲取指定序號元素的指針。

int x[] = { 1, 2, 3 };

int* p = &x[1];
*p += 10;

printf("%d\n", x[1]);

注: [] 優(yōu)先級比 & 高,* 運算符優(yōu)先級比算術運算符高。

8.4 限定符

限定符 const 可以聲明 "類型為指針的常量" 和 "指向常量的指針" 。

int x[] = { 1, 2, 3 };

// 指針常量: 指針本身為常量,不可修改,但可修改目標對象。
int* const p1 = x;
*(p1 + 1) = 22;
printf("%d\n", x[1]);

// 常量指針: 目標對象為常量,不可修改,但可修改指針。
int const *p2 = x;
p2++;
printf("%d\n", *p2);

區(qū)別在于 const 是修飾 p 還是 *p。具有 restrict 限定符的指針被稱為限定指針。告訴編譯器在指針生存周期內,只能通過該指針修改對象,但編譯器可自主決定是否采納該建議。

8.5 數(shù)組指針

指向數(shù)組本身的指針,而非指向第一元素的指針。

int x[] = { 1, 2, 3 };
int(*p)[] = &x;

for (int i = 0; i < 3; i++)
{
    printf("x[%d] = %d\n", i, (*p)[i]);
    printf("x[%d] = %d\n", i, *(*p + i));
}

&x 返回數(shù)組指針,*p 獲取和 x 相同的指針,也就是指向第一元素的指針,然后可以用下標或指針運算存儲元素。

8.6 指針數(shù)組

元素是指針的數(shù)組,通常用于表示字符串數(shù)組或交錯數(shù)組。數(shù)組元素是目標對象 (可以是數(shù)組或其他對象) 的指針,而非實際嵌入內容。

int* x[3] = {};

x[0] = (int[]){ 1 };
x[1] = (int[]){ 2, 22 };
x[2] = (int[]){ 3, 33, 33 };

int* x1 = *(x + 1);
for (int i = 0; i < 2; i++)
{
    printf("%d\n", x1[i]);
    printf("%d\n", *(*(x + 1) + i));
}

輸出:

2
2
22
22

指針數(shù)組 x 是三個指向目標對象(數(shù)組)的指針,*(x + 1) 獲取目標對象,也就是 x[1]。

9. 結構

9.1 不完整結構

結構類型無法把自己作為成員類型,但可以包含 "指向自己類型" 的指針成員。

struct list_node
{
    struct list_node* prev;
    struct list_node* next;
    void* value;
};

定義不完整結構類型,只能使用小標簽,像下面這樣的 typedef 類型名稱是不行的。

typedef struct
{
    list_node* prev;
    list_node* next;
    void* value;
} list_node;

編譯出錯:

$ make

gcc -Wall -g -c -std=c99 -o main.o main.c
main.c:15: error: expected specifier-qualifier-list before ‘list_node’

結合起來用吧。

typedef struct node_t
{
    struct node_t* prev;
    struct node_t* next;
    void* value;
} list_node;

小標簽可以和 typedef 定義的類型名相同。

typedef struct node_t
{
    struct node_t* prev;
    struct node_t* next;
    void* value;
} node_t;

9.2 匿名結構

在結構體內部使用匿名結構體成員,也是一種很常見的做法。

typedef struct
{
    struct
    {
        int length;
        char chars[100];
    } s;

    int x;
} data_t;

int main(int argc, char * argv[])
{
    data_t d = { .s.length = 100, .s.chars = "abcd", .x = 1234 };
    printf("%d\n%s\n%d\n", d.s.length, d.s.chars, d.x);

    return EXIT_SUCCESS;
}

或者直接定義一個匿名變量。

int main(int argc, char * argv[])
{
    struct { int a; char b[100]; } d = { .a = 100, .b = "abcd" };
    printf("%d\n%s\n", d.a, d.b);

    return EXIT_SUCCESS;
}

9.3 成員偏移量

利用 stddef.h 中的 offsetof 宏可以獲取結構成員的偏移量。

typedef struct
{
    int x;
    short y[3];
    long long z;
} data_t;

int main(int argc, char* argv[])
{
    printf("x %d\n", offsetof(data_t, x));
    printf("y %d\n", offsetof(data_t, y));
    printf("y[1] %d\n", offsetof(data_t, y[1]));
    printf("z %d\n", offsetof(data_t, z));

    return EXIT_SUCCESS;
}

注意:輸出結果有字節(jié)對齊。

9.4 定義

定義結構類型有多種靈活的?方式。

int main(int argc, char* argv[])
{
    /* 直接定義結構類型和變量 */
    struct { int x; short y; } a = { 1, 2 }, a2 = {};
    printf("a.x = %d, a.y = %d\n", a.x, a.y);

    /* 函數(shù)內部也可以定義結構類型 */
    struct data { int x; short y; };

    struct data b = { .y = 3 };
    printf("b.x = %d, b.y = %d\n", b.x, b.y);

    /* 復合字面值 */
    struct data* c = &(struct data){ 1, 2 };
    printf("c.x = %d, c.y = %d\n", c->x, c->y);

    /* 也可以直接將結構體類型定義放在復合字面值中 */
    void* p = &(struct data2 { int x; short y; }){ 11, 22 };

    /* 相同內存布局的結構體可以直接轉換 */
    struct data* d = (struct data*)p;
    printf("d.x = %d, d.y = %d\n", d->x, d->y);

    return EXIT_SUCCESS;
}

輸出:

a.x = 1, a.y = 2
b.x = 0, b.y = 3
c.x = 1, c.y = 2
d.x = 11, d.y = 22

9.5 初始化

結構體的初始化和數(shù)組一樣簡潔方便,包括使用初始化器初始化特定的某些成員。未被初始化器初始化的成員將被設置為0。

typedef struct
{
    int x;
    short y[3];
    long long z;
} data_t;

int main(int argc, char* argv[])
{
    data_t d = {};
    data_t d1 = { 1, { 11, 22, 33 }, 2LL };
    data_t d2 = { .z = 3LL, .y[2] = 2 };

    return EXIT_SUCCESS;
}<