指针(1)
文章目录
前言:指针就是地址,在大多数文章以及书本里,经常会把指针变量简称为指针,但是实际上指针变量不等于指针,本系列文章在后续过程中如果出现了将指针变量简称为指针的情况会标明。
1.内存和地址
①内存单元
CPU处理的数据,是在内存中读取的,处理后的数据也要被放回内存中,为了方便处理和管理这些数据,把内存空间分为不同的单元,并将他们进行编号。
内存单元编号=地址=指针
一个内存单元的大小是一个字节
②编址
地址信息被下达给内存,通过地址总线并且以电信号的形式传递到内存,在内存上,转换成数字信号后,就可以找到该地址对应的数据,将数据在通过数据总线传入CPU内寄存器。
2.指针变量
①指针变量、取地址操作符和解引用操作符
指针变量就是存放地址的变量
取地址操作符:&
解引用操作符:*
int main()
{
int i = 10;
int* p = &i;//p是指针变量,变量名;int*是变量类型,*代表p是指针变量,int代表p指向的类型是整形
*P = 20;//这里的*是解引用操作符,也就间接操作符,p就变成了20
printf("%p\n", p);
//打印出的是存储整型变量i的四个内存单元的最小编号单元的地址,只打印一个
return 0;
}
②变量在内存中的存储
无论什么指针变量的类型取地址都是取得第一个字节的地址,调用int类型的地址从最低的第一个地址开始读取,一直读完往后的一共四个字节,调用long long类型,往后八个字节。
③指针变量的大小
32位平台下地址是32个bit位,指针变量大小是4个字节
64位平台下地址是64个bit位,指针变量大小是8个字节
注意指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的。
上面的图片都是在X64平台下使用的,所以地址都是64个比特位
④指针变量类型的意义
int main()
{
int i = 0X11223344, j = 0X11223344;
int* p = &i;
*p = 0;
char* p1 = &j;
*p1 = 0;
printf("%#X\n", i);//输出0
printf("%#X\n", j);//输出为11223300
return 0;
}
指针类型决定了解引用时可以访问的权限大小,char* 可以访问一个字节,int* 可以访问四个字节。
#include <stdio.h>
int main()
{
int n = 10;
char *pc = (char*)&n;
int *pi = &n;
printf("%p\n", &n);
printf("%p\n", pc);
printf("%p\n", pc+1);
printf("%p\n", pi);
printf("%p\n", pi+1);
return 0;
}
指针类型决定了变量+整数之后跨过的字节是多少个。
⑤指针变量的运算
1.指针变量+整数
#include <stdio.h>
//指针+- 整数
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int* p = &arr[0];//int* p =arr;数组名就是数组首元素地址
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
for(i=0; i<sz; i++)
{
printf("%d ", *(p+i));//p+i,这里就是指针+整数
}
return 0;
}
重点:int* p = arr;
arr[i] = *(arr + i) = p[i] = *(p + i)
2.指针变量—指针变量
指针本质上是一个操作受限的非负整数,用十进制表示32位平台上指针的范围就是[0,4G-1] (因为32根地址总线,每根线上都可以有0和1两种状态,指针的编号一共2的32次幂种情况,1个编号代表的空间可以存储一个字节,2的32次幂个字节,也就是4GB),必须是一段连续存储空间的不同单元指针才可以做减法。(数组)
不能相加,相乘,相除
//strlen的实现过程,未优化。
#include <stdio.h>
int my_strlen(char *s)
{
char *p = s;
while(*p != '\0' )
{
p++;
}
return p-s;
}
int main()
{
printf("%d\n", my_strlen("abc"));
return 0;
}
上面的图片是arr[4]-arr[0]也就是arr[0],arr[1],arr[2],arr[3]这四个元素个数
额外补充:笔者本人在遇到像上面的my_stlen函数里的需要把形参赋给另外一个变量进行改变值的时候,经常容易使用原变量进行改变,以上面的题为例,以后不要修改s的值,要修改p的值。这是一个很不好的习惯,在此提出,既是提醒自己,也是希望大家不要有这种不好的习惯。
3.指针关系运算
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
while (p < arr + sz) //指针的大小比较
{
printf("%d ", *p);
p++;
}
return 0;
}
3.const修饰常变量和指针变量
一句话,const在星号左面时,修饰的是指针变量指向的内容,修饰的指针变量可以更换指针变量指向的对象,但是不能通过指针变量来修改指向内容的值;const在星号右面时,修饰的指针变量本身,可以通过指针变量来修改指向内容的值,但是不能更换指针变量指向的对象。
int main()
{
int i = 10, j = 20;;
const int* p = &i;
int* const p2 = &j;
p = &j;
*p = 20;//不可以
p2 = &i;//不可以
*p2 = 10;
}
4.野指针的成因及解决
①野指针的成因
野指针的本质就是指向未分配内存空间的变量或者内存空间已经被释放的变量的指针。
1.指针未初始化
#include <stdio.h>
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
2.指针越界访问
#include <stdio.h>
int main()
{
int arr[10] = {0};
int *p = &arr[0];
int i = 0;
for(i=0; i<=11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
3.指向空间被释放
#include <stdio.h>
int* test()
{
int n = 100;
return &n;//出了函数这个括号后,就被释放了。
}
int main()
{
int*p = test();
printf("%d\n", *p);
return 0;
}
这些在VS2022上编译时都不会报错,只有在运行时才能发现问题,而且几乎没有提示。
所以我们如何避免这种问题呢?
②规避野指针
1.指针初始化
2.小心越界访问
3.遇到不用的指针,及时设置为NULL,指针使用前检查有效性,NULL指针就不访问。
int main()
{
int i = 10;
int* p = &i;
int* p1 = NULL;
*p1 = 20;
printf("%d\n", *p1);
return 0;
}//及时初始化
int main()
{
int arr[3] = { 0, 1, 2 };
int* p = arr;
int i = 0;
for (i = 0; i < 3; i++)
{
*(p++) = i;//注意是自加
}
p = NULL;//p已经越界,先设置为NULL。
p = arr;
if (p != NULL)//判断p不为NULL的时候再使用。
{
for (i = 0; i < 3; i++)
{
printf("%d ", *(p + i));
}
}
return 0;
}
此时P已经越界
P被设置为NULL,变成了0
5.assert断言
头文件 #include <assert.h>
#define NDEBUG,可以忽略assert的断言,无需更改代码就可以开启和关闭assert()
在release的版本中,会直接被优化掉,不执行了
相比于if语句的优势时,assert断言会报错
会在报错的情况下额外提示assert语句的位置