在C++中,structure和class的唯一区别在于前者的member默认是public的,而后者默认是private的


class中成员函数定义时的"::"符号叫做scope resolution operator,它前面的类名通常叫做type qualifier。定义成员函数时,可以使用该类的data member和function member而不必使用"."

"::"用于类,"."用于对象;“==”符号不能用于object或structure;“=”却可以对object/structure进行互相赋值

private member不能被该类成员函数的定义部分之外的程序访问。关键词public和private可以在类定义中多次出现,如果在类定义的开头没有添加public和private,则这些开始部分的member会被定义为private类型。属于同一个class的object可以访问同class其他object的private member

允许获得private成员变量的函数叫做accessor function,允许更改private成员变量的函数叫mutator function


当compiler编译一个non-static的member function时,它会隐式地给函数添加一个名为"this"的指针。this指针是一个隐藏的const pointer,指向member function所在的object的地址。注意this是const pointer,可以用它改变所指向的object,但不能让它指向其他位置。this也不能在static member function中被调用因为static member function可以在没有任何object时被调用


constructor是声明该类的某个object时被自动调用的成员函数。constructor必须与class同名,不能有任何返回类型(也不能有void),并且应该属于public。constructor不能被以与其他成员函数相同的方式被调用(以object.function()的方式)。constructor可以被overload

class bankAccount
{
public:
	bankAccount(int dollar, double rate);
	bankAccount();
...
};

bankAccount account1(100, 1.0);
bankAccount account2();	// 错误!!但是可能不会产生错误信息,编译器可能会认为这是一个函数声明

可以通过initialization section进行constructor定义,它由":"和由","分开的成员变量列表组成,如

bankAccount::bankAccount(): dollar(0), rate(1.0) {				bankAccount::bankAccount() {
	// could be empty								 ==				dollar = 0;
}																	rate = 1.0;
																}
调用constructor可以有两种方式,它既可以在object被声明时自动调用,也可以再之后再次调用。调用constructor会生成一个匿名object,而它可以被赋给一个命名了的object。constructor就像一个返回该类object的函数

bankAccount account1(100, 1.0);
account1 = bankAccount(100, 1.0);
account1 = bankAccount();	// 注意!此时一定要加()

C++不会总是产生默认constructor,如果给一个class定义了一个constructor,C++编译器将不会再产生其他constructor。所以应该总是定义一个默认constructor,因为很可能会不用任何constructor参数而声明一个object。默认constructor的body可以为空。当创建一个class的数组时,默认的constructor会被调用,所以包含一个默认constructor是十分重要的


constructor也可以被声明为private,目的是避免这个类被继承或实现新的object(例如在singleton design pattern中)。可以通过另一个public的static member function来创建新object


copy constructor是只有一个参数的constructor,并且这个参数的类型与class要相同。这个参数必须是call by reference的,并且通常参数声明为const。当被初始化的object要成为一个完全独立的,它的参数的复制品时,需要定义copy constructor。比如当一个class的private部分有new的dynamic variable时,如果使用普通的constructor,则新定义的object会和被复制的object指向同一个位置;当其中的任意一个的destructor被调用时,dynamic variable就会被释放,然而此时另一个object还指向这个地方(compiler会自动产生一个隐式的copy constructor,但它只能进行shallow copy,就是直接赋值)。所以当class的定义包含指针和使用new动态分配的内存时,需要添加copy constructor。copy constructor通常会被在以下三种情况下调用:

1)当一个object被声明并同时被同类型的另一个object初始化时

2)当一个class类型的参数被plug-in到一个call by value的参数位置时

3)当一个函数的返回一个class时(最好使用move constructor)

MyClass origin;
MyClass a (origin);       // object initialization: copy constructor called
MyClass b = origin;       // object initialization: copy constructor called
origin = b;               // object already initialized: copy assignment called 
上例中虽然对b的初始化使用了"=",但并不是赋值运算,在这里叫做copy assignment operator,它其实是以另一种形式调用了一个参数的constructor,通过对"="进行overload来实现(注意是copy constructor被调用,而不是overload的"="被调用),通常返回*this


