面试中的C++问题

面试中的C++问题

行文开头,希望和大家达成一个共识,有些面试的问题虽然确实很刁钻,但是不可否认它们是在考察你对C++的一些本质特性的认识。希望大家是因标题吸引而来,看完的收获却不仅仅应用于面试。

1.数组指针和指针数组
首先记住后面的是主语,前面的是定语。

数组指针(也称行指针):指向数组的指针
int (*p)[4]; //该语句是定义一个数组指针,指向含4个元素的一维数组。因为()优先级高,优先结合成指针
所谓行指针: int a[3][4]; p = a; p++; 此时等于从a[0][]到a[1][]

指针数组:存放指针的数组
int *p[n]; // []优先级高,先与p结合成为一个数组,再由int*说明这是一个整型指针数组

2.函数指针问题
  函数指针是指向一个函数入口的指针
  一个函数指针只能指向一种类型的函数,即具有相同的返回值和相同的参数的函数。
函数指针数组定义:void(*fun[3])(void*);
相应指向类A的成员函数的指针:void (A::*pmf)(char *, const char *);
指向外部函数的指针:void (*pf)(char *, const char *); void strcpy(char * dest, const char * source); pf=strcpy;

3.野指针成因
a.指针变量没有被初始化。 应该在创建的时候赋NULL或者分配内存空间
char *p = NULL; char *str = (char *) malloc(100);
b.指针被delete或free之后没有及时赋NULL
c.指针操作超越了变量的作用范围,所指向的内存值对象生命期已经被销毁。比如函数中声明的指针,将其返回了

3.引用和指针的区别
1)引用必须初始化,指针则不必;
2)引用初始化以后不能改变,指针可以改变其指向的对象;
3)不存在指向空值的引用,但存在指向空值的指针;
4)引用是某个对象的别名,主要用来描述函数和参数和返回值。指针与一般的变量是一样的,会在内存中开辟一块内存。
5)如果函数的参数或返回值是类的对象的话,采用引用可以提高程序的效率。
6)引用可读性更高,指针需要解引用

4.动态内存分配
I、栈区(stack)― 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。 栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限,如果申请空间超过栈剩余空间时,将提示overflow。栈是高地址向低地址扩展的数据结构。在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。是一块连续的内存的区域。
II、堆区(heap) ― 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。用malloc或new申请内存,用free或delete释放内存。如果在申请动态内存时找不到足够大的内存块,malloc和new将返回NULL指针,判断指针是否为NULL,如果是则马上用return语句终止本函数,或者马上用exit(1)终止整个程序的运行,为new和malloc设置异常处理函数(如set_new_handler允许客户指定一个函数来处理这个异常)。堆是低地址向高地址扩展的数据结构。是不连续的内存区域。
III、全局区(静态区)(static)―,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放
IV、文字常量区 ―常量字符串就是放在这里的。 程序结束后由系统释放
V、程序代码区―存放函数体的二进制代码。

5.Const用法
a. char * const p; //修饰指针,指针不可改
const char* p; //修饰指针所指向的值,*p是常量字符串
const char * const p 和 char const * const p; // 内容和指针都不能改
b. const修饰函数参数是它最广泛的一种用途,它表示函数体中不能修改参数的值, 一定要注意多用const,在引用或者指针参数的时候使用const限制是有意义的,而对于值传递的参数使用const则没有意义。
c. const修饰类对象表示该对象为常量对象,其中的任何成员都不能被修改;该对象的任何非const成员函数都不能被调用,因为任何非const成员函数会有修改成员变量的企图。
e.const修饰类的成员变量,表示成员常量,不能被修改,同时它只能在初始化列表中赋值。static const 的成员需在声明的地方直接初始。
f.const修饰类的成员函数,则该成员函数不能修改类中任何非const成员。一般写在函数的最后来修饰。
在函数实现部分也要带const关键字。
g.不要轻易的将函数的返回值类型定为const;除了重载操作符外一般不要将返回值类型定为对某个对象的const引用。

6. define宏的一些问题
I.const常量与define宏定义的区别
a. 编译器处理方式不同
define宏是在预处理阶段展开,生命周期止于编译期。#define常量存在于程序的代码段。
const常量是编译运行阶段使用,const常量存在于程序的数据段。
b. 类型和安全检查不同
define宏没有类型,不做任何类型检查,仅仅是展开。
const常量有具体的类型,在编译阶段会执行类型检查。
c. 存储方式不同
define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存。但是确使代码变长。
const常量会在内存中分配(可以是堆中也可以是栈中)

