指针和数组

指针

变量是什么

程序中的变量只是一段存储空间的别名,那么是不是必须通过这个别名才能使用这段存储空间呢?

不是,我们还可以通过这段空间的地址或者说起始地址去使用这段空间;

*的意义

  • 在指针声明时,* 号表示所声明的变量为指针
  • 在指针使用时,* 号表示取指针所指向的内存空间的地址

zhizhendeyiyi

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

int main() {
int i = 0;
int* pI;
char* pC;
float* pF;

pI = &i;

*pI = 10;

printf("%p, %p, %d\n", pI, &i, i); // 0x7ffd4abc986c, 0x7ffd4abc986c, 10
printf("%ld, %ld, %p\n", sizeof(int*), sizeof(pI), &pI); // 8, 8, 0x7ffd4abc9870
printf("%ld, %ld, %p\n", sizeof(char*), sizeof(pC), &pC); // 8, 8, 0x7ffd4abc9878
printf("%ld, %ld, %p\n",sizeof(float*), sizeof(pC), &pF); // 8, 8, 0x7ffd4abc9880

return 0;
}

在C语言中,指针的大小取决于计算机操作系统。在32位平台上,指针的大小是4个字节;在64位平台上,指针的大小是8个字节;

传值调用与传地址调用

  • 指针是变量,因此可以声明指针参数
  • 当一个函数体内部需要改变实参的值,则需要使用指针参数
  • 函数调用时是将实参值将复制到形参
  • 指针适用于复杂数据类型作为参数的函数中

常量与指针

changliangyuzhizhen

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>

int main() {
int i = 0;

const int* p1 = &i;
int const* p2 = &i;
int* const p3 = &i;
const int* const p4 = &i;

*p1 = 1; // error, P1 这个指针指向的内存中的值是不可改变的
p1 = NULL; // ok, p1 本身是可变的,所以这个不会报错,从指向 i 的地址改为 null

*p2 = 2; // error, 同上
p2 = NULL; // ok, 同上

*p3 = 3; // ok, p3 这个指针指向的内存中的内容是可变的,所以不会报错
p3 = NULL; // error, p3 这个指针变量本身是不可变的,所以试图修改它的值就会报错

*p4 = 4; // error p4 这个指针所指向的内存中的对象是不可变的, 试图修改就会报错
p4 = NULL // error p4 这个指针本身是不可变的, 试图修改就会报错

return 0;
}

小结

  • 指针是 C 语言中一种特别的变量
  • 指针所保存的值是内存的地址
  • 可以通过指针修改内存中的任意地址内容

指针的阅读技巧

下列标识符代表什么含义?

Czhizhenliti

右左法则

  • 最里面的圆括号中未定义的标识符看起
  • 首先往右看,在往左看
  • 遇到圆括号方括号时可以确定部分类型,并调转方向
  • 重复2,3步骤,

解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int (*p1)(int*, int (*f)(int*));
==> p1 为指针,指向函数,指向的函数有两个参数,第一个是 int*,第二个是个函数指针 f,指向的函数是 int*,返回值是 int; p1 的返回值为 int 类型
==> 函数指针,其中有参数为领一个函数指针

int (*p2[5])(int*);
==> p2 为数组,有五个元素,五个元素的内容是指针,指向函数,函数类型为 int(int*)
==> 数组中存放的是函数指针

int (*(*p3)[5])(int*);
==> p3 是指针,数组指针,指向的数组元素有五个,这五个元素为指针,是函数指针,指向的函数类型为 int(int*)
==> 指针指向了一个数组,数组中的五个元素为函数指针


int*(*(*p4)(int*))(int*);
==> p4 为指针,函数指针,参数为 int* 返回值为指针,是函数指针,指向的函数类型为 int* (int*)
==> 一个函数指针的返回值是个函数指针

int (*(*p5)(int*))[5];
==> p5 为指针,函数指针,参数为 int*, 返回值为指针,指向的数组类型为 int[5]
==> 一个函数指针的返回值是个数组

以最后一个为例可以简化为:
typedef int(ArrayType)[5];
typedef ArrayType* (FuncType)(int*)
FuncType* p5;

可通过 typedef 简化复杂指针的定义

数组

数组就是相同类型的变量的有序集合

