(该篇学习内容全部来自于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
//( ) 表明这是函数定义,{ } 之间的代码是函数要实现的功能。
int main()
{
puts("C语言中文网");
return 0;
}
C语言开发者们编写了很多常用函数,并分门别类的放在了不同的文件,这些文件就称为头文件(header file)。每个头文件中都包含了若干个功能类似的函数,调用某个函数时,要引入对应的头文件,否则编译器找不到函数。
实际上,头文件往往只包含函数的说明,也就是告诉我们函数怎么用,而函数本身保存在其他文件中,在链接时才会找到。对于初学者,可以暂时理解为头文件中包含了若干函数。
引入头文件使用#include命令,并将文件名放在< >中,#include 和 < > 之间可以有空格,也可以没有。
头文件以.h为后缀,而C语言代码文件以.c为后缀,它们都是文本文件,没有本质上的区别,#include 命令的作用也仅仅是将头文件中的文本复制到当前文件,然后和当前文件一起编译。你可以尝试将头文件中的内容复制到当前文件,那样也可以不引入头文件。
.h中代码的语法规则和.c中是一样的,你也可以#include
较早的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,这个过程叫做给变量赋值;第一次赋值,也称变量的初始化,或者赋初值。
数据是放在内存中的,变量是给这块内存起的名字,有了变量就可以找到并使用这份数据。
诸如数字、文字、符号、图形、音频、视频等数据都是以二进制形式存储在内存中的,它们并没有本质上的区别,那么,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语言中,每一种数据类型所占用的字节数都是固定的,知道了数据类型,也就知道了数据的长度。
在现代操作系统中,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 的长度,只做了宽泛的限制:
总结起来,它们的长度(所占字节数)关系为:
2 ≤ short ≤ int ≤ long
这就意味着,short 并不一定真的“短”,long 也并不一定真的“长”,它们有可能和 int 占用相同的字节数。
举例:
在 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 是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);
使用不同的格式控制符可以输出不同类型的整数,它们分别是:
#include
int main()
{
short a = 10;
int b = 100;
long c = 9437;
printf("a=%hd, b=%d, c=%ld\n", 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输出即可。
八进制数字和十进制数字不区分大小写,所以格式控制符都用小写形式。
十六进制数字的表示用到了英文字母,有大小写之分,要在格式控制符中体现出来:
short a = 0b1010110; //二进制数字
int b = 02713; //八进制数字
long c = 0X1DAB83; //十六进制数字
printf("a=%ho, b=%o, c=%lo\n", a, b, c); //以八进制形似输出
printf("a=%hd, b=%d, c=%ld\n", a, b, c); //以十进制形式输出
printf("a=%hx, b=%x, c=%lx\n", a, b, c); //以十六进制形式输出(字母小写)
printf("a=%hX, b=%X, c=%lX\n", 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
一个数字不管以何种进制来表示,都能够以任意进制的形式输出。数字在内存中始终以二进制的形式存储,其它进制的数字在存储前都必须转换为二进制形式;同理,一个数字在输出时要进行逆向的转换,也就是从二进制转换为其他进制。
区分不同进制数字的一个简单办法就是,在输出时带上特定的前缀。在格式控制符中加上#即可输出前缀,例如 %#x、%#o、%#lX、%#ho 等:
short a = 0b1010110; //二进制数字
int b = 02713; //八进制数字
long c = 0X1DAB83; //十六进制数字
printf("a=%#ho, b=%#o, c=%#lo\n", a, b, c); //以八进制形似输出
printf("a=%hd, b=%d, c=%ld\n", a, b, c); //以十进制形式输出
printf("a=%#hx, b=%#x, c=%#lx\n", a, b, c); //以十六进制形式输出(字母小写)
printf("a=%#hX, b=%#X, c=%#lX\n", 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语言中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 |
严格来说,格式控制符和整数的符号是紧密相关的,具体就是:
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。
有符号数: 以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
int main()
{
unsigned int a = 0x100000000;
int b = 0xffffffff;
printf("a=%u, b=%d\n", 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;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 是指数
#include
#include
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=%f\n", 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
小数还有一种更加智能的输出方式,就是使用%g。%g 会对比小数的十进制形式和指数形式,以最短的方式来输出小数,让输出结果更加简练。所谓最短,就是输出结果占用最少的字符。
#include
#include
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=%g\n", a, b, c, d);
return 0;
}
运行结果:
a=1e-05
b=3e+07
c=12.84
d=1.22934
小数在内存中是以浮点数的形式存储的。浮点数是数字(或者说数值)在内存中的一种存储格式,它和定点数是相对的。
C语言标准规定,小数在内存中以科学计数法的形式来存储,具体形式为:
flt = (-1)sign × mantissa × baseexponent
字符类型由单引号’ '包围,字符串由双引号" "包围。
//字符正确的写法
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语言中文网";
使用宽字符的编码方式。常见的宽字符编码有 UTF-16 和 UTF-32,它们都是基于 Unicode 字符集的,能够支持全球的语言文化。
C语言推出了一种新的类型,叫做 wchar_t。wchar_t 的长度由编译器决定:
单独的字符由单引号’ ‘包围,例如’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前缀的宽字符无能为力,我们必须使用
在输出宽字符之前还要使用 setlocale 函数(setlocale 函数位于
希望设置为中文简体环境:
Windows 下请写作:
setlocale(LC_ALL, "zh-CN");
在 Linux 和 Mac OS 下请写作:
setlocale(LC_ALL, "zh_CN");
代码演示:
#include
#include
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 %lc\n", //必须使用宽字符串
a, b, c, d, e, f, g, h
);
//将本地环境设置为简体中文
setlocale(LC_ALL, "zh_CN");
//使用通用的 wprintf 输出宽字符
wprintf(L"web_url: %ls \nweb_name: %ls\n", web_url, web_name);
return 0;
}
对于 char 类型的窄字符,始终使用 ASCII 编码。
对于 wchar_t 类型的宽字符和宽字符串,使用 UTF-16 或者 UTF-32 编码,它们都是基于 Unicode 字符集的。
对于 char 类型的窄字符串,微软编译器使用本地编码,GCC、LLVM/Clang 使用和源文件编码相同的编码。
转义字符以\或者\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 | 反斜杠 | \ |
关键字 | 说明 |
---|---|
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语言支持单行注释和多行注释:
- 单行注释以//开头,直到本行末尾(不能换行);
- 多行注释以/开头,以/结尾,注释内容可以有一行或多行。
- 表达式必须有一个执行结果,这个结果必须是一个值,例如3*4+5的结果 17,a=c=d=10的结果是 10,printf(“hello”)的结果是 5(printf 的返回值是成功打印的字符的个数)。
- 以分号;结束的往往称为语句,而不是表达式,例如3*4+5;、a=c=d;等。
加法 | 减法 | 乘法 | 除法 | 求余数(取余) | |
---|---|---|---|---|---|
数学 | + | - | × | ÷ | 无 |
C语言 | + | - | × | / | % |
C语言中的除法运算有点奇怪,不同类型的除数和被除数会导致不同类型的运算结果:
- 当除数和被除数都是整数时,运算结果也是整数;如果不能整除,那么就直接丢掉小数部分,只保留整数部分,这跟将小数赋值给整数类型是一个道理。
- 一旦除数和被除数中有一个是小数,那么运算结果也是小数,并且是 double 类型的小数。
- 另外需要注意的一点是除数不能为 0,因为任何一个数字除以 0 都没有意义。
C语言中的取余运算只能针对整数,也就是说,% 的两边都必须是整数,不能出现小数,否则编译器会报错。
余数可以是正数也可以是负数,由 % 左边的整数决定:
- 如果 % 左边是正数,那么余数也是正数;
- 如果 % 左边是负数,那么余数也是负数。
在 printf 中,% 是格式控制符的开头,是一个特殊的字符,不能直接输出;要想输出 %,必须在它的前面再加一个 %,这个时候 % 就变成了普通的字符,而不是用来表示格式控制符了。
a = a # b 可以简写为:a #= b (# 表示 +、-、*、/、% 中的任何一种运算符。)
注意:a #= b 仅是一种简写形式,不会影响程序的执行效率。
++和–分别称为自增运算符和自减运算符
自增自减完成后,会用新值替换旧值,将新值保存在当前变量中
自增自减的结果必须得有变量来接收,所以自增自减只能针对变量,不能针对数字,例如10++就是错误的。
需要重点说明的是,++ 在变量前面和后面是有区别的:
- ++ 在前面叫做前自增(例如 ++a)。前自增先进行自增运算,再进行其他操作。
- ++ 在后面叫做后自增(例如 a++)。后自增先进行其他操作,再进行自增运算。
- 自减(–)也一样,有前自减和后自减之分。
#include
int main()
{
int a = 12, b = 1;
int c = a - (b--); // ①
int d = (++a) - (--b); // ②
printf("c=%d, d=%d\n", c, d);
return 0;
}
// 输出结果:c=11, d=14
在函数外部定义的变量叫做全局变量(Global Variable),在函数内部定义的变量叫做局部变量(Local Variable)
为了让编译器方便给变量分配内存,C89 标准规定,所有的局部变量(函数内部的变量)都必须定义在函数的开头位置,在定义完所有变量之前不能有其它的表达式。这种规定太过死板,虽然变量定义在函数开头,但是使用变量可能在函数的尾部,如果函数比较长,那么定义变量和使用变量的距离就有点远了,编写代码或者阅读代码时就要频繁得向前翻看代码,非常不方便,所以后来的 C99 标准就取消了这个限制。在实际开发中确认编译器开发标准是否支持代码格式.
一个变量,即使不给它赋值,它也会有一个默认的值,这个值就是默认初始值。
- 对于全局变量,它的默认初始值始终是 0,因为全局变量存储在内存分区中的全局数据区,这个区域中的数据在程序载入内存后会被初始化为 0。
- 而对于局部变量,C语言并没有规定它的默认初始值是什么,所以不同的编译器进行了不同的扩展,有的编译器会初始化为 0,有的编译器放任不管,爱是什么就是什么。(变量定义时会给变量分配一块内存空间,如果不对变量进行初始化,那就意味着不对这块内存进行写入操作,这块内存的数据会保持不变,依然是分配之前的数据。这样的数据可能是当前程序在之前的运行过程中产生的,也可能是之前运行过的其它程序产生的,我们根本无法预测这样的数据到底是什么,所以你会看到它是一个毫无意义的值,这样的值是随机的,是垃圾值,没有使用价值。)
当一个表达式中出现多个运算符时,C语言会先比较各个运算符的优先级,按照优先级从高到低的顺序依次执行;当遇到优先级相同的运算符时,再根据结合性决定先执行哪个运算符:如果是左结合性就先执行左边的运算符,如果是右结合性就先执行右边的运算符。
优先级 |
运算符 |
名称或含义 |
使用形式 |
结合方向 |
说明 |
---|---|---|---|---|---|
1 |
[] |
数组下标 |
数组名[常量表达式] |
左到右 |
|
() |
圆括号 |
(表达式) |
|||
. |
成员选择(对象) |
对象.成员名 |
|||
-> |
成员选择(指针) |
对象指针->成员名 |
|||
2 |
- |
负号运算符 |
-表达式 |
右到左 |
单目运算符 |
(类型) |
强制类型转换 |
(数据类型)表达式 |
|||
++ |
自增运算符 |
++变量名 |
单目运算符 |
||
-- |
自减运算符 |
--变量名 |
单目运算符 |
||
* |
取值运算符 |
*指针变量 |
单目运算符 |
||
& |
取地址运算符 |
&变量名 |
单目运算符 |
||
! |
逻辑非运算符 |
!表达式 |
单目运算符 |
||
~ |
按位取反运算符 |
~表达式 |
单目运算符 |
||
sizeof |
长度运算符 |
sizeof(表达式) |
|||
3 |
/ |
除 |
表达式 / 表达式 |
左到右 |
双目运算符 |
* |
乘 |
表达式*表达式 |
双目运算符 |
||
% |
余数(取模) |
整型表达式%整型表达式 |
双目运算符 |
||
4 |
+ |
加 |
表达式+表达式 |
左到右 |
双目运算符 |
- |
减 |
表达式-表达式 |
双目运算符 |
||
5 |
<< |
左移 |
变量<<表达式 |
左到右 |
双目运算符 |
>> |
右移 |
变量>>表达式 |
双目运算符 |
||
6 |
> |
大于 |
表达式>表达式 |
左到右 |
双目运算符 |
>= |
大于等于 |
表达式>=表达式 |
双目运算符 |
||
< |
小于 |
表达式<表达式 |
双目运算符 |
||
<= |
小于等于 |
表达式<=表达式 |
双目运算符 |
||
7 |
== |
等于 |
表达式==表达式 |
左到右 |
双目运算符 |
!= |
不等于 |
表达式!= 表达式 |
双目运算符 |
||
8 |
& |
按位与 |
表达式&表达式 |
左到右 |
双目运算符 |
9 |
^ |
按位异或 |
表达式^表达式 |
左到右 |
双目运算符 |
10 |
| |
按位或 |
表达式|表达式 |
左到右 |
双目运算符 |
11 |
&& |
逻辑与 |
表达式&&表达式 |
左到右 |
双目运算符 |
12 |
|| |
逻辑或 |
表达式||表达式 |
左到右 |
双目运算符 |
13 |
?: |
条件运算符 |
表达式1? 表达式2: 表达式3 |
右到左 |
三目运算符 |
14 |
= |
赋值运算符 |
变量=表达式 |
右到左 |
|
/= |
除后赋值 |
变量/=表达式 |
|||
*= |
乘后赋值 |
变量*=表达式 |
|||
%= |
取模后赋值 |
变量%=表达式 |
|||
+= |
加后赋值 |
变量+=表达式 |
|||
-= |
减后赋值 |
变量-=表达式 |
|||
<<= |
左移后赋值 |
变量<<=表达式 |
|||
>>= |
右移后赋值 |
变量>>=表达式 |
|||
&= |
按位与后赋值 |
变量&=表达式 |
|||
^= |
按位异或后赋值 |
变量^=表达式 |
|||
|= |
按位或后赋值 |
变量|=表达式 |
|||
15 |
, |
逗号运算符 |
表达式,表达式,… |
左到右 |
|
- 将一种类型的数据赋值给另外一种类型的变量时就会发生自动类型转换。在赋值运算中,赋值号两边的数据类型不同时,需要把右边表达式的类型转换为左边变量的类型,这可能会导致数据失真,或者精度降低;所以说,自动类型转换并不一定是安全的。
- 在不同类型的混合运算中,编译器也会自动地转换数据类型,将参与运算的所有数据先转换为同一种类型,然后再进行计算。转换的规则如下:
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语言中,有三个函数可以用来在显示器上输出数据,它们分别是:
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() 格式控制符的完整形式如下:
%[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),# 表示强迫输出小数点。如果没有小数部分,默认是不输出小数点的,加上 # 以后,即使没有小数部分也会带上小数点。
Windows 和 Linux、Mac OS 的缓存机制不同, 表现也不同;要想破解 printf() 输出的问题,必须要了解缓存,它能使你对输入输出的认识上升到一个更高的层次,以后不管遇到什么疑难杂症,都能迎刃而解。可以说,输入输出的“命门”就在于缓存。
在C语言中,有多个函数可以从键盘获得用户输入:
scanf() 是最灵活、最复杂、最常用的输入函数,但它不能完全取代其他函数。
&称为取地址符,也就是获取变量在内存中的地址。scanf 会根据地址把读取到的数据写入内存。
%p是一个新的格式控制符,它表示以十六进制的形式(带小写的前缀)输出数据的地址。如果写作%P,那么十六进制的前缀也将变成大写形式。
对于 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() 的要求,那么就读取结束;如果不符合要求,那么就继续等待用户输入,或者干脆读取失败。
#include
int main()
{
int a = 1, b = 2;
scanf("a=%d", &a);
scanf("b=%d", &b);
printf("a=%d, b=%d\n", 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。
字符串的两种定义形式,它们分别是:
这两种形式其实是有区别的,第一种形式的字符串所在的内存既有读取权限又有写入权限,第二种形式的字符串所在的内存只有读取权限,没有写入权限。printf()、puts() 等字符串输出函数只要求字符串有读取权限,而 scanf()、gets() 等字符串输入函数要求字符串有写入权限,所以,第一种形式的字符串既可以用于输出函数又可以用于输入函数,而第二种形式的字符串只能用于输出函数。
对于第一种形式的字符串,在[ ]里面要指明字符串的最大长度,如果不指明,也可以根据=后面的字符串来自动推算,此处,就是根据"http://c.biancheng.net"的长度来推算的。如果只是定义了一个字符串,并没有立即给它赋值,所以没法自动推算,只能手动指明最大长度,那么就需要在[]中添加类似于[30],定义一个长度。
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 | 读取一个字符串(以空白符为结束) |
最容易理解的字符输入函数是 getchar(),它就是scanf("%c", c)的替代品,除了更加简洁,没有其它优势了;或者说,getchar() 就是 scanf() 的一个简化版本。
gets() 是有缓冲区的,每次按下回车键,就代表当前输入结束了,gets() 开始从缓冲区中读取内容,这一点和 scanf() 是一样的。gets() 和 scanf() 的主要区别是:
scanf() 可以一次性读取多份类型相同或者不同的数据,getchar()、getche()、getch() 和 gets() 每次只能读取一份特定类型的数据,不能一次性读取多份数据。
计算机在内存中预留了一定的存储空间,用来暂时保存输入或输出的数据,这部分预留的空间就叫做缓冲区(缓存)。
根据缓冲区对应的是输入设备还是输出设备,可以分为输入缓冲区和输出缓冲区。
根据数据刷新(也可以称为清空缓冲区,就是将缓冲区中的数据“倒出”)的时机,可以分为全缓冲、行缓冲、不带缓冲。
C语言标准规定,输入输出缓冲区要具有以下特征:
所谓刷新缓冲区,就是将缓冲区中的内容送达到目的地。缓冲区的刷新遵循以下的规则:
scanf() 是从标准输入设备(键盘)读取数据,带有行缓冲区的。
当遇到 scanf() 函数时,程序会先检查输入缓冲区中是否有数据:
事实上这一章的学习中,有几个点的理解是很重要的,一个是 scanf 这个方法的逻辑,还有就是一些特殊的规则:
1)匹配失败意味着不会移动内部的位置指针
2)空格、制表符、换行符在大部分情况下都可以忽略,前面的两个例子就是这样。但是当控制字符串不是以格式控制符 %d、%c、%f 等开头时,空格、制表符、换行符就不能忽略了(比如:scanf(“a=%d”, &a) 这种写法),它会参与匹配过程,如果匹配失败,就意味着 scanf() 读取失败了。
事实上以上的引用部分关于特殊规则的描述,是我自己的总结,我认为这一章中是有些错误的,也反馈给了客户,不过还没有得到回复,如果有不一样的理解请在评论区给我留言,谢谢了:
清空(刷新)缓冲区即可:
清空输出缓冲区很简单,使用下面的语句即可:
fflush(stdout);
fflush() 是一个专门用来清空缓冲区的函数,stdout 是 standard output 的缩写,表示标准输出设备,也即显示器。整个语句的意思是,清空标准输出缓冲区,或者说清空显示器的缓冲区。
在 Linux 和 Mac OS 平台下清空缓冲区:
#include
#include
int main()
{
printf("C语言中文网");
fflush(stdout); //本次输出结束后立即清空缓冲区
sleep(5);
printf("http://c.biancheng.net\n");
return 0;
}
// 程序运行后,第一个 pirntf() 立即输出,等待 5 秒以后,第二个 printf() 才输出,这就符合我们的惯性思维了。如果不加fflush(stdout)语句,程序运行后,第一个 printf() 并不会立即输出,而是等待 5 秒以后和第二个 printf() 一起输出
两种通用的方案
#include
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=%d\n", a, b);
return 0;
}
scanf("%*[^\n]"); scanf("%*c");
第一个 scanf() 将逐个读取缓冲区中\n之前的其它字符,% 后面的 * 表示将读取的这些字符丢弃,遇到\n字符时便停止读取。此时,缓冲区中尚有一个\n遗留,第二个 scanf() 再将这个\n读取并丢弃,这里的星号和第一个 scanf() 的星号作用相同。由于所有从键盘的输入都是以回车结束的,而回车会产生一个\n字符,所以将\n连同它之前的字符全部读取并丢弃之后,也就相当于清除了输入缓冲区。
#include
int main()
{
int a = 1, b = 2;
scanf("a=%d", &a);
scanf("%*[^\n]"); scanf("%*c"); //在下次读取前清空缓冲区
scanf("b=%d", &b);
printf("a=%d, b=%d\n", a, b);
return 0;
}
最靠谱、最通用、最有效的清空输入缓冲区的方案就是使用 getchar() 或者 scanf() 将缓冲区中的数据逐个读取出来,其它方案都有或多或少的问题。
可以在格式控制符的中间加一个数字,用来表示读取数据的最大长度,例如:
%s 控制符会匹配除空白符以外的所有字符,它有两个缺点:
[ ]包围起来的是需要读取的字符集合。例如,%[abcd]表示只读取字符abcd,遇到其它的字符就读取结束;注意,这里并不强调字符的顺序,只要字符在 abcd 范围内都可以匹配成功,所以你可以输入 abcd、dcba、ccdc、bdcca 等。
需要注意的是虽然字符的顺序不是关键,但是字符的连续性是有要求的,依然以%[abcd]为例,如果输入字符为 a1b2c3d4,事实上最后匹配的字符是a,如果是 ab12cd34,那么匹配的字符是ab。
为了简化字符集合的写法,scanf() 支持使用连字符-来表示一个范围内的字符,例如 %[a-z]、%[0-9] 等(连字符左边的字符对应一个 ASCII 码,连字符右边的字符也对应一个 ASCII 码,位于这两个 ASCII 码范围以内的字符就是要读取的字符。注意,连字符左边的 ASCII 码要小于右边的,如果反过来,那么它的行为是未定义的)。
常用的连字符举例:
scanf() 允许我们在%[ ]中直接指定某些不能匹配的字符,具体方法就是在不匹配的字符前面加上^,例如:
scanf("%[^\n]", str2); //这个操作也能读取带空格的字符串,scanf()完全可以取代 gets()
input: c c++ java python go javascript↙
output: str2=c c++ java python go javascript
scanf() 允许把读取到的数据直接丢弃,不往变量中存放,具体方法就是在 % 后面加一个*,例如:
这里我们就可以解释,上一章节中通过scanf来清空缓存区的原理了:
scanf("%*[^\n]"); scanf("%*c");
//scanf("%*[^\n]");将换行符前面的所有字符清空,scanf("%*c");将最后剩下的换行符清空。
scanf() 控制字符串的完整写法为:%{*} {width} type
if(判断条件){
语句块1
}else{
语句块2
}
C语言提供了以下关系运算符:
关系运算符 | 含 义 | 数学中的表示 |
---|---|---|
< | 小于 | < |
<= | 小于或等于 | ≤ |
> | 大于 | > |
>= | 大于或等于 | ≥ |
== | 等于 | = |
!= | 不等于 | ≠ |
if 语句的判断条件中不是必须要包含关系运算符,它可以是赋值表达式,甚至也可以是一个变量,例如:
//情况①
if(b){
//TODO:
}
//情况②
if(b=5){
//情况①
//TODO:
}
运算符 | 说明 | 结合性 | 举例 |
---|---|---|---|
&& | 与运算,双目,对应数学中的“且” | 左结合 | 1&&0、(9>3)&&(b>a) |
|| | 或运算,双目,对应数学中的“或” | 左结合 | 1||0、(9>3)||(b>a) |
! | 非运算,单目,对应数学中的“非” | 右结合 | !a、!(2<5) |
逻辑运算的结果也只有“真”和“假”,“真”对应的值为 1,“假”对应的值为 0。
逻辑运算符和其它运算符优先级从低到高依次为:
赋值运算符(=) < 或|| < 与&& < 关系运算符 < 算术运算符 < 非(!)
按照运算符的优先顺序可以得出:
switch(表达式){
case 整型数值1: 语句 1;
case 整型数值2: 语句 2;
......
case 整型数值n: 语句 n;
default: 语句 n+1;
}
以上语句中,没有 break, 当和某个整型数值匹配成功后,会执行该分支以及后面所有分支的语句。
break 是C语言中的一个关键字,专门用于跳出 switch 语句。所谓“跳出”,是指一旦遇到 break,就不再执行 switch 中的任何语句,包括当前分支中的语句和其他分支中的语句;也就是说,整个 switch 执行结束了,接着会执行整个 switch 后面的代码。
注意点:
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; //错误,不能包含变量
语法格式为:
表达式1 ? 表达式2 : 表达式3
max = (a>b) ? a : b;
等价于:
if(a>b){
max = a;
}else{
max = b;
}
while(表达式){
语句块
}
do{
语句块
}while(表达式);
do-while循环与while循环的不同在于:它会先执行“语句块”,然后再判断表达式是否为真,如果为真则继续循环;如果为假,则终止循环。因此,do-while 循环至少要执行一次“语句块”。
for 循环,它的使用更加灵活,完全可以取代 while 循环; for 循环的一般形式为:
for(表达式1; 表达式2; 表达式3){
语句块
}
它的运行过程为:
上面的步骤中,2) 和 3) 是一次循环,会重复执行,for 语句的主要作用就是不断执行步骤 2) 和 3)。
“表达式1”仅在第一次循环时执行,以后都不会再执行,可以认为这是一个初始化语句。“表达式2”一般是一个关系表达式,决定了是否还要继续下次循环,称为“循环条件”。“表达式3”很多情况下是一个带有自增或自减操作的表达式,以使循环条件逐渐变得“不成立”。
int i = 1, sum = 0;
for( ; i<=100; i++){
sum+=i;
}
上面代码的作用与以下代码相同:
int i, sum=0;
for(i=1/*语句①*/; i<=100/*语句②*/; i++/*语句③*/){
sum+=i;
}
如果不做其它处理就会成为死循环,如下:
for(i=1; ; i++) sum=sum+i;
以上代码相当于:
i=1;
while(1){
sum=sum+i;
i++;
}
可在循环体中加入修改变量的语句, 需要注意的是,如果不在循环体中对变量进行修改,那么如果满足“表达式2”的条件的话,也会变成一个“死循环”:
for( i=1; i<=100; ){
sum=sum+i;
i++;
}
for( ; i<=100 ; ){
sum=sum+i;
i++;
}
以上代码相当于:
while(i<=100){
sum=sum+i;
i++;
}
for( ; ; )
以上代码相当于:
while(1)
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;
for( i=0; (c=getchar())!='\n'; i+=c );
使用while或for循环时,如果想提前结束循环(在不满足结束条件的情况下结束循环),可以使用break或continue关键字。
当 break 关键字用于 while、for 循环时,会终止循环而执行整个循环语句后面的代码。break 关键字通常和 if 语句一起使用,即满足条件时便跳出循环。
continue 语句的作用是跳过循环体中剩余的语句而强制进入下一次循环。continue语句只用在 while、for 循环中,常与 if 条件语句一起使用,判断条件是否成立。
在C语言中,if-else、while、do-while、for 都可以相互嵌套。所谓嵌套(Nest),就是一条语句里面还有另一条语句,例如 for 里面还有 for,while 里面还有 while,或者 for 里面有 while,while 里面有 if-else,这都是允许的。
C语言中常用的编程结构有三种(其它编程语言也是如此),它们分别是:
数组(Array)就是一组数据的集合;它所包含的每一个数据叫做数组元素(Element),所包含的数据的个数称为数组长度(Length),例如int a[4];就定义了一个长度为4的整型数组,名字是a。
数组中的每个元素都有一个序号,这个序号从0开始,而不是从我们熟悉的1开始,称为下标(Index)。使用数组元素时,指明下标即可,形式为:
arrayName[index]; 例如,a[0] 表示第0个元素,a[3] 表示第3个元素。
需要注意的是:
数组是一个整体,它的内存是连续的;也就是说,数组元素之间是相互挨着的,彼此之间没有一点点缝隙。下图演示了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:
可以通过下面的形式将数组的所有元素初始化为 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};
以上两种定义方式是等价的
二维数组定义的一般形式是:
dataType arrayName[length1][length2];
dataType 为数据类型,arrayName 为数组名,length1 为第一维下标的长度,length2 为第二维下标的长度。
二维数组在概念上是二维的,但在内存中是连续存放的;换句话说,二维数组的各个元素是相互挨着的,彼此之间没有缝隙:
在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语言中,没有专门的字符串变量,没有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';
- 需要注意的是,逐个字符地给数组赋值并不会自动添加’\0’,例如:char str[] = {‘a’, ‘b’, ‘c’}; 数组 str 的长度为 3,而不是 4,因为最后没有’\0’。
- 当用字符数组存储字符串时,要特别注意’\0’,要为’\0’留个位置;这意味着,字符数组的长度至少要比字符串的长度大 1。请看下面的例子:char str[7] = “abc123”; "abc123"看起来只包含了 6 个字符,我们却将 str 的长度定义为 7,就是为了能够容纳最后的’\0’。如果将 str 的长度定义为 6,它就无法容纳’\0’了。当字符串长度大于数组长度时,有些较老或者不严格的编译器并不会报错,甚至连警告都没有,这就为以后的错误埋下了伏笔。
需要注意的是:逐个给字符数组赋值,这个时候就很容易遗忘字符串结束标志’\0’
如下:
char str[30];
char c;
int i;
for(c=65,i=0; c<=90; c++,i++){
str[i] = c;
}
在函数内部定义的变量、数组、结构体、共用体等都称为局部数据。在很多编译器下,局部数据的初始值都是随机的、无意义的,而不是我们通常认为的“零”值,如果不加以处理,会产生奇葩的错误。
在给字符数组的成员逐一赋值的代码中,如果想要添加结束符:
char str[30];
....
str[i] = 0; //此处为添加的代码,也可以写作 str[i] = '\0';
根据 ASCII 码表,字符'\0'的编码值就是 0。
当然还有更加简便的办法,在定义的时候就确定初始值:
char str[30] = {
0}; //将所有元素都初始化为 0,或者说 '\0'
在C语言中,使用string.h头文件中的 strlen() 函数来求字符串的长度,它的用法为:
length strlen(strname);
char str[] = "http://c.biancheng.net/c/";
long len = strlen(str);// 此处 len 值为25,他并不会把结束符也算进长度
输出字符串时只需要给出名字,不能带后边的[ ],例如,下面的两种写法都是错误的:
printf("%s\n", str[]);
puts(str[10]);
正确的写法:
char str[] = "http://c.biancheng.net";
printf("%s\n", str); //通过字符串名字输出
printf("%s\n", "http://c.biancheng.net"); //直接输出
puts(str); //通过字符串名字输出
puts("http://c.biancheng.net"); //直接输出
在C语言中,有两个函数可以让用户从键盘上输入字符串,它们分别是:
scanf() 在读取数据时需要的是数据的地址,这一点是恒定不变的,所以对于 int、char、float 等类型的变量都要在前边添加&以获取它们的地址。但是在输入字符串时,却没有在前边添加&,因为字符串名字或者数组名字在使用的过程中一般都会转换为地址,所以再添加&就是多此一举,甚至会导致错误了。
意思是把两个字符串拼接在一起,语法格式为:
strcat(arrayName1, arrayName2); // arrayName1、arrayName2 为需要拼接的字符串。
意思是字符串复制,也即将字符串从一个地方复制到另外一个地方,语法格式为:
strcpy(arrayName1, arrayName2); // strcpy() 会把 arrayName2 中的字符串拷贝到 arrayName1 中,字符串结束标志’\0’也一同拷贝。将 str2 复制到 str1 后,str1 中原来的内容就被覆盖了,strcpy() 要求 arrayName1 要有足够的长度,否则不能全部装入所拷贝的字符串。
char str1[50] = "http://c.biancheng.net/cpp/u/jiaocheng/";
char str2[50] = "123";
strcpy(str1, str2);
printf("str1: %s\n", str1);
最终输出的 str1 的内容是: 123
意思是字符串比较,语法格式为:
strcmp(arrayName1, arrayName2); // arrayName1 和 arrayName2 是需要比较的两个字符串。
- 字符本身没有大小之分,strcmp() 以各个字符对应的 ASCII 码值进行比较。strcmp() 从两个字符串的第 0 个字符开始比较,如果它们相等,就继续比较下一个字符,直到遇见不同的字符,或者到字符串的末尾。
- 返回值:若 arrayName1 和 arrayName2 相同,则返回0;若 arrayName1 大于 arrayName2,则返回大于 0 的值;若 arrayName1 小于 arrayName2,则返回小于0 的值。
插入和删除数组元素都要移动内存,甚至重新开辟一块内存,这是相当消耗资源的。如果一个程序中有大量的此类操作,那么程序的性能将堪忧,这有悖于「C语言非常高效」的初衷,所以C语言并不支持动态数组。
为了保证程序执行效率,为了防止操作错误,C语言只支持静态数组,不支持动态数组。
C语言数组是静态的,不能自动扩容,当下标小于零或大于等于数组长度时,就发生了越界(Out Of Bounds),访问到数组以外的内存。如果下标小于零,就会发生下限越界(Off Normal Lower);如果下标大于等于数组长度,就会发生上限越界(Off Normal Upper)。
当赋予数组的元素个数超过数组长度时,就会发生溢出(Overflow)。
因为 C89 和 C99 对数组做出了不同的规定:
下面的代码使用常量表达式指明数组长度,在任何编译器下都能编译通过:
int a[10]; //长度为10
int b[3*5]; //长度为15
int c[4+8]; //长度为12
下面的代码使用变量指明数组长度,在 GCC 和 Xcode 下能够编译通过,而在 VC 和 VS(包括 VC 6.0、VS2010、VS2013、VS2015、VS2017 等)下都会报错:
int m = 10, n;
scanf("%d", &n);
int a[m], b[n];
变量的值在编译期间并不能确定,只有等到程序运行后,根据计算结果才能知道它的值到底是什么,所以数组长度中一旦包含了变量,那么数组长度在编译期间就不能确定了,也就不能为数组分配内存了,只有等到程序运行后,得到了变量的值,确定了具体的长度,才能给数组分配内存,我们将这样的数组称为变长数组(VLA, Variable Length Array)。
变长数组是说数组的长度在定义之前可以改变,一旦定义了,就不能再改变了,所以变长数组的容量也是不能扩大或缩小的,它仍然是静态数组。
略
dataType functionName(){
//body
return dataType
}
有的函数不需要返回值,或者返回值类型不确定(很少见),那么可以用 void 表示,例如:
void hello(){
printf ("Hello,world \n");
//没有返回值就不需要 return 语句
}
dataType functionName( dataType1 param1, dataType2 param2 ... ){
//body
}
函数定义时给出的参数称为形式参数,简称形参;函数调用时给出的参数(也就是传递的数据)称为实际参数,简称实参。函数调用时,将实参的值传递给形参,相当于一次赋值操作。
- 形参的改变并不会影响到实参,可以说,他们是两个互不影响的变量
C语言不允许函数嵌套定义;也就是说,不能在一个函数中定义另外一个函数,必须在所有函数之外定义另外一个函数。main() 也是一个函数定义,也不能在 main() 函数内部定义新函数。只能是函数调用其他函数。
return 语句的一般形式为:
return 表达式;
或者
return (表达式);
有没有( )都是正确的,为了简明,一般也不写( )。例如:
return max;
return a+b;
return (100+200);
如果一个函数 A() 在定义或调用过程中出现了对另外一个函数 B() 的调用,那么我们就称 A() 为主调函数或主函数,称 B() 为被调函数。
当主调函数遇到被调函数时,主调函数会暂停,CPU 转而执行被调函数的代码;被调函数执行完毕后再返回主调函数,主调函数根据刚才的状态继续往下执行。
一个C语言程序的执行过程可以认为是多个函数之间的相互调用过程,它们形成了一个或简单或复杂的调用链条。这个链条的起点是 main(),终点也是 main()。当 main() 调用完了所有的函数,它会返回一个值(例如return 0;)来结束自己的生命,从而结束整个程序。
函数是一个可以重复使用的代码块,CPU 会一条一条地挨着执行其中的代码,当遇到函数调用时,CPU 首先要记录下当前代码块中下一条代码的地址(假设地址为 0X1000),然后跳转到另外一个代码块,执行完毕后再回来继续执行 0X1000 处的代码。整个过程相当于 CPU 开了一个小差,暂时放下手中的工作去做点别的事情,做完了再继续刚才的工作。
所谓声明(Declaration),就是告诉编译器我要使用这个函数,你现在没有找到它的定义不要紧,请不要报错,稍后我会把定义补上。
函数声明的格式非常简单,相当于去掉函数定义中的函数体,并在最后加上分号;,如下所示:
dataType functionName( dataType1 param1, dataType2 param2 ... );
也可以不写形参,只写数据类型:
dataType functionName( dataType1, dataType2 ... );
函数声明给出了函数名、返回值类型、参数列表(重点是参数类型)等与该函数有关的信息,称为函数原型(Function Prototype)。函数原型的作用是告诉编译器与该函数有关的信息,让编译器知道函数的存在,以及存在的形式,即使函数暂时没有定义,编译器也知道如何使用它。
有了函数声明,函数定义就可以出现在任何地方了,甚至是其他文件、静态链接库、动态链接库等。
对于多个文件的程序,通常是将函数定义放到源文件(.c文件)中,将函数的声明放到头文件(.h文件)中,使用函数时引入对应的头文件就可以,编译器会在链接阶段找到函数体。
最后再补充一点,函数原型给出了使用该函数的所有细节,当我们不知道如何使用某个函数时,需要查找的是它的原型,而不是它的定义,我们往往不关心它的实现。
定义在函数内部的变量称为局部变量(Local Variable),它的作用域仅限于函数内部, 离开该函数后就是无效的,再使用就会报错。
在所有函数外部定义的变量称为全局变量(Global Variable),它的作用域默认是整个程序,也就是所有的源文件,包括 .c 和 .h 文件。
C语言规定,只能从小的作用域向大的作用域中去寻找变量,而不能反过来,使用更小的作用域中的变量。
所谓代码块,就是由{ }包围起来的代码。
C语言允许在代码块内部定义变量,这样的变量具有块级作用域;换句话说,在代码块内部定义的变量只能在代码块内部使用,出了代码块就无效了。
遵循 C99 标准的编译器允许在 for 循环条件里面定义新变量,这样的变量也是块级变量,它的作用域仅限于 for 循环内部。
C语言还允许出现单独的代码块,它也是一个作用域。请看下面的代码:
#include
int main(){
int n = 22; //编号①
//由{ }包围的代码块
{
int n = 40; //编号②
printf("block n: %d\n", n);
}
printf("main n: %d\n", n);
return 0;
}
main() 函数是主函数,它可以调用其它函数,而不允许被其它函数调用。因此,C程序的执行总是从 main() 函数开始,完成对其它函数的调用后再返回到 main() 函数,最后由 main() 函数结束整个程序。
在编译之前对源文件进行简单加工的过程,就称为预处理(即预先处理、提前处理)。
编译器会将预处理的结果保存到和源文件同名的.i文件中,例如 main.c 的预处理结果在 main.i 中。和.c一样,.i也是文本文件,可以用编辑器打开直接查看内容。
C语言提供了多种预处理功能,如宏定义、文件包含、条件编译等,合理地使用它们会使编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。
#include
//不同的平台下引入不同的头文件
#if _WIN32 //识别windows平台
#include
#elif __linux__ //识别linux平台
#include
#endif
int main() {
//不同的平台下调用不同的函数
#if _WIN32 //识别windows平台
Sleep(5000);
#elif __linux__ //识别linux平台
sleep(5);
#endif
puts("http://c.biancheng.net/");
return 0;
}
#if、#elif、#endif 就是预处理命令,它们都是在编译之前由预处理程序来执行的。
#include叫做文件包含命令,用来引入对应的头文件(.h文件)。#include 也是C语言预处理命令的一种。
#include 的处理过程很简单,就是将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件,这与复制粘贴的效果相同。
#include 的用法有两种,如下所示:
#include
#include "myHeader.h"
使用尖括号< >和双引号" "的区别在于头文件的搜索路径不同:
1)使用尖括号< >,编译器会到系统路径下查找头文件;
2)而使用双引号" ",编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找。
#define 叫做宏定义命令,它也是C语言预处理命令的一种。宏定义,就是用一个标识符来表示一个字符串,如果在后面的代码中出现了该标识符,那么就全部替换成指定的字符串。
宏定义的一般形式为:#define 宏名 字符串。#表示这是一条预处理命令,所有的预处理命令都以 # 开头。宏名是标识符的一种,命名规则和变量相同。字符串可以是数字、表达式、if 语句、函数等。
#define PI 3.14159
int main(){
// Code
return 0;
}
#undef PI
void func(){
// Code
}
表示 PI 只在 main() 函数中有效,在 func() 中无效。
#include
#define OK 100
int main(){
printf("OK\n");
return 0;
}
#define PI 3.1415926
#define S PI*y*y /* PI是已定义的宏名*/
#define UINT unsigned int
C语言允许宏带有参数。在宏定义中的参数称为“形式参数”,在宏调用中的参数称为“实际参数”
对带参数的宏,在展开过程中不仅要进行字符串替换,还要用实参去替换形参。
带参宏定义的一般形式为:
#define 宏名(形参列表) 字符串
带参宏调用的一般形式为:
宏名(实参列表);
例如:
#define M(y) y*y+3*y //宏定义
// TODO:
k=M(5); //宏调用
在宏展开时,用实参 5 去代替形参 y,经预处理程序展开后的语句为k=5*5+3*5
在宏定义中,有时还会用到#和##两个符号,它们能够对宏参数进行操作。
#用来将宏参数转换为字符串,也就是在宏参数的开头和末尾添加引号。
例如有如下宏定义:
#define STR(s) #s
printf("%s", STR(c.biancheng.net));
printf("%s", STR("c.biancheng.net"));
展开后:
printf("%s", "c.biancheng.net");
printf("%s", "\"c.biancheng.net\"");
##称为连接符,用来将宏参数或其他的串连接起来。
例如有如下的宏定义:
#define CON1(a, b) a##e##b
#define CON2(a, b) a##b##00
printf("%f\n", CON1(8.5, 2));
printf("%d\n", CON2(12, 34));
展开后:
printf("%f\n", 8.5e2);
printf("%d\n", 123400);
预定义宏就是已经预先定义好的宏,可以直接使用,无需再重新定义。
ANSI C 规定了以下几个预定义宏,它们在各个编译器下都可以使用:
__LINE__:表示当前源代码的行号;
__FILE__:表示当前源文件的名称;
__DATE__:表示当前的编译日期;
__TIME__:表示当前的编译时间;
__STDC__:当要求程序严格遵循ANSI C标准时该标识被赋值为1;
__cplusplus:当编写C++程序时该标识符被定义。
预定义宏演示:
#include
#include
int main() {
printf("Date : %s\n", __DATE__);
printf("Time : %s\n", __TIME__);
printf("File : %s\n", __FILE__);
printf("Line : %d\n", __LINE__);
system("pause");
return 0;
}
#include
int main(){
#if _WIN32
system("color 0c");
printf("http://c.biancheng.net\n");
#elif __linux__
printf("\033[22;31mhttp://c.biancheng.net\n\033[22;30m");
#else
printf("http://c.biancheng.net\n");
#endif
return 0;
}
#if、#elif、#else 和 #endif 都是预处理命令,整段代码的意思是:如果宏 _WIN32 的值为真,就保留第 4、5 行代码,删除第 7、9 行代码;如果宏 __linux__ 的值为真,就保留第 7 行代码;如果所有的宏都为假,就保留第 9 行代码。
#if 整型常量表达式1
程序段1
#elif 整型常量表达式2
程序段2
#elif 整型常量表达式3
程序段3
#else
程序段4
#endif
它的意思是:如常“表达式1”的值为真(非0),就对“程序段1”进行编译,否则就计算“表达式2”,结果为真的话就对“程序段2”进行编译,为假的话就继续往下匹配,直到遇到值为真的表达式,或者遇到 #else
#ifdef 宏名
程序段1
#else
程序段2
#endif
它的意思是,如果当前的宏已被定义过,则对“程序段1”进行编译,否则对“程序段2”进行编译。
#ifndef 宏名
程序段1
#else
程序段2
#endif
与 #ifdef 相比,仅仅是将 #ifdef 改为了 #ifndef。它的意思是,如果当前的宏未被定义,则对“程序段1”进行编译,否则对“程序段2”进行编译,这与 #ifdef 的功能正好相反。
#if 后面跟的是“整型常量表达式”,而 #ifdef 和 #ifndef 后面跟的只能是一个宏名,不能是其他的。
#error 指令用于在编译期间产生错误信息,并阻止程序的编译,其形式如下:
#error error_message
#ifdef WIN32
#error This programme cannot compile at Windows Platform
#endif
WIN32 是 Windows 下的预定义宏。当用户在 Windows 下编译该程序时,由于定义了 WIN32 这个宏,所以会执行 #error 命令,提示用户发生了编译错误
希望以 C++ 的方式来编译程序时
#ifndef __cplusplus
#error 当前程序必须以C++方式编译
#endif
计算机中所有的数据都必须放在内存中,不同类型的数据占用的字节数不一样,例如 int 占用 4 个字节,char 占用 1 个字节。为了正确地访问这些数据,必须为每个字节都编上号码,就像门牌号、身份证号一样,每个字节的编号是唯一的,根据编号可以准确地找到某个字节。
我们将内存中字节的编号称为地址(Address)或指针(Pointer)。
数据在内存中的地址也称为指针,如果一个变量存储了一份数据的指针,我们就称它为指针变量。
定义指针变量与定义普通变量非常类似,不过要在变量名前面加星号*,格式为:
datatype *name;
或者
datatype *name = value;
普通变量一样,指针变量也可以被多次写入,请看下面的代码:
//定义普通变量
float a = 99.5, b = 10.6;
char c = '@', d = '#';
//定义指针变量
float *p1 = &a;
char *p2 = &c;
//修改指针变量的值
p1 = &b;
p2 = &d;
*是一个特殊符号,表明一个变量是指针变量,定义 p1、p2 时必须带*。而给 p1、p2 赋值时,因为已经知道了它是一个指针变量,就没必要多此一举再带上*,后边可以像使用普通变量一样来使用指针变量。也就是说,定义指针变量时必须带*,给指针变量赋值时不能带*。
指针变量也可以连续定义:
int *a, *b, *c; //a、b、c 的类型都是 int*
注意每个变量前面都要带*
指针变量存储了数据的地址,通过指针变量能够获得该地址上的数据,格式为:
*pointer
这里的*称为指针运算符,用来取得某个地址上的数据,请看下面的例子:
#include
int main(){
int a = 15;
int *p = &a;
printf("%d, %d\n", a, *p); //两种方式都可以输出a的值
return 0;
}
运行结果:
15, 15
指针除了可以获取内存上的数据,也可以修改内存上的数据:
int *p = &a;
*p = 100;
需要注意的是,给指针变量本身赋值时不能加*:
int *p;
p = &a;
*p = 100;
指针变量也可以出现在普通变量能出现的任何表达式中,例如:
int x, y, *px = &x, *py = &y;
y = *px + 5; //表示把x的内容加5并赋给y,*px+5相当于(*px)+5
y = ++*px; //px的内容加上1之后赋给y,++*px相当于++(*px)
y = *px++; //相当于y=*(px++) -> y=*px; px+1
y = (*px)++; //这里的结果相当于 y = *px; (*px)+1;
py = px; //把一个指针的值赋给另一个指针
*&a和&*pa分别是什么意思:
指针变量保存的是地址,而地址本质上是一个整数,所以指针变量可以进行部分运算
定义数组时,要给出数组名和数组长度,数组名可以认为是一个指针,它指向数组的第 0 个元素。在C语言中,我们将第 0 个元素的地址称为数组的首地址。
int arr[] = {
99, 15, 100, 888, 252 };
arr 是int*类型的指针,每次加 1 时它自身的值会增加 sizeof(int),加 i 时自身的值会增加 sizeof(int) * i
定义一个指向数组的指针,例如:
int arr[] = {
99, 15, 100, 888, 252 };
int *p = arr;
或者
int *p = &arr[0];
如果一个指针指向了数组,我们就称它为数组指针(Array Pointer)。
arr 本身就是一个指针,可以直接赋值给指针变量 p。arr 是数组第 0 个元素的地址
引入数组指针后,我们就有两种方案来访问数组元素了,一种是使用下标,另外一种是使用指针。
假设 p 是指向数组 arr 中第 n 个元素的指针,那么 p++、++p、(*p)++:
C语言中没有特定的字符串类型,我们通常是将字符串放在一个字符数组中:
char str[] = "http://c.biancheng.net";
除了字符数组,C语言还支持另外一种表示字符串的方法,就是直接使用一个指针指向字符串:
char *str = "http://c.biancheng.net";
或者:
char *str;
str = "http://c.biancheng.net";
字符串中的所有字符在内存中是连续排列的,str 指向的是字符串的第 0 个字符;我们通常将第 0 个字符的地址称为字符串的首地址。字符串中每个字符的类型都是char,所以 str 的类型也必须是char *。
字符数组和指针的区别:它们最根本的区别是在内存中的存储区域不一样,字符数组存储在全局数据区或栈区,指针形式的字符串存储在常量区。全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,而常量区的字符串(也包括其他数据)只有读取权限,没有写入权限。
函数的参数不仅可以是整数、小数、字符等具体的数据,还可以是指向它们的指针。用指针变量作函数参数可以将函数外部的地址传递到函数内部,使得在函数内部可以操作函数外部的数据,并且这些数据不会随着函数的结束而被销毁。
int max(int *intArr, int len){
...
return maxValue;
}
或者
int max(int intArr[6], int len){
...
return maxValue;
}
或者
int max(int intArr[], int len){
...
return maxValue;
}
C语言允许函数的返回值是一个指针(地址),我们将这样的函数称为指针函数。
char *strlong(char *str1, char *str2){
if(strlen(str1) >= strlen(str2)){
return str1;
}else{
return str2;
}
}
#include
int main(){
int a =100;
int *p1 = &a;
int **p2 = &p1;
int ***p3 = &p2;
return 0;
}
以三级指针 p3 为例来分析上面的代码。***p3等价于*(*(*p3))。*p3 得到的是 p2 的值,也即 p1 的地址;*(*p3) 得到的是 p1 的值,也即 a 的地址;经过三次“取值”操作后,*(*(*p3)) 得到的才是 a 的值。
一个指针变量可以指向计算机中的任何一块内存,不管该内存有没有被分配,也不管该内存有没有使用权限,只要把地址给它,它就可以指向,C语言没有一种机制来保证指向的内存的正确性
建议对没有初始化的指针赋值为 NULL,例如:
char *str = NULL;
gets() 和 printf() 都对空指针做了特殊处理:
- gets() 不会让用户输入字符串,也不会向指针指向的内存中写入数据;
- printf() 不会读取指针指向的内容,只是简单地给出提示,让程序员意识到使用了一个空指针。
NULL 是“零值、等于零”的意思,在C语言中表示空指针。从表面上理解,空指针是不指向任何数据的指针,是无效指针,程序使用它不会产生效果。
NULL 是在stdio.h中定义的一个宏,它的具体内容为:
#define NULL ((void *)0)
(void *)0表示把数值 0 强制转换为void *类型
C语言没有规定 NULL 的指向,只是大部分标准库约定成俗地将 NULL 指向 0,所以不要将 NULL 和 0 等同起来
注意 NULL 和 NUL 的区别:NULL 表示空指针,是一个宏定义,可以在代码中直接使用。而 NUL 表示字符串的结束标志 ‘\0’,它是ASCII码表中的第 0 个字符。NUL 没有在C语言中定义,仅仅是对 ‘\0’ 的称呼,不能在代码中直接使用。
void 用在函数定义中可以表示函数没有返回值或者没有形式参数,用在这里表示指针指向的数据的类型是未知的。
void *表示一个有效指针,它确实指向实实在在的数据,只是数据的类型尚未确定,在后续使用过程中一般要进行强制类型转换
C语言动态内存分配函数 malloc() 的返回值就是void *类型,在使用时要进行强制类型转换,请看下面的例子:
#include
int main(){
//分配可以保存30个字符的内存,并把返回的指针转换为 char *
char *str = (char *)malloc(sizeof(char) * 30);
gets(str);
printf("%s\n", str);
return 0;
}
C语言标准规定,当数组名作为数组定义的标识符(也就是定义或声明数组时)、sizeof 或 & 的操作数时,它才表示整个数组本身,在其他的表达式中,数组名会被转换为指向第 0 个元素的指针(地址)。
C语言标准还规定,数组下标与指针的偏移量相同。通俗地理解,就是对数组下标的引用总是可以写成“一个指向数组的起始地址的指针加上偏移量”。
C语言标准规定,作为“类型的数组”的形参应该调整为“类型的指针”。在函数形参定义这个特殊情况下,编译器必须把数组形式改写成指向数组第 0 个元素的指针形式。编译器只向函数传递数组的地址,而不是整个数组的拷贝。
如果一个数组中的所有元素保存的都是指针,那么我们就称它为指针数组。指针数组的定义形式一般为:
dataType *arrayName[length];
[ ]的优先级高于*,该定义形式应该理解为
//括号里面说明arrayName是一个数组,包含了length个元素,括号外面说明每个元素的类型为dataType *。
dataType *(arrayName[length]);
int *arr[3] = {&a, &b, &c};//也可以不指定长度,直接写作 int *arr[]
指针数组还可以和字符串数组结合使用:
第一种写法:
#include
int main(){
char *str[3] = {
"c.biancheng.net",
"C语言中文网",
"C Language"
};
printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
return 0;
}
第二种写法更加明了:
#include
int main(){
char *str0 = "c.biancheng.net";
char *str1 = "C语言中文网";
char *str2 = "C Language";
char *str[3] = {
str0, str1, str2};
printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
return 0;
}
#include
int main(){
char *lines[5] = {
"COSC1283/1284",
"Programming",
"Techniques",
"is",
"great fun"
};
char *str1 = lines[1];
char *str2 = *(lines + 3);
char c1 = *(*(lines + 4) + 6);
char c2 = (*lines + 5)[5]; // 这里会比较有意思,这里其实是要理解数组在什么时候被视为指针,什么时候视为数组变量
char c3 = *lines[0] + 2;
printf("str1 = %s\n", str1);
printf("str2 = %s\n", str2);
printf(" c1 = %c\n", c1);
printf(" c2 = %c\n", c2);
printf(" c3 = %c\n", c3);
return 0;
}
定义一个指向二维数组 a 的指针变量 p:
int (*p)[4] = a;
括号中的*表明 p 是一个指针,它指向一个数组,数组的类型为int [4],这正是 a 所包含的每个一维数组的类型。
[ ]的优先级高于*,( )是必须要加的,如果赤裸裸地写作int *p[4],那么应该理解为int *(p[4]),p 就成了一个指针数组,而不是二维数组指针
*(p+1)+1表示第 1 行第 1 个元素的地址。如何理解呢:
1) *(p+1)单独使用时表示的是第 1 行数据,放在表达式中会被转换为第 1 行数据的首地址,也就是第 1 行第 0 个元素的地址,因为使用整行数据没有实际的含义,编译器遇到这种情况都会转换为指向该行第 0 个元素的指针;就像一维数组的名字,在定义时或者和 sizeof、& 一起使用时才表示整个数组,出现在表达式中就会被转换为指向数组第 0 个元素的指针。
2) *(*(p+1)+1)表示第 1 行第 1 个元素的值。很明显,增加一个 * 表示取地址上的数据。
一个函数总是占用一段连续的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址,这和数组名非常类似。我们可以把函数的这个首地址(或称入口地址)赋予一个指针变量,使指针变量指向函数所在的内存区域,然后通过指针变量就可以找到并调用该函数。这种指针就是函数指针。
函数指针的定义形式为:
returnType (*pointerName)(param list);
returnType 为函数返回值类型,pointerName 为指针名称,param list 为函数参数列表。参数列表中可以同时给出参数的类型和名称,也可以只给出参数的类型,省略参数的名称,这一点和函数原型非常类似。
注意( )的优先级高于*,第一个括号不能省略,如果写作returnType *pointerName(param list);就成了函数原型,它表明函数的返回值类型为returnType *。
示例:
#include
//返回两个数中较大的一个
int max(int a, int b){
return a>b ? a : b;
}
int main(){
int x, y, maxval;
//定义函数指针
int (*pmax)(int, int) = max; //也可以写作int (*pmax)(int a, int b)
printf("Input two numbers:");
scanf("%d %d", &x, &y);
// pmax 是一个函数指针,在前面加 * 就表示对它指向的函数进行调用。注意( )的优先级高于*,第一个括号不能省略。
maxval = (*pmax)(x, y);
printf("Max value: %d\n", maxval);
return 0;
}
规则:
1)C语言标准规定,对于一个符号的定义,编译器总是从它的名字开始读取,然后按照优先级顺序依次解析。对,从名字开始,不是从开头也不是从末尾,这是理解复杂指针的关键!
符号优先级,从高到低:
1)定义中被括号( )括起来的那部分。
2)后缀操作符:括号( )表示这是一个函数,方括号[ ]表示这是一个数组。
3)前缀操作符:星号*表示“指向xxx的指针”。
1) int *p1[6];
p1 是一个拥有 6 个 int * 元素的数组,也即指针数组。
2) int (*p3)[6];
p3 是一个指向拥有 6 个 int 元素数组的指针,也即二维数组指针
3) int (*p4)(int, int);
p4 是一个指向原型为int func(int, int);的函数的指针。
4) char *(* c[10])(int **p);
c 是一个拥有 10 个元素的指针数组,每个指针指向一个原型为char *func(int **p);的函数。
5) int (*(*(*pfunc)(int *))[5])(int *);
pfunc 是一个函数指针,该函数的返回值是一个指针,它指向一个指针数组,指针数组中的指针指向原型为int func(int *);的函数。
#include
int main(int argc, char *argv[]){
int i;
printf("The program receives %d parameters:\n", argc);
for(i=0; i<argc; i++){
printf("%s\n", argv[i]);
}
return 0;
}
定 义 | 含 义 |
---|---|
int *p; | p 可以指向 int 类型的数据,也可以指向类似 int arr[n] 的数组 |
int **p; | p 为二级指针,指向 int * 类型的数据。 |
int *p[n]; | p 为指针数组。[ ] 的优先级高于 *,所以应该理解为 int *(p[n]); |
int (*p)[n]; | p 为二维数组指针。 |
int *p(); | p 是一个函数,它的返回值类型为 int *。 |
int (*p)(); | p 是一个函数指针,指向原型为 int func() 的函数。 |
指针变量可以进行加减运算,例如p++、p+i、p-=i。指针变量的加减运算并不是简单的加上或减去一个整数,而是跟指针指向的数据类型有关。
给指针变量赋值时,要将一份数据的地址赋给它,不能直接赋给一个整数,例如int *p = 1000;是没有意义的,使用过程中一般会导致程序崩溃。
使用指针变量之前一定要初始化,否则就不能确定指针指向哪里,如果它指向的内存没有使用权限,程序就崩溃了。对于暂时没有指向的指针,建议赋值NULL。
两个指针变量可以相减。如果两个指针变量指向同一个数组中的某个元素,那么相减的结果就是两个指针之间相差的元素个数。
数组也是有类型的,数组名的本意是表示一组类型相同的数据。在定义数组时,或者和 sizeof、& 运算符一起使用时数组名才表示整个数组,表达式中的数组名会被转换为一个指向数组的指针。
在C语言中,可以使用结构体(Struct)来存放一组不同类型的数据。结构体的定义形式为:
struct 结构体名{
结构体所包含的变量或数组
};
代码示例:
struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在学习小组
float score; //成绩
};
结构体成员的定义方式与变量和数组的定义方式相同,只是不能初始化。
注意大括号后面的分号;不能少,这是一条完整的语句。
变量定义:
struct stu stu1, stu2; (定义了两个变量 stu1 和 stu2,它们都是 stu 类型,都由 5 个成员组成。注意关键字struct不能少。)
定义结构体的同时定义结构体变量:
struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在学习小组
float score; //成绩
} stu1, stu2; // 将变量放在结构体定义的最后即可。
如果只需要 stu1、stu2 两个变量,后面不需要再使用结构体名定义其他变量,那么在定义时也可以不给出结构体名,如下所示:
struct{
//没有写 stu
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在学习小组
float score; //成绩
} stu1, stu2;
结构体使用点号.获取单个成员。获取结构体成员的一般格式为:
结构体变量名.成员名;
通过这种方式可以获取成员的值,也可以给成员赋值:
#include
int main(){
struct{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1;
//给结构体成员赋值
stu1.name = "Tom";
stu1.num = 12;
stu1.age = 18;
stu1.group = 'A';
stu1.score = 136.5;
//读取结构体成员的值
printf("%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f!\n", stu1.name, stu1.num, stu1.age, stu1.group, stu1.score);
return 0;
}
除了可以对成员进行逐一赋值,也可以在定义时整体赋值(整体赋值仅限于定义结构体变量的时候,在使用过程中只能对成员逐一赋值),例如:
struct{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1, stu2 = {
"Tom", 12, 18, 'A', 136.5 };
// 此处需要注意,上面的代码中其实只给stu2整体赋值了,如果想要给stu1 stu2 都赋值
struct{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1 = {
"To00m", 121, 181, 'b', 1360.5 }, stu2 = {
"Tom", 12, 18, 'A', 136.5 };
结构体是一种自定义的数据类型,是创建变量的模板,不占用内存空间;结构体变量才包含了实实在在的数据,需要内存空间来存储。
定义结构体数组:
第一种,在定义结构体同时定义数组:
struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
}class[5];
struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
}class[5] = {
{
"Li ping", 5, 18, 'C', 145.0},
{
"Zhang ping", 4, 19, 'A', 130.5},
{
"He fang", 1, 18, 'A', 148.5},
{
"Cheng ling", 2, 17, 'F', 139.0},
{
"Wang ming", 3, 17, 'B', 144.5}
};
第二种定义分开:
struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
};
struct stu s[5];
C语言结构体指针的定义形式一般为:
struct 结构体名 *变量名;
定义结构体指针的实例:
//结构体
struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1 = {
"Tom", 12, 18, 'A', 136.5 };
//结构体指针
struct stu *pstu = &stu1;
也可以在定义结构体的同时定义结构体指针:
struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1 = {
"Tom", 12, 18, 'A', 136.5 }, *pstu = &stu1;
注意,结构体变量名和数组名不同,数组名在表达式中会被转换为数组指针,而结构体变量名不会,无论在任何表达式中它表示的都是整个集合本身,要想取得结构体变量的地址,必须在前面加&,所以给 pstu 赋值只能写作:struct stu *pstu = &stu1;
结构体和结构体变量是两个不同的概念:结构体是一种数据类型,是一种创建变量的模板,编译器不会为它分配内存空间,就像 int、float、char 这些关键字本身不占用内存一样;结构体变量才包含实实在在的数据,才需要内存来存储。
通过结构体指针可以获取结构体成员,一般形式为:
(pointer).memberName // 第一种写法中,.的优先级高于,(*pointer)两边的括号不能少。
或者:
pointer->memberName // 第二种写法中,->是一个新的运算符,习惯称它为“箭头”,有了它,可以通过结构体指针直接取得结构体成员;这也是->在C语言中的唯一用途。
结构体变量名代表的是整个集合本身,作为函数参数时传递的整个集合,也就是所有成员,而不是像数组一样被编译器转换成一个指针。如果结构体成员较多,尤其是成员为数组时,传送的时间和空间开销会很大,影响程序的运行效率。所以最好的办法就是使用结构体指针,这时由实参传向形参的只是一个地址,非常快速。
struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
}stus[] = {
{
"Li ping", 5, 18, 'C', 145.0},
{
"Zhang ping", 4, 19, 'A', 130.5},
{
"He fang", 1, 18, 'A', 148.5},
{
"Cheng ling", 2, 17, 'F', 139.0},
{
"Wang ming", 3, 17, 'B', 144.5}
};
void average(struct stu *ps, int len);
int main(){
int len = sizeof(stus) / sizeof(struct stu);
average(stus, len);
return 0;
}
枚举类型的定义形式为:
enum typeName{
valueName1, valueName2, valueName3, ...... };
列出一个星期有几天:
enum week{
Mon, Tues, Wed, Thurs, Fri, Sat, Sun };
枚举值默认从 0 开始,往后逐个加 1(递增);也就是说,week 中的 Mon、Tues … Sun 对应的值分别为 0、1 … 6。
给每个名字都指定一个值:
enum week{
Mon = 1, Tues = 2, Wed = 3, Thurs = 4, Fri = 5, Sat = 6, Sun = 7 };
// 这种有规则递增的数据,还可以这么写:
enum week{
Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun };
// 无规则的枚举
enum week{
Mon = 1, Tues = 2, Wed = 3, Thurs = 5, Fri = 5, Sat = 6, Sun = 3 };
枚举是一种类型,通过它可以定义枚举变量:
// 第一种,在定义枚举类型的同时定义变量:a/b/c 的类型就是 enum week
enum week{
Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } a, b, c;
a = Mon;
或者
enum week{
Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } a = Mon, b = Wed, c = Sat;
或者:
enum week{
Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun };
enum week a = Mon, b = Wed, c = Sat;
// 第二种,单独定义枚举变量
enum week{
Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun };
enum week a, b, c;
a = Mon;
需要注意的两点是:
枚举列表中的 Mon、Tues、Wed 这些标识符的作用范围是全局的,不能再定义与它们名字相同的变量。
Mon、Tues、Wed 等都是常量,不能对它们赋值,只能将它们的值赋给其他的变量。
枚举和宏其实非常类似:宏在预处理阶段将名字替换成对应的值,枚举在编译阶段将名字替换成对应的值。我们可以将枚举理解为编译阶段的宏。
知识点补充&回忆:
Mon、Tues、Wed 这些名字都被替换成了对应的数字。这意味着,Mon、Tues、Wed 等都不是变量,它们不占用数据区(常量区、全局数据区、栈区和堆区)的内存,而是直接被编译到命令里面,放到代码区,所以不能用&取得它们的地址。这就是枚举的本质。
case 关键字后面必须是一个整数,或者是结果为整数的表达式,但不能包含任何变量,正是由于 Mon、Tues、Wed 这些名字最终会被替换成一个整数,所以它们才能放在 case 后面。