C语言【进阶篇】之指针——涵盖基础、数组与高级概念

news2025/2/28 23:51:26

目录

  • 🚀前言
  • 🤔指针是什么
  • 🌟指针基础
    • 💯内存与地址
    • 💯指针变量
    • 💯 指针类型
    • 💯const 修饰指针
    • 💯指针运算
    • 💯野指针和 assert 断言
  • 💻数组与指针
    • 💯数组名的理解
    • 💯使用指针访问数组
    • 💯一维数组传参的本质
    • 💯指针数组
  • ✍️高级指针概念
    • 💯二级指针
    • 💯函数指针
    • 💯函数指针数组
    • 💯回调函数
    • 💯qsort 的使用与模拟实现
  • ⚙️指针与字符串
    • 💯字符指针
    • 💯字符串处理函数(如strlen和sizeof的对比)
  • 🐧总结

🚀前言

大家好!我是 EnigmaCoder。本文收录于我的专栏 C,感谢您的支持!

  • 本文我们将进入C语言进阶篇的指针部分。
  • 在C语言的编程世界里,指针堪称核心要素,发挥着不可替代的关键作用。从硬件交互层面看,指针赋予了开发者直接访问内存地址的能力,这在嵌入式系统开发等领域极为关键,能精准操控硬件设备的寄存器,达成对硬件的有效控制。从数据处理角度而言,指针极大提升了效率。例如在面对数组时,相比传统下标法,指针能更便捷、快速地遍历和操作数组元素,尤其在处理大型数组时优势尽显。同时,指针是构建复杂数据结构的基石,借助它可以巧妙连接链表节点,搭建树形、图形结构,让数据有序存储与流转。并且,在函数间的数据传递场景中,指针不仅实现了高效传递与共享,还能突破函数作用域限制,直接修改调用函数中的变量值 。
  • 话不多说,我们开始吧。

🤔指针是什么

在 C 语言中,指针是一种特殊的变量,它存储的不是普通的数据值,而是内存地址。可以把内存想象成一个庞大的、按顺序编号的仓库,每个存储单元都有对应的编号,这个编号就是地址,而指针就像是记录这些编号的小纸条,通过它能精准找到对应的数据存放位置。指针的存在,使得 C 语言能够直接对内存进行操作,极大地增强了语言的灵活性和效率,同时也是构建复杂数据结构和实现高级编程技巧的基础。

🌟指针基础

💯内存与地址

计算机的内存是由一系列连续的存储单元组成的,每个存储单元都被分配了一个唯一的编号,这个编号就称为内存地址。地址就如同房间的门牌号,通过它可以准确地找到存储在内存中的数据。例如,当我们定义一个变量int num = 10;时,系统会在内存中为num分配一块合适的空间,并赋予这块空间一个地址。
在这里插入图片描述

💯指针变量

  • 指针变量是专门用来存储内存地址的变量。其定义方式为在变量名前加上*符号,同时需要指定所指向的数据类型。
  • 以下是一个简单的示例:
#include <stdio.h>

int main() {
    int num = 10;
    int *ptr; // 定义一个指向int类型的指针变量ptr。这里的int表示ptr所指向的数据是int类型,*表明这是一个指针变量
    ptr = &num; // 将num的地址赋给ptr,&是取地址运算符,通过它获取变量num在内存中的地址,并将其存储到指针变量ptr中
    printf("num的地址是:%p\n", (void *)ptr); // %p是用于输出地址的格式说明符,(void *)是类型转换,将指针转换为通用指针类型进行输出
    return 0;
}

💯 指针类型

  • 指针的类型决定了它在解引用(即获取指针所指向的数据)时,从内存中读取多少个字节的数据。不同类型的指针占用的内存空间通常是相同的(在大多数常见系统中,指针占用 4 个字节或 8 个字节,取决于系统的位数),但它们对内存的访问方式有所不同。
  • 例如:
#include <stdio.h>

int main() {
    int num = 10;
    int *int_ptr = &num;
    char ch = 'a';
    char *char_ptr = &ch;
    printf("int指针大小:%zu字节\n", sizeof(int_ptr)); // sizeof运算符用于获取变量或数据类型占用的字节数,这里输出int类型指针的大小
    printf("char指针大小:%zu字节\n", sizeof(char_ptr)); // 输出char类型指针的大小
    return 0;
}