shuzujieshao

数组的大小

  • 数组在一片连续的内存空间中存储元素
  • 数组元素的个数可以显示或隐式指定
1
2
int a[5] = {1, 2};
int b[] = {1, 2};

数组大小与数组名

  • 数组名代表数组元素的首地址
  • 数组的地址需要用取地址符 & 才能得到
  • 数组首元素的地址值与数组的地址值相同
  • 数组首元素的地址与数组的地址是两个不同的概念
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main() {

int a[5] = {0};

printf("a = %p\n", a); // a = 0x7ffcfb8b3950
printf("&a = %p\n", &a); // &a = 0x7ffcfb8b3950
printf("&a[0] = %p\n", &a[0]); // &a[0] = 0x7ffcfb8b3950

return 0;
}

数组的地址值和数组第一个元素的地址值,从地址值上看是完全一致的,但其实他们所代表的意义是不相同的,不相同在他们所占用的空间长度是不一样的

在 C 语言中,数组的地址值数组第一个元素的地址值从表面上看是相同的,即 &array&array[0] 的值是相同的,都是数组首地址。

不同点在于:

  • &array:代表整个数组的地址,类型是 T (*)[N](指向数组的指针,T 是数组元素的类型,N 是数组的大小)
  • &array[0]:代表数组第一个元素的地址,类型是 T*(指向数组元素的指针)
  • &array(整个数组的地址):代表整个数组所占用的内存块,这个块的大小是 sizeof(array),也就是数组中所有元素的总大小。
  • &array[0](第一个元素的地址):仅仅代表数组中第一个元素的地址,它指向的内存块大小是 sizeof(array[0]),也就是单个元素的大小。

例如,假设你有一个 int array[10],在 32 位系统上:

  • sizeof(array) 是 40 字节(假设每个 int 是 4 字节)。
  • sizeof(&array) 是指针的大小,通常为 4 字节(32 位系统上)。
  • sizeof(array[0]) 是单个元素的大小,4 字节。

数组名的盲点

  • 数组名可以看做一个常量指针
  • 数组名”指向“的是内存中数组首元素的起始地址
  • 数组名不包含数组的长度信息
  • 在表达式中数组名只能作为右值使用
  • 只有在下列场合中数组名不能看作常量指针
    • 数组名作为 sizeof 操作符的参数
    • 数组名作为 & 运算符的参数

常量指针:指针指向的值是常量,不能通过该指针修改指向的值,但指针可以指向其他变量。

指针常量:指针本身是常量,不能改变指针指向的地址,但可以通过该指针修改所指向的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <stdio.h>

int main() {

int a[5] = {0};
int b[2];
int* p = NULL;

p = a;

printf("a = %p\n", a);
printf("p = %p\n", p);
printf("&p = %p\n", &p);
printf("sizeof(a) = %ld\n", sizeof(a));
printf("sizeof(p) = %ld\n", sizeof(p));

/*
a = 0x7ffc15ef2ba0
p = 0x7ffc15ef2ba0
&p = 0x7ffc15ef2b98
sizeof(a) = 20
sizeof(p) = 8
*/

p = b;

printf("a = %p\n", b);
printf("p = %p\n", p);
printf("&p = %p\n", &p);
printf("sizeof(b) = %ld\n", sizeof(b));
printf("sizeof(p) = %ld\n", sizeof(p));
/*
a = 0x7fff744ebe08
p = 0x7fff744ebe08
&p = 0x7fff744ebe00
sizeof(b) = 8
sizeof(p) = 8
*/

b = a; // Error, 数组与指针是不同的不可以互相赋值, 2.数组名不可作为左值

return 0;
}

小结

  • 数组是一片连续的内存空间
  • 数组的地址和数组首元素的地址意义不同
  • 数组名在大多数情况下被当成常量指针处理
  • 数组名其实并不是指针,不能将其等同于指针

概念的混淆是 BUG 的根源之一!

数组和指针

在数组中 a + 1 的意义是什么?结果是什么?

指针运算的意义是什么?结果又是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

