Clang-cheatsheets速查表
基本语法
语句
C 语言的代码由一行行语句(statement)组成。语句就是程序执行的一个操作命令。C 语言规定,语句必须使用分号结尾,除非有明确规定可以不写分号。
int x = 1;
int y; y = 1;
表达式
C 语言的代码由一行行语句(statement)组成。语句就是程序执行的一个操作命令。C 语言规定,语句必须使用分号结尾,除非有明确规定可以不写分号。
1 + 2
表达式与语句的区别主要是两点:
- 语句可以包含表达式,但是表达式本身不构成语句。
- 表达式都有返回值,语句不一定有。因为语句用来执行某个命令,很多时候不需要返回值,比如变量声明语句(int x = 1)就没有返回值。
语句块
C 语言允许多个语句使用一对大括号{},组成一个块,也称为复合语句(compounded statement)。在语法上,语句块可以视为多个语句组成的一个复合语句。
{
int x;
x = 1;
}
注释
单行注释
// 这是一行注释
int x = 1; // 这也是注释
多行注释
/* 注释 */
/*
这是一行注释
*/
int open(char* s /* file name */, int mode);
printf()
printf()的作用是将参数文本输出到屏幕。它名字里面的f代表format(格式化),表示可以定制输出文本的格式。
printf()不会在行尾自动添加换行符,为了让光标移到下一行的开头,可以在输出文本的结尾,添加一个换行符 \n 。
printf("Hello World\n");
printf() 占位符
printf()可以在输出文本中指定占位符。所谓“占位符”:
code | desc |
---|---|
%a |
浮点数 |
%A |
浮点数 |
%c |
字符 |
%d |
十进制整数 |
%e |
使用科学计数法的浮点数,指数部分的e为小写 |
%E |
使用科学计数法的浮点数,指数部分的E为大写 |
%i |
整数,基本等同于%d |
%f |
小数(包含float类型和double类型) |
%g |
6个有效数字的浮点数。整数部分一旦超过6位,就会自动转为科学计数法,指数部分的e为小写 |
%G |
等同于%g,唯一的区别是指数部分的E为大写 |
%hd |
十进制 short int 类型 |
%ho |
八进制 short int 类型 |
%hx |
十六进制 short int 类型 |
%hu |
unsigned short int 类型 |
%ld |
十进制 long int 类型 |
%lo |
八进制 long int 类型 |
%lx |
十六进制 long int 类型 |
%lu |
unsigned long int 类型 |
%lld |
十进制 long long int 类型 |
%llo |
八进制 long long int 类型 |
%llx |
十六进制 long long int 类型 |
%llu |
unsigned long long int 类型 |
%Le |
科学计数法表示的 long double 类型浮点数 |
%Lf |
long double 类型浮点数 |
%n |
已输出的字符串数量。该占位符本身不输出,只将值存储在指定变量之中 |
%o |
八进制整数 |
%p |
指针 |
%s |
字符串 |
%u |
无符号整数(unsigned int) |
%x |
十六进制整数 |
%zd |
size_t类型 |
%% |
输出一个百分号 |
printf() 输出格式
限定宽度
printf("%5d\n", 123); // 输出为 " 123"
printf("%-5d\n", 123); // 输出为 "123 "
%5d表示这个占位符的宽度至少为5位。如果不满5位,对应的值的前面会添加空格。-号表示左对齐。
// 输出 " 123.450000"
printf("%12f\n", 123.45);
%12f表示输出的浮点数最少要占据12位。
总是显示正负号
printf("%+d\n", 12); // 输出 +12
printf("%+d\n", -12); // 输出 -12
%+d可以确保输出的数值,总是带有正负号。
限定小数位数
// 输出为 " 0.50"
printf("%6.2f\n", 0.5);
最小宽度和小数位数这两个限定值,都可以用*代替,通过printf()的参数传入。
printf("%*.*f\n", 6, 2, 0.5);
输出部分字符串
%s占位符用来输出字符串,默认是全部输出。如果只想输出开头的部分,可以用%.[m]s
指定输出的长度,其中[m]
代表一个数字,表示所要输出的长度。
// 输出 hello
printf("%.5s\n", "hello world");
上面示例中,占位符%.5s表示只输出字符串hello world的前5个字符,即hello。
标准库,头文件
程序需要用到的功能,不一定需要自己编写,C 语言可能已经自带了。程序员只要去调用这些自带的功能,就省得自己编写代码了。举例来说,printf()这个函数就是 C 语言自带的,只要去调用它,就能实现在屏幕上输出内容。
C 语言自带的所有这些功能,统称为“标准库”(standard library),因为它们是写入标准的,到底包括哪些功能,应该怎么使用的,都是规定好的,这样才能保证代码的规范和可移植。
不同的功能定义在不同的文件里面,这些文件统称为“头文件”(header file)。如果系统自带某一个功能,就一定还会自带描述这个功能的头文件,比如printf()的头文件就是系统自带的stdio.h。头文件的后缀通常是.h。
如果要使用某个功能,就必须先加载对应的头文件,加载使用的是#include命令。这就是为什么使用printf()之前,必须先加载stdio.h的原因。
#include <stdio.h>
变量
变量名
变量名在 C 语言里面属于标识符(identifier),命名有严格的规范。
- 只能由字母(包括大写和小写)、数字和下划线(_)组成。
- 不能以数字开头。
- 长度不能超过63个字符。
关键字不能用作变量名。另外,C 语言还保留了一些词,供未来使用,这些保留字也不能用作变量名。下面就是 C 语言主要的关键字和保留字。
auto, break, case, char, const, continue, default, do, double, else,
enum, extern, float, for, goto, if, inline, int, long, register,
restrict, return, short, signed, sizeof, static, struct, switch,
typedef, union, unsigned, void, volatile, while
另外,两个下划线开头的变量名,以及一个下划线 + 大写英文字母开头的变量名,都是系统保留的,自己不应该起这样的变量名。
变量的声明
变量使用前必须先声明,每个变量都有自己的类型(type)。声明变量时,必须把变量的类型告诉编译器。
如果几个变量具有相同类型,可以在同一行声明。
int height, width;
// 等同于
int height;
int width;
变量的赋值
C 语言会在变量声明时,就为它分配内存空间,但是不会清除内存里面原来的值。这导致声明变量以后,变量会是一个随机的值。所以,变量一定要赋值以后才能使用。
int num = 42;
int x, y;
x = 1;
y = (x = 2 * x);
C 语言有左值(left value)和右值(right value)的概念。左值是可以放在赋值运算符左边的值,一般是变量;右值是可以放在赋值运算符右边的值,一般是一个具体的值。这是为了强调有些值不能放在赋值运算符的左边,比如x = 1是合法的表达式,但是1 = x就会报错。
变量的作用域
作用域(scope)指的是变量生效的范围。C 语言的变量作用域主要有两种:文件作用域(file scope)和块作用域(block scope)。
文件作用域(file scope)指的是,在源码文件顶层声明的变量,从声明的位置到文件结束都有效。
块作用域(block scope)指的是由大括号({})组成的代码块,它形成一个单独的作用域。凡是在块作用域里面声明的变量,只在当前代码块有效,代码块外部不可见。
运算符
算术运算符
code | desc |
---|---|
+ |
正值运算符(一元运算符) |
- |
负值运算符(一元运算符) |
+ |
加法运算符(二元运算符) |
- |
减法运算符(二元运算符) |
* |
乘法运算符 |
/ |
除法运算符 |
% |
余值运算符 |
赋值运算的简写形式,[运算符]=
,对自身的值进行算术运算。
自增运算符,自减运算符
code | desc |
---|---|
++ |
自增运算符 |
-- |
自减运算符 |
关系运算符
code | desc |
---|---|
> |
大于运算符 |
< |
小于运算符 |
>= |
大于等于运算符 |
<= |
小于等于运算符 |
== |
相等运算符 |
!= |
不相等运算符 |
关系表达式通常返回0或1,表示真伪。C 语言中,0表示伪,所有非零值表示真。
逻辑运算符
code | desc |
---|---|
! |
否运算符(改变单个表达式的真伪)。 |
&& |
与运算符(两侧的表达式都为真,则为真,否则为伪)。 |
\|\| |
或运算符(两侧至少有一个表达式为真,则为真,否则为伪)。 |
位运算符
code | desc |
---|---|
~ |
取反运算符 |
& |
与运算符 |
\| |
或运算符 |
^ |
异或运算符 |
<< |
左移运算符 |
>> |
右移运算符 |
位运算的简写形式,[运算符]=
,对自身的值进行位运算。
int val = 1;
val = val >> 2;
// 简写为
val >>= 2;
逗号运算符
逗号运算符用于将多个表达式写在一起,从左到右依次运行每个表达式。
x = 10, y = 20;
运算优先级
下面是部分运算符的优先级顺序(按照优先级从高到低排列)。
- 圆括号(
()
) - 自增运算符(
++
),自减运算符(--
) - 一元运算符(
+
和-
) - 乘法(
*
),除法(/
) - 加法(
+
),减法(-
) - 关系运算符(
<
、>
等) - 赋值运算符(
=
)
流程控制
if 语句
if语句用于条件判断,满足条件时,就执行指定的语句。
if (expression) statement
if语句可以带有else分支,指定条件不成立时(表达式expression的值为0),所要执行的代码。
if (expression) statement
else statement
else可以与另一个if语句连用,构成多重判断。
if (expression)
statement
else if (expression)
statement
...
else if (expression)
statement
else
statement
为了提供代码的可读性,建议使用大括号,明确else匹配哪一个if。
if (number > 6) {
if (number < 12) {
printf("The number is more than 6, less than 12.\n");
}
} else {
printf("It is wrong number.\n");
}
三元运算符 ?:
三元表达式?:,可以用作if...else的简写形式。
<expression1> ? <expression2> : <expression3>
这个操作符的含义是,表达式expression1如果为true(非0值),就执行expression2,否则执行expression3。
switch 语句
switch 语句是一种特殊形式的 if...else 结构,用于判断条件有多个结果的情况。它把多重的else if改成更易用、可读性更好的形式。
switch (expression) {
case value1: statement
case value2: statement
default: statement
}
每个case语句体的结尾,都应该有一个break语句,作用是跳出整个switch结构,不再往下执行。如果缺少break,就会导致继续执行下一个case或default分支。
switch (grade) {
case 0:
case 1:
printf("True");
break;
default:
printf("Illegal");
}
while 语句
while语句用于循环结构,满足条件时,不断执行循环体。
while (expression) {
statement;
statement;
}
只要条件为真,while会产生无限循环。下面是一种常见的无限循环的写法。
while (1) {
// ...
}
do...while 结构
do...while结构是while的变体,它会先执行一次循环体,然后再判断是否满足条件。如果满足的话,就继续执行循环体,否则跳出循环。
do statement
while (expression);
上面代码中,不管条件expression是否成立,循环体statement至少会执行一次。
for 语句
for语句是最常用的循环结构,通常用于精确控制循环次数。
for (initialization; continuation; action)
statement;
- initialization:初始化表达式,用于初始化循环变量,只执行一次。
- continuation:判断表达式,只要为true,就会不断执行循环体。
- action:循环变量处理表达式,每轮循环结束后执行,使得循环变量发生变化。
break 语句
break语句有两种用法:[1]一种是与switch语句配套使用,用来中断某个分支的执行, [2]另一种用法是在循环体内部跳出循环,不再进行后面的循环了。
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
printf("%d, %d\n", i, j);
break;
}
}
上面示例中,break语句使得循环跳到下一个i。
continue 语句
continue语句用于在循环体内部终止本轮循环,进入下一轮循环。
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
printf("%d, %d\n", i, j);
continue;
}
}
上面示例中,有没有continue语句,效果一样,都表示跳到下一个j。
goto 语句
goto 语句用于跳到指定的标签名。这会破坏结构化编程,建议不要轻易使用。
char ch;
top: ch = getchar();
if (ch == 'q')
goto top;
上面示例中,top是一个标签名,可以放在正常语句的前面,相当于为这行语句做了一个标记。程序执行到goto语句,就会跳转到它指定的标签名。
goto 只能在同一个函数之中跳转,并不能跳转到其他函数。
数据类型
基本数据类型有三种:字符(char)、整数(int)和浮点数(float)。复杂的类型都是基于它们构建的。
字符类型
字符类型指的是单个字符,类型声明使用char关键字。
char c = 'B';
字符常量必须放在单引号里面。
在计算机内部,字符类型使用一个字节(8位)存储。C 语言将其当作整数处理,所以字符类型就是宽度为一个字节的整数。每个字符对应一个整数(由 ASCII 码确定),比如B对应整数66。
字符类型在不同计算机的默认范围是不一样的。一些系统默认为-128到127,另一些系统默认为0到255。这两种范围正好都能覆盖0到127的 ASCII 字符范围。
只要在字符类型的范围之内,整数与字符是可以互换的,都可以赋值给字符类型的变量。
char c = 66;
// 等同于
char c = 'B';
两个字符类型的变量可以进行数学运算。
转义字符
一些特殊的字符,需要使用\
进行转义。
code | desc |
---|---|
\a |
警报,这会使得终端发出警报声或出现闪烁,或者两者同时发生 |
\b |
退格键,光标回退一个字符,但不删除字符 |
\f |
换页符,光标移到下一页。在现代系统上,这已经反映不出来了,行为改成类似于\v |
\n |
换行符 |
\r |
回车符,光标移到同一行的开头 |
\t |
制表符,光标移到下一个水平制表位,通常是下一个8的倍数 |
\v |
垂直分隔符,光标移到下一个垂直制表位,通常是下一行的同一列 |
\0 |
null 字符,代表没有内容。注意,这个值不等于数字0 |
转义写法还能使用八进制和十六进制表示一个字符。
code | desc |
---|---|
\nn |
字符的八进制写法,nn为八进制值 |
\xnn |
字符的十六进制写法,nn为十六进制值 |
char x = 'B';
char x = 66;
char x = '\102'; // 八进制
char x = '\x42'; // 十六进制
整数类型
整数类型用来表示较大的整数,类型声明使用int关键字。
不同计算机的int类型的大小是不一样的。比较常见的是使用4个字节(32位)存储一个int类型的值,但是2个字节(16位)或8个字节(64位)也有可能使用。
signed,unsigned
C 语言使用signed关键字,表示一个类型带有正负号,包含负值;使用unsigned关键字,表示该类型不带有正负号,只能表示零和正整数。
对于int类型,默认是带有正负号的,也就是说int等同于signed int。由于这是默认情况,关键字signed一般都省略不写,但是写了也不算错。
signed int a;
// 等同于
int a;
int类型也可以不带正负号,只表示非负整数。这时就必须使用关键字unsigned声明变量。
unsigned int里面的int可以省略,所以上面的变量声明也可以写成下面这样。
unsigned a;
整数的子类型
C 语言在int类型之外,又提供了三个整数的子类型。这样有利于更精细地限定整数变量的范围,也有利于更好地表达代码的意图。
- short int(简写为short):占用空间不多于int,一般占用2个字节(整数范围为-32768~32767)。
- long int(简写为long):占用空间不少于int,至少为4个字节。
- long long int(简写为long long):占用空间多于long,至少为8个字节。
默认情况下,short、long、long long都是带符号的(signed),即signed关键字省略了。它们也可以声明为不带符号(unsigned),使得能够表示的最大值扩大一倍。
不同的计算机,数据类型的字节长度是不一样的。确实需要32位整数时,应使用long类型而不是int类型,可以确保不少于4个字节;确实需要64位的整数时,应该使用long long类型,可以确保不少于8个字节。另一方面,为了节省空间,只需要16位整数时,应使用short类型;需要8位整数时,应该使用char类型。
整数类型的极限值
code | desc |
---|---|
SCHAR_MIN,SCHAR_MAX |
signed char 的最小值和最大值 |
SHRT_MIN,SHRT_MAX |
short 的最小值和最大值 |
INT_MIN,INT_MAX |
int 的最小值和最大值 |
LONG_MIN,LONG_MAX |
long 的最小值和最大值 |
LLONG_MIN,LLONG_MAX |
long long 的最小值和最大值 |
UCHAR_MAX |
unsigned char 的最大值 |
USHRT_MAX |
unsigned short 的最大值 |
UINT_MAX |
unsigned int 的最大值 |
ULONG_MAX |
unsigned long 的最大值 |
ULLONG_MAX |
unsigned long long 的最大值 |
整数的进制
C 语言的整数默认都是十进制数,如果要表示八进制数和十六进制数,必须使用专门的表示法。
八进制使用0作为前缀,比如017、0377。
int a = 012; // 八进制,相当于十进制的10
十六进制使用0x或0X作为前缀,比如0xf、0X10。
int a = 0x1A2B; // 十六进制,相当于十进制的6699
有些编译器使用0b前缀,表示二进制数,但不是标准。
int x = 0b101010;
注意,不同的进制只是整数的书写方法,不会对整数的实际存储方式产生影响。所有整数都是二进制形式存储,跟书写方式无关。不同进制可以混合使用,比如10 + 015 + 0x20是一个合法的表达式。
浮点数类型
任何有小数点的数值,都会被编译器解释为浮点数。所谓“浮点数”就是使用 m * be 的形式,存储一个数值,m是小数部分,b是基数(通常是2),e是指数部分。这种形式是精度和数值范围的一种结合,可以表示非常大或者非常小的数。
浮点数的类型声明使用float关键字,可以用来声明浮点数变量。
float类型占用4个字节(32位),其中8位存放指数的值和符号,剩下24位存放小数的值和符号。float类型至少能够提供(十进制的)6位有效数字,指数部分的范围为(十进制的)-37到37,即数值范围为10-37到1037。
有时候,32位浮点数提供的精度或者数值范围还不够,C 语言又提供了另外两种更大的浮点数类型。
- double:占用8个字节(64位),至少提供13位有效数字。
- long double:通常占用16个字节。
C 语言允许使用科学计数法表示浮点数,使用字母e来分隔小数部分和指数部分。
double x = 123.456e+3; // 123.456 x 10^3
// 等同于
double x = 123.456e3;
布尔类型
C 语言原来并没有为布尔值单独设置一个类型,而是使用整数0表示伪,所有非零值表示真。
C99 标准添加了类型_Bool,表示布尔值。但是,这个类型其实只是整数类型的别名,还是使用0表示伪,1表示真。加载头文件stdbool.h以后,就可以使用bool定义布尔值类型,以及false和true表示真伪。
字面量的类型
字面量(literal)指的是代码里面直接出现的值。
int x = 123;
上面代码中,x是变量,123就是字面量。
编译时,字面量也会写入内存,因此编译器必须为字面量指定数据类型,就像必须为变量指定数据类型一样。
编译器将一个整数字面量指定为int类型,但是程序员希望将其指定为long类型,这时可以为该字面量加上后缀l或L,编译器就知道要把这个字面量的类型指定为long。
code | desc |
---|---|
f和F |
float类型 |
l和L |
对于整数是long int类型,对于小数是long double类型 |
ll和LL |
Long Long 类型,比如3LL |
u和U |
表示unsigned int,比如15U、0377U |
溢出
每一种数据类型都有数值范围,如果存放的数值超出了这个范围(小于最小值或大于最大值),需要更多的二进制位存储,就会发生溢出。大于最大值,叫做向上溢出(overflow);小于最小值,叫做向下溢出(underflow)。
一般来说,编译器不会对溢出报错,会正常执行代码,但是会忽略多出来的二进制位,只保留剩下的位,这样往往会得到意想不到的结果。所以,应该避免溢出。
为了避免溢出,最好方法就是将运算结果与类型的极限值进行比较。
unsigned int ui;
unsigned int sum;
// 错误
if (sum + ui > UINT_MAX) too_big();
else sum = sum + ui;
// 正确
if (ui > UINT_MAX - sum) too_big();
else sum = sum + ui;
sizeof 运算符
sizeof是 C 语言提供的一个运算符,返回某种数据类型或某个值占用的字节数量。
C 语言提供了一个解决方法,创造了一个类型别名size_t,用来统一表示sizeof的返回值类型。
C 语言还提供了一个常量SIZE_MAX,表示size_t可以表示的最大整数。所以,size_t能够表示的整数范围为[0, SIZE_MAX]。
printf()有专门的占位符%zd或%zu,用来处理size_t类型的值。
printf("%zd\n", sizeof(int));
上面代码中,不管sizeof返回值的类型是什么,%zd占位符(或%zu)都可以正确输出。
如果当前系统不支持%zd或%zu,可使用%u(unsigned int)或%lu(unsigned long int)代替。
类型的自动转换
赋值运算
赋值运算符会自动将右边的值,转成左边变量的类型。
- 浮点数赋予整数变量时,C 语言直接丢弃小数部分,而不是四舍五入。
- 整数赋值给浮点数变量时,会自动转为浮点数。
- 窄类型自动转为宽类型。
- 宽类型赋值给窄类型,可能会发生截值(truncation),系统会自动截去多余的二进制位,导致难以预料的结果。
混合类型的运算
- 整数与浮点数混合运算时,整数转为浮点数类型,与另一个运算数类型相同。
- 不同的浮点数类型混合运算时,宽度较小的类型转为宽度较大的类型
- 不同的整数类型混合运算时,宽度较小的类型会提升为宽度较大的类型
最好避免无符号整数与有符号整数的混合运算。因为这时 C 语言会自动将signed int转为unsigned int,可能不会得到预期的结果。
函数
函数的参数和返回值,会自动转成函数定义里指定的类型。
类型的显式转换
原则上,应该避免类型的自动转换,防止出现意料之外的结果。
在一个值或变量的前面,使用圆括号指定类型(type),就可以将这个值或变量转为指定的类型,这叫做“类型指定”(casting)。
可移植类型
控制准确的字节宽度,代码可以有更好的可移植性,头文件stdint.h创造了一些新的类型别名。
精确宽度类型(exact-width integer type)
code | desc | code | desc |
---|---|---|---|
int8_t |
8位有符号整数 | int16_t |
16位有符号整数 |
int32_t |
32位有符号整数 | int64_t |
64位有符号整数 |
uint8_t |
8位无符号整数 | uint16_t |
16位无符号整数 |
uint32_t |
32位无符号整数 | uint64_t |
64位无符号整数 |
上面这些都是类型别名,编译器会指定它们指向的底层类型。比如,某个系统中,如果int类型为32位,int32_t就会指向int;如果long类型为32位,int32_t则会指向long。
最小宽度类型(minimum width type),保证某个整数类型的最小长度
- int_least8_t
- int_least16_t
- int_least32_t
- int_least64_t
- uint_least8_t
- uint_least16_t
- uint_least32_t
- uint_least64_t
最快的最小宽度类型(fast minimum width type),可以使整数计算达到最快的类型
- int_fast8_t
- int_fast16_t
- int_fast32_t
- int_fast64_t
- uint_fast8_t
- uint_fast16_t
- uint_fast32_t
- uint_fast64_t
某些机器对于特定宽度的数据,运算速度最快,举例来说,32位计算机对于32位数据的运算速度,会快于16位数据。
可以保存指针的整数类型
- intptr_t:可以存储指针(内存地址)的有符号整数类型。
- uintptr_t:可以存储指针的无符号整数类型。
最大宽度整数类型,用于存放最大的整数
- intmax_t:可以存储任何有效的有符号整数的类型。
- uintmax_t:可以存放任何有效的无符号整数的类型。
指针
指针是什么?首先,它是一个值,这个值代表一个内存地址,因此指针相当于指向某个内存地址的路标。
字符*表示指针,通常跟在类型关键字的后面。
int* intPtr;
上面示例声明了一个变量intPtr,它是一个指针,指向的内存地址存放的是一个整数。
星号*可以放在变量名与类型关键字之间的任何地方,下面的写法都是有效的。
int *intPtr;
int * intPtr;
int* intPtr;
// 正确
int * foo, * bar;
// 错误
int* foo, bar;
一个指针指向的可能还是指针,这时就要用两个星号**表示。
int** foo;
* 运算符
*这个符号除了表示指针以外,还可以作为运算符,用来取出指针变量所指向的内存地址里面的值。
void increment(int* p) {
*p = *p + 1;
}
上面示例中,函数increment()的参数是一个整数指针p。函数体里面,*p
就表示指针p所指向的那个值。对*p
赋值,就表示改变指针所指向的那个地址里面的值。函数体内部对该地址包含的值的操作,会影响到函数外部,所以不需要返回值。事实上,函数内部通过指针,将值传到外部,是 C 语言的常用方法。
对于需要大量存储空间的大型变量,复制变量值传入函数,非常浪费时间和空间,不如传入指针来得高效。
& 运算符
&运算符用来取出一个变量所在的内存地址。
void increment(int* p) {
*p = *p + 1;
}
int x = 1;
increment(&x);
printf("%d\n", x); // 2
&运算符与*运算符互为逆运算,下面的表达式总是成立。
int i = 5;
if (i == *(&i)) // 正确
指针变量的初始化
声明指针变量之后,指针变量指向的值是随机的,必须先让它指向一个分配好的地址,然后再进行读写,这叫做指针变量的初始化。
int i;
int* p = &i;
*p = 13;
为了防止读写未初始化的指针变量,可以养成习惯,将未初始化的指针变量设为NULL。
int* p = NULL;
NULL在 C 语言中是一个常量,表示地址为0的内存空间,这个地址是无法使用的,读写该地址会报错。
指针的运算
指针本质上就是一个无符号整数,代表了内存地址。它可以进行运算,但是规则并不是整数运算的规则。
指针与整数值的加减运算
指针与整数值的运算,表示指针的移动。指针移动的单位,与指针指向的数据类型有关。数据类型占据多少个字节,每单位就移动多少个字节。
short* j;
j = (short*)0x1234;
j = j + 1; // 0x1236
j + 1表示指针向内存地址的高位移动一个单位,而一个单位的short类型占据两个字节的宽度,所以相当于向高位移动两个字节。
指针与指针的加法运算
指针只能与整数值进行加减运算,两个指针进行加法是非法的。
指针与指针的减法
相同类型的指针允许进行减法运算,返回它们之间的距离,即相隔多少个数据单位。
减法返回的值属于ptrdiff_t类型,这是一个带符号的整数类型别名,具体类型根据系统不同而不同。这个类型的原型定义在头文件stddef.h里面。
指针与指针的比较运算
指针之间的比较运算,比较的是各自的内存地址哪一个更大,返回值是整数1(true)或0(false)。
函数
简介
函数是一段可以重复执行的代码。它可以接受不同的参数,完成对应的操作。函数声明的语法有以下几点:
- 返回值类型。不返回值的函数,使用void关键字表示返回值的类型。
- 参数。没有参数的函数,声明时要用void关键字表示参数类型。
- 函数体
- return语句。return语句给出函数的返回值,程序运行到这一行,就会跳出函数体,结束函数的调用。如果函数没有返回值,可以省略return语句,或者写成return;。
C 语言标准规定,函数只能声明在源码文件的顶层,不能声明在其他函数内部。
main()
C 语言规定,main()是程序的入口函数,即所有的程序一定要包含一个main()函数。
C 语言约定,返回值0表示函数运行成功,如果返回其他非零整数,就表示运行失败,代码出了问题。
正常情况下,如果main()里面省略return 0这一行,编译器会自动加上,即main()的默认返回值为0。
参数的传值引用
如果函数的参数是一个变量,那么调用时,传入的是这个变量的值的拷贝,而不是变量本身。
如果想要传入变量本身,只有一个办法,就是传入变量的地址(指针)。
函数不要返回内部变量的指针,因为当函数结束运行时,内部变量就消失了,这时指向内部变量内存地址就是无效的,再去使用这个地址是非常危险的。
函数指针
函数本身就是一段内存里面的代码,C 语言允许通过指针获取函数。
void print(int a) {
printf("%d\n", a);
}
void (*print_ptr)(int) = &print;
(*print_ptr)
一定要写在圆括号里面,否则函数参数(int)的优先级高于*
。
(*print_ptr)(10);
// 等同于
print(10);
比较特殊的是,C 语言还规定,函数名本身就是指向函数代码的指针,通过函数名就能获取函数地址。也就是说,print和&print是一回事。
为了简洁易读,一般情况下,函数名前面都不加*和&。
这种特性的一个应用是,如果一个函数的参数或返回值,也是一个函数,那么函数原型可以写成下面这样。
int compute(int (*myfunc)(int), int, int);
上面示例可以清晰地表明,函数compute()的第一个参数也是一个函数。
函数原型
只要在程序开头处给出函数原型,函数就可以先使用、后声明。所谓函数原型,就是提前告诉编译器,每个函数的返回类型和参数类型。其他信息都不需要,也不用包括函数体,具体的函数实现可以后面再补上。
int twice(int);
int main(int num) {
return twice(num);
}
int twice(int num) {
return 2 * num;
}
exit()
exit()函数用来终止整个程序的运行。一旦执行到该函数,程序就会立即结束。该函数的原型定义在头文件stdlib.h里面。
exit()可以向程序外部返回一个值,它的参数就是程序的返回值。一般来说,使用两个常量作为它的参数:EXIT_SUCCESS(相当于 0)表示程序运行成功,EXIT_FAILURE(相当于 1)表示程序异常中止。这两个常数也是定义在stdlib.h里面。
// 程序运行成功
// 等同于 exit(0);
exit(EXIT_SUCCESS);
// 程序异常中止
// 等同于 exit(1);
exit(EXIT_FAILURE);
C 语言还提供了一个atexit()函数,用来登记exit()执行时额外执行的函数,用来做一些退出程序时的收尾工作。该函数的原型也是定义在头文件stdlib.h。
int atexit(void (*func)(void));
函数说明符
C 语言提供了一些函数说明符,让函数用法更加明确。
extern 说明符
对于多文件的项目,源码文件会用到其他文件声明的函数。这时,当前文件里面,需要给出外部函数的原型,并用extern说明该函数的定义来自其他文件。
不过,由于函数原型默认就是extern,所以这里不加extern,效果是一样的。
static 说明符
static用于函数内部声明变量时,表示该变量只需要初始化一次,不需要在每次调用时都进行初始化。也就是说,它的值在两次调用之间保持不变。
#include <stdio.h>
void counter(void) {
static int count = 1; // 只初始化一次
printf("%d\n", count);
count++;
}
int main(void) {
counter(); // 1
counter(); // 2
}
static修饰的变量初始化时,只能赋值为常量,不能赋值为变量。
在块作用域中,static声明的变量有默认值0。
static可以用来修饰函数本身。
static也可以用在参数里面,修饰参数数组。
int sum_array(int a[static 3], int n) {
// ...
}
上面示例中,static对程序行为不会有任何影响,只是用来告诉编译器,该数组长度至少为3,某些情况下可以加快程序运行速度。另外,需要注意的是,对于多维数组的参数,static仅可用于第一维的说明。
const 说明符
函数参数里面的const说明符,表示函数内部不得修改该参数变量。
void f(const int* p) {
int x = 13;
p = &x; // 允许修改
*p = 0; // 该行报错
}
上面示例中,声明函数时,const指定不能修改指针p指向的值,而p本身的地址是可以修改的。
如果想限制修改p,可以把const放在p前面。
void f(int* const p) {
int x = 13;
p = &x; // 该行报错
}
如果想同时限制修改p和*p
,需要使用两个const。
void f(const int* const p) {
// ...
}
可变参数
可以使用省略号...表示可变数量的参数,必须放在参数序列的结尾,否则会报错。
头文件stdarg.h定义了一些宏,可以操作可变参数。
- va_list:一个数据类型,用来定义一个可变参数对象。它必须在操作可变参数时,首先使用。
- va_start:一个函数,用来初始化可变参数对象。它接受两个参数,第一个参数是可变参数对象,第二个参数是原始函数里面,可变参数之前的那个参数,用来为可变参数定位。
- va_arg:一个函数,用来取出当前那个可变参数,每次调用后,内部指针就会指向下一个可变参数。它接受两个参数,第一个是可变参数对象,第二个是当前可变参数的类型。
- va_end:一个函数,用来清理可变参数对象。
double average(int i, ...) {
double total = 0;
va_list ap;
va_start(ap, i);
for (int j = 1; j <= i; ++j) {
total += va_arg(ap, double);
}
va_end(ap);
return total / i;
}
上面示例中,va_list ap定义ap为可变参数对象,va_start(ap, i)将参数i后面的参数统一放入ap,va_arg(ap, double)用来从ap依次取出一个参数,并且指定该参数为 double 类型,va_end(ap)用来清理可变参数对象。
数组
简介
声明数组时,必须给出数组的大小,数组的成员从0开始编号。
数组名后面使用方括号指定编号,就可以引用该成员。也可以通过该方式,对该位置进行赋值。
int scores[100];
scores[0] = 13;
scores[99] = 42;
越界访问数组不会报错,使用时必须小心。
数组也可以在声明时,使用大括号,同时对每一个成员赋值:
int a[5] = {22, 37, 3490, 18, 95};
如果大括号里面的值,少于数组的成员数量,那么未赋值的成员自动初始化为0。如果要将整个数组的每一个成员都设置为零,最简单的写法就是下面这样。
int a[100] = {0};
数组初始化时,可以指定为哪些位置的成员赋值。
int a[15] = {[2] = 29, [9] = 7, [14] = 48};
数组长度
sizeof运算符会返回整个数组的字节长度。
int a[] = {22, 37, 3490};
int arrLen = sizeof(a); // 12
由于数组成员都是同一个类型,每个成员的字节长度都是一样的,所以数组整体的字节长度除以某个数组成员的字节长度,就可以得到数组的成员数量。
sizeof(a) / sizeof(a[0])
注意,sizeof返回值的数据类型是size_t,所以sizeof(a) / sizeof(a[0])的数据类型也是size_t。在printf()里面的占位符,要用%zd或%zu。
多维数组
C 语言允许声明多个维度的数组,有多少个维度,就用多少个方括号,比如二维数组就使用两个方括号。
int board[10][10];
board[0][0] = 13;
board[9][9] = 13;
注意,board[0][0]
不能写成board[0, 0]
,因为0, 0
是一个逗号表达式,返回第二个值,所以board[0, 0]
等同于board[0]
。
多维数组也可以使用大括号,一次性对所有成员赋值。
变长数组
数组声明的时候,数组长度除了使用常量,也可以使用变量。这叫做变长数组(variable-length array,简称 VLA)。
int n = x + y;
int arr[n];
变长数组的根本特征,就是数组长度只有运行时才能确定。
数组的地址
数组是一连串连续储存的同类型值,只要获得起始地址(首个成员的内存地址),就能推算出其他成员的地址。
int a[5] = {11, 22, 33, 44, 55};
int* p;
p = &a[0];
printf("%d\n", *p); // Prints "11"
上面示例中,&a[0]
就是数组a的首个成员11的内存地址,也是整个数组的起始地址。反过来,从这个地址(*p
),可以获得首个成员的值11。
由于数组的起始地址是常用操作,&array[0]
的写法有点麻烦,C 语言提供了便利写法,数组名等同于起始地址,也就是说,数组名就是指向第一个成员(array[0]
)的指针。
数组指针的加减法
数组名可以进行加法和减法运算,等同于在数组成员之间前后移动,即从一个成员的内存地址移动到另一个成员的内存地址。比如,a + 1返回下一个成员的地址,a - 1返回上一个成员的地址。
int a[5] = {11, 22, 33, 44, 55};
for (int i = 0; i < 5; i++) {
printf("%d\n", *(a + i));
}
上面示例中,通过指针的移动遍历数组,a + i的每轮循环每次都会指向下一个成员的地址,*(a + i)
取出该地址的值,等同于a[i]
。对于数组的第一个成员,*(a + 0)
(即*a
)等同于a[0]
。
由于数组名与指针是等价的,所以下面的等式总是成立。
a[b] == *(a + b)
上面代码给出了数组成员的两种访问方式,一种是使用方括号a[b]
,另一种是使用指针*(a + b)。
如果指针变量p指向数组的一个成员,那么p++就相当于指向下一个成员,这种方法常用来遍历数组。
int a[] = {11, 22, 33, 44, 55, 999};
int* p = a;
while (*p != 999) {
printf("%d\n", *p);
p++;
}
遍历数组一般都是通过数组长度的比较来实现,但也可以通过数组起始地址和结束地址的比较来实现。
int sum(int* start, int* end) {
int total = 0;
while (start < end) {
total += *start;
start++;
}
return total;
}
int arr[5] = {20, 10, 5, 39, 4};
printf("%i\n", sum(arr, arr + 5));
数组的复制
由于数组名是指针,所以复制数组不能简单地复制数组名。
for (i = 0; i < N; i++)
a[i] = b[i];
另一种方法是使用memcpy()函数(定义在头文件string.h),直接把数组所在的那一段内存,再复制一份。
memcpy(a, b, sizeof(b));
作为函数的参数
声明参数数组
数组作为函数的参数,一般会同时传入数组名和数组长度。
int sum_array(int a[], int n) {
// ...
}
int a[] = {3, 5, 7, 3};
int sum = sum_array(a, 4);
变长数组作为参数
int sum_array(int n, int a[n]) {
// ...
}
int a[] = {3, 5, 7, 3};
int sum = sum_array(4, a);
变量n作为参数时,顺序一定要在变长数组前面,这样运行时才能确定数组a[n]
的长度,否则就会报错。
因为函数原型可以省略参数名,所以变长数组的原型中,可以使用*
代替变量名,也可以省略变量名。
int sum_array(int, int [*]);
int sum_array(int, int []);
变长数组作为函数参数有一个好处,就是多维数组的参数声明,可以把后面的维度省掉了。
// 原来的写法
int sum_array(int a[][4], int n);
// 变长数组的写法
int sum_array(int n, int m, int a[n][m]);
上面示例中,函数sum_array()的参数是一个多维数组,按照原来的写法,一定要声明第二维的长度。但是使用变长数组的写法,就不用声明第二维长度了,因为它可以作为参数传入函数。
数组字面量作为参数
// 数组变量作为参数
int a[] = {2, 3, 4, 5};
int sum = sum_array(a, 4);
// 数组字面量作为参数
int sum = sum_array((int []){2, 3, 4, 5}, 4);
上面示例中,两种写法是等价的。第二种写法省掉了数组变量的声明,直接将数组字面量传入函数。{2, 3, 4, 5}
是数组值的字面量,(int [])
类似于强制的类型转换,告诉编译器怎么理解这组值。
字符串
简介
C 语言没有单独的字符串类型,字符串被当作字符数组,即char类型的数组。比如,字符串Hello是当作数组{'H', 'e', 'l', 'l', 'o'}
处理的。
编译器会给数组分配一段连续内存,所有字符储存在相邻的内存单元之中。在字符串结尾,C 语言会自动添加一个全是二进制0的字节,写作\0字符,表示字符串结束。字符\0不同于字符0,前者的 ASCII 码是0(二进制形式00000000),后者的 ASCII 码是48(二进制形式00110000)。所以,字符串“Hello”实际储存的数组是{'H', 'e', 'l', 'l', 'o', '\0'}
char localString[10];
上面示例声明了一个10个成员的字符数组,可以当作字符串。由于必须留一个位置给\0,所以最多只能容纳9个字符的字符串。
字符串写成数组的形式,是非常麻烦的。C 语言提供了一种简写法,双引号之中的字符,会被自动视为字符数组。
如果字符串过长,可以在需要折行的地方,使用反斜杠(\)结尾,将一行拆成多行。
"hello \
world"
C 语言允许合并多个字符串字面量,只要这些字符串之间没有间隔,或者只有空格,C 语言会将它们自动合并。
char greeting[50] = "Hello, ""how are you ""today!";
// 等同于
char greeting[50] = "Hello, how are you today!";
char greeting[50] = "Hello, "
"how are you "
"today!";
printf()使用占位符%s输出字符串。
printf("%s\n", "hello world")
字符串变量的声明
字符串变量可以声明成一个字符数组,也可以声明成一个指针,指向字符数组。
// 写法一
char s[14] = "Hello, world!";
// 写法二
char* s = "Hello, world!";
// 写法三 (编译器自动计算数组长度)
char s[] = "Hello, world!";
字符指针和字符数组,这两种声明字符串变量的写法基本是等价的,但是有两个差异。
第一个差异是,指针指向的字符串,在 C 语言内部被当作常量,不能修改字符串本身。
char* s = "Hello, world!";
s[0] = 'z'; // 错误
如果使用数组声明字符串变量,就没有这个问题,可以修改数组的任意成员。
char s[] = "Hello, world!";
s[0] = 'z';
原因是系统会将字符串的字面量保存在内存的常量区,这个区是不允许用户修改的。声明为指针时,指针变量存储的只是一个指向常量区的内存地址,因此用户不能通过这个地址去修改常量区。但是,声明为数组时,编译器会给数组单独分配一段内存,字符串字面量会被编译器解释成字符数组,逐个字符写入这段新分配的内存之中,而这段新内存是允许修改的。
为了提醒用户,字符串声明为指针后不得修改,可以在声明时使用const说明符,保证该字符串是只读的。
const char* s = "Hello, world!";
第二个差异是,指针变量可以指向其它字符串。
char* s = "hello";
s = "world";
但是,字符数组变量不能指向另一个字符串。
char s[] = "hello";
s = "world"; // 报错
原因是数组变量所在的地址无法改变,或者说,编译器一旦为数组变量分配地址后,这个地址就绑定这个数组变量了,这种绑定关系是不变的。C 语言也因此规定,数组变量是一个不可修改的左值,即不能用赋值运算符为它重新赋值。
想要重新赋值,必须使用 C 语言原生提供的strcpy()函数,通过字符串拷贝完成赋值,数组变量的地址还是不变的。
strlen()
strlen()函数返回字符串的字节长度,不包括末尾的空字符\0。该函数的原型如下。
// string.h
size_t strlen(const char* s);
返回的是size_t类型的无符号整数,除非是极长的字符串,一般情况下当作int类型处理即可。
char* str = "hello";
int len = strlen(str); // 5
strcpy()
字符串的复制,不能使用赋值运算符,直接将一个字符串赋值给字符数组变量。
char str1[10];
char str2[10];
str1 = "abc"; // 报错
str2 = str1; // 报错
因为数组的变量名是一个固定的地址,不能修改,使其指向另一个地址。
如果是字符指针,赋值运算符(=)只是将一个指针的地址复制给另一个指针,而不是复制字符串。
char* s1;
char* s2;
s1 = "abc";
s2 = s1;
两个指针变量s1和s2指向同一字符串,而不是将字符串s1的内容复制给s2。
C 语言提供了strcpy()函数,用于将一个字符串的内容复制到另一个字符串,相当于字符串赋值。该函数的原型定义在string.h头文件里面。
strcpy(char dest[], const char source[])
strcpy()的返回值是一个字符串指针(即char*),指向第一个参数。
char* s1 = "beast";
char s2[40] = "Be the best that you can be.";
char* ps;
ps = strcpy(s2 + 7, s1);
puts(s2); // Be the beast
puts(ps); // beast
上面示例中,从s2的第7个位置开始拷贝字符串beast,前面的位置不变。这导致s2后面的内容都被截去了,因为会连beast结尾的空字符一起拷贝。strcpy()返回的是一个指针,指向拷贝开始的位置。
strcpy()函数有安全风险,因为它并不检查目标字符串的长度,是否足够容纳源字符串的副本,可能导致写入溢出。如果不能保证不会发生溢出,建议使用strncpy()函数代替。
strncpy()
strncpy()跟strcpy()的用法完全一样,只是多了第3个参数,用来指定复制的最大字符数,防止溢出目标字符串变量的边界。
char *strncpy(
char *dest,
char *src,
size_t n
);
达到最大字符数以后,源字符串仍然没有复制完,就会停止复制,这时目的字符串结尾将没有终止符\0,这一点务必注意。如果源字符串的字符数小于n,则strncpy()的行为与strcpy()完全一致。
strncpy(str1, str2, sizeof(str1) - 1);
str1[sizeof(str1) - 1] = '\0';
上面示例中,字符串str2复制给str1,但是复制长度最多为str1的长度减去1,str1剩下的最后一位用于写入字符串的结尾标志\0。这是因为strncpy()不会自己添加\0,如果复制的字符串片段不包含结尾标志,就需要手动添加。
strncpy()也可以用来拷贝部分字符串。
char s1[40];
char s2[12] = "hello world";
strncpy(s1, s2, 5);
s1[5] = '\0';
printf("%s\n", s1); // hello
上面示例中,指定只拷贝前5个字符。
strcat()
strcat()函数用于连接字符串。它接受两个字符串作为参数,把第二个字符串的副本添加到第一个字符串的末尾。这个函数会改变第一个字符串,但是第二个字符串不变。
该函数的原型定义在string.h头文件里面。
char* strcat(char* s1, const char* s2);