尽管int_ptr和char_ptr的大小相同,但当解引用int_ptr时,会按照int类型的大小(通常是 4 个字节,取决于系统和编译器)从内存中读取数据;而解引用char_ptr时,只会读取 1 个字节的数据。

💯const 修饰指针

const关键字用于修饰指针时,有三种不同的情况,它们对指针的可修改性产生不同的限制:

  • 指向常量的指针:这种指针所指向的值不能通过该指针进行修改,但指针本身可以改变指向其他地址。
#include <stdio.h>

int main() {
    int num1 = 10;
    int num2 = 20;
    const int *ptr = &num1;
    // *ptr = 15;  // 这是错误的,因为ptr是指向常量的指针,不能通过它修改指向的值
    ptr = &num2;  // 这是正确的,指针ptr可以改变指向,指向num2
    return 0;
}
  • 指针常量:指针本身的值(即所指向的地址)不能改变,但指向的值可以修改。
#include <stdio.h>

int main() {
    int num1 = 10;
    int num2 = 20;
    int *const ptr = &num1;
    // ptr = &num2;  // 这是错误的,因为ptr是指针常量,不能改变其指向的地址
    *ptr = 15;  // 这是正确的,可以通过指针ptr修改其指向的值
    return 0;
}
  • 指向常量的指针常量:指针本身和它所指向的值都不能被修改。
#include <stdio.h>

int main() {
    int num1 = 10;
    const int *const ptr = &num1;
    // *ptr = 15;  // 错误,不能通过该指针修改指向的值
    // ptr = &num2;  // 错误,不能改变指针的指向
    return 0;
}

💯指针运算

指针可以进行一些特定的算术运算,主要包括加法减法运算。这些运算的结果与指针的类型密切相关。

#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr;
    printf("ptr指向的值:%d\n", *ptr); // 输出数组arr的第一个元素的值
    ptr++;  // 指针向后移动一个int类型的位置。因为ptr是指向int类型的指针,所以这里的++操作会使ptr向后移动sizeof(int)个字节
    printf("移动后ptr指向的值:%d\n", *ptr); // 输出数组arr的第二个元素的值
    return 0;
}

指针的减法运算常用于计算两个指针之间的元素个数。例如,在一个数组中,如果有两个指针ptr1ptr2都指向该数组的元素,ptr2 - ptr1的结果就是它们之间相隔的元素个数(前提是两个指针类型相同)。

💯野指针和 assert 断言

野指针是指那些指向未定义内存区域的指针,例如,指向已释放的内存或者未初始化的指针。使用野指针会导致程序出现不可预测的错误,甚至崩溃。为了避免野指针带来的问题,可以使用assert断言进行检测。assert是 C 标准库中的一个宏,它接受一个表达式作为参数,如果表达式的值为假(通常是 0),程序就会终止并输出错误信息。

#include <stdio.h>
#include <assert.h>

int main() {
    int *ptr = NULL;
    assert(ptr != NULL);  // 如果ptr为空,程序会在这里终止,并输出断言失败的错误信息,提示具体的文件和行号等信息
    return 0;
}

💻数组与指针

💯数组名的理解

在 C 语言中,数组名在大多数情况下会被隐式转换为指向数组首元素的指针。也就是说,数组名代表了数组在内存中的起始地址

#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    printf("数组首元素地址:%p\n", (void *)arr); // 输出数组arr的首元素地址
    printf("数组首元素地址:%p\n", (void *)&arr[0]); // 输出数组第一个元素的地址,和arr代表的地址相同
    return 0;
}

然而,需要注意的是,在两种情况下数组名不会被转换为指针:一是使用sizeof运算符对数组名进行操作时,sizeof(arr)返回的是整个数组占用的字节数;二是使用&运算符取数组名的地址时,&arr得到的是整个数组的地址,虽然其数值可能和数组首元素地址相同,但类型是不同的。

💯使用指针访问数组

