C 语言指针与输入输出

C 语言指针与输入输出:地址、原反补码与格式符

指针是 C 语言的魂,输入输出是和人交互的入口。这篇文章把"变量在内存里到底长什么样"讲透,接着用代码验证原码/反码/补码为什么非用补码不可,再拿进制转换、数组、printf/scanf 的格式符收尾。所有输出都是本机 gcc 真实跑出来的。

一、变量的"三段式"存储模型

C 语言里,一个普通变量在内存中可以理解为"三段式"结构:

  1. 变量名(给人看的代号)

  2. 地址(这块数据在内存里的门牌号,由系统分配)

  3. 值(真正存进去的二进制数据)

而指针变量 特殊在:它自己也是一段内存,但它里面存的不是普通数据,而是另一个变量的地址。于是普通变量是"货物本身",指针是"快递单号"------单号上写的是货物所在的货架号。

读写一个变量,CPU 先拿地址,再去那块内存取数据。记住这个模型,后面所有指针操作都不会乱。

二、& 与 *:取地址与解引用

两个最常用运算符:

  • & 取地址符 :&a 拿到变量 a 在内存里的地址。

  • * 解引用符 :放在指针前,*p 表示"访问 p 指向的那块内存里的值"。

下面的代码把这两个符号和"指针存的是地址"这件事直接打印出来:

复制代码
int a = 10;
int b = a;        /* 深拷贝:把值复制一份,b 与 a 互不影响 */
int *p = &a;      /* &a 取 a 的地址,p 是指针,存的是地址 */
printf("a=%d, b=%d, 通过指针 *p=%d\n", a, b, *p);
printf("&a=%p, p=%p(指针 p 里存的正是 a 的地址)\n", (void *)&a, (void *)p);

本机真实输出:

复制代码
a=10, b=10, 通过指针 *p=10
&a=000000000062FE08, p=000000000062FE08(指针 p 里存的正是 a 的地址)
改 b=11 后:a=10, b=11(a 不变,b 是独立副本)
再改 *p=100 后:a=100(指针穿透改到了 a 本体)

重点看最后两行:改 b 只动了副本,原变量 a 纹丝不动;但通过 *p 改,a 本体被穿透修改。 这就是"值"和"地址"的本质区别。

三、浅拷贝与深拷贝(共享地址 vs 拷贝值)

  • 深拷贝(值拷贝) :int d = c; 把 c 的值完整复制一份给 d,之后 d 是独立个体,改 d 不影响 c。

  • 浅拷贝(地址共享) :int *q = &c; 两个指针指向同一块内存,通过任意一个改值,另一个"看到"的也跟着变。

本机实测:

复制代码
通过 *q=50 改后:c=50, *q=50, d=5(d 不受影响,是深拷贝)

工程上的坑:浅拷贝让多个指针共享同一块内存,改一处处处变,好处是省内存,坏处是容易"误伤"。什么时候该深、什么时候该浅,是 C 里反复要权衡的事。

四、整数在计算机里怎么存:原码/反码/补码

计算机只认二进制,那负数怎么表示?经历了三代方案:

4.1 原码的问题

原码 = 符号位(0 正 1 负)+ 数值的二进制。比如 -10 原码写作 1,000000000001010。

问题:原码做加法会错。理论上 +10 + (-10) 应该等于 0,但符号位直接参与运算后,结果是负的 10100,明显不对。也就是说,原码没法让 CPU 用同一套加法电路同时算正数和负数。

4.2 反码与 ±0 矛盾

反码规则:正数不变;负数符号位不变,其余位按位取反 。但反码留下一个致命矛盾------+0 是 000...0,-0 是 111...1,数轴上 0 明明只有一个点,却出现了两个编码。

4.3 补码:统一加减法

补码规则:正数同原码;负数 = 反码 + 1。它一举解决上面两个问题:

  • 统一用加法算加减,-10 + 10 直接相加,最高位溢出被自动丢弃,正好得 0。

  • 0 只有一种表示(全 0),没有 ±0 冲突。

所以现代计算机一律用补码存整数 ,包括正数。下面把 short(16 位)下 +10、-10 的真实补码位模式打出来:

