IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++ 学习记录19 -> 正文阅读

[C++知识库]C++ 学习记录19

第十九章? 特殊工具与技术

19.1? ? 控制内存分配

19.1.1 重载new? delete

控制内存分配和回收, 就是重载new/delete函数. 必须保证这两个函数的正确性.

编译器优先使用当前作用域内的自定义new/delete函数, 然后外层作用域, 当全局作用域内仍然找不到时将调用标准库定义的版本.

可以使用::new? ::delete直接使用全局作用域内的版本.

  • void *operator new (size_t, nothrow_t&) noexcept;
  • void * operator new[](size_t, nothrow_t&) noexcept;
  • void * operator delete(void* , nothrow_t &) noexcept;
  • void * operator delete[](void * , nothrow_t&) noexcept;

以上版本不会抛出异常, 会抛出异常的版本未列出.

nothrow_t? new头文件中. 用户可以通过这个对象请求new的非抛出版本.

当重载new/delete时, 必须制定其不抛出异常? noexcept

如果定义成类的成员时,? 默认是隐式静态的 且不能操作任何数据成员.

operator new / operator new []? 返回值必须是void *,? 第一个形参必须是size_t, 不能含有默认值. 可以添加额外的形参.

void * operator new(size_t, void*) 此种形式不能被重载.

operator delete/delete[]? 返回类型必须是void, 第一个形参必须是void*. 当作为类内成员时, 可以包含另外一个类型为size_t的形参, 初始值是第一个形参所指对象的字节数.

malloc 函数与free函数:

cstdlib头文件中

malloc? 接收一个表示待分配字节数的size_t, 返回指向分配空间的指针或者0 表示分配失败.

free 接收一个void*, malloc返回的指针的副本,? 将相关的内存返回给系统.

重新定义operator new /delete的一种版本 利用malloc /free

void* operator new (size_t size) {
	if (void* mem = malloc(size)) return mem;
	else throw bad_alloc();
}
void operator delete(void* mem) noexcept { free(mem); }

19.1.2 定位new表达式

鉴于operator new 只分配空间并不构建空间内的对象. 如果想在一个特定的,预先分配的内存地址上构建对象, 需要使用定位new

  • new (place_address) type
  • new (place_address) type (initializers)
  • new (place_address) type [size]
  • new (place_address) type?[size] { braced initializer list}

place_address 必须是一个指针, 同时在initializers中提供一个(可以为空, 此时只构建对象但不分配内存)以逗号分隔的初始值列表, 该列表将用于构建新分配的对象.

定位new和allocator的construct成员很像, 两者有一个重要的区别:

  • construct的指针必须指向同一个allocator对象分配的空间
  • 定位new的指针无须指向operator new 分配的内存 甚至不需要指向动态内存

显式的析构函数调用:

	string* sp = new string("hello world!");
	sp->~string(); // 显式调用string的析构函数

和distroy类似, 调用析构函数可以清除对象但不会回收内存.

19.2? ? 运行时类型识别(run-time type identification, RTTI)

依赖于一下两个运算符实现:

  • typeid 运算符, 用于返回表达式的类型
  • dynamic_cast运算符, 用于将基类的指针/引用安全的转换成派生类的指针/引用.

将这两个运算符用于某种类型的指针/引用时,? 且 该类型含有虚函数时,? 运算符将使用指针/引用所绑定对象的动态类型.

一般应用于:? 使用基类对象的指针/引用 执行派生类的非虚函数操作.(虚函数操作 直接调用了, 不用使用这些函数).

RTTI只是一种手段, 尽量不要使用, 如果能够使用虚函数的方式, 尽量不用RTTI.

19.2.1 dynamic_cast

  • dynamic_cast<type*>(e)? ? e必须是一个有效的指针
  • dynamic_cast<type&>(e)? ? e必须是一个左值
  • dynamic_cast<type&&>(e)? ? e不能是左值

