编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

    • 入门教程

      • README
      • 基础语法
      • 数据类型
        • 2.1 基本数据
          • 2.1.1 基本数据类型
          • 2.1.2 字符类型char
          • 2.1.3 整数类型
          • 2.1.3.1 signed和unsigned
          • 2.1.3.2 整数的子类型
          • 2.1.3.3 整数类型极限值
          • 2.1.3.4 整数的进制
          • 2.1.3.5 固定宽度整数
          • 2.1.4 浮点型
          • 2.1.4.1 浮点型数据
          • 2.1.4.2 声明和初始化
          • 2.1.4.3 精度丢失问题
          • 2.1.4.4 科学计数法
          • 2.1.4.5 注意事项
          • 2.1.5 布尔类型
          • 2.1.6 综合案例与思考
        • 2.2 变量
          • 2.2.1 变量名
          • 2.2.2 变量的声明
          • 2.2.3 变量的赋值
          • 2.2.4 变量的作用域
          • 2.2.5 综合案例与思考
        • 2.3 常量
          • 2.3.1 const常量
          • 2.3.2 static常量
          • 2.3.3 综合案例与思考
        • 2.4 枚举和派生类型
          • 2.4.1 枚举定义
          • 2.4.2 枚举别名
          • 2.4.3 枚举常量
          • 2.4.4 综合案例与思考
        • 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.5.10 字符串转整数
          • 2.5.11 字符串转浮点数
          • 2.5.12 综合案例与思考
        • 2.7 数据溢出
          • 2.7.1 整数溢出
          • 2.7.2 综合案例与思考
      • 运算符
      • 循环和选择
      • 输入输出
      • 函数
      • 指针
      • 数组和容器
      • 类和内存
      • 流与文件
      • 结构体
      • 线程和锁
      • 预处理器
      • 高级数据
    • 综合案例

    • 专栏博客

    • 标准集库

  • Cpp入门到精通

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

  • CodeX
  • C语言入门精通
  • 入门教程
杨充
2025-07-21
目录

数据类型

# 02.数据类型

# 目录介绍

  • 2.1 基本数据
    • 2.1.1 基本数据类型
    • 2.1.2 字符类型char
    • 2.1.3 整数类型
      • 2.1.3.1 signed和unsigned
      • 2.1.3.2 整数的子类型
      • 2.1.3.3 整数类型极限值
      • 2.1.3.4 整数的进制
      • 2.1.3.5 固定宽度整数
    • 2.1.4 浮点型
      • 2.1.4.1 浮点型数据
      • 2.1.4.2 声明和初始化
      • 2.1.4.3 精度丢失问题
      • 2.1.4.4 科学计数法
      • 2.1.4.5 注意事项
    • 2.1.5 布尔类型
    • 2.1.6 综合案例与思考
  • 2.2 变量
    • 2.2.1 变量名
    • 2.2.2 变量的声明
    • 2.2.3 变量的赋值
    • 2.2.4 变量的作用域
    • 2.2.5 综合案例与思考
  • 2.3 常量
    • 2.3.1 const常量
    • 2.3.2 static常量
    • 2.3.3 综合案例与思考
  • 2.4 枚举和派生类型
    • 2.4.1 枚举定义
    • 2.4.2 枚举别名
    • 2.4.3 枚举常量
    • 2.4.4 综合案例与思考
  • 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.5.10 字符串转整数
    • 2.5.11 字符串转浮点数
    • 2.5.12 综合案例与思考
  • 2.7 数据溢出
    • 2.7.1 整数溢出
    • 2.7.2 综合案例与思考

# 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 = '你'  //这种是错误的
1
2
3

C 语言规定,字符常量必须放在单引号里面。C 语言将其当作整数处理,所以字符类型就是宽度为一个字节的整数。每个字符对应一个整数(由 ASCII 码确定),比如B对应整数66。

字符类型在不同计算机的默认范围是不一样的。这两种范围正好都能覆盖0到127的 ASCII 字符范围。

  1. 有符号 char:一些系统默认为-128到127
  2. 无符号 char:另一些系统默认为0到255。

默认符号性:char 的符号性(有符号或无符号)取决于编译器的实现。如果需要明确符号性,可以使用 signed char 或 unsigned char。

只要在字符类型的范围之内,整数与字符是可以互换的,都可以赋值给字符类型的变量。

char c = 66;
// 等同于
char c = 'B';
1
2
3

上面示例中,变量c是字符类型,赋给它的值是整数66。这跟赋值为字符B的效果是一样的。

两个字符类型的变量可以进行数学运算。

char a = 'B'; // 等同于 char a = 66;
char b = 'C'; // 等同于 char b = 67;

printf("%d\n", a + b); // 输出 133
1
2
3
4

上面示例中,字符类型变量a和b相加,视同两个整数相加。占位符%d表示输出十进制整数,因此输出结果为133。

