C++11:类的新功能和可变参数模板

news2024/11/14 9:08:09

文章目录

  • 1. 新增默认成员函数
    • 1.1 功能
    • 1.2 示例
  • 2. 类成员变量初始化
  • 3. 新关键字
    • 3.1 关键字default
    • 3.2 关键字delete
      • 补充
    • 3.3 关键字final和override
  • 4. 可变参数模板
    • 4.1 介绍
    • 4.2 定义方式
    • 4.3 展开参数包
      • 递归展开参数包
        • 优化
      • 初始化列表展开参数包
      • 逗号表达式展开参数包
        • 补充
  • 5. emplace接口
    • 5.1 区别
    • 5.2 使用方式
    • 5.3 原理
    • 5.4 意义

1. 新增默认成员函数

在C++11之前,一个类有6个默认成员函数,即构造函数、析构函数、拷贝构造函数、拷贝赋值函数、取地址重载函数和const取地址重载函数。

C++11新增了两个默认成员函数:

  • 移动构造函数
  • 移动赋值重载函数

而编译器默认生成的情况并非像其他6个默认成员函数一样,单纯未实现移动构造函数或移动赋值重载函数。编译器会在以下所有条件都满足时生成隐式的移动构造函数和移动赋值运算符:

  • 没有声明拷贝构造函数或拷贝赋值重载函数。
  • 没有声明移动构造函数或移动赋值重载函数。
  • 没有声明析构函数。

即:如果已经显式地声明移动构造函数或移动赋值重载函数,而没有实现拷贝构造函数或拷贝赋值函数,编译器也不会生成移动构造函数和移动赋值重载函数。

1.1 功能

默认生成的移动构造函数和移动赋值重载函数的功能是:

  • 移动构造函数:
    • 内置类型成员:值拷贝(浅拷贝);
    • 自定义类型成员:如果该成员实现了移动构造就调用它的移动构造,否则调用它的拷贝构造。
  • 移动赋值重载函数:
    • 内置类型成员:值拷贝(浅拷贝);
    • 自定义类型成员:如果该成员实现了移动赋值就调用它的移动赋值,否则调用它的拷贝赋值。

1.2 示例

下面用一个例子验证,首先实现一个简易的string类:

#include <iostream>
#include <string>
#include <cstddef>
using namespace std;

namespace xy
{
    class string
    {
    public:
        // 构造函数
        string(const char* str = "")
        {
            _size = strlen(str);
            _capacity = _size;
            _str = new char[_capacity + 1];
            strcpy(_str, str);
        }
        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)
                , _size(0)
                , _capacity(0)
        {
            cout << "string(const string& s) -- 深拷贝" << endl;

            string tmp(s._str);
            swap(tmp);
        }
        // 移动构造
        string(string&& s)
                :_str(nullptr)
                , _size(0)
                , _capacity(0)
        {
            cout << "string(string&& s) -- 移动构造" << endl;
            swap(s);
        }
        // 拷贝赋值运算符重载
        string& operator=(const string& s)
        {
            cout << "string& operator=(const string& s) -- 深拷贝" << endl;

            string tmp(s);
            swap(tmp);
            return *this;

        }
        // 移动赋值
        string& operator=(string&& s)
        {
            cout << "string& operator=(string&& s) -- 移动赋值" << endl;

            string tmp(s);
            swap(tmp);
            return *this;

        }
        void push_back(char ch)
        {
            if (_size == _capacity)
            {
                reserve(_capacity == 0 ? 4 : _capacity * 2);
            }
            _str[_size] = ch;
            _str[_size + 1] = '\0';
            _size++;
        }
        // +=运算符重载
        string& operator+=(char ch)
        {
            push_back(ch);
            return *this;
        }
        void reserve(size_t n)
        {
            if (n > _capacity)
            {
                char* tmp = new char[n + 1];
                strncpy(tmp, _str, _size + 1);
                delete[] _str;
                _str = tmp;
                _capacity = n;
            }
        }
        //析构函数
        ~string()
        {
            _str = nullptr;
            _size = 0;
            _capacity = 0;
        }
        // 其他接口...
    private:
        char* _str;
        size_t _size;
        size_t _capacity;
    };
}

