首页 > 编程笔记 > C语言笔记 阅读:22

C语言指针的定义和使用(非常详细)

本文将以一种轻松、有趣的方式深入探讨 C语言中的指针。接下来,我们将揭示指针的神秘面纱,让你真正理解它的本质。

内存可以被视作计算机中的庞大存储空间,用于存储程序和数据。正如我们将书籍放置在书架上一样,计算机将程序和数据都存储在内存中。因此,我们之前学习的数据类型,如 int、double 等,都是存储在内存中的。

由于数据存储在内存中,计算机必须在内存中准确地找到并访问数据。我们知道,内存是由许多字节单元组成的,每个单元都具有唯一的地址。

举一个通俗的例子,我们将这些字节单元视为数据的“房间”,为了方便查找这些“房间”,每个“房间”都有一个编号,第一个“房间”编号为 0,然后依次递增,如下图所示。


图 1 内存“房间”

我们将“房间号”称为内存地址。就像我们生活中所说的“AA 大街、BB 公寓、123 房间”一样。只是在内存中没有大街和公寓,仅需要编号便可表示地址了。

内存地址是内存中每个数据单元的唯一标识符,因此计算机系统可以通过内存地址来访问内存中的数据。

数据在内存中的存储

接下来,让我们探讨之前学过的基本数据类型是如何“居住”在这些“房间”中的,即数据是如何存储在内存中的。下表列出了基本数据类型及其大小。

表:基本数据类型及其大小
数据类型 大 小 数据类型 大 小
char 1 long long 8
short 2 float 4
int 4 double 8
long 4    

观察下表,除了 char 占用 1 字节的数据可以存储在 1 个“房间”内,其他数据类型都需要多个“房间”。各数据类型所需的“房间”数量如下图所示。既然如此,我们应该如何表示一个数据类型当前位于哪些“房间”内呢?


图 2 住几个“房间”

查看下图,以 int 为例,我们有以下两种表示方式。

图 3 int居住“房间”的表示方式

int 类型仅需要四个“房间”。如果数据需要很多“房间”,则第一种方式需要保存更多的“房间号”。显然,第二种方式更为灵活。

因此,计算机实际上使用第二种方式来记录一个数据对象在内存中的存储位置。我们将第一个“房间”的“房间号”称为该数据对象的首地址。数据对象所需的“房间”数量就是它占用的存储空间大小。

因此,记录一个数据对象在内存中的存储位置需要以下两个信息:

C语言指针的定义

现在,我们介绍一个新的运算符——取地址运算符 &。

取地址运算符是一个一元运算符(写在一个数据对象的左边),它可以获取一个数据对象的首地址和所需的存储空间大小。例如:
int n;
pn = &n;  // 获取数据对象n的首地址和存储空间大小
这里,变量 pn 存储了变量 n 的首地址和存储空间大小。因此,我们可以通过变量 pn 来访问内存中的变量 n。

然而,现在我们还不知道 pn 到底是什么类型?接下来,我们将学习指针类型,它是 C语言中非常重要的概念之一。

指针类型是一个变量,它存储了另一个变量的地址。换言之,指针类型保存了某个数据的首地址和存储空间大小,因此我们可以通过指针来访问该数据。

在 C语言中,声明指针类型的变量需要使用星号(*)来指示该变量是指针类型。例如,以下代码声明了一个指向整数的指针类型变量:
int n;
int* pn = &n;
这里的 int* 告诉编译器,pn 是一个指向整数的指针类型变量。同样的方法也可以用于其他数据类型。例如,以下代码声明一个指向字符的指针类型变量:
char c;
char* pc = &c;
因此,在上述两个例子中:int* pn 声明了一个保存了 int 类型的首地址和存储空间大小的指针变量;char* pc 声明了一个保存了 char 类型的首地址和存储空间大小的指针变量。通过这两个指针变量,我们可以在内存中找到变量 n 和 c 的值。

设一个数据对象为 x,设另一个数据对象为 p。p 保存了 x 的首地址和存储空间大小。那么,我们称 p 为 x 的指针,或者说 p 指向 x。

对于上面的代码,pn 被称作 n 的指针,或者说 pn 指向 n。pc 被称作 c 的指针,或者说 pc 指向 c。

另外,当声明指针变量时,可以将空格放在变量旁或者将空格放在类型旁,甚至不用空格。以下 3 种写法都是可以的:
int* pn;  // 将空格放在变量旁
int *pn;  // 将空格放在类型旁
int*pn;   // 不用空格
注意,在这些声明语句中,* 和指针变量名之间的空格是可选的,但建议始终使用空格以提高代码的可读性。

C语言指针的使用

由于指针存储了一个数据对象的首地址和存储空间大小,并且可以通过这两条信息在内存中找到该数据对象,因此我们可以使用指针来访问所指向的数据对象。

现在,我们再介绍一个新的运算符,叫做取值运算符 *

取值运算符是一个一元运算符(写在一个指针的左边),它可以根据指针中存储的首地址和空间大小找到目标数据对象。

取值运算符尽管类似于乘法运算符,却是一个一元运算符,仅需要一个操作对象。例如:
int n = 123;
int* pn = &n;
printf("%u\n", pn);   //  输出n的首地址
printf("%d\n", *pn);  //  根据pn中的首地址与大小,找到的数据对象的值
这段代码首先定义了一个整型变量 n 并初始化为 123,接着定义了一个指向 n 的整型指针 pn,并将 n 的地址赋值给 pn,然后使用 printf() 函数分别输出了指针 pn 所指向的地址和该地址中存储的数据值。

