现代 c++ 三:移动语义与右值引用

news/2024/7/20 18:21:07/文章来源:https://blog.csdn.net/antsmall/article/details/139249489

移动语义很简单,但它相关联的术语很复杂。本文尝试从历史的角度解释清楚这些乱七八糟的术语及其关联:

  • 表达式 (expression)、类型(type)、值类别 (value categories);

  • 左值 (lvalue)、右值 (rvalue)、广义左值 (glvalue aka “generalized” lvalue)、纯右值 (prvalue aka “pure” rvalue)、将亡值 (xvalue aka “eXpiring” value);

  • 左值引用 (lvalue reference)、右值引用 (rvalue reference)、const 引用;

  • 移动构造 (move constructor)、移动赋值 (move assignment);


实际上,如果读过 Bjarne Stroustrup 的这篇文章 《“New” Value Terminology》[1],基本就知道 c++ 委员会为什么在 c++11 引入这么复杂的值类别。


1. 移动语义

c++11 为了提高效率,引入了移动语义。移动语义很简单,它是相对于 “复制” 而言的,把一个对象里面的资源 “移动” 到另一个对象中,就是移动语义了。

比如下面这样,用临时变量构造变量 a,在 c++11 之前,会触发拷贝构造函数 S(S& other),进行数据的拷贝 (memcpy) 。

struct S {int* p_array;int len;// 构造函数S(int _len) {len = _len; p_array = new int[len];}// 拷贝构造函数S(S& other) {len = other.len; p_array = new int[len]; memcpy(p_array, other.p_array, len);}
};S a(S());

但在 c++11 后,不用拷贝数据了,可以增加一个移动拷贝构造函数 S(S&& other),直接拿对方的指针来用,不用拷贝 (memcpy) 数据。

struct S {// 定义同上,此处省略// 移动构造函数S(S&& other) {len = other.len; p_array = other.p_array; other.p_array = nullptr; }
};

S&& 表示 S 的右值引用,下面具体讲讲右值引用,以及 c++11 对于值类别的扩充。


2. 右值引用

右值引用是 c++11 引用的新概念,除了引入右值引用,还扩充了值类别。要理解这些概念并不容易,需要从历史发展的角度来看。

在展开之前,需要先记住,c++ 表达式有两个独立的属性:类型 (type)、值类别 (value categories):

  • 类型 (type),包括基本类型 (int, float,void, null 等),复合类型(class,union,引用 等),具体参见 cppreference 的 specification[2]。

  • 值类别 (value categories),包括广义左值、右值、左值、将亡值、纯右值,具体参见 cppreference 的 specification[3]。


变量 (variable) 和字面量 (literal) 是最简单的表达式。另外,对于 c++ 程序员来说,变量 (variable) 和对象 (object) 这两个术语基本可以互换使用。

可能大部分人对于类型 (type) 有概念,但对于值类别 (value categories) 没啥概念,这并不是一个新术语,而是从 c 语言时代就已经存在了的。


2.1 c 语言的左值 (lvalue)

c 语言的表达式按值类别划分为左值 (lvalue) 和非左值 (non-lvalue)。“lvalue” means an expression that identifies an object, a “locator value”[4]。从方便记忆的角度上讲,左值就是可以出现在赋值语句左边的表达式。

更精确的定义可以参考 cppreference 上面关于 c value categories 的描述[4]: https://en.cppreference.com/w/c/language/value_category 。


2.2 c++11 以前的左值和右值

c++11 之前的 c++ 继承了 c 的值类别定义,只做了一些小调整[3]:

  • 用右值 (rvalue) 指代 non-lvalue

  • 函数归为左值 (lvalue)

  • 引用只能绑定到左值 (lvalue)

  • const 引用可以绑定到右值 (rvalue)

  • 其他几个在 c 中是 non-lvalue 的在 c++ 中划分到 lvalue


2.3 c++11 的左值和右值

