C语言 cheatsheet
基本语法
语句
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 * b
e 的形式,存储一个数值,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);