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

相关文章

GO语言短变量声明的实现示例

《GO语言短变量声明的实现示例》在Go语言中,短变量声明是一种简洁的变量声明方式,使用:=运算符,可以自动推断变量类型,下面就来具体介绍一下如何使用,感兴趣的可以了解一下... 目录基本语法功能特点与var的区别适用场景注意事项基本语法variableName := value功能特点1、自动类型推

GO语言中函数命名返回值的使用

《GO语言中函数命名返回值的使用》在Go语言中,函数可以为其返回值指定名称,这被称为命名返回值或命名返回参数,这种特性可以使代码更清晰,特别是在返回多个值时,感兴趣的可以了解一下... 目录基本语法函数命名返回特点代码示例命名特点基本语法func functionName(parameters) (nam

从基础到进阶详解Python条件判断的实用指南

《从基础到进阶详解Python条件判断的实用指南》本文将通过15个实战案例,带你大家掌握条件判断的核心技巧,并从基础语法到高级应用一网打尽,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一... 目录​引言:条件判断为何如此重要一、基础语法:三行代码构建决策系统二、多条件分支:elif的魔法三、

Python进阶之列表推导式的10个核心技巧

《Python进阶之列表推导式的10个核心技巧》在Python编程中,列表推导式(ListComprehension)是提升代码效率的瑞士军刀,本文将通过真实场景案例,揭示列表推导式的进阶用法,希望对... 目录一、基础语法重构:理解推导式的底层逻辑二、嵌套循环:破解多维数据处理难题三、条件表达式:实现分支

Go语言连接MySQL数据库执行基本的增删改查

《Go语言连接MySQL数据库执行基本的增删改查》在后端开发中,MySQL是最常用的关系型数据库之一,本文主要为大家详细介绍了如何使用Go连接MySQL数据库并执行基本的增删改查吧... 目录Go语言连接mysql数据库准备工作安装 MySQL 驱动代码实现运行结果注意事项Go语言执行基本的增删改查准备工作

Go语言使用Gin处理路由参数和查询参数

《Go语言使用Gin处理路由参数和查询参数》在WebAPI开发中,处理路由参数(PathParameter)和查询参数(QueryParameter)是非常常见的需求,下面我们就来看看Go语言... 目录一、路由参数 vs 查询参数二、Gin 获取路由参数和查询参数三、示例代码四、运行与测试1. 测试编程路

基于Python编写自动化邮件发送程序(进阶版)

《基于Python编写自动化邮件发送程序(进阶版)》在数字化时代,自动化邮件发送功能已成为企业和个人提升工作效率的重要工具,本文将使用Python编写一个简单的自动化邮件发送程序,希望对大家有所帮助... 目录理解SMTP协议基础配置开发环境构建邮件发送函数核心逻辑实现完整发送流程添加附件支持功能实现htm

Go语言使用net/http构建一个RESTful API的示例代码

《Go语言使用net/http构建一个RESTfulAPI的示例代码》Go的标准库net/http提供了构建Web服务所需的强大功能,虽然众多第三方框架(如Gin、Echo)已经封装了很多功能,但... 目录引言一、什么是 RESTful API?二、实战目标:用户信息管理 API三、代码实现1. 用户数据

Go语言网络故障诊断与调试技巧

《Go语言网络故障诊断与调试技巧》在分布式系统和微服务架构的浪潮中,网络编程成为系统性能和可靠性的核心支柱,从高并发的API服务到实时通信应用,网络的稳定性直接影响用户体验,本文面向熟悉Go基本语法和... 目录1. 引言2. Go 语言网络编程的优势与特色2.1 简洁高效的标准库2.2 强大的并发模型2.

Go语言使用sync.Mutex实现资源加锁

《Go语言使用sync.Mutex实现资源加锁》数据共享是一把双刃剑,Go语言为我们提供了sync.Mutex,一种最基础也是最常用的加锁方式,用于保证在任意时刻只有一个goroutine能访问共享... 目录一、什么是 Mutex二、为什么需要加锁三、实战案例:并发安全的计数器1. 未加锁示例(存在竞态)