II.含参数的宏和函数的优缺点(用inline代替宏函数)
a. 函数调用时,先求出实参表达式的值,然后代入形参。而使用带参的宏只是进行简单的字符替换。
#define MIN(A, B) ((A) <= (B)? (A):(B))
注意i++的问题
b.函数调用是在程序运行时处理的,分配临时的内存单元;而宏展开是在编译时进行的,在展开时不进行
内存分配,不进行值得传递处理,没有“返回值”概念
c. 对函数中的形参和实参都要定义类型,类型要求一致,如不一致则进行类型转换。而宏不存在类型问题
d.调用函数只可得到一个返回值,而用宏则可以设法得到几个结果
e.实用宏次数多时,宏展开后源程序变长,没展开一次源程序增长,函数调用则不会
f.宏替换不占用运行时间,只占编译时间,而函数调用占用运行时间

7. c++的四种强制类型转化
1). const_cast
去掉类型的const或volatile属性,

struct SA{int k};  
const SA ra; 
ra.k = 10;   //直接修改const类型,编译错误  
SA& rb =  const_cast<SA&>(ra);   
rb.k = 10;   //可以修改

2).static_cast
主要用于基本类型之间和具有继承关系的类型之间的转换。用于指针类型的转换没有太大的意义。
static_cast是无条件和静态类型转换,可用于基类和子类的转换,基本类型转换,把空指针转换为目标类型的空指针,把任何类型的表达式转换成void类型,static_cast不能进行无关类型(如非基类和子类)指针之间的转换。

int a;     
double d = static_cast<double>(a);   //基本类型转换
int &pn = &a;     
void *p = static_cast<void*>(pn);   //任意类型转换为void

3). dynamic_cast
你可以用它把一个指向基类的指针或引用对象转换成继承类的对象;动态类型转换,运行时类型安全检查(转换失败返回NULL),基类必须有虚函数,保持多态特性才能用dynamic_cast,只能在继承类对象的指针之间或引用之间进行类型转换

class BaseClass{public:  int m_iNum;  virtual void foo(){};};
class DerivedClass:BaseClass{public: char* szName[100];  void bar(){};};
BaseClass* pb = new DerivedClass();

DerivedClass *p2 = dynamic_cast<DerivedClass *>(pb);
BaseClass* pParent = dynamic_cast<BaseClass*>(p2); //子类->父类,动态类型转换,正确

4). reinterpreter_cast
转换的类型必须是一个指针、引用、算术类型、函数指针或者成员指针。
主要是将一个类型的指针,转换为另一个类型的指针,最普通的用途就是在函数指针类型之间进行转换。
简单的将对应内存块复制截断。

int DoSomething(){return 0;};
typedef void(*FuncPtr)(){};
FuncPtr funcPtrArray[10];
funcPtrArray[0] = reinterpreter_cast<FuncPtr>(&DoSomething);

8. c++的空类,默认产生哪些类成员函数

class Empty
{
 public:
    Empty();                         //缺省构造函数
    Emptyconst Empty& );           //拷贝构造函数
    ~Empty();                        //虚构函数
    Empty& operator=(const Empty& )  //赋值运算符
    Empty* operator&();              //取址运算符
    const Empty* operator&() const;  //取址运算符 const 
}

9.类成员函数的overload, override 和 隐藏的区别
a.成员函数被重载的特征:相同的类范围,函数名字相同,参数不同,virtual 关键字可有可无。
b.覆盖指派生类的函数覆盖基类函数,特征是分别位于基类和派生类,函数名字相同,参数相同,基类函数必须有virtual关键字
c.隐藏是指派生类的函数屏蔽了与其同名的基类函数。1,派生类的函数与基类的函数同名,但是参数不同,
不论有无virtual关键字,基类的函数将被隐藏 2,派生类的函数与基类的函数同名,并且参数也相同,
但是基类函数没有virtual 关键字。此时,基类的函数被隐藏

3种情况怎么执行:重载:看参数;隐藏:用什么就调用什么;覆盖:调用派生类 。

10.面向对象的三个基本特征
封装性
把客观事物封装成抽象的类,对自身的数据和方法进行。

继承性
继承概念的实现方式有三类:实现继承、接口继承和可视继承。
实现继承是指使用基类的属性和方法而无需额外编码的能力;
接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力;
可视继承是指子窗体(类)使用基窗体(类)的外观和实现代码的能力。

