如何掌握 C 语言的一大利器——指针?

二十二画程序员 等级 382 1 1

一览:初学 C 语言时,大家肯定都被指针这个概念折磨过,一会指向这里、一会指向那里,最后把自己给指晕了。本文从一些基本的概念开始介绍指针的基本使用。

内存

考虑到初学 C 语言时,大家可能对计算机的组成原理不太了解,所以这里先简单介绍一些“内存”这个概念。

众所周知,任何东西都需要有物理载体作为基础。

比如说人产生的“思维”这个东西,我们看不见摸不着,但并不是说它就可以凭空存在了,思维的物理载体就是我们的大脑。“大脑”之于“思维”就如同“土地”之于“人类”。

同样地,我们看不见摸不着的软件 / 代码也需要类似于“土地”和“大脑”的物理载体——存储器

存储器分为两种:

  • 内存:计算机中正在运行的程序以及运行过程中暂时产生的数据都在这里。
  • 外存:那些暂时不需要运行的程序和最终的运算结果存储在这里。

比如一个 HelloWorld 程序:

#include <stdio.h>

int main()
{
    printf("Hello World!\n");
    return 0;
}

写完保存之后,程序会被存储在外存(硬盘)中。

当开始运行时,程序会被从外存调入内存中运行,打印 HelloWorld。

上面是内存的简单概念(一个很浅的印象):内存可以暂时存储数据。

那内存的结构是什么样的?

这里我们把内存想象为一幢有很多房间的酒店,每个房间都有一个独一无二的房间号。

人就是数据;内存就是酒店。

酒店的职责就是供人暂时居住;内存的职责就是供数据暂时存储。

如何掌握 C 语言的一大利器——指针?

内存的结构也像酒店一样,有很多“房间”,称之为“内存单元”,每个内存单元也有一个独一无二的“房间号”,称之为“内存地址”。数据就“住”在内存单元中。

假设现在张三住在酒店的 1001 号房间了。

我们就有以下关系:

房间号为1001的房间住了 客户张三

放到内存中,就是:

内存地址为1001的内存单元存储了 整数5

如何掌握 C 语言的一大利器——指针?

如此一来,我们就可以根据地址1001找到对应的内存单元,并对其中数据进行操作了。

但这样有一个问题,就是为了操作 5,而不得不记住其地址,对于人来说,记忆这么多数字太麻烦了。

想象一下你平常和别人打招呼时说:“早上好啊,某人的身份证号”,而不是“早上好啊,某人的名字”。

光是记住自己的身份证号就不容易了,更别说别人的了,所以我们平常的称呼是名字。尽管身份证号唯一,而名字可能会重复。

没错,就是名字,使用名字来代替对人不友好的内存地址。我们可以给 1001 号内存单元取个名字,就叫 a 吧。

我们取的这个“名字”就是编程语言都会有的“变量名”。

int a = 5;

如何掌握 C 语言的一大利器——指针?

变量名对我们人类来说就很友好了,什么 zhangsanlisi等等都可以起。

通过变量名,就可以访问其值了。现在我们有一个变量 a,存储了值 5,可以直接通过变量名打印其值:

int a = 5;
printf("%d", a);

但这样也出现了一个问题,就是我们不知道某个变量的地址了。

这就好比,你去酒店找张三,只知道他名字叫张三,而不知道他的房间号是多少,怎么办?一间间的敲门吗?

不可能。我们应该去前台问工作人员:“请问张三的房间号是多少?”,前台工作人员会告诉我们:“1001号”

类似地,要获取某个变量的地址,我们也可以向“前台的工作人员”询问:“请问变量 a 的‘房间号’是多少?”,当然,在现在的语境下,这句话就变成了“请问变量 a 的内存地址是多少?”。

在 C 语言中,这个充当“前台工作人员”的角色的是取地址运算符 &

int a = 5;
printf("%p", &a); //请问a的内存地址是多少?

通过 &,我们可以得到某个变量的内存地址,通常是一串十六进制数字,比如 0061FF1C

