废话不多说,直奔主题—指针🌈
文章目录
指针入门
指针的概念
🌳 什么是指针:
1. 指针是内存中一个最小单元的编号,也就是地址
2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
说白了,指针就是地址,地址又是数据,那么数据可以被保存在变量当中吗?
答:当然可以,所以指针变量和指针不同。下面我会重点谈论他们之间的关系
🌳为什么要有指针
就好比,宿舍楼里每个房间为啥有门牌号呢?
答:为了提高查找效率。计算机中也是如此,通过地址来访问数据,提高效率!
每个字节都有地址,每个地址都是不一样的,有大小之分。
数据以字节为单位,也是有高权值位和低权值位之分的。
Q1:CPU在内存中寻址的基本单位是?
答:字节
Q2: 在32位机器下,最多能够识别多大的物理内存?
答:其中,每个内存字节空间,相当于一间宿舍,房间住8个人,每个人就相当于一个比特位,宿舍门牌号就相当于指针。那么,为何要存在指针呢?为了CPU寻址的效率
🌳计算机中要指针(地址)干吗?
CPU主要负责计算,内存主要负责临时保存数据。而CPU要处理的数据和执行代码全都在内存当中。那么CPU和内存之间必须要进行数据交互。CPU要准确定位数据在内存的哪个位置。
这也使得CPU与内存之间需要构造一套完整可行的寻址方案。
🍀CPU与内存之间的线
32位机器有32根地址线(初学者可以先这样理解)
a.因为计算机只能识别二进制,那么地址线怎么表示呢?
地址线有无信号来表示:0/1二进制位。
b.CPU要读取数据,就需要将该数据的地址传给内存,内存根据地址寻找数据,再将数据返回给CPU。
c.CPU将通过地址总线将地址传给内存;内存又通过数据总线将找到的数据传给CPU。(数据总线和地址总线可能会复用,但我们目前不考虑这个)
🍀如何理解编址
CPU访问内存中的某个字节空间,必须知道这个字节空间在内存的什么位置,而因为内存中字节很多,所以需要给内存进行编
址(就如同宿舍很多,需要给宿舍编号一样)
计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件设计完成的。
钢琴 吉他 上面没有写上“都瑞咪发嗦啦”这样的信息,但演奏者照样能够准确找到每一个琴弦的每一个位置,这是为何?因
为制造商已经在乐器硬件层面上设计好了,并且所有的演奏者都知道。本质是一种约定出来的共识!
硬件编址也是如此
我们可以简单理解,32位机器有32根地址总线,每根线只有两态,表示0,1【电脉冲有无】,那么一根线,就能表示2中含
义,2根线就能表示4中含义,依次类推。32根地址线,就能表示2^32中含义,每一种含义都代表一个地址。
指针变量
🌳什么是指针变量:
🍁什么是变量?
定义一个变量,本质是在内存中根据类型来进行开辟空间。
🍁 为什么要有变量?
计算机是为了解决人计算能力不足的问题而诞生的。即,计算机是为了进行计算的。
而计算,就需要数据。
而要计算,任何一个时刻,不是所有的数据都要立马被计算。
如同:要吃饭,不是所有的饭菜都要立马被你吃掉。饭要一口一口吃,那么你还没有吃到的饭菜,就需要暂时放在盘子里。
这里的盘子,就如同变量,饭菜如同变量里面的数据。
换句话说,为何需要变量?因为有数据需要暂时被保存起来,等待后续处理。
那么,为什么吃饭要盘子?我想吃一口菜了,直接去锅里找不行吗?当然行,但是效率低。
因为我们吃饭的地方,和做饭的地方,是比较"远"的。
🍁指针变量的定义
指针变量就是存放地址的变量,任何指针变量的大小都是4个字节(32位机器)
🍁指针变量的类型
我们都知道,变量有不同的类型,整形,浮点型等。那指针有没有类型呢?
答:是有的。
int num = 10;
p = #
要将&num(num的地址)保存到p中,我们知道p就是一个指针变量,那它的类型是怎样的呢?
我们给指针变量相应的类型。
这里可以看到,指针的定义方式是: type + * 。
其实:
char* 类型的指针是为了存放 char 类型变量的地址。
short* 类型的指针是为了存放 short 类型变量的地址。
int* 类型的指针是为了存放 int 类型变量的地址
🍂字符指针
一般的使用方式:
另一种使用方式为:
上面代码的意思是把一个常量字符串的首字符 h 的地址存放到指针变量 ptr中
有一道面试题就考查了这一点:
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char *str3 = "hello bit.";
const char *str4 = "hello bit.";
if(str1 ==str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if(str3 ==str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
运行结果是什么呢?
为什么会是这个结果?
这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域(静态数据区),当几个指针
指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会
开辟出不同的内存块。所以str1和str2不同,str3和str4相同。
🍂 指针数组和数组指针
指针数组是数组还是指针? 答:数组。
数组指针式数组还是指针? 答:指针。
它们长啥样?
1.int *p1[10];(指针数组)
解释:p1先于[ ]结合,因为[ ]的优先级高于 *,说明p1是数组名,类型为int *
2.int(*p)[10],(数组指针)
解释:p先于 *结合,因为()优先级最高,说明p2是指针变量,指向一个大小为10个整形的是数组。
指针数组和数组指针怎么类型重定义呢?
typedef int(*ptr)[10];
typedef int* ptrr[10];
这样写是直接报错的!
点到为止
🍁指针运算
🍂指针±整数
指针±1,实际上移动的是其指向类型的大小。
运行结果是:
担心有人不知道NULL是什么?希望下面图片对你有帮助。
NULL是对0强制转换的结果。本质上就是0.
强制转换是什么呢?
答:强制转换不会改变数据的大小。它是给编译器看的,让它告警。它也是其它给程序员看的,当程序员看到强制类型转换时,他会认为你是刻意这么写的,而不是写错了。
运行结果为啥是这样的?
解释:
指针运算不要和整数运算搞混了
🍂指针-指针
🍂指针的关系运算
指针(地址)也是有大小之分的,所以指针之间可以比较大小.
方式1:
for(vp = &values[N_VALUES]; vp > &values[0];)
{
*--vp = 0;
}
方式2:
for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--)
{
*vp = 0;
}
实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。
标准规定: 允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与 指向第一个元素之前的那个内存位置的指针进行比较
🍁野指针
🍂概念
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
🍂野指针成因
1. 指针未初始化
2.指针越界访问
3. 指针指向的空间释放
指针进阶
🌳 指针解引用
一般情况:
int main()
{
int a = 10;
printf("before:a = %d\n",a);
int* p = &a;
*p = 20;//解引用
printf("after:a = %d\n", a);
return 0;
}
地址是数据,那我们可以根据地址数据访问吗?不用指针,可以吗?如图
答:理论上是可以的。不过,我们不知道哪块地址可以访问,哪块地址是安全的,哪块地址是允许你写入的…只有操作系统最了解内存的使用情况。
所以直接使用地址直接访问这个方案特别不好,我们还是用指针来间接访问。
不得不引用一个概念叫 ”栈随机化“。加以论证
题外话:大部分技术书,一定是落后于行业的。这本书也是,目前主流的编译器和操作系统,为了安全,已经有了很多内存
保护的机制。我们目前的win和Linux都有“栈随机化”这样的机制来方式黑客对用户数据地址进行预测。当然,还有其他的栈保
护机制,比如“金丝雀”技术之类的。这部分同学们后面可以在了解一下,如果有机会我在给大家说。
举个例子:
int main()
{
int a = 10;
printf("%p", &a);
return 0;
}
运行3次,结果如下:
a每次的地址都不一样!
🌳 指针与数组
首先声明一下,数组和指针是两个完全不同的概念。不可以混为一谈。
指针与数组的关系一:
为什么要降维?数组传参时,发生降维,降维成指针。(这是故意的)
答:如果不降维,就要发生数组拷贝,那么函数调用的效率就会降低。(在C中任何函数调用,只要参数实例化必定形成临时拷贝)
降维成什么?答:所有数组传参都要降维成指针,降维成指向其内部元素类型的指针。
指针和数组的关系2:访问方式通用。本质上两个不相关。
int main()
{
int arr[5] = { 1,2,3,4,5 };
int* ptr = arr;
//数组
printf("%d\n", arr[0]);
printf("%d\n", *(arr + 0));
//指针
printf("%d\n", ptr[0]);
printf("%d\n", *(ptr + 0));
return 0;
}
运行结果如下:
为啥它们写法都一样?
虽然写法一样,但是它们的寻址方案是不同的。arr是首元素的地址,是个字面值常量,可以直接访问,+1访问下一个元素。而ptr是指针变量,在访问ptr内容时,需要先找到ptr的地址。
所以数组和指针是两个完全不同的概念。那为什么要将指针和数组的写法设置的那么像?(下面是我个人的观点,如有雷同,纯属巧合)
答:假设指针和数组访问方式不通用,那么程序员需要在不同的代码片段处进行习惯性的切换,这样就大大增加了代码出错的概率。所以,为了让程序员降低出错的概率,将指针和数组的访问方式设置成通用的。
🌳 多维数组和多级指针
很多初学者见到多维数组就害怕。但是你只要将其作为一维数组看待就很简单。
int main()
{
char arr[3][2] = { {1,2},{3,4},{5,6} };
printf("%p\n", arr);
printf("%p\n", arr+1);
printf("%p\n", arr + 2);
return 0;
}
好戏才刚刚开始🐷🐷🐷
可否试试?
1.sizeof(a),其中a是每个层次首元素地址(因为可以是多维数组首元素地址),则计算大小为整个数组大小。首元素地址必须单独存在。
2.a本质上依然是地址,但是是首元素地址。a+0,的数值没有改变,但是含义却变了,它不在是首元素地址,而是一个普通地址。
3.a[1] == *(a+1);
a[1][2]== *( * (a+1)+2) 像这样的转换也要知道。
接下来我将选几个来解释一下。
二级指针
int main()
{
int a = 10;
int *p = &a;
int **pp = &p;
p = 100; //什么意思
*p = 100; //什么意思
pp = 100; //什么意思
*pp = 100; //什么意思
**pp = 100; //什么意思
return 0;
}
🥳🥳🥳 传参
一维数组传参
1.数组传参是要发生降维的。降维成指针,指针指向其内部元素的类型。
Q:为何要降维?
因为如果不降维,那么传过去的是整个数组,要将整个数组拷贝过去。而传数组名(首元素地址)只需要4个字节,提高效率。
Q:有没有发生临时拷贝?
传入的值需要变量保存,因此变量需要临时拷贝,使得形参实例化。比
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void show(int* pp)
{
printf("show:%p\n", pp);
printf("show:%p\n", &pp);
}
int main()
{
int a = 10;
int* p = &a;
show(p);
printf("main:%p\n", p);
printf("main:%p\n", &p);
return 0;
}
二维数组传参同一维数组传参。
1.int arr [3],数组类型是int [3],值得注意的是[]中的3也是类型的一部分。
2.数组作为形参时,如arr[][3],最高维可以省略。但其他的为啥不能省?
答:如果省略了,会导致指针类型不确定。
数组传参只能传首元素地址,这也说明参数的本质是指针。int arr[2][3][4],传入arr,那么参数的接收类型是int(*)[3][4],这说明了不需要最高维2,也说明了其余维度像3,4不可以省略。不然会使类型不确定。
🤡🤡🤡函数指针,函数指针数组,指向函数指针数组的指针(这里简单提一下)
函数指针
1.函数也是有地址的,但为啥函数也有?
函数是代码的一部分,程序在运行时,也要加载到内存以供CPU后续寻址访问。
Q:函数的地址是什么样的?
为啥是这样的?
答:所有变量既可以当成左值又可以当成右值,关键在于要不要写入的问题。然而函数只关心一件事:起始代码在哪。函数是不可以被写入。所以直接当成右值看待。那么&fun和fun没有什么区别。
😱 函数指针
Q:长什么样?
Q:怎么使用呢?
这个会解释吗?
那么
答:选A.因为
所以就是对0地址处写入。
😸函数指针数组
Q:什么样呢?
🤠指向函数指针数组的指针
Q:长什么样?
👉 如有错误或不足的地方,希望可以在评论区指出。
👉 那么有缘再见!🤟🤟🤟