第1章 程序设计: 综述

1.4 C++类

1.4.3 接口与实现的分离

对象的声明同于基本类型

//合法声明
IntCell obj1;     //零参数构造函数
IntCell obj2(12); //单参数构造函数
//不正确的声明
IntCell obj3 = 37; //单参数构造函数是explicit的,如果不是explicit则声明合法
IntCell obj4();    //函数声明,表明该函数不带参数且返回IntCell型的对象
//使用花括号统一初始化语法,C++中可以改写成这样
IntCell obj1;     //零参数构造函数
IntCell obj2{12}; //单参数构造函数
IntCell obj4{};   //零参数构造函数,相较IntCell obj4;的声明更好,因为初始化风格得到统一

1.4.4 vector类和string类

vector类

vector类用来代替内置C++数组。一个vector对象知道它本身有多大。两个字符串对象可以通过==、<等运算符进行比较。vector和string均可使用=来复制。

#include <iostream>
#include <vector>
using namespace std;
int main()
{
    vector<int> squares(100);
    for(int i = 0; i < squares.size(); ++i)   //size是个方法,返回vector对象的大小
        squares[i] = i * *;
    for(int i = 0; i < squares.size(); ++i)
        cout << i << "" << squares[i] << end1;
    return 0;
}
vector对象初始化
vector<int> daysInMonth = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};//允许
vector<int> daysInMonth = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};//允许
//歧义
vector<int> daysInMonth = {12};
//可能意思:(1)大小为12的vector对象;(2)一个在位置0上有一个元素12的其大小为1的vextor对象
//初始化表列优先权更大,应该是(2)的意思
//如果想初始化大小为12的vextor,使用小括号
vector<int> daysInMonth =(12);

String类

拥有比较两个字符串状态的所有关系运算符和相等运算符。如果两个字符串str1和str2的值相等,则str==str2就是true。string还有一个length方法,该方法返回字符串的长度。

范围for循环

int sum = 0;
for(int i = 0; i < squares.size(); ++i)
    sun += squares[i];
//依次访问每一个元素
int sum = 0;
for(int x : squares)
    sum += x;           
//允许使用保留字auto以表示编译器将会自动推断出适当的类型
int sun = 0;
for(auto x : squares)
    sun += x;
//只有每一项都陆续被访问且不需要下标,范围for循环才适用,
//以下两个循环都不能改写成范围for循环,因为下标i还用于其他目的(参与计算)
//迄今为止fanweifor循环只允许对项的查看
for(int i = 0; i < squares.size(); ++i)
    square[i] = i * i;
for(int i = 0; i < squares,size(); ++i)
    cout << i << "" << square[i] << end1;

1.5 C++细节

1.5.1 指针(pointer)

int main()
{
    IntCell* m;  
    m = new IntCell{0};   
    m -> write(5);
    cout << "Cell contents:" << m -> read() <<end1;
    delete m;
    return 0;
}

声明

m是一个指针变量,指向IntCell对象,m的值是它所指向的对象的地址,此刻m尚未被初始化。
未经初始化指针的使用通常会使程序崩溃,因为它们常导致对不存在的内存单元的访问。
初始化方式:
1、把第3行和第4行连接起来;
2、把m初始化成空指针nullptr。

对象的动态创建(第4行)

操作符new返回指向新创建对象的指针。

m = new IntCell(); //OK
m = new IntCell{}; //C++11
m = new IntCell;   //首选

垃圾收集与delete(第7行)

内存漏洞(memory leaks):

C++不进行垃圾回。当一个通过new操作符被分配地址的对象不再被引用时,必须(通过一个指针)对该对象应用delete操作。否则,该对象使用的内存就会丢失(直到程序终止)。

重要规则:

在能够使用自动变量的时候不要使用new操作符。

通过指针访问对象的成员(第5、6行)

如果一个指针变量指向一个类类型的对象,那么所指对象的(可见)成员能够通过->操作符被访问。
m->write(5)相当于是*m.write(5)

取地址操作符(&)

该操作符返回一个对象所占用的内存地址,并对实现别名测试是有用的。

1.5.2 左值、右值和引用

左值:

标识非临时性对象的表达式

