指针在C语言中不可谓不重要,指针就是C语言的灵魂也是被大家所公认的。那么关于指针的一些“小秘密”,你知道多少呢?下面就一起来看看吧
指针的“四则运算”
我们最早学习的计算就是10以内的加减法的运算,然后再逐步学习到乘除。
指针运算的学习也要先从加减法开始学习。
指针的加减法
在学习指针的加减法之前,我们必须要知道一件事:指针的类型是什么意思?
指针的类型作用是:指针指向的内存空间的大小。
指针只是指向了一个内存地址,但是当存内存中取值的时候,系统不知道你要从当前指针指向的地址,取几个字节,指定了指针的类型后,系统就知道取几个字节了。
比如说:
char* 类型的指针指向的数据空间大小是一个字节,每次读取的时候就只会读取1个字节int* 类型的指针指向的数据空间大小是四个字节,每次读取的时候就会读取4个字节
当一个指针的类型被强制类型转换后成为别的指针类型,那么该指针每次能够读取的内存的大小也会发生变化(但指针指向的数据不会因此被改变)
指针 ± 整数
指针 + n ,代表了指针向后走 n 步
指针 – n ,代表了指针向前走 n 步
这里走 n 步的意思是 指针中存放的地址 + n * sizeof((指针的类型))
而不是说指针向后走 n 个地址
指针 ± 整数 在数组中经常被使用
比如:
#incldue<stdio.h>
int main()
{
int arr[] = {1, 2, 3, 4, 5};
int* parr = arr;
parr += 1;
printf("%d\n", *parr);
return 0;
}
此时,屏幕中出现的是数组中的第二个元素 2,同理,如果想读取到数组中的其他元素,只需要将 parr 加上合适的整数即可
接下来,看看指针减去整数的效果
#incldue<stdio.h>
int main()
{
int arr[] = {1, 2, 3, 4, 5};
int* parr = arr;
parr += 1;
printf("%d\n", *parr);
parr -= 1;
printf("%d\n", *parr);
return 0;
}
此时,可以看到屏幕中出现的是数组中的第一个元素 1 ,
所以当我们的指针如果数字加多了,可以使用指针减去一个合适的数字进行回正即可
指针 ± 指针
指针的相加是没有意义且违法的!
指针变量是一个存放地址的变量,一个地址加上另外一个地址,没人知道它会指向哪里,这个是无法预测的,因此指针的相加是没有意义的,并且在C语言的标准中,他也是违背C语言语法的!因此 指针是绝不可以相加的!!!
虽然指针不可以相加,但是指针相减确实被允许的,而且它的作用也是相当大。
指针 – 指针表示的是在两个指针中间有多少个元素。
#include<stdio.h>
int main()
{
int arr[] = {1, 2, 3, 4, 5};
int* front = arr;
int* rear = arr + 1;
int num = rear - front;
printf("%d\n", num);
}
得到的 1 就是 front 和 rear 中间的元素个数。
小明:欸?front 指向第一个元素,rear 指向第二个元素,他们中间明明没有元素啊?
这时候,调试一下来观察一下它们
发现rear的地址 – front的地址 等于 4
指针的类型表示指针指向的内存空间的大小。这里的指针类型是int*,因此每次从内存中读取4个字节,即一个整形。因此,rear – front = 1是正确的。
通过观察还可以发现,两个指针相减的类型必须是相同的。
指针的乘除
之前讲到过,指针变量存放的是一个地址。指针是无法直接进行乘除的,这也是违法的。即便真的进行了乘除,对于一个地址进行了乘除法后,很难知道指针指向哪里了。
指针就像是时间很像
- 两个时刻相减,表示的是两个时刻相差的时间
- 一个时刻加减一段时间表示的是另一个时刻(指针在这里要考虑是否出现越界)
- 时刻进行相加,乘除是没有任何意义的,指针也是如此。
指针的比较
先看两段代码
代码1:
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
int* pa;
for (pa = &arr[5]; pa > &arr[0]; *--pa = 0)
{
;
}
}
代码2:
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
int* pa;
for (pa = &arr[5 - 1]; pa >= &arr[0]; pa--)
{
*pa = 0;
}
}
似乎两段代码差别并不大,都是将arr数组中的数据全部置为0,并且在绝大多数的编译器中,这两段代码也不会出现什么问题。但是,我们要避免像代码2这样写,因为标准并不保证代码2可行。
标准规定
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较
指针和数组的亲密关系
数组名和&数组名
当一个指针指向数组的首元素的地址,指针加一,表示的是指针指向第二个元素。那么,如果在定义指针的时候加一会出现什么样的情况呢?
int main()
{
int arr[] = {1, 2, 3, 4, 5};
int* pa1 = &arr;
int* pa2 = &arr + 1;
int* pa3 = arr;
int* pa4 = arr + 1;
return 0;
}
通过观察监视窗口和内存,发现
- pa1 == pa3
- pa4 指向的是第二个元素的地址
- pa2 指向了数组最后一个元素的后一个位置
在C语言中,只要数组名单独出现在 &后面 或者出现在 sizeof的括号里面,此时的数组名表示的就是整个数组
既然可以把数组名当成地址存放到一个指针中,那么我们就可以通过指针来访问数组中的各个元素,甚至我们还可以将指针当成数组的另一个数组名
#include<stdio.h>
int main()
{
int arr[] = {1, 2, 3, 4, 5};
int* parr = arr;
printf("arr[1] = %d\n", arr[1]);
printf("p[1] = %d\n", parr[1]);
return 0;
}
数组大小可变
之前出现的数组的大小通常在初始化的时候就已经被固定了,无法改变大小,这类数组被成为静态数组。
既然有静态数组,那么就一定会有动态数组。通常,我们使用malloc在堆区上开辟一块空间,并且我们使用一个指针对这块空间进行管理。当我们一开始开辟的空间不够用的时候,可以使用realloc来增加空间,从而实现数组大小可控制。
#include<stdlib.h>
int main()
{
int n = 5;
int* parr = (int*)malloc(sizeof(int) * n);//开辟空间
if (NULL == parr)
{
perror("malloc");
exit(-1);
}
int i = 0;
for (i = 0; i < n; i++)
{
*(parr + i) = i;
}
int* tmp = (int*)realloc(parr, 2 * n);//增加空间
if (tmp == NULL)
{
perror("realloc");
exit(-1);
}
parr = tmp;
n *= 2;
for (i; i < n; i++)
{
*(parr + i) = i;
}
free(parr);//动态开辟空间后需要进行释放
}
数组传参
数组在进行传参的时候,可以使用数组或者指针来接收参数。但是,实际上,无论是使用数组或者指针来接受参数,都会被当作是指针。
void test1(int arr[], int size)
{
printf("%d\n", sizeof(arr));
}
void test2(int* arr, int size)
{
printf("%d\n", sizeof(arr));
}
int main()
{
int arr[] = { 1, 2, 3, 4, 5 };
test1(arr, sizeof(arr) / sizeof(arr[0]));
test2(arr, sizeof(arr) / sizeof(arr[0]));
return 0;
}
再看监视
这是为什么呢?
实际上,C语言不支持数组名作为形参来进行调用。而且数组有两个特性,影响作用在数组上的函数:
一是不能复制数组,
二是使用数组名时, 数组名会自动指向其第一个元素的指针。(只要数组名,没有单独出现在&或者sizeof中,它表示的就是首元素的地址)
因为不能复制,所以无法编写使用数组类型的形参,数组会自动转化为指针。因此这里用数组接收时,这个数组被自动转化成了一个指针。
使用指针容易出现的问题
野指针
概念:指针指向的位置时不可知的(随机的、不正确的、没有明确限制的)
野指针的成因:
- 指针未初始化:
指针也是一个变量,当指针在main函数中定义时,指针就是一个局部变量,而局部变量未初始化的话,默认为随机值。 - 指针越界访问:
当指针指向数组时,指针指向的范围超出数组arr的范围时,p就是野指针 - 指针指向的空间释放后未置空
当我们使用动态的数组的时,将指针释放后,此时的指针指向的地址还没有改变。但是此时的指针指向的内存已经被操作系统回收。所以,此时的指针也是空指针。(如果此时将内存中的数据进行改变,将会引起十分严重的后果)
如何规避野指针:
- 每次定义指针的时候将指针初始化
- 要时刻小心指针是否越界
- 指针释放后要及时置空
- 在指针使用之前检查有效性
使用assert来进行检查,当assert如果是0,0.0,NULL,FALSE时,assert直接退出程序,并且报告出错的位置和原因。const修饰指针
在使用函数的时候,如果只是想要读取指针指向的内容,而不希望将数据改变,可以使用const来修饰指针
但是只是无法通过被const修饰的指针来修改数据,如果将const修饰的指针赋值给另一个指针,就可以通过被赋值的指针将数据改变。
void test(const int* p)
{
return;
}
也可以写成
void test(int const* p)
{
return;
}
或者不希望指针指向别的地址
void test(int* const p)
{
return;
}
如果又不想改变指针指向的数据,也不想改变指针指针指向的地址
void test(const int* const p)
{
return;
}
或者
void test(int const* const p)
{
return;
}
万能接收器void* p
void类型的指针可以作为任何指针的形参。但也就是因为它的万能,导致void类型的指针无法直接进行解引用操作。在进行解引用操作之前都必须进行强制类型转换。
感谢阅读,如有错误请大佬斧正。