当一个class的变量中有指针时,普通的赋值运算符"="会令两个object的指针指向同一个位置,而这不是我们想要的,所以此时必须重载"="(此时重载的"="叫做copy assignment operator)。当重载"="时,与重载其他运算符不同的是,它必须成为class的member而不能是friend。返回值的类型可以是void,也可以返回一个reference。注意重载"="的时候,无论是copy assignment还是move assignment,都必须检查自己给自己赋值的情况(因为可能会有dynamic variable/array)

void Student::operator =(const Student& var) {
	if(this == &var)
		return;
	...
}


对应copy constructor和copy assignment ,还有move constructor和move assignment。后两者会在初始化一个新object时将另一个object中的内容移动到这个新的object中,通常用于unnamed object,如返回值或类型转换的结果

如果一个class中既定义了copy constructor和copy assignment operator,又定义了move constructor和move assignment operator,当参数是rvalue时,选择move;当参数是lvalue时,会选择copy


对于有dynamic array的class,可能要先将原来的dynamic array释放再重新申请空间。但是这样有一个问题,就是当object1 = object1,即自己给自己赋值时原来的dynamic array会被销毁。一个解决的方法是检查"="左边的dynamic array是否有足够的空间,如果需要额外的空间再delete它的dynamic array;另一个方式是检查参数地址和this是否相等

MyClass {
public:
	MyClass() {}
	MyClass(const MyClass& obj) {}				// copy constructor
	MyClass& operator= (const MyClass& obj) {}	// copy assignment
	MyClass(MyClass&& obj) {}					// move constructor
	MyClass& operator= (MyClass&& obj) {}		// move assignment
};

MyClass foo();		// a function that returns a MyClass object

MyClass origin;
MyClass a = origin;		// copy constructor
MyClass b = foo();		// move constructor
origin = b;				// copy assignment
b = MyClass();			// move assignment


一个类只能有一个destructor,它不能被重载,在object离开有效scope的时候会被调用,可以用来释放dynamic variable的空间等

copy constructor,赋值运算符"="和destructor被称为Big Three,因为如果要定义其中的任意一个,就必须定义其他两个,否则compiler会自动生成,但可能不会按照所期望方式工作



友元函数(friend function)并不是class的member function,它只是一个普通的函数,但是它能够访问一个class的private member。

class Date
{
public:
	Date();
	friend bool equal(Date d1, Date d2);
	friend bool operator ==(const Date &d1, const Date &d2); // overloading operator "=="
	...
private:
	int month;
	int day;
}

bool equal(Date d1, Date d2) { // 注意这里不需要加"Date::"
	return d1.month == d2.month && d1.day == d2.day;
}

bool operator ==(const Date &d1, const Date &d2) {
	return d1.month == d2.month && d1.day == d2.day;
}

将一个函数声明为友元函数唯一的原因就是这样能让函数的声明变得更简单和高效,例如当函数进行的操作可能涉及多个object时


除了函数外,一个class也可以被声明为另一个class的friend


对变量类型是class的函数来说,使用call by reference可以提高程序的效率。如果成员函数不改变所调用object的值(比如现在有一个object叫test,如果test的某个成员函数foo()不应该改变test的值,则这个foo()应该在最后加上const),则该成员函数可以声明为const,这里const要在函数声明之后,分号之前,并且必须在声明和定义时都使用const


const关键词的使用是all-or-nothing的,如果对某个类型的一个参数使用const,则必须对其他所有不会被函数调用改变值的该类型的变量使用const;如果变量类型是一个class,则还必须要对每个不改变所调用object的成员函数使用const。这样做是因为一个函数可能会调用其他函数。下例中,如果不将get_value声明为const,则guarantee在大多数编译器上会产生错误信息。尽管get_value并不改变所调用object的值,但因为没有const,编译器会默认它会改变所调用的object的值。所以,如果对Money类型使用了const,那么应该对所有不改变调用object值的Money的所有成员函数使用const(注意这里的gurantee函数并不是成员函数,也不是友类函数)

void guarantee(const Money& price) {
	cout << "Double your money if not satisfied:" << (2 * price.get_value()) << endl;
}



重载运算符的一些规则:
1. 对运算符进行重载时,至少要有一个参数是class类型或枚举类型
2. 一个被重载的运算符可以是某个class的friend,可以是某个class的member function,也可以都不是,仅仅是一个普通的函数

