新闻详情

新闻详情

首页 / 资讯中心 / 详情

从C源码到.so:Android NDK编译流程与CPU架构全解析

发布时间:2026/10/1 4:08:29来源:尧图网络
从C源码到.so:Android NDK编译流程与CPU架构全解析
我最初是写 Java/Kotlin 的第一次被 Android 底层的 C 编译问题按在地上摩擦是接手一个把第三方 C 库封装成 .so 文件的项目。那会儿别说“C 的编译流程与 CPU 架构”这种复合概念了光是看到 Android Studio 里蹦出来的 NDK 错误日志就头皮发麻。后来被逼着啃完 Linux 下 C 程序的完整构建链路才意识到 Android 开发虽然大部分时间都在写 Java 层代码但一旦碰到底层库、性能优化、native 崩溃排查编译流程和 CPU 架构就成了绕不开的两根柱子。这篇是 Android 前篇系列的第二篇我打算把这两根柱子彻底拆一遍从 .c 源码到 .so 文件中间到底加工了什么同样一份 C 代码在不同的 CPU 架构上又是怎么变成不同机器码的。目标很朴素——让一个刚摸到 NDK 门槛的 Android 开发者也能看懂“从源码到机器码”这条完整链路后面所有 NDK 知识都是从这里长出来的。1. 为什么Android开发绕不开C编译与CPU架构1.1 一个让我印象深刻的崩溃现场我为什么要把这个话题单独拎出来讲因为我自己就是在真实项目里吃过亏才补的课。有一段时间App 在部分低端安卓机上偶发 native crash崩溃堆栈长得特别吓人#00 pc 000000000001a3c0 /data/app/.../libxx.so #01 pc 000000000001b884 /data/app/.../libxx.so整个 backtrace 全是这种“地址 so 文件名”完全没有 Java 层面哪个方法调用的信息。我当时对编译流程的认知还停留在“Java 代码会变成 dexdex 再变成 oat”这个层面面对这种地址型堆栈完全无从下手。后来才弄明白.so 文件里的符号表、调试信息、代码段和数据段全都是 C 编译流程不同阶段的产物一个充分 strip 过的 release 包本来就不会保留大多数符号所以你拿到的是“裸地址”。要把它翻译成你熟悉的源码行需要的是 addr2line 这类工具而理解它为什么能翻译就必须理解编译流程。另一个典型案例是 JNI 层报 UnsatisfiedLinkError。很多人第一反应是“方法名字写错了”但很多时候是因为第三方 SDK 只给了 arm64-v8a 的 .so而你的老设备是 32 位 ARM或者 APK 里根本没把那套 .so 打进对应 ABI 目录。这类问题表面上五花八门底层其实都指向同一个方向编译流程决定 so 怎么被制造出来CPU 架构决定这份 so 给谁用。1.2 Java开发者的“认知断层”Android 应用开发虽然大头是 Java/Kotlin Android 框架但几乎所有“性能敏感”和“系统耦合”的模块最终都会回到 C/C音视频编解码、图像渲染、加密算法、游戏引擎甚至很多 SDK 里代替 Java 层做耗电监控的模块。这些 C 代码不可能被 Android 虚拟机直接执行必须经过编译流程变成特定 CPU 架构的机器码。所以你会经常听到这些问题为什么同一个 .so 在 64 位手机上加载失败为什么模拟器上跑得好好的一到真机就崩为什么编译器说“你的代码违反了 ABI”这些问题的答案不在 Java 层也不在 Android Framework 里而在这两条底层链路上源码怎么变成二进制编译流程二进制在哪台机器上跑CPU 架构。把这个关系搞顺后面读 NDK 文档、调 CMakeLists、排查 native 崩溃都会顺手很多。2. 从 .c 到 .soC源码的四个加工阶段很多教材会把“编译”当作一个黑盒C 文件丢进去二进制出来。实际上 gcc/clang 执行的是四道独立的工序每一道都有对应的命令和可查看的中间产物。我建议所有人都完整地走一遍这个过程真的非常有助于建立直觉。2.1 预处理真正进入编译器之前的一场文本手术这是最容易被人忽略的阶段因为日常开发中你很少直接看到它的产物。预处理器做的事情简单说是“照着规则把源文件文本改写一遍”把#include的文件内容原封不动地粘贴进来把#define的宏展开成对应的代码处理#ifdef / #ifndef条件编译顺带删掉注释。也正因为它是纯文本操作所以宏里发生了错误报错位置经常让人匪夷所思。举个很常见的例子#define SQUARE(x) ((x) * (x))如果你在调用时写成SQUARE(i)预处理后会展开成((i) * (i))i被加了两次这几乎是 C 语言新手百踩不厌的坑。想亲眼看展开结果用gcc -E hello.c -o hello.i-E就是“只做预处理停在这里”。拿到hello.i后你会发现几百行的源文件可能变成几千行因为一堆头文件都被拆进来了。在 NDK 环境下头文件搜索路径同样重要。sysroot 里放着为特定 API level 准备好的头文件与库编译命令里的-I和-isystem决定了预处理器去哪里找 Android 平台头文件。有时候你 include 了一个本地 glibc 的头文件结果编译出一堆莫名报错多半就是头文件搜索路径指向了宿主系统而不是 NDK sysroot。2.2 编译把C翻译成语义完整的汇编“编译”这个狭义步骤才是真正的语义翻译。编译器把预处理后的.i文本做词法分析、语法分析、语义分析生成中间表示再经过优化器最后输出汇编文件。我想强调一个点编译器输出的不是机器码而是汇编。为什么不直接输出机器码因为汇编是对人还算可读的一种“中间语言”保留变量名和符号名方便调试同时汇编与具体 CPU 架构的关系一一对应开发者看一眼汇编就能大概知道最终机器码长什么样。实际操作用gcc -S hello.c -o hello.s生成的hello.s里会有类似这样的内容x86_64 下movl $0, -4(%rbp)能看懂多少不重要重要的是你要知道这一条条指令才是 CPU 准备“真正执行”的东西的文本形式。优化等级在这个阶段影响极大。-O0下编译器几乎不优化每条 C 语句都对应相对直白的汇编-O2/-O3会激进地做内联、循环展开、常量折叠。所以在 Debug 构建里断点还能逐行对应源码Release 构建开-O2之后有些局部变量可能直接被优化没了断点打上去根本不命中。这在 native 调试时是常见迷惑行为别慌不是你代码有问题是优化器在搞鬼。2.3 汇编把汇编翻译成机器码汇编器as把hello.s翻译成目标文件hello.o。这个.o文件已经是二进制了但它还不能运行。有几个值得记住的细节目标文件头部会记录目标 CPU 架构。你可以用file hello.o看到它是 x86-64 还是 ARM aarch64 的目标文件。目标文件里包含代码段.text、数据段.data / .bss、符号表、重定位信息甚至还有调试信息。符号表就是nm命令展示的内容T表示代码段的已定义符号U表示未定义符号——比如printf。用gcc -c hello.c -o hello.o编出目标文件后执行nm hello.o你会看到类似U printf 0000000000000000 T main这里的U意味着printf的地址现在还是未知的需要等到链接阶段去填。这就引出了最关键的一步链接。2.4 链接拼装符号、回收地址产出最后的可执行文件链接器Linux/Android 上默认是 ld/lldNDK 默认走 lld干的事情可以通俗理解成“组装 填空”。你编译了 a.o、b.o、c.o它们各自内部符号的地址都是从 0 开始的占位链接器把它们按段合并到一起算出每个符号在最终文件里的真实偏移再把之前那些U符号一一填上。如果某个符号哪个目标文件都没定义链接器就会报undefined reference to xxx如果两个目标文件都定义了同名符号又可能报multiple definition。到了 Android 场景链接分为两种静态链接把用到的函数代码直接复制进最终的可执行文件或 .so 里。体积变大但是运行时不再依赖外部库。动态链接只记录依赖关系运行时由系统动态链接器32 位手机是/system/bin/linker64 位是/system/bin/linker64加载。App 里的 libxxx.so 之间、以及它们与系统库 liblog.so、libandroid.so 之间的依赖都是这种运行时才解析的关系。Java 层的System.loadLibrary(xxx)最终调用的是dlopen(libxxx.so)再由 linker 把它的依赖库一起装入进程地址空间。所以你在 jniLibs 里放的绝大多数情况下是动态链接出来的.so而不是.a。我用一张表把这四个阶段收一下阶段输入输出命令核心动作预处理.c.igcc -E展开宏和头文件、处理条件编译编译.i.sgcc -S词法/语法/语义分析生成汇编汇编.s.ogcc -c汇编器翻译成目标文件链接.o可执行文件/.sogcc 或 ld/lld符号解析、重定位注意这里的命令是通用的 gcc/clang 命令Android NDK 里的 clang 使用方式一样只是命令名前面会带目标平台前缀后面我会细说。3. CPU架构决定了机器码长什么样指令集与Android ABI生态3.1 指令集才是架构的灵魂假设你手里有一段 C 代码int add(int a) { return a 1; }编译器把它翻译成机器码时得先知道目标 CPU 支持哪些指令以及每条指令的二进制编码长什么样。这个“CPU 能理解的语言”就叫指令集架构ISA。x86、ARM、RISC-V就是三套完全不同的 ISA。我们常说的 x86 属于 CISC复杂指令集指令长度不等单条指令能完成的操作比较多ARM 属于 RISC精简指令集指令数量少、格式规整但一条指令能表达的“复杂动作”也相对有限。当然这只是早期分类现在的 ARM64 早就吸收了很多 CISC 风格的设计比如部分变长指令和复杂寻址模式。但有一个事实始终没变同一段 C 代码为 x86_64 编译和为 arm64 编译产出的机器码是完全不同的两套二进制。同样的加法在 x86_64 上可能是addl $1, %eax在 arm64 上大概是add w0, w0, #1。指令名字、寄存器数量、助记符都不一样。所以“编译流程”和“CPU 架构”这两个话题是死死绑在一起的——编译器的最后阶段必须知道目标 ISA 的一切细节才能生成正确的机器码。3.2 Android里的那串后缀armeabi-v7a、arm64-v8a、x86_64到底代表什么Android 开发者最常遇到的架构名词其实是 App 打包时那一排文件夹名。它们对应的正式概念叫 ABIApplication Binary Interface可以理解为“指令集 调用约定 系统接口 二进制格式”的一整套契约。A 设备上能跑的二进制放到 B 设备上不一定能跑因为 ABI 可能不一致。Android 主流 ABI 可以简单分成这么几类ABI架构指针宽度典型设备armeabi-v7a32位 ARMv732位较老的低端机、部分平板arm64-v8a64位 ARMv8-A64位近几年的绝大多数手机x8632位 Intel/AMD32位老式模拟器镜像、极少数平板x86_6464位 Intel/AMD64位现代模拟器镜像riscv6464位 RISC-V64位少数开发板/新平台试点你可能会问为什么 .so 叫 arm64-v8a 而不是直接叫“ARM 64”因为 Android 对 32 位 ARM 的最低要求是 ARMv7 指令集加 VFPv3-D16 等浮点能力所以叫 armeabi-v7a64 位对应 ARMv8-A 架构所以叫 arm64-v8a。名字里的 v7/v8 是 ARM 架构版本号不是 Android 版本。还有一个概念叫“64 位设备能不能跑 32 位 .so”。通常是可以的因为现代 ARM 处理器大多保留了对 AArch32 的支持。但反过来32 位进程不能加载 64 位 .so。而且从 Android 5.0 开始系统就支持 64 位进程了从 2019 年起Google Play 要求新应用必须提供 64 位版本。所以现在打包arm64-v8a 几乎是必须的armeabi-v7a 更多是为了兼容老设备。3.3 为什么Android手机几乎都是ARM这个问题背后是功耗、授权模式和产业链共同作用的结果。ARM 处理器的精简设计在移动端功耗控制上有天然优势加上前期通过 IP 授权让手机厂商可以灵活定制形成了庞大的生态。x86 阵营虽然性能强但在功耗和发热上吃了亏Android 上主要出现在模拟器和少量特殊设备里。这对开发者有一个很实际的直接影响你的开发机大概率是 x86_64 或者 ARM64Apple Silicon而目标手机是 ARM64。两边 ISA 不一样于是必须用“交叉编译”把代码从开发机架构翻译成目标设备架构——这就是下一章的内容。也是为什么你经常能看到这样的怪事自己的电脑上编译出来的可执行文件拿到手机上一跑就报cannot execute binary file。那不是手机坏了是架构不对。4. NDK交叉编译在x86电脑上生产ARM机器码的完整套路4.1 什么是交叉编译Android为什么一定要用交叉编译交叉编译的定义很简单在一种 CPU 架构的机器上编译出给另一种 CPU 架构运行的二进制。你 Mac 电脑是 x86 或 ARM手机上跑的是 ARM你不可能把编译工具链都搬进手机再现场编译技术上能装 clang但性能和分发路径都不可接受所以在开发机上一键产出目标架构的 .so就是标准做法。Android 官方提供的这套交叉编译工具链叫 NDKNative Development Kit。它不仅仅是一堆编译器二进制还包括了为不同 Android API level 准备好的 sysroot头文件和系统库、lld 链接器、llvm-ar、llvm-strip、addr2line 等等。NDK 本质上就是“Android 定制的 LLVM 工具链全家桶”。有一个容易被忽略的点NDK 从很早开始就切到 clang 路线、淘汰了 GCC。很多老教程还在写用arm-linux-androideabi-gcc那是很多年前的东西了。现在你打开 NDK 目录工具链实际是放在$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/如果你用的是 Apple Silicon Mac那这里可能是 darwin-x86_64NDK 在这类机器上运行的是 x86_64 版本由系统转译。不同宿主平台目录名不一样但结构一致。4.2 NDK工具链clang、sysroot和API level的含义NDK 工具链里最核心的命令名长得很有规律比如aarch64-linux-android24-clang armv7a-linux-androideabi24-clang x86_64-linux-android24-clang拆开看就是三段目标三元组 API level clang。含义分别是aarch64-linux-android64 位 ARMarmv7a-linux-androideabi32 位 ARMx86_64-linux-android64 位 x8624表示编译产物按 Android 7.0API 24的系统库来链接这决定了你的 so 能用到哪个版本以上的 API。这里有个比较容易混淆的概念API level 不是“最低支持的 Android 版本”。链接到 API 24 的系统库意思是你的 so 可以安全地调用 API 24 才新增的函数但运行时它仍然可以在更低版本上通过 dlopen 等机制工作。NDK 文档里管这个叫 minSdkVersion 对应的 NDK API level一般建议直接用 minSdkVersion 相同或更低的 API level 来编译减少意外。sysroot 是另一个关键词。编译时用-target aarch64-linux-android24就会自动关联到 NDK 里 sysroot 对应 API 24 的头文件与库。如果在编译时误用了宿主机自带的/usr/include那就会出现“头文件是 Linux 桌面版、链接库是 Android 版”的驴唇不对马嘴报错千奇百怪。提示如果有的 NDK 版本里找不到带 API level 的短链接命令也可以直接执行clang --targetaarch64-linux-android24 --sysroot$NDK/toolchains/llvm/prebuilt/linux-x86_64/sysroot hello.c -o hello效果一样。4.3 一个最小的NDK编译实测说了这么多理论来点直接的。假设你手头有个 hello.c#include stdio.h int main(void) { printf(hello from ndk\n); return 0; }用 NDK 的 clang 交叉编译成 arm64 可执行文件export NDK/path/to/your/android-ndk $NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang hello.c -o hello编译完成之后我强烈建议你立刻做一件事用file命令看产物。这个命令会直接把 ELF 的位宽、架构、动态链接器路径、有没有被 strip 都告诉你file hello你会看到类似hello: ELF 64-bit LSB pie executable, ARM aarch64, shared object, dynamically linked, interpreter /system/bin/linker64, not stripped这里ELF 64-bit、ARM aarch64、interpreter /system/bin/linker64三个信息直接把架构和加载器都写明白了。你可以把它 adb push 到手机里跑一下adb push hello /data/local/tmp/ adb shell chmod x /data/local/tmp/hello adb shell /data/local/tmp/hello如果在命令前不写 aarch64而是换成x86_64-linux-android24-clang出来的就是 x86_64 架构的二进制在多数模拟器上也能跑。这就是编译器“面向不同 ISA 生成不同机器码”的直观验证。虽然你在 Android Studio 里不会手工敲这些 clang 命令而是通过 CMake 和 Gradle 插件自动配置但底层的原理就是这些命令。理解了这条链你再去看 CMakeLists、abiFilters、externalNativeBuild 的日志就不会觉得那是魔法了。5. 链接与动态库的真实坑符号表、SONAME、strip这些细节5.1 静态链接和动态链接的取舍谈完编译流程我想多花点篇幅聊链接因为这是 native 项目里最多坑的地方。链接阶段你首先要决定这个库是做成静态库.a还是动态库.so静态库的好处是最终产物自包含不依赖外部文件部署简单坏处是你把库代码复制进每个引用它的二进制体积变大而且公共库升级后每个二进制都要重新链接。动态库则相反代码只保留一份运行时由 linker 加载节省内存和磁盘但多了一层依赖解析处理不好就会出现“找不到 .so”或者“符号被错误覆盖”这种诡异问题。在 Android 里如果你的代码要给别人复用通常产.so如果只是自己内部的公共代码可以用.a让最终的可执行文件或动态库把它合并进去。这里面有一个容易掉进去的坑你的.so依赖了另一个没有打包进 APK 的第三方.so。在桌面 Linux 上系统库路径相对固定而 Android 上 APK 只会解出自己的 native 库到/data/app/.../lib/arm64/目录。你依赖的那个第三方库如果没打进 jniLibs运行时 dlopen 就会直接失败。所以任何 native 依赖都必须显式地打包进 APK。5.2 符号可见性为什么“一切默认导出”会成为灾难默认情况下ELF 导出目标文件里所有非 static 的符号。如果你的.so被做成一个 SDK这几乎肯定是个坏事。为什么因为动态链接的符号解析有自己的规则进程里多个.so如果导出同名符号后加载的库里的符号可能会覆盖先加载的。两个 SDK 都导出了自己的debug_log函数结果你的 App 调用 log 时命中了对方库的版本崩溃现场会非常难看。解决思路很简单把符号可见性藏起来只导出你想给外面用的那部分。C 层面可以给函数加__attribute__((visibility(default)))编译时统一加-fvisibilityhidden会默认把所有符号设成不可见。JNI 场景下最典型的导出符号就是JNI_OnLoad和所有Java_xxx开头的 JNI 函数。在 CMake 里可以这样设置set(CMAKE_C_VISIBILITY_PRESET hidden) set(CMAKE_CXX_VISIBILITY_PRESET hidden)这样你的.so导出的符号面就会小很多既减小了被覆盖的风险也让 symbol 表更干净。5.3 SONAME、strip和Android的so加载机制还有两个细节遇到问题时特别关键。第一个是 SONAME。链接时可以用-Wl,-soname,libfoo.so.1给库起一个内部名字linker 加载这个库时会校验库里的 SONAME 与它记录的依赖名。Android 上系统库大多有 SONAME应用里的.so也建议设置否则升级版本后文件名变了依赖它的库会找不到。第二个是 strip。strip 的作用是从.so里去掉符号表和调试信息显著减小体积。但副作用就是你丢失了排查 native crash 的线索。一个典型的正确做法是Release 发布用 strip 后的.so打包进 APK减小体积同时保留一份未 strip 的.so或符号表文件留存用于线上崩溃解析。排查崩溃用 NDK 里的 llvm-addr2line 把崩溃地址映射回源码行号llvm-addr2line -e libfoo.so -f -C 0x1a3c0这里0x1a3c0就是崩溃堆栈里#00 pc后面的地址。-f输出函数名-C输出 demangle 后的 C 名字。注意地址必须是相对库起始地址的偏移且这个.so必须和你线上发布包一致否则解析结果会错位。我见过很多团队把排查工具和符号表一股脑 strip 掉只留下一个裸.so结果线上 crash 堆栈全是“未知”排查效率直接打三折。正确的态度是strip 是为了发货保留符号是为了能查货两件事不冲突。6. 用 file、readelf、objdump 反推编译产物的架构6.1 拿到一个未知的.so第一件事是读ELF头日常项目里你会收到各种第三方 SDK经常是对方直接丢给你一堆.so和 jar。拿到.so之后我非常建议先做一个“体检”而不是直接塞进工程。最快的三个命令file libfoo.so readelf -h libfoo.so readelf -d libfoo.sofile能告诉你 ELF 位宽和架构readelf -h里的 Machine 字段会明确显示 AArch64、Intel 80386 等readelf -d能看到这个.so依赖了哪些动态库以及它自己的 SONAME。比如一个库是 ARM aarch64 的而你的 minSdk 设备大多是 32 位 ARM 老机器那你就知道需要让对方补一个 armeabi-v7a 版本否则一上线就是一波 UnsatisfiedLinkError。6.2 从符号表到反汇编定位崩溃和架构问题的思路再往下钻可以用nm -D libfoo.so看动态符号表确认JNI_OnLoad和Java_xxx函数是不是真的存在某个 native 方法在 Java 层已经声明了但.so里没有对应符号运行时就会报 UnsatisfiedLinkError这比崩溃还常见。如果连符号表都被 strip 了还可以用objdump -d libfoo.so反汇编看指令。同样是几条指令aarch64 的指令宽度是固定的 4 字节x86 的指令长度是变长的看到大量 4 字节定长指令、寄存器名是 w0/w1/x0/x1基本可以断定是 arm64。反过来看到push %rbp、mov %rsp,%rbp这些那肯定是 x86_64。这种“用指令特征反推架构”的思路在排查“某个 so 到底是 32 位还是 64 位”的时候特别有用。我记得有一次同事说“这个 so 在真机上报 32-bit instead of 64-bit”我拿file一看那个 so 的 ABI 是 armeabi-v7a但设备是 arm64 且 App 的进程本身被系统按 64 位拉起于是 64 位进程去 dlopen 32 位库直接被 linker 拒绝。这和编译流程无关是 ABI 打包错位但在现场就是表现得像一个“库文件损坏”问题。6.3 排查UnsatisfiedLinkError的完整链路最后我把这类问题最常见的排查链路串一遍按这个顺序走基本不会白忙先看 APK 里到底有没有你指望的那个库unzip -l app-release.apk | grep xx.so看看文件在哪个 ABI 目录下。用file确认每个.so的架构跟你在 build.gradle 里声明的 abiFilters 对齐。查看设备的 ABI读取Build.SUPPORTED_ABIS确认设备能加载的目标架构。确认 App 进程的 ABI 方向如果 so 是 32 位而 App 只提供了 64 位库目录或设备偏好 64 位就会报 mismatch。检查.so的依赖readelf -d看它依赖的系统库、第三方库是否齐全系统库是否在目标 Android 版本的/system/lib64里存在。最后用 logcat 的 dlopen failed 日志确认具体拒绝原因比如library libfoo.so not found和is 32-bit instead of 64-bit是两种完全不同的修复路径。我自己的习惯是每接一个新的 native 项目第一件事就是跑一遍file、readelf和nm把全套 so 的架构、依赖、符号都摸清楚。这个动作花不了 10 分钟但能帮你把后面一大半崩溃排查时间直接省掉。编译流程给了你这些工具CPU 架构给了你判断标准剩下的就是熟能生巧。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

