目的: 通过以下学习,希望能理解指针的概念,理解指针和数组的关系,理解指针的定义,掌握指针的用法。
1. 简述
用C语言写的代码基本上都用到指针,掌握好指针的概念对学好C有很大帮助。
为了方便理解我们可以把指针称作某一块内存的名字,这个名字可以没有对应的内容,如一个班,老师把其它班的名册拿来点名,肯定没有相应的人对应了,这种情况对应于指针没有赋值的情况。
1.内存存储数据是以8bit(字节)为一个单位去存数据的,每一个bit的数据不是【0】就是【1】,实际上是一个电平信号(高电平/低电平),我们祖国五千年前就有这个理论:阴阳学说、有无学说。
2.聪明的人类通过8个【1】【0】的不同排列次序,来表示不同数字和字母,还有符号的存储方法,这类方法称为编码,最常用的是ASCII码。
计算机的内存会被分成许多小块(每8bit为一小块),每一小块都有一个编号,为了更为直观,我们可以把全部的内存比作成一条大街, 街上并排着房子,每个房子相当于一块内存空间,房子房号(地址)相当于内存的编号,内存的操作有两种,一种是向内存写入数据,另一种是读取内存的数据,就等于住进房子或者搬离房子。
住房和搬家,大家都懂吧?
它需要先决条件:
- 房是你的,或者你有权利居住的;
- 房的地址,在几街几号;
- 房子的大小。
操作内存需要一样的先决条件。
下面我们上一个图去看看内存的具体编号是怎么样的:
2. 指针声明
指针的声明是通过【*】这个符号来告诉使用者的。
【*】是一个操作符,和我们常见的【+】【-】【x】【/]一样,这个操作符的作用是,间接取值。
声明一个指针的目的是要关联一个内存地址编号,亦可以说这是为一个编号取名字,例如:
- 通过 【int * pi】声明了一个int类型的指针pi .
- pi 是一个名字,但它还没有关联内存地址编号,只能说它是“查无此人”的名字。
- 我们怎样为pi关联内存地址呢?
(1)可以通过直接赋地址的方式,参照图1内存编号,可以这样:
pi = 0x00000001 # 地址0x1就和pi关联起来。
#但是这样有问题出现?
#回忆上面提到的住房和搬家的先决条件,程序是否有权限操作0x1?
(2)指针常用的赋值的方式是:
①通过&操作符先得到一个可以用的地址,如:已经有声明【int i】,&i 就能取到i的地址。
②将取到的地址赋值给指针,如: pi = &i
- 我们没有为pi关联地址前,千万不要使用往pi里存放东西, pi没有关联地址前,这就好比一个不存的房子,试问一个不存的房子能住人吗?
在计算机的世界里,没有关联地址的pi,也可能悄悄的关联了一个非法地址,如关联了一个已经有了主人的房,试想你入住这个房时会发生什么事情?
指针的另外一个操作符 & ,这个却是个翻译操作符,给定一个变量,"&变量"能将变量翻译成地址,就如你有绰号时,用"&绰号"可以得出真名。这个&的运行原理,是根据编译的一个表去对号入座的,每一个符号在内存里都有一席之地,编译时会对这些做好记录,当你要使用&,就会对照这个记录给出关联的席位(地址)。
3. 变量的概念
计算机编程里出现最多的就是变量,指针也属于变量。
变量的要素:
- 变量名
- 变量名在内存的位置
- 变量的值
- 变量的值的位置
- 普通变量和指针变量
- 变量名是变量的称呼,如人名。
- 变量就是内存编号的外号,内存中其实是不存在这个名称的,主要是通过编译器把变量名和一块内存空间的编号关联起来。
- 变量的值就是变量名所关联的内存空间里存放的数据,例如0X00000002里存放着数据【xyz】,那么变量pi的值就等于【xyz】。
- 普通变量可以直接取值,变量名和变量值之间没有间隔,例如声明 int i = 10, 假定i代表的是0X00000002编号, 要取值,直接在编号0X00000002的地址里抓就是,因为10就是住在0X00000002这个内存空间里。而指针变量名和变量值之间存在着间隔,指针代表的内存空间里的值,并不是最终的我们所期待的值,真正的值需要使用指针操作符(*)间接取值,如果要比喻,就像要找一个人,这个人的地址放在一个房子里,只有在这个房子里找出地址,通过找出来的地址才能找到这个人。这就是间接的含义。
- 为什么要设计指针这种间接操作的变量呢?其中涉及到内存高可用、数据复制效率、程序运行效率等方面的问题,假如我们分别为0X00000002和0X00000001这两个空间复制0X00000000的数据,0X00000002使用指针方法,只需要把0X00000000这几个数复制到0X00000002里面的空间;0X00000001用普通方法,是要把0X00000000里面的所有数据搬过来,如果0X00000000里数据非常大,两种方法的工作量和效率从中可见。
4. 实验
#include <stdio.h>
int main(void)
{
int i = 10;
int * pi;
printf("i take the place of %p\n", &i);
printf("pi take the place of %p\n", &pi);
printf("i data is %d\n", i);
printf("pi data is %p\n", pi);
pi = &i;
printf("pi take the place of %p\n", &pi);
printf("pi data is %p\n", pi);
printf("pi mean %d\n", *pi);
return 0;
}
在 centos64bit 系统下编译后运行结果如下:
- i take the place of 0x7ffd94da2c4c
i是0x7ffd94da2c4c的外号,代表的是0x7ffd94da2c4c,如人名代表的是人
- pi take the place of 0x7ffd94da2c40
pi是0x7ffd94da2c40的外号
- i data is 10
存储在i里面,0x7ffd94da2c4c这个内存地址空间里的是数字 10
- pi data is 0x7ffd94da2d30
没有赋值前,pi里面的数据是0x7ffd94da2d30,即0x7ffd94da2c40空间内是0x7ffd94da2d30
- pi take the place of 0x7ffd94da2c40
通过pi=&i赋值后,pi所代表的空间编号没有发生变化,发生变化的是里面的数据
- pi data is 0x7ffd94da2c4c
pi里面装的是0x7ffd94da2c4c,它是一个里面装有int数值10的数据地址
- pi mean 10
通过间接运算符*取pi的值,得到的正是我们期待的值。
5. 进阶
通过以上的学习我们基本上明白了int * p, 和 &p 是什么意思了, 下面我们来进一步区分 const char *p, char * const p, 注意:const char 和 char const 其实是一样的,const都是对char进行修饰。
const是常量,表示不变的意思,它用来修饰变量,使变量变得很有意思!居然有不变的变量,这不就矛盾了吗?
(1) const char *p 表示 char is const, 根据上面的房子的比喻, p是一个房号, char则为房子里的东西, 而const则限定了房子里只能为不变的东西,怎样不变法呢? 假定房里装了五个苹果,如果用const去限定,则不能增加和减少这数量,更不能把苹果换成雪梨。虽然char不能改变,但我们却是可以改变p的值, 假如p关联的是1号房,我们可以改为关联2号房, 如: <1>const char * p; char g[] = "GG"; p = g; <2>char m[] = "MM"; <3>p = m 这样p就由GG变成了MM。
总结可知:const char 只和char有关,和 *p并没有什么关系!
(2)有了上面的解释,我们很容易理解 char * const p, 意思就是 p 的指向 (p关联的内存编号)不能变更了,如p和1号关联了,那么就不成改成2号、3号、4号等等,而至于1号房里装的东西,这却是可以变更的,只要是装着char类型的东西就可以,例如装着2个苹果可以加装3个、4个、5个······ 但是不能将苹果改为装雪梨。
(3) 依此类推,我们可以掌握 const char * const p等声明的含义。
6. 不得不说的指针与数组
(1)什么是数组呢? 数组就是内存里的一段固定的连续的区域,固定说的是位置已定,用房子去解释就是编号连在一起的房子,从左到右(相对来说、从下到上)并排在一块的N个房子,且里面装都是同一类型物品,我们称它们为组。
(2)我们知道指针是某一块内存的编号,而数组又是内存的N个块的组合,两者都和内存地址有关,这就扯上关系啦。我们可以把指针指到数组里,如果指针里装着的刚好是数组的开头元素的内存地址,我们就可以把这个指针和这个数组对等起来,而实际上我们也是这样定义,指针可以等于数组的名字, 如char a[5]; char * p; 然后 p = a 。
- 不过,如果a = p反过来赋值却是不可以的, 因为数组的位置已经固定,是个常量,而指针是变量,我们不能够把变量赋值给常量,即是说可以变化的东西不能赋给不变的,比如"面包=包粉",是不可以的,因为面包做不了面粉,但"面粉=面包"却是允许的,这就是常量和变量的区别。
- 所以,我们不要奇怪变量p可以p++, 常量a却不能a++,因为++是个增量操作符,是对它的左值或者右值进行修改操作的符号,只适用于变量。
(3)数组的名字是数组所在的内存空间的地址名的外号,因此数组的第一个元素的地址和它的名字所代表的地址一样,如: 由 int i[4] 得出 i = &i[0],我们再声明一个指针 int * p, 将i赋值p,指针p就可以顶着数组i的名头办事。
(4)我们通过声明int * p , 然后将int[n]数组的地址赋给 p,这是因为两者类型相同。当我们遇到的是二维数组或者更多维数组时,指针的声明就必须要与数组的类型、长度都相同。
- 这里的长度是指数据占有的内存空间,比如我们要将int i[2][4]的地址赋给一个指针,首先我们要理解i[2][4]含义——它是一个有两个元素的数组,每个元素都包含了4个int值,即是说这个数组的长度是4个int。
- 所以我们声明指向这个数组的指针的长度必须是4个int, 声明如下: int (* p) [4], 为什么要用括号将(p)括着?是因为[]的优先级高于,如果没有括号(),p就会先和[4]结合,这就变成了声明的是一个指针数组,两者区别是: (p)[4]只有一个指针变量, 而p[4]有4个指针变量。
- (p)[4]内存只需要保留一个空间来存放包含有4个int值的内存地址,就是说只需要保留一个房号,而p[4]是需要预留4个房号的。
(5)一维数线可用线性表示,二维就是一个平面,三维则是立体,至于四维、五维这些多维度的数组就很难想象和理解了,所以非不必要千万不要使用超过三维的数组。
7. 空指针和内存泄漏
(1) 空指针是指:"指针还没有和内存地址关联"。明确的空指针是直接把指针设为NULL,但编程中会出现很多不明确的声明。如:char *p ; 编译后内存只会将一个内存地址保存p这个符号,而这个p里面究竟装着什么东西,就看系统而定,这时,需要做下一步的指向工作,如: char e[] = "我思故我在”; char *p ; p = e ; 这样p就有了明确的归属。在未确定p里面是什么前,我们千万不要往p里写东西,如:char * p; 然后直接 *p="XX",这是错误的,不过这样声明却是可以的: char * p = "XX"。
(2) 内存泄漏是指:"无法再使用某个曾经使用过的内存空间"。导致内存泄漏的原因的前提是:程序员自已想管理内存,如通过malloc申请得到了一段内存空间,为了记忆,你必须给这空间起个名字,比如叫 swap, 用了swap一段时间后,你觉得不够用,于是又申请了一段空间,一时疏忽把新空间命名为swap,这就使前一次申请的空间无法通过swap来引用,因为所有需要写入或修改的数据都写入这个新swap中,这在计算机术语里称为“悬空指针”。像电话号码,你把某人的号码忘记了,又或者某人的号码被他人用了,导致你再也找不到某人。
(3)内存泄漏的危害远远不是忘记这么简单,它会令到程序崩溃,更为可怕的是你永远不知道究竟什么原因导致崩溃,因为内存泄漏的排查是一项很艰辛的工作,编译器是不会直接告诉你这是内存泄漏。
8. 指针运算
(1)指针是可以进行加减运算的,和指针进行运算的必须是整数,而且指针加减后所得的结果是和指针指向类型有关联的,如int * p,p+1的结果是p加上int所占的字节,这个字节数可能是4、8、16,不同体系有不同定义,反正指针的加减等于指针和指针所指类型的字节和整数的乘积的加减。
(2)指针变量可以进行自增减运算,如int * p ; p++, p--,--p,++p等是可以进行的。
(3)指向同一数组内元素的指针是可以进行差值运算的,如:
int i[20];
int * p1, * p2;
p1=&i[1];
p2=&i[5];
#p2 - p1是可以运算的。
(4) 具有相同类型的指针值,可以用关系运算符进行比较。
9. 练习
声明一个数组:int i[4][2] = {{2,4},{6,8},{1,3},{5,7}};
分析:
- i 是什么?
- i + 2 ?
- *(i+2) ?
- *(i+2)+1 ?
- ((i+2)+1) ?
答:
1. i 是二维数组i[4][2]的第一个大小为2个int元素的地址
2. i+2 是二维数组i[4][2]的第三个大小为2个int元素的地址
3. *(i+2) 是(i+2)地址里的值,也是一个地址,它是数组第三个元素里的第一个int值的地址,即是&i[2][1]
4. *(i+2) + 1 是数组里第三个元素的第二个int值的地址,即是&i[2][2]
5. *(*(i+2)+1) 是数组里第三个元素的每二个int值的值,即是i[2][1] 的值。