【C++基础】

c++

一、基础知识

1.1 数据结构表格

数据结构有序可变支持操作适用场景
数组(Array)访问、修改、遍历固定大小的数据存储
向量(Vector)动态调整、随机访问、添加、删除动态数组
单向链表(Singly Linked List)插入、删除、遍历动态数据结构
双向链表(Doubly Linked List)插入、删除、遍历动态数据结构,双向操作
栈(Stack)入栈、出栈、查看栈顶元素 递归、撤销操作
队列(Queue)入队、出队、查看队

1.2 static的用法和作用

  1. 隐藏:当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性
  2. 保持变量内容的持久:(static变量中的记忆功能和全局生存期)存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围
  3. 默认初始化为0:其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是0x00,某些时候这一特点可以减少程序员的工作量
  4. C++中的类成员声明static:
  • 作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍然维持上次的值
  • 在模块内的static全局变量可以被模块内的所有函数访问,但不能被模块外的访问
  • 在类中的static成员函数属于整个类所有,这个函数不接受this指针,因为只能访问static成员变量

类内

  • static类对象必须要在类外进行初始化,static修饰的变量先于对象存在,所以static修饰的变量要在类外初始化
  • 由于static修饰的类成员属于类,不属于对象,因此static类成员函数是没有this指针的,this指针是指向本对象的指针。正因为没有this指针,所以static类成员函数不能访问非static的类成员,只能访问 static修饰的类成员
  • static成员函数不能被virtual修饰,static成员不属于任何对象或实例,所以加上virtual没有任何实际意义;静态成员函数没有this指针,虚函数的实现是为每一个对象分配一个vptr指针,而vptr是通过this指针调用的,所以不能为virtual;虚函数的调用关系,this->vptr->ctable->virtual function

1.3 指针-指针和引用的区别、this 指针、野指针,悬空指针、指针数组和数组指针、常量指针和指针常量区别

1. 指针和引用的区别

特点指针 (Pointer)引用 (Reference)
是否需要初始化不需要必须在声明时初始化
是否可以为空可以为 nullptr不能为 nullptr,必须引用一个有效的对象
是否可以改变可以改变指向的对象一旦初始化,就不能改变指向的对象
语法需要使用 * 和 & 符号进行访问和解引用语法更简洁,类似于普通变量的访问
内存管理需要手动管理动态内存(new 和 delete)无需手动管理内存,引用不会涉及动态内存管理
用途适用于需要动态内存管理、数组遍历、复杂数据结构适用于参数传递、返回值、简单的别

2. this 指针

  • 在 C++ 中,this 指针是一个特殊的指针,它指向当前对象的实例。
  • 在 C++ 中,每一个对象都能通过 this 指针来访问自己的地址。
  • this是一个隐藏的指针,可以在类的成员函数中使用,它可以用来指向调用对象。
  • 当一个对象的成员函数被调用时,编译器会隐式地传递该对象的地址作为 this 指针。
  • 友元函数没有 this 指针,因为友元不是类的成员,只有成员函数才有 this 指针。
    通过使用 this 指针,我们可以在成员函数中访问当前对象的成员变量,即使它们与函数参数或局部变量同名,这样可以避免命名冲突,并确保我们访问的是正确的变量。

野指针,悬空指针
定义

  • 是指针或引用在内存管理中的一种错误状态,它们指向的内存位置已经被释放或不再有效。这种状态可能导致不可预测的行为、崩溃或安全漏洞
  • 野指针是指向已经被释放或不再有效的内存区域的指针。当一个指针被释放后,它仍然保持原来的地址,而这个地址可能会被重新分配给其他对象或程序使用
    • 产生原因:
      • 释放内存后未将指针置空:释放动态分配的内存后,指针没有被设置为 nullptr,导致指向无效内存区域。
      • 使用已经超出作用域的指针:指针指向一个已经超出其作用域的局部变量或对象
  • 悬空指针与野指针类似,但通常指的是引用(Reference)而非指针。悬空引用指向已经被销毁或释放的对象
    • 产生原因
      • 指向超出作用域的局部变量:引用指向一个局部变量,而该变量在函数返回后超出了其作用域
      • 指向被释放的动态分配对象:引用指向一个动态分配的对象,而该对象已经被释放

如何避免野指针和悬空指针

  1. 释放内存后将指针置空:在释放动态分配的内存后,将指针设置为 nullptr
  2. 避免返回局部变量的引用:不要返回局部变量的引用或指针,确保返回的引用或指针指向有效的内存。
  3. 使用智能指针:智能指针如 std::unique_ptr 和 std::shared_ptr 自动管理内存,可以有效避免野指针和悬空指针的问题。

3. 指针数组和数组指针
4. int *p[10]表示指针数组,强调数组概念,是一个数组变量,数组大小为10,数组内每个元素都是指向int类型的指针变量
5. int (*p)[10]表示数组指针,强调是指针,只有一个变量,是指针类型,不过指向的是一个int类型的数组,这个数组大小是10

4. 常量指针和指针常量区别
7. 指针常量是一个指针,读成常量的指针,指向一个只读变量,也就是后面所指明的int const 和 const int,都是一个常量,可以写作int const *p或const int *p
8. 常量指针是一个不能给改变指向的指针。指针是个常量,必须初始化,一旦初始化完成,它的值(也就是存放在指针中的地址)就不能在改变了,即不能中途改变指向,如int *const p

1. 4 c++名词解释-类成员、构造 & 析构、拷贝构造、友元、内联、 this 指针、静态成员、内联函数