单引号本身也是一个字符,如果要表示这个字符常量,必须使用反斜杠转义。

char t = '\'';
1

上面示例中,变量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'; // 十六进制
1
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;
1
2
3

int类型也可以不带正负号,只表示非负整数。这时就必须使用关键字unsigned声明变量。

unsigned int a;
1

整数变量声明为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;
1
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 的最大值。
1
2
3
4
5
6
7
8
9
10

# 2.1.3.4 整数的进制

C 语言的整数默认都是十进制数,如果要表示八进制数和十六进制数,必须使用专门的表示法。

八进制使用0作为前缀,比如017、0377。

int a = 012; // 八进制,相当于十进制的10
1

十六进制使用0x或0X作为前缀,比如0xf、0X10。

int a = 0x1A2B; // 十六进制,相当于十进制的6699
1

有些编译器使用0b前缀,表示二进制数,但不是标准。

int x = 0b101010;
1

注意,不同的进制只是整数的书写方法,不会对整数的实际存储方式产生影响。所有整数都是二进制形式存储,跟书写方式无关。不同进制可以混合使用,比如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
1
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 位无符号整数
1
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;
1
2
3

初始化浮点变量

float f = 3.14f;       // 使用 f 后缀表示 float 类型
double d = 3.14159;    // 默认浮点类型是 double
long double ld = 3.1415926535L; // 使用 L 后缀表示 long double 类型
1
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;
}
1
2
3
4
5
6
7
8
9
10

输出:

float: 0.10000000149011611938
double: 0.10000000000000000555
1
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;
}
1
2
3
4
5
6
7

输出:

Scientific notation: 1.230000e+04
1

# 2.1.4.5 注意事项

  1. 精度问题:
  • 浮点数无法精确表示所有实数,可能存在舍入误差。
  • 例如,0.1 在二进制中是一个无限循环小数,无法精确存储。
  1. 比较浮点数:
  • 由于精度问题,直接比较两个浮点数是否相等可能不准确。
  • 通常使用一个很小的误差范围(如 1e-9)进行比较:
    if (fabs(a - b) < 1e-9) {
        printf("a and b are equal\n");
    }
    
    1
    2
    3
  1. 格式化输出:
  • 使用正确的格式说明符输出浮点数:
    • %f:float 和 double
    • %lf:double
    • %Lf:long double
    • %e:科学计数法
    • %g:自动选择 %f 或 %e
  1. 字面值后缀:
  • 使用 f 后缀表示 float 类型,L 后缀表示 long double 类型。
  • 例如:
    float f = 3.14f;
    long double ld = 3.1415926535L;
    
    1
    2
  1. 浮点数的范围:
  • 浮点数的范围很大,但精度有限。
  • 超出范围的值会导致溢出或下溢。

# 2.1.5 布尔类型

C 语言原来并没有为布尔值单独设置一个类型,而是使用整数0表示伪,所有非零值表示真。

int x = 1;
if (x) {
    printf("x is true!\n");
}
1
2
3
4

上面示例中,变量x等于1,C 语言就认为这个值代表真,从而会执行判断体内部的代码。

C99 标准添加了类型_Bool,表示布尔值。但是,这个类型其实只是整数类型的别名,还是使用0表示伪,1表示真,下面是一个示例。

_Bool isNormal;

isNormal = 1;
if (isNormal)
  printf("Everything is OK.\n");
1
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;
}
1
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 标准以符号位+指数+尾数的形式存储。

思考题:

  1. 为什么 int 类型在不同平台上大小可能不同?如何保证跨平台的数据类型一致性?
  2. char 类型为什么既可以存储字符又可以存储整数?它的本质是什么?
  3. 浮点数 0.1 + 0.2 是否等于 0.3?为什么?在实际编程中如何正确比较浮点数?

# 2.2 变量

变量(variable)可以理解成一块内存区域的名字。通过变量名,可以引用这块内存区域,获取里面存储的值。由于值可能发生变化,所以称为变量,否则就是常量了。

# 2.2.1 变量名

变量名在 C 语言里面属于标识符(identifier),命名有严格的规范。

  1. 只能由字母(包括大写和小写)、数字和下划线(_)组成。
  2. 不能以数字开头。
  3. 长度不能超过63个字符。

# 2.2.2 变量的声明

C 语言的变量,必须先声明后使用。如果一个变量没有声明,就直接使用,会报错。每个变量都有自己的类型(type)。声明变量时,必须把变量的类型告诉编译器。

int height;
1

上面代码声明了变量height,并且指定类型为int(整数)。

如果几个变量具有相同类型,可以在同一行声明。

int height, width;

// 等同于
int height;
int width;
1
2
3
4
5

注意,声明变量的语句必须以分号结尾。一旦声明,变量的类型就不能在运行时修改。

