15. C 语言的灵魂:指针初体验

前面十四篇文章,我们盖好了地基:变量存数据,函数拆模块,数组管批量。但你有没有觉得还缺一块关键拼图——为什么我们可以在函数里修改数组,却改不了普通变量?为什么 scanf 要用 & 取地址?为什么说 C 语言“贴近硬件”?

答案就在指针

指针是 C 语言最强大、也最让初学者头疼的特性。但不要怕——你已经有变量、内存、作用域的扎实基础,理解指针就只剩一层窗户纸。今天,我们就来捅破它。


一、为什么需要指针?

先看三个你或许已经遇到的困惑:

困惑一:为什么 scanf 要加 &

int age;
scanf("%d", &age);   // 这个 & 是什么?

困惑二:为什么在函数里修改数组,外面也能看到变化?

void fill_zero(int arr[], int n) {
    for (int i = 0; i < n; i++) arr[i] = 0;  // 外面的数组也变了!
}

困惑三:为什么我们写的 swap 函数没用?

void swap(int a, int b) {
    int temp = a; a = b; b = temp;   // 外面纹丝不动
}

这三个困惑都指向同一个核心概念:如何直接访问和操作内存中的数据。普通变量通过名字访问,但有时候我们需要通过地址来访问——这就引出了指针。


二、指针是什么?

简单说:指针就是一个变量,但它里面存的不是普通数值,而是另一个变量的内存地址。

int a = 10;
int *p = &a;   // p 里存的是 a 的地址

这里:

  • a 是一个普通的 int 变量,里面存的是 10
  • &a取地址运算符,得到 a 在内存里的地址(比如 0x7ffd1c)。
  • p 是一个指针变量,它里面存的就是那个地址。
  • *p间接访问运算符(解引用),沿着 p 存的地址找到 a,读写它的值。

关系图:

变量 a:  [10]   <-- 地址 0x100
              ^
              |
指针 p: [0x100]  <-- p 自己的地址是 0x200

所以 *p 就是 a 本人。你写 *p = 20;a 就变成了 20。


三、声明指针变量

声明指针时,在类型后面加一个 *

int *p;      // p 是指向 int 的指针
char *ch;    // ch 是指向 char 的指针
double *dp;  // dp 是指向 double 的指针

声明时可以初始化:

int a = 5;
int *p = &a;    // p 指向 a

也可以分开写:

int *p;
p = &a;         // 让 p 指向 a

注意int *p, q; 中,只有 p 是指针,q 是普通 int。如果想声明两个指针,写 int *p, *q;。这是 C 语法的一个小陷阱。


四、& 取地址与 * 解引用

这两个运算符是指针的基本功,必须熟练掌握。

1. & 取地址

把任意变量的地址取出来,返回指向该类型的指针。

int x = 42;
printf("%p\n", (void*)&x);   // 打印 x 的地址

%p 专门用来打印地址(指针值),需要强制转换为 void*

2. * 解引用(间接访问)

通过指针访问它所指向的变量。

int a = 10;
int *p = &a;

printf("%d\n", *p);   // 输出 10,等价于 printf("%d\n", a);

*p = 20;              // 把 a 改成 20
printf("%d\n", a);    // 输出 20

*p 可以出现在赋值号的左边(左值),用来修改指向的值。


五、指针作为函数参数:实现真正的“交换”

还记得第十二篇那个失败的 swap 吗?现在用指针来拯救它。

#include <stdio.h>

void swap(int *a, int *b) {
    int temp = *a;   // 取 a 指向的值
    *a = *b;         // 把 b 指向的值赋给 a 指向的位置
    *b = temp;
}

int main(void) {
    int x = 5, y = 10;
    printf("交换前: x=%d, y=%d\n", x, y);
    swap(&x, &y);    // 传 x 和 y 的地址
    printf("交换后: x=%d, y=%d\n", x, y);
    return 0;
}

输出:

交换前: x=5, y=10
交换后: x=10, y=5

成功!因为 swap 接收的是 xy 的地址,它通过 *a*b 直接修改了 main 里那两个变量的内存。这就是指针的核心威力:让函数有能力修改外部的变量


六、指针与数组:一对亲兄弟

1. 数组名就是首元素的地址

int arr[5] = {10, 20, 30, 40, 50};
printf("%p\n", (void*)arr);      // 数组名直接当指针用
printf("%p\n", (void*)&arr[0]);  // 和上面一样

数组名 arr 在表达式中会被自动转换成指向首元素的指针。所以:

int *p = arr;   // p 指向 arr[0]

现在你可以用指针来访问数组元素:

printf("%d\n", *p);        // arr[0] = 10
printf("%d\n", *(p+1));    // arr[1] = 20
printf("%d\n", *(p+2));    // arr[2] = 30

p + 1 不是地址值加 1 个字节,而是加 1 * sizeof(int) 个字节——也就是跳过整个元素,指向下一个 int。这称为指针算术

2. 指针算术

指针加整数 n,地址值增加 n * sizeof(指向的类型)

int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;    // 指向 arr[0]

