C语言__指针_进阶_多维数组

指针

指针是一种特殊的变量,存的是别的变量的内存的地址。提供了一种间接访问方法。

指针的两大要素

有两大要素:1、存放的内容:变量的起始地址;2、指针的类型:指明长度

引用(take reference)、解引用(dereference)

take reference 表示取变量的地址。这是一种间接引用。

此处的引用是C语言中的,要和Cpp中的引用区分开。

1
2
3
4
5
6
7
8
9
int main()
{
int a = 12;
// take reference - 引用,取地址
int * p = &a;
// derefernce - 解引用
printf("%i\n", *p);
return 0;
}//12

指针的大小

指针的大小是固定的,只和内存地址相关。32位的系统用的是32位的地址总线,那么指针则是32位大小;64位的系统,那么指针则是64位大小,但是实际上的硬件并不是64根地址总线,而是48根,因为目前基本用不到那么多。

1
2
3
4
5
6
7
8
9
10
int main()
{
int a = 12;
// take reference - 取地址
int * p = &a;
printf("%i\n", sizeof p);
return 0;
}
//64位:8
//32位:4

二级指针

1
2
3
4
5
6
7
8
9
10
11
int main()
{
int a = 12;
// take reference - 取地址
int * p = &a;
int * pp = &p;
// derefernce - 解引用
printf("%i\n", **pp);
return 0;
}
//12

**这种连解引用,但是没有&&这种连取地址!因为第一次取完地址之后得出的是一个值(临时值),不具名且没有固定的内存地址,因此无法连取地址。

在Cpp中,右值引用是可以对临时变量“取地址”的,但不是这种连取地址的方式。

万能指针void *(master/universal pointer)

master/universal pointer。只保留了指针的首地址,只在中间传输地址有用,无法直接解引用。记作void *

要解引用,必须以强制类型转换告知指针的具体的结尾位置在哪才行。

1
2
3
4
5
6
7
int main()
{
int a = 12;
void * p = &a;
printf("%i\n", *p);//error, 报错:incomplete type is not allowed
printf("%i\n", *(int*)p);
}

案例

定义一个整型数

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 = barpfun = &bar等价。

在Cpp中,对于普通函数,隐式转换同样适用,但对于类成员函数则不是了,如果要取成员函数地址,必须显式加&,即&类名::函数。且调用时必须用(obj.*p)()的形式,来表示哪一个对象实例的调用。

1
2
3
4
5
6
7
8
9
10
class MyClass {
public:
void func() {}
};
int main() {
void (MyClass::*p)() = &MyClass::func;
MyClass obj;
(obj.*p)(); // 成员函数指针调用
//p(); // 非法!!!
}

如果是静态成员函数,取地址时可以省略 &。相应地,通过函数指针调用时,可以直接加小括号,不用解引用。

为什么普通成员函数(不包括静态成员函数)取地址时必须显式使用 &

当我们取一个成员函数的地址时,我们获取到的是一个“成员函数指针”,这个指针并不指向一个普通的函数地址,而是指向一个带有额外 this 指针的特殊函数。由于成员函数并不是普通的全局函数或静态函数,编译器需要明确知道你想要的是一个带 this 指针的成员函数地址,而不是一个普通的函数指针。

两种等效的调用形式(C、Cpp均可)

  1. 直接调用pfun();
  2. 解引用后调用(*pfun)();
1
2
pfun();      // 直接调用
(*pfun)(); // 解引用后调用
1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
void bar(void);
int main()
{
void (*pfun)(void) = &bar; // C语言中也可以用 = bar
bar();
(*pfun)();
return 0;
}
void bar(void)
{
printf("Hello\n");
}

C语言和Cpp中,都可以直接拿函数指针名字来调用它指向的函数。即没必要解引用再调用
因为本质上函数调用就是在地址后加(),即使是间接地址,也生效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
void bar(void);
int main()
{
void (*pfun)(void) = &bar; // C语言中也可以用 = bar
bar();
(*pfun)();
pfun(); // 等同于(*pfun)(); 等同于bar();
return 0;
}
void bar(void)
{
printf("Hello\n");
}

C和Cpp对于函数指针的注意事项

对于普通全局函数,函数名都会被隐式转换为函数指针,因此pfun = barpfun = &bar等价。

1
2
void (*pfun)(void) = bar;  // C和C++均可
void (*pfun)(void) = &bar; // 更显式的写法

以上是函数签名完全一致的。C语言、Cpp都允许且正确。
但,如果想要把另外一个不同签名的函数赋给pfun,则C会警告,Cpp则不允许:
必须把foo强制转换为bar函数对应签名(void(*)(void))的函数指针类型才行。

