02.变量和数据类型
目录介绍
- 2.1 基本数据
- 2.1.1 基本数据类型
- 2.1.2 字符类型char
- 2.1.3 整数类型
- 2.1.4 浮点型
- 2.1.5 布尔类型
- 2.2 变量
- 2.3.1 变量名
- 2.2.2 变量的声明
- 2.2.3 变量的赋值
- 2.2.4 变量的作用域
- 2.3 常量
- 2.3.1 const常量
- 2.3.2 static常量
- 2.4 枚举和派生类型
- 2.4.1 枚举定义
- 2.4.2 枚举别名
- 2.4.3 枚举常量
- 2.5 字符串类型
- 2.5.1 字符串介绍
- 2.5.2 字符串声明
- 2.5.3 字符串长度
- 2.5.4 字符串复制
- 2.5.5 字符串部分复制
- 2.5.6 字符串连接
- 2.5.7 字符串比较
- 2.5.8 字符串输入
- 2.5.9 字符串数组
- 2.6 字面量
- 2.6.1 字面量类型
- 2.6.2 字面量后缀
- 2.7 数据溢出
- 2.8 类型自动转换
- 2.8.1 赋值运算
- 2.8.2 混合类型运算
- 2.8.3 整数类型运算
- 2.8.4 函数运算
- 2.9 类型显式转换
- 2.10 可移植类型
2.1 基本数据
2.1.1 基本数据类型
C 语言的每一种数据,都是有类型(type)的,编译器必须知道数据的类型,才能操作数据。所谓“类型”,就是相似的数据所拥有的共同特征,那么一旦知道某个值的数据类型,就能知道该值的特征和操作方式。
基本数据类型有三种:字符(char)、整数(int)和浮点数(float)。复杂的类型都是基于它们构建的。
2.1.2 字符类型char
字符类型指的是单个字符,类型声明使用char
关键字。
char c = 'B';
上面示例声明了变量c
是字符类型,并将其赋值为字母B
。
C 语言规定,字符常量必须放在单引号里面。
在计算机内部,字符类型使用一个字节(8位)存储。C 语言将其当作整数处理,所以字符类型就是宽度为一个字节的整数。每个字符对应一个整数(由 ASCII 码确定),比如B
对应整数66
。
字符类型在不同计算机的默认范围是不一样的。一些系统默认为-128
到127
,另一些系统默认为0
到255
。这两种范围正好都能覆盖0
到127
的 ASCII 字符范围。
只要在字符类型的范围之内,整数与字符是可以互换的,都可以赋值给字符类型的变量。
char c = 66;
// 等同于
char c = 'B';
上面示例中,变量c
是字符类型,赋给它的值是整数66。这跟赋值为字符B
的效果是一样的。
两个字符类型的变量可以进行数学运算。
char a = 'B'; // 等同于 char a = 66;
char b = 'C'; // 等同于 char b = 67;
printf("%d\n", a + b); // 输出 133
上面示例中,字符类型变量a
和b
相加,视同两个整数相加。占位符%d
表示输出十进制整数,因此输出结果为133。
单引号本身也是一个字符,如果要表示这个字符常量,必须使用反斜杠转义。
char t = '\'';
上面示例中,变量t
为单引号字符,由于字符常量必须放在单引号里面,所以内部的单引号要使用反斜杠转义。
这种转义的写法,主要用来表示 ASCII 码定义的一些无法打印的控制字符,它们也属于字符类型的值。
\a
:警报,这会使得终端发出警报声或出现闪烁,或者两者同时发生。\b
:退格键,光标回退一个字符,但不删除字符。\f
:换页符,光标移到下一页。在现代系统上,这已经反映不出来了,行为改成类似于\v
。\n
:换行符。\r
:回车符,光标移到同一行的开头。\t
:制表符,光标移到下一个水平制表位,通常是下一个8的倍数。\v
:垂直分隔符,光标移到下一个垂直制表位,通常是下一行的同一列。\0
:null 字符,代表没有内容。注意,这个值不等于数字0。
转义写法还能使用八进制和十六进制表示一个字符。
\nn
:字符的八进制写法,nn
为八进制值。\xnn
:字符的十六进制写法,nn
为十六进制值。
char x = 'B';
char x = 66;
char x = '\102'; // 八进制
char x = '\x42'; // 十六进制
上面示例的四种写法都是等价的。
2.1.3 整数类型
整数类型用来表示较大的整数,类型声明使用int
关键字。
int a;
上面示例声明了一个整数变量a
。
不同计算机的int
类型的大小是不一样的。比较常见的是使用4个字节(32位)存储一个int
类型的值,但是2个字节(16位)或8个字节(64位)也有可能使用。它们可以表示的整数范围如下。
- 16位:-32,768 到 32,767。
- 32位:-2,147,483,648 到 2,147,483,647。
- 64位:-9,223,372,036,854,775,808 到 9,223,372,036,854,775,807。
2.1.3.1 signed,unsigned
C 语言使用signed
关键字,表示一个类型带有正负号,包含负值;使用unsigned
关键字,表示该类型不带有正负号,只能表示零和正整数。
对于int
类型,默认是带有正负号的,也就是说int
等同于signed int
。由于这是默认情况,关键字signed
一般都省略不写,但是写了也不算错。
signed int a;
// 等同于
int a;
int
类型也可以不带正负号,只表示非负整数。这时就必须使用关键字unsigned
声明变量。
unsigned int a;
整数变量声明为unsigned
的好处是,同样长度的内存能够表示的最大整数值,增大了一倍。比如,16位的signed int
最大值为32,767,而unsigned int
的最大值增大到了65,535。
unsigned int
里面的int
可以省略,所以上面的变量声明也可以写成下面这样。
unsigned a;
字符类型char
也可以设置signed
和unsigned
。
signed char c; // 范围为 -128 到 127
unsigned char c; // 范围为 0 到 255
注意,C 语言规定char
类型默认是否带有正负号,由当前系统决定。这就是说,char
不等同于signed char
,它有可能是signed char
,也有可能是unsigned char
。这一点与int
不同,int
就是等同于signed int
。
2.1.3.2 整数的子类型
如果int
类型使用4个或8个字节表示一个整数,对于小整数,这样做很浪费空间。另一方面,某些场合需要更大的整数,8个字节还不够。为了解决这些问题,C 语言在int
类型之外,又提供了三个整数的子类型。这样有利于更精细地限定整数变量的范围,也有利于更好地表达代码的意图。
short int
(简写为short
):占用空间不多于int
,一般占用2个字节(整数范围为-32768~32767)。long int
(简写为long
):占用空间不少于int
,至少为4个字节。long long int
(简写为long long
):占用空间多于long
,至少为8个字节。
short int a;
long int b;
long long int c;
上面代码分别声明了三种整数子类型的变量。
默认情况下,short
、long
、long long
都是带符号的(signed),即signed
关键字省略了。它们也可以声明为不带符号(unsigned),使得能够表示的最大值扩大一倍。
unsigned short int a;
unsigned long int b;
unsigned long long int c;
C 语言允许省略int
,所以变量声明语句也可以写成下面这样。
short a;
unsigned short a;
long b;
unsigned long b;
long long c;
unsigned long long c;
不同的计算机,数据类型的字节长度是不一样的。确实需要32位整数时,应使用long
类型而不是int
类型,可以确保不少于4个字节;确实需要64位的整数时,应该使用long long
类型,可以确保不少于8个字节。另一方面,为了节省空间,只需要16位整数时,应使用short
类型;需要8位整数时,应该使用char
类型。
2.1.3.3 整数类型的极限值
有时候需要查看,当前系统不同整数类型的最大值和最小值,C 语言的头文件limits.h
提供了相应的常量,比如SCHAR_MIN
代表 signed char 类型的最小值-128
,SCHAR_MAX
代表 signed char 类型的最大值127
。
为了代码的可移植性,需要知道某种整数类型的极限值时,应该尽量使用这些常量。
SCHAR_MIN
,SCHAR_MAX
:signed char 的最小值和最大值。SHRT_MIN
,SHRT_MAX
:short 的最小值和最大值。INT_MIN
,INT_MAX
:int 的最小值和最大值。LONG_MIN
,LONG_MAX
:long 的最小值和最大值。LLONG_MIN
,LLONG_MAX
:long long 的最小值和最大值。UCHAR_MAX
:unsigned char 的最大值。USHRT_MAX
:unsigned short 的最大值。UINT_MAX
:unsigned int 的最大值。ULONG_MAX
:unsigned long 的最大值。ULLONG_MAX
:unsigned long long 的最大值。
2.1.3.4 整数的进制
C 语言的整数默认都是十进制数,如果要表示八进制数和十六进制数,必须使用专门的表示法。
八进制使用0
作为前缀,比如017
、0377
。
int a = 012; // 八进制,相当于十进制的10
十六进制使用0x
或0X
作为前缀,比如0xf
、0X10
。
int a = 0x1A2B; // 十六进制,相当于十进制的6699
有些编译器使用0b
前缀,表示二进制数,但不是标准。
int x = 0b101010;
注意,不同的进制只是整数的书写方法,不会对整数的实际存储方式产生影响。所有整数都是二进制形式存储,跟书写方式无关。不同进制可以混合使用,比如10 + 015 + 0x20
是一个合法的表达式。
printf()
的进制相关占位符如下。
%d
:十进制整数。%o
:八进制整数。%x
:十六进制整数。%#o
:显示前缀0
的八进制整数。%#x
:显示前缀0x
的十六进制整数。%#X
:显示前缀0X
的十六进制整数。
int x = 100;
printf("dec = %d\n", x); // 100
printf("octal = %o\n", x); // 144
printf("hex = %x\n", x); // 64
printf("octal = %#o\n", x); // 0144
printf("hex = %#x\n", x); // 0x64
printf("hex = %#X\n", x); // 0X64
2.1.4 浮点型
任何有小数点的数值,都会被编译器解释为浮点数。所谓“浮点数”就是使用 m * be 的形式,存储一个数值,m
是小数部分,b
是基数(通常是2
),e
是指数部分。这种形式是精度和数值范围的一种结合,可以表示非常大或者非常小的数。
浮点数的类型声明使用float
关键字,可以用来声明浮点数变量。
float c = 10.5;
上面示例中,变量c
的就是浮点数类型。
float
类型占用4个字节(32位),其中8位存放指数的值和符号,剩下24位存放小数的值和符号。float
类型至少能够提供(十进制的)6位有效数字,指数部分的范围为(十进制的)-37
到37
,即数值范围为10-37到1037。
有时候,32位浮点数提供的精度或者数值范围还不够,C 语言又提供了另外两种更大的浮点数类型。
double
:占用8个字节(64位),至少提供13位有效数字。long double
:通常占用16个字节。
注意,由于存在精度限制,浮点数只是一个近似值,它的计算是不精确的,比如 C 语言里面0.1 + 0.2
并不等于0.3
,而是有一个很小的误差。
if (0.1 + 0.2 == 0.3) // false
C 语言允许使用科学计数法表示浮点数,使用字母e
来分隔小数部分和指数部分。
double x = 123.456e+3; // 123.456 x 10^3
// 等同于
double x = 123.456e3;
上面示例中,e
后面如果是加号+
,加号可以省略。注意,科学计数法里面e
的前后,不能存在空格。
另外,科学计数法的小数部分如果是0.x
或x.0
的形式,那么0
可以省略。
0.3E6
// 等同于
.3E6
3.0E6
// 等同于
3.E6
2.1.5 布尔类型
C 语言原来并没有为布尔值单独设置一个类型,而是使用整数0
表示伪,所有非零值表示真。
int x = 1;
if (x) {
printf("x is true!\n");
}
上面示例中,变量x
等于1
,C 语言就认为这个值代表真,从而会执行判断体内部的代码。
C99 标准添加了类型_Bool
,表示布尔值。但是,这个类型其实只是整数类型的别名,还是使用0
表示伪,1
表示真,下面是一个示例。
_Bool isNormal;
isNormal = 1;
if (isNormal)
printf("Everything is OK.\n");
头文件stdbool.h
定义了另一个类型别名bool
,并且定义了true
代表1
、false
代表0
。只要加载这个头文件,就可以使用这几个关键字。
#include <stdbool.h>
bool flag = false;
上面示例中,加载头文件stdbool.h
以后,就可以使用bool
定义布尔值类型,以及false
和true
表示真伪。
2.2 变量
变量(variable)可以理解成一块内存区域的名字。通过变量名,可以引用这块内存区域,获取里面存储的值。由于值可能发生变化,所以称为变量,否则就是常量了。
2.2.1 变量名
变量名在 C 语言里面属于标识符(identifier),命名有严格的规范。
- 只能由字母(包括大写和小写)、数字和下划线(
_
)组成。 - 不能以数字开头。
- 长度不能超过63个字符。
下面是一些无效变量名的例子。
$zj
j**p
2cat
Hot-tab
tax rate
don't
上面示例中,每一行的变量名都是无效的。
变量名区分大小写,star
、Star
、STAR
都是不同的变量。
并非所有的词都能用作变量名,有些词在 C 语言里面有特殊含义(比如int
),另一些词是命令(比如continue
),它们都称为关键字,不能用作变量名。另外,C 语言还保留了一些词,供未来使用,这些保留字也不能用作变量名。下面就是 C 语言主要的关键字和保留字。
auto, break, case, char, const, continue, default, do, double, else, enum, extern, float, for, goto, if, inline, int, long, register, restrict, return, short, signed, sizeof, static, struct, switch, typedef, union, unsigned, void, volatile, while
另外,两个下划线开头的变量名,以及一个下划线 + 大写英文字母开头的变量名,都是系统保留的,自己不应该起这样的变量名。
2.2.2 变量的声明
C 语言的变量,必须先声明后使用。如果一个变量没有声明,就直接使用,会报错。
每个变量都有自己的类型(type)。声明变量时,必须把变量的类型告诉编译器。
int height;
上面代码声明了变量height
,并且指定类型为int
(整数)。
如果几个变量具有相同类型,可以在同一行声明。
int height, width;
// 等同于
int height;
int width;
注意,声明变量的语句必须以分号结尾。
一旦声明,变量的类型就不能在运行时修改。
2.2.3 变量的赋值
C 语言会在变量声明时,就为它分配内存空间,但是不会清除内存里面原来的值。这导致声明变量以后,变量会是一个随机的值。所以,变量一定要赋值以后才能使用。
赋值操作通过赋值运算符(=
)完成。
int num;
num = 42;
上面示例中,第一行声明了一个整数变量num
,第二行给这个变量赋值。
变量的值应该与类型一致,不应该赋予不是同一个类型的值,比如num
的类型是整数,就不应该赋值为小数。虽然 C 语言会自动转换类型,但是应该避免赋值运算符两侧的类型不一致。
变量的声明和赋值,也可以写在一行。
int num = 42;
多个相同类型变量的赋值,可以写在同一行。
int x = 1, y = 2;
注意,赋值表达式有返回值,等于等号右边的值。
int x, y;
x = 1;
y = (x = 2 * x);
上面代码中,变量y
的值就是赋值表达式(x = 2 * x
)的返回值2
。
由于赋值表达式有返回值,所以 C 语言可以写出多重赋值表达式。
int x, y, z, m, n;
x = y = z = m = n = 3;
上面的代码是合法代码,一次为多个变量赋值。赋值运算符是从右到左执行,所以先为n
赋值,然后依次为m
、z
、y
和x
赋值。
C 语言有左值(left value)和右值(right value)的概念。左值是可以放在赋值运算符左边的值,一般是变量;右值是可以放在赋值运算符右边的值,一般是一个具体的值。这是为了强调有些值不能放在赋值运算符的左边,比如x = 1
是合法的表达式,但是1 = x
就会报错。
2.2.4 变量的作用域
作用域(scope)指的是变量生效的范围。C 语言的变量作用域主要有两种:文件作用域(file scope)和块作用域(block scope)。
文件作用域(file scope)指的是,在源码文件顶层声明的变量,从声明的位置到文件结束都有效。
int x = 1;
int main(void) {
printf("%i\n", x);
}
上面示例中,变量x
是在文件顶层声明的,从声明位置开始的整个当前文件都是它的作用域,可以在这个范围的任何地方读取这个变量,比如函数main()
内部就可以读取这个变量。
块作用域(block scope)指的是由大括号({}
)组成的代码块,它形成一个单独的作用域。凡是在块作用域里面声明的变量,只在当前代码块有效,代码块外部不可见。
int a = 12;
if (a == 12) {
int b = 99;
printf("%d %d\n", a, b); // 12 99
}
printf("%d\n", a); // 12
printf("%d\n", b); // 出错
上面例子中,变量b
是在if
代码块里面声明的,所以对于大括号外面的代码,这个变量是不存在的。
代码块可以嵌套,即代码块内部还有代码块,这时就形成了多层的块作用域。它的规则是:内层代码块可以使用外层声明的变量,但外层不可以使用内层声明的变量。如果内层的变量与外层同名,那么会在当前作用域覆盖外层变量。
{
int i = 10;
{
int i = 20;
printf("%d\n", i); // 20
}
printf("%d\n", i); // 10
}
上面示例中,内层和外层都有一个变量i
,每个作用域都会优先使用当前作用域声明的i
。
最常见的块作用域就是函数,函数内部声明的变量,对于函数外部是不可见的。for
循环也是一个块作用域,循环变量只对循环体内部可见,外部是不可见的。
for (int i = 0; i < 10; i++)
printf("%d\n", i);
printf("%d\n", i); // 出错
上面示例中,for
循环省略了大括号,但依然是一个块作用域,在外部读取循环变量i
,编译器就会报错。
2.3 常量
常量固定值,常量可以是整型常量、浮点型常量、字符常量、枚举常量等。
2.3.1 const常量
const
说明符表示变量是只读的,不得被修改。示例里面的const
,表示变量PI
的值不应改变。如果改变的话,编译器会报错。
const double PI = 3.14159;
PI = 3; // 报错
对于数组,const
表示数组成员不能修改。示例中,const
使得数组arr
的成员无法修改。
const int arr[] = {1, 2, 3, 4};
arr[0] = 5; // 报错
对于指针变量,const
有两种写法,含义是不一样的。如果const
在*
前面,表示指针指向的值不可修改。示例中,对x
指向的值进行修改导致报错。
// const 表示指向的值 *x 不能修改
int const * x
// 或者
const int * x
// 案例
int p = 1
const int* x = &p;
(*x)++; // 报错
如果const
在*
后面,表示指针包含的地址不可修改。示例中,对x
进行修改导致报错。
// const 表示地址 x 不能修改
int* const x
// 案例
int p = 1
int* const x = &p;
x++; // 报错
这两者可以结合起来。
const char* const x;
上面示例中,指针变量x
指向一个字符串。两个const
意味着,x
包含的内存地址以及x
指向的字符串,都不能修改。
const
的一个用途,就是防止函数体内修改函数参数。如果某个参数在函数体内不会被修改,可以在函数声明时,对该参数添加const
说明符。这样的话,使用这个函数的人看到原型里面的const
,就知道调用函数前后,参数数组保持不变。
void find(const int* arr, int n);
上面示例中,函数find
的参数数组arr
有const
说明符,就说明该数组在函数内部将保持不变。
有一种情况需要注意,如果一个指针变量指向const
变量,那么该指针变量也不应该被修改。
const int i = 1;
int* j = &i;
*j = 2; // 报错
上面示例中,j
是一个指针变量,指向变量i
,即j
和i
指向同一个地址。j
本身没有const
说明符,但是i
有。这种情况下,j
指向的值也不能被修改。
2.3.2 static常量
static
说明符对于全局变量和局部变量有不同的含义。
1)用于局部变量(位于块作用域内部)。
static
用于函数内部声明的局部变量时,表示该变量的值会在函数每次执行后得到保留,下次执行时不会进行初始化,就类似于一个只用于函数内部的全局变量。由于不必每次执行函数时,都对该变量进行初始化,这样可以提高函数的执行速度,详见《函数》一章。
2)用于全局变量(位于块作用域外部)。
static
用于函数外部声明的全局变量时,表示该变量只用于当前文件,其他源码文件不可以引用该变量,即该变量不会被链接(link)。
static
修饰的变量,初始化时,值不能等于变量,必须是常量。
int n = 10;
static m = n; // 报错
上面示例中,变量m
有static
修饰,它的值如果等于变量n
,就会报错,必须等于常量。
只在当前文件里面使用的函数,也可以声明为static
,表明该函数只在当前文件使用,其他文件可以定义同名函数。
static int g(int i);
2.4 枚举和派生类型
什么是枚举,定义一个枚举类型,需要使用 enum 关键字,后面跟着枚举类型的名称,以及用大括号 {} 括起来的一组枚举常量。
2.4.1 枚举定义
如果一种数据类型的取值只有少数几种可能,并且每种取值都有自己的含义,为了提高代码的可读性,可以将它们定义为 Enum 类型,中文名为枚举。
enum colors {RED, GREEN, BLUE};
printf("%d\n", RED); // 0
printf("%d\n", GREEN); // 1
printf("%d\n", BLUE); // 2
上面示例中,假定程序里面需要三种颜色,就可以使用enum
命令,把这三种颜色定义成一种枚举类型colors
,它只有三种取值可能RED
、GREEN
、BLUE
。这时,这三个名字自动成为整数常量,编译器默认将它们的值设为数字0
、1
、2
。相比之下,RED
要比0
的可读性好了许多。
注意,Enum 内部的常量名,遵守标识符的命名规范,但是通常都使用大写。
使用时,可以将变量声明为 Enum 类型。
enum colors color;
上面代码将变量color
声明为enum colors
类型。这个变量的值就是常量RED
、GREEN
、BLUE
之中的一个。
color = BLUE;
printf("%i\n", color); // 2
上面代码将变量color
的值设为BLUE
,这里BLUE
就是一个常量,值等于2
。
2.4.2 枚举别名
typedef 命令可以为 Enum 类型起别名。
typedef enum {
SHEEP,
WHEAT,
WOOD,
BRICK,
ORE
} RESOURCE;
RESOURCE r;
上面示例中,RESOURCE
是 Enum 类型的别名。声明变量时,使用这个别名即可。
2.4.3 枚举常量
声明 Enum 类型时,在同一行里面为变量赋值。
enum {
SHEEP,
WHEAT,
WOOD,
BRICK,
ORE
} r = BRICK, s = WOOD;
上面示例中,r
的值是3
,s
的值是2
。
由于 Enum 的属性会自动声明为常量,所以有时候使用 Enum 的目的,不是为了自定义一种数据类型,而是为了声明一组常量。这时就可以使用下面这种写法,比较简单。
enum { ONE, TWO };
printf("%d %d", ONE, TWO); // 0 1
上面示例中,enum
是一个关键字,后面跟着一个代码块,常量就在代码内声明。ONE
和TWO
就是两个 Enum 常量。
常量之间使用逗号分隔。最后一个常量后面的尾逗号,可以省略,也可以保留。
enum { ONE, TWO, };
由于Enum 会自动编号,因此可以不必为常量赋值。C 语言会自动从0开始递增,为常量赋值。但是,C 语言也允许为 ENUM 常量指定值,不过只能指定为整数,不能是其他类型。因此,任何可以使用整数的场合,都可以使用 Enum 常量。
enum { ONE = 1, TWO = 2 };
printf("%d %d", ONE, TWO); // 1 2
Enum 常量可以是不连续的值。
enum { X = 2, Y = 18, Z = -2 };
Enum 常量也可以是同一个值。
enum { X = 2, Y = 2, Z = 2 };
如果一组常量之中,有些指定了值,有些没有指定。那么,没有指定值的常量会从上一个指定了值的常量,开始自动递增赋值。
enum {
A, // 0
B, // 1
C = 4, // 4
D, // 5
E, // 6
F = 3, // 3
G, // 4
H // 5
};
Enum 的作用域与变量相同。如果是在顶层声明,那么在整个文件内都有效;如果是在代码块内部声明,则只对该代码块有效。如果与使用int
声明的常量相比,Enum 的好处是更清晰地表示代码意图。
2.5 字符串类型
2.5.1 字符串介绍
C 语言没有单独的字符串类型,字符串被当作字符数组,即char
类型的数组。比如,字符串“Hello”是当作数组{'H', 'e', 'l', 'l', 'o'}
处理的。
编译器会给数组分配一段连续内存,所有字符储存在相邻的内存单元之中。在字符串结尾,C 语言会自动添加一个全是二进制0
的字节,写作\0
字符,表示字符串结束。字符\0
不同于字符0
,前者的 ASCII 码是0(二进制形式00000000
),后者的 ASCII 码是48(二进制形式00110000
)。所以,字符串“Hello”实际储存的数组是{'H', 'e', 'l', 'l', 'o', '\0'}
。
所有字符串的最后一个字符,都是\0
。这样做的好处是,C 语言不需要知道字符串的长度,就可以读取内存里面的字符串,只要发现有一个字符是\0
,那么就知道字符串结束了。
char localString[10];
上面示例声明了一个10个成员的字符数组,可以当作字符串。由于必须留一个位置给\0
,所以最多只能容纳9个字符的字符串。
字符串写成数组的形式,是非常麻烦的。C 语言提供了一种简写法,双引号之中的字符,会被自动视为字符数组。
{'H', 'e', 'l', 'l', 'o', '\0'}
// 等价于
"Hello"
// 报错
'Hello'
上面两种字符串的写法是等价的,内部存储方式都是一样的。双引号里面的字符串,不用自己添加结尾字符\0
,C 语言会自动添加。注意,双引号里面是字符串,单引号里面是字符,两者不能互换。如果把Hello
放在单引号里面,编译器会报错。
另一方面,即使双引号里面只有一个字符(比如"a"
),也依然被处理成字符串(存储为2个字节),而不是字符'a'
(存储为1个字节)。
如果字符串内部包含双引号,则该双引号需要使用反斜杠转义。
"She replied, \"It does.\""
反斜杠还可以表示其他特殊字符,比如换行符(\n
)、制表符(\t
)等。
"Hello, world!\n"
如果字符串过长,可以在需要折行的地方,使用反斜杠(\
)结尾,将一行拆成多行。
"hello \
world"
上面示例中,第一行尾部的反斜杠,将字符串拆成两行。
上面这种写法有一个缺点,就是第二行必须顶格书写,如果想包含缩进,那么缩进也会被计入字符串。为了解决这个问题,C 语言允许合并多个字符串字面量,只要这些字符串之间没有间隔,或者只有空格,C 语言会将它们自动合并。
char greeting[50] = "Hello, ""how are you ""today!";
// 等同于
char greeting[50] = "Hello, how are you today!";
这种新写法支持多行字符串的合并。
char greeting[50] = "Hello, "
"how are you "
"today!";
printf()
使用占位符%s
输出字符串。
printf("%s\n", "hello world")
2.5.2 字符串声明
字符串变量可以声明成一个字符数组,也可以声明成一个指针,指向字符数组。
// 写法一
char s[14] = "Hello, world!";
// 写法二
char* s = "Hello, world!";
上面两种写法都声明了一个字符串变量s
。如果采用第一种写法,由于字符数组的长度可以让编译器自动计算,所以声明时可以省略字符数组的长度。
char s[] = "Hello, world!";
上面示例中,编译器会将数组s
的长度指定为14,正好容纳后面的字符串。
字符数组的长度,可以大于字符串的实际长度。
char s[50] = "hello";
上面示例中,字符数组s
的长度是50
,但是字符串“hello”的实际长度只有6(包含结尾符号\0
),所以后面空出来的44个位置,都会被初始化为\0
。
字符数组的长度,不能小于字符串的实际长度。
char s[5] = "hello";
上面示例中,字符串数组s
的长度是5
,小于字符串“hello”的实际长度6,这时编译器会报错。因为如果只将前5个字符写入,而省略最后的结尾符号\0
,这很可能导致后面的字符串相关代码出错。
字符指针和字符数组,这两种声明字符串变量的写法基本是等价的,但是有两个差异。
第一个差异是,指针指向的字符串,在 C 语言内部被当作常量,不能修改字符串本身。
char* s = "Hello, world!";
s[0] = 'z'; // 错误
上面代码使用指针,声明了一个字符串变量,然后修改了字符串的第一个字符。这种写法是错的,会导致难以预测的后果,执行时很可能会报错。
如果使用数组声明字符串变量,就没有这个问题,可以修改数组的任意成员。
char s[] = "Hello, world!";
s[0] = 'z';
为什么字符串声明为指针时不能修改,声明为数组时就可以修改?原因是系统会将字符串的字面量保存在内存的常量区,这个区是不允许用户修改的。声明为指针时,指针变量存储的值是一个指向常量区的内存地址,因此用户不能通过这个地址去修改常量区。但是,声明为数组时,编译器会给数组单独分配一段内存,字符串字面量会被编译器解释成字符数组,逐个字符写入这段新分配的内存之中,而这段新内存是允许修改的。
为了提醒用户,字符串声明为指针后不得修改,可以在声明时使用const
说明符,保证该字符串是只读的。
const char* s = "Hello, world!";
上面字符串声明为指针时,使用了const
说明符,就保证了该字符串无法修改。一旦修改,编译器肯定会报错。
第二个差异是,指针变量可以指向其它字符串。
char* s = "hello";
s = "world";
上面示例中,字符指针可以指向另一个字符串。
但是,字符数组变量不能指向另一个字符串。
char s[] = "hello";
s = "world"; // 报错
上面示例中,字符数组的数组名,总是指向初始化时的字符串地址,不能修改。
同样的原因,声明字符数组后,不能直接用字符串赋值。
char s[10];
s = "abc"; // 错误
上面示例中,不能直接把字符串赋值给字符数组变量,会报错。原因是字符数组的变量名,跟所指向的数组是绑定的,不能指向另一个地址。
为什么数组变量不能赋值为另一个数组?原因是数组变量所在的地址无法改变,或者说,编译器一旦为数组变量分配地址后,这个地址就绑定这个数组变量了,这种绑定关系是不变的。C 语言也因此规定,数组变量是一个不可修改的左值,即不能用赋值运算符为它重新赋值。
想要重新赋值,必须使用 C 语言原生提供的strcpy()
函数,通过字符串拷贝完成赋值。这样做以后,数组变量的地址还是不变的,即strcpy()
只是在原地址写入新的字符串,而不是让数组变量指向新的地址。
char s[10];
strcpy(s, "abc");
上面示例中,strcpy()
函数把字符串abc
拷贝给变量s
,这个函数的详细用法会在后面介绍。
2.5.3 字符串长度
strlen()
函数返回字符串的字节长度,不包括末尾的空字符\0
。该函数的原型如下。
// string.h
size_t strlen(const char* s);
它的参数是字符串变量,返回的是size_t
类型的无符号整数,除非是极长的字符串,一般情况下当作int
类型处理即可。下面是一个用法实例。
char* str = "hello";
int len = strlen(str); // 5
strlen()
的原型在标准库的string.h
文件中定义,使用时需要加载头文件string.h
。
#include <stdio.h>
#include <string.h>
int main(void) {
char* s = "Hello, world!";
printf("The string is %zd characters long.\n", strlen(s));
}
注意,字符串长度(strlen()
)与字符串变量长度(sizeof()
),是两个不同的概念。
char s[50] = "hello";
printf("%d\n", strlen(s)); // 5
printf("%d\n", sizeof(s)); // 50
上面示例中,字符串长度是5,字符串变量长度是50。
如果不使用这个函数,可以通过判断字符串末尾的\0
,自己计算字符串长度。
int my_strlen(char *s) {
int count = 0;
while (s[count] != '\0')
count++;
return count;
}
2.5.4 字符串复制
strcpy(),字符串的复制,不能使用赋值运算符,直接将一个字符串赋值给字符数组变量。
char str1[10];
char str2[10];
str1 = "abc"; // 报错
str2 = str1; // 报错
上面两种字符串的复制写法,都是错的。因为数组的变量名是一个固定的地址,不能修改,使其指向另一个地址。
如果是字符指针,赋值运算符(=
)只是将一个指针的地址复制给另一个指针,而不是复制字符串。
char* s1;
char* s2;
s1 = "abc";
s2 = s1;
上面代码可以运行,结果是两个指针变量s1
和s2
指向同一字符串,而不是将字符串s1
的内容复制给s2
。
C 语言提供了strcpy()
函数,用于将一个字符串的内容复制到另一个字符串,相当于字符串赋值。该函数的原型定义在string.h
头文件里面。
strcpy(char dest[], const char source[])
strcpy()
接受两个参数,第一个参数是目的字符串数组,第二个参数是源字符串数组。复制字符串之前,必须要保证第一个参数的长度不小于第二个参数,否则虽然不会报错,但会溢出第一个字符串变量的边界,发生难以预料的结果。第二个参数的const
说明符,表示这个函数不会修改第二个字符串。
#include <stdio.h>
#include <string.h>
int main(void) {
char s[] = "Hello, world!";
char t[100];
strcpy(t, s);
t[0] = 'z';
printf("%s\n", s); // "Hello, world!"
printf("%s\n", t); // "zello, world!"
}
上面示例将变量s
的值,拷贝一份放到变量t
,变成两个不同的字符串,修改一个不会影响到另一个。另外,变量t
的长度大于s
,复制后多余的位置(结束标志\0
后面的位置)都为随机值。
strcpy()
也可以用于字符数组的赋值。
char str[10];
strcpy(str, "abcd");
上面示例将字符数组变量,赋值为字符串“abcd”。
strcpy()
的返回值是一个字符串指针(即char*
),指向第一个参数。
char* s1 = "beast";
char s2[40] = "Be the best that you can be.";
char* ps;
ps = strcpy(s2 + 7, s1);
puts(s2); // Be the beast
puts(ps); // beast
上面示例中,从s2
的第7个位置开始拷贝字符串beast
,前面的位置不变。这导致s2
后面的内容都被截去了,因为会连beast
结尾的空字符一起拷贝。strcpy()
返回的是一个指针,指向拷贝开始的位置。
strcpy()
返回值的另一个用途,是连续为多个字符数组赋值。
strcpy(str1, strcpy(str2, "abcd"));
上面示例调用两次strcpy()
,完成两个字符串变量的赋值。
另外,strcpy()
的第一个参数最好是一个已经声明的数组,而不是声明后没有进行初始化的字符指针。
char* str;
strcpy(str, "hello world"); // 错误
上面的代码是有问题的。strcpy()
将字符串分配给指针变量str
,但是str
并没有进行初始化,指向的是一个随机的位置,因此字符串可能被复制到任意地方。
如果不用strcpy()
,自己实现字符串的拷贝,可以用下面的代码。
char* strcpy(char* dest, const char* source) {
char* ptr = dest;
while (*dest++ = *source++);
return ptr;
}
int main(void) {
char str[25];
strcpy(str, "hello world");
printf("%s\n", str);
return 0;
}
上面代码中,关键的一行是while (*dest++ = *source++)
,这是一个循环,依次将source
的每个字符赋值给dest
,然后移向下一个位置,直到遇到\0
,循环判断条件不再为真,从而跳出循环。其中,*dest++
这个表达式等同于*(dest++)
,即先返回dest
这个地址,再进行自增运算移向下一个位置,而*dest
可以对当前位置赋值。
strcpy()
函数有安全风险,因为它并不检查目标字符串的长度,是否足够容纳源字符串的副本,可能导致写入溢出。如果不能保证不会发生溢出,建议使用strncpy()
函数代替。
2.5.5 字符串部分复制
strncpy()
跟strcpy()
的用法完全一样,只是多了第3个参数,用来指定复制的最大字符数,防止溢出目标字符串变量的边界。
char* strncpy(
char* dest,
char* src,
size_t n
);
上面原型中,第三个参数n
定义了复制的最大字符数。如果达到最大字符数以后,源字符串仍然没有复制完,就会停止复制,这时目的字符串结尾将没有终止符\0
,这一点务必注意。如果源字符串的字符数小于n
,则strncpy()
的行为与strcpy()
完全一致。
strncpy(str1, str2, sizeof(str1) - 1);
str1[sizeof(str1) - 1] = '\0';
上面示例中,字符串str2
复制给str1
,但是复制长度最多为str1
的长度减去1,str1
剩下的最后一位用于写入字符串的结尾标志\0
。这是因为strncpy()
不会自己添加\0
,如果复制的字符串片段不包含结尾标志,就需要手动添加。
strncpy()
也可以用来拷贝部分字符串。
char s1[40];
char s2[12] = "hello world";
strncpy(s1, s2, 5);
s1[5] = '\0';
printf("%s\n", s1); // hello
上面示例中,指定只拷贝前5个字符。
2.5.6 字符串连接
strcat()
函数用于连接字符串。它接受两个字符串作为参数,把第二个字符串的副本添加到第一个字符串的末尾。这个函数会改变第一个字符串,但是第二个字符串不变。
该函数的原型定义在string.h
头文件里面。
char* strcat(char* s1, const char* s2);
strcat()
的返回值是一个字符串指针,指向第一个参数。
char s1[12] = "hello";
char s2[6] = "world";
strcat(s1, s2);
puts(s1); // "helloworld"
上面示例中,调用strcat()
以后,可以看到字符串s1
的值变了。
注意,strcat()
的第一个参数的长度,必须足以容纳添加第二个参数字符串。否则,拼接后的字符串会溢出第一个字符串的边界,写入相邻的内存单元,这是很危险的,建议使用下面的strncat()
代替。
strncat()
用于连接两个字符串,用法与strcat()
完全一致,只是增加了第三个参数,指定最大添加的字符数。在添加过程中,一旦达到指定的字符数,或者在源字符串中遇到空字符\0
,就不再添加了。它的原型定义在string.h
头文件里面。
char* strncat(
const char* dest,
const char* src,
size_t n
);
strncat()
返回第一个参数,即目标字符串指针。
为了保证连接后的字符串,不超过目标字符串的长度,strncat()
通常会写成下面这样。
strncat(
str1,
str2,
sizeof(str1) - strlen(str1) - 1
);
strncat()
总是会在拼接结果的结尾,自动添加空字符\0
,所以第三个参数的最大值,应该是str1
的变量长度减去str1
的字符串长度,再减去1
。下面是一个用法实例。
char s1[10] = "Monday";
char s2[8] = "Tuesday";
strncat(s1, s2, 3);
puts(s1); // "MondayTue"
上面示例中,s1
的变量长度是10,字符长度是6,两者相减后再减去1,得到3
,表明s1
最多可以再添加三个字符,所以得到的结果是MondayTue
。
2.5.7 字符串比较
strcmp(),如果要比较两个字符串,无法直接比较,只能一个个字符进行比较,C 语言提供了strcmp()
函数。
strcmp()
函数用于比较两个字符串的内容。该函数的原型如下,定义在string.h
头文件里面。
int strcmp(const char* s1, const char* s2);
按照字典顺序,如果两个字符串相同,返回值为0
;如果s1
小于s2
,strcmp()
返回值小于0;如果s1
大于s2
,返回值大于0。
下面是一个用法示例。
// s1 = Happy New Year
// s2 = Happy New Year
// s3 = Happy Holidays
strcmp(s1, s2) // 0
strcmp(s1, s3) // 大于 0
strcmp(s3, s1) // 小于 0
注意,strcmp()
只用来比较字符串,不用来比较字符。因为字符就是小整数,直接用相等运算符(==
)就能比较。所以,不要把字符类型(char
)的值,放入strcmp()
当作参数。
由于strcmp()
比较的是整个字符串,C 语言又提供了strncmp()
函数,只比较到指定的位置。
该函数增加了第三个参数,指定了比较的字符数。它的原型定义在string.h
头文件里面。
int strncmp(
const char* s1,
const char* s2,
size_t n
);
它的返回值与strcmp()
一样。如果两个字符串相同,返回值为0
;如果s1
小于s2
,strcmp()
返回值小于0;如果s1
大于s2
,返回值大于0。
下面是一个例子。
char s1[12] = "hello world";
char s2[12] = "hello C";
if (strncmp(s1, s2, 5) == 0) {
printf("They all have hello.\n");
}
上面示例只比较两个字符串的前5个字符。
2.5.8 字符串输入
sprintf()
函数跟printf()
类似,但是用于将数据写入字符串,而不是输出到显示器。该函数的原型定义在stdio.h
头文件里面。
int sprintf(char* s, const char* format, ...);
sprintf()
的第一个参数是字符串指针变量,其余参数和printf()
相同,即第二个参数是格式字符串,后面的参数是待写入的变量列表。
char first[6] = "hello";
char last[6] = "world";
char s[40];
sprintf(s, "%s %s", first, last);
printf("%s\n", s); // hello world
上面示例中,sprintf()
将输出内容组合成“hello world”,然后放入了变量s
。
sprintf()
的返回值是写入变量的字符数量(不计入尾部的空字符\0
)。如果遇到错误,返回负值。
sprintf()
有严重的安全风险,如果写入的字符串过长,超过了目标字符串的长度,sprintf()
依然会将其写入,导致发生溢出。为了控制写入的字符串的长度,C 语言又提供了另一个函数snprintf()
。
snprintf()
只比sprintf()
多了一个参数n
,用来控制写入变量的字符串不超过n - 1
个字符,剩下一个位置写入空字符\0
。下面是它的原型。
int snprintf(char*s, size_t n, const char* format, ...);
snprintf()
总是会自动写入字符串结尾的空字符。如果你尝试写入的字符数超过指定的最大字符数,snprintf()
会写入 n - 1 个字符,留出最后一个位置写入空字符。
下面是一个例子。
snprintf(s, 12, "%s %s", "hello", "world");
上面的例子中,snprintf()
的第二个参数是12,表示写入字符串的最大长度不超过12(包括尾部的空字符)。
snprintf()
的返回值是写入格式字符串的字符数量(不计入尾部的空字符\0
)。如果n
足够大,返回值应该小于n
,但是有时候格式字符串的长度可能大于n
,那么这时返回值会大于n
,但实际上真正写入变量的还是n-1
个字符。如果遇到错误,返回一个负值。因此,返回值只有在非负并且小于n
时,才能确认完整的格式字符串写入了变量。
2.5.9 字符串数组
如果一个数组的每个成员都是一个字符串,需要通过二维的字符数组实现。每个字符串本身是一个字符数组,多个字符串再组成一个数组。
char weekdays[7][10] = {
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"
};
上面示例就是一个字符串数组,一共包含7个字符串,所以第一维的长度是7。其中,最长的字符串的长度是10(含结尾的终止符\0
),所以第二维的长度统一设为10。
因为第一维的长度,编译器可以自动计算,所以可以省略。
char weekdays[][10] = {
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"
};
上面示例中,二维数组第一维的长度,可以由编译器根据后面的赋值,自动计算,所以可以不写。
数组的第二维,长度统一定为10,有点浪费空间,因为大多数成员的长度都小于10。解决方法就是把数组的第二维,从字符数组改成字符指针。
char* weekdays[] = {
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"
};
上面的字符串数组,其实是一个一维数组,成员就是7个字符指针,每个指针指向一个字符串(字符数组)。
遍历字符串数组的写法如下。
for (int i = 0; i < 7; i++) {
printf("%s\n", weekdays[i]);
}
2.6 字面量
2.6.1 字面量类型
字面量(literal)指的是代码里面直接出现的值。下面代码中,x
是变量,123
就是字面量
int x = 123;
编译时,字面量也会写入内存,因此编译器必须为字面量指定数据类型,就像必须为变量指定数据类型一样。
一般情况下,十进制整数字面量(比如123
)会被编译器指定为int
类型。如果一个数值比较大,超出了int
能够表示的范围,编译器会将其指定为long int
。如果数值超过了long int
,会被指定为unsigned long
。如果还不够大,就指定为long long
或unsigned long long
。
小数(比如3.14
)会被指定为double
类型。
2.6.2 字面量后缀
有时候,程序员希望为字面量指定一个不同的类型。比如,编译器将一个整数字面量指定为int
类型,但是程序员希望将其指定为long
类型,这时可以为该字面量加上后缀l
或L
,编译器就知道要把这个字面量的类型指定为long
。
//字面量`123`有后缀`L`,编译器就会将其指定为`long`类型。这里`123L`写成`123l`,效果也是一样的
int x = 123L;
int x = 123l;
八进制和十六进制的值,也可以使用后缀l
和L
指定为 Long 类型,比如020L
和0x20L
。
int y = 0377L; //八进制
int z = 0x7fffL; //十六进制
如果希望指定为无符号整数unsigned int
,可以使用后缀u
或U
。L
和U
可以结合使用,表示unsigned long
类型。L
和U
的大小写和组合顺序无所谓。
int x = 123U; //unsigned int类型
int x = 123LU; //unsigned long类型
对于浮点数,编译器默认指定为 double 类型,如果希望指定为其他类型,需要在小数后面添加后缀f
(float)或l
(long double)。
科学计数法也可以使用后缀。
1.2345e+10F
1.2345e+10L
总结一下,常用的字面量后缀有下面这些。
f
和F
:float
类型。l
和L
:对于整数是long int
类型,对于小数是long double
类型。ll
和LL
:Long Long 类型,比如3LL
。u
和U
:表示unsigned int
,比如15U
、0377U
。
u
还可以与其他整数后缀结合,放在前面或后面都可以,比如10UL
、10ULL
和10LLU
都是合法的。
下面是一些示例。
int x = 1234;
long int x = 1234L;
long long int x = 1234LL
unsigned int x = 1234U;
unsigned long int x = 1234UL;
unsigned long long int x = 1234ULL;
float x = 3.14f;
double x = 3.14;
long double x = 3.14L;
2.7 数据溢出
每一种数据类型都有数值范围,如果存放的数值超出了这个范围(小于最小值或大于最大值),需要更多的二进制位存储,就会发生溢出。大于最大值,叫做向上溢出(overflow);小于最小值,叫做向下溢出(underflow)。
一般来说,编译器不会对溢出报错,会正常执行代码,但是会忽略多出来的二进制位,只保留剩下的位,这样往往会得到意想不到的结果。所以,应该避免溢出。
unsigned char x = 255;
x = x + 1;
printf("%d\n", x); // 0
上面示例中,变量x
加1
,得到的结果不是256
,而是0
。因为x
是unsign char
类型,最大值是255
(二进制11111111
),加1
后就发生了溢出,256
(二进制100000000
)的最高位1
被丢弃,剩下的值就是0
。
再看下面的例子。
unsigned int ui = UINT_MAX; // 4,294,967,295
ui++;
printf("ui = %u\n", ui); // 0
ui--;
printf("ui = %u\n", ui); // 4,294,967,295
上面示例中,常量UINT_MAX
是 unsigned int 类型的最大值。如果加1
,对于该类型就会溢出,从而得到0
;而0
是该类型的最小值,再减1
,又会得到UINT_MAX
。
溢出很容易被忽视,编译器又不会报错,所以必须非常小心。
for (unsigned int i = n; i >= 0; --i) // 错误
上面代码表面看似乎没有问题,但是循环变量i
的类型是 unsigned int,这个类型的最小值是0
,不可能得到小于0的结果。当i
等于0,再减去1
的时候,并不会返回-1
,而是返回 unsigned int 的类型最大值,这个值总是大于等于0
,导致无限循环。
为了避免溢出,最好方法就是将运算结果与类型的极限值进行比较。
unsigned int ui;
unsigned int sum;
// 错误
if (sum + ui > UINT_MAX) too_big();
else sum = sum + ui;
// 正确
if (ui > UINT_MAX - sum) too_big();
else sum = sum + ui;
上面示例中,变量sum
和ui
都是 unsigned int 类型,它们相加的和还是 unsigned int 类型,这就有可能发生溢出。但是,不能通过相加的和是否超出了最大值UINT_MAX
,来判断是否发生了溢出,因为sum + ui
总是返回溢出后的结果,不可能大于UINT_MAX
。正确的比较方法是,判断UINT_MAX - sum
与ui
之间的大小关系。
下面是另一种错误的写法。
unsigned int i = 5;
unsigned int j = 7;
if (i - j < 0) // 错误
printf("negative\n");
else
printf("positive\n");
上面示例的运算结果,会输出positive
。原因是变量i
和j
都是 unsigned int 类型,i - j
的结果也是这个类型,最小值为0
,不可能得到小于0
的结果。正确的写法是写成下面这样。
if (j > i) // ....
2.8 类型自动转换
某些情况下,C 语言会自动转换某个值的类型。
2.8.1 赋值运算
赋值运算符会自动将右边的值,转成左边变量的类型。
1)浮点数赋值给整数变量。浮点数赋予整数变量时,C 语言直接丢弃小数部分,而不是四舍五入。
int x = 3.14; //`x`等于`3`
int x = 12.99; //`x`等于`12`,而不是四舍五入的`13`。
上面示例中,变量x
是整数类型,赋给它的值是一个浮点数。编译器会自动把3.14
先转为int
类型,丢弃小数部分,再赋值给x
,因此x
的值是3
。 这种自动转换会导致部分数据的丢失(3.14
丢失了小数部分),所以最好不要跨类型赋值,尽量保证变量与所要赋予的值是同一个类型。 注意,舍弃小数部分时,不是四舍五入,而是整个舍弃。
2)整数赋值给浮点数变量。整数赋值给浮点数变量时,会自动转为浮点数。
float y = 12 * 2;
上面示例中,变量y
的值不是24
,而是24.0
,因为等号右边的整数自动转为了浮点数。
3)窄类型赋值给宽类型。字节宽度较小的整数类型,赋值给字节宽度较大的整数变量时,会发生类型提升,即窄类型自动转为宽类型。
比如,char
或short
类型赋值给int
类型,会自动提升为int
。
char x = 10;
int i = x + y;
上面示例中,变量x
的类型是char
,由于赋值给int
类型,所以会自动提升为int
。
4)宽类型赋值给窄类型。字节宽度较大的类型,赋值给字节宽度较小的变量时,会发生类型降级,自动转为后者的类型。这时可能会发生截值(truncation),系统会自动截去多余的二进制位,导致难以预料的结果。
int i = 321;
char ch = i; // ch 的值是 65 (321 % 256 的余值)
上面例子中,变量ch
是char
类型,宽度是8个二进制位。变量i
是int
类型,将i
赋值给ch
,后者只能容纳i
(二进制形式为101000001
,共9位)的后八位,前面多出来的二进制位被丢弃,保留后八位就变成了01000001
(十进制的65,相当于字符A
)。
浮点数赋值给整数类型的值,也会发生截值,浮点数的小数部分会被截去。
double pi = 3.14159;
int i = pi; // i 的值为 3
上面示例中,i
等于3
,pi
的小数部分被截去了。
2.8.2 混合类型运算
不同类型的值进行混合计算时,必须先转成同一个类型,才能进行计算。转换规则如下:
1)整数与浮点数混合运算时,整数转为浮点数类型,与另一个运算数类型相同。
3 + 1.2 // 4.2
上面示例是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;
if (a < sizeof(int))
do_something();
上面示例中,变量a
是带符号整数,sizeof(int)
是size_t
类型,这是一个无符号整数。按照规则,signed int 自动转为 unsigned int,所以a
会自动转成无符号整数4294967291
(转换规则是-5
加上无符号整数的最大值,再加1),导致比较失败,do_something()
不会执行。
所以,最好避免无符号整数与有符号整数的混合运算。因为这时 C 语言会自动将signed int
转为unsigned int
,可能不会得到预期的结果。
2.8.3 整数类型运算
两个相同类型的整数运算时,或者单个整数的运算,一般来说,运算结果也属于同一类型。但是有一个例外,宽度小于int
的类型,运算结果会自动提升为int
。
unsigned char a = 66;
if ((-a) < 0) printf("negative\n");
else printf("positive\n");
上面示例中,变量a
是 unsigned char 类型,这个类型不可能小于0,但是-a
不是 unsigned char 类型,会自动转为 int 类型,导致上面的代码输出 negative。
再看下面的例子。
unsigned char a = 1;
unsigned char b = 255;
unsigned char c = 255;
if ((a - 5) < 0) do_something();
if ((b + c) > 300) do_something();
上面示例中,表达式a - 5
和b + c
都会自动转为 int 类型,所以函数do_something()
会执行两次。
2.8.4 函数运算
函数的参数和返回值,会自动转成函数定义里指定的类型。
int dostuff(int, unsigned char);
char m = 42;
unsigned short n = 43;
long long int c = dostuff(m, n);
上面示例中,参数变量m
和n
不管原来的类型是什么,都会转成函数dostuff()
定义的参数类型。
下面是返回值自动转换类型的例子。
char func(void) {
int a = 42;
return a;
}
上面示例中,函数内部的变量a
是int
类型,但是返回的值是char
类型,因为函数定义中返回的是这个类型。
2.9 类型显式转换
原则上,应该避免类型的自动转换,防止出现意料之外的结果。C 语言提供了类型的显式转换,允许手动转换类型。
只要在一个值或变量的前面,使用圆括号指定类型(type)
,就可以将这个值或变量转为指定的类型,这叫做“类型指定”(casting)。
(unsigned char) ch
上面示例将变量ch
转成无符号的字符类型。
long int y = (long int) 10 + 12;
上面示例中,(long int)
将10
显式转为long int
类型。这里的显示转换其实是不必要的,因为赋值运算符会自动将右边的值,转为左边变量的类型。
2.10 可移植类型
C 语言的整数类型(short、int、long)在不同计算机上,占用的字节宽度可能是不一样的,无法提前知道它们到底占用多少个字节。
程序员有时控制准确的字节宽度,这样的话,代码可以有更好的可移植性,头文件stdint.h
创造了一些新的类型别名。
1)精确宽度类型(exact-width integer type),保证某个整数类型的宽度是确定的。
int8_t
:8位有符号整数。int16_t
:16位有符号整数。int32_t
:32位有符号整数。int64_t
:64位有符号整数。uint8_t
:8位无符号整数。uint16_t
:16位无符号整数。uint32_t
:32位无符号整数。uint64_t
:64位无符号整数。
上面这些都是类型别名,编译器会指定它们指向的底层类型。比如,某个系统中,如果int
类型为32位,int32_t
就会指向int
;如果long
类型为32位,int32_t
则会指向long
。
下面是一个使用示例。
#include <stdio.h>
#include <stdint.h>
int main(void) {
int32_t x32 = 45933945;
printf("x32 = %d\n", x32);
return 0;
}
上面示例中,变量x32
声明为int32_t
类型,可以保证是32位的宽度。
2)最小宽度类型(minimum width type),保证某个整数类型的最小长度。
- int_least8_t
- int_least16_t
- int_least32_t
- int_least64_t
- uint_least8_t
- uint_least16_t
- uint_least32_t
- uint_least64_t
上面这些类型,可以保证占据的字节不少于指定宽度。比如,int_least8_t
表示可以容纳8位有符号整数的最小宽度的类型。
3)最快的最小宽度类型(fast minimum width type),可以使整数计算达到最快的类型。
- int_fast8_t
- int_fast16_t
- int_fast32_t
- int_fast64_t
- uint_fast8_t
- uint_fast16_t
- uint_fast32_t
- uint_fast64_t
上面这些类型是保证字节宽度的同时,追求最快的运算速度,比如int_fast8_t
表示对于8位有符号整数,运算速度最快的类型。这是因为某些机器对于特定宽度的数据,运算速度最快,举例来说,32位计算机对于32位数据的运算速度,会快于16位数据。
4)可以保存指针的整数类型。
intptr_t
:可以存储指针(内存地址)的有符号整数类型。uintptr_t
:可以存储指针的无符号整数类型。
5)最大宽度整数类型,用于存放最大的整数。
intmax_t
:可以存储任何有效的有符号整数的类型。uintmax_t
:可以存放任何有效的无符号整数的类型。
上面的这两个类型的宽度比long long
和unsigned long
更大。