type 类类型, 且通常情况下含有虚函数. e的要求(符合任意一个):

  • e的类型是 type的公有派生类
  • e的类型是 type的公有基类
  • e的类型是 type的类型.

转换目标是指针类型: 转换失败 返回0, 转换类型是引用类型: 转换失败 扔出 bad_cast错误

指针类型的dynamic_cast:

空指针执行dynamic_cast, 结果是所需类型的空指针.

最好是在条件部分指向dynamic_cast, 例如: Base类至少含有一个虚函数, Derived是Base的派生类.

if (Derived* dp = dynamic_cast<Derived*> (bp)) {
	// 转换成功
	// xx
}
else {
	// 转换失败
	// xx
}

引用类型的dynamic_cast:

try {
	const Derived& d = dynamic_cast<const Derived&>(b);
	// 转换成功
}
catch (bad_cast) {
	// 转换失败
}

19.2.2 typeid 运算符

返回表达式的对象类型:? typeid(e), e任意表达式或类型,对象的名字.

  • 当运算对象不属于类类型或是一个不包含任何虚函数的类时, typeid运算符指示的是运算对象的静态类型.
  • 当运算对象定义了虚函数的类的左值时, typeid的结果知道运行时才会求得.
  • 当运算对象是数组或函数时, 不会执行指针的标准类型转换.
  • 顶层const被忽略
  • 如果运算对象是引用, 则返回引用所引对象的类型.

使用typeid运算符:

通常情况下 用于比较两条表达式的类型是否相同, 或者比较一条表达式的类型是否与指定类型相同.

指针p所指向的对象如果不包含虚函数, 则p不必是一个有效的指针,? 如果p指向的对象类型包含虚函数, 则p所指向的对象需要再去爱运行时求值, 且 p必须是一个有效指针.

19.2.3 使用RTTI

class Base {
	friend bool operator==(const Base&, const Base&);
public:
protected:
	virtual bool equal(const Base& b) const {};
};
class Derived :public Base {
protected:
	bool equal(const Base& b) const {
		auto r = dynamic_cast<const Derived&>(b);
        // 其余比较运算, 返回true/false 
	};
};
bool operator==(const Base& lhs, const Base& rhs) {
	return typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
}

19.2.4 type_info 类

定义在typeinfo头文件中

  • t1 == t2? ? 如果type_info对象t1和t2 表示同一种类型, 返回true, 否则 false
  • t1 != t2? ? 如果type_info对象t1和t2 表示不同的类型返回true, 否则false
  • t.name()? ? 返回一个C风格字符串, 表示类型名字的可打印形式.
  • t1.before(t2)? ? t1是否位于t2之前 是 true 否 false

唯一的创建方法是使用typeid运算符, typeid的运算结果类. 如

	int arr[10];
	Derived d;
	Base* p = &d;
	cout << typeid(42).name() << endl
		<< typeid(arr).name() << endl
		<< typeid(std::string).name() << endl
		<< typeid(p).name() << endl
		<< typeid(*p).name() << endl;
/*  输出
	int
	int[10]
	class std::basic_string<char, struct std::char_traits<char>, class std::allocator<char> >
	class Base*
	class Derived
*/

19.3? ? 枚举类型

枚举属于字面值常量类型.

限定作用域 和不限定作用域? 两种定义方式如下:

// 限定作用域
enum class open_modes {
	input,
	output,
	append
};
// 不限定作用域
enum open_color {
	red,
	yellow,
	green
};
// 不限定作用域的名称也可以省略
enum {
	west = 1,
	south = 2,
	north =3
};

枚举成员:

作用域相同的枚举成员不可相同.

默认 枚举成员从0开始递增, 也可以指定值. 枚举值可以重复.

枚举成员是const的常量值.

枚举也定义新的类型:

枚举成员不能赋值常量值, 可以使用该类型的成员赋值:

	open_color o = 2;  // 错误
	open_color oc1 = open_modes::append; // 类型不同
	open_color oc2 = open_color::red; // 正确

