当前位置:   article > 正文

C++面试基础知识_c++基础面试

c++基础面试


面试知识点笔记下载链接

1、static关键字作用

  • **局部静态变量**特点:
  1. 该变量在全局数据区分配内存(局部变量在栈区分配内存);
  2. 静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化(局部变量每次函数调用都会被初始化);
  3. 静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为0(局部变量不会被初始化);
  4. 它始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,也就是不能在函数体外面使用它(局部变量在栈区,在函数结束后立即释放内存);
  • 静态全局变量
    定义在函数体外,用于修饰全局变量,表示该变量只在本文件可见。静态全局变量不能被其它文件所用(全局变量可以);
    其它文件中可以定义相同名字的变量,不会发生冲突(自然了,因为static隔离了文件,其它文件使用相同的名字的变量,也跟它没关系了);
  • 静态函数
    • 函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件中可见,静态函数不能被其它文件所用;
    • 其它文件中可以定义相同名字的函数,不会发生冲突;函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突。
  • 类的静态成员
    在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对于多个对象来说,静态数据成员只存储一处,供所有对象共用。
  • 类的静态函数
    • 加上static,使它变成一个静态成员函数,可以用类名::函数名进行访问;
    • 静态成员之间可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员函数;
    • 非静态成员函数可以任意地访问静态成员函数和静态数据成员;
    • 静态成员函数不能访问非静态成员函数和非静态数据成员;
    • 调用静态成员函数,可以用成员访问操作符(.)和(->)为一个类的对象或指向类对象的指针调用静态成员函数,也可以用类名::函数名调用(因为他本来就是属于类的,用类名调用很正常)。
    • 解释:因为静态是属于类的,它是不知道你创建了10个还是100个对象,所以它对你对象的函数或者数据是一无所知的,所以它没办法调用,而反过来,你创建的对象是对类一清二楚的(不然你怎么从它那里实例化呢),所以你是可以调用类函数和类成员的。

2、四种cast类型转换

  • Static_cast:
    • 不提供运行时的检查,所以叫static_cast,因此,需要在编写程序时确认转换的安全性。
    • 用于类层次结构中,父类和子类之间指针和引用的转换;进行上行转换,把子类对象的指针/引用转换为父类的指针/引用,这种转换是安全的;进行下行转换,把父类对象的指针/引用转换成子类的指针/引用,这种转换是不安全的,需要编写程序时来确认;
    • 用于基本数据类型之间的转换,例如把int转换成char,int转换成enum等,需要编写程序时来确认安全性,非const转const。
    • 把void指针转换成目标类型的指针(这是极不安全的);
  • Dynamic_cast:
    • 相比static_cast,dynamic_cast会在运行时检查类型转换是否合法,具有一定的安全性。由于运行时的检查,所以会额外消耗一些性能。dynamic_cast使用场景与static相似,在类层次结构中使用时,只能用于含有虚函数的类,上行转换和static_cast没有区别,都是安全的;下行转换时,dynamic_cast会检查转换类型,相比static_cast更安全。仅用于指针和引用的转换。还有void *的转换
  • Const_cast:
    • const_cast用于移除类型的const、volatile和__unaligned属性。
    • 常量指针被转换成非常量指针,并且仍然指向原来的对象;常量引用被转换成非常量引用,并且仍然指向原来的对象。
  • Reinterpret_cast:
    • 非常激进的指针类型转换,在编译期完成,可以转换任何类型的指针,所以极不安全。非极端情况不要使用。
  • 为什么不使用C的强制转换:
    • C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。

3、指针和引用的区别

指针在运行时可以改变所指向的值,而引用一旦与某个对象绑定后就不再改变。意思是:指针可以被重新赋值以指向另一个对象,但是引用则总是在初始化时被指定的对象,以后不能改变,但是指向的内容可以改变。(下面是这个规则的理解)

  1. 指针有自己的一块空间,而引用只是一个别名;
  2. 使用sizeof看一个指针大小是4,而引用则是被引用对象的大小;
  3. 指针可以被初始化为NULL,而引用必须被初始化一个已有对象的引用;
  4. 可以有const指针,但是没有const引用;
  5. 指针在使用中可以指向其他对象,但是引用只能是一个对象的引用,不能被改变;
  6. 如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄漏;
    在这里插入图片描述