然后再实现一个简易的Person类,其中name成员的类型是上面实现的xy::string。

class Person
{
public:
    // 构造函数
    Person(const char* name = "", int age = 0)
            :_name(name)
            , _age(age)
    {}
    // 拷贝构造函数
    Person(const Person& p)
            :_name(p._name)
            , _age(p._age)
    {}
    // 拷贝赋值函数
    Person& operator=(const Person& p)
    {
        if (this != &p)
        {
            _name = p._name;
            _age = p._age;
        }
        return *this;
    }
    // 析构函数
    ~Person()
    {}
private:
    xy::string _name; // 姓名
    int _age;         // 年龄
};

其中,Person类只实现了拷贝构造函数、拷贝赋值函数和析构函数,并未实现移动构造函数和移动赋值重载函数。

现在,假如要用一个右值构造s2对象,编译器会为Person类生成一个默认的移动构造函数吗?

int main()
{
    Person s1("小明", 18);
    Person s2 = std::move(s1);

    return 0;
}

输出:

string(const string& s) -- 深拷贝

输出结果表明,编译器是调用Person的拷贝构造函数构造的s2对象,进而调用string的拷贝构造函数对name成员初始化,而并非生成一个默认的移动构造函数。

想让编译器为Person类生成默认移动构造函数,就需要将Person类中的拷贝构造、拷贝赋值和析构函数删去,再用右值构造对象时,编译器就会生成并调用Person类的默认移动构造函数。

注释掉要删去的函数,同样的测试代码的输出结果:

string(string&& s) -- 移动构造

虽然看起来生成默认移动构造函数和移动赋值重载函数的条件苛刻,但实际上析构函数和拷贝构造函数、赋值重载函数(需要析构,说明有资源被用来构造)都是成对出现的。实际上移动构造都是要自己写的,而且移动赋值的规则也是类似的。对于深拷贝的类,必须要自己写移动构造,因为拷贝的成本太大了。但是浅拷贝可以不用自己写。

2. 类成员变量初始化

默认构造函数只会针对自定义类型调用其构造函数初始化,而不会对内置类型处理,C++11允许在变量声明时赋予缺省值(非静态)。

class Person
{
private:
    string _name = "小明";
    int _age = 18;
    static int _s;
};

其中_s是静态成员变量,只能在类的外部初始化。

注意:在声明成员变量时,赋予缺省值并不是初始化。

3. 新关键字

3.1 关键字default

C++11关键字default是一种新的函数声明方式,它可以让编译器为某些特殊成员函数生成默认的实现。例如,如果你想要一个默认的构造函数或拷贝构造函数,你可以在函数声明后面加上=defaut。这样做有以下好处:

  • 可以避免不必要的初始化或赋值操作
  • 可以让类具有合成版本的特殊成员函数的特性,例如平凡性、可移动性等
  • 可以提高代码的可读性和一致性

使用default关键字强制生成某个默认成员函数,如构造函数:

class T
{
public:
    T() = default; // 强制生成构造函数
    // 构造函数
    T(const int t)
        : _t(t)
    {}
private:
    int _t;
};

除了构造函数之外,其他默认成员函数都可以用default关键字强制生成。

3.2 关键字delete

C++11关键字delete是一种新的函数声明方式,它可以让你禁用某些特殊成员函数或自定义函数。例如,如果你想要禁止一个类被拷贝或赋值,你可以在拷贝构造函数和赋值运算符后面加上=delete。这样做有以下好处:

  • 可以防止不合理或危险的操作
  • 可以提高编译时的错误检测
  • 可以避免不必要的代码生成

一般将被=delete修饰的函数称为删除函数。

