c++从入门到精通(四)--动态内存,模板与泛型编程

文章目录

动态内存

c++中动态内存的管理是通过new和delete运算符来实现的,c++中定义了两种智能指针来管理动态对象:shared_ptr允许多个指针指向同一个对象,unique_ptr独占所指向的对象。weak_ptr指向shared_ptr所管理的对象

在如下三种情况下需要使用动态内存:

  1. 程序不知道自己所需多少对象-容器类
  2. 程序不知道所需对象的准确类型
  3. 程序需要在多个对象间共享数据

直接管理内存

  • new返回一个指向该对象的指针
  • 出于与变量初始化相同的原因,对动态分配的对象进行初始化通常是个好主意。int *p1=new int;未定义,int *p2=new int();初始未0
  • int *p(new int(42));
  • 使用auto自动推断元素类型auto p1=new auto(obj);
  • delete接受的指针必须是指向动态分配的内存的指针。释放一块非new分配的内存行为是未定义的。

Shared_ptr类

  • 类似于vector,智能指针也是模板。

  • make_shared函数shard_ptr<int> p=make_shared<int>(42);,该函数和顺序容器类型的emplace成员类似,会用参数来构造给定类型的对象。

  • 对shared_ptr拷贝的时候,每个shard_ptr都会记录有多少个其他的指针指向相同的对象。

  • shared_ptr基于引用计数的规则管理对象。

  • ⭐共享数据,我们希望一个类的不同拷贝之间共享相同的元素。

    c++ 复制代码
    class StrBlob{
     public:
      typedef std::vector<std::string>::size_type size_type;
      StrBlob();
      StrBlob(std::initializer_list<std::string> il);
      size_type size() const{return data->size();}
      bool empty() const{return data->empty();}
      //添加或删除元素
      void push_back(const std::string &t){data->push_back(t);}
      void pop_back();
      //元素访问
      std::string& front();
      std::string& back();
    private:
      std::shared_ptr<std::vector<std::string>> data;
      //如果data[i]不合法,抛出一个异常
      void check(size_type i,const std::string &msg) const;
    }
    //构造函数
    StrBlob::StrBlob(): data(make_shared<vector<string>>()){}
    StrBlob::StrBlob(initializer_list<string> il): data(make_shared<vector<string>>(il)){}
    
    void StrBlob::check(size_type i,const string &msg) const{
      if (i>=data->size())
        throw out_of_range(msg);
    }
    
    String& StrBlob::front(){
      check(0,"font on empty StrBlob");
      return data->front();
    }
    
    string& StrBlob::back(){
      check(0,"back on empty StrBlob");
      return data->back();
    }
    const String& StrBlob::front() const {
      check(0,"font on empty StrBlob");
      return data->front();
    }
    
    const string& StrBlob::back() const {
      check(0,"back on empty StrBlob");
      return data->back();
    }
    
    void StrBlob::pop_back(){
      check(0,"pop_back on empty Strblob");
      data->pop_back();
    }
  • 我们可以使用new返回的指针来初始化智能指针。接受指针参数的智能指针的构造函数是explicit的,我们只能使用直接初始化的方式初始化智能指针,shared_ptr<ing> p1(new int(42));

  • 出于相同的原因,一个返回shared_ptr的函数不能在其返回语句中隐式转换一个普通指针。我们可以使用这种写法:return shared_ptr<int>(new int(42));

  • 默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象

  • 如果将一个shared_ptr绑定到一个普通指针,我们就不应该再使用内置指针来访问shared_ptr所指向的内存了。

  • 不能使用get初始化另一个智能指针,或者为智能指针赋值。智能指针的get函数返回的是一个普通内置指针,使用get返回的指针的代码不能delete此指针,否则会导致二次delete(第一次delete是用get返回的指针初始化的智能指针,第二次delete是调用get成员函数的智能指针)。

  • reset和unique。如果有多个shared_ptr指向同一块区域,但是某个shared_ptr不希望改变共享区域的内容,而是选择重新开辟一块区域。我们可以使用如下方式:

    c++ 复制代码
    if(!P.unique())
      p.reset(new string(*p));reset返回一个新的对象,新对象的引用计数+1,原对象的引用计数-1
    *p+=newval;
  • 使用智能指针在发生异时也可以防止内存不被释放。发生异常后会销毁智能指针导致引用计数递减,从而在合适的时候自动调用delete。

  • 可以在使用智能指针的时候传递一个函数来替代析构函数,比如管理链接的智能指针,可以构建指向连接对象的智能指针,同时传递关闭连接的函数。当连接对象无任何引用时,自动调用关闭连接的函数。

