c++ primer(第五版)学习笔记及习题答案代码版(第十二章)动态内存与智能指针

笔记较为零散,都是自己不熟悉的知识点。

习题答案置于一个 .h 和.cc 中,需要演示某一题直接修改 #define NUM****, 如运行12.22题为#define NUM1222;

chapter 12
1.  新标准库提供了两种智能指针类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。新标准提供的这两种智能指针的区别在于
管理底层指针的方式:shared_ptr允许多个指针指向同一个对象,unique_ptr则‘独占’所指向的对象。还定义了一个名为week_ptr的伴随类,它是一种弱引用,指向shared_ptr
所管理的对象。三种都定义在memory头文件中.
最安全的分配是使用动态内存的方法是调用一个名为make_shared的标准库函数,此函数在动态内存中分配一个对象并开始初始化它,返回指向此对象的shared_ptr。
shared_ptr<int> p3 = make_shared<int>(42);  //auto p3
auto q(p3);      //p和q指向相同对象,次对象有两个引用
每个shared_ptr都有一个关联的计数器,通常称为引用计数。当用一个shared_ptr初始化另一个shared_ptr,或将它作为参数传递给一个函数以及作为函数的返回值时,
它所关联的计数器就会递增,当shared_ptr被销毁或离开局部作用域时,计数器递减。
2. 如果将shared_ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用erase删除不再需要的那些元素。
使用动态生存期的资源类:
a 程序不知道自己需要使用多少对象
b 程序不知道所需对象的准确类型
c 程序需要在多个对象间共享数据
使用动态内存的一个常见原因是允许多个对象共享相同的状态。
3. 默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化。
如果new不能分配所要求的内存空间,它会抛出一个类型为bad_alloc的异常。可以改变new的方式来阻止抛出异常。
int *p2 = new(nothrow) int;   //如果分配失败,new返回一个空指针 
bad_alloc和nothrow定义在new头文件中。
4. 我们传递给delete的指针必须指向动态分配的内存,或者是一个空指针。释放一块并非new分配的内存,或者将相同的指针值释放多次,其行为是未定义的。
delete一个指针后,指针值变成无效。但很多机器上指针仍然保存着动态内存的地址。delete之后指针就变成了空悬指针。
避免空悬指针的方法: 在指针即将要离开其作用域之前释放掉它所关联的内存。
5. 接受指针参数的智能指针构造函数是explicit的,因此我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针。
shared_ptr<int> p1 = new int(1024);     //错误。必须使用直接初始化形式
shared_ptr<int> p2(new int(1024));    //正确,使用了直接初始化形式
同理,一个返回的shared_ptr的函数不能在其返回语句中隐式转换一个普通指针。
5.  使用一个内置指针访问一个智能指针所负责的对象很危险,因为我们不知道对象何时会销毁。也不要使用get初始化另一个智能指针或为智能指针赋值。
智能指针类型定义了一个名为get的函数。它返回一个内置指针,指向智能指针管理的对象。 因为我们有时需要向不能使用智能指针的代码传递一个内置指针,
用reset来将一个新的指针赋予一个shared_ptr:
shared_ptr<int> p(new int(42));
p = new int(1024);   //错误,不能将一个指针赋予shared_ptr
p.reset(new int(1024));    //正确,p指向一个新对象
6.  unique_ptr'拥有'它所指向的对象。与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。
由于unique_ptr拥有它所指向的对象,因此unique_ptr不支持普通的拷贝和赋值操作。但是可以通过调用release和reset将指针的所有权从一个unique_ptr转移给另一个
unique_ptr。当然有一个例外,我们可以拷贝或赋值一个将要被销毁的unique_ptr,常见的是从函数返回一个unique_ptr:
7. week_ptr是一个不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将week_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。
创建一个week_ptr时用一个shared_ptr来初始化它。
由于对象可能不存在,我们不能使用week_ptr直接访问对象,而必须调用lock,它检查week_ptr指向的对象是否存在。
8. 当new分配一个数组时,我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。即使使用类型别名定义了一个数组类型,new也不会分配一个数组类型的对象。
由于分配的内存不是一个数组类型,所以不能对动态数组调用begin和end。
记住我们所说的动态数组并不是数组类型。
当释放一个指向数组的指针时,空方括号是必须的:它指示编译器此指针指向一个对象数组的第一个元素。如果忽略了[]的行为是未定义的。
allocator类定义在memory中,它帮助我们将内存分配和对象构造分离开来。提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。
alloc.construct(q++, "hi");   //*q为hi
为了使用allocate返回的内存,我们必须用construct构造对象,使用未构造的内存,其行为是未定义的。

