C语言-第八章:指针进阶

2024-09-07 23:28
文章标签 语言 进阶 指针 第八章

本文主要是介绍C语言-第八章:指针进阶,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

传送门:C语言-第七章:字符和字符串函数、动态内存分配

目录

第一节:常见指针

        1-1.字符指针

                1-1-1.变量字符串

                1-1-2.常量字符串

                       1-1-2-1.const 关键字

第二节:指针数组

第三节:数组指针

第四节:函数指针

第五节:函数指针数组

下期预告:


第一节:常见指针

        1-1.字符指针

        字符指针就是指向字符串的指针

char ch[]= "Hello world";
char* ptr = &ch;

        本质是把字符串的首地址放在了 ptr 中:

#include <stdio.h>int main()
{char ch[] = "Hello world";char* ptr = &ch;printf("%p\n", ptr);return 0;
}

        字符串也分为 变量字符串 和 常量字符串:

                1-1-1.变量字符串

        用字符数组定义的变量就是变量字符串,一般存储在栈上,以下就是一个变量字符串:

char ch[] = "Hello world";

        变量字符串的特点就是单独存储,即使定义两个内容相同的字符串,它也会另外开空间:

#include <stdio.h>int main()
{char ch1[] = "Hello world";char ch2[] = "Hello world";printf("%p\n", &ch1);printf("%p\n", &ch2);return 0;
}

        常量字符串还有一个特点就是它可以改变内容:

#include <stdio.h>int main()
{char ch[] = "Hello world";for (int i = 0; i < sizeof(ch)-1; i++){ch[i] = 'a';}printf("%s\n", ch);return 0;
}

        上述的代码是将字符串作为一个一个的字符进行改变,但是字符串无法作整体的改变(字符串具有常性,但是字符不具有常性):

#include <stdio.h>int main()
{char ch[] = "Hello world";ch = "ni hao shi jie"; // 报错,无法修改return 0;
}

       

                1-1-2.常量字符串

        常量字符串存储在常量区,它只能通过指针访问,这个指针还需要 const 来修饰:

#include <stdio.h>int main()
{const char* ch = "Hello world";printf("%p\n", ch);return 0;
}
                       1-1-2-1.const 关键字

        const 意味永久的、不可改变的,用它修饰的变量在初始化后无法改变其值,而 const 修饰指针有两种用法:

        1. const 在 * 之前:

        这种方式表示指针所指向的空间存储的无法通过这个指针解引用改变:

#include <stdio.h>int main()
{int a = 0;const int* ptr = &a;*ptr = 1; // 无法改变return 0;
}

 

        2. const 在 * 之后:

        可以通过指针修改指向的空间,但是指针存储的地址不能改变,也就是它的指向不能改变:

#include <stdio.h>int main()
{int a = 0;int b = 1;int* const ptr = &a;ptr = &b;return 0;
}

        常量字符串具有常性,它的值无法改变,所以要用 const 修饰的指针接收,这是一种权限的平移,即权限没有改变,如果不用 const 修饰就是一种权限的扩大,这是不安全的。

        常量字符串具有唯一性,如果指向的内容相同的 const 指针指向同一块空间:

#include <stdio.h>int main()
{const char* ptr1 = "Hello world";const char* ptr2 = "Hello world";printf("%p\n", ptr1);printf("%p\n", ptr2);return 0;
}

        它的示意图如下:

        除了用指针访问常量字符串,我们也可以直接使用常量字符串:

#include <stdio.h>int main()
{const char* ptr1 = "Hello world";const char* ptr2 = "Hello world";printf("%p\n", ptr1);printf("%p\n", ptr2);printf("%p\n", "Hello world"); // 直接使用常量字符串return 0;
}

 

        为什么可以直接得到它的地址呢?这是因为字符串存储时,它的名字就存储了自己的首元素地址,如果直接使用 "Hello world" ,在代码的编译阶段 "Hello world" 就变成了它自己的首元素地址。

        所以我们甚至可以用 [ ] 访问它的元素:

#include <stdio.h>int main()
{printf("%c\n", "Hello world"[0]);return 0;
}

 

        常量字符串还具有常性,它的值是无法改变的:

#include <stdio.h>int main()
{const char* ptr = "Hello world";ptr[0] = 'h';return 0;
}

        

        学习了变量字符串和常量字符串之后,请看以下代码,判断 ptr 和 ch 是常量字符串还是变量字符串:

#include <stdio.h>int main()
{const char ch[] = "Hello world";const char* ptr = ch;return 0;
}

        ch 是一个变量字符串,ptr 也指向一个变量字符串,虽然 ch 用 const 修饰,但是它仍然属于局部变量,存储在栈上;ptr 也是局部变量,存储在栈上,而且它得到的地址是 ch 的首元素地址,它指向 ch。

        我们可以让它们的地址与一个常量字符串的地址作比较:

#include <stdio.h>int main()
{const char ch[] = "Hello world";const char* ptr = ch;printf("变量字符串存储位置:%p\n", ch);printf("指针的存储位置:%p,指针指向的地址:%p\n", &ptr,ptr);printf("常量字符串存储位置:%p\n", "Hello world");return 0;
}

        常量字符串的存储位置与其他位置差距大,这是因为栈区与常量区距离较远。

第二节:指针数组

        指针数组是一种数组,但它的元素类型是指针。

int* p_arr[3];

         类比 int arr[3],int*是它的元素类型,p_arr 是数组名,[3] 是它的容量。

        我们可以向里面放入指针或者地址:

int main()
{int a = 0;int b = 1;int c = 2;int* p_arr[] = {&a,&b,&c}; return 0;
}

        那么 p_arr[0] 是什么呢?它就是数组中第一个元素,也是指向 a 的指针,类型是 int*,可以解引用访问和修改a:

#include <stdio.h>
int main()
{int a = 0;int b = 1;int c = 2;int* p_arr[] = {&a,&b,&c}; printf("%d\n", * p_arr[0]);return 0;
}

        我们知道数组名存储的是首元素的地址,可以认为数组名是指向首素的指针,然后指针数组的首元素又是一个指针,它们的指向关系是:

        像这种数组名间接指向 a 的指针叫做二级指针,两次解引用就可以访问到a:

#include <stdio.h>
int main()
{int a = 0;int b = 1;int c = 2;int* p_arr[] = {&a,&b,&c}; int** pptr = p_arr; // 可以用二级指针接收指针数组名printf("%d\n", **p_arr);printf("%d\n", **pptr);return 0;
}

        二级指针的定义如下:

int a = 0;
int* ptr = &a;
int** pptr = &ptr;

        当然也有三级指针、四级指针,但是用途很小。

        二级指针的一次解引用就是一级指针,可以对一级指针进行访问和修改,这种用法在链表、二叉树中常见。

第三节:数组指针

        数组指针是一种指针,它指向一个数组。

int arr[5] = { 1,2,3,4,5 };
int(*parr)[5] = &arr;

        parr是它的指针名,(*parr) 表示 parr 的类型是个指针,int [5] 表示 parr 指向的数组类型,这个数组的元素类型是 int 数组容量是 5。

        我们之前学过的二维数组名就是一个数组指针,因为二维数组名指向首元素,首元素又是个一维数组,即二维数组名指向一个一维数组,符合数组指针的概念。

        我们可以用数组指针接收二维数组名:

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

第四节:函数指针

        函数指针也是一种指针,它指向一个函数,因为函数有返回值类型、形参类型,在定义函数指针时也需要体现出来:

int Add(int x, int y)
{return x + y;
}
int (*pAdd)(int x, int y) = &Add;

        定义函数指针时也可以忽略形参名,只写上类型:

int (*pAdd)(int, int) = &Add;

        C语言规定函数的名字就是指向这个函数指针,所以初始化时的 & 符号也可以省略,即 Add 与 &Add 是等价的:

int (*pAdd)(int, int) = Add;

        我们可以使用函数指针调用函数,用法和函数名一样,加 (参数) 调用:

#include <stdio.h>
int Add(int x, int y)
{return x + y;
}
int main()
{int (*pAdd)(int x, int y) = &Add;int ret = pAdd(1,2); // 也可以是(*pAdd)(1,2)printf("%d\n",ret);return 0;
}

        pAdd 的类型是 int(*)(int,int),这意味着它能接收其他返回值类型为 int ,参数为(int,int)的函数:

int Add(int x, int y)
{return x + y;
}
int Sub(int x, int y)
{return x - y;
}
int main()
{int (*pAdd)(int x, int y) = &Add;int ret = pAdd(1,2);printf("%d\n", ret);pAdd = Sub; // 函数指针重新赋值ret = pAdd(2,1);printf("%d\n", ret);return 0;
}

        如果函数的类型不同,pAdd 也能接收,但是调用时很可能出现问题,要避免这种情况的发生。

        函数指针的类型也可以作为函数的返回值的类型,请看以下这种写法:

int Add(int x, int y)
{return x + y;
}
int(*)(int, int) returnFunPtr() // 错误写法
{return Add;
}

        直接把 int(*)(int, int) 写到返回类型的位置是不行的,编译器会不认识它,它实际上要这样写:

int(*returnFunPtr())(int, int) // 正确写法
{return Add;
}

        即把他自己的函数名和参数放到  (*) 中,这样的写法不仅不符合常识,代码的可读性也不好, 为了解决这个问题,我们需要用到 typedef 类型重定义关键字,它的作用就是给一个类型起一个别名,例如给之前讲过的基本类型取别名:

typedef int INT;
typedef char CHAR;
typedef long long DLONG;
typedef float FLOAT;

        别名就等价于原名,可以用它定义的变量的类型和原名定义的变量类型是一样的:

INT a; // 等价于 int a
sizeof(int); // 等价于 sizeof(int) 

        但是函数类型的起别名方式有点不同,要把别名放在括号中,而不是原名的后面:

typedef int(*FuncName)(int,int);

        上述代码中的 FuncName 就是 int(*)(int,int) 的别名,它可以直接放在返回值类型的位置:

typedef int(*FuncName)(int, int);
int Add(int x, int y)
{return x + y;
}
FuncName returnFunPtr()
{return Add;
}

        此时调用 returnFunPtr 我们就可以得到 Add 函数的地址:

#include <stdio.h>typedef int(*FuncName)(int, int);
int Add(int x, int y)
{return x + y;
}
FuncName returnFunPtr()
{return Add;
}
int main()
{int (*pAdd)(int, int) = returnFunPtr(); // 接收Add函数的地址int ret = pAdd(1,2);printf("%d\n", ret);return 0;
}

        那么这个返回类型为 int(*)(int,int) 的函数的类型又是什么呢?我们可以用下列两种写法不同,但是类型相同的指针接收:

FuncName(*pRFP1)() = returnFunPtr; // 有typedef
int(*(*pRFP2)())(int, int) = returnFunPtr; // 无typedef

        无 typedef 版本的写法也要把指针名放在  (*) 里面,其中最里面的 * 表示 pRFP2 的类型是个指针,空的()是 returnFunPtr 的参数,外层的 int(*)(int,int) 是 returnFunPtr 的返回值类型。

        确实很复杂,所以还是尽量采用第一种写法。

第五节:函数指针数组

        函数指针数组是存放函数指针的数组,函数指针的类型必须相同:

int(*ptr_func_arr[4])(int,int);

        解析:

        在这里有一个便捷的方法,就是把函数类型看作一个整体,放到它“本来”的位置,然后把其他部分放到 (*) 中:

         几乎所有包含函数类型的部分都可以这样做。

        当然也可以用 typedef,让它更符合我们的“审美”:

typedef int(*FuncName)(int, int); // 函数类型重定义
FuncName ptr_func_arr[4];

        使用函数指针数组可以让我们方便的调用功能相似的函数,比如写一个简单的整数计算器:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>typedef int(*FuncName)(int, int); // 函数类型重定义
int Add(int x, int y)
{return x + y;
}
int Sub(int x, int y)
{return x - y;
}
int Mul(int x, int y)
{return x * y;
}
int Div(int x, int y)
{if (y == 0) // 避免除0错误{return 0;}return x / y;
}
int main()
{FuncName ptr_func_arr[] = {Add, Sub, Mul, Div};int x, y;char oper;while (1){printf("Please enter:");scanf("%d %c %d", &x, &oper, &y);switch (oper){case '+':printf("%d\n", ptr_func_arr[0](x, y));break;case '-':printf("%d\n", ptr_func_arr[1](x, y));break;case '*':printf("%d\n", ptr_func_arr[2](x, y));break;case '/':printf("%d\n", ptr_func_arr[3](x, y));break;}}return 0;
}

  