概念描述
类成员函数类的成员函数是指那些把定义和原型写在类定义内部的函数,就像类定义中的其他变量一样。
类访问修饰符类成员可以被定义为 public、private 或 protected。默认情况下是定义为 private。
构造函数 & 析构函数类的构造函数是一种特殊的函数,在创建一个新的对象时调用。类的析构函数也是一种特殊的函数,在删除所创建的对象时调用。跳出程序(比如关闭文件、释放内存等)前释放资源
C++ 拷贝构造函数拷贝构造函数,是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。
C++ 友元函数友元函数可以访问类的 private 和 protected 成员。
C++ 内联函数通过内联函数,编译器试图在调用函数的地方扩展函数体中的代码。
C++ 中的 this 指针每个对象都有一个特殊的指针 this,它指向对象本身。
C++ 中指向类的指针指向类的指针方式如同指向结构的指针。实际上,类可以看成是一个带有函数的结构。
C++ 类的静态成员类的数据成员和函数成员都可以被声明为静态的。
内联函数1. 内联函数以代码复杂为代价,它以省去函数调用的开销来提高执行效率。2. 所以一方面如果内联函数体内代码执行时间相比函数调用开销较大,则没有太大的意义;3. 另一方面每一处内联函数的调用都要复制代码,消耗更多的内存空间,因此以下情况不宜使用内联函数:函数体内的代码比较长,将导致内存消耗代价;函数体内有循环,函数执行时间要比函数调用开销大

1.5 友元函数&友元类

  • 友元提供了不同类的成员函数之间、类的成员函数和一般函数之间进行数据共享的机制。通过友元,一个不同函数或者另一个类中的成员函数可以访问类中的私有成员和保护成员。友元的正确使用能提高程序的运行效率,但同时也破坏了类的封装性和数据的隐藏性,导致程序可维护性变差
  • 有元函数是定义在类外的普通函数,不属于任何类,可以访问其他类的私有成员。但是需要在类的定义中声明所有可以访问它的友元函数。
  • 友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。但是另一个类里面也要相应的进行声明
  • 注意
    • 友元关系不能被继承
    • 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明
    • 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明

1.6 为什么析构函数一般写成虚函数

  1. 由于类的多态性,基类指针可以指向派生类的对象,如果删除改该基类的指针,就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放
  2. 如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全,造成内存泄漏

1.6 四种强制转换-reinterpret_cast、const_cast、tatic_cast < type-id > (expression)、dynamic_cast

  1. reinterpret_cast
    type-id 必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以用于类型之间进行强制转换。
  2. const_cast
    该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外, type_id和expression的类型是一样的。用法如下:
  3. 常量指针被转化成非常量的指针,并且仍然指向原来的对象
  4. 常量引用被转换成非常量的引用,并且仍然指向原来的对象
  5. const_cast一般用于修改底指针。如const char *p形式
  6. tatic_cast < type-id > (expression)
    该运算符把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法:
  • 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用引用的转换
    • 进行上行转换(把派生类的指针或引用转换成基类表示)是安全的
    • 进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的
  • 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。
  • 把空指针转换成目标类型的空指针
  • 把任何类型的表达式转换成void类型
  • 注意:static_cast不能转换掉expression的const、volatile、或者__unaligned属性。
  1. dynamic_cast
  • 有类型检查,基类向派生类转换比较安全,但是派生类向基类转换则不太安全
  • dynamic_cast (expression)该运算符把expression转换成type-id类型的对象。type-id 必须是类的指针、类的引用或者void*
  • 如果 type-id 是类指针类型,那么expression也必须是一个指针,如果 type-id 是一个引用,那么 expression 也必须是一个引用
  • dynamic_cast运算符可以在执行期决定真正的类型,也就是说expression必须是多态类型。如果下行转换是安全的(也就说,如果基类指针或者引用确实指向一个派生类对象)这个运算符会传回适当转型过的指针。如果 如果下行转换不安全,这个运算符会传回空指针(也就是说,基类指针或者引用没有指向一个派生类对象)
  • dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换
  • 在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的
  • 在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全

1.7 各种变量占字节大小& 空类的大小是多少

空类

  1. C++空类的大小不为0,不同编译器设置不一样,vs设置为1;
  2. C++标准指出,不允许一个对象(当然包括类对象)的大小为0,不同的对象不能具有相同的地址;
  3. 带有虚函数的C++类大小不为1,因为每一个对象会有一个vptr指向虚函数表,具体大小根据指针大小确定;
  4. C++中要求对于类的每个实例都必须有独一无二的地址,那么编译器自动为空类分配一个字节大小,这样便保证了每个实例均有独一无二的内存地址。
    在这里插入图片描述

1.8 const关键字的作用有哪些

  1. 阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;
  2. 对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
  3. 在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
  4. 对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量,类的常对象只能访问类的常成员函数;
  5. 对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。
  6. const成员函数可以访问非const对象的非const数据成员、const数据成员,也可以访问const对象内的所有数据成员;
  7. 非const成员函数可以访问非const对象的非const数据成员、const数据成员,但不可以访问const对象的任意数据成员;
  8. 一个没有明确声明为const的成员函数被看作是将要修改对象中数据成员的函数,而且编译器不允许它为一个const对象所调用。因此const对象只能调用const成员函数。
  9. const类型变量可以通过类型转换符const_cast将const类型转换为非const类型;
  10. const类型变量必须定义的时候进行初始化,因此也导致如果类的成员变量有const类型的变量,那么该变量必须在类的初始化列表中进行初始化;
  11. 对于函数值传递的情况,因为参数传递是通过复制实参创建一个临时变量传递进函数的,函数内只能改变临时变量,但无法改变实参。则这个时候无论加不加const对实参不会产生任何影响。但是在引用或指针传递函数调用中,因为传进去的是一个引用或指针,这样函数内部可以改变引用或指针所指向的变量,这时const 才是实实在在地保护了实参所指向的变量。因为在编译阶段编译器对调用函数的选择是根据实参进行的,所以,只有引用传递和指针传递可以用是否加const来重载。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。