禁止一个类被拷贝或赋值,你可以在拷贝构造函数和赋值运算符后面加上=delete

class T
{
public:
    // 构造函数
    T(const int t)
            : _t(t)
    {}
private:
    T(const T& t) = delete;
    T& operator=(const T&) = delete;
private:
    int _t;
};

还可以:假设你有一个类A,它有一个虚函数f(),你想要禁止它的子类B和C重写这个函数。你可以在A中声明f()=delete,这样B和C就不能再定义自己的f()了。代码如下:

class A 
{
public:
  virtual void f() = delete; // 禁用f()
};

class B : public A 
{
public:
  void f() override {} // 编译错误,不能重写被删除的函数
};

class C : public A 
{
public:
  void f() {} // 编译错误,不能定义同名的函数
};

补充

关于delete关键字,还有以下用途:

  • 释放程序动态申请的内存空间,例如 delete ptr; delete[] arr;
  • 删除对象的某个属性,例如 delete obj.name; delete obj['name'];
  • 删除数组的某个元素,例如 var arr = [1, 2, 3]; delete arr[0];

3.3 关键字final和override

  • override是C++11中的一个关键字,它用来修饰派生类中重写的虚函数,表示该函数确实是要重写基类的虚函数。这样可以避免因为函数签名不匹配而导致的重写失败或者隐藏基类函数的问题。使用方法如下:
class Base
{
    public:
    virtual void foo();
    virtual void bar() final; // 基类中用final修饰的虚函数不能被派生类重写
};

class Derived : public Base
{
    public:
    void foo() override; // 正确,重写了基类的虚函数
    void bar() override; // 错误,试图重写被final修饰的虚函数
};
  • final:阻止类的进一步派生和虚函数的进一步重写,例如:
// 1. final修饰类
class A final 
{ 
public:
  virtual void f() {} // 阻止A被继承
};

class B : public A 
{ 
public:
  void f() override {} // 错误,A是final类
};
// 2. final修饰虚函数
class C 
{
public:
  virtual void g() final {} // 阻止g被重写
};

class D : public C
{
public:
  void g() override {} // 错误,g是final函数
};

delete关键字的应用:

使用关键字delete定义一个只能在堆上(栈上)生成对象的类。

  • 只能在堆上生成对象的类:将构造函数和析构函数设为私有或delete析构函数,然后提供一个静态成员函数来调用newdelete操作符,如下:
class HeapOnly
{
public:
    HeapOnly() // 构造函数 
    {
        _str = new char[5];
    } 
    ~HeapOnly() = delete; // delete析构函数

    void Destory()
    {
        delete[] _str;
        operator delete(this);
    }
private:
    char* _str;
};
int main()
{
    HeapOnly* ptr = new HeapOnly;
    
    ptr->Destory();
    return 0;
}

这段代码定义了一个名为HeapOnly的类,它只能在堆上创建对象,不能在栈上创建对象。它的构造函数分配了一个长度为5的字符数组,并把指针赋给_str成员变量。它的析构函数被delete修饰符删除了,这意味着它不能被隐式调用,也就是说,当对象离开作用域时,不会自动调用析构函数释放资源。

因此,这个类提供了一个名为Destory的成员函数,用来手动释放资源和删除对象。这个函数先调用delete[]操作符释放_str指向的字符数组,然后调用operator delete(this)来释放对象占用的内存。

在main函数中,使用new操作符在堆上创建了一个HeapOnly类型的对象,并把指针赋给ptr变量。然后调用ptr->Destory()来销毁对象。它用指针ptr接受new出来的对象是为了能够访问对象的成员函数和变量。如果不用指针,就无法操作对象。因为这个类的析构函数被delete了,所以不能用引用或者值来接受对象,否则会导致编译错误。

链接:https://blog.csdn.net/niaolianjiulin/article/details/76165609

4. 可变参数模板

4.1 介绍

