惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

月光博客
月光博客
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
N
Netflix TechBlog - Medium
大猫的无限游戏
大猫的无限游戏
爱范儿
爱范儿
Martin Fowler
Martin Fowler
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Register - Security
The Register - Security
IT之家
IT之家
博客园_首页
Microsoft Security Blog
Microsoft Security Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 三生石上(FineUI控件)
I
InfoQ
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Jina AI
Jina AI
Apple Machine Learning Research
Apple Machine Learning Research
M
MIT News - Artificial intelligence
博客园 - Franky
C
Check Point Blog
T
The Blog of Author Tim Ferriss
V
Visual Studio Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
T
Tailwind CSS Blog
Recent Announcements
Recent Announcements
云风的 BLOG
云风的 BLOG
美团技术团队
The Cloudflare Blog
Y
Y Combinator Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
MyScale Blog
MyScale Blog
The GitHub Blog
The GitHub Blog
D
DataBreaches.Net
Google DeepMind News
Google DeepMind News
V
V2EX
aimingoo的专栏
aimingoo的专栏
GbyAI
GbyAI
G
Google Developers Blog
S
SegmentFault 最新的问题
Hugging Face - Blog
Hugging Face - Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
U
Unit 42
罗磊的独立博客
量子位
MongoDB | Blog
MongoDB | Blog
Last Week in AI
Last Week in AI
Stack Overflow Blog
Stack Overflow Blog
小众软件
小众软件
D
Docker
人人都是产品经理
人人都是产品经理

机核

