2018秋招C/C++面试题总结

博主从8月中旬开始大大小小面试了十几家公司,至今也许是告一段落吧,但愿后面会有好结果,所以总结记录一些C/C++方向常见的问题。和你们一块儿学习!
参考了互联网的各类资源,本身尝试归类整理,谢谢~程序员

1、C和C++的区别是什么?

C是面向过程的语言,C++是在C语言的基础上开发的一种面向对象编程语言,应用普遍。
C中函数不能进行重载,C++函数能够重载
C++在C的基础上增添类,C是一个结构化语言,它的重点在于算法和数据结构。C程序的设计首要考虑的是如何经过一个过程,对输入(或环境条件)进行运算处理获得输出(或实现过程(事务)控制),而对于C++,首要考虑的是如何构造一个对象模型,让这个模型可以契合与之对应的问题域,这样就能够经过获取对象的状态信息获得输出或实现过程(事务)控制。web

C++中struct和class除了默认访问权限外,别的功能几乎都相同。面试

2、关键字static、const、extern做用

static和const的做用在描述时主要从类内和类外两个方面去讲:算法

static关键字的做用:

(1)函数体内static变量的做用范围为该函数体,该变量的内存只被分配一次,所以其值在下次调用时仍维持上次的值;
(2)在模块内的static全局变量和函数能够被模块内的函数访问,但不能被模块外其它函数访问;
(3)在类中的static成员变量属于整个类所拥有,对类的全部对象只有一份拷贝;
(4)在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,于是只能访问类的static成员变量。express

const关键字的做用:

(1)阻止一个变量被改变
(2)声明常量指针和指针常量
(3)const修饰形参,代表它是一个输入参数,在函数内部不能改变其值
(4)对于类的成员函数,若指定其为const类型,则代表其是一个常函数,不能修改类的成员变量(const成员通常在成员初始化列表处初始化)
(5)对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为”左值”。编程

extern关键字的做用:

(1)extern能够置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其余模块中寻找其定义。
(2)extern "C"的做用是让 C++ 编译器将extern "C"声明的代码看成 C 语言代码处理,能够避免 C++ 因符号修饰致使代码不能和C语言库中的符号进行连接。数组

3、sizeof和strlen的区别

(1)sizeof是运算符,而strlen是函数;
(2)sizeof的用法是sizeof(参数),这个参数能够是数组,指针,类型,对象,甚至是函数,其值在编译的时候就计算好了,而strlen的参数必须是字符型指针(char*),其值必须在函数运行的时候才能计算出来;
(3) sizeof的功能是得到保证能容纳实现的创建的最大对象的字节的大小,而strlen的功能是返回字符串的长度,切记这里的字符串的长度是包括结束符的;
(4)当数组做为参数传递给函数的时候,传的是指针,而不是数组,传递数组的首地址;安全

char str[20] = "0123456789";
int a = strlen(str);  //10
int b = sizeof(str);//20

4、指针和引用的区别

(1)指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。
(2)指针能够有多级,可是引用只能是一级(int **p;合法 而 int &&a是不合法的)
(3)指针的值能够为空,可是引用的值不能为NULL,而且引用在定义的时候必须初始化
(4)指针的值在初始化后能够改变,即指向其它的存储单元,而引用初始化后就不会再改变。
(5)"sizeof引用"获得的是所指向的变量(对象)的大小,而"sizeof指针"获得的是指针自己的大小。
(6)做为参数传递时,两者有本质不一样:指针传参本质是值传递,被调函数的形参做为局部变量在栈中开辟内存以存放由主调函数放进来的实参值,从而造成实参的一个副本。而引用传递时,被调函数对形参的任何操做都会经过一个间接寻址的方式影响主调函数中的实参变量。
若是想经过指针参数传递来改变主调函数中的相关变量,可使用指针的指针或者指针引用。数据结构

五 、指针数组、数组指针、函数指针