Unique_ptr

  • 某时刻只能有一个unique_ptr指向一个给定的对象。

  • 当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。类似shared_ptr,初始化unique_ptr必须采用直接初始化形

  • 可以调用release或者reset将指针的所有权从一个unique_ptr转移给另一个unique

    c++ 复制代码
    unique_ptr<string> p2(p1.release());
    unique_ptr<string> p3(new string("123"));
    p2.reset(p3.release());
  • 虽然不能拷贝unique但是可以返回unique或者返回局部变量的拷贝(返回之后原本的那个就会被销毁,依旧可以保证只有一个指针指向某个区域)。因为编译器知道要返回的对象将要被销毁(可以保证之后只有一个指针指向某一个区域)。在这种情况下编译器会执行一个特殊的拷贝

    c++ 复制代码
    unique_ptr<int> clone(int p)
    {
      return unique_ptr<int>(new int(p));
    }
    
    unique_ptr<int> clone(int p){
      unique_ptr<int> ret(new int(p));
      return ret;
    }
  • 我们可以为unique_ptr提供一个删除器,与shared_ptr不同,删除其作为unique_ptr类型的一部分

    shared_ptr我们传递的函数(替代析构函数的函数)不是shared_ptr类型的一部分

    c++ 复制代码
    unique_ptr<objT,delT> p(new objT,fcn);
    unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection)

Weak_ptr

  • 将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放(即使有weak_ptr指向对象还是会被释放)
  • 创建weak_ptr需要用shared_ptr来 初始化它
  • 我们不能使用weak_ptr直接访问对象,必须调用lock
c++ 复制代码
//创建
auto p=make_shared<int>(43);
weak_ptr<ing> wp(p);

//访问,最终还是使用shared_ptr访问,访问过程中引用计数+1,结束后-1
if(shared_ptr<ing> np =wp.lock()){
  
}

动态数组

  • 动态数组并不会分配一个数组类型的对象,返回的是元素类型的指针。不能对动态数组条用begin和end,也不能用范围for循环处理动态数组。
  • 我们可以使用unique_ptr来管理new分配的数组.可以通过下标直接访问数组
  • shared_ptr不直接支持管理动态数组,如果使用,必须提供自己定义的删除器.
  • 用shared_ptr管理动态数组在访问元素的时候,shared_ptr没有定义下标运算,并且智能指针不支持算数运算。因此我们必须调用get得到普通指针。每次get的都是指向数组元素的首指针。
c++ 复制代码
//创建
int *pia=new int[get_size()];
typedef int arrT[42];
int *pia new arrT;//未定义
int *pia new arrT();//初始值未0

//释放必须用【】表明要释放的是一个动态数组。不能用类型别名
delete [] pia;

//使用ptr管理
unique_ptr<int[]> up(new int[10]);
up.release();//自动用delete[]销毁

//用shared管理。删除器(lambda表达式)接受一个指向数组的指针,删除此指针指向的动态数组
shared_ptr<int> sp(new int[10], [](int *p){deleate [] p;});
sp.reset();//使用lambda释放数组。
for(size_t i=0;i!=10;1++)
  *(sp.get()+i)=i;

allocator类

  • 将内存分配和对象构造分离开。allocator分配的内存是未构造的,采用construct函数构造,额外的参数必须是与构造的对象的类型相匹配的合法的初始化器。
  • 我们必须对每个构造的元素调用destory来销毁。销毁后我们可以重新构造元素,也可以还给系统(deallocate)
  • allocator还定义了拷贝和填充未初始化内存的算法
c++ 复制代码
//分配
allocator<string> alloc;
auto const p=alloc.allocate(n);//分配n个未初始化的sring,返回指向首元素的指针。

//构造
auto q=p;
alloc.construct(q++);
alloc.construct(q++,10,'c');
alloc.construct(q++,"hji");