1.9 浅拷贝和深拷贝的区别

  1. 钱拷贝只是一个指针 ,没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址,如果原来的指针所指向的资源释放了,那么再释放浅拷贝的指针资源就会出现错误
  2. 深拷贝不仅拷贝值,还开辟出一块新的空间来存放新的值,即使原先的对象被析构掉,释放内存了也不会影响到深拷贝得到的值

1.10 内存

智能指针是一个类,用来存储指向动态分配对象的指针负责自动释放动态分配的对象,防止堆内存泄露。动态分配的资源,交给一个类对象去管理,当类对象去管理,当类对象声明周期结束,自动调用析构函数释放资源。也是内存的一部分

内存泄漏

  1. 定义
    说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定)内存块,使用完后必须显式释放的内存。应用程序般使用malloc,、realloc、 new等函数从堆中分配到块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了

  2. 避免内存泄露的几种方式

    • 计数法:使用new或者malloc时,让该数+1,delete或free时,该数-1,程序执行完打印这个计数,如果不为0则表示存在内存泄露
    • 一定要将基类的析构函数声明为虚函数
    • 对象数组的释放一定要用delete []
    • 有new就有delete,有malloc就有free,保证它们一定成对出现

程序的内存分区/内存模型-栈、堆、全局数据区、常量区、代码区

从上到下,也就是从高地址到低地址,分别是栈、未使用内存、堆区、全局数据区、常量区、代码区

存放内容特点
代码区存储程序的可执行代码通常只可读,在多进程多线程中可以共享(提高效率)
数据段存储全局变量和静态变量
用于动态分配内存,通常通过malloc、new等堆的大小可以动态改变,但管理不当会导致内存泄漏,这是程序中未使用的内存,在程序运行时可用于动态分配内存
存储函数的局部变量、参数、返回地址每次调用都会返回一个栈帧,函数返回时栈帧会被销毁;在函数内部声明的所有变量都将占用栈内存,栈由操作系统自动分配释放 ,用于存放函数的参数值、局部变量等栈的分配素服非常快,栈溢出是由于栈空间耗损引起的错误,通常由过深的递归调用或分配过大的局部变量数组导致

内存对齐及原因

是指将数据放置在内存中的特定地址,以优化处理器的访问速度。现代处理器通常要求数据按特定边界对齐(如2字节、4字节、8字节等),以提高数据访问的效率。

  1. 性能提升:对齐的数据可以在一次内存访问中完成读取或写入,而未对齐的数据可能需要多次内存访问。
  2. 硬件要求:某些硬件架构要求数据必须对齐,不符合对齐要求的访问可能导致硬件异常。
  3. 分配内存的顺序是按照声明的顺序
  4. 每个变量相对于起始位置的偏移量必须是该变量类型大小的整数倍,不是整数倍空出内存,直到偏移量是整数倍为止
  5. 最后整个结构体的大小必须是变量类型最大值的整数倍
  6. 偏移量要是n和当前变量大小中较小的整数倍
  7. 整体大小要是和n和最大变量大小中较小值的整数倍

new、mallloc

  1. new实现
    操作符用于在堆上分配内存并调用构造函数来初始化对象
  • 首先,它会根据传入的参数计算需要分配的内存大小。
  • 然后,它会尝试分配这段内存。
  • 如果内存分配成功,new会调用相应类型的构造函数初始化对象。
  • 最后,它会返回指向分配内存的指针
  1. new返回空指针的情况
    当内存分配失败时,会返回一个空指针,这通常时由于内训不足或者操作系统无法满足分配请求引起的,可以使用try-catch语句来捕获异常或者检查返回的指针是否为空,以便采取适当的处理措施

  2. new抛出异常的情况
    new在内存分配失败时会抛出std::bad_alloc异常,这是一个派生自std::exception的异常类,用于表示内存分配失败的情况。当new无法分配所需内存时,它会抛出此异常,以便在异常处理机制下进行适当的处理。

  3. new和malloc区别

  • new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持
  • 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸
  • new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
  • new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。
  • new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现),然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)
  • malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。

1.11 ++i和i++

  • ++i是前置自增,先将变量+1,然后返回加1的值,前置返回一个引用
  • i++是后置自增运算,先返回变量i当前的值,然后将i的值+1。后置返回一个对象,临时对象会导致效率变低

1.12 strcpy函数和strncpy函数的区别项目

C 标准库中用于字符串复制的函数

hhValue特点
strcpy函数用于将源字符串复制到目标字符串中,包括终止的空字符(\0):char *strcpy(char *dest, const char *src)复制整个字符串:包括源字符串中的终止空字符。没有边界检查:如果目标缓冲区不足以容纳源字符串及其终止空字符,strcpy 会导致缓冲区溢出,进而可能引发程序崩溃或安全漏洞。返回值:返回指向目标字符串的指针
strncpy将源字符串复制到目标字符串中,但可以指定最多复制的字符数(不包括终止空字符)char *strncpy(char *dest, const char *src, size_t n);指定复制长度:可以指定要复制的最大字符数 n。不保证终止空字符:如果源字符串的长度大于或等于 n,目标字符串不会被以空字符结束。为了确保目标字符串正确结束,通常需要手动添加空字符。

1.13 标准的异常

在这里插入图片描述

1.14 模板

  • c++的模板类时实现泛型编程,即编写一个通用的类或函数,可以适用于多种不同的数据类型,适用模板类可以避免重读写代码
  • 由两部分组成:模板声明和模板定义,模板声明定义了模板参数,模板定义则实现了具体的功能,在适用模板时,需要为每一个要使用的类型提供一个对应的模板参数
  • 模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码
  • 模板是创建泛型类或函数的蓝图或公式。库容器,比如迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。每个容器都有一个单一的定义,比如向量,我们可以定义许多不同类型的向量,比如 vector <int> 或 vector <string>
    • 函数模板
