《C++新经典对象模型》之第6章 对象构造语义学

《C++新经典对象模型》之第6章 对象构造语义

  • [6.1 继承体系下的对象构造](#6.1 继承体系下的对象构造)
    • [6.1.1 对象的构造顺序](#6.1.1 对象的构造顺序)
    • [6.1.2 虚函数](#6.1.2 虚函数)
    • [6.1.3 构造函数中对虚函数的调用](#6.1.3 构造函数中对虚函数的调用)
  • [6.2 对象复制语义学与析构函数语义学](#6.2 对象复制语义学与析构函数语义学)
    • [6.2.1 对象的默认复制行为](#6.2.1 对象的默认复制行为)
    • [6.2.2 拷贝赋值运算符与拷贝构造函数](#6.2.2 拷贝赋值运算符与拷贝构造函数)
    • [6.2.3 禁止对象的拷贝构造和赋值](#6.2.3 禁止对象的拷贝构造和赋值)
    • [6.2.4 析构函数语义](#6.2.4 析构函数语义)
  • [6.3 局部对象、全局对象的构造和析构](#6.3 局部对象、全局对象的构造和析构)
    • [6.3.1 局部对象的构造和析构](#6.3.1 局部对象的构造和析构)
    • [6.3.2 全局对象的构造和析构](#6.3.2 全局对象的构造和析构)
    • [6.4 局部静态对象、对象数组构造析构和内存分配](#6.4 局部静态对象、对象数组构造析构和内存分配)
      • [6.4.1 局部静态对象的构造和析构](#6.4.1 局部静态对象的构造和析构)
      • [6.4.2 局部静态对象数组的内存分配](#6.4.2 局部静态对象数组的内存分配)
    • [6.5 new、delete运算符与内存高级话题](#6.5 new、delete运算符与内存高级话题)
    • [6.6 临时性对象的详细探讨](#6.6 临时性对象的详细探讨)
      • [6.6.1 拷贝构造函数相关的临时性对象](#6.6.1 拷贝构造函数相关的临时性对象)
      • [6.6.2 拷贝赋值运算符相关的临时性对象](#6.6.2 拷贝赋值运算符相关的临时性对象)
      • [6.6.3 直接运算产生的临时性对象](#6.6.3 直接运算产生的临时性对象)

6.1 继承体系下的对象构造

6.1.1 对象的构造顺序

从父类到子类,从根源到末端。

先成员变量(定义顺序,从上到下),后构造函数(先初始化列表,后函数体)。

析构顺序:与构造顺序相反。

从子类到父类,先析构函数,后成员变量(从下到上)。

cpp 复制代码
C::成员变量1构造函数	//7
C::成员变量2构造函数	//8
C::C()	//9
	B::成员变量1构造函数	//4
	B::成员变量2构造函数	//5
	B::B()	//6
		A::成员变量1构造函数	//1
		A::成员变量2构造函数	//2
		A::A()	//3
			构造函数初始化列表
			构造函数函数体
		
		A::~A()	//7
		A::成员变量2析构函数	//8
		A::成员变量1析构函数	//9
	B::~B()	//4
	B::成员变量2析构函数	//5
	B::成员变量1析构函数	//6
C::~C()	//1
C::成员变量2析构函数	//2
C::成员变量1析构函数	//3

6.1.2 虚函数

给虚函数表指针赋值的语句,编译器会插入到构造函数的函数体之前。

cpp 复制代码
C::C()
	B::B()	
		A::A()
			vptr = A::vftable;	//虚函数表指针赋值
			A构造函数初始化列表
			A构造函数函数体
		vptr = B::vftable;
		B构造函数初始化列表
		B构造函数函数体
	vptr = C::vftable;
	C构造函数初始化列表
	C构造函数函数体

6.1.3 构造函数中对虚函数的调用

(1)构造函数中调用虚函数,并不通过虚函数表来调用,而是直接调用(虚函数有真实地址)。

(2)在构造函数中调用的虚函数从所在类往根类回溯,逐次找这个虚函数,找到哪个就直接调用(静态方式)。

(3)构造函数中调用虚函数时,对象未构造完整,不宜采用虚函数表机制调用虚函数。

(4)不要在类的构造函数和析构函数中调用虚函数。

cpp 复制代码
#include <iostream>
using namespace std;

namespace _n1
{
	class TA
	{
	public:
		TA()
		{
			cout << "TA::TA(), this = " << this << endl;
		}
		~TA()
		{
			cout << "TA::~TA(), this = " << this << endl;
		}
	};

	class TB
	{
	public:
		TB()
		{
			cout << "TB::TB(), this = " << this << endl;
		}
		~TB()
		{
			cout << "TB::~TB(), this = " << this << endl;
		}
	};

	class T1
	{
	public:
		T1()
		{
			cout << "T1::T1(), this = " << this << endl;
		}
		~T1()
		{
			cout << "T1::~T1(), this = " << this << endl;
		}
	};

	class T2 : public T1
	{
	public:
		T2()
		{
			cout << "T2::T2(), this = " << this << endl;
		}
		~T2()
		{
			cout << "T2::~T2(), this = " << this << endl;
		}
	};

	class T3 : public T2
	{
	public:
		T3()
		{
			cout << "T3::T3(), this = " << this << endl;
		}
		~T3()
		{
			cout << "T3::~T3(), this = " << this << endl;
		}

	private:
		TA ta;
		TB tb;
	};
}

namespace _n2
{
	class A
	{
	public:
		A()
		{
			myvirfunc();
			cout << "A::A(), this = " << this << endl;
		}
		virtual ~A()
		{
			myvirfunc();
			cout << "A::~A(), this = " << this << endl;
		}

	public:
		virtual void myvirfunc()
		{
			myvirfunc2();
			cout << "A::myvirfunc(), this = " << this << endl;
		}
		virtual void myvirfunc2()
		{
			cout << "A::myvirfunc2(), this = " << this << endl;
		}
	};

	class B : public A
	{
	public:
		B()
		{
			myvirfunc();
			cout << "B::B(), this = " << this << endl;
		}
		virtual ~B()
		{
			myvirfunc();
			cout << "B::~B(), this = " << this << endl;
		}

	public:
		virtual void myvirfunc()
		{
			myvirfunc2();
			cout << "B::myvirfunc(), this = " << this << endl;
		}
		virtual void myvirfunc2()
		{
			cout << "B::myvirfunc2(), this = " << this << endl;
		}
	};

	class C : public B
	{
	public:
		C() : m_c(11)
		{
			myvirfunc(); // 调用一个虚函数
			cout << "C::C(), this = " << this << endl;
		}
		virtual ~C()
		{
			myvirfunc();
			cout << "C::~C(), this = " << this << endl;
		}

	public:
		int m_c;

	public:
		virtual void myvirfunc()
		{
			myvirfunc2();
			cout << "C::myvirfunc(), this = " << this << endl;
		}
		virtual void myvirfunc2()
		{
			cout << "C::myvirfunc2(), this = " << this << endl;
		}
	};
}

int main()
{
	if (0)
	{
		_n1::T3 t3;
	}

	if (0)
	{
		_n2::C c;
	}

	if (1)
	{
		_n2::A *mycobj = new _n2::C();
		mycobj->myvirfunc();
		delete mycobj;
	}

	return 0;
}
06.01.cpp
cpp 复制代码
#include <cstdio>
#include <iostream>
using namespace std;

struct A
{
	A() { printf("A::A(), this = %p\n", this); }
	virtual ~A() {}

	virtual void myvirfunc2()
	{
		printf("A::myvirfunc2()\n");
	}
	virtual void myvirfunc()
	{
		myvirfunc2();
		printf("A::myvirfunc()\n");
	}
};
struct B : A
{
	B()
	{
		myvirfunc();
		printf("B::B(), this = %p\n", this);
	}
	virtual ~B() {}

	virtual void myvirfunc2()
	{
		printf("B::myvirfunc2()\n");
	}
	virtual void myvirfunc()
	{
		myvirfunc2();
		printf("B::myvirfunc()\n");
	}
};
struct C : B
{
	int m_c;
	C()
		: m_c(11)
	{
		myvirfunc(); // 调用一个虚函数
		printf("C::C(), this = %p\n", this);
	}
	virtual ~C()
	{
		myvirfunc();
	}

	virtual void myvirfunc2()
	{
		printf("C::myvirfunc2()\n");
	}
	virtual void myvirfunc()
	{
		myvirfunc2();
		printf("C::myvirfunc()\n");
	}
};

int main()
{
	// C c;

	C *mycobj = new C();
	// mycobj->myvirfunc();
	delete mycobj;

	cout << "Over!\n";
	return 0;
}

6.2 对象复制语义学与析构函数语义学

6.2.1 对象的默认复制行为

无拷贝构造函数和拷贝赋值运算符时,默认的对象复制行为会发挥作用。

6.2.2 拷贝赋值运算符与拷贝构造函数

提供拷贝赋值运算符或拷贝构造函数时,需提供默认构造函数(此时编译器不会生成)。

cpp 复制代码
struct JI
{
	JI() { cout << "JI::JI()" << endl; }
	virtual ~JI() { cout << "JI::~JI()" << endl; }
};

struct A : JI
{
	int m_i, m_j;
	A() { cout << "A::A()" << endl; }
	~A() { cout << "A::~A()" << endl; }

	A &operator=(const A &tmp)
	{
		if (&tmp == this)
			return *this;

		// static_cast<JI&>(*this) = tmp; // 调用父类的拷贝赋值运算符
		// JI::operator=(tmp);// 调用父类的拷贝赋值运算符

		m_i = tmp.m_i;
		m_j = tmp.m_j;
		cout << "A::operator=(const A&)" << endl;
		return *this;
	}
	A(const A &tmp)
	//: JI(tmp) // 显式调用父类拷贝构造函数
	{
		// 编译器会插入代码,父类按位复制,调用父类的构造函数或者拷贝构造函数?
		m_i = tmp.m_i;
		m_j = tmp.m_j;
		cout << "A::A(const A&)" << endl;
	}
};

6.2.3 禁止对象的拷贝构造和赋值

(1)类中声明私有的拷贝构造函数和拷贝赋值运算符,无函数体。

cpp 复制代码
class A {
	private:
		A& operator=(const A&);
		A(const A&);
};

(2)c++11,=delete,将拷贝构造函数和拷贝赋值运算符标记为禁用。

cpp 复制代码
class A {
	public:
		A& operator=(const A&) =delete;
		A(const A&) =delete;
};

6.2.4 析构函数语义

  1. 析构函数被合成

    编译器合成析构函数的情况:

    (1)类继承的父类具有析构函数,会合成析构函数并调用父类的析构函数。

    (2)类中类类型成员变量具有析构函数,会合成析构函数并调用该成员变量所属类的析构函数。

  2. 析构函数被扩展

    已写析构函数时,向其中增加代码的情况:

    (1)类继承的父类具有析构函数,会在析构函数后面插入代码,调用父类的析构函数。

    (2)类中类类型成员变量具有析构函数,会在析构函数后面插入代码,调用该成员变量所属类的析构函数。

06.02.cpp
cpp 复制代码
#include <iostream>
using namespace std;

struct JI
{
	JI() { cout << "JI::JI()" << endl; }
	virtual ~JI() { cout << "JI::~JI()" << endl; }
};

struct A : JI
{
	int m_i, m_j;

	// private:
	//	A& operator=(const A& tmp);
	//	A(const A& tmptime);

	// public:
	//	A& operator=(const A& tmp) = delete;
	//	A(const A& tmptime) = delete;

	A() { cout << "A::A()" << endl; }
	~A() { cout << "A::~A()" << endl; }

	A &operator=(const A &tmp)
	{
		if (&tmp == this)
			return *this;

		// static_cast<JI&>(*this) = tmp; // 调用父类的拷贝赋值运算符
		// JI::operator=(tmp);// 调用父类的拷贝赋值运算符

		m_i = tmp.m_i;
		m_j = tmp.m_j;
		cout << "A::operator=(const A&)" << endl;
		return *this;
	}
	A(const A &tmp)
	//: JI(tmp) // 显式调用父类拷贝构造函数
	{
		// 编译器会插入代码,调用父类的构造函数或者拷贝构造函数?
		m_i = tmp.m_i;
		m_j = tmp.m_j;
		cout << "A::A(const A&)" << endl;
	}
};

struct ParC
{
	virtual ~ParC() { cout << "ParC::~ParC()" << endl; }
};
struct MemC
{
	ParC m_j;

	~MemC() { cout << "MemC::~MemC()" << endl; }
};

int main()
{
	{
		A aobj;
		aobj.m_i = 15;
		aobj.m_j = 20;

		A aobj2 = aobj; // 执行拷贝构造

		A aobj3;
		aobj3.m_i = 13;
		aobj3.m_j = 16;
		aobj2 = aobj3; // 执行拷贝复制运算符
	}

	{
		MemC mobj;
	}

	cout << "Over!\n";
	return 0;
}

6.3 局部对象、全局对象的构造和析构

6.3.1 局部对象的构造和析构

只要超出了对象的作用域,编译器总会在适当的地方插入调用对象析构函数的代码。

尽量把对象定义在需要立即用到它的代码段的附件。

6.3.2 全局对象的构造和析构

全局对象的初始化和释放过程:

(1)全局对象获得地址(可执行文件中确定的,和堆、栈中分配内存不一样,程序运行期间一直存在)。

(2)全局对象内存清零(静态初始化)。

(3)调用全局对象对应类的构造函数。

(4)执行main函数。

(5)main函数执行完毕后,调用全局对象对应类的析构造函数。

(6)整个可执行程序执行完毕。

06.03.cpp
cpp 复制代码
#include <iostream>
using namespace std;

struct A
{
	int m_i;
	A() { cout << "A::A()" << endl; }
	~A() { cout << "A::~A()" << endl; }
};

void myfunc()
{
	// A obja; // 这里定义不合适
	if (1 == 1)
	{
		// 这里会被编译器插入调用obja对象析构函数的代码,影响执行效率完全没必要
		return;
	}

	A obja; // 这里定义合适
	obja.m_i = 10;
	cout << "obja.m_i = " << obja.m_i << endl;
	return;
}

A g_aobj;

int main()
{
	{
		A obja;
		int mytest = 1;
		if (mytest == 0)
			return 0;
		myfunc();
	}

	g_aobj.m_i = 6; //

	cout << "Over!\n";
	return 0;
}

6.4 局部静态对象、对象数组构造析构和内存分配

6.4.1 局部静态对象的构造和析构

只有当函数调用时,局部静态对象的构造函数才会执行。

多次调用,只有第一次才构造。

编译时内存开始地址和大小已确定(BSS段),运行到对应代码时,才从事先约定好的地址分配出来。

构造实现方式:局部静态对象地址旁增加字节做标记(是否已构造),第一次构造,第二次标记存在,不构造,跳过static A s_aobj;代码行。

析构实现方式:第一次构造时,增加代码_atexit登记信息,main函数执行完后执行析构。

6.4.2 局部静态对象数组的内存分配

同局部静态对象类似。

但编译器优化,大数组不分配实际的物理地址,对象数组做有用事情时,才实际分配。

06.04.cpp
cpp 复制代码
#include <cstdio>
#include <iostream>
using namespace std;

struct A
{
	int m_i;
	// A() { cout << "A::A()" << endl; }
	//~A() { cout << "A::~A()" << endl; }
};

void myfunc1()
{
	static A s_aobj1;
	static A s_aobj2;
	printf("&s_aobj1=%p\n", &s_aobj1);
	printf("&s_aobj2=%p\n", &s_aobj2);
}
const A &myfunc2()
{
	static A s_aobj1;
	printf("&s_aobj1=%p\n", &s_aobj1);
	return s_aobj1;
}

void myfunc()
{
	static A s_aobj[1000'0000]; // '是数字分隔符, C++14
	for (int i = 0; i < 1000'0000; i++)
		s_aobj[i].m_i = i;//大数组做有用事情时,才实际分配内存
	printf("s_aobj = %p\n", s_aobj);
}

int main()
{
	myfunc1();
	myfunc1();

	printf("&A=%p\n", &myfunc2());
	printf("&A=%p\n", &myfunc2());

	myfunc();
	myfunc();

	cout << "Over!\n";
	return 0;
}

6.5 new、delete运算符与内存高级话题

void *p = malloc(0);
char *q = new char[0];

new内部调用malloc,两行代码几乎等价。

代码诡异,可能报错,别使用。

06.05.cpp
cpp 复制代码
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;

int main()
{
	void *p = malloc(0);
	char *q = new char[0];

	strcpy_s((char *)p, 100, "This is a test1!");
	strcpy_s(q, 100, "This is a test2!");

	printf("p=%s\n", p);
	printf("q=%s\n", q);

	delete[] q;
	free(p);

	cout << "Over!\n";
	return 0;
}

6.6 临时性对象的详细探讨

6.6.1 拷贝构造函数相关的临时性对象

cpp 复制代码
struct A
{
	int m_i;
	A() { cout << "A::A()" << endl; }
	~A() { cout << "A::~A()" << endl; }

	A(const A &tmp)
	{
		m_i = tmp.m_i;
		cout << "A::A(const A&)" << endl;
	}
	A &operator=(const A &tmp)
	{
		m_i = tmp.m_i;
		cout << "A::operaotor=(const A&)" << endl;
		//printf("&tmp=%p\n", &tmp);
		return *this;
	}
};
A operator+(const A &obj1, const A &obj2)
{
	// cout << "A operaotor+(const A&, const A &)" << endl;

	A tmp; // 一次构造函数
	tmp.m_i = obj1.m_i + obj2.m_i;

	printf("&tmp=%p\n", &tmp);
	return tmp; // 一次拷贝构造函数+一次析构函数(编译器优化后可能无临时对象)
}

if (1)
	{
		A myobj1;
		printf("&myobj1=%p\n", &myobj1);

		A myobj2;
		printf("&myobj2=%p\n", &myobj2);

		A resultobj1 = myobj1 + myobj2;//编译器优化后直接将tmp的地址用作resultobj1的地址,节省一次复制构造+一次析构
		cout << "------------------------\n";
		printf("&resultobj1=%p\n", &resultobj1);
	}

6.6.2 拷贝赋值运算符相关的临时性对象

cpp 复制代码
struct A
{
	int m_i;
	A() { cout << "A::A()" << endl; }
	~A() { cout << "A::~A()" << endl; }

	A(const A &tmp)
	{
		m_i = tmp.m_i;
		cout << "A::A(const A&)" << endl;
	}
	A &operator=(const A &tmp)
	{
		m_i = tmp.m_i;
		cout << "A::operaotor=(const A&)" << endl;
		//printf("&tmp=%p\n", &tmp);
		return *this;
	}
};
A operator+(const A &obj1, const A &obj2)
{
	// cout << "A operaotor+(const A&, const A &)" << endl;

	A tmp; // 一次构造函数
	tmp.m_i = obj1.m_i + obj2.m_i;

	printf("&tmp=%p\n", &tmp);
	return tmp; // 一次拷贝构造函数+一次析构函数(编译器优化后可能无临时对象)
}

if (1)
	{
		A myobj1;
		printf("&myobj1=%p\n", &myobj1);

		A myobj2;
		printf("&myobj2=%p\n", &myobj2);

		A resultobj2;
		
		resultobj2 = myobj1 + myobj2;//一次构造函数+一次拷贝赋值运算符+一次析构
		cout << "------------------------\n";
		printf("&resultobj2=%p\n", &resultobj2);
	}

6.6.3 直接运算产生的临时性对象

cpp 复制代码
if (1)
	{
		A myobj1;
		A myobj2;
		myobj1 + myobj2;//一次构造+一次析构
	}
	
if (1)
	{
		A myobj1;
		myobj1.m_i = 1;

		A myobj2;
		myobj2.m_i = 2;

		printf("(myobj1 + myobj2).m_i = %d\n", (myobj1 + myobj2).m_i); // 临时对象(operator+,一次构造+一次析构,且析构在printf后执行)
	}
	
if (1)
	{
		A myobj1;
		myobj1.m_i = 1;

		A myobj2;
		myobj2.m_i = 2;

		if ((myobj1 + myobj1).m_i > 3 || (myobj1 + myobj2).m_i > 5) // 两个临时对象
			cout << "Condition established" << endl;
		else
			cout << "Condition not established" << endl;
	}
	
if (1)
	{
		A myobj1;
		myobj1.m_i = 1;

		A myobj2;
		myobj2.m_i = 2;

		if ((myobj1 + myobj1).m_i > 1 || (myobj1 + myobj2).m_i > 5) // 一个临时对象
			cout << "Condition established" << endl;
	}

编译器会在必要的地方插入代码产生临时对象,完成要实现的意图。

  1. 临时对象被摧毁
cpp 复制代码
if (0)
	{
		const char *p = (string("123") + string("456")).c_str(); // error,临时对象被摧毁了
		printf("p = %s\n", p);

		string aaa = string("123") + string("456");
		const char *q = aaa.c_str(); // ok
		printf("q = %s\n", q);
	}
  1. 临时对象因绑定到引用而被保留
cpp 复制代码
if (0)
	{
		const string &aaa2 = string("123") + string("456");
		printf("aaa2 = %s\n", aaa2.c_str());
	}
06.06.cpp
cpp 复制代码
#include <cstdio>
#include <iostream>
using namespace std;

struct A
{
	int m_i;
	A() { cout << "A::A()" << endl; }
	~A() { cout << "A::~A()" << endl; }

	A(const A &tmp)
	{
		m_i = tmp.m_i;
		cout << "A::A(const A&)" << endl;
	}
	A &operator=(const A &tmp)
	{
		m_i = tmp.m_i;
		cout << "A::operaotor=(const A&)" << endl;
		// printf("&tmp=%p\n", &tmp);
		return *this;
	}
};
A operator+(const A &obj1, const A &obj2)
{
	// cout << "A operaotor+(const A&, const A &)" << endl;

	A tmp; // 一次构造函数
	tmp.m_i = obj1.m_i + obj2.m_i;

	printf("&tmp=%p\n", &tmp);
	return tmp; // 一次拷贝构造函数+一次析构函数(编译器优化后可能无临时对象)
}

int main()
{
	if (0)
	{
		A myobj1;
		printf("&myobj1=%p\n", &myobj1);

		A myobj2;
		printf("&myobj2=%p\n", &myobj2);

		A resultobj1 = myobj1 + myobj2; // 编译器优化后直接将tmp的地址用作resultobj1的地址,节省一次复制构造+一次析构
		cout << "------------------------\n";
		printf("&resultobj1=%p\n", &resultobj1);
	}

	if (0)
	{
		A myobj1;
		printf("&myobj1=%p\n", &myobj1);

		A myobj2;
		printf("&myobj2=%p\n", &myobj2);

		A resultobj2;

		resultobj2 = myobj1 + myobj2; // 一次构造函数+一次拷贝赋值运算符+一次析构
		cout << "------------------------\n";
		printf("&resultobj2=%p\n", &resultobj2);
	}
	if (0)
	{
		A myobj1;
		A myobj2;
		myobj1 + myobj2;
	}

	if (0)
	{
		A myobj1;
		myobj1.m_i = 1;

		A myobj2;
		myobj2.m_i = 2;

		printf("(myobj1 + myobj2).m_i = %d\n", (myobj1 + myobj2).m_i); // 临时对象(operator+,一次构造+一次析构,且析构在printf后执行)
	}
	if (0)
	{
		A myobj1;
		myobj1.m_i = 1;

		A myobj2;
		myobj2.m_i = 2;

		if ((myobj1 + myobj1).m_i > 3 || (myobj1 + myobj2).m_i > 5) // 两个临时对象
			cout << "Condition established" << endl;
		else
			cout << "Condition not established" << endl;
	}
	if (1)
	{
		A myobj1;
		myobj1.m_i = 1;

		A myobj2;
		myobj2.m_i = 2;

		if ((myobj1 + myobj1).m_i > 1 || (myobj1 + myobj2).m_i > 5) // 一个临时对象
			cout << "Condition established" << endl;
	}

	if (0)
	{
		const char *p = (string("123") + string("456")).c_str(); // error,临时对象被摧毁了
		printf("p = %s\n", p);

		string aaa = string("123") + string("456");
		const char *q = aaa.c_str(); // ok
		printf("q = %s\n", q);
	}

	if (0)
	{
		const string &aaa2 = string("123") + string("456");
		printf("aaa2 = %s\n", aaa2.c_str());
	}

	cout << "Over!\n";
	return 0;
}
相关推荐
OTWOL4 分钟前
两道数组有关的OJ练习题
c语言·开发语言·数据结构·c++·算法
问道飞鱼7 分钟前
【前端知识】强大的js动画组件anime.js
开发语言·前端·javascript·anime.js
拓端研究室7 分钟前
R基于贝叶斯加法回归树BART、MCMC的DLNM分布滞后非线性模型分析母婴PM2.5暴露与出生体重数据及GAM模型对比、关键窗口识别
android·开发语言·kotlin
Code成立9 分钟前
《Java核心技术I》Swing的网格包布局
java·开发语言·swing
Auc2413 分钟前
使用scrapy框架爬取微博热搜榜
开发语言·python
QQ同步助手20 分钟前
C++ 指针进阶:动态内存与复杂应用
开发语言·c++
凯子坚持 c26 分钟前
仓颉编程语言深入教程:基础概念和数据类型
开发语言·华为
小爬虫程序猿28 分钟前
利用Java爬虫速卖通按关键字搜索AliExpress商品
java·开发语言·爬虫
程序猿-瑞瑞30 分钟前
24 go语言(golang) - gorm框架安装及使用案例详解
开发语言·后端·golang·gorm
qq_4335545431 分钟前
C++ 面向对象编程:递增重载
开发语言·c++·算法