【C++】C++11新特性之右值引用与移动语义

文章目录

  • 一、左值与左值引用
  • 二、右值与右值引用
  • 三、 左值引用与右值引用比较
  • 四、右值引用使用场景和意义
    • 1.左值引用的短板
    • 2.移动构造和移动赋值
    • 3.STL中右值引用的使用
  • 五、万能引用与完美转发
    • 1.万能引用
    • 2.完美转发

一、左值与左值引用

在C++11之前,我们把数据分为常量和变量,在C++11之后,我们将数据分为左值和右值

此外,传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址,还可以对它赋值,左值可以出现赋值符号的左边,也可以出现在赋值符号的右边,定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址,如下:

// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;

左值引用就是给左值的引用,给左值取别名

// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;

二、右值与右值引用

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址,如下:

double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;

右值引用就是对右值的引用,给右值取别名

// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);

注意事项:

1.为什么函数返回值是右值:当函数返回的是一个局部变量的时候,因为局部变量出了函数作用域生命周期就会结束,所以返回时会将该变量拷贝到寄存器中,然后返回这个寄存器中的内容,而寄存器中的变量是临时变量,临时变量具有常量,属于右值。其实在函数建立栈帧的时候,不仅会有参数的压栈,还会有返回值的压栈,即在两个函数的栈帧之后的一个空间在存贮函数的返回值。通过这个中间值将函数的返回值进行返回。

2.为什么右值不能取地址:在C++中,右值则是一个临时使用的,不可寻址的内存空间,右值没有独立的内存空间,它只是存储在寄存器或者其他临时内存中的一个值,我们也不能将右值放入内存,因为右值没有确定的内存位置,所以右值不能取地址

需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇,这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。

int main()
{int x = 1, y = 2;int&& rr1 = 10;const int&& rr2 = x + y;rr1++;cout << &rr1 << endl;cout << rr1 << endl;cout << &rr2 << endl;return 0;
}

在这里插入图片描述

rr2 = 5;  // 报错

所以如果我们不希望改变右值引用,我们就需要将右值引用定义为const右值引用

三、 左值引用与右值引用比较

左值引用只能引用左值,不能引用右值。但是const左值引用既可引用左值,也可引用右值,因为 const左值引用也是只读的,而权限可以平移

int main()
{// 左值引用只能引用左值,不能引用右值。int a = 10;int& ra1 = a;    // ra为a的别名//int& ra2 = 10;   // 编译失败,因为10是右值// const左值引用既可引用左值,也可引用右值。const int& ra3 = 10;const int& ra4 = a;return 0;
}

右值引用只能右值,不能引用左值,但是右值引用可以move以后的左值。

int main()
{// 右值引用只能右值,不能引用左值。int&& r1 = 10;// error C2440: “初始化”: 无法从“int”转换为“int &&”// message : 无法将左值绑定到右值引用//int a = 10;//int&& r2 = a;// 右值引用可以引用move以后的左值int&& r3 = std::move(a);return 0;
}

四、右值引用使用场景和意义

1.左值引用的短板

前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引用呢?

我们先看看左值引用的两个作用:

1.修改实参的值

2.引用做参数/函数返回值可以减少拷贝

我们可以把函数形参定义为实参的引用,这样函数在传参时就不用拷贝构造形参了,从而提高程序的效率,特别是对于需要深拷贝的自定义类型来说。左值引用作为返回值的效果也一样,当返回的对象出了作用域还存在时,直接使用引用返回可以减少一次拷贝构造:

void func1(string s)
{}
void func2(const string& s)
{}
int main()
{string s1("hello world");// func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值func1(s1);func2(s1);// string operator+=(char ch) 传值返回存在深拷贝// string& operator+=(char ch) 传左值引用没有拷贝提高了效率s1 += '!';return 0;
}

但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。因为局部变量出了函数作用域就不存在了,此时引用就是一个野指针

template <class T>
T func(const T& x)
{T tmp;//...return tmp;
}

这种情况下编译器会使用这个临时对象拷贝构造一个临时对象,然后再返回这个临时对象,也就是说,这样会比引用返回多一次拷贝构造,当局部对象是一个需要进行深拷贝的自定义类型的时候,比如vector<vector>,拷贝构造的代价就会很大,所以右值引用的提出就是为了补足左值引用存在的这些短板

2.移动构造和移动赋值