右值:

标识临时性对象的表达式,或者是一个不与任何对象相联系的值(如字面值常数)。

vector<string> arr(3);
const int x = 2;
int y;
  ···
int z = x + y;
string str = "foo"
vector<string>*ptr = $arr;
//arr、str、arr[x]、&x、y、z、ptr、*ptr、(*ptr)[x]都是左值,x也是左值,但有const声明不可被修改
//一般法则:如果程序中有一个变量名,那么它就是一个左值,而不管该变量是否可被修改。
//2、"foo"、x+y、str.substr(0,1)都是右值,因为它们均为字面值

左值引用

左值引用的声明通过在某个类型后放置一个符号&来进行。此时,一个左值引用成为了它所引用的对象的同义词

string str = "hell";
string & rstr = str;   //rstr是str的另一个名字
rstr += 'o';           //把str改成“hello”
bool cond = (&str == &rstr);   //true;str和rstr是同一个对象
string &bad1 = "hello";        //非法:“hello”不是可修改的左值
string &bad2 = str + "";       //非法:str+""不是左值
string &sub == str.substr(0, 4);  //非法:str.substr(0, 4)不是左值
左值引用用途1:给结构复杂的名称起别名

最简单的应用是单独使用一个局部引用变量以达到重新命名一个被复杂表达式所知晓的对象的目的。

auto & whichList = theLists[myhash(x, theLists.size())];
if(find(begin(whichList), end(whichList), x) != end(whichList))
    return false;
whichList.push_back(x);
//引用whichList变量使复杂得多的表达式theLists[myhash(x, theLists.size())]不必书写4次
//直接写成auto whichList = theLists[myhash(x, theLists.size())];不行
//它将建立一个拷贝,然后在最后一行上的push_back操作将会用于该拷贝,而不是用在原始对象上。
左值引用的用途2:范围for循环
//设我们想要让一个vector对象所有的值都增加1
for(int i = 0; i < arr.size(); ++i)
    ++arr[i];
//但是直接使用范围for循环不行,因为x要担任vector中每一个值的拷贝,不能修改原来的值
for(auto x : arr)
    ++x;
//我们真正想要的实际上是让x是vector中每个值的另一个名字,如果x是一个引用,那么这很容易做到
for(auto & x : arr)//行得通
	++x;
左值引用的用途3:避免复制
//假设有一个函数findMax,它返回一个vector对象或其他大集合中的最大值。此时给定一个vector arr,
//如果调用findMax,自然会写
auto x = findMax(arr);
//但是,如果vector存储的是一些大的对象,那么x就将是arr中最大值的一个拷贝。在很多实例中,我们
//只需要这个值,并不想让x发生任何变化。此时,声明x为arr中最大值的另一个名字将更有效
//因此声明x为一个引用
auto & x = findMax(arr);
//该行代码阐述两个重要概念:
//(1)引用变量常常用于避免越过函数调用界限复制对象(不管是在函数调用中还是在函数返回值中)
//(2)为了使用引用代替复制能够进行传递和返回,在函数声明和返回中是需要语法的。

右值引用

通过在某个类型后放置一个符号&&而被声明
右值引用也可以引用一个右值(即一个临时量)

string str = "hell";      
string && bad1 = "hello";  //合法
string && bad2 = str + ""; //合法
string && sub = str.substr(0, 4);  //合法

1.5.3 参数传递

传值调用

把实参复制到形参

传左值引用调用

double average(double a, double b);//返回a和b的平均值
void swap(double a, double b);//交换a和b;参数类型错误
string randomItem(vector<string> arr);//返回arr中的一个随机项:低效
//如果调用
double z = average(x, y);
/*
传值调用把x复制到a,把y复制到b,假定x和y对于average是不可访问的局部变量,于是保证在average返回时
x和y是不变的,这是非常理想的性质。但是这个性质恰恰是传值调用对于swap函数执行无效的原因
*/
swap(x, y);
//传值调用保证,无论swap怎么实现,x和y都将保持不变。需要声明a和b为引用型参数
void swap(double & a, double & b);//交换a和b;参数类型正确
/*
a是x的同义词,b是y的同义词。在swap的实现中对a和b的改变就是对x和y的改变。
*/

