C++知识点梳理:C++ templates

  1. c++模板包括:类模板、类(非模板类和模板类)方法模板、函数模板、别名模板、变量模板。

    类模板模板参数列表说明:

    1)类定义

    仅模板参数列表声明,template<>行。

    类名后无需参数说明<>。

    2)类方法定义

    在类外面定义方法时,方法名前需要类型限定:类名后面需要类型说明<>。包括所有未特化模板参数和特化模板参数。

    可能:全都是未特化参数、全部特化参数、特化参数+未特化参数

    同时在首行需要template<>类型声明。

    3)类实例化

    不需要template<>声明。

    只需要类名和后面的类型说明<>,和定义时不同,此时需要提供具体的类型。

    4)template<>中只包含未特化、未实例化参数。

    类名后的参数列表<>中可包含:未实例化参数和特化参数

cpp 复制代码
// 1. 类模板
// TempClass temp(1, 2);
template <typename T, typename U>
class TempClass {
pulibc:
	TempClass(T a, U b) {
		cout << "basic template\n";
	}
}
cpp 复制代码
// 2. 类方法模板
// 2.1 非模板类的模板方法
// NonTempClass non_temp;
// non_temp.func1(123);
// non_temp.func2(456);
class NonTempClass {
public:
	// 在类中直接定义模板方法
	template<typename T>
	void func1(T t) {
		cout << "NonTempClass::func1: " << t << endl;
	}

	// 在类中仅声明模板方法,在类外面实现
	template<typename T> void func2(T t);
}
// 在类外面实现模板方法
template<typename T>
void NonTempClass::func2(T t) {
	cout << "NonTempClass::func2: " << t << endl;
}

// 2.2 模板类的模板方法
template<typename T, typename U>
class TempClass {
public:
	// 在类内部实现模板方法
	template<typename N>
	void func1(N n) {
		cout << "TempClass::func1: " << n << endl;
	}
	// 仅声明方法
	template<typename N> void func2(N n);
}

// 在类外部定义模板方法,注意需要两个template关键字,第一个是模板类的,第二个是函数模板
template<typename T, typename U>	// <----- 类模板的类型参数,第一个template关键字
template<typename N>				// <----- 函数模板的类型参数,第二个template关键字
void TempClass::func2(N n) {
	cout << "TempClass::func2: " << n << endl;
}
cpp 复制代码
// 3. 函数模板
template<typename T, typename U>
void temp_func(T t, U u) {
	cout << "temp_func: " << t << "&" << u << endl;
}

别名模板:

(1)用于为类模板定义别名。函数模板不可用,只能为类型定义别名,typedef不能用于函数。

(2)如果类模板完全特化,所有参数都明确,则不需要提供模板参数声明template<>,就不用别名模板。

(3)如果类模板偏特化,部分参数不确定,需要提供模板参数声明:template<>,此时用模板别名。

cpp 复制代码
// 4. 别名模板 alias template
// 为上面定义的TempClass模板声明一个别名模板,类似模板偏特化的语法
template<typename U>
using AliasClass = TempClass<string, U>;
cpp 复制代码
// 5. 变量模板
template<typename T>
constexpr T pi { T {3.1415} };

float fPi { pi<float> };
auto ldPi { pi<long double> };
  1. 编译器处理模板和选择性实例化
    1)编译器不编译 模板,会检查语法错误。
    2)在模板实例化时,用具体类型替换类型参数,生成实际代码。
    3)选择性实例化:编译器不会生成所有方法的代码。
    (1)所有虚方法都会生成。
    (2)非虚方法,只有被调用的才会生成。
    4)选择性实例化缺点:忽略错误,不能及时发现。
    5)解决:显式模板实例化
cpp 复制代码
Grid<int> grid;		// 选择性实例化

template class Grid<int>;	// 显示实例化
  1. 3种模板参数:类型参数、非类型参数、template template参数

    这3种参数是不同维度的,尤其是类型参数和template template参数,容易混淆,在实例化时需要区分模板参数和普通类型参数,如果定义为模板参数则只能提供模板名(不能用具体类型实例化,如vector,不是vector< int >)

    1)类模板和函数模板都可以使用非类型参数。

    2)非类型参数支持有限类型:整型 / 枚举、指针 / 引用 / nullptr_t、auto / auto& / auto*、float、class(后2种在C++支持,有限制)

    3)非类型参数需要使用constexpr,可编译时求值的数值或表达式。所有模板参数都需要在编译器确定。

    4)类型参数和非类型参数都可以提供默认值。

    注意:匿名模板参数也可以设置默认值,类型或非类型参数都可以。

    5)模板非类型参数,其类型可以使用模板的类型参数指定的类型

    6)匿名参数,在模板参数列表中定义匿名参数,这些参数在模板定义中没有用到,仅用于静态分发(通过类型选择不同的模板),或者为了应用SFINAE规则(阻止生成不符合某些规则的模板)。同时,匿名模板参数可以提供默认值。