template <typename type> ret-type func-name(parameter list)
{
   // 函数的主体
}

template <typename T>
inline T const& Max (T const& a, T const& b) 
{
   // 函数的主体
}

1.15 引用传递、值传递、地址传递

  • 值传递是将实际参数的值传递给函数的形式参数。在函数内,形式参数是实际参数的副本,对形式参数的修改不会影响实际参数。安全性高,不会影响实际参数,但会产生数据复制,消耗内存
  • 引用传递是将实际参数的**引用(即内存地址)**传递给函数的形式参数。形式参数在函数内作为实际参数的别名,对形式参数的修改会影响实际参数。实际参数和形式参数引用同一块内存,修改形式参数会直接影响实际参数,无需数据复制,效率高,但容易引起副作用(实际参数被修改)
  • 地址传递是将实际参数的内存地址传递给函数的形式参数形式参数通过解引用操作可以访问和修改实际参数,实际参数和形式参数通过指针关联,修改指针指向的值会影响实际参数,效率高、灵活性强,但使用指针容易引发内存访问错误(如空指针、悬空指针等)
  • C++: 同时支持值传递、引用传递和地址传递,Python: 变量都是对象的引用,传递的是引用的副本,但引用本身是通过值传递的

1.16 三目表达式

condition ? true_value : false_value;//c++
<true_value> if else <false_value> //python

二、面向对象编程

2.1 类和对象

类的定义和对象的创建

  1. 类(class)是用户定义的数据类型,它包含数据成员(成员变量)和成员函数(方法)来操作这些数据。对象(object)是类的实例,通过对象可以访问类的成员
  2. 访问控制修饰符:如 public、private 和 protected,用于控制成员的访问权限
  3. 在栈上创建对象:直接定义对象,对象的生命周期由它的作用域决定,作用域结束时对象会自动销毁并释放内存:创建一个class MyClass,在main函数中,创建对象:MyClass obj
  4. 在堆上创建对象:需要手动管理对象的生命周期,使用 new 关键字进行内存分配,并使用 delete 关键字释放内存:MyClass* pObj = new MyClass(); // 在堆上创建对象, delete pObj; // 手动释放内存
  5. 使用智能指针创建对象,能够自动管理堆上的对象,避免内存泄漏。常用的智能指针有 std::unique_ptr 和 std::shared_ptr