不限定作用域的枚举成员自动地转换成整型.

指定enum的大小:

可以在enum后面指定枚举成员的数据类型(限定作用域的enum 未指定 则是int, 未限定作用域的 则不存在默认类型.):

enum ullDataType : unsigned long long {
	dt1,  // 每个成员都是unsigned long long
	dt2,
	dt3
};

枚举类型的前置声明:

enum的前置声明必须制定其成员的大小.

enum preDeclear : unsigned long long;
enum class preDeclear2 : int;

enum的前置声明和定义必须一致.

形参匹配与枚举类型:

枚举成员能够精确匹配 枚举类型的形参. 整型不能传递给枚举类型的形参, 但是枚举成员可以传递给整型形参.

enum Tokens {INLINE = 128, VIRTUAL = 256};
void ff(Tokens);
void ff(int);
void newf(int);
void newf(unsigned char);

int main(int argc, char** argv) {
	Tokens t = Tokens::INLINE;
	ff(128); // 匹配ff(int)
	ff(t); // 匹配 ff(Tokens)
	ff(Tokens::VIRTUAL); // 匹配ff(Tokens)
	newf(Tokens::INLINE); // 匹配newf(int)
}

19.4? ? 类成员指针

成员指针: 可以指向类的非静态成员的指针.

成员指针的类型囊括了类的类型 和成员的类型.

初始化时 令其指向类的某个成员, 但是不指定该成员所属的对象

使用成员指针时, 提供成员所属的对象.

19.4.1 数据成员指针

定义方法:

//声明一个成员指针, 该指针指向Screen内类型是string的成员.
// const 则表示该指针既可以指向常量也可以指向非常量类型
const string Screen::* pdata;  

通常使用函数返回私有成员:

	static const std::string Screen::* data() {
		return &Screen::contents;
	}

19.4.2 成员函数指针

  • 使用classname::*的形式声明一个指向成员函数的指针.
  • 需要指定目标函数的返回类型和形参列表.
  • 如果是const成员, 或引用成员,? 必须包含const限定符/引用限定符.
  • 如果成员存在重载, 必须显式的声明函数类型以明确指出需要使用的是哪个函数.
  • 成员函数和指向该成员的指针之间不存在自动转换的规则(和普通指针不同)

使用成员指针的类型别名:

成员指针函数表:

class Screen {
public:
	typedef std::string::size_type pos;
	char get_cursor() const { return contents[cursor]; }
	char get() const { return 'a'; };
	char get(pos ht, pos wd) const { return 'c'; };
	static const std::string Screen::* data() {	return &Screen::contents;	}
	static const pos Screen::* posData() {	return &Screen::cursor;	}
	const std::string Screen::* pdata = &Screen::contents;
	char (Screen::* pmf)() const;
	char (Screen::* pmf2)(Screen::pos, Screen::pos) const;

	Screen& home() {	cout << "move home" << endl;	return *this;	};
	Screen& forward() {		cout << "move forward" << endl;		return *this;	};
	Screen& back() { cout << "move back" << endl;	return *this;	};
	Screen& up() {	cout << "move up" << endl;	return *this;	};
	Screen& down() {cout << "move down" << endl;return *this;};
	using Action = Screen & (Screen::*) ();
	enum Directions {HOME, FORWARD, BACK, UP , DOWN};
	Screen& move(Directions cm) {return (this->*Menu[cm])();};
private:
	std::string contents;
	pos cursor;
	pos height, width;
	static Action Menu[];
	
};
Screen::Action Screen::Menu[] = {
			&Screen::home,
			&Screen::forward,
			&Screen::back,
			&Screen::up,
			&Screen::down,
};

// 使用
int main(int argc, char** argv) {
	Screen myScreen, * pScreen = &myScreen;
	myScreen.move(Screen::HOME);
	myScreen.move(Screen::FORWARD);
}

19.4.3 将成员函数用作可调用对象

成员指针不是可调用对象.

