指针
一、指针概述
1.1 指针是什么
- 指针是内存中一个最小单元的编号,也就是地址
- 平时口语说的指针,通常是指指针变量,使用来存放内存地址的变量
总结:指针就是地址,口语中说的指针是指针变量(指针变量是用来专门存放地址的变量,下面会介绍)
1.2 内存
- 在计算机中,为了方便管理内存,所以给每个存储单元都编上了号,这个编号就是地址,也就是指针
- 每个内存单元都有唯一的一个地址,写C语言代码的时候。创建的变量、数组等都要在内存上开辟空间,,给出地址可以很方便找到对应存储单元
这里介绍一个操作符&,取地址操作符
int main()
{
int a = 10;
//通过&a可以将a的地址取出来,通过%p打印出来
printf("%p", &a);
return 0;
}
这里a是int型,占4个字节,所以a在内存中应该占4个单元,&a得到的是a占空间的起始地址(第一个字节的地址)
1.3 指针变量
- 我们可以通过&(取地址操作符)取出变量的内存地址,把地址可以放入到一个变量中,这个变量就是指针变量
int main()
{
int a = 10; //在内存中开辟一块空间
//a占4给字节,&a取的是a所占字节的第一个字节的地址
int* pa = &a; //&a取出a的地址后,放入pa这个指针变量中
printf("%p", pa); //打印pa里面存放的地址(a的地址)
return 0;
}
- 其中 int * pa; // int为整型指针变量,说明pa指向的变量是整形,*代表pa是指针
- 总结:指针变量,是用来存放地址的变量(存放在指针中的值都被当成地址处理)
1.4 内存的编址以及指针变量的大小
- 内存的大小跟地址线有关
- 对于32位机器,有32位地址线,那么每根地址线在寻址时产生高电平和低电平对应二进制0和1,所以1根地址线有2种表现形式,则32根就有232种形式,即有2的32次方个地址,每个地址标识一个字节,那么我们就可以给(232B=232/1024KB=232/1024/1024MB=232/1024/1024/1024GB=4GB)4GB的空间进行编址;同样64位机器即可给16GB空间编址
- 这里我们明白
在32位的机器上,地址是由32个0或者1组成的二进制序列,要存储这个地址,就需要32/8=4B的空间来存储,所以一个指针变量大小为4B
在64位的机器上,地址是由64个0或者1组成的二进制序列,要存储这个地址,就需要64/8=8B的空间来存储,所以一个指针变量大小为8B
int main()
{
//因为指针变量存放的是地址,所以不管什么类型的指针,存放的地址长度都是一样的
printf("%d\n", sizeof(int*));
printf("%d\n", sizeof(char*));
printf("%d\n", sizeof(float*));
printf("%d\n", sizeof(long long*));
printf("%d\n", sizeof(double*));
printf("%d\n", sizeof(long*));
return 0;
}
- 首先这里可以选择64位或32位,x64代表64位,x86代表32位
- 在32位机器下运行
- 在64位机器下运行
二、指针与指针类型
- 上文中已经了解了一些关于指针类型的知识,既然都是用来存放地址的,并且他们的大小还都一样,为什么还要分整形指针int*、字符型指针char*等等一系列指针类型呢?
//指针类型的意义
int main()
{
int a = 0x11223344;//0x开头是16进制数字
int* pa = &a;
int b = 0x11223344;//0x开头是16进制数字
char* pb = &b;
*pa = 0;
*pb = 0;
printf("a=%x\n", a);
printf("b=%x\n", b);
return 0;
}
-
这里我们调试起来,并且打开监视与内存窗口,赋值之前
-
赋值之后
通过调试与运行结果我们可以知道
- int*的指针解引用访问4个字节
- char*的指针解引用访问1个字节
- 结论:指针类型可以决定指针解引用的时候访问多少字节(指针的权限)
type * p;
- p指向的元素的类型为type
- P解引用的时候访问的对象大小是sizeof(type)
三、野指针
3.1 野指针的成因
- 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
-
- 指针未初始化
int main()
{
int* p;//局部变量未初始化时,里面是随机值
*p = 20;//在随机值对应的地址存上20毫无意义
printf("%p\n", p);//由于没有往p里存地址,所以p是野指针
return 0;
}
- 2.指针越界访问
由于该arr数组的地址范围为&arr[0]到&arr[9],而下面的指针p访问到了arr[11]这明显超过了数组arr申请的内存空间,导致了地址的不确定,所以从p访问到arr[9]之后,p就变成了野指针
//指针越界访问
int main()
{
int arr[10] = { 0 };
int* p = arr;
for (int i = 0; i <= 11; i++)
{
// *p的意思是p指向的地址里面的内容
//把i赋给p指向的元素
*p = i;
p++;
//这两行代码等价于*(p++)
}
return 0;
}
- 3.指针指向的空间释放
这里举一个简单的例子
虽然代码结果和预期的一直,但是这种方法是非法的
//指针指向的空间释放
int* test()
{
int a = 10;
return &a;
}
//进入test函数a创建,出函数a的空间还给操作系统
//局部变量a的生命周期是进入test到离开test
int main()
{
//所以这里的p指向的是销毁后的a,即便能正确打印出10也是因为之前遗留的脏数据
//这种打印是非法的
int* p = test();
printf("%d", *p);
return 0;
}
3.2 如何规避野指针
- 1.明确知道指针应该初始化为谁的地址,就直接初始化
- 2.不知道指针初始化为什么值,暂时初始化位NULL
- 3.指针使用之前检查其有效性
- 4.避免返回局部变量的地址
- 5.指针指向的空间释放即置为NULL
//规避野指针
int main()
{
int a = 10;
int* pa = &a;//指针初始化
//NULL本质就是0,只是让你看到NULL就能想到是指针
int* pr = NULL;//pr是空指针,这个指针不能直接使用,没有指向任何有效空间
//这样做的好处是后续可以判断是否为空指针,不为空指针的话就可以用
//但如果不致空就很危险
if (pr != NULL) //使用指针之前检查其有效性
{
}
return 0;
}
四、指针运算
4.1 指针±整数
- 通过上面学习的指针类型我们可以知道,指针类型可以决定指针解引用的时候访问多少字节(指针的权限),知道这些知识后,见下列代码
//指针+-整数
int main()
{
int a;
int* pa = &a;
char* pb = &a;
printf("%p\n", pa);
printf("%p\n", pb);
//pa是整形指针变量,其指针权限为4个字节,所以+1后应跳过4个字节
printf("%p\n", pa+1);
//pb是字符型指针变量,其指针权限为1个字节,所以+1后应跳过1个字节
printf("%p\n", pb+1);
return 0;
}
- 总结:指针类型决定+/- 1操作时的步长
- 如:type *p; //p±n 则跳过的字节为n *sizeof(type)
- 应用:不使用数组下标访问数组
//不使用下标访问数组(指针+-整数)
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
int* p = arr;
for (int i = 1; i <= 10; i++)
{
printf("%d ", *(p++));
//或者printf("%d ",*(p+i));
//注意数组名就是首元素地址,地址是指针,所以数组名arr也是指针:
//*(arr+i)=*(p+i)=arr[i]=i[arr]
//[]仅仅是一个操作符而已,就像加法一样
}
return 0;
}
4.2 指针-指针
- 指针-指针的相减的前提是这俩指针指向同一空间
- 指针-指针的数值的绝对值是指针和指针之间的元素个数
运行下列代码即可得到结果9
//指针-指针
int main()
{
int arr[10] = { 0 };
printf("%d\n", &arr[9] - &arr[0]);
return 0;
}
4.3 指针的关系运算
- 地址是有大小的,指针的关系运算就是比较指针的大小
- C语言标准规定:在数组中,允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较(其实大部分编译器都是支持的,但是有的编译器严格按照C语言标准执行)
允许指针P与指针P2进行比较,但不允许指针P与指针P1比较
五、指针与数组的联系
- 通过之前的学习我们知道:数组名=地址=指针
- 当我们知道数组首元素的地址之后,又因为数组是连续存放的,所以通过指针就可以遍历访问数组
- 数组是可以通过指针来访问的:arr[k]=*(arr+k)
//指针与数组
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
int* p = arr;
for (int i = 1; i <= 10; i++)
{
printf("%p <<====>> %p\n", p++,&arr[i-1]);
}
return 0;
}
六、二级指针
- 指针变量也是变量,是变量就有地址,那指针变量的地址该如何存储呢?
可以通过二级指针来存储,二维指针就是存放指针变量地址的指针变量;
直接看代码以及注释
//二级指针
int main()
{
int a = 66;
//指针p也是变量,也有地址可以通过&p看到p的地址
int* p = &a;
//*代表p是指针,int代表p指向的对象的类型是int
//pp是二级指针变量,二级指针变量就是存放一级指针地址的变量
int** pp = &p;
//*代表pp是指针,int *代表pp指向的对象的类型是int *
*(*pp) = 100;//可以通过二级指针解引用出一级指针的内容,再解引用就得到了a的内容
printf("%d", a);//通过二级指针pp修改a的值
//pp等价于&p
return 0;
}
七、指针数组
- 指针数组是指针还是数组?
答:是数组,是存放指针的数组
- 下面来看指针数组的应用
//通过使用指针数组打印arr1~3的内容
int main()
{
//用指针数组模拟的二维数组
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
int* parr[] = { arr1,arr2,arr3 };
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 5; j++)
{
printf("%d ", parr[i][j]);
}
printf("\n");
}
return 0;
}