可变参数模板是指一种可以定义0到任意个模板参数的模板,它可以用来实现高度泛化的函数或类。可变参数模板的语法是使用省略号(…)来表示一个或多个参数,例如:

template<class ...Args> 
返回类型 函数名(Args... args) 
{ 
    //函数体 
}

template<class ...Types> 
class 类名 
{ 
    //类体 
}

本文主要就函数模板可变参数进行介绍。

  • 可变参数模板需要使用递归的方式来展开和处理参数。可以使用sizeof…操作符来获取可变参数的个数。

  • 第一行:模板参数Args(这个名字可以任意)前面有省略号,表示它是一个可变模板参数,我们把带省略号的参数称为参数包,参数包里面包含0到N(N ≥ 0)个模板参数

  • 第二行:args是一个函数形参参数包。

可变参数是指一种可以接受不同数量和类型的参数的方法或函数。一个熟悉的函数的参数就是可变参数:printf。它不仅可以接受不同数量的参数,还能接受不同类型的参数,它的底层是数组实现的(在此不讨论底层实现)。模版参数类似,只不过穿的不是对象,而是一个模板。

4.2 定义方式

可变参数模板需要使用递归的方式来展开和处理参数。可以使用sizeof…操作符来获取可变参数的个数。

例如:

#include <iostream>
using namespace std;
template<class ...Args>
void ShowList(Args... args)
{
    cout << sizeof...(args) << endl;
}

int main()
{
    ShowList();
    ShowList(1, "11");
    ShowList("11", 1);
    ShowList("11", 1, '1');

    return 0;
}

输出:

0
2
2
3

注意sizeof的格式是sizeof...(args)

4.3 展开参数包

C++11语法并未支持直接展开参数包,例如这样是不被语法支持的:

template<class ...Args>
void ShowList(Args... args)
{
    for (int i = 0; i < sizeof...(args); i++)
    {
        cout << args[i] << " "; // 错误
    }
    cout << endl;
}

展开函数的参数包,有以下几种方式:

  • 递归
  • 逗号表达式
  • 初始化列表

递归展开参数包

递归展开参数包,利用函数模板的重载和特化,将参数包中的每个元素依次提取出来并处理。要使用递归展开参数包,需要定义一个基本情况和一个递归情况,分别处理空参数包和非空参数包。例如:

// 基本情况:当参数包为空时,调用这个函数,终止递归
void ShowList() 
{
    cout << endl;
}

// 递归情况:当参数包非空时,调用这个函数
template<class T, class... Args>
void ShowList(T head, Args... rest) 
{
    cout << head << " ";
    ShowList(rest...); // 递归调用
}

image-20230228151454809

int main()
{
    ShowList();
    ShowList(1, "11", 'A');

    return 0;
}

输出:

1 11 A 

其中,无参的递归函数表示终止状态,即无参。理解递归展开参数包的核心是知道模板参数T每次都会取到1个参数包Args中第一个参数,rest就是剩下的参数。

注意,这个递归不是运行时进行的,因为是模板参数,所以它是编译时进行的。

优化

终止的递归函数和展开递归函数分开不是很美观,可以考虑把它们包在一起,另外用一个函数调用它们:

void ShowListArg()
{
    cout << endl;
}

template<class T, class... Args>
void ShowListArg(T head, Args... rest)
{
    cout << head << " ";
    ShowListArg(rest...);
}
// 供外部调用
template<class ...Args>
void ShowList(Args... args)
{
    ShowListArg(args...);
}

注意,不可以使用sizeof判断参数包中参数个数,因为模板的推演是编译时运行,是在每次递归的函数接收到参数包以后才能知道参数包中的个数,所以不能用这样的方式终止递归:

if (sizeof...(args) == 0)
{
    return;
}

其中的if判断语句是编译后生成二进制文件后机器才会执行的语句,即运行时(runtime),函数模板的推演则是编译时(compile-time)。

