作者LoganChien (简子翔)
看板b97902HW
标题[计程] 指标 1
时间Sat Dec 13 00:23:52 2008
指标
前言
这一次我们的主题是指标(Pointer),指标有什麽用呢?这是一个拢
统的问题,答案因人而异。
1. 对程序员来说,指标是一个用来操作记忆体的强大工具;
2. 对软体公司而言,指标是用来判断一个程序员是不是个人才;
3. 对教授来说,
指标是用来当人的强大工具。
所以你说指标重不重要呢?如果你还没有搞懂指标是什麽,赶快把这
一篇文读完。
┌──────────────────┐
│一段故事: │
│ │
│有一天上课的时候,我因为太想睡了,我│
│就放纵自己,小睡片刻,不过在半梦半醒│
│之间我好像听到: │
│ │
│老师有没有在课堂上讲 │
│指标没学好你的程式会怎样 │
│会当掉 会当掉 会当掉 │
│结果咧 │
│你没有在听嘛 │
│所以你的计程就被当了嘛 │
│ │
│老师在这里要再不厌其烦的告诉你 │
│好的指标带你上天堂 │
│不好的指标令你防不甚防 │
│就这麽简单嘛 │
│ │
│好 休息一下 下课十分钟 │
│前十个推文的老师有优待 │
│ │
│以上纯属虚构,如有雷同纯属巧合。 │
└──────────────────┘
指标概观
我们先想像一下生活中马路上的路标。
1. 路标会写有一个地点的位置,例如:台北向北 17 公里,基隆向
北 50 公里,这里路标指向一个地方。
2. 我们看到路标,我们可以只看看路标上写什麽。
3. 我们也可以跟着路标走,看看「路标指向的地方」长什麽样子。
4. 我们可以拿油潻去改路标。
5. 我们也可以跟着路标去,然後在「路标指向的地方」题上到此一
游。
在 C 语言之中,我们也有类似的工具,我们称之为
指标(Pointer)。
所有路标有的功能,指标基本上都会有。
1. 指标会存放一个叫做
位址(Address)的东西,会记录一个变数在记
忆体中的位置。
2. 直接写变数名就可以知到指标的值。
3. 我们可以用
取值(Dereference)运算子(*)来取得指标指向的变数
的值。
4. 我们可以用指派(Assign)来改变指标的值。
5. 我们在取值之後,可以用指派来改变「指标所指向的变数」的值。
正如地球上每一个点都有其对应的经度、纬度,程式的
每一个变数也
都有一个唯一的位址(Address)。
在 C 语言之中,每一个 byte 都会有一个位址,位址是以 byte 为
最小分割单位。而位址的大小因 CPU 的定址能力、作业系统、编译
工具…等平台相关的规格而异。在一个 32 位元的平台位址本身就要
用 32-bits 的非负整数来储存,也就是要用 4-bytes;在一个 64
位元的平台,就要用 8-bytes 的整数来储存。下面的范例程式码在
记忆体中的配置情况如右。
位址(Address) 值
int A = 0x11223344; 0x00112223: 0x11 ┐
int *B = &A; 0x00112222: 0x22 │
↑ 0x00112221: 0x33 │
┌──────────┐ │ 0x00112220: 0x44 ┘A ←┐
│备注:这是在 32 位元│ │ 0x00112219: 0x00 ┐ │
│Little Endian 平台可│ │ 0x00112218: 0x11 │ │Point-to
│可能的执行结果,实际│ │ 0x00112217: 0x22 │ │
│结果因电脑而异。 │ │ 0x00112216: 0x20 ┘B ─┘
└──────────┘ (位址仅供参考)
就和路标一样,指标也可能会有错。对於路标而言,在你真得走到错
误的地方之前你不会知道指标是错的;对於指标而言,大多数的情况
也是如此,
1. 如果你试图读取一个不属於你的程式的记忆体区块,你会得到毫
无意义的资料。若作业系统够好,则会触发 Segmentation fault
错误,之後作业系统会强制中止你的程式。
2.
如果你试图写入一个不属於你的程式的记忆体区块,你会得到
「本程式即将要关闭」或者是你的程式会莫名的终止(因为你有
可能写到自己程式的程式码区)。
加上 C 语言本身的设计,没有设定初始值时,变数的值可能会是一
个未定义的值,当你定义一个指标而没有给定初始值时,你是定义一
个不知到指到哪里的指标。
为了区分哪些是没有指向东西的指标,哪些是有的,0x00000000 这
一个位址被保留下来了,并且在 C 语言之中被定义为 NULL。我们之
後会再谈。
指标的宣告
鬼扯了这麽多,现在就让我们看看怎麽用好了。在 C 语言中,如果
我们要宣告一个指标,我们是在宣告的时候,
在变数名称之前多加一
个星号(*)。
int *ptr;
上面我们就宣告了一个可以指向 int 的指标 ptr。
严谨的来说 ptr 的型别是 int (Type-of-int),是一个可以指向 int
的变数 (Pointer-to-Type-int)。在这里我不建议各位把 int * 当
成一个型别。事实上 ptr 的型别是 int,经过星号的修饰,而成为
一个可以指向 int 的指标。我们可以看下面的例子:
int* ptr, var; /* is var pointer? */
上面我们并不是宣告了二个指标,我们
只宣告了一个指标 ptr,另一
个是一个整数 var。从这一个范例,我们可以看出来 int * 本身不
是一个型别[注1]。如果我要让 ptr 与 var 都是指标,你应该要用:
int *ptr, *var;
或者,你可以用 typedef 来为 int * 取一个别名︰
typedef int *IntPtr;
IntPtr ptr, var;
------------------------------------------------------------
[*注1] 其实这一句是有待商榷的,在 C++ 这一种东西被叫作
Compound Type 复合型别,不过和 C 一样,你的 * 只会作
用於 * 之後的变数,有兴趣者可以自行参阅。
------------------------------------------------------------
在宣告指标的时候,我强烈建议各位
顺手就把它初始化,就像我们初
始化一个变数一样,我们可以用指派运算子(Assign Operator, =)
来指定初始值。
int *ptr = 0;
或者,为了有更高的抽象意义,stdlib.h 定义了一个 NULL,你也可
以这样写:
int *ptr = NULL;
值得注意的是 ptr 可以指派 0 是一个例外,如果你指派非零的值,
通常编译器会发出警告:
int *ptr = 12345;
/* warning: initialization makes pointer from integer without a cast */
为什麽要用 0 或 NULL 呢?如前所述,0 是一个被保留下来的位址,
我们在 ptr 取值(Dereference) 之前,前检查是不是等於 NULL,如
果是,我们就不应取值;如果不是,则我们可以取值。这是一个安全
惯例,用来确保 ptr 的确有指向某个的地方(但不一定正确,我们
有其他的惯例用来保证正确性)。
如果我们希望初值化的时候就指向另一个变数,我们就可以善用取址
运算子(我们後面会再谈)。
int a = 0;
int *ptr = &a;
有时候我们想要向系统要求记忆体,我们可以用 malloc 动态配置。
(我们後面再谈)例如:
int *a = (int *)malloc(sizeof(int));
free(a);
取址(Address-of) 与 取值(Dereference)
接着我们来讨论
取址(Address-of) 运算子与 取值(Dereference)
运算子。他们的用途分别如下所示︰
取址(Address-of) ``&'' 位址(Address) 值
0x00112223: 0x11 ┐
取得一个变数在记忆体中的位址。
0x00112222: 0x22 │
↑ 0x00112221: 0x33 │
B = &A 情境对话:
│ 0x00112220: 0x44 ┘A
│ 0x00112219: 0x00 ┐
B: 我说 A 呀,你到底住哪里呀?
│ 0x00112218: 0x11 │
A: 0x00112220。
│ 0x00112217: 0x22 │
│ 0x00112216: 0x20 ┘B
取值(Dereference) ``*'' │ 0x00112215: 0x11 ┐
│ 0x00112214: 0x22 │
取得在特定位址的值。
│ 0x00112213: 0x33 │
│ 0x00112212: 0x44 ┘C
C = *B 情境对话:
C: 我看一下,B 上面写的地址是
0x00112220,我来去看看。
A: 你来我家有事吗?
C: 告诉我你家有什麽?
A: 0x11223344
所以我们来看一下 code 的范例:
int i = 0;
int j = 1;
int *ptr = NULL; /* 宣告指标 */
printf("%p", ptr); /* 印出 0 */
ptr = &i; /* 指向 i */
printf("%p", ptr); /* 印出 i 的位址 */
printf("%d", *ptr); /* 印出 i 的值 */
*ptr = 8; /* i = 8 */
printf("%d", i); /* 印出 i 的值 */
ptr = &j; /* 指向 j */
printf("%p", ptr); /* 印出 j 的位址 */
printf("%d", *ptr); /* 印出 j 的值 */
*ptr = 16; /* j = 16 */
printf("%d", j); /* 印出 j 的值 */
const 常数修饰字
在宣告指标的时候,我们可以用 const 来修饰我们的指标。const
的套用规则是:
如果左边有东西,则 const 会套用在他身上,如果
没有,则会套用在右边的的东西。
例如︰
(1) const int *ptr = ...;
这一个是一个可以指向 const int 的指标,ptr 本身可以改动,
但是 ptr 的取值(Dereference)不能改动。
int a = 0;
int b = 1;
const int *ptr = &a;
printf("%d", *ptr); /* Prints 0 */
*ptr = 2; /* Compile error */
ptr = &b; /* Ok, ptr itself is variable */
printf("%d", *ptr); /* Prints 1 */
(2) int * const ptr = ...;
这是一个可以指向 int 的常数指标,ptr 本身不可以改动,但
ptr 的取值可以更改。
int a = 0;
int b = 1;
int * const ptr = &a;
ptr = &b; /* Compile error */
*ptr = 2; /* Ok, dereference of ptr is variable */
printf("%d", *ptr); /* Prints 2 */
printf("%d", a); /* Prints 2 */
(3) int const * const = ...;
const int * const = ...;
这二个都是可以指向常数的常数指标。
int a = 0;
int b = 1;
ptr = &b; /* Compile error */
*ptr = 2; /* Compile error */
(4) const int const * const = ...;
这一个乍看之下和 (3) 一样,不过会是
Compile error。因为
你重覆使用 const 去修饰 int。
动态配置 malloc/free
有时候我们不会知到我们在编译的时候不会知道我们要多少记忆体空
间,我们要在执行期才能决定大小,例如我们要写一个可以读入无穷
长度的字串、一个很巨大的矩阵、等等。
这时候我们就可以用 malloc 向系统要更多的记忆体。malloc 的函式
原型如下:
void *malloc(size_t size);
我们可以向系统要大小为 size 的记忆体区块,注意,系统
不一定会
接受你的要求,
如果系统拒绝,就会回传 NULL。如果接受你的要求,
就会回传一个位址。不过它是 void 的指标,你还要转成你要的型别。
char *a = (char *)malloc(256);
↑ ↑
│ └── 256 bytes
└── 转型到正确的型别。
在用 malloc 之前,你必需要用引入 stdlib.h。
#include <stdlib.h>
然後我们可以用 malloc(要多少 bytes) 向系统要若干 bytes。
char *a = (char *)malloc(256);
上面这一个就是长度为 256 bytes 的字元阵列。不过系统不一定愿
意给我们,我们要用 if 来检查。
if (a == NULL) {
printf("ERROR: unable allocate.\n");
} else {
/* Allocated Successfully! */
}
如果你是要用 int,我们可以善用 sizeof。
int *b = (int *)malloc(sizeof(int));
b 就会是指向一个配置好的整数。
不过,malloc 有一点像是向系统借空间,有借要有还,所以我们要
用 free 把要来的空间还回去。
free(b);
b = NULL; /* 把 b 设为 0 */
指标安全使用手则
1. 初始化的时候一定要顺手给定初始值。
2.
取值之前,检查是不是 NULL,如果是则不应取值。
3.
当一个指标指向的东西被 free 或 指向失效的变数时,应该要重
设成 NULL。
後记
我们在这一篇文学到了指标的基本使用,从指标的基本概念、到取址
取值、const 修饰字、到 malloc/free。希望对大家有帮助。切记:
好的指标带你上天堂!
---------
我循着一条细长的线不断找寻,
有时我会遇到一些人,但他们只能给我下一站的方向,
有时我会遇到一些事,但它们只是通往目标的考验,
有时我会发现我已经走到线的另一端,却扑了空!
我会退後一步,说不定我忽视了另一条人迹罕至的小径。
--
※ 发信站: 批踢踢实业坊(ptt.cc)
◆ From: 140.112.241.166
1F:推 benck:太强大了 12/13 00:27
2F:推 dennis2030:感谢!! 对观念很有帮助!! 12/13 01:06
3F:→ dennis2030:之前因为不熟 一直能不用就不用 现在一定要用就头痛 12/13 01:07
4F:→ dennis2030:了 不过看完这篇以後有如神助阿XD 12/13 01:07
5F:推 dennis2030:话说 学会写诗是成为强者的其中一个条件吗XD? 12/13 01:16
7F:推 jimmy319:推 12/13 17:32
8F:推 jimmyken793:推 指标这块以前几乎都没碰... 12/13 21:10
9F:推 hrs113355:推 12/14 00:05
10F:推 chopssin:推 很有帮助+1 12/15 03:06
11F:→ crystal0825:太感谢了~~~ 12/16 00:55