# 2.2.3 变量的赋值

C 语言会在变量声明时,就为它分配内存空间,但是不会清除内存里面原来的值。这导致声明变量以后,变量会是一个随机的值。所以,变量一定要赋值以后才能使用。

赋值操作通过赋值运算符(=)完成。

int num;
num = 42;
1
2

上面示例中,第一行声明了一个整数变量num,第二行给这个变量赋值。

变量的值应该与类型一致,不应该赋予不是同一个类型的值,比如num的类型是整数,就不应该赋值为小数。虽然 C 语言会自动转换类型,但是应该避免赋值运算符两侧的类型不一致。

变量的声明和赋值,也可以写在一行。

int num = 42;
1

多个相同类型变量的赋值,可以写在同一行。

int x = 1, y = 2;
1

注意,赋值表达式有返回值,等于等号右边的值。

# 2.2.4 变量的作用域

作用域(scope)指的是变量生效的范围。C 语言的变量作用域主要有两种:文件作用域(file scope)和块作用域(block scope)。

文件作用域(file scope)指的是,在源码文件顶层声明的变量,从声明的位置到文件结束都有效。

int x = 1;

int main(void) {
  printf("%i\n", x);
}
1
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);  // 出错
1
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;
}
1
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)外层变量。

思考题:

  1. 未初始化的局部变量和全局变量,它们的默认值分别是什么?为什么不同?
  2. 为什么说"变量一定要赋值以后才能使用"?使用未初始化的变量会发生什么?
  3. 全局变量使用方便,为什么不建议大量使用全局变量?

# 2.3 常量

程序运行的过程中,其值永远不会发生改变的数据

# 2.3.1 const常量

const说明符表示变量是只读的,不得被修改。示例里面的const,表示变量PI的值不应改变。如果改变的话,编译器会报错。

const double PI = 3.14159;
PI = 3; // 报错
1
2

对于数组,const表示数组成员不能修改。示例中,const使得数组arr的成员无法修改。

const int arr[] = {1, 2, 3, 4};
arr[0] = 5; // 报错
1
2

对于指针变量,const有两种写法,含义是不一样的。如果const在*前面,表示指针指向的值不可修改。示例中,对x指向的值进行修改导致报错。

// const 表示指向的值 *x 不能修改
int const * x
// 或者
const int * x

// 案例
int p = 1
const int* x = &p;
(*x)++; // 报错
1
2
3
4
5
6
7
8
9

如果const在*后面,表示指针包含的地址不可修改。示例中,对x进行修改导致报错。

// const 表示地址 x 不能修改
int* const x

// 案例
int p = 1
int* const x = &p;

x++; // 报错
1
2
3
4
5
6
7
8

const的一个用途,就是防止函数体内修改函数参数。如果某个参数在函数体内不会被修改,可以在函数声明时,对该参数添加const说明符。这样的话,使用这个函数的人看到原型里面的const,就知道调用函数前后,参数数组保持不变。

void find(const int* arr, int n);
1

上面示例中,函数find的参数数组arr有const说明符,就说明该数组在函数内部将保持不变。

# 2.3.2 static常量

static说明符对于全局变量和局部变量有不同的含义。

1)用于局部变量(位于块作用域内部)。

static用于函数内部声明的局部变量时,表示该变量的值会在函数每次执行后得到保留,下次执行时不会进行初始化,就类似于一个只用于函数内部的全局变量。由于不必每次执行函数时,都对该变量进行初始化,这样可以提高函数的执行速度,详见《函数》一章。

2)用于全局变量(位于块作用域外部)。

static用于函数外部声明的全局变量时,表示该变量只用于当前文件,其他源码文件不可以引用该变量,即该变量不会被链接(link)。

static修饰的变量,初始化时,值不能等于变量,必须是常量。

int n = 10;
static m = n; // 报错
1
2

上面示例中,变量m有static修饰,它的值如果等于变量n,就会报错,必须等于常量。

只在当前文件里面使用的函数,也可以声明为static,表明该函数只在当前文件使用,其他文件可以定义同名函数。

static int g(int i);
1

# 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;
}
1
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

思考题:

  1. #define PI 3.14 和 const double PI = 3.14 有什么区别?各有什么优缺点?
  2. static 修饰局部变量时,该变量存储在内存的哪个区域?生命周期是多久?
  3. 如何定义一个"既不能修改指向的值,也不能修改指针本身"的指针?

# 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
1
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;
1
2
3
4
5
6
7
8
9

上面示例中,RESOURCE 是 Enum 类型的别名。声明变量时,使用这个别名即可。

# 2.4.3 枚举常量

声明 Enum 类型时,在同一行里面为变量赋值。

enum {
SHEEP,
WHEAT,
WOOD,
BRICK,
ORE
} r = BRICK, s = WOOD;
1
2
3
4
5
6
7

