C++之多态

一、多态的理解

多态的概念通俗来讲就是多种形态。具体来讲就是去完成某个行为当不同的对象去完成时会产生出不同的行为。

比如买票这个行为当普通人买票时是全价买票学生买票时是半价买票。

// 静态的多态函数重载看起来调用同一个函数有不同的行为编译时实现
// 动态的多态一个父类的指针或引用去调用同一个函数传递不同的对象会调用不同的函数运行时实现
// 本质不同的对象去做同一件事情行为不同

class Person 
{
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

class Student : public Person 
{
public:
	// 子类中满足三同函数名、参数、返回值的虚函数叫做重写/覆盖
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

void Func(Person& p)
{
	p.BuyTicket();  // 多态
}

int main()
{
	Person ps;
	Student st;

	Func(ps);
	Func(st);

	return 0;
}

运行结果是
买票-全价
买票-半价

二、多态的定义及实现

1.多态的构成条件

那么在继承中要构成多态有两个条件

  1. 必须通过基类的指针或引用调用虚函数。
  2. 被调用的函数必须是虚函数且派生类必须对基类的虚函数进行重写

这两个条件缺一不可。

2.虚函数

虚函数被virtual修饰的类的非静态成员函数称为虚函数。

其它函数不能成为虚函数。

class Person 
{
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl;}
};

3.虚函数的重写/覆盖

虚函数的重写/覆盖派生类中有一个跟基类完全相同的虚函数称派生类的虚函数重写/覆盖了基类的虚函数。

注完全相同指函数名、参数、返回值完全相同。

派生类虚函数重写时继承的是基类虚函数的接口重写的是虚函数的实现

class Person 
{
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

class Student : public Person 
{
public:
	// 子类中满足三同函数名、参数、返回值的虚函数叫做重写/覆盖
	virtual void BuyTicket() { cout << "买票-半价" << endl; }

	// 注意在重写基类虚函数时若派生类的虚函数不加virtual关键字也可以构成重写
	// 因为基类的虚函数被继承下来了在派生类依旧保持虚函数属性
	// 若其访问限定符为非公有也能调用因为保持了基类虚函数的访问限定符
	// 重写的本质是重写了内容
	// 但是这种写法不是很规范不建议这样使用
	//void BuyTicket() { cout << "买票-半价" << endl; }
};

void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person ps;
	Student st;

	Func(ps);
	Func(st);

	return 0;
}

运行结果是
买票-全价
买票-半价


验证构成多态的条件

① 破坏条件1没有通过基类的指针或引用调用虚函数

class Person 
{
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

class Student : public Person 
{
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

// 若构成多态传的是哪个类的对象调用的就是这个类的虚函数 -- 跟对象有关
// 若不构成多态调用的就是p类的函数 -- 跟Func的参数类型有关
void Func(Person p)
{
	p.BuyTicket();
}

int main()
{
	Person ps;
	Student st;

	Func(ps);
	Func(st);

	return 0;
}

运行结果是
买票-全价
买票-全价

② 破坏条件2不是虚函数

class Person 
{
public:
	void BuyTicket() { cout << "买票-全价" << endl; }
};

class Student : public Person 
{
public:
	void BuyTicket() { cout << "买票-半价" << endl; }
};

// 若构成多态传的是哪个类的对象调用的就是这个类的虚函数 -- 跟对象有关
// 若不构成多态调用的就是p类的函数 -- 跟Func的参数类型有关
void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person ps;
	Student st;

	Func(ps);
	Func(st);

	return 0;
}

运行结果是
买票-全价
买票-全价

③ 破坏条件2不构成重写。

class Person 
{
public:
	virtual void BuyTicket(char ch) { cout << "买票-全价" << endl; }
};

class Student : public Person 
{
public:
	virtual void BuyTicket(int i) { cout << "买票-半价" << endl; }
};

// 若构成多态传的是哪个类的对象调用的就是这个类的虚函数 -- 跟对象有关
// 若不构成多态调用的就是p类的函数 -- 跟Func的参数类型有关
void Func(Person& p)
{
	p.BuyTicket(10);
}

int main()
{
	Person ps;
	Student st;

	Func(ps);
	Func(st);

	return 0;
}

运行结果是
买票-全价
买票-全价


虚函数重写的两个例外

