C语言语法基础总结

C语言语法基础总结

It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures. [1]

关键概念

1. 位,字节和字

位(bit): 最小的存储单元,也称比特位。可以存储 0 或 1(或者说,位用于存储“开”或“关”)

字节(byte): 1 byte = 8 bit 既然 1 位可以表示 0 或 1,那么 1 字节就有 256 (2^8)种 0/1 组合,通过二进制编码(仅用 0/1 便表示数字),便可表示 0 ~ 255 的整数或一组字符。

字(word): 是设计计算机时给定的自然存储单位。对于 8 位 的微型计算机(如:最初的苹果机),1 字长 只有 8 位,从那以后,个人计算机的字长增至 16 位,32位,直至目前的 64位。计算机字长越大,其数据转移越快,允许访问的内存越多。

2. 数据结构: 数据的表现形式,类型+组织形式

3. C语言基本程序结构

4. 类型

每一个变量都有类型(type)。类型用来描述变量的数据的种类,也称数据类型

数值型变量的类型决定了变量所能存储的最大值与最小值,以及是否允许小数点后出现数字。

int(integer):即整型,表示整数。

数据类型还有很多,目前除了 int 以外,我们只再使用另一种:

float(floating-point): 浮点型,可以表示小数

注意 :warning ::虽然 float 型可以带小数,但是进行算术运算时,float 型要比 int 型慢,而且 float 通常只是一个值的近似值。(比如在一个float 型变量中存储 0.1, 但其实可能这个变量的值为 0.09999987,这是舍入造成的误差)

5. 关键字

int 与float 都是C语言的关键字(keyword),关键字是语言定义的单词,不能用做其他用途。比如不能用作命名函数名与变量名。

关键字:斜体代表C99新增关键字

auto enum unsigned break extern
return void case float short
volatile char for signed while
const goto sizeof continue if
static default struct do int
switch double long typedef else
register union
restrict inline _Bool _Complex _Imaginary

如果关键字使用不当(关键字作为变量名),编译器会将其视为语法错误。

6. 声明

声明(declaration):在使用变量(variable)之前,必须对其进行声明(为编译器所作的描述)。

声明的方式为:数据类型 + 变量名(程序员自己决定变量名,命名规则后面会讲)

示例中的 int weight完成了两件事情。第一,变量名为 weight。第二,int 表明这个变量是整型。

编译器用这些信息为变量 weight 在内存中分配空间。

7. 命名

weight,height 都是标识符,也就是一个变量,函数或其他实体的名称。因此,声明将特定标识符与计算机内存的特定位置联系起来,同时也就确定了存储在某位置的信息类型或数据类型。

变量、函数、宏 和 其他实体的名字 叫做 标识符 。标识符可以含有字母、数字和下划线,但是 必须以「字母」或者「下划线」开头

C 语言的标识符是区分大小写的。即:star,Star,STAR 是不同的。

命名风格最好使用 下划线方法 ,并且坚持一种风格即可。

C 语言对标识符的最大长度没有限制,所以不必担心使用较长的描述性名字。

声明变量的理由

  1. 把所有变量放在一处,方便读者查找和理解程序的用途。
  2. 声明变量可以促使你在编写程序之前做好计划(比如你的程序要用什么变量,你可以提前规划)。
  3. 声明变量有助于发现程序中的小错误,如拼写错误。
  4. 不提前声明变量,C程序编译将无法通过

8. 赋值

赋值(assignment):变量通过赋值的方式获得值。

示例中,weight = 160; 是一个 赋值表达式语句。意思是“把值 160 赋给 变量 weight”。

在执行 int weight;时,编译器在计算机内存中为变量 weight 预留的空间,然后在执行这行代码时,把值存储在之前预留的位置。可以给 weight 赋不同的值,这就是 weight 之所以被称为变量的原因。

注意:

  • 该行表达式将值从右侧赋到左侧。a = (c=(b= 1 + 5));

  • 该语句以分号结尾。

  • = 在计算机中不是相等的意思,而是赋值。我们在读 weight = 160; 时,我们应该这么读:“将 160 赋给 weight”

  • ==表示相等

9. 初始化

没有默认值并且尚未在程序中被赋值的变量是未初始化的(uninitialized)。

我们可以用赋值的办法给变量赋初值,但还有更简洁的做法:在变量声明中加入初始值。

例如示例中的 int height = 180数值 180 就是一个初始化式(initializer)。

同一个声明中可以对任意数量的变量进行初始化。如:

1
int a = 10, b = 15, c = 20;

上述每个变量都拥有属于自己的初始化式。接下来的例子,只有 c 有初始化式,a,b没有。

1
int a, b, c = 20;

数据的表现形式及其运算

1. 常量

程序运行过程中不能被改变的量

- 整型常量

整数常量:在程序中以文本形式出现的数,而不是读,或计算出来的数。

C语言允许用 十进制(基数为 10),八进制(基数为 8),十六进制(基数为 16)的形式书写整数常量

8 进制 与 16 进制

8 进制数是用数字 0 ~ 7 书写的。八进制的每一位表示一个 8 的幂(这就如同 10 进制每一位表示 10 的幂一样)。因此,八进制数 237 表示成 10 进制数就是 2 * 8^2 + 3 * 8^1 + 7 * 8^0 = 128 + 24 + 7 = 159

