当前位置: 首页 > news >正文

C++11语法

1. C++11的发展历史

C++11是C++的第二个主要版本,并且是从C++98起的最重要更新。它引入了大量更改,标准化了既有实践,并改进了C++程序员可用的抽象。在它最终由ISO在2011年8月12日采纳前,人们曾使用名称C++0x,因为它曾被期待在2010年之前发布。C++03与C++11期间花了8年时间,故而这是迄今为止最长的版本间隔。从那时起,C++有规律地每3年更新一次。

2. 列表初始化

2.1 C++98传统的{}

C++98中一般数组和结构体可以用{}进行初始化。

struct Person
{string name;int age;
};int main()
{int arr[] = { 1,2,3,4,5 };Person p[] = { {"张三", 18}, {"李四", 19}, {"王五", 20} };return 0;
}

2.2 C++11中的{}

  • C++11以后想统一初始化方式,试图实现一切对象皆可用{}初始化,{}初始化也叫做列表初始化。
  • 内置类型支持,自定义类型也支持,自定义类型本质是类型转换,中间会产生临时对象,最后优化以后变成直接构造。
  • {}初始化的过程中,可以省略掉=。
  • C++列表初始化的本意是想实现一个大统一的初始化方式,其次他在有些场景下带来的不少便利,如容器push/insert多参数构造对象时,{}初始化会很方便
class Date
{
public:Date(int year = 2025, int month = 1, int day = 1):_year(year),_month(month),_day(day){cout << "Date(int year, int month, int day)" << endl;}Date(const Date& d):_year(d._year),_month(d._month),_day(d._day){cout << "Date(const Date& d)" << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1 = { 2025, 7, 23 };Date d2{ 2025, 7, 23 };vector<Date> v;v.push_back(d1);v.push_back({ 2025, 7, 23 });return 0;
}

2.3 C++11中的std::initializer_list

  • 上面的初始化已经很方便,但是对象容器初始化还是不太方便,比如一个vector对象,我想用N个值去构造初始化,那么我们得实现很多个构造函数才能支持。
  • C++11库中提出了一个std::initializer_list的类,这个类的本质是底层开一个数组,将数据拷贝过来,std::initializer_list内部有两个指针分别指向数组的开始和结束。
  • 这是他的文档:initializer_list - C++ Reference,std::initializer_list支持迭代器遍历。
  • 容器支持一个std::initializer_list的构造函数,也就支持任意多个值构成的初始化。STL中的容器支持任意多个值构成的初始化,就是通过std::initializer_list的构造函数支持的。
#include<vector>
#include<iostream>using namespace std;int main()
{initializer_list<int> list1 = { 10,20,30 };cout << list1.begin() << endl;cout << list1.end() << endl;vector<int> v1 = { 1,2,3,4,5 };for (auto& e : v1){cout << e << ' ';}cout << endl;// 也支持赋值vector<int> v2;v2 = { 1,2,3,4,5 };for (auto& e : v1){cout << e << ' ';}cout << endl;return 0;
}

3. 右值引用和移动语义

C++98的语法中就有引用的语法。而C++11中新增了右值引用语法特性,C++11之后我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。

3.1 左值和右值

  • 左值是一个表示数据的表达式(如变量名或解引用的指针),一般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现在赋值符号的左边,也可以出现在赋值符号的右边。定义时const修饰符后的左值,不能给它赋值,但是可以取它的地址。
  • 右值也是一个表示数据的表达式,要么是字面值常量、要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址。
#include<string>
#include<iostream>using namespace std;int main()
{// 常见的左值int* p = new int(0);int b = 1;const int c = b;*p = 10;string s("111111");s[0] = 'x';cout << &c << endl;cout << (void*)&s[0] << endl;// cout是流输出,想要输出地址的话需要强转一下int x = 1, y = 2;10;x + y;max(1, 2);string("111111");//cout << &10 << endl;//cout << &(x+y) << endl;//cout << &(max(x, y)) << endl;//cout << &string("111111") << endl;return 0;
}

3.2 左值引用和右值引用

