面试题

Posted weiweng

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试题相关的知识,希望对你有一定的参考价值。

以下内容均摘抄自他人博客,正确性有待考察,请以质疑的态度阅读学习,若有错误请留言指正

stl用过哪些容器?

  • Vector:动态数组
  • List:双向链表
  • Deque:与vector类似,但支持双端操作。
  • Set:关联容器-集合,底层红黑树实现。
  • Map:关联容器-键值对,底层红黑树实现。
  • Stack:栈,默认由deque封装得到。
  • Queue:队列,默认由deque封装得到。
  • C++11新加入的有:array、forward_list、unordered_set、unordered_map(这两个使用hash表来存储)

stl内存分配器的设计与实现

内存分配器(Memory Allocator)负责内存管理,实现动态内存的分配和释放。内存分配器分为两级。第一级分配器直接调用C函数分配内存,第二级分配器则采用内存池来管理内存。如果申请的内存块足够大,那么启动第一级分配器,否则启动第二级分配器。这种设计的优点是可以快速分配和释放小块内存,同时避免内存碎片;缺点是内存池的生命周期比较长,并且很难显式释放。
第一级分配器只是简单的调用函数malloc()、realloc()和free()。为了保证内存按照指定字节数对齐,则需要调用函数_aligned_malloc()、_aligned_realloc()和_aligned_free(),因此实际分配的内存块可能大于申请内存的大小。
第二级分配器需要维护16个空闲块链表和一个内存池。每个链表中的空闲块的大小都是固定的,默认对齐字节数为8,则各个链码空闲块大小依次为n、2n、3n、4n、5n、6n、7n、8n、9n、10n、11n、12n、13n、14n、15n、16n。内存池由两个指针来描述,free_start记录起始地址,free_end记录结束地址。另外两个变量heap_size和used_size分别纪录堆大小和已用内存大小。

内存池管理的内存块大小只有固定的16个规格, 当所需内存块大于16n时,则使用第一级分配器进行内存分配。否则,按照以下步骤进行内存分配:

  • 申请内存的大小上调至8的倍数,根据此大小查找对应的空闲链表;如果空闲链表中有可用的内存块,则直接返回此空闲块,并从空闲链表中删除该块,否则继续下面的步骤;
  • 计算内存池中所剩空间的大小,如果足以分配20个内存块,则从中取出20个内存块,调整内存池起始地址,返回第一个内存块,并将剩余的19个块并入空闲链表,否则继续下面的步骤;
  • 如果剩余空间足以分配至少1个内存块,则从中取出尽可能多的内存块,调整内存池起始地址,返回第一个内存块,并将剩余的内存块并入空闲链表,否则继续下面的步骤;
  • 如果内存池中还有一些内存,则将剩余空间并入其对应大小的空闲链表中;向系统申请一个较大的内存块,如果申请成功,返回第一个内存块,调整内存池起始地址,否则继续下面的步骤;
  • 山穷水尽,只能遍历后面更大的空闲链表,如果存在更大的空闲内存块,则从空闲链表中删除该块,返回该块首地址,并将剩余的部分内存交给内存池管理,否则最后尝试使用第一级配置器。如果还是失败则报错。

内存池向系统申请的内存空间,在使用过程中会被划分为更小的内存块,而这些小内存块的使用和归还几乎是随机的。如果试图对这些小内存块进行合并和释放,其高昂的代价会大幅降低内存池的性能。但在内存池的已用内存大小为0时,释放内存是安全的。内存分配器维护一个指针链表,用于内存空间的统一释放。

解决Hash冲突的几种方法

  • 开放地址法:线性探测、线性补偿探测法、伪随机探测
  • 拉链法:冲突的地方建立链表保存冲突数据。
  • 再散列:使用其他哈希函数计算地址,直到无冲突为止。
  • 建立公共溢出区:再建立一个存储向量来保存冲突的数据。

虚指针工作原理,解释了一下虚函数列表