c++11 引入了移动语义和完美转发,而这两个机制与左值右值这两个术语强相关。CWG (c++ 标准委员会下的 core working group) 的大部分人认为需要修正这两个术语的定义,才能使 c++ 规范保持一致[1]:

However, the majority of the CWG disagreed and insisted that some changed and/or novel terminology
was necessary to address known problems and to get the specification consistent.


Bjarne Stroustrup 的这篇文章 《“New” Value Terminology》[1] 详细的记录了 CWG 开会讨论的过程。

图1:CWG meeting 讨论过程的一个混乱的分类[1]

Bjarne Stroustrup 觉得上面的分类很混乱,自己尝试对表达式的属性进行分类:

Clearly we were headed for an impasse or a mess or both. I spent the lunchtime doing an analysis to see which of the properties (of values) were independent. There were only two independent properties:
• “has identity” – i.e. and address, a pointer, the user can determine whether two copies are identical, etc.
• “can be moved from” – i.e. we are allowed to leave to source of a “copy” in some indeterminate, but valid state

他 (Bjarne Stroustrup) 分析出表达式实际上有两个独立的属性:有身份的 (has identity) 、可以被移动的 (can be moved from)。基于这两个属性,他组合出三种表达式分类:

This led me to the conclusion that there are exactly three kinds of values (using the regex notational trick of using a capital letter to indicate a negative – I was in a hurry):
• iM: has identity and cannot be moved from
• im: has identity and can be moved from (e.g. the result of casting an lvalue to a rvalue reference)
• Im: does not have identity and can be moved from

The fourth possibility (“IM”: doesn’t have identity and cannot be moved) is not useful in C++ (or, I think) in any other language.

In addition to these three fundamental classifications of values, we have two obvious generalizations that correspond to the two independent properties:
• i: has identity
• m: can be moved from

他用 i 表示有身份的 (has identity),I 表示无身份的;m 表示可以被移动的 (can be moved from),M 表示不可移动的,组合起来就是:

  • iM: 有身份并且不可被移动的
  • im: 有身份但可以被移动的 (比如强制把一个左值转换成右值引用)
  • Im: 无身份且可以被移动的

而第四种 IM(无身份且不可被移动的)在 C++ 中是不存在的。

据此,他画出了分类的草图:

图2:CWG meeting Bjarne Stroustrup 的值类别初步草图[1]

经过更细致的讨论,最终确定的分类图是这样的:

图3:CWG meeting 最终确定的值类别草图[2]

上图倒过来看就是 c++11 的最终规范了:


图4:c++11 value categories[5]


了解完历史,总结一下,就是对表达式做了精确的分类,有了精确分类之后,编译器就可以确定哪些表达式可以用于移动语义。

下面具体讲一下各个分类的精确描述。


2.4 c++11 值类别详解

     expression/    \glvalue  rvalue/   \    /  \lvalue xvalue  prvalue

概括一下:

  • 主要的值类别就三种:lvalue,xvalue,prvalue,每个表达式只属于其中之一。

  • glvalue,即 “generalized” lvalue,译做 “广义左值”,是标识了一个对象或函数的表达式,差不多就是原始的左值的定义。

  • xvalue ,即 “eXpiring” value,译做 “将亡值”,是一种 glvalue,只不过它要 “消亡了”,资源将可以被复用(即被移动)。

  • lvalue,译做 “左值”,是一种 glvalue,但不是 xvalue。

  • prvalue,即 “pure” rvalue,译做 “纯右值”,这其实就是 c++98 中的右值,包括临时变量或者一些不跟对象关联的量,比如返回值是非引用的函数的返回值,比如字面量:30,100,‘c’。

  • rvalue,包含 xvalue 和 prvalue,资源可以被移动的表达式。

关于值类别 (value categories) 的精确分类可以在这个 specification 找到:cppreference value_category [3],非常琐碎。

但是作为一个语言学习者,就应该直接看 specification,去看吧。


2.5 右值引用的例子