//销毁
while(q!=p)
  alloc.destory(--q);

//释放内存
alloc.deallocate(p,n);

//拷贝
autp p=alloc.allocate(vi.size()*2);
auto q=unitialized_copy(vi.begin,vi.end(),p);//把迭代器指向的范围的元素拷贝到p所指向的动态内存空间。返回指向最后一个拷贝元素的指针
uninitialized_fill_n(q,vi.size(),42);//在q所指动态内存中填充vi.size个值为42的元素。

文本查询程序

见本人csdn文章--c++练手项目简单文本查询

模板与泛型编程

面向对象编程(OOP)和泛型编程都能处理在编写程序时不知道类型的情况。不同之处在于:OOP能处理类型在程序运行之前都未知的情况;而在泛型编程中,在编译时就能获知类型了。

模板是泛型编程的基础。一个模板就是一个创建类或函数的蓝图或者说公式。

定义模板

函数模板

模板中的函数参数是const的引用。函数体中的条件判断仅使用<比较运算。

c++ 复制代码
template <typename T>
int compare(const T &v1,const T &v2)
{
    if (v1<v2) return -1;
    if (v2>v1) return 1;
    return 0;
}

**非类型参数:**非类型参数被一个用户提供的或编译器推断出的值所代替。这些值必须是常量表达式。

c++ 复制代码
template<unsigned N,unsigned M>  //N和M是非类型参数
int compare(const char (&p1)[N], const char (&p2)[M]){
    return strcmp(p1,p2);
}
compare("hi","mom");//编译器确定N=3,M=4,注意字符串字面值常量末尾会被插入一个空字符作为终结符。

一个非类型参数可以是一个整型,或者是一个指向对象或函数类型的指针或(左值)引用。绑定到非类型整型参数的实参必须是一个常量表达式。绑定到指针或引用非类型参数的实参必须具有静态的生存期。在模板定义内,模板非类型参数是一个常量值。在需要常量表达式的地方,可以使用非类型参数,例如,指定数组大小。

**inline和constexpr的函数模板:**函数模板可以声明为inline或constexpr的。

**模板编译:**当编译器遇到一个模板定义时,它并不生成代码。为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常既包括声明也包括定义。

模板的设计者应该提供一个头文件,包含模板定义以及在类模板或成员定义中用到的所有名字的声明。模板的用户必须包含模板的头文件,以及用来实例化模板的任何类型的头文件。

保证传递给模板的实参支持模板所要求的操作,以及这些操作在模板中能正确工作,是调用者的责任。

类模板

编译器不能为类模板推断模 板参数类型,为了使用类模板,我们必须在模板名后的尖括号中提供额外信息,用来代替模板参数的模板实参列表。

类模板的成员函数本身是一个普通函数。但是,类模板的每个实例都有其自己版本的成员函数。因此,类模板的成员函数具有和模板相同的模板参数。因而,定义在类模板之外的成员函数就必须以关键字template开始,后接类模板参数列表。

默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化。

当我们使用一个类模板类型时必须提供模板实参,但这一规则有一个例外。在类模板自己的作用域中,我们可以直接使用模板名而不提供实参。

c++ 复制代码
template <typename T> class Blob{
public:
    typedef T value_type;
    Blob& operator++();//没有使用Blob<T>&
    friend class BlobPtr<T>;//每个Blob实例将访问权限授权给用相同类型实例化的BlobPtr
}
template <typename T> int Blob<T>::func(int a){
    return 1;
}

template <typename T>class C2{
    friend class Pal<T>;//c2的每个实例都将相同实例化的Pal声明为友元
    template <typename X> friend class Pal2;//pal2是一个模板,pal2的所有实例都是c2每个实例的友元,不需要前置声明,多对对。
    friend class pal3;//pal3是一个非模板类,它是c2所有实例的友元。
}

如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板实例。如果友元自身是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例。

在新标准中,我们可以将模板类型参数声明为友元。friend Type;将访问权限授予用来实例化类模板的Type类。此时Foo将成为Bar的友元

**模板别名:**当我们定义一个模板类型别名时,可以固定一个或多个模板参数

