C/C++常见面试题目

C/C++常见面试题目

与编译过程相关的问题

  • 为什么在C++里面,一个类的成员函数不能既是 template 又是 virtual 的。

因为C++的编译与链接模型是"分离"的。一个C/C++程序就可以被分开编译,然后用一个linker链接起来。这种模型有一个问题,就是各个编译单元可能对另一个编译单元一无所知。 一个 function template最后到底会被 instantiate 为多少个函数,要等整个程序(所有的编译单元)全部被编译完成才知道。 同时,virtual function的实现大多利用了一个"虚函数表"的东西,这种实现中,一个类的内存布局(或者说虚函数表的内存布局)需要在这个类编译完成的时候就被完全确定

  • C/C++编译过程

编译过程主要分4个过程:编译预处理;编译、优化阶段、汇编阶段、链接程序。

具体的细节详见https://blog.csdn.net/hycxag/article/details/82967579

  • 为什么头文件里一般只可以有声明不能有定义

头文件可以被多个编译单元包含,如果头文件里面有定义的话,那么每个包含这头文件的编译单元都会对同一个符号进行定义,如果该符号为外部链接,则会导致duplicated external symbols链接错误。

  • 为什么公共使用的内联函数要定义于头文件里

因为编译时编译单元之间互不知道,如果内联被定义于.cpp文件中,编译其他使用该函数的编译单元的时候没有办法找到函数的定义,因些无法对函数进行展开(内联函数不展开,即不采用在使用处标记函数代码再跳转的方式,而是直接将代码嵌入)。所以如果内联函数定义于.cpp里,那么就只有这个.cpp文件能使用它。故.h中的inline 函数可以被多个cpp包含而不造成符号冲突,因为它会被直接嵌入到调用的地方,内部联结不形成外部符号,对外不可见。

  • 为什么函数默认是外部链接

如果函数默认是内部链接,那么大家会倾向于把函数连同其定义都放入头文件中。然而,函数是多变的,可能会经常修改,这样一来,所以包含它的模块都需要被重新编译,很麻烦。另外一方面,如果函数中定义了静态变量,这样每一个包含该函数的模块都会有一个静态变量(因为假设是默认内部链接),导致不一致。 

  • 为什么const常量默认是内部链接而变量(全局)默认是外部链接?

因为它是常量,初始化后就不能改变,这样即使每一个包含它的模块都有一份它的复制,那也不会导致不一致。如果变量默认是内部链接,它是可变的量,所以在每个包含它的模块中,它的值可能会被改变,从而导致不一致的状况出现。

  • 为什么类的静态数据成员不可以就地初始化?

因为类体一般是放在头文件中的,如果允许其静态成员就地初始化,那就相当于允许在头文件中定义变量了。

STL中相关的问题

  • STL at()和重载的operator()有什么关系

array、deque、vector不能通过operator向容器中添加元素;而map、unordered_map类可以通过operator[]向容器中添加元素。所有容器均不能通过at()函数向容器重添加元素

at()函数在被调用时,会检查下标的有效性(与容器的size()比较而不是capacity()),若下标有效则返回对应位置的元素,否则抛出std::out_of_range异常。而operator函数在被调用时,不检查下标的有效性。

  • STL中的内存管理allocator机制

会采用两种分配的机制。大对象(>128字节)直接通过malloc向系统的堆空间分配;小对象通过预先分配好的内存池中取出。这样做的好处:小对象快速分配;避免内存碎片产生,减缓了OS的内存管理压力;尽可能最大化利用内存(内存池尚有的空闲区域不足以分配所需的大小时,分配算法会将其链入到对应的空闲列表中,然后会尝试从空闲列表中寻找是否有合适大小的区域)

具体的详细细节见https://blog.csdn.net/hycxag/article/details/82977029

网络编程相关的问题

  • 服务器端不调用accept会发生什么

不调用accept时,也能建立连接,即三次握手完成。但不能进行API的控制,即不能进行继续通讯。以及建立好连接的队列大大小为:backlog。从而在Unix系统服务器中,若客户端调用 connect() ,客户端连接超时失败。而在Linux系统中,若客户端调用 connect()。TCP 的连接队列满后,Linux 服务器不会拒绝连接,只是有些会延时连接,有些立刻连接。

详情参考https://blog.csdn.net/hycxag/article/details/82974484

C/C++中的基本问题

  • C++为什么要有class

类是C++用来实现OOP封装、继承和多态的核心机制。C++用虚函数实现多态,用RAII(和析构,异常机制)实现自动资源管理,用拷贝和移动定义资源的复制和转移,进而用隐式成员(Rule of 5,析构,拷贝构造,拷贝赋值,移动构造,移动赋值)来帮助用户省去手写冗余代码,最终达到不多写一个字的资源管理。如果说面向对象的概念已经有些过时了,资源管理却是永不过时的,也是C++从机制上不同于C的最主要一点。

  • C++多态实现及其原理

在C ++程序设计中,多态性是指具有不同功能的函数可以用同一个函数名,这样就可以用一个函数名调用不同内容的函数。一般来说多态分为两种:静态多态和运行时多态。

静态多态包含参数多态,过载多态和强制多态,参数多态:采用参数化模板,通过给出不同的类型参数,使得一个结构有多种类型;过载多态:同一个名字在不同的上下文中所代表的含义不同。典型的例子是运算符重载和函数重载;强制多态:编译程序通过语义操作,把操作对象的类型强行加以变换,以符合函数或操作符的要求。