int r = 100;
int&& r1 = r;            // 不合法,r 是一个左值
int&& r2 = 100;          // 合法,100 是一个右值
string&& s1 {"hello"};   // 合法,"hello" 是一个右值
string&& s2 {s1};        // 不合法,s1 是一个左值,"string 右值引用" 只是它的类型,它本质是上一个左值,它是有地址(内存位置)的,这点很容易犯错int x = 100;
int&& x1 = ++x;          // 不合法,++x 返回的是左值
int&& x2 = x++;          // 合法,x++ 返回的是右值,虽然可以,但项目中不要这么写

上面例子中,要特别注意的情况是:string&& s1 {"hello"};,在这里,s1 是一个类型为 “string 右值引用” 的左值,当把 右值引用 当成一种类型之后,就比较好理解 s1 是一个左值的事实了。再举一个例子:void f(int&& p1);,在这个函数声明中,p1 是一个类型为 int 右值引用 的左值。


3. 左值引用

左值引用就是绑定到左值上的引用,用 & 表示。c++11 之前,引用都是左值引用。左值引用就相当于给一个左值(对象)取一个别名。

它与指针是有显著区别的,指针可以指向 NULL 对象,指针可以只声明不初始化,但左值引用都不行。左值引用必须引用一个已经存在的对象,必须定义时初始化,像这样:

int i = 100;
int& refi = i;  // 合法
int& refi2;     // 不合法

另外,左值引用不能绑定到临时对象上:

int& i = 100;           // 不合法
string& s {"hello"};    // 不合法

4. const 引用

const 引用是一种特殊的左值引用,与常规左值引用的区别在于,它可以绑定到临时对象:

const int& i1 = 100;        // 合法,相当于:int temp = 100; const int& i1 = temp;
const string& s1 {"hello"}; // 合法,相当于:string temp {"hello"}; const string& s1 {temp};

c++ 只会为 const 引用产生临时对象,不会对非 const 引用产生临时对象,这一特性导致了一些容易让人困惑的现象:

void f1(const string& s) {cout << s << endl;
}void f2(string& s) {cout << s << endl;
}f1("hello");   // 正常,"hello" 转换成 string 类型的临时对象,临时对象可以被 const 引用 引用
f2("hello");   // 编译报错,"hello" 转换成 string 类型的临时对象,临时对象不可以被 左值引用 引用// 会报类似这样的编译错误:no known conversion from 'const char[2]' to 'string &'string s = "hello";
f1(s);         // 正常,一个左值可以被 左值引用 所引用
f2(s);         // 正常,一个左值可以被 const引用 所引用

5. 移动构造函数和移动赋值运算符函数

对象的移动是如何发生的?在 c++11 中,是通过移动构造函数和移动赋值运算符来实现的,这两个函数与拷贝构造函数和拷贝赋值运算符是相对的。前者的参数是右值引用,而后者的参数是左值引用。

如果没有定义移动函数或者源对象不是右值,用一个对象给另一个对象初始化或赋值,调用的都是拷贝函数。
如果定义了移动函数并且源对象是右值,用一个对象给另一个对象初始化或赋值,调用的都是移动函数。

复制对象的基本模式是,目标对象往往需要 new 一块内存出来,然后从源对象那里复制内存数据。
移动对象的基本模式是,直接挪用内存,不 new 内存也不拷贝数据,直接把源对象的内存数据拿来用,其代价往往只是一些指针变量的赋值。

显而易见,如果有比较多的内存需要拷贝,移动对象的效率是更高很多的。

下面举个例子证明以上的说法:
源码可在此找到:https://github.com/antsmallant/antsmallant_blog_demo/tree/main/blog_demo/modern-cpp 。