cpp 复制代码
constexpr int a = 100;
constexpr int b = 200;
// 类模板,提供模板参数默认值,constexpr
template<typename T=int, int n=a+b>
struct NonTypeArgClass {
    NonTypeArgClass() {
        cout << "NonTypeArg: n=" << n << endl;
    }
};

// 函数模板,提供模板参数默认值
template<typename T=int, int n=2>
void NonTypeArgFunc() {
    cout << "NonTypeArgFunc: n=" << n << endl;
}

NonTypeArgClass<string, 10> nonTypeArg1;	// 实例化时提供全部参数
NonTypeArgClass<string> nonTypeArg2;		// 只提供类型参数,非类型参数使用默认值
// NonTypeArgClass<15> nonTypeArg2; 		// comiplation error,如果一个参数使用非默认值,它前面的参数也不能使用默认值,和函数参数默认值一致
NonTypeArgClass<> nonTypeArg3;				// 都使用默认值
NonTypeArgClass nonTypeArg4;				// nonTypeArg3和nonTypeArg4完全相同

NonTypeArgFunc<string, 20>();				// 实例化时提供全部参数
NonTypeArgFunc<string>();					// 只提供类型参数,非类型参数使用默认值
// NonTypeArgFunc<25>(); 					// compilation error
NonTypeArgFunc();							// 和下面的调用效果完全相同
NonTypeArgFunc<>();

//    NonTypeArg: n=10			// nonTypeArg1
//    NonTypeArg: n=300			// nonTypeArg2,在类定义中没有用到T,结果中体现不出差别
//    NonTypeArg: n=300			// nonTypeArg3
//    NonTypeArg: n=300			// nonTypeArg4

//    NonTypeArgFunc: n=20		// NonTypeArgFunc<string, 20>();
//    NonTypeArgFunc: n=2		// NonTypeArgFunc<string>();
//    NonTypeArgFunc: n=2		// NonTypeArgFunc<>();
//    NonTypeArgFunc: n=2		// NonTypeArgFunc();
cpp 复制代码
// 类模板,
// 第一个参数是类型参数,第二个参数是非类型参数
// 第二个参数的类型使用的类型是模板参数指定的类型,并且设置为T的默认值T()或T{ }
template<typename T, const T defaultVal = T()>
class Grid {...}
cpp 复制代码
// template template 参数,模板的类型参数是另一个模板
// c++17后,class可用typename
template<..., template<parameter-list> class ParameterName, ...>

// 如下实例化,类型重复,且没有约束,可能错误写成不同类型
Grid<int, vector<optional<int>>> grid;

// 以下语法不能通过编译,vector是模板,不是类型,编译器不知道需要用int实例化vector
Grid<int, vector> grid;

// vector容器定义原型
// vector模板的类型,是除去名称vector外,剩余的部分,这些可作为模板类型参数使用
template<typename E, typename Allocator = std::allocator<E>>
class vector {...}

// 类模板定义
// 复制模板声明,把模板名称(vector)改为类型参数名,作为模板参数template<>
// c++17后,模板类型参数中的class可换为typename(class Container -> typename Container),
// 在模板类型中只使用关键字typename即可
// 模板参数默认类型为vector,不是vector<T>,Container是一个模板名,不是类型名,所以要对应vector
template<typename T,
		template<typename T, typename Allocator = std::allocator<E>> class Container = std::vector>
class Grid {
private:
	// 定义中使用template template参数
	// Container是模板的模板类型参数,它用optional<T>进行实例化
	// 模板Container用另一个类型参数T(相关类型)进行实例化
	vector<Container<optional<T>>> mData;
}

// 模板方法定义
// 只需更新template<>参数列表,其他部分不变,包括方法前的类型限定,例如:
template<typename T,
		template<typename E, typename Allocator = std::allocator<E>> Container = std::vector>
optional Grid<T, Container>::at(int x, int y) {...}