3. 只能对已有的运算符进行重载,不能创建新的运算符;并且不能改变运算符进行操作的参数个数
4. 运算符的运算优先级不能更改
5. 以下运算符不能重载: .   ::   .*   ?:   

6. 赋值运算符“=”的重载需要用不同的方式(必须称为class的成员,而不能声明为友元);某些运算符,如"[]"和"->"等的重载方式也是不同的

7. "++"、"--"作为前缀和后缀的重载方式是不同的(参考:http://www.tutorialspoint.com/cplusplus/increment_decrement_operators_overloading.htm)

8. ">>"和"<<"最好被overload为friend,因为这样可以保证以类似cout << some_class的形式输出(并且注意此时它的返回类型应该是ostream&)。理论上讲,"<<"可以被overload为member function,但是无法以 cout << some_class的形式输出,因为"<<"被定义为member function时,第一个参数只能是当前的object(隐藏的*this)

class Money {
public:
	Money(int dollar, int cent);
	Money(int value);
	void show(ostream& outs) const;
	double getValue() const;
	friend Money operator +(const Money& m1, const Money& m2);
	friend bool operator ==(const Money& m1, const Money& m2);
	double operator-(const Money& m2);								// overload "-" as member function
	friend ostream& operator <<(ostream& outs, const Money& m);		// the return type must be a reference of stream
private:
	double amount;
}

void Money::show(ostream &outs) const {
	outs << "the amount is: " << amount << endl;
}

ostream& operator <<(ostream& outs, const Money& m) {
	outs << "the value is: " << m.getValue() << endl;
	return outs;
}



Money base_amount(100, 60), full_amount;
full_amount = base_amount + 25;  	// valid
double value = full_amount - base_amount;		// because "-" returns a value when overloaded as member function
value = full_amount.operator-(base_amount);		// also valid
full_amount.show(cout);
cout << full_amount;

在上面的程序中,其实并没有重载操作对象是Money和int的"+",之所以能够成功执行,是因为定义了参数为一个int型变量的constructor,所以在调用"+"时自动用25生成了一个Money的object。如果使用25.00,因为没有对应的double型的constructor,就会报错





继承和多态

class Person {
public:
	Person() {}
	Person(string s) : name(s) {}
	setName(string s);
	void showInfo(void);
private:
	string name;
};

class Student : public Person {		// Student is derived from Person
public:
	Student(): Person() {}
	Student(string n, int num): Person(n), id(num) {}		// use constructor from base class
	void setId(int num);
	showInfo(void);		// only list the declaration of an inherited member function when you want to redefine
private:
	int id;
};

derived class并不会自动继承base class的constructor,但是可以在derived class的constructor的定义中调用base class的constructor,这个constructor会初始化从base class继承来的数据。应该总是在derived class的constructor中的初始化部分包含base class的constructor,否则没有参数的default constructor会被自动调用

destructor也不会被自动继承,但当derived class的destructor被调用时,它会自动调用base class的destructor,所以没有必要在derived class的destructor中显式调用base class的destructor。如果class C继承自class B,class B继承自class A,则class C的object离开作用域时,class C的destructor会先被调用,然后是class B的最后是class A的

copy constructor不会被自动继承,如果在derived class中没有定义copy constructor系统会自动产生一个copy constructor,但只会复制member variable的内容,对在member variable中有指针或dynamic data的class不能正常工作,所以如果class member有pointer,dynamic array或其他dynamic data,则无论这个类是不是derived class,都需要定义copy constructor

赋值号"="也不会被继承,如果base class中重载了"=",derived class只会有系统默认的"="而没有被重载过的"="

Student& Student::operator =(const Student& var) {	// called copy assignment, "Student&"  means return a reference to the object
	Person::operator =(var);
	...
	return *this;		// to allow operator chaining like a = b = c
}


因为Student是Person的派生类,所以所有Person object可以使用的地方,Student object也可以使用。但是不能将一个Person object赋给Student object


从base class继承来的private member不能被derived class访问;base class中的private variable只能在base class的member function中被直接访问;而对于base class中的private function则是完全无法访问,它就像没有被继承一样。如果要在derived class中访问base class的private member,只能用base class的public member function

protected的class member,对除derived class之外的所有函数或class的效果与private是相同的;但对于derived class,base class中的projected成员可以被直接访问。在derived class中,base class中的protected成员依然是protected的


只有当derived class想重定义base class中的某个member function时才需要重新声明,否则不要在derived class中重新声明base class的member function。对于在derived class中被重定义的函数来说,它在base class中的定义并没有在derived class中完全消失。如果想要使用base class中的原函数,需要显式指明,如:

Student s1("Tom", 123);
s1.Person::showInfo();


不能在base class和derive class之间进行function overload,当derive class中有与base class中同名的函数时,要么进行redefine覆盖函数在base class中的定义,要么声明为virtual通过override实现,否则在derive class和base class中出现同名函数时,即使函数签名不同也会出现问题。下面的代码就会出现问题:

class A {
public:
	virtual void foo(int a) {
		cout << "A::foo(int)\n";
	}
	virtual void foo(int* a) {
		cout << "A::foo(int*)\n";
	}
};

class B: public A {
public:
	virtual void foo(int* a) {
		cout << "B::foo(int*)\n";
	}
};

B obj;
obj.foo(7);		// compile error, msg: no matching function



class grandparent {
public:
	void foo(string s);
};

class parent: public grandparent {
public:
	int foo(int i);
};

class grandchild: public parent {
public:
	void bar() {
		string s = "bar";
		foo(s);		// compile error, msg: no matching function 
	}
};
当C++的name lookup在base class中一找到对应的name时就会停止,所以在上述代码中,parent class的foo()隐藏了grandparent class中的foo(),导致当grandchild调用foo()时,name lookup看不到grandparent class中foo()的定义。如果想要解决这个问题,可以在parent class中unhide函数foo():

class grandparent {
public:
	void foo(string s);
};

class parent: public grandparent {
public:
	int foo(int i);
	using grandparent::foo();	// unhide foo() in grandparent class
};

class grandchild: public parent {
public:
	void bar() {
		string s = "bar";
		foo(s);
	}
};



多重继承时,derived class对base class的constructor的调用顺序是由class声明继承时的顺序决定的,而不取决于derived class的constructor对base class的constructor的调用顺序

class A {
public:
	A() { cout << "A"; }
};

class B {
public:
	B() { cout << "B"; }
};

class C : public A, public B {
public:
	C(): B(), A(){}
};

C c;	// result is "AB"



多态指通过一种名为late binding的特殊机制令一个函数名具有多重含义

virtual function指在某种程度上可以在它被定义之前就可以使用的函数。当一个函数是virtual的时候,相当于告诉compiler“我并不知道这个函数是怎么实现的,等到它在程序中被使用时再通过object确定它的具体实现”。这种等到运行时再确定具体实现的方法叫late biding或dynamic binding

使用virtual function时有以下一些细节:

1)如果一个函数在base class中是virtual,那么它在derived class中也会自动变成virtual;尽管在derived class中可以省略关键词"virtual",但最好加上

