发布于 2026-01-06 0 阅读
0

面向初学者的C语言指针基础概念

面向初学者的C语言指针基础概念

本文假设读者对比特和字节有基本的了解。
此外,完全的初学者可能需要先阅读前言:《
面向初学者的基本编程概念》。

在深入探讨指针之前,让我们先来讨论一下什么是指针以及为什么它们是必要的。

什么是指针?

所有语言都包含变量,无论你是否显式声明它们。
在 C 语言(以及 C++)中,变量必须显式声明,并指定其数据类型(字符型、整型等)。
例如,表达式 `int X = 10` 表示 X 是一个整型变量,占用 2 个字节(某些情况下根据编译器类型可能占用 4 个字节),其值为 10。

图 - a 这里 1007 和 1008 分别表示整数 X 用于保存值 10 的两个连续内存空间的地址。

指针本质上是一个变量,它可以存储诸如 1007、1008 之类的地址,并访问该地址中的值。
请注意,这些地址可能并非如此友好的数字。)

简单来说,
指针是一个指向其他变量内存地址的变量。
例如,如果 int X 是一个整型变量,那么指针就保存(或指向)变量 X 的第一个字节地址(在图 a 中为 1007)。

为什么需要指针?

对于许多类型的应用程序来说,使用/操作/对变量进行操作就足够了,但是当速度是一个主要因素,并且你需要从硬件中获得最佳性能时,你就需要使用指针。

指针赋予了 C/C++ 等底层语言非凡的能力,例如操作数据、快速对图像应用效果、快速操作矩阵、访问操作系统分配的内存、视频缓冲区等。

指针可以用于快速遍历连续的内存位置,并且在遍历过程中可以极快地检查和操作数据。它还可以用于快速地将数据传递给其他过程/函数。在图像/视频处理、渲染和应用特效方面,指针的性能无可匹敌。与数组索引相比,指针在许多情况下都能展现出更优异的性能。因此,在底层编程(例如 C 和汇编语言)中,掌握内存和指针的知识至关重要。
但在深入理解指针之前,最好先对 RAM 的内存布局有一个基本的了解。

栈、堆和指针……

内存从概念上分为两个部分:栈和堆。

下图是记忆和变量的概念表示。

图 b

当我们预先知道所需的内存大小时,可以使用栈来声明变量和数组。在栈中声明变量非常简单。例如,只需写 `int n`,就会立即分配 2 个字节(或 4 个字节)的空间。

声明 int array[100],并立即分配一个连续的内存空间来容纳 100 个整数。

当我们在栈中不断声明变量时,变量会彼此堆叠。

堆内存的情况有所不同。在堆内存中,操作系统管理内存,我们请求操作系统分配一块内存供我们使用。然后,操作系统决定在堆内存中的哪个位置创建内存块。
在 C 语言中,我们通常使用 malloc 来实现这一点,而在 C++ 中,我们使用 new 关键字,它实现得更加优雅。

通常情况下,当内存分配依赖于用户输入或基于运行应用程序之前无法预见的某些情况时,就需要进行堆分配。

如果我们事先知道一架飞机有 500 个座位,
那么我们可以在栈中声明一个变量 `int seats[500]` 来存储座位相关数据。
座位相关数据可以很简单,例如用 1 或 0 表示座位是否已被预订。
但是,如果我们不知道某天会有多少乘客登机,并且需要操作员在航班起飞前输入该信息,那么我们就可以使用 `malloc` 函数在堆中预留足够的空间来存储乘客数据。

对于栈中的变量,例如“int x”,要访问 x 的值,我们只需直接使用 x(例如,y = x + 1)。
要获取 x 的内存地址,我们使用 &x。&x 返回为整数保留的两个连续字节的第一个字节的地址。
但是,直接使用 &x 只能得到一个地址,而要使用这个地址,我们需要将其存储在某个地方,这就需要用到指针。
指针是一个足够大的变量,可以存储一个内存地址。

为了区分普通变量和指针,我们在声明时使用 *,例如
char* p 或 int* p。
你可以写成 char * p、int * p,但我个人更喜欢写成 char*和 int *,这样可以明确地将指针与数据类型关联起来。

如前所述,我们需要指针及其运算来快速遍历内存地址,并在必要时快速访问数据。
如果 p 是指向某个内存地址的指针,则使用 *p 来访问该内存地址中的值。这部分内容容易让年轻的程序员感到困惑,因为他们常常将其与指针 p 的声明混淆。
声明时我们使用了 *,而访问地址中的值时,我们再次使用了 *。
我建议用以下方式来理解:

1) 指针类型的数据用 * 声明因此,一旦我们第一次写入 int* p 或 int *p,就会创建一个足够大的内存空间来保存一个内存地址(通常为 8 到 16 字节,具体取决于硬件的特性)。

2)声明之后,只要我们给指针赋值内存地址或者对指针进行指针运算,我们就直接使用指针变量名(例如 p = q + 1)。

3) 像 int x 这样的普通变量有两个属性:一个是值,我们可以通过变量名访问它;另一个是它的内存地址,我们可以通过 &x 访问它;
为了存储变量的地址(即 &x),我们需要一个指针,我们已经声明为 int* p;
所以我们写 p = &x,这意味着:
*将 x 的地址(即 &x)存储到之前声明的指针 p 中。

