C/C++
内容
1、面向对象和面向过程的区别?
面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。
面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。
优缺点:
面向过程:
优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;
缺点:没有面向对象易维护、易复用、易扩展
面向对象:
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护
缺点:性能比面向过程低。
1.1、面向对象的三大特性
封装性:数据和代码捆绑在一起,避免外界干扰和不确定性访问。
继承性:让某种类型对象获得另一个类型对象的属性和方法。
多态性:同一事物表现出不同事物的能力,即向不同对象发送同一消息,不同的对象在接收时会产生不同的行为(重载实现编译时多态,虚函数实现运行时多态)。
2、一个C++程序从编译到运行都经历了哪些阶段?
预处理:去注释、宏替换、包含头文件、处理预处理指令
编译:把代码转换成汇编语言、词法分析、语法分析、语义分析、优化处理
汇编:把汇编语言翻译成机器指令,生成目标文件
链接:把所有的目标文件进行链接,生成可执行文件
注:这些过程做了什么,底层如何做的?
3、C++类成员函数重载、覆盖、隐藏区别
- 重载:
重载就是一个相同的作用域内实现相同的操作,但是操作的对象不相同
特征:
(1)成员函数重载
(2)相同的范围(相同的作用域)
(3)函数名字相同
(4)参数不同(类型不同,个数不同,顺序不同)
##### 注意:返回值类型不同不算函数重载
覆盖:
覆盖就是面向对象的多态,指的是派生类函数覆盖基类函数。
特征:
(1)不同的范围(不同的作用域,分别是基类和派生类)
(2)参数相同
(3)函数名字必须相同
(4)函数原型相同
(5)基类函数必须有virtual关键字
隐藏:
隐藏就是派生类函数屏蔽了它的基类的同名函数
特征:
(1)不同的范围(不同的作用域,分别是基类和派生类)
(2)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(3)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)这时添加virtual将隐藏换成覆盖
4、const的作用(const成员函数,函数传递,与define区别)
资料如下:
http://www.cnblogs.com/dhls231/p/Const.html
http://www.cppblog.com/xczhang/archive/2008/01/13/41092.html
http://blog.csdn.net/Eric_Jo/article/details/4138548
4.1 宏定义和const区别,可以完全用const替换吗?
宏替换发生在编译阶段之前,直接进行替换替换;const作用发生于编译过程中。
宏不检查类型;const会检查数据类型。
宏定义的数据没有分配内存空间,只是插入替换掉;const定义的变量只是值不能改变,但要分配内存空间。
5、inline的作用
内联函数在调用函数的时候进行展开,省去了函数调用的开销,从而提高函数的执行效率。
如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。
另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
关键字inline 必须与函数定义体放在一起才能使函数成为内联,仅将inline 放在函数声明前面不起任何作用。
定义在类声明之中的成员函数将自动地成为内联函数
5.1 与宏定义的区别:
宏定义不是函数,但是使用起来像函数。预处理器用复制宏代码的方式代替函数的调用,省去了函数压栈退栈过程,提高了效率。
内联函数本质上是一个函数,内联函数一般用于函数体的代码比较简单的函数,不能包含复杂的控制语句,while、switch,并且内联函数本身不能直接调用自身。如果内联函数的函数体过大,编译器会自动的把这个内联函数变成普通函数。
宏定义是在预编译的时候把所有的宏名用宏体来替换,简单的说就是字符串替换
内联函数则是在编译的时候进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,这样可以省去函数的调用的开销,提高效率
宏定义是没有类型检查的,无论对还是错都是直接替换
内联函数在编译的时候会进行类型的检查,内联函数满足函数的性质,比如有返回值、参数列表等
宏定义和内联函数使用的时候都是进行代码展开。不同的是宏定义是在预编译的时候把所有的宏名替换,内联函数则是在编译阶段把所有调用内联函数的地方把内联函数插入。这样可以省去函数压栈退栈,提高了效率
补充资料:
http://blog.csdn.net/gengshenghong/article/details/5609212
6、static 关键字的作用,为什么它可以延长生存期?
从C和C++两方面来回答,他们都在静态全局区。C修饰变量和函数,C++中修饰成员变量和成员函数。
变量和函数:
(1)设置变量的存储域,在函数体内,static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此该值在下次调用时仍维持上次的值
(2)限制变量的作用域,在模块内的static变量可以被模块内所有函数访问,但不能被模块外其他函数访问
(3)限制函数的作用域,在模块的static函数只可被这以模块内的其他函数调用,这个函数的使用范围被限制在使用它的模块中
(4)未经初始化的静态全局变量、局部变量会被程序自动初始化为0,除非它被显示初始化。
(5)函数只能在声明它的文件当中可见,不能在其他的文件当中使用(和静态全局变量类似),同时其他文件可以定义与此函数一样名字的函数,并不会发生冲突。
类中:
(1)静态数据成员不属于某一个对象的,它属于所有这个类的对象,静态成员可以在类内互相访问,因为他们都属于本类,必须在类外初始化。
(2)可以被类访问,可以被对象访问,可以被对象指针访问
(3)静态成员函数没有this指针,因此静态成员函数与类的全局函数相比速度上会有少许的增长。
(4)静态成员函数不能访问非静态成员函数和非静态数据成员。非静态成员函数可以任意的访问静态数据成员和静态成员函数。
补充资料:
http://blog.csdn.net/archyli/article/details/78447336
7、explicit是干什么用的?
- explicit可以阻止不应该允许的经过转换构造函数进行的隐式转换的发生,声明为explicit的构造函数不能在隐式转换中使用。
8、Volatile关键字是什么
声明语法为int volatile vint
volatile的本意是“易变的” 因为访问寄存器要比访问内存单元快的多,所以编译器一般都会作减少存取内存的优化,但有可能会读脏数据。Volatile关键字和const对应,用来修饰变量,它是一种类型修饰符,表明变量是可变的。用它声明的类型变量表示可以被某些编辑器未知的因素修改,例如操作系统,硬件或者其他线程等。遇到这个关键字声明,编辑器对访问该变量的代码就不再进行一个优化,从而可以提供特殊地址的稳定访问,系统总是重新从它所在的内存读取数据,既使它前面的指令刚刚从该处读取过数据,而且读取的数据立刻被保存。
多线程中被几个任务共享的变量需要定义为volatile类型。
volatile的本质:
(1)编译器的优化
在本次线程内, 当读取一个变量时,为提高存取速度,编译器优化时有时会先把变量读取到一个寄存器中;以后,再取变量值时,就直接从寄存器中取值;当变量值在本线程里改变时,会同时把变量的新值copy到该寄存器中,以便保持一致。
当变量在因别的线程等而改变了值,该寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致。
当该寄存器在因别的线程等而改变了值,原变量的值不会改变,从而造成应用程序读取的值和实际的变量值不一致。
PS:volatile应该为“直接存取原始内存地址”比较合适,“易变的”这种解释感觉有点误导人。
9、指针和引用的区别和联系?
指针指向一块内存,它的内容是所指内存的地址;引用是某块内存的别名。
区别:
(1)引用必须初始化,指针可以不初始化
(2)指针可以为NULL,引用不能为NULL
(3)指针是一个实体,而引用仅是个别名;
(4)引用使用时无需解引用(*),指针需要解引用;
(5)引用只能在定义时被初始化一次,之后不可变;指针可变;
(6)引用没有 const,指针有 const;
(7) “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身(所指向的变量或对象的地址)的大小;
(8)指针和引用的自增(++)运算意义不一样;
(9)从内存分配上看:程序为指针变量分配内存区域,而引用不需要分配内存区域。
联系:C++中底层的引用在汇编层次是编译器通过指针来实现的,
补充资料:
http://www.cnblogs.com/hoodlum1980/archive/2012/06/19/2554270.html
http://blog.csdn.net/qq_27678917/article/details/70224813
10、malloc/free和new/delete区别和联系?
这一部分一定要仔细的研究底层上的差异,这个面试问的的非常多,一定要深挖下去了解,面试官会对你的印象分有很大的提高。
malloc/free是C++/C语言的标准库函数,new/delete是C++的运算符,他们都是可以用作申请动态内存和释放内存。
然而如果我们需要申请一个非内置类型的对象而言,此时光用malloc/free是无法满足动态对象的要求。因为对象在创建的同时要自动执行构造函数,在对象消亡前要自动执行析构函数。因为他们是库函数不是运算符,所以不再编译器控制的权限之内,所以不能够把执行构造函数和析构函数的任务强加于malloc和free。 new[]在给内置类型开辟空间时,开辟的空间不会多4个字节,而new[]在给自定义类型开辟空间时,而且还看是否有自定义析构函数,如果定义了析构函数才会多开辟四个字节,用来判断这里要调用多少次析构函数;如果没有自定义析构函数,就不会多开辟四个字节了。 在C程序中只能使用malloc和Free所以不能吧他们淘汰出局。
补充资料:
http://blog.csdn.net/zhanghefu/article/details/5003407
补充:能否malloc(1.2G),能否malloc(4G)?(malloc底层实现,了解小于128,大于128的分配方式,缺页中断等等知识)
http://blog.jobbole.com/110695/ http://blog.jobbole.com/109234/?utm_source=blog.jobbole.com&utm_medium=relatedPosts https://blog.csdn.net/gfgdsg/article/details/42709943
上面这篇文章对于malloc\free\brk()\mmap讲解挺到位的,如果不明白可以看第三个链接。
11、什么是浅拷贝、深拷贝、写时拷贝、引用计数,哪里都用到了
12、类的六个默认的成员函数、什么时候必须用初始化列表进行出初始化
六个默认的成员函数:
构造函数、析构函数、拷贝构造函数、赋值运算符重载、取地址运算符重载、const修饰的取地址操作符重载
必须在初始化列表中进行初始化:
(1)常量成员变量(常量创建的时候必须初始化,由于是常量,所以我们对他进行赋值是不对的)
(2)引用类型成员变量(引用必须初始化)
(3)没有缺省构造函数的类成员变量。(如果构造函数的参数列表中有一个类的对象,并且该对象的类里没有缺省参数的构造函数时,要是不使用初始化列表,参数中会调用无参或者全缺省的构造函数,而那个类中又没有。)
13、宏的缺点()
优点:
提高了程序的可读性,同时也方便进行修改;
提高程序的运行效率:使用带参的宏定义既可完成函数调用的功能,又能避免函数的出栈与入栈操作,减少系统开销,提高运行效率;
3.宏是由预处理器处理的,通过字符串操作可以完成很多编译器无法实现的功能。比如##连接符。
缺点
由于是直接嵌入的,所以代码可能相对多一点;
嵌套定义过多可能会影响程序的可读性,而且很容易出错;
对带参的宏而言,由于是直接替换,并不会检查参数是否合法,存在安全隐患
补充:如何进行调试宏?(这个问题腾讯面试官比较爱问,了解一下如何调试)
14、#ifndef和#pragama once两种防止头文件二次编译的区别
ifndef保证同一个文件和也可保证内容相同的两个文件不会同时被包含。
pragama once保证同一个文件,即物理上一个文件,不可被重复包含
pragama once编译效率更快
15、内存对齐?
(1)为什么要有内存对齐
如果是32位的机器,因为编译器希望四个字节四个字节的读取,如果内存没有对齐,则会进行两次读取。如果你的内存只占了四个字节中的前一个字节,读取的时候虽然你得到的是一个字节,但是机器实际上是读取了四个字节返还给你的。 同理如果数据假设是1、2、3、4、5、6、7、8,你存放的数据在3、4、5、6,此时就需要读取两次。第一次读取的是1到4中的内容,第二次读取的是5到8的内容。
(2)内存对齐的规则是什么
内存体对齐的时候分为结构体对齐和联合体对齐,第一个数据成员存放在偏移地址为0的地方,以后的每个数据存放在偏移值为对齐数的整数倍的地方,对齐数是变量大小和默认对齐数的较小值。 所以此时结构体的大小必须是最大对齐数的整数倍。
补充:这里多看一些sizeof一个类的各种情况的题。
16、多态是什么?
多态主要分为静态多态和动态多态来回答。
- 静态多态就是函数的重载
动态多态就就是我们常说的多态。
多态首先发生函数的重写,基类指针指向基类的派生类,这个时候如果访问到重写的函数,那么就会发生多态,此时把虚函数指针,虚函数表的概念说出来
虚函数表指针:
类中除了定义的函数成员,还有一个成员是虚函数表指针(占四个基本内存单元),这个指针指向一个虚函数表的起始位置,这个表会与类的定义同时出现,这个表存放着该类的虚函数指针,调用的时候可以找到该类的虚函数表指针,通过虚函数表指针找到虚函数表,通过虚函数表的偏移找到函数的入口地址,从而找到要使用的虚函数。 当实例化一个该类的子类对象的时候,(如果)该类的子类并没有定义虚函数,但是却从父类中继承了虚函数,所以在实例化该类子类对象的时候也会产生一个虚函数表,这个虚函数表是子类的虚函数表,但是记录的子类的虚函数地址却是与父类的是一样的。所以通过子类对象的虚函数表指针找到自己的虚函数表,在自己的虚函数表找到的要执行的函数指针也是父类的相应函数入口的地址。 如果我们在子类中定义了从父类继承来的虚函数,对于父类来说情况是不变的,对于子类来说它的虚函数表与之前的虚函数表是一样的,但是此时子类定义了自己的(从父类那继承来的)相应函数,所以它的虚函数表当中管于这个函数的指针就会覆盖掉原有的指向父类函数的指针的值,换句话说就是指向了自己定义的相应函数,这样如果用父类的指针,指向子类的对象,就会通过子类对象当中的虚函数表指针找到子类的虚函数表,从而通过子类的虚函数表找到子类的相应虚函数地址,而此时的地址已经是该函数自己定义的虚函数入口地址,而不是父类的相应虚函数入口地址,所以执行的将会是子类当中的虚函数。这就是多态的原理。
补充:基类的指针和引用是怎么转换的?
17、基类的析构函数是否可以设置为虚函数?
可以设置为虚函数
如果父类中定义了虚析构函数,那么父类的虚函数表当中就会有一个父类的虚析构函数的入口指针,指向的是父类的虚析构函数,子类虚函数表当中也会产生一个子类的虚析构函数的入口指针,指向的是子类的虚析构函数,这个时候使用父类的指针指向子类的对象,delete接父类指针,就会通过指向的子类的对象找到子类的虚函数表指针,从而找到虚函数表,再虚函数表中找到子类的虚析构函数,从而使得子类的析构函数得以执行,子类的析构函数执行之后系统会自动执行父类的虚析构函数。这个是虚析构函数的实现原理。
如果析构函数不是虚函数,那么在调用该函数时(对象被删除时)则只会调用当前对象对应的类的析构函数,这对于直接定义的对象是没有什么影响的,但是对于使用基类指向派生类的指针而言,因为基类指针实际上是基类类型,所以析构时自然只会调用基类的析构函数,这就可能产生内存泄漏(因为派生类的析构函数不被调用)。所以如果确定程序中有基类指针指向派生类的问题,则必须将基类的析构函数指定为虚函数,如此才能确保NEW出来的对象被正确的DELETE。
18、类的六个默认成员函数哪些不能被继承,哪些函数不能被声明为虚函数,构造函数可以声明为虚函数吗?
构造函数和析构函数是不能被继承的
不能被声明为虚函数的有:
普通函数(非成员函数)、静态成员函数、内联成员函数、构造函数、友元函数。
1、普通函数只能被重载,不能被重写,声明为虚函数也没有什么意义。
2、构造函数一般用来初始化对象的,但是多态是在对象生成之后才能发挥作用,如果我们把构造函数设置为虚函数,那么就会在对象没有生成之前来定义这个多态,这是矛盾的,同时又因为构造函数不能被继承,所以不能被声明为虚函数
3、静态成员函数也不能被声明为虚函数,因为静态成员函数是属于这个类所共有的,它不归某一个对象所拥有,所以没有动态绑定的必要
4、内联函数也不能被声明为虚函数,因为内联函数是在编译的时候被展开,而虚函数实在运行的时候才动态绑定。内联函数是为了在代码中进行直接展开,减少函数调用的开销,而虚函数是为了确定在继承后对象能准确的执行自己的动作,这也是不可能统一的。(但是在VS2015条件下测试结果显示可以声明为virtua,但是应该是有歧义的,当使用非多态调用的时候,编译器可以选择内联,具体是非内联又编译器决定,多态调用的时候是不能内联的)
5、由于友元函数不能被声明为虚函数,因为友元函数不属于类的成员函数,它是不能被继承的
19、虚函数是什么?
虚函数就是有virtual关键字的类成员函数,他是通过虚函数指针指向虚函数表来实现的,虚函数表没有名字,里面存放的全部是虚函数的入口地址。
虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,因为它是类的一个成员,并且vptr指向保存虚函数地址的vtable.
对于静态成员函数,它没有this指针,所以无法访问vptr. 这就是为何static函数不能为virtual.
20、虚函数和虚函数表里面内存是如何分配的?
分配在只读内存区,因为一旦产生虚函数表,就不会变了。
21、虚函数表的生成时间
编译期:
表格中的virtual functions地址是如何被建构起来的?在C++中,virtual functions(可经由其class object被调用)可以在编译时期获知。此外,这一组地址是固定不变的,执行期不可能新增或替换之。由于程序执行时,表格的大小和内容都不会改变,所以其建构和存取皆可以由编译器完全掌控,不需要执行期的任何介入。”
22、虚继承?
虚继承是为了解决从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题,将共同基类设置为虚基类。这时从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。这样不仅就解决了二义性问题,也节省了内存,避免了数据不一致的问题。
补充:这个问题要从虚继承解决了什么问题?如何结局的?两方面来回答。
23、如何实现只能动态分配类对象,不能定义类对象?
(1)动态分配(在堆上分配内存)
将类的构造函数和析构函数设为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;
}
};
(2)静态分配(在栈上)
把new、delete运算符重载为private属性就可以了。
class A
{
private:
void* operator new(size_t t){} // 注意函数的第一个参数和返回值都是固定的
void operator delete(void* ptr){} // 重载了new就需要重载delete
public:
A(){}
~A(){}
};
24、模板是怎么实现的?
###25、模板特化的概念,两种特化,为什么特化?
###26、继承机制对象是如何转换的?
###27、继承机制中指针和引用如何转换?
###28、this指针的作用?
一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果。this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上this指针,编译器在编译的时候也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行。
补充:this指针存放在哪里?(有时候会问,了解一下)
29、仿函数和函数指针的区别?
仿函数就是重载了operator(),它的作用一般用来堆自定义类型的行为进行一个处理,使它可以像一个函数一样使用,而函数指针本身是一个指针,指向一个函数。
30、内存泄漏如何处理和检测,出现内存泄漏的因素?
必须会,绝对会问的一个问题,这个问题要深入了解
内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的、大小任意的(内存块的大小可以在程序运行期决定)、使用完后必须显示释放的内存。应用程序一般使用malloc、realloc、new等函数从堆中分配到一块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块。否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。
补充:了解下Valgrind工具。
31、出现异常的处理方法(异常机制)
32、C++强制类型转换有几种
static_cast 、const_cast 、 dynamic_cast 、reinterpret_cast
- 基本类型转换用static_cast。(不能用于基本类型指针之间的转换。用于有继承关系类对象之间的转换和类指针之间的转换)
- 去const属性用const_cast。
- 多态类之间的类型转换用daynamic_cast(主要用来将基类的指针和引用安全的转化为派生类的指针或者应用。)。
- 不同类型的指针类型转换用reinterpreter_cast。(用于整数和指针类型间的强制转换,不可用于非指针类型的强制转换)
33、操作符重载是如何实现的
在编译器实现的时候,已经为我们提供了这个操作符的基本数据类型实现版本,但是现在他的操作数变成了用户定义的数据类型class,所以,需要用户自己来提供该参数版本的实现。
34、迭代器失效问题(iterator原理)
35、智能指针相关问题
36、strlen和sizeof区别?
sizeof是运算符,结果在编译时得到而非运行中获得;strlen是字符处理的库函数。
sizeof参数可以是任何数据的类型或者数据;strlen的参数只能是字符指针且结尾是’\0’的字符串。
sizeof值在编译时确定,所以动态分配(运行时分配)存储空间的大小无法获得。
补充:sizeof一个空类的大小为多少?为什么? 为一个字节。