春季总结(一)

目录

一、epoll

边沿触发vs水平触发

代码

二 、C++内存

三、虚函数

四、排序算法

五、C++11的新特性

右值引用延长了临时对象的生命周期

移动语义

深拷贝带来的问题

六、Lamda

七、OPENCV

八、内存



一、epoll

高并发网络编程之epoll详解_shenya1314的博客-CSDN博客_epool

深入理解 Epoll - 知乎

一般客户端使用select,服务端使用epoll。基于select模型的服务器程序,要达到10万级别的并发访问,是一个很难完成的任务,一般只能处理几千的并发连接

epoll的设计和实现与select完全不同。epoll通过在Linux内核中申请一个简易的文件系统(文件系统一般用什么数据结构实现?红黑树)。把原先的select/poll调用分成了3个部分:

第一步:epoll_create()系统调用。此调用返回一个句柄,之后所有的使用都依靠这个句柄来标识。

第二步:epoll_ctl()系统调用。通过此调用向epoll对象中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。

第三部:epoll_wait()系统调用。通过此调用收集收集在epoll监控中已经发生的事件。

使用内核级别的回调,监控list链表有没有数据,内核通过红黑树管理soket,存储有数据的list

边沿触发vs水平触发

epoll事件有两种模型,边沿触发:edge-triggered (ET), 水平触发:level-triggered (LT)

水平触发(level-triggered)

  • socket接收缓冲区不为空 有数据可读 读事件一直触发
  • socket发送缓冲区不满 可以继续写入数据 写事件一直触发

边沿触发(edge-triggered)

  • socket的接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件
  • socket的发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件

边沿触发仅触发一次,水平触发会一直触发。

浅析epoll的水平触发和边缘触发,以及边缘触发为什么要使用非阻塞IO_虚心学习进步的博客-CSDN博客_epoll水平和边缘触发

代码

#define MAX_EVENTS 10
           struct epoll_event ev, events[MAX_EVENTS];
           int listen_sock, conn_sock, nfds, epollfd;

           /* Code to set up listening socket, 'listen_sock',
              (socket(), bind(), listen()) omitted */

           // 创建epoll实例
           epollfd = epoll_create1(0);

           if (epollfd == -1) {
               perror("epoll_create1");
               exit(EXIT_FAILURE);
           }

           // 将监听的端口的socket对应的文件描述符添加到epoll事件列表中
           ev.events = EPOLLIN;
           ev.data.fd = listen_sock;
           if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
               perror("epoll_ctl: listen_sock");
               exit(EXIT_FAILURE);
           }

           for (;;) {
               // epoll_wait 阻塞线程,等待事件发生
               nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
               if (nfds == -1) {
                   perror("epoll_wait");
                   exit(EXIT_FAILURE);
               }

               for (n = 0; n < nfds; ++n) {
                   if (events[n].data.fd == listen_sock) {
                       // 新建的连接
                       conn_sock = accept(listen_sock,
                                          (struct sockaddr *) &addr, &addrlen);
                       // accept 返回新建连接的文件描述符
                       if (conn_sock == -1) {
                           perror("accept");
                           exit(EXIT_FAILURE);
                       }
                       setnonblocking(conn_sock);
                       // setnotblocking 将该文件描述符置为非阻塞状态

                       ev.events = EPOLLIN | EPOLLET;
                       ev.data.fd = conn_sock;
                       // 将该文件描述符添加到epoll事件监听的列表中,使用ET模式
                       if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
                                   &ev) == -1)
                           perror("epoll_ctl: conn_sock");
                           exit(EXIT_FAILURE);
                       }
                   } else {
                       // 使用已监听的文件描述符中的数据
                       do_use_fd(events[n].data.fd);
                   }
               }
           }

二 、C++内存

C/C++程序内存的分配_cherrydreamsover的博客-CSDN博客_c++内存分配

