12.C 语言变量入门:声明、基本数据类型与 sizeof
文章摘要: 本文从变量的名称、类型和值出发,介绍变量定义、初始化和赋值,整理命名规则及常见基本类型,解释 char、short、int、long、long long、浮点类型与布尔类型的用途。重点区分 C 标准保证与具体平台的大小,讲解 CHAR_BIT、整数范围、sizeof、size_t 和数组大小,并提供能够直接运行的数据类型观察程序。
常量让程序拥有固定数据,变量则让程序保存并更新运行过程中的状态。理解变量,不能只背“某个类型占几字节”,还要知道它能表示什么、值如何改变,以及在当前平台上怎样验证。本文采用 C11/C17 语法。
一、变量包含哪些信息
int age = 20;
这一行定义一个名为 age 的对象,类型为 int,初始值为 20。
可以从四个角度观察:
| 信息 | 示例 | 作用 |
|---|---|---|
| 名称 | age | 在作用域内引用对象 |
| 类型 | int | 规定值的范围与可进行的运算 |
| 当前值 | 20 | 表示当前保存的数据 |
| 存储与生命周期 | 由定义位置和声明方式决定 | 决定对象何时存在、如何访问 |
教学中常说“定义变量就是申请一块内存”。这有助于入门,但编译器可能把变量保存在寄存器中、消除无用变量,或直接计算结果。语言层面保证的是程序可观察的对象行为,不是每个变量都必须单独对应一块物理内存。
二、定义、初始化与赋值
1. 定义一个变量
int count;
在函数内部,这定义了一个普通自动存储期对象,但没有显式初始化。不能因为变量已经存在,就认为它的值自动等于零。
2. 定义时初始化
int count = 0;
变量在定义时得到初始值 0,后面可以安全读取并修改。
3. 定义后赋值
count = 10;
count = count + 1;
第一行把已有变量改为 10;第二行读取旧值 10,加一后写回,结果是 11。
= 是赋值运算符,不是数学中“左右永远相等”的关系。count = count + 1 在程序里很自然,表示状态更新。
4. 一个完整示例
#include <stdio.h>
int main(void)
{
int first = 10;
int second = 20;
int sum = first + second;
printf("%d + %d = %d\n", first, second, sum);
first = 100;
printf("first = %d, sum = %d\n", first, sum);
return 0;
}
输出:
10 + 20 = 30
first = 100, sum = 30
sum 保存初始化那一刻算出的 30,不会因为 first 后来变成 100 就自动变成 120。想更新结果,需要再次执行 sum = first + second;。
三、声明与定义的区别
声明告诉编译器名称与类型;定义通常还建立对象。
int total = 0; /* 声明,同时也是定义 */
而下面的形式通常用于引用定义在其他位置的对象:
extern int total; /* 声明,不在这一行定义存储对象 */
初学者写在函数里的 int count; 是定义。后续多文件程序中,才需要更仔细地区分外部声明与定义,避免重复定义或找不到符号。
不应把“声明”统一解释为申请存储,也不应把每个 extern 都解释为永远不定义对象;例如带初始化的文件作用域声明还需要按具体规则判断。这里先掌握最常见的两种写法。
四、变量命名规则
对于常用的 ASCII 标识符写法:
- 使用字母、数字和下划线;
- 第一位不能是数字;
- 大小写有区别;
- 不能使用语言关键字。
合法示例:
int age;
int sensor_value;
int count2;
double average_height;
非法示例可按原因分类:
| 名称 | 问题 |
|---|---|
2count | 以数字开头 |
sensor-value | 减号不是变量名的一部分 |
user age | 包含空格 |
double | 语言关键字 |
a#b | 包含非法记号 |
1. 保留标识符
以下名称不要用于自己的变量:
__count
_Count
以双下划线开头、或下划线加大写字母开头的标识符保留给实现使用。以下划线开头的名称在文件作用域还有额外保留规则。
为了减少冲突,日常变量不以下划线开头,也不随意占用标准库名称。C11 草案 N1570,7.1.3
2. 名称应表达数据含义和单位
double height_m = 1.75;
unsigned int timeout_ms = 500U;
相比 h、t,这些名字说明了含义和单位。短名字适用于局部、明确的上下文;保存业务数据时,名称应让后面的计算更容易读懂。
五、基本数据类型总览
| 类别 | 常见类型 | 常见用途 |
|---|---|---|
| 字符和小整数 | char、signed char、unsigned char | 字符编码、小范围整数、原始字节 |
| 有符号整数 | short、int、long、long long | 计数、温度、差值、索引等 |
| 无符号整数 | 上述整数的 unsigned 形式 | 位模式、部分非负计数 |
| 浮点数 | float、double、long double | 测量、比例、近似实数 |
| 布尔 | _Bool,C11/C17 中可用 bool 宏 | 真或假状态 |
| 无值类型 | void | 表示函数不返回值等 |
void 不能用于定义一个普通的 void 变量,sizeof(void) 也不是本文所用标准 C 的合法用法。
数组、指针、结构体、联合体等属于后续内容,实际程序会在基本类型之上组织更复杂的数据。
六、整数类型的完整写法
以下写法等价:
short = short int
long = long int
long long = long long int
unsigned = unsigned int
signed = signed int
示例:
short temperature = -30;
int count = 100;
long distance = 1000000L;
long long total = 9000000000LL;
unsigned int flags = 0x12U;
后缀与类型的搭配可以清楚表达意图,但仍需检查值是否在类型范围内。
1. 无符号不是更“安全”的整数
无符号类型不能表示负数,却允许按类型模数环绕。例如从无符号 0 减 1,会得到该类型最大值。
若数据可能出现负的差值,使用无符号类型并不能消除逻辑问题,反而可能让错误变成非常大的正数。应按数据含义和运算需要选择类型。
2. 更大的类型不必占更多字节
不同整数类型有不同等级,但存储大小可能相同,例如某些平台上的 int 与 long 都是 4 字节。
C 规定常见整数类型的大小满足非递减关系:
sizeof(char) <= sizeof(short) <= sizeof(int)
<= sizeof(long) <= sizeof(long long)
不能据此推断每一步都严格增加。
七、一个字节到底有多少位
C 规定:
sizeof(char) == 1
这里的 1 是一个 C 字节。每个字节包含多少二进制位,由 <limits.h> 中的 CHAR_BIT 指明,标准要求至少为 8。
现代常见桌面与嵌入式平台通常 CHAR_BIT == 8,但“C 的一个字节在所有实现中必然等于 8 位”并不是标准保证。
printf("CHAR_BIT = %d\n", CHAR_BIT);
printf("int object bits = %zu\n", sizeof(int) * CHAR_BIT);
后一项是对象表示的总位数。某些实现可能存在填充位,因此它不总等于能直接代入数值范围公式的有效值位数量。
八、不要死记 long 一定是 4 字节
在 8 位字节、常见编译器模型下,可参考以下对比:
| 类型 | 常见 32 位 ILP32 | 常见 64 位 Linux LP64 | 常见 64 位 Windows LLP64 |
|---|---|---|---|
char | 1 | 1 | 1 |
short | 2 | 2 | 2 |
int | 4 | 4 | 4 |
long | 4 | 8 | 4 |
long long | 8 | 8 | 8 |
| 对象指针 | 4 | 8 | 8 |
这是常见数据模型的示例表,不是 C 标准对全部平台的要求。某些嵌入式平台上的 int 只有 16 位,也有其他布局。
“64 位系统”通常反映地址空间与平台模型,不能据此推断所有整数类型都变成 8 字节。
对于需要恰好 32 位或 64 位的协议字段,优先考虑目标实现提供的 int32_t、uint32_t 等类型;对于普通运算,仍要依据范围选择类型。
九、用标准宏查看真实范围
<limits.h> 提供整数边界:
| 类型 | 最小值宏 | 最大值宏 |
|---|---|---|
char | CHAR_MIN | CHAR_MAX |
signed char | SCHAR_MIN | SCHAR_MAX |
unsigned char | 0 | UCHAR_MAX |
short | SHRT_MIN | SHRT_MAX |
int | INT_MIN | INT_MAX |
unsigned int | 0 | UINT_MAX |
long | LONG_MIN | LONG_MAX |
long long | LLONG_MIN | LLONG_MAX |
例如:
printf("int range = %d to %d\n", INT_MIN, INT_MAX);
printf("long range = %ld to %ld\n", LONG_MIN, LONG_MAX);
标准至少保证 int 能表示 -32767~32767,long 至少能表示 -2147483647~2147483647,long long 至少能表示 -9223372036854775807~9223372036854775807。具体实现通常提供更明确的边界,尤其是补码最小负数多出的那一项。
不要用 pow 等浮点函数间接推算整数边界;已有标准宏能准确表达平台信息。
十、char、signed char 与 unsigned char
三者是不同的类型。普通 char 的范围与 signed char 或 unsigned char 相同,采用哪一种由实现决定。
在常见 8 位补码环境中:
signed char: -128~127
unsigned char:0~255
char: 取决于实现的默认选择
1. 存字符
char letter = 'A';
printf("%c\n", letter);
这是最常见用法。
2. 存原始字节
unsigned char byte = 0xFF;
printf("byte = %u\n", (unsigned int)byte);
明确用无符号字节,避免将最高位为 1 的数据误当成负数。由于可变参数存在整数提升,输出时显式转换为 unsigned int,再配合 %u,类型关系也更清楚。
3. char 不等于一个完整中文字符
普通 char 只有一个 C 字节。UTF-8 编码的汉字通常需要多个 8 位字节,应使用字符数组保存对应字节序列,不能把“一个汉字”与“一个 char 对象”混为一谈。
十一、浮点类型:范围与精度要分别看
float temperature = 23.5f;
double distance = 12345.678;
long double ratio = 1.0L / 3.0L;
常见平台中 float 为 4 字节,double 为 8 字节;long double 的大小和实际精度差异较大,不能统一记成一个数字。
浮点数有两个需要区分的指标:
- 范围:可以表示多大或多小的数;
- 精度:可以可靠地区分多少有效数字。
范围大不意味着所有整数都能精确表示。常见 IEEE 754 单精度 float 只有有限的有效二进制位,足够大时相邻整数会转换成同一个浮点值。
<float.h> 可以提供:
FLT_RADIX 浮点基数
FLT_DIG float 的十进制精度指标
DBL_DIG double 的十进制精度指标
LDBL_DIG long double 的十进制精度指标
FLT_MAX float 的最大有限正值
DBL_MAX double 的最大有限正值
特别注意:FLT_MIN、DBL_MIN 是最小的正规格化正数,不是最负值;支持次正规数的实现还能表示更小的非零正数。C11 的 FLT_TRUE_MIN 等宏描述最小正值,含相关的次正规情况。
十二、布尔类型保存逻辑状态
在 C11/C17 中:
#include <stdbool.h>
bool ready = true;
bool failed = false;
bool 是 _Bool 的宏别名,true、false 分别对应 1 和 0。
转换为 _Bool 时,等于零的标量值变成 0,非零值变成 1。因此:
bool enabled = 100;
保存的逻辑值为真,不会保存整数 100。
printf("ready = %d\n", ready);
经过整数提升后可以按 int 输出。bool 的存储大小由实现决定,不能根据“只需一位逻辑信息”推断一个独立布尔对象在 C 中只占一个 bit。
C23 调整了相关名称的语言地位,本文使用头文件写法以匹配 C11/C17 学习环境。
十三、sizeof 是运算符
常见写法:
sizeof(int)
sizeof(variable)
sizeof variable
对类型名使用时必须带括号;对表达式使用时,括号通常可省略。为便于阅读,初学代码也可以统一写括号形式。
int value = 10;
printf("sizeof value = %zu\n", sizeof(value));
printf("sizeof int = %zu\n", sizeof(int));
两项相同,因为变量类型是 int。
sizeof 不是函数调用,它不负责“检查值有多大”,而是获取类型或对象所占的 C 字节数。
十四、sizeof 的结果类型与求值规则
1. 结果是 size_t
size_t 是无符号整数类型,用于表示对象大小。正确输出方式:
printf("%zu\n", sizeof(double));
不能直接使用 %d,也不要把 %zd 当作直接输出无符号 size_t 的格式。
2. 普通操作数通常不求值
int counter = 1;
size_t bytes = sizeof(counter++);
这里 counter 仍为 1,bytes 等于 sizeof(int),因为普通非变长数组类型的操作数不求值。
这不代表所有 sizeof 都绝对不会涉及运行时求值,变长数组类型是需要另行讨论的情况。初学者不应在 sizeof 中刻意安排副作用。
3. 表达式的类型可能与单个变量不同
char letter = 'A';
比较:
sizeof(letter) sizeof(char),为 1
sizeof('A') sizeof(int)
sizeof(letter + 0) 常见平台上为 sizeof(int),因为整数提升
sizeof 根据整个表达式的类型计算,不只看表达式里出现了哪个变量。
十五、数组大小与字符串长度
char message[] = "hello";
int values[5] = {1, 2, 3, 4, 5};
sizeof(message) 为 6,包含终止符;sizeof(values) 为 5 * sizeof(int)。
在真正的数组仍可见的作用域内,可以计算元素数量:
size_t count = sizeof(values) / sizeof(values[0]);
结果为 5。
如果名称实际上是指针,或数组作为函数形参已经调整为指针,上面的公式就不再表示原数组元素个数。下一篇会解释这一差别。
strlen 只计算第一个终止符之前的字符序列长度,与数组总容量不同。字符串使用数组保存,数组未必包含合法的 C 字符串。
十六、完整程序:查看当前平台的数据类型
保存为 type_inspect.c:
#include <float.h>
#include <limits.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
int main(void)
{
int counter = 1;
int values[5] = {1, 2, 3, 4, 5};
char message[] = "hello";
bool ready = true;
printf("CHAR_BIT = %d\n", CHAR_BIT);
printf("char = %zu\n", sizeof(char));
printf("short = %zu\n", sizeof(short));
printf("int = %zu\n", sizeof(int));
printf("long = %zu\n", sizeof(long));
printf("long long = %zu\n", sizeof(long long));
printf("float = %zu\n", sizeof(float));
printf("double = %zu\n", sizeof(double));
printf("long double = %zu\n", sizeof(long double));
printf("pointer = %zu\n", sizeof(void *));
printf("bool = %zu\n", sizeof(bool));
printf("char range = %jd to %ju\n", (intmax_t)CHAR_MIN, (uintmax_t)CHAR_MAX);
printf("unsigned char max = %ju\n", (uintmax_t)UCHAR_MAX);
printf("int range = %d to %d\n", INT_MIN, INT_MAX);
printf("unsigned int max = %u\n", UINT_MAX);
printf("long range = %ld to %ld\n", LONG_MIN, LONG_MAX);
printf("long long range = %lld to %lld\n", LLONG_MIN, LLONG_MAX);
printf("decimal digits = %d, %d, %d\n", FLT_DIG, DBL_DIG, LDBL_DIG);
printf("character constant bytes = %zu\n", sizeof('A'));
printf("message bytes = %zu\n", sizeof(message));
printf("array count = %zu\n", sizeof(values) / sizeof(values[0]));
size_t bytes = sizeof(counter++);
printf("sizeof expression = %zu, counter = %d\n", bytes, counter);
printf("ready = %d\n", ready);
return 0;
}
编译运行:
gcc type_inspect.c -std=c11 -Wall -Wextra -Wpedantic -o type_inspect
./type_inspect
如果在常见 64 位 Linux 的 LP64 平台上运行,通常看到 int = 4、long = 8、pointer = 8。如果在常见 64 位 Windows 的 LLP64 平台上运行,则通常是 int = 4、long = 4、pointer = 8。
其他项目应以实际程序输出为准。无论 int 具体多大,普通字符字面量的大小等于 sizeof(int),数组数量公式得到 5,counter 也不会因这里的 sizeof(counter++) 增加。
十七、定义变量时容易出现的错误
1. 缺少分号
int age = 20;
double height_m = 1.75;
每条定义语句都需要分号。缺少分号时,编译器可能在下一行才报告问题,排查时同时看报错行和上一行。
2. 同一条声明并非所有变量都初始化
int first, second = 0;
这里只有 second 被显式初始化为 0。函数内部的普通 first 仍不能直接读取。
清晰的写法是:
int first = 0;
int second = 0;
3. 字符和字符串混用
char letter = 'A';
char word[] = "hello";
单个字符用单引号,字符串用双引号,并由数组或指针引用。不要写 char letter = "A";。
4. 超出范围后指望自动变大
变量类型在声明时确定。把更大的数赋给它,不会让它自动扩展为更宽类型。可能发生转换、信息丢失,或遇到无法依赖的结果。
5. 混淆存储大小与输出格式
float 对象可能占 4 字节,但传给 printf 后会提升成 double,因此用 %f。long 即使与 int 大小相同,也使用它对应的 %ld。
十八、怎样选择变量类型
先明确数据含义、单位和可能范围,再考虑参与哪些运算。计数与差值不一定适用同一种类型,位掩码与温度也不应因为“都是整数”就忽略正负语义。
普通整数可以从 int 开始,范围不足时选择更宽的类型;需要固定宽度的字段使用实现提供的固定宽度类型;近似实数通常从 double 开始,资源或接口限制明确时再考虑 float。
平台信息通过 sizeof、CHAR_BIT 和标准范围宏验证。下一篇会继续说明:同名变量为什么可以出现在不同位置,变量什么时候自动初始化,以及不同类型参与计算时如何转换。