p = p + 1;       // 指向 arr[1]
p++;             // 指向 arr[2]
p += 2;          // 指向 arr[4]

也可以用下标访问指针:

int *p = arr;
printf("%d\n", p[2]);   // 等价于 arr[2],输出 30

为什么数组和指针这么亲?因为 arr[i] 在底层被编译器翻译成 *(arr + i)。这两者完全等价。甚至你可以写 i[arr],会被翻译成 *(i + arr),也是一样的(但千万别真这么写)。

3. 用指针遍历数组

#include <stdio.h>

int main(void) {
    int arr[] = {2, 4, 6, 8, 10};
    int *p;
    for (p = arr; p < arr + 5; p++) {
        printf("%d ", *p);
    }
    printf("\n");
    return 0;
}

p < arr + 5 判断指针是否越过了数组末尾(arr + 5 指向最后一个元素之后的位置,不能解引用,但可以做比较)。


七、数组作为函数参数的本质

现在我们可以解释那个困惑了:为什么函数里修改数组,外面也会变?

void modify(int arr[], int n) {
    arr[0] = 999;
}

实际上,编译器看到 int arr[] 时,会把它当成 int *arr。函数调用时,传进来的是数组首地址的副本,而不是整个数组的副本。通过这个地址,函数可以直接修改原数组。

modify(my_array, 5);   // 传的是 &my_array[0]

所以 arr[0] = 999; 等价于 *(arr + 0) = 999;,直接写到了原数组的内存上。这就是为什么数组“按引用传递”的真相——它传的是地址值。


八、void* 指针初识

有一种特殊的指针类型:void*(无类型指针)。它可以指向任何类型的数据,但不能直接解引用,因为编译器不知道它指向的数据类型大小。

int a = 10;
void *vp = &a;          // 可以指向 int
// printf("%d\n", *vp); // 错误!不能解引用 void*
printf("%d\n", *(int*)vp);  // 先强制转换回 int*,再解引用

void* 常用于通用内存操作函数,比如 mallocmemcpy,后面讲动态内存时会遇到。


九、常见错误与陷阱

1. 使用未初始化的指针

int *p;
*p = 10;   // 危险!p 指向哪里?可能是随机地址,导致崩溃

指针必须指向合法的内存(已声明的变量、数组、动态分配的内存)才能解引用。

2. 返回局部变量的地址

int* bad_func(void) {
    int x = 100;
    return &x;  // 函数返回后 x 已销毁,返回的地址无效
}

这是经典的“悬空指针”错误。要返回指针,可以返回静态局部变量、全局变量或动态分配的内存的地址。

3. 解引用空指针

int *p = NULL;
printf("%d\n", *p);  // 段错误!NULL 是空地址,不允许访问

NULL 是一个宏,表示空指针。在解引用前,一定要确保指针非空。

4. 指针类型不匹配

int a = 10;
double *dp = &a;  // 编译器警告,类型不兼容
printf("%f\n", *dp);  // 未定义行为

不同类型的指针不要随便互指,除非你很明白自己在做什么(并且用强制转换)。

5. 野指针

指针指向的内存已经释放(free 之后),但指针还在,指向的地址无效。后面动态内存部分会细讲。


十、小结

今天你第一次触碰了 C 语言的灵魂——指针。它们不是什么神秘魔法,只是存地址的变量。但它们解锁了:

  • 直接修改外部变量的能力(swap 终于能用了)
  • 高效操作数组的方式(指针算术)
  • 理解了数组作为函数参数的本质

你现在知道了:

  • & 取地址,* 解引用。
  • 指针变量声明用 int *p;
  • 数组名就是首元素地址,arr[i] 就是 *(arr + i)
  • 函数传数组本质是传地址,所以能在函数内修改原数组。

指针的旅途才刚刚开始。下一篇我们会深入指针与数组更复杂的关系——指针数组、数组指针、多级指针,以及字符串与指针的紧密联系。当你能轻松玩弄指针时,你就真正拥有了 C 语言。


课后小练习

  1. 写一个函数 void increment(int *p),让传入的整数加 1。在 main 中测试。
  2. 使用指针遍历一个 double 数组,打印所有元素,观察指针加 1 时地址增加了多少字节。
  3. 写一个函数 int array_sum(int *arr, int n),用指针算术(而不是下标)计算数组元素的和。
  4. 分析以下代码错在哪里:
    int* get_pointer(void) {
        int val = 5;
        return &val;
    }
    int main(void) {
        int *p = get_pointer();
        printf("%d\n", *p);
        return 0;
    }
    
    为什么输出可能不是 5?用静态局部变量怎么修正?

我们下期见!