  • int& r1 = x; int&& rr2 = y;第一个语句就是左值引用,左值引用就是给左值取别名,第二个就是右值引用,同样的道理,右值引用就是给右值取别名。
  • 左值引用不能直接引用右值,但是const左值引用可以引用右值。
  • 右值引用不能直接引用左值,但是右值引用可以引用move(左值)
  • move是库里面的一个函数模板,本质内部是进行强制类型转换,也就是可以把左值move成右值,把右值move成左值。
  • 需要注意的是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量表达式的属性是左值。
  • 语法层面看,左值引用和右值引用都是取别名,不开空间。但是从汇编底层的角度下,底层都是用指针实现的,没什么区别。底层汇编等实现和上层语法表达的意义有时是背离的。
#include<string>
#include<iostream>using namespace std;int main()
{int* p = new int(0);int b = 1;*p = 10;string s("111111");s[0] = 'x';// 左值引⽤给左值取别名int& r1 = b;int*& r2 = p;int& r3 = *p;string& r4 = s;char& r5 = s[0];int x = 1, y = 2;10;x + y;max(1, 2);string("11111");// 右值引⽤给右值取别名int&& rr1 = 10;double&& rr2 = x + y;double&& rr3 = max(x, y);string&& rr4 = string("11111");// 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值const int& rx1 = 10;const double& rx2 = x + y;const double& rx3 = max(x, y);const string& rx4 = string("11111");// 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)int&& rrx1 = move(b);int*&& rrx2 = move(p);int&& rrx3 = move(*p);string&& rrx4 = move(s);// b、r1、rr1都是变量表达式,都是左值cout << &b << endl;cout << &r1 << endl;cout << &rr1 << endl;// 这⾥要注意的是,rr1的属性是左值,所以不能再被右值引⽤绑定,除⾮move⼀下int& r6 = r1;//int&& rrx6 = rr1;int&& rrx6 = move(rr1);return 0;
}

3.3 引用延长生命周期

右值引用可用于为临时对象延长生命周期,const的左值引用也能延长临时对象生命周期,但这些对象无法被修改。

#include<string>
#include<iostream>using namespace std;int main()
{string s1 = "test";//string&& r1 = s1;// 错误:不能绑定到左值const string& r2 = s1 + s1; // OK:到 const 的左值引⽤延⻓⽣存期//r2 += "Test"; // 错误:不能通过到 const 的引⽤修改string&& r3 = s1 + s1; // OK:右值引⽤延⻓⽣存期r3 += "test"; // OK:能通过到⾮ const 的引⽤修改cout << r3 << endl;return 0;
}

3.4 左值和右值的参数匹配

  • C++98中,我们实现一个const左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。
  • C++11以后,分别重载左值引用、const左值引用、右值引用作为形参的Func函数,那么实参是左值会匹配Func(左值引用),实参是const左值会匹配Func(const 左值引用),实参是右值会匹配Func(右值引用)。
  • 右值引用变量在用于表达式时属性是左值,这个设计会感觉很怪,下面讲到右值引用的时候大家就能体会这样设计的价值了。
#include<iostream>using namespace std;void Func(int& x)
{cout << "左值引用" << endl;
}void Func(const int& x)
{cout << "const左值引用" << endl;
}void Func(int&& x)
{cout << "右值引用" << endl;
}int main()
{int n = 10;Func(n);// 左值引用const int m = 100;Func(m);// const左值引用Func(n + m);// 右值引用Func(move(n));// 右值引用int&& x = 20;Func(x);// 左值引用Func(move(x));// 右值引用return 0;
}

3.5 右值引用和移动语义的使用场景

  • 移动构造函数是一种构造函数,类似拷贝构造函数,移动构造函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须要有缺省值。
  • 移动赋值是一个赋值运算符的重载,它跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
  • 对于像string/vector这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第一个参数都是右值引用的类型,它的本质是要窃取引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从而提高效率。
#include<iostream>using namespace std;class SqTable
{
public:SqTable(const char* str = " "):_size(strlen(str)), _capacity(_size){cout << "构造" << endl;_str = new char[_capacity + 1];strcpy(_str, str);}SqTable(const SqTable& st):_str(nullptr){cout << "拷贝构造" << endl;_str = new char[st._capacity + 1];strcpy(_str, st._str);_size = st._size;_capacity = st._capacity;}void swap(SqTable& st){std::swap(_str, st._str);std::swap(_size, st._size);std::swap(_capacity, st._capacity);}SqTable(SqTable&& st){cout << "移动构造" << endl;swap(st);}SqTable& operator=(const SqTable& st){cout << "拷贝赋值" << endl;if (this != &st){_str[0] = '\0';_str = new char[st._capacity + 1];strcpy(_str, st._str);_size = st._size;_capacity = st._capacity;}return *this;}SqTable& operator=(SqTable&& st){cout << "移动赋值" << endl;swap(st);return *this;}private:char* _str;size_t _size;size_t _capacity;
};int main()
{SqTable str1("hello");// 构造SqTable str2(str1);// 拷贝构造SqTable str3(move(str1));// 移动构造SqTable str4;str4 = str2;// 拷贝赋值str4 = move(str2);// 移动赋值return 0;
}

3.6 类型分类

