上海市建设协会网站公司弄个网站多少钱
- 作者: 五速梦信息网
- 时间: 2026年03月21日 09:23
当前位置: 首页 > news >正文
上海市建设协会网站,公司弄个网站多少钱,黄骅港神华集团招聘信息,邢台网站建设好蜘蛛对象的生命周期是c中非常重要的概念#xff0c;它直接决定了你的程序是否正确以及是否存在安全问题。 今天要说的临时变量导致的生命周期问题是非常常见的#xff0c;很多时候没有一定经验甚至没法识别出来。光是我自己写、review、回答别人的问题就犯了或者看到了许许多多这…对象的生命周期是c中非常重要的概念它直接决定了你的程序是否正确以及是否存在安全问题。 今天要说的临时变量导致的生命周期问题是非常常见的很多时候没有一定经验甚至没法识别出来。光是我自己写、review、回答别人的问题就犯了或者看到了许许多多这类问题所以我想有必要做个简单的总结自己备忘的同时也尽量帮其他开发者尤其是别的语言转c的人少踩些坑。 问题主要分为三类每类我都会给出典型例子最后会给出解决办法。不过在深入讨论每一类问题之前先让我们复习点必要的基础知识。 基础回顾 基础回顾少不了否则看c的文章容易变成看天书。 但也别紧张都叫“基础”了那肯定是些简单的偏常识的东西不难的。 第一个基础是语句和表达式。语句好理解for(…){}是一个语句int a num 1;也是一个语句除了一些特殊的语法结构语句通常以分号结尾。表达式是什么呢语句中除了关键字和符号之外的东西都可以算表达式比如int a num 1中num、1、num 1都是表达式。当然单独的表达式也可以构成语句比如num;是语句。 这里就有个概率要回顾了“完整的表达式”。什么叫完整粗暴的理解就是同一个语句里的所有子表达式组合起来的那个表达式才叫“完整的表达式”。举个例子int a num 1;中int a num 1才是一个完整的表达式str().trimmed().replace(pattern, gettext());中str().trimmed().replace(pattern, gettext())才是完整的表达式。 这个概念后面会很有用。 第二个要复习的是const T 对临时变量生命周期的影响。 一个临时对象通常是prvalue可以绑定到const T 或者右值引用上。绑定后临时对象的生命周期会一直延长到绑定的引用的生命周期结束的时候。但延长有一个例外 const int func() {return 100; } 这个大家都知道是悬垂引用但const T 不是能延长100这个临时int对象的生命周期吗这里理论上不应该是和返回值的生命周期一样么这么会变成悬垂引用 答案是语法规定的例外引用绑定延长的生命周期不能跨越作用域。这里显然100是在函数内的作用域而返回的引用作用域在函数之外跨越作用域了所以这时绑定不能延长临时int对象的生命周期临时对象在函数调用结束后销毁所以产生了悬垂引用。 另外绑定带来的延长是不能传递的只有直接绑定到临时对象上才能延长生命其他情况比如通过另一个引用进行的绑定都没有效果。 复习到此为止我们来看具体问题。 函数调用中的生命周期问题 先看例子 const int value std::max(v, 100); 这是三类问题中最常见的一类甚至常见到了各大文档包括cppreference上都专门开了个脚注告诉你这么写是错的。 这个错也很难察觉我们一步步来。 首先是看std::max的函数签名当然因为实现代码也很简单所以一块看下简化版 template typename T const T max(const T a, const T b) {return ab ? a : b; } 参数用const T 有道理这样左值右值都能收返回值用引用也还算有道理毕竟这里复制一份参数语义和性能上都比较欠缺因为我们要的是a和b中最大的那个而不是最大值的副本。真正的问题是这么做之后max的返回值不能延长a或者b的生命周期但a和b却可以延长作为参数的临时对象的生命周期换句话说max只能延长临时对象的生命周期到max函数运行结束。 现在还不知道问题在哪对吧我们接着看std::max(v, 100)这个表达式。 其中v是没问题的但100是字面量在这绑定到const int时必须实例化出一个int的临时对象。正是这个临时对象上发生了问题。 有人会说这个临时对象在max返回后失效了但事实并非如此。 真相是在一个完整的表达式里产生的临时对象它的生命周期从被创建完成开始一直到完整的表达式结束时才结束。 也就是说100这个临时对象在max返回后其实还存在但max的返回值不能延长它的生命周期value是通过引用进行间接绑定的所以也不能延长这个临时对象的生命。最后完整的表达式结束临时对象100被消耗现在value是悬垂引用了。 这就是典型的临时对象导致的生命周期问题。 由于这个问题太常见所以不仅是文档和教程有列举比较新的编译器也会有警告比如GCC13。 除此之外就只能靠sanitizer来检测了。sanitizer是一种编译器在正常的生成代码中插入一些特殊的监测点来实现对程序行为监控的技术比较常见的应用是检测有没有不正常的内存读写或者是多线程有没有数据竞争等问题。这里我们对悬垂引用的使用正好是一种不正常的内存读取在检测范围内。 编译使用这个指令就能启用检测g -fsanitizeaddress xxx.cpp。遇到内存相关的问题它会立刻报错并退出执行。 问题的本质在于max很容易产生临时对象但自己又完全没法对这个临时对象的生命周期产生影响返回值不是引用可以一定程度上规避问题然而作为通用的库函数这里除了用引用又没啥其他好办法。所以这得算半个设计上的失误。 不仅仅是max和min所有参数是常量左值引用或者非转发引用的右值引用并且返回值的类型是引用且返回的是自己的某一个参数的函数都存在相同的问题。 想彻底解决问题有点难但回避这个问题倒是不难 // 方案1 const int maxValue 100; const int value std::max(v, maxValue);// 方案2 const int value std::max(v, 100); 方案1不需要产生临时对象value始终能引用到表达式结束后依然存在的变量。 方案2是比较推荐的尤其是对标量类型。由于临时变量要在完整表达式结束后才销毁所以把它复制一份给value是完全没问题的赋值表达式也是完整表达式的一部分。这个方案的缺点在于复制成本较高或者无法复制的对象上不适用。但c17把复制省略标准化了这样的表达式在大多数时候不会真的产生复制行为所以我的建议是只要业务和语义上允许优先使用值语义也就是方案2真出了问题并且定位到这里了再考虑转换成方案1。 链式调用中的生命周期问题 从其他语言转c的人相当容易踩这个坑。看个最经典的例子 const char *str path.trimmed().toStdString().c_str(); 简单说明下代码path是一个QString的实例trimmed方法会返回一个去除了首尾全部空格的新的QStringtoStdString()会复制底层数据然后转换成一个std::stringc_str应该不用我多说了这个是把string内部数据转换成一个const char*的方法。 这句表达式同样有问题问题在于表达式结束后str会成为悬垂指针。 一步步来分解问题。首先c_str保证返回的指针有效前提是调用c_str的那个string对象有效。如果string对象的生命周期结束了那么c_str返回的指针也就无效了。 path.trimmed().toStdString()本身是没问题的每一步都是返回的新的值类型的对象实例但是问题在于这些对象实例都是临时对象但我们没有做任何措施来延长临时对象的生命周期整句表达式结束后它们就全析构生命周期终结了。 现在问题应该明了了临时对象上调了c_str但这个临时对象表达式结束后不存在了。所以str最后变成了悬垂指针。 为啥会坑到其他语言转来的人呢因为对于有gc的语言上述表达式实际上又产生了新的到临时对象的可达路径所以对象是不会回收的而对于rust之类的语言还可以精细控制让对象的每一部分具有不同的生命周期上述表达式稍微改改是有机会正常使用的。这些语言转到c把老习惯带过来就要被坑了。 推荐的解决办法只有1种 auto tmp path.trimmed().toStdString(); const char *str tmp.c_str(); 能解决问题但毛病也很明显需要多个用完就扔的变量出来而且这个变量因为根据后续的操作要求很可能还不能用const修饰这东西不仅干扰思维有时候还会成为定时炸弹。 我不推荐直接用string而不用指针是因为有时候不得不用const char*这种时候啥方法都不好使只能用上面的办法去暂存临时数据以便让它的生命周期能延长到后续操作结束为止。 三元运算符中的生命周期问题 三元运算符中也有类似的问题。我们看个例子 const std::string str func(); std::string_view pretty str.empty() ? empty : str; 很简单的一行代码我们判断字符串是不是空的如果是就转换成特殊的占位符字符串。用string_view当然是因为我们不想复制出一份str所以只用string_view来引用原来的字符串而且string_view也能引用字符串字面量用在这里看起来正合适。 事实是这段代码无比的危险。而且-Wall和-Wextra都没法让编译器在编译时检测到问题我们得用sanitizerg -stdc20 -Wall -Wextra -fsanitizeaddress test.cpp。接着运行程序我们会看到这样的报错ERROR: AddressSanitizer: stack-use-after-scope on address …。 这个报错提示我们使用了某个已经析构了的变量。而且新版本的编译器还会很贴心得告诉你就是使用了pretty这个变量导致的。 不过虽然我们知道了具体是哪一行的那个变量导致的问题但原因却不知道而且当我们的字符串不为空的时候也不会触发问题。 这个时候其实就是语法规则在作祟了。 c里规定三元运算符产生的结果最终只能有一种统一的类型。这个好理解毕竟要赋值给某个固定类型的变量的表达式产生大于一种可能的结果类型既不合逻辑也很难正确编译。 但这导致了一个问题如果三元运算符两边的表达式确实有不同的结果类型怎么办现代语言通常的做法是直接报错然而c的做法是按照语法规则做类型转换实在转换不来才会报错。看起来c的做法更宽松这反过来诱发了这节所述的问题。 我们看看具体的转换规则 两个表达式有一边产生void值另一边不是那么三元运算符结果的类型和另一个不是结果不是void的表达式的相同产生void的表达式只能是throw表达式否则算语法错误 两个表达式都产生void则结果也是void这里不要求只能是throw表达式 两个表达式结果类型相同那么三元运算符的结果类型和表达式相同 两个表达式结果类型不同或者具有不同的cv限定符那么得看是否有其中一个类型能隐式转换成另一个如果没有那么是语法错误如果两方能互相转换也是语法错误。满足这个限定条件那么另一个类型的表达式的结果会被隐式类型转换成目标类型比如当出现const char *和std::string的时候因为存在const char *隐式转换成string的方法所以最终三元运算符的结果类型是std::string而T和const T通常结果类型是const T。 这还是我掐头去尾简化了好几次的总结版实际的规则更复杂如果我把实际上的规则列在那难免被喷是语言律师所以我就不自讨没趣了。但这个简化版规则虽然粗糙但实际开发倒是基本够用了。 回到我们出问题的表达式因为pretty初始化后就没再修改过那100%就是三元运算符那里有什么猫腻。恰巧的是我们正好对应在第四点上表达式类型不同但可以进行隐式转换。 按照规则字符串字面量empty要转换成const std::string正好存在这样的隐式转换序列const char[8] - const char * - std::string, 隐式转换序列怎么得出的可以看这里当表达式为真也就是我们的字符串是空的一个临时的string对象就被构造出来了。接着会从这个临时的string构造一个string_viewstring_view只是简单地和原来的string共有内部数据本身没有str的所有权而且string_view也不是“引用”所以它不能延长临时对象的生命周期。接着完整的表达式结束了这时在表达式内创建的临时对象如果没有什么能延长它生命的东西存在就会被析构。显然在这一步从empty转换来的临时string就析构了。 现在我们发现和pretty共有数据的string被销毁了后面继续用pretty显然是错误的。 从别的语言转c的开发者估计很容易踩到这种坑短的字符串字面量转换成string在libstdc还有特殊优化在这个优化下你的程序就算犯了上述错误10次里还是有七八次能正常运行然后剩下两三次得到错误或者崩溃要是换了另一个不同的标准库实现那就有更多的未知在等着你了。这也是string_view在标准中标明的几个undefined behavior之一。所以这个错误经验不足的话会非常隐蔽。 修复倒是不难如果能变更pretty的类型后续可以从pretty创建string_view那有下面几种方案可选 // 方案1 std::string_view pretty str; if (str.empty()) {pretty empty; }// 方案2 const std::string pretty str.empty() ? empty : str;// 方案3 const std::string pretty str.empty() ? empty : str; 方案1里不再有类型转换和临时对象了字符串字面量的生命周期从程序运行开始到程序退出结束没有生命周期问题。但这个方案会显得比较啰嗦而且在字符串为空的时候得多一次赋值。 方案2也没啥特别要说的就是前几节讲的在临时对象销毁前复制了一份。对于标量类型这么做一般没问题对于类类型就得考虑复制成本了不过编译器通常能做到copy elision倒不用特别担心。 方案3其实也比较容易理解我们不是产生了临时对象么那么直接用常量左值引用去绑定这样临时对象的生命周期就能被扩展延长了而且const T 本来就能绑定到str这样的左值上所以语法上没问题运行时也没有问题。 特例 说完三个典型问题还有两个特例。 第一个是关于引用临时对象的非static数据成员的。具体例子如下 具体的例子如下 struct Data {int a;std::string b;bool c; };Data get_data(int a, const std::string b, bool c) {return {a, b, c}; }int main() {std::cout get_data(1, test, false).b \n;const auto str getdata(1, test, false).b;std::cout str \n; } 这个例子是没有问题的。原因在于如果我们用引用绑定了临时对象的非static数据成员也就是subobject那么不仅仅是数据成员整个临时对象的生命周期都会得到延长。所以这里str虽然只绑定到了成员b但整个临时对象会获得和str一样的生命周期所以不会在完整的表达式结束后销毁因此后续继续使用str是安全的。 这个subobject还包括数组元素所以const int num temp-array[index];也会导致整个数组的生命周期被延长。 符合要求的形式还有很多这里就不一一列举了。 不过这个特例带来了风险因为完整表达式结束后我们访问不到其他成员了但它们都还实际存在这会留下资源泄露的隐患。现代的编程语言也基本都是这么做的为了照顾大部分人的习惯倒也无可厚非自己注意一下就行。 第二个特例是for-range循环。先看例子 class Data {std::vectorint data; public:Data(std::initializerlistint l): data(l){}const std::vectorint getdata() const{return data;} };int main() {for (const auto v: Data{1, 2, 3, 4, 5}.get_data()) {std::cout v \n;} } 在c23之前这是错的实际上我们用msvc运行会看到什么也没输出用GCC和sanitize则直接报错了。GCC同时还会直接给出警告告诉你这里有悬垂引用。 问题倒是不难理解for循环里冒号右侧的表达式实际上是一个完整的表达式并且在进入for循环之前就计算完了所以临时对象被销毁我们通过引用返回值间接传递出来的东西自然也就失效了。 然而这是语言设计上的bug。同样作为初始化语句for (int ixxx, i xx, i)中的i的生命周期就是从初始化开始到for循环结束才结束的所以形式上类似的for-range没有理由作为例外否则很容易产生陷阱并限制使用上的便利性。 如果只是和普通for循环有差异那倒还好问题是标准规定了for-range需要转换成某些规定形式这会导致下面的结果 // 正常的没有问题 for (const auto v : std::vector{1,2,3,4,5}) {std::cout v \n; } 同样都是初始化语句里的临时变量怎么一个有生命周期问题一个没有因为和标准规定的转换形式有关感兴趣的可以去深究一下。但这是实打实的行为矛盾就像一个人早上说自己是地球人但吃完午饭就改口说自己是大猩猩一样荒谬。 这个bug也有一段时间了直到前年才有提案来想办法解决不过好消息是已经被接受进c23了现在for-range的初始化语句中产生的临时对象的生命周期会延长到for-range循环结束不管是什么形式的。 可惜到目前为止我还没看到有编译器支持GCC 14.1clang 18.1.8作为临时解决办法你只能这么写 int main() {const auto tmp Data{1, 2, 3, 4, 5};for (const auto v: tmp.get_data()) {std::cout v \n;} } 如何发现生命周期问题 既然这些坑这么危险又这么隐蔽那有办法及时发现防患于未然吗 这还是比较难的也是当今的热门研究方向。 rust选择了用类型系统编译检测来扼杀生命周期问题但效果不太理想除了issue里那些bug之外缓慢的编译速度和无法简单实现某些数据结构也是不小的问题。但整体来说还是比c前进了很多步上面列举的三类问题一些是语法规则禁止的另一些则能在编译时检测出来。 c语法已经成型也很难引进太大的变化想及时发现问题就得依赖这三样了 constexpr sanitizer 静态分析 constexpr里禁止任何形式的内存泄露也禁止越界访问和使用已经析构的数据但这些检测只有在编译期计算时才进行而且不是什么东西都能放进constexpr的所以虽然能发现生命周期问题但限制太大。 sanitizer没有constexpr那么多限制而且检测的种类更多也更仔细但缺点是需要程序真正运行到有问题的代码上才能上报如果不想每次都运行整个程序你就得有一个质量上乘的单元测试集sanitizer还会拖慢性能以address检测器为例平均而言会导致性能下降1到2倍尽管已经比valgrind这样的工具快多了但有时候还是会因为太慢而带来不便。 静态分析不需要运行实际代码它会分析代码的调用路径和操作然后根据一定的模式来找出看起来有问题的代码。好处是不用实际运行安装配置简单编译器一般还自带了一个可以用坏处是容易误报分析能力有时不如人类尤其是逻辑比较复杂时。 工具各有千秋结合起来一起使用是比较常见的工程实践。 个人的知识和经验也绝不能落下因为从编码这个源头上就扼杀生命周期问题是目前最经济有效的办法。 总结 常见的表达式中临时变量导致的生命周期问题就是这些了。 modern c其实一直在推行值语义一定程度上可以缓解这些问题但c真的太复杂了永远没有银弹能解决所有问题。还是得自己慢慢积累知识和经验才行。 文章转载自apocelipes 原文链接https://www.cnblogs.com/apocelipes/p/18291697 体验地址引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构
- 上一篇: 上海市建设协会考试网站爱润妍网站开发
- 下一篇: 上海市网站建设定制百度下载官网
相关文章
-
上海市建设协会考试网站爱润妍网站开发
上海市建设协会考试网站爱润妍网站开发
- 技术栈
- 2026年03月21日
-
上海市建设网站有没有做培养基的网站
上海市建设网站有没有做培养基的网站
- 技术栈
- 2026年03月21日
-
上海市建设安全协会网站一360网站上放百度地图怎么
上海市建设安全协会网站一360网站上放百度地图怎么
- 技术栈
- 2026年03月21日
-
上海市网站建设定制百度下载官网
上海市网站建设定制百度下载官网
- 技术栈
- 2026年03月21日
-
上海市网站兰州官网seo哪家公司好
上海市网站兰州官网seo哪家公司好
- 技术栈
- 2026年03月21日
-
上海市中学生典型事例网站做竞价推广的网站要求
上海市中学生典型事例网站做竞价推广的网站要求
- 技术栈
- 2026年03月21日
