c语言修炼秘籍【第五章】指针
【心法】
【第零章】c语言概述
【第一章】分支与循环语句
【第二章】函数
【第三章】数组
【第四章】操作符
【第五章】指针
【第六章】结构体
【第七章】const与c语言中一些错误代码
【禁忌秘术】
【第一式】数据的存储
【第二式】指针
【第三式】字符函数和字符串函数
【第四式】自定义类型详解(结构体、枚举、联合)
【第五式】动态内存管理
【第六式】文件操作
【第七式】程序的编译
文章目录
前言
本文会从介绍指针是什么开始,循序渐进地介绍指针的使用、指针使用时会遇到的问题、指针能进行哪些运算、指针与数组之间的关系,以及指针数组这些内容。
一、指针是什么?
指针是什么?
指针理解的2个要点:
- 指针是内存中一个最小单元的编号,也就是地址。
- 平时说的指针,通常指的是指针变量,是用来存放地址的变量。
指针就是地址,口语中说的指针是指针变量
内存:
指针变量:
通过取地址操作符&
取出变量的内存起始地址,把地址放入一个变量中,这个存放地址的变量我们就称其为指针变量。
int main()
{
int a = 10; // 在内存中开辟一片空间
int* pa = &a; // 使用 &取出变量 a的地址,将该地址赋值给变量 pa
// 变量 a在内存中占据 4个字节的空间,&操作符将这 4个字节的起始字节的地址存放在 pa中
// pa就是一个指针变量
return 0;
}
总结:指针变量就是用来存储地址的变量。(存放在指针变量中的值都被当作地址来处理)
一个变量是需要存储在内存中的,那么问题来了,一个地址需要多大的内存空间来存储呢?换言之,一个指针变量所占的内存空间是多大呢?
对于32位的机器,可以产生232个地址,也就需要32个二进制位来存储,所以32位机器中的地址大小为4字节;
对于64位的机器,可以产生264个地址,也就需要64个二进制位来存储,所以64位机器中的地址大小为8字节;
这时又会产生新的疑问,一个地址表示的内存空间又是多大呢? - - 1个字节。
原理可以参考https://www.zhihu.com/question/421675295
一个地址能表示1个字节,32位机器能表示的地址空间大小为4GB,这很显然对于我们现在使用的计算机而言是不够用的;一台64位机器的编址范围是多大呢?要知道240就已经是1TB了,264个地址是可预见的用不完的。
总结
- 指针是用来存放地址的,地址是唯一标识一块地址空间的。
- 指针大小在32位平台是4个字节,在64位平台是8个字节。
二、指针和指针类型
我们都知道,变量是有类型的,char类型、int类型、float类型等等,那么指针变量有没有类型呢?
看下面的代码
int a = 10;
p = &a;
要把a的地址存放在p中,这里的p应该是什么类型的指针变量呢?
下面是不同类型的指针变量
#include <stdio.h>
int main()
{
char *pc = NULL;
short *ps = NULL;
int *pi = NULL;
long *pl = NULL;
float *pf = NULL;
double *pd = NULL;
return 0;
}
// 指针变量的定义方式是:
// type +*
// type表示指针指向数据的类型,*表示该变量是一个指针
char *类型就是存放char类型变量的指针;
int *类型就是存放int类型变量的指针;
…
等等。
那指针类型的有什么意义呢?请接着往下看。
1.指针 + - 整数
看代码
#include <stdio.h>
int main()
{
int num = 10;
char *pc = #
int *pi = #
printf("&num == %p\n", &num);
printf("pc == %p\n", pc);
printf("pc + 1 == %p\n", pc + 1);
printf("pi == %p\n", pi);
printf("pi + 1 == %p\n", pi + 1);
return 0;
}
运行结果:
大家发现了有什么规律吗?
char类型在内存中占1个字节,char类型的指针+1,地址往后移动了一个字节。
int类型在内存中占4个字节,int类型的指针+1,地址往后移动了4个字节。
可以发现,指针的类型是决定了指针向前或向后走的步长。
2.指针的解引用
看代码,调试时观察pc和pi指向的内存空间中值的变化。
#include <stdio.h>
int main()
{
int num = 0x11223344;
char* pc = #
int* pi = #
*pc = 0;
*pi = 0;
return 0;
}
可以看到,虽然pc和pi的地址相同,但使用它们对num进行修改时,能够修改的范围并不相同,
pc只能修改一个字节,pi可以修改4个字节。
总结
指针的类型决定了,对指针解引用的时候有多大权限(能操作几个字节)
比如:char类型的指针,只能操作一个字节,int类型的指针能操作4个字节。
三、野指针
什么是野指针?
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
1.野指针的成因
- 指针未初始化
int main()
{
int* p; // 局部指针变量,未初始化,默认为随机值,此时 p指针是野指针
*p = 20;
return 0;
}
- 指针越界访问
int main()
{
int arr[10] = { 0 };
int* pa = arr;
int i = 0;
for (i = 0; i <= 10; i++)
{
*(pa + i) = i; // 当指针指向的范围超出数组arr的范围时,pa就是野指针
}
return 0;
}
这里要注意,不要写成*pa+i = i
,原因如下:
#include <stdio.h>
int main()
{
int arr[10] = { 10,9,8,7,6,5,4,3,2,1 };
int* pa = arr;
printf("%d\n", *pa + 3); // 这里会输出什么
printf("%d\n", *(pa + 3)); // 这里会输出什么
return 0;
}
运行结果:
这是因为操作符*
的优先级比+
的更高,
*pa + 3
相当于,对pa地址解引用之后再+3;
*(pa + 3)
才是先对指针+3再解引用。
- 指针释放的空间释放
在这里插入代码片#include <stdlib.h>
int main()
{
int *p = (int*)malloc(10 * sizeof(int)); // 动态分配10个int类型大小的空间给指针p
*p = 10;
free(p); // 将malloc分配给p的空间释放
*p = 20; // 使用释放后的空间,此时p为野指针
return 0;
}
这里使用到了动态内存分配,后续章节会详细介绍。
2.如何规避野指针
- 定义指针是进行初始化
- 小心指针越界,使用指针时检查边界
- 指针指向空间释放后置空
- 避免返回局部变量的地址
- 指针使用前检查有效性
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p = NULL; // 将指针初始化为NULL,此时仍然不能使用
*p = 20; // error,错误代码
// 可改为,指针使用之前检查其有效性
if (p != NULL)
{
*p = 20;
}
int a = 10;
p = &a; // 此时指针p有了确定的值,才可以使用
// c语言本身是不会检查数组的越界行为的,当使用指针访问数组时,需要程序员自己检查指针的行为是否越界
// 动态分配的空间在释放后手动置空
int* pf = (int*)malloc(10 * sizeof(int));
//.. 使用代码
free(pf); // 释放pf的空间
pf = NULL; // 手动将pf指向空指针
// 局部变量的地址
char *pc = NULL;
{
char ch = 'a';
pc = &ch;
}
*pc = 'b'; // error,这里使用了局部变量ch的地址,但ch的空间已经销毁,pc变成了一个野指针
return 0;
}
这些内容会在后续的指针详解中介绍。
四、指针运算
1.指针 + - 整数
#include <stdio.h>
#define N_VALUES 5
int main()
{
float values[N_VALUES];
float *pf = NULL;
// 对 values数组进行初始化
// 使用了指针+整数和指针关系运算
for (pf = values; pf < &values[N_VALUES]; pf++)
{
*pf = 0;
}
return 0;
}
指针 + - 整数表示,指针向后或向前移动整数个类型的空间
2.指针 + - 指针
int my_strlen(char* str)
{
char* pstr = str;
while (*pstr)
{
pstr++;
}
return pstr - str;
}
指针 - 指针 得到两指针之间元素的个数。
注:只有类型相同的指针才能进行该运算,且两个指针必须指向同一块空间
错误示例
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
int *pa = &a;
int *pb = &b;
char c = 'a';
char *pc = &c;
printf("pb - pa == %d\n", pb - pa);
printf("pc - pa == %d\n", pc - pa);
printf("pc - pb == %d\n", pc - pb);
return 0;
}
这样的代码没有任何意义
3.指针的关系运算
#include <stdio.h>
#define N_VALUES 5
int main()
{
float values[N_VALUES];
float *pf = NULL;
// 代码一
for (pf = values; pf <= &values[N_VALUES]; pf++)
{
*pf = 0;
}
// 代码二
for (pf = &values[N_VALUES]; pf >= values; pf--)
{
*pf = 0;
}
return 0;
}
上面的两个代码逻辑上实现的效果相同,但是建议使用第一种,第二种与数组的前一个元素比较的行为是c语言未定义的。不同编译器。
标准规定:
允许
指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针进行比较,但是不允许
与指向第一个元素前面的那个内存位置的指针进行比较
五、指针和数组
看一个例子
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
printf("arr == %p\n", arr);
printf("&arr[0] == %p\n", &arr[0]);
return 0;
}
运行结果:
可以看到数组名和首元素地址是相同的。
总结
数组名表示首元素地址。有两种特例,具体请看【第三章】数组
所以下面的代码是可行的
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int *pf = arr; // 将数组名当作地址放入一个指针中,我们使用的指针来访问一个数组就成为了可能
int i = 0;
for (i = 0; i < 10; i++)
{
printf("&arr[%d] == %p <==> pf + %d == %p\n", i, &arr[i], i, pf + i); // 比较指针是否与数组元素的地址相等
}
printf("\n");
for (i = 0; i < 10; i++)
{
*(pf+i) = i; // 使用指针来对数组进行初始化
printf("arr[%d] == %d, *(pf+%d) == %d\n", i, arr[i], i, *(pf+i));
}
return 0;
}
运行结果:
六、二级指针
指针变量也是变量,是变量就有地址,那么,指针的地址存放在哪呢?
存放在二级指针
中
int main()
{
int a = 10;
int *pa = &a;
int **ppa = &pa;
return 0;
}
二级指针的运算:
*ppa
访问的实际是pa。使用操作符*
对ppa
解引用,得到的是pa的地址。
int b = 20;
*ppa = &b; // 等价于pa = &b
**ppa
访问的实际是a。使用操作符*
对*ppa
解引用,等价于*pa
。
**ppa = 40; // 等价于*pa = 40
注意,二级指针并不能用来表示二维数组
二维数组在内存空间中地址是连续的,而二级指针并不是。
看例子
#include <stdio.h>
int main()
{
int arr[3][3] = { 0 };
int **parr = arr;
int i = 0;
int j = 0;
for (i = 0; i < 3; i++)
{
for (j = 0; j < 3; j++)
{
parr[i][j] = 3 * i + j;
}
}
for (i = 0; i < 3; i++)
{
for (j = 0; j < 3; j++)
{
printf("%d ", parr[i][j]);
}
}
return 0;
}
从内存空间上来分析:
那么我们要怎么利用指针来访问二维数组呢?
使用数组指针,指针数组不行
示例代码
#include <stdio.h>
int main()
{
int arr[3][3] = { 0 };
int (*pa)[3] = arr;
int i = 0;
int j = 0;
for (i = 0; i < 3; i++)
{
for (j = 0; j < 3; j++)
{
pa[i][j] = 3 * i + j;
}
}
for (i = 0; i < 3; i++)
{
for (j = 0; j < 3; j++)
{
printf("%d ", pa[i][j]);
}
printf("\n");
}
return 0;
}
运行结果:
这里对其进行简单的分析,关于数组指针还会在后续章节中进行详细介绍。
二维数组中的元素是一个一维数组,上述代码中,该一维数组的类型为int [3],所以二维数组名代表的首元素地址其实是这个一维数组的地址,要用一个类型为int [3]的指针变量来接收,也就是int (*pa)[3]。- - []中的3必须有。
而指针数组,不可行的原因也是因为类型不匹配的缘故。
但如果你非要用二级指针来访问二维数组也不是不行
看代码:
#include <stdio.h>
void print1(int* nums, int numsSize, int numsColSize)
{
int i = 0;
int j = 0;
for (i = 0; i < numsSize / numsColSize; i++)
{
for (j = 0; j < numsColSize; j++)
{
printf("%d ", *(nums + i * numsColSize + j));
}
printf("\n");
}
}
void print2(int* nums, int numsSize, int numsColSize)
{
int i = 0;
int j = 0;
for (i = 0; i < numsSize / numsColSize; i++)
{
for (j = 0; j < numsColSize; j++)
{
printf("%d ", nums[i][j]);
}
printf("\n");
}
}
int main()
{
int nums[][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
int numsColSize = sizeof(nums[0]) / sizeof(nums[0][0]);
int numsSize = (sizeof(nums) / sizeof(nums[0])) * numsColSize;
print1(nums, numsSize, numsColSize);
print2(nums, numsSize, numsColSize);
return 0;
}
这段代码中的print1函数是能够正确输出二维数组的值的。
这样的操作是建立在我们知道二维数组在内存中的空间是连续分布的基础上来实现的,所以c语言中各种数据类型它们在内存中是以什么形式存储的是非常重要的,这部分会在后续的进阶攻略中详细介绍。
七、指针数组
指针数组,顾名思义,它是一个数组,数组中的元素是指针。
int *pa[3]; - - 这就是一个指针数组,这个数组有3个元素,每个元素的类型为int *
总结
本文对c语言中使用的指针进行了较简略的介绍,包括什么是指针,指针类型有什么用,指针的一些运算,什么是野指针,如何利用指针访问数组,二级指针使用中会出现的问题,以及指针数组。