// move_constructor_demo.cpp
// 编译&执行:g++ -std=c++14 move_constructor_demo.cpp && ./a.out
// 屏蔽rvo的编译&执行:g++ -std=c++14 -fno-elide-constructors move_constructor_demo.cpp && ./a.out#include <iostream>
#include <vector>
using namespace std;class A {
private:vector<int>* p;
public:A() {cout << "A 构造函数,无参数" << endl;p = new vector<int>();}// 构造函数A(int cnt, int val) {cout << "A 构造函数,带参数" << endl;p = new vector<int>(cnt, val);}// 析构函数~A() {if (p != nullptr) {delete p;p = nullptr;cout << "A 析构函数,释放 p" << endl;} else {cout << "A 析构函数,不需要释放 p" << endl;}}// 拷贝构造函数A(const A& other) {cout << "A 拷贝构造函数" << endl;p = new vector<int>(other.p->begin(), other.p->end());}// 拷贝赋值运算符A& operator = (const A& other) {cout << "A 拷贝赋值运算符" << endl;if (p != nullptr) {cout << "A 拷贝赋值前释放旧内存" << endl;delete p;p = nullptr;}p = new vector<int>(other.p->begin(), other.p->end());       return *this;}// 移动构造函数A(A&& other) noexcept {cout << "A 移动构造函数" << endl;this->p = other.p;  // 挪用别人的other.p = nullptr;  // 置空别人的}// 移动赋值运算符A& operator = (A&& other) noexcept {cout << "A 移动赋值运算符" << endl;this->p = other.p; other.p = nullptr;  return *this;}
};void test_copy_constructor() {A a(10, 100);A b(a);
}void test_copy_assign_operator() {A a(10, 100);A b;b = a;
}A getA(int cnt, int val) {return A(cnt, val);
}void test_move_constructor() {A a(getA(10, 200));
}void test_move_assign_operator() {A a;a = getA(10, 200);
}int main() {cout << "测试拷贝构造: " << endl;test_copy_constructor();cout << endl << "测试拷贝赋值运算符: " << endl;test_copy_assign_operator();cout << endl << "测试移动构造: " << endl;test_move_constructor();cout << endl << "测试移动赋值运算符: " << endl;test_move_assign_operator();return 0;
}

输出是:

测试拷贝构造: 
A 构造函数,带参数
A 拷贝构造函数
A 析构函数,释放 p
A 析构函数,释放 p测试拷贝赋值运算符: 
A 构造函数,带参数
A 构造函数,无参数
A 拷贝赋值运算符
A 拷贝赋值前释放旧内存
A 析构函数,释放 p
A 析构函数,释放 p测试移动构造: 
A 构造函数,带参数
A 析构函数,释放 p测试移动赋值运算符: 
A 构造函数,无参数
A 构造函数,带参数
A 移动赋值运算符
A 析构函数,不需要释放 p
A 析构函数,释放 p

上面的测试可以说有 75% 成功了,关于移动构造的测试失败了,它没调用移动构造函数。怎么回事?

这实际上是一种编译器优化,叫 RVO(Return Value Optimization),返回值优化,这个暂且不展开讲。为了避免这种优化对于测试的影响,可以给编译器传递一个选项,暂时禁用这种优化,修改一下编译命令: g++ -std=c++14 -fno-elide-constructors move_constructor_demo.cpp && ./a.out,重新编译运行,移动构造的测试输出变成:

测试移动构造: 
A 构造函数,带参数
A 移动构造函数
A 析构函数,不需要释放 p
A 移动构造函数
A 析构函数,不需要释放 p
A 析构函数,释放 p

虽然如愿输出了 “A 移动构造函数”,但输出有点多。把代码列出来,简单分析一下:

A getA(int cnt, int val) {// 1、用带参数的构造函数 A(10, 200) 生成一个局部对象 x// 2、return 的时候,用移动构造函数 A(x) 生成一个临时对象 treturn A(cnt, val);
}void test_move_constructor() {// 3、用移动构造函数 A(t) 生成局部对象 aA a(getA(10, 200));
}

6. 编译器默认生成的移动(构造/赋值运算符)函数

如果没有自己写拷贝构造函数或拷贝赋值运算符,那么编译器会帮生成默认的。