为了更好的演示左值引用和右值引用对拷贝构造的优化,我们自己实现一个string类,在拷贝构造/赋值重载函数中进行打印相关的信息便于观察:

namespace hdp
{class string{public:typedef char* iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}string(const char* str = ""):_size(strlen(str)), _capacity(_size){cout << "string(char* str) -- 构造" << endl;_str = new char[_capacity + 1];strcpy(_str, str);}// s1.swap(s2)void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}// 拷贝构造string(const string& s):_str(nullptr){cout << "string(const string& s) -- 深拷贝" << endl;string tmp(s._str);swap(tmp);}// 赋值重载string& operator=(const string& s){cout << "string& operator=(string s) -- 深拷贝" << endl;string tmp(s);swap(tmp);return *this;} 移动构造//string(string&& s)//	:_str(nullptr)//	, _size(0)//	, _capacity(0)//{//	cout << "string(string&& s) -- 移动语义" << endl;//	swap(s);//} 移动赋值//string& operator=(string&& s)//{//	cout << "string& operator=(string&& s) -- 移动语义" << endl;//	swap(s);//	return *this;//}~string(){delete[] _str;_str = nullptr;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}void push_back(char ch){if (_size >= _capacity){size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}_str[_size] = ch;++_size;_str[_size] = '\0';}//string operator+=(char ch)string& operator+=(char ch){push_back(ch);return *this;}const char* c_str() const{return _str;}private:char* _str;size_t _size;size_t _capacity; // 不包含最后做标识的\0};
}

现在假设我们要实现一个to_string函数,代码如下:

namespace hdp
{hdp::string to_string(int value){bool flag = true;if (value < 0){flag = false;value = 0 - value;}hdp::string str;while (value > 0){int x = value % 10;value /= 10;str += ('0' + x);}if (flag == false){str += '-';}std::reverse(str.begin(), str.end());return str;}
}

我们可以看到,由于to_string函数的返回值是一个局部的对象,所以我们这里只能使用传值返回,而传值返回对于string来说需要进行深拷贝

在这里插入图片描述

其实这里程序的执行结果和我们预想的并不一样,正常的情况应该是str先拷贝构造一个临时对象,然后再由这个临时对象来拷贝构造ret,所以应该是两个拷贝构造(),上面的结果第一次构造是to_string函数内部构造str,第二次是函数返回时调用拷贝构造,而我们实现的拷贝构造函数中又调用了一次构造函数,所以打印了两个构造。但是编译器的优化只能适用于部分场景,对于很多场景还是会拷贝构造产生临时对象

在这里插入图片描述

尽管编译器进行了优化,这里还是会有一次拷贝构造,那么我们能不能想办法将str的资源直接赋值给s,中间不产生拷贝构造呢,此时我们就需要用到右值引用 了

C++11中的右值可以分为两种:

1.纯右值:内置类型表达式的值

2.将亡值:自定义类型表达式的值:所谓的将亡值就是指声明周期马上就要结束 的值,一般来说匿名对象,临时对象,move后的自定义类型都可以看做将亡值

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义

我们需要注意的是,上面我们说右值不能取地址其实是右值的严格定义,但其实将亡值也是可以被当做右值看待的,而将亡值有独立的内存空间,可以取地址。既然将亡值的声明周期马上就要结束了,那么在拷贝构造中我么可以直接将将亡值的资源拿过来给我们自己使用,这样就需要需要进行深拷贝了。

void swap(string& s)
{std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);
}
// 移动构造
string(string&& s):_str(nullptr), _size(0), _capacity(0)
{cout << "string(string&& s) -- 移动语义" << endl;swap(s);
}

这样我们就重载了一个右值引用版本的构造函数–移动构造,这样当参数为右值的对象需要进行拷贝构造的时候就会调用此函数,在函数中,我们直接交换两个对象的资源,从而使得深拷贝变成了浅拷贝,提供了程序的效率

移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己

我们使用移动构造之后,我们之前的程序就会减少深拷贝的次数

在这里插入图片描述

由此,我们通过移动构造将深拷贝变成了浅拷贝

但是我们需要注意的是,只有当参数为右值时才会调用移动构造,当实参为左值的时候还是会调用拷贝构造函数,因为编译器不知道我们是否还会对左值进行操作,所以它不敢拿走左值的资源来构造新的对象

在这里插入图片描述

移动赋值和移动构造同理,只是移动赋值中将亡值还需要释放之前的资源,不过这个过程是自动的