第047篇 垃圾回收基础——可达性分析与 GC Roots 2026/10/1 10:34:53

第047篇 垃圾回收基础——可达性分析与 GC Roots

摘要:本篇是《Android软件开发面试从入门到精通》第 47 篇,主题为「垃圾回收基础——可达性分析与 GC Roots」。在Java 核心基础的进度条上,「垃圾回收基础——可达性分析与 GC Roots」承上启下。本篇从零讲起,但按面试官追问的深度推进,读到最后一节就有答案。 关键词:A…

阅读更多 →
基于Python的学生校园消费行为分析:从一卡通数据到RFM与聚类模型 2026/10/1 10:34:53

基于Python的学生校园消费行为分析:从一卡通数据到RFM与聚类模型

简介:面向计算机专业期末大作业与课程设计场景,这份基于Python的学生校园消费行为分析项目提供了完整可运行的源码、原始消费数据与分析结果集。项目由导师指导并最终获得98分评价,源码均经本地编译调试,难度适中,适合…

阅读更多 →
乳腺癌图像分类数据集实操:从数据预处理到ResNet18迁移学习 2026/10/1 10:34:46

乳腺癌图像分类数据集实操:从数据预处理到ResNet18迁移学习

简介:这是面向深度学习和医学图像分类场景的乳腺癌症图像二分类数据集,适用于需要训练卷积神经网络、进行迁移学习或验证分类算法的研究者和开发者,目前已有286人学习使用。类别由JSON类别文件定义,图像按训练集、验证集、测试集目…