4、C++智能指针

由于 C++ 语言没有自动内存回收机制,程序员每次 new 出来的内存都要手动 delete。程序员忘记 delete,流程太复杂,最终导致没有 delete,异常导致程序过早退出,没有执行 delete 的情况并不罕见,导致内存泄漏。使用智能指针可以很大程度避免这个问题,因为智能指针本身就是一个类,当超出了类的作用域,类会自动调用析构函数自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需手动释放。

  • auto_ptr:(c++98的方案,c++11已经抛弃)
    在这里插入图片描述
    std::auto_ptr 可用来管理单个对象的对内存,但是,请注意如下几点:
    • 尽量不要使用“operator=”。如果使用了,请不要再使用先前对象。罪魁祸首是“my_memory2 = my_memory”,这行代码,my_memory2 完全夺取了 my_memory 的内存管理所有权,导致 my_memory 悬空,最后使用时导致崩溃。
    • 记住 release() 函数不会释放对象,仅仅归还所有权。
    • std::auto_ptr 最好不要当成参数传递(读者可以自行写代码确定为什么不能)。
    • 由于 std::auto_ptr 的“operator=”问题,有其管理的对象不能放入 std::vector等容器中。

5、野指针

野指针,是指向不可用内存区域的指针,指向一个已删除的对象或者未申请访问受限内存区域的指针。
造成野指针的原因主要有三种:

  • 1、指针变量没有被初始化。
    • 任何指针变量刚被创建时不会自动成为NULL指针。在Debug模式下,VC++编译器会把未初始化的栈内存上的指针全部填成 0xcccccccc ,当字符串看就是 “烫烫烫烫……”;会把未初始化的堆内存上的指针全部填成 0xcdcdcdcd,当字符串看就是 “屯屯屯屯……”。把未初始化的指针自动初始化为0xcccccccc或0xcdcdcdcd,而不是就让取随机值,那是为了方便我们调试程序,使我们能够一眼就能确定我们使用了未初始化的野指针。在Release模式下,编译器则会将指针赋随机值,它会乱指一气。所以,指针变量在创建时应当被初始化,要么将其设置为NULL,要么让它指向合法的内存。
  • 2、指针指向的内存被释放了,而指针本身没有置NULL。
    • 对于堆内存操作,我们分配了一些空间(使用malloc函数、calloc函数或new操作符),使用完后释放(使用free函数或delete操作符)。指针指向的内存被释放了,而指针本身没有置NULL。通常会用语句if (p != NULL)进行防错处理。很遗憾,此时if语句起不到防错作用。因为即便p不是NULL指针,它也不指向合法的内存块。所以在指针指向的内存被释放后,应该将指针置为NULL。
  • 3 、指针超过了变量的作用范围。
    • 即在变量的作用范围之外使用了指向变量地址的指针。这一般发生在将调用函数中的局部变量的地址传出来引起的。这点容易被忽略,虽然代码是很可能可以执行无误,然而却是极其危险的。局部变量的作用范围虽然已经结束,内存已经被释放,然而地址值仍是可用的,不过随时都可能被内存管理分配给其他变量。

6、构造和析构顺序

  • 在调用构造函数时先调用基类的构造函数,再调用派生类构造函数;而当调用析构函数时,则要先调用派生类的析构函数再调用基类的析构函数
    在这里插入图片描述
  • 这道题主要考察的知识点是 :全局变量,静态局部变量,局部变量空间的堆分配和栈分配
    其中全局变量和静态局部变量时从 静态存储区中划分的空间,二者的区别在于作用域的不同,全局变量作用域大于静态局部变量(只用于声明它的函数中),而之所以是先释放 D 在释放 C的原因是, 程序中首先调用的是 C的构造函数,然后调用的是 D 的构造函数,析构函数的调用与构造函数的调用顺序刚好相反。
  • 局部变量A 是通过 new 从系统的堆空间中分配的,程序运行结束之后,系统是不会自动回收分配给它的空间的,需要程序员手动调用 delete 来释放。
    局部变量 B 对象的空间来自于系统的栈空间,在该方法执行结束就会由系统自动通过调用析构方法将其空间释放。之所以是 先 A 后 B 是因为,B 是在函数执行到 结尾 “}” 的时候才调用析构函数, 而语句 delete a ; 位于函数结尾 “}” 之前。
  • 构造函数的调用顺序 :基类构造函数、对象成员构造函数、派生类本身的构造函数
  • 析构函数的调用顺序:派生类本身的析构函数、对象成员析构函数、基类析构函数(与构造顺序正好相反)