16 进制数使用数字 0 ~ 9 加上字符 A ~ F 书写的,其中字符 A ~ F 表示 10 ~ 15 的数。16进制数每一位表示一个 16 的幂,16进制数 1AF 的十进制数值是 1 x 16^2 + 10 * 16^1 + 15 * 16^0 = 256 + 160 + 15 = 431

  • 十进制常量包含 0 ~ 9 的数字,但是不能以 0 开头

    15 255 32767

  • 八进制常量包含 0 ~ 7 的数字,必须要以 0 开头

    017 0377 077777

  • 十六进制常量包含 0 ~ 9 的数字 和 A ~ F 的字母,总是以 0x 开头

    0xf 0xff 0x7fff

    十六进制常量中的字母可以是大写也可以是小写

1
2
3
4
5
6
7
8
9
10
#include<stdio.h>

int main(void) {

int x = 100;

printf("decimal = %d octonary = %o hexadecimal = %x \n", x, x, x);
printf("decimal = %d octonary = %#o hexadecimal = %#x \n", x, x, x);
return 0;
}

输出:

1
2
decimal = 100   octonary = 144  hexadecimal = 64
decimal = 100 octonary = 0144 hexadecimal = 0x64

八进制与十六进制只是书写数的方式,他们不会对数的实际存储方式产生影响整数都是以二进制形式存储的)。任何时候都可以从一种书写方式切换的另一种,甚至可以混合使用:10 + 015 + 0x20 = 55 。八进制和十六进制更适合底层程序的编写

- 实型常量(浮点常量)

浮点常量必须包含小数点或指数

  1. 十进制小数形式
  2. 指数形式:由于在计算机输入或输出时无法表示上角或下角,故规定以字母e或E代表以10为底的指数。但应注意: e或E之前必须有数字,且e或E后面必须为整数, 可选符号 + 或 - 出现在 字母e(E)的后面。如不能写成e4,12e2.5。

浮点常量可以有多种写法。例如,下面这些写法都表示数 57.0
57.0 || 57.|| 57.0e0 || 5.7e1 || 5.7e+1 (5.710^1)
.57e2 (0.57
10^2) || 570.e-1(570*10^-1)

默认情况下,浮点常量都以双精度的形式存储换句话说,当 C语言的编译器在程序中发现常量 57.0 时,它会安排数据以 double 类型变量的格式存储在内存中。

如果只需要单精度,可以在常量末尾加上 Ff(如 57.0F);如果想以 long double 格式存储,在常量尾加上 Ll(如 57.0L) - [强制转换]

- 字符常量

普通字符(ASCII码表)、转义字符、字符串常量、符号常量

1
2
3
4
5
6
7
8
9
10
11
12

#define PRICE 30 // 符号常量,不占内存!
#include <stdio.h>

void main()
{
int num,total;
num=10;
total=num* PRICE;
printf("total=%d\n",total);
}

2. 变量

先定义再使用 e.g int a = 3;

变量图示

整数类型

有符号整数和无符号整数

有符号整数如果为零或正数,那么最左边的位(符号位,只表示符号,不表示数值)为 0 ;如果为负数,则符号位为 1。如:最大的 16 位整数(2个字节)的二进制表示形式是 01111111 11111111,对应的数值是 32767(即:2^15 - 1)

无符号整数 不带符号位(最左边的位是数值的一部分)。因此,最大的 16 位整数的二进制表示形式是:11111111 11111111(即:2^16 - 1)

默认情况下,C语言中的整型变量都是有符号的,也就是说最左位保留符号位。若要告诉编译器变量没有符号位,需要把他声明成 unsigned 类型。

整数的类型

short int

unsigned short int

int

unsigned int

long int

unsigned long int

整数的类型归根结底只有这 6 种,其他组合都是上述某种类型的同义词。

例如:long signed int 与 long int 是一样的;unsigned short int 与 short unsigned int 是一样的


16位,32位,64位机器的整数类型都各有些不同

32位机器整数类型

32位机器上,int 与 long的大小是一样的,都是 4 个字节。
16位机器上,int 与 short 大小是一样的,都是 2 个字节。
64位机器上,与 32 位机器不同的是,long 是 8 个字节。

但是,上述所说的规律并不是 C标准规定的,会随着编译器的不同而不同。可以检查头文件<limits.h>,来查看每种整数类型的最大值和最小值。

读写整数

读写无符号整数

unsigned int a;

  • 十进制

    scanf("%u", &a);
    printf("%u", a);

    1
    2
    3
    4
    5
    思考以下问题:
    unsigned int a = -1;
    printf("%u",a);

    //为什么输出的结果是4294967295?

    C语言中printf(“%u”)表示按无符号数形式输出一个整数。
    现在的机器一般为32位或64位机,整数存储占4个字节,一个字节8位,共计32位。
    整数在计算机中以补码形式存储,-1的补码为32个1组成的二进制数,按无符号数输出这个二进制数,就是2^32-1=4294967295。
    由于采用补码表示整数,计算机本身不关心整数是正数还是负数,统一按无符号数对待。具体输出时,显示为什么数,计算机按编程者的格式要求进行处理输出。如32个1组成的二进制数,按%d输出就是-1,按无符号输出就是4294967295。
    在计算机中,可以区分正负的类型,称为有符号类型(signed),无正负区分的类型,称为无符号类型(unsigned)。有符号类型数据的最高位表示符号,1表示正,0表示负。
    %u是输出无符号十进制整数。整数占4个字节,-1的十六进制为0xFFFFFFFF,如果把它看成无符号整数,就是十进制的4294967295。

  • 八进制

    scanf("%o", &a);

    printf("%o", a);

  • 十六进制

    scanf("%x", &a);

    printf("%x", a);

