指针初阶

时间:2022-09-29 22:58:29

序言

指针这个模块是C语言里面比较难理解的的,学习成本倒是不高,就是有点费脑子.我们这里重点关注什么是指针和指针的用法.这篇博客我重新写了写了一遍,原来的那个实在太简陋了,里面新增了一下内容.

地址

谈到指针我们不得不说一下地址.什么是地址呢?地址就是能够标识一件事物的确切位置.这里有一个例子.张三是你的同学,一天,张三给你打电话,说李四,今天你来我家吧,我家在XXX小区XXX号楼.说完就把电话挂了.这时候你急冲冲的跑到张三的楼下.这时候你就犯难了,我不知道他在几层哪个房间啊?你需要再次打电话询问房间号.这个时候房间号就是一个地址.

指针是什么

我们已经明白了地址的意思,那么指针又是什么呢?这里我们类比一下,我们把这栋楼看作是计算机的内存,门牌号就是地址,不过我们在计算机中更加喜欢把这个地址称为指针,也就是说指针就是地址.

我们已经知道了指针的含义,那么我们是如何把内存给划分的呢.一栋楼可以被划分为多个房间,每一个房间就有一个门牌号,我们可以把一个房间看作是楼的划分单位.在计算机中,我们把内存的最小单元字节看作划分单位.我们给每一个字节都编上序号,就像是房间的门牌号一样,我们得知门牌号就可以找到那个空间.

指针初阶

这里的门牌号就是我们说的地址,也就是指针,我们仔细验证一下

#include<stdio.h>

int main()
{
int a = 10;
return 0;
}

指针初阶

我们需要把这个int a = 10这个语句给拿出来说一下.首先是关于变量a的,我们在内存中开辟一块空间,把a的数据,这就就是10给放进去.既然开辟了空间,那么这块空间肯定存在编号,这里的编号也就是指针就是a的地址.

假设你的电脑是32位的,这里我们笼统的解释一下,可以看作你的电脑存在32位地址线,每一个地址线都可以表示0或者1,也就是我们可以得到0到232-1共232个数据,我们把这些数据作为编号,用它来充当地址.等下在指针的大小那里我们会重点分析一下.

指针和指针变量

这个话题可能很少有人谈到,我们在看一些编程语言的书籍时会发现一会儿说指针,一会儿说指针变量,那么他们究竟有什么区别?这里分析一下.

我们看下面的代码,我们知道a是一个int类型的变量,他存储的是10,那么我们是不是也可以按照这个思路理解p也也是一个变量,存储的的是a的地址.,实际上我们可以这么做.

int main()
{
int a = 10;
int* p = &a //&a 是指针 p是指针变量
return 0;
}

这里总结一下.

  • 地址就是指针,指针就是地址
  • 指针变量是一个变量,里面存储的是地址.
  • 二者有区别,不过我们一般将指针变量说成指针

为什么定义指针

这个问题非常好,指针这么难理解,我们为什么还要定义这个东西呢,不是自找麻烦吗?这里我给出两个原因,我这里就解释第二个.第一个等到进阶的部分在和大家谈.

  • 简化代码
  • 参数传递

&a就是指针 p就是指针变量这里有一个问题,我们要求写一个函数,函数的功能是交换两个变量的值.我们该如何实现.

void swap1(int x, int y)
{
int temp = x;
x = y;
y = temp;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:a = %d b = %d\n", a, b);
swap1(a, b);
printf("交换后:a = %d b = %d\n", a, b);
return 0;
}

指针初阶

这里我们就疑惑了,我们不是实现了两个值的交换吗,这里为何没有变化.我们在函数那里就明白了,函数形参是实参的一份拷贝,我们改变形参是不会影响到实参的,那么我们如何修改原本的数据,这里就要用到指针了.

void swap2(int* pa, int* pb)
{
int temp = *pa;
*pa = *pb;
*pb = temp;
}
int main()
{
int a = 10;
int b = 20;
printf("交换前:a = %d b = %d\n", a, b);
swap1(a, b);
printf("交换后:a = %d b = %d\n", a, b);
return 0;
}

指针初阶

