指针(四)

因为前期在学驱动,所以花了一天时间借鉴了别的资料,把本科学的C语言捡起来。

指针的基本概念

堆栈有栈顶指针,队列有头指针和尾指针,这些概念中的"指针"本质上是一个整数,是数组的索引,通过指针访问数组中的某个元素,经过学习我们在间接寻址那里看到了另一个指针的概念,把一个变量所在的内存单元的地址保存在另外一个内存单元中,保存地址的这个内存单元称为指针,通过指针和间接寻址访问变量,这种指针在C语言中可以用一个指针类型的变量表示,例如某程序中定义了以下全局变量:

int *pi=&i;表示定义一个指向int型的指针变量pi,并用i的地址来初始化pi.

int i;
int *pi = &i;
char c;
char *pc = &c;

在这里插入图片描述
注意:
后面两行代码定义了一个字符型变量c和一个指向c的字符型指针pc,注意pi和pc虽然是不同类型的指针变量,但它们的内存单元都占4个字节,因为要保存32位的虚拟地址,同理,在64位平台上指针变量都占8个字节。

pi是int型的,pc是char型的,pi=pc;这样赋值就是错误的。但是可以先强制类型转换然后赋值:

pi = (int *)pc;

在这里插入图片描述
但是:
**
现在pi指向的地址和pc一样,但是通过pc只能访问到一个字节,而通过pi可以访问到4个字节,后3个字节已经不属于变量c了,除非你很确定变量c的一个字节和后面3个字节组合而成的int值是有意义的,否则就不应该给pi这么赋值。因此使用指针要特别小心,很容易将指针指向错误的地址。
**
PI的地址和PC的地址一样了,这样就导致了可以访问的只有一个字节的地址了。原来是4字节。

为什么会出现野指针?

因为堆栈上分配变量的地址是随机的,也就是指针变量P所指向的内存地址也是不确定的。所以这种指向不确定地址的指针成为 “ 野指针 ”,为了避免野指针,在定义指针变量时就应该给它明确的初值,或者把它初始化位NULL:

int main(void)
{int *p = NULL;...*p = 0;...
}

空指针

把地址0转换成指针类型。
它的特殊之处在于,操作系统不会把任何数据保存在地址0及其附近,也不会把地址0-0xfff的页面映射到物理内存,所以任何对地址0的访问都会立刻导致段出错。*p=0;会导致段错误,就像放在眼前的炸弹一样很容易被找到。

void* 指针

ANSI在将C语言标准化时引入了void类型,void指针与其他类型的指针之间可以隐式转换,而不必用类型转换运算符。注意,只能定义void指针,而不能定义void型的变量,因为void指针和别的指针一样都占4个字节,而如果定义void型变量,编译器不知道该分配几个字节给变量。

