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++中的基本问题
-
struct和class关键字的区别
struct和class所定义的类型能包含成员函数,能继承,能实现多态
- 成员的默认访问权限不同:struct默认是public而class默认是private
- 默认的继承保护级别不同:struct默认是public继承而class默认是private继承
- class可用作定义模板参数的关键字,类似typename,而struct不行
- struct中存在构造函数或虚函数时,则不能采用{}进赋初值
-
C++为什么要有class
类是C++用来实现OOP封装、继承和多态的核心机制。C++用虚函数实现多态,用RAII(Resource Acquisition Is Initialization:资源获取就是初始化)(和析构,异常机制)实现自动资源管理,用拷贝和移动定义资源的复制和转移,进而用隐式成员(Rule of 5,析构,拷贝构造,拷贝赋值,移动构造,移动赋值)来帮助用户省去手写冗余代码,最终达到不多写一个字的资源管理。如果说面向对象的概念已经有些过时了,资源管理却是永不过时的,也是C++从机制上不同于C的最主要一点。
-
C++多态实现及其原理
在C ++程序设计中,多态性是指具有不同功能的函数可以用同一个函数名,这样就可以用一个函数名调用不同内容的函数。一般来说多态分为两种:静态多态和运行时多态。
静态多态包含参数多态,过载多态和强制多态,参数多态:采用参数化模板,通过给出不同的类型参数,使得一个结构有多种类型;过载多态:同一个名字在不同的上下文中所代表的含义不同。典型的例子是运算符重载和函数重载;强制多态:编译程序通过语义操作,把操作对象的类型强行加以变换,以符合函数或操作符的要求。
运行时多态主要是包含多态:包含多态的基础是虚函数。主要是通过类的继承和虚函数来实现,当基类和子类拥有同名同参同返回的方法,且该方法声明为虚方法,当基类对象,指针,引用指向的是派生类的对象的时候,基类对象,指针,引用在调用基类的方法,实际上调用的是派生类方法。
详情参考https://blog.csdn.net/hycxag/article/details/82978173
-
父类的构造方法中调用虚函数,会发生多态吗
父类的构造方法中调用虚函数,不会发生多态。这个和 vptr 的分步初始化有关。在父类中调用虚函数时,执行的还是父类的函数,没有发生多态。这是因为当创建子类对象时,编译器的执行顺序其实是这样的:
- 对象在创建时,由编译器对 vptr 进行初始化
- 子类的构造会先调用父类的构造函数,这个时候 vptr 会先指向父类的虚函数表
- 子类构造的时候,vptr 会再指向子类的虚函数表
- 对象的创建完成后,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++编译器会在空类或空结构体中增加一个虚设的字节(有的编译器可能不止一个),以确保不同的对象都具有不同的地址。
-
构造函数,复制构造函数,与赋值运算符的调用
C++中如果对象还没有被创建,无论是写成A(a)还是A = a的形式,构造函数一定被调用,不要被=迷惑。至于是复制构造函数还是普通构造函数,就要看传入的参数类型。而赋值操作符只作用于已经存在的对象,这是判断是调用构造函数还是赋值操作符的标准。
-
内存泄漏&指针悬挂&野指针
- 内存泄漏:内存泄漏时指动态申请的内存空间没有正常释放,但是也不能继续使用的情况。
char *ch1;
ch1 = new char('A');
char *ch2 = new char;
ch1 =ch2;
//程序执行后,指针ch1和ch2指向同一个地址单元,而原来的ch1所申请的存放字符A的空间就不可能再使用,产生了内存泄漏。
最常出现的情况是申请了动态内存后,没有正常的使用delete来释放,导致内存泄漏。
当基类指针指向子类对象即在子类中动态申请内存时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露
- 指针悬挂:指针指向一个已经释放的地址空间。
char *ch1, *ch2;
ch1 = new char;
ch2 =ch1;
*ch2 = 'B';
delete ch1;
程序执行到这里,指针ch2就是指向了一个已经释放的地址空间,形成指针悬挂。
- 野指针:指向被释放的或者访问受限内存的指针。产生的原因:
- 指针变量没有被初始化(如果值不定,可以初始化为NULL)
- 指针被free或者delete后,没有置为NULL, free和delete只是把指针所指向的内存给释放掉,并没有把指针本身干掉,此时指针指向的是“垃圾”内存。释放后的指针应该被置为NULL.
- 指针操作超越了变量的作用范围,比如返回指向栈内存的指针就是野指针。
避免野指针的方法:
- 将指针初始化为NULL。char * p = NULL。
- 用malloc分配内存需要注意(char * p = (char * )malloc(sizeof(char))):
检查是否分配成功(若分配成功,返回内存的首地址;分配不成功,返回NULL。可以通过if语句来判断);清空内存中的数据(malloc分配的空间里可能存在垃圾值,用memset或bzero 函数清空内存)void bzero(void *s, int n)——s是 需要置零的空间的起始地址; n是 要置零的数据字节个数,void memset(void *start, int value, int size)——如果要清空空间的首地址为p,value为值,size为字节数。
用已有合法的可访问的内存地址对指针初始化。 - 指针用完后释放内存,将指针赋NULL。
- 内存溢出:程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
-
C++重定义解决方法
C++由于头文件重复包含了所定义的变量或者常量,编译器就会报重复定义的错误。如果你碰见这样的问题可以考虑重下面几个方面去解决:
- 在出现重定义错误的头文件加上:注意如果FileName_H_这个名字已经被使用,将会出现未定义问题(这里不讨论),这是你保证FileName_H_唯一就可以。
#ifndef FileName_H_
#define FileName_H_
....(头文件内容)
#endif
- 在出现重定义错误的头文件加上这一句:#pragma once 就可以解决(VS建立的类都会默认添加这一行),方式2与1其实是一样的,二选一即可
采用方式1或方式2基本上可以解决95%以上的重复定义的问题。在开发过程中,经常会使用第三方的API,单独使用某一个API都正常,但是同时使用多个API的时候就会出现某些结构体重复定义的问题,此时可以按照下面几种方式处理:
- 将重复定义的struct、变量名、常量,提出到一个公共的.h文件中,然后将原文件中公共部分的struct、变量名、常量屏蔽或删除,同时在头文件中包含公共的.h文件。
- 如果三防库中,出现C风格、C++风格两种不同的struct定义方式,就不能按照3的方式解决了(方式3解决后编译正常,但是会出现链接问题,分析lib中的导出函数中参数与C风格参数差异)。此时只需要将C风格方式的struct修改为C++风格的struct,同时更新API头文件中对应使用C风格struct位置。
-
用常规的非递归方法遍历一个平衡二叉树,所需的时间复杂度和空间复杂度是?
遍历二叉树的算法中的基本操作是访问结点,则不论按哪一种次序进行遍历,对n个结点的二叉树,其时间复杂度均为O(n)。所需辅助空间为遍历过程中栈的最大容量,即树的深度,最坏情况下为n,则空间复杂度也为O(n)。
-
泛型的实现机制
- C++:C++泛型是编译时多态,当类型信息可得的时候,利用编译期多态能够获得最大的效率和灵活性。当具体的类型信息不可得,就必须诉诸运行期多态了,即虚函数支持的动态多态。对于C++泛型,每个实际类型都已被指明的泛型都会有独立的编码产生,也就是说
list<int>
和list<string>
生成的是不同的代码,编译程序会在此时确保类型安全性。由于知道对象确切的类型,所以编译器进行代码生成的时候就不用运用RTTI(Run-Time Type Identification),这使得泛型效率跟手动编码一样高。 显然这样的做法增加了代码空间,相比运行时多态,是以空间换时间。 - Java: 当编译器对带有泛型的 Java 代码进行编译时,它会去执行类型检查和类型推断,然后生成普通的不带泛型的字节码,这种字节码可以被一般的 Java 虚拟机接收并执行,这种技术被称为擦除(erasure)。 可见,编译器可以在对源程序(带有泛型的 Java 代码)进行编译时使用泛型类型信息保证类型安全,同时在生成的字节码当中,将这些类型信息清掉。 如在代码中定义的
List<object>
和List<String>
等类型,在编译后都会变成List。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。擦除原则:
- 所有参数化容器类都被擦除成非参数化的(raw type);如
List<E>、List<List<E>
>都被擦除成List; - 所有参数化数组都被擦除成非参数化的数组;如
List<E>[]
,被擦除成List[]
; - Raw type的容器类,被擦除成其自身,如List 被擦除成List;
- 原生类型(int,String还有wrapper类)都擦除成他们的自身;
- 参数类型E,被擦除成Object;
- 所有约束参数如
<? Extends E>、<X extends E>
都被擦除成E; - 如果有多个约束,擦除成第一个,如
<T extends Object & E>
,则擦除成Object;
进行类型擦除后,在调用时怎么知道其真实类型——编译器帮我们做了自动类型转换,编译器会尽可能的检查可能存在的类型安全问题,但任然无法避免在运行时刻出现类型转换异常的方法。
public void test() {
List<String> list= new ArrayList<String>();
List.add(123); //编译错误
}
C++泛型和Java泛型非常类似,但是有着本质不同:首先,Java 语言中的泛型不能接受基本类型作为类型参数――它只能接受引用类型。这意味着可以定义 List<Integer>
,但是不可以定义 List<int>
。 其次,在 C++ 模板中,编译器使用提供的类型参数来生成不同代码。而 Java 中的泛型,编译器仅仅对这些类型参数进行擦除和替换。类型 ArrayList<Integer>
和 ArrayList<String>
的对象共享相同的类,并且只存在一个 ArrayList 类。