可以使用function生成一个可调用对象.

	auto fp = &string::empty;
	vector<string>svec = { "a", "bc" };
	find_if(svec.begin(), svec.end(), fp); // 错误, fp是不可调用对象
	function<bool (const string&)> fcn = &string::empty;  // 引用形式
	find_if(svec.begin(), svec.end(), fcn); // 正确, 将成员函数指针转化为可调用对象

1. 使用function 生成可调用对象:

必须制定该对象所能表示的函数类型, 即可调用对象的形式.

可调用对象是一个成员函数时: 第一个形参表示该成员是在哪个对象(一般是隐式的)上执行.

提供给function的形式中必须指明对象是否是以指针或引用的形式传入的.

	vector<string*> pvec;
	function<bool (const string*)> fp = &string::empty; // 指针形式
	find_if(pvec.begin(), pvec.end(), fp);

2. 使用mem_fn 生成一个可调用对象:

可以自己推断可调用对象的类型, 无需用户显式指定.

	find_if(svec.begin(), svec.end(), mem_fn(&string::empty));
	auto f = mem_fn(&string::empty); // 自行推断调用形式
	f(*svec.begin()); // 传入一个string对象, f使用.*调用empty
	f(&svec[0]); // 传入一个string的指针, f使用->*调用empty

3. 使用bind生成一个可调用对象:

	// 生成一个调用对象, 将参数绑定到第一个形参上
	auto f2 = bind(&string::empty, placeholders::_1);
	f2(*svec.begin());
	f2(&svec[0]);

个人理解: 定义了一套类内 同一个一个调用函数的指针, 该指针可以指向类内 相同返回值, 相同参数的一类函数, 可以使得 调用时更加方便.

19.5? ? 嵌套类

定义在一个类中的类. 常用于定义 作为实现部分的类.

  • 嵌套类是一个独立的类, 与外层基本没关系.
  • 外层类的对象和嵌套类的对象是相互独立的.
  • 嵌套类的名字在外层作用域中可见, 在外层作用域之外 不可见.
  • 嵌套类中成员的种类与非嵌套类相同
  • 嵌套类中也有public, private, protected. 外层类没有特殊的访问权限.

嵌套类只能声明在类内部, 但可以定义在外层类 之外的地方.

class Out {
private:
	typedef vector<string>::size_type line_no;

	int out_1;
public:
	int out_2;
	class Out_In {
	private:
		line_no Out_In1 = 0;
		static int static_mem;
	};
};
// 嵌套类中 静态队成员的定义方式
int Out::Out_In::static_mem = 111;

19.6? ? union 一种节省空间的类

一种节省空间的类

  • union定义了一种新类型.
  • 可以有多个数据成员
  • 任意时刻只有一个数据成员有值.
  • 当给union的某个成员赋值后, 其他成员变成未定义状态
  • 默认 成员都是公有的(public).
  • 不能含有引用类型的成员
  • 不能继承自其他类.
  • 不能作为基类.
  • 不能含有虚函数

定义union:

提供了一种有效的途径使得我们可以方便的表示一组类型不同的互斥值(html中的单选框?)

union Token {  // Token 可选 类似enum中的名字
	char cval;
	int ival;
	double dval;
};
// 使用
// 初始化默认初始化 第一个成员, 所以{'a'} 初始化的是cval.
Token first_t = { 'a' }; // 初始化cval
Token second_t; // 未初始化
Token* pt = new Token;  // 指向一个未初始化的Token对象的指针

// 使用类内成员
cout << "first_t:" << first_t.cval << endl;

初始化默认初始化 第一个成员, 所以{'a'} 初始化的是cval.

匿名union:

  • 声明在全局中的匿名union 必须是静态的, 局部作用域中则不必如此.
  • 匿名union 不能包含受保护的成员 私有成员, 也不能定义成员函数.
// 声明在全局中的匿名union 必须是静态的
static union {
	char c_val;
	int i_val;
	double d_val;
};

