暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

科大讯飞C++一面

阿Q正砖 2023-10-02
339

大家好,我是阿Q。

中秋国庆假期已过半,你们玩的还好吗?

最近看完很多面经后发现,面经是真的不好写,还是很感谢同学们面试完认真的复盘和分享。为什么不好写?

多看C++后端开发的面经,你就会发现,其实在校招过程中,面试的问题七八成都是那些,好像他们都有一样的模板一样。。。因为是一个方向的,除了项目之外,八股就都大差不差的。

所有,每篇面经的输出,都要我不断的筛选,努力把不一样的分享给伙伴们。

今天是一位同学在科大讯飞面试过程中的问题,首先感谢他的分享,再就是预祝他能顺利拿到讯飞的offer。

正文如下。。

来源: 

https://www.nowcoder.com/feed/main/detail/07d1d6d07d5c4625866fd70e719d6475

1、指针存的是虚拟地址还是物理地址?
指针存储的是虚拟地址,而不是物理地址。
下面我来简单说说这两个的区别:
虚拟地址(Virtual Address):
  • 虚拟地址是由操作系统分配给进程的地址空间中的地址。
  • 每个运行的进程都有自己独立的虚拟地址空间,这使得每个进程认为它在独占整个计算机的内存。
  • 虚拟地址是相对于进程的起始地址的偏移量,通常从0开始,直到进程的最大地址。
  • 虚拟地址是一种抽象,它隐藏了底层物理硬件的复杂性。
物理地址(Physical Address):
  • 物理地址是硬件内存中的真实地址,它对应于计算机的RAM(随机存取存储器)中的特定位置。
  • 物理地址是硬件层面的,不同于虚拟地址,它不受操作系统的管理和控制。
  • 操作系统的内存管理单元(Memory Management Unit,MMU)负责将虚拟地址映射到物理地址,这个过程称为内存地址转换。
关键点是,大多数现代计算机使用虚拟内存系统,其中操作系统负责将进程的虚拟地址映射到物理内存上。这种虚拟内存系统允许多个进程同时运行,每个进程都认为自己独占计算机的全部内存,但实际上它们共享物理内存,由操作系统进行管理和分配。
指针存储的是虚拟地址,因为程序员通常使用虚拟地址来引用内存中的数据和对象,而不需要考虑底层物理地址的复杂性。操作系统和硬件负责将虚拟地址映射到物理地址,这个过程对应用程序是透明的。 
2、虚拟地址和物理地址转换,哪个阶段发生?
这个转换过程通常在操作系统的内存管理单元中进行,它在程序的执行过程中发生在多个阶段。
  1. 编译阶段:虚拟地址和物理地址的转换的第一个阶段发生在程序的编译阶段。在编译时,源代码被编译成可执行文件,而这个文件包含了程序的机器指令以及其他数据。在这个阶段,编译器生成了虚拟地址,这些地址是相对于程序的起始地址(通常是0)的偏移量。编译器不需要知道程序将在内存中的哪个位置执行,因此生成的地址是虚拟的,它们只是相对于程序的内部结构而言的。
  2. 加载阶段:当用户运行程序时,操作系统的加载器负责将可执行文件加载到内存中的某个位置。在这个阶段,操作系统分配物理内存,并将程序的代码和数据加载到这些物理地址上。此时,操作系统将虚拟地址映射到物理地址,并建立一个映射表,通常称为页表或段表。这个表将虚拟地址和物理地址之间的映射关系记录下来,以便后续的地址转换。
  3. 运行时阶段:一旦程序开始执行,它会生成虚拟地址,例如在访问变量、执行指令或调用函数时。这些虚拟地址需要在运行时被转换为物理地址,以便在物理内存中找到对应的数据或指令。这个虚拟地址到物理地址的转换是通过页表或段表来完成的。操作系统和硬件会根据虚拟地址的一部分(通常是高位或低位)来查找页表中的映射关系,以找到相应的物理地址。
  4. 访问检查阶段:在虚拟地址到物理地址的转换过程中,还可以进行访问控制检查,以确保程序只能访问其被授权的内存区域。如果程序尝试访问未授权的内存区域,操作系统将产生异常并终止该操作。