读写**短整型*数:在 d,u,o,x 前加上 h

short b

  • scanf("%hd", &b);

    printf("%hd", b);

读写长整数:在 d,u,o,x 前加上 l

long c

  • scanf("%ld", &c);

    printf("%ld", c);

读写长长整数: 在 d,u,o,x 前加上 ll

long long int d

  • scanf("%lld", &d);

    printf("%lld", d);

浮点类型


C语言提供了三种浮点类型,对应着不同的浮点格式:

  • float:单精度浮点数
  • double:双精度浮点数
  • long double:扩展精度浮点数

通常我们用到的是 double, float的精度是最低的。

字符类型


字符类型(字符型):char

char 类型的值可以根据计算机的不同而不同,因为不同的计算机可能会有不同的字符集。

字符集:当今最常用的字符集是 ASCII (美国信息交换标准码)字符集。

ASCII码表

字符操作

C语言把字符当作小整数进行处理

所有字符都是以二进制形式进行编码的。

在标准的 ASCII 码中,字符的取值范围是 00000000 ~ 01111111,可以看成是 0 ~ 127 。例如,字符 ‘A’ 的值是 65,‘a’ 的值是 97,‘0’ 的值是48,’ ’ (Space)的值是 32 。

C语言中,字符和整数的关联是很强的,字符常量事实上是 int 类型而非 char 类型

请看下面的例子,你会更深的理解字符型与整型的关联(字符集位ASCII)

1
2
3
4
5
6
char ch;
int i;

i = 'a';// i is now 97
ch = 65;//'ch' is now 'A'
ch = ch + 1;//'ch' now is 'B'

因此,字符就有了数的一些特征。比如可以像数一样比较,可以当作条件应用于 if语句,for循环。这是一个很便利的事情。

但是,以数的形式处理字符可能降低程序的可移植性(不同机器使用的字符集不同) 和 导致编译器无法检查出来的多种编程错误(‘a’ + ‘b’ * ‘c’ 这类没有意义的表达式等)。

有符号字符 和 无符号字符

有符号字符signed char:取值范围:-128 ~ 127

无符号字符unsigned char: 取值范围:0 ~ 255

可移植性技巧:不要假设 char 类型默认为 signed 或 unsigned 。如果有区别,用 signed char 和 unsigned char 代替 char 。

3.转义字符

字符转义序列(粗体比较常用,需要注意)

名称 转义序列 名称 转义序列
换行符 \n 退格 \b
水平制表符 \t 垂直制表符 \v
单引号 \’ 换页符 \f
双引号 \" 问号 \?
回车符 \r 报警(响铃)符 \a
反斜杠 \\

运算符和表达式

一 算术运算符

1.概念

一元运算符(只需要 1 个操作数)
+ 一元正号运算符
- 一元负号运算符

二元运算符

加法类 乘法类
+ 加法运算符 * 乘法运算符
- 减法运算符 / 除法运算符
% 求余运算符

注意:

  • int 型与 float 型混合在一起时,运算结果是 float 型。

    比如,9 + 2.5f 的值为 11.5;6.7f / 2 的值为 3.35。

  • 运算符 /:当两个操作数都是整型时,结果会向下取整。如,1 / 2 的值是 0,而不是 0.5 。

  • 运算符 %要求两个操作数都是整型

  • 把 0 作为 /% 的右操作数会导致未定义行为 > error。

  • 当运算符 /% 用于负操作数时,其结果难以确定。

2. 运算符的优先级和结合性

当表达式包含多个运算符时,其含义可能不是一目了然的。我们的解决方法是:

  • 用括号进行分组
  • 了解运算符的优先级和结合性
运算符优先级

(operator precedence)

最高优先级 + - (一元运算符)
最低优先级 * / %
+ - (二元运算符)

例 1-1:

1
2
i + j * k 等价于 i + (j * k)
-i + -j 等价于 (-i) + (-j)

运算符的结合性

当表达式包含两个或更多相同优先级的运算符时,仅有运算符优先级规则是不够的。这种情况下,运算符的结合性(associativity)开始发挥作用。

如果运算符是从左向右开始结合的,那么称这种运算符是左结合的。

二元运算符即:*,/,%,+,-都是左结合的。所以:

例 1-2:

1
i - j - k 等价于 (i - j) - k

运算符是右结合的,如一元运算符:+,-

例 1-3:

1
- + i 等价于 -(+i)

3.总结

在许多语言(特别是 C 语言)中,优先级和结合性规则都是十分重要的。然而 C 语言的运算符太多了(差不多 50 种)。为了自己和他人理解代码的方便,请最好加上足够多的圆括号。

二 赋值运算符

求出表达式的值后往往需要将其存储在变量中,以便将来使用。C语言的 = (简单赋值 simple assignment)运算符可以用于此目的。为了更新已经存储在变量中的值,C语言还提供了一种复合赋值(compound assignment)。

1. 简单赋值

表达式 v = e的赋值效果是求出表达式 e 的值,然后将此值赋值给 v。