2)"virtual"关键词只出现在函数声明中,不能出现在函数的定义中

之所以不将所有的member function都声明成virtual的是因为这样效率比较低。对于virtual function,compiler和run-time environment需要做很多工作,如果将不需要声明成virtual的函数也声明成了virtual,程序的效率会降低

对于普通的函数,在derived class中改变它的定义这种行为叫做redefine;而对virtual function来说,这种行为叫做override

可以将一个derived class的object赋给一个base class的object,但是不能反过来。同时,虽然这样的赋值是合法的,但是derived class的object中的某些内容会丢失,这叫做slicing problem。C++提供了能让Dog被当作Pet并且不会发生slicing problem的方法,就是使用指针和dynamic object

class Pet {
public:
	virtual void print(int x = 1) {
		cout << "value is " << x << endl;
	}
	string name;
};

class Dog: public Pet {
public:
	virtual void print(int x = 2) {
		cout << "value is " << x << endl;
	}
	string breed;
};

Pet vpet;
Dog vdog;	// Dog is derived from Pet
vpet = vdog;	// slicing problem

Pet *ppet;
Dog *pdog;
pdog = new Dog;
ppet = pdog;	// no slicing problem
ppet->print();	// valid, Dog::print() is called, result is "value is 1"
cout << "name: " << ppet->name << "breed: " << ppet->breed << endl;		// invalid

