1 同步、异步 1.1 同步与异步编程的区别
关键选择:任务是否依赖前序结果?是 → 同步;否 → 异步
1.2 代码示例说明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <stdio.h> #include <unistd.h> void task1 () { printf ("任务1开始\n" ); sleep(2 ); printf ("任务1完成\n" ); } void task2 () { printf ("任务2开始\n" ); sleep(1 ); printf ("任务2完成\n" ); } int main () { task1(); task2(); return 0 ; }
特点 :任务2必须等待任务1完成后才能开始。程序总耗时 = 2秒(task1) + 1秒(task2) = 3秒。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #include <stdio.h> #include <pthread.h> #include <unistd.h> void * task1 (void * arg) { printf ("任务1开始\n" ); sleep(2 ); printf ("任务1完成\n" ); return NULL ; } void * task2 (void * arg) { printf ("任务2开始\n" ); sleep(1 ); printf ("任务2完成\n" ); return NULL ; } int main () { pthread_t thread1, thread2; pthread_create(&thread1, NULL , task1, NULL ); pthread_create(&thread2, NULL , task2, NULL ); pthread_join(thread1, NULL ); pthread_join(thread2, NULL ); return 0 ; }
特点 :任务1和任务2同时运行 ,互不等待。程序总耗时 = 2秒 (以耗时最长的任务为准)。
2 阻塞、非阻塞 2.1 阻塞与非阻塞的区别
阻塞(Blocking)
程序在调用某个操作时,必须等待该操作完成 才能继续执行后续代码。
如果操作未完成(如等待数据读取),程序会卡住(暂停) ,直到操作完成。
非阻塞(Non-blocking)
程序在调用某个操作时,立即返回 ,无需等待操作完成。
如果操作未完成(如数据未准备好),程序会立即返回错误码 (如 EAGAIN 或 EWOULDBLOCK),允许程序继续执行其他任务。
2.2. 形象化比喻
阻塞 : 想象你去餐厅点餐,服务员需要10分钟准备食物。你必须一直等在原地 ,直到食物上桌才能离开。
程序行为:调用 read() 读取文件时,如果没有数据,程序会卡住 ,直到数据到达。
非阻塞 : 想象你去餐厅点餐,服务员告诉你“食物需要10分钟,你可以先去逛街,做好后我们会通知你”。你立即离开 ,去干其他事,等服务员通知你后再回来取餐。
程序行为:调用 read() 时,如果没有数据,程序立即返回 ,并继续执行其他任务(如轮询或事件驱动)。
2.3 代码示例说明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <stdio.h> #include <unistd.h> int main () { FILE *fp = fopen("data.txt" , "r" ); if (fp == NULL ) { perror("fopen" ); return 1 ; } char buffer[100 ]; printf ("等待数据...\n" ); size_t bytes_read = fread(buffer, 1 , sizeof (buffer), fp); printf ("读取到 %zu 字节数据\n" , bytes_read); fclose(fp); return 0 ; }
特点 :如果 data.txt 中没有数据,fread() 会无限等待 ,程序卡住。适用于数据一定存在的场景(如读取本地文件)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <string.h> int main () { int fd = open("data.txt" , O_RDONLY | O_NONBLOCK); if (fd == -1 ) { perror("open" ); return 1 ; } char buffer[100 ]; ssize_t bytes_read; while (1 ) { bytes_read = read(fd, buffer, sizeof (buffer)); if (bytes_read == -1 ) { if (errno == EAGAIN || errno == EWOULDBLOCK) { printf ("数据未准备好,继续等待...\n" ); sleep(1 ); continue ; } else { perror("read" ); break ; } } else { printf ("读取到 %zd 字节数据\n" , bytes_read); break ; } } close(fd); return 0 ; }
特点 :如果 data.txt 中没有数据,read() 会立即返回 EAGAIN 或 EWOULDBLOCK ,程序不会卡住。通过轮询或事件驱动(如 select() / epoll)处理后续逻辑,适合高并发场景。
2.4 同步、异步、阻塞、非阻塞四个概念 2.4.1 四种组合模式
同步阻塞 (Synchronous Blocking)
想象你去餐厅吃饭,点完菜后必须坐在餐桌前等待,直到服务员把菜端上来,期间不能做其他事情。这就是同步阻塞模式,你需要主动等待结果,并且在等待过程中不能做其他事情。
同步非阻塞 (Synchronous Non-blocking)
还是在餐厅,你点完菜后可以在餐厅里自由活动(比如去洗手间、和朋友聊天),但需要时不时回到餐桌前询问服务员菜是否做好了。这就是同步非阻塞模式,你不需要一直等待,但需要主动轮询结果。
异步阻塞 (Asynchronous Blocking)
这种模式比较少见,因为异步通常与非阻塞结合使用。想象你去餐厅吃饭,点完菜后坐在餐桌前等待,但服务员会在菜做好后主动通知你。虽然你可以做其他事情,但你选择坐在那里等待,这就是异步阻塞模式。
异步非阻塞 (Asynchronous Non-blocking)
最理想的模式。你点完菜后可以离开餐厅去逛街,服务员会在菜做好后打电话通知你。这就是异步非阻塞模式,你不需要等待,也不需要主动询问,系统会在任务完成时通知你。
模式
解释
同步阻塞
这是最严格的模式,必须顺序执行,且阻塞过程不能干其他事情,必须等上面的任务完成。
同步非阻塞
这种模式也是顺序执行,但是阻塞过程可以干其他的事情,但是必须主动去询问上面的任务是否完成。
异步阻塞
这种模式比较少
异步非阻塞
这种模式最合理也最常见,异步/并发执行,发起任务后可立即返回,也无需主动发起询问任务是否完成,系统会在任务完成时通知你。
2.4.2 程序示例 (一)同步非阻塞(Synchronous Non-blocking)
场景 :模拟一个“等待文件数据”的任务,但调用方不会被阻塞,而是需要主动轮询检查数据是否准备好。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include <stdio.h> #include <unistd.h> #include <pthread.h> int data_ready = 0 ;char data[] = "Hello, Synchronous Non-blocking!" ;void * prepare_data (void * arg) { sleep(3 ); data_ready = 1 ; return NULL ; } int main () { pthread_t thread; pthread_create(&thread, NULL , prepare_data, NULL ); printf ("主线程开始轮询检查数据是否准备好...\n" ); while (!data_ready) { printf ("数据未准备好,继续轮询...\n" ); sleep(1 ); } printf ("数据已准备好: %s\n" , data); pthread_join(thread, NULL ); return 0 ; }
运行流程
主线程启动一个子线程模拟读取文件(耗时3秒)。
主线程进入轮询 状态,每隔1秒检查 data_ready 是否为1。主线程必须不断检查状态(data_ready 是否为1),这会消耗一定的CPU资源(如频繁调用 printf 或检查变量)。
子线程3秒后设置 data_ready = 1。
主线程检测到数据就绪后继续执行。
关键点
同步 :主线程必须等待子线程完成任务(data_ready == 1)才能继续。
非阻塞 :主线程不会被挂起,而是主动轮询检查状态(while (!data_ready))。
缺点 :轮询浪费CPU资源(频繁检查 data_ready)。
(二)异步非阻塞(Asynchronous Non-blocking)
场景 :同样模拟“等待文件数据”,但调用方完全不阻塞,而是通过回调函数处理结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include <stdio.h> #include <unistd.h> #include <pthread.h> typedef void (*Callback) (const char *) ;void * prepare_data_async (void * arg) { sleep(3 ); Callback callback = (Callback)arg; callback("Hello, Asynchronous Non-blocking!" ); return NULL ; } int main () { printf ("主线程开始执行其他任务...\n" ); void on_data_ready (const char * result) { printf ("数据已准备好(通过回调): %s\n" , result); } pthread_t thread; pthread_create(&thread, NULL , prepare_data_async, (void *)on_data_ready); for (int i = 1 ; i <= 5 ; ++i) { printf ("主线程正在做其他事情...%d秒\n" , i); sleep(1 ); } pthread_join(thread, NULL ); return 0 ; }
运行流程
主线程启动子线程模拟读取文件(耗时3秒),并传递一个回调函数 on_data_ready。
主线程 立即返回 ,继续执行其他任务(打印 主线程正在做其他事情...)。
子线程3秒后完成任务,调用回调函数 on_data_ready 输出结果。
主线程和子线程并行执行,主线程无需等待。
关键点
异步 :主线程无需等待任务完成,任务完成后通过回调通知主线程。
非阻塞 :主线程完全不阻塞,可以继续执行其他任务。
优点 :无需轮询,资源利用率高。
2.4.3 图解对比 1 2 3 4 5 6 7 8 9 10 11 12 13 主线程: ├─ 启动异步任务(子线程准备数据) → 子线程开始执行 ├─ 进入轮询循环: │ ├─ 检查 data_ready? → 否 → 执行其他任务(如 sleep(1 )) │ └─ data_ready == 1 → 退出循环,继续执行后续逻辑 └─ 数据就绪后处理结果 主线程: ├─ 启动异步任务(子线程准备数据) → 子线程开始执行 ├─ 立即返回 → 继续执行其他任务(如打印信息) └─ 数据就绪后,系统自动调用回调函数处理结果
2.5 挂起、阻塞、等待三个概念 在编程中,挂起(Suspend) 、阻塞(Block) 和等待(Wait) 这三个术语经常用于描述任务或线程的状态,它们都与任务不能继续执行有关,但发生的原因和机制有所不同。
2.5.1 挂起(Suspend) 挂起通常指的是任务被外部因素(如操作系统、用户请求)强行暂停执行 ,并且任务的状态被保存起来,以便后续恢复执行。挂起通常与调度有关,可能因为系统资源不足、用户干预或者调试等原因,属于被动原因。
特点 :任务被挂起后,它不再参与调度,不占用CPU,直到被恢复(Resume) 。挂起操作可以由外部控制。
形象化理解 :就像你在看视频时按下了暂停键,视频画面停止在当前帧,当你再次按下播放键时,视频会从暂停的地方继续播放。
2.5.2 阻塞(Block) 阻塞指的是任务因为等待某个资源(如I/O操作完成、锁的释放、信号量等)而主动让出CPU ,进入等待状态。当资源可用时,任务会被唤醒继续执行。
特点 :任务主动放弃CPU,进入阻塞状态 ,直到等待的条件满足。阻塞通常与同步机制(如互斥锁、条件变量)或I/O操作相关。
形象化理解 :你在等公交车,但车还没来,于是你坐在车站的长椅上休息(阻塞),直到公交车来了(条件满足),你才站起来上车(继续执行)。
2.5.3 等待(Wait) 等待是一个更广义的概念,它通常指任务因为某些条件不满足而暂停执行。等待可以是阻塞的一种表现形式,也可以包括其他形式的暂停(如忙等待)。在具体上下文中,等待往往与特定的同步原语(如条件变量)一起使用。
特点 :等待通常意味着任务在某个条件上停留,直到条件满足。它可以是阻塞的(让出CPU)也可以是非阻塞的(忙等待,即循环检查条件) 。
形象化理解 :你在餐厅等位,你可以选择坐在椅子上休息(阻塞等待)或者不断去问前台还有多久(忙等待)。
2.5.4 联系与区别
联系 :三者都表示任务暂时不能执行,但挂起和阻塞通常都是被动或主动地让出CPU,而等待则是一个更通用的术语。
区别 :
挂起 通常是由外部发起的,任务自身无法控制,而阻塞 和等待 往往是任务主动的行为(等待某个条件)。
阻塞 是等待的一种方式,即让出CPU的等待(非忙等),而等待也可以包括忙等待(不释放CPU,循环检查条件)。
挂起后任务的状态被保存,且不参与调度;阻塞的任务则处于等待队列中,当条件满足时会被唤醒;等待则可能处于阻塞状态,也可能处于运行状态(忙等待)。
3 并发、并行 3.1 核心定义与本质区别
1. 并发(Concurrency)
定义 :指多个任务在同一时间段内交替执行 ,但同一时刻只有一个任务在运行。
本质 :通过 CPU 时间片轮转(或事件驱动),让用户感觉多个任务同时进行,但实际是单核 CPU “伪同时” 处理。
类比场景 : 像一个厨师同时处理切菜、炒菜、煮汤三件事:切菜时暂停炒菜,煮汤时暂停切菜,通过快速切换完成所有任务。虽然看起来同时在做,但同一时间只做一件事。
2. 并行(Parallelism)
定义 :多个任务在同一时刻真正同时执行 ,依赖多核 CPU 或多台计算机同时处理。
本质 :利用硬件资源(如多核处理器)将任务分解为多个子任务,同时并行计算。
类比场景 : 多个厨师分工合作,一人切菜、一人炒菜、一人煮汤,各自独立工作,同一时间内不同任务同步进行。
3.2 关键差异对比表
对比维度
并发(Concurrency)
并行(Parallelism)
执行方式
任务交替执行(伪同时)
任务同时执行(真同时)
硬件依赖
单核 CPU 即可(通过时间片切换)
需要多核 CPU 或多处理器(硬件支持)
解决问题
处理任务间的协作、资源竞争等复杂逻辑
加速计算密集型任务(如大数据处理)
典型场景
服务器处理多客户端请求、GUI 界面响应
科学计算、3D 渲染、区块链挖矿
编程模型
多线程、异步回调、事件循环
多进程、MPI(消息传递接口)、GPU 并行计算
3.3 程序示例
并发编程: 多线程实现,通过pthread_create创建线程,多个线程共享 CPU 时间片。
1 2 3 4 5 6 7 8 9 10 11 #include <pthread.h> void * task1 (void * arg) { }void * task2 (void * arg) { }int main () { pthread_t tid1, tid2; pthread_create(&tid1, NULL , task1, NULL ); pthread_create(&tid2, NULL , task2, NULL ); pthread_join(tid1, NULL ); pthread_join(tid2, NULL ); return 0 ; }
并行编程: 多核并行,使用 OpenMP 指令让编译器自动将循环分配到多个核心。
1 2 3 4 5 6 7 8 9 10 #include <omp.h> int main () { int i, sum = 0 ; #pragma omp parallel for reduction(+ : sum) for (i = 0 ; i < 10000 ; i++) { sum += i; } printf ("Sum: %d\n" , sum); return 0 ; }
4 回调函数callback 4.1 回调函数的基本概念 回调函数是一种函数指针 的应用,回调函数作为参数传递给另一个函数(称为 “调用者”),允许调用者在特定事件或条件发生时 执行这个函数。简单来说,回调函数是由他人调用 的函数,而非直接在代码中调用。
(函数指针,啥语言都一样,只不过各自有各自叫法,本质就是函数指针,指向函数的指针,真因为有函数指针,你才可以将函数作为变量传递给另一个函数)
4.2 回调函数与普通函数的区别
特性
普通函数
回调函数
调用方式
由程序员显式调用(如 func())
作为参数传递给其他函数,由其他函数调用
控制权
由程序员决定何时调用
由调用者(函数)决定何时调用
灵活性
固定逻辑,无法动态改变调用的逻辑
动态传递逻辑,灵活性更高
应用场景
普通流程控制(如计算、判断)
异步操作、事件驱动、解耦逻辑
4.3. 为什么要使用回调函数
(1)解耦与灵活性
解耦 :调用者不需要知道回调函数的具体实现细节,只需知道它的接口(参数和返回值)。 例如:硬件驱动模块不需要知道用户如何处理传感器数据,只需调用回调函数。
灵活性 :可以在运行时动态切换不同的回调函数,适应不同需求。 例如:一个排序算法可以接受不同的比较函数作为回调,实现升序或降序排序。
(2)异步处理
在嵌入式系统中,许多操作是异步的(如定时器、中断、通信协议)。 回调函数可以在这些异步事件发生时通知主程序,避免阻塞主流程。
(3)事件驱动
回调函数常用于事件驱动的场景(如按键按下、传感器触发)。 例如:当用户按下按钮时,系统会调用预先注册的回调函数处理按键事件。
4.4 回调函数的常见使用场景
(1)硬件中断处理
在嵌入式系统中,当硬件(如按键、定时器)触发中断时,会调用回调函数处理中断事件。 例如:定时器每隔1秒触发一次,调用回调函数更新系统时间。
(2)通信协议
在串口通信或网络通信中,数据接收完成时会调用回调函数处理数据。 例如:当串口接收到一帧数据后,调用用户定义的回调函数解析数据。
(3)状态机或事件驱动框架
在事件驱动的框架中,回调函数用于处理不同的状态或事件。 例如:GUI系统中,用户点击按钮时调用回调函数执行对应操作。
(4)通用库的设计
通用库(如排序 、过滤算法)通过回调函数允许用户自定义逻辑 。 例如:C标准库的 qsort 函数通过回调函数实现自定义排序规则。
4.5 举例嵌入式场景:模拟定时器回调 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <stdio.h> #include <unistd.h> typedef void (*TimerCallback) (void ) ;void start_timer (TimerCallback callback, int duration_seconds) { while (1 ) { sleep(duration_seconds); callback(); } } void timer_handler () { static int count = 0 ; printf ("定时器触发第 %d 次\n" , ++count); } int main () { start_timer(timer_handler, 1 ); return 0 ; }
5 C语言小知识点 5.1 volatile关键字 (一)volatile的主要作用
编译器在进行优化时,可能会假设某些变量的值在代码块中不会被修改(除非程序显式更改)。volatile 的作用就是打破这种假设,确保每次访问变量时都直接从内存中 读取,避免因为优化导致程序行为异常。
应用于特殊场景
硬件寄存器 (Memory-Mapped I/O):硬件设备的状态寄存器可能被外部硬件修改,程序必须每次读取最新值。
中断服务程序(ISR)中修改的变量 :主程序需要感知到中断中对变量的修改。
多线程环境中的共享变量 :其他线程可能修改了变量的值。
信号处理函数中修改的变量 :主程序需要根据信号处理函数的修改做出响应。
调试变量 :某些调试信息可能由外部调试器修改。
(二)使用 volatile 的示例 示例 :模拟中断服务修改变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <stdio.h> #include <pthread.h> #include <unistd.h> volatile int flag = 0 ; void * thread_func (void * arg) { sleep(2 ); flag = 1 ; return NULL ; } int main () { pthread_t tid; pthread_create(&tid, NULL , thread_func, NULL ); while (flag == 0 ) { } printf ("Flag is now 1!\n" ); pthread_join(tid, NULL ); return 0 ; }
说明:
flag 被声明为 volatile,表示其值可能在主程序之外被修改(如中断线程)。
如果未使用 volatile,编译器可能会将 flag 缓存到寄存器中,主循环可能无法检测到 flag 的变化,导致程序陷入死循环。
由于是线程间通信,更推荐使用线程同步机制(如互斥锁、条件变量),但 volatile 是基础保障。
(三)不使用 volatile 的后果 如果不使用 volatile,编译器可能会做出以下优化行为,从而引发程序错误:
编译器优化行为
后果
将变量值缓存到寄存器
无法检测到变量在程序之外的修改
删除重复读取操作
导致循环无法退出,或逻辑错误
重新排序指令
导致硬件行为异常或数据不一致
1 2 3 4 5 6 7 int flag = 0 ;void check_flag () { while (flag == 0 ) { } }
如果 flag 在中断中被设置为 1,但由于未使用 volatile,编译器可能将循环优化为:
1 2 3 4 mov r0, #0 loop: cmp r0, #0 beq loop
即,寄存器值始终为 0,导致循环永不退出。
简要解释原因:
(四)何时需要使用 volatile
场景
是否需要 volatile
硬件寄存器
✅ 必须使用
中断服务程序中修改的变量
✅ 必须使用
多线程共享变量
✅ 推荐使用,但更应使用线程同步机制
信号处理函数中修改的变量
✅ 必须使用
单线程中未被修改的局部变量
❌ 不需要
(五)小结
volatile 的核心作用是禁止编译器对变量进行优化 ,确保每次访问都从内存中读取。
它适用于外部因素可能修改变量值 的场景,如硬件寄存器、中断、多线程和信号处理。
在多线程或并发编程中,volatile 是基础保障,但不能替代线程同步机制 (如互斥锁、原子操作)。
不使用 volatile 时,编译器可能优化掉变量访问,导致程序行为与预期不符,甚至陷入死循环。
5.2 指针 5.2.1 指针常量 (一)指针常量基本概念 定义:指针常量是说指针本身是常量,不能改变指针的指向,本质是常量。 特点:声明后指针指向的地址不可改变 ,但可以通过指针修改该地址上的值 。 理解:指针常量——指针的常量,加一个的字豁然开朗。 “指针的”是形容词,去掉形容词就剩常量,也就是说指针常量是一个常量,是指针的常量。
进一步说明1:指针常量本质上是一个常量,常量具有不可修改的特性。也就是说指针常量定义赋值后就不可以修改 。 但是该指针指向的地址里面存储的东西是可以进行修改 的(常量除外)。
进一步说明2:指针常量本质上还是常量,而常量最重要的性质: ① 不可被修改。② 声明和定义时必须被初始化 。指针常量具有常量的以上两种特性。而指针常量和整形常量一样,不同的是指针常量。只能存储的是地址,整形常量中存储的是,整形数。指针常量能指向常量,也能指向变量 。
代码形式:int *const p1;
代码示例:
1 2 3 4 5 6 7 8 9 int a = 10 ;int b = 20 ;int * const p = &a;*p = 15 ; p = &b;
(二)指针常量的使用场景 指针常量的核心价值是「固定指针指向,防止意外修改指针的绑定关系」,主要有两个核心场景:
场景 1:硬件编程 —— 指向固定地址的硬件寄存器
硬件寄存器(如温度寄存器、控制寄存器)的地址是固定的,使用指针常量可以固定指针指向该寄存器地址,防止误修改指针指向,同时允许修改寄存器的数值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <stdio.h> int main () { int * const temp_reg = (int *)0x10000000 ; *temp_reg = 25 ; printf ("设置温度阈值:%d℃\n" , *temp_reg); return 0 ; }
场景 2:C++ 类成员 —— 固定指向某个资源,防止指向被篡改
当类需要持有一个固定资源的指针(如内存缓冲区、设备句柄),不允许后续修改指针指向时,可使用指针常量作为类成员(必须在构造函数初始化列表中初始化)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #include <iostream> using namespace std ; class BufferManager { private: int * const buffer_ptr; const int buffer_size; public: BufferManager(int * init_buffer, int size) : buffer_ptr(init_buffer), buffer_size(size) {} void writeBuffer (int index, int value) { if (index >= 0 && index < buffer_size) { buffer_ptr[index] = value; } } int readBuffer (int index) { if (index >= 0 && index < buffer_size) { return buffer_ptr[index]; } return -1 ; } }; int main () { int buffer[5 ] = {1 , 2 , 3 , 4 , 5 }; BufferManager manager (buffer, 5 ) ; manager.writeBuffer(2 , 30 ); cout << "缓冲区索引2的值:" << manager.readBuffer(2 ) << endl ; return 0 ; }
5.2.2 常量指针 (一)常量指针基本概念 定义:指针指向的内容是常量,指向常量的指针,本质是指针。 含义:可以通过指针访问数据,但不能通过指针修改数据 ,可以保护数据。 理解:常量指针——常量的指针。“常量的”是形容词,去掉形容词就剩指针,也就是说常量指针是一个指针,指向常量。
进一步说明1:常量指针本质上还是指针,不过前面的修饰语“常量”,需要注意的是常量指针,本身可以指向常量 ,可以指向变量 。其常量修饰,只是限定了,其不能通过指针引用改变指向变量或常量的值。
进一步说明2:指针指向的内容不一定是常量,只是通过该指针无法修改数据而已。
代码形式:int const*p2或const int*p3(两种书写形式是等价的 )
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int variable = 10 ;const int constant = 20 ; const int *ptr;ptr = &variable; printf ("指向变量时,*ptr = %d\n" , *ptr); ptr = &constant; printf ("指向常量时,*ptr = %d\n" , *ptr); int another = 42 ;ptr = &another; printf ("重新指向后,*ptr = %d\n" , *ptr);
(二)常量指针的使用场景 常量指针的核心价值是「保护数据不被意外修改」,同时保留指针灵活指向的能力,主要有两个核心场景:
场景 1:函数参数传递 —— 保护输入参数不被误修改
常用于函数参数,表示函数不会通过该指针修改目标数据,例如 void printArray(const int *arr, int size) ,确保函数只读取arr中的数据,不在函数内部改变arr中的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <stdio.h> #include <string.h> int getStrLength (const char * str) { if (str == NULL ) return 0 ; return strlen (str); } int main () { char str1[] = "Hello" ; char str2[] = "World" ; printf ("str1长度:%d\n" , getStrLength(str1)); printf ("str2长度:%d\n" , getStrLength(str2)); return 0 ; }
场景 2:访问只读内存数据 —— 避免修改只读区域导致程序崩溃
字符串常量(如 "Hello Pointer")存储在程序的只读数据区,使用常量指针指向它,可以防止误修改只读内存,避免程序崩溃。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <stdio.h> int main () { const char * str = "Hello, Pointer!" ; printf ("字符串:%s\n" , str); str = "Hi, Pointer!" ; printf ("新字符串:%s\n" , str); return 0 ; }
5.2.3 补充:普通指针与常量 在C语言和C++中,普通指针(如 int*)设计用于指向变量,允许通过指针修改所指数据;而常量(如 const int)的值在定义后不应被修改,因此普通指针不能直接指向常量,以避免意外修改常量值。
1 2 const int value = 10 ;int * ptr = &value;
5.2.4 指针函数 指针函数比较好理解,简单说就是返回值为指针类型的函数。
1 2 3 4 5 int * getArrayPointer (int index) { static int arr[] = {10 , 20 , 30 , 40 }; return &arr[index]; }
5.2.5 函数指针 在 C 语言中,函数在内存中会占用一段连续的存储空间,它的 “入口地址”(函数第一条指令的内存地址)可以被获取并存储 —— 而用于存储函数入口地址的指针,就是「函数指针」。
普通指针(如 int* p中的p):存储的是数据变量 的内存地址;
函数指针(如void (*PrintInfo)(Person*);中 PrintInfo):存储的是函数 的内存入口地址;
核心价值:通过函数指针可以间接调用函数,还能实现 “方法绑定”“回调函数”“多态模拟” 等高级功能(之前用结构体模拟面向对象,就是用它绑定对象方法)。
函数指针的格式
1 2 3 4 5 6 返回值类型 (*函数指针变量名)(参数类型列表); void (*PrintInfo) (Person*);void (*pFunc) (int , char );
typedef 结合函数指针时,是给 “函数指针类型” 起一个别名 ,而非给函数指针变量起别名,基本格式:
1 2 3 4 5 6 typedef 返回值类型 (*TypeName)(参数类型列表);typedef void (*FuncPtr) (int , char ) ;typedef void (*PersonPrintFunc) (Person*) ;
代码片段
含义说明
typedef
关键字:用于定义类型别名,此处是给函数指针类型起别名。
PersonPrintFunc
最终的类型别名 :后续可以用它直接声明函数指针变量,无需重复写繁琐语法。
(*PersonPrintFunc)
括号不可省略:表示这是一个函数指针类型(而非指针函数)。
void
该函数指针类型指向的函数,返回值为 void
(Person*)
该函数指针类型指向的函数,参数列表为 Person* 类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include <stdio.h> int add (int a, int b) { return a+b; }int sub (int a, int b) { return a-b; }int mul (int a, int b) { return a*b; }int main () { int (*p1)(int , int ); int (*p2)(int , int ); int (*p3)(int , int ); p1 = add; p2 = sub; p3 = mul; printf ("原生写法:%d, %d, %d\n" , p1(2 ,3 ), p2(5 ,1 ), p3(4 ,5 )); typedef int (*CalcFunc) (int , int ) ; CalcFunc q1, q2, q3; q1 = add; q2 = sub; q3 = mul; printf ("typedef写法:%d, %d, %d\n" , q1(2 ,3 ), q2(5 ,1 ), q3(4 ,5 )); return 0 ; }
5.3 结构体的前向声明 结构体前向声明(forward declaration of a struct )是 C 语言中一种常见的编程技巧,它的作用是:在不定义结构体具体内容的情况下,提前告诉编译器“这个结构体类型存在” 。
(一)语法形式
这行代码的意思是:“编译器,请记住有一个叫 MyStruct 的结构体类型,我现在不告诉你它里面有什么成员,但你要知道它存在。”
其实有点类似于使用extern声明一个外部变量,或者文件上方声明一个函数句柄,然后在文件下方实现函数。
(二)示例使用 1 2 3 4 5 6 7 8 9 struct MyObject ;struct MyObject* create_object (void ) ;void destroy_object (struct MyObject* obj) ;void do_something (struct MyObject* obj) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include "mylib.h" #include <stdlib.h> struct MyObject { int secret_value; char buffer[100 ]; }; struct MyObject* create_object (void ) { return calloc (1 , sizeof (struct MyObject)); } void destroy_object (struct MyObject* obj) { free (obj); } void do_something (struct MyObject* obj) { obj->secret_value = 42 ; }
1 2 3 4 5 6 7 8 9 10 #include "mylib.h" int main () { struct MyObject * obj = create_object(); do_something(obj); destroy_object(obj); return 0 ; }
6 内存单位解析 6.1 基本定义
KB(Kilobyte)
含义:KB 是 千字节(KiloByte),表示存储容量的单位。
换算:1 KB = 1024 字节(B)(基于二进制,即 2¹⁰)。
用途:常用于描述文件大小、内存分配、存储容量等。
示例:一个文本文件大小为 5 KB,表示占用 5 × 1024 = 5120 字节的空间。
kb(kilobit)
含义:kb 是 千比特(kiloBit),表示数据传输速率的单位。
换算:1 kb = 1000 比特(bit)(基于十进制,即 10³)。
用途:常见于网络带宽描述(如宽带速率、下载速度)。
示例:网络速度标注为 100 Mbps(兆比特每秒),即 100,000 kbps。
6.2 大小写区别
B vs b
B(Byte):字节,是计算机存储的基本单位,1 字节 = 8 位(bit)。
b(bit):位,是二进制数据的最小单位(0 或 1)。
关键区别:
1 Byte = 8 bit,即 1 B = 8 b。
KB 与 Kb 的关系:
1 KB = 8 Kb(因为 1 字节 = 8 位)。
1 kB = 8 kb(如果 kB 表示 1000 字节,kb 表示 1000 位)。
K vs k
K(大写):通常表示 1024(基于二进制,即 2¹⁰),用于存储容量(如 KB、MB、GB)。
k(小写):通常表示 1000(基于十进制,即 10³),用于数据传输速率(如 kb、MBps)。
7 C语言结构体面向对象 C 语言虽非面向对象语言,但可通过「结构体(存储数据)+ 函数指针(绑定操作方法)」的组合,模拟面向对象的三大核心特性:封装、继承、多态。
C 语言实现思路:
用「结构体」存储对象的属性(数据成员);
把操作该结构体的函数指针存入结构体,实现 “方法与数据绑定”;
通过「不透明指针(不完全类型)」隐藏结构体内部细节,实现信息隐藏。
示例代码:封装一个「Person」对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #ifndef PERSON_H #define PERSON_H typedef struct Person Person ;Person* Person_Create (const char * name, int age) ; void Person_Destroy (Person* person) ;void Person_PrintInfo (Person* person) ;void Person_SetAge (Person* person, int new_age) ;int Person_GetAge (Person* person) ;#endif
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 #include "person.h" #include <stdio.h> #include <stdlib.h> #include <string.h> struct Person { char name[20 ]; int age; void (*PrintInfo)(Person*); void (*SetAge)(Person*, int ); int (*GetAge)(Person*); }; static void Person_PrintInfo_Impl (Person* person) { if (person == NULL ) return ; printf ("姓名:%s,年龄:%d\n" , person->name, person->age); } static void Person_SetAge_Impl (Person* person, int new_age) { if (person == NULL || new_age < 0 ) return ; person->age = new_age; } static int Person_GetAge_Impl (Person* person) { if (person == NULL ) return -1 ; return person->age; } Person* Person_Create (const char * name, int age) { Person* person = (Person*)malloc (sizeof (Person)); if (person == NULL || name == NULL ) return NULL ; strncpy (person->name, name, sizeof (person->name) - 1 ); person->name[sizeof (person->name) - 1 ] = '\0' ; person->age = (age >= 0 ) ? age : 0 ; person->PrintInfo = Person_PrintInfo_Impl; person->SetAge = Person_SetAge_Impl; person->GetAge = Person_GetAge_Impl; return person; } void Person_Destroy (Person* person) { if (person != NULL ) { free (person); person = NULL ; } } void Person_PrintInfo (Person* person) { if (person != NULL && person->PrintInfo != NULL ) { person->PrintInfo(person); } } void Person_SetAge (Person* person, int new_age) { if (person != NULL && person->SetAge != NULL ) { person->SetAge(person, new_age); } } int Person_GetAge (Person* person) { if (person != NULL && person->GetAge != NULL ) { return person->GetAge(person); } return -1 ; }
8 C语言常用库函数 8.1 字符串相关函数
strlen 是 计算字符串长度 的函数,它定义在标准库头文件 <string.h> 中。
功能 :返回一个 C 风格字符串(以 \0 结尾的字符数组)中 不包括结尾空字符 \0 的字符个数。
返回类型 :size_t(通常是无符号整数类型,如 unsigned int 或 unsigned long)。
注意 :strlen 不会 统计字符串末尾的 \0。
1 size_t strlen (const char *s) ;
1 2 3 4 5 6 7 8 9 10 #include <stdio.h> #include <string.h> int main () { char str[] = "Hello" ; printf ("字符串的长度是: %zu\n" , strlen (str)); return 0 ; }
strcmp 是 C 语言中用于比较两个字符串 的重要函数,定义在 <string.h> 头文件中。
功能 :按字典顺序(即 ASCII 值逐字符比较)比较两个 C 风格字符串。
返回值:
等于 0 :两个字符串完全相同 。
小于 0 :第一个字符串在字典序中小于 第二个字符串。
大于 0 :第一个字符串在字典序中大于 第二个字符串。
1 2 3 int strcmp (const char *s1, const char *s2) ;- s1 和 s2 是要比较的两个字符串(指向它们首字符的指针)。 - 比较过程会一直进行,直到遇到不同的字符或遇到 \0 (字符串结束符)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <stdio.h> #include <string.h> int main () { char str1[] = "apple" ; char str2[] = "banana" ; char str3[] = "apple" ; int result1 = strcmp (str1, str2); int result2 = strcmp (str1, str3); printf ("strcmp(\"%s\", \"%s\") = %d\n" , str1, str2, result1); printf ("strcmp(\"%s\", \"%s\") = %d\n" , str1, str3, result2); if (strcmp (str1, str3) == 0 ) { printf ("str1 和 str3 相等!\n" ); } return 0 ; }
strncmp 是 strcmp 的“限定长度”版本
功能 :比较两个字符串的前 n 个字符 ,和 strcmp 一样,它也是按字典序(ASCII 值)逐字符比较。
一旦发现不同字符,或已比较了 n 个字符,或遇到任一字符串的结尾 \0,就停止比较。
返回值规则(与 strcmp 相同):
等于 0 :前 n 个字符完全相同 。
小于 0 :第一个字符串的前 n 个字符 小于 第二个。
大于 0 :第一个字符串的前 n 个字符 大于 第二个。
⚠️ 注意:即使某个字符串长度小于 n,只要在遇到 \0 之前都相等,也会正常返回结果。
1 2 3 int strncmp (const char *s1, const char *s2, size_t n) ;- s1, s2:要比较的两个字符串。 - n:最多比较的字符数(类型为 size_t ,无符号整数)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include <stdio.h> #include <string.h> int main () { char str1[] = "Hello, world!" ; char str2[] = "Hello, C!" ; char str3[] = "Hello, universe!" ; int r1 = strncmp (str1, str2, 6 ); int r2 = strncmp (str1, str3, 7 ); printf ("strncmp(str1, str2, 6) = %d\n" , r1); printf ("strncmp(str1, str3, 7) = %d\n" , r2); int r3 = strncmp (str1, str2, 8 ); printf ("strncmp(str1, str2, 8) = %d\n" , r3); if (r3 > 0 ) { printf ("str1 的前8字符 > str2 的前8字符\n" ); } return 0 ; }
strcpy 是 C 语言标准库中的一个函数,用于将一个字符串复制到另一个字符串中。 其原型如下:
关键特性
覆盖目标字符串:strcpy 会将目标字符串 dest 的原有内容覆盖。
终止符处理:strcpy 会自动在目标字符串末尾添加空字符 \0。
安全风险:如果目标字符串空间不足,会导致缓冲区溢出,存在安全风险。
1 2 3 4 5 char *strcpy (char *dest, const char *src) ;- dest:目标字符串的指针,即要复制到的字符串。 - src:源字符串的指针,即要复制的字符串。 - 返回值:指向目标字符串 dest 的指针。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <stdio.h> #include <string.h> int main () { char src[] = "Hello, World!" ; char dest[50 ]; strcpy (dest, src); printf ("Source: %s\n" , src); printf ("Destination: %s\n" , dest); return 0 ; }
strncpy 是 C 语言中用于复制字符串 的一个函数,它是 strcpy 的“限定长度”版本,定义在 <string.h> 头文件中。
功能 :从源字符串 src 最多复制 n 个字符到目标缓冲区 dest。
它的主要目的是防止缓冲区溢出 (如果使用得当)。
重要行为特点(容易出错!):
最多复制 n 个字符
如果 src 的长度 小于 n ,strncpy 会用 \0 填充剩余部分,直到总共写入 n 个字符。
如果 src 的长度 大于或等于 n ,strncpy 只复制前 n 个字符,且不会自动添加结尾的 \0!
❗ 这意味着:strncpy 不保证目标字符串以 \0 结尾! 如果 src 长度 ≥ n,你必须手动添加 \0,否则 dest 不是一个合法的 C 字符串!
1 2 3 4 5 6 char *strncpy (char *dest, const char *src, size_t n) ;- dest:目标数组(必须足够大,至少 n 字节)。 - src:源字符串。 - n:要复制的最大字符数(包括是否包含 \0 )。 - 返回值:指向 dest 的指针。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <stdio.h> #include <string.h> int main () { char src[] = "Hello, world!" ; char dest1[20 ]; char dest2[8 ]; strncpy (dest1, src, sizeof (dest1) - 1 ); dest1[sizeof (dest1) - 1 ] = '\0' ; printf ("dest1: %s\n" , dest1); strncpy (dest2, src, sizeof (dest2) - 1 ); dest2[sizeof (dest2) - 1 ] = '\0' ; printf ("dest2: %s\n" , dest2); return 0 ; }
sprintf 是 C 语言标准库中的一个函数,用于将格式化的数据写入字符串缓冲区。
1 2 3 4 5 6 int sprintf (char *buffer, const char *format, ...) ;- buffer:目标字符串缓冲区的指针,用于存储格式化后的结果。 - format:格式化字符串,包含要写入的文本以及格式说明符(如 %d, %f, %s 等)。 - ...:可变参数列表,其数量和类型由格式字符串中的格式说明符决定。 - 返回值:成功时返回写入的字符总数(不包括终止空字符 \0 );失败时返回负数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <stdio.h> int main () { char buffer[50 ]; int num = 42 ; float pi = 3.14159 ; char str[] = "Hello" ; int len = sprintf (buffer, "Number: %d, Pi: %.2f, String: %s" , num, pi, str); printf ("Formatted string: %s\n" , buffer); printf ("Length of formatted string: %d\n" , len); return 0 ; }
snprintf 函数的作用
功能 :将格式化的数据写入一个字符数组(字符串缓冲区),最多写入 n 个字符(包括结尾的 \0) 。
它是 sprintf 的“安全版本”——不会写入超过指定大小的内存 ,因此能防止缓冲区溢出。
✅ 核心优势:
自动以 \0 结尾 (只要 n > 0)。
严格限制写入长度 ,即使格式化结果很长,也只写入 n - 1 个字符 + 1 个 \0。
支持所有 printf 风格的格式说明符(如 %d, %s, %f 等)。
1 2 3 4 5 6 7 8 9 int snprintf (char *str, size_t n, const char *format, ...) ;- str:目标缓冲区(字符数组)。 - n:最大写入字符数(包括结尾的 \0 )。 - format:格式化字符串(如 "Hello %s, you are %d years old" )。 - ...:可变参数,对应格式说明符。 - 返回值: 如果成功,返回完整格式化结果的字符数(不包括 \0 ),即使它被截断了。 如果 n == 0 ,不写入任何内容,但返回所需长度(可用于动态分配内存)。
示例 1:安全复制字符串(替代 strcpy / strncpy)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <stdio.h> int main () { char src[] = "Hello, world!" ; char dest[10 ]; int ret = snprintf (dest, sizeof (dest), "%s" , src); printf ("返回值: %d\n" , ret); printf ("dest 内容: \"%s\"\n" , dest); if (ret >= (int )sizeof (dest)) { printf ("警告:字符串被截断了!\n" ); } return 0 ; }
示例 2:格式化拼接(类似 sprintf,但安全)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <stdio.h> int main () { char buffer[50 ]; char name[] = "Alice" ; int age = 25 ; snprintf (buffer, sizeof (buffer), "Name: %s, Age: %d" , name, age); printf ("%s\n" , buffer); return 0 ; }
sscanf 函数的作用
功能 :从一个字符串(const char *str)中,按照指定的格式(format)提取数据,并存入对应的变量中。
常用于解析结构化文本 ,比如日期、坐标、配置项、日志行等。
💡 可以把它理解为 “反向的 sprintf ”:
sprintf 把变量 → 拼成字符串
sscanf 把字符串 → 拆成变量
1 2 3 4 5 6 int sscanf (const char *str, const char *format, ...) ;- str:要解析的源字符串。 - format:格式控制字符串(如 "%d %s" )。 - ...:可变参数,是要写入的变量的地址(注意:要加 &,除非是指针或数组)。 - 返回值:成功匹配并赋值的项目数量(即成功转换的数据个数)。如果一开始就匹配失败,返回 0 ;如果遇到文件结束(这里指字符串结束),可能返回 EOF(但很少见)。
示例1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <stdio.h> int main () { char date_str[] = "2026-01-06" ; int year, month, day; int ret = sscanf (date_str, "%d-%d-%d" , &year, &month, &day); if (ret == 3 ) { printf ("日期解析成功:%04d年%02d月%02d日\n" , year, month, day); } else { printf ("日期格式错误!\n" ); } return 0 ; }
示例2:跳过部分字段(使用 * 忽略)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <stdio.h> int main () { char log [] = "INFO 2026-01-06 14:30 User logged in" ; char level[10 ]; char message[100 ]; int ret = sscanf (log , "%s %*s %*s %99[^\n]" , level, message); if (ret == 2 ) { printf ("日志级别: %s\n" , level); printf ("消息内容: %s\n" , message); } return 0 ; }
strstr 函数的作用
功能 :在主字符串 haystack 中查找子字符串 needle 第一次出现的位置。
如果找到,返回指向主字符串中该位置的指针 ;
如果没找到,返回 NULL。
它区分大小写 ,并且包括空字符 \0 的匹配逻辑 (如果 needle 是空字符串 "",则返回 haystack 本身)。
1 2 3 4 5 6 7 8 9 char *strstr (const char *haystack, const char *needle) ;- haystack:要被搜索的主字符串。 - needle:要查找的子字符串。 - 返回值: 成功:指向 haystack 中第一次出现 needle 的位置(类型为 char *)。 失败:NULL 。 ⚠️ 注意:返回的是原字符串中的地址,不是新分配的内存,不要 free ()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <stdio.h> #include <string.h> int main () { char text[] = "Hello, welcome to C programming!" ; char *result = strstr (text, "welcome" ); if (result != NULL ) { printf ("找到了!位置: %s\n" , result); } else { printf ("未找到子字符串。\n" ); } return 0 ; }
atof 函数的作用
功能 :将一个表示数字的 C 字符串(const char*) 转换为对应的 double 类型浮点数 。
它会跳过开头的空白字符 (如空格、制表符),然后尽可能多地解析合法的浮点数格式,直到遇到无法识别的字符为止。
✅ 支持的格式包括:
整数:"123"
小数:"123.456"
科学计数法:"1.23e4" 或 "1.23E-5"
带正负号:"-3.14", "+2.718"
1 2 3 4 5 6 7 double atof (const char *str) ;- 参数:str —— 要转换的字符串。 - 返回值:转换后的 double 值。 如果字符串无法转换(如 "abc" ),则返回 0.0 。 如果字符串部分可转换(如 "3.14abc" ),则转换到第一个非法字符为止(结果为 3.14 )。 ⚠️ 注意:atof 不会报告错误!它无法区分“真的 0 ”和“转换失败返回的 0 ”。如果需要错误检测,应使用更安全的 strtod。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <stdio.h> #include <stdlib.h> int main () { char str1[] = "3.14159" ; char str2[] = " -2.5e3 " ; char str3[] = "123.45abc" ; char str4[] = "hello" ; double d1 = atof(str1); double d2 = atof(str2); double d3 = atof(str3); double d4 = atof(str4); printf ("atof(\"%s\") = %.6f\n" , str1, d1); printf ("atof(\"%s\") = %.6f\n" , str2, d2); printf ("atof(\"%s\") = %.6f\n" , str3, d3); printf ("atof(\"%s\") = %.6f\n" , str4, d4); return 0 ; }
strtod 函数的作用
功能 :将字符串开头的合法浮点数部分转换为 double 类型。
1 2 3 4 5 6 7 8 9 10 11 12 double strtod (const char *str, char **endptr) ;- str:要转换的源字符串。 - endptr(可选): 如果不为 NULL ,strtod 会把指向第一个无法转换的字符的指针存入 *endptr。 如果整个字符串都有效,*endptr 指向结尾的 \0 。 如果没做任何转换(如输入 "abc" ),*endptr 会被设为 str 本身。 返回值: 成功:转换后的 double 值; 溢出:返回 ±HUGE_VAL(正/负无穷),并设置 errno = ERANGE; 无有效数字:返回 0.0 ,但可通过 endptr 判断是否真的为 0 。 ✅ 需要包含头文件:<stdlib.h>,有时还需 <errno.h> 来检查溢出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #include <stdio.h> #include <stdlib.h> #include <errno.h> int main () { char *inputs[] = { "3.14159" , " -2.5e3 " , "123.45abc" , "hello" , "0.0" , "1e500" , "" }; int n = sizeof (inputs) / sizeof (inputs[0 ]); for (int i = 0 ; i < n; i++) { char *end; errno = 0 ; double val = strtod(inputs[i], &end); if (end == inputs[i]) { printf ("❌ 输入 \"%s\":无有效数字\n" , inputs[i]); } else if (*end != '\0' ) { printf ("⚠️ 输入 \"%s\":转换为 %g,剩余未识别: \"%s\"\n" , inputs[i], val, end); } else if (errno == ERANGE) { printf ("❗ 输入 \"%s\":数值溢出!结果 = %g\n" , inputs[i], val); } else { printf ("✅ 输入 \"%s\":成功转换为 %g\n" , inputs[i], val); } } return 0 ; }
8.2 内存相关函数 memcpy(void* dest, const void* src, size_t n)
memmove(void* dest, const void* src, size_t n)
memset(void* s, int c, size_t n)
memcmp(const void* s1, const void* s2, size_t n)