1栈区(stack):由编译器自动分配与释放,存放为运行时函数分配的局部变量、函数参数、返回数据、返回地址等。其操作类似于数据结构中的栈。
2堆区(heap):一般由程序员自动分配,如果程序员没有释放,程序结束时可能有OS回收。其分配类似于链表。
3全局区(静态区static):存放全局变量、静态数据、常量。程序结束后由系统释放。全局区分为已初始化全局区(data)和未初始化全局区(bss)。
4常量区(文字常量区):存放常量字符串,程序结束后有系统释放。代码区:存放函数体(类成员函数和全局区)的二进制代码。

1new delete

从堆上分配

亦称为动态内存分配。
程序在运行的时候使用malloc或者new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。
动态内存的生命周期有程序员决定,使用非常灵活,但如果在堆上分配了空间,既有责任回收它,否则运行的程序会出现内存泄漏,频繁的分配和释放不同大小的堆空间将会产生内存碎片
注意,局部变量即使是new的,也需要手动进行delete。是否需要对指针delete取决于指针指向的内存是否是用new操作符申请的。 用了new表示动态分配了内存,需要用delete将内存还给系统。 对于不是动态申请的内存,在对象声明周期结束后就会自动删除,不需要delete。

2深拷贝 浅拷贝:

C++ 类(深拷贝和浅拷贝)_下忍的博客-CSDN博客_c++深拷贝和浅拷贝

浅拷贝: 将原对象或原数组的引用直接赋给新对象,新数组,新对象/数组只是原对象的一个引用。深拷贝: 创建一个新的对象和数组,将原对象的各项属性的“值”(数组的所有元素)拷贝过来,是“值”而不是“引用”

简单来说,浅拷贝就是拷贝了指针,就是赋值,两个对象指向同一个地址。深拷贝就是重新申请内存,将内容复制一份出来。

var arr1 = new Array(12,23,34)

Var arr2 = arr1;///浅拷贝

通过开辟空间的方式,进行深拷贝,

 //深拷贝(拷贝构造函数)
    Test(const Test& a)
    {
        this->p = new int(*a.p);
        cout << "对象被创建" << endl;
    }

浅拷贝:位拷贝,拷贝构造函数,赋值重载,多个对象共用同一块资源,同一块资源释放多次,崩溃或者内存泄漏

深拷贝:每个对象共同拥有自己的资源,必须显式提供拷贝构造函数和赋值运算符

int *a = new int(10);

3拷贝构造函数

什么情况使用复制构造函数:类的对象需要拷贝时,拷贝构造函数将会被调用。以下情况都会调用拷贝构造函数:
(1)一个对象以值传递的方式传入函数体 
(2)一个对象以值传递的方式从函数返回 
(3)一个对象需要通过另外一个对象进行初始化。

如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝。上面提到,如果没有自定义复制构造函数,则系统会创建默认的复制构造函数,但系统创建的默认复制构造函数只会执行“浅拷贝”,即将被拷贝对象的数据成员的值一一赋值给新创建的对象,若该类的数据成员中有指针成员,则会使得新的对象的指针所指向的地址与被拷贝对象的指针所指向的地址相同,delete该指针时则会导致两次重复delete而出错