上面代码的最后一行之所以是错误的,是因为*ppet的类型是指向Pet的指针,而Pet并没有成员breed。之所以ppet->print()可以正常工作是因为print()被声明为virtual,当compiler看到ppet->print()时,它会检查Pet和Dog的virtual table,然后发现ppet指向了Dog类型的object,所以它调用了Dog::print(),而不是Pet::print()。总结一下,当p_base = p_derived时不会发生slicing problem,但是需要virtual member去访问derived类的dynamic变量的member


在一个class的member function被定义为virtual但还没具体实现时,编译会产生错误。即使没有derived class并且只有一个virtual member,如果这个virtual member没被定义也会产生错误,但是产生的错误信息可能很难理解(可能会有指出undefined reference to default constructor这种错误信息,即使这些constructor已经被定义了)

class Base {
public:
	virtual ~Base() {
		f();
	}
	virtual void f() {
		cout << "Base::f" << endl;
	}
};

class Derived : public Base {
public:
	~Derived() {}
	virtual void f() {
		cout << "Derived::f" << endl;
	}
};

Base *p = new Derived;
delete p;			// result is "Base::f"


可以将一个virtual function声明为pure virtual function。使用pure virtual function有两个结果:1)有一个或多个pure virtual function的class会成为abstract base class,这种class不能被实例化;2)所有abstract base class的derived class都必须对pure virtual function进行定义,否则这个derived class也会成为abstract class。当对base class的某个函数进行定义是没有意义的时候,可以将其声明为pure virtual function

pure virtual function大多数情况下是没有body的,但其实可以对pure virtual function进行实现(但是不能在声明时进行实现,VC++除外),这样做的原因通常是为了给derived class的pure virtual function提供一些参考的default行为

class Base {
public:
    virtual void foo() = 0;	// pure virtual function, "= 0" just indicates it's a pure virtual function
	virtual void bar() = 0;	// cannot implement at declaration
};

void Base::bar() {		// it's valid for pure virtual function to have body
	cout << "bar from Base\n";
}

class Derive : public Base {
public:
	virtual void foo() {};
	virtual void bar() {
		Base::bar();
		cout << "bar from Derive\n";
	}
};

Base b;		// invalid, because Base is abstract class
Derive d;
d.bar();		// valid


interface class是指没有member variable,并且所有function都是pure virtual function的class


最好总是将destructor声明成virtual的。说明destructor如何作用于virtual function机制的最简单的方式就是,所有的destructor都被当作具有相同名字的destructor来处理(尽管他们其实有不同的名字)。例如:

Base *pBase = new Derived;
...
delete pBase;
当执行delete时,因为base class的destructor被声明为virtual并且被指向的object是Derived类型的,class Derived的destructor会被调用(它会自动调用class Base的destructor)。如果class Base的destructor没有被声明为virtual类型,那么只有class Base的destructor会被调用(这样对于dynamic variable就会产生问题)

并且注意,当一个destructor被声明为virtual时,所有由它derive的class的destructor不管有没有显式声明virtual都会被自动声明为virtual,并且和之前一样,所有的destructor都会被当作具有同样的名字(尽管他们的名字其实不同)



Virtual Table

virtual table是函数的lookup table,用来解决在dynamic/late binding情境中的函数调用问题。每个含有virtual function的class都有自己的virtual table,这个virtual table由所有该class的object共享,是由compiler在compile时建立的static数组。virtual table中对每一个virtual function都有一个entry,每个entry是一个函数指针,指向这个class所能访问到的most-derived函数

尽量避免在constructor/destructor中调用virtual function,因为derived class的virtual table可能还没就绪,导致其实是base class的virtual function被调用了

compiler还会给base class添加一个隐藏的指针(假设叫*__vptr),导致每个object都会增加一个指针的大小,这个指针会被derived class继承,意味着每个class都有自己的*__vptr。*__vptr会在一个class实例被创建时自动产生,指向这个class的virtual table

class Base {
public:
    virtual void function1() {};
    virtual void function2() {};
};
 
class D1: public Base {
public:
    virtual void function1() {};
};
 
