C语言__指针_进阶_多维数组
指针
指针是一种特殊的变量,存的是别的变量的内存的地址。提供了一种间接访问方法。
指针的两大要素
有两大要素:1、存放的内容:变量的起始地址;2、指针的类型:指明长度
引用(take reference)、解引用(dereference)
take reference 表示取变量的地址。这是一种间接引用。
此处的引用是C语言中的,要和Cpp中的引用区分开。
1 | int main() |
指针的大小
指针的大小是固定的,只和内存地址相关。32位的系统用的是32位的地址总线,那么指针则是32位大小;64位的系统,那么指针则是64位大小,但是实际上的硬件并不是64根地址总线,而是48根,因为目前基本用不到那么多。
1 | int main() |
二级指针
1 | int main() |
有**
这种连解引用,但是没有&&
这种连取地址!因为第一次取完地址之后得出的是一个值(临时值),不具名且没有固定的内存地址,因此无法连取地址。
在Cpp中,右值引用是可以对临时变量“取地址”的,但不是这种连取地址的方式。
万能指针void *
(master/universal pointer)
master/universal pointer。只保留了指针的首地址,只在中间传输地址有用,无法直接解引用。记作void *
。
要解引用,必须以强制类型转换告知指针的具体的结尾位置在哪才行。
1 | int main() |
案例
定义一个整型数
1 | int a; |
定义一个指向整型数的指针
1 | int * a; |
定义一个指向指针的指针,它指向的指针是一个指向整型数指针
1 | int ** a; |
定义一个有10个整型数的数组
1 | int a[10]; |
指针数组
定义一个有10个指针的数组,该指针是一个指向整型数的指针
此时需要考虑*
和[]
的优先级关系,谁与a先结合?
[]
是最高优先级,*
次之。如题意,我们要定义a为数组,所以不用给*
加()
。
1 | int* a[10]; |
数组指针
定义一个指向有10个整型数数组的指针
如题意,我们要定义a为指针,所以需要给*
加()
。
但是如下写是错误的:
1 | int (*) a[10]; |
这才是正确的,需要让*
和a包在一起。
1 | int (*a) [10]; |
函数指针
定义一个指向函数的指针,该函数有一个整型参数并返回整型数
a是个指针,而a右边又需要小括号表示函数的参数类型,所以需要让*a
加括号。再在最左边标志函数的返回类型。
1 | int (*a)(int); |
函数名的隐式转换,Cpp中到底要不要加&
在C语言中,函数名bar和&函数名bar
的效果一样,都是取函数地址。
函数名会被隐式转换为函数指针,因此
pfun = bar
和pfun = &bar
等价。
在Cpp中,对于普通函数,隐式转换同样适用,但对于类成员函数则不是了,如果要取成员函数地址,必须显式加&
,即&类名::函数
。且调用时必须用(obj.*p)()
的形式,来表示哪一个对象实例的调用。
1 | class MyClass { |
如果是静态成员函数,取地址时可以省略 &
。相应地,通过函数指针调用时,可以直接加小括号,不用解引用。
为什么普通成员函数(不包括静态成员函数)取地址时必须显式使用
&
?当我们取一个成员函数的地址时,我们获取到的是一个“成员函数指针”,这个指针并不指向一个普通的函数地址,而是指向一个带有额外
this
指针的特殊函数。由于成员函数并不是普通的全局函数或静态函数,编译器需要明确知道你想要的是一个带this
指针的成员函数地址,而不是一个普通的函数指针。
两种等效的调用形式(C、Cpp均可)
- 直接调用:
pfun();
- 解引用后调用:
(*pfun)();
1 | pfun(); // 直接调用 |
1 |
|
C语言和Cpp中,都可以直接拿函数指针名字来调用它指向的函数。即没必要解引用再调用。
因为本质上函数调用就是在地址后加()
,即使是间接地址,也生效。
1 |
|
C和Cpp对于函数指针的注意事项
对于普通全局函数,函数名都会被隐式转换为函数指针,因此pfun = bar
和pfun = &bar
等价。
1 | void (*pfun)(void) = bar; // C和C++均可 |
以上是函数签名完全一致的。C语言、Cpp都允许且正确。
但,如果想要把另外一个不同签名的函数赋给pfun,则C会警告,Cpp则不允许:
必须把foo强制转换为bar函数对应签名(void(*)(void))
的函数指针类型才行。
1 | void foo(int); |
总结
- 类型检查
- C中,函数指针可以不管函数签名类型,随意互传,可以编译通过,但是会有警告。
- Cpp中,函数指针互传时必须类型一致。不同类型的需要强转。
- 取函数地址
- Cpp中,普通函数名字或者静态成员函数,与C一样,可以隐式转换为函数地址。不用加
&
。 - Cpp中,普通成员函数,取其地址,必须用
&类名::函数
的形式。
- Cpp中,普通函数名字或者静态成员函数,与C一样,可以隐式转换为函数地址。不用加
- 调用
- 普通函数、静态成员函数可以直接用函数指针加小括号调用。
- 普通成员函数,必须绑定对象实例,无法直接调用。因此必须先解引用。如
(obj.*p)
。
函数指针类型的typedef
1 | void bar(void); |
练习:定义函数指针数组
定义一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回整型数。
1 | int (*a[10])(int); |
- 首先是一个数组,则
p[10]
。 - 其次,内容是指针,
*p[10]
,因为[]
优先级比*
高,则不用加小括号处理。 - 指针是指向函数的,得在后面加小括号表示参数类型:
*p[10](int)
,但是()
优先级比*
高,则系统会误以为这是函数调用(比如fun(123);
)如此一来,就得在前面整体加小括号,让*p[10]
先处理:(*p[10])(int)
。 - 再加上返回类型,则:
int(*p[10])(int)
。
函数指针:函数的返回值是一个指针,这个指针指向一个数组
a是一个指针,指向一个函数,这个函数的参数为int型,函数的返回值是一个指针,这个指针指向一个数组,这个数组有10个元素,每个元素是一个void *
型指针。
1 | void*(*(*a)(int))[10]; |
可以通过 typedef
分步定义类型,让声明更清晰:
1 | typedef void* ElementType; // 数组元素类型是 void* |
在C语言中,理解复杂声明需要从变量名开始,逐步向外解析。对于声明 void*(*(*a)(int))[10];
,我们可以分步拆解:
- 变量名
a
a
是一个指针(由*a
可知)。 a
指向一个函数
(*a)(int)
表示a
指向一个函数,该函数接受int
类型的参数。- 函数的返回值类型
函数的返回值是*(...)
,即一个指针。
进一步分析*(*a)(int)
,说明返回值是一个指针。 - 指针指向一个数组
(*(*a)(int))[10]
表示返回值指向一个包含10个元素的数组。 - 数组元素的类型
数组的每个元素是void*
类型(即void*
指针)。
为什么[10]
要放在后面?
如果要声明一个指针,指向一个包含10个int元素的数组:
1 | int (*ptr)[10]; // ptr是一个指针,指向int[10]的数组 |
ptr
的类型是int(*)[10]
。- 语法规则:
[10]
必须放在指针标识符ptr
后面,表示指针指向的是一个数组,而不是数组中的单个元素。
如果函数返回一个指向数组的指针,声明如下:
1 | int (*func(int))[10]; // func是一个函数,接受int参数,返回指向int[10]的指针 |
func(int)
是一个函数,返回类型是int(*)[10]
。- 语法规则:
[10]
必须放在函数返回类型的后面,表示返回值是一个指向数组的指针。
此函数的声明和定义为以下形式:
1 | int (*func(int))[10]; |
以下这么写是错误的:
1 | int(*)[10] func(int); // error |
函数指针:指向的函数返回值是另一个函数指针
a是一个指针,指向一个函数,这个函数的参数为3个int型,函数的返回值是一个指针,这个指针指向一个函数,这个函数的参数为int型,函数的返回值是float型。
1 | float (*(*a)(int, int, int)) (int); |
函数指针:指向的函数返回值是函数指针数组
a是一个指针,指向一个函数,这个函数的参数为空,函数的返回值是一个指针,这个指针指向一个数组,这个数组有10个元素,每个元素是一个指针,指向一个函数,这个函数的参数为空,函数的返回值是int型
1 | int(*(*(*a)(void)) [10])(void); |
指针与数组的关系
1 | int main() |
指针的运算
指针有加减运算。没有乘除运算。但是只能整数计算,不能小数计算。
1 | int main() |
指针减法的应用
1 | int main() |
但是要注意,指针类型要一致,如果类型不一致:
1 | int main() |
语法糖
- 可以把上面的
*(p + 1)
直接用p[1]
表示,那么arr[1]
也就是*(arr + 1)
,同理arr[0]
也就是*arr
。也就是说,数组的名字可以当做指向首元素的指针。可以作arr + n
这种计算获得数组中后n
的元素指针。 - 但是,不能对
arr
本身进行+=
、++
。因为这修改了arr
本身的值,而数组一旦定义完成是不可删除、移动的。本质上,数组名字是一个const类型的。 - sizeof:对于数组名字,sizeof计算出来的是整个数组的大小。而对于sizeof p,(
int * p = arr
),计算出来的是一个元素的大小。 - 除了上面两种特殊情况,数组名字arr和指针完全一样,可以替换使用。
- 对数组名进行取地址:
&arr
会是什么?是指向一维数组的指针。int(*p)[8] = &arr
。用p如何打印数组某一元素?首先得对数组指针解引用*p
,再对解引用后的值进行加减*p + 1
(*
解引用优先级大于+
,所以不用给*p
加括号),然后对加减后的值第二次解引用*(*p + 1)
。或者:第一次解引用之后,直接(*p)[1]
。
1 | int main() |
但是要注意:虽然int(*p)[8] = &arr
把数组地址(不是首元素地址,虽然值一样)取出来了,即使值和首元素地址一样,但是它和数组首元素指针是有区别的,&arr
的全部实际意义指的是从0到7的整个范围。如何证明呢?可以用p + 1
来说明:p+1后p指向了整个数组的末尾。
1 | int main() |
如果p+1后,要打印数组中的内容6,则得用(*(p + 1))[-3]
来打印。
1 | int main() |
反过来讲,如果对p解引用:*p
(int(*p)[8] = &arr
),虽然值没变,都是首地址。但是解引用后的*p
对应的是单个元素的指针,就失去了数组大小的属性。
结论:一维数组arr名字相当于元素指针,对数组名字取地址,则取到整个数组的指针,是行指针。如果对行指针解引用,则得到了元素指针。针对元素指针,可以移动,再次解引用,去取任意位置的元素。
二维数组
上面5讨论的是指针*
形式的解引用。如果要用中括号来取元素呢?
1 | int main() |
*(*p + 1)
等同于p[0][1]
所以,实质上,指向一维数组的指针,是一种特殊的二维数组,即只有一行的二维数组。
初始化
1 | int main() |
1 | int main() |
取二维数组的地址
打印6时,[]
比*
优先级高,因此*p
需要加括号。如果不加括号,则先运算p[1][1]
,再解引用,就完全不对了,p[1]
指的是(从0开始)第1个二维数组,直接跨过了本身全部的数据。
1 | int main() |
如何用纯指针方式打印6。
1 | int main() |
二维数组的本质内存结构
内存只有1维结构。
设arr是一维数组arr[8]
,a是二维数组a[2][4]
。则a[1][1] => arr[4 * 1 + 1] => arr[5]
用一维数组指针指向二维数组
1 | int main() |
三维数组
用上面的二维数组指针p,只用中括号,打印6。
1 | int main() |
*(*(*p + 1) + 1)
等同于p[0][1][1]
所以,实质上,指向二维数组的指针,是一种特殊的三维数组,即只有一个面的三维数组。
初始化
1 | int main() |
取三维数组的地址
1 | int main() |
*(**(*p + 1) + 1)
等同于p[0][1][0][1]
所以,实质上,指向3维数组的指针,是一种特殊的4维数组,即只有一个体的3维数组。
三维数组的应用
- 三维建模。面、地形、探测、体元素、医疗。
关联性
降维
int a[2][4] = { { 1, 2, 3, 4 }, { 5, 6, 7, 8 } };
如何 让p 是一个 相当于 二维数组a的名字?
首先分析,二维数组名字 相当于 行指针。一行有4个元素,因此是指向4个元素的数组指针
- 一维数组的名字相当于列指针(元素指针)
- 二维数组的名字相当于行指针(列数个元素的一维数组指针)
- 三维数组的名字相当于面指针(行数个元素列数个元素的二维数组指针)
1 | int main() |
计算数组行数、列数大小
遍历三维数组打印
1 | int main() |