指针数组:首先它是一个数组,数组的元素都是指针,数组占多少个字节由数组自己的大小决定,每个元素都是一个指针,在32 位系统下任何类型的指针永远是占4 个字节。它是“储存指针的数组”的简称。
数组指针:首先它是一个指针,它指向一个数组。在32 位系统下任何类型的指针永远是占4 个字节,至于它指向的数组占多少字节,不知道,具体要看数组大小。它是“指向数组的指针”的简称。
一个小栗子:编程语言

int arr[] ={1,2,3,4,5};
    int *ptr =(int *)(&arr+1);  //2 5
    int *ptr =(int *)(arr+1);   //2 1
    cout<<*(arr+1)<<" "<<*(ptr-1)<<endl;
//数组名arr能够做为数组的首地址,而&a是数组的指针。
//arr和&arr指向的是同一块地址,但他们+1后的效果不一样,arr+1是一个元素的内存大小(增长4)
//而&arr+1增长的是整个数组的内存

数组指针(行指针)

int a[2][3] = {{1,2,3},{4,5,6}};
    int (*p)[3];
    p = a;
    p++;
    cout<<**p<<endl;  //4 the second rank

6、C++内存布局

C/C++程序编译时内存分为5大存储区

(1)栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量值等,其操做方法相似数据结构中的栈。
(2)堆区(heap):通常由程序员分配释放,与数据结构中的堆毫无关系,分配方式相似于链表。
(3)全局/静态区(static):全局变量和静态变量的存储是放在一块儿的,在程序编译时分配。
(4)文字常量区:存放常量字符串。
(5)程序代码区:存放函数体(类的成员函数、全局函数)的二进制代码

int a=0; //全局初始化区
char *p1; //全局未初始化区
void main()
{
	int b; //栈
	char s[]="bb"; //栈
	char *p2; //栈
	char *p3="123"; //其中,“123\0”常量区,p3在栈区
	static int c=0; //全局区
	p1=(char*)malloc(10); //10个字节区域在堆区
	strcpy(p1,"123"); //"123\0"在常量区,编译器 可能 会优化为和p3的指向同一块区域
C/C++内存分配有三种方式:

(1)从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
(2)在栈上建立。在执行函数时,函数内局部变量的存储单元均可以在栈上建立,函数执行结束时这些存储单元自动被释放。
栈内存分配运算内置于处理器的指令集中,效率很高,可是分配的内存容量有限。
(3)从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员本身负责在什么时候用free或delete释放内存。
动态内存的生存期由程序员决定,使用很是灵活,但若是在堆上分配了空间,就有责任回收它,不然运行的程序会出现内存泄漏。
另外频繁地分配和释放不一样大小的堆空间将会产生堆内碎块。

7、堆和栈的区别

(1)申请方式
stack:
由系统自动分配。例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间
heap:
须要程序员本身申请,并指明大小,在c中malloc函数

如p1 = (char *)malloc(10); 
在C++中用new运算符 
如p2 = (char *)malloc(10); 
可是注意p一、p2自己是在栈中的。

(2)申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,不然将报异常提示栈溢出。
堆: 首先应该知道操做系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,而后将该结点从空闲 结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,因为找到的堆结点的大小不必定正好等于申请的大小,系统会自动的将多余的那部分从新放入空闲链表中。
(3)申请大小的限制及生长方向
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也多是1M,它是一个编译时就肯定的常数),若是申请的空间超过栈的剩余空间时,将提示overflow。所以,能从栈得到的空间较小 。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是因为系统是用链表来存储的空闲内存地址的,天然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。因而可知,堆得到的空间比较灵活,也比较大。
(4)申请效率的比较:
栈由系统自动分配,速度较快。但程序员是没法控制的。
堆是由new分配的内存,通常速度比较慢,并且容易产生内存碎片,不过用起来最方便.
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。可是速度快,也最灵活。
(5)堆和栈中的存储内容
栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,而后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,而后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,而后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:通常是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。

8、malloc/free 、new/delete区别