// 移动赋值
string& operator=(string&& s)
{cout << "string& operator=(string&& s) -- 移动语义" << endl;swap(s);return *this;
}

在这里插入图片描述

有人提出这样的一个观点:右值引用延长了变量的声明周期,这种说法是不准确的,因为右值引用只是将变量的资源转移给了另一个变量,让它的资源能够不随着变量的销毁而释放,而该变量本身的生命周期是没有变化的

【总结】

1.左值引用让形参称为实参的别名,直接减少拷贝

2.右值引用是通过实现移动构造和移动赋值,将将亡值的资源进行转移,间接的减少拷贝(浅拷贝的类不需要进行资源的转移,所以也没有移动赋值和移动拷贝)

3.STL中右值引用的使用

C++11在设计出右值引用之后,为STL所有容器都提供了移动构造和移动赋值,包括容器适配器

在这里插入图片描述

在这里插入图片描述

此外,还提供了右值版本的插入接口:

在这里插入图片描述

所以,以后如果我们要向容器中插入需要深拷贝的自定义类型的数据时,我们尽量使用匿名构造对象进行插入,这样调用的就是右值插入接口,元素会调用移动拷贝函数完成拷贝,从而提高程序的效率

我们可以将我们自己实现的list类支持右值版本的插入接口,部分代码如下:

#pragma once
#include <assert.h>
#include <algorithm>namespace hdp
{// 定义节点结构template<class T>struct list_node{list_node<T>* _prev;list_node<T>* _next;T _data;// 构造list_node(const T& x):_prev(nullptr), _next(nullptr), _data(x){}// 移动构造list_node(T&& x):_prev(nullptr), _next(nullptr), _data(move(x)){}};// 封装迭代器// 同一个类模板实例化出的两个类型// typedef __list_iterator<T, T&, T*> iterator;// typedef __list_iterator<T, const T&, const T*> const_iterator;template<class T, class Ref, class Ptr>// 迭代器类struct __list_iterator{typedef list_node<T> node;   // 重命名为list节点typedef __list_iterator<T, Ref, Ptr> Self;// 成员变量node* _pnode;// 构造__list_iterator(node* p):_pnode(p){}// 重载箭头Ptr operator->(){return &_pnode->_data;}// 重载*Ref operator*(){return _pnode->_data;}// 重载前置++Self& operator++(){_pnode = _pnode->_next;return *this;}// 重载后置++Self operator++(int){Self tmp(*this);_pnode = _pnode->next;return tmp;}//重载前置--Self& operator--(){_pnode = _pnode->_prev;return *this;}// 重载后置--Self operator--(int){Self tmp(*this);_pnode = _pnode->_prev;return tmp;}// 重载不等于bool operator!=(const Self& it) const{return it._pnode != _pnode;}// 重载等于bool operator==(const Self& it) const{return _pnode == it._pnode;}};// 定义list类template<class T>class list{// list 节点typedef list_node<T> node;public:typedef __list_iterator<T, T&, T*> iterator;  //迭代器typedef __list_iterator<T, const T&, const T*> const_iterator;  //const迭代器// 迭代器iterator begin(){return iterator(_head->_next);}iterator end(){// iterator it(_head);// return it;// 匿名对象构造return iterator(_head);}// const 迭代器const_iterator begin() const{return const_iterator(_head->_next);}const_iterator end() const{return const_iterator(_head);}// 创建哨兵节点void empty_initialize(){_head = new node(T());_head->_next = _head;_head->_prev = _head;_size = 0;}// 构造 不是list<T>的原因,构造函数名和类名相同,而list<T>是类型list(){empty_initialize();}// 迭代器构造template<class InputIterator>list(InputIterator first, InputIterator last){empty_initialize();while (first != last){push_back(*first);++first;}}// 拷贝构造// lt2(lt1)list(const list<T>& lt){empty_initialize();for (const auto& e : lt){push_back(e);}}// 拷贝构造现代写法list(list<T>& lt){empty_initialize();list<T> tmp(lt.begin(), lt.end());swap(tmp);}// 赋值重载list<T>& operator=(const list<T>& lt){if (this != &lt){clear();for (const auto& e : lt){push_back(e);}}return *this;}// 赋值重载现代写法list<T>& operator=(list<T> lt){swap(lt);return *this;}// 交换void swap(list<T>& lt){std::swap(_head, lt._head);std::swap(_size, lt._size);}size_t size() const{return _size;}bool empty() const{// return _head->_next == _head;// return _head->_prev == _head;return _size == 0;}// 析构~list(){clear();delete _head;_head = nullptr;}// 清理void clear(){iterator it = begin();while (it != end()){it = erase(it);}_size = 0;}// 尾插void push_back(const T& x){//node* newnode = new node(x);//node* tail = _head->_prev; _head tail newnode//tail->_next = newnode;//newnode->_prev = tail;//newnode->_next = _head;//_head->_prev = newnode;insert(end(), x);}// 尾插--右值版本void push_back(T&& x){insert(end(), move(x));}// 头插void push_front(T& x){insert(begin(), move(x));}// 头插  -- 右值版本void push_front(const T& x){insert(begin(), x);}// 尾删void pop_back(){//earse(end()->prev);erase(--end());}// 头删void pop_front(){erase(begin());}// 在pos之前插入数据iterator insert(iterator pos, const T& x){node* newnode = new node(x);node* cur = pos._pnode;node* prev = cur->_prev;// prev newnode curprev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;++_size;return iterator(newnode);}// 在pos之前插入数据  -- 右值版本iterator insert(iterator pos, T&& x){node* newnode = new node(move(x));node* cur = pos._pnode;node* prev = cur->_prev;// prev newnode curprev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;++_size;return iterator(newnode);}// 删除pos位置的数据iterator erase(iterator pos){assert(pos != end());node* prev = pos._pnode->_prev;node* next = pos._pnode->_next;prev->_next = next;next->_prev = prev;delete pos._pnode;--_size;return iterator(next);}private:node* _head;size_t _size = 0; // 保存节点个数};
}