这种形式的参数传递被叫做传引用调用,在C++11中被称为传左值引用调用。

传常量引用调用

以函数randomItem进行阐述。该函数要从vector arr 中返回一个随机项。使用传值调用作为参数传递机制迫使在调用randomItem(vector)中对vector vec 的复制,这比起计算和返回一个随机选取的数组下标的开销来是一个极其昂贵的操作。
可以通过声明arr对vec的常量引用以避免复制而达到相同的语义效果。这里,arr是vec的同义词,不用复制,而由于它是const的,因此不能被修改。这本质上提供了与传值调用相同的可见行为

string randomItem(const vector<string> & arr);//返回arr中的一个随机项

这种形式的参数传递在C++中叫作传对常量引用的调用,简称传常量引用调用

传统C++中的3中参数传递机制

1、对于小的不应被函数改变的对象,采取传值调用是合适的。//无&
2、对于大的不应被函数改变且复制代价昂贵的对象,采取传常量引用调用是合适的。//const 类 & 同义词
3、对于所有可以被函数改变的对象,采取传引用调用是合适的。 //&,无const

第4种传递参数的方式:传右值引用调用

由于右值存储的是要被销毁的临时量,像x = rval这样的表达式(其中rval是一个右值)可以通过移动(move)而不是复制来实现。移动一个对象的状态常常比复制它要容易得多,因为这可能就涉及一个简单的指针改变。
如果y是一个左值,那么x = y可以是一次复制;如果y是一个右值,那就得使用一次移动

string randomItem(const vector<string> &arr);//返回左值arr中的随机项
string randonItem(vector<string> && arr);//返回右值arr中的随机项

vector<string> v{"hello", "world"};
cout << randomItem{v} << end1;//调用左值的方法
cout <<randomItem({"hello", "world"}) << end1;//调用右值的方法

1.5.4 返回值传递

传值返回

double average(double a, double b);//返回a和b的平均值
LargeType randomItem(const vector<LargeType> & arr);//潜在低效
vector<int> partialSum(const vector<int> & arr);//在C++中高效

函数返回一个可以被调用者使用的适当类型的对象。
在所有情况下函数调用的结果都是一个右值。
然而,对于randomItem的调用却含有潜在的低效风险。对partialSum的调用同样隐含有低效风险。

LargeTyoe randomItem1(const vector<LargeType> & arr)
{
    return arr[randomInt(0, arr.size()-1)];  //randomInt返回下标,return返回左值
}

const LargeType & randomItem2(const vector<LargeType> & arr)
{
    return arr[randomInt(0, arr.size()-1)];
}

vector<LargeType> vec;
  ···
LargeType item1 = randomItem1(vec);//复制
LargeType item2 = randomItem2(vec);//复制
const LargeType& item3 = randomItem2(vec);//不复制

首先考虑randomItem的两种实现。
第一种实现见1-4行,使用的是传值返回。具有随机的数组下标的LargeType型对象将作为返回序列的 一部分被复制。这种复制之所以存在,是因为返回表达式可能是右值(例如返回x+4),因此,到函数调用在第13行返回时逻辑上它是不存在的。可是,在上述的这种情况下,返回类型是一个左值,它在函数调用返回以后将长期存在,因为arr是与vec相同的。
第二种实现利用这一点,并使用传常量引用返回以避免直接复制。如第6-9行。不过,调用者必须也使用常量引用以存取这个返回值,如第15行所示;否则,仍将存在一个拷贝。
常量引用(const ···&)意味着我们不想让调用者通过使用返回值制造变化(改变item3就会改变取出的这个arr);此时需要这样,因为arr本身就是一个不可修改的vector对象。另一种做法是在第15行使用auto&声明item3,编译器会自动识别出它的类型就是const LargeType&。

移动语义

vector<int> partialSum(const vector<int>& arr)
{
	vector<int> result(arr.size());

	result[0] = arr[0];
	for(int i = 1; i < arr.size(); i++)
  	result[i] = result[i-1] + arr[i];

	return result;
}

	vector<int> vec;
  	···
	vector<int> sums = partialSum(vec);//在老的C++中是复制,在C++11中是移动