1
2
3
void foo(int);
void (*pfun)(void) = foo; // C中允许(有警告),C++报错
void (*pfun)(void) = (void(*)(void))foo;

总结

  1. 类型检查
    1. C中,函数指针可以不管函数签名类型,随意互传,可以编译通过,但是会有警告。
    2. Cpp中,函数指针互传时必须类型一致。不同类型的需要强转。
  2. 取函数地址
    1. Cpp中,普通函数名字或者静态成员函数,与C一样,可以隐式转换为函数地址。不用加&
    2. Cpp中,普通成员函数,取其地址,必须用&类名::函数的形式。
  3. 调用
    1. 普通函数、静态成员函数可以直接用函数指针加小括号调用。
    2. 普通成员函数,必须绑定对象实例,无法直接调用。因此必须先解引用。如(obj.*p)

函数指针类型的typedef

1
2
3
4
void bar(void);
void (*pfun)(void) = bar; // pfun是变量
typdef void(*PFun)(void); // PFun是类型
PFun pfun = bar; // 等效于上面的pfun

练习:定义函数指针数组

定义一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回整型数。

1
int (*a[10])(int);
  1. 首先是一个数组,则p[10]
  2. 其次,内容是指针,*p[10],因为[]优先级比*高,则不用加小括号处理。
  3. 指针是指向函数的,得在后面加小括号表示参数类型:*p[10](int),但是()优先级比*高,则系统会误以为这是函数调用(比如fun(123);)如此一来,就得在前面整体加小括号,让*p[10]先处理:(*p[10])(int)
  4. 再加上返回类型,则:int(*p[10])(int)

函数指针:函数的返回值是一个指针,这个指针指向一个数组

a是一个指针,指向一个函数,这个函数的参数为int型,函数的返回值是一个指针,这个指针指向一个数组,这个数组有10个元素,每个元素是一个void *型指针。

1
void*(*(*a)(int))[10];

可以通过 typedef 分步定义类型,让声明更清晰:

1
2
3
4
5
6
typedef void* ElementType;         // 数组元素类型是 void*
typedef ElementType ArrayType[10]; // 数组类型:10个void*元素的数组
typedef ArrayType* ArrayPtr; // 指针,指向上述数组
typedef ArrayPtr (*FuncPtr)(int); // 函数指针,接受int参数,返回ArrayPtr

FuncPtr a; // 等价于 void*(*(*a)(int))[10]

在C语言中,理解复杂声明需要从变量名开始,逐步向外解析。对于声明 void*(*(*a)(int))[10];,我们可以分步拆解:

  1. 变量名 a
    a 是一个指针(由 *a 可知)。
  2. a 指向一个函数
    (*a)(int) 表示 a 指向一个函数,该函数接受 int 类型的参数。
  3. 函数的返回值类型
    函数的返回值是 *(...),即一个指针。
    进一步分析 *(*a)(int),说明返回值是一个指针。
  4. 指针指向一个数组
    (*(*a)(int))[10] 表示返回值指向一个包含10个元素的数组。
  5. 数组元素的类型
    数组的每个元素是 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
2
3
4
5
6
7
8
9
10
int (*func(int))[10];
int main()
{
func(1);
}
int (*func(int))[10]
{
int (*arr)[10];
return arr;
}

以下这么写是错误的:

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
2
3
4
5
6
int main()
{
int arr[8] = { 1, 2, 3, 4, 5, 6, 7, 8 };
int * p = arr; //此时arr当做数组的首位int元素的int指针赋给了p
printf("%i\n", *p);//1
}

指针的运算

指针有加减运算。没有乘除运算。但是只能整数计算,不能小数计算。

1
2
3
4
5
6
7
8
9
int main()
{
int arr[8] = { 1, 2, 3, 4, 5, 6, 7, 8 };
int * p = arr; //此时arr当做数组的首位int元素(第0位置)的int指针赋给了p
p += 2; //移动2个int长度。即从0位置到2位置了。
printf("%i\n", *p); //3
printf("%i\n", *(p + 1)); //4
printf("%i\n", *(arr + 1)); //2 (arr + 1)即 移动到第1位置
}

指针减法的应用

1
2
3
4
5
6
7
8
9
10
int main()
{
int arr[8] = { 1, 2, 3, 4, 5, 6, 7, 8 };
int * p = arr; //此时arr当做数组的首位int元素(第0位置)的int指针赋给了p
p += 2; //移动2个int长度。即从0位置到2位置了。
printf("%i\n", p - arr); //2 是两个指针的差值,差出来2个元素

int * p2 = p + 3;
printf("%i\n", p2 - p); //3 是两个指针的差值,差出来3个元素
}

但是要注意,指针类型要一致,如果类型不一致:

1
2
3
4
5
6
7
8
9
10
int main()
{
int arr[8] = { 1, 2, 3, 4, 5, 6, 7, 8 };
int * p = arr; //此时arr当做数组的首位int元素(第0位置)的int指针赋给了p
p += 2; //移动2个int长度。即从0位置到2位置了。
printf("%i\n", p - arr); //2 是两个指针的差值,差出来2个int。

char * p2 = p + 3;//注意此处的指针类型是char*
printf("%i\n", p2 - p); //12 p的类型被转换为p2的类型了,差了12个char大小
}

语法糖

  1. 可以把上面的*(p + 1)直接用p[1]表示,那么arr[1]也就是*(arr + 1),同理arr[0]也就是*arr。也就是说,数组的名字可以当做指向首元素的指针。可以作arr + n这种计算获得数组中后n的元素指针。
  2. 但是,不能对arr本身进行+=++。因为这修改了arr本身的值,而数组一旦定义完成是不可删除、移动的。本质上,数组名字是一个const类型的。
  3. sizeof:对于数组名字,sizeof计算出来的是整个数组的大小。而对于sizeof p,(int * p = arr),计算出来的是一个元素的大小。
  4. 除了上面两种特殊情况,数组名字arr和指针完全一样,可以替换使用。
  5. 对数组名进行取地址:&arr会是什么?是指向一维数组的指针。int(*p)[8] = &arr。用p如何打印数组某一元素?首先得对数组指针解引用*p,再对解引用后的值进行加减*p + 1*解引用优先级大于+,所以不用给*p加括号),然后对加减后的值第二次解引用*(*p + 1)或者:第一次解引用之后,直接(*p)[1]
1
2
3
4
5
6
int main()
{
int arr[8] = { 1, 2, 3, 4, 5, 6, 7, 8 };
int(*p)[8] = &arr;
printf("%i\n", *(*p + 1)); //2
}

但是要注意:虽然int(*p)[8] = &arr把数组地址(不是首元素地址,虽然值一样)取出来了,即使值和首元素地址一样,但是它和数组首元素指针是有区别的,&arr的全部实际意义指的是从0到7的整个范围。如何证明呢?可以用p + 1来说明:p+1后p指向了整个数组的末尾。

1
2
3
4
5
6
int main()
{
int arr[8] = { 1, 2, 3, 4, 5, 6, 7, 8 };
int(*p)[8] = &arr;
printf("%i\n", *(p + 1)); //-85893460 - 打印出来一个未定义值,因为p+1后指向了整个数组的末尾。
}

如果p+1后,要打印数组中的内容6,则得用(*(p + 1))[-3]来打印。

1
2
3
4
5
6
int main()
{
int arr[8] = { 1, 2, 3, 4, 5, 6, 7, 8 };
int(*p)[8] = &arr;
printf("%i\n", (*(p + 1))[-3]); //6
}

反过来讲,如果对p解引用:*pint(*p)[8] = &arr),虽然值没变,都是首地址。但是解引用后的*p对应的是单个元素的指针,就失去了数组大小的属性。

结论:一维数组arr名字相当于元素指针,对数组名字取地址,则取到整个数组的指针,是行指针。如果对行指针解引用,则得到了元素指针。针对元素指针,可以移动,再次解引用,去取任意位置的元素。

二维数组

上面5讨论的是指针*形式的解引用。如果要用中括号来取元素呢?

1
2
3
4
5
6
7
int main()
{
int arr[8] = { 1, 2, 3, 4, 5, 6, 7, 8 };
int(*p)[8] = &arr;
//printf("%i\n", *(*p + 1)); //2
printf("%i\n", p[0][1]); //2
}

*(*p + 1)等同于p[0][1]所以,实质上,指向一维数组的指针,是一种特殊的二维数组,即只有一行的二维数组。

初始化

1
2
3
4
5
6
7
int main()
{
int a[2][4] = { { 1, 2, 3, 4 }, { 5, 6, 7, 8 } };
for(int i = 0; i < 2; ++i)
for(int j = 0; j < 4; ++j)
printf("%i\n", a[i][j]);
}// 1 2 3 4 5 6 7 8
1
2
3
4
5
6
7
int main()
{
int a[2][4] = { { 1, 2, 3, 4 }, { 5, 6, 7, 8 } };
// 欲打印数组中值为6的元素,则需要取出(从0开始)第1行、第1列的元素
printf("%i\n", a[1][1]);
}
// 6

取二维数组的地址

打印6时,[]*优先级高,因此*p需要加括号。如果不加括号,则先运算p[1][1],再解引用,就完全不对了,p[1]指的是(从0开始)第1个二维数组,直接跨过了本身全部的数据。

