当前位置: 首页 > news >正文

C语言基础05——指针

一、引言

        指针是 C 语言中一个非常重要且强大的概念,也是C语言的一个重要特色。它允许程序直接操作内存地址,这使得 C 语言具有极高的灵活性和效率。正确而灵活地运用它,可以使程序简洁、紧凑,高效。下面将详细讲解 C 语言中的指针:

二、指针的基本概念

在此之前,请务必弄清楚存储单元的地址和存储单元的内容这两个概念的区别。

2.1 指针是什么?

  1. 指针本质上是一个变量,不过它存储的不是普通变量的值,而是另一个变量的内存地址。​
  2. 我们知道,计算机内存中每个字节都有唯一的编号,这个编号就是内存地址。​
  3. 通过指针,我们可以间接访问和修改它所指向的变量的值。

2.2 指针的作用

        1.操作其他函数中的变量。

        2.函数返回多个值。

        3.函数的结果和计算状态分开。

        4.方便操作数组和函数。

三、指针变量

3.1 什么是指针变量

       如果有一个变量专门用来存放另一变量的地址(即指针),则它称为“指针变量”。指针变量就是地址变量,用来存放地址,指针变量的值是地址(即指针)。

        注意:区分“指针”和“指针变量”这两个概念。例如,可以说变量i的指针是 2000,而不能说i的指针变量是 2000。指针是一个地址,而指针变量是存放地址的变量。

3.2 怎样定义指针变量

        声明指针的基本语法:

数据类型 *指针变量名;

        例如:

int *p;       // 声明一个指向int类型变量的指针p
float *f_ptr; // 声明一个指向float类型变量的指针f_ptr
char *c;      // 声明一个指向char类型变量的指针c

    * 在这里表示 "指向... 的指针"

 注意:

        1.指针变量的数据类型要跟指向变量的类型保持一致。左端的int是在定义指针变量时必须指定的“基类型”。指针变量的基类型用来指定此指针变量可以指向的变量的类型。例如,上面定义的、基类型为int的指针变量p,可以用来指向整型的变量,但不能指向浮点型变量。

        2.指针变量占用的大小,跟数据类型无关,跟编译器有关。(32位:4字节;64位:8字节),如下代码:

#include <iostream>int main()
{int a = 10;int* p1 = &a;char c = 'max';char* p2 = &c;long long n = 10000;long long* p3 = &n;printf("%zu\n", sizeof(p1));printf("%zu\n", sizeof(p2));printf("%zu\n", sizeof(p3));
}

        在这里使用64位编译器,运行结果如图:

        再用32位编译器,运行结果如图:

        3.给指针变量赋值的时候,不能把一个数值赋值给指针变量。

3.3 怎样引用指针变量

        在引用指针变量时,可能有3种情况:

(1)给指针变量赋值。

p=&a;      //把a的地址赋给指针变量P

        指针变量p的值是变量a的地址,p指向a。

(2)引用指针变量指向的变量。

        如果已执行“p=&a;”,即指针变量p指向了整型变量a.,则

