[C++] C++11新特性介绍 分析(2): lambda表达式、function包装器、bind()接口

news2024/12/23 23:59:42

|wide


文章目录

    • @[toc]
  • C++11
    • **`lambda` 表达式**
      • `lambda` 表达式
      • `lambda` 表达式底层
    • 包装器 `function`
      • `function` 包装普通可调用对象
      • `function` 包装类内成员函数
    • `bind()`
      • `bind()` 使用 及 功能
        • 1. 调整参数位置
        • 2. 绑定参数

C++11

上一篇介绍C++11常用的新特性只介绍了一部分. 本篇文章继续分析介绍.

lambda 表达式

C++11之前, 我们使用 std::sort() 接口对自定义类型进行排序 或者 设置降序排列, 需要使用 仿函数, 就像这样:

#include <iostream>
#include <iterator>
#include <vector>
#include <string>
#include <algorithm>
#include <functional>

using std::cout;
using std::endl;
using std::greater;
using std::string;
using std::vector;

struct Goods {
    string _name;  // 名字
    double _price; // 价格
    int _evaluate; // 评价
    Goods(const char* str, double price, int evaluate)
        : _name(str)
        , _price(price)
        , _evaluate(evaluate)
    {}
};

// 价格升序仿函数
struct ComparePriceLess {
    bool operator()(const Goods& gl, const Goods& gr) {
        return gl._price < gr._price;
    }
};

// 价格降序仿函数
struct ComparePriceGreater {
    bool operator()(const Goods& gl, const Goods& gr) {
        return gl._price > gr._price;
    }
};

int main() {
    int array[] = {4, 1, 8, 5, 3, 7, 0, 9, 2, 6};

    // 默认按照小于比较,排出来结果是升序
    std::sort(array, array + sizeof(array) / sizeof(array[0]));
    cout << "升序" << endl;
    for (auto e : array) {
        cout << e << " ";
    }
    cout << endl << endl;

    // 如果需要降序,需要改变元素的比较规则
    std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());
    cout << "降序" << endl;
    for (auto e : array) {
        cout << e << " ";
    }
    cout << endl << endl;

    vector<Goods> v = {
        {"苹果", 2.1, 5},
        {"香蕉", 3.1, 4}, 
        {"橙子", 2.2, 3}, 
        {"菠萝", 1.5, 4}
    };

    sort(v.begin(), v.end(), ComparePriceLess());
    cout << "价格升序" << endl;
    cout << "物品   价格   评价" << endl;
    for (auto e : v) {
        cout << e._name << "   " << e._price << "    " << e._evaluate << endl;
    }
    cout << endl;

    sort(v.begin(), v.end(), ComparePriceGreater());
    cout << "价格降序" << endl;
    cout << "物品   价格   评价" << endl;
    for (auto e : v) {
        cout << e._name << "   " << e._price << "    " << e._evaluate << endl;
    }
    cout << endl;

    return 0;
}

std::sort() 针对标准类型, 默认是按照降序排序的.

在上述代码中, 我们通过 标准中的仿函数 std::greater 可以让 std::sort 对数组升序排序.

而如果我们想要实现 对自定义类型进行排序, 就需要根据排序需求 定义相应的仿函数传给 std::sort() 然后才可以正确的排序.

上面代码的执行结果是:

|inlinecat

但是, 如果每次想要对不同自定义类型实现按需求排序, 那就 每次都需要定义一个满足需求的仿函数. 这样好像太麻烦了.

所以, C++11 引入了 lambda 表达式

lambda 表达式

什么是 lambda 表达式呢?

先来看一下效果:

// 其他部分还是 上面代码中的部分
void printGoods(vector<Goods>& v) {
    cout << "物品   价格   评价" << endl;
    for (auto e : v) {
        cout << e._name << "   " << e._price << "    " << e._evaluate << endl;
    }
    cout << endl;
}