多态性
多态性(polymorphisn)是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,
父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。允许将子类类型的指针赋值给父类类型的指针。
实现多态,有二种方式,覆盖(子类重新定义父类的虚函数),重载(允许存在多个同名函数,参数个数,类型不同)。

11.static关键字
a. 函数体内作用范围为该函数体,该变量内存只被分配一次,具有记忆能力
b. 在模块内的static全局变量可以被模块内所有函数访问,但不能被模块外其它函数访问;
c. 在模块内的static函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内;
d. 在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;
e.在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。

12.头文件的作用
a. 通过头文件来调用库功能。在很多场合,源代码不便(或不准)向用户公布,只要向用户提供头文件
和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口怎么实现的。
编译器会从库中提取相应的代码。
b. 头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,
编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。

13.c++中哪些函数不能被声明为虚函数
a.普通函数(非成员函数): 虚函数用于基类和派生类
b.构造函数:虚函数可以在只知道部分信息的前提下,只需要知道接口,不需要知道具体类型的时候调用,而构造函数是要创建一个对象,势必要知道准备类型。
c.内联成员函数:在调用的地方直接将代码扩展开,故不能是虚函数
d.静态成员函数:静态成员函数是不能被继承的,它只属于一个类,因此也不存在动态联编
e.友元函数:不是类的成员函数,因此也不能被继承

14. main函数执行前和退出后,都会做什么工作或执行什么代码
I. 执行前:
a. 运行全局构造器,全局对象的构造函数会在main函数之前执行;
b. 设置栈指针,初始化static静态和global全局变量,即数据段的内容;
c. 将未初始化部分的赋初值:数值型short,int,long等为0,bool为FALSE,指针为NULL等;
d.将main函数的参数,argc,argv等传递给main函数
II.退出后
a.可以用_onexit 注册一个函数,它会在main 之后执行int fn1(void), fn2(void), fn3(void), fn4 (void)

15. 不允许重载的5个运算符

运算符 说明
.* 成员指针访问运算符
:: 域运算符
sizeof 长度运算符
?: 条件运算符
. 成员访问运算符

15. 动态链接库和静态链接库
a. lib是编译链接时用到的,dll是运行时用到的。 如果要完成源码编译,只需lib;要使动态链接的程序运行,只需dll;
b. 对于lib,那么是静态编译出来的,索引和实现都在其中。使用静态编译的lib,程序运行时候不再需要dll,缺点是导致程序比较大,它在编译时复制展开了。而且失去了动态库的灵活性,发布新版本要发布新的应用程序才行。
c. 对于dll,载入时动态链接(load-time dynamic linking),需要一个lib包含索引和入口信息,应用程序的可执行文件中,存放的不是被调用函数的代码,而是dll中相应函数的代码地址,从而节省了内存资源。如果不想用lib文件,运行时动态链接(run-time dynamic linking),可以用win32的api中LoadLibrary或LoadLibraryEx函数载入DLL,用GetProcAddress获取DLL函数的出口地址。

16. 编译的四个阶段
预处理(Pre-Processing):处理宏定义指令#define a b,直接替代掉。 条件编译指令#ifndef, #ifdef等,可以把不需要 代码直接过滤。头文件包含指令,展开头中的定义。特殊符号的替换,如LINE,FILE等。 预处理后文件为.i和.ii后缀
编译(Compiling),优化: 编译就是通过词法分析和语法分析,在确认所有指令都符合语法规则之后,将其翻译成等价的中间代码或者汇编代码。删除公共表达式,循环优化,无用赋值删除等。 同时有与机器硬件相关的优化。生成.s文件
汇编(Assembling):把汇编语言代码翻译目标机器指令的过程。目标文件包含代码段和数据段 .o文件
链接(Linking):静态链接和动态链接。 某个文件引用了另一个源文件中定义的符号;调用库文件等。将目标文件彼此相连。可执行的exe

17. 堆栈溢出的原因
a. 没有回收垃圾资源
b. 层次太深的递归调用

18.Itearator和指针的区别
I.相似点:
a. 指针和iterator都支持与整数进行+,-运算,而且其含义都是从当前位置向前或者向后移动n个位置
b. 指针和iterator都支持减法运算,指针-指针得到的是两个指针之间的距离,迭代器-迭代器得到的是两个迭代器之间的距离
c.通过指针或者iterator都能够修改其指向的元素
II.不同点:
a. cout操作符可以直接输出指针的值,但是对迭代器进行在操作的时候会报错。通过看报错信息和头文件知道,迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用*取值后的值而不能直接输出其自身。
b. 指针能指向函数而迭代器不行,迭代器只能指向容器
c. 指针是一种特殊的变量,它专门用来存放另一变量的地址,而迭代器只是参考了指针的特性进行设计的一种STL接口。