7、析构函数为什么要定义为虚函数?

将可能会被继承的父类的析构函数设置为虚函数,可以保证基类指针指向派生类对象时,释放基类指针也可以释放掉子类的空间,防止内存泄漏。
对于下图左:基类指针指向派生类对象时,这是因为在调用普通函数时,在编译期间已经确定了它所要调用的函数(静态绑定),因为a是A*类型,因此只会调用基类的析构函数。 对于下图右,基类析构使用虚函数,这是因为当我们把基类析构函数定义为虚函数时,在调用析构函数时,会在程序运行期间根据指向的对象类型到它的虚函数表中找到对应的虚函数(动态绑定),此时找到的是派生类的析构函数,因此调用该析构函数;而调用派生类析构函数之后会再调用基类的析构函数,因此不会导致内存泄漏。
在这里插入图片描述

8、为什么C++默认析构函数不是虚函数?

  • C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数是虚函数的话会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要作为父类时候,设置为虚函数。
  • 虚函数实现:在有虚函数的类中,类的开始部分是一个虚函数表的指针,指向一个虚函数表,表中放了虚函数的地址,实际虚函数在代码段.text。当子类继承父类时,也会继承其虚函数表,当子类重写父类虚函数时,会将继承到的虚函数表中地址替换为重新写的函数地址。根据对象类型调用方法,而不是指针类型。

9、纯虚函数

纯虚函数是一种特殊的虚函数,在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。这就是纯虚函数的作用。

  • 引入原因/纯虚函数的作用:
    • 为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。
    • 在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
    • 为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。

10、虚函数表创建时机

虚拟函数表是在编译期就建立了,各个虚拟函数这时被组织成了一个虚拟函数的入口地址的数组.而对象的隐藏成员–虚拟函数表指针是在运行期–也就是构造函数被调用时进行初始化的,编译器会把虚函数表的首地址赋值给虚函数表指针,这是实现多态的关键。

11、在main函数之前执行的函数

在这里插入图片描述
也可以用类的构造函数。

12、C++怎么定义常量

可以使用constdefine
在这里插入图片描述

13、const和define的区别?

  • 就定义常量来说:const定义的常数是变量也带类型,#define定义只是个常数不带类型;
  • 作用阶段:define是在编译的预处理阶段起作用,const是在编译、运行时起作用。
  • 起作用方式:define只是简单的字符串替换,无类型检查。const有对应数据类型,要进行判断,可避免一些低级错误。 如#define N 2+3 double a = N/2 不是2.5,而是3.5
  • 空间占用:#define预处理后,占用代码段空间,const本质是个变量占用数据段空间。
  • 代码调试方便程度:const常量可以进行调试,define不能进行调试,在预编译替换掉了。
  • 再定义角度:const的不足是与生俱来,不能重定义;#define可通过#undef取消某个符号的定义,再重新定义。
  • 某些特殊功能:define可用来防止头文件重复引用,const不能。如#ifndef ** #define **

14、const修饰成员函数

将成员函数声明为const,则该函数不允许修改类的数据成员。只有被声明为const的成员函数才能被一个const类对象调用,而非const对象可以访问任意的成员函数,包括const成员函数

  • const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数.
  • const对象的成员变量不可以修改。
  • mutable修饰的成员变量,在任何情况下都可以修改。也就是说,const成员函数也可以修改mutable修饰的成员变量。c++很shit的地方就是mutable和friendly这样的特性,很乱。
  • const成员函数可以访问const成员变量和非const成员变量,但不能修改任何变量。检查发生在编译时。
  • 非const成员函数可以访问非const对象的非const数据成员、const数据成员,但不可以访问const对象的任意数据成员。
  • const成员函数只是用于非静态成员函数,不能用于静态成员函数。
  • const成员函数const修饰不仅在函数声明中要加(包括内联函数),在类外定义也要加。
  • 作为一种良好的编程风格,在声明一个成员函数时,若该成员函数并不对数据成员进行修改操作,应尽可能将该成员函数声明为const 成员函数。