  • C++11以后,进一步对类型进行了划分,右值被划分纯右值(pure value,简称prvalue)和将亡值(expiring value,简称xvalue)。
  • 纯右值是指那些字面值常量或求值结果相当于字面值或是一个不具名的临时对象。如:42、true、nullptr或者类似str.substr(1,2)、str1 + str2传值返回函数调用。C++中的纯右值概念划分等价于C++98中的右值。
  • 将亡值是指返回右值引用的函数的调用表达式和转换为右值引用的转换函数的调用表达,如move(x)等。
  • 泛左值(generalized  value,简称glvalue),泛左值包含将亡值和左值。

3.7 引用折叠

  • C++中不能直接定义引用的引用如 int& && r = i;,这样写会直接报错,通过模板或typedef中的类型操作可以构成引用的引用。
  • 通过模板或typedef中的类型操作构成引用的引用时,这时C++11给出了一个引用折叠的规则,右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。
  • 下面的程序中很好的展示了模板和typedef时构成引用的引用时的引用折叠规则。
  • 像Func2这样的函数模板中,T&& x参数看起来是右值引用,有些地方也把这种函数模板的参数叫做万能引用。
#include<iostream>using namespace std;// 由于引⽤折叠限定,Func1实例化以后总是⼀个左值引⽤
template<class T>
void Func1(T& x)
{}// 由于引⽤折叠限定,Func2实例化以后可以是左值引⽤,也可以是右值引⽤
template<class T>
void Func2(T&& x)
{}int main()
{typedef int& lref;typedef int&& rref;int n = 0;lref& r1 = n; // r1 的类型是 int&lref&& r2 = n; // r2 的类型是 int&rref& r3 = n; // r3 的类型是 int&rref&& r4 = 1; // r4 的类型是 int&&// 没有折叠->实例化为void f1(int& x)Func1<int>(n);//Func1<int>(0); // 报错// 折叠->实例化为void f1(int& x)Func1<int&>(n);//Func1<int&>(0); // 报错// 折叠->实例化为void f1(int& x)Func1<int&&>(n);//Func1<int&&>(0); // 报错// 折叠->实例化为void f1(const int& x)Func1<const int&>(n);Func1<const int&>(0);// 折叠->实例化为void f1(const int& x)Func1<const int&&>(n);Func1<const int&&>(0);// 没有折叠->实例化为void f2(int&& x)//Func2<int>(n); // 报错Func2<int>(0);// 折叠->实例化为void f2(int& x)Func2<int&>(n);//Func2<int&>(0); // 报错// 折叠->实例化为void f2(int&& x)//Func2<int&&>(n); // 报错Func2<int&&>(0);return 0;
}

4. 可变参数模板

4.1 基本语法及原理

  • C++11支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包,存在两种参数包。模板参数包表示零或多个模板参数;函数参数包:表示零或多个函数参数。
  • template<class ...Args> void Func(Args... args) {}。用省略号来指出一个模板参数或函数参数的表示一个包,在模板参数列表中,class...或typename...指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟...指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板一样,每个参数实例化时遵循引用折叠规则。
  • 可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
  • 我们可以使用sizeof...运算符去计算参数包中参数的个数。
#include<iostream>using namespace std;template<class ...Args>
void Print(Args... args)
{cout << sizeof...(args) << endl;
}int main()
{double x = 2.2;Print(); // 包⾥有0个参数Print(1); // 包⾥有1个参数Print(1, string("xxxxx")); // 包⾥有2个参数Print(1.1, string("xxxxx"), x); // 包⾥有3个参数return 0;
}

4.2 包扩展

对于一个参数包,我们除了能计算他的参数个数,我们能做的唯一的事情就是扩展它,当扩展一个包时,我们还要提供用于每个扩展元素的模式,扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式的右边放一个省略号(...)来触发扩展操作。

#include<string>
#include<iostream>using namespace std;template<class T>
const T& GetArgs(const T& x)
{cout << x << ' ';return x;
}template<class ...Args>
void Arguments(Args... args)
{}template<class ...Args>
void Print(Args... args)
{Arguments(GetArgs(args)...);
}int main()
{Print(10, 3.14, string("hello"));// 底层是递归处理,所以从最后一个参数开始打印return 0;
}

5. 新的类功能

5.1 默认的移动构造和移动赋值