在这里插入图片描述

list的函数调用逻辑如下:

在这里插入图片描述

我们需要注意的是,右值函数的形参的类型都是T&& x,而不是const T&& x,这是因为最终在hdp::string类中我们需要将x的资源转移给别人,这就要求x必须是可以修改的,能够交换_str,_size,_capacity三个指针,此外,右值引用x之所以能够被修改是因为给右值取别名之后,右值会被存储起来,右值引用虽然引用的是右值,但是右值引用本身是左值,所以当我们继续往下一层进行传递参数的时候,我们需要将x重新move为右值,否则下一层调用时就会调用参数为左值的函数

五、万能引用与完美转发

1.万能引用

我们上面都是单独定义一个参数为右值引用的函数,然后让编译器根据实参的类型来判断调用左值引用还是右值引用的函数,我们能不能让函数能够根据实参的类型自动实例化出对应的不同函数呢,万能引用就实现了这个功能

万能引用是一个函数模板,且函数的形参类型为右值引用,对于这样的函数模板模板,编译器能够自动根据实参的类型–左值/const 左值/右值/const右值,自动推演实例化出不同参数形参分别为左值引用/const左值引用/右值引用/const右值引用的函数

template<typename T>
void PerfectForward(T&& t)
{Fun(t);
}
int main()
{PerfectForward(10);  // 右值int a;const int b = 8;PerfectForward(a);  // 左值PerfectForward(std::move(a)); // 右值PerfectForward(b); // const 左值PerfectForward(std::move(b)); // const 右值return 0;
}

在这里插入图片描述

我们可以看到,无论实参是什么类型,模板函数都能正确的接收并实例化出对应的引用类型,所以我们把形参为右值引用的函数模板你叫做万能引用,其中,当实参为左值或者const左值的时候,T&&会被实例化为T& 或者const T&,我们称其为引用折叠,即将&&折叠为&

2.完美转发

尽管完整引用能够接收任何类型的参数,但是这里还是存在一个很大的问题,万能引用实例化后函数的形参的属性全都是左值,如果实参为左值/const左值,那么实例化函数的形参是左值/const左值,如果实参是右值/const右值,虽然实例化函数的形参是右值引用/const右值引用,但是右值引用本身是左值,所以就会出现下面的情况:

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{Fun(t);
}
int main()
{int a;const int b = 8;PerfectForward(a);  // 左值PerfectForward(b); // const 左值PerfectForward(10);  // 右值PerfectForward(std::move(b)); // const 右值return 0;
}

在这里插入图片描述

此外,我们这里也不能简单的将t move后传递给Fun函数,因为这样会让t全部变为右值,又满足不了实参为左值的情况

在这里插入图片描述

为了在传参的时候能够保留对象的原始类型,C++设计了 完美转发–forward

