概述
(该篇学习内容全部来自于C语言中文网, 本篇内容仅仅是简易学习笔记 , 以自己的理解+网站部分描述结合+个人补充,并不适合编程初学者观看!!! 需要有一定的编程基础)
数据在内存中存储
内存条包含了上亿个电子元器件。这些元器件,实际上就是电路;电路的电压会变化,要么是 0V,要么是 5V,只有这两种电压。5V 是通电,用1来表示,0V 是断电,用0来表示。所以,一个元器件有2种状态,0 或者 1。
一般情况下将8个元器件看做一个单位,即使表示很小的数,例如 1,也需要8个,也就是 00000001。
1个元器件称为1比特(Bit)或1位,8个元器件称为1字节(Byte),那么16个元器件就是2Byte,32个就是4Byte,以此类推:
单位换算:
1Byte = 8 Bit
1KB = 1024Byte
函数
C语言规定,一个程序必须有且只有一个 main 函数。main 被称为主函数,是程序的入口函数,程序运行时从 main 函数开始,直到 main 函数结束(遇到 return 或者执行到函数末尾时,函数才结束)。
#include <stdio.h>
//( ) 表明这是函数定义,{ } 之间的代码是函数要实现的功能。
int main()
{
puts("C语言中文网");
return 0;
}
头文件的概念
C语言开发者们编写了很多常用函数,并分门别类的放在了不同的文件,这些文件就称为头文件(header file)。每个头文件中都包含了若干个功能类似的函数,调用某个函数时,要引入对应的头文件,否则编译器找不到函数。
实际上,头文件往往只包含函数的说明,也就是告诉我们函数怎么用,而函数本身保存在其他文件中,在链接时才会找到。对于初学者,可以暂时理解为头文件中包含了若干函数。
引入头文件使用#include命令,并将文件名放在< >中,#include 和 < > 之间可以有空格,也可以没有。
头文件以.h为后缀,而C语言代码文件以.c为后缀,它们都是文本文件,没有本质上的区别,#include 命令的作用也仅仅是将头文件中的文本复制到当前文件,然后和当前文件一起编译。你可以尝试将头文件中的内容复制到当前文件,那样也可以不引入头文件。
.h中代码的语法规则和.c中是一样的,你也可以#include <xxx.c>,这是完全正确的。不过实际开发中没有人会这样做,这样看起来非常不专业,也不规范。
较早的C语言标准库包含了15个头文件,stdio.h 和 stdlib.h 是最常用的两个: stdio 是 standard input
output 的缩写,stdio.h 被称为“标准输入输出文件”,包含的函数大都和输入输出有关,puts() 就是其中之一。 stdlib
是 standard library 的缩写,stdlib.h
被称为“标准库文件”,包含的函数比较杂乱,多是一些通用工具型函数,system() 就是其中之一。
变量和数据类型
变量定义&赋值
int a; // 这个语句的意思是:在内存中找一块区域,命名为 a,用它来存放整数。
a=123; // 把 123 交给了变量 a,这个过程叫做给变量赋值;第一次赋值,也称变量的初始化,或者赋初值。
数据类型(Data Type)
数据是放在内存中的,变量是给这块内存起的名字,有了变量就可以找到并使用这份数据。
诸如数字、文字、符号、图形、音频、视频等数据都是以二进制形式存储在内存中的,它们并没有本质上的区别,那么,00010000 该理解为数字16呢,还是图像中某个像素的颜色呢,还是要发出某个声音呢?如果没有特别指明,我们并不知道。
也就是说,内存中的数据有多种解释方式,使用之前必须要确定;上面的int a;就表明,这份数据是整数,不能理解为像素、声音等。int 有一个专业的称呼,叫做数据类型(Data Type)。
顾名思义,数据类型用来说明数据的类型,确定了数据的解释方式,让计算机和程序员不会产生歧义。
C语言基本的数据类型:
说 明 | 字符型 | 短整型 | 整型 | 长整型 | 单精度浮点型 | 双精度浮点型 | 无类型 |
---|---|---|---|---|---|---|---|
数据类型 | char | short | int | long | float | double | void |
连续定义的多个变量以逗号,分隔,并且要拥有相同的数据类型;变量可以初始化,也可以不初始化。
int a, b, c;
float m = 10.9, n = 20.56;
char p, q = '@';
数据的长度
所谓数据长度(Length),是指数据占用多少个字节。占用的字节越多,能存储的数据就越多,对于数字来说,值就会更大,反之能存储的数据就有限。
多个数据在内存中是连续存储的,彼此之间没有明显的界限,如果不明确指明数据的长度,计算机就不知道何时存取结束。例如我们保存了一个整数1000,它占用4个字节的内存,而读取时却认为它占用3个字节或5个字节,这显然是不正确的。
所以,在定义变量时还要指明数据的长度。而这恰恰是数据类型的另外一个作用。数据类型除了指明数据的解释方式,还指明了数据的长度。因为在C语言中,每一种数据类型所占用的字节数都是固定的,知道了数据类型,也就知道了数据的长度。
整数(short,int,long)
在现代操作系统中,int一般占用 4 个字节(Byte)的内存,共计 32 位(Bit)。
int 是基本的整数类型,short 和 long 是在 int 的基础上进行的扩展,short 可以节省内存,long 可以容纳更大的值。
short a = 10;
short b, c = 99;
long m = 102023;
long n, p = 562131;
这样 a、b、c 只占用 2 个字节的内存,而 m、n、p 可能会占用 8 个字节的内存。
整型的长度
在不同的环境下, 只有 short 的长度是确定的,是两个字节,而 int 和 long 的长度无法确定。
C语言并没有严格规定 short、int、long 的长度,只做了宽泛的限制:
- short 至少占用 2 个字节。
- int 建议为一个机器字长。32 位环境下机器字长为 4 字节,64 位环境下机器字长为 8 字节。
- short 的长度不能大于 int,long 的长度不能小于 int。
总结起来,它们的长度(所占字节数)关系为:
2 ≤ short ≤ int ≤ long
这就意味着,short 并不一定真的“短”,long 也并不一定真的“长”,它们有可能和 int 占用相同的字节数。
举例:
- 在 16 位环境下,short 的长度为 2 个字节,int 也为 2 个字节,long 为 4 个字节。16 位环境多用于单片机和低级嵌入式系统,在PC和服务器上已经见不到了。
- 对于 32 位的 Windows、Linux 和 Mac OS,short 的长度为 2 个字节,int 为 4 个字节,long 也为 4 个字节。PC和服务器上的 32 位系统占有率也在慢慢下降,嵌入式系统使用 32 位越来越多。
在 64 位环境下,不同的操作系统会有不同的结果,如下所示:
操作系统 | short | int | long |
---|---|---|---|
Win64(64位 Windows) | 2 | 4 | 4 |
类Unix系统(包括 Unix、Linux、Mac OS、BSD、Solaris 等) | 2 | 4 | 8 |
目前我们使用较多的PC系统为 Win XP、Win 7、Win 8、Win 10、Mac OS、Linux,在这些系统中,short 和 int 的长度都是固定的,分别为 2 和 4,大家可以放心使用,只有 long 的长度在 Win64 和类 Unix 系统下会有所不同,使用时要注意移植性。
sizeof 操作符
sizeof 用来获取某个数据类型或变量所占用的字节数,需要注意的是,sizeof 是C语言中的操作符,不是函数,所以可以不带( );
如果后面跟的是变量名称,那么可以省略( ),如果跟的是数据类型,就必须带上( )。
short a = 10;
int b = 100;
int short_length = sizeof a;
int int_length = sizeof(b);
int long_length = sizeof(long);
int char_length = sizeof(char);
不同整型的输出
使用不同的格式控制符可以输出不同类型的整数,它们分别是:
- %hd用来输出 short int 类型,hd 是 short decimal 的简写;
- %d用来输出 int 类型,d 是 decimal 的简写;
- %ld用来输出 long int 类型,ld 是 long decimal 的简写。
#include <stdio.h>
int main()
{
short a = 10;
int b = 100;
long c = 9437;
printf("a=%hd, b=%d, c=%ldn", a, b, c);
return 0;
}
当使用%d输出 short,或者使用%ld输出 short、int 时,不管值有多大,都不会发生错误,因为格式控制符足够容纳这些值。
当使用%hd输出 int、long,或者使用%d输出 long 时,如果要输出的值比较小(就像上面的情况),一般也不会发生错误,如果要输出的值比较大,就很有可能发生错误。
二进制数、八进制数和十六进制数的表示
二进制
二进制由 0 和 1 两个数字组成,使用时必须以0b或0B(不区分大小写)开头
//合法的二进制
int a = 0b101; //换算成十进制为 5
int b = -0b110010; //换算成十进制为 -50
int c = 0B100001; //换算成十进制为 33
//非法的二进制
int m = 101010; //无前缀 0B,相当于十进制
int n = 0B410; //4不是有效的二进制数字
读者请注意,标准的C语言并不支持上面的二进制写法,只是有些编译器自己进行了扩展,才支持二进制数字。换句话说,并不是所有的编译器都支持二进制数字,只有一部分编译器支持,并且跟编译器的版本有关系。
下面是实际测试的结果:
- Visual C++ 6.0 不支持。
- Visual Studio 2015 支持,但是 Visual Studio 2010 不支持;可以认为,高版本的 Visual Studio 支持二进制数字,低版本的 Visual Studio 不支持。
- GCC 4.8.2 支持,但是 GCC 3.4.5 不支持;可以认为,高版本的 GCC 支持二进制数字,低版本的 GCC 不支持。
- LLVM/Clang 支持(内嵌于 Mac OS 下的 Xcode 中)。
八进制
八进制由 0~7 八个数字组成,使用时必须以0开头(注意是数字 0,不是字母 o)
//合法的八进制数
int a = 015; //换算成十进制为 13
int b = -0101; //换算成十进制为 -65
int c = 0177777; //换算成十进制为 65535
//非法的八进制
int m = 256; //无前缀 0,相当于十进制
int n = 03A2; //A不是有效的八进制数字
十六进制
十六进制由数字 0~9、字母 A~F 或 a~f(不区分大小写)组成,使用时必须以0x或0X(不区分大小写)开头
//合法的十六进制
int a = 0X2A; //换算成十进制为 42
int b = -0XA0; //换算成十进制为 -160
int c = 0xffff; //换算成十进制为 65535
//非法的十六进制
int m = 5A; //没有前缀 0X,是一个无效数字
int n = 0X3H; //H不是有效的十六进制数字
二进制数、八进制数和十六进制数的输出
C语言中常用的整数有 short、int 和 long 三种类型,通过 printf 函数,可以将它们以八进制、十进制和十六进制的形式输出。
short | int | long | |
---|---|---|---|
八进制 | %ho | %o | %lo |
十进制 | %hd | %d | %ld |
十六进制 | %hx 或者 %hX | %x 或者 %X | %lx 或者 %lX |
二进制数的输出
部分编译器支持二进制数字的表示,但是却不能使用 printf 函数输出二进制。
通过转换函数可以将其它进制数字转换成二进制数字,并以字符串的形式存储,然后在 printf 函数中使用%s输出即可。
八进制数的输出
八进制数字和十进制数字不区分大小写,所以格式控制符都用小写形式。
十六进制数的输出
十六进制数字的表示用到了英文字母,有大小写之分,要在格式控制符中体现出来:
- %hx、%x 和 %lx 中的x小写,表明以小写字母的形式输出十六进制数;
- %hX、%X 和 %lX 中的X大写,表明以大写字母的形式输出十六进制数。
short a = 0b1010110; //二进制数字
int b = 02713; //八进制数字
long c = 0X1DAB83; //十六进制数字
printf("a=%ho, b=%o, c=%lon", a, b, c); //以八进制形似输出
printf("a=%hd, b=%d, c=%ldn", a, b, c); //以十进制形式输出
printf("a=%hx, b=%x, c=%lxn", a, b, c); //以十六进制形式输出(字母小写)
printf("a=%hX, b=%X, c=%lXn", a, b, c); //以十六进制形式输出(字母大写)
运行结果:
a=126, b=2713, c=7325603
a=86, b=1483, c=1944451
a=56, b=5cb, c=1dab83
a=56, b=5CB, c=1DAB83
一个数字不管以何种进制来表示,都能够以任意进制的形式输出。数字在内存中始终以二进制的形式存储,其它进制的数字在存储前都必须转换为二进制形式;同理,一个数字在输出时要进行逆向的转换,也就是从二进制转换为其他进制。
输出时加上前缀
- 对于八进制数字,它没法和十进制、十六进制区分,因为八进制、十进制和十六进制都包含 0~7 这几个数字。
- 对于十进制数字,它没法和十六进制区分,因为十六进制也包含 0~9 这几个数字。如果十进制数字中还不包含 8 和 9,那么也不能和八进制区分了。
- 对于十六进制数字,如果没有包含 a~f 或者 A~F,那么就无法和十进制区分,如果还不包含 8 和 9,那么也不能和八进制区分了。
区分不同进制数字的一个简单办法就是,在输出时带上特定的前缀。在格式控制符中加上#即可输出前缀,例如 %#x、%#o、%#lX、%#ho 等:
short a = 0b1010110; //二进制数字
int b = 02713; //八进制数字
long c = 0X1DAB83; //十六进制数字
printf("a=%#ho, b=%#o, c=%#lon", a, b, c); //以八进制形似输出
printf("a=%hd, b=%d, c=%ldn", a, b, c); //以十进制形式输出
printf("a=%#hx, b=%#x, c=%#lxn", a, b, c); //以十六进制形式输出(字母小写)
printf("a=%#hX, b=%#X, c=%#lXn", a, b, c); //以十六进制形式输出(字母大写)
运行结果:
a=0126, b=02713, c=07325603
a=86, b=1483, c=1944451
a=0x56, b=0x5cb, c=0x1dab83
a=0X56, b=0X5CB, c=0X1DAB83
C语言中的正负数及其输出
在C语言中short、int、long 都可以带上正负号,如果不带正负号,默认就是正数。
如果将一个数字分为符号和数值两部分,那么不加 unsigned 的数字称为有符号数,能表示正数和负数,加了 unsigned 的数字称为无符号数,只能表示正数。
请读者注意一个小细节,如果是unsigned int类型,那么可以省略 int ,只写 unsigned,例如:
unsigned n = 100; 它等价于: unsigned int n = 100;
对于有符号数,符号也是数字的一部分,也要在内存中体现出来。符号只有正负两种情况,用1位(Bit)就足以表示;C语言规定,把内存的最高位作为符号位;C语言规定,在符号位中,用 0 表示正数,用 1 表示负数。。
对于有符号数,如果只考虑正数,那么各种类型能表示的数值范围(取值范围)就比原来小了一半。但是在很多情况下,我们非常确定某个数字只能是正数,比如班级学生的人数、字符串的长度、内存地址等,这个时候符号位就是多余的了,就不如删掉符号位,把所有的位都用来存储数值,这样能表示的数值范围更大(大一倍)。
如果不希望设置符号位,可以在数据类型前面加上 unsigned 关键字:
unsigned short a = 12;
unsigned int b = 1002;
unsigned long c = 9892320;
无符号数的输出
无符号数可以以八进制、十进制和十六进制的形式输出,它们对应的格式控制符分别为:
unsigned short | unsigned int | unsigned long | |
---|---|---|---|
八进制 | %ho | %o | %lo |
十进制 | %hu | %u | %lu |
十六进制 | %hx 或者 %hX | %x 或者 %X | %lx 或者 %lX |
严格来说,格式控制符和整数的符号是紧密相关的,具体就是:
- %d 以十进制形式输出有符号数;
- %u 以十进制形式输出无符号数;
- %o 以八进制形式输出无符号数;
- %x 以十六进制形式输出无符号数。
C 语言中printf 并不支持八进制和十六进制形式输出有符号数呢
下表全面地总结了不同类型的整数,以不同进制的形式输出时对应的格式控制符(- -表示没有对应的格式控制符):
short | int | long | unsigned short | unsigned int | unsigned long | |
---|---|---|---|---|---|---|
八进制 | - - | - - | - - | %ho | %o | %lo |
十进制 | %hd | %d | %ld | %hu | %u | %lu |
十六进制 | - - | - - | - - | %hx 或者 %hX | %x 或者 %X | %lx 或者 %lX |
- 当以有符号数的形式输出时,printf 会读取数字所占用的内存,并把最高位作为符号位,把剩下的内存作为数值位;
- 当以无符号数的形式输出时,printf 也会读取数字所占用的内存,并把所有的内存都作为数值位对待;
- 对于一个有符号的正数,它的符号位是 0,当按照无符号数的形式读取时,符号位就变成了数值位,但是该位恰好是 0 而不是 1,所以对数值不会产生影响;
- “有符号正数的最高位是 0”这个巧合才使得 %o 和 %x 输出有符号数时不会出错。
- 不管是以 %o、%u、%x 输出有符号数,还是以 %d 输出无符号数,编译器都不会报错,只是对内存的解释不同了;
整数在内存中是如何存储的
存储方式的目的:为了提高加减法的运算效率,硬件电路要设计得尽量简单。
原因:加法和减法是计算机中最基本的运算,计算机时时刻刻都离不开它们,所以它们由硬件直接支持。
存储方案难点:
- 加法和减法是计算机中最基本的运算,计算机时时刻刻都离不开它们,所以它们由硬件直接支持,加法和减法是两种运算,增加了电路设计的复杂度。
- 对于有符号数,内存要区分符号位和数值位,对于人脑来说,很容易辨别,但是对于计算机来说,就要设计专门的电路,这无疑增加了硬件的复杂性,增加了计算的时间。
设计思路:
- 加法和减法可以合并为一种运算,就是加法运算,因为减去一个数相当于加上这个数的相反数。
- 把符号位和数值位等同起来,让它们一起参与运算,不再加以区分,这样硬件电路就变得简单了。
存储方法:
在计算机内存中,整数一律采用补码的形式来存储。这意味着,当读取整数时还要采用逆向的转换,也就是将补码转换为原码。
原码:将一个整数转换成二进制形式,就是其原码。例如short a = 6;,a 的原码就是0000 0000 0000 0110;更改 a 的值a = -18;,此时 a 的原码就是1000 0000 0001 0010。
反码:对于正数,它的反码就是其原码(原码和反码相同);负数的反码是将原码中除符号位以外的所有位(数值位)取反,也就是 0 变成 1,1 变成 0。例如short a = 6;,a 的原码和反码都是0000 0000 0000 0110;更改 a 的值a = -18;,此时 a 的反码是1111 1111 1110 1101。
补码:对于正数,它的补码就是其原码(原码、反码、补码都相同);负数的补码是其反码加 1。例如short a = 6;,a 的原码、反码、补码都是0000 0000 0000 0110;更改 a 的值a = -18;,此时 a 的补码是1111 1111 1110 1110。
C语言整数的取值范围以及数值溢出
有符号数: 以int为例,共32位,最高位为符号位,31位为数值位
无符号数: 以int为例,共32位,无符号位,32位均为数值位
无符号数的取值范围
计算无符号数(unsigned 类型)的取值范围(或者说最大值和最小值)很容易,将内存中的所有位(Bit)都置为 1 就是最大值,都置为 0 就是最小值。
以 unsigned char 类型为例,它的长度是 1,占用 8 位的内存,所有位都置为 1 时,它的值为 28 - 1 =
255,所有位都置为 0 时,它的值很显然为 0。由此可得,unsigned char 类型的取值范围是 0~255。
有符号数的取值范围
char | short | int(4个字节) | long(8个字节) | |
---|---|---|---|---|
最小值 | -2^7 = -128 | -2^15 = -32,768 ≈ -3.2万 | -2^31 = -2,147,483,648 ≈ -21亿 | -2^63 ≈ -9.22×10^18 |
最大值 | 2^7 - 1= 127 | 2^15 - 1 = 32,767 ≈ 3.2万 | 2^31 - 1 = 2,147,483,647 ≈ 21亿 | 2^63 - 1≈ 9.22×10^18 |
数值溢出
#include <stdio.h>
int main()
{
unsigned int a = 0x100000000;
int b = 0xffffffff;
printf("a=%u, b=%dn", a, b);
return 0;
}
运行结果:
a=0, b=-1
a 定义为无符号int类型,占4个字节,32位,最大值是 0xFFFFFFFF,0x100000000 = 0xFFFFFFFF + 1,溢出1,最高位溢出,只能读到32位0,所以读出来是0
b 定义为有符号数,有效数值位是31位,而0xffffffff,转化为二进制是32位1:1111 1111 … 1111, 所以最高位的 1 会覆盖符号位,数值位只留下 31 个 1,所以 b 的原码为:1111 1111 …… 1111 1111, 这也是 b 在内存中的存储形式。当 printf 读取到 b 时,由于最高位是 1,所以会被判定为负数,要从补码转换为原码:
[1111 1111 …… 1111 1111]补
= [1111 1111 …… 1111 1110]反
= [1000 0000 …… 0000 0001]原
= -1
最终 b 的输出结果为 -1。(关于b在内存中存储为1111 1111 …… 1111 1111这一点,尚不能理解;也无法理解最高位覆盖符号位的描述)
C语言中的小数(float,double)
C语言中常用的小数有两种类型,分别是 float 或 double;float 称为单精度浮点型,double 称为双精度浮点型。
小数的长度是固定的,float 始终占用4个字节,double 始终占用8个字节。
C语言同时支持两种形式的小数(十进制形式&指数形式)。但是在书写时,C语言中的指数形式和数学中的指数形式有所差异:
数学中的指数:7.25×10^2
C语言中的指数:aEn 或 aen
2.1E5 = 2.1×10^5,其中 2.1 是尾数,5 是指数。
3.7E-2 = 3.7×10^-2,其中 3.7 是尾数,-2 是指数
小数的输出
- %f 以十进制形式输出 float 类型;
- %lf 以十进制形式输出 double 类型;
- %e 以指数形式输出 float 类型,输出结果中的 e 小写;
- %E 以指数形式输出 float 类型,输出结果中的 E 大写;
- %le 以指数形式输出 double 类型,输出结果中的 e 小写;
- %lE 以指数形式输出 double 类型,输出结果中的 E 大写。
#include <stdio.h>
#include <stdlib.h>
int main()
{
float a = 0.302;
float b = 128.101;
double c = 123;
float d = 112.64E3;
double e = 0.7623e-2;
float f = 1.23002398;
printf("a=%e nb=%f nc=%lf nd=%lE ne=%lf nf=%fn", a, b, c, d, e, f);
return 0;
}
运行结果:
a=3.020000e-01
b=128.100998
c=123.000000
d=1.126400E+05
e=0.007623
f=1.230024
- %f 和 %lf 默认保留六位小数,不足六位以 0 补齐,超过六位按四舍五入截断。
- 将整数赋值给 float 变量时会变成小数。
- 以指数形式输出小数时,输出结果为科学计数法;也就是说,尾数部分的取值为:0 ≤ 尾数 < 10。
- b 的输出结果让人费解,才三位小数,为什么不能精确输出,而是输出一个近似值呢?这和小数在内存中的存储形式有关,很多简单的小数压根不能精确存储,所以也就不能精确输出。
小数还有一种更加智能的输出方式,就是使用%g。%g 会对比小数的十进制形式和指数形式,以最短的方式来输出小数,让输出结果更加简练。所谓最短,就是输出结果占用最少的字符。
#include <stdio.h>
#include <stdlib.h>
int main()
{
float a = 0.00001;
float b = 30000000;
float c = 12.84;
float d = 1.229338455;
printf("a=%g nb=%g nc=%g nd=%gn", a, b, c, d);
return 0;
}
运行结果:
a=1e-05
b=3e+07
c=12.84
d=1.22934
- %g 默认最多保留六位有效数字,包括整数部分和小数部分;%f 和 %e 默认保留六位小数,只包括小数部分。
- %g 不会在最后强加 0 来凑够有效数字的位数,而 %f 和 %e 会在最后强加 0 来凑够小数部分的位数。
- %g 和 %lg 分别用来输出 float 类型和 double 类型,并且当以指数形式输出时,e小写。
- %G 和 %lG 也分别用来输出 float 类型和 double 类型,只是当以指数形式输出时,E大写。
数字的后缀
- 在整数后面紧跟 l 或者 L(不区分大小写)表明该数字是 long 类型;
- 在小数后面紧跟 f 或者 F(不区分大小写)表明该数字是 float 类型。
小数和整数相互赋值
- 将一个整数赋值给小数类型,在小数点后面加 0 就可以,加几个都无所谓。
- 将一个小数赋值给整数类型,就得把小数部分丢掉,只能取整数部分,这会改变数字本来的值。注意是直接丢掉小数部分,而不是按照四舍五入取近似值。
小数在内存中是如何存储的
小数在内存中是以浮点数的形式存储的。浮点数是数字(或者说数值)在内存中的一种存储格式,它和定点数是相对的。
C语言标准规定,小数在内存中以科学计数法的形式来存储,具体形式为:
flt = (-1)sign × mantissa × baseexponent
在C语言中使用英文字符
字符类型由单引号’ '包围,字符串由双引号" "包围。
//字符正确的写法
char a = '1';
char b = '$';
char c = 'X';
char d = ' '; // 空格也是一个字符
//错误的写法
char x = '中'; //char 类型不能包含 ASCII 编码之外的字符
char y = 'A'; //A 是一个全角字符
char z = "t"; //字符类型应该由单引号包围
// 字符串
char str1[] = "http://c.biancheng.net";
char *str2 = "C语言中文网";
在C语言中使用中文字符
非ASCII 编码字符存储
使用宽字符的编码方式。常见的宽字符编码有 UTF-16 和 UTF-32,它们都是基于 Unicode 字符集的,能够支持全球的语言文化。
C语言推出了一种新的类型,叫做 wchar_t。wchar_t 的长度由编译器决定:
- 在微软编译器下,它的长度是 2,等价于 unsigned short;
- 在GCC、LLVM/Clang 下,它的长度是 4,等价于 unsigned int。
单独的字符由单引号’ ‘包围,例如’B’、‘@’、‘9’等;但是,这样的字符只能使用 ASCII 编码,要想使用宽字符的编码方式,就得加上L前缀,例如L’A’、L’9’、L’中’、L’国’、L’。'。
注意,加上L前缀后,所有的字符都将成为宽字符,占用 2 个字节或者 4 个字节的内存,包括 ASCII 中的英文字符。给字符串加上L前缀就变成了宽字符串,它包含的每个字符都是宽字符,一律采用 UTF-16 或者 UTF-32 编码。
wchar_t a = L'A'; //英文字符(基本拉丁字符)
wchar_t b = L'9'; //英文数字(阿拉伯数字)
wchar_t c = L'中'; //中文汉字
wchar_t d = L'国'; //中文汉字
wchar_t e = L'。'; //中文标点
wchar_t f = L'ヅ'; //日文片假名
wchar_t g = L'♥'; //特殊符号
wchar_t h = L'༄'; //藏文
wchar_t web_url[] = L"http://c.biancheng.net";
wchar_t *web_name = L"C语言中文网";
将不加L前缀的字符称为窄字符,将加上L前缀的字符称为宽字符。窄字符使用 ASCII 编码,宽字符使用 UTF-16 或者 UTF-32 编码。
宽字符(串)的输出
putchar、printf 只能输出不加L前缀的窄字符,对加了L前缀的宽字符无能为力,我们必须使用 <wchar.h> 头文件中的宽字符输出函数,它们分别是 putwchar 和 wprintf:
- putwchar 函数专门用来输出一个宽字符,它和 putchar 的用法类似;
- wprintf 是通用的、格式化的宽字符输出函数,它除了可以输出单个宽字符,还可以输出宽字符串。宽字符对应的格式控制符为%lc, 宽字符串对应的格式控制符是%ls。
在输出宽字符之前还要使用 setlocale 函数(setlocale 函数位于 <locale.h> 头文件中)进行本地化设置,告诉程序如何才能正确地处理各个国家的语言文化。(先记住)
希望设置为中文简体环境:
Windows 下请写作:
setlocale(LC_ALL, "zh-CN");
在 Linux 和 Mac OS 下请写作:
setlocale(LC_ALL, "zh_CN");
代码演示:
#include <wchar.h>
#include <locale.h>
int main(){
wchar_t a = L'A'; //英文字符(基本拉丁字符)
wchar_t b = L'9'; //英文数字(阿拉伯数字)
wchar_t c = L'中'; //中文汉字
wchar_t d = L'国'; //中文汉字
wchar_t e = L'。'; //中文标点
wchar_t f = L'ヅ'; //日文片假名
wchar_t g = L'♥'; //特殊符号
wchar_t h = L'༄'; //藏文
//将本地环境设置为简体中文
setlocale(LC_ALL, "zh_CN");
//使用专门的 putwchar 输出宽字符
putwchar(a); putwchar(b); putwchar(c); putwchar(d);
putwchar(e); putwchar(f); putwchar(g); putwchar(h);
putwchar(L'n'); //只能使用宽字符
//使用通用的 wprintf 输出宽字符
wprintf(
L"Wide chars: %lc %lc %lc %lc %lc %lc %lc %lcn", //必须使用宽字符串
a, b, c, d, e, f, g, h
);
//将本地环境设置为简体中文
setlocale(LC_ALL, "zh_CN");
//使用通用的 wprintf 输出宽字符
wprintf(L"web_url: %ls nweb_name: %lsn", web_url, web_name);
return 0;
}
C语言到底使用的编码
-
对于 char 类型的窄字符,始终使用 ASCII 编码。
-
对于 wchar_t 类型的宽字符和宽字符串,使用 UTF-16 或者 UTF-32 编码,它们都是基于 Unicode 字符集的。
-
对于 char 类型的窄字符串,微软编译器使用本地编码,GCC、LLVM/Clang 使用和源文件编码相同的编码。
C语言转义字符
转义字符以或者x开头:
- 以开头表示后跟八进制形式的编码值,
- 以x开头表示后跟十六进制形式的编码值。
- 对于转义字符来说,只能使用八进制或者十六进制。
- 转义字符既可以用于单个字符,也可以用于字符串,并且一个字符串中可以同时使用八进制形式和十六进制形式。
转义字符的初衷是用于 ASCII 编码,所以它的取值范围有限:
- 八进制形式的转义字符最多后跟三个数字,也即ddd,最大取值是177;
- 十六进制形式的转义字符最多后跟两个数字,也即xdd,最大取值是x7f。
- 超出范围的转义字符的行为是未定义的,有的编译器会将编码值直接输出,有的编译器会报错。
对于 ASCII 编码,0~31(十进制)范围内的字符为控制字符,它们都是看不见的,不能在显示器上显示,甚至无法从键盘输入,只能用转义字符的形式来表示。不过,直接使用 ASCII 码记忆不方便,也不容易理解,所以,针对常用的控制字符,C语言又定义了简写方式,完整的列表如下:
ASCII码值(十进制) | 意义 | 转义字符 |
---|---|---|
007 | 响铃(BEL) | a |
008 | 退格(BS) ,将当前位置移到前一列 | b |
012 | 换页(FF),将当前位置移到下页开头 | f |
010 | 换行(LF) ,将当前位置移到下一行开头 | n |
013 | 回车(CR) ,将当前位置移到本行开头 | r |
009 | 水平制表(HT) | t |
011 | 垂直制表(VT) | v |
039 | 单引号 | ’ |
034 | 双引号 | " |
092 | 反斜杠 |
C语言标识符、关键字、注释、表达式和语句
标识符
- C语言规定,标识符只能由字母(A~Z, az)、数字(09)和下划线(_)组成,并且第一个字符必须是字母或下划线,不能是数字。
- C语言虽然不限制标识符的长度,但是它受到不同编译器的限制,同时也受到操作系统的限制。例如在某个编译器中规定标识符前128位有效,当两个标识符前128位相同时,则被认为是同一个标识符。
- 在标识符中,大小写是有区别的,例如 BOOK 和 book 是两个不同的标识符。
- 标识符虽然可由程序员随意定义,但标识符是用于标识某个量的符号,因此,命名应尽量有相应的意义,以便于阅读和理解,作到“顾名思义”。
关键字
关键字 | 说明 |
---|---|
enum | 声明枚举类型 |
union | 声明共用数据类型 |
struct | 声明结构体变量或函数 |
char | 声明字符型变量或函数 |
double | 声明双精度变量或函数 |
float | 声明浮点型变量或函数 |
long | 声明长整型变量或函数 |
int | 声明整型变量或函数 |
short | 声明短整型变量或函数 |
case | 开关语句分支 |
switch | 用于开关语句 |
else | 条件语句否定分支(与 if 连用) |
if | 条件语句 |
void | 声明函数无返回值或无参数,声明无类型指针 |
volatile | 说明变量在程序执行中可被隐含地改变 |
static | 声明静态变量 |
register | 声明寄存器变量 |
extern | 声明变量是在其他文件正声明 |
signed | 声明有符号类型变量或函数 |
unsigned | 声明无符号类型变量或函数 |
const | 声明只读变量 |
typedef | 用以给数据类型取别名 |
auto | 声明自动变量 |
return | 子程序返回语句(可以带参数,也可不带参数)循环条件 |
sizeof | 计算数据类型长度 |
default | 开关语句中的“其他”分支 |
break | 跳出当前循环 |
continue | 结束当前循环,开始下一轮循环 |
goto | 无条件跳转语句 |
while | 循环语句的循环条件 |
do | 循环语句的循环体 |
for | 一种循环语句 |
注释
C语言支持单行注释和多行注释:
- 单行注释以//开头,直到本行末尾(不能换行);
- 多行注释以/开头,以/结尾,注释内容可以有一行或多行。
表达式(Expression)和语句(Statement)
- 表达式必须有一个执行结果,这个结果必须是一个值,例如3*4+5的结果 17,a=c=d=10的结果是 10,printf(“hello”)的结果是 5(printf 的返回值是成功打印的字符的个数)。
- 以分号;结束的往往称为语句,而不是表达式,例如3*4+5;、a=c=d;等。
C语言加减乘除运算
加法 | 减法 | 乘法 | 除法 | 求余数(取余) | |
---|---|---|---|---|---|
数学 | + | - | × | ÷ | 无 |
C语言 | + | - | × | / | % |
对除法的说明
C语言中的除法运算有点奇怪,不同类型的除数和被除数会导致不同类型的运算结果:
- 当除数和被除数都是整数时,运算结果也是整数;如果不能整除,那么就直接丢掉小数部分,只保留整数部分,这跟将小数赋值给整数类型是一个道理。
- 一旦除数和被除数中有一个是小数,那么运算结果也是小数,并且是 double 类型的小数。
- 另外需要注意的一点是除数不能为 0,因为任何一个数字除以 0 都没有意义。
对取余运算的说明
C语言中的取余运算只能针对整数,也就是说,% 的两边都必须是整数,不能出现小数,否则编译器会报错。
余数可以是正数也可以是负数,由 % 左边的整数决定:
- 如果 % 左边是正数,那么余数也是正数;
- 如果 % 左边是负数,那么余数也是负数。
在 printf 中,% 是格式控制符的开头,是一个特殊的字符,不能直接输出;要想输出 %,必须在它的前面再加一个 %,这个时候 % 就变成了普通的字符,而不是用来表示格式控制符了。
运算的简写
a = a # b 可以简写为:a #= b (# 表示 +、-、*、/、% 中的任何一种运算符。)
注意:a #= b 仅是一种简写形式,不会影响程序的执行效率。
C语言自增(++)和自减(–)
++和–分别称为自增运算符和自减运算符
自增自减完成后,会用新值替换旧值,将新值保存在当前变量中
自增自减的结果必须得有变量来接收,所以自增自减只能针对变量,不能针对数字,例如10++就是错误的。
需要重点说明的是,++ 在变量前面和后面是有区别的:
- ++ 在前面叫做前自增(例如 ++a)。前自增先进行自增运算,再进行其他操作。
- ++ 在后面叫做后自增(例如 a++)。后自增先进行其他操作,再进行自增运算。
- 自减(–)也一样,有前自减和后自减之分。
#include <stdio.h>
int main()
{
int a = 12, b = 1;
int c = a - (b--); // ①
int d = (++a) - (--b); // ②
printf("c=%d, d=%dn", c, d);
return 0;
}
// 输出结果:c=11, d=14
C语言变量的定义位置以及初始值
在函数外部定义的变量叫做全局变量(Global Variable),在函数内部定义的变量叫做局部变量(Local Variable)
局部变量的定义位置
为了让编译器方便给变量分配内存,C89 标准规定,所有的局部变量(函数内部的变量)都必须定义在函数的开头位置,在定义完所有变量之前不能有其它的表达式。这种规定太过死板,虽然变量定义在函数开头,但是使用变量可能在函数的尾部,如果函数比较长,那么定义变量和使用变量的距离就有点远了,编写代码或者阅读代码时就要频繁得向前翻看代码,非常不方便,所以后来的 C99 标准就取消了这个限制。在实际开发中确认编译器开发标准是否支持代码格式.
变量的默认初始值
一个变量,即使不给它赋值,它也会有一个默认的值,这个值就是默认初始值。
- 对于全局变量,它的默认初始值始终是 0,因为全局变量存储在内存分区中的全局数据区,这个区域中的数据在程序载入内存后会被初始化为 0。
- 而对于局部变量,C语言并没有规定它的默认初始值是什么,所以不同的编译器进行了不同的扩展,有的编译器会初始化为 0,有的编译器放任不管,爱是什么就是什么。(变量定义时会给变量分配一块内存空间,如果不对变量进行初始化,那就意味着不对这块内存进行写入操作,这块内存的数据会保持不变,依然是分配之前的数据。这样的数据可能是当前程序在之前的运行过程中产生的,也可能是之前运行过的其它程序产生的,我们根本无法预测这样的数据到底是什么,所以你会看到它是一个毫无意义的值,这样的值是随机的,是垃圾值,没有使用价值。)
C语言运算符的优先级和结合性
当一个表达式中出现多个运算符时,C语言会先比较各个运算符的优先级,按照优先级从高到低的顺序依次执行;当遇到优先级相同的运算符时,再根据结合性决定先执行哪个运算符:如果是左结合性就先执行左边的运算符,如果是右结合性就先执行右边的运算符。
运算符优先级和结合性一览表
优先级 | 运算符 | 名称或含义 | 使用形式 | 结合方向 | 说明 |
---|---|---|---|---|---|
1 | [] | 数组下标 | 数组名[常量表达式] | 左到右 | |
() | 圆括号 | (表达式) | |||
. | 成员选择(对象) | 对象.成员名 | |||
-> | 成员选择(指针) | 对象指针->成员名 | |||
2 | - | 负号运算符 | -表达式 | 右到左 | 单目运算符 |
(类型) | 强制类型转换 | (数据类型)表达式 | |||
++ | 自增运算符 | ++变量名 | 单目运算符 | ||
-- | 自减运算符 | --变量名 | 单目运算符 | ||
* | 取值运算符 | *指针变量 | 单目运算符 | ||
& | 取地址运算符 | &变量名 | 单目运算符 | ||
! | 逻辑非运算符 | !表达式 | 单目运算符 | ||
~ | 按位取反运算符 | ~表达式 | 单目运算符 | ||
sizeof | 长度运算符 | sizeof(表达式) | |||
3 | / | 除 | 表达式 / 表达式 | 左到右 | 双目运算符 |
* | 乘 | 表达式*表达式 | 双目运算符 | ||
% | 余数(取模) | 整型表达式%整型表达式 | 双目运算符 | ||
4 | + | 加 | 表达式+表达式 | 左到右 | 双目运算符 |
- | 减 | 表达式-表达式 | 双目运算符 | ||
5 | << | 左移 | 变量<<表达式 | 左到右 | 双目运算符 |
>> | 右移 | 变量>>表达式 | 双目运算符 | ||
6 | > | 大于 | 表达式>表达式 | 左到右 | 双目运算符 |
>= | 大于等于 | 表达式>=表达式 | 双目运算符 | ||
< | 小于 | 表达式<表达式 | 双目运算符 | ||
<= | 小于等于 | 表达式<=表达式 | 双目运算符 | ||
7 | == | 等于 | 表达式==表达式 | 左到右 | 双目运算符 |
!= | 不等于 | 表达式!= 表达式 | 双目运算符 | ||
8 | & | 按位与 | 表达式&表达式 | 左到右 | 双目运算符 |
9 | ^ | 按位异或 | 表达式^表达式 | 左到右 | 双目运算符 |
10 | | | 按位或 | 表达式|表达式 | 左到右 | 双目运算符 |
11 | && | 逻辑与 | 表达式&&表达式 | 左到右 | 双目运算符 |
12 | || | 逻辑或 | 表达式||表达式 | 左到右 | 双目运算符 |
13 | ?: | 条件运算符 | 表达式1? 表达式2: 表达式3 | 右到左 | 三目运算符 |
14 | = | 赋值运算符 | 变量=表达式 | 右到左 | |
/= | 除后赋值 | 变量/=表达式 | |||
*= | 乘后赋值 | 变量*=表达式 | |||
%= | 取模后赋值 | 变量%=表达式 | |||
+= | 加后赋值 | 变量+=表达式 | |||
-= | 减后赋值 | 变量-=表达式 | |||
<<= | 左移后赋值 | 变量<<=表达式 | |||
>>= | 右移后赋值 | 变量>>=表达式 | |||
&= | 按位与后赋值 | 变量&=表达式 | |||
^= | 按位异或后赋值 | 变量^=表达式 | |||
|= | 按位或后赋值 | 变量|=表达式 | |||
15 | , | 逗号运算符 | 表达式,表达式,… | 左到右 |
|
C语言数据类型转换(自动类型转换+强制类型转换)
自动类型转换
- 将一种类型的数据赋值给另外一种类型的变量时就会发生自动类型转换。在赋值运算中,赋值号两边的数据类型不同时,需要把右边表达式的类型转换为左边变量的类型,这可能会导致数据失真,或者精度降低;所以说,自动类型转换并不一定是安全的。
- 在不同类型的混合运算中,编译器也会自动地转换数据类型,将参与运算的所有数据先转换为同一种类型,然后再进行计算。转换的规则如下:
2-1: 转换按数据长度增加的方向进行,以保证数值不失真,或者精度不降低。例如,int 和 long 参与运算时,先把 int 类型的数据转成 long 类型后再进行运算。
2-2: 所有的浮点运算都是以双精度进行的,即使运算中只有 float 类型,也要先转换为 double 类型,才能进行运算。
2-3: char 和 short 参与运算时,必须先转换成 int 类型。
强制类型转换
自动类型转换是编译器默默地、隐式地进行的一种类型转换,不需要在代码中体现出来;强制类型转换是程序员明确提出的、需要通过特定格式的代码来指明的一种类型转换。换句话说,自动类型转换不需要程序员干预,强制类型转换必须有程序员干预。
强制类型转换的格式为:(type_name) expression
例如:
(float) a; //将变量 a 转换为 float 类型
(int)(x+y); //把表达式 x+y 的结果转换为 int 整型
(float) 100; //将数值 100(默认为int类型)转换为 float 类型
( )的优先级高于/,对于表达式(double) sum / count,会先执行(double) sum,将 sum 转换为 double 类型,然后再进行除法运算,这样运算结果也是 double 类型,能够保留小数部分。注意不要写作(double) (sum / count),这样写运算结果将仍然不能保留小数部分。
类型转换只是临时性的
无论是自动类型转换还是强制类型转换,都只是为了本次运算而进行的临时性转换,转换的结果也会保存到临时的内存空间,不会改变数据本来的类型或者值。
C语言输入输出
C语言数据输出大汇总以及轻量进阶
在C语言中,有三个函数可以用来在显示器上输出数据,它们分别是:
puts():只能输出字符串,并且输出结束后会自动换行
putchar():只能输出单个字符
printf():可以输出各种类型的数据,是最灵活、最复杂、最常用的输出函数,完全可以替代 puts() 和 putchar()
汇总一下前面学到的格式控制符:
格式控制符 | 说明 |
---|---|
%c | 输出一个单一的字符 |
%hd、%d、%ld | 以十进制、有符号的形式输出 short、int、long 类型的整数 |
%hu、%u、%lu | 以十进制、无符号的形式输出 short、int、long 类型的整数 |
%ho、%o、%lo | 以八进制、不带前缀、无符号的形式输出 short、int、long 类型的整数 |
%#ho、%#o、%#lo | 以八进制、带前缀、无符号的形式输出 short、int、long 类型的整数 |
%hx、%x、%lx %hX、%X、%lX | 以十六进制、不带前缀、无符号的形式输出 short、int、long 类型的整数。如果 x 小写,那么输出的十六进制数字也小写;如果 X 大写,那么输出的十六进制数字也大写。 |
%#hx、%#x、%#lx %#hX、%#X、%#lX | 以十六进制、带前缀、无符号的形式输出 short、int、long 类型的整数。如果 x 小写,那么输出的十六进制数字和前缀都小写;如果 X 大写,那么输出的十六进制数字和前缀都大写。 |
%f、%lf | 以十进制的形式输出 float、double 类型的小数 |
%e、%le %E、%lE | 以指数的形式输出 float、double 类型的小数。如果 e 小写,那么输出结果中的 e 也小写;如果 E 大写,那么输出结果中的 E 也大写。 |
%g、%lg %G、%lG | 以十进制和指数中较短的形式输出 float、double 类型的小数,并且小数部分的最后不会添加多余的 0。如果 g 小写,那么当以指数形式输出时 e 也小写;如果 G 大写,那么当以指数形式输出时 E 也大写。 |
%s | 输出一个字符串 |
printf() 的高级用法
printf() 格式控制符的完整形式如下:
%[flag][width][.precision]type ( [ ] 表示此处的内容可有可无,是可以省略的)
- type 表示输出类型,比如 %d、%f、%c、%lf,type 就分别对应 d、f、c、lf;再如,%-9d中 type 对应 d。type 这一项必须有,这意味着输出时必须要知道是什么类型。
- width 表示最小输出宽度,也就是至少占用几个字符的位置;例如,%-9d中 width 对应 9,表示输出结果最少占用 9 个字符的宽度。当输出结果的宽度不足 width 时,以空格补齐(如果没有指定对齐方式,默认会在左边补齐空格);当输出结果的宽度超过 width 时,width 不再起作用,按照数据本身的宽度来输出。
- .precision 表示输出精度
当.precision用于小数时,作用于小数的位数: 当小数部分的位数大于 precision 时,会按照四舍五入的原则丢掉多余的数字;当小数部分的位数小于 precision 时,会在后面补 0。
.precision 也可以用于整数和字符串:用于整数时,.precision 表示最小输出宽度。与 width 不同的是,整数的宽度不足时会在左边补 0,而不是补空格; 用于字符串时,.precision 表示最大输出宽度,或者说截取字符串。当字符串的长度大于 precision 时,会截掉多余的字符;当字符串的长度小于 precision 时,.precision 就不再起作用。- flag 是标志字符。例如,%#x中 flag 对应 #,%-9d中 flags 对应-。下表列出了 printf() 可以用的 flag:
(-)表示左对齐。如果没有,就按照默认的对齐方式,默认一般为右对齐。
(+)用于整数或者小数,表示输出符号(正负号)。如果没有,那么只有负数才会输出符号。
(空格)用于整数或者小数,输出值为正时冠以空格,为负时冠以负号。
(#)对于八进制(%o)和十六进制(%x / %X)整数,# 表示在输出时添加前缀;八进制的前缀是 0,十六进制的前缀是 0x / 0X; 对于小数(%f / %e / %g),# 表示强迫输出小数点。如果没有小数部分,默认是不输出小数点的,加上 # 以后,即使没有小数部分也会带上小数点。
printf() 不能立即输出的问题
Windows 和 Linux、Mac OS 的缓存机制不同, 表现也不同;要想破解 printf() 输出的问题,必须要了解缓存,它能使你对输入输出的认识上升到一个更高的层次,以后不管遇到什么疑难杂症,都能迎刃而解。可以说,输入输出的“命门”就在于缓存。
C语言scanf:读取从键盘输入的数据(含输入格式汇总表)
在C语言中,有多个函数可以从键盘获得用户输入:
- scanf():和 printf() 类似,scanf() 可以输入多种类型的数据。
- getchar()、getche()、getch():这三个函数都用于输入单个字符。
- gets():获取一行数据,并作为字符串处理。
scanf() 是最灵活、最复杂、最常用的输入函数,但它不能完全取代其他函数。
&称为取地址符,也就是获取变量在内存中的地址。scanf 会根据地址把读取到的数据写入内存。
%p是一个新的格式控制符,它表示以十六进制的形式(带小写的前缀)输出数据的地址。如果写作%P,那么十六进制的前缀也将变成大写形式。
scanf()函数
对于 scanf(),输入数据的格式要和控制字符串的格式保持一致。
举例:
scanf("%d %d", &c, &d); //输入两个整数并分别赋值给c、d
// 输入:10 23↙ (↙表示按下回车键)
// "%d %d"之间是有空格的,所以输入数据时也要有空格
scanf("%d, %d, %d", &a, &b, &c);
// 56,45,78↙
// 控制字符串为"%d, %d, %d",中间以逗号分隔,所以输入的整数也要以逗号分隔。
连续输入
我们从键盘输入的数据并没有直接交给 scanf(),而是放入了缓冲区中,直到我们按下回车键,scanf() 才到缓冲区中读取数据。如果缓冲区中的数据符合 scanf() 的要求,那么就读取结束;如果不符合要求,那么就继续等待用户输入,或者干脆读取失败。
scanf()的问题
#include <stdio.h>
int main()
{
int a = 1, b = 2;
scanf("a=%d", &a);
scanf("b=%d", &b);
printf("a=%d, b=%dn", a, b);
return 0;
}
case1:
a=99↙
a=99, b=2
输入a=99,按下回车键,程序竟然运行结束了,只有第一个 scanf() 成功读取了数据,第二个 scanf() 仿佛没有执行一样,根本没有给用户任何机会去输入数据。
case2:
a=99b=200↙
a=99, b=200
a 和 b 都能够正确读取了。注意,a=99b=200中间是没有任何空格的。
case3:
a=99 b=200↙
a=99, b=2
a=99 b=200 中间有空格,第二个 scanf() 又读取失败了!
以上case都是出字C语言中文网,未实际验证,需要谨慎对待, 只能作为参照。
要想破解 scanf() 输入的问题,一定要学习缓冲区,它能使你对输入输出的认识上升到一个更高的层次,以后不管遇到什么疑难杂症,都能迎刃而解。可以说,输入输出的“命门”就在于缓冲区。
输入其它数据
除了输入整数,scanf() 还可以输入单个字符、字符串、小数等:scanf() 和 printf() 虽然功能相反,但是格式控制符是一样的,单个字符、整数、小数、字符串对应的格式控制符分别是 %c、%d、%f、%s。
对读取字符串的说明
字符串的两种定义形式,它们分别是:
- char str1[] = “http://c.biancheng.net”;
- char *str2 = “C语言中文网”;
这两种形式其实是有区别的,第一种形式的字符串所在的内存既有读取权限又有写入权限,第二种形式的字符串所在的内存只有读取权限,没有写入权限。printf()、puts() 等字符串输出函数只要求字符串有读取权限,而 scanf()、gets() 等字符串输入函数要求字符串有写入权限,所以,第一种形式的字符串既可以用于输出函数又可以用于输入函数,而第二种形式的字符串只能用于输出函数。
对于第一种形式的字符串,在[ ]里面要指明字符串的最大长度,如果不指明,也可以根据=后面的字符串来自动推算,此处,就是根据"http://c.biancheng.net"的长度来推算的。如果只是定义了一个字符串,并没有立即给它赋值,所以没法自动推算,只能手动指明最大长度,那么就需要在[]中添加类似于[30],定义一个长度。
scanf() 读取字符串时以空格为分隔,遇到空格就认为当前字符串结束了,所以无法读取含有空格的字符串
scanf() 格式控制符汇总
格式控制符 | 说明 |
---|---|
%c | 读取一个单一的字符 |
%hd、%d、%ld | 读取一个十进制整数,并分别赋值给 short、int、long 类型 |
%ho、%o、%lo | 读取一个八进制整数(可带前缀也可不带),并分别赋值给 short、int、long 类型 |
%hx、%x、%lx | 读取一个十六进制整数(可带前缀也可不带),并分别赋值给 short、int、long 类型 |
%hu、%u、%lu | 读取一个无符号整数,并分别赋值给 unsigned short、unsigned int、unsigned long 类型 |
%f、%lf | 读取一个十进制形式的小数,并分别赋值给 float、double 类型 |
%e、%le | 读取一个指数形式的小数,并分别赋值给 float、double 类型 |
%g、%lg | 既可以读取一个十进制形式的小数,也可以读取一个指数形式的小数,并分别赋值给 float、double 类型 |
%s | 读取一个字符串(以空白符为结束) |
C语言输入字符和字符串(所有函数大汇总)
getchar()
最容易理解的字符输入函数是 getchar(),它就是scanf(“%c”, c)的替代品,除了更加简洁,没有其它优势了;或者说,getchar() 就是 scanf() 的一个简化版本。
getche()
- getche() 没有缓冲区,输入一个字符后会立即读取,不用等待用户按下回车键,这是它和 scanf()、getchar() 的最大区别。
- getche() 并不是标准函数,默认只能在 Windows 下使用,不能在 Linux 和 Mac OS 下使用。
getch()
- getch() 也没有缓冲区,输入一个字符后会立即读取,不用按下回车键,这一点和 getche() 相同。
- getch() 的特别之处是它没有回显,看不到输入的字符。所谓回显,就是在控制台上显示出用户输入的字符;没有回显,就不会显示用户输入的字符,就好像根本没有输入一样。回显在大部分情况下是有必要的,它能够与用户及时交互,让用户清楚地看到自己输入的内容。但在某些特殊情况下,我们却不希望有回显,例如输入密码,有回显是非常危险的,容易被偷窥。
- 和 getche() 一样,getch也不是标准函数,默认只能在 Windows 下使用,不能在 Linux 和 Mac OS 下使用。
gets()
gets() 是有缓冲区的,每次按下回车键,就代表当前输入结束了,gets() 开始从缓冲区中读取内容,这一点和 scanf() 是一样的。gets() 和 scanf() 的主要区别是:
- scanf() 读取字符串时以空格为分隔,遇到空格就认为当前字符串结束了,所以无法读取含有空格的字符串。
- gets() 认为空格也是字符串的一部分,只有遇到回车键时才认为字符串输入结束,所以,不管输入了多少个空格,只要不按下回车键,对 gets() 来说就是一个完整的字符串。
总结
scanf() 可以一次性读取多份类型相同或者不同的数据,getchar()、getche()、getch() 和 gets() 每次只能读取一份特定类型的数据,不能一次性读取多份数据。
进入缓冲区(缓存)的世界,破解一切与输入输出有关的疑难杂症
计算机在内存中预留了一定的存储空间,用来暂时保存输入或输出的数据,这部分预留的空间就叫做缓冲区(缓存)。
为什么要引入缓冲区(缓存)
- 减少了等待写入硬盘的次数
- 减少硬件设备的读写次数, 从而减少时间和空间的开销
缓冲区的类型
-
根据缓冲区对应的是输入设备还是输出设备,可以分为输入缓冲区和输出缓冲区。
-
根据数据刷新(也可以称为清空缓冲区,就是将缓冲区中的数据“倒出”)的时机,可以分为全缓冲、行缓冲、不带缓冲。
-
- 全缓冲
当缓冲区被填满以后才进行真正的输入输出操作。缓冲区的大小都有限制的,比如 1KB、4MB 等,数据量达到最大值时就清空缓冲区。全缓冲的典型代表是对硬盘文件的读写,在实际开发中,将数据写入文件后,打开文件并不能立即看到内容,只有清空缓冲区,或者关闭文件,或者关闭程序后,才能在文件中看到内容。这种现象,就是缓冲区在作怪。
- 全缓冲
-
- 行缓冲
在输入或者输出的过程中遇到换行符时,才执行真正的输入输出操作。行缓冲的典型代表就是标准输入设备(也即键盘)和标准输出设备(也即显示器)。
- 行缓冲
-
- 不带缓冲
不带缓冲区,数据就没有地方缓存,必须立即进行输入输出。getche()、getch() 就不带缓冲区,输入一个字符后立即就执行了,根本不用按下回车键。
- 不带缓冲
C语言标准的模棱两可
C语言标准规定,输入输出缓冲区要具有以下特征:
- 当且仅当输入输出不涉及交互设备时,它们才可以是全缓冲的。
- 错误显示设备不能带有缓冲区。
缓冲区的刷新(清空)
所谓刷新缓冲区,就是将缓冲区中的内容送达到目的地。缓冲区的刷新遵循以下的规则:
- 不管是行缓冲还是全缓冲,缓冲区满时会自动刷新;
- 行缓冲遇到换行符n时会刷新;
- 关闭文件时会刷新缓冲区;
- 程序关闭时一般也会刷新缓冲区,这个是由标准库来保障的;
- 使用特定的函数也可以手动刷新缓冲区
结合C语言缓冲区谈scanf函数,那些奇怪的行为其实都有章可循
scanf() 是从标准输入设备(键盘)读取数据,带有行缓冲区的。
当遇到 scanf() 函数时,程序会先检查输入缓冲区中是否有数据:
- 如果没有,就等待用户输入。用户从键盘输入的每个字符都会暂时保存到缓冲区,直到按下回车键,产生换行符n,输入结束,scanf() 再从缓冲区中读取数据,赋值给变量。
- 如果有数据,那就看是否符合控制字符串的规则:
-
- 如果能够匹配整个控制字符串,那最好了,直接从缓冲区中读取就可以了,就不用等待用户输入了。
-
- 如果缓冲区中剩余的所有数据只能匹配前半部分控制字符串,那就等待用户输入剩下的数据。
-
- 如果不符合,scanf() 还会尝试忽略一些空白符,例如空格、制表符、换行符等:
– 3-1) 如果这种尝试成功(可以忽略一些空白符),那么再重复以上的匹配过程。
– 3-2) 如果这种尝试失败(不能忽略空白符),那么只有一种结果,就是读取失败。
- 如果不符合,scanf() 还会尝试忽略一些空白符,例如空格、制表符、换行符等:
事实上这一章的学习中,有几个点的理解是很重要的,一个是 scanf 这个方法的逻辑,还有就是一些特殊的规则:
1)匹配失败意味着不会移动内部的位置指针
2)空格、制表符、换行符在大部分情况下都可以忽略,前面的两个例子就是这样。但是当控制字符串不是以格式控制符 %d、%c、%f 等开头时,空格、制表符、换行符就不能忽略了(比如:scanf(“a=%d”, &a) 这种写法),它会参与匹配过程,如果匹配失败,就意味着 scanf() 读取失败了。
事实上以上的引用部分关于特殊规则的描述,是我自己的总结,我认为这一章中是有些错误的,也反馈给了客户,不过还没有得到回复,如果有不一样的理解请在评论区给我留言,谢谢了:
C语言清空(刷新)缓冲区,从根本上消除那些奇怪的行为
清空(刷新)缓冲区即可:
- 对于输出操作,清空缓冲区会使得缓冲区中的所有数据立即显示到屏幕上;很明显,这些数据没有地方存放了,只能输出了。
- 对于输入操作,清空缓冲区就是丢弃残留字符,让程序直接等待用户输入,避免引发奇怪的行为。
清空输出缓冲区
清空输出缓冲区很简单,使用下面的语句即可:
fflush(stdout);
fflush() 是一个专门用来清空缓冲区的函数,stdout 是 standard output 的缩写,表示标准输出设备,也即显示器。整个语句的意思是,清空标准输出缓冲区,或者说清空显示器的缓冲区。
在 Linux 和 Mac OS 平台下清空缓冲区:
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("C语言中文网");
fflush(stdout); //本次输出结束后立即清空缓冲区
sleep(5);
printf("http://c.biancheng.netn");
return 0;
}
// 程序运行后,第一个 pirntf() 立即输出,等待 5 秒以后,第二个 printf() 才输出,这就符合我们的惯性思维了。如果不加fflush(stdout)语句,程序运行后,第一个 printf() 并不会立即输出,而是等待 5 秒以后和第二个 printf() 一起输出
清空输入缓冲区
两种通用的方案
- 使用 getchar() 清空缓冲区; 原理:getchar() 是带有缓冲区(行缓冲区)的,每次从缓冲区中读取一个字符,包括空格、制表符、换行符等空白符,只要我们让 getchar() 不停地读取,直到读完缓冲区中的所有字符,就能达到清空缓冲区的效果。请看下面的代码:
#include <stdio.h>
int main()
{
int a = 1, b = 2;
char c;
scanf("a=%d", &a);
while((c = getchar()) != 'n' && c != EOF); //在下次读取前清空缓冲区;该代码不停地使用 getchar() 获取缓冲区中的字符,直到遇见换行符n或者到达文件结尾才停止。
scanf("b=%d", &b);
printf("a=%d, b=%dn", a, b);
return 0;
}
- 使用 scanf() 清空缓冲区;原理:scanf() 还有一种高级用法,就是使用类似于正则表达式的通配符,这样它就可以读取所有的字符了,包括空格、换行符、制表符等空白符,不会再忽略它们了。并且,scanf() 还允许把读取到的数据直接丢弃,不用赋值给变量。
scanf(“%*[^n]”); scanf(“%*c”);
第一个 scanf() 将逐个读取缓冲区中n之前的其它字符,% 后面的 * 表示将读取的这些字符丢弃,遇到n字符时便停止读取。此时,缓冲区中尚有一个n遗留,第二个 scanf() 再将这个n读取并丢弃,这里的星号和第一个 scanf() 的星号作用相同。由于所有从键盘的输入都是以回车结束的,而回车会产生一个n字符,所以将n连同它之前的字符全部读取并丢弃之后,也就相当于清除了输入缓冲区。
#include <stdio.h>
int main()
{
int a = 1, b = 2;
scanf("a=%d", &a);
scanf("%*[^n]"); scanf("%*c"); //在下次读取前清空缓冲区
scanf("b=%d", &b);
printf("a=%d, b=%dn", a, b);
return 0;
}
总结
最靠谱、最通用、最有效的清空输入缓冲区的方案就是使用 getchar() 或者 scanf() 将缓冲区中的数据逐个读取出来,其它方案都有或多或少的问题。
C语言scanf的高级用法,原来scanf还有这么多新技能
指定读取长度
可以在格式控制符的中间加一个数字,用来表示读取数据的最大长度,例如:
- %2d表示最多读取两位整数;
- %10s表示读取的字符串的最大长度为 10,或者说,最多读取 10 个字符。
匹配特定的字符
%s
%s 控制符会匹配除空白符以外的所有字符,它有两个缺点:
- %s 不能读取特定的字符,比如只想读取小写字母,或者十进制数字等,%s 就无能为力;
- %s 读取到的字符串中不能包含空白符,有些情况会比较尴尬,例如,无法将多个单词存放到一个字符串中,因为单词之间就是以空格为分隔的,%s 遇到空格就读取结束了。
%[xxx]
[ ]包围起来的是需要读取的字符集合。例如,%[abcd]表示只读取字符abcd,遇到其它的字符就读取结束;注意,这里并不强调字符的顺序,只要字符在 abcd 范围内都可以匹配成功,所以你可以输入 abcd、dcba、ccdc、bdcca 等。
需要注意的是虽然字符的顺序不是关键,但是字符的连续性是有要求的,依然以%[abcd]为例,如果输入字符为 a1b2c3d4,事实上最后匹配的字符是a,如果是 ab12cd34,那么匹配的字符是ab。
使用连接符
为了简化字符集合的写法,scanf() 支持使用连字符-来表示一个范围内的字符,例如 %[a-z]、%[0-9] 等(连字符左边的字符对应一个 ASCII 码,连字符右边的字符也对应一个 ASCII 码,位于这两个 ASCII 码范围以内的字符就是要读取的字符。注意,连字符左边的 ASCII 码要小于右边的,如果反过来,那么它的行为是未定义的)。
常用的连字符举例:
- %[a-z]表示读取 abc…xyz 范围内的字符,也即小写字母;
- %[A-Z]表示读取 ABC…XYZ 范围内的字符,也即大写字母;
- %[0-9]表示读取 012…789 范围内的字符,也即十进制数字。
- %[a-zA-Z]表示读取大写字母和小写字母,也即所有英文字母;
- %[a-z-A-Z0-9]表示读取所有的英文字母和十进制数字;
- %[0-9a-f]表示读取十六进制数字。
不匹配某些字符
scanf() 允许我们在%[ ]中直接指定某些不能匹配的字符,具体方法就是在不匹配的字符前面加上^,例如:
- %[^n]表示匹配除换行符以外的所有字符,遇到换行符就停止读取;
- %[^0-9]表示匹配除十进制数字以外的所有字符,遇到十进制数字就停止读取。
scanf("%[^n]", str2); //这个操作也能读取带空格的字符串,scanf()完全可以取代 gets()
input: c c++ java python go javascript↙
output: str2=c c++ java python go javascript
丢弃读取到的字符
scanf() 允许把读取到的数据直接丢弃,不往变量中存放,具体方法就是在 % 后面加一个*,例如:
- %*d表示读取一个整数并丢弃;
- %*[a-z]表示读取小写字母并丢弃;
- %*[^n]表示将换行符以外的字符全部丢弃。
这里我们就可以解释,上一章节中通过scanf来清空缓存区的原理了:
scanf("%*[^n]"); scanf("%*c");
//scanf("%*[^n]");将换行符前面的所有字符清空,scanf("%*c");将最后剩下的换行符清空。
总结
scanf() 控制字符串的完整写法为:%{*} {width} type
- { } 表示可有可无。
- type表示读取什么类型的数据,例如 %d、%s、%[a-z]、%[^n] 等;type 必须有。
- width表示最大读取宽度,可有可无。
- *表示丢弃读取到的数据,可有可无。
C语言循环结构和选择结构
C语言if else语句详解
if(判断条件){
语句块1
}else{
语句块2
}
C语言关系运算符详解
C语言提供了以下关系运算符:
关系运算符 | 含 义 | 数学中的表示 |
---|---|---|
< | 小于 | < |
<= | 小于或等于 | ≤ |
> | 大于 | > |
>= | 大于或等于 | ≥ |
== | 等于 | = |
!= | 不等于 | ≠ |
- 关系运算符都是双目运算符,其结合性均为左结合;(对于含多个关系运算符的表达式,如 k == j == i+5,根据运算符的左结合性,先计算k == j,该式不成立,其值为0,再计算0==i+5,也不成立,故表达式值为0。)
- 关系运算符的优先级低于算术运算符,高于赋值运算符
- 在六个关系运算符中,<、<=、>、>=的优先级相同,高于 == 和 != , == 和 != 的优先级相同。
- 在C语言中,有的运算符有两个操作数,例如 10+20,10和20都是操作数,+ 是运算符。我们将这样的运算符称为双目运算符。同理,将有一个操作数的运算符称为单目运算符,将有三个操作数的运算符称为三目运算符。
- 常见的双目运算符有 +、-、*、/ 等,单目运算符有 ++、-- 等,三目运算符只有一个,就是 ? :
- 关系运算符的运算结果只有 0 或 1;运算结果 1 称为“真”,表示条件成立,将 0 称为“假”,表示条件不成立。
再谈 if 语句的判断条件
if 语句的判断条件中不是必须要包含关系运算符,它可以是赋值表达式,甚至也可以是一个变量,例如:
//情况①
if(b){
//TODO:
}
//情况②
if(b=5){ //情况①
//TODO:
}
C语言逻辑运算符详解
运算符 | 说明 | 结合性 | 举例 |
---|---|---|---|
&& | 与运算,双目,对应数学中的“且” | 左结合 | 1&&0、(9>3)&&(b>a) |
|| | 或运算,双目,对应数学中的“或” | 左结合 | 1||0、(9>3)||(b>a) |
! | 非运算,单目,对应数学中的“非” | 右结合 | !a、!(2<5) |
逻辑运算的结果
逻辑运算的结果也只有“真”和“假”,“真”对应的值为 1,“假”对应的值为 0。
优先级
逻辑运算符和其它运算符优先级从低到高依次为:
赋值运算符(=) < 或|| < 与&& < 关系运算符 < 算术运算符 < 非(!)
按照运算符的优先顺序可以得出:
- a>b && c>d 等价于 (a>b)&&(c>d)
- !b==c||d<a 等价于 ((!b)==c)||(d<a)
- a+b>c&&x+y<b 等价于 ((a+b)>c)&&((x+y)<b)
- a||b&&c-3 等价于 a||(b&&(c-3))
C语言switch case语句详解
switch(表达式){
case 整型数值1: 语句 1;
case 整型数值2: 语句 2;
......
case 整型数值n: 语句 n;
default: 语句 n+1;
}
以上语句中,没有 break, 当和某个整型数值匹配成功后,会执行该分支以及后面所有分支的语句。
break 是C语言中的一个关键字,专门用于跳出 switch 语句。所谓“跳出”,是指一旦遇到 break,就不再执行 switch 中的任何语句,包括当前分支中的语句和其他分支中的语句;也就是说,整个 switch 执行结束了,接着会执行整个 switch 后面的代码。
注意点:
- case 后面必须是一个整数,或者是结果为整数的表达式,但不能包含任何变量。请看下面的例子:
case 10: printf("..."); break; //正确
case 8+9: printf("..."); break; //正确
case 'A': printf("..."); break; //正确,字符和整数可以相互转换
case 'A'+19: printf("..."); break; //正确,字符和整数可以相互转换
case 9.5: printf("..."); break; //错误,不能为小数
case a: printf("..."); break; //错误,不能包含变量
case a+10: printf("..."); break; //错误,不能包含变量
- default 不是必须的。当没有 default 时,如果所有 case 都匹配失败,那么就什么都不执行。
C语言?和:详解,C语言条件运算符详解
语法格式为:
表达式1 ? 表达式2 : 表达式3
max = (a>b) ? a : b;
等价于:
if(a>b){
max = a;
}else{
max = b;
}
- 条件运算符的优先级低于关系运算符和算术运算符,但高于赋值符。因此以上还可以直接买写作:max=a>b ? a : b;
- 条件运算符?和:是一对运算符,不能分开单独使用。
- 条件运算符的结合方向是自右至左。例如:a>b ? a : c>d ? c : d 应该理解为:a>b ? a : ( c>d ? c : d )
C语言while循环和do while循环详解
while循环
while(表达式){
语句块
}
do-while
do{
语句块
}while(表达式);
do-while循环与while循环的不同在于:它会先执行“语句块”,然后再判断表达式是否为真,如果为真则继续循环;如果为假,则终止循环。因此,do-while 循环至少要执行一次“语句块”。
C语言for循环(for语句)详解
for 循环,它的使用更加灵活,完全可以取代 while 循环; for 循环的一般形式为:
for(表达式1; 表达式2; 表达式3){
语句块
}
它的运行过程为:
- 先执行“表达式1”。
- 再执行“表达式2”,如果它的值为真(非0),则执行循环体,否则结束循环。
- 执行完循环体后再执行“表达式3”。
- 重复执行步骤 2) 和 3),直到“表达式2”的值为假,就结束循环。
上面的步骤中,2) 和 3) 是一次循环,会重复执行,for 语句的主要作用就是不断执行步骤 2) 和 3)。
“表达式1”仅在第一次循环时执行,以后都不会再执行,可以认为这是一个初始化语句。“表达式2”一般是一个关系表达式,决定了是否还要继续下次循环,称为“循环条件”。“表达式3”很多情况下是一个带有自增或自减操作的表达式,以使循环条件逐渐变得“不成立”。
C语言for循环中的三个表达式
省略“表达式1(初始化条件)
int i = 1, sum = 0;
for( ; i<=100; i++){
sum+=i;
}
上面代码的作用与以下代码相同:
int i, sum=0;
for(i=1/*语句①*/; i<=100/*语句②*/; i++/*语句③*/){
sum+=i;
}
省略了“表达式2(循环条件)”
如果不做其它处理就会成为死循环,如下:
for(i=1; ; i++) sum=sum+i;
以上代码相当于:
i=1;
while(1){
sum=sum+i;
i++;
}
省略了“表达式3(自增或自减)”
可在循环体中加入修改变量的语句, 需要注意的是,如果不在循环体中对变量进行修改,那么如果满足“表达式2”的条件的话,也会变成一个“死循环”:
for( i=1; i<=100; ){
sum=sum+i;
i++;
}
省略了“表达式1(初始化语句)”和“表达式3(自增或自减)”
for( ; i<=100 ; ){
sum=sum+i;
i++;
}
以上代码相当于:
while(i<=100){
sum=sum+i;
i++;
}
3个表达式可以同时省略
for( ; ; )
以上代码相当于:
while(1)
“表达式1”和“表达式3”可以是一个简单表达式也可以是其他语句
for( sum=0; i<=100; i++ ) sum=sum+i;
for( i=0,j=100; i<=100; i++,j-- ) k=i+j;
for( i=0,j=100; i<=100; i++,j-- ) k=i+j;
“表达式2”一般是关系表达式或逻辑表达式,但也可是数值或字符,只要其值非零,就执行循环体
for( i=0; (c=getchar())!='n'; i+=c );
C语言break和continue用法详解(跳出循环)
使用while或for循环时,如果想提前结束循环(在不满足结束条件的情况下结束循环),可以使用break或continue关键字。
break关键字
当 break 关键字用于 while、for 循环时,会终止循环而执行整个循环语句后面的代码。break 关键字通常和 if 语句一起使用,即满足条件时便跳出循环。
continue语句
continue 语句的作用是跳过循环体中剩余的语句而强制进入下一次循环。continue语句只用在 while、for 循环中,常与 if 条件语句一起使用,判断条件是否成立。
C语言循环嵌套详解
在C语言中,if-else、while、do-while、for 都可以相互嵌套。所谓嵌套(Nest),就是一条语句里面还有另一条语句,例如 for 里面还有 for,while 里面还有 while,或者 for 里面有 while,while 里面有 if-else,这都是允许的。
对C语言选择结构和循环结构的总结
C语言中常用的编程结构有三种(其它编程语言也是如此),它们分别是:
- 顺序结构:代码从前往后依次执行,没有任何“拐弯抹角”,不跳过任何一条语句,所有的语句都会被执行到。
- 选择结构:也叫分支结构。代码会被分成多个部分,程序会根据特定条件(某个表达式的运算结果)来判断到底执行哪一部分。
- 循环结构:程序会重新执行同一段代码,直到条件不再满足,或者遇到强行跳出语句(break/continue 关键字)。
C语言数组详解
C语言数组的基本概念
数组(Array)就是一组数据的集合;它所包含的每一个数据叫做数组元素(Element),所包含的数据的个数称为数组长度(Length),例如int a[4];就定义了一个长度为4的整型数组,名字是a。
数组中的每个元素都有一个序号,这个序号从0开始,而不是从我们熟悉的1开始,称为下标(Index)。使用数组元素时,指明下标即可,形式为:
arrayName[index]; 例如,a[0] 表示第0个元素,a[3] 表示第3个元素。
需要注意的是:
- 数组中每个元素的数据类型必须相同,对于int a[4];,每个元素都必须为 int
- 数组长度 length 最好是整数或者常量表达式,例如 10、20 * 4 等,这样在所有编译器下都能运行通过;如果 length 中包含了变量,例如 n、4*m 等,在某些编译器下就会报错
- 访问数组元素时,下标的取值范围为 0 ≤ index < length,过大或过小都会越界,导致数组溢出,发生不可预测的情况
数组内存是连续的
数组是一个整体,它的内存是连续的;也就是说,数组元素之间是相互挨着的,彼此之间没有一点点缝隙。下图演示了int a[4];在内存中的存储情形:
数组的初始化
数组的定义方式:
dataType arrayName[length];
先定义,后赋值
int a[4];
a[0]=20;
a[1]=345;
a[2]=700;
a[3]=22;
定义数组的同时赋值
数组元素的值由{ }包围,各个值之间以,分隔。
int a[4] = {20, 345, 700, 22};
数组的初始化注意点
可以只给部分元素赋值。当{ }中值的个数少于元素个数时,只给前面部分元素赋值。
// 表示只给 a[0]~a[4] 5个元素赋值,而后面 5 个元素自动初始化为 0。
int a[10]={12, 19, 22 , 993, 344};
当赋值的元素少于数组总体元素的时候,剩余的元素自动初始化为 0:
- 对于short、int、long,就是整数 0;
- 对于char,就是字符 ‘ ’;
- 对于float、double,就是小数 0.0。
可以通过下面的形式将数组的所有元素初始化为 0:
// 由于剩余的元素会自动初始化为 0,所以只需要给第 0 个元素赋值为 0 即可。
int nums[10] = {0};
char str[10] = {0};
float scores[10] = {0.0};
只能给元素逐个赋值,不能给数组整体赋值。
例如给 10 个元素全部赋值为 1:
int a[10] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
而不是:
int a[10] = 1;
如给全部元素赋值,那么在定义数组时可以不给出数组长度
int a[] = {1, 2, 3, 4, 5};
int a[5] = {1, 2, 3, 4, 5};
以上两种定义方式是等价的
C语言二维数组的定义、初始化、赋值
二维数组的定义
二维数组定义的一般形式是:
dataType arrayName[length1][length2];
dataType 为数据类型,arrayName 为数组名,length1 为第一维下标的长度,length2 为第二维下标的长度。
二维数组在C语言内存中的存储方式
二维数组在概念上是二维的,但在内存中是连续存放的;换句话说,二维数组的各个元素是相互挨着的,彼此之间没有缝隙:
在C语言中,二维数组是按行排列的,即放完一行之后再放入第二行。以 int a[3][4] 为例,先存放 a[0] 行,再存放 a[1] 行,最后存放 a[2] 行;每行中的 4 个元素也是依次存放。数组 a 为 int 类型,每个元素占用 4 个字节,整个数组共占用 4×(3×4)=48 个字节。
可以这样认为,二维数组是由多个长度相同的一维数组构成的。
二维数组的初始化(赋值)
按行分段赋值
int a[5][3]={ {80,75,92}, {61,65,71}, {59,63,70}, {85,87,90}, {76,77,85} };
按行连续赋值
int a[5][3]={80, 75, 92, 61, 65, 71, 59, 63, 70, 85, 87, 90, 76, 77, 85};
两种赋初值的结果是完全相同的。
注意点
可以只对部分元素赋值,未赋值的元素自动取“零”值
int a[3][3] = {{1}, {2}, {3}};
对每一行的第一列元素赋值,未赋值的元素的值为 0。赋值后各元素的值为:
1 0 0
2 0 0
3 0 0
int a[3][3] = {{0,1}, {0,0,2}, {3}};
赋值后各元素的值为:
0 1 0
0 0 2
3 0 0
如果对全部元素赋值,那么第一维的长度可以不给出
int a[3][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
int a[][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
以上两种写法均可
C语言字符数组和字符串详解
在C语言中,没有专门的字符串变量,没有string类型,通常就用一个字符数组来存放一个字符串。
C语言规定,可以将字符串直接赋值给字符数组,例如:
char str[30] = {"c.biancheng.net"};
char str[30] = "c.biancheng.net"; //这种形式更加简洁,实际开发中常用
也可以不指定数组长度,从而写作:
char str[] = {"c.biancheng.net"};
char str[] = "c.biancheng.net"; //这种形式更加简洁,实际开发中常用
字符数组只有在定义时才能将整个字符串一次性地赋值给它,一旦定义完了,就只能一个字符一个字符地赋值了。请看下面的例子:
char str[7];
str = "abc123"; //错误
//正确
str[0] = 'a'; str[1] = 'b'; str[2] = 'c';
str[3] = '1'; str[4] = '2'; str[5] = '3';
字符串结束标志
- 在C语言中,字符串总是以’ ’作为结尾,所以’ ’也被称为字符串结束标志,或者字符串结束符。
- '