初始化列表展开参数包

数组可以用列表初始化:

int a[] = {1,2,3,4};

如果参数包中的类型都相同且在其外部是明确的,那么也可以用类似的方式用参数包初始化数组,遍历数组即可展开参数包:

template<class ...Args>
void ShowList(Args... args)
{
    int arr[] = { args... }; // 列表初始化

    for (auto e : arr)
    {
        cout << e << " "; // 打印
    }
    cout << endl;
}
int main()
{
    ShowList(1);
    ShowList(1, 2);
    ShowList(1, 2, 3);

    return 0;
}

输出:

1 
1 2 
1 2 3

但是,C++中的数组是相同类型元素的集合,且不能传入0个参数,所以使用初始化列表展开参数包有很大的局限性,需要搭配逗号表达式展开参数包。

逗号表达式展开参数包

逗号表达式是一种用逗号运算符连接两个或多个表达式的语法,它的运算过程是从左到右依次计算每个表达式,并且整个逗号表达式的值为最后一个表达式的值(并返回)。例如,a = 3 + 5, b = 6 + 8是一个逗号表达式,它会先计算a = 3 + 5,然后计算b = 6 + 8,并且整个逗号表达式的值为14(即b = 6 + 8的值)。

例如,下面的代码定义了一个函数模板,它可以接受任意数量和类型的参数,并将它们打印出来:

template<class T>
void PrintArg(T t)
{
    cout << t << " ";
}

template<class... Args>
void ShowList(Args... args)
{
    int arr[] = { (PrintArg(args), 0)... };
    cout << endl;
}
int main()
{
    ShowList(1);
    ShowList(1, 2);
    ShowList(1, 2, 3);

    return 0;
}

输出:

1 
1 2 
1 2 3 
  • 返回0,形成了一个由0组成的初始化列表,确保逗号表达式返回的是一个整型值,以符合数组元素的类型,此步骤是为了符合语法。
  • 将打印功能封装为一个函数,并作为逗号表达式中的第一个表达式,这样逗号表达式就会从左到右先调用函数。然后将最后一个整型值作为返回值初始化整型数组。
  • 初始化列表会按照顺序执行每个逗号表达式,并丢弃其结果。这样就实现了对参数包中每个元素的操作。

注意:

可变参数的省略号要加在逗号表达式外面,表示需要将逗号表达式展开。如果将省略号加在args的后面,那么参数包将会被展开之后再整体传入PrintArg函数,代码中的{(PrintArg(args), 0)...}将会展开为{(PrintArg(arg1), 0), (PrintArg(arg2), 0)等等...}

此时已经可以传入不同类型的模板参数,但是依然不能传入0个参数,原因是数组长度不为0,可以增加一个无参版本的ShowList以供匹配。

// 无参调用
void ShowList()
{
	cout << endl;
}

补充

初始化列表展开参数包最大的问题就是不能传入不同类型的模版参数,原因是数组类型必须是相同的,但实际上传入的模板参数类型与数组元素类型之间并没有制约关系,所以将打印函数的返回值改为数组元素类型(例如int),即可满足语法,但个人认为这样做有点怪。

// 无参调用
void ShowList()
{
    cout << endl;
}
// 打印函数
template<class T>
int PrintArg(T t)
{
    cout << t << " ";

    return 0;   // 返回整型
}

template<typename... Args>
void ShowList(Args... args)
{
    int arr[] = { PrintArg(args)... };
    cout << endl;
}
int main()
{
    ShowList(1);
    ShowList(1, 2);
    ShowList(1, 2, 3);

    return 0;
}

输出:

1 
1 2 
1 2 3 

5. emplace接口

STL的emplace(动词,安放)接口是C++11标准新增加的一组成员函数,用于在容器中构造而不是拷贝元素,从而提高插入性能和避免内存拷贝和移动。不同的容器有不同的emplace接口,例如vector有emplace()和emplace_back(),map有emplace()和emplace_hint()等。