1.5.5 std::swap和std::move

void swap(double& x, double&y)
{
    double tmp = x;
    x = y;
    y = tmp;
}

void swap(vector<string>&x, vector<string>&y)
{
    vector<string> tmp = x;
    x = y;
    y = tmp;
}

使用3次复制可以容易实现两个double型数据的交换,同样的方法也能完成对两个类型更大的数据的交换,但此时的复制非常昂贵。没有必要复制,我们实际想要的是移动而不是复制。
如果赋值运算符的右边(或构造函数)是一个右值,那么当对象支持移动操作时我们能够自动地避免复制。换句话说,如果vector支持高效的移动,而且第10行上的x是一个右值,那么x就可以移动到tmp。
事实上,vector确实支持移动,但第10、11、12行上的x、y、tmp都是左值(如果对象有名字,那么它就是左值)
下面代码解决了这个问题。通过3次移动进行交换。第1种交换通过强制类型转换实现,第2种交换使用的是std::move

void swap(vector<string>&x, vector<string>&y)
{
    vector<string>tmp = static_cast<vector<string>&&>(x);
    x = static_cast<vector<string>&&>(y);
    y = static_cast<vector<string>&&>(tmp);
}

void swap(vector<string>& x, vecot<string>& y)
{
    vector<string> tmp = std::move(x);
    x = std::move(y);
    y = std::move(tmp);
}
//std::move能够把任何左值(或右值)转换成右值,可以对任何类型的数据进行交换

1.5.6 五大函数:析构函数,拷贝构造函数,移动构造函数,拷贝赋值operator=,移动赋值operator=

析构函数

只要一个对象运行越出范围,或经受一次delete,则析构函数就要被调用。析构函数的唯一责任就是释放掉在对象使用期间获得的资源,包括关于任意的new操作调用对应的delete,关闭任何打开的文件。

拷贝构造函数和移动函数

有两个特殊的构造函数用来构造一个新的对象,它被初始化为与另一个同样类型对象相同的状态。如果这个已存在的对象是左值,那么就用拷贝构造函数;而如果这个已存在的对象是右值(一个迟早要被删除的临时量),那么就用移动构造函数。

//带有初始化的声明,如
InCell B = C;//若C是左值则调用拷贝构造函数;若C是右值则调用移动构造函数
InCell B {C};//若C是左值则调用拷贝构造函数;若C是右值则调用移动构造函数
//但不适用于
B = C;//赋值运算符

使用传值调用(而不是通过&或const&)所传递的对象很少这么做
传值返回(而不是通过&或const&返回)的对象。同样,如果返回的对象是一个左值,则调用拷贝构造函数;如果是一个右值,则调用移动构造函数

拷贝赋值和移动赋值(operator=)

当=用于两个先前均被构造过的对象时,则调用赋值运算符。lhs=rhs是要拷贝rhs的状态到lhs上。如果rhs是一个左值,那么可通过使用拷贝赋值运算符完成;如果rhs是一个右值,那么可通过使用移动赋值运算符做到。

默认情形

设类包含一个数据成员,它是个指针。这个指针指向一个动态定址的对象。默认的析构函数对那些指针类型的数据成员无能为力.不仅如此,拷贝构造函数和拷贝赋值运算均复制指针的值而不是指针指向的对象。
这样,我们将有两个类实例,它们都包含指针,而指针又都指向相同的对象,这就是所谓的浅拷贝
但典型的情况是我们应该得到深拷贝,从而得到整个对象的复制品
因此,当一个类包含指针作为数据成员时,重要的是深层语义,一般我们必须自己实现析构函数、拷贝赋值和拷贝构造函数。这么做排除了移动的默认情形,因此还必须实现移动赋值和移动构造函数。
一般来说,我们对所有的5个函数都给出定义

~IntCell();//析构函数
IntCell(const IntCell& rhs);//拷贝构造函数
IntCell(IntCell&& rhs);//移动构造函数
IntCell& operator = (const IntCell& rhs);//拷贝赋值
IntCell& operator = (IntCell&& rhs);//移动赋值