例 2-1:

1
2
3
i = 5;// i is now 5
j = i;// j is now 5
k = 10 * i + j;// k is now 55

如果 v 与 e 的类型不同,那么赋值运算发生时会将 e 的值转化为 v 的类型:

例 2-2:

1
2
3
4
int i;
double j;
i = 72.99f;// i is now 72
j = 136;// f is now 136.0

在很多编程语言中,赋值是语句;然而在 C语言中,赋值就像 + 那样是运算符

既然赋值是运算符,那么多个赋值语句可以串联在一起:

例 2-3:

1
i = j = k = m = 0;

运算符 = 是右结合的,所以,上面的语句等价于:

1
i = (j = (k = (m = 0)));

作用是先将 0 赋值给 m,再将 m 赋值给 k,再将 k 赋值给 j,再将 j 赋值给 i 。

! 注意

因为赋值运算符存在类型转换,串在一起赋值运算的结果可能不是预期的结果:

1
2
3
4
5
int i;
float j;

j = i = 33.3f;
//先将 33 赋值给 i,然后将 33.0 赋值给 j

2. 左值

赋值运算要求它的左操作数必须是左值(lvalue)。左值表示在计算机中的存储对象,而不是常量或计算的结果。左值是变量。

例 2-4:

1
2
3
12 = i;
i + j = 0;
-i = j;

以上三种表达式都是错误的。

3. 复合赋值

1
2
3
i = i + 2;
//等同于
i += 2;

上面的例子中 += 就是一种符合运算符,表示:将自身表示的数增加 2 后再赋值给自己。


与加法相似,所有赋值运算符的工作原理大体相同。

+=

-=

*=

/=

%=

注意:

  1. i *= j + ki = i * j + k 是不一样的。

  2. 使用复合赋值运算符时,注意不要交换组成运算符的两个字符的位置。如:

    i += j写成了i =+ j 后者等价于:i = (+j)

  3. 复合运算符有着和 =运算符一样的特性。它们也是右结合的,所以:
    i += j += k等价于i += (j += k)

4. 自增运算符和自减运算符(重点!)

++

--

“自增”(加1)和“自减”(减1)也可以通过下面的方式完成:

1
2
i = i + 1;
j = j - 1;

复合赋值运算符可以简化上面的语句:

1
2
i += 1;
j -= 1;

而 C语言 允许用 ++ 和 – 运算符将这些语句缩的更短。比如:

1
2
i++;
j--;

或者:

1
2
++i;
--j;

这两种形式的写法的意义不同的:

  • ++i (前缀(prefix)自增),意味着“立即自增 1 ”

    1
    2
    3
    4
    5
    6
    int i = 1;
    printf("%d\n", ++i);
    printf("%d\n", i);
    //输出
    2
    2
  • i++(后缀(postfix)自增),意味着“先使用 i 的原始值,稍后再自增”。稍后是多久?C语言标准没有给出精确的时间,但是可以放心的假设 i 再下一条语句执行之前进行自增。

    1
    2
    3
    4
    5
    6
    int i = 1;
    printf("%d\n", i++);
    printf("%d\n", i);
    //输出
    1
    2

--运算符具有相同的特性。

后缀的 ++ 和 – 比一元的正号,负号优先级高,而且都是左结合的。

前缀的 ++ 和 – 与一元的正号,负号优先级相同,并且是右结合的。

比如:

1
2
3
4
5
6
7
8
9
10
int main(void) {

int i = 1;

printf("%d", -i++);
printf("%d", i);
}
//输出:
-1
2

建议谨慎使用++和–运算符,只用最简单的形式,即i++,i–。而且把它们作为单独的表达式,而不要在一个复杂的表达式中使用++或–运算符。

5.表达式求值

部分C语言运算符表

优先级 类型名称 符号 结合性
1 (后缀)自增 ++ 左结合
(后缀)自减 - -
2 (前缀)自增 ++ 右结合
(前缀)自减 - -
一元正号 +
一元符号 -
3 乘法类 * / % 左结合
4 加法类 + - 左结合
5 赋值 = *= /= -= += 右结合

能理解下面这个表达式的意义,就算掌握了这一部分的表达式求值规则:

1
a = b += c++ - d + --e / -f

等价于:

1
a = ( b += ( (c++) - d + (--e) / (-f) ) )
子表达式的求值顺序

C语言没有定义子表达式的求值顺序(除了含有 逻辑与,逻辑或 或 逗号运算符的表达式(后面会讲))。

但是不管子表达式的计算顺序如何,大多数表达式都有相同的值。但是,当子表达式改变了某个操作数的值时,产生的值就可能不一致了。思考下面的例子:

1
2
a = 5;
c = (b = a + 2) + (a = 1);

第二条语句的执行结果是未定义的。对大多数编译器而言,c 的值是 6 或者 2。取决于 子表达式 b = a + 2 和 a = 1 的求值顺序。

像上例那样,在表达式中,既在某处访问变量的值,又在别处修改它的值是不可取的。

为了避免出现此类情况,我们可以将子表达式分离:

1
2
3
4
a = 5;
b = a + 2;
a = 1;
c = b - a;

执行完这些语句后,c 的值将始终是 6

除此之外,自增自减运算符也要小心使用。如下例:

1
2
i = 2;
j = i * i++;

你觉得 printf(“%d”,j)的值是什么呢

