深入浅出C语言指针(进阶篇)

news2024/11/23 1:58:18

深入浅出C语言指针(基础篇)

深入浅出C语言指针(进阶篇)

目录

引言

一、指针和数组

1.数组名的理解

2.指针访问数组 

3.一维数组传参的本质

二、二级指针

1.二级指针的概念

2.二级指针的内存表示

3.二级指针的解引用

三、字符指针

1.指针指向单个字符

2.指针指向字符串

3.一道面试题

四、指针数组

1.指针数组的概念

 2.初始化指针数组

3.指针数组模拟二维数组

五、数组指针

1.数组指针的概念

2.初始化数组指针

3.二维数组传参的本质

六、函数指针

1.函数指针的概念

2.初始化函数指针

3.函数指针的应用

七、函数指针数组

1.函数指针数组的概念

2.初始化函数指针数组

 3.函数指针数组的应用

 总结


引言

在C语言中,指针是至关重要的一部分,掌握指针的用法对于编写高效、简洁的代码具有极大帮助。本文将带您深入了解C语言指针的高级用法,助您迈向编程高手之路。

一、指针和数组

1.数组名的理解

请看下面一段代码:

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

运行结果如下: 

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

 两个特例

sizeof(数组名) ,sizeof中单独放数组名,这⾥的数组名表⽰整个数组,计算的是整个数组的⼤⼩,
单位是字节
&数组名 ,这⾥的数组名表⽰整个数组,取出的是整个数组的地址(整个数组的地址和数组⾸元素的地址是有区别的)
除此之外,任何地⽅使⽤数组名,数组名都表⽰⾸元素的地址。

2.指针访问数组 

我们搞清楚数组名就是数组首元素的地址之后,那么这样写代码就是可行的:

int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr;//p存放的是数组首元素的地址

当我们把数组名当成地址存放到指针中,就可以用指针访问数组了:

#include <stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int len = sizeof(arr) / sizeof(arr[0]);
	int* p = arr;//使用指针来访问数组
	int i = 0;
	for (i = 0; i < len; i++)
	{
		printf("%d ", *(p + i));//这里*(p+i)等价于arr[i]
	}
	return 0;
}

可以看到成功访问了数组每个元素: 

3.一维数组传参的本质

再请看下面代码:

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

输出结果如下:

上面我们学习了:数组名是数组⾸元素的地址;那么在数组传参的时候,传递的是数组名,也就是说本质上 数组传参本质上传递的是数组⾸元素的地址。
所以函数形参的部分理论上应该使⽤指针变量来接收⾸元素的地址。那么在函数内部我们写
sizeof(arr) 计算的是⼀个地址的⼤⼩(单位字节)⽽不是数组的⼤⼩(单位字节)
由此我们可以总结

数组传参的本质是传递了数组首元素的地址,所以形参访问的数组和实参访问的数组是同一个数组。(所以形参的数组并不会再开辟空间,故也可以省略数组大小只写成arr[])
⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。

二、二级指针

指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪⾥呢?

答案是二级指针

1.二级指针的概念

在C语言中,指针是一个变量,其值为另一个变量的地址。而二级指针是一个指向指针的指针,即它的值是另一个指针的地址。

二级指针的定义可以表示为:

int a = 10;        // 声明一个整型变量a
int *p = &a;       // 声明一个指向整型变量a的指针p
int **pp = &p;     // 声明一个指向指针p的二级指针pp

在这个例子中,a的地址存放在p中,p的地址存放在pp中。p是一级指针,存储a的地址;pp 是一个二级指针,它存储的是指针 p 的地址。

2.二级指针的内存表示

3.二级指针的解引用

要获取二级指针所指向的指针所指向的变量的值,需要进行两次解引用:

#include <stdio.h>
int main() {
    int a = 10;
    int* p = &a;
    int** pp = &p;
    // 通过二级指针修改a的值
    **pp = 20;
    printf("a = %d\n", a);
    return 0;
}

这里的** pp 首先通过* pp 获取 p 的值(即 a 的地址)然后通过* 解引用这个地址,从而修改 a 的值。

三、字符指针

1.指针指向单个字符

这是一般的使用方法,这里进阶篇我们就不过多赘述:

int main()
{
 char ch = 'w';
 char *pc = &ch;
 *pc = 'w';
 return 0;
}

2.指针指向字符串

#include<stdio.h>
int main()
{
	const char* pstr = "abcdef";//这里是把一个字符串放到pstr指针变量里了吗?
	printf("%s\n", pstr);//%s打印字符串后面需提供地址
	return 0;
}

代码 const char* pstr = "abcdef"; 特别容易让人以为是把字符串abcdef放到字符指针pstr里了,但是本质是把字符串abcdef的首字符a的地址放到了pstr中。由于字符串是连续存放的,故也可以通过通过指针访问到整个字符串。

3.一道面试题

《剑指offer》中有一道这样的面试题:

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

答案如下:

我们来分析一下:这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同。

总结如下:
.str1==str2比较的是地址,而不是内容。
每个数组对象都会分配属于自己的存储空间,地址各不相同。
指针变量并不分配存储区,而是指向常量字符串所在的静态存储区,由于指向的是同一个常量,所以str3=str4。

四、指针数组

1.指针数组的概念

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

指针数组:在C语言中,指针数组是一种特殊类型的数组,指针数组常用于存储一系列的地址,这些地址可以是变量的地址、数组元素的地址或者其他指针的地址。简单来说指针数组是一个数组,其中的每一个元素都是指向某种数据类型的指针。

声明数据类型*指针数组名[数组长度]

int *arr[5];

 2.初始化指针数组

在声明时进行初始化:

int a=1,b=2,c=3,d=4,e=5;
int *arr[5]={&a,&b,&c,&d,&e};

 也可以先声明一个指针数组,然后在后续代码中赋值:

int a=1,b=2,c=3,d=4,e=5;
int *arr[5];
arr[0]=&a;
arr[1]=&b;
arr[2]=&c;
arr[3]=&d;
arr[4]=&e;

3.指针数组模拟二维数组

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

parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型⼀维数组,parr[i][j]就是整型⼀维数组中的元素。上述的代码模拟出⼆维数组的效果,实际上并⾮完全是⼆维数组,因为每⼀⾏并⾮是连续的。

五、数组指针

1.数组指针的概念

数组指针是指针?还是数组?
答案是:指针。
我们已经熟悉:
整形指针: int * pi ; 能够指向整形数据的指针。
浮点型指针: float * pf ; 能够指向浮点型数据的指针。
那数组指针应该是:能够指向数组的指针。
数组指针:在C语言中,数组指针是一种特殊的指针类型,它指向的是一个数组。 更具体地说,数组指针是一个指针,其指向的是包含特定数量元素的数组。数组指针与指向单个变量的指针不同,它指向的是一个数组的首元素, 但是它的解引用操作会返回整个数组,而不是单个元素
声明:数据类型(*指针名)[数组长度]
int(*ptr)[5];

2.初始化数组指针

数组指针可以通过取数组的地址来初始化:

int arr[5] = {1, 2, 3, 4, 5};
int (*ptr)[5] = &arr; // 初始化数组指针,使其指向数组arr
 |    |    |
 |    |    |
 |    |    ptr指向数组的元素个数
 |    ptr是数组指针变量名
 ptr指向的数组的元素类

在这里,&arr 表示取数组 arr 的地址(而不是首元素的地址),这个地址就是数组指针 ptr 的初始值。

3.二维数组传参的本质

 有了数组指针的理解,我们就能够讲⼀下⼆维数组传参的本质了。