int main() {
int a[5] = { 0 };
int* p = NULL;

printf("a = 0x%x\n", (unsigned int)(a));
printf("a + 1 = 0x%x\n", (unsigned int)(a + 1));

printf("p = 0x%x\n", (unsigned int)(p));
printf("p + 1 = 0x%x\n", (unsigned int)(p + 1));

/*
a = 0xb9ff2670
a + 1 = 0xb9ff2674
p = 0x0
p + 1 = 0x4
*/
}

一、a 是数组元素的首地址,首地址 + 1,它却增加了 4,因为数组的中元素的大小是 4 个字节;

二、a 是数组元素的首地址,首地址进行操作,他得到的结果也是个地址(指针s)值;

指针是一种特殊的变量,与整数的运算规则为:

zhizhenzhenshuyunsuanguize

指针和指针之间的运算

  • 指针之间只支持减法运算
  • 参与减法运算的指针类型必须相同

规则为:

zhizhenzhijianyunsuanguize

指针之间也可以进行比较运算

  • 指针也可以进行关系运算(<、<=、>、>=)
  • 指针关系运算的前提是同时指向同一个数组的元素
  • 任意两个指针之间的*比较运算(==,!=)无限制
  • 参与比较运算的指针类型必须相同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>

int main() {

int arr[] = {10, 20, 30, 40, 50};
int *p1 = &arr[1]; // 指向数组的第二个元素 20
int *p2 = &arr[3]; // 指向数组的第四个元素 40

// 比较指针是否相等
if (p1 == p2) {
printf("p1 and p2 point to the same location.\n");
} else {
printf("p1 and p2 point to different locations.\n");
}

// 比较指针大小,检查指针指向的地址位置
if (p1 < p2) {
printf("p1 points to an earlier element in the array than p2.\n");
} else {
printf("p1 points to a later element in the array than p2.\n");
}

return 0;

/*
p1 and p2 point to different locations.
p1 points to an earlier element in the array than p2.
*/
}

指针之间的比较是比较二者在数组中的位置,比较的是二者指向的地址,而不是地址中的数据;

数组的访问方式

shuzudefangwenfangshi

下标形式 vs 数组形式

  • 指针以固定增量在数组中移动时,效率高于下标形式
  • 指针增量为 1 且硬件具有硬件模型时,效率更高
  • 下标形式与指针形式的转换
1
a[n] <--> *(a + n) <--> *(n + a) <--> n[a]

注意:

现代编译器的生成代码优化率已大大提高,在固定增量时,下标形式的效率已经和指针形式相当;

但从可读性和代码的维护角度来看,下标形式更优;

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdio.h>

int main() {

int a[5] = {0};
int* p = a;
int i = 0;

for(i = 0; i < 5; i++) {
p[i] = i + 1;
}

for (i = 0; i < 5; i++) {
printf("a[%d] = %d\n", i, *(a + i));
}

printf("\n");

for(i = 0; i < 5; i++) {
i[a] = i + 10;
}

for(i = 0; i < 5; i++) {
printf("p[%d] = %d\n", i, p[i]);
}

/*
a[0] = 1
a[1] = 2
a[2] = 3
a[3] = 4
a[4] = 5

p[0] = 10
p[1] = 11
p[2] = 12
p[3] = 13
p[4] = 14
*/

return 0;
}

数组名是不是指针?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// ext.c
int a[] = {1, 2, 3, 4};

// main.c
#include <stdio.h>

int main() {

extern int a[];

printf("&a = %p\n", &a);
printf("a = %p\n", a);
printf("*a = %d\n", *a);

/*
&a = 0x55c4991b9010
a = 0x55c4991b9010
*a = 1
*/
return 0;
}
===================================
// main
#include <stdio.h>

int main() {

extern int* a;

printf("&a = %p\n", &a); // 获取变量 a 在内存中的地址
printf("a = %p\n", a);
printf("*a = %d\n", *a);

/*
&a = 0x556c7aeb5010
a = 0x200000001
Segmentation fault (core dumped)
*/

return 0;
}

数组名不是指针,只不过在某些情况下可以看作指针,或者当作指针来进行使用;

**&a**:这行代码打印的是变量 a 本身的地址。

**a**:这里的 a 是被 extern int* a; 声明为一个指针。然而,由于指针 a 没有被正确初始化,它的值是未定义的,可能是垃圾值或无效的内存地址。这个地址是一个无效的或随机的内存地址,可能指向了程序无法访问的区域。

