C++ 面试100题

作者: 夜风_3b8d | 来源:发表于2019-03-16 22:05 被阅读0次

    首先郑重声明,这些面试题的答案都是参考网上的答案和自己理解的部分整合起来,如有错误,欢迎指针。

    1 多态的实现

    存在虚函数的类至少有一个(多继承会有多个)一维的虚函数表叫做虚表(virtual table),属于类成员,虚表的元素值是虚函数的入口地址,在编译时就已经为其在数据端分配了空间。编译器另外还为每个类的对象提供一个虚表指针(vptr),指向虚表入口地址,属于对象成员。在实例化派生类对象时,先实例化基类,将基类的虚表入口地址赋值给基类的虚表指针,当基类构造函数执行完时,再将派生类的虚表入口地址赋值给基类的虚表指针(派生类和基类此时共享一个虚表指针,并没有各自都生成一个),在执行父类的构造函数。
    以上是C++多态的实现过程,可以得出结论:

    • 1 有虚函数的类必存在一个虚表。
    • 2 虚表的构建:基类的虚表构建,先填上虚析构函数的入口地址,之后所有虚函数的入口地址按在类中声明顺序填入虚表;派生类的虚表构建,先将基类的虚表内容复制到派生类虚表中,如果派生类覆盖了基类的虚函数,则虚表中对应的虚函数入口地址也会被覆盖,为了后面寻址的一致性。
    
    class Person{ 
         . . . 
     public : 
        Person (){} 
        virtual ~Person (){}; 
        virtual void speak (){}; 
        virtual void eat (){}; 
     }; 
     
    class Girl : public Person{ 
         . . . 
       public : 
       Girl(){} 
       virtual ~Girl(){}; 
       virtual void speak(){}; 
       virtual void sing(){}; 
    
    
    虚表构建图

    虚函数表中有序放置了父类和子类中的所有虚函数,并且相同虚函数在类继承链中的每一个虚函数表中的偏移量都是一致的。所以确定的虚函数对应virtual table中一个固定位置n,n是一个在编译时期就确定的常量,所以,使用vptr加上对应的n,就可以得到对应的函数入口地址。C++采用的这种绝对地址+偏移量的方法调用虚函数,查找速度快执行效率高,时间复杂度为O(1)
    这里概括一下虚函数的寻址过程:

    1、获取类型名和函数名

    2、从符号表中获得当前虚函数的偏移量

    3、利用偏移量得到虚函数的访问地址,并调用虚函数。vptrn

    2  C/C++的区别

    C面向过程,C++面向对象。C++几乎是C的一个超集,几乎包含了C。

    3 const 关键字

    常变量: const 类型说明符 变量名

    常引用: const 类型说明符 &引用名

    常对象: 类名 const 对象名

    常成员函数: 类名::fun(形参) const

    常数组: 类型说明符 const 数组名[大小]

    常指针: const 类型说明符* 指针名 ,类型说明符* const 指针名

    用法1:常量
    取代了C中的宏定义,声明时必须进行初始化(!c++类中则不然)。const限制了常量的使用方式,并没有描述常量应该如何分配。如果编译器知道了某const的所有使用,它甚至可以不为该const分配空间。最简单的常见情况就是常量的值在编译时已知,而且不需要分配存储。―《C++ Program Language》
     用const声明的变量虽然增加了分配空间,但是可以保证类型安全
    用法2:指针和常量
    使用指针时涉及到两个对象:该指针本身和被它所指的对象。将一个指针的声明用const“预先固定”将使那个对象而不是使这个指针成为常量。要将指针本身而不是被指对象声明为常量,必须使用声明运算符*const。
    所以出现在 * 之前的const是作为基础类型的一部分:
    char *const cp; //到char的const指针
    char const *pc1; //到const char的指针
    const char pc2; //到const char的指针(后两个声明是等同的)
    从右向左读的记忆方式:
    cp is a const pointer to char. 故pc不能指向别的字符串,但可以修改其指向的字符串的内容
    pc2 is a pointer to const char. 故
    pc2的内容不可以改变,但pc2可以指向别的字符串
    且注意:允许把非 const 对象的地址赋给指向 const 对象的指针,不允许把一个 const 对象的地址赋给一个普通的、非 const 对象的指针。
    用法3:const修饰函数传入参数
    将函数传入参数声明为const,以指明使用这种参数仅仅是为了效率的原因,而不是想让调用函数能够修改对象的值。同理,将指针参数声明为const,函数将不修改由这个参数所指的对象。
    通常修饰指针参数和引用参数:
    void Fun( const A *in); //修饰指针型传入参数
    void Fun(const A &in); //修饰引用型传入参数
    用法4:修饰函数返回值
    可以阻止用户修改返回值。返回值也要相应的付给一个常量或常指针。
    用法5:const修饰成员函数(c++特性)
    const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数;
    const对象的成员是不能修改的,而通过指针维护的对象确实可以修改的;
    const成员函数不可以修改对象的数据,不管对象是否具有const性质。编译时以是否修改成员数据为依据进行检查。

    4 malloc/free 和new/delete 区别

    相同点:都可用于申请动态内存和释放内存
    不同点:
    简单点说,malloc只分配指定大小的堆内存空间,而new可以根据对象类型分配合适的堆内存空间,当然还可以通过重载operator new 自定义内存分配策略,其次还能够构造对象,free释放对应的堆内存空间,delete,先执行对象的析构函数,在释放对象所占空间。
    malloc与free是C++/C 语言的标准库函数,new/delete 是C++的运算符。malloc分配时的大小是人为计算的,返回类型是void*,使用时需要类型转换,而new在分配时,编译器能够根据对象类型自动计算出大小,返回类型是指向对象类型的指针,其封装了sizeof和类型转换功能,实际上new分为两步,第一步是通过调用operator new函数分配一块合适,原始的,未命名的内存空间,返回类型也是void *,而且operator new可以重载,可以自定义内存分配策略,甚至不做内存分配,甚至分配到非内存设备上,而malloc无能为力,第二步,调用构造函数构造对象,new将调用constructor,而malloc不能;delete将调用destructor,而free不能

    5 指针和引用的区别

    1、引用在创建时必须初始化,引用到一个有效对象,不是对象,不占用内存空间;而指针在定义时不必初始化,可以在定义后的任何地方重新赋值,是对象,占用内存空间。
    2、指针可以是NULL,引用不行
    3、引用貌似一个对象的小名,一旦初始化指向一个对象,就不能将其他对象重新赋值给该引用,这样引用和原对象的值都会被更改。

    6 C++中堆和栈的区别

    一、预备知识—程序的内存分配
    一个由C/C++编译的程序占用的内存分为以下几个部分
    1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
    2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
    3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的 全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另 一块区域。 - 程序结束后由系统释放。
    4、文字常量区 —常量字符串就是放在这里的。程序结束后由系统释放
    5、程序代码区—存放函数体的二进制代码。

    二、例子程序
    这是一个前辈写的,非常详细
    //main.cpp
    int a = 0; 全局初始化区
    char *p1; 全局未初始化区
    main()
    {
    int b; 栈
    char s[] = "abc"; 栈
    char *p2; 栈
    char *p3 = "123456"; 123456/0在常量区,p3在栈上。
    static int c =0; 全局(静态)初始化区
    p1 = (char *)malloc(10);
    p2 = (char *)malloc(20);
    分配得来得10和20字节的区域就在堆区。
    strcpy(p1, "123456"); 123456/0放在常量区,编译器可能会将它与p3所指向的"123456"
    优化成一个地方。
    }

    二、堆和栈的理论知识
    2.1申请方式
    stack:
    由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间
    heap:
    需要程序员自己申请,并指明大小,在c中malloc函数
    如p1 = (char *)malloc(10);
    在C++中用new运算符
    如p2 = new char[10];
    但是注意p1、p2本身是在栈中的。

    2.2
    申请后系统的响应
    栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
    堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间,另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

    2.3申请大小的限制
    栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意 思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
    堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储
    的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小
    受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

    2.4申请效率的比较:
    栈由系统自动分配,速度较快。但程序员是无法控制的。
    堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
    另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是
    直接在进程的地址空间中保留一块内存,虽然用起来最不方便。但是速度快,也最灵活。

    2.5堆和栈中的存储内容
    栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可
    执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈
    的,然后是函数中的局部变量。注意静态变量是不入栈的。
    当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地
    址,也就是主函数中的下一条指令,程序由该点继续运行。
    堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。

    2.6存取效率的比较

    char s1[] = "aaaaaaaaaaaaaaa";
    char *s2 = "bbbbbbbbbbbbbbbbb";
    aaaaaaaaaaa是在运行时刻赋值的;
    而bbbbbbbbbbb是在编译时就确定的;
    但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。
    比如:
    #include
    void main()
    {
    char a = 1;
    char c[] = "1234567890";
    char *p ="1234567890";
    a = c[1];
    a = p[1];
    return;
    }
    对应的汇编代码
    10: a = c[1];
    00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
    0040106A 88 4D FC mov byte ptr [ebp-4],cl
    11: a = p[1];
    0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]
    00401070 8A 42 01 mov al,byte ptr [edx+1]
    00401073 88 45 FC mov byte ptr [ebp-4],al
    第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到
    edx中,再根据edx读取字符,显然慢了。


    • 管理方式不同
      栈是编译器管理,堆的占用和释放都是由程序员进行控制的;
    • 空间大小不同
      在32位系统下,一般堆的内存可以达到4G的空间,可以说堆内存几乎是没有限制的。但是对于栈,一般都有一定空间大小(跟编译器有关),比如在VC6下默认的栈空间大小是1M
    • 能否产生碎片不同
      对于堆来说,频繁的new/delete操作会造成内存空间的不连续,从而造成大量碎片,使程序效率降低;
      但是对于栈来说,因为总是先进后出不存在内存块不连续的问题。
    • 生长方向不同
      堆的生长方向是向上的,即向着内存地址增加的方向;
      栈的生长方向是向下的,即向着内存地址减小的方向增长。
    • 分配方式不同
      堆总是动态分配的,需要程序员手动释放;
      栈存在静态分配和动态分配的:
      其中静态分配是由编译器完成的(比如局部变量的分配);
      动态分配是由alloca函数进行分配的(这个函数会在栈帧的调用处上分配一定空间,当调用alloca的函数返回到调用位置时,这些临时空间会被自动释放),栈的动态分配是由编译器自己进行释放的。
    • 分配效率不同:
      栈是机器系统提供的数据结构,计算机会在底层对栈提供支持,包括:分配专门的寄存器来存放栈的地址、入出栈都有专门指令,因此栈的效率会比较高。
      堆是C/C++函数库提供的,其机制非常复杂,比如为了分配一块内存,库函数会按照一定的算法在堆内存中搜索可用的足够大小的空间,如果找不到(可能是因为内存碎片过多),就可能调用系统功能区(用户模式和内核模式的切换)增加程序数据段的内存空间,如此便有机会分到足够大小的内存,然后进行返回。

    7 关键字static

    C++的static有两种用法:面向过程程序设计中的static和面向对象程序设计中的static。前者应用于普通变量和函数,不涉及类;后者主要说明static在类中的作用。

    1.面向过程设计中的static

    1.1静态全局变量

    静态全局变量有以下特点:
    • 该变量在全局数据区分配内存;
    • 未经初始化的静态全局变量会被程序自动初始化为0(自动变量的值是随机的,除非它被显式初始化);
    • 静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的;

    1.2静态局部变量

    静态局部变量有以下特点:
    • 该变量在全局数据区分配内存;
    • 静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化;
    • 静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为0;
    • 它始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束;

    1.3静态函数

    定义静态函数的好处:
    • 静态函数不能被其它文件所用;
    • 其它文件中可以定义相同名字的函数,不会发生冲突


    二、面向对象的static关键字(类中的static关键字)

    2.1静态数据成员

    静态数据成员有以下特点:
    • 对于非静态数据成员,每个类对象都有自己的拷贝。而静态数据成员被当作是类的成员。无论这个类的对象被定义了多少个,静态数据成员在程序中也只有一份拷贝,由该类型的所有对象共享访问;
    • 静态数据成员存储在全局数据区。静态数据成员定义时要分配空间,所以不能在类声明中定义;
    • 因为静态数据成员在全局数据区分配内存,属于本类的所有对象共享,所以,它不属于特定的类对象,在没有产生类对象时其作用域就可见,即在没有产生类的实例时,我们就可以操;
    • 静态数据成员主要用在各个对象都有相同的某项属性的时候
    同全局变量相比,使用静态数据成员有两个优势:

    1. 静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其它全局名字冲突的可能性;
    2. 可以实现信息隐藏。静态数据成员可以是private成员,而全局变量不能

    2.2静态成员函数

    一个静态成员函数,它为类的全部服务而不是为某一个类的具体对象服务,静态成员函数由于不是与任何的对象相联系,因此它不具有this指针
    关于静态成员函数,可以总结为以下几点:
    • 出现在类体外的函数定义不能指定关键字static;
    • 静态成员之间可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员函数;
    • 非静态成员函数可以任意地访问静态成员函数和静态数据成员;
    • 静态成员函数不能访问非静态成员函数和非静态数据成员;
    • 由于没有this指针的额外开销,因此静态成员函数与类的全局函数相比速度上会有少许的增长;


    在C++程序中调用被C 语言修饰的函数,为什么要加extern “C”?

    extern "C"指令非常有用,因为C和C++的近亲关系。注意:extern "C"指令中的C,表示的一种编译和连接规约,而不是一种语言。C表示符合C语言的编译和连接规约的任何语言,如Fortran、assembler等。

    extern "C"指令仅指定编译和连接规约,但不影响语义。例如在函数声明中,指定了extern "C",仍然要遵守C++的类型检测、参数转换规则

    extern "C"的真实目的是实现类C和C++的混合编程。在C++源文件中的语句前面加上extern "C",表明它按照类C的编译和连接规约来编译和连接,而不是C++的编译的连接规约。这样在类C的代码中就可以调用C++的函数or变量等

    C++是一个面向对象语言(虽不是纯粹的面向对象语言),它支持函数的重载,重载这个特性给我们带来了很大的便利。为了支持函数重载的这个特性,C++编译器实际上将下面这些重载函数:

    void print(int i);
    void print(char c);
    void print(float f);
    void print(char* s);
    

    编译为:

    _print_int
    _print_char
    _print_float
    _pirnt_string
    

    这样的函数名,来唯一标识每个函数。C++中的变量,编译也类似,如全局变量可能编译g_xx,类变量编译为c_xx等。连接是也是按照这种机制去查找相应的变量。
    C语言中并没有重载和类这些特性,故并不像C++那样print(int i),会被编译为_print_int,而是直接编译为_print等。因此如果直接在C++中调用C的函数会失败,因为连接是调用C中的print(3)时,它会去找_print_int(3)。因此extern "C"的作用就体现出来了

    当我们C和C++混合编程时,有时候会用一种语言定义函数指针,而在应用中将函数指针指向另一中语言定义的函数。如果C和C++共享同一中编译和连接、函数调用机制,这样做是可以的。然而,这样的通用机制,通常不然假定它存在,因此我们必须小心地确保函数以期望的方式调用。

    而且当指定一个函数指针的编译和连接方式时,函数的所有类型,包括函数名、函数引入的变量也按照指定的方式编译和连接。如下例:

    typedef int (*FT) (const void* ,const void*);//style of C++
     
    extern "C"{
        typedef int (*CFT) (const void*,const void*);//style of C
        void qsort(void* p,size_t n,size_t sz,CFT cmp);//style of C
    }
     
    void isort(void* p,size_t n,size_t sz,FT cmp);//style of C++
    void xsort(void* p,size_t n,size_t sz,CFT cmp);//style of C
     
    //style of C
    extern "C" void ysort(void* p,size_t n,size_t sz,FT cmp);
     
    int compare(const void*,const void*);//style of C++
    extern "C" ccomp(const void*,const void*);//style of C
     
    void f(char* v,int sz)
    {
        //error,as qsort is style of C
        //but compare is style of C++
        qsort(v,sz,1,&compare);
        qsort(v,sz,1,&ccomp);//ok
         
        isort(v,sz,1,&compare);//ok
        //error,as isort is style of C++
        //but ccomp is style of C
        isort(v,sz,1,&ccopm);
    }
    

    10 什么是内存泄漏?什么是野指针?什么是内存越界?如何避免?

    10.1内存泄漏

    概念:用动态内存分配函数动态开辟的空间,在使用完毕后未释放,程序结束后,会导致一直占据该内存单元,直到程序结束,在现代操作系统中,一个应用程序使用的常规内存在程序终止时被释放。这表示一个短暂运行的应用程序中的内存泄漏不会导致严重后果。但是在内存非常有限的系统中都可能导致非常严重的后果,shared_ptr来避免内存泄漏,但是要正确使用

    10.2野指针

    “野指针”不是NULL指针,是指指向“垃圾”内存的指针。即指针指向的内容是不确定的。
    产生的原因:
    1)指针变量没有初始化。因此,创建指针变量时,该变量要被置为NULL或者指向合法的内存单元。
    2)指针p被free之后,没有置为NULL,让人误以为p是个合法的指针。
    3)指针跨越合法范围操作。不要返回指向栈内存(非静态局部变量)的指针或引用。
    可能后果:

    • 若操作系统将这部分已经释放的内存重新分配给另外一个进程,而原来的程序重新引用现在的迷途指针,向其中写入数据,则这部分程序内容将被破坏,而导致程序错误。这种类型的程序错误,通常会导致segment fault和一般的保护错误。
    • 其他常见错误:返回一个基于栈分配的局部变量的地址时,一旦调用的函数返回,分配给这些变量的空间将回收,此时它们拥有的是垃圾值,如return &num,如果要使它的生命周期边长,应该将其声明为static

    10.3 内存越界

    存在一种情况就是调用栈溢出(stackoverflow),还有一种情况是缓冲区溢出,这两种情况都会导致安全漏洞。

    10.3.1缓冲区溢出

    strcpy会一直复制直到碰到\0,很多平台的栈变量是按照地址顺序倒着分配的(高地址向低地址),所以destination溢出后会先修改先前定义的变量,这样黑客就可以把is_administrator改为true,从而造成缓冲区溢出攻击,当然数组越界也可以造成类似的效果,不过现在C++都提供了越界检查的版本

    // 缓冲区溢出攻击
    const int MAX_LENGTH = 16;
    bool is_administrator = false;
    char destination[MAX_LENGTH];
    std::string source = read_string_from_client(); //内容存储在缓冲区
    strcpy(destination,source.c_str());
    

    10.3.2栈溢出攻击

    栈溢出攻击:在栈上分配length字节的空间,再往栈顶放上一个data。当Length十分大,会把data挤到栈空间之外,此时如果编译器不做越界检查的话,那么黑客只要用客户端送特定的length和data,就能改写服务器的任意内存(比如黑客可以修改服务器代码的机器码,注入一些JMP指令跳转到黑客想执行的函数)

    // 栈溢出攻击
    int length = read_int_from_client();
    char buffer[length];    //栈空间分配
    int data = read_int_from_client();
    

    11 堆栈缓存的区别

    1、栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放;
    2、堆是存放在二级缓存中,堆的首地址放在一级缓存缓存中,分配和释放会产生系统调用,由用户态进入内核态,所以速度会慢一些


    12 STL 容器有哪些,常用的算法


    13 如何理解智能指针,什么时候改变引用计数


    14 share_ptr 与weak_ptr 的区别与联系


    15 C++构造函数是否可以抛出异常

    构造函数可以抛出异常。但从逻辑上和风险控制上,构造函数中尽量不要抛出异常,既需要分配内存,又需要抛出异常时要特别注意防止内存泄露的情况发生。因为在构造函数中抛出异常,在概念上将被视为该对象没有被成功构造,因此当前对象的析构函数就不会被调用,就会造成内存泄漏。同时,由于构造函数本身也是一个函数,在函数体内抛出异常将导致当前函数运行结束,并释放已经构造的成员对象,包括其基类的成员,即执行直接基类和成员对象的析构函数


    16 是否在析构函数抛出异常

    1)如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
    2)通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。

    1. 那么当无法保证在析构函数中不发生异常时, 其实还是有很好办法来解决的。那就是把异常完全封装在析构函数内部,决不让异常抛出函数之外。这是一种非常简单,也非常有效的方法。

    17 volatile 的作用


    18 构造函数和析构函数可以调用虚函数吗

    虽然可以对虚函数进行实调用,但程序员编写虚函数的本意应该是实现动态联编。在构造函数中调用虚函数,函数的入口地址是在编译时静态确定的,并未实现虚调用。但是为什么在构造函数中调用虚函数,实际上没有发生动态联编呢?
    第一个原因,在概念上,构造函数的工作是为对象进行初始化。在构造函数完成之前,被构造的对象被认为“未完全生成”。当创建某个派生类的对象时,如果在它的基类的构造函数中调用虚函数,那么此时派生类的构造函数并未执行,所调用的函数可能操作还没有被初始化的成员,将导致灾难的发生。
    第二个原因,即使想在构造函数中实现动态联编,在实现上也会遇到困难。这涉及到对象虚指针(vptr)的建立问题。在Visual C++中,包含虚函数的类对象的虚指针被安排在对象的起始地址处,并且虚函数表(vtable)的地址是由构造函数写入虚指针的。所以,一个类的构造函数在执行时,并不能保证该函数所能访问到的虚指针就是当前被构造对象最后所拥有的虚指针,因为后面派生类的构造函数会对当前被构造对象的虚指针进行重写,因此无法完成动态联编

    19 内存对齐的原则

    1).数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int在32位机为4字节, 则要从4的整数倍地址开始存储),基本类型不包括struct/class/uinon。
    2).结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部"最宽基本类型成员"的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)。
    3).收尾工作:结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的"最宽基本类型成员"的整数倍.不足的要补齐.(基本类型不包括struct/class/uinon)。
    4).sizeof(union),以结构里面size最大元素为union的size,因为在某一时刻,union只有一个成员真正存储于该地

    20 内联函数有什么优点?内联函数和宏定义的区别。

    1.内联函数在运行时可调试,而宏定义不可以;
    2.编译器会对内联函数的参数类型做安全检查或自动类型转换(同普通函数),而宏定义则不会;
    3.内联函数可以访问类的成员变量,宏定义则不能;
    4.在类中声明同时定义的成员函数,自动转化为内联函数
    内联函数和普通函数相比可以加快程序运行的速度,因为不需要中断调用,在编译的时候内联函数可以直接被镶嵌到目标代码中。
    内联函数要做参数类型检查,这是内联函数跟宏相比的优势。

    inline一般只用于如下情况:
    (1)一个函数不断被重复调用。
    (2)函数只有简单的几行,且函数不包含for、while、switch语句,递归。

    21 数组与指针的区别与联系,函数指针,指针函数,指针数组,数组指针

    22 STL set 和map 都是基于什么实现的

    23 C++内存泄露及检测工具

    检测内存泄漏的关键是要能截获住对分配内存和释放内存的函数的调用。截获住这两个函数,我们就能跟踪每一 块内存的生命周期,比如,每当成功的分配一块内存后,就把它的指针加入一个全局的list中;每当释放一块内存,再把它的指针从list中删除。这样,当程序结束的时候,list中剩余的指针就是指向那些没有被释放的内存

    相关文章

      网友评论

        本文标题:C++ 面试100题

        本文链接:https://www.haomeiwen.com/subject/ajnsmqtx.html