游戏性能旗舰最强之选,一加 Ace 6 至尊版国补到手价2999元起 6元钱自己更换电动车刹车线 《生化危机9:安魂曲》编剧Haris Orkin专访 摸金游戏?音乐游戏!暗区新赛季这把能弹的琴有何来历? 好评国产武侠SRPG《息风谷战略》免费DLC现已推出 | 机核 GCORES 碎片 《生化危机:安魂曲》将于5月实装首个免费更新内容 | 机核 GCORES 新锐东方游戏,谱写世界新章! 沉浸式恋爱视觉小说游戏《心象演算》免费试玩版现已正式上线 | 机核 GCORES 互动影游《代号三国:龙起》上线!穿越三国与曹操并肩、与佳人同行、与权谋博弈! 《老头收集梦想生活》,游戏酒桌会6,录音笔VOL.689 | 机核 GCORES 破界·共生——《白日梦:无限世界》五大核心游戏特点解析 价格已到史低,锐龙5 9600X/锐龙7 9700X正适合抄底 时间循环之旅即刻启程!《归环》一周目测试今日开启 热门在线韩游变魂游,洛奇衍生作能否打破“花瓶”魔咒? LG UltraGear evo 全新高端显示器系列:当“5K”遇见“AI”,不止强大,更懂热爱 反套路三国互动影游《代号三国:龙起》今日上线! 愿望单登记人数突破10万!备受瞩目的“女儿养成游戏” 《梦幻魔法公主》今日于Steam平台上线!限时八折优惠中 《黑神话:悟空》全球音乐会2026巡演将于4月29日12时开票 | 机核 GCORES “Snowguelike”生存肉鸽挖矿新作《蛙穿雪境》公布发售日期,5月7日正式上线 | 机核 GCORES 重塑移动办公、AI创作新境!全新华硕灵耀Air系列、ProArt 骁龙版震撼首发,创芯未来 均分88:《Saros》媒体评分汇总 | 机核 GCORES 上海烛龙公布合作遗迹探险游戏《吉时已到》首支预告片 | 机核 GCORES 首个独立游戏《萝薇日记》已上线Steam! 烛龙新IP《吉时已到》首曝,打造国内首款中式合作遗迹探险游戏 喜加一:《暗黑破坏神Ⅳ》国服现已开启限时免费领取本体活动 | 机核 GCORES 新版《生化危机》电影定于9月18日上映,官方网站现已上线 | 机核 GCORES 《冲就完事模拟器2》“星球大战”联动DLC正式宣布 | 机核 GCORES SteamController将于5月4日发售,售价99美元 | 机核 GCORES 事已至此,内存这么用也算省钱了,“2+1”非对称双通道应用实测 505游戏母公司现已收购《明末:渊虚之羽》IP | 机核 GCORES 基石 手动杂谈12|格斗游戏也能讲好故事 | 机核 GCORES I Love You Mr Snowball 我的向日葵小姐 会比GTA6先发售吗?最硬核的生存游戏《DAYZ》要推新DLC了 周记02:在2026遇到新怪谈 什么硬件,能让游戏Loading界面快速消失? | 机核 GCORES GadioSpec《百年风云世界杯》免费试听集 | 机核 GCORES 百年风云世界杯Vol.1丨足球崛起 | 机核 GCORES 100年前,一群人提前替我们经历了AI恐惧 | 机核 GCORES 《呼啸山庄》2026 ——一辆当代艺术的大卡车冲撞了我的大脑 手游《天穗之咲稻姬:日之香巡灵传》宣布将于7月27日停服 | 机核 GCORES 集结梦之队,征战世界杯,《最佳球会ONLINE》上线Steam 山水绝景随心拼 休闲建造游戏《千里山河录》Steam商店页公开 巫师帽、法袍、魔杖,为什么它们是影视、游戏里的法师必备三件套? 《无鞘信使》-第一章 复古风自动战斗肉鸽《终结之终结》Steam商店页面现已上线 电脑里有一款不破不立的MMO,录音笔VOL.688 | 机核 GCORES 《生化危机》30周年纪念周边发售,这次是真的“保护伞” 可靠耐用+AI全能,惠普战66 2025锐龙版深度体验 《时之书:无尽终章》关卡“大航海时代”全球首次公开 经典名作《乌龙派出所》改编经营模拟游戏《乌龙派出所~阿两的商店街物语~》正式宣布支持简体中文 明日开冲,解锁反套路三国互动影游《代号三国:龙起》即将上线 独立游戏《这是我的宝藏!》已发售~ 降低难度不是唯一解,无压力死亡也是好体验 Netflix官宣新片《普通人》:讲述韩国现代史上的权力风暴 《生化危机:安魂曲》全球销量现已突破700万份 《绝地鸭卫》PC版5月15日正式发售 亡妻回忆录?女性向情感叙事游戏《S-mail》现已正式发售 全新酷黑风格,酷睿Ultra 200S PLUS的高性价比搭档!七彩虹BATTLE-AX B860M-PLUS S WIFI7 V20 超级黑刃主板测评 胖狗 索尼发布了港台地区PS5产品价格调整公告,将于5月1日起实施 二次元怪猎+性感美女!《碧蓝幻想:无尽黄昏》开启Beta公测 这款怪谈类型中式恐怖游戏居然更新了?! 《百日战纪 -最终防卫学园-》改编漫画将于今年冬季开启连载 《如被附身,请致电我们》:匈牙利黑色幽默恐怖小说 《竹屿山房杂部卷五》(译文) 大树 超越引擎 摄影分享丨四月 碎片杂记vol.72 当老式FPS与老式动画技术碰撞出新时代的火花—《神探杰克鼠》 想做独游,如何避免首个项目就褒姒? 在线多人动作游戏《OCTOPinbs》将于5月12日上线Steam! 《酒鬼女神的酒诡》确定将于2026年登陆Steam 《CRYMELIGHT》将于11月5日(周四)正式发售!4月25日开始预购! 钢铁国度MKI部落Evolution,蛮兵部分 【钢铁国度】部落-Primal MK I,蛮兵部分 这是一个高中生用ai跑出来的作品,我自称他为物理神话 钢铁国度MKI部落Metamorphosis,蛮兵部分上 钢铁国度MKI部落Metamorphosis,蛮兵部分下 INDIE Live Expo于4月25日举办:首发9款新作,超200款独立游戏亮相 暗黑卡牌策略新作《魔忌:穷鼠啮狸》发布全新中文试玩版和发售预告片,4月30日正式上线在即 战锤40K长篇小说:变节者・苦难主宰(三)(全书完) 战锤40K长篇小说:变节者・苦难主宰(二) 战锤40K长篇小说:变节者・苦难主宰(一) 英语语言学习丨短语专题1:短语的特点 唯一的EVE战争 【少前同人】【M200】战术人形会梦见音乐会吗 【昏迷3】即将发售,前作主角悉数到场!“恶灵”宋老师化身可操控角色 业内人士:游戏公司“十有八九”使用生成式AI,包括卡普空 欢庆一周年:《光与影:33号远征队》全新纪念艺术图、超值折扣与免费更新同步上线 才刚刚开始呢 【抽奖】《星际卡车司机》推出免费大型更新,四折平史低折扣进行中 四人合作FPS游戏《佣兵猎手》抢先体验重大更新 1 现已上线 Raw Fury新作《深馅地牢 Deep Dish Dungeon》将于今秋加入 XGP 塔防幸存者游戏《魔怪来袭》推出首个 DLC 《饿狼传说:群狼之城》1周年纪念!新DLC“沃尔夫冈·克劳萨”今日参战 猫狗相伴 欢乐闯关 双人合作平台跳跃游戏《猫狗同行》上线Steam商店页 《同行:月球逃脱》(Together: Moon Escape)上线将于明日上线Steam
通过C语言入门计算机科学6:内存管理
韩大 · 2024-11-07 · via 机核

