C++ Primer 第十六章 模板与泛型编程 重点解读

文章目录

  • 1 定义模板
    • 1.1 类模板成员函数的实例化:
    • 1.2 在类代码内简化模板类名的使用:
    • 1.3 令模板自己的类型参数成为友元(C++11)
    • 1.4 模板类型别名
      • 1.4.1 typedef
      • 1.4.2 为模板定义类型别名(C++11)
    • 1.5 函数模板与类模板的区别
    • 1.6 使用类的类型成员——`typename`
    • 1.7 成员模板
    • 1.8 控制实例化(C++11)
      • 1.8.1 实例化定义会实例化所有成员
    • 1.9 ⭐关于智能指针的删除器
      • 1.9.1 [在运行时绑定删除器](https://gcc.gnu.org/onlinedocs/libstdc++/libstdc++-api-4.6/a01034_source.html) —— `shared_ptr`
      • 1.9.1 在编译时绑定删除器 ——`unique_ptr`
  • 2 模板实参推断
    • 2.1 类型转换与模板类型参数
    • 2.2 函数模板显示实参
    • 2.3 尾置返回类型(C++11)与类型转换
    • 2.4 进行类型转换的标准库模板类
    • 2.5 引用折叠和右值引用参数
    • 2.6 理解 `std:move`
    • 2.7 在调用中使用`std::forward`保持类型信息
  • 3 重载与模板
    • 3.1 如何选择重载版本
    • 3.2 处理C风格字符串指针和字符串字面常量
  • 4 可变参数模板(C++11)
    • 4.1 编写可变模板参数函数模板
    • 4.2 包扩展
    • 4.3 转发参数包 —— `emplace`
  • 5 模板特例化
    • 5.1 类模板特例化
    • 5.2 类模板部分特例化(偏特化)
      • 5.2.1 类模板的全特化和偏特化
      • 5.2.2 通过类模板的偏特化实现`std::remove_reference`
      • 5.2.3 函数模板不支持偏特化
    • 5.3 间接的对函数模板进行偏特化
      • 5.3.1 借助类模板偏特化
  • ※ 5.3.2`Concepts`(C++20)
      • 5.3.3 使用concept间接实现函数模板的偏特化

1 定义模板

1.1 类模板成员函数的实例化:

如果一个成员函数没有被使用,则它不会被实例化。成员函数只有在被用到时才进行实例化

1.2 在类代码内简化模板类名的使用:

在一个类模板的作用域内,我们可以直接使用模板名而不必指定模板实参

1.3 令模板自己的类型参数成为友元(C++11)

template<typename T>
class Test2 {friend T;
private:int t2 = 0;
};class Test1 {
public:void func() {Test2<Test1> t;cout << t.t2 << endl;    //访问Test2类的私有成员}
private:int t = 1;};int main() {Test1 t;t.func();return 0;
}

1.4 模板类型别名

1.4.1 typedef

  • typedef可以为一个实例化的模板定义别名,但是不能定义一个typedef引用一个模板
typedef Blob<string> StrBlob;   //ok
typedef Blob<T> TBlob  //error

1.4.2 为模板定义类型别名(C++11)

  • template<typename T> using twin = pair<T, T>;
    twin<string> authors;  //authors 的类型为pair<string, string>
    
  • template<typename T> using twin = pair<T, int>;
    twin<string> authors;    //authors 的类型为pair<string, int>

1.5 函数模板与类模板的区别

  • 默认参数:函数模板不允许由默认参数(C++11之后支持了函数模板的默认实参);类模板在模板参数列表中可以有默认参数
  • 特化:函数模板只能全特化;类模板可以全特化、偏特化
  • 调用方式:函数模板可以进行类型推导、隐式调用、显示调用;类模板只能显示调用
template<typename T> constexpr
auto func(const T& x, const T& y) -> decltype(x + y) {cout << typeid(T).name() << ": ";return x + y;
}int main() {int x1 = 10, y1 = 20;cout << func<int>(x1, y1) << endl;    //显示double x2 = 2.2, y2 = 3.3;cout << func(x2, y2) << endl;	//隐式
}

1.6 使用类的类型成员——typename

string::size_type* t;

上述代码由于编译器有string的定义,从而知道size_type是一个类型

  • 在普通(非模板)代码中,编译器掌握类的定义,因此,他知道通过作用域访问的名字是类型还是static成员。
T::size_type * p;    
//1.p是一个size_type类型的指针
//2.表达式为size_type这个静态成员与p相乘

如果T是一个模板类型参数,编译器无法知道size_type是一个类型还是一个static成员,

  • 默认情况下,C++语言假定通过作用域运算符(::)访问的名字不是类型。因此,我们需要手动声明使用一个模板类型参数的类型成员——typename
template <typename T>
typename T::value_type top(const T& c) {if (!c.empty()) return c.back();else return typename T::value_type();    //当容器为空,返回容器的value_type的默认值
}

1.7 成员模板

  • 一个类可以包含本身是模板的成员函数。这种成员被称为成员模板

问:为什么成员模板不能是虚函数?

答:C++中的成员模板函数不能被声明为虚函数。这是因为模板函数只有在使用时才会被实例化而虚函数表的大小在编译期间就需要确定。如果允许成员模板函数为虚函数,那么编译器就必须知道我们一共为该成员模板函数实例化了多少个不同的版本,以便确定类的虚函数表的大小。这是不可能的,因为模板函数可以被实例化为任意数量的版本,这取决于模板参数的类型和数量。

  • 实例:定义unique_ptr的删除器

1.8 控制实例化(C++11)

在大系统中,在多个文件中实例化相同模板的额外开销可能非常严。在新标准中,我们可以通过显示实例化(explicit instantiation)来避免这种开销

//显式实例化的形式:
template [declaration];  	//实例化定义extern template [declaration];   //实例化声明
//Application.cc//这些模板类型必须在程序其他位置进行实例化
extern template class Blob<string>;
extern template int compare(const int&, const int&);
Blob<string> sa1, sa2;  	//实例化会出现在其他位置//接收initializer_list的构造和拷贝构造函数在本文件中实例化
Blob<int> a1 = {0,1,2,3}		
Blob<int> a2(a1);int i = compare(a1[0], a2[0]); 		//实例化出现在其他位置
//templateBuild.cc
//实例化文件必须为每个在其他文件中声明extern的类型和函数提供一个(非extern)的定义
template int compare(const int&, const int&);
template class Blob<string>;    //实例化模板的所有成员

1.8.1 实例化定义会实例化所有成员

一个类模板的实例化定义会实例化该模板的所有成员,包括内联的成员函数。因为当编译器遇到一个实例化定义时,他不了解程序使用哪些成员函数

1.9 ⭐关于智能指针的删除器

shared_ptr:

在这里插入图片描述

unique_ptr:

在这里插入图片描述

通过观察shared_ptrunique_ptr的模板可以发现,shared_ptr的删除器只需要在创建或reset指针时传递给它一个可调用对象即可;而删除器类型是一个unique_ptr对象的类型的一部分,用户必须在定义unique_ptr时以显示模板实参的形式提供删除器类型

1.9.1 在运行时绑定删除器 —— shared_ptr

  • shared_ptr必须能直接访问其删除器,删除器必须保存为一个指针或一个封装了指针的类(如function)
int main() {int* p = new int(1);shared_ptr<int> sp(p, [](int* p) { delete(p); });int* p2 = new int(2);sp.reset(p2, [](int* p) { delete(p); cout << "haha" << endl; });//程序最后打印haha 
}

我们可以确定shared_ptr不是将删除器直接保存为一个成员,因为删除器的类型知道运行时才会确定

1.9.1 在编译时绑定删除器 ——unique_ptr

unqiue_ptr类中,删除器的类型是类类型的一部分,即unique_ptr有两个模板参数.

由于删除器的类型是unique_ptr类型的一部分,因此删除器成员的类型在编译时就确定了。从而删除器可以直接保存在unique_ptr对象中

通过在编译时绑定删除器,unique_ptr避免了间接调用删除器的运行时开销。

通过在运行时绑定删除器,shared_ptr使用户重载删除器更为方便

2 模板实参推断

2.1 类型转换与模板类型参数

template <typename T> T fobj(T t1, T t2) { return T(); }
template <typename T> T fref(const T& t1, const T& t2) { return T(); }
int main() {int x1 = 1;const int x2 = 2;fobj(x1, x2);   //调用fobj(int, int)fref(x1, x2);	//调用fref(const int&, const int&)int a[10], b[10];fobj(a, b);		//调用f(int*, int*)fref(a, b);		//error:没有与参数列表匹配的函数模板
}

不能用于函数模板的类型转换:

  1. 算数转换
  2. 派生类向基类的转换
  3. 用户定义的转换

2.2 函数模板显示实参

template<typename T1, typename T2, typename T3>
T1 sum(T2 x, T3 y) {return x + y;
}int main() {cout << sum<int>(1.1, 1) << endl;    //2cout << sum<double>(1.1, 1) << endl;     //2.1
}

2.3 尾置返回类型(C++11)与类型转换

template <typename It>
auto fcn(It beg, It en) -> decltype(*beg) {return *beg;
}

解引用返回一个序列的左值,因此通过decltype推断的类型为beg表示的元素的类型的引用

2.4 进行类型转换的标准库模板类

  • 其中typeremove_reference模板类的一个类型成员
template<typename It>
//为了使用模板参数的成员,type是一个类的成员,而该类依赖于一个模板参数,因此我们必须使用typename
//type的类型为非引用的
auto func2(It beg, It end) -> typename remove_reference<decltype(*beg)>::type {reutrn *beg;
}

在这里插入图片描述

其他标准类型转换模板在书606页

习题:

template<typename It>
//为了使用模板参数的成员,必须用typename
auto func2(It beg, It end) ->  decltype(*beg + 0) {		//合法:左值引用 + 右值 -> 左值return *beg;    //返回序列中一个元素的拷贝
}int main() {list<int> lt{ 1,2 };decltype(func2(lt.begin(), lt.end())) t = 10;
}

2.5 引用折叠和右值引用参数

template <typename T> void func(T&&);int main() {int i = 0;func(i);    //看起来实参的类型为int& && 
}
  • 我们不能直接定义一个引用的引用, 但是可以通过类型别名模板类型参数间接定义。
  • 新标准中(C++11), 折叠规则拓展到右值引用
    • X& &X& &&X&& & --折叠–> X&
    • X&& && --折叠–> X&&
typedef int& ref_type;
typedef ref_type&& refs_type;
int main() {int x = 10;refs_type y = x;   //y是一个左值引用:int& && -> 引用折叠 -> int&
}

2.6 理解 std:move

//std::move是如何定义的:
template <typename T>
typename remove_reference<T>::type&& move(T&& t) {return static_cast<typename remove_reference<T>::type&&>(t);
}//考虑以下两种情况  P611
int main() {string s1("lvalue s1"), s2;s = move(string("rvalue s"));	//右值移动s = move(s1);	//左值移动,移动之后,s1的值是不确定的
}

2.7 在调用中使用std::forward保持类型信息

3 重载与模板

3.1 如何选择重载版本

  • 当有多个重载模板对一个调用提供同样好的匹配时,因该选择最特例化的版本
template <typename T> string debug_rep(const T& t) {}template <typename T> string debug_rep(T* t) {}int main() {string s("ss");const string* sp = &s;debug_rep(sp);    //调用第二个,T被绑定到const string
}
  • 如果一个非模板函数和一个模板函数提供同样好的匹配,则选择非模板版本

3.2 处理C风格字符串指针和字符串字面常量

4 可变参数模板(C++11)

4.1 编写可变模板参数函数模板

 template <typename T>ostream& print(ostream& os, const T& t) {return os << t;}template <typename T, typename... Args>ostream& print(ostream& os, const T& t, const Args&... args) {os << t << ' ';return print(os, args...);    //包拓展}

4.2 包扩展

  1. 函数参数包名 + ...
print(os, args...); 
  1. function(函数参数包名) + ...
print(os, debug_rep(args...));    //扩展中的模式会独立地应用于包中的元素

4.3 转发参数包 —— emplace

template <class... Args>
inline
void StrVec::emplace_back(Args&&... args) {chk_n_alloc();alloc.construct(first_free++, std::forward<Args>(args)...);
}

5 模板特例化

5.1 类模板特例化

namespace std {template<>struct hash<Sales_data> {//用来散列一个无需容器的类型必须要定义下列类型typedef size_t result_type;typedef Sales_data argument_type;    //默认情况下,此类型需要operator==size_t operator()(const Sales_data& s) const {return hash<string>() (s.bookNo) ^      //构造匿名对象调用operator()返回一个哈希值hash<unsigned>() (s.units_sold) ^ hash<double>() (s.revenue);}};
}  //关闭std命名空间

5.2 类模板部分特例化(偏特化)

5.2.1 类模板的全特化和偏特化

我们只能部分特例化类模板,而不能部分特例化函数模板 —— P628

template <typename T>
class add {
public:T operator()(T a, T b) {return a + b;}
};template <typename T>  //类模板的全特化
class add<T*> { 
public:T operator()(T* a, T* b) {cout << "pointer" << endl;return *a + *b;}
};template<>
class add<int*> {   //类模板的偏特化
public:int operator()(int* a, int* b) {cout << "int pointer" << endl;return *a + *b;}
};

5.2.2 通过类模板的偏特化实现std::remove_reference

代码更具模板的匹配规则进行精确的匹配,当传入的是一个右值引用时,匹配第三个模板,此时T的类型为去掉右值引用性质之后的类型

template<class T> struct remove_reference {typedef T type;
};
template<class T> struct remove_reference<T&> {typedef T type;
};
template<class T> struct remove_reference<T&&> {typedef T type;
};

5.2.3 函数模板不支持偏特化

问:为什么函数模板不能支持偏特化,如果可以,发产生怎样的歧义?

template <typename T>
T add(T a, T b) {
return a + b;
}template <typename T*>   //函数模板的偏特化,错误,产生歧义
T add(T* a, T* b) {
return *a + *b;
}template<>
int add(int a, int b) {   //函数模板的全特化return a + b;
}/*
template<>
class add<int*> {   //类模板的偏特化
public:int operator()(int* a, int* b) {cout << "int pointer" << endl;return *a + *b;}
};
*/

在这个例子中,如果我们尝试调用add(a, b),其中a和b都是指针类型,编译器将无法确定应该调用哪个函数。因为第一个函数模板可以通过类型推导将T推导为指针类型,而第二个函数模板是针对指针类型的偏特化版本。这就产生了歧义,编译器无法确定应该调用哪个函数。

5.3 间接的对函数模板进行偏特化

C++标准并不支持函数模板偏特化。然而在实际开发中,我们确实需要对一些函数模板进行偏特化。这是因为偏特化和函数重载容易出现歧义。

  1. 借助类模板偏特化:由于类可以进行偏特化处理,因此一种非常直观的方案就是使用Functor代替函数,并实现 operator ()。详细例子看5.2.1

  2. 使用Concepts:C++20提供了 Concepts 特性, Concepts 特性提出的动机是为了解决模板元编程过程中,编译器给出的报错信息冗余及编译器不能很好的给出准确的出错信息的问题。

5.3.1 借助类模板偏特化

template <typename T>
class add {
public:T operator()(T a, T b) {return a + b;}
};
template <typename T>  //全特化
class add<T*> {
public:T operator()(T* a, T* b) {cout << "pointer" << endl;return *a + *b;}
};
template<>
class add<int*> {   //偏特化
public:int operator()(int* a, int* b) {cout << "int pointer" << endl;return *a + *b;}
};int main() {cout << add<int>()(10, 20) << endl;int x = 10, y = 20;cout << add<int*>()(&x, &y) << endl;
}

※ 5.3.2Concepts(C++20)

Concepts的提出就是为了解决上述问题,它通过将模板的类型约束抽象出来,然后在模板定义时再使用。这样成功解耦了模板类型约束和模板本身的一些类型逻辑

  • concept的定义形式
template < template-parameter-list > concept concept-name = constraint-expression;

其中,constraint-expression 是一个可以被evalbool的表达式或者编译期函数。在使用定义好的concept时,constraint-expression 会根据上面 template-parameter-list 传入的类型,执行编译期计算,判断使用该concept的模板定义是否满足。如果不满足,则编译期会给定一个具有明确语义的错误

template <typename T>
concept always_satisfied = true;/*		将类型T抽象成integral类型,当模板实参使constraint-expression为true时,才匹配template<inntegral T>的函数模板*/
template <typename T>
concept integral = std::is_integral_v<T>;   template <typename T>
concept signed_integral = integral<T> && std::is_signed_v<T>;template <always_satisfied T>
T mul(T a, T b) {puts("always_satisfied");return a * b;
}template <integral T>
T add(T a, T b) {puts("integral");return a + b;
}template <signed_integral T>
T subtract(T a, T b) {puts("signed_integral");return a - b;
}int main() {mul(1, 2);mul(1.0f, 2.0f);add(1, 2);subtract(1, 2);
}

5.3.3 使用concept间接实现函数模板的偏特化

  • 我们以5.2.2的代码为基准,使用concept编写函数模板的偏特化
//T是一个整数类型
template <typename T>
concept integral = std::is_integral_v<T>;//T为一个整数类型的指针,特别注意,使用remove_pointer去除模板实参的指针性质
template <typename T>
concept integral_pointer = integral<typename std::remove_pointer<T>::type> && std::is_pointer_v<T>;template <integral T>
void add(T a, T b) {cout << "integral: " << a + b << endl;
}template <integral_pointer T>
void add(T a, T b) {cout << "integral_pointer: " << *a + *b << endl;
}int main() {int x = 10, y = 20;add(&x, &y);add(10, 20);/*output:integral_pointer: 30integral: 30*/float a = 1.1, b = 2.2;add(a, b);    //error:没有于参数列表匹配的重载函数,add实例参数类型为(float, float)add(&a, &b);    //error:没有于参数列表匹配的重载函数,add实例参数类型为(float*, float*)
}

template
concept integral_pointer = integral<typename std::remove_pointer::type> && std::is_pointer_v;

template
void add(T a, T b) {
cout << "integral: " << a + b << endl;
}

template <integral_pointer T>
void add(T a, T b) {

cout << "integral_pointer: " << *a + *b << endl;

}

int main() {
int x = 10, y = 20;
add(&x, &y);
add(10, 20);
/*
output:
integral_pointer: 30
integral: 30
*/

float a = 1.1, b = 2.2;
add(a, b);    //error:没有于参数列表匹配的重载函数,add实例参数类型为(float, float)
add(&a, &b);    //error:没有于参数列表匹配的重载函数,add实例参数类型为(float*, float*)

}


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

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

相关文章

数据结构——图解链表OJ题目

学完了单链表之后&#xff0c;我们对其基本结构已经有了一定的了解&#xff0c;接下来我们通过一些题目强化对链表的理解&#xff0c;同时学习一些面试笔试题目的新思路以及加强对数据结构单链表的掌握。 目录 题目一.876. 链表的中间结点 - 力扣&#xff08;LeetCode&#x…

生成对抗网络(GAN)手写数字生成

文章目录 一、前言二、前期工作1. 设置GPU&#xff08;如果使用的是CPU可以忽略这步&#xff09; 二、什么是生成对抗网络1. 简单介绍2. 应用领域 三、网络结构四、构建生成器五、构建鉴别器六、训练模型1. 保存样例图片2. 训练模型 七、生成动图 一、前言 我的环境&#xff1…

基于SSM实现的图书管理系统

一、系统架构 前端&#xff1a;jsp | js | css | jquery | layui 后端&#xff1a;spring | springmvc | mybatis 环境&#xff1a;jdk1.7 | mysql | maven | tomcat 二、代码及数据库 三、功能介绍 01. 登录页 02. 首页 03. 借阅管理 04. 图书管理 05. 读者管理 06. 类型管理…

【EI会议征稿】第三届网络安全、人工智能与数字经济国际学术会议(CSAIDE 2024)

第三届网络安全、人工智能与数字经济国际学术会议&#xff08;CSAIDE 2024&#xff09; 2024 3rd International Conference on Cyber Security, Artificial Intelligence and Digital Economy 第三届网络安全、人工智能与数字经济国际学术会议&#xff08;CSAIDE 2024&#…

c题目13:验证100以内的数是否满足哥德巴赫猜想。(任一大于2的偶数都可以写成两个质数之和)

每日小语 活下去的诀窍是&#xff1a;保持愚蠢&#xff0c;又不能知道自己有多蠢。——王小波 自己思考 即要让第一个质数与这个数减去第一个质数的值都为质数&#xff0c;所以要满足几个条件 1.abc 2.a为质数 3.b为质数 这里是否可以用到我之前刚学的自己设置的那个判断…

带头结点的双向循环链表

目录 带头结点的双向循环链表 1.存储定义 2.结点的创建 3.结点的初始化 4.尾插结点 5.尾删结点 6.头插结点 7.头删结点 8.查找并返回结点 9.在pos结点前插入结点 10.删除pos结点 11.打印链表 12.销毁链表 13.头插结点2.0版 14.尾插结点2.0版 前言&#xff1a; 当…

深入探究Python中的JSON、Pickle和Shelve模块:特性与区别

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 在Python中&#xff0c;处理数据序列化和持久化是极其重要的。JSON、Pickle和Shelve是三种常用的模块&#xff0c;它们提供了不同的方法来处理数据的序列化和持久化。本文将深入研究这三个模块&#xff0c;探讨它…

VBA_MF系列技术资料1-232

MF系列VBA技术资料 为了让广大学员在VBA编程中有切实可行的思路及有效的提高自己的编程技巧&#xff0c;我参考大量的资料&#xff0c;并结合自己的经验总结了这份MF系列VBA技术综合资料&#xff0c;而且开放源码&#xff08;MF04除外&#xff09;&#xff0c;其中MF01-04属于定…

【高效开发工具系列】Hutool DateUtil工具类

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

Python+Requests模块获取响应内容

Requests模块获取响应内容 响应包括响应行、响应头、响应正文内容&#xff0c;这些返回的响应信息都可以通过Requests模块获取。这些 获取到的响应内容也是接口测试执行得到的实际结果。 获取响应行 获取响应头 获取其它响应信息 代码示例&#xff1a; # 导入requests模块…

算法通关村第十四关-青铜挑战认识堆

大家好我是苏麟 , 今天带大家认识认识堆 . 堆 堆是将一组数据按照完全二叉树的存储顺序&#xff0c;将数据存储在一个一维数组中的结构。 堆有两种结构&#xff0c;一种称为大顶堆&#xff0c;一种称为小顶堆 : 大顶堆 大顶堆的任何一个父节点的值&#xff0c;都大于或等于…

前端下拉框select标签的插件——select2.js

本文采用的是select2 版本:Select2 4.0.6-rc.1。 可以兼容IE8及以上。亲测过。 官网:Getting Started | Select2 - The jQuery replacement for select boxes 一、认识select2.js 1、使用插件,首先要引入别人的插件了,你可以选择离线(无网络)或者在线引用的(如果有网…

ios 逆向分分析,某业帮逆向算法(一)

用到工具: 爱思助手CrackerXL(砸壳软件)越狱手机ida反汇编软件分析login 的sign 签名算法中自己写算法 已知我们32位,我们不妨猜测是md5 ,那我们试图使用CC_MD5 ,这个是ios 中的标准库, 我们使用frida-trace 注入hook一下,看看有没有 经过 是经过了这个函数,密码也是…

新建的springboot项目中application.xml没有绿色小叶子(不可用)

经常有朋友会遇到新建了一个springboot项目&#xff0c;发现为啥我创建的application.xml配置文件不是绿色的&#xff1f;&#xff1f;&#xff1f; 下面教大家如何解决&#xff0c;这也是博主在做测试的时候遇到的&#xff1a; 将当前位置application.xml删掉&#xff0c;重新…

在Spring Boot中使用@Async实现一个异步调用

在使用异步注解之前&#xff0c;我们需要先了解&#xff0c;什么是异步调用&#xff1f; 异步调用对应的事同步调用&#xff0c;同步调用是值程序按照我们定义的顺序依次执行&#xff0c;每一行程序都必须等待上一行的程序执行完成之后才执行&#xff0c;而异步是指在顺序执行…

YOLOv8 第Y7周 水果识别

1.创建文件夹&#xff1a; YOLOv8开源地址 -- ultralytics-main文件下载链接&#xff1a;GitHub - ultralytics/ultralytics: NEW - YOLOv8 &#x1f680; in PyTorch > ONNX > OpenVINO > CoreML > TFLite 其余文件由代码生成。 数据集下载地址&#xff1a;Frui…

使用NVM管理多个版本的node.js

1、nvm介绍&#xff1a; nvm全英文也叫node.js version management&#xff0c;是一个nodejs的版本管理工具。nvm是node.js版本管理工具&#xff0c;为了解决node.js各种版本存在不兼容现象可以通过它可以安装和切换不同版本的node.js 2、下载nvm地址&#xff1a; https://d…

Mybatis如何执行批量操作

文章目录 Mybatis如何执行批量操作使用foreach标签 使用ExecutorType.BATCH如何获取生成的主键 Mybatis如何执行批量操作 使用foreach标签 foreach的主要用在构建in条件中&#xff0c;它可以在SQL语句中进行迭代一个集合。foreach标签的属性主要有item&#xff0c;index&…

iPhone苹果手机如何将词令网页添加到苹果iPhone手机桌面快捷打开?

iPhone苹果手机如何将词令网页添加到苹果iPhone手机桌面快捷打开&#xff1f; 1、在iPhone苹果手机上找到「Safari浏览器」,并点击打开&#xff1b; 2、打开Safari浏览器后&#xff0c;输入词令官方网站地址&#xff1a;ciling.cn ; 3、打开词令官网后&#xff0c;点击Safari…

Maven的配置亲测有效

文章目录 前言一、maven网址二、操作步骤三.配置环境变量四.配置本地仓库五.找到mirror 和配置JDK六.胜利七.提醒⏰;总结 前言 &#xff08;我讲一下什么是maven&#xff0c;不想看跳到下一步就行了&#xff0c;也没必要看&#xff09; Maven&#xff08;Apache Maven&#x…