//Chapter12.h
#ifndef CHAPTER12_H
#define CHAPTER12_H

#include <iostream>
#include <vector>
using namespace std;

/*12.2*/
class StrBlobPtr;  //12.19
class ConstStrBlobPtr;
class StrBlob{
	friend class StrBlobPtr;
    friend class ConstStrBlobPtr;
public:
    typedef vector<string>::size_type size_type;
    StrBlob(): data(make_shared<vector<string>>()){ }
    StrBlob(initializer_list<string> li) : data(make_shared<vector<string>>(li)) {}
    size_type size() const { return data->size(); }
    bool empty() const { return data -> empty(); }
    void push_back(const string &t){ data-> push_back(t); }
    void pop_back();
//    void push_back(const string &t)const { data-> push_back(t); }
    string& back() {
		check(0, "back on empty StrBlob");
		return data->back();
	}
    string& front(){
		check(0, "front on empty StrBlob");
		return data->front();
	}
    const string& front()const{
		check(0, "front on empty StrBlob");
		return data->front();
	}
    const string& back() const{
		check(0, "back on empty StrBlob");
		return data->back();
	}
	StrBlobPtr begin();             //12,19
	StrBlobPtr end();               //12.19
    ConstStrBlobPtr begin() const; // should add const 12.22
    ConstStrBlobPtr end() const;   // should add const 12.22
private:
    void check(size_type i, const string &msg) const{
		if(i >= data->size())	
			throw out_of_range(msg);
	}
    shared_ptr<vector<string>> data;
};

/*12.27*/
class QueryResult;
class TextQuery{
public:
	typedef vector<string>::size_type LineNo;
	TextQuery(ifstream&);
	QueryResult query(const string&) const;
private:
	shared_ptr<vector<string>> input;
	map<string, shared_ptr<set<LineNo>>> result;
};


class QueryResult{
	friend ostream& print(ostream&, const QueryResult&);
public:
	typedef set<StrBlob::size_type>::iterator ResultIter;
	QueryResult(const string& s, shared_ptr<set<TextQuery::LineNo>> set, 
			shared_ptr<vector<string>> pvec): word(s), nos(set), input(pvec){ }

	
	ResultIter begin() const { return nos->begin(); }  //12.33
	ResultIter end() const { return nos->end(); }               
	shared_ptr<vector<string>> get_file() const { return input; }  //12.33
private: 
	string word;
	shared_ptr<set<TextQuery::LineNo>> nos;
	shared_ptr<vector<string>> input;
};

/*12.32*/
class QueryResult_32;
class TextQuery_32 {
public:
    TextQuery_32(ifstream&);
    QueryResult_32 query(const string&) const;

private:
    shared_ptr<StrBlob> input;
    map<string, shared_ptr<set<StrBlob::size_type>>> result;
};

class QueryResult_32 {
public:
    friend ostream& print(ostream&, const QueryResult_32&);

public:
    QueryResult_32(const string& s, shared_ptr<set<StrBlob::size_type>> set,
                shared_ptr<StrBlob> v)
        : word(s), nos(set), input(v)   {  }

private:
    string word;
    shared_ptr<set<StrBlob::size_type>> nos;
    shared_ptr<StrBlob> input;
};

ostream& print(ostream&, const QueryResult_32&);

#endif


//main.cc
#include <fstream>
#include <sstream>
#include <iostream>
#include <vector>
#include <memory>
#include <algorithm>
#include <cstring>
#include <map>
#include <set>
#include "Chapter12.h"
using namespace std;
#define NUM1232