(1)malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们均可用于申请动态内存和释放内存。
(2)对于非内部数据类型的对象而言,光用maloc/free没法知足动态对象的要求。对象在建立的同时要自动执行构造函数,对象在消亡以前要自动执行析构函数。
     因为malloc/free是库函数而不是运算符,不在编译器控制权限以内,不可以把执行构造函数和析构函数的任务强加于malloc/free。所以C++语言须要一个能完成动态内存分配和初始化工做的运算符new,以一个能完成清理与释放内存工做的运算符delete。注意new/delete不是库函数。
(3)C++程序常常要调用C函数,而C程序只能用malloc/free管理动态内存。
(4)new能够认为是malloc加构造函数的执行。new出来的指针是直接带类型信息的。而malloc返回的都是void指针。

九 、常见的内存错误及对策

(1)内存还没有分配成功,却使用了它;

解决办法:在使用内存以前检查指针是否为NULL。若是指针p是函数的参数,那么在函数的入口使用assert(p != NULL) 进行检查,若是是用malloc或者new来申请的,应该用
if (p == NULL)或者 if (p != NULL)来进行防错处理。
(2)内存分配虽然成功,可是还没有初始化就引用它;
错误缘由:一是没有初始化的观念,二是误觉得内存的缺省初值全为零,致使引用初值错误(如数组)。
解决办法:内存的缺省初值是什么并无统一的标准,尽管有些时候为零值,可是宁肯信其有,不可信其无,不管以何种方式建立数组,都要赋初值。

(3)内存分配成功并初始化,可是超过了内存的边界;
这种问题常出如今数组越界,写程序是要仔细。
(4)忘记释放内存,形成内存泄露;
含有这种错误的函数每次被调用都会丢失一块内存,开始时内存充足,看不到错误,但终有一次程序死掉,报告内存耗尽。
(5)释放了内存却继续使用它
产生缘由:1.程序中的对象调用关系过于复杂,难以搞清楚某个对象到底是否已经释放了内存,此时应该从新设计数据结构,从根本上解决对象管理的混乱局面。
2.函数return语句写错了,注意不要返回指向“栈内存”的指针或者引用,由于该内存在函数体结束时理论上被自动销毁。
3.使用free或者delete释放了内存后,没有将指针设置为null,致使产生野指针。
解决办法:当心仔细。

内存管理须要遵循的规则

(1)用malloc 或者 new 申请内存以后,应该当即检查指针值是否为 NULL ,防止使用指针值为NULL的内存;
(2)不要忘记数组和动态内存赋初值,防止未被初始化的内存做为右值使用;
(3)避免数组或者指针下标越界,特别要小心“多1”或者“少1”的操做;
(4)动态内存的申请与释放必须配对,防止内存泄露;
(5)用free或者delete释放了内存以后,当即将指针设置为NULL,防止产生“野指针”;

10、字节对齐问题

为何要使用字节对齐?

字节对齐是C/C++编译器的一种技术手段,主要是在可接受空间浪费的前提下,尽量地提升对相同元素过程的快速处理。(好比32位系统,4字节对齐能使CPU访问速度提升)
须要字节对齐的根本缘由在于CPU访问数据的效率问题。

字节对齐的原则

(1)结构体中每一个成员相对于结构体首地址的偏移量都是成员大小的整数倍,若有须要编译器会填充字节
(2)结构体的总大小为结构体最宽基本类型成员大小的整数倍,若有须要,编译器会填充字节。
固然这里还要考虑#pragma pack(n)伪指令的影响,若是有取较小值。

// 用于测试的结构体
typedef struct MemAlign
{
	char a[18];	// 18 bytes
	double b;	// 08 bytes 
	char c;		// 01 bytes
	int d;		// 04 bytes
	short e;	// 02 bytes
}MemAlign;

//大小为:48字节

对于union:sizeof(union),以结构里面size最大元素为union的size,由于在某一时刻,union只有一个成员真正存储于该地址。

11、0的比较判断,浮点数存储

1. int型变量
if ( n == 0 )
if ( n != 0 )
2. bool型
 if (value == 0)