复制代码
short pos = 10, neg = -10;
printf("+10 的补码位模式:"); print_bits((unsigned short)pos, 16); printf("\n");
printf("-10 的补码位模式:"); print_bits((unsigned short)neg, 16); printf("\n");
short r = (short)((unsigned short)pos + (unsigned short)neg);
printf("-10 + 10 用补码相加:"); print_bits((unsigned short)r, 16);
printf("= %d(最高位溢出自动丢弃,结果正确为 0)\n", r);

本机真实输出:

复制代码
+10 的补码位模式:0000 0000 0000 1010
-10 的补码位模式:1111 1111 1111 0110
-10 + 10 用补码相加:0000 0000 0000 0000 = 0(最高位溢出自动丢弃,结果正确为 0)

五、进制转换:二/八/十/十六

计算机内部只有二进制,但人写起来太长,于是引入八进制、十六进制做"缩写":

  • 八进制 ↔ 二进制:每 3 位二进制对应 1 位八进制(因为 2³=8)。

  • 十六进制 ↔ 二进制 :每 4 位二进制对应 1 位十六进制(0~9,A~F 表示 10~15,因为 2⁴=16)。

  • 十六进制常以 0x 前缀标记,如 0xDB。

C 里用 printf 的 %o(八进制)、%x/%X(十六进制)直接打印。以十进制 219 为例验证:

复制代码
int v = 219;
printf("八进制 %%o -> %o\n", v);
printf("十六进制 %%x -> %x ,大写 %%X -> %X\n", v, v);
printf("二进制(32 位):"); print_bits((unsigned int)v, 32); printf("\n");

本机真实输出:

复制代码
十进制 219:
  八进制 %o  -> 333
  十六进制 %x -> db ,大写 %X -> DB
  二进制(32 位补码):0000 0000 0000 0000 0000 0000 1101 1011
验证:219 = 128+64+16+8+2+1 = 11011011(二进制) = 333(八进制) = DB(十六)

219 = 11011011 二进制,从右每 3 位分组 011 011 011 = 3 3 3 八进制,每 4 位分组 1101 1011 = D B 十六进制,完全对得上。

六、数组:连续、同类型、下标从 0

数组是"相同类型数据的集合",三个铁律:

  1. 连续存储:元素在内存里一个挨一个,地址等差递增。

  2. 同类型 :一旦是 int 数组,每个元素都固定 4 字节,CPU 才能用"首地址 + 下标 × 4"精准定位。混类型会破坏这种规律。

  3. 下标从 0 :第 n 个元素实际偏移是 (n-1) 个单位长度,从 0 开始算偏移最整齐。

6.1 下标为什么从 0

因为"第一个元素"的偏移就是 0(地址 = 首地址 + 0),不需要额外减一。若从 1 开始,每次访问都要多一次减一,纯属浪费。

6.2 数组不能整体赋值、不能动态扩容

  • arr2 = arr1; 在 C 里直接编译报错------数组名是地址常量,不能当左值整体赋值。

  • 数组长度创建时定死。想"加长"只能:申请更大的新空间 → 把旧数据拷过去 → 释放旧的。原地往后接是不允许的,因为后面可能已被别人占用。

6.3 malloc 动态开辟数组

复制代码
int *dyn = (int *)malloc(5 * sizeof(int));

malloc 返回的是"原始内存"(无类型),必须用 (int*) 强制类型转换给它贴上"这是 int 数组"的标签,之后才能按 int 存取。free 用完要还回去。

6.4 用 sizeof 求数组长度

C 不会主动告诉你数组有几个元素,但可以用"总大小 ÷ 单个元素大小"算:

复制代码
int arr[5] = {1, 2, 3, 4, 5};
printf("数组长度 = sizeof(arr)/sizeof(arr[0]) = %lu\n",
       (unsigned long)(sizeof(arr) / sizeof(arr[0])));

本机真实输出:

复制代码
arr=000000000062FE20,&arr[0]=000000000062FE20(数组名就是首元素地址)
arr[0] @ 000000000062FE20 = 1
arr[1] @ 000000000062FE24 = 2
arr[2] @ 000000000062FE28 = 3
arr[3] @ 000000000062FE2C = 4
arr[4] @ 000000000062FE30 = 5
数组长度 = sizeof(arr)/sizeof(arr[0]) = 5
malloc 动态数组((int*)强制转换):10 20 30 40 50

注意每个元素地址相差正好 4 字节(一个 int),这就是"连续 + 同类型"的直接证据。

七、printf 格式符全家福

printf 靠格式符决定"把内存里的二进制按什么规则解释出来"。常用一览:

格式符 含义 示例
%d 有符号十进制整数 42
%f 浮点数(默认 6 位小数) 3.141590
%.2f 浮点,保留 2 位小数 2.72
%c 单个字符 Z
%s 字符串(双引号包裹) CSDN
%p 指针/地址 000000000062FE0C
%o / %x 八进制 / 十六进制整数 377 / ff
%% 输出一个百分号 % 100%

本机真实输出(把上面这些格式符一次打出来):

复制代码
%d 整数      : 42
%f 浮点      : 3.141590
%.2f 两位小数 : 2.72
%c 字符      : Z
%s 字符串    : CSDN
%p 地址      : 000000000062FE0C
%% 百分号    : 正确率 100%
%o/%x 进制   : 255 的八进制=377 十六进制=ff

要点:%f 默认打 6 位小数,%.Nf 控制小数位;想输出字面 % 必须写 %%,否则会被当成格式符开头。

八、scanf 的坑:为什么必须加 &

scanf 是把键盘输入写进变量地址。%d 读 int 时,必须加 & 取地址,否则会把变量当前的值误当成目标地址去写,直接内存报错崩溃。

复制代码
int  age;
float score;
char name[20];
scanf("%d %f %s", &age, &score, name);

注意三个参数:

  • &age、&score:普通变量,得取地址才能让 scanf 把值写进去。

  • name:字符串本来就是 char 数组,数组名本身就是首元素地址 ,所以不用加 &。

本机实测(输入 20 99.5 Alice):

复制代码
请输入:年龄 成绩 姓名(空格分隔,回车结束)
读到了 -> 年龄=20, 成绩=99.50, 姓名=Alice
注意:如果写成 scanf("%d", age) 漏掉 &,会把变量的值当地址去写,直接崩。

九、转义字符 \n \t 与 %%

输出排版离不开转义字符:

  • \n 换行:不加的话所有内容挤成一行。

  • \t 制表符:一个"长空格",用来对齐。

  • %%:输出百分号本身(见第七节)。

这些在 printf 的双引号字符串里才生效,单引号字符里只表示一个普通字符。

附:速查表

名称 作用 备注
& 取地址 &a 得到 a 的地址
* 解引用 / 声明指针 int *p 声明;*p 取值
浅拷贝 复制地址(共享内存) 一处改,处处变
深拷贝 复制值(独立副本) 互不影响
原码/反码/补码 负数表示方案 现代统一用补码
%o / %x 八/十六进制输出 4 位二进制 = 1 位十六进制
malloc / free 动态内存 返回 void*,需强转
sizeof(arr)/sizeof(arr[0]) 数组长度 仅在数组本名处有效
scanf 的 & 读入普通变量必须加 字符串 %s 不用加
\n / \t 换行 / 制表符 转义字符

附:实测代码

完整源码见 ../demos/818-pointers-io/pointers_io.c 与 scanf_demo.c,编译运行:

复制代码
gcc pointers_io.c -o pointers_io
./pointers_io
gcc scanf_demo.c -o scanf_demo
./scanf_demo   # 运行时输入:20 99.5 Alice

实测环境:Windows + MinGW-w64 (tdm64-1) gcc 4.9.2,64 位程序。文中所有地址、补码位模式、进制与格式符输出均为该程序真实运行结果。

相关推荐
路溪非溪1 小时前
C++标准库之智能指针
linux·开发语言·c++
hetao17338372 小时前
2026-11-02 hetao1733837 的刷题记录
c++·算法
Tairitsu_H2 小时前
[C++高阶数据结构·图论下] 最短路径:为什么“挑最小的”一定正确?
数据结构·c++·图论
weixin199701080163 小时前
官方授权通道和灰色爬虫,做数据服务该选哪条路(附 python 源码)
c++·爬虫·python
wuminyu3 小时前
JVM执行引擎学习总结:热点字节码编译成本地机器码
java·linux·c语言·jvm·c++
Tairitsu_H3 小时前
[C++高阶数据结构·图论上] 最小生成树:为什么“选边”和“选点”是两条路?
数据结构·c++·图论·最小生成树
Bczheng14 小时前
VS2022 安装MFC和QT
c++·qt·mfc
dialect_4 小时前
CODEFORCES ROUND 1127 DIV2
开发语言·c++·算法
悠仁さん12 小时前
【C++】AVL 树
c++