C语言_函数_栈帧_参数_递归

函数

  1. 每个编程语言都有函数。
  2. 函数是代码的模块/容器/载体,代码如果要执行,必须在函数内才能执行。

内存结构、地址空间

  1. 对于可执行文件(Windows下是exe文件;Linux下是out文件),可执行文件有固定格式,从磁盘中加载到内存后,文件中相应部分的代码、数据等会映射到内存的五个不同的模块中。
  2. 每个程序都认为自己独占了整个的地址空间。如果是32位系统,则程序认为独占了32位的内存空间(0到4G)。这就是虚拟地址空间,自从386就开始有这个概念,便于多路程运行。

栈帧

  1. m和n是actual parameters.(实参)
  2. a和b是formal parameters.(形参)
  3. 所有的参数都是按值传递的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
void bar(int a, int b);

int main()
{
int m = 5, n = 6;
bar(m, n);
return 0;
}
void bar(int a, int b)
{
int c = a + b;
printf("%i\n", c);
}

反了?

image-20240316220318599

栈是从高地址向低地址扩展的。越后定义的变量地址越低。

int m = 5, n = 6;这条语句:先定义n,再定义m,因此n在高地址、m在低地址。

printf的参数

variable parameters

1
int printf(char const * const _Format, ...);

启示:对外开放的函数,如果要防止误改内容,则加const表明只读,否则别人不敢传参数。

const是什么

const有三个位置可以放。

看表达式:

  1. 无论右侧是什么,左侧是int,则右侧也是一个int
    1. 左侧一个int,右侧一个a。
1
int a;
  1. 左侧一个int,右侧是*a*a是一个int:表示a这个变量解引用之后则是一个int,则a是一个指向int的指针。
1
int *a;
  1. const是修饰右侧东西的。
    1. 如果在int后写const:*a是一个不能修改的int值。同理const写到int前面也一样:const int * a;
1
int const * a;
  1. 如果在*后写const:a是一个不能修改的值。*a是一个能修改的int值,a这个变量解引用之后则是一个int,则a是一个指向int的指针,但a是一个不能修改的值。

后缀为.c测试

1
2
3
4
5
int main()
{
char * p = "xxx"; //可以编译通过
p[1] = 'M'; //可以编译通过
}

但在运行阶段,会抛出异常:write access violation

不要企图改变常量区。

后缀为.cpp测试

1
2
3
4
5
6
7
int main()
{
char str[] = "Hello";
char * p = str;
p[2] = 'x';
}// 是可以通过p间接修改str[]中的值的。
//最后str[]变为"Hexlo"
1
2
3
4
5
6
int main()
{
char str[] = "Hello";
char * p = str;
p = "xxx";// error //char * p虽然可以改变指针值,但是不能指向常量字符串
}
1
2
3
4
5
6
7
int main()
{
char str[] = "Hello";
const char * p = str;
p = "aaa"; //char * p既可以改变指针值,也可以指向常量字符串
p[2] = 'x'; //error 虽然p可以指向常量字符串,但是不能间接修改值
}
1
2
3
4
5
6
7
int main()
{
char str[] = "Hello";
const char * const p = str;
p = "aaa";//error //char * p不可以改变指针值,也不能通过p间接修改值
p[2] = 'x';//error
}

可变参数

欲用show打印可变参数中第n个值:

需要取函数栈帧中n变量的地址,然后向上寻找n个int大小(因为参数是从高地址到低地址扩展的),即得到可变参数中第n个参数的地址。

栈帧示意图:

image-20240316231327944

1
2
3
4
5
6
7
8
9
10
11
12
void show(int n, ...);
int main()
{
show(2, 10, 20, 30, 40, 50); // 欲打印第2个值,20
}
void show(int n, ...)
{
// 指针进行整数加运算。
printf("%i\n", *(&n + n));
}
//64位下:打印10
//32位下:正确,打印20

奇怪的是,64位下:打印10;32位下:打印20。因为:int固然是4字节大小,而且int的指针加减1的大小也应该是4字节(p + n = p的值 + sizeof(int)* n)。但是,**在64位下的字长是8字节的,因为地址总线每一次至少会传64位(8字节)的内容。因此,每个int实际占用了8个字节。**所以我们p+n仅仅移动了8个字节,只能打到10。

经过调试,可以看到实际的内存内容(16进制):

image-20240316234542234

可以看到,02 0a 14 1e 28 32依次是6个int参数,实际都占用了8个字节。大端地址存放高字节。则如果要打印可变参数中的第n个int,需要*(&n + 2 * n)或者把&n强制转换为64位大小的long long**((long long*)&n + n)

1
2
3
4
5
6
7
8
9
10
11
12
void show(int n, ...);
int main()
{
show(2, 10, 20, 30, 40, 50); // 欲打印第2个值,20
}
void show(int n, ...)
{
// 指针进行整数加运算。
printf("%i\n", *((long long*)&n + n));
}
//64位下:正确,打印20
//32位下:错误,打印286331153

但是,这样的话,虽然64位下打印正确了,但是32位下又错了!因为移动了n个8字节,打到了函数栈帧之外!所以,必须想一个能判断32位、64位的通用方法,去控制int指针的大小(即64位下8字节、32位下4字节)。