c++ 复制代码
typedef Blob<string> StrBlob;
template<typename T> using twin=pair<T,T>;
twin<string> authors;//==pair<string,string>

template <typename T> using parNo=pair<T,unsigned>;
parNo<string> books;//==pair<string,unsigned>

**类模板的static成员:**模板类的每个static数据成员必须有且仅有一个定义。但是,类模板的每个实例都有一个独有的static对象

c++ 复制代码
template <typename T> class Foo{
    public:
    static std::size_t count(){return ctr;}
    private:
    static std::size_t ctr;
}
template <typename T> size_t Foo<T>::ctr=0;
模板参数

模板参数于作用域:

  • 模板内不能重用模板参数名字,如果B是模板参数名字,double B;是错误的语法。

模板声明:

  • 一个特定文件所需要的所有模板的声明通常一起放置在文件开始位置,出现于任何使用这些模板的代码之前

在模板中使用类的类型成员

  • c++默认通过作用域运算符访问的名字不是类型而是一个数据成员。如果我们想要使用模板类型参数的类型成员,就必须用typename T::value_type。这表示我们使用的是T类型的类型成员value_type而不是static成员。

函数模板和类模板的默认实参

  • 与函数默认实参一样,对于一个模板参数,只有当它右侧的所有参数都有默认实参时,它才可以有默认实参。
  • 我们如果使用一个类模板的全部默认实参,不能省略<>
成员模板

一个类的成员函数可以是模板,成员模板不能是虚函数

普通类的成员模板:

c++ 复制代码
class DebugDelete{
  public:
  DebugDelete(std::ostream &s =std::cerr): os(s){}
  template <typename T> void operator()(T *p) const{
    os<<"deleting unique_ptr"<<std::endl; delete *p;
  }
  private:
  std::ostream &os;
}

double *p=new double;
DebugDelete d;
d(p);//编译器自动判断T为double类型
int *ip = new int;
DebugDelete()(ip);//编译器自动推断T未int类型

//使用DebugDelete作为unique_ptr的删除器
unique_ptr<int,DebugDelete> p(new int,DebugDelete());//编译器自动判断Debug Delete的T为int
uneqie_ptr<string,DebugDelete> sp(new string, DebugDelete());编译器自动判断Debug Delete的T为string

**类模板的成员模板:**对于类模板,我们也可以为其定义成员模板。类和成员各自有自己的、独立的模板参数。

当我们在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表。类模板的参数列表在前,后跟成员自己的模板参数列表

c++ 复制代码
template <typename T> class Blob{
  template <typename IT> Blob(IT b,IT e);
}
templage <typename T>
template <typename IT>
Blob<T>::Blob(IT b,IT e):data(std::make_shared<std::vector<T>>(b,e))
控制实例化
  • 在大型系统中,实例化相同参数的模板造成的额外开销可能非常严重,我们可以通过显示实例化来避免这种开销。
  • 当编译器遇到extern模板声明时,它不会再本文件中生成实例化代码 。
  • 实例化定义会实例化所有成员。即使我们不使用某个成员,它也会被实例化。因此,我们用来显式实例化一个类模板的类型,必须能用于模板的所有成员。
c++ 复制代码
extern template class Blob<string>;
extern template int compare (const int&, const int&);
Blob<string> sa1,sa2;//实例化出现在其他位置
Blob<int> a1={1,2,3}//在本文件中实例化。

//实例化定义文件
template int compare(const int&, const int&);
template class Blob<string>;