1
2
3
4
5
6
7
8
int main()
{
int a[2][4] = { { 1, 2, 3, 4 }, { 5, 6, 7, 8 } };
int(*p)[2][4] = &a;
// 欲打印数组中值为6的元素,则需要取出(从0开始)第1行、第1列的元素
printf("%i\n", (*p)[1][1]); // []比*优先级高,因此*p需要加括号。
}
// 6

如何用纯指针方式打印6。

1
2
3
4
5
6
int main()
{
int a[2][4] = { { 1, 2, 3, 4 }, { 5, 6, 7, 8 } };
int(*p)[2][4] = &a;
printf("%i\n", *(*(*p + 1) + 1));
}

二维数组的本质内存结构

内存只有1维结构。

设arr是一维数组arr[8],a是二维数组a[2][4]。则a[1][1] => arr[4 * 1 + 1] => arr[5]

用一维数组指针指向二维数组

1
2
3
4
5
6
int main()
{
int a[2][4] = { { 1, 2, 3, 4 }, { 5, 6, 7, 8 } };
int(*p)[8] = (int(*)[8])&a;
printf("%i\n", (*p)[5]);
}// 6

三维数组

用上面的二维数组指针p,只用中括号,打印6。

1
2
3
4
5
6
int main()
{
int a[2][4] = { { 1, 2, 3, 4 }, { 5, 6, 7, 8 } };
int(*p)[2][4] = &a;
printf("%i\n", p[0][1][1]);
}

*(*(*p + 1) + 1)等同于p[0][1][1]所以,实质上,指向二维数组的指针,是一种特殊的三维数组,即只有一个面的三维数组。

初始化

1
2
3
4
5
6
7
int main()
{
int a[2][2][2] = {
{ { 1, 2 }, { 3, 4 } }, // 面0
{ { 5, 6 }, { 7, 8 } } // 面1
};
}

取三维数组的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main()
{
int a[2][2][2] = {
{ { 1, 2 }, { 3, 4 } }, // 面0
{ { 5, 6 }, { 7, 8 } } // 面1
};
int(*p)[2][2][2] = &a;
printf("%i\n", (*p)[1][0][1]); // 第1面,第0行,第1列
printf("%i\n", *(**(*p + 1) + 1)); // 第0体,第1面,第0行,第1列
printf("%i\n", p[0][1][0][1]); // 第0体,第1面,第0行,第1列
}
// 6
// 6
// 6

*(**(*p + 1) + 1)等同于p[0][1][0][1]所以,实质上,指向3维数组的指针,是一种特殊的4维数组,即只有一个体的3维数组。

三维数组的应用

  1. 三维建模。面、地形、探测、体元素、医疗。

关联性

降维

int a[2][4] = { { 1, 2, 3, 4 }, { 5, 6, 7, 8 } };

如何 让p 是一个 相当于 二维数组a的名字?

首先分析,二维数组名字 相当于 行指针。一行有4个元素,因此是指向4个元素的数组指针

  1. 一维数组的名字相当于列指针(元素指针)
  2. 二维数组的名字相当于行指针(列数个元素的一维数组指针)
  3. 三维数组的名字相当于面指针(行数个元素×\times列数个元素的二维数组指针)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int main()
{
int a[2][4] = { { 1, 2, 3, 4 }, { 5, 6, 7, 8 } };
// 数组a降维至0维,损失了行数、列数
int * p = a;
// 数组a变换维至1维
int(*p)[8] = (int(*)[8])&a;
// 数组a
int(*p)[2][4] = &a;

// 如何 让p 是一个 相当于 二维数组名字?
// 首先分析,二维数组名字 相当于 行指针。一行有4个元素,因此是指向4个元素的数组指针
// 数组a降维至1维,损失了行数。
int(*p)[4] = a;
printf("%i\n", p[1][1]);

// 如何 让p 是一个 相当于 3维数组名字?
// 首先分析,3维数组名字 相当于 面指针。一面有2×2个元素,因此是指向2×2个元素的数组指针
// 数组a降维至2维,损失了面数。
int(*p)[2][2] = a;
printf("%i\n", p[1][0][1]);
}

计算数组行数、列数大小

遍历三维数组打印

1
2
3
4
5
6
7
8
9
10
11
int main()
{
int a[2][2][2] = {
{ { 1, 2 }, { 3, 4 } }, // 面0
{ { 5, 6 }, { 7, 8 } } // 面1
};
for(int i = 0; i < sizeof a / sizeof a[0]; ++i)
for(int j = 0; j < sizeof a[0] / sizeof a[0][0]; ++j)
for(int k = 0; k < sizeof a[0][0] / sizeof a[0][0][0]; ++k)
printf("%i\n", a[i][j][k]);
}// 1 2 3 4 5 6 7 8