上一篇我们介绍了指针的运算,这个操作实际上隐藏着相当大的危险性。因为通过指针的运算,你可以让指针指向任何一块内存,然后去读写里面的信息。为了了解这些危险性,我们需要知道 C 语言大概是如何使用内存的。

函数返回一个字符串:局部变量的指针

假如我们需要编写一个C语言函数,用来返回一个“字符串”,那么我们应该如何写呢?根据之前的知识,C字符串就是一个字符数组,我们似乎返回一个字符数组的指针就可以了。

#include <stdio.h> char *test() { char arr[12] = {'a', 'b', 'c', 'd', 'e', 0}; return arr; } int main() { char *p = test(); printf("%s\n", p); }

这个程序编译的时候,会有下面一条警告:

mem.c:7:12: warning: address of stack memory associated with local variable 'arr' returned [-Wreturn-stack-address] 7 | return arr; | ^~~ 1 warning generated.

上面这个程序,运行结果却很奇怪:

abcd,s薌?

看起来字符串的前 4 个字节的内容是对的,但是后面的内容就看不懂了,显然这么写这个程序是有问题的。我们先来看看那条编译报警信息,它的意思是:在代码的第7行:返回了栈(stack)内存的地址,这个地址是一个局部变量 arr 的。这个“栈内存”又是啥意思呢?为了知道这里的意思,我们需要了解一下C语言在调用函数时,是怎么使用内存的。

C语言在每个函数开始调用时,都会分配一块内存,用于存放函数的输入参数、返回值、程序返回指令地址(危险)、函数内局部变量等。函数里面调用新的函数,内存会“向下”新增一块区域,放上被调用的函数参数、返回值、返回指令地址、局部变量。随着函数的层层嵌套调用,这个内存块的使用会持续的“往下”延申使用。

当一个函数调用结束,进行返回的时候,此函数所使用的内存块会被“释放”。上层调用者函数如果再次调用另外一个函数,刚才被“释放”的这块内存又会被新的函数所使用。——这样一块内存,随着函数的调用、返回而不断变化使用长度,就被称为“栈内存”(stack memory)。存放在“栈内存”中的数据,随着程序运行的情况,会不断的变化。同一个地址的内存,可能在不同时候,被不同的函数写上各自的内容。这就是上面那个例程出问题的原因。

当 main() 调用 test() 的时候,计算机分配了一块内存供 test() 使用,test() 的局部变量 arr 数组也存放其中。当 test() 函数完成运行返回时,这块内存就被计算机“回收”了,准备用作其他的功能。然而,曾经使用这块已经被回收的内存的变量 arr 的地址,却被 main() 的 p 指针变量记录了起来。当 main() 函数调用 printf() 函数时,之前被 test() 使用的内存,又被分配给 printf() 使用。而 printf() 在运行的时候,并不会在意之前 test() 的 arr 变量用了什么位置,直接就按自己的需要写入这块内存。因此 arr 曾经使用的内存,从第 5 个字节处开始,刚好被 printf() 给“覆盖”了,写上了他自己的数据(我们光从代码看,并没法预测是什么内容)。因此我们看到, arr 变量的内容,就从第 5 个字节开始变得奇怪了。