  1. 协变
    派生类重写基类虚函数时与基类虚函数返回值类型可以不同不过必须是基类虚函数返回基类对象的指针或者引用派生类虚函数返回派生类对象的指针或者引用称为协变。

测试代码1

// 重写要求返回值相同有一个例外协变 -- 返回值是父子关系的指针或引用
class Person 
{
public:
	virtual Person* BuyTicket() { cout << "买票-全价" << endl; return nullptr; }
};

class Student : public Person 
{
public:
	virtual Student* BuyTicket() { cout << "买票-半价" << endl; return nullptr; }
};

void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person ps;
	Student st;

	Func(ps);
	Func(st);

	return 0;
}

测试代码2

// 重写要求返回值相同有一个例外协变 -- 返回值是父子关系的指针或引用
class A {};
class B : public A {};

class Person 
{
public:
	virtual A* f() { cout << "Person" << endl; return nullptr; }
};
class Student : public Person 
{
public:
	virtual B* f() { cout << "Student" << endl; return nullptr; }
};

void Func(Person& p)
{
	p.f();
}

int main()
{
	Person ps;
	Student st;
	
	Func(ps);
	Func(st);
	
	return 0;
}
  1. 析构函数的重写
    如果基类的析构函数为虚函数此时派生类析构函数只要定义无论是否加virtual关键字都与基类的析构函数构成重写虽然基类与派生类的析构函数名字不同。析构函数名不相同看起来违背了重写的规则其实不然这里可以理解为编译器对析构函数的名称做了特殊处理编译后析构函数的名称统一处理成destructor
// 若析构函数是虚函数则构成重写
// 析构函数名被特殊处理了处理成了destructor
class Person 
{
public:
	virtual ~Person() { cout << "~Person()" << endl; }
};

class Student : public Person 
{
public:
	virtual ~Student() { cout << "~Student()" << endl; }
};

int main()
{
	// 普通对象析构函数是否是虚函数是否完成重写都正确调用了
	//Person p;
	//Student s;

	// 动态申请的父子对象如果给了父类指针管理
	// 那么需要析构函数是虚函数子类完成重写构成多态
	// 这样才能正确调用子类的析构函数
	Person* p1 = new Person;
	Person* p2 = new Student;

	delete p1;
	delete p2;
	// 若父类的析构函数不是虚函数
	// 那么delete p2时没有正确调用子类析构函数导致内存泄漏


	// 其它场景析构函数是否为虚函数都可以正确调用析构函数
	// 比如上面的普通对象场景

	return 0;
}

4. C++11 的 override 和 final

  1. final
    ① 修饰类表示该类不能被继承。
    ② 修饰虚函数表示该虚函数不能被重写。
// C++11 final 修饰类直接限制它不能被继承
class A final
{
protected:
	int _a;
};

class B : public A    // 编译报错
{

};

// C++11 final 修饰虚函数限制它不能被子类中的虚函数重写
class C
{
public:
	virtual void f() final
	{
		cout << "C::f()" << endl;
	}
};

class D : public C
{
public:
	virtual void f()    // 编译报错
	{
		cout << "D::f()" << endl;
	}
};
  1. override检查派生类虚函数是否重写了基类某个虚函数。如果没有重写则会编译报错。
// C++11 override 放在子类虚函数后面检查它是否重写了父类的某个虚函数
// 如果没有重写则会编译报错

// 比如想让父类的某个函数被子类重写但是忘了加virtual关键字
// 给子类虚函数加了override就会检查到没有重写父类的某个虚函数就会编译报错
class Car 
{
public:
	void Drive() 
	{}
};

class Benz :public Car 
{
public:
	virtual void Drive() override  // 编译报错
	{
		cout << "Benz-舒适" << endl;
	}
};

5.重载、重写/覆盖、隐藏/重定义

在这里插入图片描述

三、抽象类

  • 在虚函数的后面写上 = 0则这个函数为纯虚函数
  • 包含纯虚函数的类叫做抽象类也叫接口类抽象类不能实例化出对象。
    派生类继承抽象类后由于包含纯虚函数也是抽象类也不能实例化出对象只有重写纯虚函数派生类才能实例化出对象。
  • 纯虚函数更体现出了接口继承。

override只是在语法上检查派生类虚函数是否重写了基类某个虚函数。

接口继承和实现继承
 ① 普通函数的继承是一种实现继承派生类继承了基类函数可以使用函数继承的是函数的实现。
 ② 虚函数的继承是一种接口继承派生类继承的是基类虚函数的接口目的是为了重写达成多态继承的是接口。所以如果不实现多态不要把函数定义成虚函数。

// 抽象 -- 在现实世界中没有对应的实物
// 一个类型如果一般在现实世界中没有具体的对应实物就定义成抽象类比较好
class Car  // 抽象类
{
public:
	// 纯虚函数一般只声明不实现可以实现但是没有价值
	virtual void Drive() = 0;
};

class Benz : public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};