class Complex
{
private :
    double m_real;
    double m_imag;

public:
    // 无参数构造函数
    // 如果创建一个类你没有写任何构造函数,则系统会自动生成默认的无参构造函数,函数为空,什么都不做
    // 只要你写了一个下面的某一种构造函数,系统就不会再自动生成这样一个默认的构造函数,如果希望有一个这样的无参构造函数,则需要自己显示地写出来
    Complex(void)
    {
         m_real = 0.0;
         m_imag = 0.0;
    }
    // 复制构造函数(也称为拷贝构造函数)
    // 复制构造函数参数为类对象本身的引用,用于根据一个已存在的对象复制出一个新的该类的对象,一般在函数中会将已存在对象的数据成员的值复制一份到新创建的对象中
    // 若没有显示的写复制构造函数,则系统会默认创建一个复制构造函数,但当类中有指针成员时,由系统默认创建该复制构造函数会存在风险,具体原因请查询有关 “浅拷贝” 、“深拷贝”的文章论述
    Complex(const Complex & c)
    {
        // 将对象c中的数据成员值复制过来
        m_real = c.m_real;
        m_img  = c.m_img;
    }
    // 一般构造函数(也称重载构造函数)
    // 一般构造函数可以有各种参数形式,一个类可以有多个一般构造函数,前提是参数的个数或者类型不同(基于c++的重载函数原理)
    // 例如:你还可以写一个 Complex( int num)的构造函数出来
    // 创建对象时根据传入的参数不同调用不同的构造函数
    Complex(double real, double imag)
    {
         m_real = real;
         m_imag = imag;
     }
    // 类型转换构造函数,根据一个指定的类型的对象创建一个本类的对象,基本同上
    // 例如:下面将根据一个double类型的对象创建了一个Complex对象
    Complex::Complex(double r)
    {
        m_real = r;
        m_imag = 0.0;
    }
    // 等号运算符重载
    // 注意,这个类似复制构造函数,将=右边的本类对象的值复制给等号左边的对象,它不属于构造函数,等号左右两边的对象必须已经被创建
    // 若没有显示的写=运算符重载,则系统也会创建一个默认的=运算符重载,只做一些基本的拷贝工作
    Complex &operator=(const Complex &rhs)
    {
        // 首先检测等号右边的是否就是左边的对象本,若是本对象本身,则直接返回
        if ( this == &rhs )
        {
            return *this;
        }
        // 复制等号右边的成员到左边的对象中
        this->m_real = rhs.m_real;
        this->m_imag = rhs.m_imag;
        // 把等号左边的对象再次传出
        // 目的是为了支持连等 eg:    a=b=c 系统首先运行 b=c
        // 然后运行 a= ( b=c的返回值,这里应该是复制c值后的b对象)
        return *this;
    }
};

4for_each

for_each()事实上是個 function template,其实质如下  [effective STL item 41]

复制代码
template<typename InputIterator, typename Function>
Function for_each(InputIterator beg, InputIterator end, Function f) {
  while(beg != end) 
    f(*beg++);
}

前两个参数列表是遍历容器的迭代器,第三个参数是对应的回调函数.回调函数的原理都是将参数传递至相应的函数体,再进行操作.经常与LAMda表达式配合使用。此外,这是stl的函数。其深入了解:c++ for_each( )学习 - zhangkele - 博客园

void Print(int val)
{
	cout << val << " ";
}
void test1()
{
	vector<int> v;
	
	v.push_back(1);
	v.push_back(5);
	v.push_back(4);
	v.push_back(2);
	v.push_back(6);
	
	for_each(v.begin(),v.end(),Print);
 } 
//======
int main()
{
    int a[4] = { 1, 2, 3, 4 };
    int total = 0;
    for_each(a, a + 4, [&](int & x) { total += x; x *= 2; });
    cout << total << endl;  //输出 10
    for_each(a, a + 4, [=](int x) { cout << x << " "; });
    return 0;
}
表 1 C++ STL头文件
<iterator><functional><vector><deque>
<list><queue><stack><set>
<map><algorithm><numeric><memory>
<utility>

三、虚函数

1基类的虚析构函数

C++中基类的析构函数为什么要用virtual虚析构函数_IIcyZhao的博客-CSDN博客_为什么要虚析构函数

总的来说,基类的析构函数一定要写成虚的。原因是为了动态绑定。

Base *d = new Drived();
delete d; //如果Base类中的析构函数是非虚的,那么此delete操作只会调用Base的析构函数,而不会调用Drived的析构函数。

直接的讲,C++中基类采用virtual虚析构函数是为了防止内存泄漏。具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。所以,为了防止这种情况的发生,C++中基类的析构函数应采用virtual虚析构函数。

虚函数是动态绑定的基础。现在析构函数不是virtual的,因此不会发生动态绑定,而是静态绑定,指针的静态类型为基类指针,因此在delete时候只会调用基类的析构函数,而不会调用派生类的析构函数。