虚函数的实现要求对象携带额外的信息,这些信息用于在运行时确定该对象应该调用哪一个虚函数。典型情况下,这一信息具有一种被称为 vptr(virtual table pointer,虚函数表指针)的指针的形式。vptr 指向一个被称为 vtbl(virtual table,虚函数表)的函数指针数组,每一个包含虚函数的类都关联到 vtbl。当一个对象调用了虚函数,实际的被调用函数通过下面的步骤确定:找到对象的 vptr 指向的 vtbl,然后在 vtbl 中寻找合适的函数指针。
点击链接跳转

构造函数、析构函数能不能是虚函数?

构造函数不能声明为虚函数的原因是:

  • 构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的。而在构造一个对象时,由于对象还未构造成功。编译器无法知道对象 的实际类型,是该类本身,还是该类的一个派生类,或是更深层次的派生类。无法确定。
  • 虚函数的执行依赖于虚函数表。而虚函数表在构造函数中进行初始化工作,即初始化vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初 始化,将无法进行。
    虚函数的意思就是开启动态绑定,程序会根据对象的动态类型来选择要调用的方法。然而在构造函数运行的时候,这个对象的动态类型还不完整,没有办法确定它到底是什么类型,故构造函数不能动态绑定。(动态绑定是根据对象的动态类型而不是函数名,在调用构造函数之前,这个对象根本就不存在,它怎么动态绑定?)
    编译器在调用基类的构造函数的时候并不知道你要构造的是一个基类的对象还是一个派生类的对象。
  • 析构函数设为虚函数的作用:在类的继承中,如果有基类指针指向派生类,那么用基类指针delete时,如果不定义成虚函数,派生类中派生的那部分无法析构。

虚函数可不可以是内联函数

内联函数不能为虚函数,原因在于虚表机制需要一个真正的函数地址,而内联函数展开以后,就不是一个函数,而是一段简单的代码(多数C++对象模型使用虚表实现多态,对此标准提供支持),可能有些内联函数会无法内联展开,而编译成为函数。

vector怎么释放内存

采用 Vector存储一些数据,但是发现在执行 clear() 之后内存并没有释放。
方法:vector 的 clear 不影响 capacity , 应该 swap 一个空的 vector。交换后临时变量注销,则vector的内存分配得到释放。

赋值运算符重载 注意点

点击链接跳转-一文说尽C++赋值运算符重载函数

什么样的对象才能作为STL容器的元素

元素需要具备构造、析构、赋值、拷贝等函数,关联容器还要求元素可以比较大小。
引申:指针作为容器的元素时,额外的内存管理问题直接以普通指针作为容器的元素时。我们知道,指针就是一个地址值,因此以指针为元素的容器存放的就是一些内存地址,而不是真正的数据。但是,容器只负责指针元素一级的内存问题,即它负责指针元素本身的内存分配和释放,而不会负责指针指向对象的内存管理事务,因为那是程序员的责任。
点击链接跳转

const 有什么用途

  • 定义只读变量,即常量
  • 修饰函数的参数和函数的返回值
  • 修饰函数的定义体,这里的函数为类的成员函数,被const修饰的成员函数代表不修改成员变量的值

指针和引用的区别

  • 引用是变量的一个别名,内部实现是只读指针
  • 引用只能在初始化时被赋值,其他时候值不能被改变,指针的值可以在任何时候被改变
  • 引用不能为NULL,指针可以为NULL
  • 引用变量内存单元保存的是被引用变量的地址
  • “sizeof 引用" = 指向变量的大小 , "sizeof 指针"= 指针本身的大小
  • 引用可以取地址操作,返回的是被引用变量本身所在的内存单元地址
  • 引用使用在源代码级相当于普通的变量一样使用,做函数参数时,内部传递的实际是变量地址

C++中有了malloc / free , 为什么还需要 new / delete

  • malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
  • 对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。 对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
  • 因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。