  • 原来C++类中,右6个默认成员函数:构造函数/析构函数/拷贝构造函数/拷贝赋值重载/取地址重载/const 取地址重载,最重要的是前4个,后两个用处不大。C++11新增了两个默认成员函数,也就是移动构造函数和移动赋值运算符重载。
  • 如果吗没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
  • 如果吗没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动赋值。默认生成的移动赋值函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。
  • 如果你提供了移动构造或移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
#include<iostream>using namespace std;class SqTable
{
public:SqTable(const char* str = " "):_size(strlen(str)), _capacity(_size){cout << "构造" << endl;_str = new char[_capacity + 1];strcpy(_str, str);}SqTable(const SqTable& st):_str(nullptr){cout << "拷贝构造" << endl;_str = new char[st._capacity + 1];strcpy(_str, st._str);_size = st._size;_capacity = st._capacity;}void swap(SqTable& st){std::swap(_str, st._str);std::swap(_size, st._size);std::swap(_capacity, st._capacity);}SqTable(SqTable&& st){cout << "移动构造" << endl;swap(st);}SqTable& operator=(const SqTable& st){cout << "拷贝赋值" << endl;if (this != &st){_str[0] = '\0';_str = new char[st._capacity + 1];strcpy(_str, st._str);_size = st._size;_capacity = st._capacity;}return *this;}SqTable& operator=(SqTable&& st){cout << "移动赋值" << endl;swap(st);return *this;}private:char* _str;size_t _size;size_t _capacity;
};class Person
{
public:/*~Person(){}*/
private:SqTable _tables;
};int main()
{Person s1;Person s2 = s1;Person s3 = move(s1);Person s4;s4 = move(s2);return 0;
}

5.2 default和delete

  • C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。
  • 如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private。这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
class Person
{
public:Person(const char* tables = " "):_tables(tables){ }Person(const Person& p):_tables(p._tables){ }private:SqTable _tables;
};int main()
{Person s1;Person s2 = s1;Person s3 = move(s1);Person s4;s4 = move(s2);return 0;
}

6. lambda

6.1 lambda表达式语法

  • lambda表达式本质是一个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。lambda表达式语法使用层而言没有类型,所以我们一般是用auto或模板参数定义的对象去接收lambda对象。
  • lambda表达式的格式:[capture-list] (parameters)-> return type {function body}。
  • [capture-list]:捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用,捕捉列表可以传值和传引用捕捉,捕捉列表为空也不能省略。
  • (parameters):参数列表,与普通函数的参数列表类似,如果不需要参数传递,则可以连同()一起省略。
  • -> return type:返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
  • {function body}:函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。
#include<iostream>using namespace std;int main()
{auto Add1 = [](int x, int y)->int {return x + y; };cout << Add1(10, 20) << endl;// 1、捕捉为空也不能省略// 2、参数为空可以省略// 3、返回值可以省略,可以通过返回对象⾃动推导// 4、函数题不能省略auto Func1 = []{cout << "hello world" << endl;return 0;};Func1();return 0;
}

6.2 捕捉列表