代码下载链接: https://pan.quark.cn/s/a4b39357ea24 第 一 章 概述 1-1 简述计算机程序设计语言的发展阶段。 解: 自从计算机诞生以来,程序设计语言经历了从机器语言、汇编语言到高级语言的演变过程,C++语言作为一种面向对象的编程语言,也属于高级语言范畴。 1-2 面向对象的编程语言具备哪些特性? 解: 面向对象的编程语言与传统的编程语言有着本质的区别,其设计初衷是为了更直观地模拟现实世界中存在的事物及其相互关系。这类编程语言将客观事物视为具有属性和行为的对象,通过抽象方法提取出同一类对象的共同属性(静态特征)和行为(动态特征),从而构建类。借助类的继承与多态机制,能够便捷地实现代码复用,显著缩短软件开发周期,并确保软件风格的一致性。因此,面向对象的编程语言使得程序能够较为准确地反映问题域的本质,软件开发人员可以运用人类惯用的思维模式进行开发工作。C++语言是目前应用最为广泛的面向对象编程语言。 1-3 结构化程序设计方法是什么?这种方法有哪些优势和不足? 解: 结构化程序设计的核心思想是自顶向下、逐步求精;其程序结构按照功能划分为多个基本模块;各模块之间的关联尽可能简化,在功能上保持相对独立性;每个模块内部均由顺序、选择和循环三种基本结构构成;模块化实现的具体途径是利用子程序。结构化程序设计由于采用模块分解与功能抽象,自顶向下、分而治之的策略,从而有效地将一个较为复杂的程序系统设计任务分解成许多易于管理和处理的子任务,便于开发与维护。 尽管结构化程序设计方法具备诸多优点,但它本质上仍是一种面向过程的程序设计方法,将数据与处理数据的操作分离为相互独立的实体。当数据结构发生变化时,所有相关的处理过程都需要进行相应的调整,每一种...
已经博主授权,源码转载自 https://pan.quark.cn/s/a4b39357ea24 【高清晰度壁纸】是一种适用于计算机或移动设备的高解析度图像,通常用于定制用户界面,以增强视觉感受。$4K$分辨率指的是宽度约为$3840$像素,高度约为$2160$像素的显示标准,这种分辨率提供了极为清晰的细节,使得图像在大尺寸屏幕上呈现更为生动和逼真的效果。本压缩文件内含$20$张$4K$高清晰度壁纸,每张均从知名搜索引擎必应及彼岸图网中经过细致挑选。这些壁纸的题材丰富多样,涵盖了自然景观、科幻元素、游戏场景以及人物画像等多个方面,能够满足不同用户的需求。 1. **$125c1aa02ad94869ef055b870a54af560ad1574e144e03-qL6oaN_fw658.gif$**:这可能是一张动态壁纸,由于$gif$格式支持动态效果,或许包含有趣的动画元素,为桌面增添活力。 2. **$204b05b99e9b404aa6436f3c7c03d9c9.jpeg$**:$JPEG$是一种常见的静态图像格式,适合存储高品质照片,可能是一张风景或人物图片。 3. **加拿大班夫国家公园的朱砂湖的星空$4K$壁纸_彼岸图网.jpg**:这张壁纸展现了自然的宏伟,将班夫国家公园的优美湖泊与璀璨星空相结合,为用户带来宁静且和谐的视觉体验。 4. **《星球大战堕落秩序(Star Wars Jedi_ Fallen Order)》$4K$游戏壁纸_彼岸图网.jpg**:这是一张基于热门游戏《星球大战:堕落秩序》设计的壁纸,对于游戏爱好者而言极具吸引力,可能包含游戏中的角色或场景。 5. **陈钰琪倚天屠龙记$4K$壁纸_彼岸图网.jpg**:陈钰琪...
源码下载地址: https://pan.quark.cn/s/95927341e579 该方法适用于二进制数值向十进制数值的转化,其中A代表十进制数值,B代表二进制数值。{A,B}序列会执行位移操作,每次左移一位,同时检验A中的每四位数值是否>4,若超过四则进行加三调整,否则维持原状;B的位数决定了左移操作的重复次数。最终,A的数值即为B转换后的十进制表达。此代码示例专注于32位二进制数值向十进制数值的转换。在数字操作领域,二进制与十进制之间的相互转换是一项基础性操作。二进制体系(Base-2)采用0和1两种符号来表示数值,而十进制体系(Base-10)则使用0到9这十个符号。在计算机科学范畴内,特别是在硬件描述语言(例如Verilog)的应用中,掌握并执行此类转换显得尤为关键。下文将深入阐述如何借助Verilog代码实现32位二进制数值向十进制数值的转换。 我们必须明确Verilog是一种用于数字系统逻辑设计与验证的硬件描述语言。在所提及的代码中,`module b32_o(bdata, odata)`定义了一个名为 `b32_o` 的Verilog模块,该模块接收一个32位输入 `bdata`(二进制数据)并输出一个32位结果 `odata`(十进制数据)。 转换的核心逻辑在于对二进制数值进行逐位解析并依据特定规则实施调整。文中指出,针对每四位分组,我们需评估这四位数值是否大于4(4h4)。若超过四,则执行加三操作,此调整源于二进制的1000相当于十进制的8,故需将此部分值递增至下一位,即加三。该操作会在32位二进制数值的每个四位组上反复执行,总共进行32次。 代码中的 `always @(bdata)` 区块设定了一个触发机制,当 `bdata` 发生变化...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值