指针初阶(C 语言)

一、内存和地址

为了便于计算机进行管理,内存被划分为一个个内存单元,内存单元的大小通常为一个字节。每个内存单元都有一个编号,而这个编号就可以理解为这个内存单元的地址。而地址通常分为 32/64 位,这主要和所使用的环境有关,因为我们使用的设别上面都配备地址线,通常是 32/64 根地址线,而一根地址线只能显示高电平(1)或者低电平(0)两种可能,那么一共就有 32 位或者 64 位 2 进制数,也就是 2 的 32 次方或者 2 的 64 次方种编号。而地址通常使用 16 进制进行表示,所以也就是 8 位或者 16 为 十六进制数。如下图(32位):
在这里插入图片描述
通过前面学习的知识我们知道,可以对变量使用取地址符(&)获得该变量的首字节的地址。而通过对应类型的指针变量就可以存储该地址。

二、指针的创建

指针是 C 语言中,专门用来存储地址的类型。而创建某种类型的指针就是在创建某种类型的变量的基础上加上符号(*),我们通常称为指向该类型的指针。如下:

char *pc;  // 指向 char 的指针
short *ps;  // 指向 short 的指针
int *pi;  // 指向 int 的指针
long *pl;  // 指向 long 的指针

三、指针的使用

如果需要存储一个变量的地址,则创建该指向该变量类型的指针,然后通过取地址符(&)获取该变量的地址进行赋值。如下:

int a = 10;
int *p = &a;  // 把 a 的地址赋给 p

而通过对指针 p 使用解引用操作符(*)就可以得到其指向的变量 a,如下:
在这里插入图片描述
通过上述代码可以看到,*p 就是 a,我们可以像使用变量 a 那样使用 *p。

四、指针的初始化

在函数中创建的变量通常都是局部变量,若不初始化则其值是随机的(之前编译器遗留的值)。如:

int \*p;
*p = 10;

创建了一个指向 int 的指针,但是 p 的值是随机的,倘若对其进行解引用操作,那么我们所访问的这块空间是未知的,可能其中存储着目前正在运行的程序的信息,如果我们对其进行修改那么可能会引起程序崩溃。这是极其危险的操作。而且,前面就说了,我们只能访问操作系统允许我们访问的空间(申请的空间),访问其他空间的操作是非法的。而且在大多数编译器中会报出使用未初始化的局部变量的错误信息:
在这里插入图片描述

所以,不仅对于指针来说,对于所有的局部变量来说,我们都需要养成对其初始化的好习惯。而当不知道指针初始化指向谁的时候,可以把指针初始化为空(NULL),而对于空指针是不能进行解引用操作的,许多编译器会报错或者给出警告。如下:
在这里插入图片描述

我们也可以通过 if 条件判断语句,判断该指针是否为空。如下:

int* p = NULL;
// 如果不是空指针
if (p != NULL)
	printf("haha\n");

五、指针变量类型的意义

指向 int 的指针和指向 char 的指针都是一个 4/8 个字节,但是它们有以下不同:
(1)解引用所访问的字节数和进行的处理
指向 int 类型的指针解引用时访问该地址开始的四个字节,且把它当作一个 int 类型来进行处理;而 char 类型的指针解引用时访问该地址开始的一个字节,且把它当作一个 char 类型来进行处理。
如下图:
在这里插入图片描述
这是变量 a 的四个字节空间上所存储的值(小端存储),然后 *pi 对其四个字节进行修改,如下:
在这里插入图片描述
然后 *pc 对其第一个字节进行修改,如下:
在这里插入图片描述

(2)对指针进行加减整数、自增和自减
指针进行加减整数、自增和自减都是基于其所指向类型的大小(单位字节)来进行运算的。如:
在这里插入图片描述
通过上述代码可以看到,int 类型的指针 pi 加 1 是加了 4 个字节,也就是 1 * sizeof(int)。那么如果 pi 加 n,那么就是加 n*sizeof(int),减法同样如此。其它类型也就是把 sizeof 中的类型名换一下而已。

(3)void* 指针
void* 指针是没有类型的,它可以指向任意类型的变量,可以存储任何类型的地址。但是不能对它进行解引用和加减整数的操作,否则编译器会报错。如下:
在这里插入图片描述