class D2: public Base {
public:
    virtual void function2() {};
};
上述代码中各个class的virtual table中每个function指向的具体内容如下图:


之所以调用virtual function比调用普通函数更慢,是因为:1)要用*__vptr访问virtual table;2)需要在virtual table中找到真正要调用的函数



Diamond Problem

假设有两个class,B和C都继承自A,当D继承自B和C时,会发生diamond problem,如图所示:


class Animal {
public:
	Animal() {
		cout << "Animal() is called\n";
	}
	Animal(int w) {
		cout << "Animal(int) is called\n";
	}
	int weight;
	int getWeight();
};

class Tiger: public Animal {
public:
	...
	Tiger(int w): Animal(w) {
		cout << "Tiger(int) is called\n";
	}
};

class Lion: public Animal {
public:
	...
	Lion(int w): Animal(w) {
		cout << "Lion(int) is called\n";
	}
};

class Liger: public Tiger, public Lion {
public:
	...
	Liger(int w): Tiger(w), Lion(w) {
		cout << "Liger(int) is called\n";
	}
};

...
Liger l(10);	// Animal(int w) is called twice
int w = l.getWeight();	// compile error
在上例中,Liger继承自Tiger和Lion,而Tiger和Lion又都继承自Animal,导致Liger有两套Animal的subobject,进而在调用Liger的object的getWeight()时,compiler不知道应该调用继承自Tiger的函数还是继承自Lion的函数;另外Liger的constructor调用了Tiger和Lion的constructor,而Tiger和Lion的constructor又各自调用了Animal的constructor,进而会导致在调用Liger的constructor时,Animal的constructor会被调用两遍。解决的方法是使用virtual inheritance,如下例所示:

class Animal {
public:
	Animal() {	// error if default constructor is not defined
		cout << "Animal() is called\n";
	}
	Animal(int w) {
		cout << "Animal(int) is called\n";
	}
	int weight;
	int getWeight();
};

class Tiger: virtual public Animal {
public:
	...
	Tiger(int w): Animal(w) {
		cout << "Tiger(int) is called\n";
	}
};

class Lion: virtual public Animal {
public:
	...
	Lion(int w): Animal(w) {
		cout << "Lion(int) is called\n";
	}
};

class Liger: public Tiger, public Lion {
public:
	...
	Liger(int w): Tiger(w), Lion(w) {
		cout << "Liger(int) is called\n";
	}
};

...
Liger l(10);	// default constructor of Animal, i.e "Animal()" is called

在使用了virtual inheritance后,Liger中Tiger的Animal部分和Lion的Animal部分是相同的,也就是说Liger只有一个共享的Animal实例。令Tiger和Lion共享一个Animal实例的功能是通过在Liger中记录Tiger、Lion和Animal成员的memory offset来实现的;然而,这个offset通常在runtime才能获知,所以Liger就变成了(vpointer, Tiger, vpointer, Lion, Liger, Animal)。这里有两个vtable pointer,每个继承关系一个,并且都virtually继承自Animal,所以Liger object的大小会增加两个指针。所有的Liger类型都会使用同样的vpointer,但是每个Liger object都有自己独特的Animal object



要注意的是,在添加了virtual之后,grandparent class(Animal)的default constructor会被调用,即使parent class(Tiger和Lion)显式调用了grandparent带参数的constructor。其实对于grandchild class来说,parent class的constructor中调用的grandparent class的constructor被跳过了,也就是说,即使在parent class的constructor中调用了grandparent class的constructor,在构建grandchild class时,parent constructor中所调用的grandparent的constructor也不会被调用。如果想要调用grandparent class带参数的的constructor,必须在grandchild class中调用,即:

class Liger: public Tiger, public Lion {
public:
	...
	Liger(int w): Animal(w), Tiger(w), Lion(w) {	// explicityly call parameterized constructor of grandparent class here
		cout << "Liger(int) is called\n";
	}
};

也就是说,其实grandparent对应内容的初始化是在grandchild中进行的

通常应该通过parent class调用grandparent class的constructor,不能在grandchild class中直接调用它,只有在有"virtual"关键词时才能这么做

另外,virtual base class的constructor总会在non-virtual base class的constructor之前被调用,这样保证了一个继承自virtual base class的class能够安全的在derive class的constructor中被调用

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值