C语言教程-13_1-初识指针

news2024/9/27 21:26:14

title: C语言教程-13_1-初识指针
tags: [C]
categories: C语言教程
description: 接触C语言的灵魂-指针

概要:

  1. 简要讲解内存地址与内存模型
  2. 简单介绍C语言的指针这一数据类型
  3. 掌握指针相关最基本的两种互逆运算

前置知识:

  1. 理解能力和想象能力
  2. 耐心和实验精神
  3. 数组与函数的知识

交换两个变量的问题

我们从一个问题开始引入指针.

考虑这个问题:在main()函数中有两个int变量a和b,我该如何交换这两个变量的值?

如果我们要求仅在一个函数中解决这个问题,那么很容易想到,最简单的办法就是新建一个int类型的中间变量,比如命名为temp.那么我们就有如下操作进行交换(十分简单不详细解释):

#include <stdio.h>
int main() {
    int a=3,b=4;
    int c; // 中间变量
    // 经典3步进行交换
    c = a;
    a = b;
    b = c;
    printf("a=%d,b=%d\n",a,b); // 输出结果 a=4,b=3   
    return 0;
}

只需要这3步即可进行交换.


现在问题来了,如果我们要求创建一个函数swap()来实现这个操作,该如何实现?

也许我们可以这样:

#include <stdio.h>
void swap(int a, int b) {
    int temp;
    temp = a;
    a = b;
    b = temp;
}
int main() {
    int a = 3, b = 4;
    swap(a, b);
    printf("a=%d,b=%d\n", a, b); // 输出错误的结果:a=3,b=4   
    return 0;
}

我们尝试简单地把a和b传递给swap()函数,运行一下,结果显然是错误的,a和b的值并没有交换.

回顾前面函数的知识,前面讲过,C函数的参数都是按值传递,这里也就是将main()中的a和b的值简单地复制给swap()的两个参数a和b,换句话说,此a,b非彼a,b.

结果就是,在swap()中的a,b确实被成功地交换了,但是main()中的a,b完全没有变化.


我们想要在swap()函数中交换main()中的a和b,根本的问题在于我们需要访问到他们,C语言的指针类型提供了这种功能.

地址和指针

计算机内存与地址

计算机运行时需要的各种数据都存储于内存中(就是平时说的内存条),从逻辑上来看,一整个内存可以视为一个超级巨大的数组,例如我们的内存是4GB,那么这个数组的总大小就是4GB.我们仅仅讲解内存地址这个概念,具体的内存结构这里并不关心.

程序的相关数据就存储在内存中,例如执行的机器代码,局部变量,全局变量(定义在函数外的变量),常量字面值.他们以某种特定的模式进行存储,存储的位置各不相同,为了找到他们,我们需要以字节为单位为整个内存进行编号.也就是所谓的内存地址.需要注意的是,我们通常以16进制来表示地址值(毕竟内存如此巨大,2,10进制是不够方便的).

以4GB内存举例,我们需要8个16进制位来完整编号,即16^8==4,294,967,296,也就是4GB的大小.从0开始,第一个字节编号为0x00000000,最后一个字节编号为0xFFFFFFFF.当然,如此庞大的内存范围不可能全部让我们任意取用,实际上我们自己的应用程序只能使用操作系统(例如Windows)规定的一块内存,当然这完全够用.

例如,我们在内存地址0x2~0x8存储了一些特定的数据,内容如下:

image-20231121002855398

可以看到每一个地址都指向内存中的一个特定字节,这个1字节大小的空间中存储了某些特定的数据,程序根据数据的地址从内存中找到他们,以进行运算.


另一方面,尽管我们对每一个字节都进行了编号,但是往往我们将若干个字节组合起来使用,例如一个int变量,就直接占用了4个字节,此时我们将4个连续的字节视为一个整体,取最开头的那个字节作为代表,指代整个int变量.

例如我们有int i=2;则i在内存中的布局如下:

image-20231121004333759

注意,我们上面的地址值仅仅作为演示,在现在流行的x64机器中并不是这样的:

#include <stdio.h>
int main() {
    int i = 2;
    // 使用循环,逐字节输出i在内存中的值(十六进制),同时输出每个字节的地址
    for (int j = 0; j < sizeof(i); j++) {
        printf("0x%p %02x\n", (char *)&i + j, *((char *)&i + j));
    }
    return 0;
}

在我的笔记本电脑运行如下:

image-20231121004620266

先不管上面的代码是什么原理,仅仅看一下结果,输出了i占用的这4个字节内部的值,同时可以看到前面的内存是连续的.如果我们使用printf("%p",&i);输出i的地址,结果将会是第一个地址,这意味着使用最小的那个地址指代整个变量i.

读者无需关心为什么是02 00 00 00而不是00 00 00 02,这涉及到大小端序的问题.

处理地址-C语言的指针

现在我们已经了解了最基本的内存常识,并举了一个int变量的例子,了解了变量的存储.下面引入指针.

用最简单的一句话概括指针就是:指针就是地址.我们有时候需要在程序中获取到某个变量的地址,C语言提供了指针这一数据类型,用指针类型声明的变量就叫指针变量,其内部存储一个无符号的整数(往往是4或8字节大小),代表一个地址.

顾名思义,指针,就像一个箭头指向一个地方,和地址的作用相同.只不过前面说的地址是指计算机内存的编址,而指针,是C语言为了能够处理内存地址而引入的一种机制.某种角度而言,地址值仍然是一个整数,所以我们想要存储他,和普通的整数无异,但是为了特殊化,C语言引入了指针类型这种数据类型,这种(类)类型的变量存储的是一个特殊整数代表一个指针(地址).


获取一个变量的地址十分简单,使用&取地址运算符,输入一个指针也很简单,在printf()中使用%p即可:

#include <stdio.h>
int main() {
    int i = 3; // 声明并初始化为3一个int类型变量i
    printf("%p",&i); // &i这个表达式代表获取到i在内存中的地址
    return 0;
}

输出如下:

image-20231121005931082

这代表着变量i就存储在这个地址值指向的内存块中.


如果我们想要将这个地址存储下来,那么就需要使用C语言的指针.

声明一个最简单的指针变量仅仅需要在变量名前多加一个*,语法如下:

#include <stdio.h>
int main() {
    int i = 3; // 声明并初始化为3一个int类型变量i
    int *p; // 声明一个变量p,其类型为int*,代表它是一个指针
    p = &i; // 将i的地址赋值给p
    printf("%p",p); // 此时不再需要&,因为p存储的就是i的地址
    return 0;
}

输出结果同样,也是一个地址.


再进一步,我们既然存储了某个变量的地址(指针),那么就意味着我们想要根据这个地址(指针)去访问其指向的内存单元,我们使用另一种运算符,即*解引用运算符(或者叫指针运算符),与&相反,*用于对一个地址进行访问,反向获取到此处存储的具体变量(值),这种相对于取地址操作的逆操作称为解引用操作.

仍然是上面的例子,我们尝试使用p存储的地址去间接访问变量i:

#include <stdio.h>
int main() {
    int i = 3; // 声明并初始化为3一个int类型变量i
    int *p; // 声明一个变量p,其类型为int*,代表它是一个指针
    p = &i; // 将i的地址赋值给p
    printf("%d",*p); // 此时对p的值进行*运算,也就是解引用,效果就是将p中的值作为一个
    // 指针,去访问对应的内存,从而取出变量i的值
    // 换句话说,这里的*p和i是等价的.
    return 0;
}

运行结果:

image-20231121011323454

注意:在对一个指针进行解引用时,一定要确保其指向了有效的地址!!!这是一个十分重要的问题!对未正确赋值的指针进行解引用(访问)是十分危险的行为.

指针声明的问题

我们在声明指针的时候,一定要注意和普通类型进行区分.普通类型与其对应的指针可以在一个声明中出现:

int *p1,a,*p2;