int main(int argc, char** argv) {
    // 直接使用全局匿名union中的成员
// 输出20
	i_val = 20;
	cout << i_val << endl;
	union {
		char c_val;
		int i_val;
		double d_val;
	};
// 使用内层作用域中的匿名union
	i_val = 30;
// 输出30
	cout << i_val << endl;
}

含有类类型的union:

  • 当包含的是内置类型的成员时, 可以使用普通的赋值语句改变union保存的值.
  • 当包含的是特殊类类型时, 在更改该类型的值时, 需要先调用该类型的构造/析构函数.
  • 如果含有类型A, A自定义了默认的构造,拷贝控制函数, 则编译器自动为union合成对应的版本并声明为删除的.
  • 如果类型A中含有一个union成员, union含有删除的拷贝控制成员, 则A中对应的拷贝控制操作也是删除的.

使用类管理union成员:

// 用于管理union的类
class Token{
public:
	Token() :tok(INT), ival{ 0 } {};
	Token(const Token& t) : tok(t.tok) { copyUnion(t); }
	Token& operator= (const Token& t) {
		// 三种情况: 左侧和右侧都是string, 左右都不是, 左右中有一个是string.
		if (tok == STR && t.tok != STR) sval.~basic_string();
		if (tok == STR && t.tok == STR) sval = t.sval;
		else
		{
			copyUnion(t);
		}
		tok = t.tok;
		return *this;
	};
	~Token() { if (tok == STR) sval.~basic_string(); }
	Token& operator=(const std::string& s) {
		if (tok == STR) sval = s;
		else new(&sval) string(s); // 构造一个string
		tok =STR;
		return *this;
	};
	Token& operator=(char c) {
		// 如果原空间存储的string, 需要释放原空间
		if (tok == STR) sval.~basic_string();
		tok = CHAR;
		cval = c;
		return *this;
	};
	Token& operator=(int i) {
		if (tok == STR) sval.~basic_string();
		tok = INT;
		ival = i;
		return *this;
	}
	Token& operator=(double d) {
		if (tok == STR) sval.~basic_string();
		tok = DBL;
		dval = d;
		return *this;
	}
	
	// 移动构造函数
	Token(const Token&& t) noexcept :tok(t.tok) { moveUnion(std::move(t)); };
	// 移动赋值运算符
	Token& operator=(const Token&& t) noexcept {
		if (this != &t) {
			free();
			moveUnion(std::move(t));
			tok = std::move(t.tok);
		}
		return *this;
	};

private:
	enum { INT, CHAR, DBL, STR } tok;
	union {
		char cval; int ival; double dval; std::string sval;
	};
	void copyUnion(const Token& t) {
		switch (t.tok) {
		case Token::INT: ival = t.ival; break;
		case Token::CHAR: cval = t.cval; break;
		case Token::DBL: dval = t.dval; break;
			// 拷贝一个string可以使用定位new表达式构建
		case Token::STR: new(&sval) string(t.sval); break;
		}
	};
	void moveUnion(const Token&& t) {
		switch (t.tok) {
		case Token::INT: ival = std::move(t.ival); break;
		case Token::CHAR: cval = std::move(t.cval); break;
		case Token::DBL: dval = std::move(t.dval); break;
		case Token::STR: new(&sval) string(std::move(t.sval)); break;
		}
	}
	void free() {
		if (tok == STR) sval.~basic_string();
	}
};

19.7? ? 局部类

定义在某个函数内部.

  • 只在定义它的作用域内可见.
  • 所有成员都必须完整的定义在类的内部.
  • 不允许声明静态数据成员.
  • 不能使用函数作用域中的局部变量
  • 只能访问外层作用域定义的类型名, 静态变量, 枚举成员.