  • lambda表达式中默认只能使用lambda函数体和参数中的变量,如果想用外层作用域中的变量就需要进行捕捉。
  • 第一种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割。[x,y,&z]表示x和y值捕捉,z引用捕捉。
  • 第二种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表写一个=表示隐式值捕捉,在捕捉列表写一个&表示隐式引用捕捉,这样我们lambda表达式中用了哪些变量,编译器就会自动捕捉那些变量。
  • 第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉。[=,&x]表示其他变量隐式值捕捉,x引用捕捉。当使用混合捕捉时,第一个元素必须是&或=,并且&混合捕捉时,后面的捕捉变量必须是值捕捉,同理=混合捕捉时,后面的捕捉变量必须是引用捕捉。
  • lambda表达式如果在函数局部域中,他可以捕捉lambda位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉,lambda表达式中可以直接使用。这也意味着lambda表达式如果定义在全局位置,捕捉列表必须为空。
  • 默认情况下,lambda捕捉列表是被const修饰的,也就是说传值捕捉过来的对象不能修改,mutable加在参数列表的后面可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改的还是形参变量,不会影响实参。使用该修饰符后,参数列表不可省略(即使参数为空)
#include<iostream>using namespace std;int x = 0;
// 捕捉列表必须为空,因为全局变量不⽤捕捉就可以⽤,没有可被捕捉的变量
auto Func1 = []()
{x++;
};int main()
{// 只能⽤当前lambda局部域和捕捉的对象和全局对象int a = 0, b = 1, c = 2, d = 3;auto Func1 = [a, &b]{// 值捕捉的变量不能修改,引⽤捕捉的变量可以修改//a++;b++;int ret = a + b;return ret;};cout << Func1() << endl;// 隐式值捕捉// ⽤了哪些变量就捕捉哪些变量auto Func2 = [=]{int ret = a + b + c;return ret;};cout << Func2() << endl;// 隐式引⽤捕捉// ⽤了哪些变量就捕捉哪些变量auto Func3 = [&]{a++;c++;d++;};Func3();cout << a << ' ' << b << ' ' << c << ' ' << d << endl;// 混合捕捉1auto Func4 = [&, a, b]{//a++;//b++;c++;d++;return a + b + c + d;};Func4();cout << a << ' ' << b << ' ' << c << ' ' << d << endl;// 混合捕捉2auto Func5 = [=, &a, &b]{a++;b++;/*c++;d++;*/return a + b + c + d;};Func5();cout << a << ' ' << b << ' ' << c << ' ' << d << endl;// 局部的静态和全局变量不能捕捉,也不需要捕捉static int m = 0;auto Func6 = []{int ret = x + m;return ret;};// 传值捕捉本质是⼀种拷⻉,并且被const修饰了// mutable相当于去掉const属性,可以修改了// 但是修改了不会影响外⾯被捕捉的值,因为是⼀种拷⻉auto Func7 = [=]()mutable{a++;b++;c++;d++;return a + b + c + d;};cout << Func7() << endl;cout << a << ' ' << b << ' ' << c << ' ' << d << endl;return 0;
}

6.3 lambda的原理

  • lambda的原理和范围for很像,编译后从汇编指令层的角度看,压根就没有lambda和范围for这样的东西。范围for底层是迭代器,而lambda底层是仿函数对象。也就说我们写一个lambda以后,编译器会生成一个对应的仿函数的类。
  • 仿函数的类名是编译按一定规则生成的,保证不同的lambda生成的类名不同,lambda参数/返回类型/函数体就是仿函数operator()的参数/返回类型/函数体,lambda的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是lambda类构造函数的实参,当然隐式捕捉,编译器要看使用哪些就传那些对象。
http://www.dtcms.com/a/300063.html

相关文章:

  • 计算机中的数据表示
  • C++ TAP(基于任务的异步编程模式)
  • 停止所有docker容器的命令
  • 【SSM】第二章 网上蛋糕项目商城-首页
  • 进程线程协程深度对比分析
  • 2025年渗透测试面试题总结-2025年HW(护网面试) 71(题目+回答)
  • HarmonyOS应用上架流程详解
  • element-plus安装以及使用
  • STM32概况
  • Matlab自学笔记六十五:解方程的数值解法(代码速成)
  • 如何查看电脑后门IP和流量?
  • ECSPI控制器
  • 【Spring AI】SiliconFlow-硅基流动
  • 如何使用Anaconda(miniconda)和Pycharm
  • 宝塔面板如何升级OpenSSL
  • LED滚动字幕投射器:免费好用的字幕工具
  • [硬件电路-91]:模拟器件 - 半导体与常规导体不一样,其电阻式动态变化的,浅谈静态电阻与动态电阻
  • 倍思鹿数值仿真-实现设备型号库的可编辑、便捷导入项目
  • python I 本地 html 文件读取方法及编码报错问题详解
  • 【坐车n次最少钱】2022-8-31
  • Python项目结构与打包工具实践
  • java测试题(下)
  • 创意竖状滚动轮播图
  • 嵌入式硬件篇---zigbee无线串口通信问题
  • 【科普】ESP8266 连接 OneNet 平台传输数据与 4G 模块传输数据在多个维度存在显著区别,主要体现在以下几个方面:
  • 802.11系列无线局域网标准详细对比
  • buuctf_一叶障目_just a rar_snack
  • golang实现一个规则引擎,功能包括实时增加、修改、删除规则
  • 基于springboot的图书借阅系统
  • AI Agent开发学习系列 - LangGraph(1): 用LangGraph创建我们的第一个Agent