最新C语言篇+ 指针进阶(上)

目录

一、前言

在上一篇博客中作者已将指针的基础知识归纳在这篇博客中,《 指针和结构体(初级)》,而今天我们要进入指针的下一个环节,指针进阶!!为了怕大家忘记之前讲过的知识,本人再次梳理出一些知识,请各位耐心观看,以下又是老调重弹

  1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
  2. 指针的大小是固定的4/8个字节(32位平台/64位平台)。
  3. 指针是有类型,指针的类型决定了指针的±整数的步长,指针解引用操作的时候的权限。
  4. 指针的运算。

1.1进入主题

#define \_CRT\_SECURE\_NO\_WARNINGS
#include <stdio.h>
#include<assert.h>
#include <stdio.h>

int main() 
{
	char \*p = NULL;
	p = "hello word";
	return 0;
}

在这里插入图片描述

进入正题,我们都知道指针的大小(所占用的字节),是跟操作系统有关的,指针的大小是固定的4/8个字节(32位平台/64位平台),而字符在内存中是占用一个字节的,那么上面的代码是将整个的 hello word 存放到字符指针中去,还是只存放字符串的第一个字符呢?
在这里插入图片描述
在内存窗口中我们可以看到指针变量p存放的恰好就是字符串的首字符的地址,直到后面的 00 00 ,前面的都是有效字符,试想字符在内存中是占一个字节的,如果要把整个字符串存放到一个指针变量中去,那不就得溢出了吗,所以存放的是首字符的地址,字符串在内存中是挨着存的,有了首字符的地址不难找到其他字符串内容,另外值得一提的一点就是字符串是存放在字符串常量区的,既然是一个常量,那么就不可以通过别的方式对其修改,否则就会引发程序出错,如果不小心修改了怎么办呢,建议在前面加const,这样即使你想改,编译器也会编译不过去
在这里插入图片描述

1.2牛刀小试

你见过这样的一道题吗?

#include <stdio.h>
int main()
{
    char str1[] = "hello word.";
    char str2[] = "hello word.";
    const char \*str3 = "hello word.";
    const char \*str4 = "hello word.";
    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; 
}

请把你心目中的答案,在心里默默的讲一句
没错我想你应该知道了,但是这里还是得再说一遍

第一个if语句中比较的是两个地址,因为数组名是字符首地址,那么既然是不同的数组名,想必在内存中的存放位置也是不一样的,即使在内存中存放的值都是 hello word ,但是在比较的时候还是比较的地址,试想不是在同一块内存开辟的同一块空间,他们的地址会一样吗?显然不是,而在 str3 和 str4比较的时候比较的也是地址,并且这两个指针变量指向的都是一块空间,既然是指向同一块空间,那么指向的就是字符首地址,因为在内存中压根就不会存放两份一样的数据,我们都知道字符串是常量不能被修改,既然是不可变的那么被两个指针去指向这一份不会被修改的内容又有什么关系呢而且还不会浪费内存,所以在这个地方比较的是同一个地址,当我们的程序执行起来,可以看到和我们的预期结果是一样的
在这里插入图片描述
结论:
这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针。指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始
化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4不同

二、指针数组

指针数组的主体是一个数组,数组中存放的每一个元素是一个指针

int\* arr1[10]; //整形指针的数组
char \*arr2[4]; //一级字符指针的数组
char \*\*arr3[5];//二级字符指针的数组

这么理解指针数组的语法:

就拿arr1来说,[ ]的优先级是最高的,所以[ ]会先和arr1结合,那么arr1表示的是一个数组,而数组的元素类型是什么,int * 表示的就是数组的元素类型,所以arr1是一个每个元素都是int *的指针数组

1.1指针数组的初始化

int main()
{
	int a = 10;
	int b = 20;
	int c = 30;
	int d = 40;
	
	int \*arr[4] = {&a,&b,&c,&d};
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		printf("%d ", \*(arr[i]));
	}
	return 0;
}

数组是连续存储的,而指针数组的每一个元素都是一个指针变量,并且这个指针变量指向的元素类型是int类型,所以在初始化的时候是将变量的地址存入到数组中,以下就是指针数组在内存中的布局,而打印的过程也很简单,通过数组下标索引数组中的每一个元素,元素对应变量的地址,对地址解引用就能找到此地址对应的变量值
在这里插入图片描述