int main() {
    vector<Goods> v = {
        {"苹果", 2.1, 5},
        {"香蕉", 3.1, 4}, 
        {"橙子", 2.2, 3}, 
        {"菠萝", 1.5, 4}
    };
    
    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
        return g1._price < g2._price; });
    cout << "价格升序" << endl;
    printGoods(v);
    
    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
        return g1._price > g2._price; });
    cout << "价格降序" << endl;
    printGoods(v);
    
    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
        return g1._evaluate < g2._evaluate; });
    cout << "评价升序" << endl;
    printGoods(v);
    
    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
        return g1._evaluate > g2._evaluate; });
    cout << "评价降序" << endl;
    printGoods(v);

    return 0;
}

这段代码的执行结果是:

|inline

我们没有实现仿函数, 但是依旧实现了按需求排序. 这就是因为使用了 lambda 表达式

上面的例子中, std::sort() 的第三个参数:

|inline

这些都是 lambda 表达式. 不过 并不完整, lambda 表达式的完整格式为:

[captureList] (parameters) mutable -> returnType { statement }
[]()mutable->xxx{ }

这些都表示什么意思呢?

  1. [capture-list]: 捕捉列表

    该列表总是出现在 lambda 表达式的开始位置, 编译器根据[]来判断接下来的代码是否为 lambda 表达式, 捕捉列表能够捕捉上下文中的变量供 lambda 表达式使用

  2. (parameters): 参数列表

    与 普通函数的参数列表 一致. 如果不需要参数传递, 则可以连同()一起省略

  3. mutable

    默认情况下,lambda 函数总是一个 const 函数, 即 它的捕捉的 值对象 可以被看作默认被 const 修饰. mutable 可以取消其常量性. 使用该修饰符时, 参数列表不可省略(即使参数为空)

  4. -->returnType: 返回值类型

    追踪返回类型形式 声明函数的返回值类型, 没有返回值时此部分可省略. 返回值类型明确情况下. 也可省略. 由编译器对返回类型进行推导

    什么是返回值类型明确的情况下?

    看一看上面的例子, 结果一定是一个 bool 所以可以省略返回值声明

    类似的情况就可以省略

  5. { statement }: 函数体

    与普通函数的编写方式相同. 但是 在此作用域内, 只能使用传入的参数 或 捕捉到的父级作用域对象.

参数列表mutable返回值类型 在特定情况下都可以省略

lambda 表达式的组成来看, 与函数非常的相似. 而实际上, lambda 表达式就是一个 匿名函数对象, 可以被称为 匿名函数, 因为它没有函数名.

且, lambda 表达式 除了可以直接匿名使用之外, 还可以这样:

int main() {
    vector<Goods> v = {
        {"苹果", 2.1, 5},
        {"香蕉", 3.1, 4}, 
        {"橙子", 2.2, 3}, 
        {"菠萝", 1.5, 4}
    };
    
    auto priceUp = [](const Goods& g1, const Goods& g2) {
        return g1._price < g2._price; };
    sort(v.begin(), v.end(), priceUp);
    cout << "价格升序" << endl;
    printGoods(v);
    
    return 0;
}

通过 auto 来对 lambda 表达式 定义一个变量. 这样 相应的 lambda 表达式 就可以通过此变量被调用.

认识了 lambda 表达式之后, 有关于 lambda 表达式的使用还有一些关于 捕捉列表 细节需要介绍.

我们使用 lambda 表达式, 尝试实现交换对象的值:

int main() {
    int a = 10, b = 20;
    cout << a << "  " << b << endl;
 
    auto lamSwap1 = [](int x, int y){
        int tmp = x;
        x = y;
        y = tmp;
    };
    auto lamSwap2 = [](int& x, int& y){
        int tmp = x;
        x = y;
        y = tmp;
    };
    lamSwap1(a, b);
    cout << "lamSwap1:: "<< a << "  " << b << endl;
    
    lamSwap2(a, b);
    cout << "lamSwap2:: "<< a << "  " << b << endl;
    
    return 0;
}

上面两个 lambda 表达式, 哪一个可以成功让 ab 交换值?