在这里插入图片描述

在这里插入图片描述
【总结】

1.为了弥补左值引用局部对象返回会发生拷贝构造问题,C++11设计出了右值引用,右值引用可以通过移动构造和移动赋值来实现资源转移,将深拷贝转化为浅拷贝,从而提高了效率,此外,还为STL容器提供了右值版本的插入接口,由于右值引用本身是左值,所以函数参数往下一层传递时不能保证参数仍为右值,所以提供了move,可以将左值变为右值

2.为了使得函数模板能够同时接收const左值和const右值并正确实例化为对应的引用类型,C++11设计了万能引用,但是无论是左值引用还是右值引用,其本身是左值,所以往下一层传递时不能保证其类型了,此时move也不能够解决问题了,所以C++11设计了完美转发,来保证传参过程中原生类型属性能够保持不变

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

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

相关文章

[论文笔记]GPT-2

引言 今天继续GPT系列论文&#xff0c; 这次是Language Models are Unsupervised Multitask Learners&#xff0c;即GPT-2&#xff0c;中文题目的意思是 语言模型是无监督多任务学习器。 自然语言任务&#xff0c;比如问答、机器翻译、阅读理解和摘要&#xff0c;是在任务相关…

框架篇

一、Spring中的单例Bean是线程安全的吗 二、AOP相关面试题 三、Spring中的事务 四、Spring中事务失效的场景有 五、Spring bean的生命周期 六、Spring的循环依赖 七、SpringMVC的执行流程 八、自动配置原理 九、Spring框架常见的注解 十、Mybatis的执行流程 十一、MyBatis延迟加…

postgresql(openGauss)模糊匹配参数

被pg系这个show要求精准匹配参数恶心的不轻。 原理是用.psqlrc&#xff08;openGauss用.gsqlrc&#xff09;文件set一个select常量进去&#xff0c;需要用&#xff1a;调用这个常量。理论上也可以增强其他的各种功能。 我在openGauss做的一个例子 .gsqlrc&#xff08;.psqlrc…

自学SLAM(4)《第二讲:三维物体刚体运动》作业

前言 小编研究生的研究方向是视觉SLAM&#xff0c;目前在自学&#xff0c;本篇文章为初学高翔老师课的第二次作业。 文章目录 前言1.熟悉 Eigen 矩阵运算2.几何运算练习3.旋转的表达4.罗德里格斯公式的证明5.四元数运算性质的验证6.熟悉 C11 1.熟悉 Eigen 矩阵运算 设线性⽅程 …

分布式共识算法及落地

摘要 本文介绍常见的分布式共识算法&#xff0c;使用场景&#xff0c;以及相关已经落地了的程序或框架 1. 为什么要分布式共识算法 在分布式系统中&#xff0c;不同节点之间可能存在网络延迟、故障等原因导致彼此之间存在数据不一致的情况&#xff0c;为了保证分布式系统中的…

[TCP1P 2023] 部分crypto,pwn,reverse

Crypto Final Consensus 这是个AES爆破密钥的题&#xff0c;加密方法是先后用两个密钥加密。远程先给出加密后的flag&#xff0c;然后允许输入值并进行加密。 from Crypto.Cipher import AES import random from Crypto.Util.Padding import pada b"" b b"&…

系统架构师考试科目一:综合知识

某软件公司欲开发一个 Windows 平台上的公告板系统。在明确用户需求后&#xff0c;该公司的 架构师决定采用 Command 模式实现该系统的界面显示部分&#xff0c;并设计 UML 类图如下 图所示。图中与 Command 模式中的 Invoker 角色相对应的类是( ) &#xff0c;与 ConcreteComm…

一篇文章讲懂mysql中的锁

事务的隔离性是由锁来实现的。 为什么需要锁 锁是计算机协调多个进程或线程并发访问某一资源的机制。在程序开发中会存在多线程同步的问题&#xff0c;当多个线程并发访问某个数据的时候&#xff0c;尤其是针对一些敏感的数据&#xff08;比如订单、金额等&#xff09;&#x…

C语言 ——宽字符

前言&#xff1a; 过去C语⾔并不适合⾮英语国家&#xff08;地区&#xff09;使⽤。 C语⾔最初假定字符都是单字节的。但是这些假定并不是在世界的任何地⽅都适⽤。 C语⾔字符默认是采⽤ASCII编码的&#xff0c;ASCII字符集采⽤的是单字节编码&#xff0c;且只使⽤了单字节中…