六、const 修饰指针

const 修饰的变量,其值不能修改,被视为只读变量。但是当 const 修饰指针时,其修饰的是指针本身,还是指针所指向的对象?

1. 底层 const

当 const 在符号 * 的左侧时,该 const 修饰指针所指向的对象,该 const 也称为底层 const。如下:
在这里插入图片描述
可以看到上述代码中不能修改指针 pi 所指向的对象,但是可以修改指针 pi 本身,让它指针别的对象。

2. 顶层 const

当 const 在符号 * 的右侧时,该 const 修饰指针本身,该 const 也被称为 顶层 const。如下:
在这里插入图片描述
可以看到上述代码中可以修改指针 pi 所指向的对象,但是不能修改指针 pi 本身。

七、指针运算

(1)指针加减整数、自增和自减操作
前面已经讲述,这里就不再赘述了。

(2)指针相减
当两个指针指向同一块内存空间的时候,两个指针相减的结果为两个指针之间相隔的元素个数。如下代码:
在这里插入图片描述

(3)指针的关系运算
当两个指针指向同一块内存空间的时候,两个指针可以使用关系运算符进行比较。如下代码:
在这里插入图片描述
上述代码通过两个指针分别指向数组的首元素和尾后元素,并且通过关系运算符(!=)当作判断条件,使用 while 循环来遍历显示数组。这里指针 end 虽然指向了数组后面的第一个元素(尾后元素),但是并没有构成越界访问,因为我们并没有对其进行解引用操作。

C 语言规定,指向数组元素的指针可以和指向数组尾后元素的指针进行关系运算。其他位置不保证运算结果不保证。

八、野指针

(1)指针为初始化
指针如果没有初始化,那么该指针就是野指针。如前面所讲述的局部指针变量未初始化,其值是随机的。

(2)指针越界访问
指针越界访问主要出现在数组中,因为数组的下标是从 0 开始的。如下代码:
在这里插入图片描述
可以看到程序可以正常运行,只是给出了警告。但是这种行为是非常危险的,可能会引起程序崩溃。程序员需要确保自己的代码没有非法下标,不出现越界访问的现象。

(3)指针指向的空间被释放
如果指针指向的空间已经被释放了,那么该指针也是野指针。如下代码:
在这里插入图片描述
从上述代码中可以看到,函数 fun1() 返回了局部变量 i 的地址并赋值给了 main() 函数里的 int 指针 pi。但是局部变量是函数调用的时候产生,函数调用结束后销毁。所以,指针 pi 所指向的空间已经被释放了。也就是说该空间的访问权限已经被操作系统回收了,我们是不能访问的,否则就是非法访问。但是编译器并没有报错,而且程序也能运行。这就需要程序员在编写代码的时候更加小心谨慎。

九、assert 断言

assert 断言的使用方式如下:
assert(条件判断);
如果程序运行过程中,满足该条件,那么程序继续执行,否则终止程序并报错。使用之前需要包含头文件 assert.h

优点:
使用 assert 的好处:它不仅能⾃动标识⽂件和出问题的⾏号,还有⼀种⽆需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问题,不需要再做断⾔,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG 。如下代码:

#define NDEBUG
#include <assert.h>

然后,重新编译程序,编译器就会禁⽤⽂件中所有的 assert() 语句。如果程序⼜出现问题,可以移除这条 #define NDEBUG 指令(或者把它注掉),再次编译,这样就重新启⽤了 assert() 语句。

缺点:
assert() 的缺点是,因为引⼊了额外的检查,增加了程序的运⾏时间。

一般使用 assert() 来检查指针是否为空。如下代码:

int *p = NULL;
assert(p != NULL);

十、按址传递

按址传递就是在传递函数参数的时候,传递变量的地址,这样在被调函数中就可以通过传入的地址来访问主调函数中的实参。

(1)优点
该传递方式在传递大型数组或结构体时,极大的减少了传参所消耗的时间和空间,提升了效率。

(2)缺点
被调函数可以修改实参。

可以通过在函数参数前面加上 const 解决这个缺点。

以下是两个按址传递的案例:

a. 交换两个整型变量
在这里插入图片描述

b. 给数组初始化
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_70742989/article/details/143020681
今日推荐