注意:emplace版本的插入接口支持模板的可变参数,参数类型都带有&&,表示万能引用,而不是右值引用,即可以传入左值对象或者右值对象。

5.1 区别

例如vector的emplace_back(),对应的是push_back(),它们使用方式类似,不同之处:

  • emplace_back()可以直接在vector的末尾构造元素,而不需要创建临时对象或者移动或拷贝元素,这样可以节省内存和提高效率。
  • emplace_back()可以自动进行类型转换,而push_back()需要显式地调用构造函数。
  • emplace_back()和push_back()都可以传入左值或右值对象,前者不能使用初始化列表而后者可以。
  • emplace_back()可以接受多个参数,即可以传入用于构造元素的参数包,而push_back()只能接受一个对象或者一个右值引用。

5.2 使用方式

#include <vector>
int main()
{
    vector<pair<int, string>> v;
    pair<int, string> kv(2, "world");

    v.emplace_back(make_pair<int, string>(1, "hello")); // 右值
    v.emplace_back(kv); // 左值
    v.emplace_back(3, "!!"); // 参数包

    for(auto e: v)
    {
        cout << e.second << " ";
    }
    cout << endl;
    return 0;
}

输出:

hello world !! 

为了使用参数包,vector的元素是pair。

5.3 原理

emplace接口直接在容器中构造新元素(类型可能是一个对象),而不进行传值或传引用操作,进而避免拷贝或移动带来的开销。

  1. 首先通过空间配置器获取内存空间,不调用构造函数初始化;
  2. 然后调用allocator_traits::construct函数对这块空间进行初始化,通过完美转发,接收空间的地址和用户传入的参数;
  3. allocator_traits::construct函数中使用定位new调用构造函数初始化这块空间,通过完美转发接收用户传入的参数;
  4. 将初始化好的新元素插入到容器中。

emplace接口的可变模板参数类型是万能引用,即左值引用、右值引用和参数包都能接收,对于不同类型的参数:

  • 参数是左值:首先调用构造函数实例化出一个左值对象,然后用定位new调用构造函数初始化申请的空间时,匹配拷贝构造函数
  • 参数是右值:首先调用构造函数实例化出一个右值对象,然后用定位new调用构造函数初始化申请的空间时,匹配移动构造函数
  • 参数是参数包:直接调用函数插入,用定位new调用构造函数初始化申请的空间时,匹配构造函数

注意:一般需要深拷贝的类才需要移动构造,否则使用左值和右值在效率上是没有区别的,都是调用一个构造函数和一次拷贝构造函数。

关于定位new:

定位new是一种特殊的new表达式,它可以在已分配的原始内存空间中调用构造函数初始化一个对象。这样可以避免常规new申请空间的开销,或者满足一些特殊的需求,比如内存池或硬件访问。

5.4 意义

emplace接口(统称)最大的意义就是它支持传入参数包,直接在容器的内部根据参数包构造新元素,这样就能减少一次拷贝或移动资源带来的开销。参数包就像一张图纸,C++11之前STL的插入接口都是在仓库门口(容器)卸货(元素),现在有了参数包,直接用图纸在仓库里建厂。要知道,可变参数模板的参数包的大小是由传入的实参个数决定的,远比拷贝或资源转移带来的开销小。

当一个类是深拷贝时,emplace接口的意义重大,当其他情况时,效率和普通的push接口相差不大。

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

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

相关文章

华为OD机试用Python实现 -【报数游戏】2023Q1 A卷

华为OD机试题 本篇题目:报数游戏题目输入输出示例 1输入输出示例 2输入输出Code代码编写思路最近更新的博客 华为od 2023 | 什么是华为od,od 薪资待遇,od机试题清单华为OD机试真题大全,用 Python 解华为机试题 | 机试宝典【华为OD机试】全流程解

会声会影2023旗舰版新功能介绍,Corel VideoStudio Ultimate2023以及电脑系统配置要求