Opensuse Tumbleweed快速部署K8S-1.28.2

** #查看你的网卡信息 nmcli device show#设置你的静态IP nmcli connection modify ens32 ipv4.method manual \ipv4.addresses 172.16.20.214/24 ipv4.gateway 172.16.20.1 \ipv4.dns 114.114.114.114#重启网卡生效 nmcli c down ens32 && nmcli c up ens32#添加DNS …

代码随想录算法训练营第二十九天 | 回溯算法总结

​ 代码随想录算法训练营第二十九天 | 回溯算法总结 1. 组合问题 1.1 组合问题 在77. 组合中&#xff0c;我们开始用回溯法解决第一道题目&#xff1a;组合问题。 回溯算法跟k层for循环同样是暴力解法&#xff0c;为什么用回溯呢&#xff1f;回溯法的魅力&#xff0c;用递…

解决appium或selenium使用时driver.find_element_by_xpath中间有删除线问题

一、问题描述 Darren洋在公司电脑搭建完成appium后准备运行appium2.0版本执行脚本时发现执行脚本中的driver.find_element_by_xpath中间有删除线&#xff0c;说明较高版本的appium及selenium中该方法已被弃用。 二、解决办法 该问题解决办法为将driver.find_element_by_xpath()…

Linux线程--创建及等待

1.进程与线程 典型的UNIX/Linux进程可以看成只有一个控制线程&#xff1a;一个进程在同一时刻只做一件事情。有了多个控制线程后&#xff0c;在程序设计时可以把进程设计成在同一时刻做不止一件事&#xff0c;每个线程各自处理独立的任务。  线程是操作系统能够进行运算调度的…

Android Studio快速实现Flutter应用的国际化和多语言支持

文章目录 Flutter实现国际化和多语言支持添加依赖库Android Studio 安装flutter Intl插件项目初始化增加语言app中使用国际化在应用中切换语言&#xff1a;运行应用 总结easy_localization 插件intl 包Flutter GetX 包flutter_i18n 插件JSON 文件 Flutter实现国际化和多语言支持…

01、Python 的数据类型

目录 数据类型Python变量具有如下两个特征&#xff1a;输出变量 标识符规则整型四种表示形式浮点数复数 数据类型 使用Python变量 Python的基础类型 Python变量具有如下两个特征&#xff1a; 变量无需声明即可直接赋值&#xff1a;对一个不存在的变量赋值就相当于定义了一个…

html表格标签

<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title> </head> <body><!--表格table 行 tr 列 td --> <table border"1px"><tr> <!--colsp…

mac 升级node到指定版本

node版本14.15.1升级到最新稳定版18.18.2 mac系统 先查看一下自己的node版本 node -v开始升级 第一步 清除node的缓存 sudo npm cache clean -f第二步 安装n模块【管理模块 n是管理 nodejs版本】 sudo npm install -g n第三步升级node sudo n stable // 把当前系统的 Node…

【试题039】 多个逻辑或例题

题目&#xff1a;设int n;,执行表达式(n0)||(n1)||(n2)||(n3)后,n的值是&#xff1f;代码分析&#xff1a; //设int n; , 执行表达式(n 0) || (n 1) ||(n 2) ||(n 3)后, n的值是?int n;printf("n%d\n", (n 0) || (n 1) || (n 2) || (n 3));//分析&#xff1…

[Spring] SpringBoot2 简介(一)—— 基础配置

目录 一、SpringBoot 简介 1、Spring 的缺点 2、SpringBoot 功能 二、SpringBoot 入门案例 1、实现步骤 2、访问服务器 3、入门小结 4、Idea 快速构建 SpringBoot 工程 5、起步依赖无需版本号 6、主启动类的在项目中的位置&#xff08;*重要*&#xff09; 三、Sprin…

【Unity3D编辑器拓展】Unity3D的IMGUI、GUI、GUILayout、EditorGUI、EditorGUILayout、OnGUI【全面总结】

推荐阅读 CSDN主页GitHub开源地址Unity3D插件分享简书地址我的个人博客 大家好&#xff0c;我是佛系工程师☆恬静的小魔龙☆&#xff0c;不定时更新Unity开发技巧&#xff0c;觉得有用记得一键三连哦。 一、前言 在开发中&#xff0c;常常会遇到要使用OnGUI的地方。 也会遇到…