数据类型
# 02.数据类型
# 目录介绍
# 2.1 基本数据
# 2.1.1 基本数据类型
C 语言的每一种数据,都是有类型(type)的,编译器必须知道数据的类型,才能操作数据。
所谓“类型”,就是相似的数据所拥有的共同特征,那么一旦知道某个值的数据类型,就能知道该值的特征和操作方式。
基本数据类型有三种:字符(char)、整数(int)和浮点数(float)。复杂的类型都是基于它们构建的。
# 2.1.2 字符类型char
字符类型指的是单个字符,类型声明使用char关键字。char 类型通常占用 1 字节(8 位)的内存空间。
//声明了变量c是字符类型,并将其赋值为字母B。
char c = 'B'; //是ASCII码
char a = '你' //这种是错误的
2
3
C 语言规定,字符常量必须放在单引号里面。C 语言将其当作整数处理,所以字符类型就是宽度为一个字节的整数。每个字符对应一个整数(由 ASCII 码确定),比如B对应整数66。
字符类型在不同计算机的默认范围是不一样的。这两种范围正好都能覆盖0到127的 ASCII 字符范围。
- 有符号 char:一些系统默认为-128到127
- 无符号 char:另一些系统默认为0到255。
默认符号性:char 的符号性(有符号或无符号)取决于编译器的实现。如果需要明确符号性,可以使用 signed char 或 unsigned char。
只要在字符类型的范围之内,整数与字符是可以互换的,都可以赋值给字符类型的变量。
char c = 66;
// 等同于
char c = 'B';
2
3
上面示例中,变量c是字符类型,赋给它的值是整数66。这跟赋值为字符B的效果是一样的。
两个字符类型的变量可以进行数学运算。
char a = 'B'; // 等同于 char a = 66;
char b = 'C'; // 等同于 char b = 67;
printf("%d\n", a + b); // 输出 133
2
3
4
上面示例中,字符类型变量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
3
4
上面示例的四种写法都是等价的。
# 2.1.3 整数类型
| 类型 | 大小(通常) | 范围(通常) | 说明 |
|---|---|---|---|
char | 1 字节 | -128 到 127 或 0 到 255 | 字符类型,也可用于小整数 |
unsigned char | 1 字节 | 0 到 255 | 无符号字符类型 |
short | 2 字节 | -32768 到 32767 | 短整数 |
unsigned short | 2 字节 | 0 到 65535 | 无符号短整数 |
int | 4 字节 | -2147483648 到 2147483647 | 整数 |
unsigned int | 4 字节 | 0 到 4294967295 | 无符号整数 |
long | 4 或 8 字节 | -2147483648 到 2147483647(4 字节)或 -9223372036854775808 到 9223372036854775807(8 字节) | 长整数 |
unsigned long | 4 或 8 字节 | 0 到 4294967295(4 字节)或 0 到 18446744073709551615(8 字节) | 无符号长整数 |
long long | 8 字节 | -9223372036854775808 到 9223372036854775807 | 长长整数 |
unsigned long long | 8 字节 | 0 到 18446744073709551615 | 无符号长长整数 |
# 2.1.3.1 signed和unsigned
C 语言使用signed关键字,表示一个类型带有正负号,包含负值;使用unsigned关键字,表示该类型不带有正负号,只能表示零和正整数。
对于int类型,默认是带有正负号的,也就是说int等同于signed int。由于这是默认情况,关键字signed一般都省略不写,但是写了也不算错。、
signed int a;
// 等同于
int a;
2
3
int类型也可以不带正负号,只表示非负整数。这时就必须使用关键字unsigned声明变量。
unsigned int a;
整数变量声明为unsigned的好处是,同样长度的内存能够表示的最大整数值,增大了一倍。比如,16位的signed int最大值为32,767,而unsigned int的最大值增大到了65,535。
# 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;
2
3
上面代码分别声明了三种整数子类型的变量。
# 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
3
4
5
6
7
8
9
10
# 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
3
4
5
6
7
# 2.1.3.5 固定宽度整数
C99 标准引入了固定宽度整数类型,定义在 <stdint.h> 头文件中。 例如:
#include <stdint.h>
int8_t a; // 8 位有符号整数
uint16_t b; // 16 位无符号整数
int32_t c; // 32 位有符号整数
uint64_t d; // 64 位无符号整数
2
3
4
5
# 2.1.4 浮点型
任何有小数点的数值,都会被编译器解释为浮点数。所谓“浮点数”就是使用 m * be 的形式,存储一个数值,m是小数部分,b是基数(通常是2),e是指数部分。这种形式是精度和数值范围的一种结合,可以表示非常大或者非常小的数。
# 2.1.4.1 浮点型数据
C 语言提供了三种浮点类型:float、double 和 long double。以下是它们的详细说明。
| 类型 | 大小(通常) | 精度(通常) | 范围(通常) | 说明 |
|---|---|---|---|---|
float | 4 字节 | 6-7 位有效数字 | 约 ±3.4e-38 到 ±3.4e+38 | 单精度浮点数 |
double | 8 字节 | 15-16 位有效数字 | 约 ±1.7e-308 到 ±1.7e+308 | 双精度浮点数 |
long double | 10 或 16 字节 | 18-19 位有效数字 | 约 ±1.1e-4932 到 ±1.1e+4932 | 扩展精度浮点数 |
# 2.1.4.2 声明和初始化
声明浮点变量
float f;
double d;
long double ld;
2
3
初始化浮点变量
float f = 3.14f; // 使用 f 后缀表示 float 类型
double d = 3.14159; // 默认浮点类型是 double
long double ld = 3.1415926535L; // 使用 L 后缀表示 long double 类型
2
3
# 2.1.4.3 精度丢失问题
#include <stdio.h>
int main() {
float f = 0.1f;
double d = 0.1;
printf("float: %.20f\n", f); // 显示 20 位小数
printf("double: %.20lf\n", d);
return 0;
}
2
3
4
5
6
7
8
9
10
输出:
float: 0.10000000149011611938
double: 0.10000000000000000555
2
说明:浮点数在计算机中以二进制形式存储,无法精确表示某些十进制小数(如 0.1)。double 的精度高于 float。
# 2.1.4.4 科学计数法
#include <stdio.h>
int main() {
double d = 1.23e4; // 1.23 × 10^4
printf("Scientific notation: %e\n", d);
return 0;
}
2
3
4
5
6
7
输出:
Scientific notation: 1.230000e+04
# 2.1.4.5 注意事项
- 精度问题:
- 浮点数无法精确表示所有实数,可能存在舍入误差。
- 例如,
0.1在二进制中是一个无限循环小数,无法精确存储。
- 比较浮点数:
- 由于精度问题,直接比较两个浮点数是否相等可能不准确。
- 通常使用一个很小的误差范围(如
1e-9)进行比较:if (fabs(a - b) < 1e-9) { printf("a and b are equal\n"); }1
2
3
- 格式化输出:
- 使用正确的格式说明符输出浮点数:
%f:float和double%lf:double%Lf:long double%e:科学计数法%g:自动选择%f或%e
- 字面值后缀:
- 使用
f后缀表示float类型,L后缀表示long double类型。 - 例如:
float f = 3.14f; long double ld = 3.1415926535L;1
2
- 浮点数的范围:
- 浮点数的范围很大,但精度有限。
- 超出范围的值会导致溢出或下溢。
# 2.1.5 布尔类型
C 语言原来并没有为布尔值单独设置一个类型,而是使用整数0表示伪,所有非零值表示真。
int x = 1;
if (x) {
printf("x is true!\n");
}
2
3
4
上面示例中,变量x等于1,C 语言就认为这个值代表真,从而会执行判断体内部的代码。
C99 标准添加了类型_Bool,表示布尔值。但是,这个类型其实只是整数类型的别名,还是使用0表示伪,1表示真,下面是一个示例。
_Bool isNormal;
isNormal = 1;
if (isNormal)
printf("Everything is OK.\n");
2
3
4
5
头文件stdbool.h定义了另一个类型别名bool,并且定义了true代表1、false代表0。只要加载这个头文件,就可以使用这几个关键字。
# 2.1.6 综合案例与思考
综合案例:各种基本数据类型的大小和范围检测
#include <stdio.h>
#include <limits.h>
#include <float.h>
#include <stdbool.h>
int main() {
// 整数类型大小和范围
printf("=== 整数类型 ===\n");
printf("char: %zu字节, 范围[%d, %d]\n", sizeof(char), CHAR_MIN, CHAR_MAX);
printf("short: %zu字节, 范围[%d, %d]\n", sizeof(short), SHRT_MIN, SHRT_MAX);
printf("int: %zu字节, 范围[%d, %d]\n", sizeof(int), INT_MIN, INT_MAX);
printf("long: %zu字节, 范围[%ld, %ld]\n", sizeof(long), LONG_MIN, LONG_MAX);
printf("long long: %zu字节, 范围[%lld, %lld]\n", sizeof(long long), LLONG_MIN, LLONG_MAX);
// 浮点类型
printf("\n=== 浮点类型 ===\n");
printf("float: %zu字节, 精度%d位, 范围[%e, %e]\n", sizeof(float), FLT_DIG, FLT_MIN, FLT_MAX);
printf("double: %zu字节, 精度%d位, 范围[%e, %e]\n", sizeof(double), DBL_DIG, DBL_MIN, DBL_MAX);
// 布尔类型
printf("\n=== 布尔类型 ===\n");
bool flag = true;
printf("bool大小: %zu字节, true=%d, false=%d\n", sizeof(bool), true, false);
// 字符与整数互换
printf("\n=== 字符与整数 ===\n");
char ch = 'A';
printf("字符'%c'的ASCII码: %d\n", ch, ch);
printf("ASCII码65对应字符: %c\n", 65);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
原理说明:C语言中每种数据类型占用的内存大小取决于编译器和平台。limits.h 定义了整数类型的极限值,float.h 定义了浮点类型的极限值。在内存中,整数以补码形式存储(int 的最小值的绝对值比最大值大1),浮点数遵循 IEEE 754 标准以符号位+指数+尾数的形式存储。
思考题:
- 为什么
int类型在不同平台上大小可能不同?如何保证跨平台的数据类型一致性? char类型为什么既可以存储字符又可以存储整数?它的本质是什么?- 浮点数
0.1 + 0.2是否等于0.3?为什么?在实际编程中如何正确比较浮点数?
# 2.2 变量
变量(variable)可以理解成一块内存区域的名字。通过变量名,可以引用这块内存区域,获取里面存储的值。由于值可能发生变化,所以称为变量,否则就是常量了。
# 2.2.1 变量名
变量名在 C 语言里面属于标识符(identifier),命名有严格的规范。
- 只能由字母(包括大写和小写)、数字和下划线(_)组成。
- 不能以数字开头。
- 长度不能超过63个字符。
# 2.2.2 变量的声明
C 语言的变量,必须先声明后使用。如果一个变量没有声明,就直接使用,会报错。每个变量都有自己的类型(type)。声明变量时,必须把变量的类型告诉编译器。
int height;
上面代码声明了变量height,并且指定类型为int(整数)。
如果几个变量具有相同类型,可以在同一行声明。
int height, width;
// 等同于
int height;
int width;
2
3
4
5
注意,声明变量的语句必须以分号结尾。一旦声明,变量的类型就不能在运行时修改。
# 2.2.3 变量的赋值
C 语言会在变量声明时,就为它分配内存空间,但是不会清除内存里面原来的值。这导致声明变量以后,变量会是一个随机的值。所以,变量一定要赋值以后才能使用。
赋值操作通过赋值运算符(=)完成。
int num;
num = 42;
2
上面示例中,第一行声明了一个整数变量num,第二行给这个变量赋值。
变量的值应该与类型一致,不应该赋予不是同一个类型的值,比如num的类型是整数,就不应该赋值为小数。虽然 C 语言会自动转换类型,但是应该避免赋值运算符两侧的类型不一致。
变量的声明和赋值,也可以写在一行。
int num = 42;
多个相同类型变量的赋值,可以写在同一行。
int x = 1, y = 2;
注意,赋值表达式有返回值,等于等号右边的值。
# 2.2.4 变量的作用域
作用域(scope)指的是变量生效的范围。C 语言的变量作用域主要有两种:文件作用域(file scope)和块作用域(block scope)。
文件作用域(file scope)指的是,在源码文件顶层声明的变量,从声明的位置到文件结束都有效。
int x = 1;
int main(void) {
printf("%i\n", x);
}
2
3
4
5
上面示例中,变量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); // 出错
2
3
4
5
6
7
8
9
上面例子中,变量b是在if代码块里面声明的,所以对于大括号外面的代码,这个变量是不存在的。
# 2.2.5 综合案例与思考
综合案例:变量的声明、赋值和作用域演示
#include <stdio.h>
int global_var = 100; // 全局变量:整个文件可见
void testScope() {
int local_var = 50; // 局部变量:仅在函数内可见
printf("函数内 - global_var=%d, local_var=%d\n", global_var, local_var);
// 块作用域
{
int block_var = 25;
int local_var = 999; // 同名局部变量会遮蔽外层变量
printf("块内 - block_var=%d, local_var=%d(被遮蔽)\n", block_var, local_var);
}
// printf("%d", block_var); // 编译错误:block_var不可见
printf("块外 - local_var恢复为%d\n", local_var);
}
int main() {
// 声明并初始化
int x = 10, y = 20;
// 交换两个变量的值
int temp = x;
x = y;
y = temp;
printf("交换后: x=%d, y=%d\n", x, y);
testScope();
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
原理说明:变量的作用域决定了变量在哪些代码区域可见。全局变量存储在全局/静态区,程序运行期间一直存在;局部变量存储在栈上,函数返回后自动销毁。当内层作用域定义了与外层同名的变量时,内层变量会"遮蔽"(shadow)外层变量。
思考题:
- 未初始化的局部变量和全局变量,它们的默认值分别是什么?为什么不同?
- 为什么说"变量一定要赋值以后才能使用"?使用未初始化的变量会发生什么?
- 全局变量使用方便,为什么不建议大量使用全局变量?
# 2.3 常量
程序运行的过程中,其值永远不会发生改变的数据
# 2.3.1 const常量
const说明符表示变量是只读的,不得被修改。示例里面的const,表示变量PI的值不应改变。如果改变的话,编译器会报错。
const double PI = 3.14159;
PI = 3; // 报错
2
对于数组,const表示数组成员不能修改。示例中,const使得数组arr的成员无法修改。
const int arr[] = {1, 2, 3, 4};
arr[0] = 5; // 报错
2
对于指针变量,const有两种写法,含义是不一样的。如果const在*前面,表示指针指向的值不可修改。示例中,对x指向的值进行修改导致报错。
// const 表示指向的值 *x 不能修改
int const * x
// 或者
const int * x
// 案例
int p = 1
const int* x = &p;
(*x)++; // 报错
2
3
4
5
6
7
8
9
如果const在*后面,表示指针包含的地址不可修改。示例中,对x进行修改导致报错。
// const 表示地址 x 不能修改
int* const x
// 案例
int p = 1
int* const x = &p;
x++; // 报错
2
3
4
5
6
7
8
const的一个用途,就是防止函数体内修改函数参数。如果某个参数在函数体内不会被修改,可以在函数声明时,对该参数添加const说明符。这样的话,使用这个函数的人看到原型里面的const,就知道调用函数前后,参数数组保持不变。
void find(const int* arr, int n);
上面示例中,函数find的参数数组arr有const说明符,就说明该数组在函数内部将保持不变。
# 2.3.2 static常量
static说明符对于全局变量和局部变量有不同的含义。
1)用于局部变量(位于块作用域内部)。
static用于函数内部声明的局部变量时,表示该变量的值会在函数每次执行后得到保留,下次执行时不会进行初始化,就类似于一个只用于函数内部的全局变量。由于不必每次执行函数时,都对该变量进行初始化,这样可以提高函数的执行速度,详见《函数》一章。
2)用于全局变量(位于块作用域外部)。
static用于函数外部声明的全局变量时,表示该变量只用于当前文件,其他源码文件不可以引用该变量,即该变量不会被链接(link)。
static修饰的变量,初始化时,值不能等于变量,必须是常量。
int n = 10;
static m = n; // 报错
2
上面示例中,变量m有static修饰,它的值如果等于变量n,就会报错,必须等于常量。
只在当前文件里面使用的函数,也可以声明为static,表明该函数只在当前文件使用,其他文件可以定义同名函数。
static int g(int i);
# 2.3.3 综合案例与思考
综合案例:const和static的使用对比
#include <stdio.h>
const int MAX_SIZE = 100; // const全局常量
void counter() {
static int count = 0; // static局部变量:值在调用间保留
count++;
printf("第%d次调用counter()\n", count);
}
int main() {
// const不可修改
// MAX_SIZE = 200; // 编译错误
// const指针的两种用法
int a = 10, b = 20;
const int *p1 = &a; // 指向的值不可变
int * const p2 = &a; // 指针本身不可变
// *p1 = 30; // 错误:不能通过p1修改值
p1 = &b; // 正确:可以改变p1指向的地址
*p2 = 30; // 正确:可以通过p2修改值
// p2 = &b; // 错误:不能改变p2指向的地址
// static演示
for (int i = 0; i < 3; i++) {
counter();
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
思考题:
#define PI 3.14和const double PI = 3.14有什么区别?各有什么优缺点?static修饰局部变量时,该变量存储在内存的哪个区域?生命周期是多久?- 如何定义一个"既不能修改指向的值,也不能修改指针本身"的指针?
# 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
2
3
4
5
上面示例中,假定程序里面需要三种颜色,就可以使用enum命令,把这三种颜色定义成一种枚举类型colors,它只有三种取值可能RED、GREEN、BLUE。这时,这三个名字自动成为整数常量,编译器默认将它们的值设为数字0、1、2。相比之下,RED要比0的可读性好了许多。
注意,Enum 内部的常量名,遵守标识符的命名规范,但是通常都使用大写。
# 2.4.2 枚举别名
typedef 命令可以为 Enum 类型起别名。
typedef enum {
SHEEP,
WHEAT,
WOOD,
BRICK,
ORE
} RESOURCE;
RESOURCE r;
2
3
4
5
6
7
8
9
上面示例中,RESOURCE 是 Enum 类型的别名。声明变量时,使用这个别名即可。
# 2.4.3 枚举常量
声明 Enum 类型时,在同一行里面为变量赋值。
enum {
SHEEP,
WHEAT,
WOOD,
BRICK,
ORE
} r = BRICK, s = WOOD;
2
3
4
5
6
7
上面示例中,r的值是3,s的值是2。
由于 Enum 的属性会自动声明为常量,所以有时候使用 Enum 的目的,不是为了自定义一种数据类型,而是为了声明一组常量。这时就可以使用下面这种写法,比较简单。
enum { ONE, TWO };
printf("%d %d", ONE, TWO); // 0 1
2
3
上面示例中,enum是一个关键字,后面跟着一个代码块,常量就在代码内声明。ONE和TWO就是两个 Enum 常量。
由于Enum 会自动编号,因此可以不必为常量赋值。C 语言会自动从0开始递增,为常量赋值。但是,C 语言也允许为 ENUM 常量指定值,不过只能指定为整数,不能是其他类型。因此,任何可以使用整数的场合,都可以使用 Enum 常量。
enum { ONE = 1, TWO = 2 };
printf("%d %d", ONE, TWO); // 1 2
2
3
如果一组常量之中,有些指定了值,有些没有指定。那么,没有指定值的常量会从上一个指定了值的常量,开始自动递增赋值。
enum {
A, // 0
B, // 1
C = 4, // 4
D, // 5
E, // 6
F = 3, // 3
G, // 4
H // 5
};
2
3
4
5
6
7
8
9
10
Enum 的作用域与变量相同。如果是在顶层声明,那么在整个文件内都有效;如果是在代码块内部声明,则只对该代码块有效。如果与使用int声明的常量相比,Enum 的好处是更清晰地表示代码意图。
# 2.4.4 综合案例与思考
综合案例:用枚举实现简单的状态机
#include <stdio.h>
// 定义交通灯状态
typedef enum {
RED = 0,
YELLOW = 1,
GREEN = 2
} TrafficLight;
const char* getLightName(TrafficLight light) {
switch (light) {
case RED: return "红灯";
case YELLOW: return "黄灯";
case GREEN: return "绿灯";
default: return "未知";
}
}
TrafficLight nextLight(TrafficLight current) {
return (current + 1) % 3; // 循环切换:红->黄->绿->红
}
int main() {
TrafficLight light = RED;
for (int i = 0; i < 6; i++) {
printf("当前: %s (值=%d)\n", getLightName(light), light);
light = nextLight(light);
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
原理说明:枚举本质上是整数常量的集合,编译器会将枚举值替换为对应的整数。枚举的好处是提高代码可读性,让代码意图更明确。typedef 配合 enum 可以简化类型声明。
思考题:
- 枚举类型本质上是什么类型?能否对枚举变量赋一个不在枚举列表中的整数值?
enum { A, B=5, C, D }中 C 和 D 的值分别是多少?为什么?- 枚举和
#define定义常量相比,有什么优势?
# 2.5 字符串类型
C 标准库提供了许多字符串操作函数,定义在 <string.h> 头文件中。
# 2.5.1 字符串介绍
在 C 语言中,字符串 是由字符组成的数组,以空字符 \0 结尾。字符串是 C 语言中处理文本数据的基本方式。
由于 C 语言没有内置的字符串类型,字符串通常通过字符数组或字符指针来表示。
# 2.5.2 字符串声明
字符数组:字符串可以通过字符数组来存储,数组的最后一个元素必须是 \0,表示字符串的结束。示例:
char str[] = "Hello, World!";
//等价于:
char str[] = {'H', 'e', 'l', 'l', 'o', ',', ' ', 'W', 'o', 'r', 'l', 'd', '!', '\0'};
2
3
字符指针: 字符串也可以通过字符指针来指向。示例:
const char *str = "Hello, World!";
char *ptr = "Hello";
2
# 2.5.3 字符串长度
使用 strlen 函数获取字符串长度(不包括 \0):
size_t strlen(const char *str);
示例:
char str[] = "Hello";
int len = strlen(str); // len = 5
2
# 2.5.4 字符串复制
使用 strcpy 或 strncpy 函数复制字符串:
char *strcpy(char *dest, const char *src); // 复制整个字符串
char *strncpy(char *dest, const char *src, size_t n); // 复制前 n 个字符
2
示例:
char dest[20];
strcpy(dest, "Hello"); // dest = "Hello"
strncpy(dest, "World", 3); // dest = "Worlo"
2
3
# 2.5.5 字符串部分复制
# 2.5.6 字符串连接
使用 strcat 或 strncat 函数连接字符串:
char *strcat(char *dest, const char *src); // 连接整个字符串
char *strncat(char *dest, const char *src, size_t n); // 连接前 n 个字符
2
示例:
char dest[20] = "Hello";
strcat(dest, " World"); // dest = "Hello World"
strncat(dest, "!!!", 2); // dest = "Hello World!!"
2
3
# 2.5.7 字符串比较
使用 strcmp 或 strncmp 函数比较字符串:
int strcmp(const char *str1, const char *str2); // 比较整个字符串
int strncmp(const char *str1, const char *str2, size_t n); // 比较前 n 个字符
2
返回值:
- 如果
str1小于str2,返回负值。 - 如果
str1等于str2,返回 0。 - 如果
str1大于str2,返回正值。
示例:
int result = strcmp("apple", "banana"); // result < 0
# 2.5.8 字符串输入
输出字符串:使用 printf 函数输出字符串
printf("%s\n", str);
输入字符串:使用 scanf 或 gets 函数输入字符串
char str[100];
scanf("%s", str); // 输入字符串(遇到空格停止)
gets(str); // 输入一行字符串(不推荐使用,存在安全隐患)
2
3
注意:
scanf遇到空格会停止输入。gets不检查缓冲区大小,可能导致缓冲区溢出,建议使用fgets。
推荐使用 fgets:
char str[100];
fgets(str, sizeof(str), stdin); // 安全地输入一行字符串
2
# 2.5.9 字符串查找
使用 strchr 或 strstr 函数查找字符或子字符串:
char *strchr(const char *str, int c); // 查找字符 c 第一次出现的位置
char *strstr(const char *haystack, const char *needle); // 查找子字符串 needle
2
示例:
char *p = strchr("Hello", 'e'); // p 指向 'e'
char *q = strstr("Hello, World!", "World"); // q 指向 "World"
2
# 2.5.10 字符串转整数
C 标准库提供了将字符串转换为数值的函数,定义在 <stdlib.h> 头文件中。
使用 atoi 或 strtol 函数:
int atoi(const char *str); // 将字符串转换为整数
long strtol(const char *str, char **endptr, int base); // 将字符串转换为长整数
2
示例:
int num = atoi("123"); // num = 123
# 2.5.11 字符串转浮点数
使用 atof 或 strtod 函数:
double atof(const char *str); // 将字符串转换为浮点数
double strtod(const char *str, char **endptr); // 将字符串转换为双精度浮点数
2
示例:
double num = atof("3.14"); // num = 3.14
# 2.5.12 综合案例与思考
综合案例:实现一个简单的字符串工具库
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
// 字符串转大写
void toUpperCase(char *str) {
for (int i = 0; str[i] != '\0'; i++) {
str[i] = toupper(str[i]);
}
}
// 统计字符串中某字符出现次数
int countChar(const char *str, char target) {
int count = 0;
for (int i = 0; str[i] != '\0'; i++) {
if (str[i] == target) count++;
}
return count;
}
// 字符串反转
void reverseString(char *str) {
int len = strlen(str);
for (int i = 0; i < len / 2; i++) {
char temp = str[i];
str[i] = str[len - 1 - i];
str[len - 1 - i] = temp;
}
}
int main() {
char str[100] = "Hello, World!";
printf("原始: %s, 长度: %zu\n", str, strlen(str));
printf("'l'出现次数: %d\n", countChar(str, 'l'));
reverseString(str);
printf("反转: %s\n", str);
reverseString(str); // 恢复
toUpperCase(str);
printf("大写: %s\n", str);
// 字符串转数字
int num = atoi("12345");
double pi = atof("3.14159");
printf("字符串转整数: %d, 转浮点: %f\n", num, pi);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
思考题:
- C语言为什么没有内置的字符串类型?字符串
"Hello"在内存中实际占用几个字节? char str[] = "Hello"和char *str = "Hello"有什么区别?哪个可以修改内容?- 为什么
gets()函数不推荐使用?fgets()是如何解决安全问题的?
# 2.7 数据溢出
# 2.7.1 整数溢出
#include <stdio.h>
int main() {
int a = 2147483647; // int 的最大值
a = a + 1; // 溢出
printf("Overflowed value: %d\n", a);
return 0;
}
2
3
4
5
6
7
8
输出内容:
Overflowed value: -2147483648
# 2.7.2 综合案例与思考
综合案例:演示各种数据溢出和类型转换问题
#include <stdio.h>
#include <limits.h>
int main() {
// 整数溢出
int max_int = INT_MAX;
printf("INT_MAX = %d\n", max_int);
printf("INT_MAX + 1 = %d (溢出!)\n", max_int + 1);
// 无符号整数下溢
unsigned int u = 0;
printf("unsigned 0 - 1 = %u (下溢!)\n", u - 1);
// 隐式类型转换陷阱
int a = -1;
unsigned int b = 1;
if (a < b) {
printf("-1 < 1: 正确\n");
} else {
printf("-1 >= 1: 因为隐式转换为unsigned,-1变成了%u\n", (unsigned int)a);
}
// 整数除法精度丢失
int x = 7, y = 2;
printf("7 / 2 = %d (整数除法截断)\n", x / y);
printf("7.0 / 2 = %f (浮点除法)\n", 7.0 / 2);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
原理说明:整数在内存中以补码形式存储。int 最大值 2147483647(0x7FFFFFFF)加 1 后变为 0x80000000,即补码表示的最小负数 -2147483648。无符号整数 0 减 1 会因为回绕变成 UINT_MAX。有符号数和无符号数混合运算时,有符号数会被隐式转换为无符号数,这是许多bug的根源。
思考题:
- 为什么
INT_MAX + 1会变成负数?用补码的角度解释这个现象。 - 有符号整数和无符号整数比较时,为什么
-1 > 1u是成立的? - 如何在编程中避免整数溢出?有哪些检测溢出的方法?