printf("%d,*p);

        其作用是以整数形式输出指针变量p所指向的变量的值,即变量a的值.

        如果有以下赋值语句:

*p = 1;

        表示将整数1赋给p当前所指向的变量,如果p指向变量a,则相当于把1赋给a,即“a=1;”.

(3)引用指针变量的值。如:

printf("%o",p);

        作用是以八进制数形式输出指针变量p的值,如果p指向了a,就是输出了a的地址,即&a。

四、取地址运算符 &

    & 运算符用于获取变量的内存地址:

int a = 10;
int *p;
p = &a;  // 将变量a的地址赋值给指针p,此时p指向a

        通过&a,我们得到了变量a在内存中的地址,并把它赋给了指针p,这样p就指向了a。

五、解引用运算符 *(指针运算符或间接访问运算符)

    * 运算符用于访问指针所指向的变量的值:

int a = 10;
int *p = &a;
printf("%d", *p);  // 输出10,*p表示访问p所指向的变量的值
*p = 20;           // 修改p所指向的变量的值
printf("%d", a);   // 输出20,a的值已被修改

        在这个例子中,*p访问了p所指向的a的值,并且我们还通过*p = 20修改了a的值。

六、指针的初始化

        1.可以在声明指针的同时进行初始化:

int a = 10;
int *p = &a;  // 正确,p指向a

        2.需要注意的是,未初始化的指针称为野指针,使用野指针非常危险,可能导致程序崩溃。

        3.可以将指针初始化为NULL,表示它不指向任何有效内存:

int *p = NULL;  // 空指针

七、指针与函数

7.1 指针可以作为函数参数,实现函数对实参的修改(操作其他函数中的变量)。

        函数的参数不仅可以是整型、浮点型、字符型等数据,还可以是指针类型。它的作用是将一个变量的地址传送到另一个函数中。

        例如交换两个数的值:

        我们先引入一个无法用函数完成交换的例子(后称代码1):

#include<stdio.h>
void swap(int num1, int num2);
int main() {//定义两个变量,要求交换变量中记录的值//注意 : 交换的代码写在一个新的函数swap中//1.定义两个变量int a = 10;int b = 20;// 2.调用swap函数printf("调用前:%d,%d", a, b);swap(a, b);printf("调用后:%d,%d", a, b);
return 0;
}
void swap(int num1, int num2) {//仅仅交换的是num1和num2里面记录的值int temp = num1;num1 = num2;num2 = temp;
}

        结果如图:

         这是因为,a和b的值是传给了函数中的num1与num2,交换的是num1与num2的值。

        再来用指针(后称代码2):

#include<stdio.h>void swap(int* a, int* b) {int temp = *a;*a = *b;*b = temp;
}int main() {int x = 10, y = 20;printf("调用前:%d,%d\n", x, y);swap(&x, &y);  // 传递x和y的地址// 现在x=20,y=10printf("调用后:%d,%d\n", x, y);return 0;
}

            结果如图:

            在这个swap函数中,我们通过指针参数接收到了x和y的地址,然后利用解引用运算符修改了它们的值。

    7.1.1 如果想通过函数调用得到n个要改变的值,可以这样做:

    • 在主调函数中设n个变量,用n个指针变量指向它们;
    • 设计一个函数,有n个指针形参。在这个函数中改变这n个形参的值;
    • 在主调函数中调用这个函数,在调用时将这n个指针变量作实参,将它们的值,也就是相关变量的地址传给该函数的形参;
    • 在执行该函数的过程中,通过形参指针变量,改变它们所指向的n个变量的值;
    • 主调函数中就可以使用这些改变了值的变量。

           注意:

            1.不能企图通过改变指针形参的值而使指针实参的值改变。

            2.函数的调用可以(而且只可以)得到一个返回值(即函数值),而使用指针变量作参数,可以得到多个变化了的值。如果不用指针变量是难以做到这一点的。要善于利用指针法。

    7.1.2 关于指针操作函数变量的疑问

            1.为什么代码块2中用了指针后可以修改。

            在 C 语言中,用指针能够修改函数外部变量的值,核心原因是指针传递的是变量的地址,而不是变量的副本。

    具体来说:

            (1)普通值传递的问题:

            当你直接传递变量a和b给swap函数时,函数接收的是a和b的副本(num1和num2)。这就像你把文件复印了一份给别人,别人修改复印件不会影响你的原件。所以普通的swap函数只能交换副本,无法改变原始变量。

            (2)指针传递的原理:

            当你传递&a和&b(变量的地址)给指针参数*num1和*num2时:

            指针num1保存了变量a的内存地址

            指针num2保存了变量b的内存地址

            通过*num1和*num2(解引用操作),可以直接访问和修改a和b所在的内存空间

            简单说,指针就像给函数提供了一把 “钥匙”,让函数能够直接找到原始变量在内存中的位置并修改它,而不是只操作副本。这就是为什么用指针后能够真正交换a和b的值。

            2.代码块1中直接传递变量a和b给swap函数时,函数接收的是a和b的副本(num1和num2),但是同理,在代码块2中用指针的话不也是传递函数指针的副本吗

            指针作为参数传递时,函数也会接收指针的 “副本”。但关键区别在于:这个指针副本指向的是原始变量的内存地址。

            我们用 “地址” 和 “内容” 的关系来类比,就能明白:

            假设变量a=10存放在内存地址0x1000,b=20存放在0x2000。

            1. 普通值传递(失败的交换)

            调用swap(a, b)时,函数收到的是:

            num1 = 10(a的副本,存放在新地址0x3000)

            num2 = 20(b的副本,存放在新地址0x4000)

            函数内部交换的是0x3000和0x4000里的内容,而原始变量a(0x1000)和b(0x2000)的地址和内容都没变。

            就像:你告诉朋友 “我家有 10 个苹果”,朋友自己记成 “我有 10 个苹果”,他把自己的 10 改成20,跟你家的苹果数量无关。

            2. 指针传递(成功的交换)

            调用swap(&a, &b)时,函数收到的是指针的副本:

            num1 = 0x1000(a的地址,这个指针副本存放在新地址0x5000)

            num2 = 0x2000(b的地址,这个指针副本存放在新地址0x6000)

            函数内部的*num1和*num2操作,是通过0x1000和0x2000这两个地址,直接找到原始变量a和b的内存空间。

            交换的是0x1000和0x2000里的内容(即a和b本身的值)。

            就像:你告诉朋友 “我家在 0x1000 号房”,朋友自己记下这个地址(副本),然后直接去你家(原始地址)把苹果换成了 20 个,你回家后自然能看到变化。

    核心总结

            普通传递:副本是 “值”,修改副本不影响原始值。

            指针传递:副本是 “地址”,通过地址能直接操作原始值。

            指针的副本虽然也是临时的,但它保存的 “地址信息” 能帮我们找到原始变量的家,这就是指针能实现修改的关键。

            3.按照上面说的,这样运行swap前num1 = 0x1000,num2 = 0x2000;运行函数后,num1 = 0x2000,num2 = 0x1000;这个函数运行后num1和num2变量被销毁,不是还是不能完成交换吗

            这个疑问很关键,我们需要明确一个核心点:swap 函数中交换的不是指针本身的值,而是指针所指向的内存空间中的值。

            用之前的地址例子再详细拆解:

            假设:

            a=10 存放在 0x1000 地址

            b=20 存放在 0x2000 地址

            指针参数 num1 副本存着 0x1000(指向 a)

            指针参数 num2 副本存着 0x2000(指向 b)

            函数内部实际执行的操作:

    void swap(int *num1, int *num2) {int temp = *num1;  // 取出0x1000地址中的值(10)存到temp*num1 = *num2;     // 把0x2000地址中的值(20)放到0x1000地址*num2 = temp;      // 把temp中的值(10)放到0x2000地址
    }
    

            注意这里的关键:

            我们操作的是 *num1 和 *num2(指针指向的地址里的内容),而不是 num1 和 num2 本身

            执行后:

            0x1000 地址中的值变成了 20(a 的值被修改)

            0x2000 地址中的值变成了 10(b 的值被修改)

            num1 仍然是 0x1000,num2 仍然是 0x2000(我们根本没有交换指针本身)

            为什么函数结束后修改仍然有效?

            虽然函数结束后,指针副本 num1 和 num2 会被销毁

            但它们已经完成了使命:通过地址找到 a 和 b 的内存空间,并修改了里面的值

            a 和 b 是在 main 函数中定义的变量,它们的生命周期直到程序结束,所以修改后的值会保留

            简单说:指针副本就像一个临时的 "信使",它带着地址找到原始变量,帮我们完成修改后就消失了,但原始变量的变化已经被保留下来了。

            7.2 函数可以返回指针,比如下面这个获取两个数中较大数的指针的函数:

    int *get_max(int *a, int *b) {if (*a > *b)return a;elsereturn b;
    }

    细节:

            函数中变量的生命周期跟函数相关,函数结束了,变量也会消失。

            此时在其他函数中,就无法通过指针使用了。

            如果不想函数中的变量被回收,可以在变量前加static关键字。

    八、指针与数组

            8.1 数组元素的指针

            一个变量有地址,一个数组包含若干元素,每个数组元素都在内存中占用存储单元,它们都有相应的地址。指针变量既然可以指向变量,当然也可以指向数组元素(把某一元素的地址放到一个指针变量中)。所谓数组元素的指针就是数组元素的地址。

            可以用一个指针变量指向一个数组元素。例如:

    int a[10]={1,3,5,7,9,11,13,15,17,19};   //定义a为包含10个整型数据的数组
    int *p; 					            //定义P为指向整型变量的指针变量
    p=&.a[0];                               //把a[0]元素的地址赋给指针变量p
    

           以上是使指针变量p指向a数组的第0号元素,见下图,引用数组元素可以用下标法(如a[3]),也可以用指针法,即通过指向数组元素的指针找到所需的元素。使用指针法能使目标程序质量高(占内存少,运行速度快)。

            在C语言中,数组名(不包括形参数组名)代表数组中首元素(即序号为0的元素)的地址。因此,下面两个语句等价:

    p=&a[0];          //p的值是a[0]的地址
    p=a;             //p的值是数组a首元素(即a[0])的地址
    

            注意:程序中的数组名不代表整个数组,只代表数组首元素的地址。上述“p=a;”的作用是“把a数组的首元素的地址赋给指针变量p”,而不是“把数组a各元素的值赋给p”。

            在定义指针变量时可以对它初始化,如:

    int *p=&a[0];

            它等效于下面两行:

    int * p;
    p=&a[0];  //不应写成*p=&.a[0];
    

            当然定义时也可以写成

    int *p=a;

    它的作用是将a数组首元素(即a[0])的地址赋给指针变量p(而不是赋给*p)。

    8.2 在引用数组元素时指针的运算

            我们知道,常见运算有加减乘除,但是对指针进行运算又是什么意思呢?下面我们探讨有意义的运算——加和减。

            在指针已指向一个数组元素时,可以对指针进行以下运算:

    (1)加一个整数(用+或+=),如p+1;

    (2)减一个整数(用-或-=),如p-1;

    (3)自加运算,如p++,++p;

    (4)自减运算,如p--,--p。

    (5)两个指针相减,如p1-p2(只有p1和p2都指向同一数组中的元素时才有意义)

            如果指针变量p已指向数组中的一个元素,则p+1指向同一数组中的下一个元素,p-1指向同一数组中的上一个元素。

    注意:执行p+1时并不是将p的值(地址)简单地加1,而是加上一个数组元素所占用的字节数。

    说明:[ ] 实际上是变址运算符,即将a[i]按a+i计算地址,然后找出此地址单元中的值。

            如果指针变量p1和p2都指向同一数组中的元素,如执行p2-p1,结果是p2-p1的值(两个地址之差)除以数组元素的长度。

    注意:两个地址不能相加,如p1十p2是无实际意义的。

    8.3通过指针引用数组元素

            根据以上叙述,引用一个数组元素,可以用下面两种方法:

            (1)下标法,如a[i]形式;

            (2)指针法,如*(a+i)或*(p+i)。其中a是数组名,p是指向数组元素的指针变量,其初值 p=a。

            数组名本质上是数组首元素的地址:

    #include<stdio.h>
    int main() {int arr[5] = { 1, 2, 3, 4, 5 };int* p = arr;  // 等价于 p = &arr[0]int len = sizeof(arr) / sizeof(int);// 通过指针访问数组元素printf("%d\n", *p);      // 输出1,访问arr[0]printf("%d\n", arr[0]);      // 输出1,访问arr[0]printf("%d\n", *(p + 1));  // 输出2,访问arr[1]printf("%d\n", arr[1]);  // 输出2,访问arr[1]printf("%d\n", *(p + 2));  // 输出3,访问arr[2]printf("%d\n", arr[2]);  // 输出3,访问arr[2]printf("-------------------------------\n");//利用循环和指针遍历数组获取里面的每一个元素for (int i = 0; i < len; i++){printf("%d\n", *p++);}
    }

     运行结果如图:

    九、二级指针和多级指针

            指向指针的指针,也就是二级指针,它用于存储指针变量的地址:

    int a = 10;
    int *p = &a;    // p是一级指针,指向a
    int **pp = &p;  // pp是二级指针,指向p// 访问a的值
    printf("%d", a);      // 直接访问
    printf("%d", *p);     // 通过一级指针访问
    printf("%d", **pp);   // 通过二级指针访问
    

            通过二级指针pp,我们先解引用得到一级指针p,再解引用p就得到了a的值。

    十、指针与动态内存分配

            C 语言提供了动态内存分配函数,这些函数需要使用指针来操作,常见的有malloc函数,使用示例如下:

    #include <stdlib.h>int main() {int *p = (int*)malloc(5 * sizeof(int));  // 分配可以存储5个int的内存if (p == NULL) {// 内存分配失败处理}// 使用分配的内存for (int i = 0; i < 5; i++) {p[i] = i;}free(p);  // 释放动态分配的内存p = NULL; // 避免野指针return 0;
    }
    

            在使用完动态分配的内存后,一定要用free函数释放,并且将指针置为NULL,避免成为野指针。

    十一、指针使用注意事项

    1. 避免使用未初始化的指针(野指针),野指针可能指向任意内存地址,操作它会导致不可预测的错误。​
    2. 不要访问已经释放的内存,释放后的内存可能被系统重新分配,访问它会干扰其他数据。​
    3. 避免指针越界访问,比如在数组中,指针超出数组的范围进行访问,会破坏其他内存的数据。​
    4. 指针类型必须与所指向变量的类型匹配,否则可能会导致数据解析错误。​
    5. 使用完动态分配的内存后要及时释放,否则会造成内存泄漏。​
    6. 不要返回指向函数内部局部变量的指针,函数调用结束后,局部变量会被销毁,其内存地址不再有效。

    十二、总结

           指针是 C 语言的灵魂,它为我们提供了直接操作内存的能力,让程序更加高效和灵活。但同时,指针的使用也需要我们格外小心,遵循相关的注意事项,避免出现内存错误。希望通过本次学习,大家能对指针有更清晰的认识,多进行编程练习,逐步掌握指针的使用。

    http://www.dtcms.com/a/324532.html

    相关文章:

  • Pinia 状态管理库
  • Redis - 使用 Redis HyperLogLog 进行高效基数统计
  • 无人机集群协同三维路径规划,采用梦境优化算法(DOA)实现,Matlab代码
  • strace的常用案例
  • 基于Qt/QML 5.14和YOLOv8的工业异常检测Demo:冲压点智能识别
  • VSCODE+GDB+QEMU调试内核
  • 为 Prometheus 告警规则增加 UI 管理能力
  • 力扣经典算法篇-47-Pow(x, n)(快速幂思路)
  • 每日算法刷题Day60:8.10:leetcode 队列5道题,用时2h
  • Java Stream流详解:从基础语法到实战应用
  • 安装1panel之后如何通过nginx代理访问
  • Linux系统编程Day11 -- 进程属性和常见进程
  • 智慧社区(十一)——Spring Boot 实现 Excel 导出、上传与数据导入全流程详解
  • Langchain调用MCP服务和工具
  • MySQL的逻辑架构和SQL执行的流程:
  • 正确使用SQL Server中的Hint(10)—Hint简介与Hint分类及语法(1)
  • Spring Boot + SSH 客户端:在浏览器中执行远程命令
  • 深入理解 Java 中的线程池:原理、参数与最佳实践
  • 【密码学】8. 密码协议
  • 金融机构在元宇宙中的业务开展与创新路径
  • 【教学类-29-06】20250809灰色门牌号-黏贴版(6层*5间层2间)题目和答案(剪贴卡片)
  • 使用Python调用OpenAI的function calling源码
  • Pytorch深度学习框架实战教程-番外篇02-Pytorch池化层概念定义、工作原理和作用
  • ROS2 QT 多线程功能包设计
  • PHP项目运行
  • (LeetCode 每日一题) 869. 重新排序得到 2 的幂 (哈希表+枚举)
  • Framework开发之Zygote进程2(基于开源的AOSP15)--init.rc在start zygote之后的事情(详细完整版逐行代码走读)
  • springboot骚操作
  • 【论文阅读】Deep Adversarial Multi-view Clustering Network
  • 视觉障碍物后处理