示例基于 GCC 32bit...
以下是基本整數(shù)關鍵詞:
由于在不同系統(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
注: 宏定義中的 "##" 運算符表示把左和右結合在一起,作為一個符號。
C 提供了不同精度的浮點。
浮點數(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
和 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);
字面值 (literal) 是源代碼中用來描述固定值的記號 (token),可能是整數(shù)、浮點數(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
可以用十進制或十六進制表示浮點數(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 類型。
字符常量默認是 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
C 語言中的字符串是一個以 NULL (也就是 \0) 結尾的 char 數(shù)組??兆址趦?nèi)存中占用一個字節(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!";
注意:"\" 換行后左側的空格會被當做字符串的一部分。
當運算符的幾個操作數(shù)類型不同時,就需要進行類型轉換。通常編譯器會做某些自動的隱式轉換操作,在不丟失信息的前提下,將位寬 "窄" 的操作數(shù)轉換為 "寬" 類型。
編譯器默認的隱式轉換等級:
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
其他隱式轉換還包括:
將寬類型轉換為窄類型時,編譯器會嘗試丟棄高位或者四舍五入等手段返回一個 "近似值"。
(1) 數(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
基本的表達式和運算符用法無需多言,僅記錄一些特殊的地方。
C99 新增的內(nèi)容,我們可以直接用該語法聲明一個結構或數(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)或全局變量,那么初始化列表必須是編譯期常量。
返回操作數(shù)占用內(nèi)存空間大小,單位字節(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 長度不同。
逗號是一個二元運算符,確保操作數(shù)從左到右被順序處理,并返回右操作數(shù)的值和類型。
int i = 1;
long long x = (i++, (long long)i);
printf("%lld\n", x);
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
語句塊代表了一個作用域,在語句塊內(nèi)聲明的自動變量超出范圍后立即被釋放。除了用 "{...}" 表示一個常規(guī)語句塊外,還可以直接用于復雜的賦值操作,這在宏中經(jīng)常使用。
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);
在宏里使用變量通常會添加下劃線前綴,以避免展開后跟上層語句塊的同名變量沖突。
C 支持 while、for、do...while 幾種循環(huán)語句。注意下面的例子,循環(huán)會導致 get_len 函數(shù)被多次調(diào)用。
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;
}
除了 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;
}
無條件跳轉: break, continue, goto, return。goto 僅在函數(shù)內(nèi)跳轉,常用于跳出嵌套循環(huán)。如果在函數(shù)外跳轉,可使用 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)
函數(shù)只能被定義一次,但可以被多次 "聲明" 和 "調(diào)用"。
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;
}
內(nèi)層函數(shù)可以 "讀寫" 外層函數(shù)的參數(shù)和變量,外層變量必須在內(nèi)嵌函數(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);
注意區(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; // 已經(jīng)是指針類型
void (*func3)(); // 聲明一個包含函數(shù)原型的函數(shù)指針變量
func3 = test;
func();
func2();
func3();
return EXIT_SUCCESS;
}
C 函數(shù)默認采用 cdecl 調(diào)用約定,參數(shù)從右往左入棧,且由調(diào)用者負責參數(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 中的棧變量。
C99 修飾符:
GNU inline 相關說明:
附:inline 關鍵字只能用在函數(shù)定義處。
使用可選性自變量實現(xiàn)變參。
#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;
}
如果數(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;
}
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* 類型指針,指向目標序號元素。
除了使用下標初始化外,還可以直接用初始化器。
int x[] = { 1, 2, 3 };
int y[5] = { 1, 2 };
int a[3] = {};
int z[][2] =
{
{ 1, 1 },
{ 2, 1 },
{ 3, 1 },
};
初始化規(guī)則:
我們還可以在初始化器中初始化特定的元素。
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
字符串是以 '\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
實際上就是 "元素為數(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
當數(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;
}
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
可以用初始化器初始化指針。
非自動周期指針變量或靜態(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;
}
(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)先級比算術運算符高。
限定符 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 限定符的指針被稱為限定指針。告訴編譯器在指針生存周期內(nèi),只能通過該指針修改對象,但編譯器可自主決定是否采納該建議。
指向數(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 相同的指針,也就是指向第一元素的指針,然后可以用下標或指針運算存儲元素。
元素是指針的數(shù)組,通常用于表示字符串數(shù)組或交錯數(shù)組。數(shù)組元素是目標對象 (可以是數(shù)組或其他對象) 的指針,而非實際嵌入內(nèi)容。
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]。
結構類型無法把自己作為成員類型,但可以包含 "指向自己類型" 的指針成員。
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;
在結構體內(nèi)部使用匿名結構體成員,也是一種很常見的做法。
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;
}
利用 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é)對齊。
定義結構類型有多種靈活的?方式。
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ù)內(nèi)部也可以定義結構類型 */
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 };
/* 相同內(nèi)存布局的結構體可以直接轉換 */
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
結構體的初始化和數(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;
}
結果:
d = {x = 0, y = {0, 0, 0}, z = 0}
d1 = {x = 1, y = {11, 22, 33}, z = 2}
d2 = {x = 0, y = {0, 0, 2}, z = 3}
通常又稱作 “不定長結構”,就是在結構體尾部聲明一個未指定長度的數(shù)組。用 sizeof 運算符時,該數(shù)組未計入結果。
typedef struct string
{
int length;
char chars[];
} string;
int main(int argc, char * argv[])
{
int len = sizeof(string) + 10; // 計算存儲一個 10 字節(jié)長度的字符串(包括 \0)所需的長度。
char buf[len]; // 從棧上分配所需的內(nèi)存空間。
string *s = (string*)buf; // 轉換成 struct string 指針。
s->length = 9;
strcpy(s->chars, "123456789");
printf("%d\n%s\n", s->length, s->chars);
return EXIT_SUCCESS;
}
考慮到不同編譯器和 ANSI C 標準的問題,也用 char chars[1] 或 char chars[0] 來代替。對這類結構體進行拷貝的時候,尾部結構成員不會被復制。
int main(int argc, char * argv[])
{
int len = sizeof(string) + 10;
char buf[len];
string *s = (string*)buf;
s->length = 10;
strcpy(s->chars, "123456789");
string s2 = *s; ! ! ! ! ! // 復制 struct string s。
printf("%d\n%s\n", s2.length, s2.chars); ! // s2.length 正常,s2.chars 就悲劇了。
return EXIT_SUCCESS;
}
而且不能直接對彈性成員進行初始化。
聯(lián)合和結構的區(qū)別在于:聯(lián)合每次只能存儲一個成員,聯(lián)合的長度由最寬成員類型決定。
typedef struct
{
int type;
union
{
int ivalue;
long long lvalue;
} value;
} data_t;
data_t d = { 0x8899, .value.lvalue = 0x1234LL };
data_t d2;
memcpy(&d2, &d, sizeof(d));
printf("type:%d, value:%lld\n", d2.type, d2.value.lvalue);
當然也可以用指針來實現(xiàn)上例功能,但 union 會將數(shù)據(jù)內(nèi)嵌在結構體中,這對于進行 memcpy 等操作更加方便快捷,而且無需進行指針類型轉換。
可以使用初始化器初始化聯(lián)合,如果沒有指定成員修飾符,則默認是第一個成員。
union value_t
{
int ivalue;
long long lvalue;
};
union value_t v1 = { 10 };
printf("%d\n", v1.ivalue);
union value_t v2 = { .lvalue = 20LL };
printf("%lld\n", v2.lvalue);
union value2_t { char c; int x; } v3 = { .x = 100 };
printf("%d\n", v3.x);
一個常用的聯(lián)合用法。
union { int x; struct {char a, b, c, d;} bytes; } n = { 0x12345678 };
printf("%#x => %x, %x, %x, %x\n", n.x, n.bytes.a, n.bytes.b, n.bytes.c, n.bytes.d);
輸出:
0x12345678 => 78, 56, 34, 12
可以把結構或聯(lián)合的多個成員 "壓縮存儲" 在一個字段中,以節(jié)約內(nèi)存。
struct
{
unsigned int year : 22;
unsigned int month : 4;
unsigned int day : 5;
} d = { 2010, 4, 30 };
printf("size: %d\n", sizeof(d));
printf("year = %u, month = %u, day = %u\n", d.year, d.month, d.day);
輸出:
size: 4
year = 2010, month = 4, day = 30
用來做標志位也挺好的,比用位移運算符更直觀,更節(jié)省內(nèi)存。
int main(int argc, char * argv[])
{
struct
{
bool a: 1;
bool b: 1;
bool c: 1;
} flags = { .b = true };
printf("%s\n", flags.b ? "b.T" : "b.F");
printf("%s\n", flags.c ? "c.T" : "c.F");
return EXIT_SUCCESS;
}
不能對位字段成員使用 offsetof。
聲明 (declaration) 表示目標樣式,可以在多處聲明同一個目標,但只能有一個定義(definition)。定義將創(chuàng)建對象實體,為其分配存儲空間 (內(nèi)存),而聲明不會。
聲明通常包括:
如果聲明函數(shù)時同時出現(xiàn)函數(shù)體,則此函數(shù)的聲明同時也是定義。如果聲明對象時給此對象分配內(nèi)存 (比如定義變量),那么此對象聲明的同時也是定義。
C99 定義的類型修飾符:
元素 | 存儲類型 | 作用域 | 生存周期 | 鏈接類型 |
---|---|---|---|---|
全局UDT | - | 文件 | - | 內(nèi)鏈接 |
嵌套UDT | - | 類 | - | 內(nèi)鏈接 |
局部UDT | - | 程序塊 | - | 無鏈接 |
全局函數(shù)、變量 | extern | 文件 | 永久 | 外連接 |
靜態(tài)全局函數(shù)和變量 | static | 文件 | 永久 | 內(nèi)鏈接 |
局部變量、常量 | auto | 程序塊 | 臨時 | 無鏈接 |
局部靜態(tài)變量、常量 | static | 程序塊 | 永久 | 無鏈接 |
全局常量 | - | 文件 | 永久 | 內(nèi)鏈接 |
靜態(tài)全局常量 | static | 文件 | 永久 | 內(nèi)鏈接 |
宏定義 | - | 文件 | - | 內(nèi)鏈接 |
具有靜態(tài)生存周期的對象,會被初始化位默認值0(指針為NULL)。
預處理指令以 # 開始 (其前面可以有 space 或 tab),通常獨立一行,但可以用 "\" 換行。
編譯器會展開替換掉宏。
#define SIZE 10
int main(int argc, char* argv[])
{
int x[SIZE] = {};
return EXIT_SUCCESS;
}
展開:
$ gcc -E main.c
int main(int argc, char* argv[])
{
int x[10] = {};
return 0;
}
利用宏可以定義偽函數(shù),通常用 ({ ... }) 來組織多行語句,最后一個表達式作為返回值 (無 return,且有個 ";" 結束)。
#define test(x, y) ({ \
int _z = x + y; \
_z; })
int main(int argc, char* argv[])
{
printf("%d\n", test(1, 2));
return EXIT_SUCCESS;
}
展開:
int main(int argc, char* argv[])
{
printf("%d\n", ({ int _z = 1 + 2; _z; }));
return 0;
}
__VA_ARGS__ 標識符用來表示一組可選性自變量。
#define println(format, ...) ({ \
printf(format "\n", __VA_ARGS__); })
int main(int argc, char* argv[])
{
println("%s, %d", "string", 1234);
return EXIT_SUCCESS;
}
展開:
int main(int argc, char* argv[])
{
({ printf("%s, %d" "\n", "string", 1234); });
return 0;
}
單元運算符 # 將一個宏參數(shù)轉換為字符串。
#define test(name) ({ \
printf("%s\n", #name); })
int main(int argc, char* argv[])
{
test(main);
test("\"main");
return EXIT_SUCCESS;
}
展開:
int main(int argc, char* argv[])
{
({ printf("%s\n", "main"); });
({ printf("%s\n", "\"\\\"main\""); });
return 0;
}
這個不錯,會自動進行轉義操作。
二元運算符 ## 將左和右操作數(shù)結合成一個記號。
#define test(name, index) ({ \
int i, len = sizeof(name ## index) / sizeof(int); \
for (i = 0; i < len; i++) \
{ \
printf("%d\n", name ## index[i]); \
}})
int main(int argc, char* argv[])
{
int x1[] = { 1, 2, 3 };
int x2[] = { 11, 22, 33, 44, 55 };
test(x, 1);
test(x, 2);
return EXIT_SUCCESS;
}
展開:
int main(int argc, char* argv[])
{
int x1[] = { 1, 2, 3 };
int x2[] = { 11, 22, 33, 44, 55 };
({ int i, len = sizeof(x1) / sizeof(int); for (i = 0; i < len; i++) { printf("%d\n",
x1[i]); }});
({ int i, len = sizeof(x2) / sizeof(int); for (i = 0; i < len; i++) { printf("%d\n",
x2[i]); }});
return 0;
}
可以使用 "#if ... #elif ... #else ... #endif"、#define、#undef 進行條件編譯。
#define V1
#if defined(V1) || defined(V2)
printf("Old\n");
#else
printf("New\n");
#endif
#undef V1
展開:
int main(int argc, char* argv[])
{
printf("Old\n");
return 0;
}
也可以用 #ifdef、#ifndef 代替 #if。
#define V1
#ifdef V1
printf("Old\n");
#else
printf("New\n");
#endif
#undef A
展開:
int main(int argc, char* argv[])
{
printf("Old\n");
return 0;
}
使用 GCC 擴展 typeof 可以獲知參數(shù)的類型。
#define test(x) ({ \
typeof(x) _x = (x); \
_x += 1; \
_x; \
})
int main(int argc, char* argv[])
{
float f = 0.5F;
float f2 = test(f);
printf("%f\n", f2);
return EXIT_SUCCESS;
}
一些常用的特殊常量。
要習慣使用 assert 宏進行函數(shù)參數(shù)和執(zhí)行條件判斷,這可以省卻很多麻煩。
#include <assert.h>
void test(int x)
{
assert(x > 0);
printf("%d\n", x);
}
int main(int argc, char* argv[])
{
test(-1);
return EXIT_SUCCESS;
}
展開效果:
$ gcc -E main.c
void test(int x)
{
((x > 0) ? (void) (0) : __assert_fail ("x > 0", "main.c", 16, __PRETTY_FUNCTION__));
printf("%d\n", x);
}
如果 assert 條件表達式不為 true,則出錯并終止進程。
$ ./test
test: main.c:16: test: Assertion `x > 0' failed.
Aborted
不過呢在編譯 Release 版本時,記得加上 -DNDEBUG 參數(shù)。
$ gcc -E -DNDEBUG main.c
void test(int x)
{
((void) (0));
printf("%d\n", x);
}
更多建議: