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

相关文章

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

科研绘图系列:R语言扩展物种堆积图(Extended Stacked Barplot)

介绍 R语言的扩展物种堆积图是一种数据可视化工具,它不仅展示了物种的堆积结果,还整合了不同样本分组之间的差异性分析结果。这种图形表示方法能够直观地比较不同物种在各个分组中的显著性差异,为研究者提供了一种有效的数据解读方式。 加载R包 knitr::opts_chunk$set(warning = F, message = F)library(tidyverse)library(phyl

透彻!驯服大型语言模型(LLMs)的五种方法,及具体方法选择思路

引言 随着时间的发展,大型语言模型不再停留在演示阶段而是逐步面向生产系统的应用,随着人们期望的不断增加,目标也发生了巨大的变化。在短短的几个月的时间里,人们对大模型的认识已经从对其zero-shot能力感到惊讶,转变为考虑改进模型质量、提高模型可用性。 「大语言模型(LLMs)其实就是利用高容量的模型架构(例如Transformer)对海量的、多种多样的数据分布进行建模得到,它包含了大量的先验

[MySQL表的增删改查-进阶]

🌈个人主页:努力学编程’ ⛅个人推荐: c语言从初阶到进阶 JavaEE详解 数据结构 ⚡学好数据结构,刷题刻不容缓:点击一起刷题 🌙心灵鸡汤:总有人要赢,为什么不能是我呢 💻💻💻数据库约束 🔭🔭🔭约束类型 not null: 指示某列不能存储 NULL 值unique: 保证某列的每行必须有唯一的值default: 规定没有给列赋值时的默认值.primary key:

【Linux 从基础到进阶】Ansible自动化运维工具使用

Ansible自动化运维工具使用 Ansible 是一款开源的自动化运维工具,采用无代理架构(agentless),基于 SSH 连接进行管理,具有简单易用、灵活强大、可扩展性高等特点。它广泛用于服务器管理、应用部署、配置管理等任务。本文将介绍 Ansible 的安装、基本使用方法及一些实际运维场景中的应用,旨在帮助运维人员快速上手并熟练运用 Ansible。 1. Ansible的核心概念

Flutter 进阶:绘制加载动画

绘制加载动画:由小圆组成的大圆 1. 定义 LoadingScreen 类2. 实现 _LoadingScreenState 类3. 定义 LoadingPainter 类4. 总结 实现加载动画 我们需要定义两个类:LoadingScreen 和 LoadingPainter。LoadingScreen 负责控制动画的状态,而 LoadingPainter 则负责绘制动画。

《数据结构(C语言版)第二版》第八章-排序(8.3-交换排序、8.4-选择排序)

8.3 交换排序 8.3.1 冒泡排序 【算法特点】 (1) 稳定排序。 (2) 可用于链式存储结构。 (3) 移动记录次数较多,算法平均时间性能比直接插入排序差。当初始记录无序,n较大时, 此算法不宜采用。 #include <stdio.h>#include <stdlib.h>#define MAXSIZE 26typedef int KeyType;typedef char In

从0到1,AI我来了- (7)AI应用-ComfyUI-II(进阶)

上篇comfyUI 入门 ,了解了TA是个啥,这篇,我们通过ComfyUI 及其相关Lora 模型,生成一些更惊艳的图片。这篇主要了解这些内容:         1、哪里获取模型?         2、实践如何画一个美女?         3、附录:               1)相关SD(稳定扩散模型的组成部分)               2)模型放置目录(重要)

C语言 | Leetcode C语言题解之第393题UTF-8编码验证

题目: 题解: static const int MASK1 = 1 << 7;static const int MASK2 = (1 << 7) + (1 << 6);bool isValid(int num) {return (num & MASK2) == MASK1;}int getBytes(int num) {if ((num & MASK1) == 0) {return