15、c++函数栈空间的最大值

线程中函数堆栈默认是1M,Linux下默认是8M ulimit –s 可查看 这些都可以修改。

16、Extern “C”作用?

extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。

17、 Malloc/free和new/delete的区别?

  • new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持。
  • 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。
  • new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
  • new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。
  • malloc开辟的内存永远是通过free来释放的;而new单个元素内存,用的是delete,如果new[]数组,用的是delete[]来释放内存的。

18、函数调用栈过程

在这里插入图片描述
EBP 基址指针,是保存调用者函数的地址,总是指向函数栈栈底,ESP被调函数的指针,总是指向函数栈栈顶。
在这里插入图片描述
在这里插入图片描述

19、Select函数和epoll函数

Select函数可以实现I/O的多路复用,用于监视文件描述符的状态,fd_set中的每一bit可以对应一个文件描述符fd,先将需要监控的描述符对应的bit位置1,然后将其传给select,当有任何一个事件发生时,select将会返回所有的描述符,需要应用程序自己遍历检查哪个描述符上有事件发生,效率有点低,并且不断在内核态和用户态进行描述符的拷贝,开销有点大。

  • 执行fd_set set;FD_ZERO(&set);则set用位表示是0000,0000。
  • 若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
  • 若再加入fd=2,fd=1,则set变为0001,0011
  • 执行select(6,&set,0,0,0)阻塞等待
  • 若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。
    **返回值:**超时返回0,错误返回-1并设置errno,成功时:返回三中描述符集合中”准备好了“的文件描述符数量。

注意事项:每次调用select之前,timeout都用重新设置。由于select()修改其文件描述符集,如果调用在循环中使用,则必须在每次调用之前重新初始化这些集。

Epoll
首先epoll_create创建一个epoll对象,然后使用epoll_ctl对这个对象进行操作,把需要监控的描述添加进去,这些描述将会以epoll_event结构体形式组成一颗红黑树,接着阻塞在epoll_wait,进入大循环,当某个fd上有事件发生时,内核将会把对应的结构体放入到一个链表中,返回有事件发生的链表。
在这里插入图片描述
目前的常用的IO复用模型有三种:select,poll,epoll。

  • select==>时间复杂度O(n)
    • 它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。
  • poll==>时间复杂度O(n)
    • poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.
  • epoll==>时间复杂度O(1)
    • epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))

20、请你说说fork,exec,wait函数?

父进程产生子进程,使用fork拷贝出来的一个父进程的副本,此时只拷贝了父进程的页表,两个进程都读同一块内存,当有进程写的时候,使用写实拷贝机制分配内存,fork从父进程返回子进程的pid,从子进程返回0.;exec函数可以加载一个elf文件去替换父进程,从而父进程和子进程就可以运行不同的程序,exec执行成功,则子进程从新的进程开始运行,无返回值;如果执行失败,则返回-1;调用了wait的父进程将会发生阻塞,直到有子进程的状态改变,执行成功返回0,错误返回-1。

21、inline函数优缺点?

场景:当频繁调用小函数时,为了节省函数调用的开销,可以使用内联函数。

  • 优点:
    • 执行速度快,内联函数代码被放入符号表中,在调用位置进行替换,和宏展开一样,效率很高,好处是减少函数入栈、出栈以及跳转的开销,从而提高程序的性能。
    • 调用时会检查参数类型,比较安全;
    • 可以用修饰保护成员和私有成员。
  • 缺点:
    • 以函数复制为代价,如果过多使用,会消耗内存;
    • 如果函数体内有循环或开关语句,那么执行函数代码的时间比调用要开销大;
    • 是否内联,程序不可控。inline只是对编译器建议,是否内联取决于编译器。