Ctrl + 左键点入size_t,会出来一些宏定义:

1
2
3
4
5
6
7
8
#ifdef _WIN64
typedef unsigned __int64 size_t;
typedef __int64 ptrdiff_t;
typedef __int64 intptr_t;
#else
typedef unsigned int size_t;
typedef int ptrdiff_t;
typedef int intptr_t;

在程序为64位编译时,上面三个会生效、下面会失效;32位编译时反之。

通过这个,可以控制int指针的大小。

1
2
3
4
5
6
7
8
9
10
11
12
void show(int n, ...);
int main()
{
show(2, 10, 20, 30, 40, 50); // 欲打印第2个值,20
}
void show(int n, ...)
{
// 指针进行整数加运算。
printf("%i\n", *((intptr_t*)&n + n));
}
//64位下:正确,打印20
//32位下:正确,打印20

数组和函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
void bar(int a[8]);
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8 };
bar(arr);
return 0;
}
// 虽然传的是数组类型,但实际退化为指针了
void bar(int a[8])
{
printf("%i\n", sizeof a);
}
// 64位:8
// 32位:4

虽然形参写的是带元素个数的数组类型,但是因为实际退化为指针了,所以写不写具体数目无所谓:int a[],甚至直接写个int * a也是一样的。如果要告知数组具体个数,需要另传一个int参数n。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
void bar(int * a, int n);
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8 };
bar(arr, sizeof arr / sizeof arr[0]);
return 0;
}
void bar(int * a, int n)
{
for(int i = 0; i < n; ++i)
{
printf("%i\n", a[i]);
}
}// 1 2 3 4 5 6 7 8

二维数组

行信息丢失,需要用int n代替,而我们要保留列信息,才能保证二维数组的有效。

即传一个包含4列元素的行指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void bar(int(*a)[4], int n);
int main()
{
int arr[2][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}};
bar(arr, sizeof arr / sizeof arr[0]);
return 0;
}
void bar(int(*a)[4], int n)
{
for(int i = 0; i < n; ++i, printf("\n");)
{
for(int j = 0; j < 4; ++j)
{
printf("%i\n", a[i][j]);
}
}
}
// 1 2 3 4
// 5 6 7 8

main参数

1
2
3
4
int main(int ac, char * av[])
{
return 0;
}

ac指argument count;av指argument value。

1
2
3
4
5
6
7
8
int main(int ac, char * av[])
{
for(int i = 0; i < ac; ++i)
{
printf("%s\n", av[i]);
}
return 0;
}

编译程序后,在生成目录下命令行(cmd)测试

1
2
3
4
5
6
7
8
9
10
11
C:\Users\xcg\Project1.exe #输入的

C:\Users\xcg\Project1.exe #输出的

C:\Users\xcg\Project1.exe -h -m #输入的

#输出的
C:\Users\xcg\Project1.exe
-h
-m

实质上

实质上,av的类型被退化为了二级指针。

1
2
3
4
5
6
7
8
int main(int ac, char ** av)
{
for(int i = 0; i < ac; ++i)
{
printf("%s\n", *(av + i));
}
return 0;
}

更安全地,加const,让av指向的内容、av的行数组、av本身不可变

1
2
3
4
5
6
7
8
int main(int ac, char const * const * const av)
{
for(int i = 0; i < ac; ++i)
{
printf("%s\n", *(av + i));
}
return 0;
}

递归

递归表现在:行为一致,只是每次数据不一样。

递归的两大要素:递推公式(状态转移方程);终止条件。

  1. 下降的时候执行行为,即行为在调用递归之前,叫首递归
  2. 上升的时候执行行为,即行为在调用递归之后,叫尾递归
  3. 如果递归前后都有行为,叫中间递归
1
2
3
4
5
6
7
8
9
10
11
void show(int n); // show(10) -> 10 9 8 ... 1
int main()
{
show(10);
}
void show(int n)
{
if(n < 1) return;
printf("%i\n", n);
show(n - 1);
}
1
2
3
4
5
6
7
8
9
10
11
void show(int n); // show(10) -> 1 2 3 ... 10
int main()
{
show(10);
}
void show(int n)
{
if(n < 1) return;
show(n - 1);
printf("%i\n", n);
}

求加和

1
2
3
4
5
6
7
8
9
10
int sum(int n); // 1 + 2 + 3 + ... + 10
int main()
{
int r = sum(10);
}
int sum(int n)
{
if(n == 1) return 1;
return n + sum(n - 1);
}// r = 55

Hanoi

1 ~ n的盘子通过A、B、C三个柱子挪到全部C。小的在上面,大的在下面。

1
2
3
4
5
6
7
void hanoi(int n, char from, char via, char to)
{
if(n == 0) return;
hanoi(n - 1, from, to, via);
printf("%d: %c --> %c\n", n, from, to);
hanoi(n - 1, via, from, to);
}

测试

1
2
3
4
int main()
{
hanoi(3, 'A', 'B', 'C');
}
1
2
3
4
5
6
7
1: A --> C
2: A --> B
1: C --> B
3: A --> C
1: B --> A
2: B --> C
1: A --> C

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