int a = 1, val = 2;
void foo(int val) {
	static int si;
	enum Loc {a = 11, b};
	struct Bar {
		Loc locVal;   // 正确 可以使用外层foo作用域中的类型
		int barVal;
		void fooBar(Loc L = a) {
			barVal = val;  // 错误 不能使用外层foo作用域中的局部变量
			barVal = ::val;  // 正确 可以使用全局变量(foo的外层作用域)
			barVal = si;    // 正确 可以使用外层foo作用域中的静态变量
			locVal = b;    // 正确 可以使用外层foo作用域中的枚举变量
		}
	};
}

局部类内的嵌套类, 必须定义在跟局部类相同的作用域内.

19.8? ?固有的不可移植的特性

算术类型的大小在不同的机器上不一样.

19.8.1 位域

类可以将其非静态的数据成员定义成位域, 一个位域中含有一定数量的二进制位.

位域在内存中的布局是与机器相关.

位域的类型必须是整型或枚举类型.(通常是无符号类型)

声明方式是 成员名字之后 紧跟一个冒号以及一个常量表达式, 该表达式指定成员所占的二进制位数

取地址运算符& 不能作用于位域, 因此任何指针都无法指向类的位域.

class c{
	typedef unsigned int Bit;
	Bit m : 2; // m占2位
	Bit b : 1;
    // 使用
    inline bool isRead() const { return m & b; }
};

19.8.2 volatile限定符

  • volatile 确切含义与机器有关.
  • 当对象的值可能在程序的控制或检测之外被修改时,? 应该将该对象声明为volatile.
  • 该关键字告诉编译器不应对这样的对象进行优化.
  • 用法与const相似. 两者互相没有影响.
  • 只有volatile的成员函数才能被volatile的对象调用.
  • 合成的拷贝对volatile对象无效.

19.8.3 链接指示: extern "C"

  • C++ 使用链接指示 指出任意 非C++ 函数所用的语言.
  • 有两种形式: 单个的和复合的.
  • 链接指示不能出现在类定义或函数定义的内部.
  • 必须在函数的每个声明中都出现.
// 单语句链接指示
extern "C" size_t strlen(const char*);
// 复合语句链接指示
extern "C" {
	int strcmp(const char*, const char*);
	char* strcat(char*, const char*);
}

链接指示与头文件:

当一个include指示被放置到复合链接指示的花括号中时, 头文件中的所有普通函数声明都被认为是由链接指示的语言编写的.

extern "C" {
	int strcmp(const char*, const char*);
	char* strcat(char*, const char*);
#include <string.h> // string.h中的函数声明 被认为是C编写的
}

链接指示可以嵌套.

指向extern "C" 函数的指针:

// 声明一个函数指针, 该指针接收一个int返回void, 并且指向的是一个C写的函数.
extern "C" void (*pf) (int);

?指向C函数的指针和指向C++函数的指针 是不同的类型.

// 声明一个函数指针, 该指针接收一个int返回void, 并且指向的是一个C写的函数.
extern "C" void (*pf) (int);
// 指向C++的函数指针
void(*pf2)(int);
pf = pf2; // 这两个pf不是一个相同的类型, 两者也不能相互赋值.

链接指示对整个声明都有效:

链接指示不进对函数有效, 而且对作为返回类型或形参类型的函数指针也有效.

// 声明一个C语言编写的函数f1, 该函数返回值为void, 形参为 一个指向C语言编写的,返回值是void,形参时int的函数指针
// 链接指示 不仅针对f1 还针对后面的形参
extern "C" void f1(void(*)(int));

为解决此问题, 如果希望给C++函数传入一个指向C函数的指针, 必须使用类型别名:

// 指向C函数的指针
extern "C" typedef void FC(int);
// C++的函数, 形参指向C函数的指针
void f2(FC*);

在其他语言中使用C++函数:

// calc 可以在C语言编译器中被调用
extern "C" double calc(double dparm) {};

预处理器标志 __cplusplus:

#ifdef __cplusplus
// 当正在编译C++时, 扩展C
extern "C"
#endif

重载函数与链接指示:

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2021-10-28 12:13:13  更:2021-10-28 12:15:45 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 5:48:01-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码