// 实例化,和其他参数类型模板相同,注意区分template template参数,只能提供模板名
// Grid模板第二个参数(Container)是模板类型,实例化时需要提供vector/deque(模板名)
// 不能是vector<int>或deque<int>,这些是具体的类型名
Grid<int, vector> grid1;
Grid<int, deque> grid2;

// template template参数可多层嵌套(纯属好奇心驱动的尝试)
template<typename U, // 类型参数U
		// GridType的模板类型
        template<typename T, template<typename E, typename Allocator = std::allocator<E>> class Container = std::vector>
        class GridType // 类型参数GridType
        >
class Foo {
    
};
cpp 复制代码
// 匿名类型参数 和 匿名非类型参数,都有默认值
// 貌似提供具体的默认值没有意义,定义中用不到所以才匿名,默认值也不会被引用
// 只有使用trait提供编译时的动态值,应用SFINAE规则,控制模板在符合条件时生成,参见下面的示例。
template<typename = int, size_t = 12>
struct funcAnonymousDefault { };

// 匿名模板参数,编译期默认值
// 函数模板,比较两个值是否相等,只有两个值的类型相同时才能实例化这个模板
// typename = enable_if< is_same_v<T, U> >::type 匿名参数,默认值为enable_if<>::type
//		注意:要用enable_if::type,如果没有type则无法应用SFINAE,因为enable_if<>总是一种类型;
//		错误:typename = enable_if< is_same_v<T, U> >  (末尾缺少::type)
// enable_if<arg1, arg2=void>,
//	(1)如果arg1为true,则enable_if<>::type值为arg2;
//	(2)如果arg1为false,则enable_if<>::type值为空,导致模板语法不合规,SFINAE阻止模板实例化
template<typename T, typename U,
        typename = enable_if< is_same_v<T, U> >::type
        >
bool isSameValue(T t, U u) {
    return t == u;
}

int a = 3;
int b = 3;
double b = 3; // 编译错误,参见下面错误信息

cout << "is same? : " << isSameValue(a, b) << endl;
// 输出结果
// is same? : 1

// 编译错误信息:
error: no matching function for call to 'isSameValue(int&, int&)'
   23 |     cout << "is same? : " << isSameValue(a, b) << endl;
      |                              ~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~
note: candidate: 'template<class T, class U, class> bool isSameValue(T, U)'
   15 | bool isSameValue(T t, U u) {
      |      ^~~~~~~~~~~~~~~~~~~~~~~~~~~
note:   template argument deduction/substitution failed:
error: no type named 'type' in 'struct std::enable_if<false, void>'
   13 |         typename = enable_if< !is_same_v<T, U> >::type
      |         ^~~~~~~~
  1. CTAD(class template argument deduction)自动推导类型参数
    1)函数模板天生支持类型推导。
    2)在较新版本c++中,类模板支持类型推导。需要构造函数中使用模板参数,在初始化过程中可以推断类型和值参数。
    如果模板参数在初始化过程中使用不到,则无法完成类型推导。
    3)通过辅助模板函数,在内部实例化模板类,支持类型自动推导。
    4)特殊:unique_ptr,shared_ptr不支持类型推导,需要用make_unique() make_shared()。
    原因:传入T*时,编译器无法确定是类型T还是T[ ]。
    5)自定义推导规则:可以自定义推导规则,规避上面的歧义。
    (1)必须定义在类定义外面。
    (2)必须和类定义在同一个namespace。
    (3)可选使用explicit关键字,规则和应用在构造函数上一样。
    6)函数模板中,返回类型不能自动推导。可让编译器推导部分参数
cpp 复制代码
// 自定义推导规则
template<typename T>
struct DeductionRule {
    explicit DeductionRule(T t) {
        cout << "DeductionRule: T=" << typeid(T).name() << endl;
    }

	template<typename Iter>
	DeductionRule(Iter iter) {}
};

// 自定义规则语法:构造函数 -> 实例化类型;
DeductionRule(int) -> DeductionRule<double>;
DeductionRule(const char*) -> DeductionRule<string>;

template<typename T>
DeductionRule(Iter) -> DeductionRule<typename iterator_traits<Iter>::value_type>;

// Output: 无自定义推导规则
// DeductionRule: T=i
// DeductionRule: T=PKc

// Output: 添加自定义推导规则后
// DeductionRule: T=d
// DeductionRule: T=NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
cpp 复制代码
// 部分参数推导
template<typename R, typename T, typename S>
R func(T t, S s);