具体来说,第一个 printf() 函数输出了指针 pn 所指向的地址,并且该函数使用了 %u 格式化输出指针的地址值;第二个 printf() 函数输出使用了 *pn 来访问指针所指向地址中的内容,并且该函数使用了 %d 格式化输出该内容值。因为 n 的值为 123,所以程序的输出结果为:

1035466804
123

总结,变量 pn 内存储的值是变量 n 的首地址;*pn 表达式的结果是根据 pn 中的首地址与大小所找到的数据对象的值,也就是 n 的值。

除了通过指针访问所指向的数据对象,我们还可以通过指针修改所指向的数据对象,具体代码为:
#include <stdio.h>
int main()
{
    int n = 0;
    int* pn = &n;
    char c = 0;
    char* pc = &c;
    // 使用指针修改所指向数据对象
    *pn = 123;
    *pc = 'A';
    printf("n = %d\n", n);
    printf("c = %c\n", c);
    // 使用指针访问所指向数据对象
    printf("n = %d\n", *pn);
    printf("c = %c\n", *pc);
    return 0;
}
这段代码使用指针分别为 pn 和 pc 修改了变量 n 和 c 的值。其中,*pn = 123 用于将指针 pn 所指向地址中的数据值修改为 123,*pc = 'A' 用于将指针 pc 所指向地址中的数据值修改为字符 'A'。

最后,使用 printf() 函数分别输出了变量 n 和 c 的值,以及指针 pn 和 pc 所指向地址中的数据值。

因为 *pn =123 和 *pc = 'A' 修改了变量 n 和 c 的值,所以程序的输出结果为:
n = 123
c = A
n = 123
c = A

指针类型的大小

如下是一个使用 sizeof 关键字来测量指针类型的大小的示例。
#include <stdio.h>
int main()
{
    int n = 0;
    int* pn = &n;
    char c = 0;
    char* pc = &c;
    printf("sizeof pn = %d\n", sizeof(pn));
    printf("sizeof pc = %d\n", sizeof(pc));
    return 0;
}
运行结果为:

sizeof pn = 8
sizeof pc = 8

可以看出,int 类型指针和 char 类型指针的大小均为 8 字节。

此时,有些同学可能会产生疑问:我们之前学过 char 占用 1 字节的内存空间,而 int 占用 4 字节的内存空间,但为何它们的指针变量大小都是 8 字节呢?这个问题源于对指针和指针指向的数据两个概念的混淆。

仍然以“房间”和“房间号”为例,指针相当于“房间号”,而数据则居住在“房间”里。那么,“房间”里住着什么类型的数据和“房间号”有关系吗?实际上,指针仅用来表示“房间号”,它的大小与可以表示的“房间号”的大小有关,而与“房间”里的数据类型无关。

因此,指针指向的数据类型的大小不会影响指针变量的大小。

指针类型的大小取决于所在计算机体系结构和编译器。通常情况下,指针类型的大小等于所在计算机体系结构的地址大小,也就是操作系统位数。例如:在 32 位体系结构中,指针大小通常为 4 字节;在 64 位体系结构中,指针大小通常为 8 字节。上面实例中的代码编译成了 64 位程序,因此指针类型的大小为 8 字节。

Visual Studio 允许编译器生成 32 位或 64 位程序,有时也称为 x86 或 x64。在 Visual Studio 中,你可以通过下拉列表切换编译生成的程序为 32 位或 64 位:


图 4 在Visual Studio中切换x86或x64

若指针类型的大小为 4 字节,使用 %u 作为 printf() 的占位符是合适的。然而,当指针类型的大小为 8 字节时,使用 %u 可能无法完整地输出地址。在这种情况下,你可以使用长度指示符将长度扩展到 8 字节,如 %llu。

占位符 %p 专为指针类型设计,无论是 32 位还是 64 位程序,都可以使用它来确保输出结果的正确性。不过,它通常以十六进制形式显示。

下面展示了正确输出指针存储的内存地址的方法:
#include <stdio.h>
int main()
{
    int n = 0;
    int* pn = &n;
    char c = 0;
    char* pc = &c;
    printf("pn = %llu\n", pn);
    printf("pc = %llu\n", pc);
    printf("pn = %p\n", pn);
    printf("pc = %p\n", pc);
    return 0;
}
运行结果为:

pn = 44670753035656
pc = 44670753035420
pn = 0000006801D7FA74
pc = 0000006801D7FAB4

指针类型转换

下面是一个指针类型转换的示例:
#include <stdio.h>
int main()
{
    int n = 50000;
    int* pn = &n;
    char* pc = pn;
    printf("pn = %llu\n", pn);
    printf("pc = %llu\n", pc);
    printf("n = %d\n", n);
    printf("*pn = %d\n", *pn);
    printf("*pc = %d\n", *pc);
    return 0;
}
在此示例中,变量 pn 是指向 int 类型的指针,变量 pc 是指向 char 类型的指针。pn 被赋值给 pc,即 pc 指向 pn 指向的地址,因此 pn 和 pc 都存储了 n 的首地址,并输出相同的首地址。运行结果为:

pn = 2221931188
pc = 2221931188
n = 50000
*pn = 50000
*pc = 80

pn 和 pc 尽管都存储了 n 的首地址并输出了相同的首地址,但指向的数据是不一样的。

pn 是 int* 类型,*pn 表达式会从首地址开始取 4 字节的数据,并将其转换为 int 类型作为表达式结果。因此,结果为 50000。

pc 是 char* 类型,*pc 表达式会从首地址开始取 1 字节的数据,并将其转换为 char 类型作为表达式结果。因此,结果为 80。

下图清晰地展示了转换的结果,4 字节的二进制数据转换为十进制结果为 50000,而 1 字节的二进制数据转换为十进制结果为 80。


图 4 强制转换解析

相关文章