阅读更多 →
从零安装 OpenAI Codex:环境配置、登录认证与 DeepSeek 接入指南 2026/10/1 10:34:46

从零安装 OpenAI Codex:环境配置、登录认证与 DeepSeek 接入指南

1. 开工之前:先弄懂 Codex 是什么,再决定怎么装1.1 一句话认识 Codex:终端里的编程搭档Codex 是 OpenAI 出品的命令行编程代理,也是目前把“自然语言描述需求”直接落成代码操作做得最成熟的一批工具之一。它跟网页版聊天窗口最大…

阅读更多 →
435张毛巾缺陷图训练yolov8:标签格式转换与避坑指南 2026/10/1 10:34:46

435张毛巾缺陷图训练yolov8:标签格式转换与避坑指南

简介:用于毛巾缺陷检测的标注数据集,定位在计算机视觉与工业质检场景,特别适合本科毕业设计、课程实训以及刚接触目标检测的初学者。资源提供435张毛巾样本图,并配套VOC格式的XML标签与YOLO格式的TXT标签,可直接用于目…

阅读更多 →
c++构造函数初始化列表 2026/10/1 10:34:46

c++构造函数初始化列表

初始化列表使用示例话不多说&#xff0c;直接上一段最简单的代码#include <iostream>class Point{ private: int x; int y; public: Point(int i 0, int j 0): x(i), y(j) {} int getX() const { return x; } int getY() const { return y; } };int main() {Point p…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