if (value != 0)
3. char*if(p == NULL) / if(p != NULL)
5. 浮点型
const float EPSINON = 0.0000001;
if ((x >= - EPSINON) && (x <= EPSINON)

12、内联函数有什么优势?内联函数和宏定义的区别

优势:函数会在它所调用的位置上展开。这么作能够消除函数调用和返回所带来的开销(寄存器存储和恢复),并且,因为编译器会把调用函数的代码和函数自己放在一块儿优化,因此也有进一步优化代码的可能。
内联函数使用的场合:对于简短的函数而且调用次数比较多的状况,适合使用内联函数。

内联函数和宏定义区别:

1)内联函数在编译时展开,而宏在预编译时展开
2)在编译的时候,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换。
3)内联函数能够进行诸如类型安全检查、语句是否正确等编译功能,宏不具备这样的功能。
4)宏不是函数,而inline是函数

十3、调用惯例及printf变参实现

函数在被调用时,函数的调用方和被调用方对于函数时如何调用的必须有一个明确的规定。只有双方同时遵循一样的规定,函数才可以被正确调用。这样的规定被称为:调用惯例。

函数的调用惯例包含两个方面:

1.函数参数的传递顺序和方式

函数的传递有不少种方式,最多见的是经过栈传递。函数的调用方将参数压入栈中,函数本身再从栈中将参数取出。对于有多个参数的函数,调用惯例要规定函数调用方将参数压栈的顺序,是从左往右压栈,仍是从右往左压栈。

2.栈的维护方式

在函数将参数压入栈中以后,函数体会被调用,此后须要将被压入的参数所有弹出,使得栈在函数调用先后保持一致。这个弹出的工做能够由函数调用方来完成,也能够由函数自己来完成。在不指定调用惯例的状况下,默认采用cdecl惯例。

在这里插入图片描述

十四 、覆盖、重载、隐藏的区别

(1)重载:重载翻译自overload,是指同一可访问区内被声明的几个具备不一样参数列表(参数的类型,个数,顺序不一样)的同名函数,根据参数列表肯定调用哪一个函数,重载不关心函数返回类型。
(2)重写:重写翻译自override,是指派生类中存在从新定义的函数。其函数名,参数列表,返回值类型,全部都必须同基类中被重写的函数一致,只有函数体不一样。

1.成员函数被重载的特征:

(1)相同的范围(在同一个类中);

(2)函数名字相同;

(3)参数不一样;

(4)virtual 关键字无关紧要。

2.覆盖是指派生类函数覆盖基类函数,特征是:

(1)不一样的范围(分别位于派生类与基类);

(2)函数名字相同;

(3)参数相同;

(4)基类函数必须有virtual 关键字。

3.“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则以下:

(1)若是派生类的函数与基类的函数同名,可是参数不一样。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。

(2)若是派生类的函数与基类的函数同名,而且参数也相同,可是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

十5、四种强制类型转换

static_cast( expression )
  1. 用于数值类型之间的转换,也能够用于指针之间的转换,编译时已经肯定好,效率高,但须要保证其安全性。
    a) 指针要先转换成void才能继续往下转换。
    b) 在基类和派生类之间进行转换(必须有继承关系的两个类)
  2. 子类对象能够转为基类对象(安全),基类对象不能转为子类对象(能够转换,但不安全,dynamic_cast能够实现安全的向下转换)。
  3. static_cast不能转换掉expression的const、volatile、或者__unaligned属性
dynamic_cast < type-id> ( expression )
  1. 只能用于对象的指针和引用之间的转换,须要虚函数。
  2. dynamic_cast会检查转换是否会返回一个被请求的有效的完整对象,不然返回NULL;
  3. Type-id必须是类的指针、类的引用或者void *,用于将基类的指针或引用安全地转换成派生类的指针或引用。
const_cast < type-id> ( expression )

这个转换类型操纵传递对象的const属性,或者是设置或者是移除。

reinterpret_cast < type-id> ( expression )

用在任意指针类型之间的转换;以及指针与足够大的整数类型之间的转换,从整数到指针,无视大小。

隐式类型转换
  1. 两种经常使用的实现隐式类类型转换的方式:
    a、使用单参数的构造函数或N个参数中有N-1个是默认参数的构造函数。
    b、使用operator目标类型() const
  2. 避免隐式转换:前面加explicit。