编写类String 的构造函数,析构函数,拷贝构造函数和赋值函数

    #include <iostream>  
    class String  
    {  
    public:  
        String(const char *str=NULL);//普通构造函数  
        String(const String &str);//拷贝构造函数  
        String & operator =(const String &str);//赋值函数  
        ~String();//析构函数  
    protected:  
    private:  
        char* m_data;//用于保存字符串  
    };  
    //普通构造函数  
    String::String(const char *str){  
        if (str==NULL){  
            m_data=new char[1]; //对空字符串自动申请存放结束标志''的空间  
        if (m_data==NULL){//内存是否申请成功  
            std::cout<<"申请内存失败!"<<std::endl;  
            exit(1);  
        }  
        m_data[0]='';  
        }  
        else{  
            int length=strlen(str);  
            m_data=new char[length+1];  
            if (m_data==NULL){//内存是否申请成功  
                std::cout<<"申请内存失败!"<<std::endl;  
                exit(1);  
            }  
            strcpy(m_data,str);  
        }  
    }  
    //拷贝构造函数  
    String::String(const String &str){ //输入参数为const型  
        int length=strlen(str.m_data);  
        m_data=new char[length+1];  
        if (m_data==NULL){//内存是否申请成功  
            std::cout<<"申请内存失败!"<<std::endl;  
            exit(1);  
        }  
        strcpy(m_data,str.m_data);  
    }  
    //赋值函数  
    String& String::operator =(const String &str){//输入参数为const型  
        if (this==&str) //检查自赋值  
            return *this;  
        int length=strlen(str.m_data);  
        delete [] m_data;//释放原来的内存资源  
        m_data= new char[length+1];  
        if (m_data==NULL){//内存是否申请成功  
            std::cout<<"申请内存失败!"<<std::endl;  
            exit(1);  
        }  
        strcpy(m_data,str.m_data);  
        return *this;//返回本对象的引用  
    }  
    //析构函数  
    String::~String(){  
        delete [] m_data;  
    }  
      
    void main(){  
        String a;  
        String b("abc");  
        system("pause");  
}  

多态的实现

多态性可以简单的概括为“1个接口,多种方法”,在程序运行的过程中才决定调用的机制程序实现上是这样,通过父类指针调用子类的函数,可以让父类指针有多种形态。
(多态是指相同的操作或函数、过程可作用于多种类型的对象上并获得不同的结果。不同的对象,收到同一消息可以产生不同的结果,这种现象称为多态。)
对于虚函数调用来说,每一个对象内部都有一个虚表指针,该虚表指针被初始化为本类的虚表。所以在程序中,不管你的对象类型如何转换,但该对象内部的虚表指针是固定的,所以呢,才能实现动态的对象函数调用,这就是C++多态性实现的原理。
总结(基类有虚函数):

  • 每一个类都有虚表。
  • 虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。
  • 派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。

堆和栈的区别

一个由C/C++编译的程序占用的内存分为以下几个部分

  • 栈区(stack) 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
  • 堆区(heap) 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
  • 全局区(静态区)(static) 全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域。C语言中未初始化的全局变量和未初始化的静态变量在相邻的另一块区域BSS。程序结束后有系统释放。
  • 文字常量区 常量字符串就是放在这里的。 程序结束后由系统释放
  • 程序代码区 存放函数体的二进制代码。

在c++程序中调用被C编译器编译后的函数,为什么要加extern“C”

C++语言支持函数重载,C语言不支持函数重载,函数被C++编译器编译后在库中的名字与C语言的不同,假设某个函数原型为:
void foo(int x, inty);
该函数被C编译器编译后在库中的名字为: _foo 而C++编译器则会产生像: _foo_int_int 之类的名字。为了解决此类名字匹配的问题,C++提供了C链接交换指定符号 extern "C"。

头文件种的ifndef/define/endif 是干什么用的

防止头文件被重复包含

c++怎样让返回对象的函数不调用拷贝构造函数

拷贝构造函数前加 “explicit” 关键字

C++中四种类型转换方式

