• C/C++多级指针与多维数组


    使用指针访问数组

    指针类型的加减运算可以使指针内保存的首地址移动。
    指针类型加n后。首地址向后移动 n * 步长 字节。
    指针类型减n后。首地址向前移动 n * 步长 字节。
    步长为指针所指向的类型所占空间大小。
    例如:

    int *p = (int *)100;
    
    • 1
    • p + 1,结果为首地址向后移动sizeof(int)字节,即104。
    • p - 1,结果为首地址向前移动sizeof(int)字节,即96。

    因此,指针加减运算对于访问在内存中连续排布的数据对象非常方便。
    而数组这种数据对象,每个元素在内存中一定是连续排布的。下面,我们来探究怎样使用指针访问数组。

    使用第一个元素获取数组首地址

    既然数组元素在内存中的存储可以保证是连续的,那么第一个元素的首地址,就是整个数组的首地址。

    #include 
    int main()
    {
    	int arr[5] = { 111, 222, 333, 444, 555 };
    	int* p = &arr[0];
    	printf("%d\n", *p); // 第1个元素
    	printf("%d\n", *(p + 1)); // 第2个元素
    	printf("%d\n", *(p + 2)); // 第3个元素
    	printf("%d\n", *(p + 3)); // 第4个元素
    	printf("%d\n", *(p + 4)); // 第5个元素
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    我们可以使用取地址运算符&,获取第一个元素的首地址和空间大小,即获取一个 int * 类型的指针。
    通过取值运算符*,可以使用指针中的首地址和空间大小访问或修改目标数据对象。

    表达式 p + 1 必须先被括号包裹,再使用取值运算符*。

    这是因为取值运算符*的优先级高于算术运算符。
    我们需要先让首地址移动,再进行取值操作。
    若不使用括号,*p会先被取值,之后值再被加1。

    • 不使用括号:
      • *p的值为111,*p + 1的结果为112。
    • 使用括号:
      • (p + 1) 使得首地址移动到第二个元素, *(p + 1) 得到结果为222。

    使用数组名获取数组首地址

    #include 
    int main()
    {
    	int arr[5] = { 111, 222, 333, 444, 555 };
    	int* p = arr;
    	printf("sizeof arr = %d\n", sizeof(arr));
    	printf("sizeof p = %d\n", sizeof(p));
    	printf("sizeof arr + 1 = %d\n", sizeof(arr + 1));
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    使用32位进行编译

    sizeof arr = 20
    sizeof p = 4
    sizeof arr + 1 = 4
    
    • 1
    • 2
    • 3

    arr 的大小为20。
    p 的大小为4。
    arr + 1 的大小为4。
    p 是一个指针大小为4是理所当然的。
    但是 arr 的大小为20,那么arr应该不是一个指针类型才对,但是它却又可以成功赋值给 int * 。
    而 arr + 1 的大小却又为4。

    类型为“以T为元素的数组arr”与“指向T的指针p”的关系。

    当数组名arr出现在一个表达式当中,数组名arr将会被转换为指向数组第一个元素的指针。但是,这个规则有两个例外:

    • 对数组名arr使用sizeof时。
    • 对数组名arr使用&时。

    也就是说,数组名arr的类型其实是 int [5] ,因此 sizeof(arr) 的结果才会是20。
    数组名arr出现在表达式int* p = arr中,会被转换为指向数组第一个元素的指针,即 int [5] 转为 int * 类型。之后进行赋值运算。
    arr + 1 也是一个表达式,数组名 arr 被转换为 int * 类型,进行加法运算后,仍然为 int * 类型。

    使用指针访问数组等价于下标访问

    现在我们学会了访问数组元素的两种办法:

    • 数组名[下标]
    • *(数组名 + 偏移量)

    其中,偏移量就是指针指向的地址与数组首地址之间相差几个元素。
    事实上,这两种形式是等价的。
    中括号 [] ,被称作下标运算符,它的优先级高于一切其他运算符。通常的形式为:

    A[k]
    
    • 1

    而表达式运算时,最终会将下标运算符展开为:

    *(A + k)
    
    • 1

    测试一下

    #include 
    int main()
    {
    	int arr[5] = { 111, 222, 333, 444, 555 };
    	printf("arr[2] = %d\n", arr[2]);
    	printf("2[arr] = %d\n", 2[arr]);
    	printf("*(arr + 2) = %d\n", *(arr + 2));
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • arr[2] 展开为 *(arr + 2) 。
    • 2[arr] 展开为 *(2 + arr) 。

    因此,使用指针访问数组等价于下标访问。

    指针作为参数传递

    形参与实参相互独立

    #include 
    void swap(int x, int y)
    {
    	// 打印x,y的首地址
    	printf("&x= %u\n", &x);
    	printf("&y= %u\n", &y);
    	int temp = x;
    	x = y;
    	y = temp;
    }
    int main()
    {
    	int a, b;
    	int temp;
    	a = 1;
    	b = 2;
    	// 打印a,b的首地址
    	printf("&a= %u\n", &a);
    	printf("&b= %u\n", &b);
    	// 交换a,b变量
    	swap(a, b);
    	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

    分析过程

    {% gallery::::one %}


    {% endgallery %}

    将指针作为参数传递

    #include 
    void swap(int* x, int* y)
    {
    	int temp = *x;
    	*x = *y;
    	*y = temp;
    }
    int main()
    {
    	int a, b;
    	int temp;
    	a = 1;
    	b = 2;
    	printf("a=%d b=%d\n", a, b);
    	// 交换a,b变量
    	swap(&a, &b);
    	printf("a=%d b=%d\n", a, b);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    不是交换指针x,y的值,而是交换目标数据对象a,b的值。所以,需要在指针前使用取值运算符*

    为何在使用 scanf 函数时,需要对变量先取地址再传入参数

    int n;
    scanf("%d", &n);
    
    • 1
    • 2

    scanf 会从读取从键盘的输入,转换后存储到变量n当中。被调函数 scanf 无法直接修改在主调函数中的变量n。因此,我们将变量n的指针传入 scanf 函数。通过指针使得被调函数间接地修改主调函数中的变量

    指针不仅仅是首地址

    再次强调,指针内保存的不仅仅是目标数据对象首地址,指针的类型也非常重要。
    要在内存中找到一个数据对象,需要有以下两个信息。

    • 数据对象的首地址。
    • 数据对象占用存储空间大小。

    指针的值保存着数据对象首地址,指针类型对应着目标数据对象的类型,用于标记目标数据对象的空间大小和指针运算时的步长。

    仅有首地址的指针类型void *

    由于指针类型定死了指针所指向的数据类型。为了让函数可以交换更多的数据类型,我们仅需要指针类型中保存的首地址,目标数据大小通过额外的参数传入。

    • 不同指针类型不能相互赋值,相互赋值后会造成目标数据对象类型的改变,无法通过编译。
    • void* 类型为特例,它可以接受任意指针类型的赋值,也可以赋值给任意类型的指针。
    void swap(void* x, void* y, int size)
    {
    	// 指针转为char *,单个字节操作内存
    	char* pX = (char*)x;
    	char* pY = (char*)y;
    	char temp;
    	for (int i = 0; i < size; i++)
    	{
    		temp = pX[i];
    		pX[i] = pY[i];
    		pY[i] = temp;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    由于 void _ 不能取值和加减,所以我们将其转换为 char _ 。 char * 可以提供单个单个操作内存的能力。
    在C语言中 void *类型不但可以接受任意类型的指针,也可以自动转换为任意类型的指针。
    但在C++中,规则稍微严格了一点, void * 仅能接受任意类型的指针,不能自动转换为其他类型的指针。为了保证代码的兼容性,我们将 void * 强制转为 char * ,避免在C++中编译出错。

    char *pX = (char *)x;
    char *pY = (char *)y;
    
    • 1
    • 2

    多级指针与指针数组

    int * 的指针的类型为 int **

    int **p; // 正确
    int**p; // 正确
    int* *p; // 正确
    int * *p; // 正确
    int * * p; // 正确
    
    • 1
    • 2
    • 3
    • 4
    • 5

    二级指针为例

    #include 
    int main()
    {
    	int n = 123;
    	int* pn = &n;
    	int** pnn = &pn;
    	printf("**pnn = %d\n", **pnn);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    取地址过程

    • 对n使用取地址运算符,获得n的指针pn,类型为 int * 。
    • 对pn使用取地址运算符,获得pn的指针pnn,类型为 int ** 。

    取值过程

    • 对pnn使用取值运算符,将 int ** 还原为 int * 。
    • 对_pnn使用取值运算符,将 int _ 还原为 int 。即,还原为n。

    指针数组

    p ,指向 pToArr 的第一个元素,类型为 int ** 。
    *p ,指向 arr1 的第一个元素,类型为 int * 。
    *p + j ,指向 arr1 中的第j个元素,类型为 int * 。
    *(*p + j) ,为 arr1 中的第j个元素。
    
    • 1
    • 2
    • 3
    • 4

    多维数组名与指针

    数组指针的移动

    #include 
    int main()
    {
    	int b[5][10] =
    	{
    		{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,43,44,45,46,47,48,49},
    	};
    	int(*pInt10)[10] = b;   // int[5][10]转为int (*)[10]
    	int(*pInt) = *pInt10;   // *pInt10从int[10]转换为int *
    	printf("pInt[0]=%d\n", pInt[0]);    // 等价于*(pInt + 0)
    	printf("pInt[1]=%d\n", pInt[1]);    // 等价于*(pInt + 1)
    	printf("pInt[2]=%d\n", pInt[2]);    // 等价于*(pInt + 2)
    	printf("pInt[3]=%d\n", pInt[3]);    // 等价于*(pInt + 3)
    	printf("pInt[4]=%d\n", pInt[4]);    // 等价于*(pInt + 4)
    	printf("pInt[5]=%d\n", pInt[5]);    // 等价于*(pInt + 5)
    	printf("pInt[6]=%d\n", pInt[6]);    // 等价于*(pInt + 6)
    	printf("pInt[7]=%d\n", pInt[7]);    // 等价于*(pInt + 7)
    	printf("pInt[8]=%d\n", pInt[8]);    // 等价于*(pInt + 8)
    	printf("pInt[9]=%d\n", pInt[9]);    // 等价于*(pInt + 9)
    	printf("pInt[10]=%d\n", pInt[10]);  // 等价于*(pInt + 10)
    	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

    另一种结果相同的表达

    #include 
    int main()
    {
    	int b[5][10] =
    	{
    		{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,43,44,45,46,47,48,49},
    	};
    	int(*pInt10)[10] = b;   // int[5][10]转为int (*)[10]
    	printf("*pInt10[0]=%d\n", (*pInt10)[0]);    // pInt10先从int(*)[10]转为int *,再通过下标访问
    	printf("*pInt10[1]=%d\n", (*pInt10)[1]);
    	printf("*pInt10[2]=%d\n", (*pInt10)[2]);
    	printf("*pInt10[3]=%d\n", (*pInt10)[3]);
    	printf("*pInt10[4]=%d\n", (*pInt10)[4]);
    	printf("*pInt10[5]=%d\n", (*pInt10)[5]);
    	printf("*pInt10[6]=%d\n", (*pInt10)[6]);
    	printf("*pInt10[7]=%d\n", (*pInt10)[7]);
    	printf("*pInt10[8]=%d\n", (*pInt10)[8]);
    	printf("*pInt10[9]=%d\n", (*pInt10)[9]);
    	printf("*pInt10[10]=%d", (*pInt10)[10]);
    	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

    输出结果

    *pInt10[0]=0
    *pInt10[1]=1
    *pInt10[2]=2
    *pInt10[3]=3
    *pInt10[4]=4
    *pInt10[5]=5
    *pInt10[6]=6
    *pInt10[7]=7
    *pInt10[8]=8
    *pInt10[9]=9
    *pInt10[10]=10
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    由于下标运算符[]的优先级比取值运算符*的优先级高。我们想要pInt10先从int (*)[10]转为int *,再通过下标访问。所以,需要用括号让取值先进行。

    如果数组名B出现在表达式中,会从int[5][10]转为int (*)[10]
    *B又可以看作*(B + 0),所以*B等价于B[0]

    对数组取地址

    当数组名arr出现在一个表达式当中,数组名arr将会被转换为指向数组首元素的指针。但是,这个规则有两个例外:

    • 对数组名arr使用sizeof时。
    • 对数组名arr使用&时。

    现在开始讨论第二个例外。

    int arr[10];
    &arr;
    
    • 1
    • 2

    arr的类型为int[10],而对数组使用取地址运算符&。触发第二个例外,数组不会进行类型转换,而是直接取地址

    • int取地址为int (*)类型的指针。
    • int[10]取地址为int (*)[10]类型的指针。
    #include 
    int main()
    {
    	int arr[10];
    	int(*pInt10)[10] = &arr;
    	printf("pInt10=%u\n", pInt10);
    	printf("pInt10+1=%u\n", pInt10 + 1);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    pInt10是类型int (*)[10]类型的数组指针,步长为40。

    pInt10=7601480
    pInt10+1=7601520
    
    • 1
    • 2

    如果再对pInt10取地址呢?

    • int[10]类型的数组取地址为int (*)[10]类型的数组指针,它指向int[10]的数组。
    • int (*)[10]类型的数组指针取地址为int (**)[10]的二级指针,它指向int(*)[10]的指针。

    注意&&arr是不对的,&arr确实可以获得一个指针。但是,这个指针是一个临时数据对象,应当将其赋值给变量才能保存它的值。

    指针与三维数组示例

    #include 
    int main()
    {
    	int S[2][5][10] = {
    	{{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, 43, 44, 45, 46, 47, 48, 49}},
    	{{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, 43, 44, 45, 46, 47, 48, 49}}
    	};
    	// 访问元素S[1][2][3]
    	printf("S[1][2][3] = %d", *(*(*(S + 1) + 2) + 3));
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    分析

    S + 1 类型为 int(*)[5][10] 的指针。
    *(S + 1) 类型为 int[5][10] 的数组。
    *(S + 1) + 2 类型为 int(*)[10] 的指针。
    *(*(S + 1) + 2) 类型为 int[10] 的数组。
    *(*(S + 1) + 2) + 3 类型为 int(*) 的指针。
    *(*(*(S + 1) + 2) + 3) 类型为 int 的整型。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    {% gallery::::one %}






    {% endgallery %}

    也可以故意将表达式结果赋值给一个无法转换的变量。让报错信息告诉我们表达式结果具体的类型

    验证


    指针的大小为4,整型的大小为4。
    int[5][10] 数组的大小为200。
    int[10] 数组的大小为40。

    对数组取地址

    int[2][5][10] 取地址为 int (*)[2][5][10] 类型的指针。

    多级指针应用

    从函数中返回指针

    return关键词可以从被调函数中返回一个值到主调函数。
    现在我们尝试让它返回一个指针到主调函数中。

    #include 
    int* func()
    {
    	int n = 100;
    	return &n;
    }
    int main()
    {
    	int* p = func();
    	printf("%d\n", *p);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    我们在函数 func 中定义了变量n。接着return &n;取得变量n的指针,并返回到main函数。
    main函数收到返回值后赋值给p,并使用指针p来访问变量n。

    这个程序看似正确,并且可以通过编译。但是,却存在潜在问题。
    这是因为函数结束后,函数内部的变量也会被回收。所以,变量 n 已经失效了。再去访问它有可能正常,也有可能得到一些无意义的值或者引发错误。
    这样设计的原因是因为函数与函数之间的变量是独立的,即使是同一个函数多次运行,这些变量也是独立的。在函数返回后,函数内的变量没有继续存在的意义了。所以,函数内的变量将被回收,回收后的内存空间将给接下来运行的函数使用。
    如果不想让变量被回收,那么可以在变量前加上关键词**static**

    int* func()
    {
    	static int n = 100; // 关键词static让变量n不被回收
    	n++; // 变量n自增
    	return &n;
    }
    #include 
    int main()
    {
    	int* p = func();
    	printf("%d\n", *p);
    	func();
    	printf("%d\n", *p);
    	func();
    	printf("%d\n", *p);
    	func();
    	printf("%d\n", *p);
    	func();
    	printf("%d\n", *p);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    现在函数 func 结束后,变量n不会被回收了。并且,重复调用func函数,使用的是同一个地址上的变量n
    因此,我们只需获取一次变量n的地址,即可观察到变量n每次调用函数都被自增。

    从函数中返回多个变量

    将指针的指针,也就是二级指针作为参数传入函数。即可让被调函数“返回”多个指针。

    void func(int** a, int** b)
    {
    	static int x = 100;
    	static int y = 200;
    	*a = &x;
    	*b = &y;
    }
    #include 
    int main()
    {
    	// 两个指针,初始化为空
    	int* a = NULL;
    	int* b = NULL;
    	func(&a, &b); // 将指针的指针传入被调函数
    	if (a != NULL && b != NULL)
    		printf("a=%d b=%d\n", *a, *b);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在main函数中,声明两个指针并把它们初始化为 NULL
    **NULL**** 是一个由 **#define NULL 0** 定义的符号常量。**
    将指针初始化为NULL,也就是将指针内保存的地址设置为0。
    让指针初始化为零是一个非常好的编码习惯。
    一般结合指针判空,即 if (a != NULL && b != NULL) ,来判断指针是不是有一个正确的指向了。
    调用函数 func 后,两个指针均被修改为有效指针,即非0。
    我们通过判断指针是不是非零来确定函数 func 已经给指针赋值了。
    若指针仍然为0,则说明函数 func 并未给指针赋值,不可以使用没有明确指向的指针。
    函数 func 内部, &x 、 &y 取得变量 x 、 y 的指针,类型为 int *
    在被调函数内,为了修改主调函数中的变量,先对二级指针 a、b 取值,将 int ** 转换为 int * ,再赋
    值一个 int 给它。
    类似于使用一级指针作为参数时,先对一级指针 a、b 取值,将 int * 转换为 int ,再赋值一个 int 给它。

  • 相关阅读:
    【Apache Hudi】一种基于增量日志文件数的压缩策略
    windows redis安装与开机自启动
    pycharm虚拟环境安装指定python版本/ python3.8 / 从python3.9降级到3.8
    windows系统一键开启和关闭虚拟化
    C#窗体设计SaveFileDialog的用法
    绘制核密度估计图
    微信小程序 —— 基本结构
    vue3+element-plus权限控制实现(el-tree父子级不关联情况处理)
    第四次考核 Jimmy 学徒考核 Linux安装软件 rnaseq上游分析-2 ascp kingfisher数据下载ena
    evnoy协议转换关键日志
  • 原文地址:https://blog.csdn.net/m0_49303993/article/details/134521613