1.2指针数组存放一维数组的地址

int main() 
{
	int arr1[5] = { 1,2,3,4,5 };
	int arr2[5] = { 2,3,4,5,6 };
	int arr3[5] = { 3,4,5,6,7 };

	int \*parr[3] = { arr1,arr2,arr3 };
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 5; j++) 
		{
			printf("%d ",parr[i][j]);
			//另外一种写法 printf("%d ",\*(\*(parr + i) + j));
		}
		printf("\n");
	}
	
	return 0;
}


parr是一个指针数组,这个指针数组存放的元素是一维数组的地址,每一个元素类型是int 因为数组名是数组首元素的地址(数组首地址),而数组首地址的类型是int ,所以指针数组的每一个元素都是int ,而指针数组的类型是int [3]
printf("%d ",
(
(parr + i) + j)); 如何理解这句代码,
(parr + i)是找到下标为i的那个元素,而这个元素是一个数组首地址啊
(parr + i) + j,找到了一维数组的首地址再偏移j个长度,不是就找到了一维数组中的第j个元素的地址了吗,((parr + i) + j),再通过()解引用就能找到地址上对应的数值,其实(*(parr + i) + j) 和 parr[i][j]这两种访问数组元素的原理是差不多的

在这里插入图片描述

int main() 
{
	char \*arr[] = { "春眠不觉晓","处处闻啼鸟","夜来风雨声","花落知多少" };
	
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		printf("%s\n",arr[i]);
	}
	return 0;
}

字符指针数组的内存布局
在这里插入图片描述

指针数组中存放的每一个元素都是一个指针变量,这个指针变量的类型char*,每一个元素都指向着字符串起始地址,在以%s打印的时候会通过指针指向的起始地址找到‘\0’出现的前面字符,依次打印

三、数组指针

数组指针的主体是指针,该指针指向一个数组首地址(数组名)

int (\*parr)[10]

这么理解数组指针的语法,()的优先级是最高的,所以* 会先和
parr结合,这么一看是一个指针,往后一看[10],表示的是指针指向的数组的元素个数是10,前面的int 表示数组指针指向的数组的每一个元素都是int ,而数组指针的类型是 int (*)[10]

1.1&数组名VS数组名

前提概念:1、数组名是数组首地址,数组名表示数组首元素的地址
2、&arr取出的是整个数组的地址
3、&arr的类型是数组指针 int (*)[10]

int mian()
{
	int arr[10] = { 0 };
	printf("arr = %p\n", arr);
	printf("&arr= %p\n", &arr);
	printf("arr+1 = %p\n", arr+1);
	printf("&arr+1= %p\n", &arr+1);
	return 0;
}

在这里插入图片描述
从打印结果可以看出数组首元素的地址 + 1会跳过一个整形,而&数组名会跳过40个整形,原因是因为&arr的类型是 int(*)[10],对它 + 1会跳过40个字节,这里的地址不是数组首元素的地址,而是数组的地址,在这里要有一个概念性的了解

1.2数组指针的使用

1.2.1数组指针的错误示范
void func(int (\*arr)[10],int sz) 
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ",arr[i]);//err
		printf("%d ", \*(arr + i));//err
	}
}

int main() 
{
	int arr[10] = {1,2,3,4,5,67,8,9,10};
	int len = sizeof(arr) / sizeof(arr[0]);
	func(&arr,len);
	return 0;
}

在这里插入图片描述

根据前面的逻辑我们知道&arr取出的是数组的地址,int (*arr)[10] 是个数组指针,本质上还是一个指针,访问的是整个数组的地址,而数组的地址 + 1会偏移40个字节,随后访问的都是随机值。

1.2.2数组指针的正确示范示范
void func(int (\*arr)[10],int sz) 
{
	int i = 0;
	for (i = 0; i < sz; i++)
	{
	//以下几种方式均可以
		printf("%d ", arr[0][i]);
		printf("%d ",(\*arr)[i]);
		printf("%d ",\*(\*arr + i));
	}
}

int main() 
{
	int arr[10] = {1,2,3,4,5,67,8,9,10};
	int len = sizeof(arr) / sizeof(arr[0]);
	func(&arr,len);
	return 0;
}