6.不同类型数据间的混合运算

如果一个运算符两侧的数据类型不同,则先自动进行类型转换,使二者成为同一种类型,然后进行运算。整型、实型、字符型数据间可以进行混合运算。规律为:

  1. +、-、*、/运算的两个数中有一个数为float或double型,结果是double型,因为系统将所有float型数据都先转换为double型,然后进行运算。
  2. 如果int型与float或double型数据进行运算,先把int型和float型数据转换为double型,然后进行运算,结果是double型。
  3. 字符(char)型数据与整型数据进行运算,就是把字符的ASCII代码与整型数据进行运算。如果字符型数据与实型数据进行运算,则将字符的ASCII代码转换为double型数据,然后进行运算。

三 逻辑运算符

逻辑运算符运算说明

逻辑运算的结果

在编程中,我们一般将零值称为“假”,将非零值称为“真”。逻辑运算的结果也只有“真”和“假”,“真”对应的值为 1,“假”对应的值为 0。

  • 与运算(&&)
    参与运算的两个表达式都为真时,结果才为真,否则为假。例如:

5&&0
5为真,0为假,相与的结果为假,也就是 0。

(5>0) && (4>2)
5>0 的结果是1,为真,4>2结果是1,也为真,所以相与的结果为真,也就是1。

  • 或运算(||)
    参与运算的两个表达式只要有一个为真,结果就为真;两个表达式都为假时结果才为假。例如:

10 || 0
10为真,0为假,相或的结果为真,也就是 1。

(5>0) || (5>8)
5>0 的结果是1,为真,5>8 的结果是0,为假,所以相或的结果为真,也就是1。

  • 非运算(!)
    参与运算的表达式为真时,结果为假;参与运算的表达式为假时,结果为真。例如:

!0
0 为假,非运算的结果为真,也就是 1。

!(5>0)
5>0 的结果是1,为真,非运算的结果为假,也就是 0。

输出逻辑运算的结果:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main(){
int a = 0, b = 10, c = -6;
int result_1 = a&&b, result_2 = c||0;
printf("%d, %d\n", result_1, !c);
printf("%d, %d\n", 9&&0, result_2);
printf("%d, %d\n", b||100, 0&&0);
return 0;
}

运行结果:
0, 0
0, 1
1, 0

C语句

  1. 控制语句
  2. 函数调用语句
  3. 表达式语句
  4. 空语句
  5. 符合语句(语句块)

数据的输入输出

有关输入输出的概念

printf() 函数

printf()函数打印数据的指令要与待打印数据的类型相匹配。例如,打印整数时使用 %d,打印字符时使用 %c 。这些符号被称为转换说明(conversion specification),它们指定了如何把数据(以2进制形式)转换成可显示的形式。

例如:

1
printf("I am %d years old", 18);

这是 printf()的格式:

printf(格式字符串,待打印项1,待打印项2,...);

待打印项都是要打印的的项。它们可以是变量,常量,甚至是在打印之前计算的表达式。上例中,只有一个待打印项: 18 。

格式字符串包含两种不同信息:

  • 普通字符:以字符串中出现的形式打印出来。上例中,“I am” 与 " years old" 为普通字符
  • 转换说明:用待打印项的值来替换。上例中,“%d” 为转换说明

C语言的编译器不会检测格式字符串中转换说明中的数量与待打印项总个数是否相匹配。

1.缺少参数

1
printf("%d %d\n", i); // wrong

printf 会正确显示 i 的值,然后显示一个无意义的整数值。

2.参数过多

1
printf("%d\n", i, j);// wrong

而在这种情况下,printf 函数会显示变量 i 的值,但是不会显示变量 j 的值