/*12.6*/
#ifdef NUM126
vector<int>* vector_create(){
	vector<int>* pvec = new vector<int>(); 
	return pvec;
}
void vector_input(vector<int>* pvec){
	cout << "Input number: " <<endl;
	for(int i=0; cin >> i; pvec->push_back(i), ++i)
		;
}
void vector_print(vector<int>* pvec){
	for(auto &i : *pvec) cout << i << " ";
	cout <<endl;
}
#endif
/*12.7*/
#ifdef NUM127
shared_ptr<vector<int>> vector_create(){
	auto pvec = make_shared<vector<int>>(); 
	return pvec;
}
void vector_input(shared_ptr<vector<int>> pvec){
	cout << "Input number: " <<endl;
	for(int i=0; cin >> i; pvec->push_back(i), ++i)
		;
}
void vector_print(shared_ptr<vector<int>> pvec){
	for(auto &i : *pvec) cout << i << " ";
	cout <<endl;
}
#endif
/*12.10*/
void process(shared_ptr<int> ptr)
{
    std::cout << "inside the process function:" << ptr.use_count() << "\n";
}
/*12.14*/
struct destination{
	string ip;
	int port;
	destination(string _ip, int _port):ip(_ip), port(_port){}
};
struct connection{
	string ip;
	int port;
	connection(string _ip, int _port): ip(_ip), port(_port){}
};
connection connect(destination* pdes){
	shared_ptr<connection> pcon = make_shared<connection>(pdes-> ip, pdes->port);
	cout << "Connection... "<< pcon.use_count() <<endl;
	return *pcon;
}

void disconnect(connection pcon){
	cout <<"connection close "<< pcon.ip <<":"<<pcon.port<<endl;
}
void end_connection(connection *p){ 
	disconnect(*p);
}
void f(destination &d){
	connection c = connect(&d);
	shared_ptr<connection> p(&c, end_connection);
	cout << "maked connection: " <<p.use_count() <<endl;
}
/*12.15*/
void f_15(destination &d){
	connection c = connect(&d);
	shared_ptr<connection> p(&c, [](connection *p){disconnect(*p); }); 
	cout << "maked connection: " <<p.use_count() <<endl;
}
/*12.19*/
class StrBlobPtr{
public:
	StrBlobPtr() : curr(0){}
	StrBlobPtr(StrBlob &a, size_t sz = 0): wptr(a.data), curr(sz)  { }
	bool operator !=(const StrBlobPtr& p){ return p.curr != curr; }  //重载函数
	string& deref() const;
	StrBlobPtr& incr();
private:
	shared_ptr<vector<string>> check(size_t, const string&)const;
	weak_ptr<vector<string>> wptr;
	size_t curr;
};

StrBlobPtr StrBlob::begin() {
	return StrBlobPtr(*this); 
} 

StrBlobPtr StrBlob:: end()  {
	auto ret = StrBlobPtr(*this, data->size());
	return ret;
}

shared_ptr<vector<string>> StrBlobPtr::check(size_t i, const string& msg)const {
	auto ret = wptr.lock();
	if(!ret)
		throw runtime_error("unbound StrBlobPtr" );
	if(i >= ret->size())
		throw out_of_range(msg);
	return ret;
}

string& StrBlobPtr::deref()const{
	auto p = check(curr, "dereference past end" );
	return (*p)[curr];
}

StrBlobPtr&	StrBlobPtr::incr(){
	check(curr, "increment past end of StrBlobPtr");
	++curr;
	return *this;
}
/*12.22*/
class ConstStrBlobPtr {
public:
    ConstStrBlobPtr() : curr(0) {}
    ConstStrBlobPtr(const StrBlob& a, size_t sz = 0) : wptr(a.data), curr(sz) {} // should add const
    bool operator!=(ConstStrBlobPtr& p) { return p.curr != curr; }
    const string& deref() const
    { // return value should add const
        auto p = check(curr, "dereference past end");
        return (*p)[curr];
    }
    ConstStrBlobPtr& incr()
    {
        check(curr, "increment past end of StrBlobPtr");
        ++curr;
        return *this;
    }

private:
    std::shared_ptr<vector<string>> check(size_t i, const string& msg) const
    {
        auto ret = wptr.lock();
        if (!ret) throw std::runtime_error("unbound StrBlobPtr");
        if (i >= ret->size()) throw std::out_of_range(msg);
        return ret;
    }
    std::weak_ptr<vector<string>> wptr;
    size_t curr;
};

