【C的葵花宝典进阶篇】之指针进阶(一)

【C语言进阶篇】之指针进阶(一)

  • 1. 字符指针
  • 2. 指针数组
    • 2.1 整形指针数组
    • 2.2 用指针数组模拟二维数组
  • 3. 数组指针
    • 3.1 数组指针的表示方法
    • 3.2 深度剖析&数组名和数组名
    • 3.3 数组指针的使用
      • 3.3.1 在同一函数内直接将数组的地址赋给数组指针
      • 3.3.2 数组指针在二维数组传参上的应用
        • 3.3.2.1 二维数组传参使用二维数组
        • 3.3.2.2 二维数组传参使用数组指针
  • 4. 数组传参和指针传参
    • 4.1 一维数组传参
    • 4.2 二维数组传参
    • 4.3 一级指针传参
    • 4.4 二级指针传参
  • 5. 函数指针
    • 5.1 函数的地址
    • 5.2 函数指针的表示方法
    • 5.3 两行有趣的代码
      • 5.3.1 将0强制转换为函数指针类型然后调用
      • 5.3.2 数组指针、函数指针等特殊变量类型typedef的使用
      • 5.3.3 函数指针类型的函数的声明,并使用别名typedef化简

❤️博客主页: 小镇敲码人
🍏 欢迎关注:👍点赞 👂🏽留言 😍收藏
🌞回来4天了,加油!!!🍎🍎🍎
💗当你的能力匹配不上你的梦想,当你需要实现的目标匹配不上你的圈子的时候,你就会出现错位。当我们的能力还匹配不上我们的梦想时,我们就需要沉淀学习。✡️✡️✡️

1. 字符指针

字符指针,就是储存字符变量地址的指针,这是一种指针的类型。

  • 通常我们字符指针有如下两种用途
  1. 通过字符指针间接改变字符的值
    int main()
    {
      char ch = 'c';
     // ch = 'a';//直接通过变量赋值更改ch的值
      char *p = &ch;//将字符ch的地址存入p中
      //*p = 'a';//通过对p里面的地址解引用找到ch,间接的改变ch的值
    }
  1. 借助字符指针对字符串进行储存和打印
 int main()
 {
   const char* ptr = "abcdef";
   printf("%s\n",ptr);
   return 0;
 }
  • const修饰字符串表示"abcdef"不可修改,为常量字符串
  • 字符指针变量ptr只储存了字符'a'的地址。 (可类比字符数组理解,实际上字符串是字符数组的一种形式,在C语言中,字符串是由字符数组表示的,以空字符(‘\0’)作为字符串的结束标志,又因为数组名表示首元素地址,所以字符指针也只储存了首元素的地址)

2. 指针数组

      指针数组是由指针组成的数组。在C和C++等编程语言中,指针是一个变量,用于存储内存地址。指针数组是一个数组,其每个元素都是指针类型。
    int *arr1[10];    整形指针数组
    char *arr2[10];  一级字符指针的数组
    char **arr3[10]; 二级字符指针的数组

2.1 整形指针数组

int main()
{
    int a = 0;
        int b = 1;
    int c = 2;
    int* arr[] = { &a,&b,&c };
    for (int i = 0; i < 3; i++)
    {
        printf("%d ", *arr[i]);
    }
}

2.2 用指针数组模拟二维数组

int main()
{
	int arr1[] = { 1,2,3,4,5 };//arr1-int*
	int arr2[] = { 2,3,4,5,6 };//arr2-int*
	int arr3[] = { 3,4,5,6,7 };//arr3-int*
   //指针数组
	int* p[] = { arr1,arr2,arr3 };
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 5; j++)
		{
		  //这两种方式打印都可以,两者择一
			printf("%d ", *(*(p + i) + j));
            printf("%d ",p[i][j]);
		}
		printf("\n");
	}
}

3. 数组指针

我们可以通过类比来理解

整型指针:int * a;能够指向整形数据的指针
浮点型指针:float* b;能够指向浮点型数据的指针。
那么数组指针应该是:能够指向数组的指针。

3.1 数组指针的表示方法

数组的类型表示是什么?通过简单的类比可以知道。

  1. int a = 3;类型为除变量名以外的部分int就是整形的类型表示形式。
  2. char b = 'a';类型为除变量名以外的部分char就是字符类型的的表示形式。
  3. int arr[10] = {0};类型为除变量名以外的部分int [10]就是这个数组的变量类型表示形式。
  4. 而数组指针的类型是指针,指针的类型是数组,通过前面的学习我们知道,变量类型+变量名为一个变量,所以一个数组指针变量就是,指向数组的指针变量,int [10] *p = &arr;,看起来似乎是这样,但是通过下面符号的优先级我们可以知道[]的优先级要比*号高,所以我们要给*p加上括号,让其成为指针,int [10] (*p) = &arr,那这样是否正确呢?还是不对,放进vs编译器会报错,数组的类型比较特殊,应该是这样int (*p) [10] = &arr;才正确。

3.2 深度剖析&数组名和数组名

对于下面的数组

 int arr[10];
  • arr&arr分别是什么呢?
    对于arr,我们知道它是一个数组名,数组名表示首元素的地址,
    那&arr是什么呢,我们来看下面一段代码:
int main()
{
	int arr[10] = { 0 };
	printf("arr       = %p\n", arr);
	printf("arr+1     = %p\n", arr+1);

	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);
	return 0;
}

运行结果如下:
在这里插入图片描述
可以看到数组名和&数组名打印的地址和首元素打印的地址是一样的,这是不是意味着它们是一样的呢?很明显通过代码我们也可以发现,它们并不是相同的,因为&arr+1打印的地址与arr+1打印的地址相差了40个字节,而arr+1打印的地址和首元素&arr+1的仍然一样,进一步说明,arr就表示首元素地址。

  • 实际上:&arr表示的是整个数组的地址,它的类型是int (*)[10],是一种数组指针类型。
                   而arr代表的是数组首元素的地址,它的类型是int *
                   数组的地址加1,跳过整个数组的大小,所以&arr+1与&arr的差值就是 10 ∗ 4 = 40 10*4 =40 104=40个字节。

3.3 数组指针的使用

3.3.1 在同一函数内直接将数组的地址赋给数组指针

#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	//int[10] *p = &arr;err
	int (*p)[10] = &arr;
	for (int i = 0; i < 10; i++)
	{
	    //两种打印方式择一
	    printf("%d ",*((*p)+i));
		printf("%d ", (*p)[i]);
	}
	//int* p2 = &arr;//err
	return 0;
}

  • 可以看到,这样做简直就是多此一举,因为想打印一维数组,我们直接打印就行了,使用数组指针,反倒是比较麻烦。

3.3.2 数组指针在二维数组传参上的应用

3.3.2.1 二维数组传参使用二维数组

#include<stdio.h>
void Print(int arr[][5], int r, int c)
{
	for (int i = 0; i < r; i++)
	{
		for (int j = 0; j < c; 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 };
	Print(arr, 3, 5);
	return 0;
}

3.3.2.2 二维数组传参使用数组指针

#include<stdio.h>
Print(int(*p)[5], int r, int c)
{
	for (int i = 0; i < r; i++)
	{
		for (int j = 0; j < c; j++)
		{
		  //两种打印方式择一
			printf("%d ", *(*(p + i) + 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 };
	//arr表示数组名,数组名表示首元素的地址,也就是那一行的地址
	Print(arr, 3, 5);
	return 0;
}
  • 因为二维数组的数组名代表首元素的地址,首元素地址就是那一行的地址,就等价于一维数组的地址(&一维数组数组名),故又称二维数组为一维数组的数组,所以可以用数组指针来接收。

4. 数组传参和指针传参

4.1 一维数组传参

  • 有以下主要两种方式
  1. 传递数组名,用一级指针的方式去接收,通过地址访问
  2. 传递数组的副本
#include<stdio.h>
void test1(int arr[])
{}
void test1(int arr[10])
{}
void test1(int* arr)
{}
void test2(int* arr[20])
{}
void test2(int** arr)
{}
int main()
{
	int arr1[10] = { 0 };
	int* arr2[20] = { 20 };
	test1(arr1);
	test2(arr2);
	return 0;
}

4.2 二维数组传参

  • 正确的方法已经在“3.3.2 数组指针在二维数组传参上的应用“版块给出,下面就几种错误的传参方法做一下阐述。
#include<stdio.h>
void test(int arr[3][5]){}
void test(int arr[][])//err,二维数组传参,如果是传递数组的副本,需要给出一行有多少数字,因为二维数组其实是一维数组的扩展,所以需要提供一行有多少元素,来正确进行内存访问
{}
void test (int arr[][5]){}
void test(int *arr)//err,传的是一行的地址,指针的类型是数组,int(*)[5]。
{}
void test(int *arr[5])//err,传的是第一行的地址,用指针数组接收不对,因为传的是一行的地址。
{}
void  test(int (*arr)[5]){}
void test(int **arr)//err,传的是数组的地址(第一行的地址),不是一级指针的地址,不能用二级指针接收。
{}
int main()
{
	int arr[3][5] = { 0 };
	test(&arr);
	return 0;
}
  • 注意:传二维数组的副本必须给定一行有多少个数,在C语言中,二维数组在内存中以连续的块存储,可以看作是一维数组的扩展。当传递二维数组作为参数时,需要提供一行有多少个元素的信息,以便在函数内部正确地进行内存访问。
    二维数组在内存中按行主序(row-major order)存储。也就是说,二维数组的每一行依次存储在内存中,并且相邻的元素在内存中也是相邻的。通过提供一行有多少个元素的信息,可以根据内存布局准确地计算出每个元素的地址,从而进行正确的访问。
  • 二维数组不能用二级指针来接收,因为二级指针是指向一级指针的,很明显,二维数组的数组名的类型是第一行即数组的地址应该用这个int (*)[5]即用数组指针接收。
  • 二维数组也不能用指针数组接收,类型不匹配。

4.3 一级指针传参

当函数形参为一级指针时,实参可以是同类型一维数组数组名、同类型变量的地址、同类型指针。

  • 请看如下代码加深理解:
#incldue<stdio.h>
void print(int* p, int sz)
{
    for (int i = 0; i < sz; i++)
    {
        printf("%d ", *(p + i));
        printf("%d ", p[i]);
    }
}
int main()
{
    int arr[5] = { 0,1,2,3,4 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    print(arr, sz);
}
#include<stdio.h>
void test(char *p)
{}
int main()
{
  char ch = 'a';
  char *str = "abcdef";
  char* ptr = &ch; 
  test(str);
  test(&ch);
  test(ptr);
  return 0;
}

4.4 二级指针传参

当函数形参为二级指针时,实参可以是一级指针的地址、二级指针。

  • 请看下面代码,加深理解:
#incldue<stdio.h>
void test(int **ptr)
{
  printf("num = %d\n",**ptr);
}
int main()
{
  int a = 3;
  int* p = &a;
  int **pp = &p;
  test(pp);
  test(&p);
  return 0;
}

5. 函数指针

函数指针和数组指针、字符指针一样都是指针,只不过指针所指向的对象的类型不同,其中函数指针与数组指针最为相似。

5.1 函数的地址

我们先看下面代码,函数名的地址,类比数组的地址

int Add(int a, int b)
{
	return a + b;
}
int main()
{
    printf("%p\n", &Add);
	printf("%p\n", Add);
	return 0;
}

运行的结果:
在这里插入图片描述
        可以看见输出的是一段地址,这两个地址都是函数Add的地址。既然函数的地址我们知道了,就可以用指针去保存它,我们只需要知道指针指向的对象的类型就行了。

5.2 函数指针的表示方法

  • *号代表变量是一个指针变量,函数指针所指向的类型就是函数声明去掉变量名和分号后保留的部分,包括返回值和参数的类型,与数组指针相似,函数指针的(*变量名)也要放在函数名的位置,而数组指针是放在数组名的位置。

请看如下代码:

void test()
{
  printf("i love programming!\n");
}
//除函数名以外的部分,就是的类型
void (*pf)();//函数指针,指向一个返回值为void,无参数的函数。
void* pf();//表示返回值类型为void*,无参数的函数。。
#incldue<stdio.h>
int Add(int a, int b)
{
	return a + b;
}
int main()
{
	int arr[10] = { 0 };
	int(*p)[10] = &arr;//数组指针
	//int (*)[10]是数组指针类型
	int(*pf) (int, int) = &Add;
	//int (*)(int,int)是函数指针类型
	return 0;
}

再给出一组代码帮助理解:

#include<stdio.h>
int Add(int x, int y)
{
	return x + y;
}
int main()
{
     //&Add和Add都可以
	int (*pf)(int, int) = &Add;
	int (*pf)(int, int) = Add;
	
	int r = Add(3, 5);
	printf("%d\n", r);

	int m = pf(3, 5);
	printf("%d\n", m);
    return 0;
}

运行结果如下:在这里插入图片描述

  • 函数指针与数组指针有相似性,可以对比着去理解。

5.3 两行有趣的代码

5.3.1 将0强制转换为函数指针类型然后调用

void (*p)() -p是函数指针
void (*)()  -是函数指针类型 

(*  (   ( void(*)() )0 ) )();
//(((void(*)())0)是将0强制转换为函数至真
//(*(函数指针))是将函数指针解引用
//然后不传参数调用(*(函数指针))(); 

在这里插入图片描述

5.3.2 数组指针、函数指针等特殊变量类型typedef的使用

#include<stdio.h>
typedef int* ptr_t;//一级指针别名
typedef unsigned int uint;//无符号整形的别名

typedef int(*parr_t)[10];//数组指针的别名
typedef int (*pf_t)(int, int);//函数指针的别名
int main()
{
	ptr_t p1;
	uint u2;
	parr_t p2;
	pf_t p3;
}
  • 函数指针与数组指针别名的创建与其变量的创建类似,可类比理解。

5.3.3 函数指针类型的函数的声明,并使用别名typedef化简

请看如下代码:

   void(* signal ( int, void(*)(int) ) )(int);

解析:
在这里插入图片描述
          我们可以观察到signal函数的返回类型和其中一个参数是相同的都是函数指针类型,下面我们利用别名typedef来使这段代码看起来更加易懂:

typedef void (*pf_t)(int);
pf_t signal(int,pf_t);

          CSDN上的别名没有高亮,但VS2019里面是有高亮的,定义函数后,你会发现编译器是不会报错,说明这样是可行的。
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Spark】RDD转换算子

目录 map mapPartitions mapPartitionsWithIndex flatMap glom groupBy shuffle filter sample distinct coalesce repartition sortBy ByKey intersection union subtract zip partitionBy reduceByKey groupByKey reduceByKey 和 groupByKey 的区别 a…

C#学习之路-常量

C# 常量 常量是固定值&#xff0c;程序执行期间不会改变。常量可以是任何基本数据类型&#xff0c;比如整数常量、浮点常量、字符常量或者字符串常量&#xff0c;还有枚举常量。 常量可以被当作常规的变量&#xff0c;只是它们的值在定义后不能被修改。 整数常量 整数常量可…

跨境平台做测评、采退、Lu卡、lu货要怎么做安全?

大家好&#xff0c;我是珑哥测评&#xff0c;今天和大家聊聊比较小众的圈子&#xff0c;也就是测评衍生出来的分支&#xff0c;采购和退款。因为最近也有很多客户咨询这个问题&#xff0c;由于沃尔玛风控升级了&#xff0c;很多客户下不成功的问题。 大家都知道无论是做测评还是…

从源码全面解析 dubbo 服务端服务调用的来龙去脉

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱敲代码的小黄&#xff0c;独角兽企业的Java开发工程师&#xff0c;CSDN博客专家&#xff0c;阿里云专家博主&#x1f4d5;系列专栏&#xff1a;Java设计模式、Spring源码系列、Netty源码系列、Kafka源码系列、JUC源码…

Docker自学记录笔记

安装联系Docker命令 1. 搜索镜像 docker search nagin 2. 下载镜像 3. 启动nginx 强调文本 强调文本 加粗文本 加粗文本 标记文本 删除文本 引用文本 H2O is是液体。 210 运算结果是 1024. 插入链接与图片 链接: link. 图片: 带尺寸的图片: 居中的图片: 居中并…

OpenCV的remap实现图像垂直翻转

以下是完整的代码: #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> #include <iostream>int main() {

Redis10大性能优化点(上)

1.Redis真的变慢了吗&#xff1f; 对 Redis 进行基准性能测试 例如&#xff0c;我的机器配置比较低&#xff0c;当延迟为 2ms 时&#xff0c;我就认为 Redis 变慢了&#xff0c;但是如果你的硬件配置比较高&#xff0c;那么在你的运行环境下&#xff0c;可能延迟是 0.5ms 时就…

flutter:实现一个简单的appBar上的搜索框、一个简单的搜索历史

搜索框 效果图 代码 import package:flutter/material.dart;class NovelSearch extends StatefulWidget {overrideState<StatefulWidget> createState() > _NovelSearchState(); }class _NovelSearchState extends State<NovelSearch> {String searchVal ;o…

Debian 12 静态IP / 固定IP的设置

环境&#xff1a;Debian 12 amd64-lxde 局域网&#xff1a;PT925E电信光猫 手机APP 网络管家 一般用动态IP就可以了&#xff0c;但如果软件环境比较小众&#xff0c;问题就随之而来。起始问题&#xff1a;路由器无法解析设备名和IP&#xff0c;网络管家也不让设置固定IP&…

基于深度学习的高精度水果检测识别系统(PyTorch+Pyside6+YOLOv5模型)

摘要&#xff1a;基于深度学习的高精度水果&#xff08;苹果、香蕉、葡萄、橘子、菠萝和西瓜&#xff09;检测识别系统可用于日常生活中或野外来检测与定位水果目标&#xff0c;利用深度学习算法可实现图片、视频、摄像头等方式的水果目标检测识别&#xff0c;另外支持结果可视…

Python教程(2)——开发python常用的IDE

为什么需要IDE 在理解IDE之前&#xff0c;我们先做以下的实验&#xff0c;新建一个文件&#xff0c;输入以下代码 total_sum 0 for x in range(1,101):total_sum x print(total_sum)非常非常简单的一个程序&#xff0c;主要就是计算1加到100的值&#xff0c;我们将它重命名…

SpringBoot第19讲:SpringBoot 如何保证接口幂等

SpringBoot第19讲&#xff1a;SpringBoot 如何保证接口幂等 在以SpringBoot开发Restful接口时&#xff0c;如何防止接口的重复提交呢&#xff1f; 本文是SpringBoot第19讲&#xff0c;主要介绍接口幂等相关的知识点&#xff0c;并实践常见基于Token实现接口幂等。 文章目录 Spr…

动态规划之343 整数拆分(第6道)

题目&#xff1a; 给定一个正整数 n &#xff0c;将其拆分为 k 个 正整数 的和&#xff08; k > 2 &#xff09;&#xff0c;并使这些整数的乘积最大化。 返回 你可以获得的最大乘积 。 示例&#xff1a; 解法&#xff1a; 其实可以从1开始遍历 j &#xff0c;然后有两种…

通用分页【下】(将分页封装成标签)

目录 一、debug调试 1、什么是debug调试&#xff1f; 2、debug调试步骤 3、实践 二、分页的核心 三、优化 分页工具类 编写servlet jsp代码页面&#xff1a; 分页工具类PageBean完整代码 四、分页标签 jsp代码 编写标签 tld文件 助手类 改写servlet 解析&…

CTFshow-pwn入门-栈溢出pwn49(静态链接pwn-mprotect函数的应用)

pwn49 首先我们先将pwn文件下载下来&#xff0c;然后赋上可执行权限&#xff0c;再来查看pwn文件的保护信息。 chomd x pwn checksec pwn file pwn我们可以看到这是一个32位的pwn文件&#xff0c;并且保护信息开启了NX和canary&#xff0c;也就是堆栈不可执行且有canary。最最…

html案例2

效果 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"widthdevice-width, initia…

redis如何实现持久化

RDB快照 RDB是一种快照存储持久化方式&#xff0c;具体就是将Redis某一时刻的内存数据保存到硬盘的文件当中&#xff0c;默认保存的文件名为dump.rdb&#xff0c;而在Redis服务器启动时&#xff0c;会重新加载dump.rdb文件的数据到内存当中恢复数据。 开启RDB持久化方式 开启…

缓存更新策略,先更新数据库还是缓存呢?

学了这么多&#xff0c;相信大家对缓存更新的策略都已经有了清晰的认识。最后稍稍总结一下。 缓存更新的策略主要分为三种&#xff1a; Cache aside Cache aside Cache aside也就是旁路缓存&#xff0c;是比较常用的缓存策略。 &#xff08;1&#xff09;读请求常见流程 应…

亿级日活业务稳如磐石 华为云发布性能测试服务CodeArts PerfTest

HDC期间可参与华为云PaaS生态抽奖活动&#xff0c;活动链接在文末 计算机软件作为人类逻辑智慧的伟大结晶之一&#xff0c;已经渗透到了人类社会的各个角落。早期的计算机发展对硬件有很强的依赖性&#xff0c;只有少数的个人或者机构才能拥有软件这种“奢侈品”。但随着软件行…

java 并发 随笔7 ThreadLocal源码走读

0. 刚刚见了下老朋友&#xff0c;桌球撞起来的感觉很爽 可以看到 Thread 是内部是维护了局部变量的(thread-local-map) 1. 源码走读 很多的细节都在代码块中备注了 package java.lang;// 现在回来起来&#xff0c;很多经验不太丰富的人之所以在接触、学习java.lang.thread的…