***a**:这里试图访问 a 指针指向的内存位置,并打印出该位置的值。但是,由于 a 指针指向的是无效的内存地址(如上例中的 0x200000001),所以试图访问该地址时会引发段错误。

a 和 &a 的区别

  • a 为数组首元素的地址
  • &a 为整个数组的地址
  • a 和 &a 的区别在于指针运算

ahedizhiadequbie

面试示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

int main() {

int a[5] = {1, 2, 3, 4, 5};
int* p1 = (int*)(&a + 1);
int* p2 = (int*)((int)a + 1);
int* p3 = (int*)(a + 1);

printf("%d\n", p1[-1]);
printf("%d\n", p2[0]);
printf("%d\n", p3[3]);

/*
5 // p1[-1],因为 P1 指向了数组末尾的边界,-1 就变成了指向 5 这个元素的地址 p1[-1] ==> *(p1 - 1)
Segmentation fault (core dumped) //这是指向了一个未知的地址,如果该地址不可读那么就会报段错误,否则就是一个未知的地址
2 // 这里输出2 就是数组首地址 + 1
*/
}

&a 是数组的地址,数组地址加一,这时候就超出了数组的地址范围了,指向了 5 这个元素右边界的位置;

a 是元素的首地址,转为一个整数,就是将一个八位的地址值截取为一个四位的整数;加一,那就是简单的整数加一,这时候再将它转为一个指针,就是将一个四位的整数转为一个八位的指针地址,那么这个地址就会变成一个指向未知数据的一个地址,这时候我们想想获取一个字节的内容,这时候就是一个未知状态的访问,那么也就报错了;

a 是元素的首地址,首地址加一,就是指向数组的下一个元素,也就是数据为 2 的未知

数组参数

  • 数组作为函数参数时,编译器将其编译成对应的指针
1
2
void f(int a[]) <==> void f(int* a)
void f(int a[5]) <==> void f(int* a)

注意:

在传递数组时,数组的大小信息是不会传递过去的,因此当定义的函数中有数组参数时,需要定义另一个参数来表示数组的大小

虚幻的数组参数

数组参数会退化为指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>

void func1(char a[5]) {

printf("In func1: sizeof(a) = %lu\n", sizeof(a));

*a = 'a';

a = NULL;
}

void func2(char b[]) {

printf("In func2 sizeof(b) = %lu\n", sizeof(b));

*b = 'b';

b = NULL;
}

int main() {

char array[10] = {0};

func1(array);
printf("array[0] = %c\n", array[0]);

func2(array);
printf("array[0] = %c\n", array[0]);

/*
In func1: sizeof(a) = 8
array[0] = a
In func2 sizeof(b) = 8
array[0] = b
*/
return 0;
}

从上面的输出中可以看出将数组作为函数参数时,他会退化为指针:

否则 输出的 sizeof 大小应该是数组的长度,而不是指针的长度,且 a = NULL 也会报错,因为数组名不可以作为左值使用;

小结

  • 数组名和指针使用方式相同
    • 数组名的本质不是指针
    • 指针的本质不是数组
  • 数组名并不是数组的地址,而是数组首元素的地址
  • 函数的数组参数退化为指针

数组指针和指针数组

1
2
3
4
int array[5];
int matrix[3][3];
int* pa = array;
int* pm = matrix;
  1. array 代表数组名,即数组元素的首地址

  2. 那么 matrix 代表什么?

  3. &array 表示数组的地址,虽然与 array 二者在值上是一致的,但二者意义是不同的,二者所代表的类型相同么?

数组类型

  • C 语言中的数组有自己的类型
  • 数组的类型由元素类型数组大小共同决定
1
2
// 例如
int array[5] 的类型为 int[5]

定义数组类型

1
2
3
4
5
6
7
8
9
10
// C 语言中通过 typedef 为数字类型重命名
typedef type(name)[size];

// 数组类型:
typedef int(AINT5)[5];
typedef float(AFLOAT10)[10];

// 数组定义:
AINT5 iArray;
AFLOAT10 fArray;

typedef 是重命名关键字,在 type(name)[size]; 表示要对数组重命名,重命名为 name;