解读:

arr [0][i] 有了第一个元素的下标向后访问i下标的元素,因为数组是连续存储的,有了第一个元素的下标,自然也能找到其他下标位置处的元素

(*arr)[i],数组指针是指向数组的地址,对指针解引用就能找到指针指向的内容,*arr表示的是数组的首元素地址,有了首元素的地址就可以向后偏移i位,其实等价于arr[i]

*(*arr + i),数组指针是指向数组的地址,对指针解引用就能找到指针指向的内容,*arr表示的是数组的首元素地址,对首元素的地址(数组首地址),偏移i个长度,会跳过4 * i个字节,再将偏移后的指针解引用找到指针指向的内容

希望以上的解释对大家的理解有所帮助,这也是博主对这些知识在一定程度上的理解,对这些语法的解释

接下来再看看另外一种用法


/\* 常规写法 \*/
void func(int arr[3][5],int row,int col) 
{
	int i = 0;
	for (i = 0; i < row; i++)
	{
		int j = 0;
		for (j = 0; j < col; j++)
		{
			printf("%d ",arr[i][j]);
		}
		printf("\n");
	}
}

/\* 使用数组指针 \*/
void func(int (\*arr)[5],int row,int col) 
{
	int i = 0;
	for (i = 0; i < row; i++)
	{
		int j = 0;
		for (j = 0; j < col; j++)
		{
		/\* 以下两种方式都可以 \*/
			printf("%d ",arr[i][j]);
			printf("%d ", \*((\*arr + i) + j));
		}
		printf("\n");
	}
}

int main() 
{
	int arr[3][5] = {1,2,3,4,5,2,3,4,5,6,3,4,5,6,7};
	int row = sizeof(arr) / sizeof(arr[0]);
	int col = sizeof(arr[0]) / sizeof(arr[0][0]);
	func(&arr,row,col);
	return 0;
}

主要还是说一下数组指针的使用方式

首先我们来看这是35的二维数组
在这里插入图片描述
当使用数组指针的时候获取的是它的下标为【0】的一维数组的地址,数组指针里面存放的就是它的地址
在这里插入图片描述
假设现在要找处arr[1][4]的元素,下面我们直接看图
在这里插入图片描述
在这里是通过让数组指针指向下标【1】的位置处,有了数组的起始地址向后偏移,方便找到其他的元素,不过这种int(
)[5]类型的数组指针只是针对一维数组的,存放的是一维数组的地址,这一点请大家务必注意

有了上面知识的理解,再来看一组,你现在知道它们代表着什么吗?
在这里插入图片描述

1、int arr[5]; --》 表示的是一个数组,数组的每一个元素都是一个int
2、int *parr1[10]; --》表示的是指针数组,数组中的每个元素都是int *类型的指针变量
3、 int (*parr2)[10]; --》数组指针,本质上是一个指针,指向的是一个数组,数组的元素个数是10,数组的元素类型是int
4、int (*parr3[10])[5]; 重点来了,先看操作符的优先级,()的优先级是最高的,所以先看()里的内容,其次是[ ]优先级是第二,arr3先和[ ] 结合就是一个数组,数组里面存放着10个元素,每一个元素的类型是 int( * )[5],所以每一个元素都是一个数组指针,该数组指针指向的数组是【5】个元素,每一个元素是int类型,在这里插入图片描述

四、数组参数、指针参数

1.1一维数组传参

#include <stdio.h>
void test(int arr[])//ok
{}
void test(int arr[10])//ok
{}
void test(int \*arr)//ok
{}
void test2(int \*arr[20])//ok
{}
void test2(int \*\*arr)//ok
{}
int main()
{
 int arr[10] = {0};
 int \*arr2[20] = {0};
 test(arr);
 test2(arr2);
}

1.2二维数组传参

void test(int arr[3][5])//ok
{}
void test(int arr[][])//err
{}
void test(int arr[][5])//ok
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int \*arr)//err
{}
void test(int\* arr[5])//err
{}
void test(int (\*arr)[5])//ok
{}
void test(int \*\*arr)//err
{}
int main()
{
 int arr[3][5] = {0};
 test(arr);
}

1.3一级指针传参

