C语言中的指针是一种强大的特性,它允许程序直接访问和操作内存中的数据。指针的概念对于理解和高效使用C语言至关重要,它为数据结构的构建、内存管理、函数参数传递等提供了灵活性和效率。那么接下来,我疆带着大家来详细了解指正及其用法。
一、 什么是指针
指针 是作为c语言中的重难点,也是一位程序员必须学会的知识点之一。其本质上是一种变量,它存储了另一个变量的""内存地址""。
总的来说:指针就是地址,口语中说的指针通常指的是指针变量。
那我们如何理解 指针 这个抽象概念呢?
1.内存
在了解指针之前,我先为大家讲解 内存 这一概念。
内存 是 \ 计算机 \ 中的一块区域,用于 临时 存储数据 和 程序。当我们运行一个程序,比如用C语言写的小程序,程序的数据和代码会被加载到内存中。
内存 分为 \不同的部分\ ,比如有的部分用来存放 正在运行的函数和它的局部变量(这叫做 栈 ),有的部分则用来 动态地 申请和释放 空间(这叫做 堆 )<对于栈与堆,会在数据结构内作详细介绍,此处只是为大家简单了解一下>。
了解内存如何工作对编程很重要,因为合理地使用内存能让程序运行得更快,也能避免很多问题,比如内存泄漏。
变量与内存
在C语言中,当我们声明一个变量时,计算机会在内存中为这个变量分配一定的空间。这个空间的大小取决于变量的类型。
在计算机中,内存 是以 字节<byte> 为单位存储数据的,一个字节 由 8 个比特<bit> 组成。比特 是 最小的信息单位,可以存储0或1。而 字节 是用来组织和管理数据的单位。
每个字节 都有 一个 唯一的地址,可以通过 这个地址 来 访问和操作数据。
因此,字节<byte> 和 比特<bit> 是计算机中数据处理的基本单位,字节是用来存储数据的,而比特则是数据的最小表示单位。
以下是各个类型所占的字节以及范围:
2.指针变量和地址
理解了内存和地址的关系,我们再回到C语⾔,在C语⾔中创建变量其实就是向内存申请空间。
2.1 取地址操作符(&)
举个例子:
#include <stdio.h>
int main()
{
int a = 10;
return 0;
}
代码调试如下:
0x00000058A192F5D4
0x00000058A192F5D5
0x00000058A192F5D6
0x00000058A192F5D7
在此处,我们就可以使用 &<取地址操作符> 来获取 a 的 首地址
#include <stdio.h>
int main()
{
int a = 10;
//在输出地址,我们使用%p来打印出地址。
printf("%d 的首地址是 %p",a,&a);
return 0;
}
我们不难发现,在每次运行代码的时候,对a这个变量所分配的空间也是随机的。虽然只会输出其首地址,但通过上述所说,int 类型只占4个字节,其余的地址也是可以顺藤摸瓜找上来的。
2.2 指针变量与解引用操作符(*)
2.2.1指针变量
了解完‘ & ’ 之后,紧接着我们我们就要了解一个新的概念: 指针变量 。
当我们使用 & 之后,得到的地址,例如:0x006FFD72 ,这个数值有的时候也要存储起来,方便以后再使用,那么我们就要用到 指针变量 去储存这个地址。
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;//取出a的地址并存放到指针变量pa中。
return 0;
}
在这里,(int*)为整形指针变量。同样的,对于每一种类型的数据,他们都有一一对应的指针。
例如:int ---- int* char ---- char * 等等。
不难发现,在每一种类型后,加一个 ‘ * ’ 之后,它就会变成对应的指针类型。
那么,* 是什么呢,接下来我讲详细为大家讲讲---> 解引用操作符: *
2.2.2 解应用操作符( ‘ * ’ )
在c语言中,我们只要拿了地址(指针),就可以通过地址(指正)来找到该地址(指针)所存储(指向)的对象,就要用到解应用操作符( ‘ * ’ )。
当我们看到
int *ptr;
这样的代码时,*
符号表示ptr
是一个指针,而且是一个指向int
类型数据的指针。这意味着ptr
里面存的不是一个整数,而是一个整数的地址。为什么要在
int
后面加上*
呢?这是为了告诉编译器,这个变量是一个指针,而不是一个普通的整数变量。这样,编译器就知道如何处理这个变量了。简单来说,
int *ptr;
就是声明了一个可以存储整数地址的变量ptr
。这样,我们就可以通过这个地址来找到和操作那个整数了。
#include <stdio.h>
int main()
{
int a = 100;
int* pa = &a;
*pa = 0;
return 0;
}
在上述代码中,*pa 的意思就是通过pa中存放的地址 ,其指向的空间(*pa)其实就是 a变量了,所以*pa = 0,这个操作符 是把 a 改成了 0 。
2.2.3 指针的解引用
一般来说 :指针的类型 决定了对指针解引⽤的时候有多⼤的权限(⼀次能操作⼏个字节)。
例子:
#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;
}
代码运行结果:
不难发现,char* 的指针解引⽤就只能访问⼀个字节,⽽ int* 的指针的解引⽤就能访问四个字节。
即:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)。
2.2 野指针
野指针是指那些指向无效内存地址的指针。它们通常是由于程序员的不当操作而产生的,并且可能导致程序崩溃或数据损坏。以下是野指针产生的一些常见原因以及如何避免它们:
野指针产生原因:
未初始化的指针:声明指针变量后,没有为其分配有效的内存地址,就直接使用它。
指针越界访问:访问数组或动态分配内存时超出了其有效范围。
释放内存后继续使用指针:使用
free
或delete
释放了内存,但指针本身没有被置为NULL
,之后仍尝试通过该指针访问内存。非法内存访问:比如通过野指针或悬挂指针(悬挂指针是曾指向已释放内存的指针)来访问内存。
如何避免产生野指针:
- 初始化指针:在声明指针变量时,立即将其初始化为
NULL
或指向有效的内存地址。- 检查指针是否有效:在解引用指针之前,检查它是否指向有效的内存地址。
- 释放内存后置空指针:在释放指针所指向的内存后,立即将指针置为
NULL
。或者使用智能指针(C++特有)来自动管理内存。避免指针运算超出边界:当对数组或动态分配的内存进行指针运算时,确保指针不会超出其有效范围。
使用安全的内存操作函数:使用像
strcpy_s
(C11)这样的安全函数来替代可能产生越界问题的函数,如strcpy
。使用工具检测内存错误:使用如 Valgrind 这样的内存检测工具来发现潜在的内存错误,包括野指针问题。
通过遵循这些最佳实践,可以大大减少野指针的产生,从而提高程序的稳定性和可靠性。
3. 指针运算
1 指针+- 整数2 指针-指针3 指针的关系运算
1 指针+- 整数
#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]);
for(i=0; i<sz; i++)
{
printf("%d ", *(p+i));//p+i 这⾥就是指针+整数
}
return 0;
}
因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺藤摸⽠就能找到后⾯的所有素,而对于指针来说,自增(p++)相当于本身加上(int)字节数(例如 0x00000058A192F5D4 运算玩就会变成0x00000058A192F5D8),同样的,p+i也相当与自身 加上 i 倍(int)字节数,实现跨越试访问。
2 指针-指针
在减法运算中,值是两个指针在内存中的距离(以数组元素的长度为单位,而不是以字节为单位),因为减法运算的结果将除以数组元素类型的长度。
//指针-指针
#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;
}
在上述代码中,应为char 所占字节为1,所以当p-s执行完之后,会精确返回字符串字数(
p= 0x...F24,s=0x...F21,两者相减后结果就为3)
3 指针的关系运算
对于指针的关系运算,其实与整形(int)的关系运算大差不差。
设两个指针 p , q
p > q 表示 p 指向的存储地址是否大于 q 指向的地址
2. p < q 表示 px 指向的存储地址是否小于 py 指向的地址
3. p == q 表示 px 和 py 是否指向同一个存储地址
4. p == 0 和 p != 0 表示 p 是否为空指针(当p为空指针时,在if(判别式)中的判别式会返回为假的值,即if(0),跳过此if语句)
4 二级指针
二级指针,也被称为指向指针的指针,是一个存储指针地址的变量。它主要用于获取指针的存放地址,或者用于在内存中对指针进行间接操作。在C语言中,二级指针常用于多级数据间的传递。
在定义二级指针时,通常的形式为“数据类型 **变量名”,其中数据类型为指针类型,变量名为二级指针变量的名称。例如,如果有一个指向整数的指针int *p,那么一个指向这个指针的二级指针就可以定义为int **pp = &p。
二级指针的主要使用场景包括:
- 在栈区建立数组,指向常量区数据的字符串,并通过二级指针进行传参。
- 在堆区手动分配空间,创建指针数组,并使用二级指针进行操作。
- 当需要改变数组所指向的数据时,可以使用二级指针进行输出操作。
在使用二级指针时,需要注意避免野指针的产生,即确保所有指针在使用前都已经正确初始化,指向有效的内存地址,并在不再需要时及时释放内存,避免内存泄漏。
此处仅仅为指针的冰山一角,更多深层内容我以后面会慢慢为大家说明。
--------------------------------------------------------------------------