初始指针
指针的基本概念
指针是计算机内存地址的别名。在C语言中,指针变量用于存储内存地址。指针使得我们能够直接访问和操作内存中的数据。
内存的本质
计算机的内存是一系列连续的存储单元,每个单元可以存储一定量的数据。这些存储单元通常以字节(byte)为单位进行寻址,每个字节由8个比特(bit)组成。
指针与内存的关系
指针存储的是变量的内存地址。了解内存模型对于深入理解指针至关重要。例如,通过指针,我们可以直接访问内存中的特定位置,这对于进行高效的数据操作非常重要。
以这张图举例,这是一个三十二位系统下的内存存储情况,内存从0000 0000到FFFF FFFF存储,每个十六进制数代表四个二进制数,例如1234 5678这个十六进制数
1(0001)2(0010)3(0011)4(0100) 5(0101)6(0110)7(0111)8(1000)
由此可见,在三十二位系统下,一共会产生16的8次方 16^8(2的32次方 2^32)中地址位置,也就是4GB,这代表三十二位系统的物理内存是4GB,32位地址可以寻址的最大内存空间是2^32字节,也就是4GB。
在代码里的一些应用
地址解密
我们在使用指针时,首先要定义这个指针,我们使用的定义方法是 数据类型* 指针变量(例如 int* pa),在这里我们要理解pa储存的并不是int类型的数据,而是我们通过他储存的地址进行寻址,可以找到一个int类型的数据,他储存的实际上是上方图片中类似与0X00000000这类的八位六进制。
让我们通过一段代码感受一下赋值地址与读取地址
#include <stdio.h>
#include <stdlib.h>
int main() {
int a = 10;
int* pa = &a;
printf("%p\n",&a);
printf("%p",pa);
return 0;
}
运行截图
既然我们打印出了数据的地址,那让我们来学习一下不同数据类型存储需要几个地址。
假设我们有一个int类型的变量a,其地址是0060FEF8。
int类型占用4个字节,所以这个int变量将占用地址0060FEF8到0060FEFB(包括0060FEF8)。
当我们声明一个指向int的指针int* pa = &a;时,pa将直接包含地址0060FEF8,并且当我们通过pa来访问或修改a的值时,编译器和操作系统会自动处理这4个连续的字节。
然而当指针指向的数据类型为char时,它的首地址就是完整的地址,因为char数据类型存储只需要一个字节,以此类推。
接下来我把各种类型的数据类型存储所需要的不同字节给大家。
#include <stdio.h>
#include <stdlib.h>
int main() {
printf("Size of char: %zu byte(s)\n", sizeof(char));
printf("Size of int: %zu byte(s)\n", sizeof(int));
printf("Size of long: %zu byte(s)\n", sizeof(long));
printf("Size of long long: %zu byte(s)\n", sizeof(long long));
printf("Size of float: %zu byte(s)\n", sizeof(float));
printf("Size of double: %zu byte(s)\n", sizeof(double));
printf("Size of long double: %zu byte(s)\n", sizeof(long double));
printf("Size of void*: %zu byte(s)\n", sizeof(void*));
return 0;
}
运行截图
但是指针变量不管它指向的是什么数据类型,它本身所占据的空间是4字节不改变。
&(取地址符号)解密
此外,我们可以看到代码中我们会用到&符号,这是取地址符号,我们通过这个符号加上变量就可以得到输出这个变量的首地址,从而找到这个变量。它的使用方式是&+变量名。
*+指针变量(解引用操作)
我们使用地址的目的是通过地址找到我们需要的数据,那我们该怎么通过地址找数据呢,这时候我们就要用到解引用操作,具体操作为*+指针变量,这是我们得到的数据不是地址而是地址指向的数据,如int* pa得到的是int类型,char* pa得到的是char类型
代码演示
#include <stdio.h>
#include <stdlib.h>
int main() {
int a = 10;
int* pa = &a;
*pa = 20;
printf("%d",a);
}
运行截图
为什么我们没有对变量a进行操作,最后输出的a却改变了呢,因为我们通过*pa解引用找到了a,通过改变*pa就成功改变a了,即*pa=a。
野指针
当我们写指针问题时遇到错误经常能听到是野指针的原因,那这令人厌烦的野指针到底是什么呢?
野指针: 指针指向的位置是是不可知的(随机的、不正确的、没有明确限制的)
野指针成因
1:指针未初始化
当声明一个指针变量时,如果没有为其分配内存或初始化其指向一个已知的有效地址,那么这个指针就是野指针。
代码示例
#include <stdio.h>
#include <stdlib.h>
int main() {
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
这个代码中 int* p 是错误的,正确的解决方法如下
解决方法
1:将int *p正确的赋一个变量的值,首先定义正确的变量,如 int a;在 int *p = &a;这样指针变量p就可以指向一个确切的地址。
2:倘若我们不知道这个指针是给谁用的,我们可以先给它赋空值,也可以避免没有确切地址的问题, int* p = NULL;
2:指针越界访问
如果一个指针指向了一个数组,而我们通过指针来遍历这个数组,当数组越界时我们指针访问的地址就不是数组的确切地址,而是不可知的。
代码示例
#include <stdio.h>
#include <stdlib.h>
int main() {
int arr[10] = {0};
int *p = arr;
for(int i = 0; i <= 10 ; i++)
{
*(p++) = i;
}
return 0;
}
在这段代码中,我们数组只定义了十个长度,也就是0——9,然后下面的for循环中我们一直累加指针地址到arr[9]这个尽头后继续加1,这时会造成指针访问越界。
解决方法
注意指针越界。当我们使用这个指针时,可以加一个前提代码,if( p != NULL),注意这个p指的是指针变量。
3:指针指向的空间释放
当我们使用指针时,它的空间已经被提前释放了,使用它便找不到确切的位置,将导致野指针。
代码示例
#include <stdio.h>
#include <stdlib.h>
int* test()
{
int a = 10;
return &a;
}
int main() {
int *p = test();
*p = 20;
printf("%p\n",p);
return 0;
}
在这段代码中,我们定义了一个test()函数,在这个函数中我们定义了一个 int a 并且最后会返回一个&a值,但我们最后输出指针变量p是却会报错,这是因为我们虽然定义了一个int类型的a变量,但这是在函数中定义局部变量,当我们结束这个函数时,这个局部变量也就被释放了,主函数中的指针变量也就找不到确切的地址值,最后导致野指针报错。
解决方法
留心释放函数。
指针运算
指针运算分为三种:
指针 + - 整数
指针 - 指针
指针的关系运算
指针 +- 整数
指针是可以进行加减运算的,在上面我们有提到指针就是数据的地址,这个地址是八位十六进制组成的,然后根据指针指向的数据类型的大小进行加减,因为指针的+1并不是从0060FEF8到0060FEF9那么简单,而是如果指针指向的类型是int,它的+1就是+ 所存储一个int数据的字节大小,也就是+4,从0060FEF8到0060FEFC,而如果指向的是char类型,则是+1,从0060FEF8到0060FEF9,它的加一指的是下一个数据。
示例代码
#include <stdio.h>
#include <stdlib.h>
int main() {
int arr[5];
int* p;
int i = 0;
for(p = &arr[0]; p < &arr[5]; i++)
{
*p++ = 1;
printf("%d\n",arr[i]);
}
return 0;
}
运算结果
在这个代码中,我们定义了一个长度为5的数组arr,将指针变量p指向arr[0]的首地址,然后对着指针变量p不断累加给arr数组赋值,按理来说累加是在数的基础上+1,而int类型占用4个字节,每个数组的首地址相隔为4,然后这段代码却能成功给arr数组赋值,说明每次累加p指针都能指向下一个数组的首地址,由此可以推断指针的+1指的是移动一个数组大小的字节。
指针加减整数的一些简单应用
在编写代码中,我们经常用到strlen()函数来求元素的长度,而指针加减整数也可以实现这一功能,我们可以利用指针加减整数遍历的特性来写一个我们自己的my_strlen()函数
示例代码
#include <stdio.h>
#include <stdlib.h>
int my_strlen(char* str)
{
int count = 0;
while(*str != '\0')
{
count++;
str++;
}
return count;
}
int main() {
int len = my_strlen("abc");
printf("%d\n",len);
return 0;
}
指针 - 指针
指针减指针是计算计算两个指针之间的距离
示例代码
#include <stdio.h>
#include <stdlib.h>
int main() {
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
printf("%d\n", &arr[9] - &arr[0]);
return 0;
}
运行截图
这段代码我们将数组arr[9]与arr[0]进行取地址然后相减,即可得从它们中间相差的数组个数。
当然,指针可以相减的前提是他们在同一块空间,分别定义两个毫不相干的指针是不能相减的。
指针-指针的一些简单应用
刚才在上方我们利用指针++的累加特性来实现了my_strlen()函数,这里我们也可以尝试用指针-指针来实现这一功能。
示例代码
#include <stdio.h>
#include <stdlib.h>
int my_strlen(char* str)
{
char* start = str;
while(*str != '\0')
{
str++;
}
return str - start;
}
int main() {
int len = my_strlen("abc");
printf("%d\n",len);
return 0;
}
运行截图
指针的关系运算
在前面其实我们已经运用过指针的关系运算了,接下来我用一段代码来让大家更进一步理解指针的关系运算
示例代码
#include <stdio.h>
#include <stdlib.h>
int main() {
int arr[5] = {0};
int* vp = &arr[5];
for(int i = 4; vp > &arr[0]; )
{
*--vp = 1;
printf("%d\n",arr[i]);
i--;
}
return 0;
}
运行截图
这一段代码我们通过比较现在遍历到的指针位置是否与指针的初始位置地址相同,如果不相同则给数组进行赋值, 再遍历到上一个数组直到成功给每一个数组都赋完值。