class BMW : public Car
{
public:
	virtual void Drive()
	{
		cout << "BMW-操控" << endl;
	}
};

int main()
{
	//Car c;  // 编译报错因为抽象类不能实例化出对象

	Car* p1 = new Benz;
	p1->Drive();

	Car* p2 = new BMW;
	p2->Drive();

	return 0;
}

四、多态的原理

1.虚函数表

先看下面的代码

// 在32位平台下sizeof(Base) == ?
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}

	virtual void Func2()
	{
		cout << "Func2()" << endl;
	}

protected:
	int _b = 1;
	char _ch = 'A';
};

int main()
{
	Base b;
	cout << sizeof(Base) << endl;

	return 0;
}

在 32 位平台下sizeof(Base) == 12 。
在这里插入图片描述
原因
虚函数表指针即 vfptr v 代表 virtual f 代表 function指向虚函数表。
一个含有虚函数的类其对象至少有一个虚函数表指针在当前平台放在对象的最前面注意有些平台可能会放到对象的最后面这个跟平台有关。
所以加入虚函数表指针后再按照内存对齐规则计算在 32 位平台下该类的大小为 12 。

  • 一个虚函数表指针指向一张虚函数表也简称为虚表虚函数表里存放虚函数的地址即虚函数表本质就是函数指针数组。

在这里插入图片描述

  • 虚函数表在编译阶段生成存放在常量区常量区是 C/C++ 语言角度在操作系统角度常量区和代码段都是代码区。

用一段代码来进行验证

// 还是使用上面的代码
int main()
{
	int a = 0;
	printf("栈%p\n", &a);

	int* p = (int*)malloc(4);
	printf("堆%p\n", p);

	static int b = 0;
	printf("数据段%p\n", &b);

	const char* str = "aaaaaa";
	printf("常量区%p\n", str);

	printf("代码段%p\n", &Base::Func1);

	Base bs;
	printf("虚函数表%p\n", *((int*)&bs));  // 查看虚表指针内容虚表的地址

	return 0;
}

我们可以看到虚表存放的位置离常量区最近实际上虚表存放在常量区。
为什么虚表会存放在常量区呢因为虚表创建好后是不会被修改的。
在这里插入图片描述

  • 虚函数的重写也叫做覆盖。子类继承父类后会先拷贝一份父类的虚表然后再把子类重写的虚函数地址覆盖虚表中父类虚函数地址形成子类的虚表。
    因此可以认为重写是语法层的概念覆盖是原理层的概念
class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}

protected:
	int _b = 1;
};

class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}

protected:
	int _d = 2;
};

int main()
{
	Base b;
	Derive d;

	return 0;
}

虚表里只存放虚函数的地址非虚函数的地址不会被放进虚表。
在这里插入图片描述

2.多态的原理

class Person 
{
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
	
protected:
	int _a = 1;
};

class Student : public Person 
{
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }

protected:
	int _b = 2;
};

void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person Mike;
	Func(Mike);

	Student Johnson;
	Func(Johnson);

	return 0;
}

解释多态的原理在这里插入图片描述

多态是如何实现的
 ① 普通函数的调用都是在编译时直接决定地址的而满足多态的虚函数的调用是运行时才能决定地址的先通过父类的指针或引用找到对象的虚表指针再通过虚表指针找到其指向的虚表然后在虚表中找对应函数的地址。
 ② 若传递的是父类对象父类的指针或引用指向的是父类对象它找父类对象的虚表调用的是父类的虚函数。
 ③ 若传递的是子类对象会发生切片父类的指针或引用指向的是子类对象中的父类对象它找父类对象的虚表由于子类的虚函数完成了覆盖子类虚函数地址覆盖了父类虚函数地址调用的是子类的虚函数。
 ④ 所以满足多态的虚函数的调用过程中执行相同的指令调用的是不同的虚函数。

多态的实现依赖于虚函数的重写虚函数重写了以后父子类对象的虚表中存的就是不同的虚函数地址就能实现多态。

为什么多态的实现必须是基类的指针或引用呢