只有虚函数,才会有虚函数表,析构时才会找到子类的重写函数。

2虚函数表

C++虚函数表(多态的实现原理)

C++ 虚函数表解析_haoel的博客-CSDN博客_虚函数表

在计算机发展的早期,计算机非常昂贵稀有,运行速度慢,计算机的运算时间和内存是宝贵的,因此人们不惜多花人力编写运行速度更快、更节省内存的程序;如今,计算机的运算时间和内存往往没有人的时间宝贵,运算速度也很快,因此,在用户可以接受的前提下,降低程序运行的效率以提升人员的开发效率就是值得的了。“多态”的应用就是典型例子。

C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置,占用4个字节,64位则占用8个字节。

多重继承(有虚函数覆盖)

下面我们再来看看,如果发生虚函数覆盖的情况。下图中,我们在子类中覆盖了父类的f()函数。

下面是对于子类实例中的虚函数表的图:

我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。如:


            Derive d;

            Base1 *b1 = &d;

            Base2 *b2 = &d;

            Base3 *b3 = &d;

            b1->f(); //Derive::f()

            b2->f(); //Derive::f()

            b3->f(); //Derive::f()



            b1->g(); //Base1::g()

            b2->g(); //Base2::g()

            b3->g(); //Base3::g()

amazing

  • 任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,所以,这样的程序根本无法编译通过。
  • 可以再IDE查看虚函数表

四、排序算法

堆排序:堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。方法为:

1、创建一个堆,最大堆或者最小堆。这一般修要单独写一个heapify排序函数,将数组构造成堆的样子。

2、在heapify中进行操作,可以递归,或者不递归循环,从最后一个父节点开始,将数组的父子节点依次进行交换,使得堆变成最大堆。

3、输出排列结果。将堆的顶端与最末节点进行交换,然后截断,输出此截断值,之后重新建立堆。重复操作直到全部输出完毕。

冒泡排序    :平均时间复杂度为O(n2)。稳定排序,遍历两次,依次比较相邻的数并进行交换操作。

快速排序:名字起的真朴实无华。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次。是一个不稳定的排序算法,方法为随机找一个数,然后分区域为<>=三个或两个区域,然后对每个区域递归进行划分。

五、C++11的新特性

1左值和右值

【C++深陷】之“左值与右值”_Jinxk8的博客-CSDN博客

当对象被用作左值的时候,用的是对象的身份(在内存中的位置);当一个对象被用作右值的时候,用的是对象的值(内容)。——C++ Primer

2.1 生存时间

左值持久,右值短暂

左值有持久的状态,通常是程序员声明、定义的对象。比如a

右值要么是字面值常量,要么是表达式求值过程中创建的临时对象。比如a+3, 9

2.2 访问关系

从定义直观的去理解,右值关注它的值(内容),不能被修改;左值可以获得它的身份(内存中的位置,内存中的地址),因此可以被修改,也可以通过地址获得它的值(内容)。这里可以简单的理解为“读写”关系,右值可以读,不可以写;左值既可以读,也可以写。但是他们不仅仅是“读写”的区别。

一个例外情况,const限定符constexpr关键字修饰的对象,即使它是左值,也不可以被修改。

还有一个例外情况,就是使用右值引用绑定的右值对象,我们是可以对它进行赋值的,只不过我们不能对移后源对象的内容,作任何假设

区分它们还有一个原则:需要右值的地方,都可以使用左值来替代。

例如:

int a = 5, b = 8;
int c = a + b;

该原则有一个例外情况——右值引用(见第3节):获得右值引用的地方,不可以使用左值替代。

2.3总结

左值通常持久,右值通常是编译器创建的临时变量。左值大部分情况下可以修改,右值不可以,有两个例外情况。可以在要求使用右值的地方使用左值代替,有一个例外情况。

左值引用和右值引用是两种复合数据类型,他们都是左值,是C++为了区分开左值和右值创建的。我们可以使用右值引用实现内存的移动操作,避免二次开销。