printf() 转换说明

  • 转换说明符

    转换说明符 含义
    d,i 把 int 类型转换为 十进制形式,多用%d
    o,u,x,X 把无符号整型转换为八进制(o),十进制(u),十六进制形式(x,X)。
    f,F (F C99) 把 double 类型转换为 十进制形式,并把小数点放置在正确位置上。如果没有指定精度,那么小数点后显示6个数字。
    e,E 把 double 类型转换为 科学计数法形式。如果没有指定精度,那么小数点后显示6个数字。
    g,G 把double 类型转换为 f 形式或 e 形式。当数值的指数部分小于 -4,或大于等于精度时,会选择以 e 的形式显示。尾部的 0 不显示(除非用#标志),且小数点后跟有数字才会显示出来。
    a,A (C99) 把 double 类型转换为十六进制科学计数法(p计数法)。
    c 以字符形式输出,只输出一个字符。
    s 写出由实参指向的字符串。
    % 百分号 %

    示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    printf("%i\n", 123);
    printf("%d\n", 123);

    printf("%o\n", 123);
    printf("%u\n", 123);
    printf("%x\n", 123);
    printf("%X\n", 123);

    printf("%f\n", 123.0);

    printf("%e\n", 123.0);

    printf("%g\n", 123.0);

    printf("%a\n", 123);

    printf("%c\n", 65);

    printf("%s\n", "123");

    printf("%%\n");

    输出结果如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    123
    123

    173
    123
    7b
    7B

    123.000000

    1.230000e+02

    123

    0x1.e13430000007bp-1021

    A

    123

    %
  • 标志(可选,允许出现多于一个)

- 字段内左对齐(默认右对齐)
+ 在打印的数前加上 + 或 - (通常只有负数前面附上减号)例1
空格 在打印的非负数前前面加空格( + 标志优先于空格标志)例2
# 对象:八进制数,十六进制数,以g/G 转换输出的数 例3
0 用前导 0 在字段宽度内对输出进行填充。如果转换格式为d,i,o,u,x(X),而且指定了精度,可以忽略 0 例4

例 1:

1
2
3
4
printf("%d\n", 123);
printf("%d\n", -123);
printf("%+d\n", 123);
printf("%+d\n", -123);

运行结果如下:

例 2:

1
2
3
printf("% d\n", 123);
printf("% d\n", -123);
printf("% +d\n", 123);

运行结果如下:

例 3:

1
2
3
4
5
6
printf("%o\n", 0123);
printf("%x\n", 0x123);
printf("%#o\n", 0123);
printf("%#x\n", 0x123);
printf("%#g\n", 123.0);
printf("%g\n", 123.0);

运行结果如下:

例 4:

1
2
3
printf("%5d\n", 123);
printf("%05d\n", 123);
printf("%5.3d\n", 123);

运行结果如下:

  • 最小字段宽度(可选)

    如果数据项太小无法达到这个宽度,那么会对字段进行填充。(默认情况下会在数据项左侧添加空格,从而使字段宽度内右对齐)。

    如果数据项过大以至于超过了这个宽度,那么会完整的显示数据项。

例 5:

1
2
printf("%5d\n", 123);
printf("%2d\n", 123);

运行结果如下:

  • 精度(可选项)

    如果转换说明是:

    d,i,o,u,x,X 那么精度表示最少位数(如果位数不够,则添加前导 0 )

    a,A,e,E,f,F 那么精度表示小数点后的位数

    g,G 那么精度表示有效数字个数

    s那么精度表示最大字节数

    精度是由小数点(.)后跟一个整数,如果只有小数点,那么精度为0 。例 6

    例 6:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    printf("%.4d\n", 123);

    printf("\n");

    printf("%f\n", 123.0);
    printf("%.1f\n", 123.0);

    printf("\n");

    printf("%g\n", 123.0);
    printf("%.5g\n", 123.0);

    printf("\n");

    printf("%s\n", "Hello");
    printf("%.2s\n", "Hello");

    printf("\n");

运行结果如下:

  • 长度修饰符(可选)。

    长度修饰符表明待显示的数据项的长度大于或小于特定转换说明中的正常值。例7

    长度修饰符 转换说明符 含义
    hh (C99) d,i,o,u,x,X signed char, unsigned char
    h d,i,o,u,x,X short, unsigned short
    l d,i,o,u,x,X long, unsigned long
    ll (C99) d,i,o,u,x,X long long, unsigned long long
    L a,A,e,E,f,F,g,G long double
    z (C99) d,i,o,u,x,X size_t
    j (C99) d,i,o,u,x,X ptrdiff_t

    例 7:

    1
    2
    3
    4
    5
    printf("%#hhX\n", 0xAABBCCDDEEFF1122);//这是一个占用内存为 8 个字节的十六进制数
    printf("%#hX\n", 0xAABBCCDDEEFF1122);
    printf("%#X\n", 0xAABBCCDDEEFF1122);
    printf("%#lX\n", 0xAABBCCDDEEFF1122);
    printf("%#llX\n", 0xAABBCCDDEEFF1122);
    1
    2
    3
    4
    5
    0X22
    0X1122
    0XEEFF1122
    0XEEFF1122
    0XAABBCCDDEEFF1122

给定身高体重,输出身高体重比.

1
2
3
4
5
6
7
8
9
10
11
12
13
#include "stdio.h"
int main(void)
{
int height,weight, scale;//scale:比例
printf("请输入您的身高(请输入整数,厘米为单位):\n");
scanf("%d",&height);
printf("请输入您的体重(请输入整数,斤为单位):\n");
scanf("%d",&weight);
scale = height / weight;
printf("身高与体重的比为:%d\n", weight, scale);
return 0;
}

我们发现,最终我们屏幕上看到的是引号内的内容。我们可以来看一下输出的内容:

1
2
我的体重是:160
身高与体重的比为:1

为什么 180 / 160 == 1 (180 / 160 的值是 1)呢?

因为 weight 和 height 都整数,它们相除结果取整数(向下取整)。

如何输出精确的结果呢?

1
2
3
float scale;
scale = (float)height / weight;
printf("我的体重是:%d斤\n身高与体重的比为:%.3f\n", weight, scale);

scanf() 函数

scanf_function

scanf() 函数功能:

将从键盘输入的字符转化为“输入控制符”所规定格式的数据,然后存入以输入参数的值为地址的变量中。

scanf() 把输入的字符串转换成整数,浮点数,字符和字符串,而 printf() 正好与之相反,把整数,浮点数,字符,字符串转换成显示在屏幕上的文本。

scanf() 与 printf() 类似,也要使用 格式字符串 和 参数列表。scanf() 中的格式字符串表明字符输入流的目标数据类型。两个函数的主要区别在于参数列表中。printf() 函数使用变量,常量和表达式,而 scanf() 函数使用指向变量的指针。这里不需要了解指针,只需要记住一下简单的两条:

scanf()函数的工作方法

scanf 函数本质上是一种 模式匹配 函数,试图把输入的字符组与转换说明相匹配。

printf 函数一样,scanf 函数是由格式字符串控制的。调用时,scanf 函数从左边开始处理字符串中的信息。对于格式串中的每一个转换说明,scanf 函数从输入的数据中定位适当类型的项,并且在必要时跳过空格。

然后,scanf 函数读入数据项,并且在遇到不可能属于此项的字符时停止。如果读入数据成功,那么 scanf 函数会继续处理格式串的剩余部分; 如果某一项不能成功读入,那么 scanf 函数将不再查看格式串的剩余部分而立即返回。

在寻找函数的起始位置时,scanf 函数会忽略空白字符。

在要求读入整数时,scanf 函数首先寻找正号或者负号,然后读取数字直到读到一个非数字时才停止。当要求读入浮点数时, scanf 函数会寻找一个正号或者负号,随后是一串数字(可能含有小数点),再后是一个指数(可选)。指数由字母 e (或者字母 E )、可选的符号和一个或者多个数字构成。

何时使用&

用 scanf 读取

  • 基本变量类型的值,在变量名前加上一个 &
  • 把字符串读入数组中,不要使用 &

下面的程序演示了这两条规则:

_test2.c —— 何时使用 &

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//scanf 的 & 使用规则
#include<stdio.h>
int main(void){

int age;
float assets;
char pets[30];//字符数组,存放字符串

printf("Enter you age, assets and you favorite pet:\n");
scanf("%d %f", &age, &assets); // 这里要用 & , 字符数组不使用 &
scanf("%s", pets);
printf("your age:%d\n your assets:%f\n your favorite pet:%s\n",age,assets,pets);

return 0;
}


初学者在使用 scanf 时,在应该写 & 的时候容易忽略 & ,所以每次使用 scanf 的时候一定要格外小心。通常情况下,必要的地方缺少 & 会让程序崩溃(编译器没有警告),但是也有时候程序并不会崩溃,这时候找 bug 可能会让你头痛。


scanf 的 长度修饰符 和 转换说明符 与 printf 几乎相同。主要的区别如下:

  • 长度修饰符 :(可选项)对于 float 与 double 类型,printf() 的转换说明都用 f; 而对于 scanf() ,float 保持不变,double 要在 f 前加长度修饰符 l ,即:lf例 1

    例 1:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include<stdio.h>

    int main(void) {

    double a = 3.0;

    scanf("%lf", &a);
    printf("%lf", a);
    return 0;
    }
  • 转换说明符

  • 字符 *:(可选项)字符 * 出现意味着赋值屏蔽(assignment suppression): 读入此数据项,但是不会将其赋值给对象。用 * 匹配的数据项不包含在 …scanf 函数返回的计数中。例 3

    例 3:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include<stdio.h>

    int main(void) {

    int a = 0;

    scanf("%*d%d", &a);
    printf("%d", a);

    return 0;
    }
    输入:1 2
    输出:2
  • 最大字段宽度:(可选项)最大字段宽度限制了输入项中的字符数量。如果达到最大值,那么次数据项的转换结束。转换开始跳过的空白不计。例 4

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //输入:1234 Hello
    //先猜测一下输出
    #include<stdio.h>

    int main(void) {

    int a = 0;
    char str[10];

    scanf("%2d%3s", &a, str);
    printf("%d %s", a, str);

    return 0;
    }
    //输出:12 34

    💥 ❗️ 思考 为什么%s 只输出了 34 ?

    scanf 在读入字符串时会跳过空白字符并开始读入第一个非空白字符,保存非空白字符直到再遇到空白字符结束。

scanf()函数在不同情况下对空白符的处理 💦🎓

首先,要清楚一个概念:空白字符(white space)。一般,程序中所指的空白字符是指空格(space),回车(enter)和制表符(Tab)。

1. 整数%d

对于整型数据的输入,也就是说"%d"类型的输入,scanf默认的分割符是所有的空白字符(空格,回车和制表符都行)。也就是说如果一个scanf函数中出现scanf(“%d%d”,&a,&b),那么用任何一个空白字符来分隔两个整数a,b的值,变量a,b都可以接收到正确的输入。另外,要注意的是,scanf对于数字输入,会忽略输入数据项前面的空白字符。 具体请参看如下例题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

#include "stdio.h"
int main(void)
{
int a,b,c;

scanf("%d%d%d\n",&a,&b,&c);
printf("%d,%d,%d/n",a,b,c);
return 0;
}

=======================================
运行时按如下方式输入三个值:
345 ↙(输入a,b,c的值)
345printf输出的a,b,c的值)