这里声明了2个int*类型的指针p1和p2,和一个int类型的变量a.

要注意的是,p2前面仍然需要一个*代表它是一个指针,另外,尽管p1前面已经有了一个*,但是a仍然仅仅是一个int的变量而已.

解决交换问题

现在我们可以尝试使用指针进行交换两个变量的值.

我们既然要使用函数交换两个变量,那么就要求函数能够访问到这两个变量,现在,我们可以使用两个指针参数实现.

// 在函数中使用指针进行交换两个变量的值
#include <stdio.h>
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}
int main() {
    int a = 10, b = 20;
    printf("a = %d, b = %d\n", a, b);
    swap(&a, &b);
    printf("a = %d, b = %d\n", a, b);
    return 0;
}

我们的swap()函数的两个参数不再是两个int类型的参数,而是int *类型的指针,代表着这个指针可以指向一个int变量.

main()函数中,swap(&a,&b);使用&取地址运算符计算a和b的地址,传递给swap的两个形参.

接下来,在swap()中使用一个中间变量(temp仍然需要),进行交换,对指针变量使用*进行解引用,获取到两个要交换的int值,然后进行交换即可.

运行结果如下:

image-20231124172141354


上面的例子我们了解了如下内容:

  1. 如何获取一个地址(取地址运算符)
  2. 如何存储一个地址(指针变量)
  3. 如何使用一个地址去访问内存(解引用运算符)

接下来探究指针类型.

指针的类型

不同基本类型的指针

前面的例子都是使用了int *这个类型,代表着对应的指针变量(应该)指向的是一个int类型的变量.

其他类型的变量同理,如果我们需要指向一个float类型的变量,那么就使用float *即可:

// 其他类型的变量同理,如果我们需要指向一个float类型的变量,那么就使用`float *`即可:
#include <stdio.h>
int main(){
    float var = 3.1415;
    float *ptr = &var;
    printf("var == %f\n", var);
    return 0;
}

使用ptr指针就能访问到var.

此外,指针类型不匹配是不允许的操作:

// 此外,指针类型不匹配是不允许的操作
#include <stdio.h>
int main(){
    int a = 10;
    int *p = &a;
    char *q = p; // 这里报错: cannot convert 'int*' to 'char*' in initialization
    return 0;
}

代表着两个指针不兼容,即一个int *的指针不能赋值给一个char *的指针.

空类型指针

有时候,我们可能仅仅想要存一个地址,而不关心其类型,那么可以使用void *类型,即空类型指针,任何类型的指针都能赋值给void *:

#include <stdio.h>
int main() {
    int a = 1024;
    void *p = &a; // &a为指针,其类型为int*,可以直接赋值给void*而无需任何处理
    printf("%d\n", *(int *)p); // void*指针不允许直接解引用,必须进行强制类型转换
    return 0;
}

上面的代码使用void*指针p存储了a的地址,在使用p访问a的时候,必须使用强制类型转换void*转换为int*才能进行解引用.因为对void*解引用的话,无法判断实际占用了多少内存,所以下面的代码编译器报错"不允许使用不完整的类型":

#include <stdio.h>
int main() {
    int a = 1024;
    void *p = &a;
    printf("%d\n", *p);
    return 0;
}
image-20231124231605327

这就是一个重要的问题:指针指向的数据类型的大小.

后面我们会慢慢的接触到void* 指针的重要作用.

特殊的指针值-NULL

还有的时候,我们希望一个指针变量不指向任何有效的地址,那么我们可以对其赋值为NULL空指针值.

#include <stdio.h>
int main() {
    int *p = NULL;
    printf("%p\n", p);
    // 实际上, NULL 就是 0
    return 0;
}

运行结果:

image-20231124232132101

可以看到,p的值就是0. NULL是一个(宏定义),定义在stdio.h中(宏定义将在后面的头文件部分讲解):

// stdio.h
#define NULL ((void *)0)

这个宏意味着NULL预处理的时候直接替换为((void *)0)