3、上面提到页表,讲讲?
页?
为了有效地管理虚拟内存,物理内存和虚拟地址空间都被划分成固定大小的块,这些块被称为页(Page)。通常,页的大小是2的幂次方,如4KB或4MB。
页表?
存储了虚拟地址与物理地址之间的映射关系。每个进程都有其自己的页表,操作系统会在进程切换时更新页表以确保正确的地址映射。当进程访问虚拟地址时,操作系统通过页表将虚拟地址翻译为物理地址。
多级页表?
对于大型的虚拟地址空间,单级页表可能会变得过于庞大,浪费内存。因此,多级页表引入了分层的页表结构,以减小页表的规模。例如,一个两级页表包括一个顶级页表和多个底层页表。顶级页表用于索引底层页表,而底层页表负责虚拟地址到物理地址的映射。
虚拟地址到物理地址? 
当进程访问虚拟地址时,CPU会通过硬件支持的机制(如内存管理单元,Memory Management Unit,MMU)来自动进行地址转换。CPU使用虚拟地址的高位或其他标志来索引页表,以查找对应的物理地址。一旦找到了对应的物理地址,CPU就可以从物理内存中读取或写入数据。
页表的更新? 
页表需要在进程加载、卸载或分配新的内存页时进行更新。这包括将虚拟地址映射到物理地址、解除映射、调整映射关系等操作。这些更新由操作系统内核负责管理。
总的来说,页表是操作系统中用于实现虚拟内存系统的关键组成部分。它允许多个进程在有限的物理内存中同时运行,通过将虚拟地址映射到物理地址来提供隔离和内存保护。页表的设计和管理对于操作系统的性能和稳定性至关重要。多级页表是一种有效管理大型虚拟地址空间的方法,它可以显著减小页表的大小。
关于页表的一些理解,我之前还有分享过一篇:
操作系统学习路线及相关面试题
4、字符串常量存储在哪个段?
字符串常量通常存储在程序的数据段(Data Segment)中,也被称为常量数据段(Const Data Segment)或只读数据段(Read-Only Data Segment)。
下面简单说一下程序内存的五大分区:
  1. 代码段(Text Segment):代码段是程序的机器指令所在的内存区域。它包含了程序的可执行指令,也就是程序的代码部分。这些指令是程序在运行时被CPU执行的操作。代码段通常是只读的,因为程序的指令在运行时不应该被修改。通常,代码段位于程序的起始位置,并在内存中保留一个固定的地址范围。
  2. 数据段(Data Segment):数据段用于存储程序的静态数据,包括全局变量、静态变量和字符串常量等。这些数据在程序运行期间保持不变,因此数据段通常是可读写的,但也可能包含只读数据,如字符串常量。数据段的大小在程序编译时就确定,并在运行时分配。
  3. (Heap):堆是用于动态内存分配的内存区域。在堆上分配的内存由程序员显式分配和释放,通常用于存储动态生成的数据结构,如链表、树等。堆内存的生命周期由程序员控制,因此需要手动释放以防止内存泄漏。
  4. (Stack):栈用于存储函数调用的上下文信息以及局部变量。每当函数被调用时,其局部变量和函数调用信息都会被推入栈中,当函数返回时,这些信息会被弹出栈。栈是一种先进先出(LIFO)的数据结构,它具有快速的内存分配和释放速度,但生命周期受限于函数调用的范围。
  5. BSS 段:BSS 段(Block Started by Symbol)是用于存储未初始化的全局和静态变量的内存区域。这些变量在程序启动时会被初始化为零或默认值。BSS 段通常不存储实际的数据,而只是为这些变量分配了足够的内存空间。
这个图在我之前的总结中还是有的,我自己都不知道画了多少遍了,说明它在C++面试中还是相当重要的。
5、场景题 
6、static?
  1. 修饰局部变量
static修饰局部变量时,使得被修饰的变量成为静态变量,存储在静态区。存储在静态区的数据生命周期与程序相同,在main函数之前初始化,在程序退出时销毁。(无论是局部静态还是全局静态)
  1. 修饰全局变量
全局变量本来就存储在静态区,因此static并不能改变其存储位置。但是,static限制了其链接属性。被static修饰的全局变量只能被该包含该定义的文件访问(即改变了作用域)。
  1. 修饰函数
static修饰函数使得函数只能在包含该函数定义的文件中被调用。对于静态函数,声明和定义需要放在同一个文件夹中。
  1. 修饰成员变量