到这里就一切安好了吗?不!

指针

概念

至此,我们只有能力得到某个变量的内存地址,即使用 &。现在的问题是我们如何使用它。

为什么现实中的人和事都会有一个名字?为了方便称呼和使用。

名字之于事物,就好比刀柄之于刀身。一件事物一旦有了名字,我们就有了使用他的力量。

在程序中,我们会有大量的数据,为了使用这些数据,我们有了变量和变量名的概念。比如整型数据用整型变量存储:

int i = 5;
float f = 5.0;
char c = 'x';

地址也属于数据,换句话说,我们也应该有某种类型的变量来存储地址:

int a = 5;
int p = &i; //错误代码

我们的目的是使用变量 p 来存储 int 类型变量a的地址,但是上面的代码是错误的。因为我们的变量 p 被声明为 int类型,所以变量 p 就只能存储 int 类型数据,而不能存储 int 类型变量的地址。

这个时候我们就需要一种能存储整型变量的地址的变量,C 语言为我们提供了一种机制——指针。

int a = 5;
int *pa = &a;

现在我们声明了一个能存储 int 类型变量的地址 的变量 pa,然后使用 & 获取变量 a 的地址,赋值给变量 pa,非常完美。

如何掌握 C 语言的一大利器——指针?

这里的 pa,就是一个指针(pointer)。可以看一下指针的定义:

In computer science, a pointer is an object in many programming languages that stores a memory address.

In computer science, an object can be a variable, a data structure, a function, or a method, and as such, is a value in memory referenced by an identifier.

在计算机科学中,指针是许多语言中存储内存地址的对象。这里的对象可以是变量、结构体、函数或方法。

即,指针中存储的是内存地址

指针的声明需要使用 * 来表示该变量是一个指针变量:

[pointer_type] *[pointer_name];
int a = 5;
float b = 5.0;
char c = 'x';

int *pa = &a; 
float *pb = &b;
char *pc = &c;

由于指针中存储了某个变量的地址,所以我们可以说该指针指向了那个变量。比如 pa 被声明为了指向 int 类型的指针,指向了变量 a

如何掌握 C 语言的一大利器——指针?

间接访问操作符

我们有了取地址运算符 &用来获取某个变量的地址,也知道了如何声明某种类型的指针用来存储地址。

知道如何获取了、懂了怎么存储了,那么怎么使用指针呢?

房间号不是用来好看的,而是用来找到房间和房间中的人。我们已经通过 & 这个“前台工作人员”找到了房间号并记了下来,下一步就是上门把人找出来。

通过间接访问操作符 *,我们就可以根据指针“上门找人”了。

int a = 5; //变量a中存储5
int *pa = &a; //获取房间号
printf("%d", *pa); //上门找人

*pa,就是取指针 pa 所指向的变量的值。

区分

初学 C语言时会容易混淆一些概念,所以这里区分一下。

int a = 5;
int b = 6;
int c = 7;

int *pa = &a;
int *pb = &b;
int *pc = &b;

printf("a = %d\n", a);
printf("b = %d\n", b);
printf("c = %d\n", c);

printf("&a = %p\n", &a);
printf("&b = %p\n", &b);
printf("&c = %p\n", &c);

printf("pa = %p\n", pa);
printf("pb = %p\n", pb);
printf("pc = %p\n", pc);

printf("*pa = %d\n", *pa);
printf("*pb = %d\n", *pb);
printf("*pc = %d\n", *pc);

输出为

a = 5
b = 6
c = 7
&a = 0061FF10
&b = 0061FF0C
&c = 0061FF08
pa = 0061FF10
pb = 0061FF0C
pc = 0061FF0C
*pa = 5
*pb = 6
*pc = 6
  • a:变量
  • &aa的地址
  • int *pa:声明一个指向 int 类型的指针 pa
  • pa:指针
  • *pa:指针 pa 指向的变量值