2右值引用

​​​​​​​终于来到了右值引用。这一章比较烦的原因是,别的或多或少都用过或者见过,但是右值引用我压根没用过也没见过。纯理论知识。

我们是可以使用右值引用进行赋值的(2.2例外情况),这也很意外,因为明明是右值的引用,却可以进行内容上的修改。因为右值引用是具有表达式的左值属性(如上例中的rr_1)。

int a = 0;
int &&rr_a = a + 3;
rr_a = 6;


使用右值引用有2个重要原则,我们必须保证接下来:

所引用的对象要被销毁
该对象没有其他的代码在使用
上述代码,a + 3的结果是一个临时量,它肯定是会在作用域结束后被销毁的。我们也没有机会访问到该临时量的真正身份,只能通过赋值=运算得到它的值。

对于返回左值的表达式:

返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符,都是返回左值的表达式的例子。我们可以将一个左值引用绑定到这类表达式的结果上。
对于返回右值的表达式:

返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符,都生成右值。我们不能将一个左值引用绑定到这类表达式上,但是我们可以将一个const的左值引用或者一个右值引用绑定到这类表达式上。
(以上两句话摘选自摘选自《C++ Primer》第5版本,抽象却精湛,建议背诵)

使用右值引用的两个原则,在对象移动时,可以完美的使用。C++引入右值引用,也是为了进行对象的移动,

既然是数据类型,就存在转换关系。

我们可以使用标准库函数std::move得到左值的右值引用类型。

int &&rr_1 = 42;
int &&rr_2 = std::move(rr_1);

move调用告诉编译器,我们有一个左值rr_1,但是我希望像一个右值一样处理它。

用处:

右值引用延长了临时对象的生命周期

int i = getI();  // getI() 会返回一个 int 型的临时变量
T&& t = getT();  // t 是一个右值引用
                  // getT() 同样返回一个临时变量,但是该临时变量被“引用”了
                  // 因此生命周期得到了延长

利用右值引用避免临时对象的拷贝和析构 

  • 非右值引用,关闭返回值优化
    construct: 1        // 第一次构造,getA() 中的局部变量 a
    copy construct: 1   // 第二次构造,将 a 复制给一个临时变量
    destruct: 1           // 析构局部变量 a
    copy construct: 2   // 第三次构造,将临时变量复制给 a2
    destruct: 2           // 析构临时变量
    destruct: 3           // 程序结束,析构变量 a2
    
  • 右值引用,关闭返回值优化
    construct: 1        // 第一次构造,getA() 中的局部变量 a
    copy construct: 1   // 第二次构造,将 a 复制给一个临时变量
                        // 右值引用 a3 延长了临时变量的声明周期,使其没有马上被析构
    destruct: 1           // 析构局部变量 a
    destruct: 2           // 程序结束,析构变量 a3

总的来说,少构造了一次。这应该是主要应用场景了。

3decltype

【C++深陷】之“decltype”_Jinxk8的博客-CSDN博客_decltype

decltype被称作类型说明符,它的作用是选择并返回操作数的数据类型。​​​​​​​decltype并不会实际计算表达式的值,编译器分析表达式并得到它的类型。函数调用也算一种表达式,因此不必担心在使用decltype时真正的执行了函数,正如前例中的f()

const int ci = 0, &cj = ci;
// x的类型是const int
decltype(ci) x = 0;
// y的类型是const int &
decltype(cj) y = x;

移动语义

深拷贝带来的问题

  • 带有堆内存的类,必须提供一个深拷贝构造函数,以避免“指针悬挂”问题 

    所谓指针悬挂,指的是两个对象内部的成员指针变量指向了同一块地址,析构时这块内存会因被删除两次而发生错误

  • 利用右值引用可以避免临时对象的拷贝可析构
  • 但编译器的返回值优化(Return Value Optimization, RVO)做得“更绝”,直接回避了所有拷贝构造

