变量(variable)可以理解成一块内存区域的名字。通过变量名,可以引用这块内存区域,获取里面存储的值。由于值可能发生变化,所以称为变量,否则就是常量了。
变量名在 C 语言里面属于标识符(identifier),命名有严格的规范。
_
)组成。
下面是一些无效变量名的例子。
上面示例中,每一行的变量名都是无效的。
变量名区分大小写,star
、Star
、STAR
都是不同的变量。
并非所有的词都能用作变量名,有些词在 C 语言里面有特殊含义(比如int
),另一些词是命令(比如continue
),它们都称为关键字,不能用作变量名。另外,C 语言还保留了一些词,供未来使用,这些保留字也不能用作变量名。下面就是 C 语言主要的关键字和保留字。
另外,两个下划线开头的变量名,以及一个下划线 + 大写英文字母开头的变量名,都是系统保留的,自己不应该起这样的变量名。
C 语言的变量,必须先声明后使用。如果一个变量没有声明,就直接使用,会报错。
每个变量都有自己的类型(type)。声明变量时,必须把变量的类型告诉编译器。
上面代码声明了变量height
,并且指定类型为int
(整数)。
如果几个变量具有相同类型,可以在同一行声明。
注意,声明变量的语句必须以分号结尾。
一旦声明,变量的类型就不能在运行时修改。
C 语言会在变量声明时,就为它分配内存空间,但是不会清除内存里面原来的值。这导致声明变量以后,变量会是一个随机的值。所以,变量一定要赋值以后才能使用。
赋值操作通过赋值运算符(=
)完成。
上面示例中,第一行声明了一个整数变量num
,第二行给这个变量赋值。
变量的值应该与类型一致,不应该赋予不是同一个类型的值,比如num
的类型是整数,就不应该赋值为小数。虽然 C 语言会自动转换类型,但是应该避免赋值运算符两侧的类型不一致。
变量的声明和赋值,也可以写在一行。
多个相同类型变量的赋值,可以写在同一行。
注意,赋值表达式有返回值,等于等号右边的值。
上面代码中,变量y
的值就是赋值表达式(x = 2 * x
)的返回值2
。
由于赋值表达式有返回值,所以 C 语言可以写出多重赋值表达式。
上面的代码是合法代码,一次为多个变量赋值。赋值运算符是从右到左执行,所以先为n
赋值,然后依次为m
、z
、y
和x
赋值。
C 语言有左值(left value)和右值(right value)的概念。左值是可以放在赋值运算符左边的值,一般是变量;右值是可以放在赋值运算符右边的值,一般是一个具体的值。这是为了强调有些值不能放在赋值运算符的左边,比如x = 1
是合法的表达式,但是1 = x
就会报错。
作用域(scope)指的是变量生效的范围。C 语言的变量作用域主要有两种:文件作用域(file scope)和块作用域(block scope)。
文件作用域(file scope)指的是,在源码文件顶层声明的变量,从声明的位置到文件结束都有效。
上面示例中,变量x
是在文件顶层声明的,从声明位置开始的整个当前文件都是它的作用域,可以在这个范围的任何地方读取这个变量,比如函数main()
内部就可以读取这个变量。
块作用域(block scope)指的是由大括号({}
)组成的代码块,它形成一个单独的作用域。凡是在块作用域里面声明的变量,只在当前代码块有效,代码块外部不可见。
上面例子中,变量b
是在if
代码块里面声明的,所以对于大括号外面的代码,这个变量是不存在的。
代码块可以嵌套,即代码块内部还有代码块,这时就形成了多层的块作用域。它的规则是:内层代码块可以使用外层声明的变量,但外层不可以使用内层声明的变量。如果内层的变量与外层同名,那么会在当前作用域覆盖外层变量。
上面示例中,内层和外层都有一个变量i
,每个作用域都会优先使用当前作用域声明的i
。
最常见的块作用域就是函数,函数内部声明的变量,对于函数外部是不可见的。for
循环也是一个块作用域,循环变量只对循环体内部可见,外部是不可见的。
上面示例中,for
循环省略了大括号,但依然是一个块作用域,在外部读取循环变量i
,编译器就会报错。
C 语言的每一种数据,都是有类型(type)的,编译器必须知道数据的类型,才能操作数据。所谓“类型”,就是相似的数据所拥有的共同特征,那么一旦知道某个值的数据类型,就能知道该值的特征和操作方式。
基本数据类型有三种:字符(char)、整数(int)和浮点数(float)。复杂的类型都是基于它们构建的。
字符类型指的是单个字符,类型声明使用char
关键字。
上面示例声明了变量c
是字符类型,并将其赋值为字母B
。
C 语言规定,字符常量必须放在单引号里面。
在计算机内部,字符类型使用一个字节(8位)存储。C 语言将其当作整数处理,所以字符类型就是宽度为一个字节的整数。每个字符对应一个整数(由 ASCII 码确定),比如B
对应整数66
。
字符类型在不同计算机的默认范围是不一样的。一些系统默认为-128
到127
,另一些系统默认为0
到255
。这两种范围正好都能覆盖0
到127
的 ASCII 字符范围。
只要在字符类型的范围之内,整数与字符是可以互换的,都可以赋值给字符类型的变量。
上面示例中,变量c
是字符类型,赋给它的值是整数66。这跟赋值为字符B
的效果是一样的。
两个字符类型的变量可以进行数学运算。
上面示例中,字符类型变量a
和b
相加,视同两个整数相加。占位符%d
表示输出十进制整数,因此输出结果为133。
单引号本身也是一个字符,如果要表示这个字符常量,必须使用反斜杠转义。
上面示例中,变量t
为单引号字符,由于字符常量必须放在单引号里面,所以内部的单引号要使用反斜杠转义。
这种转义的写法,主要用来表示 ASCII 码定义的一些无法打印的控制字符,它们也属于字符类型的值。
\a
:警报,这会使得终端发出警报声或出现闪烁,或者两者同时发生。
\b
:退格键,光标回退一个字符,但不删除字符。
\f
:换页符,光标移到下一页。在现代系统上,这已经反映不出来了,行为改成类似于\v
。
\r
:回车符,光标移到同一行的开头。
\t
:制表符,光标移到下一个水平制表位,通常是下一个8的倍数。
\v
:垂直分隔符,光标移到下一个垂直制表位,通常是下一行的同一列。
\0
:null 字符,代表没有内容。注意,这个值不等于数字0。
转义写法还能使用八进制和十六进制表示一个字符。
\nn
:字符的八进制写法,nn
为八进制值。
\xnn
:字符的十六进制写法,nn
为十六进制值。
上面示例的四种写法都是等价的。
整数类型用来表示较大的整数,类型声明使用int
关键字。
上面示例声明了一个整数变量a
。
不同计算机的int
类型的大小是不一样的。比较常见的是使用4个字节(32位)存储一个int
类型的值,但是2个字节(16位)或8个字节(64位)也有可能使用。它们可以表示的整数范围如下。
C 语言使用signed
关键字,表示一个类型带有正负号,包含负值;使用unsigned
关键字,表示该类型不带有正负号,只能表示零和正整数。
对于int
类型,默认是带有正负号的,也就是说int
等同于signed int
。由于这是默认情况,关键字signed
一般都省略不写,但是写了也不算错。
int
类型也可以不带正负号,只表示非负整数。这时就必须使用关键字unsigned
声明变量。
整数变量声明为unsigned
的好处是,同样长度的内存能够表示的最大整数值,增大了一倍。比如,16位的signed int
最大值为32,767,而unsigned int
的最大值增大到了65,535。
unsigned int
里面的int
可以省略,所以上面的变量声明也可以写成下面这样。
注意,C 语言规定char
类型默认是否带有正负号,由当前系统决定。这就是说,char
不等同于signed char
,它有可能是signed char
,也有可能是unsigned
如果
int类型使用4个或8个字节表示一个整数,对于小整数,这样做很浪费空间。另一方面,某些场合需要更大的整数,8个字节还不够。为了解决这些问题,C 语言在int
类型之外,又提供了三个整数的子类型。这样有利于更精细地限定整数变量的范围,也有利于更好地表达代码的意图。
long int
(简写为long
):占用空间不少于int
,至少为4个字节。
上面代码分别声明了三种整数子类型的变量。
默认情况下,short
、long
、long long
都是带符号的(signed),即signed
关键字省略了。它们也可以声明为不带符号(unsigned),使得能够表示的最大值扩大一倍。
C 语言允许省略int
,所以变量声明语句也可以写成下面这样。
不同的计算机,数据类型的字节长度是不一样的。确实需要32位整数时,应使用long
类型而不是int
类型,可以确保不少于4个字节;确实需要64位的整数时,应该使用long
long
类型,可以确保不少于8个字节。另一方面,为了节省空间,只需要16位整数时,应使用short
类型;需要8位整数时,应该使用char
类型。
有时候需要查看,当前系统不同整数类型的最大值和最小值,C 语言的头文件limits.h
提供了相应的常量,比如SCHAR_MIN
代表 signed char 类型的最小值-128
,SCHAR_MAX
代表 signed char 类型的最大值127
。
为了代码的可移植性,需要知道某种整数类型的极限值时,应该尽量使用这些常量。
C 语言的整数默认都是十进制数,如果要表示八进制数和十六进制数,必须使用专门的表示法。
八进制使用0
作为前缀,比如017
、0377
。
十六进制使用0x
或0X
作为前缀,比如0xf
、0X10
。
有些编译器使用0b
前缀,表示二进制数,但不是标准。
注意,不同的进制只是整数的书写方法,不会对整数的实际存储方式产生影响。所有整数都是二进制形式存储,跟书写方式无关。不同进制可以混合使用,比如10 + 015 + 0x20
是一个合法的表达式。
printf()
的进制相关占位符如下。
%#o
:显示前缀0
的八进制整数。
%#x
:显示前缀0x
的十六进制整数。
%#X
:显示前缀0X
的十六进制整数。
任何有小数点的数值,都会被编译器解释为浮点数。所谓“浮点数”就是使用 m * be 的形式,存储一个数值,m
是小数部分,b
是基数(通常是2
),e
是指数部分。这种形式是精度和数值范围的一种结合,可以表示非常大或者非常小的数。
浮点数的类型声明使用float
关键字,可以用来声明浮点数变量。
上面示例中,变量c
的就是浮点数类型。
float
类型占用4个字节(32位),其中8位存放指数的值和符号,剩下24位存放小数的值和符号。float
类型至少能够提供(十进制的)6位有效数字,指数部分的范围为(十进制的)-37
到37
,即数值范围为10-37到1037。
有时候,32位浮点数提供的精度或者数值范围还不够,C 语言又提供了另外两种更大的浮点数类型。
double
:占用8个字节(64位),至少提供13位有效数字。
注意,由于存在精度限制,浮点数只是一个近似值,它的计算是不精确的,比如 C 语言里面0.1 + 0.2
并不等于0.3
,而是有一个很小的误差。
C 语言允许使用科学计数法表示浮点数,使用字母e
来分隔小数部分和指数部分。
上面示例中,e
后面如果是加号+
,加号可以省略。注意,科学计数法里面e
的前后,不能存在空格。
另外,科学计数法的小数部分如果是0.x
或x.0
的形式,那么0
可以省略。
C 语言原来并没有为布尔值单独设置一个类型,而是使用整数0
表示伪,所有非零值表示真。
上面示例中,变量x
等于1
,C 语言就认为这个值代表真,从而会执行判断体内部的代码。
C99 标准添加了类型_Bool
,表示布尔值。但是,这个类型其实只是整数类型的别名,还是使用0
表示伪,1
表示真,下面是一个示例。
头文件stdbool.h
定义了另一个类型别名bool
,并且定义了true
代表1
、false
代表0
。只要加载这个头文件,就可以使用这几个关键字。
上面示例中,加载头文件stdbool.h
以后,就可以使用bool
定义布尔值类型,以及false
和true
表示真伪。
字面量(literal)指的是代码里面直接出现的值。
上面代码中,x
是变量,123
就是字面量。
编译时,字面量也会写入内存,因此编译器必须为字面量指定数据类型,就像必须为变量指定数据类型一样。
一般情况下,十进制整数字面量(比如123
)会被编译器指定为int
类型。如果一个数值比较大,超出了int
能够表示的范围,编译器会将其指定为long int
。如果数值超过了long int
,会被指定为unsigned
小数(比如
3.14)会被指定为double
类型。
有时候,程序员希望为字面量指定一个不同的类型。比如,编译器将一个整数字面量指定为int
类型,但是程序员希望将其指定为long
类型,这时可以为该字面量加上后缀l
或L
,编译器就知道要把这个字面量的类型指定为long
。
上面代码中,字面量123
有后缀L
,编译器就会将其指定为long
类型。这里123L
写成123l
,效果也是一样的,但是建议优先使用L
,因为小写的l
容易跟数字1
混淆。
八进制和十六进制的值,也可以使用后缀l
和L
指定为 Long 类型,比如020L
和0x20L
。
如果希望指定为无符号整数unsigned int
,可以使用后缀u
或U
。
L
和U
可以结合使用,表示unsigned long
类型。L
和U
的大小写和组合顺序无所谓。
对于浮点数,编译器默认指定为 double 类型,如果希望指定为其他类型,需要在小数后面添加后缀f
(float)或l
(long double)。
科学计数法也可以使用后缀。
总结一下,常用的字面量后缀有下面这些。
u
还可以与其他整数后缀结合,放在前面或后面都可以,比如10UL
、10ULL
和10LLU
都是合法的。
每一种数据类型都有数值范围,如果存放的数值超出了这个范围(小于最小值或大于最大值),需要更多的二进制位存储,就会发生溢出。大于最大值,叫做向上溢出(overflow);小于最小值,叫做向下溢出(underflow)。
一般来说,编译器不会对溢出报错,会正常执行代码,但是会忽略多出来的二进制位,只保留剩下的位,这样往往会得到意想不到的结果。所以,应该避免溢出。
上面示例中,变量x
加1
,得到的结果不是256
,而是0
。因为x
是unsign
char
类型,最大值是255
(二进制),加1
后就发生了溢出,256
(二进制)的最高位1
被丢弃,剩下的值就是0
。
上面示例中,常量UINT_MAX
是 unsigned int 类型的最大值。如果加1
,对于该类型就会溢出,从而得到0
;而0
是该类型的最小值,再减1
,又会得到UINT_MAX
。
溢出很容易被忽视,编译器又不会报错,所以必须非常小心。
上面代码表面看似乎没有问题,但是循环变量i
的类型是 unsigned int,这个类型的最小值是0
,不可能得到小于0的结果。当i
等于0,再减去1
的时候,并不会返回-1
,而是返回 unsigned int
的类型最大值,这个值总是大于等于0
,导致无限循环。
为了避免溢出,最好方法就是将运算结果与类型的极限值进行比较。
上面示例中,变量sum
和ui
都是 unsigned int 类型,它们相加的和还是 unsigned int 类型,这就有可能发生溢出。但是,不能通过相加的和是否超出了最大值UINT_MAX
,来判断是否发生了溢出,因为sum +
ui
总是返回溢出后的结果,不可能大于UINT_MAX
。正确的比较方法是,判断UINT_MAX - sum
与ui
之间的大小关系。
下面是另一种错误的写法。
上面示例的运算结果,会输出positive
。原因是变量i
和j
都是 unsigned int 类型,i - j
的结果也是这个类型,最小值为0
,不可能得到小于0
的结果。正确的写法是写成下面这样。
sizeof
是 C 语言提供的一个运算符,返回某种数据类型或某个值占用的字节数量。它的参数可以是数据类型的关键字,也可以是变量名或某个具体的值。
上面的第一个示例,返回得到int
类型占用的字节数量(通常是4
或8
)。第二个示例返回整数变量占用字节数量,结果与前一个示例完全一样。第三个示例返回浮点数3.14
占用的字节数量,由于浮点数的字面量一律存储为 double 类型,所以会返回8
,因为 double
sizeof
运算符的返回值,C 语言只规定是无符号整数,并没有规定具体的类型,而是留给系统自己去决定,sizeof
到底返回什么类型。不同的系统中,返回值的类型有可能是unsigned int
,也有可能是unsigned long
,甚至是unsigned long
long
,对应的printf()
占位符分别是%u
、%lu
和%llu
。这样不利于程序的可移植性。
C 语言提供了一个解决方法,创造了一个类型别名size_t
,用来统一表示sizeof
的返回值类型。该别名定义在stddef.h
头文件(引入stdio.h
时会自动引入)里面,对应当前系统的sizeof
的返回值类型,可能是unsigned
C 语言还提供了一个常量
SIZE_MAX,表示size_t
可以表示的最大整数。所以,size_t
能够表示的整数范围为[0, SIZE_MAX]
。
上面代码中,不管sizeof
返回值的类型是什么,%zd
占位符(或%zu
)都可以正确输出。
某些情况下,C 语言会自动转换某个值的类型。
赋值运算符会自动将右边的值,转成左边变量的类型。
(1)浮点数赋值给整数变量
浮点数赋予整数变量时,C 语言直接丢弃小数部分,而不是四舍五入。
上面示例中,变量x
是整数类型,赋给它的值是一个浮点数。编译器会自动把3.14
先转为int
类型,丢弃小数部分,再赋值给x
,因此x
的值是3
。
这种自动转换会导致部分数据的丢失(3.14
丢失了小数部分),所以最好不要跨类型赋值,尽量保证变量与所要赋予的值是同一个类型。
注意,舍弃小数部分时,不是四舍五入,而是整个舍弃。
上面示例中,x
等于12
,而不是四舍五入的13
。
(2)整数赋值给浮点数变量
整数赋值给浮点数变量时,会自动转为浮点数。
上面示例中,变量y
的值不是24
,而是24.0
,因为等号右边的整数自动转为了浮点数。
(3)窄类型赋值给宽类型
字节宽度较小的整数类型,赋值给字节宽度较大的整数变量时,会发生类型提升,即窄类型自动转为宽类型。
比如,char
或short
类型赋值给int
类型,会自动提升为int
。
上面示例中,变量x
的类型是char
,由于赋值给int
类型,所以会自动提升为int
。
(4)宽类型赋值给窄类型
字节宽度较大的类型,赋值给字节宽度较小的变量时,会发生类型降级,自动转为后者的类型。这时可能会发生截值(truncation),系统会自动截去多余的二进制位,导致难以预料的结果。
上面例子中,变量ch
是char
类型,宽度是8个二进制位。变量i
是int
类型,将i
赋值给ch
,后者只能容纳i
(二进制形式为,共9位)的后八位,前面多出来的二进制位被丢弃,保留后八位就变成了(十进制的65,相当于字符A
)。
浮点数赋值给整数类型的值,也会发生截值,浮点数的小数部分会被截去。
上面示例中,i
等于3
,pi
的小数部分被截去了。
不同类型的值进行混合计算时,必须先转成同一个类型,才能进行计算。转换规则如下:
(1)整数与浮点数混合运算时,整数转为浮点数类型,与另一个运算数类型相同。
上面示例是int
类型与float
类型的混合计算,int
类型的3
会先转成float
的3.0
,再进行计算,得到4.2
。
(2)不同的浮点数类型混合运算时,宽度较小的类型转为宽度较大的类型,比如float
转为double
,double
转为long double
。
(3)不同的整数类型混合运算时,宽度较小的类型会提升为宽度较大的类型。比如short
转为int
,int
转为long
等,有时还会将带符号的类型signed
转为无符号unsigned
。
下面例子的执行结果,可能会出人意料。
int,所以a
会自动转成无符号整数(转换规则是-5
加上无符号整数的最大值,再加1),导致比较失败,do_something()
不会执行。
所以,最好避免无符号整数与有符号整数的混合运算。因为这时 C 语言会自动将signed int
转为unsigned int
,可能不会得到预期的结果。
两个相同类型的整数运算时,或者单个整数的运算,一般来说,运算结果也属于同一类型。但是有一个例外,宽度小于int
的类型,运算结果会自动提升为int
。
函数的参数和返回值,会自动转成函数定义里指定的类型。
上面示例中,参数变量m
和n
不管原来的类型是什么,都会转成函数dostuff()
定义的参数类型。
下面是返回值自动转换类型的例子。
上面示例中,函数内部的变量a
是int
类型,但是返回的值是char
类型,因为函数定义中返回的是这个类型。
原则上,应该避免类型的自动转换,防止出现意料之外的结果。C 语言提供了类型的显式转换,允许手动转换类型。
只要在一个值或变量的前面,使用圆括号指定类型(type)
,就可以将这个值或变量转为指定的类型,这叫做“类型指定”(casting)。
上面示例将变量ch
转成无符号的字符类型。
上面示例中,(long int)
将10
显式转为long int
类型。这里的显示转换其实是不必要的,因为赋值运算符会自动将右边的值,转为左边变量的类型。
C 语言的整数类型(short、int、long)在不同计算机上,占用的字节宽度可能是不一样的,无法提前知道它们到底占用多少个字节。
程序员有时控制准确的字节宽度,这样的话,代码可以有更好的可移植性,头文件stdint.h
创造了一些新的类型别名。
int8_t
:8位有符号整数。
上面这些都是类型别名,编译器会指定它们指向的底层类型。比如,某个系统中,如果int
类型为32位,int32_t
就会指向int
;如果long
类型为32位,int32_t
则会指向long
。
上面示例中,变量x32
声明为int32_t
类型,可以保证是32位的宽度。
(2)最小宽度类型(minimum width type),保证某个整数类型的最小长度。
上面这些类型,可以保证占据的字节不少于指定宽度。比如,int_least8_t
表示可以容纳8位有符号整数的最小宽度的类型。
(3)最快的最小宽度类型(fast minimum width type),可以使整数计算达到最快的类型。
上面这些类型是保证字节宽度的同时,追求最快的运算速度,比如int_fast8_t
表示对于8位有符号整数,运算速度最快的类型。这是因为某些机器对于特定宽度的数据,运算速度最快,举例来说,32位计算机对于32位数据的运算速度,会快于16位数据。
(4)可以保存指针的整数类型。
intptr_t
:可以存储指针(内存地址)的有符号整数类型。
uintptr_t
:可以存储指针的无符号整数类型。
(5)最大宽度整数类型,用于存放最大的整数。
intmax_t
:可以存储任何有效的有符号整数的类型。
uintmax_t
:可以存放任何有效的无符号整数的类型。