由于数组名可以看作是指向首元素的指针,所以可以通过指针来访问数组中的元素。常见的方式有两种:一是使用指针的算术运算,通过*(指针 + 偏移量)的形式来访问;二是使用下标运算符[ ],实际上arr[i]*(arr + i)是等价的。

#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr;
    for (int i = 0; i < 5; i++) {
        printf("arr[%d]的值:%d(通过指针访问:%d)\n", i, arr[i], *(ptr + i)); 
        // arr[i]使用数组下标访问元素,*(ptr + i)通过指针算术运算访问元素,两者结果相同
    }
    return 0;
}

💯一维数组传参的本质

当一维数组作为函数的参数进行传递时,实际上传递的是数组首元素的地址,而不是整个数组的副本。在函数内部,形参可以看作是一个指针。

#include <stdio.h>

void printArray(int *arr, int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", *(arr + i)); // 通过指针访问数组元素并输出
    }
    printf("\n");
}

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    printArray(arr, 5); // 将数组名arr作为参数传递给函数,实际上传递的是数组首元素的地址
    return 0;
}

printArray函数中,int *arr接收的是数组的起始地址,因此在函数内部对arr的操作实际上是对原数组的操作。

💯指针数组

指针数组是一种特殊的数组,它的每个元素都是一个指针。通常用于处理多个字符串或者指向不同数据类型的指针集合。

#include <stdio.h>

int main() {
    int num1 = 10, num2 = 20, num3 = 30;
    int *arr[3] = {&num1, &num2, &num3}; // 定义一个指针数组arr,其元素分别指向num1, num2, num3
    for (int i = 0; i < 3; i++) {
        printf("num的值:%d\n", *arr[i]); // 通过解引用指针数组的元素,获取所指向的变量的值
    }
    return 0;
}

在处理字符串时,指针数组非常有用,因为每个指针可以指向一个字符串的起始地址,方便对多个字符串进行管理和操作。

✍️高级指针概念

💯二级指针

二级指针是指向指针的指针,也就是说,它存储的是一个指针变量的地址。二级指针通常用于需要对指针进行间接操作或者处理指针数组的情况。

#include <stdio.h>

int main() {
    int num = 10;
    int *ptr = &num;
    int **ptr2 = &ptr; // 定义二级指针ptr2,使其指向指针ptr
    printf("num的值:%d\n", **ptr2); // 两次解引用二级指针,第一次得到ptr所指向的地址(即num的地址),第二次解引用得到num的值
    return 0;
}

二级指针在动态内存分配和处理多维数组等场景中经常被使用。

💯函数指针

函数指针是一种特殊的指针,它指向的是函数在内存中的入口地址。通过函数指针,可以像调用普通函数一样调用它所指向的函数。函数指针的定义需要指定函数的返回类型和参数列表。

#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int main() {
    int (*func_ptr)(int, int);  // 定义一个函数指针func_ptr,其指向的函数返回类型为int,接受两个int类型的参数
    func_ptr = add;  // 让函数指针指向add函数
    int result = func_ptr(3, 5); // 通过函数指针调用add函数
    printf("结果:%d\n", result);
    return 0;
}

函数指针在实现回调函数、函数表等功能时非常有用,可以提高程序的灵活性和可扩展性。

💯函数指针数组

函数指针数组是一个数组,数组的每个元素都是一个函数指针。它常用于根据不同的条件选择调用不同的函数,类似于一个函数选择表。

#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

int main() {
    int (*func_arr[2])(int, int) = {add, subtract}; // 定义函数指针数组func_arr,包含两个函数指针,分别指向add和subtract函数
    int result1 = func_arr[0](5, 3); // 通过函数指针数组调用add函数
    int result2 = func_arr[1](5, 3); // 通过函数指针数组调用subtract函数
    printf("加法结果:%d\n", result1);
    printf("减法结果:%d\n", result2);
    return 0;
}

在一些复杂的程序中,使用函数指针数组可以使代码结构更加清晰,便于维护和扩展。

💯回调函数

回调函数是通过函数指针实现的一种编程机制,即一个函数将另一个函数的指针作为参数传递给其他函数,在适当的时候,被调用的函数通过这个指针调用传入的函数。回调函数常用于异步操作、事件处理等场景。