由于函数的局部变量所在的内存区域,是会随着函数的调用和返回被分配和回收,进行重复使用的,所以记录下这块内存的地址,留着在上层调用者函数中使用,就是一种明显的“错误”,因此编译器也会“警告”我们。

那么,问题来了,如果我要写一个函数,返回一个数组的内容,到底应该怎么做才是对的呢?答案有两个:

  1. 使用输出参数

  2. 使用堆

指针越界

上面说的第一种方法“使用输出参数”,其原理就是:在调用函数前,先把需要返回的数据所存放的内存准备好,然后把这块内存指针传入函数,让函数把内容写到这个指针指向的内存上。这有点像我们去食堂打饭,需要先拿个盘子,让服务员把食物放到我们选的盘子里,然后我们再从这个盘子里面拿食物。代码的示例如下:

#include <stdio.h> void test(char *output) { char arr[12] = {'a', 'b', 'c', 'd', 'e', 0}; // 字符常量 for (int i = 0; i < 12; i++) { output[i] = arr[i]; } } int main() { char p[12] = {0}; test(p); printf("%s\n", p); }

程序运行显示:abcde

在上面的例子中,test() 的参数 char *output 就是所谓的“输出参数”,而这个指针参数所指向的内存,就是上面举例的那个“盘子”。在 test() 函数中,我们通过一个循环对输出结果内存进行赋值,当 test() 返回后,main 的 p 数组里面就存好了函数的结果了。然而,上面的这个写法,其实是有一些漏洞甚至安全隐患的,我们稍微修改一下上面的代码。

#include <stdio.h> void test(char *output) { char arr[12] = {'a', 'b', 'c', 'd', 'e', 0}; // 字符常量 for (int i = 0; i < 12; i++) { output[i] = arr[i]; } } int main() { char q[12] = {'x', 'y', 'z', 'r', 's', 't', 'u', 'v', 'w', 0}; char p[3] = {0}; test(p); printf("p: %s\n", p); printf("q: %s\n", q); }

程序运行结果:

p: abcde q: de

这个结果比较奇怪的有两个地方,一个是我们的数组 p 命名只有 3 个字节的长度,打印出来却有 6 个字节的内容(abcde\0);第二个问题是,我们定义的数组 q 本来没有参与 test() 的调用,最后结果从 "xyzrstuvw" 变成了 "de"。造成这个问题的原因,是因为在 test() 函数内部,并没有管输出参数 output,或者叫 p 数组,它的长度是不是有 12 个元素,而是直接去赋值了。当 test() 的 for 循环,运行到 i>2 的时候,其实已经超过了 p 数组的内存限定空间了,这个问题叫做“指针越界”。在 p 数组限定的长度以外的内存,到底是用来做什么的,一般是难以预料的。在这个例子里,p 数组的内存后面,就是数组 q 的内存,所以这次越界,就把 q 数组的内容给写错了。

从这个例子来看,指针越界造成了数组 q 被意料之外的改写了,这当然会造成程序运行的错误(bug),但其实指针越界有可能造成更危险的问题:在栈内存中,除了存放了函数中用到的局部变量以外,还有很多其他的重要数据,包括当前函数返回之后,应该执行的程序的地址信息。如果有个程序,会从网络或者其他外部输入数据,这些数据又被写入内存的时候越界了,那么这些数据就有可能被写入栈内存里存放了程序执行地址的内存。这样就提供了一个“漏洞”,让恶意使用者,通过构造专门的输入数据,改写程序执行地址——当前函数返回后,就会去执行这个恶意写入的程序地址,从而让计算机去执行开发者预料之外的代码。这种内存越界的 bug,也是很多程序的安全漏洞的原因。具体更多关于这类漏洞的知识,本文不作过多介绍,只是告诉大家这类漏洞非常容易出现。