一定是 lamSwap2, 因为 传入函数内的参数 如果是临时变量改变 不会影响到原数据. 而 lamSwap2 使用的是 左值引用.

|inline

直接传参可以实现交换数据.

不过我们还可以是用另外一种写法. 使用 捕捉列表

捕捉列表 描述了上下文中哪些数据可以在 lambda表达式作用域内 使用, 以及使用的方式传值还是传引用.

  1. [var]:表示值传递方式捕捉变量var
  2. [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
  3. [&var]:表示引用传递捕捉变量var
  4. [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
  5. [this]:表示值传递方式捕捉当前的this指针

具体的使用如下:

int main() {
    int a = 10, b = 20;
    cout << a << "  " << b << endl;
 
    auto lamSwap1 = [a, b](){
        int tmp = a;
        a = b;
        b = tmp;
    };
    auto lamSwap2 = [&a, &b](){
        int tmp = a;
        a = b;
        b = tmp;
    };
    lamSwap1();
    cout << "lamSwap1:: "<< a << "  " << b << endl;
    
    lamSwap2();
    cout << "lamSwap2:: "<< a << "  " << b << endl;
    
    return 0;
}

此次的两个 lambda 表达式, 哪一个可以成功让 ab 交换值?

lamSwap2 肯定可以, 不过 lamSwap1 可以吗?

inline

很可惜, 这段代码编译无法通过, 并且 问题出在了 某个 lambda 表达式上.

lamSwap1. 我们在介绍 lambda 表达式的结构时 介绍过一个内容:

默认情况下,lambda 函数总是一个 const 函数, 即 它的捕捉的 值对象 可以被看作默认被 const 修饰. mutable 可以取消其常量性. 使用该修饰符时, 参数列表不可省略(即使参数为空)

lamSwap1 中, 我们捕捉的 [a, b] 就是 值对象. 所以, 此时 lamSwap1 内的 ab 都是被 const 修饰的值.

所以是无法修改的. 要想在内部修改, 就要使用 mutable :

auto lamSwap1 = [a, b]()mutable{
    int tmp = a;
    a = b;
    b = tmp;
};

修改之后:

|inline

可以看到, 可以编译通过. 但是 执行之后, 值并没有被交换. 因为, lamSwap1传值捕捉.

所以, 其实 mutable 这个关键词, 显得很没有用.

传值捕捉 默认const修饰, 想要修改需要使用mutable. 但是使用之后, 又因为传值捕捉, 导致无法修改实际内容. 所以 mutable 好像没有什么太大作用.

不过, 现在我们知道了 []捕捉列表 的用法. 可以捕捉 父级作用域的对象, 让其可以在 lambda 表达式内部使用.

并且, 使用 [&var] 就是传引用捕捉, [var] 就是传值捕捉.

但是, 如果想要使用父级作用域的所有对象, 如果还需要一一显式捕捉的话, 就太繁琐了

所以, C++11还设计了, 使用 [&] 就是传引用捕捉所有父级作用域内的对象, 使用 [=] 就是传值捕捉所有父级作用域内的对象

并且, C++11还设计了 混合使用. 即, 可以实现类似下面这样的捕捉:

[&, a] 			// a对象传值捕捉, 其他父级作用域内的所有对象 传引用捕捉
[=, &a]			// a对象传引用捕捉, 其他父级作用域的所有对象 传值捕捉

所以, lambda 表达式的使用, 实际上是非常灵活的.

但是, lambda 表达式 禁止重复捕捉:

[&, &a]
[&a, &a]
[=, a]
[a, a]

像类似上面的这些操作, 在某些编译器上 是会报错的:

|inline

GCC不会报错, 只会报出警告:

|wide

还有一点就是, lambda 表达式 只能捕捉父级作用域中的对象. 即 包含此 lambda 表达式的作用域


lambda 表达式底层

关于 lambda 表达式 还有一个非常重要的内容是 关于它的类型.

我们可以通过 auto 接收一个 lambda 表达式. 但是他究竟是什么呢?

lambda 表达式能不能相互赋值呢?

答案是, 不能

即使是像这样的 看起来几乎一样的 lambda 表达式, 也不能相互赋值:

int main() {
    auto f1 = []{cout << "hello world" << endl; };
    auto f2 = []{cout << "hello world" << endl; };
    f1 = f2;

    return 0;
}

|inline

但是, 可以通过 拷贝构造的形式, 创建一个 lambda 表达式的副本.

也可以, 将 lambda 表达式赋值给一个 参数和返回值类型都相同的函数指针

void (*pF)();
int main() {
    auto f1 = []{cout << "hello world" << endl; };
    
    auto f2(f1);
    f2();
    
    pF = f1;
    pF();

    return 0;
}

|inline

这就是 lambda 表达式.

这是为什么呢?

为什么会出现 这一部分现象呢? 其实是由于 lambda 的底层实现 是 仿函数.

对, 就是可以像函数一样使用的对象, 就是在类中重载了 operator()运算符 的类对象

这一点, 可以在 VS中通过反汇编代码看出来:

|inline

包装器 function

C++11 增添了 lambda 表达式之后.

C++中 可调用类型的对象就变成了三种: 函数指针, 仿函数, lambda

可以通过这三种对象 调用函数.

三种不同类型的可调用对象, 用法又非常的相似. 如果, 恰好模板有需求, 需要在不同场景 使用这三种不同类型的可调用对象, 怎么才能实现呢?

大概就像这样实现:

template<class F, class T>
T useF(F f, T x) {
	static int count = 0; 		// static 用来记录实例化出的同一个函数被执行多少次
	cout << "count:" << ++count << endl;
	cout << "count:" << &count << endl;

	return f(x);
}

double f(double i) {
	return i / 2;
}

struct Functor {
	double operator()(double d) {
		return d / 3;
	}
};

int main() {
	// 函数名
	cout << useF(f, 11.11) << endl;

	// 函数对象
	cout << useF(Functor(), 11.11) << endl;

	// lamber表达式
	cout << useF([](double d)->double{ return d / 4; }, 11.11) << endl;

	return 0;
}

useF() 模板中, 第一个模板参数是用来接收 可调用对象类型的.

当分别传入 函数名函数对象lambda 之后, 此模板会被实例化三个不同的函数:

|inline

可以发现, 在 useF() 内定义的 static 每次执行都是1, 且不同的地址.

这就说明 三次执行实例化了三个不同的 useF() 函数.

三种不同类型 传给模板, 实例化三个不同的函数, 非常的正常.

但是 这三种可调用对象的使用方式几乎一样, 通过一个模板 却需要 实例化三个不同的函数, 好像又有一点浪费.

所以, C++11 引入了 包装器

function 包装普通可调用对象

function包装器 也叫作 适配器.

C++11 之后, 设计了一个 std::function类模板, 也是一个包装器.

|inline

function 类模板:

  1. 第一个模板参数 Ret 需要传入可调用对象的返回值类型

  2. 后面则是 模板可变参数包, 需要传入 可调用对象的参数类型列表

  3. 不过, function 类模板传参 不需要向其他类模板一样, 将参数用逗号隔开

    可以直接 这样传参 function< retType(paraType1, paraType2, paraType3, ...)>

可以使用 std::function 类模板 将 不同类型的可调用对象, 包装成一个类型 即 function 类型

使用时, 需要使用 #include <functional> 包含 functional 头文件

#include <functional>

double f(double i) {
	return i / 2;
}

struct Functor {
	double operator()(double d) {
		return d / 3;
	}
};

int main() {
    // 函数名
	std::function<double(double)> func1 = f;
    cout << "未使用包装器 f 类型" << typeid(f).name() << endl;
    cout << "包装器后 类型" << typeid(func1).name() << endl << endl;

    // 函数对象
    std::function<double(double)> func2 = Functor();
    cout << "未使用包装器 Functor() 类型" << typeid(Functor()).name() << endl;
    cout << "包装器后 类型" << typeid(func2).name() << endl << endl;

    // lamber表达式
    std::function<double(double)> func3 = [](double d)->double{ return d / 4; };
    auto lam1 = [](double d)->double{ return d / 4; };
    cout << "未使用包装器 lambda 类型" << typeid(lam1).name() << endl;
    cout << "包装器后 类型" << typeid(func3).name() << endl;

	return 0;
}

这段代码的执行结果:

|inline

不用在意未使用包装器时, 究竟是什么类型. 只需要看到 使用包装器之后, 三种可调用对象的类型被统一了.

其实就是使用 不同的可调用对象 实例化了 function 对象. 都是同一个类模板, 肯定是相同的类型.

并且, 我们还可以通过 function 对象来执行 可调用对象的功能:

#include <functional>

double f(double i) {
	return i / 2;
}

struct Functor {
	double operator()(double d) {
		return d / 3;
	}
};

int main() {
    // 函数名(函数指针)
    std::function<double(double)> func1 = f;
    cout << func1(1) << endl;

    // 函数对象
    std::function<double(double)> func2 = Functor();
    cout << func2(4) << endl;

    // lamber表达式
    std::function<int(int, int)> func3 = [](const int a, const int b){ return a + b; };
    cout << func3(1, 2) << endl;

    return 0;
}

|inline

在对不同类型的 可调用对象包装之后, 可以将 function 对象, 传入给其他模板共其使用:

#include <iostream>
#include <functional>

using std::cout;
using std::endl;

template<class F, class T>
T useF(F f, T x) {
	static int count = 0; 		// static 用来记录实例化出的同一个函数被执行多少次
	cout << "count:" << ++count << endl;
	cout << "count:" << &count << endl;

	return f(x);
}

double f(double i) {
	return i / 2;
}

struct Functor {
	double operator()(double d) {
		return d / 3;
	}
};

int main() {
    // 函数名
    std::function<double(double)> func1 = f;
    cout << useF(func1, 11.11) << endl << endl;
    
    // 函数对象
    std::function<double(double)> func2 = Functor();
    cout << useF(func2, 11.11) << endl << endl;
    
    // lamber表达式
    std::function<double(double)> func3 = [](double d)->double{ return d / 4; };
    cout << useF(func3, 11.11) << endl;
}

这段代码的执行结果:

|inline

可以看出, 此时 useF 函数模板 只实例化出了一个函数. 因为, 同一个 count++、输出了 3 次

function 包装类内成员函数

function 包装器, 除了可以包装这三种基础的可调用对象之外. 还可以包装 类内的成员函数:

class Plus{
public:
    static int plusi(int a, int b) {
		return a + b;
	}

    double plusd(double a, double b) {
        return a + b;
    }
};

int main() {
    // 包装静态成员变量
    std::function<int(int, int)> func1 = Plus::plusi;
    cout << func1(1, 2) << endl;
    
    // 包赚非静态成员变量
    std::function<double(Plus, double, double)> func2 = &Plus::plusd;
    cout << func2(Plus(), 3.3, 4.4) << endl;
    
    return 0;
}

这段代码的执行结果是:

|inline

是可以正常执行相应的函数的.

不过, 通过观察代码 就可以发现, 包装静态成员函数 和 包装非静态成员函数 的方式是 存在不同的

静态成员函数, 因为 不存在隐含的this指针, 本身就可以直接通过类名调用, 比如 Plus::plusi(1, 2). 所以在 实例化 function 对象 进行包装时, 不用在参数中 传入一个 Plus 对象. 并且, 不需要取地址包装.

非静态成员函数, 由于是类内函数且非静态 第一个参数是隐含的this指针, 所以需要 通过对象调用. 所以 在进行包装时, 第一个参数是需要指定类的, 并且在使用时, 需要传入一个对象, 例子中使用了 Plus() 来传递匿名对象. 并且, 需要取地址包装.

bind()

什么是 bind() ?

|inline

C++中, bind() 是一个函数模板,它可以 接受一个可调用对象, 生成一个新的可调用对象来“适应”原对象的参数列表

什么意思呢?

直接看一个使用例子:

#include <functional>

int Sub1(int a, int b) {
    return a - b;
}

int main() {
    std::function<int(int, int)> func1 = std::bind(Sub1, std::placeholders::_1, std::placeholders::_2);
    cout << func1(4, 8) << endl;
    
    std::function<int(int, int)> func2 = std::bind(Sub1, std::placeholders::_2, std::placeholders::_1);
    cout << func2(4, 8) << endl;
    
    return 0;
}

这段代码的执行结果:

|inline

我们通过 std::bind() 绑定了 Sub1() 函数, 并将其包装.

不过, std::bind() 的参数传递, 有一些差别. 导致 包装之后的 函数的执行结果不同.

bind() 使用 及 功能

那么, bind() 的功能究竟是什么呢?

1. 调整参数位置

相信大多数人看不懂上面的例子的原因是因为 std::placeholders::_1std::placeholders::_2. 第一个参数容易理解, 就是需要 bind() 的可调用对象.

那么 std::placeholders::_n 是什么意思呢?

解释之前, 先来看一张图, 看完图或许可以直接理解:

|big

看了还是不理解. 那就要 再介绍一下 bind() 的作用了.

接受一个可调用对象, 生成一个新的可调用对象来“适应”原对象的参数列表

bind() 的第一个参数, 就是接受的可调用对象. 而 后面的 std::placeholders::_n 其实表示的是 可调用对象对应的参数

std::placeholders::_1, 表示 可调用对象的第一个参数

std::placeholders::_2, 表示 可调用对象的第二个参数

std::placeholders::_3, 表示 可调用对象的第三个参数, 以此类推.

那么在上述例子中:

  1. std::bind(Sub1, std::placeholders::_1, std::placeholders::_2)

    _1 表示Sub1()的第一个参数, _2 表示Sub1()的第一个参数

    那么, bind() 执行之后, 获得的函数就是 Sub1(int a, int b), 函数体不变.

  2. std::bind(Sub1, std::placeholders::_2, std::placeholders::_1)

    同样的, _1 表示Sub1()的第一个参数, _2 表示Sub1()的第一个参数

    那么, 此次 bind() 执行之后, 获得的函数就是 Sub1(int b, int a), 函数体不变.

Sub1() 的函数体是 reutn b - a;

当我们执行 func1(4, 8), func1() <==> Sub1(int a, int b), 所以 结果是 8 - 4 = 4

当我们执行 func2(4, 8), func2() <==> Sub2(int b, int a), 所以 结果是 4 - 8 = -4

所以, std::placeholders::_n 强绑定了 所接受的可调用对象的参数的位置. bind() 后面的参数, 表示了 bind() 执行之后的可调用对象的参数位置

2. 绑定参数

类的非静态成员函数, 因为第一个参数默认为this指针 且无法更改. 所以 在包装时, 需要将包装的函数的第一个参数类型 指明为类. 且在使用时需要传入一个对象.

bind() 的作用是 接受一个可调用对象, 生成一个新的可调用对象来“适应”原对象的参数列表

所以, 我们还可以使用 bind(), 将 类的对象 绑定在类的成员函数上, 以便之后使用时 不在手动传入对象:

#include <functional>

class Sub {
public:
    int sub(int a, int b) {
        return a - b;
    }
};

int main() {
    Sub s;
    // 绑定成员函数
    std::function<int(int, int)> func = std::bind(&Sub::sub, s, placeholders::_1, placeholders::_2);
	cout << func(3, 4) << endl;
    
    return 0;
}

这段代码的执行结果:

|inline

std::bind(&Sub::sub, s, placeholders::_1, placeholders::_2) 通过 在 this 指针的位置 直接传入一个对应类对象.

包装时 就不用在相应位置指定类, 包装之后, 使用包装完成的对象执行可调用对象, 也不用再显式传入一个类对象.

这 才是 bind() 真正常用的功能. 调整参数位置 相应的没有那么常用.

而且, 通过此功能还可以了解到, std::placeholders::_n 占位符 是不包括 this 指针的.

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

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

相关文章

Linux简介与安装

文章目录 前言一、Linux简介1.Linux是什么2.学完Linux后能做什么 二、Linux安装1.安装方式介绍2.安装Linux3.网卡设置4.安装SSH连接工具5. Linux目录结构 总结 前言 为了巩固所学的知识&#xff0c;作者尝试着开始发布一些学习笔记类的博客&#xff0c;方便日后回顾。当然&…

括号生成(力扣)递归 JAVA

目录 题目描述&#xff1a;纯递归解法&#xff1a;递归 回溯&#xff1a; 题目描述&#xff1a; 数字 n 代表生成括号的对数&#xff0c;请你设计一个函数&#xff0c;用于能够生成所有可能的并且 有效的 括号组合。 示例 1&#xff1a; 输入&#xff1a;n 3 输出&#xff1a…

《手把手教你》系列基础篇之1-python+ selenium自动化测试-环境搭建(详细)

1.环境搭建 基于python3和selenium3做自动化测试&#xff0c;俗话说&#xff1a;工欲善其事必先利其器&#xff1b;没有金刚钻就不揽那瓷器活&#xff0c;磨刀不误砍柴工&#xff0c;因此你必须会搭建基本的开发环境&#xff0c;掌握python基本的语法和一个IDE来进行开发&…

modelscope魔塔初探--TTS

官网 step1 可以选择指定模型&#xff0c;对于多情感的模型&#xff0c;还可以通过标签实现语气情感 from modelscope.outputs import OutputKeys from modelscope.pipelines import pipeline from modelscope.utils.constant import Taskstext <speak><emotion …

Springboot实现热部署

目录 1、问题阐述 2、实现方式 3、开始配置 3.1在pom.xml中添加依赖 3.2devtools配置 3.3修改IDEA配置 3.4测试一下 1、问题阐述 在实际项目开发过程中&#xff0c;每次修改代码就得将项目重启&#xff0c;重新部署&#xff0c;对于一些大型应用来说&#xff0c;重启时…

特征选择算法 | Matlab实现基于ReliefF特征选择算法的回归数据特征选择 ReliefF

文章目录 效果一览文章概述部分源码参考资料效果一览 文章概述 特征选择算法 | Matlab实现基于ReliefF特征选择算法的回归数据特征选择 ReliefF 部分源码 %--------------------

五分钟就可以安装MySQL

目录 ⛈️一.什么是MySQL ⛈️二.为什么要使用MySQL ⛈️三.MySQL有什么优点 ⛈️四.官网&#xff1a; ⛈️五.下载 ⛈️六.安装 ⛈️七.查看 ⛈️八.修改密码 一.什么是MySQL MySQL是一种开源的关系型数据库管理系统&#xff08;RDBMS&#xff09;&#xff0c;它使用结…

【Python编程系列】1、Python安装

Python安装 1、下载安装包 官网地址:https://www.python.org/ 进入后,在Downloads菜单下选择python运行的系统环境: 以Windows系统为例,进入后,选择合适的版本下载: 2、安装Python软件包 双击可执行文件exe: 弹出安装窗口后: 我们一般选择"Install Now"的…

Linux 配置dns覆盖默认127.0.0.53

Linux dns默认127.0.0.53&#xff0c;在/etc/resolve.conf中存在 nameserver 127.0.0.53&#xff0c;手动修改无果&#xff0c;每次重启依旧127.0.0.53&#xff0c;因为这是系统生成的文件&#xff0c;resolvectl命令来查dns的配置。 要修改dns&#xff0c;先暂停dns服务&…

6 JSR303校验

6.1 加入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId> </dependency> 6.2 在实体类上加注解 在实体类上加入Validated注解。并且在属性上方加入Emall(messag…

Selenium自动化测试技巧

目录 前言&#xff1a; Selenium自动化 跨浏览器测试中的Selenium 利用正确的定位器 数据驱动的测试 不要依赖特定的驱动程序 选择器顺序 使用PageObjects设计模式 提倡wait避免sleep 关闭Firebug起始页 前言&#xff1a; Selenium是一个广泛使用的自动化测试框架&a…

Android11 Settings加载流程

一、系统设置首页(一级菜单)&#xff1a; 1、Settings 之所以要在此定义空的Activity&#xff0c;是为了外部应用能直接跳转到XX_SettingsActivity界面&#xff0c;因为如果只是fragment的话&#xff0c;外部是没法跳转到fragment界面的&#xff0c;跳转到XX_SettingsActivit…

【无公网IP】在外Windows远程连接MongoDB数据库

文章目录 前言1. 安装数据库2. 内网穿透2.1 安装cpolar内网穿透2.2 创建隧道映射2.3 测试随机公网地址远程连接 3. 配置固定TCP端口地址3.1 保留一个固定的公网TCP端口地址3.2 配置固定公网TCP端口地址3.3 测试固定地址公网远程访问 转载自cpolar极点云文章&#xff1a;公网远程…

Camera API1 使用说明

Camera API2 使用说明 目录 一、开启相机 1.1创建项目 1.2注册权限 1.3配置相机特性要求 1.4 获取摄像头的个数 1.5 根据 ID 获取 CameraInfo facing 1.6 开启相机 1.7 关闭相机 二、预览 2.1认识 Parameters 2.2 设置预览尺寸 2.3添加预览 Surface 2.4 开启和关…

高级篇九、性能分析工具的使用

目录 2、查看系统性能参数3、统计SQL的查询成本&#xff1a;last_query_cost4、 定位执行慢的 SQL&#xff1a;慢查询日志4.1 开启慢查询日志参数4.2 查看慢查询数目4.3 案例演示 2、查看系统性能参数 在MySQL中&#xff0c;可以使用 SHOW STATUS 语句查询一些MySQL数据库服务…

【C++】STL——list介绍及使用

&#x1f680; 作者简介&#xff1a;一名在后端领域学习&#xff0c;并渴望能够学有所成的追梦人。 &#x1f681; 个人主页&#xff1a;不 良 &#x1f525; 系列专栏&#xff1a;&#x1f6f8;C &#x1f6f9;Linux &#x1f4d5; 学习格言&#xff1a;博观而约取&#xff0…

安装Anaconda

一、Anaconda简介 Anaconda&#xff0c;一个开源的Python发行版本&#xff0c;可用于管理Python及其相关包&#xff0c;包含了conda、Python等180多个科学包及其依赖项。当我们需要不同的Pytorch版本的时候&#xff0c;不需要卸载重新安装&#xff0c;可以通过Anaconda创建不同…

短视频抖音账号矩阵系统源码---功能架构示例1

一、短视频账号矩阵系统源码开发服务器集群化处理开发成本更低&#xff08;前提&#xff09; 什么是集群化处理视频内存内耗&#xff1f;集群化处理视频内存内耗是指通过建立集群系统&#xff0c;将视频处理任务分配给多个计算节点进行并行处理&#xff0c;以减少单个计算节点…

氨基酸中间体35309-53-6,cyclo(Asp-Asp),CYCLO(-天冬氨酸-ASP)

&#xff08;文章资料汇总来源于&#xff1a;陕西新研博美生物科技有限公司小编MISSwu&#xff09;​ 试剂基团反应特点&#xff08;Reagent group reaction characteristics&#xff09;&#xff1a; cyclo(Asp-Asp)&#xff0c;35309-53-6&#xff0c;一种氨基酸中间体&…

平衡搜索二叉树——AVL树

AVL树 1. AVL树的概念2. AVL树节点的定义3. AVL树的插入思路4. AVL树的平衡调整思路平衡因子更新思路LL型——右单旋RR型——左单旋LR型——左右旋RL型——右左旋 5. AVL树插入判断平衡调整类型6. AVL树插入的代码实现7. AVL树总结8. AVL树的验证9. AVL树的性能 1. AVL树的概念…