运行时多态主要是包含多态:包含多态的基础是虚函数。主要是通过类的继承和虚函数来实现,当基类和子类拥有同名同参同返回的方法,且该方法声明为虚方法,当基类对象,指针,引用指向的是派生类的对象的时候,基类对象,指针,引用在调用基类的方法,实际上调用的是派生类方法。

详情参考https://blog.csdn.net/hycxag/article/details/82978173

  • 父类的构造方法中调用虚函数,会发生多态吗

父类的构造方法中调用虚函数,不会发生多态。这个和 vptr 的分步初始化有关。在父类中调用虚函数时,执行的还是父类的函数,没有发生多态。这是因为当创建子类对象时,编译器的执行顺序其实是这样的:

  1. 对象在创建时,由编译器对 vptr 进行初始化
  2. 子类的构造会先调用父类的构造函数,这个时候 vptr 会先指向父类的虚函数表
  3. 子类构造的时候,vptr 会再指向子类的虚函数表
  4. 对象的创建完成后,vptr 最终的指向才确定
  • C++的虚析构函数

通过基类的指针来删除派生类的对象时,若析构函数不是虚析构函数,只会调用基类的析构函数,而派生类的析构函数不会被调用,从而造成内存泄漏。

  • 如果父类的析构函数不加virtual关键字 :当父类的析构函数不声明成虚析构函数的时候,当子类继承父类,父类的指针指向子类时,delete掉父类的指针,只调动父类的析构函数,而不调动子类的析构函数。
  • 如果父类的析构函数加virtual关键字 :当父类的析构函数声明成虚析构函数的时候,当子类继承父类,父类的指针指向子类时,delete掉父类的指针,先调动子类的析构函数,再调动父类的析构函数。

而在虚函数表中,存放了父类的虚析构函数。故调用父类的析构函数时,此虚析构函数中:先调用子类的析构函数的,再调用父类的析构函数。

  • C++中的纯虚函数与抽象类

纯虚函数声明如下:virtual void function()=0;纯虚函数一定没有定义,用来规范派生类的行为,即接口。包含纯虚函数的类是抽象类,抽象类不能定义实例,但是可以声明指向该抽象类的具体类的指针或者引用;

如果是一个纯虚函数,那么,在虚函数表中,其函数指针的值就是0;即在虚函数表当中,如果是纯虚函数,那么就实实在在的写上0

  • 不能声明为虚函数的函数

  • 普通函数(非成员函数):只能被重载,不能被覆盖;声明虚函数也是可以的,但带来运行效率的降低。故编译器不会将此函数声明为虚函数,而是编译器在编译时绑定此函数。
  • 构造函数:因为构造函数本来就是为了明确初始化对象成员才产生的,然而虚函数主要是为了再不完全了解细节的情况下也能正确处理对象。另外,virtual函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用virtual函数来完成你想完成的动作。
  • 内联函数:内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。而且,inline函数在编译时被展开,虚函数在运行时才能动态的邦定函数。
  • 静态成员函数:每个类来说只有一份代码,所有的对象都共享这一份代码,他也没有要动态邦定的必要性。
  • 友元函数:友元函数并不是成员函数,故不讨论是否为虚函数;但是可以通过让友元函数调用虚成员函数来解决友元动态绑定的问题
  • 不能被继承的函数

  • 构造函数(拷贝构造函数):在创建子类对象时,为了初始化从父类继承来的数据成员,系统需要调用其父类的构造方法。 。如果没有显式的构造函数,编译器会给一个默认的构造函数,并且该默认的构造函数仅仅在没有显式地声明构造函数情况下创建。
  • 析构函数:只是在子类的析构函数中会自动调用父类的析构函数。
  • 赋值运算符重载函数:子类的赋值运算符重载函数中会调用父类的赋值运算符重载函数。
  • 构造函数和析构函数中不应该调用虚函数

构造派生类对象时,首先调用基类构造函数初始化对象的基类部分。在执行基类构造函数时,对象的派生类部分是未初始化的。实际上,此时的对象还不是一个派生类对象。

析构派生类对象时,首先撤销/析构他的派生类部分,然后按照与构造顺序的逆序撤销他的基类部分。

因此,在运行构造函数或者析构函数时,对象都是不完整的。为了适应这种不完整,编译器将对象的类型视为在调用构造/析构函数时发生了变换,即:视对象的类型为当前构造函数/析构函数所在的类的类类型。由此造成的结果是:在基类构造函数或者析构函数中,会将派生类对象当做基类类型对象对待。

而这样一个结果,会对构造函数、析构函数调用期间调用的虚函数类型的动态绑定对象产生影响,最终的结果是:如果在构造函数或者析构函数中调用虚函数,运行的都将是为构造函数或者析构函数自身类类型定义的虚函数版本。 无论有构造函数、析构函数直接还是间接调用虚函数

对象的虚函数表地址在对象的构造和析构过程中会随着部分类的构造和析构而发生变化,这一点应该是编译器实现相关的。

  • new与malloc,以及delete与free的区别

  • 空结构体的大小

C++语言中的确规定了空结构体和空类所占内存大小为1,而C语言中空类和空结构体占用的大小是0。由于C++语言标准规定了任何不同的对象不能拥有相同的内存地址。如果空类对象大小为0,那么此类数组中的各个对象的地址将会一致,明显违反了此原则。为了满足C++标准规定的不同对象不能有相同地址,最简单方法就是:C++编译器保证任何类型对象大小不能为0。故C++编译器会在空类或空结构体中增加一个虚设的字节(有的编译器可能不止一个),以确保不同的对象都具有不同的地址。

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章