编译器在特定条件下,也会帮生成默认的移动函数[6]:

  1. 一个类没有定义任何版本的拷贝构造函数、拷贝赋值运算符、析构函数;
  2. 类的每个非静态成员都可以移动
    • 内置类型(如整型、浮点型)
    • 定义了移动操作的类类型

第1点,应该是确保系统可以生成符合程序员需要的移动函数,如果代码中定义了那三种函数,说明程序员有自己控制复制或释放的倾向,这时候编译器就不默认生成了。
第2点,只有确保成员都可移动,才能生成正确的移动函数。


7. std::move

上面讲移动构造和移动赋值运算符的时候,发现由于编译器的 RVO 优化,导致即使构造了合适的场景,也没能验证移动构造的使用。

接下来介绍的 std::move,能够做到即使不屏蔽 RVO,也可以验证移动构造的使用,只需要这样修改:

// g++ -std=c++14 move_constructor_demo.cpp && ./a.outvoid test_move_constructor_use_stdmove() {A x(10, 200);A a(std::move(x));
}int main() {cout << endl << "使用 std::move 测试移动构造:" << endl;test_move_constructor_use_stdmove();
}

输出:

A 构造函数,带参数
A 移动构造函数
A 析构函数,释放 p
A 析构函数,不需要释放 p

std::move 能够强制产生一个右值引用,当编译器匹配到右值引用,就会调用移动构造函数。

特别注意,std::move 并不完成对象的移动,它的作用只是强制产生一个右值引用,真正起移动作用的是移动构造函数或移动赋值运算符函数,要在这两个函数中写移动逻辑。

std::move 的一种可能实现如下[7]:

template<typename T>
typename remove_reference<T>::type&&
move(T&& param)
{using ReturnType = typename remove_reference<T>::type&&;return static_cast<ReturnType>(param);
}

实参可以是左值,也可以是右值:

int a = 100;
int&& r1 = std::move(a);    // 合法
int&& r2 = std::move(200);  // 合法

综上,std::move 是一种危险操作,调用时必须确认源对象没有其他用户了,否则容易发生一些意外的难以理解的状况。


8. 运算符的运算对象和运算结果

  • 赋值运算符:运算对象是左值,运算结果也是左值。

  • 取地址符:运算对象是左值,运算结果是右值。

  • 内置解引用运算符、下标运算符、迭代器解引用运算符、string&vector的下标运算符:运算对象是左值,运算结果也是左值。

  • 内置类型和迭代器的递增递减运算符:运算对象是左值,运算结果,前置版本是左值,后置版本是右值。

解引用运算符就是 * 操作符,用于获得指针所指的对象,比如:

int v = 100;
int* p = &v;
*p = 200;

p 是一个指向了对象的指针,则 *p 就是获得指针 p 所指的对象,比如 *p = 100;

递增递减的前置和后置版本的具体区别:

  • 前置版本,比如: ++i 返回的是左值,过程是直接把 i 加 1,然后返回 i。

  • 后置版本,比如: i++ 返回的是右值,过程是先用一个临时变量保存 i 的值,然后把 i 值加1,然后返回临时变量。
    所以,建议是:除非必须,不要使用递增递减的后置版本,它们生成了临时变量,是一种浪费。


9. 拓展阅读

  • C++的复杂,C是原罪:从值类别说开去 :这篇文章从 C 语言、汇编和 C++ 设计发展的角度,分析了为什么 c++ 搞了这么复杂的值类别:左值、右值、纯右值、广义左值、将亡值。

10. 参考

[1] Bjarne Stroustrup. “New” Value Terminology. Available at https://www.stroustrup.com/terminology.pdf.

[2] cppreference. Type. Available at https://en.cppreference.com/w/cpp/language/type.

[3] cppreference. cpp Value categories. Available at https://en.cppreference.com/w/cpp/language/value_category.

[4] cppreference. c value categories. Available at https://en.cppreference.com/w/c/language/value_category.

[5] wg21. Working Draft, Standard for Programming Language C++ (N4878). Available at https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/n4878.pdf, 2020-12-15: 91.