也就是说,当一个指针值为NULL时,我们认为他不指向任何地址,并且认为NULL是安全的—我们检查一个指针是否等于NULL来判断这个指针是否被初始化等…

后面会深入强调初始化的问题.

强制类型转换

指针本质上还是一个整数(无符号的),但是指针类型仍然不能和普通的整型互相赋值,如果我们想要将某个数值作为指针值进行赋值,可以使用强制类型转换.

int value=0x7fffffff;
int *p = (int *)value;

这样,指针p就指向了0x7fffffff这个内存地址对应的内存单元.

此外,这里也能看出,intint*是完全不同的两种类型!

多级指针

指针用于指向某种类型的变量(地址),同样,指针变量也可以被另外一个指针变量所指向,即指向指针的指针,这就是多级指针.

使用二级指针

可以这样声明一个二级指针:

#include <stdio.h>

int main(){
    int a = 3,*p = &a;
    int **p2 = &p; // p2是一个二级指针,指向p
    printf("%d\n",**p2); // 输出3, **p2 == *p == a == 3
    return 0;
}

运行结果:

image-20240111225130328

声明int **p2就等价于int *(*p2);,也就是说p2是一个指针,指向的类型为int*,因此显然p2是一个二级指针,int *p可以称为一级指针.

二级指针仍然是一个指针,只不过我们可以对它进行2次解引用:

#include <stdio.h>

int main() {
    int a = 3, *p = &a; // 声明一个int类型的指针变量p,指向a
    int **p2 = &p; // 声明一个int类型的二级指针变量p2,指向p
    /* 输出a的值 */
    printf("a = %d\n", a); // a = 3
    printf("*p = %d\n", *p); // *p = 3
    printf("**p2 = %d\n", **p2); // **p2 = 3
    /* 输出a的地址 */
    printf("&a = %p\n", &a); // &a即为a的地址
    printf("p = %p\n", p); // p存储的值即为a的地址
    /* 输出指针变量p的地址 */
    printf("&p = %p\n", &p); // &p即为p的地址
    printf("p2 = %p\n", p2); // p2存储的值即为p的地址
    return 0;
}

运行结果:

image-20240111225215625

对二级指针的解引用

我们可以看出,二级指针可以进行2次解引用,第一次解引用的结果是访问其指向的变量,例如上面的例子中,

*p2即为p,p仍然是一个指针,指向整型变量a,则对其再次解引用**p即可访问到a.

换言之,**p2可以视为*(*p2),读者应该清楚地意识到,这里的**完全是2步操作,你甚至可以在中间加一个空格.

#include <stdio.h>

int main() {
    int a = 3, *p = &a; // 声明一个int类型的指针变量p,指向a
    int **p2 = &p; // 声明一个int类型的二级指针变量p2,指向p
    /* 输出a的值 */
    printf("* *p2 = %d\n", * *p2); // **p2 = 3
    
    return 0;
}

运行结果:

image-20240111225239103

以上使用二级指针进行了举例,三级指针等更"高级"的指针同理,只不过可以指向级数更高的指针而已,实际应用中,基本只用到二级指针.

#include <stdio.h>

int main() {
    int a = 3, *p = &a; // 声明一个int类型的指针变量p,指向a
    int **p2 = &p; // 声明一个int类型的二级指针变量p2,指向p
    int ***p3 = &p2; // 声明一个int类型的三级指针变量p3,指向p2
    return 0;
}

二级指针十分重要,特别是在使用C实现各种数据结构时,需要修改某些指针的指向时非常关键,后面的学习会频繁遇到.


指针是C语言的"灵魂",指针的内容几乎占有了C语言的半壁江山,本部分简单讲解了指针的基本概念和使用方法,后面会详细展开讲解.

---WAHAHA

注:文章原文在本人博客https://gngtwhh.github.io/上发布

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ES(Elasticsearch)可视化界面-浏览器插件