// 以下2种调用方式等效,
// 第二种省略了部分类型参数,由编译器自动推导,返回类型需明确指出
auto ret = func<long, int, int>(1, 2);
auto ret = func<long>(1, 2);
auto ret = func(1, 2); // compilation error

// 不能自动推导的类型参数,通过提供默认值,可以使调用时省略全部参数
// 本质上:部分由编译器推导,部分使用默认值,都不需要手动指定
template<typename R=long, typename T, typename S>
R func(T t, S s);
auto ret = func(1, 2);
  1. 模板特化:全特化、偏特化
    1)类模板:支持全特化、偏特化
    2)函数模板:只能全特化
    (1)函数有重载机制,本质上根据不提供的类型,提供不同的实现。和模板机制类似。
    (2)在函数重载解析时,函数模板不参与重载解析。
    (3)函数模板特化不常用 ,可能遇到非"预期"行为。模板特化和函数重载可能误用。
    3)特化语法:
    (1)在模板参数列表(template<>)中,去掉特化的(类型/非类型)参数。(因为这个参数已经确定,不再需要占位符"变量")
    在全特化时,只保留template<>,里面没有任何参数。
    (2)类似模板实例化的语法,在类名称或者函数名称后,增加参数列表<>,列出所有 类型/非类型参数。
    (3)一种"半实例化"状态,template<>告诉编译器这是一个模板,所有参数都在类定义 中类名后面列出。
    偏(部分)特化 < ---- > 偏/部分 实例化
cpp 复制代码
template <typename T, typename S>
struct TempClass {
    TempClass(T t, S s) {
        cout << "basic temp class\n";
    }
};

// 类模板偏特化:用int特化第二个参数S
template <typename T>
struct TempClass<T, int> {
    TempClass(T t, int s) {
        cout << "specialize S\n";
    }
};
// 类模板偏特化:用int特化第一个参数T
template <typename S>
struct TempClass<int, S> {
    TempClass() = default;

    TempClass(int t, S s) {
        cout << "specialize T\n";
    }
};
// 类模板全特化
template<>
struct TempClass<bool, bool> {
    TempClass(bool a, bool b) {
        cout << "TempClass<bool, bool>\n";
    }
};

TempClass temp1 {1.0, 2.0};
TempClass temp2 {1, 2.0};
TempClass temp3 {1.0, 2};
TempClass temp4 {false, true};

// Output:
//    basic template class
//    TempClass<int, S>
//    TempClass<T, int>
//    TempClass<bool, bool>
cpp 复制代码
// 非类型参数特化
// 语法和类型参数特化一样,可用于模板递归中,作为递归结束条件
template<int i>
struct TempClass2 {
    TempClass2() {
        cout << "TempClass2: basic template, i=" << i << endl;
    }
};

template<>
struct TempClass2<10> {
    TempClass2() {
        cout << "TempClass2: specialized template for i=10" << endl;
    }
};

TempClass2<666> temp2_1;
TempClass2<10> temp2_2;

// Output:
// TempClass2: basic template, i=666
// TempClass2: specialized template for i=10
cpp 复制代码
// 函数模板全特化
template<typename T, typename S>
void TempFunc(T t, S s) {
    cout << "TempFunc(T t, S s)\n";
}

template<>
void TempFunc<double, double>(double t, double s) {
    cout << "TempFunc<double, double>(double t, double s)\n";
}

// 实例化函数模板并调用
TempFunc(1, 2);
TempFunc(3.0, 4);
TempFunc(5.0, 6.0);

// Output:
// TempFunc(T t, S s)
// TempFunc(T t, S s)
// TempFunc<double, double>(double t, double s)
  1. 模板继承

  2. 友元 friend:类模板的友元模板函数和友元模板类

    关键:operator+后面的< T >

cpp 复制代码
// 前向声明
template<typename T> class Grid;

// operator+ 声明
// 运算符+用到了模板类Grid,也要定义为模板,template<typename T>
// 其中用到的类型都为模板类型Grid<T>
template<typename T>
Grid<T> operator+ (const Grid<T>& left, const Grid<T>& right);

// 1. friend关键字声明友元。
// 2. operator+后面<T>告诉编译器此operator是模板。
// 3. 在Grid模板内部,使用Grid或Grid<T>是等效的(CLion验证)。
template<typename T>
class Grid {
public:
	friend Grid<T> operator+ <T> (const Grid& left, const Grid& right);
};