模板实参推断

  • 算数类型转换,派生类向基类的转换,用户定义的转换都不能应用于函数模板。

  • 函数模板形参如果是引用,数组不会被转换为指针,此时不同大小的数组类型是不匹配的。

  • 我们可以定义用户指定返回类型的函数模板 T1 sum(T2,T3),用户提供显示模板实参的方式于定义类模板实例的方式相同sum<long>(i,lng),此调用显式指定T1的类型。而T2和T3的类型则由编译器从i和lng的类型推断出来。

  • 显示指出模板类型参数可以进行正常的类型转换

    c++ 复制代码
    long lng;
    compare(lng,1024);//错误,模板参数不匹配 lng和1024类型不匹配
    compare<long>(lng,1024);//正确,1024被转换为long类型
  • 尾置返回类型,如果函数的返回类型依赖于模板实参的类型,我们可以用尾置返回类型加decltype。编译器遇到函数的参数列表前beg是不存在的,所以只能用尾置返回。

    c++ 复制代码
    template <typename It>
    auto func(It beg)->decltype(*beg)
  • decltype(*beg)返回元素类型的引用类型,我们可以用remove_reference::type脱去引用,剩下元素类型本身.

    c++ 复制代码
    template <typename T>
    auto fcn2(T *beg)->typename remove_reference<decltype(*beg)>::type//我们需要声明typename表示type是一个类型。type是一个类的成员,该类依赖于一个模板参数。
    {
      return *beg;
    }
  • 标准库提供了一些类型转换模板,我们可以利用这些模板得到转换后的类型。

  • 我们使用函数模板初始化函数指针时,编译器根据指针的类型推断模板实参。如果存在重载版本,编译器无法确定模板实参类型,此时我们可以使用显示模板实参来消除歧义,
c++ 复制代码
template <typename T> int compare(const T&,const T&);
int (*p)(const  int&,const int&)=compare;//T为int

void func(int(*)(const string &));
void func(int(*)(const int &));
func(compare);//歧义,无法确定compare的模板实参是int还是string

func(conpare<ing>);//显示模板实参消除歧义。传递compare(const int&);

模板实参推断和右值引用:

  • 一般而言我们不能将一个左值引用绑定到右值引用参数,但是有两个例外(这也是标准库设施move的实现基础)。

  • 例外一:我们将左值绑定给函数右值引用参数时候,右值引用指向模板类型参数T&&时候,编译器推断模板类型参数为实值的左值引用类型(T 为int,我们传递int左值时,编译器推断T的类型为int &)。这表明我们开可以通过模板类型参数间接定义引用的引用。

  • 例外二:如果我们间接建一个引用的引用,则这些引用形成了"折叠"。

    X&&、X&&&和X&&&都折叠成类型X&

    类型X&&&&折叠成X&&

  • 例外1和2组合在一起就会产生下面奇妙的效果:

    总结,如果如果一个函数参数是一个指向模板类型参数的右值引用(如,T&&),则它可以被绑定到一个左值。如果实参是一个左值,则推断出的模板实参类型将是一个左值引用,且函数参数将被实例化为一个(普通)左值引用参数(T&)

  • 接受右值引用参数的模板函数可能出现不同的程序行为。如果我们提供右值引用,则T t=val,是val的拷贝,改变t不会改变val。如果我们提供左值引用T t=val,t是val的引用,改变t会改变val。因此我们一般需要重载接受右值引用参数的模板函数

    template <typename T> void f(T &&)接受右值引用

    template <typename T> void f(const T&)接受左值引用和const右值。接受const右值,T是const引用。

  • 有了上面的了解后,我们就可以探究std::move函数了,该函数用remove_reference保证返回值是右值引用,用static_case把左值引用转换为右值引用==(从一个左值static_cast到一个右值引用是允许的)。虽然不能隐式地将一个左值转换为右值引用,但我们可以用static_cast显式地将一个左值转换为一个右值引用。

    因此我们可以直接使用static_case转化右值,但使用标准库的move函数容易的多,并且统一使用std::move是的我们在程序中查找潜在的截断左值代码变得很容易。

转发:

使用forward来保持传递给模板类型参数的实参属性。如果实参是一个右值,则T1是一个普通类型,forward将返回T1&&,结果是一个右值引用。如果实参是一个左值,T1是一个左值引用类型,forward返回的类型是一个左值引用,结果是一个左值引用。

c++ 复制代码
template <typename F,typename T1,typename T2>
void fun(F f,T1 &&t1,T2 &&t2){
  f(std::forward<T2>(t2),std::forward<T1>(t1));
}

重载与模板

  • 模板函数重载的时候编译器优先选择精确匹配的函数模板(不需要类型转换),如果都是精确匹配的,再选择更特例化的模板==(例如const T&本质上可以用于任何类型,包括指针类型,而T*只能用于指针类型,因此编译器会选择T*为函数形参的模板函数)==
  • 实参从数组类型或函数类型转换成对应的指针类型是精确匹配的。
  • 在定义任何函数之前,记得声明所有重载的函数版本。这样就不必担心编译器由于未遇到你希望调用的函数而实例化一个并非你所需的版本(通过模板实例化出并不需要的版本)。

