1.存放数据的小箱子——变量
在《二进制思想以及数据的存储》一节中讲到:
- 计算机要处理的数据(诸如数字、文字、符号、图形、音频、视频等)是以二进制的形式存放在内存中的;
- 我们将8个比特(Bit)称为一个字节(Byte),并将字节作为最小的可操作单元。
编程中我们会经常处理各种数据,与内存打交道。我们不妨先从最简单的整数说起,看看它是如何放到内存中去的。
现实生活中我们会找一个小箱子来存放物品,一来显得不那么凌乱,二来方便以后找到。计算机也是这个道理,我们需要先在内存中找一块区域,规定用它来存放整数,并起一个好记的名字,方便以后查找。这块区域就是“小箱子”,我们可以把整数放进去了。
C语言中这样在内存中找一块区域:
int a;
int 又是一个新单词,它是 Integer 的简写,意思是整数。a 是我们给这块区域起的名字;当然也可以叫其他名字,例如 abc、mn123 等。
这个语句的意思是:在内存中找一块区域,命名为 a,用它来存放整数。
注意 int 和 a 之间是有空格的,它们是两个词。也注意最后的分号,int a表达了完整的意思,是一个语句,要用分号来结束。
不过int a;仅仅是在内存中找了一块可以保存整数的区域,那么如何将 123、100、999 这样的数字放进去呢?
C语言中这样向内存中放整数:
a=123;
= 是一个新符号,它在数学中叫“等于号”,例如 1+2=3,但在C语言中,这个过程叫做赋值(Assign)。赋值是指把数据放到内存的过程。
把上面的两个语句连起来:
int a;
a=123;
就把 123 放到了一块叫做 a 的内存区域。你也可以写成一个语句:
int a=123;
a 中的整数不是一成不变的,只要我们需要,随时可以更改。更改的方式就是再次赋值,例如:
int a=123;
a=1000;
a=9999;
第二次赋值,会把第一次的数据覆盖(擦除)掉,也就是说,a 中最后的值是9999,123、1000 已经不存在了,再也找不回来了。
因为 a 的值可以改变,所以我们给它起了一个形象的名字,叫做变量(Variable)。
int a;创造了一个变量 a,我们把这个过程叫做变量定义。a=123;把 123 交给了变量 a,我们把这个过程叫做给变量赋值;又因为是第一次赋值,也称变量的初始化,或者2.赋初值。
你可以先定义变量,再初始化,例如:
int abc;
abc=999;
也可以在定义的同时进行初始化,例如:
int abc=999;
这两种方式是等价的。
2.在屏幕上显示整数——输出
我们定义了一个变量 abc 并给它赋值,例如:
int abc=100;
现在我们希望运行一下,看看效果,怎么才能在屏幕上显示呢?
puts 是 output string 的缩写,只能用来输出字符串,不能输出整数,我们需要用另外一种方法,那就是——printf。
printf 比 puts 更加强大,不仅可以输出字符串,还可以输出整数、小数、单个字符等;输出格式也可以自己定义,例如:
- 以二进制、八进制、十六进制形式输出;
- 要求输出的数字占 n 个字符的位置;
- 控制小数的位数。
printf 是 print format 的缩写,意思是“格式化打印”。这里所谓的“打印”就是在屏幕上显示内容,与“输出”的含义相同,所以我们一般称 printf 是用来格式化输出的。
先来看一个简单的例子:
printf("C语言中文网");
这个语句可以在屏幕上显示“C语言中文网”,与puts("C语言中文网");的效果完全相同。
输出变量 abc 的值:
int abc=999;
printf("%d", abc);
这里就比较有趣了。先来看%d,d 是 decimal 的缩写,意思是十进制数,%d 表示以十进制的形式输出。输出什么呢?输出 abc 的值。%d 与 abc 是对应的,也就是说,会用 abc 的值来替换 %d。
再来看个复杂点的:
int abc=999;
printf("The value of abc is %d !", abc);
会在屏幕上显示:
The value of abc is 999 !
你看,字符串 "The value of abc is %d !" 中的 %d 被替换成了 abc 的值,其他字符没有改变。这说明 %d 比较特殊,不会原样输出,会被替换成对应的变量的值。
再来看:
int a=100;
int b=200;
int c=300;
printf("a=%d, b=%d, c=%d", a, b, c);
会在屏幕上显示:
a=100, b=200, c=300
再次证明了 %d 与后面的变量是一一对应的,第一个 %d 对应第一个变量,第二个 %d 对应第二个变量……
我们把代码补充完整,体验一下:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a=100;
int b=200;
int c=300;
printf("a=%d, b=%d, c=%d ", a, b, c);
system("pause");
return 0;
}
输出结果:
a=100, b=200, c=300
请按任意键继续. . .
我们也可以不用变量,直接将数据输出:
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("The output: %d, %s ", 1234, "Think you");
system("pause");
return 0;
}
输出结果:
The output: 1234, Think you
s 是 string 的简写,%s 表示输出字符串。
3.更多类型的数据——小数和字符
int 可以用来表示整数,但是对小数却无能为力。请看下面的代码:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a=1234.888;
printf("a=%d ", a);
system("pause");
return 0;
}
输出结果:
a=1234
请按任意键继续. . .
看,小数点后面的数字被丢弃了。
①.小数
在C语言中,我们使用 float 来表示小数,例如:
float a=123.888;
float b=0.302;
float c=98.0;
float 是“浮点数”的意思,我们暂时可以认为浮点数就是小数,不必深究,不影响我们的学习。
输出浮点数用%f,请看下面的例子:
#include <stdio.h>
#include <stdlib.h>
int main()
{
float a=123.888;
float b=0.302;
float c=98.0;
float d=1.23002398;
float e=123; // 整数
printf("a=%f b=%f c=%f d=%f e=%f ", a, b, c, d, e);
system("pause");
return 0;
}
输出结果:
a=123.888000
b=0.302000
c=98.000000
d=1.230024
e=123.000000
%f 默认保留六位小数,不足六位以 0 补齐,超过六位按四舍五入截断。也可以将整数赋值给 float 类型变量。
②.字符
我们前面提到过字符串,它是多个字符的集合,例如 "abc123";当然也可以只包含一个字符,例如 "a"、"1"。
但是为了使用方便,我们可以用 char 来专门表示一个字符,例如:
char a='1';
char b='$';
char c='X';
char d=' '; // 空格也是一个字符
char 类型的字符用单引号 ' ' 来包围,而字符串用双引号" "包围,大家不要混淆,否则编译会出错。
数据类型
整数、浮点数、字符都是C语言中会使用到的数据,它们有一个专业的称呼,叫做数据类型(Data Type)。例如:
int a=1;
float b=10.09;
char c='@';
我们就可以说 a、b、c 的数据类型不同,a 是整型变量,b 是浮点型变量,c 是字符型变量。
整型、浮点型、字符型是C语言中的基本数据类型,必须要掌握,后续我们还会介绍更多数据类型,甚至可以自己定义一种新的类型。
注意:字符串比较特殊,C语言中没有这个数据类型,不能使用string a="abc123";这种方式来定义字符串,后面我们会讲解为什么。
③.字符与整数
先看下面一段代码:
#include <stdio.h>
#include <stdlib.h>
int main()
{
char a=69;
char b=0x46; // 十六进制数以 0x 或 0X开头
char c='G';
int d=72;
printf("a1=%c, a2=%d, b=%c, c=%d, d=%c ", a, a, b, c, d);
system("pause");
return 0;
}
输出结果:
a1=E, a2=69, b=F, c=71, d=H
代码第6行中,//及其后面的内容为注释(Comments)。注释用来对程序进行说明,编译器会忽略它,不去执行。
在ASCII码表中,E、F、G、H 的值分别是 69、0x46 (十进制为 70)、71、72。给 char 变量一个整数,然后以 %c 输出,它会根据 ASCII 码表转换成对应的字符,如果以 %d 输出,那么还是整数。同样,一个 char 变量以 %d 输出,会转换成对应的 ASCII 码值。反过来,int 变量以 %c 输出,会转换成对应的字符。
可以看出,ASCII 码将字符和整数联系起来了,你可以给 char 变量一个整数,也可以给 int 变量一个字符,它们可以相互转换。
4.小箱子有多大——取值范围
2015年05月20日,在纳斯达克上市的苹果公司市值达到了 7628.40 亿美元,是有史以来市值最高的公司,并且最有希望成为世界上第一个市值破万亿的公司。乔布斯辞世后,很多人质疑库克能不能再创辉煌,数据不会说谎,苹果公司市值已经翻倍,乔布斯没有选错接班人,iPhone 6 也顺应了大屏手机的趋势。
那么,我们不妨将苹果公司的市值输出一下,看看是多长的一串数字:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int value=7628400000000;
// AAPL 是苹果公司的股票代码
printf("The market value of AAPL is %d ! ", value);
system("pause");
return 0;
}
输出结果:
The market value of AAPL is 538082304 !
让人惊讶的是,市值瞬间蒸发,只剩下 5.38 亿美元,与达内的市值相当,刚刚够上市的体量。
这是因为,一般情况下 int 类型在内存中占用 4 个字节的空间,也就是32位,理论上所能表示的最大数是 2^32 - 1 = 0xFFFFFFFF = 4,294,967,296 ≈ 42.95 亿,7628.40 亿显然超出了它的范围,会发生溢出(Overflow)。就像水一样,多出的会流到木桶外面,如果数字超过32位,比如35位,那么就会有3位丢失,当然不能表示原来的值。
我们来验证一下 int 的最大值:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int value=0xffffffff;
printf("The maximum value of "int" is %d . ", value);
system("pause");
return 0;
}
运行结果:
The maximum value of "int" is -1 .
这和我们期望的结果不一样:我们期望输出 4294967296,但实际输出的却是八竿子打不着的 -1。
这是因为,整数有正负之分,我们需要在32位中选择一位用来标明到底是正数还是负数,这一位就是最高位。也就是说,0~30位表示数值,31 位表示正负号。如下图所示:
注意:在编程语言中,计数往往是从0开始,例如字符串 "abc123",我们称第 0 个字符是 a,第 1 个字符是 b,第 5 个字符是 3。这和我们平时从 1 开始计数的习惯不一样,大家要慢慢适应,培养编程思维。
如果我们希望能表示更大些的数值,也可以不使用符号位,而用关键字unsigned来声明变量,例如:
// 下面的两种写法都是正确的,使用 unsigned 时可以不写 int
unsigned int a=100;
unsigned a=999;
这样,就只能表示正数了。unsigned 类型的整数需要用%u来输出。
我们来验证一下上面的说法:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a=1234;
unsigned a1=1234;
int b=0x7fffffff;
int c=0x80000000; // 0x80000000 = 0x7fffffff + 0x1
int d=0xffffffff;
unsigned e=0xffffffff;
printf("a=%d, a(u)=%u ", a, a);
printf("a1=%d, a1(u)=%u ", a1, a1);
printf("b=%d, b(u)=%u ", b, b);
printf("c=%d, c(u)=%u ", c, c);
printf("d=%d, d(u)=%u ", d, d);
printf("e=%d, e(u)=%u ", e, e);
system("pause");
return 0;
}
输出结果:
a=1234, a(u)=1234
a1=1234, a1(u)=1234
b=2147483647, b(u)=2147483647
c=-2147483648, c(u)=2147483648
d=-1, d(u)=4294967295
e=-1, e(u)=4294967295
我们发现,无论变量声明为 int 还是 unsigned,只有当以 %u 格式输出时,才会作为无符号数处理;如果声明为 unsigned,却以 %d 输出,那么也是有符号数。
为了更加灵活的表示数据,合理利用内存资源,C语言还使用另外两个关键字来表示整数,那就是 short 和 long。short 表示短整型,long 表示长整型,这里的“长短”是针对 int 来说的。C语言规定,short 的长度不能大于 int,long 的长度不能小于 int。
unsigned表示有无符号位,short、int、long表示所占内存的大小,不要混淆,请看下面的例子:
long int a=1234;
long b=299;
unsigned long int c=999;
unsigned long d=98720;
short int m=222;
short n=2094;
unsigned short int p=3099;
unsigned int q=68893;
值得一提的是:C语言是70年代的产物,那个时候以及后面的十几年,是8086的天下,计算机的软硬件都是16位的,按照C语言的规定,short 一般占2个字节,int 也占2个字节,long 占4个字节。
现在的计算机已经甩开8086几条街,CPU、操作系统、编译器都已经支持到32位或者64位,所以 short 一般占2个字节,int 占 4 个字节,long 也占 4 个字节。
你看,C语言为了兼容硬件,追求效率,对数据类型的规定不是那么严谨,short 不一定真的“短”,long 也不一定真的“长”,这增加了我们学习和开发的成本。
在Java中不存在这样的情况,数据类型的长度是板上钉钉的,short 一定是 2 个字节,int 是 4 个字节,long 是 8 个字节,多么简单。但这是以牺牲效率为代价的。在编程语言中,简单和效率一直是相互矛盾的,几个语句搞定的代码往往效率不高,追求高效的代码往往需要自己实现,可能要几百行。
①.其他数据类型的长度
现在的计算机软硬件配置都已经支持到32位或者64位,这里以及后面提到的数据类型长度,都只考虑现代计算机,不再关心古董机器——16位机。
char 的长度始终都是1个字节。
float 的长度为 4 个字节,是单精度浮点数。为了更加精确,表示更大或者更小的浮点数,可以使用 double。double 占用 8 个字节,是双精度浮点数。
最后,我们来总结一下现代计算机中各种数据类型的长度。
说 明 | 字符型 | 短整型 | 整型 | 长整型 | 单精度浮点型 | 双精度浮点型 |
数据类型 | char | short | int | long | float | double |
所占字节 | 1 | 2 | 4 | 4 | 4 | 8 |
注意:unsigned 表示有无正负号,不会影响数据类型长度。
sizeof
sizeof 用来计算数据所占用的内存空间,以字节计。如果你忘记了某个数据类型的长度,可以用 sizeof 求得,请看下面的代码:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int m=290;
float n=23;
printf("int = %d ", sizeof(m));
printf("float = %d ", sizeof(n));
printf("double = %d ", sizeof(double));
printf("A = %d ", sizeof('A'));
printf("23 = %d ", sizeof(23));
printf("14.67 = %d ", sizeof(14.67));
system("pause");
return 0;
}
输出结果:
int = 4
float = 4
double = 8
A = 1
23 = 4
14.67 = 8
sizeof 的操作数既可以是变量、数据类型,还可以是某个具体的数据。细心的读者会发现,sizeof(14.67) 的结果是8,不错,因为小数默认的类型就是double。
5.加减乘除运算
C语言也可以进行加减乘除运算,但是运算符号与数学中的略有不同,见下表。
加法 | 减法 | 乘法 | 除法 | 求余数 | |
数学 | + | - | × | ÷ | 无 |
C语言 | + | - | * | / | % |
加号、减号与数学中的一样,乘号、除号不同,另外C语言还多了一个求余数的运算符。
我们先来看一段代码:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a=12;
int b=100;
float c=8.9;
int m=a+b;
float n=b*c;
double p=a/c;
int q=b%a;
printf("m=%d, n=%f, p=%lf, q=%d ", m, n, p, q);
system("pause");
return 0;
}
输出结果:
m=112, n=889.999962, p=1.348315, q=4
b*c 的结果很明显是890,但是却输出一个近似数,这里涉及到浮点数的运算原理,并且实际开发中较少使用浮点数运算,所以这里我们不再深究。
你也可以让数字直接参与运算:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a=12;
int b=100;
float c=8.9;
int m=a-b; // 变量参与运算
int n=a+239; // 有变量也有数字
double p=12.7*34.3; // 数字直接参与运算
printf("m=%d, n=%f, p=%lf ", m, n, p);
printf("m*2=%d, 6/3=%d, m*n=%ld ", m*2, 6/3, m*n);
system("pause");
return 0;
}
输出结果:
m=-88, n=-0.000000, p=0.000000
m*2=-176, 6/3=2, m*n=-22088
注意:除数不能是 0,所以诸如int a=3/0;这样的语句是错误的。
再来看一个例子:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a=12;
int b=10;
printf("a=%d ", a);
a=a+8;
printf("a=%d ", a);
a=a*b;
printf("a=%d ", a);
system("pause");
return 0;
}
输出结果:
a=12
a=20
a=200
第一次输出 a 原来的值;a=a+8;相当于用a+8的值替换原来 a 的值,所以第二次输出 20;第三次用a*b的值替换第二次的值,所以是 200。
注意:a=a+8;可以写成a+=8;,a=a*b;可以写成a*=b。这仅是一种简写,不会影响效率,其他运算符也可以这样写。
①.加一和减一
一个整数自身加一可以这样写:
a+=1;
它等价于 a=a+1;。
但是在C语言中还有一种更简单的写法,就是 a++; 或者++a; 。这种写法叫做自加或自增;意思很明确,就是自身加一。
相应的,也有a--和--a,叫做自减,表示自身减一。
++和--分别称为自增和自减运算符。
那么,++ 在前面和后面有区别吗?
请看下面的例子:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a=12;
int a1=a++;
int b=10;
int b1=++b;
printf("a=%d, a1=%d ", a, a1);
printf("b=%d, b1=%d ", b, b1);
system("pause");
return 0;
}
输出结果:
a=13, a1=12
b=11, b1=11
我们来分析一下:
1) 执行a1=a++;,a 的值并不会立马加 1,而是先把 a 原来的值交给 a1,然后才能加 1。a 原来的值是 12,所以 a1 的值是 12;而 a 因为加 1,最终的值是 13。
2) 执行b1=++b;,b 的值会立马加1,变成11,然后才将 b 的值交给 b1。因为此时 b 的值已经是11,所以 b1 的值自然也就是 11 了。
可以看出:a1=a++;会先进行赋值操作,再进行自增操作;而b1=++b;会先进行自增操作,再进行赋值操作。
所以,我们把诸如a++这样的操作叫做后自增,也就是先进行其他操作,再进行自增操作;而把++a叫做前自增,会先进行自增操作,再进行其他操作。
相应的,a--叫做后自减,--a叫做前自减,意思与++相同。
自增自减非常方便,后续编程中会经常用到,大家要注意区分。
为了强化记忆,我们再来看一个自增自减的综合示例:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a=12;
int b=1;
int c=a-(b--); // ①
int d=(++a)-(--b); // ②
printf("c=%d, d=%d ", c, d);
system("pause");
return 0;
}
输出结果:
c=11, d=14
我们来分析一下:
1) 执行语句①时,会先进行a-b运算,结果是11,然后 b 再自减,就变成了 0,最后再将a-b的结果(也就是11)交给 c,所以 c 的值是 11。
2) 执行语句②之前,b 的值已经变成 0。对于d=(++a)-(--b),a 会先自增,变成 13,然后 b 再自减,变成 -1,最后再进行13-(-1),结果是14,交给 d,所以 d 最终是 14。
6.更加优美的C语言输出
虽然我们已经熟悉了 printf,但是还没有把它发挥到极致,printf 可以有更加“炫酷”的输出。
假如现在老师要求我们用C语言输出一个 4×4 的整数矩阵,为了增强阅读性,数字要对齐,怎么办呢?我们显然可以这样来做:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a1=20, a2=345, a3=700, a4=22;
int b1=56720, b2=9999, b3=20098, b4=2;
int c1=233, c2=205, c3=1, c4=6666;
int d1=34, d2=0, d3=23, d4=23006783;
printf("%d %d %d %d ", a1, a2, a3, a4);
printf("%d %d %d %d ", b1, b2, b3, b4);
printf("%d %d %d %d ", c1, c2, c3, c4);
printf("%d %d %d %d ", d1, d2, d3, d4);
system("pause");
return 0;
}
运行结果:
20 345 700 22
56720 9999 20098 2
233 205 1 6666
34 0 23 23006783
矩阵一般在大学的《高等数学》中会讲到,m×n 的数字矩阵可以理解为把 m×n 个数字摆放成 m 行 n 列的样子。
看,这是多么地自虐,要敲那么多空格,还要严格控制空格数,否则输出就会错位。
类似的需求随处可见,整齐的格式会更加美观,让人觉得生动有趣。我们大可不必像上面一样,printf 可以更好的控制输出格式。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a1=20, a2=345, a3=700, a4=22;
int b1=56720, b2=9999, b3=20098, b4=2;
int c1=233, c2=205, c3=1, c4=6666;
int d1=34, d2=0, d3=23, d4=23006783;
printf("%-9d %-9d %-9d %-9d ", a1, a2, a3, a4);
printf("%-9d %-9d %-9d %-9d ", b1, b2, b3, b4);
printf("%-9d %-9d %-9d %-9d ", c1, c2, c3, c4);
printf("%-9d %-9d %-9d %-9d ", d1, d2, d3, d4);
system("pause");
return 0;
}
输出结果:
20 345 700 22
56720 9999 20098 2
233 205 1 6666
34 0 23 23006783
这样写起来更加方便,即使改变某个数字,也无需修改 printf 语句。
%-9d中,d表示以十进制输出,9表示最少占9个字符的宽度,宽度不足以空格补齐,-表示左对齐。综合起来,%-9d表示以十进制输出,左对齐,宽度最小为9个字符。大家可以亲自试试%9d的输出效果。
printf 格式控制字符串的完整形式如下:
%[flags][width][.precision]type
1) type 也就是以什么类型输出,比如 %d、%f、%c,type 就分别对应 d、f、c;%-9d中 type 对应 d。type 必须有。
2) width 表示最小输出宽度,也就是占几个字符的位置;%-9d中 width 对应 9。
注意:[xxx] 并不是C语言规定的格式,只是一种习惯写法,表示此处的内容可有可无,后面会经常见到这样的写法。
对于整数和小数,默认右对齐,不足的宽度以空格补齐,例如:
printf("%10d%12f", 234, 9.8);
输出结果为:
234 9.800000
234 前面共有7个空格,9.8 前面有4个空格。
3) .precision 表示输出精度。
对于 %d,.precision 表示的其实是最小输出宽度,与 width 不同的是,不足的宽度以 0 补齐,例如:
printf("%.10d
", 4309);
输出结果为:
0000004309
对于 %f,.precision 表示小数的位数,不足以 0 补齐,也就是精度,例如:
printf("%.10f %.3f
", 23.988, 2.9328745);
输出结果为:
23.9880000000 2.933
4) flags 是标志字符,%-9d中 flags 对应-
几种常见的标志字符
标志字符 | 含 义 |
- | 左对齐 |
+ | 输出符号(正号或负号) |
空格 | 输出值为正时冠以空格,为负时冠以负号 |
# | 对c、s、d、u类无影响; 对o类,在输出时加前缀o; 对x类,在输出时加前缀0x; 对e、g、f 类当结果有小数时才给出小数点。 |
7.从键盘输入数据
puts 可以输出字符串,printf 可以输出多种类型的数据,另外还有 putchar,可以输出单个字符,例如:
putchar('a');
putchar(7);
putchar('x46');
输出结果:
aF // 同时会听到喇叭发出“嘟”的声音
程序是人机交互的媒介,有输出必然也有输入。在C语言中,使用 scanf 从键盘获取用户输入的数据。
scanf 是 scan format 的缩写,意思是格式化扫描,这里可以理解为从键盘获得用户输入。我们先来看一个例子:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a, b;
scanf("%d %d", &a, &b);
printf("a+b=%d ", (a+b));
system("pause");
return 0;
}
运行结果:
34 29 ↙
a+b=63
从键盘输入 34,空格,再输入 29,然后回车(↙表示回车),就会看到两个数相加的结果。
scanf 和 printf 非常相似:
scanf("%d %d", &a, &b); // 获取用户输入的两个整数,分别赋值给变量 a 和 b
printf("%d %d", a, b); // 将变量 a 和 b 的是在显示器上输出。
它们都有格式控制字符串,都有变量列表。不同的是,scanf 的变量前要带一个&符号;&称为取地址符,也就是获取变量在内存中的地址。
在《二进制思想以及数据的存储》一节中讲到,数据是以二进制的形式保存在内存中的,字节(Byte)是最小的可操作单位。为了便于管理,我们给每个字节分配了一个编号,使用该字节时,只要知道编号就可以,就像每个学生都有学号,老师会随机抽取学号来让学生回答问题。字节的编号是有顺序的,从 0 开始,接下来是 1、2、3……
下图是 4G 内存中每个字节的编号(以十六进制表示):
这个编号,就叫做地址(Address)。int a; 会在内存中分配四个字节的空间,我们将第一个字节的地址称为变量 a 的地址,也就是 &a 的值。对于前面讲到的整数、浮点数、字符,都要使用 & 获取它们的地址,scanf 会根据地址把读取到的数据写入内存。
我们不妨将它们的地址输出看一下:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a='F';
int b=12;
int c=452;
printf("&a=%#x, &b=%#x, &c=%#x ", &a, &b, &c);
system("pause");
return 0;
}
输出结果:
&a=0x18ff48, &b=0x18ff44, &c=0x18ff40
图:a、b、c 的内存地址
注意:你看到的地址是虚拟地址,并不等于它在物理内存中的地址。虚拟内存是现代操作系统因内存管理的需要才提出的概念,dos 下没有这个概念,用户看到的都是真实的地址。CPU 操作的是物理内存地址,所以虚拟地址必须经过转换才能交给 CPU,这是 OS 的工作,对用户是透明的。
再来看一个 scanf 的例子:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a, b, c, age;
float scores;
scanf("a=%d,b=%d,c=%d", &a, &b, &c);
printf("a+b+c=%d ", (a+b+c));
fflush(stdin); // 清空缓冲区
scanf("Tom's age is %d, his scores is %f.", &age, &scores);
printf("age=%d, scores=%f. ", age, scores);
system("pause");
return 0;
}
运行结果:
a=230,b=400,c=5009↙
a+b+c=5639
Tom's age is 16, his scores is 94.5.↙
age=16, scores=94.500000.
先输入a=230,b=400,c=5009,回车,就可以得到 a+b+c 的值;再输入Tom's age is 16, his scores is 94.5.,回车,就可以得到年龄和成绩。
fflush(stdin);用来清空输入缓冲区,这是什么意思呢?
在内存中,有一块区域(比如512字节)专门用来保存用户输入的数据,遇到 scanf 时,程序会首先检查该区域是否有数据:
- 如果没有,就等待用户输入,用户从键盘输入的每个字符都会暂时保存到这里,直到按下回车键,输入结束,scanf 再从这里读取数据,赋值给变量。
- 如果有数据,哪怕是一个字符,scanf 也会直接读取,不会等待用户输入。
这块内存区域,就叫做缓冲区(Buffer),或者缓存(Cache);又因为它是用来暂存用户输入的数据的,所以又叫输入缓冲区。
缓冲区与普通的内存没有什么两样,都是物理内存上的若干字节,只是作用不同而已。
上面的代码如果没有fflush(stdin);,运行时就会大有不同:
a=23,b=900,c=399↙
a+b+c=1322
age=4239360, scores=0.000000.
第一次输入后,程序并没有等待我们第二次输入,age 和 scores 都是无效值。这是因为,第一次输入的数据为a=23,b=900,c=399↙(包含最后的回车),回车后 scanf 读取到的数据是a=23,b=900,c=399,还有一个回车符留在缓冲区,遇到第二个 scanf 时,因为缓冲区中有数据,所以会直接读取,不给我们输入的机会。
所以要用fflush(stdin);来清空缓冲区,这样遇到第二个 scanf 时因为缓冲区中没有数据,就会等待用户输入。
这里重点是告诉大家 scanf 是带有缓冲区的,fflush(stdin);的用法不再展开讨论,请大家先记住,后续我们再讲解。
①.getchar
scanf 用于接收用户输入的各种数据,如果仅仅是输入单个字符,也可以使用 getchar。请看下面的例子:
#include <stdio.h>
#include <stdlib.h>
int main()
{
char a, b, c;
a=getchar();
printf("a='%c' ", a);
b=getchar();
printf("b='%c' ", b);
c=getchar();
printf("c='%c' ", c);
system("pause");
return 0;
}
运行结果:
XYZ↙
a='X'
b='Y'
c='Z'
你也可以这样输入:
X↙
a='X'
b='
'
Y↙
c='Y'
第二次之所以是这样的结果,是因为 getchar 也带有缓冲区。输入 X 并回车,第一个 getchar 读取到 'X',接着第二个 getchar 读取到回车换行符,到第三个 getchar,缓冲区中已经没有数据,所以会等待用户输入。
8.运算符的优先级和结合性
先来看一个例子:
#include <stdio.h>
int main(){
int a=10,b=1,c=2;
a=b=c;
printf( "12+3*5=%d ", 12+3*5);
printf( "a=%d, c=%d ", a, c);
return 0;
}
运行结果:
12+3*5=27
a=2, c=2
1) 对于表达式12+3*5,很明显先进行乘法运算,计算3*5,结果为15,再进行加法运算,计算12+15,结果为27。也就是说,乘法的优先级比加法高,要先计算,这与数学中的规则是一样的。
所谓优先级,就是当有多个运算符在同一个表达式中出现时,先执行哪个运算符。如果不想按照默认的规则执行,可以加( ),例如(12+3)*5的结果为 75,(2+5)*(10-4)的结果为 42。大部分情况下,它们的规则和数学中是相同的。
2) 对于语句赋值语句a=b=c;,先执行b=c,再执行a=b,而不是反过来,这说明赋值操作符=具有右结合性。
所谓结合性,就是当一个运算符多次出现时,先执行哪个运算符。先执行右边的叫右结合性,先执行左边的叫左结合性。
表达式(Expression)和语句(Statement)的概念在C语言中并没有明确的定义:
表达式可以看做一个计算的公式,往往由数据、变量、运算符等组成,例如3*4+5、a=c=d等,它的结果必定是一个值;
语句的范围更加广泛,不一定是计算,不一定有值,可以是某个操作、某个函数、选择结构、循环等。
值得注意的是:以分号;结束的往往称为语句,而不是表达式,例如3*4+5;、a=c=d;等。
3) 像 +、-、*、/ 这样的运算符,它的两边都有数据,例如 3+4、a*3 等,有两个操作数,我们称这样的运算符为双目运算符。后面还会讲解单目运算符和三目运算符。
9.几个重要的 C 语言概念
①.标识符
定义变量时,我们使用了诸如“a”“abc”“mn123”这样的名字,它们都是程序员自己起的,一般能够表达出变量的作用,这叫做标识符(Identifier)。
标识符就是程序员自己起的名字,除了变量名,后面还会讲到函数名、标号等。不过,名字也不能随便起,C语言规定,标识符只能由字母(A~Z, a~z)、数字(0~9)和下划线(_)组成,并且第一个字符必须是字母或下划线。
以下标识符是合法的:
a, x, x3, BOOK_1, sum5
以下标识符是非法的:
3s 不能以数字开头
s*T 出现非法字符*
-3x 不能以减号(-)开头
bowy-1 出现非法字符减号(-)
在使用标识符时还必须注意以下几点:
- C语言虽然不限制标识符的长度,但是它受到不同编译器的限制,同时也受到具体机器的限制。例如在某个编译器中规定标识符前128位有效,当两个标识符前128位相同时,则被认为是同一个标识符。
- 在标识符中,大小写是有区别的,例如BOOK和book 是两个不同的标识符。
- 标识符虽然可由程序员随意定义,但标识符是用于标识某个量的符号,因此,命名应尽量有相应的意义,以便于阅读理解,作到“顾名思义”。
②.关键字
关键字(Keywords)是由C语言规定的具有特定意义的字符串,通常也称为保留字,例如 int、char、long、float、unsigned 等。我们定义的标识符不能与关键字相同,否则会出现错误。
你也可以将关键字理解为具有特殊含义的标识符,它们已经被系统使用,我们不能再使用了。
标准C语言中一共规定了32个关键字。
③.注释
注释(Comments)可以出现在代码中的任何位置,用来向用户提示或解释程度的意义。程序编译时,会忽略注释,不做任何处理,就好像它不存在一样。
C语言支持单行注释和多行注释:
- 单行注释以//开头,直到本行末尾(不能换行);
- 多行注释以/*开头,以*/结尾,注释内容可以有一行或多行。
一个使用注释的例子:
/*
Powered by: c.biancheng.net
Author: xiao p
Date: 2015-6-26
*/
#include <stdio.h>
int main()
{
/* puts 会在末尾自动添加换行符 */
puts("http://c.biancheng.net");
printf("C语言中文网 "); //printf要手动添加换行符
return 0;
}
运行结果:
http://c.biancheng.net
C语言中文网
在调试程序的过程中可以将暂时不使用的语句注释掉,使编译器跳过不作处理,待调试结束后再去掉注释。
④.函数和头文件
在C语言中,有的语句使用时不能带括号,例如声明类型的关键字 int、char、long 等,有的语句必须带括号,例如 puts()、printf()、scanf() 等。带括号的称为函数(Function)。
C语言发布时,它的开发者已经为我们编写了常用的代码(例如输入输出代码),我们可以拿来直接使用,不必再自己编写。这些代码,以函数(Function)的形式提供给我们,我们直接调用函数就可以。
函数的一个明显特征就是使用时带括号( ),必要的话,括号中还要包含数据或变量。我们将在《函数》一章中详细介绍,请大家先记住这个概念。
C语言中的函数和数学中的函数有很大的不同,最好不要拿两者对比。
C语言包含了几百个函数,为了便于管理和使用,这些函数被分门别类地保存到了不同的文件,使用时要引入对应的文件。这样的文件称为头文件(Header File),后缀为.h。
引入头文件使用#include命令,并将头文件放在< >之间,例如#include <stdio.h>、#include <stdlib.h>。
stdio 是 standard input and output 的缩写,称为“标准输入输出文件”。stdio.h中包含了很多常用函数,如 puts、printf、scanf 等。
需要注意的是:头文件必须在程度开头引入。
⑤.main 函数
我们说过,编写的代码要位于main(){ }之中,main 也是一个函数,称为主函数。main 比较特殊,程序运行时,会从 main 函数开始,直到 main 函数结束,所以代码中只能有一个 main 函数,否则程序将不知道从哪里开始执行,出现错误。