operator是一个关键字,operatpr=可看成一个函数名,它的返回类型是对调用对象的一个引用(引用返回类型),以便允许链式赋值a=b=c。
有关operator细节可参考这篇博客
https://blog.csdn.net/liitdar/article/details/80654324?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522166478312716782414932262%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=166478312716782414932262&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_positive~default-1-80654324-null-null.142v51pc_rank_34_2,201v3add_ask&utm_term=operator&spm=1018.2226.3001.4187

当默认操作不起作用时

最常见的默认操作不起作用的情况出现在数据成员为指针类型,并且指针由某个对象成员函数(譬如构造函数)定址的时候。
设我们通过动态定址一个int型变量来实现IntCell。

class IntCell
{
	public:
    	explicit IntCell(int initialValiue = 0) //带有默认值的构造函数,IntCell a;时调用此函数intialValue默认为0;创建IntCell a{2}时,仍调用这个函数initValue为2
          {storedValue = new int{initialValue};}  //storedVlaue指向一个值为0的int数

    	int read() const   //const修饰成员函数,则该成员函数中不能修改当前类的成员变量
         {return* storedValue;}
    	void write(int x)
         {*storedValue = x;}

    private:
    	int* storedValue;
}
int f()
{
    IntCell a{2};
    IntCell b = a;
    IntCell c;

    c = b;
    a.write(4);
    cout << a.read() <<end1 << b.read() <<end1 << c.read() << end1;

    return 0
}

输出是3个4,逻辑上只有a应该是4.
问题在于,默认的拷贝赋值运算符和拷贝构造函数复制了指针storedValue.于是,a.storedValue、b.storedValue、c.storedValue都指向同一个int量。这些复制为浅拷贝。被复制的是这些指针而不是被指向的对象。其次,不太明显的问题是内存漏洞。由a的构造函数初始定位的int型变量依旧定位不变但需要被回收。由c的构造函数定位的int型变量不再被任何指针变量引用,它也需要回收,但我们不再有指针指向它。

class IntCell
{
	public:
    	explicit IntCell(int initialValue = 0)
         {storedValue = new int(initialValur);}
    	
		~IntCell();//析构函数
          {delete storedValue;}
		
		IntCell(const IntCell& rhs);//拷贝构造函数
          {storedValue = new int{*rhs.storedValue};}
        
		IntCell(IntCell&& rhs): storedValue{rhs.storedValue}//移动构造函数
          {rhs.storedValue = nullptr;}
		
		IntCell& operator = (const IntCell& rhs);//拷贝赋值
          {
              if(this != &rhs)      //this指当前类的一个实例
                  *storedValue = *rhs.storedValue;
              return *this;
          }
		
		IntCell& operator = (IntCell&& rhs);//移动赋值
          {
              std:: swap(storedValue, rhs.storedValue);
              return *this;    //返回当前对象的引用
          }

    	int read() const
          {return *storedValue;}
    	void write(int x)
          {*storedValue = x;}

    	private:
        	int* storedValue;
}
//假如定义temp *get(){return this;},那么返回的this就是地址,即返回一个指向对象的指针
//假如定义temp get(){return *this;} 那么返回的就是对象的克隆,是一个临时变量
//假如定义temp &get(){return *this;} 那么返回的就是对象本身

一旦析构函数被实现,浅拷贝就将导致一个错误:两个IntCell对象使它们的storedValue指向同一个int对象。一旦第一个IntCell对象的析构函数被调用以回收其storedValue指针正在关注的对象,第二个IntCell对象拥有的storedValue指针就过时了。
在C++11中,常常使用拷贝和交换格式编写拷贝赋值

        IntCell& operator = (const IntCell& rhs);//拷贝赋值
        {
              IntCell copy = rhs;      
    		  std::swap(*this, copy);
              return *this;
        }

第13行和第14行的移动构造函数把数据表示从rhs移动到*this,然后设置rhs的原始数据(包括指针)到合法但容易被销毁的状态。如果存在非原始数据,那么这些数据必须被移入初始化表列中。
例如如果还有vector items,则此时构造函数将是:

IntCell(IntCell&& rhs): storedValue{rhs.storedValue}//移动构造函数
                    	items{std::move(rhs.ietms)}
          {rhs.storedValue = nullptr;}

