如何掌握 C 语言的一大利器——指针?
一览:初学 C 语言时,大家肯定都被指针这个概念折磨过,一会指向这里、一会指向那里,最后把自己给指晕了。本文从一些基本的概念开始介绍指针的基本使用。
内存
考虑到初学 C 语言时,大家可能对计算机的组成原理不太了解,所以这里先简单介绍一些“内存”这个概念。
众所周知,任何东西都需要有物理载体作为基础。
比如说人产生的“思维”这个东西,我们看不见摸不着,但并不是说它就可以凭空存在了,思维的物理载体就是我们的大脑。“大脑”之于“思维”就如同“土地”之于“人类”。
同样地,我们看不见摸不着的软件 / 代码也需要类似于“土地”和“大脑”的物理载体——存储器。
存储器分为两种:
- 内存:计算机中正在运行的程序以及运行过程中暂时产生的数据都在这里。
- 外存:那些暂时不需要运行的程序和最终的运算结果存储在这里。
比如一个 HelloWorld 程序:
#include <stdio.h>
int main()
{
printf("Hello World!\n");
return 0;
}
写完保存之后,程序会被存储在外存(硬盘)中。
当开始运行时,程序会被从外存调入内存中运行,打印 HelloWorld。
上面是内存的简单概念(一个很浅的印象):内存可以暂时存储数据。
那内存的结构是什么样的?
这里我们把内存想象为一幢有很多房间的酒店,每个房间都有一个独一无二的房间号。
人就是数据;内存就是酒店。
酒店的职责就是供人暂时居住;内存的职责就是供数据暂时存储。
内存的结构也像酒店一样,有很多“房间”,称之为“内存单元”,每个内存单元也有一个独一无二的“房间号”,称之为“内存地址”。数据就“住”在内存单元中。
假设现在张三住在酒店的 1001 号房间了。
我们就有以下关系:
房间号为1001的房间住了 客户张三
放到内存中,就是:
内存地址为1001的内存单元存储了 整数5
如此一来,我们就可以根据地址1001找到对应的内存单元,并对其中数据进行操作了。
但这样有一个问题,就是为了操作 5,而不得不记住其地址,对于人来说,记忆这么多数字太麻烦了。
想象一下你平常和别人打招呼时说:“早上好啊,某人的身份证号”,而不是“早上好啊,某人的名字”。
光是记住自己的身份证号就不容易了,更别说别人的了,所以我们平常的称呼是名字。尽管身份证号唯一,而名字可能会重复。
没错,就是名字,使用名字来代替对人不友好的内存地址。我们可以给 1001 号内存单元取个名字,就叫 a 吧。
我们取的这个“名字”就是编程语言都会有的“变量名”。
int a = 5;
变量名对我们人类来说就很友好了,什么 zhangsan、lisi等等都可以起。
通过变量名,就可以访问其值了。现在我们有一个变量 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,非常完美。
这里的 pa,就是一个指针(pointer)。可以看一下指针的定义:
In computer science, a pointer is an object in many programming languages that stores a memory address.
在计算机科学中,指针是许多语言中存储内存地址的对象。这里的对象可以是变量、结构体、函数或方法。
即,指针中存储的是内存地址。
指针的声明需要使用 * 来表示该变量是一个指针变量:
[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。
间接访问操作符
我们有了取地址运算符 &用来获取某个变量的地址,也知道了如何声明某种类型的指针用来存储地址。
知道如何获取了、懂了怎么存储了,那么怎么使用指针呢?
房间号不是用来好看的,而是用来找到房间和房间中的人。我们已经通过
&这个“前台工作人员”找到了房间号并记了下来,下一步就是上门把人找出来。
通过间接访问操作符 *,我们就可以根据指针“上门找人”了。
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:变量&a:a的地址int *pa:声明一个指向int类型的指针papa:指针*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
空指针
空指针的值为 NULL, 表示不指向任何对象。
int *p = NULL;
当我们初始化一个指针的时候,如果还不知道要指向谁的时候,就把它初始化为空指针。
一些用法
我们已经以”指向变量的指针”为例,介绍了指针的基本用法。现在介绍一些指针的其他用法。
指向指针的指针
前面我们介绍了“指向变量的指针”:
int a = 5;
int *pa = &5;
指针也是个变量,只不过相对于其他类型的变量有点特殊,指针变量中存储的是其他变量的地址。
也就是说,指针作为一个变量也有地址,该地址可以被其他指针存储,即指向了指针的指针。
对应代码如下:
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 |
a、5 |
*ppa |
pa、&a |
**ppa |
*pa、a、5 |
举一反三,你还可以试试 {指向[指向(指针)的指针]的指针}。
指针和数组
首先运行以下代码:
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 函数的目的很简单:传进来两个值,交换他们。
但是结果令人失望——根本没交换。原因是什么?
我们打印一些东西:
#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() 中的 x 、y 和 main() 中的 x 、y 的地址并不相同,这就意味着**此 x 、y 非彼 x 、y **。
原因很简单,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()打印出来的 x 、y 的地址不同,也解释了为什么交换失败。
那么,为了通过函数直接操作实参,我们必须使形参和实参是同一块内存。所以我们直接把实参的地址传给函数,也即,函数的参数为指针,指向实参的内存单元。这种参数传递为地址传递。
地址传递保证了形参的变化即为实参的变化。
代码更正:
#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语言高手(函数指针是解耦对象关系的最佳利器,还有signal)
记得刚开始工作时,一位高手告诉我说,longjmp和setjmp玩得不熟,就不要自称为C语言高手.当时我半信半疑,为了让自己向高手方向迈进,还是花了一点时间去学习longjmp和setjmp的用法.后 ...
- Atitit java方法引用(Method References) 与c#委托与脚本语言js的函数指针
Atitit java方法引用(Method References) 与c#委托与脚本语言js的函数指针 1.1. java方法引用(Method References) 与c#委托与脚本语言js ...
- 【转载】C/C++语言void及void指针深层探索
C/C++语言void及void指针深层探索 1.概述许多初学者对C/C++语言中的void及void指针类型不甚理解,因此在使用上出现了一些错误.本文将对void关键字的深刻含义进行解说,并详述vo ...
- C语言中的函数指针
C语言中的函数指针 函数指针的概念: 函数指针是一个指向位于代码段的函数代码的指针. 函数指针的使用: #include<stdio.h> typedef struct (*fun_t ...
- C语言精要总结-指针系列(一)
考虑到指针内容繁多,这里将指针作为一个系列,从简入繁,一点一点深挖并掌握这C语言的精华.初步计划如下 此文为指针系列第一篇: C语言精要总结-指针系列(一) 内存与地址 我们可以把内存看做一排连续的房 ...
- C语言精要总结-指针系列(二)
此文为指针系列第二篇: C语言精要总结-指针系列(一) C语言精要总结-指针系列(二) 指针运算 前面提到过指针的解引用运算,除此之外,指针还能进行部分算数运算.关系运算 指针能进行的有意义的算术运算 ...
- C语言第八讲,指针*
C语言第八讲,指针* 一丶简单理解指针 说到指针,很多人都说是C语言的重点. 也说是C语言的难点. 其实指针并不是难.而是很多人搞不清地址 和 值.以及指针类型. 为什么这样说. 假设有两个变量,如下 ...
- 【ButterKnife】 安卓程序猿的一大利器
注:近期才看到的这个类库,来自于jakewharton大神的力作,安卓里面的视图注入库 另小弟水平有限,翻译的不好,还请多多指正 首先是地址(托管在github上):http://jakewharto ...
- [C语言] 数据结构-预备知识指针
所有的伟大源于一个勇敢的开始 数据结构预备知识 指针 1.指针:是C语言的灵魂,指针=地址 地址:内存单元的编号 指针变量:存放内存单元地址的变量 int *p;//p是指针变量,int *表示该p变 ...
随机推荐
- Scala面向对象—类详解2(继承相关)
1.单例类 package com.zzzy class AAA {//单例 /*//java 思路--私有化构造方法,提供公开的getAAA 行不通 private def this(){ this ...
- 2019 Multi-University Training Contest 1 D.Vacation(思维)
题意:有n俩车行驶在一条道路上,每辆车有车长li,距离终点的距离si,速度vi,不能超出,并且驶过终点后会依旧保持原状态行驶,问最后一辆车过终点的时间. 思路:因为行驶过终点后还是要保持之前的行驶方式 ...
- P1714 切蛋糕 dp+单调队列
题意: 题目描述 在幻想乡,琪露诺是以笨蛋闻名的冰之妖精. 某一天,琪露诺又在玩速冻青蛙,就是用冰把青蛙瞬间冻起来.但是这只青蛙比以往的要聪明许多,在琪露诺来之前就已经跑到了河的对岸.于是琪露诺决定到 ...
- CodeForces - 1100E 二分+拓扑排序
题意: 一个n个节点的有向图,节点标号从1到n,存在m条单向边.每条单向边有一个权值,代表翻转其方向所需的代价.求使图变成无环图,其中翻转的最大边权值最小的方案,以及该方案翻转的最大的边权. Inpu ...
- URAL - 1029 dp
题意: n层楼,每层楼有m个房间.找出一个路径从第一层到达第M层,使得路径上的所有数的和是所有可达路径中最小的,每次上到下一层以后就不能再上去,依次输出路径上的各点在所在层的列数. 题解: 参考链接: ...
- forEachRemaining()方法的用法
forEachRemaining()是java1.8新增的Iterator接口中的默认方法对于这个方法,官方文档是这么描述的:Performs the given action for each re ...
- 查找命令中grep,find,which和whereis的使用及区别
在linux系统中,许多时候需要查找某些文件或者字符,如果用ls, cd 等基础命令就显得很无力了,那么Linux提供了grep,find,which 三种查找命令,在这里我记录一下: 一.grep命 ...
- TypeScript tuple 元组
TypeScript tuple 元组 元组类型允许您用固定数量的元素表示数组,这些元素的类型是已知的,但不必相同. "use strict"; /** * * @author x ...
- Web Vitals
Web Vitals https://www.youtube.com/watch?v=2k1QbgLAFdI&feature=youtu.be $ npm i -S web-vitals ht ...
- 关于USDN你想知道的一切
稳定币的发展只有短短几年的时间.稳定币最早起源于 2014 年 Bitfinex 组建的泰德公司发行的 USDT.稳定币是一种具有稳定价值的加密货币.稳定币诞生的背景是加密货币的价格波动巨大,其作为一 ...