int *pa*pa 中的 * 不一样,这一点容易让人迷惑。在声明时,int * 是一起的,用来声明一个指向 int 类型变量的指针,虽然写开了,但不要分开来看。

int a; //声明了一个变量a
int *pa; //声明了一个变量pa

&* 是一对相反的操作,& 根据变量求地址, * 根据地址求变量。

int a = 5;
printf("%d", *&a); //5
printf("%d", a); //5

*&a 的值为 5,即 a

初始化

我们在声明某个变量后,在使用某个变量前,一定要对其进行初始化。

比如在声明变量 a 的同时将其初始化为 5:

int a = 5;

也可以声明后再初始化:

int a;
a = 5;

如果不初始化,那么变量的值将是难以想象的。

指针也是变量,也必须对其进行初始化。先运行下面一段代码:

int *p;
*p = 5;
return 0;

这段代码的意思很简单:声明一个指针 p, 将 5 赋值给指针 p 所指向的那个变量。但这种代码是错误的!

请问指针 p 指向了谁?由于我们没有对其进行初始化,所以根本就不知道指针 p 指向了谁,那怎么赋值?

这就好比一个人对你说:“请把这个包裹给李四”。但是你根本就不知道李四是谁,李四住在哪里,你怎么给?

快递员不认识你就能送货,那是因为包裹上有地址,这就足够了。

但是在上面的代码中,你告诉 p 地址了吗?没有!因为我们没有对指针进行初始化!

所以初始化指针非常重要!!!未初始化的指针不能用!!!

更改如下:

int a = 4;
int *p = &a;
*p = 5;

或者:

int a = 4;
int *p;
p = &a;
*p = 5;

现在变量 a 的值由 4 变为 5 了。

因为我们在“包裹”上写了变量 a 地址,所以能把 5 送给变量 a

赋值

我们可以将一个指针赋值给另外一个指针。

int a = 5;

int *p1 = &a;
int *p2;
p2 = p1;

我们将指针 p1 的值赋给 p2,然后打印以下内容:

printf("a = %d\n", a);
printf("&a = %p\n", &a);

printf("p1 = %p\n", p1);
printf("p2 = %p\n", p2);

printf("*p1 = %d\n", *p1);
printf("*p2 = %d\n", *p2);

printf("&p1 = %p\n", &p1);
printf("&p2 = %p\n", &p2);

输出为:

a = 5
&a = 0061FF1C
p1 = 0061FF1C
p2 = 0061FF1C
*p1 = 5
*p2 = 5
&p1 = 0061FF18
&p2 = 0061FF14

可以看到,将指针 p1 赋值给另一个指针 p2 的结果是: p1 指向哪里, p2 就指向哪里。如此一来,我们可以通过两个指针操作变量 a

*p1 = 4;
printf("a = %d\n", a); //从5变为4

*p2 = 3;
printf("a = %d\n", a); //从4变为3

如何掌握 C 语言的一大利器——指针?

空指针

空指针的值为 NULL, 表示不指向任何对象。

int *p = NULL;

当我们初始化一个指针的时候,如果还不知道要指向谁的时候,就把它初始化为空指针。

一些用法

我们已经以”指向变量的指针”为例,介绍了指针的基本用法。现在介绍一些指针的其他用法。

指向指针的指针

前面我们介绍了“指向变量的指针”:

int a = 5;
int *pa = &5;

如何掌握 C 语言的一大利器——指针?

指针也是个变量,只不过相对于其他类型的变量有点特殊,指针变量中存储的是其他变量的地址。

也就是说,指针作为一个变量也有地址,该地址可以被其他指针存储,即指向了指针的指针。

如何掌握 C 语言的一大利器——指针?

对应代码如下:

int a = 5;
int *pa = &5;
int **ppa = &pa;

如你所见,声明一个“指向指针的指针”需要使用两个*

[pointer_type] **[pointer_name];

同样地,要获取 指向指针的指针 指向的 指针 指向的 变量值 需要进行两次间接访问,即**ppa

请仔细体会以下代码:

#include <stdio.h>