1.6 模板

1.6.1 函数模板

一个函数模板不是一个具体的函数,而是可以变成一个函数型式。
下列代码阐释了一个函数模板findMax,其中包含template声明的那一行指出,Comparale是一个模板参数:它可能被任何类型替代而生成一个函数。

/**
 * Return the maximum item in array a.
 * Assumes a.size( ) > 0.
 * Comparable objects must provide operator< and operator=
 */
template <typename Comparable>
const Comparable & findMax( const vector<Comparable> & a )
{
    int maxIndex = 0;

    for( int i = 1; i < a.size( ); ++i )
        if( a[ maxIndex ] < a[ i ] )
            maxIndex = i;
    
    return a[ maxIndex ];
}

调用findMax(v4)将导致编译时错误,因为当Comparable被IntCell替代时,上述第12行非法,不存在为IntCell定义的<函数。
因为模板参数可以假设为任意类型的类类型,所以在决定参数传递和返回传递的约定时,应该假定模板参数不是基本类型。因此采用常量引用返回。

int main( )
{
    vector<int>     v1(37;   //v1是vector<int>类型,有37个元素
    vector<double>  v2(40;
    vector<string>  v3(80;
    vector<IntCell> v4( 75 );

    // 填入到未显示的那些vector中的附加代码

    cout << findMax( v1 ) << endl;  // OK: Comparable = int
    cout << findMax( v2 ) << endl;  // OK: Comparable = double
    cout << findMax( v3 ) << endl;  // OK: Comparable = string
    cout << findMax( v4 ) << endl;  // Illegal; operator< undefined

    return 0;
}

1.6.2 类模板

假设Obect有一个零参数构造函数、一个拷贝构造函数和一个拷贝赋值运算符,Memory虽像IntCell类,但是却为任何类型的Object工作

/**
 * 一个模拟内存单元的类.
 */
template <typename Object>
class MemoryCell
{
  public:
    explicit MemoryCell( const Object & initialValue = Object{ } );
    	:storedValue{initialValue}{ }
    const Object & read( ) const;
    	{return storedValue;}
    void write( const Object & x );
        {storedValue = x;}
  private:
    Object storedValue;
};

Object是通过常量引用传递的。构造函数的默认参数不是0,因为0不可能是一个合理的Object对象。取而代之的,默认参数则是使用零参数构造函数构造一个Object对象所得的结果。

int main( )
{
    MemoryCell<int>    m1;
    MemoryCell<string> m2{ "hello" };

    m1.write( 37 );
    m2.write( m2.read( ) + " world" );
    cout << m1.read( ) << endl << m2.read( ) << endl;

    return 0;
}

Memory既存储基本类型,又存储类类型的对象。Memory不是一个类,只是一个类模板。MemoryCell和MemoryCell才是两个具体的类。

1.6.3 Object、Comparable和一个例子

class Square
{
  public:
    explicit Square( double s = 0.0 ) : side{ s }
      { }
    
    double getSide( ) const
      { return side; }
    double getArea( ) const
      { return side * side; }
    double getPerimeter( ) const
      { return 4 * side; }

    void print( ostream & out = cout ) const
      { out << "(square " << getSide( ) << ")"; }
    bool operator< ( const Square & rhs ) const
      { return getSide( ) < rhs.getSide( ); }

  private:
    double side;
};

    // 给Square定义一个输出操作符
ostream & operator<< ( ostream & out, const Square & rhs )
{
    rhs.print( out );
    return out;
}

int main( )
{
    vector<Square> v = { Square{ 3.0 }, Square{ 2.0 }, Square{ 2.5 } };
    
    cout << "Largest square: " << findMax( v ) << endl;

    return 0;
}

运算符重载使我们能够为内置运算符重新定义一个含义。Square类通过存储边长来表示一个正方形并定义了operator<
有关operator<<,可参考
https://blog.csdn.net/u011028345/article/details/76445970?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-76445970-blog-12947181.pc_relevant_multi_platform_whitelistv3&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-76445970-blog-12947181.pc_relevant_multi_platform_whitelistv3&utm_relevant_index=1

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值