构造函数和析构函数

  1. 构造函数
  • 构造函数是一个特殊的成员函数,用于在对象创建时初始化对象。构造函数的名称与类名相同,没有返回类型
  • 特点
    • 自动调用:在对象创建时自动调用
    • 重载:可以重载,即一个类可以有多个构造函数
    • 初始化列表:可以使用初始化列表来初始化数据成员
  1. 析构函数
  • 析构函数是一个特殊的成员函数,用于在对象销毁时执行清理操作。析构函数的名称与类名相同,但在前面加上波浪号(~),没有返回类型,也没有参数。
  • 特点
    • 自动调用:在对象生命周期结束时自动调用(如对象超出作用域或被删除
    • 无参数和无返回类型:不能重载,每个类只能有一个析构函数。
    • 释放资源:常用于释放动态分配的内存或其他资源。

2.3 面向对象特性-封装、继承和多态

1. 多态

  1. 定义
  • 当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数
  1. 分类编译时多态&运行时多态
编译时多态:函数重载、运算符重载
  • 函数重载是指在同一个作用域内有多个同名函数,但它们的参数列表不同(参数个数或参数类型不同)
  • 运算符重载是指为用户定义的类型提供特定的运算符实现,使其行为类似于内置类型
运行时多态:继承、虚函数
  • 运行时多态通过基类指针或引用指向派生类对象,并通过虚函数表(vtable)在运行时决定调用哪个类的函数实现
  • 虚函数是基类中用 virtual 关键字修饰的成员函数,允许在派生类中重写,
  • 补充:纯虚函数是没有实现的虚函数,用 = 0 语法表示。包含纯虚函数的类称为抽象类,不能实例化,只能作为基类。想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数
  1. 运行时多态实现原理
  • 简介回答:运行时多态的实现依赖于虚函数表(vtable)和虚函数指针(vptr)。每个类都有一个虚函数表,存储着指向该类的虚函数实现的指针。每个对象包含一个虚函数指针,指向它所属类的虚函数表。在调用虚函数时,通过虚函数指针查找虚函数表,以确定实际调用的函数。
  • 详细回答
    • 1、编译器在发现基类中有虚函数时,会自动为每个含有虚函数的类生成一份虚表,该表是一个一维数组,虚表里保存了虚函数的入口地址
    • 2、编译器会在每个对象的前四个字节中保存一个虚表指针,即vptr,指向对象所属类的虚表。在构造时,根据对象的类型去初始化虚指针vptr,从而让vptr指向正确的虚表,从而在调用虚函数时,能找到正确的函数
    • 3、所谓的合适时机,在派生类定义对象时,程序运行会自动调用构造函数,在构造函数中创建虚表并对虚表指针进行初始化。在构造子类对象时,会先调用父类的构造函数,此时,编译器只“看到了”父类,并为父类对象初始化虚表指针,令它指向父类的虚表;当调用子类的构造函数时,为子类对象初始化虚表指针,令它指向子类的虚表
    • 4、当派生类对基类的虚函数没有重写时,派生类的虚表指针指向的是基类的虚表;当派生类对基类的虚函数重写时,派生类的虚表指针指向的是自身的虚表;当派生类中有自己的虚函数时,在自己的虚表中将此虚函数地址添加在后面

C++中的重载、重写(覆盖)和隐藏的区别

  1. 重载
    重载是指在同一范围定义中的同名函数才存在重载关系,主要特点是函数名相同,参数类型和数目不同,不能出现参数个数和类型均相同,仅仅依靠返回值不同来区分函数,重载和函数成员是否是虚函数无关
  2. 重写
    是在派生类中覆盖基类中的同名函数,重写就是重写函数体,要求基类函数必须是虚函数且:与基类的虚函数有相同的参数个数、参数类型、返回值类型
  3. 重载与重写的区别
  • 重写是父类和子类之间的垂直关系,重载是不同函数之间的水平关系
  • 重写要求参数列表相同,重载则要求参数列表不同,返回值不要求
  • 重写关系中,调用方法根据对象类型决定,重载根据调用时实参表与形参表的对应关系来选择函数体
  1. 隐藏
    指某些情况下,派生类中的函数屏蔽了基类中的同名函数
  • 两个函数参数相同,但是基类函数不是虚函数。**和重写的区别在于基类函数是否是虚函数。
  • 两个函数参数不同,无论基类函数是不是虚函数,都会被隐藏。和重载的区别在于两个函数不在同一个类中。

补充-虚函数

特征

  1. 虚函树表时全局共享的元素,即全局只有一个,在编译的时候完成
  2. 虚函数类似于一个数组,类对象中存储vptr指针,指向虚函数表,即虚函数表不是函数,不是程序代码,不可能存储在代码段
  3. 虚函数表存储虚函数的地址,即虚函数表的元素是指向类成员的指针,而类中虚函数的个数在编译时期就可以确定,即虚函数表的大小可以确定,大小是在编译时期确定的,不必动态分配内存空间存储在虚函数表中,所以不在堆中

所以

  • 虚函数表类似于类中静态成员变量,静态成员变量也是全局共享,大小确定,因此最有可能存在全局数据区
  • 由于虚表指针vptr跟虚函数密不可分,对于有虚函数或者继承于拥有虚函数的基类,对该类进行实例化时,在构造函数执行时会对虚表指针进行初始化,并且存在对象内存布局的最前面。
  • C++中虚函数表位于只读数据段(.rodata),也就是C++内存模型中的常量区;而虚函数则位于代码段(.text),也就是C++内存模型中的代码区

纯虚函数与虚函数的区别

  1. 虚函数是为了实现动态编联产生的,目的是通过基类类型的指针指向不同对象时,自动调用相应的、和基类同名的函数(使用同一种调用形式,既能调用派生类又能调用基类的同名函数)。虚函数需要在基类中加上virtual修饰符修饰,因为virtual会被隐式继承,所以子类中相同函数都是虚函数。当一个成员函数被声明为虚函数之后,其派生类中同名函数自动成为虚函数,在派生类中重新定义此函数时要求函数名、返回值类型、参数个数和类型全部与基类函数相同。
  2. 纯虚函数只是相当于一个接口名,但含有纯虚函数的类不能够实例化

2. 继承

  1. 定义
  • 让某种类型对象获得另一个类型对象的属性和方法
  1. 实现
  • 实现继承:指使用基类的属性和方法而无需额外编码的能力
  • 接口继承:指仅使用属性和方法的名称、但是子类必须提供实现的能力

3. 封装

  1. 数据和代码捆绑在一起,避免外界干扰和不确定性访问。
  2. 封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏,例如:将公共的数据或方法使用public修饰,而不希望被访问的数据或方法采用private修饰。

三、 标准模板库(STL)

STL组成-容器、算法、迭代器、函数对象、适配器

在这里插入图片描述

3.1 Lambda 表达式

  1. 利用lambda可以编写内嵌的匿名函数,用以替换独立函数或者函数对象
  2. 每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类当然重载了()运算符),我们称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,其实一个右值。
  3. 所以,我们上面的lambda表达式的结果就是一个个闭包。闭包的一个强大之处是其可以通过传值或者引用的方式捕捉其封装作用域内的变量,前面的方括号就是用来定义捕捉模式以及变量,我们又将其称为lambda捕捉块。
  4. lambda表达式的语法定义如下
[capture] (parameters) mutable ->return-type {statement};[捕获列表](参数)mutable -&gt; 返回值 {函数体}

3.2 容器

1. 序列容器-vector、deque、list对比

存储元素的序列,允许双向遍历

  1. std::vector:动态数组,支持快速随机访问。
  2. std::deque:双端队列,支持快速插入和删除。
  3. std::list:链表,支持快速插入和删除,但不支持随机访问

补:STL中vector是如何实现的?

  1. vector是一种序列式容器,其数据安排以及操作方式与array非常类似
  2. 两者的唯一差别就是对于空间运用的灵活性,array占用的是静态空间,一旦配置了就不可以改变大小,如果遇到空间不足的情况还要自行创建更大的空间,并手动将数据拷贝到新的空间中,再把原来的空间释放。
  3. vector则使用灵活的动态空间配置,维护一块连续的线性空间,在空间不足时,可以自动扩展空间容纳新元素,做到按需供给。其在扩充空间的过程中仍然需要经历:重新配置空间,移动数据,释放原空间等操作。这里需要说明一下动态扩容的规则:以原大小的两倍配置另外一块较大的空间(或者旧长度+新增元素的个数),
  4. 源码:
    const size_type len = old_size + max(old_size, n);
  5. Vector扩容倍数与平台有关,在Win + VS 下是 1.5倍,在 Linux + GCC 下是 2 倍
  6. 频繁对vector调用push_back()对性能是有影响的,这是因为每插入一个元素,如果空间够用的话还能直接插入,若空间不够用,则需要重新配置空间,移动数据,释放原空间等操作,对程序性能会造成一定的影响

补:Vector的resize和reserve有什么区别?

  • resize(n)会改变vector的大小,使其包含n个元素。如果n大于当前的大小,那么新的元素会被添加到vector的末尾,如果n小于当前的大小,那么末尾的元素会被删除。resize会改变vector的size()
  • reserve(n)不会改变vector的大小,它只是预先分配足够的内存,以便在未来可以容纳n个元素。reserve不会改变vector的size(),但可能会改变capacity()。reserve的主要目的是为了优化性能,避免在添加元素时频繁进行内存分配。简单来说,resize改变的是vector中元素的数量,而reserve改变的是vector的内存容量。