会声会影2023中文旗舰版功能非常强大的视频编辑软件&#xff0c;非常专业的使用效果&#xff0c;会声会影2023中文版可以针对剪辑电影进行使用&#xff0c;非常强大的色彩校正方式&#xff0c;无论什么光线下进行拍摄&#xff0c;都可以通过后期进行调整&#xff0c;并且里面超…

Python SEO采集海量文本标题,用倒排索引找出“类似的标题“代码实现

Python SEO采集海量文本标题,用倒排索引找出“类似的标题“代码实现 作者:虚坏叔叔 博客:https://xuhss.com 早餐店不会开到晚上,想吃的人早就来了!😄 一、说明 假设这个是采集到的海量文本标题: 现在要判断找到的这个标题 title = "拜登称特朗普拒绝承认选举…

C语言实验小项目实例源码大全订票信息管理系统贪吃蛇图书商品管理网络通信等

wx供重浩&#xff1a;创享日记 对话框发送&#xff1a;c项目 获取完整源码源文件视频讲解环境资源包文档说明等 包括火车订票系统、学生个人消费管理系统、超级万年历、学生信息管理系统、网络通信编程、商品管理系统、通讯录管理系统、企业员工管理系统、贪吃蛇游戏、图书管理…

再见 ETHDenver 2023

我们来一起回顾Web3中规模最大&#xff0c;持续时间最长的以太坊史诗级建造周我们正在庆祝#YearoftheSpork&#xff0c;并借助 Web3 中最大的以太坊社区活动之一拉开了黑客马拉松赛季的序幕。ETH Denver 旨在围绕一个共同的目标聚集了志同道合的人&#xff0c;我们非常高兴今年…

开学新装备 - 学生党是否该入手 MacBook

学生党是否该入手 macbook 这个问题&#xff0c;相信许多人在许多社区都有看到过类似讨论。只不过&#xff0c;许多讨论都掺杂了信仰、智商税、不懂、不熟悉未来需求等各种因素&#xff0c;导致内容空洞价值不大。这篇文章&#xff0c;抛开了所有非理性因素&#xff0c;详细的告…

创建第一个QT程序

系列文章目录 QT学习与实战 创建第一个QT程序系列文章目录一、创建第一个QT程序1.1Location(项目简介和位置)1.2Kits(构建套件)1.3Details(类的信息)1.4汇总二、常用操作2.1显示文件分类2.2代码分栏三、代码分析四、总结一、创建第一个QT程序 1.1Location(项目简介和位置) 创…

2.进程和线程

1.进程1.1 终止正常退出(自愿)出错退出(自愿)严重错误(非自愿)被其他进程杀死(非自愿)1.2 状态就绪态&#xff1a;可运行&#xff0c;但因为其他进程正在运行而暂时停止阻塞态&#xff1a;除非某种外部事件发生&#xff0c;否则进程不能运行1.3 实现一个进程在执行过程中可能被…

淘宝十年资深架构师吐血总结淘宝的数据库架构设计和采用的技术手段。

淘宝十年资深架构师吐血总结淘宝的数据库架构设计和采用的技术手段。 文章目录淘宝十年资深架构师吐血总结淘宝的数据库架构设计和采用的技术手段。本文导读1.分库分表2.数据冗余3.异步复制4.读写分离总结本文导读 淘宝的数据库架构设计采用了分布式数据库技术&#xff0c;通过…

MES系统消除制造型企业的九大浪费!

在生产制造型企业&#xff0c;正确地减少不必要的浪费才是降低生产成本、提升企业利润的关键&#xff01; 许多制造型企业的管理者&#xff0c;尤其是中层管理者没有认识到在生产管理过程中哪些行为是在真正提升企业效益、哪些行为是给企业制造浪费。 对于传统的浪费有过量生…

基于Hyperledger Fabric的学位学历认证管理系统