19.ifndef/define/endif和#pragma once的区别
都是为了避免被重复包含。
 a. #pragma once用来防止同一个头文件被多次include,这里的“同一个文件”是指物理上的一个文件,而不是指内容相同的两个文件。#ifndef,#define,#endif用来防止某个宏被多次定义,依赖于宏名字不能冲突。
 b. #pragma once是编译相关,就是说这个编译系统上能用,但在其他编译系统不一定可以,也就是说移植性差,不过现在基本上已经是每个编译器都有这个定义了。
 #ifndef,#define,#endif这个是C++语言相关,这是C++语言中的宏定义,通过宏定义避免文件多次编译。所以在所有支持C++语言的编译器上都是有效的,如果写的程序要跨平台,最好使用这种方式。

18.容器
STL是一个标准的C++库,容器只是其中一个重要的组成部分,有顺序容器和关联容器
a. 顺序容器: 指的是一组具有相同类型T的对象,以严格的线性形式组织在一起
vector:底层数据结构为数组 ,支持快速随机访问
deque:底层数据结构为一个中央控制器和多个缓冲区,支持首尾(中间不能)快速增删,也支持随机访问
list:底层数据结构为双向链表,支持快速增删
stack:底层一般用23实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩容耗时
queue:底层一般用23实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩容耗时
priority_queue: 的底层数据结构一般为vector为底层容器,堆heap为处理规则来管理底层容器实现
b. 关联容器,提供一个key实现对对象的随机访问,其特点是key是有序的元素是按照预定义的键顺序插入的
set 底层数据结构为红黑树,有序,不重复
multiset 底层数据结构为红黑树,有序,可重复
map 底层数据结构为红黑树,有序,不重复
multimap 底层数据结构为红黑树,有序,可重复
hash_set 底层数据结构为hash表,无序,不重复
hash_multiset 底层数据结构为hash表,无序,可重复
hash_map 底层数据结构为hash表,无序,不重复
hash_multimap 底层数据结构为hash表,无序,可重复

vector、list和deque提供给程序员不同的复杂度,因此应该这么用:vector是一种可以默认使用的序列类型,当很频繁地对序列中部进行插入和删除时应该用list,当大部分插入和删除发生在序列的头或尾时可以选择deque这种数据结构。

你需要“可以在容器的任意位置插入一个新元素”的能力吗?如果是,你需要序列容器,关联容器做不到。

你关心元素在容器中的顺序吗?如果不,散列容器就是可行的选择。否则,你要避免使用散列容器。

你需要哪一类迭代器?如果必须是随机访问迭代器,在技术上你就只能限于vector、deque和string。

当插入或者删除数据时,是否非常在意容器内现有元素的移动?如果是,你就必须放弃连续内存容器。

容器中的数据的内存布局需要兼容C吗?如果是,你就只能用vector。

查找速度很重要吗?如果是,你就应该看看散列容器(优于)排序的vector(优于)标准的关联容器大概是这个顺序。

你需要插入和删除的事务性语义吗?也就是说,你需要有可靠地回退插入和删除的能力吗?如果是,你就需要使用基于节点的容器。如果你需要多元素插入的事务性语义,你就应该选择list,因为list是唯一提供多元素插入事务性语义的标准容器。事务性语义对于有兴趣写异常安全代码的程序员来说非常重要。

你要把迭代器、指针和引用的失效次数减到最少吗?如果是,你就应该使用基于节点的容器,因为在这些容器上进行插入和删除不会使迭代器、指针和引用失效(除非它们指向你删除的元素)。一般来说,在连续内存容器上插入和删除会使所有指向容器的迭代器、指针和引用失效。

你需要具有以下特性的序列容器吗:1)可以使用随机访问迭代器;2)只要没有删除而且插入只发生在容器结尾,指针和引用的数据就不会失效?这个一个非常特殊的情况,但如果你遇到这种情况,deque就是你梦想的容器。(有趣的是,当插入只在容器结尾时,deque的迭代器也可能会失效,deque是唯一一个“在迭代器失效时不会使它的指针和引用失效”的标准STL容器。)

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