12.C 语言变量入门:声明、基本数据类型与 sizeof

0 阅读15分钟

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
char111
short222
int444
long484
long long888
对象指针488

这是常见数据模型的示例表,不是 C 标准对全部平台的要求。某些嵌入式平台上的 int 只有 16 位,也有其他布局。

“64 位系统”通常反映地址空间与平台模型,不能据此推断所有整数类型都变成 8 字节。

对于需要恰好 32 位或 64 位的协议字段,优先考虑目标实现提供的 int32_t、uint32_t 等类型;对于普通运算,仍要依据范围选择类型。

九、用标准宏查看真实范围

<limits.h> 提供整数边界:

类型最小值宏最大值宏
charCHAR_MINCHAR_MAX
signed charSCHAR_MINSCHAR_MAX
unsigned char0UCHAR_MAX
shortSHRT_MINSHRT_MAX
intINT_MININT_MAX
unsigned int0UINT_MAX
longLONG_MINLONG_MAX
long longLLONG_MINLLONG_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 和标准范围宏验证。下一篇会继续说明:同名变量为什么可以出现在不同位置,变量什么时候自动初始化,以及不同类型参与计算时如何转换。