// android: StrongPointer.h
template<typename T>
class sp {
	// ...
private:
	// 把sp自身和wp类声明为sp类的友元
	// sp和wp都是模板类,friend关键字挨着class,在class前面(位于template<>之后)
	template<typename Y> friend class sp;
	template<typename Y> friend class wp;
}
  1. 函数模板返回类型和简化的函数模板语法

    如果让编译器自动推导函数返回类型,可使用以下几种方式:

    1)auto // 去掉const和&

    2)decltype(auto) // 不会去掉const和&

    3)decltype( func() )

    4)auto add(T t, S s) -> decltype(t+s)

cpp 复制代码
// Error,在前面decltype使用t和s时,尚未定义
template<typename T, typename S>
decltype(t + s) add(const T& t, const S& s)
{ return t+s; }

template<typename T, typename S>
auto add(const T& t, const S& s) -> decltype(t+s)
{ return t+s; }

简化的函数模板

1)所有类型都用auto(或const auto&),省略了template<typename ...>声明。

2)使用auto是编译器语法糖,效果和使用template<typename...>声明一致。

3)局限性 1:使用auto的类型每个都不同,如果需要指定多个入参为同一种类型,需要使用template的方式声明。

局限性 2: 因为auto没有类型名,在函数体内无法直接使用对应的类型,可使用decltype。

cpp 复制代码
template<typename T, typename S>
decltype(auto) func(const T& t, contst S& s) { 
	return t + s;
}

decltype(auto) func(const auto& t, const auto& s) { 
	return t + s;
}
  1. c++20 concept
    1)concept由constraints-expression(约束表达式)构成。
    2)concept表达式(concept expression),应用已有concept
    3)constraints-expression组成:
    (1)一个简单的常量表达式,返回bool值。
    (2)一种新的特殊的常量表达式,require表达式。
    4)require表达式由requirement构成
    5)requirement分为4种:简单、类型、复合、嵌套。
    6)concept repression可使用&&或||组合使用。
cpp 复制代码
// concept定义语法
template<parameter-list>
concept concept-name = constraints-expressions;

// concept表达式
// 应用已有的概念,例如:Incrementable<T>, convertible_to<bool>
// 类似于调用已定义函数:func(arg);
concept-name<argument-list>


// 约束表达式
template<typename T>
concept C = sizeof(T) == 4;

// require表达式定义语法
requires (parameter-list) { requirements; }

// requirement分为4种:
// 1. simple requirement (不以requires开头,相对嵌套requirement来说)
//		可以是任意表达式,不求值,只验证编译通过(验证语法和语义功能)
template<typename T>
concept Incremental = requires(T x) {
	x++;
	++x;
}

// 2. type requirement
template<typename T>
concept C = requires {
	typename T::value_type;		// 验证T是否有类型value_type
	typename SomeTemplate<T>;	// 是否可以用T实例化SomeTemplate
}

// 3. 复合 requirement	compound requirement
// 验证不会抛出异常,或者返回某种类型
// noexcept, 或->type-constraint可选的,可验证某一种或同时验证
// { }是必须的,即使一个语句,也需要有。
{ expression } noexcept -> type-constraint;

template<typename T>
concept C = requires(const T x, const T y) {
	{ x.swap(y) } noexcept;					// noecept验证时,{}不能省略
	{ x.size() } -> convertible_to(size_t);	// 验证返回值时,一个语句时,{}也不可省略
	{ x==y } -> convertible_to<bool>;		// 不可省略{}
}

// 4. 嵌套 requirement
template<typename T>
concept C = requires (T t) {
	// 这里不能省略requires,只用sizeof(T) == 4是不行的,不会实际校验,感觉requires类似assert
	requires sizeof(T) == 4;
//	sizeof(T) == 4;  // 错误,可编译通过,逻辑不对
	++t;
	t++;
}

// 5. 组合概念表达式,不是-不是-不是requirement,combined CONCEPT expression
// 使用&&,||组合
template<typename T>
concept MyConcept = Incrementable<T> && Decrementable<T>;
  1. 模板递归

  2. 可变参数模板

    1)< typename... Tn >表示0个或多个参数。

    2)...展开其左侧的'表达式',根据参数个数重复多次,用逗号分隔。

    3)...前后的空格可选。

    4)可变参数没有直接的遍历方式,只能通过模板递归遍历。

cpp 复制代码
template<typename... Tn>
class Temp {...}

// 可变参数函数模板

// typename... Tn,声明可变类型个数

// Tn... args,声明可变参数个数

// args...,使用可变参数

// 使用完美转发perfect forward,避免参数复制和使用字面量

