返回
首页

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()可以在输出文本中指定占位符。所谓“占位符”:

codedesc
%a浮点数
%A浮点数
%c字符
%d十进制整数
%e使用科学计数法的浮点数,指数部分的e为小写
%E使用科学计数法的浮点数,指数部分的E为大写
%i整数,基本等同于%d
%f小数(包含float类型和double类型)
%g6个有效数字的浮点数。整数部分一旦超过6位,就会自动转为科学计数法,指数部分的e为小写
%G等同于%g,唯一的区别是指数部分的E为大写
%hd十进制 short int 类型
%ho八进制 short int 类型
%hx十六进制 short int 类型
%huunsigned short int 类型
%ld十进制 long int 类型
%lo八进制 long int 类型
%lx十六进制 long int 类型
%luunsigned long int 类型
%lld十进制 long long int 类型
%llo八进制 long long int 类型
%llx十六进制 long long int 类型
%lluunsigned long long int 类型
%Le科学计数法表示的 long double 类型浮点数
%Lflong double 类型浮点数
%n已输出的字符串数量。该占位符本身不输出,只将值存储在指定变量之中
%o八进制整数
%p指针
%s字符串
%u无符号整数(unsigned int)
%x十六进制整数
%zdsize_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)指的是由大括号({})组成的代码块,它形成一个单独的作用域。凡是在块作用域里面声明的变量,只在当前代码块有效,代码块外部不可见。

运算符

算术运算符

codedesc
+正值运算符(一元运算符)
-负值运算符(一元运算符)
+加法运算符(二元运算符)
-减法运算符(二元运算符)
*乘法运算符
/除法运算符
%余值运算符

赋值运算的简写形式,[运算符]=,对自身的值进行算术运算。

自增运算符,自减运算符

codedesc
++自增运算符
--自减运算符

关系运算符

codedesc
> 大于运算符
< 小于运算符
>=大于等于运算符
<=小于等于运算符
==相等运算符
!=不相等运算符

关系表达式通常返回01,表示真伪。C 语言中,0表示伪,所有非零值表示真。

逻辑运算符

codedesc
!否运算符(改变单个表达式的真伪)。
&&与运算符(两侧的表达式都为真,则为真,否则为伪)。
||或运算符(两侧至少有一个表达式为真,则为真,否则为伪)。

位运算符

codedesc
取反运算符
&与运算符
|或运算符
^异或运算符
<<左移运算符
>>右移运算符

位运算的简写形式,[运算符]=,对自身的值进行位运算。

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,就会导致继续执行下一个casedefault分支。

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

字符类型在不同计算机的默认范围是不一样的。一些系统默认为-128127,另一些系统默认为0255。这两种范围正好都能覆盖0127ASCII 字符范围。

只要在字符类型的范围之内,整数与字符是可以互换的,都可以赋值给字符类型的变量。

char c = 66;
// 等同于
char c = 'B';

两个字符类型的变量可以进行数学运算

转义字符

一些特殊的字符,需要使用\进行转义。

codedesc
\a警报,这会使得终端发出警报声或出现闪烁,或者两者同时发生
\b退格键,光标回退一个字符,但不删除字符
\f换页符,光标移到下一页。在现代系统上,这已经反映不出来了,行为改成类似于\v
\n换行符
\r回车符,光标移到同一行的开头
\t制表符,光标移到下一个水平制表位,通常是下一个8的倍数
\v垂直分隔符,光标移到下一个垂直制表位,通常是下一行的同一列
\0null 字符,代表没有内容。注意,这个值不等于数字0

转义写法还能使用八进制和十六进制表示一个字符。

codedesc
\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个字节(整数范围为-3276832767)
  • long int(简写为long):占用空间不少于int,至少为4个字节。
  • long long int(简写为long long):占用空间多于long,至少为8个字节。

默认情况下,shortlonglong long都是带符号的(signed),即signed关键字省略了。它们也可以声明为不带符号(unsigned),使得能够表示的最大值扩大一倍。

不同的计算机,数据类型的字节长度是不一样的。确实需要32位整数时,应使用long类型而不是int类型,可以确保不少于4个字节;确实需要64位的整数时,应该使用long long类型,可以确保不少于8个字节。另一方面,为了节省空间,只需要16位整数时,应使用short类型;需要8位整数时,应该使用char类型。

整数类型的极限值

codedesc
SCHAR_MIN,SCHAR_MAXsigned char 的最小值和最大值
SHRT_MIN,SHRT_MAXshort 的最小值和最大值
INT_MIN,INT_MAXint 的最小值和最大值
LONG_MIN,LONG_MAXlong 的最小值和最大值
LLONG_MIN,LLONG_MAXlong long 的最小值和最大值
UCHAR_MAXunsigned char 的最大值
USHRT_MAXunsigned short 的最大值
UINT_MAXunsigned int 的最大值
ULONG_MAXunsigned long 的最大值
ULLONG_MAXunsigned long long 的最大值