[6] 王健伟. C++新经典. 北京: 清华大学出版社, 2020-08-01.

[7] [美]Scott Meyers. Effective Modern C++(中文版). 高博. 北京: 中国电力出版社, 2018-4: 149, 151.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_1052589.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

UI问题 --- CardView和其它的控件在同一布局中时,始终覆盖其它控件

原本代码&#xff1a; <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width"40dp"android:layout_height"wrap_content"andr…

代码随想录——最大二叉树(Leetcode654)

题目链接 递归 二叉树 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val val; }* TreeNode(int val, TreeNode left, TreeNode rig…

ssh远程连接的相关配置

连接同一个局域网下&#xff1a; 正好这里来理解一下计算机网络配置中的ip地址配置细节&#xff0c; inet 172.20.10.13: 这是主机的IP地址&#xff0c;用于在网络中唯一标识一台设备。在这个例子中&#xff0c;IP地址是172.20.10.13。 netmask 255.255.255.240: 这是子网掩码…

Laravel和ThinkPHP框架比较

一、开发体验与易用性比较 1. 代码可读性&#xff1a; - Laravel以其优雅的语法和良好的代码结构著称&#xff0c;使得代码更加易读易懂。 - 相比之下&#xff0c;ThinkPHP的代码可读性较为一般&#xff0c;在一些复杂业务场景下&#xff0c;可能会稍显混乱。 让您能够一站式…

刷题记录5.22-5.27

文章目录 刷题记录5.22-5.2717.电话号码的字母组合78.子集131.分割回文串77.组合22.括号生成198.打家劫舍---从递归到记忆化搜索再到递推动态规划背包搜索模板494.目标和322.零钱兑换牛客小白月赛---数字合并线性DP1143.最长公共子序列72.编辑距离300.最长递增子序列状态机DP12…

自动化您的任务——crewAI 初学者教程

今天&#xff0c;我写这篇文章是为了分享您开始使用一个非常流行的多智能体框架所需了解的所有信息&#xff1a;crewAI。 我将在这里或那里跳过一些内容&#xff0c;使本教程成为一个精炼的教程&#xff0c;概述帮助您入门的关键概念和要点 今天&#xff0c;我写这篇文章是为了…

机器学习实验----逻辑回归实现二分类

目录 一、介绍 二、sigmoid函数 &#xff08;1&#xff09;公式&#xff1a; &#xff08;2&#xff09;sigmoid函数的输入 预测函数&#xff1a; 以下是sigmoid函数代码&#xff1a; 三、梯度上升 &#xff08;1&#xff09;似然函数 公式&#xff1a; 概念&#xff…

fpga系列 HDL 00 : 可编程逻辑器件原理

一次性可编程器件&#xff08;融保险丝实现&#xff09; 一次性可编程器件&#xff08;One-Time Programmable Device&#xff0c;简称 OTP&#xff09;是一种在制造后仅能编程一次的存储设备。OTP器件在编程后数据不可更改。这些器件在很多应用场景中具有独特的优势和用途。 …

python中import的搜索路径

文章目录 前言 一 python中import的搜索路径1. python中import的搜索路径先判断是否内置模块根据sys.path查找1.1 脚本当前目录和所属项目目录1.2 环境变量1.3 标准库1.4 .pth 文件1.5 第三方库 2. 解决ModuleNotFoundError 前言 码python时经常会遇到找不到包或者找不到模块的…

【考研数学】线代除了「李永乐」,还能跟谁?

考研线代&#xff0c;除了利用了老师&#xff0c;我觉得还有一个宝藏老师的课程值得听&#xff01; 那就是喻老&#xff0c;这个是我在b站上面新发现的老师&#xff0c;听完他的课程发现真的喜欢 他不仅在B站上开设了课程&#xff0c;还编写了配套的线性代数辅导讲义&#xff…

Chrome谷歌浏览器如何打开不安全页面的禁止权限?

目录 一、背景二、如何打开不安全页面被禁止的权限&#xff1f;2.1 第一步&#xff0c;添加信任站点2.2 第二步&#xff0c;打开不安全页面的权限2.3 结果展示 一、背景 在开发过程中&#xff0c;由于测试环境没有配置 HTTPS 请求&#xff0c;所以谷歌浏览器的地址栏会有这样一…

我用 Midjourney 的这种风格治愈了强迫症

在 Midjourney 能够实现的各种布局之中&#xff0c;有两种风格因其简洁、有序而独居魅力&#xff0c;它们就是平铺 (Flat Lay) 和 Knolling (Knolling 就是 Knolling, 无法翻译&#x1f923;)。要在现实生活中实现这样的美学效果并不容易&#xff0c;你需要精心挑选各种小物件&…

【UE Slate】 虚幻引擎Slate开发快速入门

目录 0 引言1 Slate框架1.0 控件布局1.1 SWidget1.1.1 SWidget的主要作用1.1.2 SWidget的关键方法1.1.3 使用SWidget创建自定义控件1.1.4 结论 1.2 SCompoundWidget1.2.1 SCompoundWidget的主要作用1.2.2 SCompoundWidget的使用示例1.2.3 SCompoundWidget的关系1.2.4 总结 1.3 …

python数据分析:爬取某东商城商品评论数据并做词云展示(含完整源码及详细注解)

python数据分析,爬取某东商城商品评论数据并做词云展示。 一、明确爬取的网页及结构 找到要爬取的网页地址,发现有一个获取json格式评论数据的接口: url = "https://club.jd.com/comment/productPageComments.action?callback=fetchJSON_comment98&productId=217…

数据结构——不相交集(并查集)

一、基本概念 关系&#xff1a;定义在集合S上的关系指对于a&#xff0c;b∈S&#xff0c;若aRb为真&#xff0c;则a与b相关 等价关系&#xff1a;满足以下三个特性的关系R称为等价关系 (1)对称性&#xff0c;aRb为真则bRa为真&#xff1b; (2)反身性,aRa为真; (3)传递性,aRb为真…

【Qt Creator】跨平台的C++图形用户界面应用程序开发框架---QT

&#x1f341;你好&#xff0c;我是 RO-BERRY &#x1f4d7; 致力于C、C、数据结构、TCP/IP、数据库等等一系列知识 &#x1f384;感谢你的陪伴与支持 &#xff0c;故事既有了开头&#xff0c;就要画上一个完美的句号&#xff0c;让我们一起加油 目录 1.互联网的核心岗位以及职…

3D工业视觉

前言 本文主要介绍3D视觉技术、工业领域的应用、市场格局等&#xff0c;主要技术包括激光三角测量、结构光、ToF、立体视觉。 一、核心内容 3D视觉技术满足工业领域更高精度、更高速度、更柔性化的需求&#xff0c;扩大工业自动化的场景。 2D视觉技术基于物体平面轮廓&#…

JavaScript面试 题

1.延时加载JS有哪些方式 延时加载 :async defer 例如:<script defer type"type/javascript" srcscript.js></ script> defer:等html全部解析完成,才会执行js代码,顺次执行的 async: js和html解析是同步的,不是顺次执行js脚本(谁先加载完先执行谁)2.JS数…

C语言实现Hash Map(3):Map代码优化

在上一节中&#xff0c;我们学习了C语言实现Hash Map(2)&#xff1a;Map代码实现详解&#xff0c;通过代码&#xff0c;我们更深入地了解了Map实现的原理&#xff0c;学习了如何通过key找到对应的桶并加入节点。也正如上一节提到的&#xff0c;虽然这是github中star比较多的代码…

其二:使用递归法实现二分搜索

开篇 本文主要是利用递归法来实现一个简单的二分搜索程序。题目来源是《编程珠玑》第4章课后习题3。 问题概要 编写并验证一个递归的二分搜索程序, 并返回t在数组x[0…n-1]中第一次出现的位置。 思路分析 本题的思路与第一版相似&#xff0c;不过不同的是&#xff0c;为确保返回…