这里我们要谈一下为何这里可以实现.我们这样想,我们把a和b看作是一个上锁的箱子,指针就是钥匙.我们函数传参了,形参是实参的一份拷贝,不就是重新打造一个一摸一样的钥匙吗.这里的*就是开锁的动作,我们后面还要谈这个运算符,相同的额钥匙是可以打开同一把锁的,这就完成了函数的功能.

指针

现在我们就可以好好谈一下指针了,这里的指针还是比较简单的,主要是为了后面的进阶部分打下基础,不过要想理解透还是很困难的.我们先来谈一下指针的大小.

指针的大小

前面我们谈过,32位平台下,电脑存在32位地址线,每一个地址线都可以表示0或者1,也就是我们可以得到0到232-1共232个数据,我们把这些数据作为编号,用它来充当地址.那么我们就需要32个比特位来存储这个个指针,我们知道1字节等于8个比特位,那么32个比特位就是4字节.我们等下测试,这里想换算一下32位平台总共有多少指针.我们知道了共有2^32个指针,按照下面的换算方法.也就是说32位平台下,指针共有4g.

指针初阶

这里测试一下不同的类型对应指针的大小,结果和我们说的一样.

#include<stdio.h>

int main()
{
printf("char* : %d\n", sizeof(char*));
printf("short* : %d\n", sizeof(short*));
printf("int* : %d\n", sizeof(int*));
printf("long* : %d\n", sizeof(long*));
printf("long long* : %d\n", sizeof(long long*));
printf("float* : %d\n", sizeof(float*));
printf("double* : %d\n", sizeof(double*));
return 0;
}

指针初阶

同理在64位机器下,我们指针的大小是8,共有16g的指针.

#include<stdio.h>

int main()
{
printf("char* : %d\n", sizeof(char*));
printf("short* : %d\n", sizeof(short*));
printf("int* : %d\n", sizeof(int*));
printf("long* : %d\n", sizeof(long*));
printf("long long* : %d\n", sizeof(long long*));
printf("float* : %d\n", sizeof(float*));
printf("double* : %d\n", sizeof(double*));
return 0;
}

指针初阶

类型

这里就有问题了,既然我们每一种指针类型的大小都已经确定了,那么还设置这么多了类型干什么,直接用一个类型代替不久完了吗?这里就有很大意思.,需要我们一一分析.

  • 指针类型决定解引用可以访问几个字节
  • 指针类型决定 +1 可以走几步

指针的解引用

我们先来看下一下下面的用法,我们解引用赋值的的时候改变的是4个字节,这一点我们很容易接受,毕竟int类型是4个字节.

int main()
{
int a = 0x11223344;
int* p1 = &a;
*p1 = 0;
printf("%#x", a);
return 0;
}

指针初阶

如果说上面的我们很容易理解,那么下面这个你就会很迷惑.

#include<stdio.h>

int main()
{
int a = 0x11223344;
int* p1 = &a;
char* p2 = (char*)p1; // 强制类型转换
*p2 = 0;
printf("%#x", a);
return 0;
}

指针初阶

我们把int*指针强制类型转换成char*,这里去解引用,我们发现只能修改一个字节.这里我们就可以下一个结论了.

指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节).比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节.

指针+-整数

我们先来测试下下面的额用例.这个还是比较简单的,不过有的选择题会考这个这个,而且还是比较困难的,我们这里简单的简绍,等到后面专门出一个博客,好好分析一下相关的题目.

#include<stdio.h>

int main()
{
int a = 10;
int* p1 = &a;
int* p2 = p1 + 1;
printf("p1 = %p\n", p1);
printf("p2 = %p\n", p2);
return 0;
}

指针初阶

#include<stdio.h>

int main()
{
char a = 10;
char* p1 = &a;
char* p2 = p1 + 1;
printf("p1 = %p\n", p1);
printf("p2 = %p\n", p2);
return 0;
}

指针初阶

这里我们下一个结论,指针的类型决定了它加1跳过几个字节,而这个字节是跟解引用得到的类型有关.

指针运算

我们在谈一下指针的两个运算.都是比较简单的.

指针 - 指针

指针-指针得到的是两个指针之间元素的个数,我们知道指针是第一个字节的地址.

#include<stdio.h>

int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p1 = arr;
int* p2 = &arr[9];
printf("%d", p2 - p1);
return 0;
}

