『C/C++』指针与函数传参杂谈
2022-05-08更新:针对新的渲染器优化了显示
对于大部分C语言初学者,指针是最大的一块骨头 ——沃兹基·硕德
本节内容:
-
指针的简单引入
-
[What] 指针是什么
-
[Why] 为什么要用指针
-
[How] 指针怎么玩(声明,使用,运算,数组指针,结构体指针,函数指针)
-
-
函数传参的几种方式
-
值传递
-
地址传递
-
引用传递
-
指针的简单引入
指针是什么
首先,我们要清楚指针是什么,下面是指针的原始定义
系统在内存中,为变量(本人按:这里应加上函数)分配存储空间的首个字节单元的地址,称之为该变量的地址。地址用来标识每一个存储单元,方便用户对存储单元中的数据进行正确的访问。在高级语言中地址形象地称为指针。
指针变量就是保存指针的变量,但很多人 将「指针变量」简称为「指针」,故本文的指针都是指针变量的意思
下面要认两个重要概念:指针的值和类型
「指针的值」 内存地址一般用十六进制数表示,故 指针的值就是一个十六进制数
「指针的类型」 指针不仅是一个地址那么简单,对象不同则类型不同,如指向 int类型 的指针就称这个指针是 int 型的,int 型指针就只能存 int 类型变量 的地址,这种要求可以一定程度上避免混乱。指针的类型用于推断对象的长度,以便进行指针运算(后面会讲)
注意:「泛型对象指针」或称「void*指针」可以指向任何对象类型,但不能提供对象的长度,故无法直接运算与引用
PS:实际上在 C 中是可以隐式(或者说自动)转换的, 但是在 C++ 中不能 ,必须显式(或者说手动)地转换,我建议还是保留显式转换的习惯,这是一个好码风
已经声明的 数组 、结构体 、函数 等本身实际上都能看作指针(准确说是常量)
一切标识符皆为指针(笑)
为什么要用指针
a. 使用指针保存 变量 的地址,这样就可以通过指向某变量的指针来 间接传递或操纵 某变量
b. 使用指针保存 函数 的地址,这样就可以通过指向某变量的指针来 间接使用 某函数,不要以为这是没事找事,我们可以通过这个模拟C++中的类
c. 我们可以使用指针 构建某些数据结构 ,如链表,二叉树等
指针怎么玩
先认识两个字符
-
&
取地址运算符(用于 取 某变量的地址) -
*
引用运算符(用于通过地址 使用/操纵 某变量) -
*
也充当 类型标识符(声明的时候告诉编译器这个变量是指针)
对于 &
,想必大家都用过,在 scanf
函数 中,我们使用它向函数提供变量的地址,这样 scanf
就能知道将数据写入到哪个位置
对于 *
, 引用运算符 和 类型标识符 统称为 指针运算符
1 | int a; |
指针的声明和初始化
作为一种变量,指针的声明和初始化和普通变量形式类似
公式:类型名 *
指针名
加个 *
表示「指针名」是个指针,它是「类型名」类型的指针
1 | int *p; //声明 p 是一个指针,它指向一个int,称 p 为int类型的指针,或 p 是 int* 类型的变量 |
类型标识符可以紧跟在变量名前面,也可以接在类型后面,也可以放中间,都是等效的
1 | int* p; |
混合声明是合法的,但我不觉得是好的码风
1 | int* p,q; //声明了一个指针和一个int类型变量 |
是变量,那当然也能构成数组,但 指针数组 和 数组指针(后面会讲) 不是一个东西
1 | int *arr[10] // 声明一个指针数组,该数组有10个元素,每个元素都是int类型的指针 |
同一般变量一样,函数外声明后内容为空(空地址叫nil),函数内声明内容不确定(指向地址不确定)
应尽快初始化,其实最好声明时就初始化
1 | int a; |
不允许把一个地址直接赋予指针变量,必须转换为指针类型,但是允许你直接赋成NULL(空指针)
1 | int *q = 0x000000000061FDF0; //编译错误 |
指针可以套娃,结合结构体我们可以弄一些好玩的东西
1 | int a=10; |
指针的使用
当某指针保存了某变量的地址后,加上引用运算符就直接等价于原变量
例如,指针 p
保存了 a
的地址,那么 *p
与 a
直接等同
1 | int a=1; |
但这句话不是完全正确的,有时,你会遇到如下的错误
1 | *p++; //这看似应该等同于a++ |
这个的具体的原因是什么 ,下面会解释
但你发现如果为 *p
加了个括号的话,就不会有问题
1 | (*p)++; //加一个括号试试 |
所以请你记住,为了保险,建议在使用指针时加个括号,并且现在可以得出下面这个结论(极其重要)
当
p
指向a
时,(*p)
与a
完全等效(连读三遍)
上面的例子中有 *p++
,那这到底是什么意义呢?这牵扯就到指针的运算
指针的运算
指针的运算包括「指针 ± 整型」和「指针 - 指针」
「指针 ± 整型」
指针加/减 i
,指针的值 前进/后退 i
个 指针类型 长度
1 | printf("%p\n",p); //输出 000000000061FE14 |
对于 *p++
通过查表得知 ++
和 *
(指针运算符)优先级相同,根据右结合性先运行 p++
,再引用变量。
此时 *p
会根据当前的地址取一个 int
长度(4 字节)并据此返回一个 int
,虽然 p++
后 p
指向的不一定是一个恰当的地址,但指针不管这些,我们应避免这种情况发生,除非你能确定指针加减后也能指向一个 int
(如数组)。也就是说,你应当只在数组中使用指针的运算。
1 | printf("%p\n",p); //输出 000000000061FE14 |
其他的举一反三
「指针 - 指针」(没有加)
只有指向数组中的元素的指针才能相减,相减返回两者的距离(类型长度为单位),是一个整型
1 | int a[10]; |
数组指针
既然提到了数组,那么就先来说说数组的指针特性
先声明一个数组
1 | int a[10]={0,1,2,3,4,5,6,7,8,9}; |
还记得开头说的数组可以看作指针吗?(准确说是指针常量)
1 | printf("%p\n",a); //输出 000000000061FDF0 |
可见 a
可以看成一个 int
类型的指针,它的值为第一个元素(即 a[0]
)的地址
既然是指针,我们可以用另一种方式使用数组
1 | printf("%d\n",a[2]); //输出 2 |
也就是说,*(a + i)
等同于 a[ i ]
PS:若想在调试时查看数组却突然以指针的格式显示(常见于把数组传递到其他函数时),可以在“查看”添加下面的表达式
1 | *(int(*)[20])a //20是数组大小 |
既然数组是指针,我们可以直接把数组赋给指针
1 | int *p=a; |
这样,我们就可以通过 p
引用 a
数组了
1 | printf("%d\n",a[4]); //输出 4 |
下面我们来看看数组指针,但是我们先区分下 「指针数组」 和 「数组指针」
1 | int *p[3]; //声明了一个指针数组,该数组有3个元素,其中每个元素都是一个指向int类型的指针 |
数组指针就是指向数组的指针,对比普通指针,区别在由于对象的不同,故数组指针在进行加减法时的单位长度不同,就是说这里 p + 1
会让 p
的值增加 4*3=12
,数组指针一般用于配合二(多)维数组
先声明一个二维数组
1 | int a[2][3]={{1,2,3},{4,5,6}}; |
再把它的值赋给数组指针 p
1 | p=a; |
结构指针
先声明一个结构体
1 | struct student |
按照指针的声明法,声明一个 struct student
类型的指针
1 | struct student *p=&a; |
这样,(*p)
就与 a
等价了,所以下面两句话是等价的
1 | a.mark=100; |
还有一种更简单的写法,使用 「指向结构体成员运算符」 ->
1 | p->name=100; |
综上所述,下面三种形式是等价的
- 结构体变量.成员名
- (*指针变量).成员名
- 指针变量->成员名
函数指针
指向函数的指针叫函数指针
先声明一个函数
1 | int Max(int a,int b) |
函数指针的定义法: 返回值类型 (* 指针变量名) ( [形参列表] );
注意括号不要忘
1 | int (*p)(int,int); //声明 p 为一个函数指针,它能指向某个输入参数为两个int,返回参数为一个int的函数 |
函数指针是一个指针,是指针就能作为变量传给函数,所以我们可以这么玩
1 |
|
这段代码能输入两个数,并输出最大值和最小值
关于指针的两个函数
严格来说,这两个是关于动态分配的,我在这也一并说了吧
malloc 函数
malloc 函数可用于分配若干字节的内存空间。若系统不能提供足够的内存单元,函数返回空指针 NULL
,否则返回分配到的内存空间的起始地址。该函数对于的头文件为 stdlib.h
,原型如下:
1 | void *malloc(unsigned int size); |
size 表示申请空间的大小(单位为字节),返回一个 void*
指针,void*
指针在上面讲过了,这里不再提
使用举例:这里声明了一个大小为 n
的数组:
1 | int *a; |
这里将 malloc
返回的空指针转换成 int
型指针,再赋给 a
sizeof(int)
返回系统中 int
类型所占的字节数,n * sizeof(int)
表示 n
个 int
的空间大小
有申请就有释放的,下面的 free
函数就是 malloc
的反操作
free 函数
free
用于释放申请的空间,原型为:
1 | void free(void *p); |
p
为申请空间的起始地址,执行本函数就将申请的空间返还给系统
PS:指针还可以用来保存字符串常量
具体用法是在初始化时给指针赋上一个字符串,该字符串会存到常量区的一个字符数组中,这个指针会指向存放这个字符数组的首地址
1 | char *s; |
函数传参的几种方式
虎头蛇尾…这边基本直接复制了,以后有啥再慢慢加哈
值传递
1 | void exchange1(int x,int y) |
值的确是传进去了,但 这个是没有用的,值传递只传递值,交换x和y不改变原来的a和b的值
地址传递
1 | void exchange2(int *px,int *py) |
将ab的地址传递给函数,对*px,*py的操作即是对a,b变量本身的操作。可以实现a,b的值交换
注意: 数组的传递不管想不想传地址,你实际上传的都是地址
1 |
|
引用传递(仅限C++)
引用是变量的一个别名,调用这个别名和调用这个变量是完全一样的,如:
1 | int* c = &a;//c是指向a的指针 |
1 | void exchange3(int &x,int &y) |
仅形式参数的格式与值传递不同,内部定义域调用与值传递完全相同,可以实现ab值得对调
因为在x,y 前有一个取地址符号&,在调用exchang3(a,b)时会用a,b替换x,y,称xy引用了变量ab,在函数内部便是对实参ab进行操作了,函数 内部可以直接修改a,b的值
来源:
C语言函数传递数组和传递地址的区别你知道吗_C 语言_脚本之家
函数参数传递三种方式(传值方式,地址传递,引用传递) - long_ago - 博客园
思考题:试解释下面的现象并总结 *p-=1;
的作用
1 |
|