【问题反馈】JNI 开发:为什么 C++ 在 Debug 正常,Release 却返回 NaN?

【问题反馈】JNI 开发:为什么 C++ 在 Debug 正常,Release 却返回 NaN?
摘要:
在 Android NDK / JNI 开发中,经常会遇到这样一种“诡异”问题:Debug 模式下运行完全正常,而 Release 模式却出现 NaN、Infinity 甚至随机结果。
本文通过一次真实的 JNI 坐标转换案例,深入分析了该问题的根本原因——C++ 返回局部栈内存指针所导致的未定义行为(Undefined Behavior)。

请添加图片描述

【问题反馈】JNI 开发:为什么 C++ 在 Debug 正常,Release 却返回 NaN?

本文为以下问题的解决记录。由于问题较为典型,故梳理备忘。
https://github.com/eqgis/Sceneform-EQR/discussions/16

一、问题现象描述

1. 现象

  • Debug 构建
    • JNI 返回的坐标数值正常
  • Release 构建
    • 返回坐标中出现 NaN / Infinity

且仅在Release出现

2. 出问题的方法

JNIEXPORT void JNICALL Java_com_eqgis_eqr_core_CoordinateUtilsNative_jni_1ToScenePosition( JNIEnv *env, jclass clazz, jdouble ref_x, jdouble ref_y, jdouble target_location_x, jdouble target_location_y, jdouble azimuth_rad, jdoubleArray outJNIArray){double*offset =ComputeTranslation(ref_x, ref_y, target_location_x, target_location_y);double deX =*offset;double deY =*(offset +1);double x = deX *cos(azimuth_rad)- deY *sin(azimuth_rad);double y = deX *sin(azimuth_rad)+ deY *cos(azimuth_rad);double outArray[]={x, y}; env->SetDoubleArrayRegion(outJNIArray,0,2, outArray);}

二、问题根因定位:一个“看起来没问题”的函数

问题最终锁定在这个函数:

double*ComputeTranslation(double x1,double y1,double x2,double y2){double res[2]={0,0};... res[0]= flagX * x; res[1]= flagY * y;return res;}

乍一看逻辑完全正确,但这里隐藏了一个致命错误


三、致命问题:返回了栈内存指针(未定义行为)

1. res 是什么?

double res[2];
  • res函数内部的局部变量
  • 存储在 当前函数的栈帧(stack frame)中

2. 函数返回后发生了什么?

ComputeTranslation 返回时:

  • 函数栈帧被销毁
  • res 对应的内存 立刻失效
  • 返回的指针指向:
    • 已被释放的栈空间
    • 或即将被复用的内存区域

3. C++ 标准如何定义这种行为?

这是典型的 Undefined Behavior(未定义行为)

含义是:

  • 编译器不保证任何结果
  • 程序:
    • 可能“看起来能跑”
    • 也可能随机崩溃
    • 也可能只在 Release 模式出问题

四、为什么 Debug 正常,而 Release 出 NaN?

这是很多开发者最困惑的地方。

1. Debug 模式的特点

  • 编译器优化极少
  • 栈内存分配保守
  • 局部变量:
    • 生命周期“看起来”更长
    • 内存内容不容易被覆盖

返回的指针虽然非法,但数据暂时还在


2. Release 模式的特点

  • 启用 -O2 / -O3 等激进优化
  • 栈空间:
    • 快速复用
    • 指令重排
    • 寄存器替代变量

编译器甚至可能认为:

“你返回这个指针是非法的,那我随便优化”

结果就是:

  • *offset 变成随机值
  • 或直接成为 NaN / Inf

五、为什么 NaN 特别容易出现?

在 Release 下,offset 可能是:

  • 未初始化内存
  • 被 SIMD / 浮点寄存器覆盖
  • 任意 bit pattern

浮点数中

  • 特定 bit pattern ⇒ NaN
  • 一旦参与计算:
    • NaN + x = NaN
    • sin(NaN) = NaN

导致后续都是NaN


六、这不是 JNI 的问题

值得特别强调的是:

  • JNI 只是一个函数调用边界
  • 问题在 C++ 层就已经发生
  • Java 侧只是“忠实地接收了 NaN”

如果这是纯 C++ 工程:

  • 现象 完全一致

七、正确修复方式:返回值语义而不是指针

修复方案:使用结构体返回

structVec2{double x;double y;}; Vec2 ComputeTranslation(double x1,double y1,double x2,double y2){ Vec2 res{0.0,0.0};... res.x = flagX * x; res.y = flagY * y;return res;}

调用:

Vec2 offset =ComputeTranslation(...);double deX = offset.x;double deY = offset.y;

八、为什么“以前这么写也没事”?

原因通常是:

  1. 旧编译器优化弱
  2. Debug 构建长期被使用
  3. 数据规模较小
  4. 没踩到“恰好会覆盖栈”的场景

但:

未定义行为从来不是“偶尔才错”,而是“早晚会炸”。

九、如何系统性避免这类问题?