scanf 函数遵循什么规则来识别整数或浮点数呢?

在要读入整数时,scanf 函数首先会寻找正号或负号,然后从读入一个数字开始直到读入一个非数字为止。

当要求读入浮点数时,scanf 函数首先会寻找正号或负号(可选),然后是一串数字(可能含有小数点),再后是一个指数(可选)。指数由一个字母e,可选的符号,一个或多个数字组成。

当 scanf 函数遇到一个不可能输入当前项的字符时,它会把此字符“放回原处”,以便在扫描下一项或下一次调用 scanf 时再次读入。思考下面(公认有问题的)4个数的排列:【非常重要!!!】

1
1-20.3-4.0e3lol

然后我们用这个 scanf 函数来读入:

1
scanf("%d%d%f%f", &i, &j, &x, &y);

scanf 会如何处理这组输入呢?

  • %d :读入 1
  • %d :读入 -20
  • %f :读入 .3 (当作 0.3 处理)
  • %f : 将 -4.0*10^3存入变量y中,lol不处理"放回原处"

2. 字符串%s

scanf对于字符串输入的处理和对整数类似,会忽略前导的空白字符,而且默认的分隔符是所有的空白字符。但是,要注意的是,由于C语言中,没有string类型,都是用char型数组来表示。因此,scanf会为每一个输入的字符串最后加一个‘\0’。