下期预告:

        下一次是文件相关操作,主要是文件的打开、关闭,文件的读写操作的函数

传送门:C语言-第九章:文件读写

这篇关于C语言-第八章:指针进阶的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1146468

相关文章

从基础到进阶详解Pandas时间数据处理指南

《从基础到进阶详解Pandas时间数据处理指南》Pandas构建了完整的时间数据处理生态,核心由四个基础类构成,Timestamp,DatetimeIndex,Period和Timedelta,下面我... 目录1. 时间数据类型与基础操作1.1 核心时间对象体系1.2 时间数据生成技巧2. 时间索引与数据

Go语言中泄漏缓冲区的问题解决

《Go语言中泄漏缓冲区的问题解决》缓冲区是一种常见的数据结构,常被用于在不同的并发单元之间传递数据,然而,若缓冲区使用不当,就可能引发泄漏缓冲区问题,本文就来介绍一下问题的解决,感兴趣的可以了解一下... 目录引言泄漏缓冲区的基本概念代码示例:泄漏缓冲区的产生项目场景:Web 服务器中的请求缓冲场景描述代码

Go语言如何判断两张图片的相似度

《Go语言如何判断两张图片的相似度》这篇文章主要为大家详细介绍了Go语言如何中实现判断两张图片的相似度的两种方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 在介绍技术细节前,我们先来看看图片对比在哪些场景下可以用得到:图片去重:自动删除重复图片,为存储空间"瘦身"。想象你是一个

Go语言中Recover机制的使用

《Go语言中Recover机制的使用》Go语言的recover机制通过defer函数捕获panic,实现异常恢复与程序稳定性,具有一定的参考价值,感兴趣的可以了解一下... 目录引言Recover 的基本概念基本代码示例简单的 Recover 示例嵌套函数中的 Recover项目场景中的应用Web 服务器中

Java空指针异常NullPointerException的原因与解决方案

《Java空指针异常NullPointerException的原因与解决方案》在Java开发中,NullPointerException(空指针异常)是最常见的运行时异常之一,通常发生在程序尝试访问或... 目录一、空指针异常产生的原因1. 变量未初始化2. 对象引用被显式置为null3. 方法返回null

Go语言中使用JWT进行身份验证的几种方式

《Go语言中使用JWT进行身份验证的几种方式》本文主要介绍了Go语言中使用JWT进行身份验证的几种方式,包括dgrijalva/jwt-go、golang-jwt/jwt、lestrrat-go/jw... 目录简介1. github.com/dgrijalva/jwt-go安装:使用示例:解释:2. gi

Go 语言中的 Struct Tag 的用法详解

《Go语言中的StructTag的用法详解》在Go语言中,结构体字段标签(StructTag)是一种用于给字段添加元信息(metadata)的机制,常用于序列化(如JSON、XML)、ORM映... 目录一、结构体标签的基本语法二、json:"token"的具体含义三、常见的标签格式变体四、使用示例五、使用

Go语言使用slices包轻松实现排序功能

《Go语言使用slices包轻松实现排序功能》在Go语言开发中,对数据进行排序是常见的需求,Go1.18版本引入的slices包提供了简洁高效的排序解决方案,支持内置类型和用户自定义类型的排序操作,本... 目录一、内置类型排序:字符串与整数的应用1. 字符串切片排序2. 整数切片排序二、检查切片排序状态:

基于Go语言实现Base62编码的三种方式以及对比分析

《基于Go语言实现Base62编码的三种方式以及对比分析》Base62编码是一种在字符编码中使用62个字符的编码方式,在计算机科学中,,Go语言是一种静态类型、编译型语言,它由Google开发并开源,... 目录一、标准库现状与解决方案1. 标准库对比表2. 解决方案完整实现代码(含边界处理)二、关键实现细

如何合理管控Java语言的异常

《如何合理管控Java语言的异常》:本文主要介绍如何合理管控Java语言的异常问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、介绍2、Thorwable类3、Error4、Exception类4.1、检查异常4.2、运行时异常5、处理方式5.1. 捕获异常