类型转换有c风格的,当然还有c++风格的。c风格的转换的格式很简单(TYPE)EXPRESSION,但是c风格的类型转换有不少的缺点,有的时候用c风格的转换是不合适的,因为它可以在任意类型之间转换,比如你可以把一个指向const对象的指针转换成指向非const对象的指针,把一个指向基类对象的指针转换成指向一个派生类对象的指针,这两种转换之间的差别是巨大的,但是传统的c语言风格的类型转换没有区分这些。还有一个缺点就是,c风格的转换不容易查找,他由一个括号加上一个标识符组成,而这样的东西在c++程序里一大堆。所以c++为了克服这些缺点,引进了4新的类型转换操作符

  • static_cast 最常用的类型转换符,在正常状况下的类型转换,如把int转换为float,如
int i;float f; f=(float)i;
或者
f=static_cast<float>(i); 
  • const_cast 用于取出const属性,把const类型的指针变为非const类型的指针,如
const int *fun(int x,int y){}  
int *ptr=const_cast<int *>(fun(2.3))
  • dynamic_cast
    • 其他三种都是编译时完成的,dynamic_cast是运行时处理的,运行时要进行类型检查。
    • 不能用于内置的基本数据类型的强制转换。
    • dynamic_cast转换如果成功的话返回的是指向类的指针或引用,转换失败的话则会返回NULL。
    • 使用dynamic_cast进行转换的,基类中一定要有虚函数,否则编译不通过。需要检测有虚函数的原因:类中存在虚函数,就说明它有想要让基类指针或引用指向派生类对象的情况,此时转换才有意义。这是由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表关于虚函数表的概念,只有定义了虚函数的类才有虚函数表。
    • 在类的转换时,在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的。在进行下行转换 时,dynamic_cast具有类型检查的功能,比 static_cast更安全。向下转换,即将父类指针转化子类指针。向下转换的成功与否还与将要转换的类型有关,即要转换的指针指向的对象的实际类型与转换以后的对象类型一定要相同,否则转换失败。
  • reinterpret_cast interpret是解释的意思,reinterpret即为重新解释,此标识符的意思即为数据的二进制形式重新解释,但是不改变其值。如下转换方式,但很少使用。
int i; char *ptr="hello freind!"; 
i=reinterpret_cast<int>(ptr);

内联函数和宏定义的区别

使用宏和内联函数都可以节省在函数调用方面所带来的时间和空间开销。二者都采用了空间换时间的方式,在其调用处进行展开:

  • 在预编译时期,宏定义在调用处执行字符串的原样替换。在编译时期,内联函数在调用处展开,同时进行参数类型检查。
  • 内联函数首先是函数,可以像调用普通函数一样调用内联函数。而宏定义往往需要添加很多括号防止歧义,编写更加复杂。
  • 内联函数可以作为某个类的成员函数,这样可以使用类的保护成员和私有成员。而当一个表达式涉及到类保护成员或私有成员时,宏就不能实现了(无法将this指针放在合适位置)。
  • 使用内联函数的注意点:
    • 可以用内联函数完全替代宏。
    • 在编写内联函数时,函数体应该短小而简洁,不应该包含循环等较复杂结构,否则编译器不会将其当作内联函数看待,而是把它决议成为一个静态函数。
    • 有些编译器甚至会优化内联函数,通常为避免一些不必要拷贝和构造,提高工作效率。
    • 频繁的调用内联函数和宏定义容易造成代码膨胀,消耗更大的内存而造成过多的换页操作。

Volatile作用

volatile的本意是“易变的” 因为访问寄存器要比访问内存单元快的多,所以编译器一般都会作减少存取内存的优化,但有可能会读脏数据。当要求使用volatile声明变量值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。精确地说就是,遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问;如果不使用valatile,则编译器将对所声明的语句进行优化。(简洁的说就是:volatile关键词影响编译器编译的结果,用volatile声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化,以免出错)

explicit有什么用?

C++提供了关键字explicit,可以阻止不应该允许的经过转换构造函数进行的隐式转换的发生。声明为explicit的构造函数不能在隐式转换中使用。

C++如何限制类对象只能静态分配或者只能只能动态分配

动态分配就是用运算符new来创建一个类的对象,在堆上分配内存。
静态分配就是A a;这样来由编译器来创建一个对象,在栈 上分配内存。

  • 动态分配(在堆上分配内存) 将类的构造函数和析构函数设为protected属性,这样类对象不能够访问,但是派生类能够访问,能够正常的继承。同时创建另外两个create和destory函数类创建对象。(将create设为static原因是:创建对象的时候是A *p = A::create(); 只有静态成员函数才能够通过类名来访问。)