int main()
{
    int a = 5;
    int *pa = &a;
    int **ppa = &pa;

    printf("a = %d\n", a);
    printf("&a = %p\n", &a);

    printf("pa = %p\n", pa);
    printf("*pa = %d\n", *pa);
    printf("&pa = %p\n", &pa);

    printf("ppa = %p\n", ppa);
    printf("*ppa = %p\n", *ppa);
    printf("**ppa = %d\n", **ppa);
    printf("&ppa = %p\n", &ppa);

    return 0;
}

通过代码,我们可以得到以下等价关系:

表达式 等价表达式
a 5
pa &a
ppa &pa
*pa a5
*ppa pa&a
**ppa *paa5

举一反三,你还可以试试 {指向[指向(指针)的指针]的指针}。

指针和数组

首先运行以下代码:

int arr[5] = {1, 2, 3, 4, 5};
printf("arr = %p\n", arr);

int *p = &arr[0];
printf("&arr[0] = %p\n", &arr[0]);
printf("p = %p\n", p);
printf("arr[0] = %d\n", arr[0]);
printf("*p = %d\n", *p);

p++;
printf("运行p++之后...\n");

printf("&arr[1] = %p\n", &arr[1]);
printf("p = %p\n", p);
printf("arr[1] = %d\n", arr[1]);
printf("*p = %d\n", *p);

输出为:

arr = 0061FF08
&arr[0] = 0061FF08
p = 0061FF08
arr[0] = 1
*p = 1
运行p++之后...
&arr[1] = 0061FF0C
p = 0061FF0C
arr[1] = 2
*p = 2

可以得到以下结论:

  • arr =&arr[0]arr是数组的首元素指针
  • int *p = &arr[0]int *p = arr 是等效的
  • arr[n]*(p+n)是等效的

指针和函数

先运行以下函数:

#include <stdio.h>

void swap(int x, int y)
{
    int temp = x;
    x = y;
    y = temp;
}

int main()
{
    int x = 5, y = 10;
    printf("交换前 x = %d, y = %d\n", x, y);
    swap(x, y);
    printf("交换后 x = %d, y = %d\n", x, y);
    return 0;
}

swap 函数的目的很简单:传进来两个值,交换他们。

如何掌握 C 语言的一大利器——指针?

但是结果令人失望——根本没交换。原因是什么?

我们打印一些东西:

#include <stdio.h>

void swap(int x, int y)
{
    printf("在swap()中,x的地址为%p,y的地址为%p\n", &x, &y);
    printf("swap() 交换前 x = %d, y = %d\n", x, y);
    int temp = x;
    x = y;
    y = temp;
    printf("swap() 交换后 x = %d, y = %d\n", x, y);
}

int main()
{
    int x = 5, y = 10;
    printf("在main()中,x的地址为%p,y的地址为%p\n", &x, &y);
    printf("main() 交换前 x = %d, y = %d\n", x, y);
    swap(x, y);
    printf("main() 交换后 x = %d, y = %d\n", x, y);
    return 0;
}

输出为:

在main()中,x的地址为0061FF1C,y的地址为0061FF18
main() 交换前 x = 5, y = 10
在swap()中,x的地址为0061FF00,y的地址为0061FF04
swap() 交换前 x = 5, y = 10
swap() 交换后 x = 10, y = 5
main() 交换后 x = 5, y = 10

可以看到,在 swap() 中,我们确实交换了值,但是swap() 函数执行完后回到 main() 中,值却没有交换。

可以看到,swap() 中的 xymain() 中的 xy 的地址并不相同,这就意味着*xy 非彼 xy *

如何掌握 C 语言的一大利器——指针?

原因很简单,swap(int x, int y) 的参数传递为值传递,所谓值传递,即将实参的值复制到形参的对应内存单元中。函数操作的是形参的内存单元,无论形参如何变化,都不会影响到实参。

void swap(int x, int y) //xy为形参
{.....}

int main()
{
    int x = 5, y = 10;
    swap(x, y); //xy为实参
}