2. 关联容器-set、multiset、map、multimap对比

存储键值对,每个元素都有一个键(key)和一个值(value),并且通过键来组织元素

  1. std::set:集合,不允许重复元素。
  2. std::multiset:多重集合,允许多个元素具有相同的键。
  3. std::map:映射,每个键映射到一个值。
  4. std::multimap:多重映射,允许多个键映射到相同的值。

3. 无序容器(C++11 引入)unordered_set、unordered_multiset、unordered_map、unordered_multimap

哈希表,支持快速的查找、插入和删除

  1. std::unordered_set:无序集合。
  2. std::unordered_multiset:无序多重集合。
  3. std::unordered_map:无序映射。
  4. std::unordered_multimap:无序多重映射。

map、set、unordered_map、unordered_set对比?

类别描述特点用途
map是一个有序的关联容器,基于红黑树实现(或其他平衡树),用于存储键值对(key-value pairs)时间复杂度0logn;内存需要平衡树结构,开销比较大需要按键排序的场合:如实现有序字典;快速查找;有序遍历:可以按键的顺序进行遍历
unordered_map是一个无序的关联容器,基于哈希表实现,用于存储键值对(key-value pairs)无序:元素没有特定的顺序,基于哈希表存储;时间复杂度:平均情况下,查找、插入和删除操作的时间复杂度为 o(1),最坏为0(n);内存开销:由于哈希表的存在,内存开销通常比 std::map 小
set是一个有序的集合容器,基于红黑树实现(或其他平衡树),用于存储唯一的键元素:存储的每个元素都是一个键,没有值;有序:元素按键的升序排列;时间复杂度logn需要有序集合的场合;快速查找;有序遍历
unordered_set是一个无序的集合容器,基于哈希表实现,用于存储唯一的键

3.3 迭代器

迭代器的基本概念

迭代器(Iterator)是一种对象,它提供了对容器(如数组、向量、链表等)元素的顺序访问而不暴露其内部表示。迭代器的概念类似于指针,可以用来遍历容器中的元素。标准模板库(STL)提供了多种类型的迭代器,以适应不同的需求。

  • 迭代器:类似于指针的对象,用于遍历容器中的元素。
  • 容器:存储一组数据的对象,如数组、向量、链表等。
  • 范围:一对迭代器,表示一个开始和一个结束的位置。

各种类型的迭代器(输入迭代器、输出迭代器、前向迭代器、双向迭代器、随机访问迭代器)对比

类型特点用途操作举例
输入迭代器只读,单向遍历从输入流中读取数据或从容器中读取数据*(解引用)、++(前缀和后缀递增)、== 和 !=(比较)标准输入流迭代器(std::istream_iterator)
只写,单向遍历将数据写入输出流或容器*(解引用,用于赋值)、++(前缀和后缀递增)标准输出流迭代器(std::ostream_iterator)
前向迭代器(Forward Iterator)只读或只写,单向遍历,可以多次遍历同一范围适用于单向链表等数据结构*(解引用)、++(前缀和后缀递增)、== 和 !=(比较)标准容器的普通迭代器(如 std::forward_list 的迭代器)
双向迭代器(Bidirectional Iterator)只读或只写,双向遍历适用于双向链表等数据结构前向迭代器的所有操作加上 --(前缀和后缀递减)标准容器的普通迭代器(如 std::list 的迭代器)
随机访问迭代器(Random Access Iterator)只读或只写,支持随机访问适用于动态数组等数据结构双向迭代器的所有操作加上 +、-(算术运算符)、[](下标运算符)、<、<=、>、>=(比较运算符标准容器的普通迭代器(如 std::vector、std::deque 的迭代器)

3.4 算法

sort()函数重点色排序是快排还是插入排序

sort()源码中采用的是一种叫做IntroSort内省式排序的混合式排序算法

  1. 首先进行判断排序的元素个数是否大于stl_threshold,stl_threshold是一个常量值是16,意思就是说我传入的元素规模小于我们的16的时候直接采用插入排序。
  • (为什么用插入排序?因为插入排序在面对“几近排序”的序列时,表现更好,而快排是通过递归实现的,会为了极小的子序列产生很多的递归调用在区间长度小的时候经常不如插入排序效率高)
  1. 如果说我们的元素规模大于16,需要去判断如果是不是能采用快速排序
  • 快排是使用递归来实现的,如果说我们进行判断我们的递归深度有没有到达递归深度的限制阈值2*lg(n),如果递归深度没达到阈值就使用快速排序来进行排序
  1. 大于我们的最深递归深度阈值的话,这个时候说明快排复杂度退化了,这种情况下,每次划分只能将区间缩小1个元素,造成递归深度过深),就会采用我们的堆排序,堆排序是可以保证稳定O(nlogn)的时间复杂度的

常用算法(排序、搜索、变换、聚合)

  1. 排序算法(Sorting)
2. 用于对范围内的元素进行升序排序
std::sort(vec.begin(), vec.end());
3.  对部分元素进行排序,使得前 N 个元素按顺序排列
  std::partial_sort(vec.begin(), vec.begin() + 3, vec.end());