#include <stdio.h>

void callback(int (*func)(int, int), int a, int b) {
    int result = func(a, b); // 通过传入的函数指针调用函数
    printf("回调函数结果:%d\n", result);
}

int multiply(int a, int b) {
    return a * b;
}

int main() {
    callback(multiply, 4, 5); // 将multiply函数的指针作为参数传递给callback函数
    return 0;
}

这个例子中,multiply函数作为回调函数被callback函数调用,实现了灵活的函数调用方式。

💯qsort 的使用与模拟实现

qsort是 C 标准库中的一个通用排序函数,它可以对任意类型的数组进行排序。qsort函数使用函数指针来比较数组元素,以确定它们的顺序。

  • 以下是qsort的使用示例:
#include <stdio.h>
#include <stdlib.h>

int compare(const void *a, const void *b) {
    return *(int *)a - *(int *)b; // 比较两个int类型元素的大小,用于qsort函数进行排序
}

int main() {
    int arr[5] = {5, 4, 3, 2, 1};
    qsort(arr, 5, sizeof(int), compare); // 对数组arr进行排序,参数分别为数组首地址、元素个数、每个元素的大小、比较函数
    for (int i = 0; i < 5; i++) {
        printf("%d ", arr[i]);
    }
    return 0;
}

模拟实现qsort函数可以帮助我们更好地理解其内部工作原理,虽然实际使用中直接调用标准库的qsort更为方便,但模拟过程能加深对指针和排序算法的理解。

⚙️指针与字符串

💯字符指针

字符指针可以指向字符串常量或者字符数组。当字符指针指向字符串常量时,该字符串存储在内存的只读区域,不能被修改;当指向字符数组时,可以对数组中的字符进行修改。

#include <stdio.h>

int main() {
    char *str = "Hello, World!";  // 字符指针str指向一个字符串常量。这里的字符串常量存储在只读内存区域,不能通过str修改其内容
    printf("%s\n", str);
    char arr[] = "C语言";
    char *ptr = arr;  // 字符指针ptr指向字符数组arr,此时可以通过ptr修改数组中的字符
    printf ("% s\n", ptr);
     ptr [0] = 'C'; // 修改字符数组中的字符,合法操作
    //str [0] = 'h'; // 错误操作,试图修改字符串常量会导致运行时错误
    return 0;
}

💯字符串处理函数(如strlen和sizeof的对比)

strlensizeof是两个在处理字符串时常用但功能完全不同的操作。

  • strlen是C标准库中的一个函数,用于计算字符串的实际长度(不包括字符串结束标志\0)。它从字符串的起始地址开始,逐个字符进行检查,直到遇到\0为止,并返回已经检查过的字符个数。例如:
#include <stdio.h>
#include <string.h>

int main() {
    char str[] = "Hello";
    printf("strlen结果:%zu\n", strlen(str));  // 输出字符串"Hello"的长度,结果为5
    return 0;
}
  • sizeof是 C 语言的一个操作符,用于计算变量或数据类型占用的内存字节数。对于字符数组,sizeof返回的是整个数组占用的内存空间大小,包括字符串结束标志\0。例如:
#include <stdio.h>
#include <string.h>

int main() {
    char str[] = "Hello";
    printf("sizeof结果:%zu\n", sizeof(str));  // 输出字符数组str占用的字节数,结果为6(包括\0)
    return 0;
}

理解strlensizeof的区别对于正确处理字符串长度和内存分配等问题至关重要。

🐧总结

  • 本文围绕 C 语言指针知识展开,从基础的内存地址、指针变量等概念,到数组与指针的关联应用,如用指针访问数组及一维数组传参本质;还涵盖了二级指针、函数指针等高级概念,以及指针在字符串处理中的运用,深入探讨复杂指针的实际应用 ,全面且系统地呈现了指针相关的知识体系。
  • 希望能帮助到您!

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

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

相关文章

关于命令行下的 git( git add、git commit、git push)