#include <stdio.h>
void print(int \*p, int sz) {
 int i = 0;
 for(i=0; i<sz; i++)
 {
 printf("%d\n", \*(p+i));
 }
}
int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9};
 int \*p = arr;
 int sz = sizeof(arr)/sizeof(arr[0]);
 //一级指针p,传给函数
 print(p, sz);
 return 0; }

1.4二级指针传参

#include <stdio.h>
void test(int\*\* ptr) {
 printf("num = %d\n", \*\*ptr); 
}
int main()
{
 int n = 10;
 int\*p = &n;
 int \*\*pp = &p;
 int \*arr[10];//指针数组
 test(pp);//ok,二级指针传参给,二级指针接收
 test(&p);//ok,二级指针存放一级指针的地址
 test(arr);//ok,指针数组传参,传递的是首元素的地址,元素类型是int,元素地址是int \*,二级指针能存放一级指针的地址
 return 0; }

五、函数指针

我们都知道函数指针变量是用来存放函数的地址的,那么函数的地址长什么样呢

在这里插入图片描述

1.1函数的地址和数组的地址有哪些地方不太一样

&数组名 - 数组的地址
数组名 - 数组首元素的地址

函数名 - 函数的地址
&函数名 - 函数的地址

1.2函数的地址又要存放到哪里去

void test()
{
 printf("hello word\n");
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (\*pfun1)();
void \*pfun2();

pfun1可以存放。pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void,函数指针的类型是int ( *)()。

1.3函数指针的使用

int Add(int x,int y)
{
	return x + y;
}

int main()
{
	int ret = Add(10,20);
	printf("%d\n", ret);

	int (\*pf)(int x, int y) = NULL;//定义函数指针变量
	pf = Add;//函数指针变量指向函数地址
	//pf = &Add; 可以写成这种方式
	ret = pf(20,30);//有了函数的地址,就可以调用该函数
	//ret = (\*pf)(20,30); 可以写成这种形式,便于初学者理解
	//ret = (\*\*\*\*\*\*pf)(20,30); 多加几颗\*也没有影响
	printf("%d\n",ret);


如果你也是看准了Python,想自学Python,在这里为大家准备了丰厚的免费**学习**大礼包,带大家一起学习,给大家剖析Python兼职、就业行情前景的这些事儿。



### 一、Python所有方向的学习路线

Python所有方向路线就是把Python常用的技术点做整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。



![](https://img-blog.csdnimg.cn/img_convert/9f49b566129f47b8a67243c1008edf79.png)

### 二、学习软件



工欲善其必先利其器。学习Python常用的开发软件都在这里了,给大家节省了很多时间。



![](https://img-blog.csdnimg.cn/img_convert/8c4513c1a906b72cbf93031e6781512b.png)



### 三、全套PDF电子书

书籍的好处就在于权威和体系健全,刚开始学习的时候你可以只看视频或者听某个人讲课,但等你学完之后,你觉得你掌握了,这时候建议还是得去看一下书籍,看权威技术书籍也是每个程序员必经之路。

![](https://img-blog.csdnimg.cn/img_convert/eec417a3d4d977b313558a11d3c13e43.png)



### 四、入门学习视频

我们在看视频学习的时候,不能光动眼动脑不动手,比较科学的学习方法是在理解之后运用它们,这时候练手项目就很适合了。



![](https://img-blog.csdnimg.cn/img_convert/ec690501ea1dbe2cb209cbf4013c2477.png)  

![](https://img-blog.csdnimg.cn/img_convert/3eaeaa6747419c9d86c72e0d10d0a6a2.png)



### 四、实战案例

光学理论是没用的,要学会跟着一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。



![](https://img-blog.csdnimg.cn/img_convert/252731a671c1fb70aad5355a2c5eeff0.png)



### 五、面试资料

我们学习Python必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有阿里大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。

![](https://img-blog.csdnimg.cn/img_convert/6c361282296f86381401c05e862fe4e9.png)

成为一个Python程序员专家或许需要花费数年时间,但是打下坚实的基础只要几周就可以,如果你按照我提供的学习路线以及资料有意识地去实践,你就有很大可能成功!
最后祝你好运!!!




**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化学习资料的朋友,可以戳这里无偿获取](https://bbs.youkuaiyun.com/topics/618317507)**

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值