AINT5 iArray; 中 iArray 就是个数组类型,元素类型为 int,数组长度为 5;

数组指针

数组指针是个指针用于指向一个数组(不是指向数组的首元素)

  • 数组指针用于指向一个数组
  • 数组名是数组首元素的起始地址,但并不是数组的起始地址
  • 通过将取地址符 & 作用于数组名可以得到数组的起始地址
  • 通过数组类型定义数组的指针:ArrayType* pointer;
  • 也可以直接定义:type(*pointer)[n];
    • pointer 为数组指针变量名
    • type 为指向的数组中元素的类型
    • n 为指向的数组的大小
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include "stdio.h"

typedef int(AINT5)[5];
typedef float(AFLOAT10)[10];
typedef char(ACHAR9)[9];

int main() {
AINT5 al;
float fArray[10];
AFLOAT10* pf = &fArray;
ACHAR9 cArray;

char(*pc)[9] = &cArray;
char(*pcw)[4] = cArray; // cArray 是数组的首地址,类型为 char*

int i = 0;

// sizeof(al) 中 al 是个变量,是个 AINT5 类型的变量所以需要占 AINT5 类型大小的字节数,也就是 20
printf("%lu, %lu\n", sizeof(AINT5), sizeof(al)); // 20 20?

for(i = 0; i < 10; i++) {
(*pf)[i] = i;
}

for(i = 0; i < 10; i++) {
printf("%f\n", fArray[i]);
}

printf("%p, %p, %p\n", &cArray, pc + 1, pcw + 1);

/*
20, 20
0.000000
1.000000
2.000000
3.000000
4.000000
5.000000
6.000000
7.000000
8.000000
9.000000
0x7ffc5f3943ef, 0x7ffc5f3943f8, 0x7ffc5f3943f3
*/
}

=======================================================
// 同类型试题:
#include "stdio.h"


typedef char(ACHAR9)[9];

int main() {

ACHAR9 cArray;
char(*pc)[9] = &cArray;

int a[] = {1, 2, 3, 4, 5};
int* pa = (int*)(&a + 1);

printf("%p, %p, %p\n", &a, (&a + 1), pa); // &a 和 &a + 1 差了 0x14 也就是 20 个字节
printf("%p, %p\n", &cArray, pc + 1);

}

pc 是个变量,指针指向的内容为 &cArray;pc + 1 也就是 &cArray 的地址 + 他数据类型的长短小,而 &cArray 的类型为 ACHAR9 它占了 9 个字节的大小;所以 pc + 1 在地址中的实际偏移量为 9;0x7ffc5f3943f8 = 0x7ffc5f3943ef + 9;

同理 pcw + 1 的地址 0x7ffc5f3943f3 = 0x7ffc5f3943ef + 4;

指针数组

  • 指针数组是一个普通的数组
  • 指针数组中每一个元素为一个指针
  • 指针数组的定义:type* pArray[n];

Czhizhenshuzudingyi

小结

  • 数组的类型由元素类型数组大小共同决定
  • 数组指针是一个指针,指向对应类型的数组
  • 指针数组是一个数组,其中每个元素为指针
  • 数组指针遵循指针运算法则
  • 指针数组拥有 C 语言数组的各种特性

多维数组与多维指针

指向指针的指针

  • 指针的本质是变量

  • 指针会占用一定的内存空间

  • 可以定义指针的指针来保存指针变量的地址

1
2
3
4
5
6
7
8
9
10
11
int main() {
int i = 0;
int* p = NULL;
int** pp = NULL;

pp = &p;

*pp = &i; // 给 p 一个 &i 的地址

return 0
}

为什么需要指向指针的指针?

  • 指针在本质上也是变量
  • 对于指针也同样存在传值调用传址调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 重置一个动态空间的大小,本来一个 5 大小的空间不够了,申请一个 10 大小的空间,然后把内容复制过去
#include<stdio.h>
#include<malloc.h>