还是上面的代码通过对比对象切片和引用切片就可以理解了
在这里插入图片描述

 ① 引用切片r1 是子类对象中父类对象的别名指针的话就是指向子类对象中的父类对象指针或引用切片都没有出现对子类对象中的父类对象进行拷贝的行为。实现多态时调用的虚函数一定是子类对象虚表中的虚函数。
 ② 对象切片p 是 Johnson 子类对象中父类对象的拷贝但唯独 p 的虚表指针不是 Johnson 子类对象中虚表指针的拷贝而且 p 的虚表指针也不可能是 Johnson 子类对象中虚表指针的拷贝父类对象的虚表指针一定指向父类虚表子类对象的虚表指针一定指向子类虚表。
 ③ 因此多态的实现必须是基类的指针或引用。


同类型的对象虚表指针是相等的指向同一张虚表。

// 还是使用上面的代码
int main()
{
	Person p1;
	Person p2;

	Student s1;
	Student s2;
	
	return 0;
}

在这里插入图片描述


普通函数和虚函数的存储位置是一样的都在代码段。只是虚函数又把地址存到虚表中方便实现多态。

  • 普通函数的调用在编译时就能决定它的地址。
  • 满足多态的虚函数的调用不是编译时就能决定它的地址的而是运行时需要到对象的虚表中寻找才能决定它的地址。
  • 不满足多态的虚函数的调用跟普通函数的调用一样在编译时就能决定它的地址不需要在运行时到对象的虚表中寻找。

总结在编译时能直接决定调用函数的地址的一定会在编译时决定。满足多态的虚函数调用在编译时是不能直接决定调用虚函数的地址的只能在运行时去对象虚表中找虚函数的地址这样才能决定。

class Person
{
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
	void f() { cout << "f()" << endl; }

public:
	int _a = 1;
};

class Student : public Person
{
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }

protected:
	int _b = 2;
};

void Func(Person& p)
{
	p.BuyTicket();  // 满足多态运行时决定函数地址
	p.f();          // 编译时决定函数地址
}

int main()
{
	Person Mike;
	Func(Mike);
	
	Student Johnson;
	Func(Johnson);
	
	Mike.BuyTicket();     // 编译时决定函数地址
	Johnson.BuyTicket();  // 编译时决定函数地址

	return 0;
}

在这里插入图片描述

3.动态绑定与静态绑定

  • 静态绑定又称为前期绑定早绑定在程序编译期间确定了程序的行为也称为静态多态比如函数重载。
  • 动态绑定又称为后期绑定晚绑定是在程序运行期间根据具体拿到的类型确定程序的具体行为调用具体的函数也称为动态多态

五、单继承和多继承关系的虚函数表

1.单继承中的虚函数表

我们都知道派生类中的虚函数无论是否重写都会被放到虚表中重写的虚函数会覆盖自己新增加的虚函数会按其在派生类中的声明次序增加到虚表的最后。

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}

protected:
	int _b = 1;
};

class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
	virtual void Func4()
	{
		cout << "Derive::Func4()" << endl;
	}

protected:
	int _d = 2;
};

int main()
{
	Derive d;
	
	Base* p1 = &d;
	p1->Func1();

	return 0;
}

由于监视窗口隐藏了派生类虚表中未重写的虚函数所以需要通过内存窗口观察派生类对象的虚表才能看到
在这里插入图片描述


虚表中存的地址严格上来说其实也不是真正意义上的虚函数地址而是在虚函数地址基础上封装了一层的地址。

这里虚函数的调用满足多态eax 中存放的是从虚表中取出来的地址这个就是封装过的虚函数地址。
执行 call 指令后会到那个地址jmp 指令的地址。接着执行 jmp 指令才会跳到真正意义上的虚函数地址。
换言之虚表中存的是 jmp 指令的地址而不是真正的虚函数地址实际调用时相当于多跳了一层。在这里插入图片描述


我们也可以通过打印虚表的方式去查看虚表

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}

protected:
	int _b = 1;
};

class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
	virtual void Func4()
	{
		cout << "Derive::Func4()" << endl;
	}

protected:
	int _d = 2;
};

// 这里虚函数的类型都是void(*)()方便我们打印虚表

// 将函数指针类型void(*)()重命名为VF_PTR简化类型名称
typedef void(*VF_PTR)();

// 依次取虚表中的虚函数指针打印并调用
// 通过调用可以直观地看出存的是哪个虚函数
//void PrintVFTable(VF_PTR table[])    // 两种写法均可
void PrintVFTable(VF_PTR* table)
{
	for (int i = 0; table[i] != nullptr; ++i)  // VS在虚表的最后会放nullptr 
	{
		printf("vft[%d]:%p -> ", i, table[i]);
		table[i]();
	}
	cout << endl;
}

