C++面试题整理(持续更新中)
一. 内联函数和宏定义的区别
1.内联函数在运行时可调试,而宏定义不可以;
2.编译器会对内联函数的参数类型做安全检查或自动类型转换(同普通类型),而宏定义不会;
3.内联函数可以访问类的成员变量,而宏定义则不能;
4.在类中声明同时定义的成员函数,自动转化为内联函数;
5.在预编译时,宏定义在调用处执行字符串的原样替换。在编译期间,内联函数在调用处展开,同时进行参数检查;
6.两者都可节省函数调用是所带来的时间和空间开销,采用空间换时间的方式,在其调用处进行展开;
7.内联函数可作为某个类的成员函数,这样就可以使用类的保护成员和私有成员。当一个表达式涉及到类的保护成员和私有成员时,宏就不能实现了。
宏定义的缺点:会产生二义性(括号的使用等),不会检查参数是否合法,存在安全隐患,不能访问类的成员也不能成为类的成员函数
二. C++多态
1.多态分为静态多态(函数重载和泛型编程)和动态多态(虚函数);
2.静态多态和动态多态的实际区别就是函数地址是早绑定还是晚绑定。如果函数调用时在编译期间就可以确定函数的调用地址并产生代码,就是静态的,也就是说地址是早绑定的(函数重载和泛型编程)。而如果函数调用地址不能在编译期间确定,需要在运行时才能确定,这就属于晚绑定(通过虚函数实现);
3. C++的多态性用一句话概括就是:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时基类指针或引用将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数
每当创建一个包含有虚函数的类或从包含有虚函数的类派生一个类时,编译器就为这个类创建一个vtable,如上图所示。在这个表中,编译器放置了在这个类中或在它的基类中所有已声明为virtual的函数的地址。如果在这个派生类中没有对在基类中声明为virtual的函数进行重新定义,编译器就使用基类 的这个虚函数地址。然后编译器在这个类中放置vptr。当使用简单继承时,对于每个对象只有一个vptr。vptr必须被初始化为指向相应的vtable,这在构造函数中发生。
一旦vptr被初始化为指向相应的vtable,对象就"知道"它自己是什么类型。但只有当虚函数被调用时这种自我认知才有用。
没有虚函数类对象的大小正好是数据成员的大小,包含有一个或者多个虚函数的类对象编译器向里面插入了一个vptr指针(void *),指向一个存放函数地址的表就是我们上面说的VTABLE,这些都是编译器为我们做的我们完全可以不关心这些。所以有虚函数的类对象的大小是数据成员的大小加上一个vptr指针(void *)的大小。
总结vtable和vptr和类对象的关系:
每一个具有虚函数的类都有一个虚函数表vtable,里面按在类中声明的虚函数的顺序存放着虚函数的地址,这个虚函数表vtable是这个类的所有对象所共有的,也就是说无论用户声明了多少个类对象,但是这个vtable虚函数表只有一个。
在每个具有虚函数的类的对象里面都有一个vptr虚函数指针,这个指针指向vtable的首地址,每个类的对象都有这么一种指针。
以下扩展:
在定义一个派生类对象时,派生类中新增加的数据成员当然用派生类的构造函数初始化,但是对于从基类继承来的数据成员的初始化工作就必须由基类的构造函数完成,这就需要在派生类的构造函数中完成对基类构造函数的调用。同样,派生类的析构函数能完成派生类中新增加数据成员的扫尾、清理工作,而从基类继承来的数据成员的扫尾工作也应有基类的析构函数完成。由于析构函数不能带参数,因此派生类的析构函数默认直接调用了基类的析构函数。
定义派生类对象时构造函数的调用顺序(析构函数的调用顺序相反):
a. 先调用用基类的构造函数
b. 然后调用派生类对象成员所属的构造函数(如果有对象成员)
c. 最后调用派生类的构造函数
参考博客:https://www.cnblogs.com/cxq0017/p/6074247.html
四. 为什么析构函数要定义成虚函数
首先明确:1. 每个析构函数(不加virtual)只负责清除自己的的成员。2. 基类指针可指向派生类成员(这很正常)。
那么当析构一个指向派生类成员的基类指针时,程序就不知道该怎么办了,所以要保证运行适当的析构函数,基类的析构函数必须为虚函数。
五. 中struct和class有什么区别?C语言中的struct和C++中的struct一样吗?有什么区别?
C++中的struct和class区别:
1. 默认访问权限不同,struct是C中的升级版本,默认是public,class默认是private
2. class可以有默认的构造和析构函数,而struct没有
C中的struct和C++中struct区别:C中struct只是一个自定义的数据类型(结构体),struct是抽象的数据类型,支持一些类的操作和定义
六. 说说什么是野指针?野指针什么情况下出现?
野指针:指向一个已经删除的对象或者未申请的访问受限地址的指针。
出现情况:
1. 指针未初始化(指针定义不会自动初始化成空指针,而是随机的一个值,可能指向任意空间)
2. 指针所指变量释放后没有置为NULL
3. 指针所指变量已超过生存周期(如返回栈内存的指针)
七. C++四种类型转换机制
1. static_cast(静态类型转换)主要用在继承关系的对象类型或内置类型的转换(不用于指针)。如:A a;B b; a=static_cast<A>b;(强制转换)
2. dynamic_cast(动态类型转换)用于有继承关系的对象类型转换(指针或引用)或者有虚函数的对象。如:A a;B b;B*p=&b;A *pp=dynamic_cast<A*>p;
3. const_cast:用于将指针常量转换为普通的常量。如:const int * p="2"; int * pp= const_cast<int *> p;
4. reinterpret_cast:将一个类型的指针转换为另一个类型的指针。如:double * b=2.0;int *a=reinterpret_cast<double*>b;
八. const的作用
1. const定义的常量必须赋值初始化。不能另外的初始化
2. const修饰函数的输入参数:当传入的参数是用户自定义的类型,最好是用const引用修饰,可以提高效率。
3. const修饰函数的返回值
4. const修饰类的成员函数
九. 内存管理你懂多少?(包括内存泄漏,野指针知识,非法调用,内存溢出等)
1. 内存泄漏:
十. 深拷贝和浅拷贝
https://blog.csdn.net/weixin_41143631/article/details/81486817
十一. #include<file.h> 与 #include "file.h"的区别?
前者先从标准库中寻找和引用file.h,后者先从当前路径寻找和引用file.h
十二. 全局变量和局部变量有什么区别?是怎么实现的?操作系统和编译器是怎么知道的?
两者的主要区别是作用域和生命周期不同。全局变量有效范围是从变量定义的位置开始到本源文件结束,而局部变量只能在自己的作用域有效。全局变量生命周期和整个程序生命周期一样,而局部变量的生命周期和函数的生命周期一样。全局变量的内存分配是静态的,在main函数前初始化,如果没有初始化,会被初始化为0。局部变量的内存分配是动态的,位于线程堆栈中,如果没有初始化的,初值视当前内存的值而定。
操作系统和编译器从变量的定义和存储区域来区分局部变量和全局变量。
十三. 什么函数不能声明为虚函数
一个类中将所有成员函数都尽可能设置为虚函数总是有益的。
但设置虚函数须注意:
1. 只有类的成员函数才能声明为虚函数
2. 静态成员函数不能是虚函数
3. 内联函数不能是虚函数
4. 构造函数不能是虚函数
5. 析构函数可以是虚函数,而且通常声明为虚函数
十四. 面向对象
面向对象的三大特性:封装、继承、多态。
类和对象:类由数据成员和成员函数构成,代表抽象派,玩的就是概念,某种意义上来说是一种行为艺术;而对象是具体的,比如说过年回家和老爹下中国象棋,发现棋盘上少了一对‘象’,那是你爸在告诉你该找“对象”了(单身狗表示选择GoDie)。
封装:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的类进行信息隐藏。(C++最大的优点:可以隐藏代码的实现细节,使得代码更模块化)
继承:可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展,但是基类的构造函数、复制构造函数、析构函数、赋值运算符不能被派生类继承。(优点是实可以扩展已存在的代码模块类)
多态:一个类实例的相同方法在不同情形有不同表现形式。多态实现的两种方式:将子类对象的指针赋给父类类型的指针或将一个基类的引用指向它的派生类实例。(其中比较重要的是虚函数的使用以及指针或引用)
this指针:一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果。this作用域是在类的内部,当在类的非静态(前面没加Static)成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,各成员的访问均通过this指针进行。(静态成员是没有this指针的)
十五. 指针和数组定义常见笔试题
链接:https://blog.csdn.net/sinat_38972110/article/details/82372140
int *a[10]; 指向int类型的指针数组a[10] int (*a)[10]; 指向有10个int类型的数组的指针a int (*a)(int);函数指针,指向有一个参数并且返回类型均为int的函数 int* a(int); 定义一个int参数并且返回类型为int*的函数 int (*a[10])(int); 函数指针的数组,指向有一个参数并且返回类型均为int的函数的数组
十六. 简述sizeof和strlen的区别
最常考察的题目之一,主要区别如下:
1. sizeof是一个操作符,strlen是库函数。
2. sizeof的参数可以是数据的类型,也可以是变量,而strlen只能以结尾为'\0'的字符串作参数。
3. 编译器在编译时就计算出了sizeof的结果。而strlen函数只能在运行时才能计算出来。并且sizeof计算出来的是数据类型占内存的大小,而strlen计算的是字符串的实际长度。
4. 数组做sizeof的参数不退化,传递strlen就退化为指针了。
十七. vector的实现原理以及实现机制
vector简单来说就是一个动态增长的数组,里面有一个指针指向一片连续的内存空间,当空间装不下要容纳的数据时会自动申请一块更大的空间(空间适配器)将原来的数据拷贝到新的空间,然后释放旧的空间。当删除时空间不释放只清空里面的数据。
在vector动态增加大小时,并不是在原有的空间持续增加新的空间(无法保证原空间的后面还有可供配置的空间),而是以原来大小的两倍另外开辟一块较大的空间,然后将原来的内容拷贝过来,并释放原来的空间。因此,对vector的任何操作一旦引起空间的重新配置,指向原vector的所有迭代器都会失效,这是比较容易犯的错误。
十八. list和vector有什么区别
vector和数组类似,它拥有一段连续的内存空间,并且起始地址不变,因此非常好的支持随机存取(即使用[]操作符访问其中的元素),但在中间进行插入和删除会造成内存块的拷贝(复杂度是O(n)),另外,当数组的内存空间不够时,需要重新申请一块足够大的内存并进行内存的拷贝。这些都大大影响了vector的效率。
list是由数据机构中的双向链表实现的,因此它的内存空间可以是不连续的。因此只能通过指针来进行数据的访问,这个特点使得它的随机存取非常没有效率,需要遍历中间的元素,搜索复杂度O(n),因此它没有提供[]操作符的重载。但由于链表的特点,它在任意位置的删除和插入的效率都非常高。
十九. 指针和引用的区别
1. 指针是一个变量,指向一个地址,引用是一个原变量的别名
2. 有const指针,没有const应用
3. 指针可以有多级,而引用只能有一级
4. 指针可以为空,引用不能为空
5. 引用必须初始化,指针不必
6. 引用在定义时初始化一次,之后不可变,指针可以改变所指的对象
二十. string简单实现
String { public: String(const char* str = NULL); String(const String& other); ~String(); String& operator=(const String& rhs); private: char* _str; }; String::String(const char* str) { if (str == NULL) { _str = new char(1); _str = '\0'; } else { _str = new char(strlen(str) + 1); strcpy(_str, str); } } String::String(const String& other) { _str = new char(strlen(other._str) + 1); strcpy(_str, other._str); } String::~String() { delete[]_str; } String& String::operator=(const String& rhs) { if (this == &rhs) return *this; delete []_str; _str = new char(strlen(_str) + 1); strcpy(_str, rhs._str); }
二十一. TCP和UDP的区别
1. TCP面向连接;UDP是无连接的,即发送数据之前不需要建立连接
2. TCP提供可靠的服务。也就是说,通过 TCP连接传输的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
TCP通过校验和,重传控制,序号标识,滑动窗口,确认应答实现可靠传输。如丢包时重发控制,还可以对次序乱掉的分包进行顺序控制。
3. UDP具有较好的实时性,工作效率比TCP高,适用于对高速传输和实时性有较高的通信或广播通信
4. 每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
5. TCP对系统资源要求较多,UDP对系统资源要求较少
简单回答:TCP是面向连接的可靠传输,UDP是尽最大努力传输的不可靠传输
二十二. 智能指针总结
1.简述智能指针的原理
智能指针是一种资源管理类,这个类在构造函数中传入一个原始指针,在析构函数中释放传入的指针。智能指针都是栈上的对象,所以当函数(或者程序)结束时,会自动释放。
2.C++中常用的智能指针有哪些?
C++中常用的智能指针有,在C++11中的<memory>中有unique_ptr、shared_ptr、weak_ptr
unique_ptr:同一时刻只能由唯一的unique_ptr指向给定对象,不支持拷贝和赋值操作。
shared_ptr:可以有多个指针指向相同的对象,通过引用计数机制,支持拷贝和赋值操作。每使用一次,内部引用计数器加1,析构一次,引用计数减1,当计数为0时,释放所指的堆空间。
weak_ptr:弱引用。引用计数器有一个问题就是相互引用形成环,这样两个指针指向的内存都无法释放。需要手动打破循环引用或者使用weak_ptr。顾名思义,weak_ptr是一个弱引用,只引用不计数。如果一块内存被shared_ptr和weak_ptr同时引用,当所有的shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放,所以weak_ptr不保证它指向的内存一定有效,在使用之前需要检查weak_ptr是否为空指针。