可变参数模板

  • 编译器会通过函数调用时传递的实参数量自动推断出模板参数包的类型和数量以及函数参数包的数量。
  • 调用sizeof...运算符可以得到函数参数的数目或类型参数的数目(模板类型参数)
c++ 复制代码
//Args是模板类型参数包,rest是函数参数包
//Args表示零个或多个函数类型参数
//rest表示零个或多个函数参数
template <typename T,typename... Args>
void foo(const T&, const Args& ... rest);
	cout<<sizeof...(Args)<<endl;
	cout<<sizeof...(res)<<endl;
  • 我们可以使用initializer_list来定义接收可变参数的函数,但是这些参数必须具有相同的类型。我们可以编写可变类型参数的模板作为接受可变参数的函数(这些参数类型不同)

**包扩展:**我们在参数包后面用...来展开包。注意debug_rep(rest)...和debug_rep(rest...)的扩展含义不同,后者表示在debug_rep函数内扩展rest参数

**转发参数包:**我们可以组合使用可变参数模板与forward机制来编写函数,实现将其实参不变地传递给其他函数。

模板特例化

  • 在某些情况下,通用模板的定义对特定类型是不适合的。当我们不能(或不希望)使用模板版本时,可以定义类或函数模板的一个特例化版本。

  • 一个特例化版本本质上是一个实例,而非函数名的一个重载版本。

  • 模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是这些模板的特例化版本。

c++ 复制代码
//模板为
template <typename T>int compare(const T&, const T&);
//我们想要特例化的模板中T类型为 char* const。即底层const指针。
template<>
int compare(const char* const & p1. );//指向const类型的const指针。

特例化类模板:

  • 特例化类模板,我们必须在原模板定义所在的命名空间特例化,以特列化hash类模板来计算我们自定义类型的hash时:

  • 与函数模板不同,类模板的特例化不必为所有模板参数提供实参。我们可以只指定一部分而非所有模板参数,或是参数的一部分而非全部特性。一个类模板的部分特例化(partial specialization)本身是一个模板

  • 部分特例化版本的模板参数列表是原始模板的参数列表的一个子集或者是一个特例化版本。在本例中,特例化版本的模板参数的数目与原始模板相同,但是类型不同。两个特例化版本分别用于左值引用和右值引用类型

特例化成员而不是类:

  • 我们可以只特例化特定成员函数而不是特例化整个模板。例如,如果Foo是一个模板类,包含一个成员Bar,我们可以只特例化该成员:

相关推荐
愛~杦辷个訾1 小时前
芋道项目,商城模块数据表结构
java·sql·芋道·yudao-cloud·芋道商城
TIF星空1 小时前
【使用 C# 获取 USB 设备信息及进行通信】
开发语言·经验分享·笔记·学习·microsoft·c#
Smile丶凉轩3 小时前
Qt 界面优化(绘图)
开发语言·数据库·c++·qt
reasonsummer3 小时前
【办公类-100-01】20250515手机导出教学照片,自动上传csdn+最大化、最小化Vs界面
开发语言·python
C_Liu_4 小时前
C语言:深入理解指针(5)
java·c语言·算法
small_wh1te_coder4 小时前
从经典力扣题发掘DFS与记忆化搜索的本质 -从矩阵最长递增路径入手 一步步探究dfs思维优化与编程深度思考
c语言·数据结构·c++·stm32·算法·leetcode·深度优先
苏三福5 小时前
ros2 hunmle bag 数据包转为图片数据 python版
开发语言·python·ros2humble
佛祖保佑永不宕机5 小时前
麒麟系统ARM64架构部署mysql、jdk和java项目
java·arm
qqxhb6 小时前
零基础学Java——第十一章:实战项目 - 桌面应用开发(JavaFX入门)
java·开发语言·javafx
大神薯条老师6 小时前
Python零基础入门到高手8.4节: 元组与列表的区别
开发语言·爬虫·python·深度学习·机器学习·数据分析