引言
在我的上一篇文章《C语言——指针(上)》中,我们初步了解了指针的一些基本概念。今天,我们将继续深入探讨指针的相关内容,包括二级指针以及数组与指针之间的关系。
二级指针
二级指针是指指向指针的指针。在C语言中,指针是一个变量,它存储了内存中另一个变量的地址。而二级指针则是指向指针变量本身的指针。
简单来说,如果有一个指针变量,比如 int* ptr,那么 ptr 存储的是某个整数变量的地址。而如果有一个指向指针变量的指针,比如 int** pptr ;
,那么pptr存储的是指向 int* 类型的指针变量的地址。
比如这样:
int main()
{
int a = 10;
int* pa = &a;
int** ppa = &pa;
return 0;
}
这样,变量 a 存储了整数值 10,指针 pa 存储了变量 a 的地址,而指针 ppa 存储了指针 pa 的地址。
由此还能衍生出三级指针、四级指针
数组与指针的关系
1.数组名的理解
我们来看看这段代码:
#include<stdio.h>
int main()
{
int arr[] = { 0,1,2,3,4,5,6,7,8,9 };
printf("&arr[0]=%p\n", &arr[0]);
printf("&arr =%p\n", &arr);
return 0;
}
输出结果为:
我们可以看到,数组名和数组首元素的地址是一样的,数组名就是数组首元素的地址
但是如果说数组名是数组首元素的地址,那要如何解释下面的代码?
#include <stdio.h>
int main()
{
int arr[] = { 0,1,2,3,4,5,6,7,8,9 };
printf("%d\n", sizeof(arr));
return 0;
}
输出结果为:40
如果说arr是数组首元素的地址,拿输出结果应该是4/8才对,这是为什么呢?
我们需要注意这两点:
sizeof(数组名),sizeof中单独放数组名,这里的数组名表示的是整个数组,计算的是整个数组的大小,单位是字节
&数组名,这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址是不同的)
除此之外,其他地方使用数组,数组名都表示数组首元素的地址
我们再来看看这么一段代码:
#include <stdio.h>
int main()
{
int arr[] = { 0,1,2,3,4,5,6,7,8,9 };
printf("&arr[0]=%p\n", &arr[0]);
printf("arr =%p\n", arr);
printf("&arr =%p\n", &arr);
return 0;
}
结果如下:
欸?我们发现这三个结果一致,那么&arr[0]、arr和&arr到底有什么区别呢
我们再来看看这一段代码:
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);
printf("&arr[0]+1 = %p\n", &arr[0] + 1);
printf("arr = %p\n", arr);
printf("arr+1 = %p\n", arr + 1);
printf("&arr = %p\n", &arr);
printf("&arr+1 = %p\n", &arr + 1);
return 0;
}
输出结果为:
我们来找找规律:
1.&arr[0]和&arr[0]+1相差4个字节,相当于跳过一个整型元素
2.&arr和&arr+1相差40个字节,相当于十个元素,也就是整个数组
2.使用指针访问数组
看到这里,我们可以尝试使用指针来访问数组了
代码如下:
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr;
int i = 0;
//输入
for (i = 0; i < sz; i++)
{
scanf("%d", p + i);
//scanf("%d",arr+i); 也可以这样子写
}
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
我们分析一下:数组名arr是首元素的地址,可以赋值给p,在这里数组名和p是等价的。既然如此,我们可以使用arr[i]来访问数组元素,是否意味着我们可以使用p[i]来访问数组元素呢?
代码如下:
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* p = arr;
int i = 0;
//输入
for (i = 0; i < sz; i++)
{
scanf("%d", p + i);
//scanf("%d",arr+i);
}
for (i = 0; i < sz; i++)
{
printf("%d ", p[i]);
}
return 0;
}
这样子也是可以正常打印的,因此我们推断:p[i]等价于*(p+i),也等价于*(arr+i),数组元素的访问在编译器处理时,是转换成首元素的地址+偏移量求出元素的地址,然后解引用访问
3.指针数组
3.1 指针数组的概念
指针数组是一个数组,其元素都是指针,存放的都是地址。
int* parr1[5];//存放五个整型指针变量
char* parr2[5];//存放五个字符指针变量
float* parr3[5];//存放五个浮点数指针变量
拿int* parr[5]举例:
指针数组的每个元素都是地址,而每个元素又可以指向内存中的某块区域
3.2 指针数组的理解
来看看这段代码:
int main()
{
int arr1[] = { 1, 2, 3 }; // 定义一个包含3个整数的数组arr1
int arr2[] = { 4, 5, 6 }; // 定义一个包含3个整数的数组arr2
int arr3[] = { 7, 8, 9 }; // 定义一个包含3个整数的数组arr3
int* parr[3] = { arr1, arr2, arr3 }; // 定义一个指针数组,每个元素指向一个整数数组
printf("%p\n", parr); // 打印指针数组的地址,即parr的地址
printf("%p\n", parr[0]); // 打印第一个元素(arr1)的地址
printf("%p\n", *parr); // 打印指针数组第一个元素(arr1)的地址,与上一行结果相同
printf("%d\n", **parr); // 打印指针数组第一个元素(arr1)的第一个元素的值,即arr1[0],结果
// 为1
printf("%d\n", *parr[0]); // 打印指针数组第一个元素(arr1)的第一个元素的值,与上一行结果相
// 同,即arr1[0],结果为1
printf("%d\n", *parr[1]); // 打印指针数组第二个元素(arr2)的第一个元素的值,即arr2[0],结果
// 为4
return 0;
}
输出结果为:
001BF9D4
001BFA10
001BFA10
1
1
4
3.3 指针数组模拟二维数组
通过我们对指针数组的分析,现在我们可以来尝试一下用指针数组模拟二维数组
#include <stdio.h>
int main()
{
int arr1[] = { 1, 2, 3, 4 };
int arr2[] = { 2, 3, 4, 5 };
int arr3[] = { 3, 4, 5, 6 };
// 定义一个指针数组,每个元素指向一个一维数组
int* arr[] = { arr1, arr2, arr3 };
// 访问指针数组中的元素,模拟二维数组的访问
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
务必注意!!!这并不是真正的二维数组,真正的二维数组在内存中是连续存放的,模拟的二维数组内存并不连续
4.数组指针
4.1 数组指针的概念
数组指针是指向数组的指针。它是一种特殊类型的指针,可以指向数组的首地址。与普通指针不同的是,数组指针可以对整个数组进行操作,而不仅仅是数组的单个元素。
int (*parr1)[5]; //指向一个有五个元素的整型数组
char (*parr2)[5]; //指向一个有五个元素的字符数组
float (*parr3)[5]; //指向一个有五个元素的浮点数数组
4.2 数组指针的理解
int main()
{
int arr[5] = { 1, 2, 3, 4, 5 }; // 定义一个包含5个整数的数组arr
int(*parr)[5] = &arr; // 定义一个指向包含5个整数的数组arr的指针parr
// 对数组名取地址代表整个数组的地址
printf("%p\n", parr); // 打印指向数组arr的指针parr的地址
printf("%p\n", parr[0]); // 打印parr指向的数组arr的地址,与上一行结果相同
printf("%p\n", *parr); // 打印parr指向的数组arr的地址,与上一行结果相同
printf("%d\n", **parr); // 打印parr指向的数组arr的第一个元素的值,即arr[0],结果为1
printf("%d\n", *parr[0]); // 打印parr指向的数组arr的第一个元素的值,与上一行结果相
// 同,即arr[0],结果为1
printf("%d\n", *parr[1]); // 等价于*(*(parr+1)),parr+1跳过一个数组大小的地址,越界访
// 问
return 0;
}
输出结果为:
0051FC2C
0051FC2C
0051FC2C
1
1
-858993460(越界访问,生成随机数)
5.字符指针
字符指针是一个指向字符变量的指针,它可以用来存储和操作字符型数据的内存地址。在C语言中,字符指针常用于处理字符串,因为字符串在内存中是以字符数组的形式存储的,而字符指针可以指向字符串的首字符,从而实现对整个字符串的访问和操作。
5.1 字符指针的基本概念
字符指针是一个变量,其值是一个内存地址,该地址指向一个字符型变量。通过字符指针,我们可以间接访问和修改该内存地址所指向的字符型数据。
在声明字符指针时,我们通常使用 char * 作为类型说明符。例如:
char *ptr;
这里, ptr 是一个字符指针变量,它可以存储一个字符变量的内存地址。
5.2 字符指针的理解
在C语言中,字符串是以字符数组的形式存储的。字符指针常用于指向字符串的首字符,从而实现对字符串的访问和操作。
例如:
int main()
{
char str[] = "Hello, world!";
char* p = str;;
printf("%s\n", p);
return 0;
}
在这个例子中, str 是一个字符数组,用于存储字符串 "Hello, world!"。s是一个字符指针,它指向 str 数组的首字符 'H'。通过 p ,我们可以访问和修改 str 数组中的字符。
我们再来看看这个例子:
int main()
{
char str1[] = "Hello, world!";
char str2[] = "Hello, world!";
const char* str3 = "Hello, world!";
const char* str4 = "Hello, world!";
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
输出结果为:
这是为什么呢?
这是因为:
str1和
str2` 都是字符数组,它们分别存储了字符串 "Hello, world!" 。由于它们是数组,它们在内存中占据不同的空间,即使它们的内容相同。
str3和str4指向的是⼀个同⼀个常量字符串。
5.字符指针
字符指针是一个指向字符变量的指针,它可以用来存储和操作字符型数据的内存地址。在C语言中,字符指针常用于处理字符串,因为字符串在内存中是以字符数组的形式存储的,而字符指针可以指向字符串的首字符,从而实现对整个字符串的访问和操作。
5.1 字符指针的基本概念
字符指针是一个变量,其值是一个内存地址,该地址指向一个字符型变量。通过字符指针,我们可以间接访问和修改该内存地址所指向的字符型数据。
在声明字符指针时,我们通常使用 char * 作为类型说明符。例如:
char *ptr;
这里, ptr 是一个字符指针变量,它可以存储一个字符变量的内存地址。
5.2 字符指针的理解
在C语言中,字符串是以字符数组的形式存储的。字符指针常用于指向字符串的首字符,从而实现对字符串的访问和操作。
例如:
int main()
{
char str[] = "Hello, world!";
char* p = str;;
printf("%s\n", p);
return 0;
}
在这个例子中, str 是一个字符数组,用于存储字符串 "Hello, world!"。s是一个字符指针,它指向 str 数组的首字符 'H'。通过 p ,我们可以访问和修改 str 数组中的字符。
我们再来看看这个例子:
int main()
{
char str1[] = "Hello, world!";
char str2[] = "Hello, world!";
const char* str3 = "Hello, world!";
const char* str4 = "Hello, world!";
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
输出结果为:
这是为什么呢?
这是因为:
str1和
str2` 都是字符数组,它们分别存储了字符串 "Hello, world!" 。由于它们是数组,它们在内存中占据不同的空间,即使它们的内容相同。
str3和str4指向的是⼀个同⼀个常量字符串。
5.字符指针
字符指针是一个指向字符变量的指针,它可以用来存储和操作字符型数据的内存地址。在C语言中,字符指针常用于处理字符串,因为字符串在内存中是以字符数组的形式存储的,而字符指针可以指向字符串的首字符,从而实现对整个字符串的访问和操作。
5.1 字符指针的基本概念
字符指针是一个变量,其值是一个内存地址,该地址指向一个字符型变量。通过字符指针,我们可以间接访问和修改该内存地址所指向的字符型数据。
在声明字符指针时,我们通常使用 char * 作为类型说明符。例如:
char *ptr;
这里, ptr 是一个字符指针变量,它可以存储一个字符变量的内存地址。
5.2 字符指针的理解
在C语言中,字符串是以字符数组的形式存储的。字符指针常用于指向字符串的首字符,从而实现对字符串的访问和操作。
例如:
int main()
{
char str[] = "Hello, world!";
char* p = str;;
printf("%s\n", p);
return 0;
}
在这个例子中, str 是一个字符数组,用于存储字符串 "Hello, world!"。s是一个字符指针,它指向 str 数组的首字符 'H'。通过 p ,我们可以访问和修改 str 数组中的字符。
我们再来看看这个例子:
int main()
{
char str1[] = "Hello, world!";
char str2[] = "Hello, world!";
const char* str3 = "Hello, world!";
const char* str4 = "Hello, world!";
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
输出结果为:
这是为什么呢?
这是因为:
str1和
str2` 都是字符数组,它们分别存储了字符串 "Hello, world!" 。由于它们是数组,它们在内存中占据不同的空间,即使它们的内容相同。
str3和str4指向的是⼀个同⼀个常量字符串。在 Visual Studio(VS)中,字符串字面量的存储行为确实可能因编译器的实现和优化策略而有所不同。通常情况下,Visual Studio 的编译器会尝试将相同的字符串字面量合并到同一个内存位置,以减少内存占用和提高效率。
所以str1和str2不同,str3和str4相同。
所以str1和str2不同,str3和str4相同。
6.数组传参
6.1 一维数组传参
首先,数组是可以传递给函数的,例如:
void print(int arr[], int sz)//写成数组的形式
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
printf("%d",sz);
}
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
int sz = sizeof(arr) / sizeof(arr[0]);
print(arr, sz);
return 0;
}
输出结果为:
0 1 2 3 4 5 6 7 8 9
10
这里我们是在main函数求元素的个数然后将sz传递给print函数,那么我们可以把函数传递给函数后,然后在函数内部求数组的元素个数吗?
我们来尝试一下:
void print(int arr[])
{
int sz = sizeof(arr) / sizeof(arr[0]);
printf("%d\n", sz);
}
int main()
{
int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
print(arr);
return 0;
}
我们运行之后发现输出结果为1,这显然没能正确计算出数组的元素个数
这是为什么呢?
在之前的学习,我们知道:数组名是数组首元素的地址;在数组传参中,传递的是数组名,也就是数组传参传递的是数组首元素的地址
当数组作为函数参数传递时,它本质上就退化为一个指向数组首元素的指针。这意味着,在函数内部,你实际上只获得了一个地址,而没有获得整个数组的信息。
所以函数形参的部分理论上应该使用指针变量来接收⾸元素的地址。在函数内部我们使用sizeof(arr)计算的是一个地址的大小而不是数组的大小。正因为函数的参数部分是指针,所以在函数内部是无法求出数组的元素个数的
6.2 二维数组传参
接下来我们来讲讲二维数组传参的本质。
我们来看看一般二维数组是如何传参的:
void print(int arr[3][5])
{
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 5; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1, 2, 3, 4, 5}, { 3,4,5,6,7}, { 5,6,7,8,9 } };
print(arr);
return 0;
}
这里实参是二维数组,形参也写成二维数组的形式,那还有其他写法吗?
我们来分析一下二维数组:
二维数组,我们可以想象成一个大的表格,就像我们平时用的Excel表格那样。这个大表格里的每一行都是一个小数组(一维数组),而二维数组的第一个元素就是这个大表格的第一行。
所以,根据数组名是数组首元素的地址这个规则,二维数组的数组名表示的就是第一行的地址,是一维数组的地址,根据上面的例子,第一行的一维数组的类型就是int[5],所以第一行的地址类型就是数组指针类型int(*)[5]。这就意味着二维数组传参的本质是地址的传递,传递的是数组首元素的地址,即第一行的地址。
那么我们可以尝试写个新方法实现二维数组传参
代码如下:
void print(int (*p)[5],int x,int y)
{
int i = 0;
for (i = 0; i < x; i++)
{
int j = 0;
for (j = 0; j < y; j++)
{
printf("%d ", *(*(p+i)+j));
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1, 2, 3, 4, 5}, { 3,4,5,6,7}, { 5,6,7,8,9 } };
print(arr, 3, 5);
return 0;
}
总结:二维数组传参,形参部分可以写成数组,也可以写成指针的形式。
结束语
这是指针内容的第二篇,再接再厉!!!
希望看到这篇文章的家人们能点点关注,赞赞和收藏!!!
十分感谢!!!