// 使用确定类型参数T1和可变参数结合的方式,递归分离、解析可变参数

cpp 复制代码
void handleValue(int v) { cout << "int: " << v << endl; }
void handleValue(double v) { }
void handleValue(string_view v) { }

// 递归终止case
void processValues() { }

template<typename T1, typename... Tn>
void processValues(T1 arg1, Tn... args) {
	handleValue(arg1);
	processValues(args...);
}

template<typename T1, typename... Tn>
void processValues(T1&& arg1, Tn&&... args) {
	handleValue(std::forward<T1>(arg1));
	processValues(std::forward<Tn>(args)...); // 注意此处...,在每个参数上执行forward
}

// sizeof...操作符,和sizeof是不同的操作符
int count { sizeof...(args) }

使用可变参数模板实现Mixin模板类

cpp 复制代码
struct Base1 {
    int mValue;

    Base1(int i): mValue(i) { }
    void func1() {
        cout << "Base1: value=" << mValue << endl;
    }
};

struct Base2 {
    int mValue;

    Base2(int i): mValue(i) { }
    void func2() {
        cout << "Base2: value=" << mValue << endl;
    }
};

template<typename... Cn>								// 1. 声明可变参数:typename... Cn
struct Mixin: public Cn... {							// 2. 继承可变参数:public Cn...
    Mixin(const Cn&... classes): Cn {classes} ... {}	// 3. 可变参数作为函数参数:Cn&... cls,
    virtual ~Mixin() = default;							//    以及初始化可变参数成员变量:Cn{classes}...
};

// 调用
Mixin<Base1, Base2> mix { Base1{123}, Base2{456}}; // 初始化方式1

auto b1 = Base1{123};
auto b2 = Base2{456};
Mixin<Base1, Base2> mix {b1, b2};  // 初始化方式2

mix.func1();
mix.func2();

// output:
// Base1: value=123
// Base2: value=456
  1. constexpr if
  2. 折叠表达式:folld expressions
  3. 元编程
cpp 复制代码
// 模板递归,计算阶乘
template<size_t n>
struct Factorial {
    constexpr static int value = n * Factorial<n -1>::value;
};

template<>
struct Factorial<0> {
    constexpr static int value = 1;
};

cout << "fact: " << Factorial<1000>::value << endl;
cpp 复制代码
// 模板递归,循环
template<size_t n>
struct Loop {
    template<typename Func>
    static void run(Func func) {
    	// Loop::run和func()的执行顺序,决定是正序循环还是逆序循环
        Loop<n-1>::run(func);
        func(n);
    }
};

template<>
struct Loop<0> {
    template<typename Func>
    static void run(Func func) {}
};

Loop<5>::run([](int i){
    cout << "run: " << i << endl;
});
  1. traits
  2. 在一个声明中多次使用template的场景
    1)模板类的模板方法定义
    2)模板的template template参数
cpp 复制代码
// 顺序并列使用2个template关键字
template<typename ClassArgType>
template<typename MethodArgType>
void Temp<ClassArgType>::func(MethodArgType data) { ... }

// template中嵌套template
// 可嵌套多层,参见上面template template参数中的例子
template<typename T,
		template<typename E, typename Allocator<E> = std::allocator<E>> Container = std::vector>
class Grid { ... }
相关推荐
PieroPc9 分钟前
Python 写的 智慧记 进销存 辅助 程序 导入导出 excel 可打印
开发语言·python·excel
tinker在coding12 分钟前
Coding Caprice - Linked-List 1
算法·leetcode
watermelonoops1 小时前
Deepin和Windows传文件(Xftp,WinSCP)
linux·ssh·deepin·winscp·xftp
疯狂飙车的蜗牛2 小时前
从零玩转CanMV-K230(4)-小核Linux驱动开发参考
linux·运维·驱动开发
2401_857439693 小时前
SSM 架构下 Vue 电脑测评系统:为电脑性能评估赋能
开发语言·php
SoraLuna3 小时前
「Mac畅玩鸿蒙与硬件47」UI互动应用篇24 - 虚拟音乐控制台
开发语言·macos·ui·华为·harmonyos
xlsw_3 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
Dream_Snowar4 小时前
速通Python 第三节
开发语言·python
远游客07134 小时前
centos stream 8下载安装遇到的坑
linux·服务器·centos
马甲是掉不了一点的<.<4 小时前
本地电脑使用命令行上传文件至远程服务器
linux·scp·cmd·远程文件上传