这里就解释了为什么 main()swap()打印出来的 xy 的地址不同,也解释了为什么交换失败。

那么,为了通过函数直接操作实参,我们必须使形参和实参是同一块内存。所以我们直接把实参的地址传给函数,也即,函数的参数为指针,指向实参的内存单元。这种参数传递为地址传递

地址传递保证了形参的变化即为实参的变化。

如何掌握 C 语言的一大利器——指针?

代码更正:

#include <stdio.h>

void swap(int *px, int *py) //形参为指针,接收实参的地址
{
    printf("在swap()中,px = %p,py = %p\n", px, py);
    printf("swap() 交换前 x = %d, y = %d\n", *px, *py);
    int temp = *px;
    *px = *py;
    *py = temp;
    printf("swap() 交换后 x = %d, y = %d\n", *px, *py);
}

int main()
{
    int x = 5, y = 10;
    printf("在main()中,x的地址为%p,y的地址为%p\n", &x, &y);
    printf("main() 交换前 x = %d, y = %d\n", x, y);
    swap(&x, &y);
    printf("main() 交换后 x = %d, y = %d\n", x, y);
    return 0;
}

输出为:

在main()中,x的地址为0061FF1C,y的地址为0061FF18
main() 交换前 x = 5, y = 10
在swap()中,px = 0061FF1C,py = 0061FF18
swap() 交换前 x = 5, y = 10
swap() 交换后 x = 10, y = 5
main() 交换后 x = 10, y = 5

指针和结构体

先定义一个结构体:

typedef struct Node {
    int data;
    struct Node *next;
} Node;

然后声明一个结构体:

Node node;

要访问结构体内的成员,需要使用 . 操作符:

node.data;
node.next;

现在我们有一个指向该结构体的指针:

Node *p = &node;

想要通过指针访问结构体的成员:

(*p).data;
(*p).next;

也可以使用 -> 操作符:

p->data;
p->next;

注意,-> 要对指向结构体的指针使用才行。

对于初学者,某个概念一时搞不懂其实很正常。谁都不是一下子就学会用筷子和走路的,我们需要的是花时间进行大量的实践。就拿指针来说吧,初学者觉得难以理解是因为用得少,反过来说,对于经常使用 C/C++ 写代码的人,指针肯定早就不是问题了。所以搞清基本原理,接下来就花时间去大量实践吧,时间到了,自然就会豁然开朗。

如有错误,还请指正。

如果觉得写的不错可以关注一下我。

如何掌握 C 语言的一大利器——指针?

收藏
评论区

相关推荐