class A {
public:
    A(): m_ptr(new int(0)) {                    // new 堆内存
        cout << "construct" << endl;
    }

    A(const A& a): m_ptr(new int(*a.m_ptr)) {   // 深拷贝构造函数
        cout << "copy construct" << endl;
    }

    A(A&& a): m_ptr(a.m_ptr) {                  // 移动构造函数
        a.m_ptr = nullptr;      // 把参数对象的指针指向 nullptr
        cout << "move construct" << endl;
    }

    ~A(){
        // cout << "destruct" << endl;
        delete m_ptr;   // 析构函数,释放堆内存的资源
    }
private:
    int* m_ptr;         // 成员指针变量
};

A getA() {
    return A();
}

int main() {
    A a = getA();
    return 0;
}
construct
move construct        // 没有调用深拷贝,值调用了移动构造函数
move construct
  • 这里没有自动类型推断,所以 A&& 一定是右值引用类型,因此所有临时对象(右值)会匹配到这个构造函数,而不会调用深拷贝
  • 对于临时对象而言,没有必要调用深拷贝
  • 这就是所谓的移动语义——右值引用的一个重要目的就是为了支持移动语义

六、Lamda

类似c#中的匿名委托。实际上就是一个匿名函数,比较特别的是他可以使用外部的变量,而不需要传参。当然,最好还是传进去,因为可以控制是否被改变。

定义在{}外面的变量在{}中是否允许被改变。=表示不允许,&表示允许。实际上,“外部变量访问方式说明符”还可以有更加复杂和灵活的用法。例如:

  • [=, &x, &y]表示外部变量 x、y 的值可以被修改,其余外部变量不能被修改;
  • [&, x, y]表示除 x、y 以外的外部变量,值都可以被修改。
  • 还可以将一个函数指针标识为LAMDA表达式
=表示不允许,&表示允许。当然,在{}中也可以不使用定义在外面的变量。“-> 返回值类型”可以省略。

下面是一个合法的Lambda表达式:
[=] (int x, int y) -> bool {return x%10 < y%10; }
Lambda 表达式实际上是一个函数,只是它没有名字。下面的程序段使用了上面的 Lambda 表达式:
int main()
{
    int a[4] = { 1, 2, 3, 4 };
    int total = 0;
    for_each(a, a + 4, [&](int & x) { total += x; x *= 2; });
    cout << total << endl;  //输出 10
    for_each(a, a + 4, [=](int x) { cout << x << " "; });//如果是 x *= 2则会报错
    return 0;
}

更复杂的:
#include <iostream>
using namespace std;
int main()
{   
    int x = 100,y=200,z=300;
    auto ff  = [=,&y,&z](int n) {
        cout <<x << endl;
        y++; z++;
        return n*n;
    };
    cout << ff(15) << endl;
    cout << y << "," << z << endl;
}

程序的输出结果如下:
100
225
201, 301

第 6 行定义了一个变量 ff,ff 的类型是 auto,表示由编译器自动判断其类型(这也是 C++11 的新特性)。本行将一个 Lambda 表达式赋值给 ff,以后就可以通过 ff 来调用该 Lambda 表达式了。
第 11 行通过 ff,以 15 作为参数 n 调用上面的 Lambda 表达式。该 Lambda 表达式指明,对于外部变量 y、z,可以修改其值;对于其他外部变量,例如 x,不能修改其值。因此在该表达式执行时,可以修改外部变量 y、z 的值,但如果出现试图修改 x 值的语句,就会编译出错。

一些隐藏:

[] (int x, int y) { return x + y; } // 隐式返回类型
[] (int& x) { ++x;  } // 没有 return 语句 -> Lambda 函数的返回类型是 'void'
[] () { ++global_x;  } // 没有参数,仅访问某个全局变量
[] { ++global_x; } // 与上一个相同,省略了 (操作符重载函数参数)

七、OPENCV

八、内存

创建一个对象和给对象赋值的时候,内存的变化
数据结构的实现方式,内存的实现方式

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值