(一) 指针知识先导
int num=100;
计算机中数据都是存储在内存中,因此读写数据的本质其实是读写内存,而目前读写内存的唯一方式就是通过变量名,这种方式被称为“直接访问”内存。
在计算机中,内存空间的最小单位为字节,操作系统会为每一个字节内存空间编号,并且这个编号在当前程序中是唯一的。
假设图是宾馆中的一排房间,每个房间中都住着一个球员, 例如:101 号房间住着 7号球员、105 号房间住着 2 号球员、113 号房间住着 1 号球员。
如果想要在这排房间中找到 2 号球员,只需知道他住 105 号房间即可。101 房号相当于内存地址、101 房间相当于内存空间、7 号球员相当于内存空间中的数据。
前面讲过,要想找到 7 号球员,必须通过房间号来查找。同理,在计算机中,要想读写内存空间中的数据,也可以先通过内存地址找到该内存空间,然后进行读写操作。
读写内存的 2 种方式:
第 1 种 通过变量名读写内存。
变量本质上是一块有名字的内存空间,通过变量名读写内存,如图所示:
第 2 种 通过内存地址读写内存。
在计算机内存中,每一个字节内存空间都有一个编号,这个编号被称为内存地址。通过该地址可以读写对应的内存空间,如图所示:
在上一节中,讨论了内存空间与内存地址的关系,为了更加深入了解这 2 者之间的关系,将使用 vs2012 自带的工具,来更加形象的分析。
测试代码如下:
【注意】
第 5 行代码中,&(shift+7)是 c 语言中的取地址符。&num 表示计算变量 num 所对应内存空间的地址编号,也就是所谓的内存地址。%p 表示以 16 进制格式输出内存地址。
编写完上述程序后,下面就来通过工具一步步探索内存空间。
第 1 步 测试程序第 6 行,鼠标单击加断点:
第 2 步 运行程序,控制台会输出 16 进制的地址数据(每次运行可能都不一样),
如图所示:
第 3 步 依次点击菜单【调试】->【窗口】->【内存】->【内存 1】,打开内存窗口,如图所示:
第 4 步 将控制台输出的数据,输入到【内存 1】窗口的【地址】栏中,然后按下回车键,如图所示:
第 5 步 在【内存 1】窗体内右键点击,然后选择【4 字节整数(4)】、【带符号显示】如图所示:
【说明】
因为 num 是 int 类型,32 位系统下占 4 字节,所以第 5 步选【4 字节整数】,其他类型数据依次类推。
第 6 步 查看【内存 1】窗口,可以看到整数+999,其实就是 999 只是显示了符号位+而已,如图所示:
分析:
1、 以上整个过程,首先第 4 行,通过变量名 num 将整数 999 写入内存空间中。
2、 第 5 行,使用&num 计算出变量 num 对应的内存空间地址 0019fe50。
3、 通过断点调试的方式,查看 0019fe50 地址空间中保存的数据 999。
4、 经过以上分析,变量 num 完整内存模型如图所示:
5、可以看到,访问一块内存空间可以通过变量名,也可以内存地址。
【调试技巧】
为了方便使用【内存 1】查看内存,建议将【列】选项设置为【自动】,如图所示:
前面介绍过,变量的本质是一块有名字的内存空间。其实这块内存空间不仅有名字,而且有编号,在 32 位系统下,这个编号是一个 4 字节的整数。通过(&变量名)的方式可以得到这个整数,例如:
int num=10;
printf(“%p\n”,&num); //以 16 进制格式输出
这个编号与一块内存空间是一一对应的,通过这个编号可以找到对应内存空间。类似于现实生活中,知道某个人的家庭地址,就可以通过地址找他家一样。在 c 语言程序中将这个编号形象的称呼为“内存地址”,通过内存地址就可以找到对应的内存空间。
现阶段目前,在程序中得到内存地址的唯一方式就是:&变量名。由于这种方式得到的内存地址就是变量所对应的内存空间地址,又是通过变量名得到的,因此可以称为“变量地址”。这里务必清楚,变量地址本质上就是内存地址。
用于保存内存地址的变量,称为指针变量。在 c 语言程序中不仅变量有类型,数据也是有类型的,例如:1(整数)、3.14(浮点数)、’c’(字符),需要使用与之匹配的类型变量进行保存。同理,内存地址也是一种数据,这种数据都是指针类型,因此需要指针类型变量来保存这种数据。
定义指针变量的一般形式为:
类型名 *变量名;
类型名表示该指针变量只能保存该类型变量的地址,*表示该变量是指针变量只能保存地址数据,变量名即该变量的名称。
例如:int *p_a;
int 表示该指针变量只能保存 int 类型变量的地址,*表示变量 p_a 是指针变量只能保存地址数据,p_a 即指针变量的名称。
指针变量和普通变量初始化方式相同,可以在变量定义时初始化,也可以先定义后初始化。例如:
在 c 语言程序中,将某个变量的地址赋值给指针变量,就认为该指针变量指向了某个变量,例如:
int a=10;
int*p_a=&a;
上述程序中,将整数变量 a 的地址赋值给指针变量 p_a,就认为 p_a 指向了变量 a,如图所示:
可以看到,变量 a 中存储的是整数 10,而变量 p_a 中存储的是变量 a 的地址。有点像现实生活中的中介,想要访问数据 10,必须先找到指针变量 p_a,通过变量 p_a 中的数据&a,再找到变量 a,最后访问数据 10。
指针变量的引用分 2 种情况:
第 1 种 引用指针变量。
运行结果如图所示(每次运行结果可能都不一样):
第 2 种 引用指针变量指向的变量。
运行结果如图所示:
【易混】在定义变量的时候,*放到变量前,表名变量是指针类型;在使用变量的时候用来读写指针变量指向的值。
速记:
&取变量地址;
*定义时表示是指针变量;*使用时表示读写指针变量的值。
在 c 语言中,函数参数不仅可以是字符型、整型、浮点型……等,还可以是指针类型,作用是将变量地址传递给函数形参。
主要目的就是:函数内部修改外部变量的值。
下面通过两个例子来说明指针变量做函数参数的用法。
案例 1,函数内部改变外部变量的值
案例 2: 封装函数,交换两个整型变量的值。
scanf 还可以接收多个输入数据,例如:
输入:1 2 (1 和 2 之间以空格隔开),然后按下回车键,运行结果如图所示:
scanf 中数据类型一定不能用错,float 类型必须使用%f、double 类型必须用%lf、int 类型必须使用%d,如果使用错了就会发现结果很奇怪。
使用 scanf 需要注意的问题
(1)scanf 函数中应该传入变量地址,而不是变量名,例如:
int a,b;
scanf(“%d %d”,a,b);
这种写法是错误的,应该将“a,b”修改为“&a,&b”。
(2)从键盘获取多个数据时,相邻数据之间可以使用空格、回车、tab 键作为两个数据之间的分隔符。例如:
scanf(“%d %d”,&a,&b);
第 1 种输入方式:
1 2 //1 和 2 之间以空格分隔
第 2 种输入方式:
1 2 //1 和 2 之间以 tab 键分隔
第 3 种输入方式:
1
2 //1 和 2 之间以回车分隔
(3)*如果在 scanf 函数中的格式控制字符串中除了占位符之外,还有其他字符,则在输入时也必须在对应的位置上输入相同的字符。例如:
int a,b,c;
scanf(“%d,%d,%d”,&a,&b,&c); //注意 scanf 中%d 之间以“,”分隔
输入:
1,2,3 (输入数据时,也必须以“,”分隔)
(4)使用 scanf 获取字符串时,只需传入字符数组名即可,取地址符&可以省略不写。例如:
char c[10];
scanf(“%s”,c); //可以省去&
输入:
hello
注意使用%s 的时候,字符串中不要有空格,否则行为很怪异。
scanf 有很多怪异的行为和坑,但是深入的东西研究价值不大,因此只要会常规的用法即可。
(二) 数组与指针
数组本质上是一片连续的内存空间,每个数组元素都对应一块独立的内存空间,它们都有相应的地址。因此,指针变量既然可以指向变量,也就可以指向数组元素。
在 c 语言中数组可以看作是相同类型变量的集合。通俗点讲,数组中每个元素类型都是相同的。例如:
char ch[10] //数组 ch 可以看作是由 10 个 char 变量组成
int a[10] //数组 a 可以看作是由 10 个 int 变量组成
float f[10] //数组 f 可以看作是由 10 个 float 变量组成
数组本质上是一片连续的内存空间,数组元素又可以看作是单独的内存空间,数组就好像是一排房间,数组元素是单独的一个房间。因此,每个数组元素也都有自己的内存空间地址,简称数组元素地址。
可以使用指针变量来保存数组元素地址,例如:
int a[5]={1,2,3,4,5}; //定义长度为 5 的 int 数组
int* p_a; //定义指向 int 变量的指针变量 p_a
p_a=&a[0] //把 a 数组第 0 个元素地址赋给指针变量 p_a。相当于&(a[0])
p_a 中保存了数组 a 第 0 个元素地址,可以认为指针变量 p_a 指向数组 a 第 0 个元素,
因为数组元素本质上可以看作是单独的变量,所以引用指向数组元素的指针变量与引用指向变量的指针变量方式相同,直接使用*指针变量名即可。
和访问变量方式类似,可以将通过数组名访问元素方式称为“直接访问”,将通过数组元素地址访问元素方式称为“间接访问”。
在计算机中内存的最小单位是字节,每个字节都对应一个地址。如果一个变量占用多个字节,就会占用多个内存地址。例如:char 类型变量占 1 字节就对应 1 个地址、short 类型变量占 2 字节对应 2 个地址、int 类型变量占 4 字节对应 4 个地址…..其他类型依次类推。同理,数组元素类型不同占用的内存地址也不同。
在 c 语言中,数组名与数组首元素地址等价。
指针本质上就是内存地址,在 32 位操作系统下,内存地址只是 4 字节的整数。既然是整数,就可以进行加、减、乘、除…..等算术运算。不过需要注意,在 c 语言中一般只讨论指针加、减运算,乘、除等其他算术运算是没有意义。
在实际开发中,指针加、减多用于数组(或者连续内存空间)。当指针变量 p 指向数组元素时,p+1 表示指向下一个数组元素,p-1 表示指向上一个数组元素。注意加减运算都不是“移动一个字节”,而是移动一个“单元”,对于 int 来讲一个单元是 4 个字节。
指针的加减法是指针和普通整数运算才有意义,两个指针加法没意义:p=p+n 表示 p 向下指 n 个单元,p=p-1 表示 p 向上指 n 个单元。
下面通过例子来了解一下指针减法。
当指针变量 p 指向数组元素时,p+1、p-1 分别表示指向下一个、上一个数组元素。依次类推,p+i、p-i 分别表示指向下 i 个元素,上 i 个元素。
p+i 不能超过数组最后一个元素,p-i 不能小于数组第一个元素。否则就会发生数组越界。
两个指针的加法没意义,两个指针的减法表示相差的单元的个数。
还经常使用到两个指针相减,例如:p2-p1。
当 p1 和 p2 都指向同一个数组中的元素时,p2-p1 才有意义。以数组 int a[5]为例:假设p2 指向元素 a[2],p1 指向元素 a[0],执行 p2-p1 时不是表示隔了多少个字节,而是表示 p2所指向的元素与 p1 所指向的元素之间隔了多少个元素。
下面通过例子来了解两个指针相减。
总结:
两个指针之间的减法表示:相差的单元的个数。
两个指针之间的加法,没有意义;
指针+普通整数表示指针向挪 n 个单元;指针-普通整数表示指针后挪 n 个单元;
函数参数不仅可以是变量,也可以是数组,它的作用是将数组首元素地址传给函数形参。
在 c 语言中,数组做函数参数时,是没有副本机制的,只能传递地址。也可以认为,数组做函数参数时,会退化为指针。
下面通过例子来了解数组做形参时,退化为指针。
可以看到 2 次输出结果不一样。这是因为当数组做函数形参时,会退化为指针。
void getsize(int nums[5])
退化为:
void getsize(int *nums)
在 32 位系统下,所有指针变量都占 4 个字节,因此第 5 行输出结果为 4。
由于数组做函数参数时,会退化为指针,导致无法在被调函数中计算传入的数组大小以及长度。为了解决这种问题,规定数组做函数参数时,必须传入数组长度,例如:
void getsize(int *nums,int length);
其中形参 length 表示数组 nums 的长度。
只有在数组声明的函数中才能通过sizeof(数组名)算出来数组的字节数;
int nums[]={1,5,8,9,666}; int *p=nums;这种情况sizeof(p)=4,因为p是指针,为啥sizeof(nums)就能算出20呢,因为编译器特殊对待。
因为c编译器比较低级,函数参数声明中即使使用数组类型void dy(int data[]),也会被退化成指针类型:void dy(int *data),因此在给函数传递数组的时候,要传递数组的名字,同时要在声明数组的函数中通过sizeof把数组元素个数算出来,穿进去,函数内部是算不出数组有几个元素的。
在 c 语言中,数组名等价于数组首元素地址。例如:int a[5],a 与&a[0]完全等价。可以认为 a+i 等价于&a[i],a+i 指向 a[i],那么*(a+i)就是 a+i 所指向的数组元素 a[i]。因此,*(a+i)与 a[i]等价。
无论是对于数组名来讲还是对于指针来讲:*(a+i)与 a[i]等效
除了在声明数组时候 sizeof(数组名)和 sizeof(指针变量名)的不同,其他时候“数组名”和“指针变量名”用法都是一样的。
(三) 字符串与指针
在 c 语言中字符串本质上就是采用字符数组形式进行存储。前面介绍过,指针可以指向数值类型数组元素,也就可以指向字符类型的数组元素。本节将介绍指向字符数组元素的指针。
在 c 语言中,字符串存放在字符数组中,要想引用字符串有两种方式:
使用字符数组存放字符串,通过数组名引用字符串,通过下标引用字符串中的字符
使用字符指针变量指向字符串,通过字符指针变量引用字符串、字符串中的字符。
下面的代码很简单:
当然也可以这样使用:
两个 sizeof 一样吗?
需要注意的是,不管是通过字符数组,还是通过字符指针引用字符串。编译器都会自动在字符串末尾添加 0,下面通过内存工具,来查看字符串在内存中是如何存储的。
第 1 步 编写测试程序
第 2 步 在 getchar 添加断点。
第 3 步 运行程序,记录 str 指向字符串地址。
第 4 步 在【内存 1】中输入字符串地址,然后按下回车键,如图所示:
如果没有显示出如图的效果,可以参考以下步骤配置查看内存方式。
右键窗口任意位置,依次选择【1 字节整数】、【不带符号显示】、【ansi 文本】。
可以看到字符串在内存中,是按照字符的 ascii 码进行存储的,并且最后一位是 0 作为字符串结束标志。
(四) 字符串处理函数
在 c 语言中字符串是非常重要的概念,字符串处理函数是针对字符串进行操作的一系列函数。主要包含在头文件中,本节将介绍常用的字符串处理函数。
strcpy 和 memcpy 的区别:strcpy 是把源和目标都看过字符串类型,因此会碰到'\0'停止;而 memcpy 则会原样复制。
ansi 标准规定,返回值为正数、负数、0 。而确切数值是依赖不同的 c 实现的,比如有的平台就是返回 1、-1、0。
字符串比较大小,不能使用算术运算符进行比较,例如:
str1>str2、str1==str2、str1<str2
使用算术运算符比较的是字符串首元素的地址,并不是比较字符串的内容。