c++临时对象导致的生命周期问题
对象的生命周期是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 a>b ? 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++ -fsanitize=address 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是完全没问题的,赋值表达式也是完整表达式的一部分。这个方案的缺点在于复制成本较高或者无法复制的对象上不适用。但c++17把复制省略标准化了,这样的表达式在大多数时候不会真的产生复制行为,所以我的建议是只要业务和语义上允许,优先使用值语义也就是方案2,真出了问题并且定位到这里了再考虑转换成方案1。
链式调用中的生命周期问题
从其他语言转c++的人相当容易踩这个坑。看个最经典的例子:
const char *str = path.trimmed().toStdString().c_str();
简单说明下代码,path
是一个QString
的实例,trimmed
方法会返回一个去除了首尾全部空格的新的QString
,toStdString()
会复制底层数据然后转换成一个std::string
,c_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
都没法让编译器在编译时检测到问题,我们得用sanitizer:g++ -std=c++20 -Wall -Wextra -fsanitize=address 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_view
,string_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 = get_data(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::vector<int> data_;
public:Data(std::initializer_list<int> l): data_(l){}const std::vector<int> &get_data() const{return data_;}
};int main()
{for (const auto &v: Data{1, 2, 3, 4, 5}.get_data()) {std::cout << v << '\n';}
}
在c++23之前,这是错的,实际上我们用msvc运行会看到什么也没输出,用GCC和sanitize则直接报错了。GCC同时还会直接给出警告告诉你这里有悬垂引用。
问题倒是不难理解,for循环里冒号右侧的表达式实际上是一个完整的表达式,并且在进入for循环之前就计算完了,所以临时对象被销毁,我们通过引用返回值间接传递出来的东西自然也就失效了。
然而这是语言设计上的bug。同样作为初始化语句,for (int i=xxx, 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也有一段时间了,直到前年才有提案来想办法解决,不过好消息是已经被接受进c++23了,现在for-range的初始化语句中产生的临时对象的生命周期会延长到for-range循环结束,不管是什么形式的。
可惜到目前为止,我还没看到有编译器支持(GCC 14.1,clang 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快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构
相关文章:

c++临时对象导致的生命周期问题
对象的生命周期是c中非常重要的概念,它直接决定了你的程序是否正确以及是否存在安全问题。 今天要说的临时变量导致的生命周期问题是非常常见的,很多时候没有一定经验甚至没法识别出来。光是我自己写、review、回答别人的问题就犯了或者看到了许许多多这…...

CSP-J 算法基础 深度优先搜索
文章目录 前言深度优先搜索通俗解释例子深度优先搜索的步骤DFS 的特点生活中的类比 为什么递归问题会变成深度优先搜索?递归与深度优先搜索的关系:递归与系统栈递归调用的过程:栈的作用: 递归与系统栈的简单示例递归实现 DFS 的简…...

LeetCode题练习与总结:基本计算器 Ⅱ--227
一、题目描述 给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。 整数除法仅保留整数部分。 你可以假设给定的表达式总是有效的。所有中间结果将在 [-2^31, 2^31 - 1] 的范围内。 注意:不允许使用任何将字符串作为数学表达式计算…...

Elasticsearch基础(七):Logstash如何开启死信队列
文章目录 Logstash如何开启死信队列 一、确保 Elasticsearch 输出插件启用 DLQ 支持 二、配置 Logstash DLQ 设置 三、查看死信队列 四、排查 CSV 到 Elasticsearch 数据量不一致的问题 Logstash如何开启死信队列 在 Logstash 中,死信队列(Dead Le…...

c语言--力扣简单题目(链表的中间节点)讲解
题目如下: 给你单链表的头结点 head ,请你找出并返回链表的中间结点。 如果有两个中间结点,则返回第二个中间结点。 示例 1: 输入:head [1,2,3,4,5] 输出:[3,4,5] 解释:链表只有一个中间结点…...

【STM32 Blue Pill编程】-定时器计数模式
定时器计数模式 文章目录 定时器计数模式1、定时器计数模式介绍2、硬件准备及接线3、模块配置3.1 定时器计数模式配置3.2 定时器中断配置3.3 串口配置4、代码实现在本文中,我们将讨论如何在计数器模式下配置 STM32 Blue Pill 定时器模块。 要将定时器用作计数器,我们将其配置…...

【例题】lanqiao1331 二进制中 1 的个数
二进制中 1 的个数 题目描述 给定一个整数 x,输出该数二进制表示中 1 的个数。 例:9 的二进制表示为 1001,有 2 位是 1 ,所以函数返回 2。 输入描述 输入 x (内存空间为 32 位的整数)。 输出描述 第一…...

【论文解读】图像序列识别:CRNN技术在场景文本识别中的应用与突破(附论文地址)
论文地址:https://arxiv.org/pdf/1507.05717 这篇文章的标题是《An End-to-End Trainable Neural Network for Image-based Sequence Recognition and Its Application to Scene Text Recognition》,作者是Baoguang Shi, Xiang Bai和Cong Yao,…...

Vue3+CesiumJS相机定位camera
new Cesium.Camera (scene) 摄像机由位置,方向和视锥台定义。 方向与视图形成正交基准,上和右视图x上单位矢量。 视锥由6个平面定义。每个平面都由 Cartesian4 对象表示,其中x,y和z分量定义垂直于平面的单位矢量,w分量…...

turbo译码算法MAX, MAX_SCALE and MAX_STAR的比较
在Turbo码的译码算法中,MAX、MAX_SCALE和MAX_STAR是涉及对数似然比(LLR)计算时,对MAP(最大后验概率)算法或其变种Log-MAP算法中分支度量计算的几种不同处理方式。下面是对这三种方法的比较: 1.…...

关于HarmonyOS的学习
day31 购物车案例 一、加入购物车 1、点击按钮后,把当前这个列表的数据拿到,应该存储到一个数组里面 --- 数据结构,把数据存储进行数组2、假如已经把所有的数据添加数组完毕,最终应该存储进购物车里面,所谓的购物车说…...

【雅特力AT32】搭建模板工程及GPIO点灯操作
目录 AT32模板工程建立及点灯操作 建立AT32模板工程 AT32点灯操作 LED原理图GPIO寄存器LED源码分析 建立AT32模板工程 从0到编译运行详细搭建保姆教程: 【雅特力AT32】Keil 环境:搭建标准库模板工程、使用 AT-Link、Debug 里选择 CMSIS-DAP调试器 下面做…...

实战千问2大模型第三天——Qwen2-VL-7B(多模态)视频检测和批处理代码测试
画面描述:这个视频中,一位穿着蓝色西装的女性站在室内,背景中可以看到一些装饰品和植物。她双手交叉放在身前,面带微笑,似乎在进行一场演讲或主持活动。她的服装整洁,显得非常专业和自信。 一、简介 阿里通义千问开源新一代视觉语言模型Qwen2-VL。其中,Qwen2-VL-72B在大…...

数据库索引底层数据结构之B+树MySQL中的页索引分类【纯理论干货,面试必备】
目录 1、索引简介 1.1 什么是索引 1.2 使用索引的原因 2、索引中数据结构的设计 —— B树 2.1 哈希 2.2 二叉搜索树 2.3 B树 2.4 最终选择之——B树 2.4.1 B树与B树的对比(面向索引)【面试题】 3、MySQL中的页 3.1 页的使用原因 3.2 页的结构 3.2.1 页文件头和页文件…...

编译QT源码时的configure参数须知
文章目录 一、configure help原文二、configure help机译三、features 执行命令得到configure帮助文件 qtsrc/configure --help一、configure help原文 Usage: configure [options] [-- cmake-options]This is a convenience script for configuring Qt with CMake. Options…...

如何利用人工智能大模型来进行数字化营销?
这是一本关于如何利用人工智能大模型来进行数字化营销并驱动业绩增长的书。人工智能大模型是指那些具有超大规模的参数和数据的人工智能模型,它们能够在各种复杂的任务上表现出惊人的能力。 在本书中,你将学习到如何在电商、广告和用户增长等数字化营销业…...

【MRI基础】回波序列长度-echo train length ETL概念
回波序列长度 回波序列长度 (echo train length, ETL) 是磁共振成像 (MRI) 中的一个重要参数,它对图像采集时间和图像质量有显著影响。ETL 是指在单个激励脉冲之后的 MRI 序列中采集的回波数量。通过增加 ETL,可以在一个重复时间 (TR) 内收集多个回波&a…...

(179)时序收敛--->(29)时序收敛二九
1 目录 (a)FPGA简介 (b)Verilog简介 (c)时钟简介 (d)时序收敛二九 (e)结束 1 FPGA简介 (a)FPGA(Field Programmable Gate Array)是在PAL (可编程阵列逻辑)、GAL(通用阵列逻辑)等可编程器件的基础上进一步发展的产物。它是作为专用集成电路(ASIC)领域…...

[Visual Stuidio 2022使用技巧]2.配置及常用快捷键
使用vs2022开发WPF桌面程序时常用配置及快捷键。 语言:C# IDE:Microsoft Visual Studio Community 2022 框架:WPF,.net 8.0 一、配置 1.1 内联提示 未开启时: 开启后: 开启方法: 工具-选…...

每日奇难怪题(持续更新)
1.以下程序输出结果是() int main() {int a 1, b 2, c 2, t;while (a < b < c) {t a;a b;b t;c--;}printf("%d %d %d", a, b, c); } 解析:a1 b2 c2 a<b 成立 ,等于一个真值1 1<2 执行循环体 t被赋值为1 a被赋值2 b赋值1 c-- c变成1 a<b 不成立…...

江协科技STM32学习- P13 TIM定时器中断
🚀write in front🚀 🔎大家好,我是黄桃罐头,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流 🎁欢迎各位→点赞👍 收藏⭐️ 留言📝…...

git github仓库管理
原文链接:git github仓库管理 拉取镜像 github的仓库有两种下载方式,http和ssh,http是对外公开的,可以直接clone,ssh的一般是自己的或内部的仓库,仓库需要配置ssh-key才能使用git clone. 或者直接网页下载 #https git clone https://github.com/git/git.git #ssh…...

【JavaEE】线程安全性问题,线程不安全是怎么产生的,该如何应对
产生线程不安全的原因 在Java多线程编程中,线程不安全通常是由于多个线程同时访问共享资源而引发的竞争条件。以下是一些导致线程不安全的常见原因: 共享可变状态:当多个线程对共享的可变数据进行读写时,如果没有适当的同步机制&…...

低代码-赋能新能源汽车产业加速前行
在“双碳”战略目标的引领下,全球新能源汽车产业正经历着前所未有的发展和变革,新能源汽车整车制造成为绿色低碳转型的重要领域。在政府的大力扶持下,新能源整车制造领域蓬勃发展,已成为全球汽车产业不可逆转的重要趋势。新能源汽…...

基于UDP的简易网络通信程序
目录 0.前言 1.前置知识 网络通信的大致流程 IP地址 端口号(port) 客户端如何得知服务器端的IP地址和端口号? 服务器端如何得知客户端的IP地址和端口号? 2.实现代码 代码模块的设计 服务器端代码 成员说明 成员实现 U…...

AI大模型在知识管理平台上的应用:泛微·采知连实现自动采集.精准搜索.智能问答.主动推荐
AI技术的发展,正在推动组织知识管理模式发生变革。知识管理系统通过各种应用实现知识体系落地,当前聚焦于整合生成式AI技术,以提升业务效率。 组织在数字化进程中面临着知识增量增多、知识更新频率变快、知识与业务结合更紧密等挑战ÿ…...

JavaEE:文件内容操作(一)
文章目录 文件内容的读写---数据流字节流和字符流打开和关闭文件文件资源泄漏try with resources 文件内容的读写—数据流 文件内容的操作,读文件和写文件,都是操作系统本身提供了API,在Java中也进行了封装. Java中封装了操作文件的这些类,我们给它们起了个名字,叫做"文…...

无人机视角下落水救援检测数据集
无人机视角下落水救援检测数据集,利用无人机快速搜索落水者对增加受害者的生存机会至关重要,该数据集共收集12万帧视频图像,涵盖无人机高度从10m-60m高度,检测包括落水者(11万标注量)、流木(900…...

openssl+keepalived安装部署
文章目录 OpenSSL安装下载地址编译安装修改系统配置版本 Keepalived安装下载地址安装遇到问题安装完成配置文件 keepalived运行检查运行状态查看系统日志修改服务service重新加载systemd检查配置文件语法错误 OpenSSL安装 下载地址 考虑到后面设备可能没法连接到外网&…...

float存储原理
float存储原理基于IEEE 754标准,主要包括符号位、指数位和有效数字位三部分。以下是对其存储原理的具体介绍: 符号位:符号位是浮点数中用于表示正负的位。在单精度浮点数(32位)中,最左边的第1位是符号位&a…...