这一章在干嘛?
把 C 里的「数据」盘一遍:类型家族(多大、怎么表示)、声明怎么写、常量怎么定义,以及最有分量的部分—— 作用域、链接属性、存储类型 。这三者共同决定一个变量「在哪儿可见、活了多久」。
3.1 基本数据类型:整型家族与浮点
C 只直接定义了寥寥几种基本类型,其余都是它们的变化与组合:
| 类型 | 典型大小 | 要点 |
|---|---|---|
| char | 1 字节 | 本质是小整数,signed/unsigned 由实现决定 |
| short / int / long / long long | ≥ 前者 | 标准只保证下限和相对大小,具体位数看平台;limits.h 里有答案 |
| float / double / long double | 4 / 8 / ≥8 字节 | 字面量 3.14 默认是 double,想给 float 用 3.14f |
| 指针 | 同宽(如 8 字节) | 存的是地址,类型决定「解引用时读几个字节、怎么解释」 |
整型字面量的暗号:
10是 int,10L是 long,10U是 unsigned,010是八进制,0x10是十六进制。溢出行为:有符号溢出是未定义,无符号溢出是回绕(模 2 N )——这是两族最重要的行为差异。
3.2 声明与初始化
声明的读法:从标识符出发,按优先级结合类型说明符。int a; 是「a 是 int」,int a[10]; 是「a 是数组」,int *p; 是「*p 是 int,所以 p 是指向 int 的指针」。声明只分配「描述」,定义才分配内存——但对变量而言,带初始化或不带 extern 的声明同时也是定义:
int a = 10; /* 定义并初始化 */
int b; /* 定义:静态存储的 b 被零初始化,栈上的 b 是随机值! */
extern int c; /* 纯声明:告诉编译器 c 在别处定义 */
int arr[5] = {1, 2}; /* 不完整初始化:剩余元素全部补 0 */
char msg[] = "hi"; /* 数组大小自动算:3 字节(含结尾 '\0') */
最高频的坑:
局部变量不初始化就读——值是垃圾。声明即初始化(指针一律先置 NULL),能消掉一大半诡异 bug。
3.3 typedef 与常量
typedef 给已有类型起别名,语法和声明一模一样,只是把变量名换成新类型名:
typedef char *String; /* String 就是 char* */
typedef struct Node Node; /* 前置声明常用,配自引用结构 */
const int limit = 100; /* limit 不可修改(值常量) */
int const *p1; /* 指向 const int 的指针:*p1 不能写,p1 能改 */
int *const p2 = &a; /* const 指针:p2 不能改,*p2 能写 */
const int *const p3 = &a; /* 都不能改 */
记法:const 在 * 左边修饰「被指向的东西」,在 * 右边修饰「指针本身」。另外 define 也能定义常量,但它是文本替换、没有类型、不进符号表——能用 const 就优先 const(第 14 章细讲两者差异)。
3.4 作用域、链接属性、存储类型
这是本章的压轴三件套,决定一个名字「哪里可见、是否跨文件共享、活多久」:
| 维度 | 取值 | 含义 |
|---|---|---|
| 作用域 | 代码块 | {} 之内可见,出了就没了 |
| 文件 | static 全局:本翻译单元私有 | |
| 原型 / 函数 | 只存在于函数原型参数表 / 只对 goto 标签有效 | |
| 链接属性 | external | 全程序共享,跨文件同名指向同一实体(默认的全局函数/变量) |
| internal | static 修饰:仅本单元可见,不同文件同名互不相干 | |
| none | 局部变量:无链接一说 | |
| 存储类型 | static | 静态区,程序全程存活,只初始化一次 |
| automatic | 栈上,进块创建出块销毁(默认) | |
| register | 提示放寄存器;不可取地址 |
static int counter = 0; /* 文件作用域 + internal 链接 + 静态存储 */
void tick(void)
{
static int calls; /* 只初始化一次,函数间保留 */
calls++;
int local = 0; /* 每次进函数都重新来,栈上 */
(void)local;
}
static 的两种用法一网打尽:
修饰全局变量/函数 → 收窄为文件私有(internal 链接,防止命名污染);修饰局部变量 → 改存静态区、寿命延长到整个程序。本质不同,记场景就行。
1. 有符号溢出和无符号溢出的行为差在哪?
无符号溢出按模 2^N 回绕,行为有定义;有符号溢出是未定义行为,编译器可能做任何优化假设(比如删除 overflow 检查)。涉及增长计算的整型尽量用无符号或提前判断边界。
2. int const *p 和 int *const p 的区别?
前者 const 在 * 左侧:被指向的值不可通过 p 修改,但 p 可改指别处;后者 const 在 * 右侧:指针本身不可改,但指向的值可以写。口诀「左定值、右定针」。
3. static 局部变量和普通局部变量的差别?
static 局部变量存静态区,只初始化一次,函数多次调用间值保留;普通局部变量在栈上,每次进入函数重新创建、退出即销毁,初值不确定。