4. std::stable_sort:稳定排序,保持相等元素的相对顺序
std::stable_sort(people.begin(), people.end(), [](const Person &a, const Person &b) {
       return a.age < b.age;
  1. 搜索算法(Searching)
5. std::find在范围内查找等于某个值的元素
auto it = std::find(vec.begin(), vec.end(), 4);
6. std::binary_search在有序范围内使用二分查找确定某个值是否存在。
 bool found = std::binary_search(vec.begin(), vec.end(), 4);
  1. 变换算法(Transformation)
8. std::transform对范围内的元素应用函数,并将结果存储在另一个范围内
 std::transform(vec.begin(), vec.end(), result.begin(), [](int x) {
        return x * x;
    });
9. std::replace将范围内等于某个值的元素替换为新值
 std::replace(vec.begin(), vec.end(), 2, 5); 
  1. 聚合算法(Aggregation)\
11. std::accumulate计算范围内所有元素的累积和
12. std::count 计算范围内等于某个值的元素数量。 

四、c++11

c++11有哪些新的特性?

  1. nullptr代替了null
  2. 引入了aoto和decltype这两个关键字
  3. 基于范围的for循环
  4. 类和结构体中初始化列表
  5. lambda表达式
  6. std::forward_list(单向链表)
  7. 右值引用和MOve语义
  8. 无序容器和正则表达式
  9. 成员变量默认初始化
  10. 智能指针

4.1 智能指针

原理

智能指针是一个类,用来存储指向动态分配对象的指针负责自动释放动态分配的对象,防止堆内存泄露。动态分配的资源,交给一个类对象去管理,当类对象去管理,当类对象声明周期结束,自动调用析构函数释放资源。

常见的智能指针

名称定义特点
unique_ptr独占所有权的智能指针,表示唯一的所有者,负责管理动态分配的对象的生命周期独占所有权:一个 std::unique_ptr 对象拥有动态分配对象的唯一所有权,不能被复制。移动语义:可以通过移动构造函数或移动赋值操作符转移所有权。自动释放资源:当 std::unique_ptr 被销毁时,自动释放其管理的资源。轻量级:开销小,不需要额外的内存来存储引用计数。
shared_ptr是一种共享所有权的智能指针,多个 std::shared_ptr 对象可以共享同一个动态分配的对象共享所有权:多个shared_ptr 对象可以共同拥有一个对象。对象在最后一个shared_ptr 被销毁时才会被释放。引用计数:内部维护一个引用计数,当引用计数为零时,对象被释放。较高开销:需要额外的内存来存储引用计数,并进行原子操作以确保线程安全。
weak_ptr一种不控制对象生命周期的智能指针,用于打破 shared_ptr 的循环引用问题。它与shared_ptr 共同使用,用于观察对象而不改变其引用计数观察者:std::weak_ptr 不增加引用计数,只是观察对象的存在。**用法:**通常与 std::shared_ptr 结合使用,通过 std::weak_ptr::lock() 可以获得一个 std::shared_ptr 来访问对象。避免循环引用:用于解决 std::shared_ptr 之间可能形成的循环引用问题。

手写实现智能指针类需要实现哪些函数

  1. 智能指针是一个数据类型,一般用模板实现,模拟指针行为的同时还提供自动垃圾回收机制。它会自动记录SmartPointer<T*>对象的引用计数,一旦T类型对象的引用计数为0,就释放该对象
  2. 除了指针对象外,我们还需要一个引用计数的指针设定对象的值,并将引用计数计为1,需要一个构造函数。新增对象还需要一个构造函数,析构函数负责引用计数减少和释放内存。
  3. 通过覆写赋值运算符,才能将一个旧的智能指针赋值给另一个指针,同时旧的引用计数减1,新的引用计数加1
  4. 一个构造函数、拷贝构造函数、复制构造函数、析构函数、移动函数

缺点

  1. 内存消耗:智能指针通常会占用额外的内存来存储引用计数或其他相关信息。这可能会增加程序的内存消耗。
  2. 运行时开销:由于智能指针需要进行引用计数或其他管理操作,因此在创建、复制、销毁等操作时可能会引入一定的运行时开销。
  3. 循环引用问题:如果存在循环引用(两个或多个对象相互引用),那么智能指针可能导致内存泄漏,因为循环引用的对象无法被正确释放。
  4. 不适合所有情况:智能指针并不适用于所有情况。例如,在某些性能关键的场景中,手动管理内存可能更加高效
  5. 引用计数不准确:智能指针使用引用计数来跟踪对象的引用情况,但在某些情况下,引用计数可能不准确。例如,如果使用裸指针进行对象的拷贝或赋值操作,那么引用计数可能无法正确更新。
  6. 不支持循环数据结构:智能指针通常无法正确处理循环数据结构,如循环链表等。这可能导致内存泄漏或其他问题。

4.2 move?

move语义的核心概念是移动而不是复制对象的数据,以减少不必要的拷贝操作,尤其是对于大对象或资源管理类(如智能指针)比较适用,在c++11中引入

移动构造函数

  1. 定义
  • 移动构造函数允许从另一个对象“移动”数据,而不是复制数据。移动构造函数的参数是一个右值引用
  1. 特点
  • 右值引用:移动构造函数接受一个右值引用参数。
  • 资源转移:将资源(如动态内存、文件句柄等)从源对象转移到目标对象。
  • 源对象:移动构造后,源对象应处于有效但未定义的状态(通常被设置为nullptr或0等状态)

左值和右值&左值引用右值引用

  • 左值(Lvalue):表示内存位置的表达式,可取地址和赋值,具有持久性。
  • 右值(Rvalue):表示临时值的表达式,不可取地址,只能读,生命周期短。
  • 左值引用:绑定到左值,类型名后加上 &。
  • 右值引用:绑定到右值,类型名后加上 &&,主要用于移动语义和优化资源转移。

move与拷贝对比

移动语义是一种高效的对象复制方式,通过转移资源所有权而不是复制资源来实现。移动构造函数和移动赋值运算符使用右值引用(rvalue reference)来接收临时对象,并将其资源“移动”到新的对象中,避免不必要的资源分配和拷贝。

4.3 auto 关键字

  1. 定义
    C++11中的auto关键字是用来自动推导表达式或变量的实际类型的。使用auto关键字做类型自动推导时,依次施加一下规则:如果初始化表达式是引用,则去除引用语义。
  2. 工作原理
  • 编译器看到auto,会查看初始化表达式的类型,并将该类型作为auto变量的类型。
  • 如果初始化表达式的类型可以确定,则使用该类型。如果初始化表达式包含了多个类型,则使用与初始化表达式兼容的共同类型。
  • 如果无法确定类型,则报错。

4.4 nullptr(与null对比)

  1. Null出现
    C++中为了避免“野指针”(即指针在首次使用之前没有进行初始化)的出现,声明一个指针后最好马上对其进行初始化操作。如果暂时不明确该指针指向哪个变量,则需要赋予NULL值

    源码中
    NULL在C++中的定义,NULL在C++中被明确定义为整数0
    #define NULL ((void *)0)
    也就是说NULL实质上是一个void *指针

  2. 问题
    C++让NULL也支持void *的隐式类型转换,C++把NULL定义为0,在函数重载的时候,因为 0 既可以被解释为整数也可以被解释为空字符指针,就不知道匹配那个函数了,

    void Func(char *);
    void Func(int);
    int main()
    {
    Func(NULL); // 调用Func(int)
    }
    Func(NULL)应该调用的是Func(char *)但实际上NULL的值是0,所以调用了Func(int)。nullptr关键字真是为了解决这个问题而引入的

  3. nullptr 定义
    实际上是一个常量,它的类型是 nullptr_t。当我们使用 nullptr 时,编译器会为我们生成相应的代码,确保它只能被用作空指针,而不能被用作其他类型

范围循环

  1. 定义
    是一种简化数组或容器遍历的语法,从 C++11 开始引入。它使得代码更加简洁和可读,特别适用于遍历 std::vector、std::array、std::list 等标准库容器以及原生数组
for (element_type variable_name : container) {
    // 使用 variable_name 进行操作
}
其中 element_type 是容器中元素的类型,variable_name 是循环中的变量名,container 是要遍历的数组或容器。

统一初始化语法

constexpr

std::thread 和多线程编程

  1. 使用 std::thread 可以创建并启动一个新线程。线程可以执行一个函数、一个可调用对象或一个 lambda 表达式
#include <iostream>
#include <thread>

// 要在线程中执行的函数
void printHello() {
    std::cout << "Hello from thread!" << std::endl;
}

int main() {
    std::thread t(printHello);  // 创建并启动新线程
    t.join();  // 等待线程完成
    return 0;
}
  1. 可以使用 lambda 表达式来定义线程执行的代码
#include <iostream>
#include <thread>

int main() {
    std::thread t([] {
        std::cout << "Hello from lambda thread!" << std::endl;
    });
    t.join();
    return 0;
}
  1. 线程管理
    join 和 detach
    join():等待线程完成。如果线程已经完成,join() 立即返回。如果线程尚未完成,join() 会阻塞调用线程,直到被调用线程完成。
    detach():将线程与当前执行环境分离。分离的线程将继续运行,但无法再被主线程控制。程序终止时,所有仍在运行的分离线程将强制终止

  2. 线程同步
    在多线程编程中,线程间的数据共享需要同步,以避免数据竞争和不确定的行为。C++ 提供了多种同步机制,如互斥(std::mutex)、条件变量(std::condition_variable)等。

  • 互斥(Mutex):互斥用于保护共享数据,确保在任何时刻只有一个线程可以访问共享数据
  • 条件变量(Condition Variable):条件变量用于线程间的通知机制。一个线程可以等待某个条件的发生,另一个线程可以通知该条件的发生。

五、并发编程

创建和管理线程\线程同步(互斥量、条件变量)

  • std::thread 用于创建和管理线程。可以使用函数指针、函数对象或 lambda 表达式来启动线程
  • std::mutex 用于保护共享数据,确保同一时间只有一个线程能够访问数据
  • std::condition_variable 用于线程间的通知机制。一个线程可以等待某个条件,另一个线程可以通知该条件
  • std::shared_mutex 允许多个线程同时读取,但只有一个线程可以写入

std::atomic

  • std::atomic 提供了一种无锁的方式来操作基本数据类型,保证操作的原子性

内存模型

  1. 原子操作(Atomic Operations)
    原子操作是不可分割的操作,确保在多线程环境下操作的原子性。C++11 提供了 std::atomic 模板类,用于定义原子类型变量
  2. 顺序一致性(Sequential Consistency)
    顺序一致性是最强的内存序模型,保证所有的内存操作按照程序中的顺序执行。C++ 中默认的原子操作是顺序一致的
  3. 内存顺序(Memory Orders)
    C++ 提供了多种内存顺序选项,允许开发者在性能和内存操作顺序保证之间进行权衡:
  • std::memory_order_relaxed:不保证任何顺序,仅保证原子操作的原子性
  • std::memory_order_acquire:获取操作,不允许在此操作之前的任何读写操作被重排序到此操作之后
  • std::memory_order_release:释放操作,不允许在此操作之后的任何读写操作被重排序到此操作之前
  • std::memory_order_acq_rel:获取和释放操作,结合了 memory_order_acquire 和 memory_order_release 的保证
  • std::memory_order_seq_cst:顺序一致性,最强的内存顺序,所有线程看到的操作顺序一致
  1. 原子操作和内存顺序
    C++ 提供了一系列原子操作,允许开发者指定内存顺序
  • store(value, order)-存储操作
  • load(order)-加载操作
  • exchange(value, order)-交换操作
  • compare_exchange_weak(expected, desired, order)
  • compare_exchange_strong(expected, desired, order)-比较并交换操作

并行算法(C++17)

并行执行策略

六、设计模式和高级编程

常用设计模式

单例模式

工厂模式

观察者模式

代理模式

高级编程概念

模板编程

元编程

表达式模板

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lweiwei@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值