22、C++三大特性:(面对对象四大特性)

  • 1、抽象:把现实世界中某一类东西提取出来,用程序代码表示,抽象出来的一般叫做类或者接口。抽象一般包括两个方面,一个数据抽象、二是过程抽象。数据抽象:表示世界中一类事物的特征,就是对象的属性。比如鸟有翅膀、羽毛等(类的属性)。过程抽象:表示世界中一类事物的行为,就是对象的行为。比如鸟会飞(类的方法)。
  • 2、封装:在面向对象的思想中,将数据和对数据的操作封装在一起——即类。
    类只对外界开放接口(即有权访问的函数接口),而将接口的实现细节和该类的一些属性(变量)隐藏起来,达到数据的抽象性(使具有相同行为的不同数据可以抽象为同一个类)、隐藏性和封装性。
  • 3、继承:就像孩子会继承父母的一些性格或特点,两个类如果存在继承关系,其子类必定具有父类的相关属性(即变量)和方法(即函数)。
  • 4、多态:多态是通过父类的指针或引用,调用了一个在父类中是virtual类型的函数,实现动态绑定机制。我们知道,若想使用父类的指针/引用调用子类的函数,需要在父类中将其声明为虚函数(virtual),且必须与子类中的函数参数列表相同(包括参数个数、类型、顺序),返回值也相同(若不同为协变的情况,需要各自返回各自类的指针/引用)。

C++的多态是通过覆盖实现的,即父类的函数被子类覆盖了!
父类的该函数为虚函数,告诉父类的指针/引用,你调用这个函数的时候必须看一看你绑定的对象到底是哪个类的对象,然后去那个类里调用该函数!

23、面对对象基本原则(SOLDI)

  • 单一职责原则(SRP):指一个类的功能要单一,不能包罗万象。一个类最好只做一件事,只有一个引起它的变化。
  • 开放封闭原则(OCP):软件实体应该是扩展的,而不可修改的。即对象或实体应该对扩展开放、对修改封闭。体现在两方面:对扩展开放意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况;对修改封闭意味着类一旦设计完成,就可以独立完成其工作,而不需对其进行任何尝试的修改。
  • 里氏替换原则(LSP):子类应当可以替换父类并出现在父类能够出现的任何地方。这一思想体现为对继承机制的约束规范,只有子类能够替换基类时,才能保证系统在运行期间识别子类,这是保证继承复用的基础。这一约束反过来不成立,子类可以替换基类,但基类不一定能替换子类。
  • 依赖倒置原则(DIP):高层次模块不应该依赖于低层次模块,它们都应该依赖于抽象,而不是抽象依赖于实现。我们知道依赖一定会存在于类与类、模块与模块之间,当两个模块之间存在紧密耦合关系时,最好方法是分离接口和实现:在依赖之间定义一个抽象的接口使得高层模块调用接口,而底层模块实现接口的定义,从此有效控制耦合关系。
  • 接口隔离原则(ISP):使用多个小的专门的接口,而不是使用一个大的总接口。
  • https://blog.csdn.net/qq_36607646/article/details/53447002
  • https://www.cnblogs.com/fzz9/p/8973315.html

24、函数名相同,参数列表也相同,返回类型不同,怎么重载?

  • 在C++中,只要原来的返回类型是指向基类的指针或引用,新的返回类型是指向派生类的指针或引用,覆盖的方法就可以改变返回类型。这样的类型称为协变返回类型(Covariant returns type).
  • 协变返回类型的优势在于,总是可以在适当程度的抽象层面工作。目前,一般认为,返回值可以协变,参数则不可以。因此,在C++标准的虚函数中,返回值可以协变,参数不行。
  • https://www.cnblogs.com/readlearn/p/10806487.html
  • https://www.cnblogs.com/lsgxeva/p/7684545.html

25、字符串长度

char str1[]="12345678";   后面有一个看不见的‘\0char str2[]={'1','2','3','4','5','6','7','8'};
char str3[]={'1','2','3','\0','5','6','7','8'};
str1是一个字符数组,由字符串"12345678"进行初始化。由于"12345678"含有一个结尾字符'\0',所以str1数组共有9个字符。 因此sizeof(str1)=9。
str2也是一个字符数组,它的长度由'1','2','3','4','5','6','7','8'8个字符初始化,并没有指明零字符。 因此sizeof(str2)=8。
str3同样由8个字符初始化,因此sizeof(str3)=8。
strlen函数只计算字符串中不含零的字符个数。因此:strlen(str1)=8。
而由于str2中最后一个字符不包含零,所以,str2不是一个有效的字符串, 因此strlen(str2)不确定。
而对于字符数组str3,在第4个字符为'\0',所以作为一个字符串,在此处结尾。所以strlen(str3)=3
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/672488
推荐阅读
相关标签
  

闽ICP备14008679号