安装 支持Micrsoft Edge、谷歌、火狐浏览器 此处我以IE为例 使用 extension://aonamamifdfigcflbeokdndfappnmogo/es-client/index.html?td_channelidchrome#/more/about 输入相关连接信息即可 rest client语法 和kibana的开发者工具查询方式一致,可以参考我另一篇文章Kiba…

Python生成指定数量的随机XML文件

我的需求是随机生成18位数字的XML文件名&#xff0c;其中前12位数字是随机数&#xff0c;后6位是时间信息 其中XML中写入CSN的tag值&#xff0c;代码如下 import os import random import time import xml.etree.ElementTree as ETdef generate_random_filename():random_part…

光耦合器的简要揭秘

光耦合器&#xff0c;也称为光隔离器或光电耦合器&#xff0c;是电子器件中必不可少的元件&#xff0c;它提供了一种在电路隔离部分之间传输电信号的方法。这种隔离对于保护系统的敏感部分免受高压尖峰或电气噪声的影响至关重要&#xff0c;这使得光耦合器在许多应用中成为一种…

Linux内核编程(十四)IIC总线驱动FT5X06触摸屏

本文目录 前述&#xff1a;一、IIC子系统框架二、I2C设备驱动层1. i2c_client编写&#xff08;C语言版-旧内核&#xff09;2. i2c_client编写&#xff08;设备树版-新内核&#xff09; 前述&#xff1a; 对于IIC的基础知识&#xff0c;这里不做过多的介绍&#xff0c;详细情况…

智能地理信息系统平台应该是什么样子?

现在GIS平台除了三维GIS属于重大突破&#xff0c;这些年基本上都属于蹭热点概念&#xff0c;并在这些热点概念之间左右逢源&#xff0c;究其本质&#xff0c;还是在于没有把握好GIS的立足之本与用户之间的巨大鸿沟。回归到题目上&#xff0c;智能地理信息系统平台&#xff0c;从…

钣金展开计算工具【机械设计工具集】

一款非常实用的计算器工具&#xff0c;它可以帮助用户计算直角弯曲展开长度的工具&#xff0c;无需直角弯曲展开长度计算公式&#xff0c;选择对应的图形&#xff0c;输入已知的数据、查询α系数并输入&#xff0c;就可以快速计算出长度了&#xff01; 方便钣金件下料长度的计算…

Uniapp:WebSocket 重连之后累加触发 uni.onSocketOpen()

省流 不要用 uni.xxx 那一套&#xff0c;用 socketTask await uni.connectSocket({}) 的 socketTask 去控制 业务逻辑描述 第一次进入应用主页&#xff0c;连接 WebSocket手机熄屏之后&#xff0c;断开当前连接的 WebSocket手机亮屏之后&#xff0c;再次进入应用后&#x…

SpringBoot异常处理原理分析

springboot默认机制 错误处理的自动配置都在ErrorMvcAutoConfiguration中&#xff0c;两大核心机制&#xff1a; SpringBoot 会自适应处理错误&#xff0c;响应页面或JSON数据 SpringMVC的错误处理机制依然保留&#xff0c;MVC处理不了&#xff0c;才会交给boot进行处理 发生…

《黑神话:悟空》一只横扫全球的中国“猴子”,这里也有!

这个夏天&#xff0c;除了火辣辣的太阳让人燥热难耐&#xff0c;还有一只横空出世的“猴子”让众多网友热血沸腾——8月20日&#xff0c;筹谋7年的首款国产现象级3A游戏大作《黑神话&#xff1a;悟空》&#xff0c;准时登录各大平台&#xff0c;期待已久的玩家们一饱“猴瘾”。…

2024年第四届《英语世界》杯全国大学生翻译大赛

2024年第四届《英语世界》杯全国大学生翻译大赛 第一场下周日开考&#xff01; 一、参赛福利&#xff1a; 1、报名即可获得大赛专属题库、《英语世界》数字刊阅读权限、《英语世界》杯系列赛事公开课珍贵资料&#xff1b; 2、开展线上公开课邀请名师讲解&#xff1b; 3、获…

linux之网络子系统-MAC帧、数据报、段 的头部信息