ConstStrBlobPtr StrBlob::begin() const{ //add const
	return ConstStrBlobPtr(*this);
}
ConstStrBlobPtr StrBlob::end() const{ //add const
	return ConstStrBlobPtr(*this, data->size());
}

/*12.27*/
TextQuery::TextQuery(ifstream& ifs) : input(new vector<string>){
	LineNo lineNo{0};
	for(string line; getline(ifs, line); ++lineNo){
		input -> push_back(line);
		istringstream line_stream(line);
		for(string text, word; line_stream >> text; word.clear()){
			remove_copy_if(text.begin(), text.end(), back_inserter(word), ::ispunct);
			//防止shared_ptr的计数增加,用引用
			auto &nos = result[word];
			if(!nos)
				nos.reset(new set<LineNo>);
			nos -> insert(lineNo);
		}
	}
}

/*12.30*/
QueryResult TextQuery::query(const string& str)const{
	static shared_ptr<set<LineNo>> nodata(new set<LineNo>);
	auto found = result.find(str);
	if(found == result.end())
		return QueryResult(str, nodata, input);
	else
		return QueryResult(str, found->second, input);
} 


ostream& print(ostream& out, const QueryResult& res){
	out << res.word << " occurs " <<res.nos->size() <<
			(res.nos->size() >1 ? " times" : " time") <<endl;
	for(auto i : *res.nos)
		out << "\t(line " << i+1<<") "<<res.input->at(i) <<endl;
	return out;
}

void runQueries(ifstream& infile){
    TextQuery tq(infile);
    while (true) {
        cout << "Enter word to find, input 'quit' out: ";
        string s;
        if (!(cin >> s) || s == "q") break;
        print(cout, tq.query(s)) << endl;
    }
}

/*12.32*/
TextQuery_32::TextQuery_32(ifstream& ifs) : input(new StrBlob){
    StrBlob::size_type lineNo{0};
    for (string line; getline(ifs, line); ++lineNo) {
        input->push_back(line);
        istringstream line_stream(line);
        for (string text, word; line_stream >> text; word.clear()) {
				remove_copy_if(text.begin(), text.end(),
                                back_inserter(word), ::ispunct);
            auto& nos = result[word];
            if (!nos) nos.reset(new set<StrBlob::size_type>);
            nos->insert(lineNo);
        }
    }
}

QueryResult_32 TextQuery_32::query(const string& str) const{
    static shared_ptr<set<StrBlob::size_type>> nodate(
        new set<StrBlob::size_type>);
    auto found = result.find(str);
    if (found == result.end())
        return QueryResult_32(str, nodate, input);
    else
        return QueryResult_32(str, found->second, input);
}

ostream& print(ostream& out, const QueryResult_32& qr){
    out << qr.word << " occurs " << qr.nos->size()
        << (qr.nos->size() > 1 ? " times" : " time") << endl;
    for (auto i : *qr.nos) {
        ConstStrBlobPtr p(*qr.input, i);
        out << "\t(line " << i + 1 << ") " << p.deref() << endl;
    }
    return out;
}