文章目录 关于 gitgit 的概念git 操作&#xff08;git add、git commit、git push 三板斧&#xff09;安装 git新建仓库及配置git clone.gitignoregit addgit commitgit push其他 git 指令git pull&#xff08;把远端的东西拉到本地进行同步&#xff09;其他指令 关于 git git…

DaoCloud 亮相 2025 GDC丨开源赋能 AI 更多可能

2025 年 2 月 21 日至 23 日&#xff0c;上海徐汇西岸&#xff0c;2025 全球开发者先锋大会以 “模塑全球&#xff0c;无限可能” 的主题&#xff0c;围绕云计算、机器人、元宇宙等多元领域&#xff0c;探讨前沿技术创新、应用场景拓展和产业生态赋能&#xff0c;各类专业论坛、…

极速探索 HarmonyOS NEXT:开启国产操作系统开发的新篇章

极速探索 HarmonyOS NEXT&#xff1a;开启国产操作系统开发的新篇章 一、引言二、HarmonyOS NEXT 是什么&#xff1f;背景核心特性 三、HarmonyOS NEXT 的发展历程从 LiteOS 到 HarmonyOS 的逐步演进HarmonyOS NEXT 5.0 的发布 四、HarmonyOS NEXT 对科技的影响技术突破开发者生…

火狐浏览器多开指南:独立窗口独立IP教程

无论是跨境电商从业者需要管理多个店铺账号&#xff0c;还是海外社交媒体营销人员要运营多个社交平台账号&#xff0c;亦或是从事多账号广告投放的人员&#xff0c;都面临着一个共同的挑战 —— 如何高效管理多个账号&#xff0c;并确保每个账号的独立性。 在这种情况下&#…

内容中台是什么?内容管理平台解析

内容中台的核心价值 现代企业数字化转型进程中&#xff0c;内容中台作为中枢系统&#xff0c;通过构建统一化的内容管理平台实现数据资产的高效整合与智能调度。其核心价值体现在打破传统信息孤岛&#xff0c;将分散于CRM、ERP等系统的文档、知识库、产品资料进行标准化归集&a…

1.2 Kaggle大白话:Eedi竞赛Transformer框架解决方案02-GPT_4o生成训练集缺失数据

目录 0. 本栏目竞赛汇总表1. 本文主旨2. AI工程架构3. 数据预处理模块3.1 配置数据路径和处理参数3.2 配置API参数3.3 配置输出路径 4. AI并行处理模块4.1 定义LLM客户端类4.2 定义数据处理函数4.3 定义JSON保存函数4.4 定义数据分片函数4.5 定义分片处理函数4.5 定义文件名排序…

sql server笔记

创建数据库 use master gocreate database stuuuuu//删除数据库if db_id ($$$) is not nullDrop database [$$$] go//新建表USE [studyTest] GOSET ANSI_NULLS ON GOSET QUOTED_IDENTIFIER ON GOCREATE TABLE [dbo].[Table_1]([id] [int] NULL,[name] [varchar](10) NULL ) ON…

uni小程序wx.switchTab有时候跳转错误tab问题,解决办法

在一个子页面里面使用uni.switchTab或者wx.switchTab跳转到tab菜单的时候&#xff0c;先发送了一个请求&#xff0c;然后执行跳转到tab菜单&#xff0c;但是这个时候&#xff0c;出错了........也是非常的奇怪&#xff0c;不加请求就没问题......但是业务逻辑就是要先执行某个请…

【第十节】C++设计模式(结构型模式)-Flyweight( 享元)模式

目录 一、问题背景 二、模式选择 三、代码实现 四、总结讨论 一、问题背景 享元模式&#xff08;Flyweight Pattern&#xff09;在对象存储优化中的应用 在面向对象系统的设计与实现中&#xff0c;创建对象是最常见的操作之一。然而&#xff0c;如果一个应用程序使用了过多…

AORO M6北斗短报文终端:将“太空黑科技”转化为安全保障

在卫星导航领域&#xff0c;北斗系统作为我国自主研发的全球卫星导航系统&#xff0c;正以其独特的短报文通信功能引发全球范围内的广泛关注。这一突破性技术不仅使北斗系统在全球四大导航系统中独树一帜&#xff0c;具备了双向通信能力&#xff0c;更通过遨游通讯推出的AORO M…