class A   {  
    protected:  
        A(){}  
        ~A(){}  
    public:  
        static A* create()  {  
            return new A();  
        }  
        void destory()  {  
            delete this;  
        }  
    };  
  • 静态分配(在栈上) 把new、delete运算符重载为private属性就可以了。
class A  {  
    private:  
        void* operator new(size_t t){}     // 注意函数的第一个参数和返回值都是固定的  
        void operator delete(void* ptr){} // 重载了new就需要重载delete  
    public:  
        A(){}  
        ~A(){}  
};  

内存溢出和内存泄漏

  • 内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
  • 内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。memory leak会最终会导致out of memory!
  • 内存溢出就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出。
  • 内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。一个盘子用尽各种方法只能装4个果子,你装了5个,结果掉倒地上不能吃了。这就是溢出!比方说栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。就是分配的内存不足以放下数据项序列,称为内存溢出.

能同时使用const和static修饰成员函数吗?

C++编译器在实现const的成员函数的时候为了确保该函数不能修改类的实例的状态,会在函数中添加一个隐式的参数const this*。但当一个成员为static的时候,该函数是没有this指针的。也就是说此时const的用法和static是冲突的。
我们也可以这样理解:两者的语意是矛盾的。static的作用是表示该函数只作用在类型的静态变量上,与类的实例没有关系;而const的作用是确保函数不能修改类的实例的状态,与类型的静态变量没有关系。因此不能同时用它们。

重载、重写、隐藏(总是不记得)的区别

  • Overload(重载):在C++程序中,可以将语义、功能相似的几个函数用同一个名字表示,但参数或返回值不同(包括类型、顺序不同),即函数重载。

    • 相同的范围(在同一个类中);
    • 函数名字相同;
    • 参数不同;
    • virtual 关键字可有可无。
  • Override(覆盖):是指派生类函数覆盖基类函数,特征是:
    • 不同的范围(分别位于派生类与基类);
    • 函数名字相同;
    • 参数相同;
    • 基类函数必须有virtual 关键字。
      注:重写基类虚函数的时候,会自动转换这个函数为virtual函数,不管有没有加virtual,因此重写的时候不加virtual也是可以的,不过为了易读性,还是加上比较好。
  • Overwrite(重写):隐藏,是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
    • 如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
    • 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

虚函数工作原理

点击链接跳转-C++中虚函数工作原理

智能指针

简单的说智能指针就是将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。
智能指针(smart pointer)是存储指向动态分配(堆)对象指针的类,用于生存期控制,能够确保自动正确的销毁动态分配的对象,防止内存泄露。它的一种通用实现技术是使用引用计数(reference count)。智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。

switch的参数为什么不能为实型

因为switch后面只能带自动转换为整形(包括整形)的类型,比如字符型char,unsigned int等,实数型不能自动转换为整形。

多重继承的缺点是什么呢?

  • 多重继承的优点:对象可以调用多个基类中的接口。
  • 多重继承的缺点:如果派生类所继承的多个基类有相同的基类,而派生类对象需要调用这个祖先类的接口方法,就会容易出现二义性。对于这个问题通常有两个解决方案:
    • 加上全局符确定调用哪一份拷贝。
    • 使用虚拟继承。

结构体sizeof()字节对齐问题

  • 结构体变量的首地址能够被其最宽基本类型成员的大小所整除。
  • 结构体每个成员相对于结构体首地址的偏移量都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节。
  • 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。

以上是关于面试题的主要内容,如果未能解决你的问题,请参考以下文章

Java进阶之光!2021必看-Java高级面试题总结

经验总结:Java高级工程师面试题-字节跳动,成功跳槽阿里!

一道经典面试题:字符串在Java中如何通过“引用”传递

Mybatis最全的高质量面试题和答案—3

2021-12-24:划分字母区间。 字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。 力扣763。某大厂面试

前端面试题-