整数的进制

C 语言的整数默认都是十进制数,如果要表示八进制数和十六进制数,必须使用专门的表示法。

八进制使用0作为前缀,比如0170377

int a = 012; // 八进制,相当于十进制的10

十六进制使用0x0X作为前缀,比如0xf0X10

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位有效数字,指数部分的范围为(十进制的)-3737,即数值范围为10-371037

有时候,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定义布尔值类型,以及falsetrue表示真伪。

字面量的类型

字面量(literal)指的是代码里面直接出现的值。

int x = 123;

上面代码中,x是变量,123就是字面量。

编译时,字面量也会写入内存,因此编译器必须为字面量指定数据类型,就像必须为变量指定数据类型一样。

编译器将一个整数字面量指定为int类型,但是程序员希望将其指定为long类型,这时可以为该字面量加上后缀lL,编译器就知道要把这个字面量的类型指定为long

codedesc
f和Ffloat类型
l和L对于整数是long int类型,对于小数是long double类型
ll和LLLong 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 运算符

sizeofC 语言提供的一个运算符,返回某种数据类型或某个值占用的字节数量。

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,可使用%uunsigned int)或%luunsigned long int)代替。

类型的自动转换

赋值运算

赋值运算符会自动将右边的值,转成左边变量的类型。

  • 浮点数赋予整数变量时,C 语言直接丢弃小数部分,而不是四舍五入。
  • 整数赋值给浮点数变量时,会自动转为浮点数。
  • 窄类型自动转为宽类型。
  • 宽类型赋值给窄类型,可能会发生截值(truncation),系统会自动截去多余的二进制位,导致难以预料的结果。

混合类型的运算

  • 整数与浮点数混合运算时,整数转为浮点数类型,与另一个运算数类型相同。
  • 不同的浮点数类型混合运算时,宽度较小的类型转为宽度较大的类型
  • 不同的整数类型混合运算时,宽度较小的类型会提升为宽度较大的类型

最好避免无符号整数与有符号整数的混合运算。因为这时 C 语言会自动将signed int转为unsigned int,可能不会得到预期的结果。

函数

函数的参数和返回值,会自动转成函数定义里指定的类型。

类型的显式转换

原则上,应该避免类型的自动转换,防止出现意料之外的结果。

在一个值或变量的前面,使用圆括号指定类型(type),就可以将这个值或变量转为指定的类型,这叫做“类型指定”(casting)。

可移植类型

控制准确的字节宽度,代码可以有更好的可移植性,头文件stdint.h创造了一些新的类型别名。

精确宽度类型(exact-width integer type)

codedesccodedesc
int8_t8位有符号整数int16_t16位有符号整数
int32_t32位有符号整数int64_t64位有符号整数
uint8_t8位无符号整数uint16_t16位无符号整数
uint32_t32位无符号整数uint64_t64位无符号整数

上面这些都是类型别名,编译器会指定它们指向的底层类型。比如,某个系统中,如果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;

NULLC 语言中是一个常量,表示地址为0的内存空间,这个地址是无法使用的,读写该地址会报错。

指针的运算

指针本质上就是一个无符号整数,代表了内存地址。它可以进行运算,但是规则并不是整数运算的规则。

指针与整数值的加减运算

指针与整数值的运算,表示指针的移动。指针移动的单位,与指针指向的数据类型有关。数据类型占据多少个字节,每单位就移动多少个字节。

short* j;
j = (short*)0x1234;
j = j + 1; // 0x1236

j + 1表示指针向内存地址的高位移动一个单位,而一个单位的short类型占据两个字节的宽度,所以相当于向高位移动两个字节。

指针与指针的加法运算

指针只能与整数值进行加减运算,两个指针进行加法是非法的。

指针与指针的减法

相同类型的指针允许进行减法运算,返回它们之间的距离,即相隔多少个数据单位。

减法返回的值属于ptrdiff_t类型,这是一个带符号的整数类型别名,具体类型根据系统不同而不同。这个类型的原型定义在头文件stddef.h里面。

指针与指针的比较运算

指针之间的比较运算,比较的是各自的内存地址哪一个更大,返回值是整数1true)或0false)。

函数

简介

函数是一段可以重复执行的代码。它可以接受不同的参数,完成对应的操作。函数声明的语法有以下几点:

  • 返回值类型。不返回值的函数,使用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后面的参数统一放入apva_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(a0)的数据类型也是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;

两个指针变量s1s2指向同一字符串,而不是将字符串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的长度减去1str1剩下的最后一位用于写入字符串的结尾标志\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);