int reset(char** p, int size, int new_size) {
int ret = 1;
int i = 0;
int len = 0;
char* pt = NULL;
char* tmp = NULL;
char* pp = *p;

if(p != NULL) && (new_size > 0) { // 安全性检测
pt = (char*)malloc(new_size); // 动态的申请一个 new_size 大小的空间
tmp = pt;
len = (size < new_size)? size : new_size; // 取小的哪一个进行赋值
for(i = 0; i < len; i++) {
*tmp++ = *pp++ // 将 pp 中保存的内容(p 指向的地址)传递给 tem 的空间并自增;
}
free(*p); // 释放掉原来的空间
*p = pt; // 指向新申请的地址的地址;
}else {
ret = 0
}
return ret;
}

int main() {
char* p = (char*)malloc(5);
printf("%p\n", p);
if(reset(&p, 5, 3)) {
printf("%p\n", p);
}
free(p);

return 0;

/*
0x5559925152a0
0x5559925156d0
*/
}

二维数组与二级指针

  • 二维数组在内存中以一维的方式排布

  • 二维数组中的第一维四一维数组

  • 二维数组中的第二维才是具体的值

  • 二维数组的数组名可看做常量指针

二维数组是一个一维数组,其中的值是另一个一维数组

Cerweishuzushili

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 二维数组的访问方式

#include<stdio.h>

int printArray(int a[], int size) {
int i = 0;

printf("printArray: %ld\n", sizeof(a));

for(i = 0; i < size; i++) {
printf("%d\n", a[i]);
}
}

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

int i = 0;
int j = 0;

for(i = 0; i < 3; i++) {
for(j = 0; j < 3; j++) {
// *(a + i) 表示拿到一维数组的首地址,也就是二维数组中每个一维数组的首地址
// a 是二维数组的数组名
// a + i,以 a + 1 为例,也就是昂上图中 a1 中第一个元素的位置
// *(a + 1) 也就是 a1 找个元素的地址,也就是 a1 这个一维指针的首地址
// 这里要注意,不要理解为 *(a + 1) 是 a1 中第一个元素的值
// 因为二维数组也就是一维数组,只不过它的值是一维数组,我们才将他称为二维数组
// 所以这里 *(a + i) 获取的是二速数组中 a1 找个元素的值也就是 a1 所在的一维数组的首地址
// *(*(a + 1) + j) 也就是 a1 数组中的第 j 个元素
// *(a + i) ==> a[i], *(a[i] + j) ==> a[i][j]
printf("%d\n, ", *(*(a + i) + j));
}
printf("\n");
}

printf("\n");

printArray(p, 9);

/*
0, 1, 2,
3, 4, 5,
6, 7, 8,

printArray: 8
0
1
2
3
4
5
6
7
8
*/

return 0;
}

在 C 语言中,*(*(a + i) + j)a[i][j] 之间的等价关系可以通过以下步骤理解:

  1. 数组名作为指针a 是一个指向数组的指针,a + i 计算的是指向第 i 行的指针。
  2. 解引用*(a + i) 取得第 i 行的首地址,实际上是一个指向该行一维数组的指针。
  3. 偏移量*(a + i) + j 计算的是第 i 行中第 j 列的地址。
  4. 解引用:最后,通过 *(*(a + i) + j) 解引用这个地址,得到了 a[i][j] 的值。

因此,*(*(a + i) + j) 实际上是先获取第 i 行的指针,然后再访问该行的第 j 列的元素,这与 a[i][j] 的操作是等价的。

数组名

1
2
3
4
// 一维数组名代表数组首元素的地址
int a[5],a 的类型为 int*
// 二维数组名同样代表数组首元素的地址
int m[2][5],m 的类型为 int(*)[5]

二维数组名可以看作是指向数组的常量指针

二维数组可以看作是一维数组

二维数组中的每个元素都是同类型的一维数组

如何动态的申请二维数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include<stdio.h>
#include <malloc.h>

int** malloc2d(int row, int col) {
int** ret = NULL;

if((row >0 && (col > 0))) {
int* p = NULL;
// 申请指向指针的内存
ret = (int**)malloc(row * sizeof(int*));
// 具体的元素的个数
p = (int*)malloc(row * col);

if((ret != NULL) && (p != NULL)) {
int i = 0;
for(i = 0; i < row; i++) {
ret[i] = p + i * col;
}
}else {
free(ret);
free(p);
ret = NULL;
}
}
return ret;
}

// 释放数组空间
void free2d(int** p) {
if (*p != NULL) {
free(*p);
}
free(p);
}

