零基础学C语言——指针

发布时间:2023年12月24日

这是一个C语言系列文章,如果是初学者的话,建议先行阅读之前的文章。笔者也会按照章节顺序发布。

指针的定义

有很多文章将C语言指针说得犹如蜀道之难一般,其实指针这个概念很容易。

我们依旧以变量一文中介绍变量时使用的停车场为例。假设每一个停车位就是一个变量,我们可以对每一个停车位命名(例如用26个英文字母),同时每个停车位也有其自身的位置(例如经纬度或者其他人为编号)。

例如,有一辆奔驰停入了第十个车位,这个车位编号为9,车位名称为码哥车位,转换成代码就是类似:

码哥车位 = 奔驰

对应到C语言中有可能就是:

int mage_bit = 1001; //加入1001代表奔驰

这时,如果码哥我想要在停车场中找到刚才停入的奔驰车,那么直观的方式有,找码哥车位在哪,或找编号为9的车位在哪。不那么直观的方式有哪些呢?比如,在第1个车位(编号为0的车位,例如名叫通用车位)上放一个牌子,写着码哥的奔驰车(1001)在9号车位。

此时,0号车位(即名为通用车位)的区域所代表的C语言变量就是一个指针变量,它的内容指向另一个车位编号。

在C语言中,指针可以被看作为一个长整型变量,其内容是一个长整型的整数,这个整数是另一个变量的内存地址。上面的例子对应到C语言中,车位的编号就是车位变量的内存地址,车位的名字就是变量的名字。可以转换成类似如下代码:

int mage_bit = 1001;
int *common = &mage_bit;

指针定义的一般形式:

数据类型 *指针变量名;

数据类型 *指针变量名 = 初始地址值;

这里,数据类型不仅仅包含了基本数据类型,还包含了我们以后会介绍的自定义类型、结构体类型等。

在上例中common就是一个整型指针变量,它的初始值为整型变量mage_bit的内存地址。&为取地址运算符,在想要获取其内存地址的变量名前加上&即可获取其地址。

还有一种特殊的指针——空指针,其关键字为NULL。这个指针的含义是不指向任何地方。一般情况下,这个NULL也等价于整数0。

指针的声明

声明指针的一般形式:

数据类型 *指针变量名;

用来告知编译器指针变量已被定义该如何寻找。

指针运算

指针是可以进行四则运算的,但是这些运算会受到指针的数据类型限制。举个例子:

int a[3], *p, *q; //定义整型数组a和整型指针p、q
q = p = a; //数组名字即为数组首地址,我们后面的小节中马上要谈到
//此时p和q中存放的数值是数组a的首地址,即数组第一个单元(下标为0的元素)所对应的内存地址
++q;//整型指针q自增1

此时,(unsigned long)q - (unsigned long)p等于多少呢?

答案是,4。为什么自增1,但相减后等于4呢?

原因是,指针每一次增1并不代表其存储的地址数值加1,而是增长一个其所属类型的字节长度,对于int来说就是4。

类似的,–操作,+、-等操作都是如此。同样,不同数据类型,其字节跨度大小取决于类型所占字节大小

如果非要将q只增加1字节,那么可以将之强制类型转换为 unisgned char型指针 然后加1。

指针常见的操作有:


  • ++
  • +=
  • -=
  • +
  • -

指针的用途显然也不仅限于四则运算,毕竟徒有地址并无太大用途。我们可以通过指针所存储的内存地址来获取或修改该地址中存放的具体数据,例如:

int a = 10;
int *b = &a;
*b = 1;

这里,*为取值运算符,*b在赋值运算前的值为10,即变量a中的值。赋值后,变量a的值则变为1。这就相当于上面停车场例子中,将奔驰直接换成拖拉机,但0号车位的牌子不变。

指针的指针

既然指针变量中存放的是一个变量的内存地址,而指针本身也是一个变量,那么指针变量本身也会有对应的内存地址。因此,是否可以用指针记录另一个指针的地址呢?

当然可以,我们来看一个例子:

int a = 10;
int *b = &a; //整型指针b存储变量a的内存地址
int **c = &b; //整型二级指针c存储整型指针b的内存地址

这里引入多级指针的概念,即一级指针的指针(即一级指针变量的地址)为二级指针,二级指针的指针为三级指针,以此类推。是几级指针就在定义是加几个*。例如:

int ***d = &c;

一般三级以上指针在实际应用中极少见到,应用场景很少。

指针与const

在常量一文中谈到过,定义常量用const关键字,下面看几个写法:

int a = 10;

const int *b = &a;
int const *c = &a;
int * const d = &a;
const int * const e = &a;

上面这几种写法有什么区别呢?下面我们逐个给出:

指针b:可以给b赋新值,但是不可以用*b去修改a的值

指针c:与指针b作用一致

指针d:可以用*d修改a的值,但是不可以给d赋新的地址值

指针e:既不可以给e赋新地址值,也不可以用*e修改变量a的值

当然,也有绕过限制的方式,看其中一个例子:

#include <stdio.h>

int main(void)
{
    int a = 10, b = 1;
    int const *p = &a;
    int **q = &p;
    *q = &b;
    printf("%d\n", *p);
    return 0;
}

这里使用了一个二级指针q指向受限的p,然后来修改p的指向。运行结果为1,即p的指向被成功修改。

本例编译时,不排除一些较新版本的编译器报错,毕竟const是为安全性而设计的,不推荐这么用。

指针与数组

谈起指针就必然会牵扯到数组,因为编译器会将数组名处理为数组的首地址。

int a[10];
int *p = a;
int *q = &a[0];//取下标为0的元素的内存地址,&a[0]等价于&(a[0]),因为[]优先级高于&

这样的写法并不会报错,因为这是符合语法也符合我们预期的。a是数组,但数组名a也是数组的首地址。数组是整型数组,因此其地址也要用整型指针存放。首地址与下标为0的元素的首地址是相等的。因此这里p - q等于0。

在数组定义一文中,其数据类型也包含了指针,因此指针也可以组成数组——指针数组。例如:

int a = 10;
int *b[3];
b[0] = b[1] = b[2] = &a;

即,每一个数组元素都是一个指针

既然各种数据类型都有指针,那数组是否也有指针呢?当然有——数组指针。例如:

int a[3] = {1, 2, 3};
int (*b)[3] = &a;

这个例子中,a指代的数组首地址和b这个数组指针中存放的地址是相同的。那么这两者有什么差别呢?

(unsigned long)(b+1) - (unsigned long)(a+1) //结果为8

为什么两个值差了那么多字节呢?

还记得前面指针运算小节中,指针的四则运算会受到其数据类型的限制吗?即运算跨度与数据类型占的字节数有关。本例中,b是数组a的指针,而数组a的大小是多大呢?是12字节,因为它包含了3个int元素。而a的类型是什么呢?是int,因此a每次操作的跨度都是以4字节为单位的,而b则是以12字节为单位的。

指针与函数

函数一文中函数参数小节有提及函数参数为数组的情况。在阅读了本篇前几小节后,大家也应该明白函数参数的值传递传递的其实就是数组的首地址,即一个指针。

下面来说说函数的返回值为数组的情况,看一个例子:

int *foo(int *array)
{
  return array;
}

这个例子非常简单,参数是一个整型指针,返回值是一个整型指针,函数的功能就是把参数返回出去。

换言之,这个函数参数可以是一个整型数组,那么返回的也就是整型数组。

最后,我们说一种特殊的指针——函数指针每一个函数,其名字就是这个函数的地址。这不奇怪,因为所有的代码都是以二进制指令形式放在内存中运行的,因此函数也必然存在内存地址。也因为其存在内存地址,那么必然可以将这个地址赋给一个符合这个函数类型的变量。但关于这部分的内容我将放在后续介绍typedef关键字的文章中说明。

一个综合例子**

#include <stdio.h>

int *arr[2];

int **modify(void)
{
    int ** const ptr = &arr[1];
    **ptr = 1;
    return arr;
}

int main(void)
{
    int num = 10;
    arr[0] = NULL;
    arr[1] = #
    int **ret = modify();
    printf("%d %d %d %lx %lx\n", (int)ret[0], num, *ret[1], (unsigned long)&num, (unsigned long)ret[1]);
    return 0;
}

读者可以自行编译运行这段代码,看看终端输出内容是什么。

代码的含义很简单,定义了一个指针数组arr,在main函数中将数组第一个元素置空指针,第二个元素设为整型变量num的地址,然后调用modify函数。在modify函数中,定义指针ptr,这个ptr可以修改其指向的地址单元中的值,但ptr存放的地址值不允许修改,然后利用**运算符修改arr第二个元素所指向的地址单元(即num)中的值,最后将arr作为返回值返回。

printf(以后详细介绍)中,%d为格式输出,即输出int型数据的值,%lx为输出十六进制表示的长整型整数值。可以看到,我分别打印出NULL的整数形式的值,num的值,指针数组第二个元素指向的内存单元中的值,num的内存地址,指针数组第二个元素的数值。



喜欢的小伙伴可以关注码哥,也可以给码哥留言评论,如有建议或者意见也欢迎私信码哥,我会第一时间回复。
感谢阅读!
文章来源:https://blog.csdn.net/weixin_40960130/article/details/135179306
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。