4) 以后,如果我们需要知道 p 指向的内存地址中存储的值是什么,我们使用 *p。p
包含一个内存地址,*p 访问内存地址中的值。

另外,
当你看到像 int* p = &x 这样的语句时,实际上是将两个语句合并为一个语句,即
i) int* p;
ii) p = &x;

从上图(图 b)可以看出两种情况(情况 1 和情况 2)。

有趣的是,指针实际上并不需要与 char/integer 数据类型关联,你可以直接写 void* p = (void*) &x;
我们在声明时指定数据类型,是为了让编译器理解当你尝试对指针进行操作时,指针应该如何工作。

在情况 1 中,声明一个字符指针(例如 char* p)意味着指针操作基于字节,因此如果您写 p++,p 将指向下一个字节。如果您写 char* q = p + 1,q 将指向 p 之后紧邻的下一个字节的内存地址。

对于情况 2,当 `int* p = &x` 且 `p++` 时,`p` 会跳过两个字节。当 `int* q = p + 1` 时,`q` 会跳过两个字节(整数的大小),并指向整数 `p` 之后的下一个地址块。

因此,借助指针,我们可以在内存中自由移动,但请记住,自由越大,责任越大。

那么数组及其指针呢?

数组是内存中连续的字节块。

指向数组的指针的概念与普通变量(例如 int x)的概念几乎相同,但稍作解释可能会有所帮助:

当我们处理数组指针时,通常的做法是将指针赋值给数组的头部(即数组第一个元素的地址)。

如果我们在栈上声明一个数组 `int myArray[3]`,那么数组的第一个元素是 `Array[0]`,我们可以用获取普通变量地址的相同方法来获取 `Array[0]` 的地址。
记住,我们之前将 `int x` 的地址赋给了指针 `p`:
`p = &x;`,因此对于 `myArray[0]`,我们用同样的方法:
`p = &myArray[0];`

为了方便阅读,我们可以将 &myArray[0] 写成 myArray。
因此,我们可以写成 p = myArray; 而不是 p = &myArray[0];

有些初学者会误以为 `myArray` 也是一个指针变量,因为我们可以直接给指针赋值。其实不然,`myArray` 只是 `&myArray[0]` 的一种便捷写法。
你不能直接使用 `myArray++` 或 `myArray = Array1`(其中 `Array1` 是另一个数组变量)。
就像 `&x + 1` 会跳过 x 获取下一个地址一样,`myArray + 1` 会跳过 `myArray[0]` 获取下一个地址。(所以 `myArray + 1` 实际上就是 `myArray[1]` 的地址。)

我们最多只能说:
如果我们在栈中声明了一个名为 myArray 的数组,其值为 int myArray[100],那么 myArray 既表示第一个元素的内存地址(即 &myArray[0]),也是栈变量 myArray 的名称,占用内存中的一块空间。

希望下图(图 c)能更好地阐明数组背后的概念,并且与图 b 中的情况 1 和 2 相比,情况 1 和情况 2 也更容易理解:

图 c

现在我们已经了解了一些栈的知识,接下来让我们把注意力转向堆。

堆中的变量通过系统调用(例如 malloc 和 new,new 仅适用于 C++)进行分配。
由于操作系统会选择内存中的位置,因此访问该内存的唯一方法是通过指针。

因此,像 malloc 这样的函数会接受要分配的连续字节数,并返回已分配内存的地址/位置,我们会将该地址/位置保存在指针中以便稍后访问。

例如,
int numOfPassengers = 10;
int* p = malloc(numberOfPassengers * sizeof(int));
p[0] = 1(或 *p = 1)
p[1] = 1(或 *(p+1) = 1)

这里,由于我们计划分配一个整数数组,因此首先通过 `sizeof` 获取一个 `int` 类型的大小(通常是 2 或 4,取决于编译器)。然后将 `sizeof(int)` 乘以所需的整数数量(例如 10),即可得到容纳 10 个整数所需的内存大小。

在 C++ 中,代码看起来比较简洁,例如 int * p = new int [10];)

当我们完成对该内存和块中包含的值的操作后,我们必须通过调用 free 函数(例如 free(p))来释放内存,否则程序将发生内存泄漏,并可能被系统关闭。

下图进一步形象地展示了从ad的堆内存分配过程。
步骤 a 表示需要分配一块连续分布在内存中的内存块,它本质上是一个数组(表达式 A[dynamically 10] 只是我们思路的体现,即我们需要一个动态数组)。
步骤 b 告诉我们需要一个指针来保存这个数组
,此时我们还没有进行任何内存分配操作,这并非 C 代码的一部分,只是在执行操作之前解释思路
步骤 c是实际的 C 代码,它执行了步骤 a 和 b 中讨论的操作,即 malloc(10*sizeof(int)) 动态地声明了 int A[10],并将指针 int* a 指向该数组。
步骤 d 是在工作完成后释放内存。

图 d

希望这能帮助消除许多计划使用 C/C++ 开始激动人心的职业生涯、深入研究复杂系统的初学者所面临的困惑。


穆基特下线

附注:给初学者的任务——尝试分析封面图片中的内容(注意,`int **ptr` 应该命名为 `int **doubleptr`)。
提示:指针是一个变量,它可以被另一个指针指向。

文章来源:https://dev.to/lucpattyn/basic-c-pointer-concepts-for-beginners-1epa