使用 %s 转换说明,scanf 会读取除了空白字符以外的所有字符。scanf 跳过空白字符并开始读入第一个非空白字符,保存非空白字符直到再遇到空白字符结束。 这意味着,scanf 最多只能读取一个单词。无法利用字段宽度使得 scanf 读取多个单词,scanf 会在字段宽度结束或遇到空白字符处停止。scanf 将字符串放入数组时,会在字符串序列末尾加上一个 \0

输入字符串匹配

%[集合]匹配集合中的任意序列;%[^集合]匹配非集合中的任意序列。查看以下例题

例 :

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>

int main(void) {

char str[10];//字符串数组

scanf("%[123]", str);
printf("%s", str);

return 0;
}
//输入:123456abc123
//输出:123
1
2
3
4
5
6
7
8
9
10
11
int main(void) {

char str[10];//字符串数组

scanf("%[^123]", str);
printf("%s", str);

return 0;
}
//输入:abc4123a
//输出:abc4
3. 字符%c

scanf在处理对字符数据的输入时,既不会忽略前导空白字符,默认也没有任何分隔字符。所有的字符,包括空白字符都会被当成输入字符。

scanf 函数如果发生了 输入失败(没有字符输入)或 匹配失败 (即输入字符和格式串不匹配),那么…scanf 会提前返回。返回就意味着这个 scanf 的读入结束。

scanf 返回的又是什么呢?

成功赋值的接收参数的数量(可以为零,在首个接收用参数赋值前匹配失败的情况下),或者若输入在首个接收用参数赋值前发生失败,则为EOF(EOF 的值是 -1)。

例如:

1
2
3
4
5
6
7
8
9

#include <stdio.h>
int main(int argc, char *argv[])
{
char s[10];
scanf("%s",s);
printf("s[10]存储的值为:%s\n",s);

}

运行结果如下:

格式串中的普通字符

  • 空白字符:参见scanf()函数在不同情况下对空白符的处理

  • 非空白字符:看个程序就明白了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include<stdio.h>
    int main(void) {

    int i, j, k;

    printf("Enter a date: ");
    scanf("%d - %d - %d", &i, &j, &k);
    printf("date: %d - %d - %d", i, j, k);

    return 0;
    }
    1
    2
    3
    4
    //输入:
    Enter a date: 2020 - 2-22
    //输出:
    date: 2020 - 2 - 22

    空格你可以随便空,换行都可以随便换,但是一定要打 ‘’-‘’ 符号。

易混淆的 printf() 与 scanf()

1
printf("%d", &i);

输出的并不是 i 的值 (而是 i 的地址的十进制数值)

1
scanf("%d, %d", &i, &j);

scanf 在第一个 %d 读入一个整数后,试图把逗号与输入流中的下一个字符相匹配,如果这个字符不是 ,,那 scanf 就会终止操作,而不再读取变量 j 的值。

1
scanf("%d\n", &i);

printf 函数中经常有 \n ,但是如果在 scanf 格式串结尾放一个 \n 通常会引发你预期之外的问题。

对于 scanf 函数来说,\n 等同于空格,那么 scanf 就会在流中寻找空白字符,但是我们上面说过,scanf 格式串中的空白字符会与 输入流中的零个或多个空白字符匹配。所以当你输入完成后按下回车,这个回车会与 scanf 中的 \n 匹配,之后你无论打多少回车都不会使 scanf 结束,除非你输入一个非空字符,使 scanf 因匹配失败而退出。

getchar() putchar() 函数


从键盘输入单个字符 > getchar()
将单个字符打印到屏幕 > putchar()

例 1. 从键盘输入BOY 3个字符,然后把它们输出到屏幕。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

#include <stdio.h>
int main()
{ char a,b,c; //定义字符变量a,b,c
a=getchar(); //从键盘输入一个字符,送给字符变量a
b=getchar(); //从键盘输入一个字符,送给字符变量b
c=getchar(); //从键盘输入一个字符,送给字符变量c
putchar(a); //将变量a的值输出
putchar(b); //将变量b的值输出
putchar(c); //将变量c的值输出
putchar('\n');//换行
return 0;
}



例 2. 用getchar函数从键盘读入一个大写字母,把它转换为小写字母,然后分别用putchar、printf函数输出该小写字母。

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
int main ()
{
char c1,c2;
c1=getchar(); //从键盘读入一个大写字母,赋给字符变量c1
c2=c1+32; //得到对应的小写字母的ASCII代码,放在字符变量c2中
printf("大写字母: %c\n小写字母: %c\n",c1,c2); //输出c1,c2的值
putchar(c2);
return 0;
}

参考资料:《C Primer Plus》《C语言程序设计:现代方法》

  1. 用100个函数操作一个数据结构比仅用10个函数但是操作10个不同的数据结构要好。Epigrams on Programming 编程警句

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!