最后,关于输出参数的正确写法,应该是如下例程:对于输出参数,除了有指针外,还需要再输入一个参数来表示输出参数内存长度。并且用返回值来告诉调用者,输出的内容的具体长度,避免调用者错误使用。

#include <stdio.h> int test(char *output, int len) { char arr[12] = {'a', 'b', 'c', 'd', 'e', 0}; // 字符常量 int size = len > 12 ? 12 : len; // 计算应该返回的长度 for (int i = 0; i < size; i++) { output[i] = arr[i]; } return size; } int main() { char q[12] = {'x', 'y', 'z', 'r', 's', 't', 'u', 'v', 'w', 0}; char p[3] = {0}; int len = test(p, 3); printf("p: %.*s\n", len, p); printf("q: %s\n", q); }

运行返回

p: abc q: xyzrstuvw

上面的程序中 test() 增加了参数 len 和返回值 int,在循环赋值之前,先判断了一下输出参数所指向的内存(缓冲区)的长度,如果小于返回数值的长度,就最多写满这个缓冲区的长度,不会越界写到其他地方去,然后返回写了的数据长度给调用者。

在 main() 中,我们把输出参数内存的长度 3 传入 test(),然后从 test() 的返回值获得输出缓冲区中数据的长度,然后用了 printf() 的一个特殊占位符 %.*s 来打印字符串,这个占位符的意思是,根据一个长度和一个指针,来打印最多这个长度的数据,而不是一直读到碰到内存中的字节数字 0 为止。这样就避免了打印超过预定数据长度以外的数据。

输出参数需要在调用前,就要预设返回结果的内存长度,但是如果没法预测,是不是有其他方法也能完成任务呢?也是有的,就是使用堆内存,动态的申请内存使用。

使用堆

所谓堆(heap)内存,就是一类不通过直接写变量名来使用的内存。而是通过一对特殊的函数,来分配和回收的内存。这些内存不会使用上面所说的栈(stack)内存的空间。同样是返回一个字符数组,我们可以用堆内存如下实现:

#include <stdio.h> #include <stdlib.h> char *test() { char arr[12] = {'a', 'b', 'c', 'd', 'e', 0}; // 字符常量 char *output = malloc(12); for (int i = 0; i < 12; i++) { output[i] = arr[i]; } return output; } int main() { char *p = test(); printf("p: %s\n", p); free(p); }

程序输出:abcde

在上面的例程中,出现了两个新的函数 malloc()free(),这两个函数是来自 C 的标准库的,所以需要 #include <stdlib.h>。其中 malloc() 的参数表示要申请多少个字节的内存,返回值就是申请后的内存指针。注意申请多少就用多少,如果写入数据的时候,超过了申请内存的长度,有可能会把程序里其他变量给写“坏”了。而 free() 是释放通过 malloc() 申请的内存,传入指针即可,至于要释放的内存有多长,其实是存在申请的内存指针前面的某个位置(头部内存),所以千万要记得释放 malloc() 返回的数值,而不是随便在堆内存中选择一个地址用来释放。

在堆内存上分配的空间,C 语言是不会像栈内存一样自动跟随函数的调用来释放的。需要程序员主动调用 free() 进行释放,否则程序就会占用内存越来越多,形成了所谓的“内存泄漏”。当然这种问题在实际程序中非常常见,所以我们偶尔也会碰到,一个程序运行的时候,占内存越来越多,电脑越来越卡的情况。同样的,对于释放过的堆内存重复释放,也可能存在问题,通常会导致进程在这里退出(崩溃),或者是导致下一次 malloc() 的时候,错误的使用了被其他代码使用的内存而产生问题。所以,确保堆内存的 malloc() 和 free() 能正确的,一对一的运行,是一个非常考验程序员的需求。这也是后来很多编程语言,都设计了所谓内存自动管理的机制,避免让程序员来处理这个事情。

当我们掌握了指针,了解了栈内存和堆内存,我们才能比较完整的了解 C 语言的能力。下一篇,我们尝试用之前说的这些能力,做一点真正有用的东西,譬如说用起来没有那么麻烦的“字符串”,而不是以0结尾的字符数组。