C++概述
概述 C 是静态,可编译,通用,大小写敏感,格式自由的编程语言,它支持程序化,面向对象的,和泛型编程方式。 C 被看作是中间层语言,因为它同时包含了低级语言和高级语言的特性。 C 是于 1979 年在新泽西的茉莉山丘的贝尔实验室由 Bjarne Stroustrup 开发的,它是 C 语言的加强版,最开始它被称作 “C with Classes”,但是
C++ 基本语法
C 程序可以定义为对象的集合,这些对象通过调用彼此的方法进行交互。现在让我们简要地看一下什么是类、对象,方法、即时变量。 对象 对象具有状态和行为。例如:一只狗的状态 颜色、名称、品种,行为 摇动、叫唤、吃。对象是类的实例。 类 类可以定义为描述对象行为/状态的模板/蓝图。 方法 从基本上说,一个方法表示一种行为。一个类可以包含多个
c语言中static 用法
static在c里面可以用来修饰变量,也可以用来修饰函数。 先看用来修饰变量的时候。变量在c里面可分为存在全局数据区、栈和堆里。其实我们平时所说的堆栈是栈而不是堆,不要弄混。 c int a ; int main() { int b ; int c (int )malloc(sizeof(int)); } a是全局变量,b是栈变
C语言_格式化输入输出
C语言_格式化输入输出 我们知道在最开始输入输出的printf和scanf里可以用百分号跟上一个字母d 说明要整数的类型,%f说明要以浮点数输出 printf() %flagswidth.prechlLty
c++11 实现单例模式
C11出来后,里面新增加了好多好用的功能 下面的单例就是使用了C11中的标准库中的mutex和unique_prt 进行内存管理的. 此单例模式不用担心内存的释放问题 pragma once include <memory include <mutex template <class T class Singleton { public: ty
我的C语言基础
C语言32个关键字auto 声明自动变量short 声明短整型变量或函数int 声明整型变量或函数long 声明长整型变量或函数float 声明浮点型变量或函数double 声明双精度变量或函数char 声明字符型变量或函数struct 声明结构体变量或函数union 声明共用数据类型enum 声明枚举类型typedef 用以给数据类型取别名co
.c文件和.h文件的关系
最近在做一个稍微有些复杂的项目。涉及到的函数、结构体、变量等比较多。通常,我编写c/c项目的方式是,有一个main.c文件,该文件的main函数作为接口,调用其他函数。所有其他函数按功能,分别放在不同的.h文件中,这样的方式在编译和运行上肯定是没有什么
C语言基础习题50例(一)1-5
虎为百兽尊,罔敢触其怒。惟有父子情,一步一回顾。 习题1 有 1 、 2 、 3 、 4 个数字,能组成多少个互不相同且无重复数字的三位数?都是多少?实现思路:显然,这个题目需要用到循环,并且是循环嵌套,先列出所有可能的组合,再去掉重复的组合即可。代码如下:cinclude <stdio.hint main(){ int i, j, k,
C语言基础习题50例(二)6-10
给大家推荐一门大数据Spark入门课程,希望大家喜欢。 习题6 用 号输出字母C的图案。实现思路:单行打印即可。代码如下:cinclude <stdio.h int main (void){ printf("\n"); printf("\n"); printf("\n"); printf("
C语言基础习题50例(六)26-30
习题26 利用递归方法求5。实现思路:使用递归。代码如下:cinclude<stdio.hint main(){ int rec(int n); int result rec(5); printf("5 %d\n", result); return 0;}int rec(int n){ if(n 1 || n
游戏安全实践的一些思考
移动的游戏能够稳定健康的上线。主要需要依赖以下在四个方面:1.前端展示,或者说客户端正常运行。性能稳定不崩溃,不过热能够稳定运行。2.后端,或者游戏后台服务端的。不但要稳定。还有能在有限的服务器资源下,能承受大量的同时在线用户。而且要让游戏中的每个模块都能够承受承受大量的同时在线用户。3.安全也是重点之中。这既包括客户端,又包括服务端。客户端的安全,包括要防
关于我想要编写dll文件这件事
0x1前言首先我们要用C、C编写dll,肯定是需要一个合适的编写软件,什么!?你不知道什么是dll,那你这有必要去看看我得上一篇了虽然篇幅不长,但是可以让你对dll有个大致得印象,由于本人用的CLion 2019的版本的,至于为什么不用VS,因为里面配置过于繁琐,很不友好,CLion安装包以及破解 群文件都有,可以去下载直接使用。话不多说,进入今天
使用Java 语言编写一个贪吃蛇游戏
使用 Java 语言编写一个贪吃蛇游戏,代码如下:GreedSnake .java package jzjsfx;public class GreedSnake public static void main(String[] args) SnakeModel model new SnakeModel(20, 30); SnakeControl c
计算(变量)
include <stdio.hint main() int price 0; printf ("请输入金额(元)"); scanf ("%d",&price); int change 100 price; printf ("找您%d元\n",change); return 0;上述程序编译执行的结果如下: $ cp
题解5道c++面试题第一期(含解题思路、答案解析和实现代码)
本篇文章送上5道c/c++面试题目,并附上答案、解题思路以及扩展知识。 1. 求下面函数的返回值c++include <stdio.hint func(int x) int iCnt 0; while(x) iCnt++; x x&(x1); return iCnt;int main() printf("cnt %d\n", func(9999