深度生成模型(二)——基本概念与数学建模

上一篇笔记中提到了端到端模型底层核心采用了深度生成模型&#xff0c;先简单梳理一下 生成式人工智能&#xff08;Artificial Intelligence Generated Content&#xff0c;AIGC&#xff09;经历了从早期基于概率模型和规则系统的方法到现代深度生成模型的跨越式发展 深度神经…

Mac本地部署Deep Seek R1

Mac本地部署Deep Seek R1 1.安装本地部署大型语言模型的工具 ollama 官网&#xff1a;https://ollama.com/ 2.下载Deepseek R1模型 网址&#xff1a;https://ollama.com/library/deepseek-r1 根据电脑配置&#xff0c;选择模型。 我的电脑&#xff1a;Mac M3 24G内存。 这…

项目——仿RabbitMQ实现消息队列

1.项目介绍 曾经在学习Linux的过程中&#xff0c;我们学习过阻塞队列 (BlockingQueue) 。 当时我们说阻塞队列最大的用途, 就是用来实现生产者消费者模型。 生产者消费者模型是后端开发的常用编程方式&#xff0c; 它存在诸多好处&#xff1a; 解耦合支持并发支持忙闲不均削峰…

【nextjs官方demo】Chapter 6连接数据库报错

问题&#xff1a;跟着demo创建完成postgres数据库&#xff0c;并修改了env文件&#xff0c;需要访问/seed去初始化数据的时候&#xff1a; 报错信息如下&#xff0c;看信息就是bcrypt模块有问题&#xff1a; 排除了你的环境问题后&#xff0c;就看下面这句话&#xff1a; 它的…

Nginx的反向代理(超详细)

正向代理与反向代理概念 1.概念&#xff1a; 反向代理服务器位于用户与目标服务器之间&#xff0c;但对用户而言&#xff0c;反向代理服务器就相当于目标服务器&#xff0c;即用户直接访问反向代理服务器就可以获得目标服务器的资源。同时&#xff0c;用户不需要知道目标服务…

Plantsimulation中机器人怎么通过阻塞角度设置旋转135°

创建一个这样的简单模型。 检查PickAndPlace的角度表。源位于180的角位置&#xff0c;而物料终结位于90的角位置。“返回默认位置”选项未被勾选。源每分钟生成一个零件。启动模拟时&#xff0c;Plant Simulation会选择两个位置之间的最短路径。示例中的机器人无法绕135的角位…

Docker数据卷容器实战

数据卷容器 数据共享 上面讲述的是主机和容器之间共享数据&#xff0c;那么如何实现容器和容器之间的共享数据呢&#xff1f;那就是创建 创建数据卷容器。 命名的容器挂载数据卷&#xff0c;其他容器通过挂载这个&#xff08;父容器&#xff09;实现数据共享&#xff0c;挂载…

【Rust中级教程】2.13. 结语(杂谈):我学习Rust的心路历程

2.13.1. 【Rust自学】专栏的缘起 笔者我在去年12月份之前对Rust还一无所知&#xff0c;后来看到JetBrains推出了Rust Rover&#xff0c;想着自己毕竟是买的全产品证书就下载下来玩了一下。原本就是看看&#xff0c;都打算卸载了&#xff0c;后来去网上查才发现Rust这门语言挺牛…

【备赛】点亮LED

LED部分的原理图 led前面有锁存器&#xff0c;这是为了防止led会受到lcd的干扰&#xff08;lcd也需要用到这些引脚&#xff09;。 每次想要对led操作&#xff0c;就需要先打开锁存器&#xff0c;再执行操作&#xff0c;最后关闭锁存器。 这里需要注意的是&#xff0c;引脚配置…

cpp中的继承

一、继承概念 在cpp中&#xff0c;封装、继承、多态是面向对象的三大特性。这里的继承就是允许已经存在的类&#xff08;也就是基类&#xff09;的基础上创建新类&#xff08;派生类或者子类&#xff09;&#xff0c;从而实现代码的复用。 如上图所示&#xff0c;Person是基类&…