过去我们有⼀个⼆维数组的需要传参给⼀个函数的时候,我们是这样写的:
#include <stdio.h>
void test(int a[3][5], int r, int c)
{
	int i = 0;
	int j = 0; 
	for (i = 0; i < r; i++)
	{
		for (j = 0; j < c; j++)
		{
			printf("%d ", a[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} };
	test(arr, 3, 5);
	return 0;
} 
这⾥实参是⼆维数组,形参也写成⼆维数组的形式,那还有什么其他的写法吗?
⾸先我们再次理解⼀下⼆维数组,⼆维数组起始可以看做是每个元素是⼀维数组的数组,也就是⼆维数组的每个元素是⼀个⼀维数组。那么⼆维数组的⾸元素就是第⼀⾏,是个⼀维数组。
如下图:

所以,根据数组名是数组⾸元素的地址这个规则, ⼆维数组的数组名表⽰的就是第⼀⾏的地址,是
维数组的地址,类型是数组指针类型 。根据上⾯的例⼦,第⼀⾏的⼀维数组的类型就是 int [5] , 所以第⼀⾏的地址的类
型就是数组指针类型 int(*)[5] 。那就意味着⼆维数组传参本质上也是传递了地址,传递的是第⼀
⾏这个⼀维数组的地址,那么形参也是可以写成指针形式的。如下:
#include <stdio.h>
void test(int(*p)[5], int r, int c)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < r; i++)
	{
		for (j = 0; j < c; j++)
		{
			printf("%d ", *(*(p + 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} };
	test(arr, 3, 5);
	return 0;
} 

运行结果:

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

六、函数指针

1.函数指针的概念

函数指针:在C语言中,函数指针是一种特殊类型的指针,它存储的是函数的地址,而不是变量的地址。通过函数指针,你可以间接地调用函数,类似于通过普通指针间接访问变量。

声明返回类型 (*函数指针名)(参数类型1, 参数类型2, ...);

这里的返回类型是函数返回的类型,函数指针名是你给这个指针起的名字,而参数类型1, 参数类型2, ... 是函数的参数列表。

int(*fubPtr)(int,int);

2.初始化函数指针

函数指针可以通过取函数的地址来初始化:

int add(int a, int b) {
    return a + b;
}
int (*funcPtr)(int, int) = &add; // 初始化函数指针,使其指向add函数
 |    |       ----------          //也可以写成int (*funcPtr)(int, int)=add;
 |    |           |
 |    |         funPtr指向函数的参数类型和个数的交代
 |   函数指针变量名
 funPtr指向函数的返回类型

在这里,&add 表示取函数 add 的地址,这个地址就是函数指针 funcPtr 的初始值。

(这里add和&add是一样的,函数名和&函数名都表示函数地址,二者没有区别)

3.函数指针的应用

通过函数指针调⽤指针指向的函数
#include <stdio.h>
int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int(*pf3)(int, int) = Add;
	printf("%d\n", Add(1, 2));//通过函数名调用
	printf("%d\n", (*pf3)(2, 3));//通过函数指针调用
	printf("%d\n", pf3(3, 5));//由于pf存储的是函数的地址,也可以通过函数地址直接调用,故也可以不写*号
	return 0;
}

运行结果如下:

七、函数指针数组

1.函数指针数组的概念

函数指针数组:在C语言中,函数指针数组是一个数组,其元素是函数指针。这意味着函数指针数组中的每个元素都是一个指针,指向一个函数。函数指针数组可以用来存储多个函数的地址,并允许你在运行时根据需要选择其中一个函数来执行。

声明:返回类型 (*数组名)[数组长度];

这里的 返回类型 是函数返回的类型,数组名 是数组的名字,而 数组长度 是一个常量表达式,表示数组中函数指针的数量。

int(*arr[5])(int,int);//在这个声明中,arr是一个数组,它包含5个指向整型函数的指针。

2.初始化函数指针数组

函数指针数组可以通过取函数的地址来初始化:

int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

int (*funcArray[2])(int, int) = {add, subtract}; // 初始化函数指针数组

在这里,funcArray 是一个数组,它包含两个指向整型函数的指针,分别指向 add 和 subtract 函数。

 3.函数指针数组的应用

转移表

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int add(int a, int b)
{
	return a + b;
}
int sub(int a, int b)
{
	return a - b;
}
int mul(int a, int b)
{
	return a * b;
}
int div(int a, int b)
{
	return a / b;
}
int main()
{
	int x, y;
	int input;
	int ret = 0;
	int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
	do {
		printf("**********计算器**********\n");
		printf("******1.加法  2.减法******\n");
		printf("******3.乘法  4.除法******\n");
		printf("*********0.退出**********\n");
		printf("请选择:\n");
		scanf("%d", &input);
		if (input >= 1 && input <= 4)
		{
			printf("输入需要操作的两个数\n");
			scanf("%d%d", &x, &y);
			ret = (*p[input])(x, y);
			printf("结果为%d\n", ret);
		}
		else if (input = 0)
		{
			break;
		}
		else
		{
			printf("输入有误,请重新输入!\n");
		}
	} while (input);
}

运行如下:

 总结

通过本文的学习,相信你对C语言指针有了更深入的理解,能够更好地运用指针来编写高效的C语言程序。加油!祝你更上一层楼!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1942465.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaWeb(4)JavaScript入门2—— JS的对象和JSON

一、JS的对象 1.声明语法1 通过new Object()直接创建对象 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><ti…

Linux——vi和vim编辑器

目录 基本介绍 vi和vim常用的三种模式 vi和vim的常用快捷键 基本介绍 vi和vim常用的三种模式 vi和vim的常用快捷键 网上找的快捷键盘图

docker搭建ES 8.14 集群

参考&#xff1a;【docker搭建es8集群kibana】_docker 安装生产级 es 8.14 集群-CSDN博客 1、之前已搭建一台单机版的dockerES集群 参见 Elasticsearch docker 安装_docker 安装es8.14.3-CSDN博客 2、现在需要重新搭建为docker ES集群 准备新搭建3个点 一、准备工作 提前开…

txt格式单词导入有道词典生词本 (java代码方式)

txt格式单词导入有道词典生词本 (java代码方式) 首先要求txt文档里单词的格式&#xff0c;大概需要像这种&#xff1a; 每行是一个单词&#xff0c;格式为&#xff1a;英文单词空格词性单词意思。 注意 导出单词本的名字就是你 txt 文件的名字 我这里是 公共英语三级 单词本 …

WPF项目实战视频《二》(主要为prism框架)

14.prism框架知识&#xff08;1&#xff09; 使用在多个平台的MVVM框架 新建WPF项目prismDemo 项目中&#xff1a;工具-NuGet包管理&#xff1a;安装Prism.DryIoc框架 在git中能看Prism的结构和源代码&#xff1a;git链接地址 例如&#xff1a;Prism/src/Wpf/Prism.DryIoc.Wpf…

Linux中tomcat下载教程

一.安装tomcat 1.安装 EPEL 仓库&#xff1a; sudo yum install epel-release2.安装 Tomcat&#xff1a; sudo yum install tomcat3.启动 Tomcat 服务&#xff1a; sudo systemctl start tomcat4.启用 Tomcat 服务开机启动&#xff1a; sudo systemctl enable tomcat5.检查…

SpringCloud 环境工程搭建

SpringCloud 环境&工程搭建 文章目录 SpringCloud 环境&工程搭建1. SpringCloud介绍2. 服务拆分原则2.1 单一职责原则2.2 服务自治2.3 单向依赖2.4 服务拆分示例 3. 数据准备4. 工程搭建4.1 创建父工程4.2 创建子工程4.2.1 子项目-订单服务4.2.2 子项目-商品服务 4.3 完…

物联网专业创新人才培养体系的探索与实践

一、引言 随着物联网&#xff08;IoT&#xff09;技术的迅猛发展&#xff0c;物联网领域的人才需求日益增加。物联网技术作为新一轮信息技术革命的核心&#xff0c;已经渗透到社会生活的各个领域&#xff0c;对推动经济转型升级、提升国家竞争力具有重要意义。因此&#xff0c…

Redis 7.x 系列【26】集群模式动态扩容、动态缩容

有道无术&#xff0c;术尚可求&#xff0c;有术无道&#xff0c;止于术。 本系列Redis 版本 7.2.5 源码地址&#xff1a;https://gitee.com/pearl-organization/study-redis-demo 文章目录 1. 动态扩容1.1 安装、启动1.2 加入新节点1.3 分配哈希槽1.4 加入从节点 2. 缩容2.1 删…

PHP场地预约共享茶室棋牌室小程序系统源码

&#x1f375;&#x1f3b2;【聚会新宠】场地预约神器&#xff0c;共享茶室棋牌室小程序大揭秘&#xff01;&#x1f389; &#x1f3e1;【开篇&#xff1a;告别繁琐&#xff0c;聚会新选择】&#x1f3e1; 还在为找不到合适的聚会场地而烦恼吗&#xff1f;想要一个既私密又舒…

python+onlyoffice+vue3项目实战20240722笔记,环境搭建和前后端基础代码

开发后端 先创建data目录,然后在data目录下创建一个test.docx测试文档。 后端代码: import json import req import api from api import middleware, PlainTextResponseasync def doc_callback(request):data = await api.req.get_json(request)print("callback ==…

微信小程序-CANVAS写入图片素材、文字等数据生成图片

微信小程序中&#xff0c;CANVAS写入图片素材、文字等数据生成图片&#xff0c;最终可将生成的 base64 格式图片保存至相册操作 Tips&#xff1a; 1、canvas 标签默认宽度 300px、高度 150px canvas 生成图片时&#xff0c;写入图片素材、文字等数据前&#xff0c;需要根据实…

git的一些使用技巧(git fetch 和 git pull的区别,git merge 和 git rebase的区别)

最近闲来无聊&#xff0c;虽然会使用git操作&#xff0c;但是 git fetch 和 git pull 的区别&#xff0c;git merge 和 git rebase的区别只是一知半解&#xff0c;稍微研究一下&#xff1b; git fetch 和 git pull 的区别 git fetch git fetch 是将远程仓库中的改动拉到本地…

鸿蒙仓颉语言【扩展Redis仓颉语言客户端】

2. 扩展Redis仓颉语言客户端 2.1 Redis命令处理模块的架构 Redis命令处理的架构图如下&#xff1a; RedisCommand类 Redis命令的实现类 包含以下成员&#xff1a; commandType: Redis命令的名称 commandArgs: Redis命令的参数列表 response: Redis命令的响应消息&#xff…

【学习笔记】无人机系统(UAS)的连接、识别和跟踪(十)-无人机A2X服务

引言 3GPP TS 23.256 技术规范&#xff0c;主要定义了3GPP系统对无人机&#xff08;UAV&#xff09;的连接性、身份识别、跟踪及A2X&#xff08;Aircraft-to-Everything&#xff09;服务的支持。 3GPP TS 23.256 技术规范&#xff1a; 【免费】3GPPTS23.256技术报告-无人机系…

当当网数据采集:Scrapy框架的异步处理能力

在互联网数据采集领域&#xff0c;Scrapy框架以其强大的异步处理能力而著称。Scrapy利用了Python的异步网络请求库&#xff0c;如twisted&#xff0c;来实现高效的并发数据采集。本文将深入探讨Scrapy框架的异步处理能力&#xff0c;并展示如何在当当网数据采集项目中应用这一能…

npm 安装报错(已解决)+ 运行 “wue-cli-service”不是内部或外部命令,也不是可运行的程序(已解决)

首先先说一下我这个项目是3年前的一个项目了&#xff0c;中间也是经过了多个人的修改惨咋了布置多少个人的思想&#xff0c;这这道我手里直接npm都安装不上&#xff0c;在网上也查询了多种方法&#xff0c;终于是找到问题所在了 问题1&#xff1a; 先是npm i 报错在下面图片&…

下拉菜单过渡

下拉过渡&#xff0c;利用Y轴的transform&#xff1a;scaleY(0) —》transform&#xff1a;scaleY(1) 代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8" /><meta name"viewport" cont…

实战:MyBatis适配多种数据库:MySQL、Oracle、PostGresql等

概叙 很多时候&#xff0c;一套代码要适配多种数据库&#xff0c;主流的三种库&#xff1a;MySQL、Oracle、PostGresql&#xff0c;刚好mybatis支持这种扩展&#xff0c;如下图所示&#xff0c;在一个“namespace”&#xff0c;判断唯一的标志是iddatabaseId&#xff0c;刚好写…

学习vue3的搭建

Vue3 Vite项目构建 环境准备 1. NodeJs安装 安装NodeJs&#xff0c;安装成功后&#xff0c;以管理员身份打开命令行&#xff0c;输入命令 node -v查看NodeJs版本&#xff1b;输入命令 npm -v查看npm版本。 2. 安装cnpm 因为npm是国外的&#xff0c;下载资源的时候会翻墙&…