int main()
{
	Base b;
	// 为了拿到虚表指针的值需要取b头上4/832位/64位个字节的内容
	// 先强转成能看到b头上4/8个字节的指针类型int*/long long*
	// 解引用就能取到b头上4/8个字节的内容
	// 指针类型int*/long long*解引用后就是int/long long
	// 但这不是我们想要的类型所以还需强转为VF_PTR*
	// 因为这里的虚表就是一个存VF_PTR类型函数指针类型的数组
	// 最后传参给PrintVFTable函数
	//PrintVFTable((VF_PTR*)(*(int*)&b));        // 32位平台
	//PrintVFTable((VF_PTR*)(*(long long*)&b));  // 64位平台
	// 上面的这种方式不能根据32位和64位平台自适应不是很好
	// 其实最好的方式是先强转为二级指针
	// 因为二级指针能看到b头上的一个指针这么多个字节
	// 解引用就能取到b头上的一个指针大小的内容
	// 又因为指针的大小在32/64位下是4/8个字节
	// 所以这种方式在32位和64位平台下能自适应
	// 虽说是二级指针都可以但是建议用void**免得引起别人疑惑
	PrintVFTable((VF_PTR*)(*(void**)&b));

	Derive d;
	PrintVFTable((VF_PTR*)(*(void**)&d));

	return 0;
}

运行结果
在这里插入图片描述

其实这种方式有点非法因为可以乱调用虚表中的虚函数。所以说虚表是有安全小隐患的。当然我们在这里只是以这种方式去查看虚表而已实际中并不会这样用。

2.多继承中的虚函数表

多继承中派生类未重写的虚函数会被放到第一个继承基类的虚表中。

派生类中基类继承顺序的声明就是派生类实际继承基类的顺序。

class Base1 
{
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }

protected:
	int _b1;
};

class Base2 
{
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }

protected:
	int _b2;
};

class Derive : public Base1, public Base2 
{
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }

protected:
	int _d;
};

int main()
{
	Derive d;
	
	Base1* p1 = &d;
	p1->func1();
	Base2* p2 = &d;
	p2->func1();
	
	return 0;
}

通过内存窗口观察派生类对象的虚表
在这里插入图片描述

在多继承中子类重写了父类 Base1 和 Base2 的虚函数 func1() 可是两个虚表中重写的 func1() 的地址不相同这是因为虚表中存的是 jmp 指令的地址而不是真正的虚函数地址但最终都会跳到同一个真正的虚函数地址。这个可以通过调试反汇编来进行验证。

实际上在多继承中满足多态的情况下相比于用第一个继承基类的指针或引用用非第一个继承基类的指针或引用去调用派生类对象的虚函数时会多做一些准备工作主要是修正 ecx 存放的值this 指针即指针的偏移。

在这里插入图片描述


通过打印虚表的方式去查看虚表

class Base1
{
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }

protected:
	int _b1;
};

class Base2
{
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }

protected:
	int _b2;
};

class Derive : public Base1, public Base2
{
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }

protected:
	int _d;
};

typedef void(*VF_PTR)();

// 依次取虚表中的虚函数指针打印并调用
// 通过调用可以直观地看出存的是哪个虚函数
void PrintVFTable(VF_PTR* table)
{
	for (int i = 0; table[i] != nullptr; ++i)
	{
		printf("vft[%d]:%p -> ", i, table[i]);
		table[i]();
	}
	cout << endl;
}

int main()
{
	Base1 b1;
	PrintVFTable((VF_PTR*)(*(void**)&b1));
	
	Base2 b2;
	PrintVFTable((VF_PTR*)(*(void**)&b2));

	Derive d;
	// 打印Derive从Base1继承的虚表
	PrintVFTable((VF_PTR*)(*(void**)&d));
	// 打印Derive从Base2继承的虚表
	PrintVFTable((VF_PTR*)(*(void**)((char*)&d+sizeof(Base1))));
	
	// 打印Derive从Base2继承的虚表的另一种方式
	// 即利用切片时指针的自动偏移
	// 跟上面的方式本质是一样的
	//Base2* p = &d;
	//PrintVFTable((VF_PTR*)(*(void**)p));

	return 0;
}

运行结果
在这里插入图片描述

3.菱形继承、菱形虚拟继承中的虚函数表

实际中我们不建议设计出菱形继承及菱形虚拟继承它们再叠加上多态就会非常复杂所以我们就不看它们的虚表了一般我们也不需要研究清楚因为实际中很少用。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: c++