基于Hyperledger Fabric的学位学历认证管理系统 项目源码&#xff1a;https://github.com/Pistachiout/Academic-Degree-BlockChain 一、选题背景 学历造假、认证造假等是一个全球日益普遍的现象&#xff0c;不仅对社会产生了巨大的负面影响&#xff0c;同时也极大增加了企业…

极限的准则

目录 定理&#xff1a; 极限运算法则&#xff1a; 极限存在之间的计算&#xff1a; 例题&#xff1a; 定理&#xff1a; 定理&#xff1a; 定理1和定理2的证明方式类似&#xff0c;我们对定理2进行证明。 我们举一个例子&#xff1a; 这道题目的结果是0&#xff0c;但是计算…

excel 数据查询,几个模式化公式请收好

1、一对多查询 所谓一对多&#xff0c;就是符合某个指定条件的有多个结果&#xff0c;要把这些结果都提取出来。 如下图所示&#xff0c;希望根据F2单元格中指定的部门&#xff0c;提取出左侧列表中“生产部”的所有人员姓名。 Excel 2019及以下版本&#xff1a;在H2单元格输…

【教学典型案例】17.环境混用带来的影响

目录一&#xff1a;背景介绍二&#xff1a;思路&方案思路方案1、分权2、定期对比环境混乱的危害三&#xff1a;过程1、排查nginx请求转发是否正常2、找到开发环境项目的服务器&#xff0c;查看服务器配置的nginx3、从fastdfs服务器上找到安装存储的位置4、排查结果四&#…

Java代码优化|提高代码质量的一些小技巧

1.需要 Map 的主键和取值时&#xff0c;应该迭代 entrySet()当循环中只需要 Map 的主键时&#xff0c;迭代 keySet() 是正确的。但是&#xff0c;当需要主键和取值时&#xff0c;迭代 entrySet() 才是更高效的做法&#xff0c;比先迭代 keySet() 后再去 get 取值性能更佳。正例…

python进程间通信

进程间通信表示进程之间的数据交换。 为了开发并行应用程序&#xff0c;需要在进程间交换数据。 下图显示了多个子过程之间同步的各种通信机制 - 各种通信机制 队列 队列可以用于多进程程序。 多处理模块的Queue类与Queue.Queue类相似。 因此&#xff0c;可以使用相同的API…

【带你搞定第二、三、四层交换机】

​ 01 第二层交换机 OSI参考模型的第二层叫做数据链路层&#xff0c;第二层交换机通过链路层中的MAC地址实现不同端口间的数据交换。 第二层交换机主要功能&#xff0c;就包括物理编址、错误校验、帧序列以及数据流控制。 因为这是最基本的交换技术产品&#xff0c;目前桌面…

07-PL/SQL基础(if语句,case语句,循环语句)

本章主要内容&#xff1a; 1.PL/SQL的基本构成&#xff1a;declare,begin,exception,end; 2.结构控制语句:IF语句,CASE语句 3.循环结构&#xff1a;loop循环&#xff0c;for loop循环&#xff0c;while loop循环 PL/SQL的基本构成 特点 PL/SQL语言是SQL语言的扩展&#xff…

JS学习笔记day03

今日内容 零、 复习昨日 CSS 美化,复用,样式文件和表现文件分离便于维护 选择器 {属性:值;…} 引入css 内联文件内部使用style标签外部文件 <link href"路径" rel"stylesheet" type"text/css"> 选择器 基本 idclass标签名 属性 标签名…

【并发编程】深入理解Java内存模型及相关面试题

文章目录优秀引用1、引入2、概述3、JMM内存模型的实现3.1、简介3.2、原子性3.3、可见性3.4、有序性4、相关面试题4.1、你知道什么是Java内存模型JMM吗&#xff1f;4.2、JMM和volatile他们两个之间的关系是什么&#xff1f;4.3、JMM有哪些特性/能说说JMM的三大特性吗&#xff1f…