字符常量默认是一个int整数,但编译器可以自行决定将其解释为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;
浮点数默认类型是double,可以添加后缀F来表示float,L表示long double
字面值(literal)是源代码中用来描述固定值的记号(token),可能是整数、浮点数、字符、字符串。
字符常量默认是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系统中,默认字符集是UTF-8,可以用 wctomb 等函数进行转换。
wchar_t 默认是4字节长度,足以容纳所有 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数组。
空字符串在内存中占用一个字节,包含一个NULL字符,也就是说要表示一个长度为1的字符串最少需要2个字节(strlen和sizeof表示的含义不同)。
当运算符的几个操作数类型不同时,就需要进行类型转换。通常编译器会做某些自动的隐式转换操作,在不丢失信息的前提下,将位宽“窄”的操作数转换成“宽”类型。
其他隐式转换还包括:
C99新增的内容,我们可以直接用以下语法声明一个结构或数组指针:
(类型名称){初始化列表}
演示:
int* i = &(int){ 123 }; // 整型变量,指针
int* x = (int[]){ 1, 2, 3, 4}; // 数组,指针
struct data_t* data = &(struct data_t){ .x = 123 }; // 结构,指针
func(123, &(struct data_t){ .x = 123 }); // 函数参数,结构指针参数
如果是静态或全局变量,那么初始化列表必须是编译期变量。
sizeof:返回操作数占用内存空间大小,单位字节(byte)。sizeof返回值是size_t类型,操作数可以是类型或变量。
逗号运算符是一个二元运算符,确保操作数从左到右被顺序处理,并返回右操作数的值和类型。
如:
int i = 1;
long long x = (i++, (long long)i);
printf("%lld\n", x);
输出为:
2
无条件跳转:break, continue, goto, return。
goto仅在函数内跳转,常用于跳出嵌套循环。如果在函数外跳转,可使用longjmp。
setjmp将当前位置的相关信息(堆栈帧、寄存器等)保存到jmp_buf结构中,并返回0。当后续代码执行longjmp跳转时,需要提高一个状态码。代码执行时将返回setjmp处,并返回longjmp所提供的状态码。
示例:
#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); // 执行 longjmp 将返回该位置,ret 等于 longjmp 提供的状态码。
if (ret == 0)
{
test(&env);
}
else
{
printf("2....(%d)\n", ret);
}
return EXIT_SUCCESS;
}
注意区分定义“函数类型”和“函数指针 类型”的区别。函数名是一个指向当前函数的指针。
如:
#include <stdio.h>
#include <stdlib.h>
typedef void(func_t)(); // 函数类型
typedef void(*func_ptr_t)(); // 函数指针类型
void test()
{
printf("%s\n", __func__);
}
int main(int argc, char* argv[])
{
func_t* func = test; // 声明一个指针
func_ptr_t func2 = test; // 已经是指针类型
void (*func3)(); // 声明⼀一个包含函数原型的函数指针变量
func3 = test;
func();
func2();
func3();
return EXIT_SUCCESS;
}
输出为:
test
test
test
C语言中所有对象,包括指针本身都是“复制传值”传递,我们可以通过传递“指针的指针”来实现传出参数。
示例:
#include <stdio.h>
#include <stdlib.h>
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;
}
C99修饰符:
当数组作为函数参数时,总是被隐式转换为指向数组第一元素的指针,也就是说我们再也无法用sizeof获得数组的实际长度了。
示例:
#include <stdio.h>
#include <stdlib.h>
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
C99支持长度可变数组作为函数参数。
void指针:void*
又被称为 万能指针 ,可以代表任何对象的地址,但没有该对象的类型。也就是说必须转型后才能进行对象操作。 void*
指针可以与其他任何类型指针进行隐式转换。
示例:
#include <stdio.h>
#include <stdlib.h>
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;
// sizeof的单位为字节,正好可以存储2个十六进制数
test(&x, sizeof(x));
return EXIT_SUCCESS;
}
输出为:
33 22 11 00
可以用初始化器初始化指针:
指针运算:
&x[i]
获取指定序号元素的指针限定符 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);
区别在于const是修饰 p
还是 *p
。
结构类型无法把自己作为成员类型,但可以包含“指向自己类型”的指针成员:
struct list_node
{
struct list_node* prev;
struct list_node* next;
void* value;
};
定义不完整结构类型,只能使用小标签:
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;
在结构体内部使用匿名结构体成员,也是一种很常见的做法:
#include <stdio.h>
#include <stdlib.h>
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;
}
利用 stddef.h 中的 offsetof 宏可以获取结构成员的偏移量。
“不定长结构”就是在结构体尾部声明一个未指定长度的数组。用sizeof运算符时,该数组未计入结果。
示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct string
{
int length;
char chars[];
} string;
int main(int argc, char* argv[])
{
int len = sizeof(string) + 10; // 计算存储一个 10 字节长度的字符串(包括\0)所需的长度
char buf[len]; // 从栈上分配所需的内存空间
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;
}
对这类结构体进行拷贝的时候,尾部结构成员不会被复制,而且不能直接对弹性结构成员进行初始化。
示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct string
{
int length;
char chars[];
} string;
int main(int argc, char* argv[])
{
int len = sizeof(string) + 10;
char buf[len];
string *s = (string*)buf;
s->length = 9;
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;
}
联合和结构的区别在于:联合每次只能存储一个成员,联合的长度由最宽成员类型决定。
位字段:可以把结构或联合的多个成员“压缩存储”在一个字段中,以节约内存。
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
用来做标志位也挺好,比用位移运算符更直观,更节省内存。
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
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。
具有静态生存周期的对象,会被初始化为默认值0(指针为NULL)。
预处理指令以 #
开始(其前面可以有space或tab),通常独立一行,但可以用“ \
”换行。
编译器会展开替换掉宏。如:
#define SIZE 10
int main(int argc, char* argv[])
{
int x[SIZE] = {};
return EXIT_SUCCESS;
}
展开:
int main(int argc, char* argv[])
{
int x[10] = {};
return 0;
}
利用宏可以定义伪函数,通常用 ({...})
来组织多行语句,最后一个表达式作为返回值(无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;
}
可以使用“ #if ... #elif ... #else ... #endif
”、 #define
、 #undef
进行条件编译。