语言深入理解指针(非常详细)(三)

简介: 语言深入理解指针(非常详细)(三)

数组名的理解

在上⼀个章节我们在使用指针访问数组的内容时,有这样的代码:

int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];

这里我们使用 &arr[0] 的方式拿到了数组第⼀个元素的地址,但是其实数组名本来就是地址,而且

是数组首元素的地址,我们来做个测试

#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 = %p\n", arr);
return 0;
}

输出结果如下:

我们发现数组名和数组首元素的地址打印出的结果⼀模⼀样,数组名就是数组首元素(第⼀个元素)的地

址。

但是我们再来看一个代码

#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%d\n", sizeof(arr));
return 0;
}

如果是首元素的地址,那么sizeof(arr)也应该是首元素的大小,那么就是4或者是8,然而结果却是40。

其实首元素地址是对的,但是这里的sizeof(arr)是整个数组的地址(只是两个地址相同而已,但是还是有区别的就像sizeof(arr)),下面是两个例外:

• sizeof(数组名),sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节

• &数组名,这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素

的地址是有区别的)

除此之外,任何地方使用数组名,数组名都表示首元素的地址。

我们再来看一段代码

#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;
}

运行结果如下:

我们可以看到&arr[0]和arr+1后与原来的地址相比较相差4个字节,而&arr+1后相差40个字节,刚好是整个数组元素的大小,因此我们可以推断,取整个元素地址时我们+1是移动整个数组的大小.

使用指针访问数组

#include <stdio.h>
int main()
{
int arr[10] = {0};
//输⼊
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
//输⼊
int* p = arr;
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,其实数组名arr和p在这里是等价的。那我们可以使用arr[i]可以访问数组的元素,那p[i]是否也可以访问数组呢

#include <stdio.h>
int main()
{
int arr[10] = {0};
//输⼊
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
//输⼊
int* p = arr;
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;
}

在第18行的地方,将* (p+i)换成p[i]也是能够正常打印的,所以本质上p[i]是等价于*(p+i)。

同理arr[i]应该等价于*(arr+i),数组元素的访问在编译器处理的时候,也是转换成首元素的地址+偏移

量求出元素的地址,然后解引用来访问的

其实在计算机处理时都是变成为*(arr+i)的形式

这种其实就类似于加法的交换性质

arr[i]=(arr+i)=(i+arr)=i[arr]

一维数组传参的本质

#include <stdio.h>
void test(int arr[])
{
int sz2 = sizeof(arr)/sizeof(arr[0]);
printf("sz2 = %d\n", sz2);
}
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int sz1 = sizeof(arr)/sizeof(arr[0]);
printf("sz1 = %d\n", sz1);
test(arr);
return 0;
}

我们发现在函数内部是没有正确获得数组的元素个数

数组名是数组首元素的地址;那么在数组传参的时候,传递的是数组名,也就是说本质上数组传参本质上传递的是数组首元素的地址

所以函数形参的部分理论上应该使用指针变量来接收首元素的地址。那么在函数内部我们写sizeof(arr) 计算的是一个地址的大小(单位字节)而不是数组的大小(单位字节)。正是因为函数的参数部分的本质是指针,所以在函数内部是没办法求的数组元素个数的

void test(int arr[])//参数写成数组形式,本质上还是指针
{
printf("%d\n", sizeof(arr));
}
void test(int* arr)//参数写成指针形式
{
printf("%d\n", sizeof(arr));//计算⼀个指针变量的⼤⼩
}
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
test(arr);
return 0;
}

运行结果也验证了一维数组的传参本质就是指针

总结:一维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式

二级指针

指针变量也是变量,是变量就有地址。

因此二级指针是可以存放一级指针的地址。(在学二级指针时我感觉有点像数学中的复合函数,函数里面又有一个函数)

我们来看看一个代码

#include<stdio.h>
int mian()
{
int a=10;
int *pa=&a;
int **ppa=&pa;
return 0;
}

我们分析一下这个代码