int main() {
int** a = malloc2d(3, 3);
int i = 0;
int j = 0;

for (i = 0; i < 3; i++) {
for(j = 0; j < 3; j++) {
printf("%d, ", a[i][j]);
}
printf("\n");
}
free2d(a);

/*
0, 0, 0,
0, 0, 0,
1041, 0, 825503793,

*/
return 0;
}

malloc 这个函数不保证申请出来的空间所有值都为 0;

小结

  • C 语言中只支持一维数组

  • C 语言中的数组大小必须在编译期就作为常数确定

  • C 语言中的数组元素可以是任何类型的数据

  • C 语言中数组的元素可以是另一个数组

数组参数和指针参数

数组作为函数参数时会退化为指针;

为什么 C 语言中的数组参数会退化为指针?

退化的意义:

  • C 语言中只会以值拷贝的方式传递参数
  • 当向函数传递数组时:
    • 将整个数组拷贝一份传入函数(错误的,会造成空间上的浪费)
    • 将数组名看作常量指针传递数组元素的首地址(正确的,显著的提高效率不会造成空间上的浪费)

C 语言以高效作为最初的设计目标:

a) 参数传递的时候如果拷贝整个数组执行效率将大大降低

b) 参数位于栈上,太大的数组拷贝将导致栈溢出

二维数组参数

  • 二维数组参数同样存在退化的问题
    • 二维数组可以看作是一维数组
    • 二维数组中的每个元素都是一维数组
  • 二维数组参数中第一维的参数可以省略
1
2
void f(int a[5]) <--> void f(int a[]) <--> void f(int* a)
void g(int a[3][3]) <--> void g(int a[][3]) <--> void g(int (*a)[3])

一维数组 退化为 int*a,int 是数组中元素的类型

二维数组退化为 int(*a)[5],int[5] 是二维数组中元素的类型

可见,一维数组退化为指针,二维数组退化为数组指针

等价关系

shuzuzhizhencanshudengjiaguanxi

注意

  • C 语言中无法向一个函数传递任意的多维数组(接收二维参数,你就只能传递二维的)
  • 必须提供除第一维之外的所有维度的长度
    • 第一维之外的维度信息用于完成指针运算
    • N 维数组的本质是一维数组,元素是 N-1 维的数组
    • 对于多维数组的函数参数只有第一维是可变的(其他的是参数类型不可变,这话好像有问题?)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include<stdio.h>

// 只接受列数为 3 的二维数组
void access(int a[][3], int row) {
int col = sizeof(*a) / sizeof(int);
int j = 0;
int i = 0;

// sizeof(a) = 8,因为在 64 位的系统中指针大小为 8 个字节
printf("sizeof(a) = %ld\n", sizeof(a));
// sizeof(*a) = 12,因为二维数组 a 中每个元素(一维数组)占 12 个字节
printf("sizeof(*a) = %ld\n", sizeof(*a));

for(i = 0; i < row; i++) {
for(j = 0; j < col; j++) {
printf("%d\n", a[i][j]);
}
}
}

void access_ex(int b[][2][3], int n) {
int i = 0;
int j = 0;
int k = 0;

// sizeof(b) = 8
printf("sizeof(b) = %ld\n", sizeof(b));
// sizeof(*b) = 24 int[2][3] ==> 24
printf("sizeof(*b) = %ld\n", sizeof(*b));

for(i = 0; i < n; i++) {
for(j = 0; j < 2; j++) {
for(k = 0; k < 3; k++) {
printf("%d\n", b[i][j][k]);
}
}
}
}

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

access(a, 3);
/*
sizeof(a) = 8
sizeof(*a) = 12
0 1 2 3 4 5 6 7 8
*/

access(aa, 3);
/*
sizeof(a) = 8
sizeof(*a) = 12
0 0 0 0 0 0 0 0 0
*/

access_ex(b, 1); // 0 0 0 0 0 0

access_ex(aa, 1); // 0 0 0 0 0 0

return 0;
}

小结

  • C 语言中只会以值拷贝的方式传递参数
  • C 语言中的数组参数必然退化为指针
  • 多维数组必须提供除第一维之外的所有维的长度
  • 对于多维数组的函数参数只有第一维是可变的