int main(){
/*12.1*/
#ifdef NUM121
    cout << "对象b2被销毁,但是b2中的元素并没有被销毁,因为b1正在使用,所以b1和b2都有4个元素. " <<endl;
#endif
/*12.2*/
#ifdef NUM122
    const StrBlob csb{"hello", "world", "linux"};
    StrBlob sb{"hello", "world", "c++"};
    cout << csb.front() << " " << csb.back() << endl;
    sb.back() = "primer";
    cout << sb.front() << " " << sb.back() << endl;
#endif
/*12.3*/
#ifdef NUM123
	cout <<"可以加,因为data并没有被修改,它是指向vetor类型的指针,但是没有意义,"<<endl;
#endif
/*12.4*/
#ifdef NUM124
	cout << "因为vector<string>::size_type是unsigned类型,当小于0时会自动转换为大于0的数." <<endl;
#endif
/*12.5*/
#ifdef NUM125
	cout <<"explicit防止initializer_list到StrBlob的自动转换. "<<endl;
#endif
/*12.6*/
#ifdef NUM126
	vector<int>* ptr = vector_create();
	vector_input(ptr);
	vector_print(ptr);
	delete ptr;
#endif
/*12.7*/
#ifdef NUM127
	shared_ptr<vector<int>> ptr = vector_create();
	vector_input(ptr);
	vector_print(ptr);
#endif
/*12.8*/
#ifdef NUM128
	cout << "p转换成bool类型,最后没有释放,内存泄漏. "<<endl;
#endif
/*12.9*/
#ifdef NUM129
	cout << "智能指针会自动释放,但r被赋值之后没有释放,内存泄漏. "<<endl;
#endif
/*12.10*/
#ifdef NUM1210
    shared_ptr<int> p(new int(42));
    process(shared_ptr<int>(p));
    cout << p.use_count() << endl;
    auto q = p;
    cout << p.use_count() << "\n";
    cout << "the int p now points to is:" << *p << "\n";
#endif
/*12.11*/
#ifdef NUM1211
    shared_ptr<int> p(new int(42));
	process(shared_ptr<int>(p.get()));
	cout << "shared_ptr<int>(p.get())创建一个临时的智能指针,并且拷贝到参数中.因为其并不是p的拷贝,函数结束内存就被释放."<<endl;
#endif
/*12.12*/
#ifdef NUM1212
	cout << "(a)合法(b)不合法,普通指针不能隐式的转换成智能指针.(c)不合法,原因同上.(d)合法,但不提倡,很容易导致问题. "<<endl;
#endif
/*12.13*/
#ifdef NUM1213
	cout <<"导致错误,两次释放同一内存. "<<endl;
	auto sp = std::make_shared<int>();
	auto p = sp.get();
	delete p;	
#endif
/*12.14*/
#ifdef NUM1214
	destination dest("192.168.93.129", 8080);
	f(dest);	
#endif
/*12.15*/
#ifdef NUM1215
	destination dest("192.168.93.129", 8080);
	f_15(dest);	
#endif
/*12.16*/
#ifdef NUM1216
	unique_ptr<string> p1(new string("Selinux"));
	unique_ptr<string> p2(p1); //拷贝操作:error
	unique_ptr<string> p3;
	p3 = p2;                   //赋值:error
#endif
/*12.17*/
#ifdef NUM1217
	int ix = 1024, *pi = &ix, *pi2 = new int(2048);
	typedef std::unique_ptr<int> IntP;
	IntP p0(ix);  //invalid conversion from ‘int’ to ‘std::unique_ptr<int>::pointer 
	IntP p1(pi);  //能编译通过,但运行时出错, unique_ptr p1处理完成时要delete这时没有对象来free
	IntP p2(pi2);  //编译通过,但导致悬吊指针
	IntP p3(&ix);  //同Intp p1(pi);
	IntP p4(new int(2048));  //正确
    IntP p5(p2.get());  //error, double free
#endif
/*12.18*/
#ifdef NUM1218
	cout <<"因为shared_ptr可以拷贝,其他指向对象的指针也能够释放该对象的内存,因此release是无效的."<<endl;
#endif
/*12.19*/
#ifdef NUM1219
	StrBlob b1; 
	{
		StrBlob b2{ "a", "an", "the" };	
		b1 = b2;  
		b2.push_back("about");
		cout << b2.size() << endl;
	}
	cout << b1.size() << endl;
	for (StrBlobPtr it = b1.begin(), pend = b1.end(); it != pend; it.incr())
		cout << it.deref() << endl;
#endif
/*12.20*/
#ifdef NUM1220
    fstream ifs("./book_sales");
    StrBlob blob;
    for(string str; getline(ifs, str); blob.push_back(str))
        ;
    for(StrBlobPtr pbeg(blob.begin()), pend(blob.end()); pbeg !=pend; pbeg.incr())
        cout << pbeg.deref() <<endl;    
#endif
/*12.21*/
#ifdef NUM1221
    cout << "运来的版本更好,因为其看上去更为简单." <<endl;   
#endif
/*12.22*/
#ifdef NUM1222
	cout <<"见Chapter12.h声明. "<<endl;
#endif
/*12.23*/
#ifdef NUM1223
{
    char *pchar = new char[256]();
    const char* str1 = "hello"; const char* str2 = "world";
    strcat(pchar, str1);
    strcat(pchar, str2);
    cout << pchar << endl;
	delete []pchar;
}
  // string
    string str1{"hello "}, str2{"world"};
    cout << str1 + str2 << endl;
#endif
/*12.24*/
#ifdef NUM1224
    cout << "How long do you want the string be ? ";
    int size{0};
    cin >> size;
    char* input = new char[size + 1]();
    cin.ignore();    //cin.ingore(256, '\n');
    cout << "input the string: ";
    cin.get(input, size + 1);
    cout << input <<endl;
    delete[] input;
#endif
/*12.25*/
#ifdef NUM1225
    int *pa = new int[10];
    delete []pa;
#endif
/*12.26*/
#ifdef NUM1226
	int n = 5;
	allocator<string> alloc;
	auto const p = alloc.allocate(n); //allocator<T> a, a.allocate(n),保存n个类型为T的对象.
	string s;
	auto q = p;
	while(cin >> s && q != p + n)
		alloc.construct(q++, s);   //p为T*的指针
	while(q != p){
		cout << *--q <<" ";
		alloc.destroy(q);
	}
	alloc.deallocate(p, n);
#endif
/*12.27*/
#ifdef NUM1227
	cout <<"见Chapter.h 及 main()外声明. "<<endl;
#endif
/*12.28*/
#ifdef NUM1228
	ifstream file("./storyDataFile"); 
	vector<string> input;
	map<string, set<decltype(input.size())>> dictionary;
	decltype(input.size()) LineNo{0};
	for(string line; getline(file, line); ++LineNo){
		input.push_back(line);
		istringstream line_stream(line);
		for(string text, word; line_stream >> text; word.clear()){
			remove_copy_if(text.begin(), text.end(), back_inserter(word), ::ispunct);
			dictionary[word].insert(LineNo);
		}
	}

	while(1){
		cout << "Enter word to query, or input 'quit' out"<<endl;
		string s;
		if(!(cin >> s) || s == "quit")
			break;
		auto found = dictionary.find(s);
		if(found != dictionary.end()){
			cout << s <<" occurs: " << found->second.size() 
					<<(found->second.size() > 1 ? " times" : " time")<<endl;
			for(auto i : found->second)
					cout << "\t(line " << i+1 << ") "<<input.at(i) <<endl;
		}else
			cout << s <<" occurs 0 time"<<endl;
	}
#endif
/*12.29*/
#ifdef NUM1229
	string rap;
	do{
		cout << "Enter word to query, or input 'quit' out"<<endl;
		string s;
		if(!(cin >> s))
			print(cout, tq.query(s)) << endl;
		cin >> rap;
	}while(!rap.empty() && rap[0] != 'n'); 
	cout << "do while 版本更符合直觉. "<<endl;
#endif
/*12.30*/
#ifdef NUM1230
	ifstream file("./storyDataFile");
	runQueries(file);
#endif
/*12.31*/
#ifdef NUM1231
	cout << "set中不存在重复的关键字,vector并不能保证这一点.所以这里set更合适."<<endl;
#endif
/*12.32*/
#ifdef NUM1232
	ifstream infile("./storyDataFile");
    TextQuery_32 tq(infile);
    while (true) {
        cout << "Enter word to find, input 'quit' out: ";
        string s;
        if (!(cin >> s) || s == "q") break;
        print(cout, tq.query(s)) << endl;
    }
#endif
/*12.33*/
#ifdef NUM1233
	cout <<"见Chapter12.h声明. "<<endl;
#endif
	return 0;
}
参考资料:
c++ primer中文版第五版,电子工业出版社。
c++ primer第四版习题解答,人民邮电出版社。
pezy@github https://github.com/pezy/CppPrimer

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值