1. 什么是指针
指针英文名pointer,因此通常用p来表示。指针是一种存放内存的东西。
最简单的指针:
其中 在定义指针变量时需要使用 ‘*’这个符号,这个符号叫做解引用操作符。(&)叫做取地址操作符。第二行代码的意思就是将a的地址取出放到指针变量p中去。
1.1 如何理解指针类型
int * pa = &a; pa是指针变量名,* 说明pa是一个指针变量,int 说明 指针变量pa指向的是整型(int)类型的对象。
1.1.3 解引用操作符 ‘*’
我们已经在第二行将 a的地址赋给了pa,通过解引用操作符*找到了pa指向的地址即a,然后将其赋值为10;这便是解引用操作符的作用--通过指针中的地址,找到指针指向的空间。
2. 指针的基本相关概念
2.1 const修饰指针
const 指常量的,不变的。它修饰指针有三种不同的修饰方法。
2.1.1 const 修饰 p
4
const 单独放在p的前面,意味着指针p所指向的地址不能变。怎么更好理解呢? const放在p的前面,p的内容是什么?p的内容是地址,因此p的内容不能改变 ,不然就像最后一行那样,会报错。
2.1.2 const修饰 * p
const 放在 * p的前面,意味着指针p所指向的地址的值不能变。怎么更好理解呢? const放在* p的前面,* p的内容是什么?p的内容是a的值,因此* p的内容不能改变 ,不然就像倒数第二行那样,会报错。
2.1.3 const 修饰全部
const将全部都修饰,通过前面两个说明,我们很容易理解,p保存的地址不能改变,p指向的值也不能改变。倒数两行都报错
2.2 指针的各种类型
指针的类型包括整型指针,字符指针,函数指针,数组指针,void* 等。
2.2.1 指针的运算
2.2.1.1 指针+ / - 整数
指针的类型决定了指针 + / -整数的地址变化大小。
从图中可以看出来,char *类型的指针+1之后,地址变化了1个字节,而 int *类型的指针+1后地址变为了4个字节。
一般的指针类型都可以进行+/-整数的运算,但有一种特殊的类型,void* 不可以进行运算。
2.2.1.1.1 void * 指针
void * 指针又称泛型指针,可以接受来自任何类型的地址。我们知道,在给指针给予地址的时候,需要指针的类型与地址指向的数据的类型相同才好赋值,不然可能会出现不兼容的现象。我们的void * 型指针就完美的解决了这个问题,但它的问题就是不能够进行指针计算。这里说的指针计算不止是指针加减整数,还包括指针之间的加减。
2.2.1.2 指针之间的减法运算
指针-指针 :表示两个指针之间的数据的个数
如图所示,p1为首元素的地址,p2为第四个元素的地址,因为地址由小变大,因此用p2减去p1,再输出的结果为3.这里的3是包含了首地址的元素,但是不包含p2地址的元素的
2.2.1.3 指针之间的关系运算
指针之间是可以进行比较的,但仅限于同一个数组的指针进行比较,不然没有意义。如下图的代码:
图中while循环的条件就是p的地址小于arr + size,当满足条件时进行循环体运算。输出的地址是16进制的地址,每一个地址之间差了4,也就是int 的类型大小。
2.3 野指针
野指针就是没有指向一个确切地址的指针。野指针的来源有多个,如指针未初始化,指针越界问题,以及指针指向的了已经被释放的空间。
2.3.1 未初始化的野指针
指针定义时如果没有初始化,将不能使用。
2.3.2 指针越界访问的问题
当指针指向的范围超过一个数组的范围时,p成为野指针。当越界时,系统会报错
2.3.3 指针指向已被释放的内存
当内存被释放之后,指针指向该空间时,如同指向了一个不存在的空间,就会变成野指针。如函数返回局部变量,当函数结束时内存已经被释放。
2.3.4 避免产生野指针
知道了野指针的产生原因,我们需要采用对应的方法去避免产生野指针。
对于未初始化的野指针,我们需要在定义指针时就将其初始化。
对于越界的野指针,我们需要避免其越界,就需要检查我们的代码,避免其越界。
对于指针指向释放内存的野指针,我们需要尽量不返回局部变量的地址。
2.3.5 assert判断
assert:断言,用于判断是否程序满足条件,不满足条件会报错。需要包含头文件<assert.h>。
我们这里便运用assert判断我们的指针是否为空指针。
当assert的条件满足时,便会报错并中止程序。
当assert条件不满足时,便不会报错。
当程序确定没有问题时,再在#include<assert.h>前面加上#define NDEBUG,便可以自动跳过所有的assert语句。节约资源。
assert只能用于debug模式。
2.4 传址调用与传值调用
我们需要知道,实参传递给形参时,传递的只是实参的一份临时拷贝。在形参中改变实参的值并不会影响到实参。因此传参的时候分为传址调用与传值调用,只有当传址调用时才会影响到实参的值。
例如,想要交换两个函数的值,有两种写法。分别是传址和传值。
传值:
这里使用第一种传值调用,可以看到a,b的值实际上并没有交换。
传址:
这里使用了传址调用,将a,b的地址进行调用,就可交换a,b的值了。