指针初阶

指针初阶

这里我们就可以模拟实现一下 strlen函数了

int myStrlen(char* p)
{
if (p == NULL)
return 0;
// 记录 '\0' 的 地址
char* end = p;
while (*end != '\0')
{
end++;
}
return end - p;
}

int main()
{
char* p = "abcde";
printf("%d", myStrlen(p));
return 0;
}

指针初阶

指针比较

指针比较这个知识点我就说一下,我们一般谈的是高地址、低地址,也可以说成大地址、小地址.这就可以比较打大小了.这个知识点主要用在双指针的题目中,这里我们以逆置字符串为例子.

void resreval(char arr[], int sz)
{
char* left = arr;
char* right = arr + sz - 1;
while (left < right) // 这里 就是 指针的比较
{
char temp = *left;
*left = *right;
*right = temp;
left++;
right--;
}
}
int main()
{
char arr[] = { '1','2','3','4','5' };
int sz = sizeof(arr) / sizeof(arr[0]);
resreval(arr, sz);
for (int i = 0; i < sz; i++)
{
printf("%c ", arr[i]);
}

return 0;
}

指针初阶

野指针

我们先来解释一下什么是野指针.现实生活中,我们可能会遇到野猫野狗,它们没有人管理,有可能会伤人.在计算机中我们把这种不受人管理的指针称为野指针. 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的.

现在我们类看一下未初始化的的情况,不同的编译器对未初始化的指针的处理是不一样的,我们VS的这个比较严格,不经初始化的是禁止使用的.

#include<stdio.h>

int main()
{
int* p;
printf("%d \n", *p);
return 0;
}

指针初阶

第二类就是这个指针前面是可用的,后面出了作用域这块空间就会被释放,新手总是喜欢返回栈指针.下面的代码是一定错误的,要知道a出了函数空间就会被销毁,你返回它的地址有什么用,里面的数据又不确定.

#include<stdio.h>
int* func()
{
int a = 10;
return &a;
}

我们测试一下代码,你就会明白了.

指针初阶

你一看这个不是很正确吗,那么你再试试多打印一次,相同的语句,得到不同的结果.这个是和函数栈帧有关,到时候你看了就会明白.

int main()
{
int* p;
p = func();
printf("%d \n", *p);
printf("%d \n", *p);
return 0;
}

指针初阶

还有一类情况,这个情况是我们以后最头疼的,访问越界.准确来说以后边界问题是我们的难点,这里简单的说一下.前面的10次这个指针都是正常的,当我们访问下标是10的这个时候就是访问越界,指针就变成了野指针.

#include<stdio.h>

int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
for (int i = 0; i <= 10; i++)
{
printf("%d\n", arr[i]);
}
return 0;
}

指针初阶

我们总结一下,可能造成野指针的情况.

  • 未初始化
  • 释放了,但是再次使用 返回 函数栈指针
  • 访问越界

这里说一下,我们在应用层面是检测不出野指针的,只有在运行时编译器有可能给我们报错,所以对于我们不在用的指针我们让他变成NULL.这样就可以减少野指针的出现概率,使用的时候,最好还是判断指针的有效性.

二级指针

我们知道了int的地址是指针,那么指针变量的地址又是什么?我们对指针变量取地址得到就是二级指针.同一我们可以一层一层套娃下去,得到三级、四级...不过我们一般就用到三级指针.

int main()
{
int a = 10;
int* pa = &a;
int** ppa = &pa;
return 0;
}

指针初阶

指针数组

我们在谈初阶最后的一个内容,指针数组.就像我们好孩子一样,我们的重点是孩子,同理指针数组是一个数组,只不过这个数组存储的是一个指针.

int main()
{
int a = 10;
int b = 20;
int c = 30;
int* parr[] = { &a,&b,&c };
return 0;
}

这里的parr就是一个指针数组.我们这里主要是分变这个东西究竟是什么,这个方法在进阶里面很有用,这里算是一个铺垫.首先我们知道[]的优先级高于*,所以parr先和[]结合,表明它是一个数组,此时我们再判断,我们看到一个*,该数组,每一个元素是一个指针,再判断一下,就剩下int了,那么指针的类型是int*.