一、MAC帧 格式 MAC帧是属于链路层&#xff0c;网卡发送数据的格式。 MAC帧主要有两种格式&#xff0c;一种是以太网V2标准&#xff0c;一种是IEEE 802.3&#xff0c;常用的是前者。 DMAC&#xff08;Destination MAC&#xff09;是目的MAC地址。DMAC字段长度为6个字节&#…

突破速度障碍:探索25MBd数字光耦合器在工业自动化中的作用

在快节奏的工业自动化世界中&#xff0c;对能够跟上高速运行同时保持可靠性和安全性的组件的需求至关重要。这些系统中最关键的组件之一是光耦合器&#xff0c;它在机器的不同部分或机器之间传输信号时提供电气隔离。25MBd数字光耦合器的推出代表了该领域的重大进步&#xff0c…

18959 二叉树的之字形遍历

### 思路 1. **输入读取**&#xff1a; - 读取输入字符串&#xff0c;表示完全二叉树的顺序存储结构。 2. **构建二叉树**&#xff1a; - 使用队列构建二叉树&#xff0c;按层次顺序插入节点。 3. **之字形层序遍历**&#xff1a; - 使用双端队列进行层序遍历&…

为何要引入服务注册组件及组件对比

我的后端学习大纲 SpringCloud学习大纲 1.为什么要引入服务注册中心&#xff1a; 1.1.原因说明 1.微服务所在的IP地址和端口号硬编码到订单微服务中&#xff0c;属于硬编码&#xff0c;会存在非常多的问题 如果订单微服务和支付微服务的IP地址或者端口号发生了变化&#xff…

idea配置FTP文件上传

idea配置FTP 连接测试 打开 工具拦打开 maven依赖 <!-- FTP --> <dependency><groupId>commons-net</groupId><artifactId>commons-net</artifactId><version>3.8.0</version> </dependency>FTP-配置 /*** FTP-配置*/ …

数字时代的内容安全治理:审核与管理的艺术

《互联网内容审核与信息安全管理》提供了全面实施互联网内容审核与信息安全管理的方法&#xff0c;主要包括三部分内容。 第一部分&#xff1a;阐释什么是互联网内容审核与信息安全管理&#xff0c;为什么要进行互联网内容审核与信息安全管理&#xff1b;重点分析互联网内容…

vs 项目.gitignore设置过滤某个文件夹无效

问题描述 项目使用tfs进行管理&#xff0c;在使用uniapp开发小程序的时候&#xff0c;每次vs中的更改都会出现99的更改&#xff0c;查看详情发现都是uniapp下面的unpackage文件夹下面的内容。原因是每次重新运行该下面的文件都会重新生成。后来在该项目下面的.gitignore中配置…

【Vue】Echart渲染数据时页面不显示内容

背景 做的一个对话交互的功能&#xff0c;根据后台返回的数据&#xff0c;渲染成Echart图表展示因为图表种类多&#xff0c;因此根据不同图表单独做了一个个vue组件&#xff0c;将数据根据展示类型传到这些子组件中进行渲染无论哪种图表&#xff0c;第一次展示时都能正常展示&…

2024实战指南:四款全免费的数据恢复工具盘点!

在这个数字化的时代里&#xff0c;数据的安全至关重要。如果一不小心删除或丢失了重要数据应该怎么办呢&#xff1f;这几个全免费的数据恢复工具可以帮你解决问题&#xff0c;亲测好用哦&#xff01; 第一款&#xff1a;福昕数据恢复 直达链接&#xff1a;www.pdf365.cn/foxi…

跨系统备忘录迁移有哪些方法?备忘录内容如何跨平台转移?

备忘录作为我们日常生活中常用的软件&#xff0c;帮助我们随时记录重要事项和灵感。然而&#xff0c;随着科技的发展&#xff0c;我们可能会更换不同系统的设备&#xff0c;这时就需要将备忘录内容进行迁移。特别是跨系统的迁移&#xff0c;往往让人感到不便。那么&#xff0c;…