intp中代表p是指针变量,储存的是a的地址,而对应的a类型是整形类型,因此pa前面有一个int。
而int **p中int * 是表示pa的类型是指针变量,因此要用int
来表示pa,而第二个*则表示ppa是指针变量,储存的是pa的地址

对于二级指针的运算有:

*ppa 通过对ppa中的地址进行解引用,这样找到的是 pa , *ppa 其实访问的就是 pa

int b = 20;
*ppa = &b;//等价于 pa = &b

*ppa 先通过 *ppa 找到 pa ,然后对 pa 进行解引用操作: *pa ,那找到的是 a

**ppa = 30;
//等价于*pa = 30;
//等价于a = 30

指针数组

指针数组是指针还是数组?

我们类比一下,整型数组,是存放整型的数组,字符数组是存放字符的数组。那指针数组呢?是存放指针的数组。

我们可以看出像(###)(xxx)这样的表示,###就是存放的元素,而xxx则是存放的方式

数组指针的每个元素都是用来存放地址(指针)的((数组)(指针)就是通过指针来存放数组)

如下图:

数组指针的每个元素是地址,又可以指向一块区域

指针数组模拟二维数组

#include <stdio.h>
int main()
{
int arr1[] = {1,2,3,4,5};
int arr2[] = {2,3,4,5,6};
int arr3[] = {3,4,5,6,7};
//数组名是数组⾸元素的地址,类型是int*的,就可以存放在parr数组中
int* parr[3] = {arr1, arr2, arr3};
int i = 0;
int j = 0;
for(i=0; i<3; i++)
{
for(j=0; j<5; j++)
{
printf("%d ", parr[i][j]);
}
printf("\n");
}
return 0;
}

我们来看看运行结果

这是二维数组的运行结果

arr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型⼀维数组,parr[i][j]就是整型⼀维数

组中的元素。

上述的代码模拟出二维数组的效果,实际上并非完全是二维数组,因为每一行并非是连续的(其实就是因为在穿件数组时,因为不是同一个数组,因此中间不知道隔了多少的字节)

目录
相关文章
|
3月前
|
Go
Golang语言高级数据类型之指针篇
这篇文章详细讲解了Golang语言中的指针概念、指针地址和类型、定义指针变量、指针的细节操作、指针传值,以及内置函数new和make的用法和它们之间的区别。
33 0
|
4月前
|
存储 安全 Go
深入理解 Go 语言中的指针类型
【8月更文挑战第31天】
52 0
|
4月前
|
存储 Go 开发者
掌握Go语言中的指针
【8月更文挑战第31天】
25 0
|
6月前
|
存储 Java C#
C++语言模板类对原生指针的封装与模拟
C++|智能指针的智能性和指针性:模板类对原生指针的封装与模拟
|
6月前
|
算法 Java 程序员
面向对象编程(OOP)通过对象组合构建软件,C语言虽是过程式语言,但可通过结构体、函数指针模拟OOP特性
【6月更文挑战第15天】面向对象编程(OOP)通过对象组合构建软件,C语言虽是过程式语言,但可通过结构体、函数指针模拟OOP特性。封装可使用结构体封装数据和方法,如模拟矩形对象。继承则通过结构体嵌套实现静态继承。多态可通过函数指针模拟,但C不支持虚函数表,实现复杂。C语言能体现OOP思想,但不如C++、Java等语言原生支持。
68 7
|
6月前
|
JSON Go 数据格式
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】(4)
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】
|
6月前
|
Java 编译器 Go
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】(3)
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】
|
6月前
|
存储 安全 Go
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】(2)
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】
|
6月前
|
Java Go 索引
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】(1)
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】
|
6月前
|
Go
如何理解Go语言中的值接收者和指针接收者?
Go语言中,函数和方法可使用值或指针接收者。值接收者是参数副本,内部修改不影响原值,如示例中`ChangeValue`无法改变`MyStruct`的`Value`。指针接收者则允许修改原值,因为传递的是内存地址。选择接收者类型应基于是否需要修改参数,值接收者用于防止修改,指针接收者用于允许修改。理解这一区别对编写高效Go代码至关重要。