void func(void *pv)
{/* *pv = 'A' is illegal */char *pchar = pv;*pchar = 'A';
}
int main(void)
{char c;func(&c);printf("%c\n", c);

指针类型的参数和返回值

#include <stdio.h>
int *swap(int *px, int *py)
{int temp;temp = *px;*px = *py;*py = temp;return px;
}
int main(void)
{int i = 10, j = 20;int *p = swap(&i, &j);printf("now i=%d j=%d *p=%d\n", i, j, *p);return 0;
}

通过地址访问到内部变量,将内部变量进行交换。

指针和数组

int a[10];
int *pa = &a[0];
pa++;

在这里插入图片描述
在函数原型中,如果参数是数组,则等价于参数是指针的形式,例如:

void func(int a[10])
{...
}

等价于

void func(int *a)
{...
}

等价于

void func(int a[])
{...
}

指针与const 限定符

  1. 如下
const int *a;
int const *a;

a是一个指向const int型的指针,a所指向的内存中的内容是不可改变的,所以(*a)++是不允许的,但a可以改写,所以a++是允许的。
2. 如下

int * const a;

a是一个指向int型的const指针,*a是可以改写的,但a不允许改写。
3.

int const* const a;

a是一个指向const int型的const指针,因此*a和a都不允许改写。
指向非const变量的指针或者非const变量的地址可以传给指向const变量的指针,编译器可以做隐式类型转换,例如:

char c = 'a';
const char *pc = &c;

但是,指向const变量的指针或者const变量的地址不可以传给指向非const变量的指针,以免投过后者意外改写了前者所指向的内存单元,例如对下面的代码编译器会报警告:

const char c = 'a';
char *pc = &c;

即使不用const限定符也能写出正确的程序,但良好的编程习惯应该尽可能多的使用const,因为:

1.const给读代码的人传达非常有用的信息。比如一个函数的参数是const char *,你在调用这个函数时就可以放心地传给它char *或const char *指针,而不必担心指针所指的内存单元被改写。

2.尽可能多地使用const限定符,把不该变的都声明成只读,这样可以依靠编译器检查程序中的Bug,防止意外改写数据。

3.const对编译器优化是一个有用的提示,编译器也许会把const变量优化成常量。

指针与结构体

首先定义一个结构体类型,然后定义这种类型的变量和指针:

struct unit {char c;int num;
};
struct unit u;
struct unit *p = &u;

要通过指针p访问结构体成员可以写成(*p).c和(*p).num,为了书写方便,C语言提供了->运算符,可以写成p->c和p->num。

指向指针的指针与指针数组

也就是二级指针:

int i;
int *pi = &i;
int **ppi = &pi;

我们知道main函数的标准原型应该是int main(int argc,char *argv[]);argc是命令行参数的个数,而argv是一个指向指针的指针,为什么不是指针数组?因为前面讲过,函数原型中的[]表示指针而不表示数组,等价于char **argv.那为什么要写成char *argv[]而不写成char **argv?这样写给读代码的人提供了有用的信息,argv不是指向单个指针,而是指向一个指针数组的首元素。数组中每个元素都是char *指针,指向一个命令行参数字符串。

打开命令行参数

#include <stdio.h>
int main(int argc, char *argv[])
{int i;for(i = 0; i < argc; i++)printf("argv[%d]=%s\n", i, argv[i]);return 0;
}

编译:

$ gcc main.c
$ ./a.out a b c
argv[0]=./a.out
argv[1]=a
argv[2]=b
argv[3]=c
$ ln -s a.out printargv
$ ./printargv d e 
argv[0]=./printargv
argv[1]=d
argv[2]=e

在这里插入图片描述由于argv[4]是NULL,我们也可以这样循环遍历argv:

for(i=0; argv[i] != NULL; i++)

NULL标识着argv的结尾,这个循环碰到NULL就结束,因而不会访问越界,这种用法很形象地称为Sentinel,NULL就像一个哨兵守卫着数组的边界。

指向数组的指针与多维数组

指向数字的指针其实前面讲过的数组指针。

int (*a)[10];

int (*a)[10]可以拆分成

typedef int t[10];
t *a;

t代表由10个int组成的数组类型,a则是指向这种类型的指针。
也就是:

int a[10];
int (*pa)[10] = &a;

a是一个数组,在&a这个表达式中,数组名做左值,取整个数组的首地址赋给指针pa。注意,&a[0]表示数组a的首元素的首地址,而&a表示数组a的首地址,显然这两个地址的数值是相同的,但这两个表达式的类型是两种不同的指针类型,前者的类型是int ,而后者的类型是int()[10]。

函数类型和函数指针类型

在C语言中,函数也是一种类型,可以定义指向函数的指针。我们知道,指针变量的内存单元存放一个地址值,而函数指针存放的就是函数的入口地址(位于.text段)。下面看一个简单的例子:

函数指针:

#include <stdio.h>
void say_hello(const char *str)
{printf("Hello %s\n", str);
}
int main(void)
{void (*f)(const char *) = say_hello;f("Guys");return 0;
}

分析一下变量f的类型声明void(*f)(const char ),f首先跟号结合在一起,因此是一个指针。(*f)外面是一个函数原型的格式,参数是const char *,返回值是void,所以f是指向这种函数的指针。而say_hello的参数是const char *,返回值是void,正好是这种函数,因此f可以指向say_hello。注意,say_hello是一种函数类型,**而函数类型和数组类型类似,做右值使用时自动转换成函数指针类型,所以可以直接赋给f,当然也可以写成void (*f)(const char *) =&say_hello;**把函数say_hello先取地址再赋给f,就不需要自动类型转换了。

可以直接通过函数指针调用函数,如上面的f(“Guys”),也可以先用*f取出它所指的函数类型,再调用函数,即(*f)(“Guys”)。可以这么理解:函数调用运算符()要求操作数是函数指针,所以f(“Guys”)是最直接的写法,而say_hello(“Guys”)或(*f)(“Guys”)则是把函数类型自动转换成函数指针然后做函数调用。

文章来源:
https://blog.csdn.net/m0_58367586/article/details/128612310

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

微信小程序收款手续费多少,怎么降低?

在竞争激烈、各行各业都如此内卷的当下&#xff0c;商家需要不断寻求提高利润的方法。所谓开源节流&#xff0c;既要学会提高利润率&#xff0c;也要学会节省成本&#xff0c;毕竟钱是挣出来的&#xff0c;同时也是省出来的。作为一个经常使用收款工具的商家&#xff0c;很多人…

命令查询pg 数据库版本,并且分析结果行各代表什么意思

目录 1 问题2 实现 1 问题 命令查询pg 数据库版本&#xff0c;并且分析结果行各代表什么意思 2 实现 SELECT version(); PostgreSQL 11.7 (Debian 11.7-2.pgdg1001) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit这是一条关于 PostgreSQL 数据库…

Java 线程池到底是如何复用线程的

原理概述 其实 Java 线程池的实现原理很简单&#xff0c;说白了就是一个线程集合 workerSet 和一个阻塞队列 workQueue。 当用户向线程池提交一个任务时&#xff0c;线程池会先将任务放入 workQueue 中。workerSet 中的线程会不断的从 workQueue 中获取线程然后执行。当 work…

软件设计中如何画各类图之六状态图:生动呈现对象生命周期状态转换的重要工具

目录 1 状态图简介2 状态图的符号及说明2.1 状态&#xff08;State&#xff09;2.2 转移&#xff08;Transition&#xff09;2.3 起始状态与终止状态2.4 动作&#xff08;Action&#xff09; 3 画状态图的步骤3.1 确定对象3.2 定义状态3.3 标识转移3.4 标注动作3.5 添加起始和结…

L1-026:I Love GPLT

题目描述 这道超级简单的题目没有任何输入。 你只需要把这句很重要的话 —— “I Love GPLT”——竖着输出就可以了。 所谓“竖着输出”&#xff0c;是指每个字符占一行&#xff08;包括空格&#xff09;&#xff0c;即每行只能有1个字符和回车。 输入样例&#xff1a; 无输出样…

Java中lambda表达式的使用

&#x1f495;"我不要麻木的死去"&#x1f495; 作者&#xff1a;Mylvzi 文章主要内容&#xff1a;Java中lambda表达式的使用 一.背景 lambda表达式是Java SE 8中一个重要的新特性&#xff0c;允许你使用一个表达式来代替功能接口。lambda表达式可以看作一个没有返…

Linux | tar,bc,uname指令

Linux | tar&#xff0c;bc&#xff0c; 文章目录 Linux | tar&#xff0c;bc&#xff0c;tar指令【重要】bc指令uname –r指令 tar指令【重要】 tar [-cxtzjvf] 文件与目录 … 参数&#xff1a; -c &#xff1a;建立一个压缩文件的参数指令(create 的意思)&#xff1b;-x &am…

某60区块链安全之薅羊毛攻击实战一学习记录

区块链安全 文章目录 区块链安全薅羊毛攻击实战一实验目的实验环境实验工具实验原理实验内容薅羊毛攻击实战一 实验步骤EXP利用 薅羊毛攻击实战一 实验目的 学会使用python3的web3模块 学会分析以太坊智能合约薅羊毛攻击漏洞 找到合约漏洞进行分析并形成利用 实验环境 Ubun…

SQL Server权限管理与数据恢复

SQL Server的安全机制 SOL Server 的安全性是建立在认证和访问许可两种安全机制之上的&#xff0c;其中&#xff0c;认证用来确定登录 SQlL Server 的用户的登录账户和密码是否正确&#xff0c;以此来验证其是否具有连接 SQL. Server的权限&#xff1a;访 问许可用来授予用户或…

SpringSecurity(五)

深入理解HttpSecurity的设计 一、HttpSecurity的应用 在前章节的介绍中我们讲解了基于配置文件的使用方式&#xff0c;也就是如下的使用。 也就是在配置文件中通过 security:http 等标签来定义了认证需要的相关信息&#xff0c;但是在SpringBoot项目中&#xff0c;我们慢慢脱离…

SpringBoot3.x代码生成器构建的三层架构主启动类报错

【场景复现】 jdk21环境变量springboot3.x、mybatisplus generator3.5.3构建工程启动&#xff1a; 【原因分析】 显示 factoryBeanObjectType 属性的值类型不正确&#xff0c;应该是一个实现了 FactoryBean 接口的类的全限定名。 注解注入每层对象&#xff0c;找不到Factory…

目标检测——SPPNet算法解读

论文&#xff1a;Spatial Pyramid Pooling in Deep Convolutional Networks for Visual Recognition 作者&#xff1a;Kaiming He, Xiangyu Zhang, Shaoqing Ren, and Jian Sun 链接&#xff1a;https://arxiv.org/abs/1406.4729 目录 1、算法概述2、Deep Networks with Spatia…