这篇面经本来是很少的几个问题,后来写起来是又臭又长。其中有些问题还是重复的。哪里有问题的还望各位大神指教。
问题一:关于宏
首先,C++中不鼓励使用宏,因为宏有一些局限性还是容易出一些问题的。但是,有些地方,用宏定义来解决函数传参的问题还是能解决一些函数无法解决的问题。
宏的优点:1,可以直接替代一些函数;比如 #define Min(a,b) (a>b?b:a) 这里,你不用考虑a,b的参数类型,在整型中使用就是整型,浮点型和双精度也都可以,而不用再多针对不同的类型去多写函数;
2,函数调用会带来额外的开销,它需要开辟一片栈空间,记录返回地址,将形参压栈,从函数返回还要释放堆栈,宏就很好的解决了这个问题;
3,宏能完成函数无法解决的问题:
使用宏需要注意的:1,宏只是替换;
2,宏内不能使用类的私有变量和函数,宏也无法实现隐形的类型转换;
3,宏在预处理阶段进行处理的,实质上就是对宏定义的语句进行了替换,所以宏一般是比较短的,否则,定义很长的宏会显著增加代码长度,程序的执行效率并没有明显提升;
4,如果宏定义是多行,在行末尾要加一个 \ 符号;
5,宏虽然提高了效率,但是不能接受调试;
问题二:关于内联函数
C++中的内联函数在C中是不存在的。内联函数必须定义在头文件中,在定义函数头加上inline关键字。如果不加入inline关键字编译器也默认为是内联函数;需要注意的是,inline必须和函数定义是一起的,只有inline而没有函数定义的函数仍然按照正常函数来对待。
内联函数和宏的区别:
1,内联函数的处理是在编译阶段,宏的处理是在预处理阶段;
2,内联函数和普通函数的不同之处在于其没有了调用和返回的开销,这是普通函数必不可少的。此外,内联函数仍然会有变量和函数的压栈操作;
3,由于内联函数的特性,决定了内联函数可以使用私有成员,还可以进行调试,这是宏所不具备的功能。
4,inline不能包含逻辑判断(if)或者循环,递归;
5,内联函数写成短小、频繁调用的函数才能最小可能的减少代码的潜在膨胀和提高程序的运行效率;
问题三:函数模板 template
template < 类型形式参数表 >
返回类型 FunctionName( 形式参数表 )
{
// 函数定义体
}
//
///一个实现的例子//
//
template <class Type>
Type min( Type a, Type b ) {
return a < b ? a : b;
}
模板的声明定义一般是写在头文件中,否则在.CPP文件中则会出现链接错误。因为,通常的编译只是编译.CPP文件,如果.CPP文件中的函数或者类能在头文件中找到声明,那么就不会报错。但是到了链接部分就不一样了,因为在链接时候需要实例化模板,这时候就需要模板的具体实现了。如果在main函数中调用了模板函数,这时候就需要去实例化该函数的模板。注意main函数中只包含了.h文件,也就是只有函数的声明而没有具体的实现。这时就会报错。
(
模板是在需要的时候才会去生成一个具体化的实例的。例如,你要生成一个int类型的实例,模板只会给你生成一个int类型的实例,模板本身是不会被执行的(也就是说模板本身不会产生汇编指令),是模板生成的具体化实例才会产生指令(而这个实例是隐藏的,我们看不到))
函数模板和宏以及内联函数都是不同的,相对来说函数模板更方面更强大。他具有宏和内联函数无可比拟的优点。
1,函数模板包括模板函数和类模板;
2,函数模板可以避免#define中的二次调用和inline中无法使用循环和逻辑判断的缺点;
3,函数模板还可以提供类模板,这对处理类的实现提供了良好的帮助;
4,通过一系列模板类形成的完整类库,是STL和ATL的核心基础(
标准C++库(STL)提供了很多可重用和灵活的类及算法,而ATL则是使用C++进行COM编程的事实标准。要掌握这些及其它的模板库,理解模板是如何工作的这一基础是非常重要的)
问题四:C++标准库和STL的区别
一个误区:.h和<>的区别
.h只是C中的头文件标准。也就是说,标准的C++中并没有.h后缀,只是有时候加上C前缀来表明这个头文件夹来自于C。比如,math.h在C++中就写成cmath。也就是说,C中的头文件在C++中都有一个不带.h的头文件与之一一对应,区别两者除了后者做了改进之外,还有一点就是后者的东西都塞进了std的命名空间中。(之所以 using namespace std 在.h头文件出现的地方不用出现的原因是.h头文件定义的都是在全局命名空间,而<>的都是在std的命名空间,C++能使用.h说明C++能够兼容C的标准库)
C/C++模板库主要包括三部分:C的函数库,IO流和本地化(常见的就是<iostream>,STL;(大概有50个左右的库函数,其中18个提供了C库的功能)
可以看出,STL(Standard Template Library,标准模板库)是C/C++模板库的一个部分或者说是标准库的一个子集。它包括五大类的组件:算法,容器,迭代器,函数对象,适配器。其中包含了很多实用的算法和数据结构。STL是一个泛型思维的集中体现。
问题五:代码生成过程
生成主要包括三步:预编译、编译,链接和载入。
预编译:主要包括宏替换,文件包含和条件编译三个部分。经过此步骤完成的程序任然是C/C++程序;
编译(compile):就是将用户代码的高级语言翻译成计算机可以理解的机器语言,将用户编写的文件编译成一个个的可链接的模块(.o(Unix/Linux)或.obj文件)。在此过程中,编译器只是检查语法、变量和函数等是否被声明,如果没有声明编译器会给出一个错误或警告。但是,仍然是可以生成 .o或.obj 文件。
汇编:将上一步生成的汇编语言翻译成CPU可执行的机器码;
链接(link):将一组编译好的模块(.obj)连同相应的库函数链接在一起形成一个可以载入的完整载入模块。在link过程中,主要是链接函数和全局变量。主要操作的就是 .obj 或 .o 文件。有时我们的cpp文件太多,生成的 .o 或 .obj 文件太多,我们要给中间的目标文件打个包,在windows下就叫库文件(Library Files),也就是 .lib 文件。在unix和Linux下就是Arcchive File,也就是 .a 文件。这时候,如果链接的相应函数找不到或者没有定义,那么就会有LINK2001错误出现。
载入:载入过程比较简单,就是将链接生成的.exe文件载入内存。
问题六:回调函数
要说回调函数必须先说下函数指针。函数指针和指针函数两者的区别当然不用说了,这个很明显。但是有了函数对象为什么还是要用函数指针这个问题还是挺有意思。
首先,函数指针是C的核心科技,C++为了最大程度的兼容C++当然也会容许使用函数指针。但是,C++中是不推荐使用函数指针的。(就像C中有数组,C++不推荐使用数组而是推荐使用容器,但是还是有很多人在C++中使用数组)。
其次,通过对函数对象和函数指针的反汇编可以发现,函数对象需要编译大概50条左右的指令,而使用函数指针则只需要找到函数地址和执行函数这2条指令,效率的对比高下立判(这就回答了上面的疑问:为什么要使用函数指针来代替函数对象)。但是,C++中仍然不推荐使用函数指针,无他,要有自己的特点和规则。函数指针在定义和引用上:
例如:Type (*ptr)() //定义形式
int (*prt)();//定义
ptr=max;//max是一个函数,这里相当于把函数地址赋给指针
int a,b;//传参
int res=(*ptr) (a,b);//比如max函数有两个形参
以上就是一个函数指针定义到使用的完整过程。这个过程显然没有直接调用 int res=max(a,b); 来的简洁,而且
定义指针需要额外的四个字节。这可能是C++考虑不使用函数指针的一个因素。
现在就可以说回调函数了。回调函数说白了就是一个函数指针的使用问题,就是在一个函数里调用另一个函数,调用的方式是函数指针,这种行为就叫回调函数。在STL里面充斥着很多回调函数使用的例子。这是值得注意的。
callback函数为B层,main函数和print*函数为A层,A层调用了B层的回调函数callmeback,而B层的回调函数调用了A层的实现函数print*。说白了B层就是一个接口。这个是一个挺有意思的解释。
使用回调函数可以把调用函数和被调用函数分开,回调函数中存储的只是指向函数的指针,当一个函数需要调用另外的函数时只需要将被调用函数的相关参数传递给回调函数就可以完成调用。而不用在代码中写死去调用某个函数,这样的使得程序的灵活度更高。
优点:1,用指针代替指令,汇编指令只需要两条,而函数对象则需要50条左右,效率很高;
2,用起来更加灵活,一个指针可以指向返回值类型相同的很多对象;
缺点:1,额外增加了4个字节的使用;
问题七:指针和引用的区别
这简直是一个操蛋的问题。但是还是不明白有点,所以就找找答案,发现图样。。。
这两者之间的区别岂止是大,简直是大啊~!
(1)这个指针是什么,指针其实就是一个变量一个4字节的变量,只不过这个变量存储的是另一个变量的地址而已。引用是一个啥,它不过是原有变量的一个别名,仅仅是一个别名,而已。所以,这个应该是很容易理解了;
(2)可以有const指针,但是不能有const引用,因为const绝对不能接受别名;
(3)指针可以有多级,比如 int **a,但是引用只能有一级 &a ,否则就成了别名的别名,没有意义;
(4)指针可以是空,但是引用决不能是空,因为给空的东西加别名没有意义;
(5)指针的值初始化了之后可以改变,但是引用不能,引用只是一个别名;
(6)sizeof去处理指针,你得到的是4个字节。但是去处理引用得到的就是引用的原对象的大小了;
(7)指针和引用的自增自减运算符(++,--)意义是不一样的;
(8)将引用作为参数传入就是将实参传入,不考虑形参,改变的也是实参;
具体的实例参考下面的网站:
问题八:__stdcall / __cdecall / __fastcall
这几个关键字主要是程序/函数的调用约定。所谓的调用约定主要是针对如何清空传递参数的堆栈来进行规约的。
__stdcall是WINAPI的规范调用关键字,在这一点上它与__cdecal是有区别的。由于在传递函数时候需要另外开辟堆栈空间去传递,在传递参数时候就会有压栈方向的问题:比如一个函数 int Add(int a,int b){return a+b;}这个堆栈在压栈时候是怎么传递的呢?是先压入a还是先压入b呢?在__stdcall中是从右到左压入的,也即先2后1。而且在调用函数完成之后__stdcall自带清理堆栈方式,也就是在调用函数完成之前清空堆栈。由于WINAPI是应用在不同平台的,不同平台的清理堆栈方式不一样,容易出现问题。所以自带清理堆栈是API的生存能力的保证。
__cdecal跟__stdcall是不一样的。不同之处就在于这种调用本身不包含清理堆栈的代码,每个调用它的函数必须自身带着清理堆栈的代码,这就使得代码编译后的长度变长。但是它的压栈方式还是从左到右的。一般是用在C/C++和MFC中的
__fastcall调用比较快,因为它可以通过CPU内部寄存器调用参数。_fastcall约定用于对性能要求非常高的场合。__fastcall约定将函数的从左边开始的两个大小不大于4个字节(DWORD)的参数分别放在ECX和EDX寄存器,其余的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的堆栈。
问题九:_far和_near
_far / _near主要是针对的内存的地址分配方式不同来说的。主要是在之前的16位的计算机上应用的地址分配方式,在目前32位系统上可以忽略。
问题十:关于堆和栈
堆和栈都是一种数据结构,先进后出的栈结构。不过,不同的地方还是很多:
(1)栈是由编译器来管理的,而堆是提供给程序猿使用的。堆空间都是由程序猿分配和释放,程序结束后所有释放和未释放的空间一并由OS回收。这里的堆和数据结构中的堆是两码事。一般用到的关键字就是:new、free,delete、malloc等等;
(2)堆需要程序猿自己申请并且需要指定申请的空间的大小。比如 p=(char *)malloc(10);这就申请了一个10个字符的空间。栈则是由编译器自动分配释放的,存放函数的参数值,局部变量的值等等。
(3)在系统的响应速度和相应方式上,堆和栈是有明显不同的。堆空间有一个专门记录地址的链表,在申请内存时系统遍历该链表找到适合申请内存块大小的地址,然后将这个节点从链表中删除,并将该节点分配给程序。对于大多数系统而言,会在这块内存空间的首地址记录本次分配的空间大小,这样在delete的时候才能够正确的释放内存空间。此外,由于找到的堆节点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分空间重新放到空闲的链表中。对栈来说,只要栈的剩余空间大雨所申请的空间,系统将为程序提供内存。否者提示栈溢出。
(4)在容量大小方面:堆的地址是由低地址向高地址扩展的,是不连续的区域,因为是用链表来存储的地址嘛。堆的大小受限于计算机系统幼小的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。但是栈就不一样了,由于栈的大小是根据不同的操作系统环境而变化的。一般系统的是1M或者2M。
(5)栈是和OS之间进行交互的,每一个线程对应一个栈。在程序的主线程启动时OS会自动分配相应的栈给应用程序。栈都有一个sp(stack pointer),sp最初指向栈顶(高地址);CPU使用push和pop命令来进栈和出栈,当push时,sp值减少(向低地址扩展),当pop时,sp向高地址扩展。
(6)当进入一个函数时,sp向下扩展,一直到能够提供足够局部变量的空间。然后执行函数,在执行完毕之后sp通过返回原来的位置值。如果sp向下扩展的可用空间不满足函数局部变量的大小,则会提示栈溢出;
(7)堆:
堆的通常比栈大得多,对是和语言类型和runtime有关的。一般在堆顶有会有一个链表来维护已用和空闲的内存块。这个链表在堆顶且一般占较少的空间;
频繁申请和释放小的内存块会产生很多的空间碎片。有时候申请的内存小于堆的可用内存也会出问题,就是因为这些内存是不连续的。不过一般来说相邻的内存碎片是会自动合并的。
问题十一:关于内存分配方式
在C中的内存分配的三个申明方式是:
void* realloc(void* ptr , unsigned newsize);
void* malloc(unsigned size);
void* calloc(size_t numElements,size_t sizeOfElement);
都在stdlib.h函数库内,返回值都是请求系统分配的地址,如果请求失败就返回NULL;
三者的区别:
malloc是用于申请一段新的地址,参数size是需要内存空间的长度:
char* p;
p=(char*)mallor(10);//分配的空间
realloc是给一个已经分配了地址的指针重新分配空间,参数p是原有变量,newsize是新分配空间:
int *p;
p=(int*)malloc(sizeof(int)*10);
p=(int*)realloc(p,,sizeof(int)*20);//重新分配20个int的空间
calloc与malloc相似,numElements是申请元素个数,sizeOfElement是元素大小:例子就不举了
在C中就是用free(void *p)来释放空间
但是,在C++中没有以上的关键字,所有的关键字就只有new 和 delete;
问题十二:进程和线程
这个问题有两个小问题:1,为什么使用多线程,多线程通信、多线程访问共享内存和共享代码片段是怎么处理的?
2,什么是多进程编程?
首先回答第一个问题:
使用多线程编程的优势:
1,对于耗时长的程序,多线程可以提高程序的系统响应;
2,并行操作时候使用线程;
3,多CPU系统中,使用线程能够提高CPU的利用率;
4,使用多线程可以改善程序架构;
5,相对于进程来讲,线程花销小、切换快。此外,线程之间还有方便的通信机制,不像进程之间所占的都是独立的数据空间。线程之间由于所在共同数据空间,所以线程之间共享数据是方便的;
多线程访问共享内存的方式:
1,加锁;
2,不加锁方式:多线程之间的相互协作通常是通过不断的添加指令到共享区域来实现的--生产者线程将自己生产的数据不断加入到共享的临界区域,消费者线程则从共享的临界区域内取出数据。使用队列也许能解决这个问题,不过前提是在消费者线程使用之前需要判断共享内存区域是否是空;
问题十三:继承和虚函数
继承
继承方式有public、private和protect三种方式,三种方式下继承的成员的访问方式不同。如果public继承,保留基类的访问权限;如果private继承,则继承的基类成员都改成private权限;如果是protect继承,除了将基类的public成员改成protect权限外,其他的不变。
在这里B继承A,此时B中有一个普通函数fun()和基类A中的fun()重名,在调用函数时候就会产生不同的情况:
我们只需要定义基类A的对象就可以使用此对象实现对其派生类中函数的调用,这就是多态的精髓所在。通过基类引用或者指针调用基类中定义的函数时,我们并不知道执行函数对象的确切类型,执行函数的对象可能是基类类型的,也可能是派生类型的。于是对相应的类对象进行
动态绑定。
如果调用非虚函数,则无论实际对象是虚或者非虚,都执行基类类型所定义的函数。如果调用虚函数,则直到运行时才能确定调用哪个函数,运行的虚函数是引用所绑定的或指针所指向的对象所属类型定义的版本。这就解释了上面调用的结果差异的问题,也从侧面证明了虚函数是实现多态的基础,以及为什么一般不推荐一个类去继承另一个不含虚函数的类,因为没啥意义。
动态绑定:是将一个过程调用与相应的代码链接起来的行为。通俗来说就是将相应的变量和函数绑定到一个类引用或者指针上的行为。动态绑定是多态实现的具体形式。在上面的A *p=&b;就是动态绑定。动态绑定是针对静态绑定来说的,静态绑定是在编译阶段就确定了引用或者指针的指向,而动态绑定则是在程序运行阶段才能确定的指向;
虚函数:
虚函数在基类中一定要加入virtual关键字,在派生类中可加可不加。
定义虚函数的限制:
(1)非类的成员函数不能定义为虚函数,类的静态成员函数和构造函数也不能定义成虚函数,但是析构函数一般是定义成虚函数。因为在delete一个指向派生类定义的对象指针时,系统会调用相应类的析构函数。否则,只会调用基类的析构函数;
(2)只需要在声明函数的类中使用,则派生类中的同名函数自动变成虚函数,可不用vitual声明;
(4)基类中声明了一个虚函数,则不能再出现与这个虚函数同名、同参数、同返回类型的非虚函数。在基类的派生类中也不能出现这种函数;
(5)如果一个类中有virtual函数,那么就要给它一个virtual的 析构函数,因为virtual函数意味着这个类具有多态性;相反的,如果一个类不具有虚函数,那就不要给一个虚的析构函数了;
纯虚函数:
pure virtual int fun(){}
加上了pure关键字,类似接口中的函数。在基类中的pure是不能定义的,而且在派生类中必须实现。所以,这就很像接口中的函数。
虚函数和纯虚函数的区别:
1,虚函数是实现多态的基本要求,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里可以被重载,这样编译器就可以通过后期绑定来达到多态了。而纯virtual,在实际的定义中并不需要使用virtual;
(3)只要基类的函数声明为虚函数虚函数只是一个借口,一定要留到派生类中去实现的。
2,虚函数在派生类中也可以不重载的,但是纯虚函数必须重载。但是有时候加上加上virtual是个好习惯,主要考虑为了增加多态性;
3,带纯虚函数的类叫虚基类,就是传说中的抽象类。抽象类就是java中的接口。它不能生成对象,只有在被继承并重写其纯虚函数之后才能使用。
问题十四:JAVA的最大优势真的是跨平台么
这个问题不是面试问题,后来自己找的。
Java的“Write once , run everywhere”在客户端流行的过去给java打下了良好基础。现在的情况是越来越倾向于B/S,客户端应用就显得没有那么重要了。即使要做客户端,QT等第三方的框架也远比SWing更强大。java的在桌面应用领域几乎销声匿迹,就是当初的的applet现在也是销声匿迹了。如果说java还有一席之地,那就是在Android了客户端了,由于是基于Linux内核,java的使用显得顺理成章。但是随着Android 4.4的ART的出现,Dalvik的使用可能会被颠覆。更何况Google还想着让GO语言取代java呢。
那么
,java的优势到底还存在哪里呢?
首先,以前积累的庞大程序猿群。这个不用说;
其次,强大的第三方类库,比如说你要解析HTML,使用C/C++你只能自己写解析包,但是使用java的话你直接去github上找Jsoup就行了,然后使用Maven导入就可以使用了。因为,开发公司的成本主要是程序猿的工资,这样的话就可以大量省钱,所以,何乐而不为呢。
再次,java有强大的IDE。Eclipse,IntillJ IDEA,特别是后者几乎可以媲美VS;
再再次,java有很多杀手级应用,Spring,Struts,Hibernate,Hadoop,Tomcat,JBoss等;
最后,java的语法特性很少,这点就把C++虐出翔了。代码可读性比C++好多了。在硬件发展相对迅速,对性能要求不是极为苛刻的条件下,java已经能够满足用户的需求了。因为java的效率也不算低了。这也是python这类动态脚本语言能够流行的原因。
问题十五:弱类型,强类型,动态类型和静态类型
问题十六:32位和64位系统的区别
两者的区别在于寻址能力不同,32位的系统寻址能力大概是2的32次方位(bit),能支持的内存大概只有4G左右,而64位系统可支持多大128G的内存和多大16T的缓存。
问题十七:关于拷贝构造函数和析构函数
只有单个形参,而且该形参是对本类类型对象的引用(一般用const修饰),这样的构造函数成为复制构造函数。拷贝构造函数和默认构造函数一样,是由编译器隐式调用的。拷贝构造函数一般用于:
1,根据另一个同类型的对象显式或者隐式初始化一个对象,一般是复制初始化;
2,复制一个对象,将它作为实参传递给一个函数;
3,从函数返回时复制一个对象;
4,初始化顺序容器中的元素时候会调用容器的拷贝构造函数,实际还是初始化;
5,根据初始化列表初始化数组元素时候;
关于析构函数:
若非在类中定义和使用了指针,一般不需要显式写出构造函数。在delete类对象时候,类对象会自动合成析构函数释放资源。在析构函数释放资源时,一般是按照类中声明的次序来释放资源。
这里有一个“
三原则”:如果需要析构函数,一般都需要赋值操作符和拷贝构造函数。
问题十八:悬垂指针、哑指针、野指针
悬垂指针:指针指向曾经指向的对象,但是该对象已经不存在了,该指针就成了悬垂指针。
例如:int *ptr;
void fun(){
int i=10;
p=&i;
}
int main(){
fun();
cout<<"p="<<*p<<endl;
cout<<"p="<<*p<<endl;
}
输出结果:
p=10;
p=123234325435;
从输出的结果可以看出,由于临时的变量被销毁,在第二次输出时候p已经成为了悬垂指针。
哑指针:我们传统的指针就叫做哑指针。指针的定义、释放都需要程序猿手动进行。
野指针:指的是指向垃圾内存(内存不可用)的指针,对野指针来讲,判断NULL是没用的。只能靠程序猿的经验来防止。
造成野指针的原因:
1,指针变量没有被初始化。指针变量在定义之后实际已经有了一个指向的地址,但是这个地址是一个并不知道的地址。一般的方法是给指针初始化或者置NULL;
2,指针被free和delete之后,没有置NULL。如果只是释放了指针指向的内存,但是并没有把指针本身给干掉。所以这时候如果没有置NULL,误认为p还是一个合法的指针的话,就会产生悲剧。此时的p也是一个野指针;
3,指针的操作超出了变量作用的范围,就会产生野指针。具体例子如下(其实还是和第一个原因是一样的):
在使用指针之前必须明白的一件事是,在实例化多个类对象时候,一般是多个类对象同时共享一个指针的拷贝,所以在一个对象被释放之后这个指针指向的内存就释放掉了,指针就成了悬垂指针。
从以上可以看出,指针出现问题是防不胜防,所以通常引入指针管理。指针管理通常有两种方式,
一种是才用值型的方式管理,就是所有的类对象都保存一份指针的拷贝。
还有一种方式就是使用智能指针。
智能指针:智能指针的出现正是为了解决野指针和悬垂指针出现的问题。因为C++没有类似java的自动的内存回收机制,使用传统指针又非常容易出错,所以就使用改良的指针,是为智能指针。STL中有auto_ptr和boost中的智能指针也不错,不过没有引用参数。所以,有时候需要自己来写智能指针;
在上面的程序中,类TestPtr的每一次析构都会引起ptr的指向内存的释放,这样就会影响到该类的其他对象,形成悬垂指针。
解决方法:1,引用计数来解决:创建多少个对象就使用计数器计多少个数,每释放一个对象,计数器的个数-1,计数器归零时才释放指针。不过计数器不能放在Testptr类中,因为这样的的话不能同步更新计数器。所以就另外使用一个类来解决。
这种方法有一个缺点:在每一个使用指针的类中都需要自己控制引用计数,比较繁琐。特别是很多类中都含有指针时候,这种维护引用的计数会很繁琐。
所以,C++中面向对象编程的一个颇具讽刺意味的地方就是,不能使用对象支持面向对象编程。相反,只能使用指针和引用。
2,使用句柄类
问题十九:C++中的类前置声明
如果类A引用了类B,B也引用了A,那么在定义的时候是先定义A还是先定义B就有了一个矛盾。如果先定义B,那么此时A还没有声明和定义,这时候就很扯淡了,所以就在类B的前面直接做一个A的声明。这就叫类前置声明。
类前置声明有两个好处,一个是解决了刚才说的交叉引用。另一个就是加快了编译速度,因为这样的话就可以不再使用头文件引用了,因为在编译的时候编译器总是要去检查头文件的引用,一级一级的往上查,知道最后查不到为止。所以用类前置声明就解决了这个问题,加快了编译速度。在很多C++的标准库里面,类前置声明用处很广泛。这样带来一个附加的好处就是,去掉了头文件之间的引用依赖更有利于代码的封装和保密性。
问题二十:函数模板和类模板
类似于类的多态,函数模板也是泛型编程中实现多态的重要方式。面向对象编程其实是运行时的多态,泛型编程是编译时的多态。
函数模板在之前和宏以及内联函数的时候已经有。这里主要说说函数模板的形式:
template<typename T> //这一行是一定要有的,而且所有的关键字都不能加在这一行前面,比如内联的inline。其实这里的typename和class是等价的,可以用class代替typename
(inline) T max(const T& , const T& , ....){
//执行部分
return T类型的一个值;
}
在类模板的定义中:
template<class Type>
class Queue<Type>{
public:
Queue();
Type &front();
const Type &front() const;
.......
}
在实例化类时,例如:Queue<int> qi;或者 Queue<string> qi;就是将尖括号中的类型替换Type,得到相应的类。
需要注意的问题:模板函数和类模板必须有形参,形参的作用域同普通的函数一样,但是不能没有形参(typename/class/type);
函数模板可以重载,但是一般不会重载,容易造成二义性。
问题二十一:多重继承和虚继承
多重继承的构造函数是按照次序一个一个调用相应的基类的构造函数来完成的,析构的过程也是一样的。不过因为作用域的问题,容易造成二义性,所以不是很推荐使用多重继承。如果多重继承中有虚继承,则先实现依次序虚继承的构造函数,然后再实现正常继承的构造函数。
问题二十二:动态链接库和静态链接库
所谓动态链接库就是一些dll文件,这些dll文件是编译好的文件,封装的比较好。在引用的时候直接添加代码引用,而不是#include引用,#include引用叫静态链接库。这就是主要的区别。
使用动态链接库能够很好简化编译和简化代码工程,另外,使用别人已经编译好的dll也可以大大减轻工作量。这里dll类似第三方的类库。
静态链接库和动态链接库的区别:
1,静态链接库就是写 .lib 文件,如果有多个程序同时调用.lib文件,在内存中就会有多份程序拷贝,这些程序拷贝会占用多余的内存空间;
2,静态链接库的更新部署需要重新编译,这是最麻烦的地方,用于在使用程序更新时最好是只更新链接库而不需要重新编译或者重新下载安装客户端;
3,动态链接库可以动态加载到项目中,一旦加载到内存中,只存在一份拷贝。不会出现.lib的占用内存资源的情况,而且多线程可以共享dll的资源;
4,使用dll有点众多,但是使用dll也会造成“dll Hell”的问题。就是一个程序更新了dll,这个dll可能位于system32中,那么其他程序在调用这个dll时候可能就会出现不能使用的bug,这就是dll Hell。
问题二十三:特殊数据成员的初始化问题(包括静态数据成员和容器)
用全局变量和静态变量的区别:一般在使用静态变量的地方都能使用全局变量,但是,全局变量一般比较难以维护。而静态变量的使用一般是在文件内,程序的可读性强。另外,在static int a;这种赋值时,未初始化的a会自动初始化为0.而普通变量则不会;
问题二十四:排序
排序方式有很多种;主要非为几类--交换排序,选择排序,插入排序,归并排序和分配排序
交换排序:主要有冒泡和快排
冒泡排序:复杂度就是O(n^2),稳定排序,
快排:复杂度O(nlgn) 不稳定
选择排序:直接选择排序和堆排
直接选择排序 复杂度O(n^2) 不稳定排序
堆排:复杂度nlgn,不稳定排序
插入排序:常见包括直接插入排序和希尔排序
直接插入排序:复杂度O(n^2) 稳定排序
希尔排序:缩小增量排序 复杂度跟选择的增量序列有关 不稳定排序
归并排序:两路归并,然后复杂度稳定nlgn,但是需要额外空间
分配排序:应该就是基数排序和桶排序
问题二十五:哈希表和哈希算法
哈希表就是使用键值可以直接查找到数据的一个结构。哈希表查找的复杂度是O(1)。类似于STL中的map和python的字典。不过,不同的是,你可以自己设计映射关系,这个从键到值的映射关系就是哈希函数或哈希算法。
哈希算法:
1,直接寻址法:直接取关键字或者关键字的某个函数作为哈希地址;
2,数字分析法:找出数字的规律,如果一组数字有规律,那么使用有规律的数字来映射哈希表的地址就可以了;
3,平方取中法:当无法确定关键字中哪几位分布比较均匀时候,一般使用平方取中,这样就会有很大几率会将不同的元素区分开;
4,折叠法:将关键字分割成位数相同的几部分,最后一部分位数可以不同,然后就取这几部分的叠加和作为哈希地址。
5,随机数法:每个关键字生成一个随机数来作为哈希地址;
6,除留余数法:取关键字被某个不大于表长的值去除,得到的余数作为散列地址。
问题二十六:类和结构体的区别
在C中:
1,结构体中不能有函数,而类中可以有函数,构造函数、析构函数和this指针;
2,结构体中默认成员变量的访问权限是public,而类中成员的访问权限包括public,private,protect;
3,结构体是不可以继承的,而类是可以继承;
以上就是面向对象和面向过程的区别。结构体只包括了数据变量,并不包括算法。而类是将数据成员和算法一并包括进来并给与不同的访问权限。C中是没有类的概念的,只能通过内建函数指针来模拟面向对象的思想。
在C++中:
1,结构体中成员变量的访问权限默认是public,而类中成员默认访问权限是private;
2,结构体继承默认是public,而类继承默认是private;
3,C++结构体内可以有函数;
二十七:同步和异步
同步可以认为是一个线程,一个线程是等待式的,也就是说执行完上一个函数等它返回之后接着执行下一个函数。异步是相当于多线程,多个线程大家各干各的,一个线程是否完成任务不会对另一个线程产生影响。所以才有了线程同步的问题。线程同步是多线程问题里一个非常重要的问题。
线程同步的方式:临界区、互斥量、信号量和时间四种方式。
问题二十八:预编译和编译
生成工程的过程依次包括:预编译→编译→汇编→链接→生成。
编译--将C/C++语言编译成中间语言或者汇编语言;
汇编--将汇编语言翻译成CPU可以识别的机器码;
链接--将汇编完成后的.lib文件链接成一个.exe文件;
问题二十九:new和非new的区别
class A{};
A a;//在栈内
A *p=new A();//在堆上,注意要自己释放
A a =new A();//这种用法在C#和java中使用,在C++中是错的。另,C#和java的new的对象都是在堆上 of course
//大程序用new,小程序就直接分配在栈内了
问题三十:智能指针
要把大象装冰箱,一共分三步;
1,为什么要使用智能指针?
首先,如果你有多个指针指向同一对象,一旦delete了其中一个,也就意味着内存已经释放,剩下的指针就孤魂野鬼了,再用就等着出事儿吧。
其次,即使你木有多个指针,只有一个。那么,在类似 void f(){A *a=new A();。。。delete a;}这样的程序中,可能在delete之前就有了一个return或者exception,这样就不会执行到delete,那就等着泄漏吧;
2,智能指针的智能之处?
智能指针就是要解决上个小问题的。几乎智能指针总离不开reference counting 引用计数的。智能指针可以在我们忘记释放或者出现以上异常时候自动释放资源。在STL中,auto_ptr是一个不错的选择,因为它完美的解决了以上问题。但是auto_ptr有一个蛋疼的问题;
3,智能指针的优劣?
auto_ptr的蛋疼之处在于这货不能完美完成copying。比如,std::auto_ptr<A> a; //此时a指向A的一个对象 std::auto_ptr<A> b(a);//此时b指向这个对象,而a是NULL,这就是autp_ptr的copying constructor蛋疼之处 a=b;//此时a又指向对象,而b为NULL了
auto_ptr是没有引用计数的。这时候可以考虑boost库中的shared_ptr了,它才是使用引用计数的智能指针。在释放对象时候,调用智能指针的构造函数,然后计数器-1,计数器为0时释放对象。
问题三十一:注意iterator的失效问题
在使用iterator时候,容易造成迭代器失效的问题。在vector中,由于vector是存放空间连续的容器,所以在vector扩展空间时会申请一块更大的内存空间,然后将原来的数据拷贝到新的内存空间中去,这就会造成迭代器失效。同理,在进行erase操作时,转向更小空间时也会造成迭代器失效。针对迭代器失效没有更好的解决办法,要时时注意,或者在使用时候随时返回迭代器的指针。
由此引发出一个新的问题:
list、dequeue、vector有什么区别呢?
主要是在存储方式上的区别。vector在内存中是连续存储的方式,list则和dequeue类似,是链式存储的方式。所以vector的查找更方便,而list和dequeue的插入删除效率更高。在使用迭代器的问题上,list和dequeue更不容易失效。但是也要同时注意这个问题。dequeue是一个双向队列,更方便进行头尾的插入和删除操作。
所以,vector 适用于对象数量变化较少的简单对象。list更适用于对象变化较大,频繁的进行插入和删除操作的数据。
问题三十二:C程序在执行main函数之前还执行哪些动作
从操作系统的角度去想,在执行main函数之前
1,编译连接;
2,创建进程内核对象;
3,分配进程内存空间--分配静态变量和全局变量,也就是data段;
4,load可执行文件;
5,然后执行全局的初始化;
6,执行main函数;
单纯的从语言的角度上来讲,可以有这样的解释:
1,设置堆栈指针,也就是初始化堆栈;
2, 初始化static变量和global全局变量,即data段的内容;
3, 将未初始化部分的赋初值,数值型如short、int、long等为0 ,bool类型就是false;
4,运行全局构造器;
5,将main函数的参数,argc、argv等传递给main函数,然后才真正运行main函数;
注:一个程序一般分成三段:text段,data段,bss段。
text段:就是存放程序代码的,编译时就确定的,只读;
data段:存放在编译阶段(而非运行阶段)就能确定的数据,可读写。通常说的就是静态存储区、赋初值的全局变量和静态变量存放在这个区,常量也常存放在这个区;
bss段:定义而没有赋初值的全局变量和静态变量,放在这个区;
问题三十三:C/C++的内存分配
1,栈区
栈区是由编译器负责分配和释放的,所以运行的效率比较高。栈是向低地址扩展的数据结构,是一块连续的内存。但是栈区的空间比较小,window一般是在2M左右,如果你申请的空间超过了2M,就会粗线stack overflow。栈区主要放一些形参,局部变量和临时变量。
2,堆区
堆区是由程序猿分配和释放的,其空间一般是比较大。和数据结构中的堆不一样,这里的堆区更像是一个链表。堆是向高地质扩展的,其内存是不连续的。程序猿申请空间,就去内存中寻找一个相应大小的空间,多余的返回。在申请的这块空间一般会在开头存放这块空间的大小信息,以便于在释放空间时候会正常的free掉。
3,全局变量去
全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放。
4,字符常量区
常量字符串就是放在这里的。 程序结束后由系统释放
5,代码区
存放函数体的二进制代码。
//main.cpp
int a = 0; 全局初始化区
char *p1; 全局未初始化区
main()
{
int b; //栈
char s[] = "abc"; //栈
char *p2; //栈
char *p3 = "123456"; //123456\0在常量区,p3在栈上。
static int c =0; // 全局(静态)初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);
//分配得来得10和20字节的区域就在堆区。
strcpy(p1, "123456"); //123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
}
问题三十四:C/C++的内存架构
应该是这样的,内存--缓存--寄存器--CPU
CPU的缓存一般是有二级和三级缓存,二级缓存可以认为是一级缓存的缓存,同理三级缓存可以认为是二级缓存的缓存。例如二级缓存主要存储一级缓存不能存数的一些指令和一些相应的数据,却不能像一级缓存一眼更直接存储CPU的指令集。从一级到三级缓存,容量是递增的,造价是递减的。在CPU执行指令需要一个数据时候,先去寄存器查找,找不到就去一级缓存,找不到然后去二级和三级缓存,再找不到就去内存,然后去硬盘找。
寄存器有很多种,AX/BX/CX/DX等等,寄存器是离CPU最近的存储器,使用调用速度最快。所以将非常经常用的变量设置成寄存器变量是最好的。