1. 永远不要返回局部变量地址

return&localVar;return localArray;

2. 优先使用值语义

struct/ std::array / std::pair 

3. Debug ≠ 正确

Debug 只能说明:

“在当前编译条件下恰好没炸”

十、总结

问题结论
Debug 正常不代表代码正确
Release 出 NaN典型 UB 表现
根因返回栈内存指针
JNI 是否有问题没有
正确解法返回结构体 / 值语义

这次问题再次验证了一点:

C++ 中,最危险的 Bug 往往不是“复杂算法”,
而是“看起来理所当然的代码”。

如果在 Debug / Release 行为不一致时遇到诡异问题,
第一时间检查:是否触发了未定义行为。


Read more

【C++笔记】STL详解:string的实现

【C++笔记】STL详解:string的实现

前言:                 在前面的学习中,我们已经初步掌握了string类接口函数的使用方法,本文将带领大家从零开始,逐步实现一个完整的string类。          一、string类总览                 温馨提示: 为了避免与标准库中的string产生命名冲突,我们使用mystd命名空间进行封装。 namespace mystd { class string { public: //迭代器 typedef char* iterator; typedef const char* const_iterator; //默认成员函数 string(); string(const char* str); //构造函数 string(const string& s); //拷贝构造函数 string& operator=(const string& s); //赋值运算符重载函数 ~string(); //析构函数 //迭代器相关函数 iterator begin(

By Ne0inhk
计算机基础知识总结(八股文总结----计算机网络、操作系统、数据库、c++、数据结构与算法)

计算机基础知识总结(八股文总结----计算机网络、操作系统、数据库、c++、数据结构与算法)

一、操作系统 0.内存管理 01.什么是虚拟内存?为什么需要虚拟内存? 虚拟内存为程序提供比实际物理内存更大的内存空间,同时提高内存管理的灵活性和系统的多任务处理能力。虚拟地址空间就是进程所能看到的内存空间,这段空间是连续的、独立的,实际地址空间则是内存上的空间,这段是所有进程共享的、有限的空间。虚拟内存就是把实际地址空间映射到虚拟地址空间的技术,这样就实现了内存隔离、内存扩展、物理内存管理、页面交换等技术。内存隔离就是每个进程都有自己的虚拟地址空间,因此一个进程无法访问另一个进程的内存。内存扩展就是虚拟内存让每个进程拥有比实际大的内存空间地址,可以处理更多的数据、更大的进程。物理内存管理,内存空间不足时把不常用的数据转移到硬盘上,释放内存,以助于更多进程使用。页面交换,进程可能会造成外部内存碎片,可能会导致内存空间不足,这时把不常用的数据交换到硬盘上,再交换回来,就能消除内存碎片,之前技术是内存分段,现在都是内存分页,一页或几页的内存交换就能解决内存不足的问题,而且效率高,内存分段的大数据在硬盘上读取速度慢。 02.什么是内存分段和分页?作用是什么? 内存分段是将一个程序

By Ne0inhk
《MySQL 权限与访问进阶:普通用户搭建、跨端登录及 C/C++ 开发对接教程》

《MySQL 权限与访问进阶:普通用户搭建、跨端登录及 C/C++ 开发对接教程》

前引:在 MySQL 开发与运维中,普通用户的创建与权限管控是保障数据库安全的基础,而本地连接、远程访问的配置,以及 C/C++ 程序的对接调用,则是打通 “数据库 - 应用” 链路的核心环节。很多开发者在实际操作中会遇到 “用户创建后登录失败”“远程连接被拒绝”“C/C++ 接口调用报错” 等问题,本文将从实战出发,一步步拆解 MySQL 普通用户的创建配置、本地 / 远程登录的关键步骤,以及 C/C++ 访问 MySQL 的完整流程(含环境搭建、代码实现、常见问题排查),帮助开发者快速搞定多场景下的 MySQL 访问需求! 目录 【一】普通用户的创建 (1)查看user表 (2)创建普通用户 (3)删除普通用户

By Ne0inhk
C++ 类与对象:封装特性的实现与实战应用

C++ 类与对象:封装特性的实现与实战应用

C++ 类与对象:封装特性的实现与实战应用 💡 学习目标:掌握类与对象的核心概念,理解封装的本质与价值,能够独立设计并实现具有封装特性的 C++ 类。 💡 学习重点:类的定义与对象实例化、访问权限控制、构造函数与析构函数的使用、封装的实战场景应用。 一、类与对象的核心概念 ✅ 结论:类是 C++ 面向对象编程的核心载体,是对一类事物属性和行为的抽象描述;对象是类的具体实例,是内存中实际存在的实体。 1.1 类的组成 一个完整的 C++ 类通常包含两部分: * 成员变量:描述类的属性,如人的姓名、年龄,圆的半径等。 * 成员函数:描述类的行为,如人的吃饭、跑步,圆的面积计算等。 1.2 类的定义格式 #include<iostream>#include<string>

By Ne0inhk