上面示例中,r的值是3,s的值是2。

由于 Enum 的属性会自动声明为常量,所以有时候使用 Enum 的目的,不是为了自定义一种数据类型,而是为了声明一组常量。这时就可以使用下面这种写法,比较简单。

enum { ONE, TWO };

printf("%d %d", ONE, TWO);  // 0 1
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
1
2
3

如果一组常量之中,有些指定了值,有些没有指定。那么,没有指定值的常量会从上一个指定了值的常量,开始自动递增赋值。

enum {
  A,    // 0
  B,    // 1
  C = 4,  // 4
  D,    // 5
  E,    // 6
  F = 3,   // 3
  G,    // 4
  H     // 5
};
1
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;
}
1
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 可以简化类型声明。

思考题:

  1. 枚举类型本质上是什么类型?能否对枚举变量赋一个不在枚举列表中的整数值?
  2. enum { A, B=5, C, D } 中 C 和 D 的值分别是多少?为什么?
  3. 枚举和 #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'};
1
2
3

字符指针: 字符串也可以通过字符指针来指向。示例:

const char *str = "Hello, World!";
char *ptr = "Hello";
1
2

# 2.5.3 字符串长度

使用 strlen 函数获取字符串长度(不包括 \0):

size_t strlen(const char *str);
1

示例:

char str[] = "Hello";
int len = strlen(str); // len = 5
1
2

# 2.5.4 字符串复制

使用 strcpy 或 strncpy 函数复制字符串:

char *strcpy(char *dest, const char *src); // 复制整个字符串
char *strncpy(char *dest, const char *src, size_t n); // 复制前 n 个字符
1
2

示例:

char dest[20];
strcpy(dest, "Hello"); // dest = "Hello"
strncpy(dest, "World", 3); // dest = "Worlo"
1
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 个字符
1
2

示例:

char dest[20] = "Hello";
strcat(dest, " World"); // dest = "Hello World"
strncat(dest, "!!!", 2); // dest = "Hello World!!"
1
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 个字符
1
2

返回值:

  • 如果 str1 小于 str2,返回负值。
  • 如果 str1 等于 str2,返回 0。
  • 如果 str1 大于 str2,返回正值。

示例:

int result = strcmp("apple", "banana"); // result < 0
1

# 2.5.8 字符串输入

输出字符串:使用 printf 函数输出字符串

printf("%s\n", str);
1

输入字符串:使用 scanf 或 gets 函数输入字符串

char str[100];
scanf("%s", str); // 输入字符串(遇到空格停止)
gets(str);        // 输入一行字符串(不推荐使用,存在安全隐患)
1
2
3

注意:

  • scanf 遇到空格会停止输入。
  • gets 不检查缓冲区大小,可能导致缓冲区溢出,建议使用 fgets。

推荐使用 fgets:

char str[100];
fgets(str, sizeof(str), stdin); // 安全地输入一行字符串
1
2

# 2.5.9 字符串查找

使用 strchr 或 strstr 函数查找字符或子字符串:

char *strchr(const char *str, int c); // 查找字符 c 第一次出现的位置
char *strstr(const char *haystack, const char *needle); // 查找子字符串 needle
1
2

示例:

char *p = strchr("Hello", 'e'); // p 指向 'e'
char *q = strstr("Hello, World!", "World"); // q 指向 "World"
1
2

# 2.5.10 字符串转整数

C 标准库提供了将字符串转换为数值的函数,定义在 <stdlib.h> 头文件中。

使用 atoi 或 strtol 函数:

int atoi(const char *str); // 将字符串转换为整数
long strtol(const char *str, char **endptr, int base); // 将字符串转换为长整数
1
2

示例:

int num = atoi("123"); // num = 123
1

# 2.5.11 字符串转浮点数

使用 atof 或 strtod 函数:

double atof(const char *str); // 将字符串转换为浮点数
double strtod(const char *str, char **endptr); // 将字符串转换为双精度浮点数
1
2

示例:

double num = atof("3.14"); // num = 3.14
1

# 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;
}
1
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

思考题:

  1. C语言为什么没有内置的字符串类型?字符串 "Hello" 在内存中实际占用几个字节?
  2. char str[] = "Hello" 和 char *str = "Hello" 有什么区别?哪个可以修改内容?
  3. 为什么 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;
}
1
2
3
4
5
6
7
8

输出内容:

Overflowed value: -2147483648
1

# 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;
}
1
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的根源。

思考题:

  1. 为什么 INT_MAX + 1 会变成负数?用补码的角度解释这个现象。
  2. 有符号整数和无符号整数比较时,为什么 -1 > 1u 是成立的?
  3. 如何在编程中避免整数溢出?有哪些检测溢出的方法?
上次更新: 2026/06/10, 11:13:41
基础语法
运算符

← 基础语法 运算符→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式