用static修饰类的数据成员使其成为类的全局变量,会被类的所有对象共享,包括派生类的对象,所有的对象都只维持同一个实例。因此,static成员必须在类外进行初始化(初始化格式:int base::var=5;),而不能在构造函数内进行初始化,不过也可以用const修饰static数据成员在类内初始化。
  1. 修饰成员函数
用static修饰成员函数,使这个类只存在这一份函数,所有对象共享该函数,不含this指针,因而只能访问类的static成员变量。静态成员是可以独立访问的,也就是说,无须创建任何对象实例就可以访问。例如可以封装某些算法,比如数学函数,如ln,sin,tan等等,这些函数本就没必要属于任何一个对象,所以从类上调用感觉更好。
  1. 最重要的特性:隐藏
当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性,其它的源文件也能访问。利用这一特性可以在不同的文件中定义同名函数和同名变量,而不必担心命名冲突。static可以用作函数和变量的前缀,对于函数来讲,static的作用仅限于隐藏。
注:不可以同时用const和static修饰成员函数。
7、拷贝构造函数形参去掉&,会出现什么问题?
当拷贝构造函数的形参去掉&
(即不使用引用参数)时,会触发额外的拷贝操作,导致对象的拷贝构造函数被递归调用,最终导致栈溢出或性能下降,具体取决于对象的复杂性和大小。这是因为对象的拷贝构造函数会被用于复制参数传递给函数的对象,从而导致无限循环的拷贝。
让我们用一个示例说明一下这个问题。
    #include <iostream>


    class MyClass {
    public:
    MyClass() {
    std::cout << "Default Constructor" << std::endl;
    }


    MyClass(const MyClass& other) {
    std::cout << "Copy Constructor" << std::endl;
    }
    };


    void foo(MyClass obj) {
    // Some code here
    }


    int main() {
    MyClass obj;
    foo(obj);
    return 0;
    }

    上面的代码中,foo
    函数的参数是一个 MyClass
    类型的对象,没有使用引用。当 foo
    函数被调用时,会触发对象的拷贝构造函数,这导致了无限循环的拷贝操作,最终导致栈溢出错误。
    为了避免这个问题,通常会将拷贝构造函数的形参声明为引用(const T&
    ),以避免不必要的拷贝,提高性能,并避免递归拷贝。
      class MyClass {
      public:
      // ...


      MyClass(const MyClass& other) {
      std::cout << "Copy Constructor" << std::endl;
      }
      };


      void foo(const MyClass& obj) {
      // Some code here
      }

      这段代码中,foo
      函数的参数是一个常量引用,不会触发额外的拷贝,因此不会出现无限循环的问题。
      8、虚函数原理?
      允许在派生类中重新定义基类中的函数,并且在运行时根据对象的实际类型来调用相应的函数。虚函数的实现依赖于虚函数表(vtable)和虚函数指针(vpointer)
      1. 虚函数的声明和定义:
      在基类中,将希望被派生类重写的函数声明为虚函数。使用 virtual
      关键字来标记虚函数。
        class Base {
        public:
        virtual void foo() {
        // Base class implementation
        }
        };

        1. 虚函数表(vtable):
          1. 对于每个包含虚函数的类,编译器会为其创建一个虚函数表,该表是一个指针数组,其中存储了虚函数的地址。
          2. 虚函数表是在程序运行时分配和维护的,每个包含虚函数的对象都有一个指向其类的虚函数表的指针。
          3. 虚函数表包含了该类中所有虚函数的地址,按照它们在类中声明的顺序排列。
        2. 虚函数指针(vpointer):
          1. 对于每个包含虚函数的对象,编译器会在对象的内存布局中添加一个虚函数指针(通常位于对象的开头)。
          2. 虚函数指针指向该对象的虚函数表,这样程序可以在运行时动态查找并调用适当的虚函数。
        3. 动态分派:
          1. 当通过基类指针或引用调用虚函数时,程序会使用虚函数指针来查找该对象的虚函数表。
          2. 然后,程序根据函数在虚函数表中的位置来调用相应的虚函数。
          3. 这个过程称为动态分派,因为它在运行时根据对象的实际类型来决定调用哪个函数。
        前几天总结了一篇针对虚函数的详细解释,大家可以点击查看:
        C++虚函数详解
        9、场景题(A、B两个团队,实现特定功能,虚函数应该添加到哪个位置)?
        阿Q给出几种情况。
        情景1:基类定义接口,派生类实现功能
        • 如果功能的接口由团队A(或者是一个通用的功能提供者)来定义,而团队B负责实际的功能实现,那么通常情况下虚函数应该添加到基类中。
        • 团队A定义基类,其中包含虚函数声明,但不提供实现。
        • 团队B创建派生类,继承自基类,并重写虚函数以提供特定功能的实现。
        • 这种方式允许团队A定义接口规范,而团队B则根据规范来实现功能。
        情景2:接口类和功能类分离
        • 如果团队A负责定义接口规范,而团队B负责实际功能的实现,并且这两个团队分离开来,可以采用接口类和功能类分离的方式。
        • 团队A定义接口类,其中包含纯虚函数(纯虚函数没有默认实现),这些函数代表功能的接口规范。
        • 团队B创建功能类,继承自接口类,并实现纯虚函数以提供特定功能的实现。
        • 这种方式强调了接口和实现的分离,确保团队B实现了接口中定义的功能。
        情景3:单一实现类的虚函数
        • 如果整个功能由一个团队负责实现,并且不需要将接口规范分离为基类和派生类,那么可以在单一实现类中定义虚函数。
        • 团队A定义一个类,其中包含虚函数,用于表示功能的不同方面或操作。
        • 团队A负责提供这些虚函数的实现,以完成功能。
        • 这种方式适用于较小的项目或不需要多态性的情况。
        10、STL?
        这个可以看这篇总结:
        深入理解STL库
        和这篇面试必备:
        C++面试连环问-STL篇
        11、vector尾插时间复杂度,vector扩容机制?
        1. 尾插入时间复杂度:
          1. vector
            的尾插入操作的平均时间复杂度是 常数时间,通常为 O(1)。
          2. 这是因为 vector
            内部是一个动态数组,当进行尾插入操作时,只需要将元素添加到当前的末尾,并更新 vector
            的大小和末尾指针即可。
        2. vector扩容机制:
          1. vector
            的内部数组空间不足以容纳新的元素时,需要进行扩容。vector
            会创建一个更大的新数组,然后将现有元素逐个拷贝到新数组中。
          2. 扩容时通常会分配比当前容量更大的内存块,以减少扩容的频率,常见的策略是将容量翻倍。
          3. 扩容操作的时间复杂度是线性的,通常为 O(N),其中 N 是当前 vector
            中的元素数量。
          4. 扩容可能会导致重新分配内存、复制数据等开销,但由于扩容不频繁(每次扩容都可以容纳多个元素),平均情况下尾插入操作仍然具有常数时间复杂度。
        12、无序map和map,插入删除的时间复杂度?
        1. std::map:
          1. std::map
            基于红黑树实现的有序关联容器
          2. 插入(Insertion)和删除(Deletion)操作的时间复杂度是 O(log N),其中 N 是 map
            中元素的数量。
          3. 这是因为红黑树的高度受到平衡性的限制,因此操作的时间复杂度保持在对数级别。
        2. std::unordered_map:
          1. std::unordered_map
            基于哈希表实现的无序关联容器
          2. 插入和删除操作的平均时间复杂度是 O(1)。但是在最坏情况下,如果发生哈希冲突,时间复杂度可能会退化到 O(N),其中 N 是 unordered_map
            中元素的数量。
          3. 通常情况下,哈希表的均匀分布和好的哈希函数可以确保大多数操作都在常数时间内完成。
        总结:
        • 如果需要保持元素的有序性,并且对插入和删除操作的性能要求不是非常苛刻,可以使用 std::map
        • 如果对元素的顺序没有要求,且对插入和删除操作的性能有较高要求,可以使用 std::unordered_map
          ,但需要注意处理哈希冲突的情况。
        13、排序算法的时空复杂度?
        时间复杂度:
        时间复杂度表示排序算法在执行时所需的时间量,通常以操作的数量(比较、交换等)来度量。
        空间复杂度:
        空间复杂度表示排序算法在执行时所需的额外内存空间,通常以数据集的大小为度量。
        下边这个表格进行详细的总结:
        排序算法
        平均时间复杂度
        最好情况
        最坏情况
        空间复杂度
        稳定性
        冒泡排序
        O(n^2)
        O(n)
        O(n^2)
        O(1)
        稳定
        快速排序
        O(nlogn)
        O(nlogn)
        O(n^2)
        O(logn)
        不稳定
        选择排序
        O(n^2)
        O(n^2)
        O(n^2)
        O(1)
        不稳定
        插入排序
        O(n^2)
        O(n)
        O(n^2)
        O(1)
        稳定
        希尔排序
        O(nlogn)
        O(log^2n)
        O(log^2n)
        O(1)
        不稳定
        归并排序
        O(nlogn)
        O(nlogn)
        O(nlogn)
        O(n)
        稳定
        堆排序
        O(nlogn)
        O(nlogn)
        O(nlogn)
        O(1)
        不稳定
        计数排序
        O(n+k)
        O(n+k)
        O(n+k)
        O(k)
        稳定
        桶排序
        O(n+k)
        O(n+k)
        O(n^2)
        O(n+k)
        稳定
        基数排序
        O(nxk)
        O(nxk)
        O(nxk)
        O(n+k)
        稳定
        14、在哪个项目中成长最多,详细介绍一下?
        15、操作系统,原子操作?
        原子操作:
        原子操作是多线程编程中的重要概念,它是一种能够在不被中断的情况下执行的操作,不会受到其他线程的干扰。
        1. C++ 原子操作库:
          1. C++ 的 <atomic>
            库定义了一组模板类和函数,用于执行原子操作。这些操作可用于多线程编程,以确保共享资源的线程安全性。
          2. 主要的原子操作类型包括 std::atomic
            , std::atomic_flag
            , std::atomic<T>
            ,以及各种原子操作函数。
        2. std::atomic<T>:
          1. std::atomic<T>
            是一个模板类,用于实现原子操作,其中 T 是要操作的数据类型。
          2. 可以使用 std::atomic<T>
            对象来执行一些常见的原子操作,如加载、存储、交换、递增、递减等。
          3. std::atomic<int> counter(0);
            counter.fetch_add(1); / 原子递增

        3. 原子操作的优点:
          1. 原子操作保证了多线程环境下的数据一致性和线程安全性,避免了竞态条件(Race Condition)。
          2. 不需要使用互斥锁等同步机制,因此可以提高多线程程序的性能。
        4. std::atomic_flag:
          1. std::atomic_flag
            是一个特殊的原子类型,通常用于实现互斥锁。
          2. std::atomic_flag
            只有两个操作:test_and_set
            clear
            ,分别用于设置标志和清除标志。
        5. 内存顺序:
          1. C++ 的原子操作库还提供了内存顺序(Memory Order)的概念,用于控制原子操作的执行顺序。
          2. 可以使用 memory_order
            参数来指定操作的内存顺序,如 memory_order_relaxed
            , memory_order_acquire
            , memory_order_release
            , memory_order_seq_cst
            等。
          3. 内存顺序可以帮助程序员控制原子操作之间的可见性和排序规则。
        它可以提高多线程程序的性能和可维护性,减少了使用底层锁和同步机制的复杂性。
        这里再给大家说说C++一些其它的新特性。
        C++11 新特性:
        1. 自动类型推断(Type Inference):引入 auto
          关键字,允许编译器自动推断变量的类型。
        2. 范围-based for 循环:for (auto elem : container)
          ,简化了迭代容器的代码。
        3. Lambda 表达式:允许定义匿名函数,提供更方便的函数对象。
        4. 智能指针:std::shared_ptr
          , std::unique_ptr
          , 和 std::weak_ptr
          ,用于管理动态分配的内存。
        5. 右值引用和移动语义:提高了内存和性能效率,引入 &&
          来支持移动构造函数和移动赋值操作符。
        6. 初始化列表(Initializer List):使用大括号 {}
          初始化对象和容器。
        7. 新的容器类:std::array
          , std::unordered_set
          , std::unordered_map
          等。
        8. 多线程支持:std::thread
          , std::mutex
          , std::condition_variable
          等多线程相关的库。
        9. 普通字符串字面值:u8
          , u
          , U
          R
          前缀的字符串字面值。
        10. 类型别名(Type Aliases):使用 using
          定义类型别名。
        C++14 新特性:
        1. 泛型 Lambda 表达式:Lambda 表达式支持模板参数。
        2. 返回类型推断:可以使用 auto
          推断函数的返回类型。
        3. 二进制文字:引入 0b
          前缀支持二进制文字。
        4. std::make_unique:类似于 std::make_shared
          ,用于创建 std::unique_ptr
        5. std::index_sequence 和 std::make_index_sequence:用于元编程中生成索引序列。
        C++17 新特性:
        1. 结构化绑定:允许从 std::tuple
          或类似结构中轻松提取成员。
        2. 折叠表达式(Fold Expressions):简化可变参数模板的使用。
        3. if constexpr:编译时条件语句,可以根据条件在编译时决定不同的代码路径。
        4. std::optional:表示可能为空的值。
        5. 并行算法:引入 std::for_each
          , std::transform
          , std::reduce
          等并行算法。
        6. 文件系统库:std::filesystem
          提供了文件系统操作的标准接口。
        C++20 新特性:
        1. 概念(Concepts):引入了概念检查,允许对模板参数进行约束。
        2. 协程(Coroutines):支持异步操作和生成器。
        3. 范围(Ranges):引入了范围操作,如 std::ranges::for_each
          , std::ranges::filter
          等。
        4. 三向比较操作符(Three-Way Comparison):通过 <=>
          运算符实现自定义类型的比较。
        5. 初始化改进:聚合初始化支持使用 =
          进行。
        6. 多线程改进:引入 std::jthread
          std::stop_token
          等用于线程管理的类。
        7. std::format:提供了更灵活的格式化字符串功能。
        C++23 新特性(截至知识截止日期 2021 年 9 月):
        1. 泛型编程的进一步增强:引入更多的概念、约束和元编程功能。
        2. 协程改进:进一步完善协程支持。
        3. 范围模式匹配(Pattern Matching):类似于其他编程语言的模式匹配功能。
        4. 元组改进:引入更多元组相关的特性。
        上边这些最近的新特性在我们日常开发中大抵是用不到的,所以C++11标准还是我们学习的重点。
        16、volatile关键字?
        主要用于告诉编译器不要对其所修饰的变量进行优化,因为这些变量可能会在程序执行过程中被外部因素改变,而编译器不应该假定它们的值是稳定的。
        1. volatile
          的作用:
          1. volatile
            主要用于修饰变量,告诉编译器不要对该变量进行优化。这通常用于描述一些可能会被外部因素(如硬件、操作系统、其他线程等)更改的变量。
        2. 使用场景:
          1. 硬件寄存器:volatile
            可用于描述与硬件寄存器通信的变量,因为这些变量的值可能在编译器无法预测的时间被硬件更改。
          2. 多线程编程:在多线程环境中,一个线程修改的变量可能会被另一个线程读取,这时 volatile
            可以确保对变量的读取和写入不会被优化掉。
          3. 信号处理器中使用:在信号处理器中,被信号处理函数修改的变量应该声明为 volatile
            ,以确保编译器不会对它们进行优化。
        3. 不足之处:
          1. volatile
            仅告诉编译器不要对变量进行优化,但它并不能解决多线程并发问题。在多线程环境中,还需要使用更强大的同步机制(如互斥锁、条件变量等)来确保线程安全性。
          2. volatile
            并不适用于所有情况,因为它仅告诉编译器不要优化,但不提供同步机制。如果需要精确的同步和互斥,应该使用其他多线程编程工具。
        4. 示例
          volatile int hardwareRegister; // 描述硬件寄存器的变量


          void signalHandler(int sig) {
          volatile bool flag = true; // 信号处理器中的变量
          // ...
          }

          总之,volatile
          关键字用于告诉编译器不要对变量进行优化,通常用于描述那些可能被外部因素改变的变量。在多线程环境中,它应该与其他同步机制一起使用来确保线程安全性。但需要注意,volatile
          并不是解决多线程问题的最终解决方案,更复杂的同步机制可能需要用于确保数据一致性。
          17、深挖项目 
          18、反问 

          以上便是我对这篇面经的一些认识和建议,希望大家能够有更多的收获,也再次感谢这位同学毫不吝啬的分享。

          看完还麻烦你们用拿offer的小手帮我点点赞和关注~

          最后,阿Q最近刚刚建立了一个学习交流群,人虽然不多,但都是想努力上进的小伙伴,感兴趣可免费进,我们一起进步和成长!!

          文章转载自阿Q正砖,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

          评论