C++中那些你不知道的未定义行为
引子
开篇我们先看一个非常有趣的引子:
// test.cpp
int f(long *a, int *b) {*b = 5;*a = 1;return *b;
}int main() {int x = 10;int *p = &x;auto q = (long *)&x;auto ret = f(q, p);std::cout << x << std::endl;std::cout << ret << std::endl;return 0;
}
请问输出的x
和ret
分别是什么?
相信大家都能一眼看出,在函数f
中,a
和b
指向的都是main
中的x
,而*a = 1
后执行,所以x
的值应该是1
,而*b
同样表示x
的值,所以程序输出的结果应该是两个1
。
没错!我们简单编译、运行一下,发现确实是输出两个1。但,有趣的事情来了!如果在编译的时候,加上一个-O2
参数,情况就会超出你的想象:
$ g++ test.cpp -o test.out
$ ./test.out
1
1$ g++ -O2 test.cpp -o test.out
$ ./test.out
1
5
这种神奇的现象是如何产生的呢?这一篇我们就来盘一盘C++中的未定义行为(Undefined Behavior,简称UB),以及编译期优化(Optimization)。
语义(Semantics)&实现(Implementation)
这应该是笔者第N+1次提及这两个概念了,要想深入掌握C++,语义和实现的问题是一定绕不开的。再次说明,「语义」表达的是代码字面上的含义,或者说它代表了程序员的意愿;而「实现」则是编译器层面,如何将一种语义翻译为机器指令的过程。
同一种语义可以有不同的编译器实现方式,当然,也就存在优化好的实现和优化差的实现,不过通常情况下无论哪种编译器,哪种实现方式,都是一定要保证其行为一致的,否则我们就可以认为这个编译器写得有问题。
然而,对于一些语义中的未定义行为(以下都简称UB),则不同的编译器实现就可能出现不同的行为。这是C++非常纠结与奇怪的地方,照理说,UB都应该直接报错,不允许编译才对,但C++很特殊,它在引入高层语义的同时又保留了很多C语言继承来的底层特性,这样做的好处有两个:其一,是为了兼容C,比如说在项目中引入C的开源库,那你就必须支持C的灵活性;其二,是C++有一个设计哲学,就是说如果你不希望使用某一部分特性,那么这部分特性就不会对你产生附加影响(虽然说不能百分百做到吧,但至少是这种导向)。
了解了这件事情以后,我们就来回头看一下引子中的例子:
auto q = (long *)&x;
这是一个C风格的强转,如果用cast
方式表示,它应当写作:
auto q = reinterpret_cast<long *>(&x);
这其实就是一个UB,原因在于,int
和long
的长度是不同的,因此对q
进行操作的时候,会向后多操作一片空间。
我们再来看看前面的函数:
int f(long *a, int *b) {*b = 5;*a = 1;return *b;
}
由于a
跟b
类型不同,并且长度也不同,那么在编译器的-O2
优化当中,就会认为他们一定指向的不是同一个对象。而在函数内部*b = 5
中,5
已经是一个常量了,因此,就可以在编译期确定,而最后函数return *b
,那么就可以判断返回值一定是5
。
因此,这就是一个UB导致了编译器优化后行为与预期不一致的情况,因为在这个例子中,return *b
并没有做真正的解指针操作,而是被编译期优化为了return 5
。
以下是这段代码的AMD64汇编情况,不进行优化时:
f(long*, int*):push rbpmov rbp, rspmov QWORD PTR [rbp-8], rdimov QWORD PTR [rbp-16], rsimov rax, QWORD PTR [rbp-16]mov DWORD PTR [rax], 5mov rax, QWORD PTR [rbp-8]mov QWORD PTR [rax], 1mov rax, QWORD PTR [rbp-16]mov eax, DWORD PTR [rax] ; 返回值入eax,这里是真实的取内存数据pop rbpret
而加上-O2
优化后则是:
f(long*, int*):mov DWORD PTR [rsi], 5mov eax, 5 ; 返回值入eax,直接写的5mov QWORD PTR [rdi], 1ret
那么,如何解决这个问题呢?如何可以在-O2
优化时,还能让他实际去取数据呢?这个问题我们在下一节探讨。
std::launder
std::launder
绝对可以算得上C++的黑魔法之一,我最初看到它的说明的时候可以说是一脸懵逼,我们来看一下它的原型:
template <class T>
[[nodiscard]] constexpr T* launder(T* p) noexcept;
需要注意,这个[[nodiscard]]
是在C++20标准上补充进去的,C++17时没有强行做规定,但实际上它的返回值确实不可丢弃,否则无意义。
那么,std::launder
究竟做了什么?我可以负责人的告诉你,其实它什么都没做,它就是把参数原样输出了。也就是说:
std::lanuder(p) == p; // true
std::lanuder(q) == q; // true
你穿进去一个指针,它会把这个指针原样输出……
啥玩意?!在逗我吗?!这就是C++的黑魔法了。从语义上来说,std::launder
确实是什么都没做,但它是给编译器看的,也就是说,它改变了「实现」而不是「语义」。
它的功能就是告诉编译器,对于传入的指针所指向的地址不进行静态优化,强制编译成运行时从内存取数据的操作。所以,拿到它的返回值后必须立刻处理才能达成这个目的,这也是这个函数返回值不可丢弃的原因,因为如果丢弃的话就没有什么意义了。
对应引子中的例子,我们既然已经知道出问题的语句是在f
函数中的返回语句上,编译期会认为*b
的值一定是5
,所以常量化了,那么,在这一行语句上加上std::launder
就可以解决问题:
#include <new> // launder在这个头文件中声明
int f(long *a, int *b) {*b = 5;*a = 1;return *std::launder(b);
}
再用-O2
参数编译,可以得到结果:
f(long*, int*):mov DWORD PTR [rsi], 5mov eax, DWORD PTR [rsi] ; 这里真的去取数据了,而不是直接写常数5mov QWORD PTR [rdi], 1ret
唉?等会!好像还是不对呀,虽然它确实是从[rsi]
中取数据放到eax
中了,但是,这时候仍然放进去的是5
呀!因为赋值时1
的那句在下面。
别急,这个操作会有连带反应,这时我们找一个调用它的函数也编译一下:
void Demo() {int x = 10;int *p = &x;auto q = (long *)&x;auto ret = f(q, p);std::printf("%d", ret);
}
会变成这样:
.LC0:.string "%d"
Demo():mov edi, OFFSET FLAT:.LC0mov DWORD PTR [rsp-12], 1 ; 这里会写进去1call printf
惊不惊喜!意不意外!实际上外层都没有实际去调用f
函数,它是整个优化了。所以这里-O2
参数法就是「语义」跟「实现」基本八竿子打不着的优化方式了,编译器似乎特别有自己的想法似的,它会认为它「读懂」了代码,然后重写了一份新的再去编译,看起来就是把整个代码搞得面目全非。
不过这里要强调一点,对于UB来说,编译期是可以自由发挥的,如果读者自行尝试时输出结果跟我不一致也不用奇怪,不同的编译器、甚至不同版本的编译器都可能有不同的行为。
而本节介绍的std::launder
仅仅是强制编译器取取地址而已,而如果代码顺序发生变化,那么它的结果可能仍然不是我们想要的,所以并不是说有了launder
就万事大吉,根本上来说要想彻底解决这个问题,只能是不开启-O
优化,或者是不使用这次UB。
UB到底能不能用?
需要注意的是,前面章节提到的这个例子是一个很极端的例子,必须很多条件都要同时具备,比如说:
- 必须是gcc较高版本的编译器;
- 必须开启
-O2
或更高级别的优化; f
函数的两个参数必须是不同含义的(比如说int *
和float *
也会触发,但如果是int *
和unsigned *
则不会触发);- 必须是编译为AMD64架构的指令;
因此,并不是说只要是UB就一定会触发bug,很多时候要随缘。所以相信很多读者也跟我一样,有一个虽然标准定义为UB的写法,但是写了很多年也从来没有出现过问题。
那么我们就可以发现,UB要想触发bug,必须满足两个条件:
- 代码中出现了未定义行为(UB);
- 编译器进行了优化,并且正巧这种优化引发了bug。
这个很好理解,如果你的代码本身就是标准定义的行为,那么无论编译器怎么优化,都必须保证行为一致。而即便用了UB,编译器如果不进行优化,那么我们还是可以知道这个行为的后果,做一些魔法操作。而就算编译器对UB进行优化了,也未必就会导致一个不合预期的结果。所以说,能复现出这种现象还是比较难的。
那么,到底能不能写UB呢?其实结论也显而易见,如果你能够确定这段代码编译后的行为,那么你就可以使用,否则不行。
举例来说,对于服务器后端开发的同学而言,通常情况下服务器的架构是明确的,程序只会进行一次编译,之后只要没有改动,那么就会一直在服务器上运行。对于这种场景来说,只要你的程序能通过测试,那么它就是正确的,因为UB是针对代码而言的,一旦经过编译,那么行为就是确定的了。
而如果对于那些写代码库的同学而言,你的代码可能会被不同的项目所引用,而使用你代码库的项目会使用哪种编译器、哪种架构、哪种编译参数等都是不确定的。那么在这种情况下,UB就可能造成部分实例命中bug。
因此,UB也并不是说一定不能使用,我们还是要根据实际场景,毕竟对于C++来说,UB呀、告警呀什么的也是家常便饭了……(手动狗头保命~)
4中行为(Behavior)
接下来我们就来详细介绍一下C++当中可能出现的4种行为,它们分别是:
- WB,Well-defined Behavior,明确定义的行为
- IB,Implementation-defined Behavior,由实现方定义的行为
- USB,Unspecified Behavior,未明确定义的行为
- UB,Undefined Behavior,未定义的行为
其中、WB和IB又合称SB,Specified Behavior,确定的行为。下面我们来分别解释:
Well-defined Behavior
这一个应当很好理解,WB就是语言标准中明确定义的行为,任何编译器实现都必须按照语言规定的方式,与优化与否无关。
Implementation-defined Behavior
IB则是语言标准中未定义,但是在编译器实现的时候会明确定义的,IB的行为也是确定的,但是对编译环境有要求,不同环境可能会出现不同行为。
举个例子来说,sizeof(long)
的行为就是一个IB,因为long
在不同环境下的长度不同,32位环境下长度是4,而64位环境下长度是8。但对于这个IB来说,只要环境的字长确定了,那么这个值就也是确定的,所以它也属于SB,也就是确定的行为。
Unspecified Behavior
USB这个缩写歧义比较大(你懂得……),所以一般也不会出现,我这里就直接用中文翻译来讲了。
未明确定义的行为指的是,它的行为一定在一个集合内,但具体不确定。举例来说,一个非常经典的逗号运算的结合顺序:
int i = 0;
int j = ++i, i; // 请问j的值?
我们知道逗号运算的返回值是最右边的表达式,但是在逗号运算中是从左到右处理还是从右到左处理,这是没有明确定义的。但是,它的结果只能是「从左到右」或「从右到左」的其中一种,不可能出现「从中间劈开处理」的这种……
再比如说:
int f1(int &a) {if (a > 0) {a++;}return a;
}int f2(int &a) {a = 0;return 0;
}void Demo() {int i = 0;int j = f1(i) + f2(i); // 请问j的值?
}
同理,f1
和f2
谁先算是不确定的,但是反正肯定要么先算f1
,要么先算f2
,不会出现其他的情况,所以这也是一个未明确定义的行为。
未明确定义的行为虽然说结果也不确定,但一般都是比较可控的,而且即便出现了不符合预期的情况,它也不会太离谱,通常情况下可以快速定位到。
Undefined Behavior
UB就是今天的主角了,它就是哪哪都没有定义的行为,非常的薛定谔,哪怕就是换个编译器版本,或者只是换个编译参数,都可能会导致行为发生改变。这也是我们需要注意的地方,因为UB命中bug的时候(参考引子中的例子)通常都会完全超乎意料之外,非常不容易定位到。
几个有趣的UB
那么既然能遇到薛定谔的编译器优化,会让UB变得也如此薛定谔,那我们就来盘一盘哪些UB是我们可能容易踩到的,在编译器优化的时候会出现离谱的情况。(注:这里笔者也只是总结了一些常见的比较经典的UB,但肯定不全面,后续发现了还会进行补充,如果读者知道其他有趣的UB也欢迎评论区补充~)
重解释转换
这个应该不用多说了,前面篇幅的例子已经很好地解释了这个问题。对指针进行重解释转换后,再使用原先的指针,就会触发UB。
我们知道在语义层面,「指针」是与「对象」相绑定的,也就是说一个指针,就表示某一个对象的索引(正是因为这种语义,C++才引入了「引用」,因为它可以更好地表示这种属性)。而在「实现」层面,指针则是一个单独的变量,只不过保存了一个内存地址而已。如果进行重解释转换,在语义上就表示所指对象发生了改变,那么原本的对象就应该没有了才对。
int16_t a = 5; // 原本的对象
int16_t *p = &a; // p就是a的指针
uint8_t *q = reinterpret_cast<uint8_t *>(p); // 语义上来说,这里应当是生成了一个新的对象,只是新的对象复用了原本对象的地址而已
*q = 8; // WB
*p = 0; // UB
a = 1; // UB
上面的例子中,p
和q
在语义层面就是指向两个不同的对象的,但是实现的时候,它们却指向了同一片内存空间。所以,如果编译器进行优化,就会忽略它们可能重叠的这一特性。
因此,如果对指针进行了重解释转换,那么从语义上来说,原来的对象就应当被替代了。用上面的例子讲就是,当p
转换为q
后,q
指向的是一个新的uint8_t
类型的对象,代替了原本int16_t
类型的对象,那么这一句之后,对p
进行解指针就会成为UB,同理操作变量a
也是UB。
placement new
这个跟上一节的UB比较类似,都是「复用了地址」但「不同对象」的情况。举例来说:
struct Test {int a, b;
};void Demo() {Test *p1 = new Test{1, 2}; // 原对象Test *p2 = new(p1) Test{10, 20}; // 就地构造新对象p2->a = 5; // WBp1->b = 10; // UB
}
从语义上来说,p1
和p2
指向两个对象,但由于p2
是在p1
指向的位置上构造的,那么照理说,原本的对象就被代替了,不应当再去使用。所以,在placement new语句后,再对p1
进行任何解操作就属于UB。
不过这种UB(包括上一节的情况,以及比如说realloc
之类的情况)是可以有办法破解的,就是使用std::launder
,比如说:
p1->b = 10; // UB
std::launder(p1)->b = 10; // WB
为什么呢?因为launder
的语义就是「读取内存」,它已经脱离了原本「指向某个对象」的这种语义了。这里p1
指向的对象已经被代替了,所以操作它是UB,而std::launder(p1)
表示的是「操作p1
所指向的内存空间」,这种语义下并不再指向原本的对象,那自然就不是UB了。
if语句的假定
来看下面这个例子:
void Demo(int *p) {*p = 5;if (p == nullptr) {// ...return;}
}
我们是在对p
进行解操作以后才对p
进行判空的(这里可能是不小心写错了,或者说考虑了一些多线程的情况之类的吧,反正就是结果写成上面这样了),但这已经出发了UB,编译期优化时会认为,如果p
为空,那么*p = 5
这一句就会CoreDump了,所以,能走到if
这里就说明p
一定不为空,所以判定这是一个恒为假的if
语句,从而直接不编译。
再来看一种:
int Demo(int a) {int b; // 这里没有初始化if (a > 0) {b = 5;}return b;
}
变量不进行初始化,会触发UB,在编译器优化时,会认为直接把未初始化的变量返回是不正常的,所以就会把这里的if
优化为恒为真,也就是默认a
是大于零的,因此最终会把调用Demo
函数的地方直接编译成常数5
。(有木有很离谱~)
返回值遗漏
看这样一个例子:
int g_count; // 一个全局变量计数器
int Demo() {while (g_count-- > 0) {std::cout << "Get it!" << std::endl;}// 这里丢弃了返回值
}
一个有int
返回值的函数,但没有书写返回语句,会触发UB。而在编译器优化时,会直接把上面的循环编译成死循环,也就是一直输出"Get it!"。
溢出
有一个比较离谱的事情,无符号数的反转是WB,但有符号的溢出是UB,请看下面的例子:
bool f1(int8_t x) {return x > x + 1;
}bool f2(uint8_t x) {return x > x + 1;
}
正常来讲,f1
中如果x
是127
时,加一发生溢出,应当返回true
,其他情况返回false
。而f2
中是255
时加一发生反转,返回true
,其他情况返回false
。
但有「溢出」是一种UB,反转则是「WB」,所以f1
会被优化为返回恒false
,而f2
则会被优化为return !(x ^ 0xff);
。
编译器的优化方向
其实从上一章的几个例子中,我们能发现一个规律,就是说编译器在优化时,都是「假定UB不会发生」为原则去进行优化的,如果出现了UB,则会以当前语句不会执行到为前提进行预想,然后优化代码。
就比如说引子中的例子,重解释转换后使用原指针是UB,那么编译期优化的时候就会假定两个参数并不是通过重解释转换得来的,也就是说两个指针的值一定不相同。以这个为原则进行优化,自然就会出现一开始的结果。
总结
以上就是以一个有趣的例子为引子,引发笔者对UB和编译器优化方向的一些学习和研究,分享给大家。
相关文章:

C++中那些你不知道的未定义行为
引子 开篇我们先看一个非常有趣的引子: // test.cpp int f(long *a, int *b) {*b 5;*a 1;return *b; }int main() {int x 10;int *p &x;auto q (long *)&x;auto ret f(q, p);std::cout << x << std::endl;std::cout << ret <&…...

java基础面试题(四)
Mysql索引的基本原理 索引是用来快速寻找特定的记录;把无序的数据变成有序的查询把创建索引的列数据进行排序对排序结果生成倒排表在倒排表的内容上拼接上地址链在查询时,先拿到倒排表内容,再取出地址链,最后拿到数据聚簇索引和非…...

@PropertySource使用场景
文章目录一、简单介绍二、注解说明1. 注解源码① PropertySource注解② PropertySources注解2. 注解使用场景3. 使用案例(1)新增test.properties文件(2)新增PropertySourceConfig类(3)新增PropertySourceTe…...

【C语言进阶:刨根究底字符串函数】strtok strerror函数
本节重点内容: 深入理解strtok函数的使用深入理解strerror函数的使用⚡strtok Returns a pointer to the first occurrence of str2 in str1, or a null pointer if str2 is not part ofstr1sep参数是个字符串,定义了用作分隔符的字符集合。第一个参数指…...

西安石油大学C语言期末重点知识点总结
大一学生一周十万字爆肝版C语言总结笔记 是我自己在学习完C语言的一次总结,尽管会有许多的瑕疵和不足,但也是自己对C语言的一次思考和探索,也让我开始有了写作博客的习惯和学习思考总结,争取等我将来变得更强的时候再去给它优化出…...

读《Multi-level Wavelet-CNN for Image Restoration》
Multi-level Wavelet-CNN for Image Restoration:MWCNN摘要一. 介绍二.相关工作三.方法摘要 存在的问题: 在低级视觉任务中,对于感受野尺寸与效率之间的平衡是一个关键的问题;普通卷积网络通常以牺牲计算成本去扩大感受野&#…...

【Linux】安装DHCP服务器
1、先检测网络是否通 get dhcp.txt rpm -qa //查看软件包 rpm -qa |grep dhcp //确定是否安装 yum install dhcp //进行安装 安装完成后 查询 rpm -ql dhcp 进行配置 cd /etc/dhcp 查看是否有遗留dhcpd.conf.rpmsave 删除该文件 cp /usr/share/doc/dhcp-4.1.1/dhcpd.conf.sampl…...

功能测试转型测试开发年薪27W,又一名功能测试摆脱点点点,进了大厂
咱们直接开门见山,没错我的粉丝向我投来了喜报,从功能测试转型测试开发,进入大厂,摆脱最初级的点点点功能测试,拿到高薪,遗憾的是,这名粉丝因为个人原因没有经过指导就去面试了,否则…...

数据结构之哈希表
常见的三种哈希结构 数组set(集合)map(映射) set(集合) 集合底层实现是否有序数值是否可以重复能否更改数值查询效率增删效率std::set红黑树有序否否O(log n)O(log n)std::multiset红黑树有序是否O(log n)O(log n)std::unordere…...

linux信号理解
linux信号:用户、系统或进程发送给目标进程的信息,以通知目标进程中某个状态的改变或是异常。 信号产生原因:软中断或者硬中断。可细分为如下几种原因: ①系统终端Terminal中输入特殊的字符来产生一个信号,比如按下&am…...

HC小区管理系统window系统安装教程
实操视频 HC小区管理系统局域网window物理机部署教程_哔哩哔哩_bilibili 一、下载安装包 百度网盘: 链接:https://pan.baidu.com/s/1XAjxtpeBjHIQUZs4M7TsRg 提取码:hchc 或者 123盘 hc-window.zip官方版下载丨最新版下载丨绿色版下…...

自动化测试工具软测界的不二之选,还不快速来了解
目录 引言: 前言: 一.龙测AI-TestOps云平台使用教程 1.如何登录龙测AI-TestOps云平台 登录方法① 登录方法② 2.龙测AI-TestOps云平台界面布局 3.龙测AI-TestOps云平台菜单功能 ①创建项目 ②应用管理 ③设备管理 ④订单 二.总结 引言&#…...

centos系统/dev/mapper/centos-root目录被占满的解决方式
最近在做虚拟机部署docker微服务时,发现磁盘内存占满,无法进行操作。open /var/lib/dpkg/info/libc6:amd64.templates: no space left on device接下来就写下我在备份虚拟机上如何解决根目录被占满的问题:1、查看虚拟机磁盘使用情况df -h可以…...

【C++】STL容器、算法的简单认识
几种模板首先认识一下函数模板、类模板、栈模板。函数模板函数模板就是一个模型,而模板函数是函数模板经过类型实例化的函数。如下template<class T>是一个简单的函数模板:template<class T> T Max(T a, T b) {return a > b ? a : b; } …...

把python开发的web服务,打包成docker镜像的方法
要将Python开发的服务打成Docker镜像,可以按照以下步骤操作:1. 创建一个Dockerfile文件,该文件描述了如何构建Docker镜像。例如,以下是一个简单的Dockerfile文件,用于构建一个基于Python的Web应用程序: FRO…...

【Linux】多线程
进程和线程进程:一个正在运行的程序。状态:就绪,运行,阻塞;线程是进程中的一个执行路径,一个进程中至少有一个主线程(main函数);有多条执行路径为多线程。创建一个线程用…...

Qt 设置窗口背景图片的几种方法实例
1.在paintEvent事件中绘制图片 void Widget::paintEvent(QPaintEvent * ev) {QPainter painter(this);painter.drawPixmap(rect(),QPixmap(":/bg.jpg"),QRect()); } drawPixmap在Widget的整个矩形区域绘制背景图片,第三个参数为要绘制的图片区域&#x…...

springcloud微服务架构搭建过程
项目地址:源代码 仅作为学习用例使用,是我开发过程中的总结、实际的一部分使用方式 开发环境: jdk11 springboot2.7.6 springcloud2021.0.5 alibabacloud 2021.0.4.0 redis6.0 mysql8.0 一、项目搭建 wdz-api:存放远程服务调用相关…...

LeetCode:215. 数组中的第K个最大元素
🍎道阻且长,行则将至。🍓 🌻算法,不如说它是一种思考方式🍀算法专栏: 👉🏻123 一、🌱215. 数组中的第K个最大元素 题目描述:给定整数数组nums和整…...

vue面试题(day06)
文章目录前言请谈谈WXML与标准的html的异同?请谈谈WXSS和CSS的异同?请谈谈微信小程序主要目录和文件的作用?请谈谈小程序的双向绑定和vue的异同?简单描述下微信小程序的相关文件类型?微信小程序有哪些传值(传递数据)方…...

22 k8s常用命令
一、k8s网络 service网络 pod网络 节点网络 》 svc、pod网络都是虚拟机网络,真实网络是节点网络 二、内核升级 因为coentos系统3.10存在一些bug,docker、kubernetes不稳定,建议升级到4.4版本以上 三、集群资源分类 名称空间级别࿱…...

基于ESP32做低功耗墨水屏时钟
基于ESP32做低功耗墨水屏时钟电子墨水屏概述ESP32实验低功耗电子时钟功能描述接线开发实验结果电子墨水屏 概述 电子墨水是一种革新信息显示的新方法和技术。和传统纸差异是电子墨水在通电时改变颜色,并且可以显示变化的图象,像计算器或手机那样的显示。…...

常见路由器开源系统(固件)简介
前段时间在折腾如何通过 SD-WAN 组网方式打通办公室和家里的异地局域网。需要用到路由器的静态路由表功能,但是遍历整个家用路由器市场几乎没有支持这个功能的路由器(只有华硕 RT-AX57 有这个功能,但是成本超出了我的预算)。所有就…...

HCIE-Cloud Computing LAB备考第二步:逐题攻破--第二题:FusionAccess-搭建FA实验环境之安装基础组件和初始化ITA组件
HCIE-Cloud Computing LAB备考第二步:逐题攻破–第二题:FusionAccess-思维导图+题目=建立逻辑 专业术语 名词描述备注FusionAccess华为推出的桌面云产品,是一种虚拟桌面应用,它主要通过在硬件上部署FusionAccess配套的软件基础上,虚拟化出相互隔离的桌面,用户通过瘦客户端…...

Android APP检查设备是否为平板
正文 Android APP判断设备是否为平板的三种方法: 通过屏幕尺寸判断。一般来说,平板电脑的屏幕尺寸比手机大很多,可以根据屏幕的长宽比和尺寸等信息来区分设备类型。通过屏幕像素密度判断。一般来说,平板电脑的屏幕像素密度比手机…...

MP:使用步骤、分页、queryWrapper
Mybatis-Plus 官网: MyBatis-Plus (baomidou.com) 1. 意义 mybatis-plus是一个插件,它不能单独使用,必须配合mybatis使用,作用是简化mybatis操作,通过使用MP提供的方法,自动生成SQL语句进行CRUD 2. 使用步骤…...

C++ string类
C string类讲解 1、为什么学习string类? C语言中的字符串 在C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符…...

虚拟机断电centos无法启动
虚拟机断电后centos7无法正常启动 XFS(sda3) 首先需要查找日志 在界面中查找日志是 journalctl 1.由于我的电脑死机,虚拟机没有正常关闭导致重启后 node1节点:可以登陆但是出现XFS(sda3):Corruption of in-memoru data detectednode2节点&…...

python学习之基于Python的人脸识别技术学习
摘要: 面部识别技术的应用越来越广泛,它广泛应用于安全系统、人机交互、社交媒体、医疗保健等领域。本文介绍了基于Python的人脸识别技术,包括人脸检测、人脸特征提取和人脸识别三个部分。我们使用OpenCV和Dlib库来实现这些功能,…...
[Qt][Android] Qt for Android 环境搭建
建议使用 Linux 环境开发 Qt for Android,Windows 环境不好弄,问题多。 直接按照官方文档给的流程进行一步步做就行了: Getting Started with Qt for Android | Qt 6.4https://doc.qt.io/qt-6/android-getting-started.html建议使用 ubuntu…...