1.先做两分钟的自我介绍
2.说一说全局变量、静态变量、栈区变量、堆区变量的区别
全局变量:全局变量具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然,其他不包含全局变量定义的源文件需要用extern关键字再次声明这个全局变量。
静态局部变量:静态局部变量具有局部作用域,它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。
静态全局变量:静态全局变量也具有全局作用域,它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被static关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。
栈:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
堆:亦称动态内存分配。程序在运行的时候用malloc或new申请任意大小的内存,程序员自己负责在适当的时候用free或delete释放内存。动态内存的生存期可以由我们决定,如果我们不释放内存,程序将在最后才释放掉动态内存。 但是,良好的编程习惯是:如果某动态内存不再使用,需要将其释放掉,否则,我们认为发生了内存泄漏现象。
- 栈又叫堆栈,非静态局部变量/函数参数/返回值等等,栈是向下增长的。
- 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。(Linux课程如果没学到这块,现在只需要了解一下)
- 堆用于程序运行时动态内存分配,堆是可以上增长的。
- 数据段–存储全局数据和静态数据。
- 代码段–可执行的代码/只读常量。
3.一般怎样在堆上申请空间?malloc和new的区别
- malloc和free是函数,new和delete是操作符
- malloc申请的空间不会初始化,new可以初始化
- malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可
- malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
- malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
- 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理
- new/delete比malloc和free的效率稍微低点,因为new/delete的底层封装malloc/free
4.说一说传值、传指针、传引用的区别
1.直接传值是直接开辟了一个跟主函数实参一样的空间(地址不一样),里面存放了了跟实参一样大小的值,就相当于数值大小相同但是位置不同。你在这个调用函数里使用这个一样大小的值,完全不影响主函数实参的值。就好比主函数的空间就是一栋楼,里面的一个房间里放着一些东西(相当于实参变量值)。现在我调用了一个函数,就相当于我在另一栋楼的另一个房间里面,把刚才第一个放东西的房间里面存的东西完全复制过来,所以你操作现在这个房间里面的东西,完全不影响原来的房间的东西呀。
2.传指针就不一样了。指针就是地址,我们要去找一个房间里面的东西,那么你得先找到门牌号,才能对照着门牌号去找到房间,从而找到你想要的东西,这就是指针的使用原理。传指针就是把实参的地址传过去了,而不是像刚才传值一样,直接开辟一个新的空间去复制数值,而是开辟了一个新的空间把实参的地址复制了过去。主函数的空间就是一栋楼,里面的一个房间放着一些东西(相当于实参变量值),这个房间有个门牌号(也就是实参的地址)。现在调用函数,就好比我把他家的门牌号(实参的指针)给你,跟你说你按照这个门牌号去找这个房间,然后再去找里面的东西。这样一来,你根据门牌号找到了原来的房间,一旦修改房间里面的东西,就一定会产生改变。所以根据指针修改指向的变量时,如果调用函数进行了修改,主函数的变量也就被修改了。
3.传引用就是传指针的升级版。引用可以看成变量的别称,就好像正常的姓名和乳名,名字不一样但是人就是那一个人。所以你传引用的时候,修改了调用函数里的传递参数值,主函数的对应变量也会随之改变。但是原理还是传递指针,也就是地址。传引用的时候实际上是拷贝了实参的地址,然后你在调用函数里的操作表面上看是对变量的直接赋值,实际上是通过找到地址再改变变量的,这是一种间接寻址。但是为啥不直接用指针找地址再操作呢?而是封装成引用的外表了,很大的原因是安全。因为直接指针操作,那你很可能改变了指针,然后就找不到原来的地址了。就好比,我现在要去找房间(调用了其他函数要去访问主函数的实参变量值),然后给了你一块门牌号(相当于指针,也就是地址),万一你一不小心掉沟里了,门牌号弄丢了(指针被错误的修改),那你就找不到原来的房子了呀,你要是还继续去找错误的房子,把别人家房子里面的东西改了,等下直接程序就出错了(走错家门很危险的…)。所以别人就是怕你乱改,直接就给你封装好了。引用其实还可以让代码更加简洁清晰,一目了然(因为就相当于同一个变量在操作的感觉)。C语言是没有传引用的,C++把它加上了。原因我觉得是更方便了,也更安全了
5.说一说C语言和C++语言的区别
设计思想上:C++是面向对象的语言,而C是面向过程的
结构化编程语言语法上:C++具有封装、继承和多态三种特性C++相比C,增加多许多类型安全的功能,比如强制类型转换、C++支持范式编程,比如模板类、函数模板等
6.说一说封装、继承、多态(基类析构函数为什么要写成虚函数?类中创建一个虚函数,类的大小有什么变化,为什么?)
封装:
定义:封装就是将抽象得到的数据和行为相结合,形成一个有机的整体,也就是将数据与操作数据的源代码进行有机的结合,形成类,其中数据和函数都是类的成员,目的在于将对象的使用者和设计者分开,以提高软件的可维护性和可修改性
特性:1. 结合性,即是将属性和方法结合 2. 信息隐蔽性,利用接口机制隐蔽内部实现细节,只留下接口给外界调用 3. 实现代码重用
继承:
定义:继承就是新类从已有类那里得到已有的特性。 类的派生指的是从已有类产生新类的过程。原有的类成为基类或父类,产生的新类称为派生类或子类,子类继承基类后,可以创建子类对象来调用基类函数,变量等
单一继承:继承一个父类,这种继承称为单一继承,一般情况尽量使用单一继承,使用多重继承容易造成混乱易出问题
多重继承:继承多个父类,类与类之间要用逗号隔开,类名之前要有继承权限,假使两个或两个基类都有某变量或函数,在子类中调用时需要加类名限定符如c.a::i = 1; 菱形继承:多重继承掺杂隔代继承1-n-1模式,此时需要用到虚继承,例如 B,C虚拟继承于A,D再多重继承B,C,否则会出错
继承权限:继承方式规定了如何访问继承的基类的成员。继承方式指定了派生类成员以及类外对象对于从基类继承来的成员的访问权限
继承权限:子类继承基类除构造和析构函数以外的所有成员
继承可以扩展已存在的代码,目的也是为了代码重用
多态:
定义:可以简单概括为“一个接口,多种方法”,即用的是同一个接口,但是效果各不相同,多态有两种形式的多态,一种是静态多态,一种是动态多态
静态多态:是在编译期就把函数链接起来,此时即可确定调用哪个函数或模板,静态多态是由模板和重载实现的,在宏多态中,是通过定义变量,编译时直接把变量替换,实现宏多态
优点: 带来了泛型编程的概念,使得C++拥有泛型编程与STL这样的武器; 在编译期完成多态,提高运行期效率; 具有很强的适配性和松耦合性,(耦合性指的是两个功能模块之间的依赖关系)
缺点: 程序可读性降低,代码调试带来困难;无法实现模板的分离编译,当工程很大时,编译时间不可小觑 ;无法处理异质对象集合
动态多态: 是指在程序运行时才能确定函数和实现的链接,此时才能确定调用哪个函数,父类指针或者引用能够指向子类对象,调用子类的函数,所以在编译时是无法确定调用哪个函数;使用时在父类中写一个虚函数,在子类中分别重写,用这个父类指针调用这个虚函数,它实际上会调用各自子类重写的虚函数。运行期多态的设计思想要归结到类继承体系的设计上去。对于有相关功能的对象集合,我们总希望能够抽象出它们共有的功能集合,在基类中将这些功能声明为虚接口(虚函数);然后由子类继承基类去重写这些虚接口,以实现子类特有的具体功能。运行期多态的实现依赖于虚函数机制。当某个类声明了虚函数时,编译器将为该类对象安插一个虚函数表指针,并为该类设置一张唯一的虚函数表,虚函数表中存放的是该类虚函数地址。运行期间通过虚函数表指针与虚函数表去确定该类虚函数的真正实现。
7.说一说list、vector、map的区别概念:
1.Vector连续存储的容器,动态数组,在堆上分配空间底层实现:数组两倍容量增长:vector 增加(插入)新元素时,如果未超过当时的容量,则还有剩余空间,那么直接添加到最后(插入指定位置),然后调整迭代器。如果没有剩余空间了,则会重新配置原有元素个数的两倍空间,然后将原空间元素通过复制的方式初始化新空间,再向新空间增加元素,最后析构并释放原空间,之前的迭代器会失效。性能:访问:O(1)插入:在最后插入(空间够):很快在最后插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。在中间插入(空间够):内存拷贝在中间插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。删除:在最后删除:很快在中间删除:内存拷贝适用场景:经常随机访问,且不经常对非尾节点进行插入删除。
2.List动态链表,在堆上分配空间,每插入一个元数都会分配空间,每删除一个元素都会释放空间。底层:双向链表性能:访问:随机访问性能很差,只能快速访问头尾节点。插入:很快,一般是常数开销删除:很快,一般是常数开销适用场景:经常插入删除大量数据区别:
1)vector底层实现是数组;list是双向 链表。
2)vector支持随机访问,list不支持。
3)vector是顺序内存,list不是。
4)vector在中间节点进行插入删除会导致内存拷贝,list不会。
5)vector一次性分配好内存,不够时才进行2倍扩容;list每次插入新节点都会进行内存申请。
6)vector随机访问性能好,插入删除性能差;list随机访问性能差,插入删除性能好。应用vector拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除的效率,使用vector。list拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用list。
map和set的区别:
集合,所有元素都会根据元素的值自动被排序,且不允许重复。底层实现:红黑树set 底层是通过红黑树(RB-tree)来实现的,由于红黑树是一种平衡二叉搜索树,自动排序的效果很不错,所以标准的 STL 的 set 即以 RB-Tree 为底层机制。又由于 set 所开放的各种操作接口,RB-tree 也都提供了,所以几乎所有的 set 操作行为,都只有转调用 RB-tree 的操作行为而已。适用场景:有序不重复集合
map映射。map 的所有元素都是 pair,同时拥有实值(value)和键值(key)。pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。不允许键值重复。底层:红黑树适用场景:有序键值对不重复映射
8.说一说static成员函数和普通成员函数的区别和联系类的静态成员:
在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员。对多个对象来说,静态数据成员只存储一处,供所有对象共用类的静态函数静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员。因此,对静态成员的引用不需要用对象名。
在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。如果静态成员函数中要引用非静态成员时,可通过对象来引用。从中可看出,调用静态成员函数使用如下格式:<类名>::<静态成员函数名>(<参数表>);
9.说一说线程和进程
●多进程和多线程的区别进程它是具有独立地址空间的,优点就是隔离度好,稳定,因为它是操作系统管理的,进程和进程之间是逻辑隔离的,只要操作系统不出问题的话,一个进程的错误一般不会影响到其它进程,缺点就是信息资源共享麻烦。而线程只是进程启动的执行单元,它是共享进程资源的,创建销毁、切换简单,速度很快,占用内存少,CPU利用率高。但是需要程序员管控的东西也比较多,相互影响出问题的机率较大,一个线程挂掉将导致整个进程挂掉,所以从程序员的角度来讲,我们只能看到某种代码是线程安全的,而没有说进程安全的。
●在进程和线程上,应该怎么选择我们平时在写代码的时候一般使用线程会比较多,像需要频繁创建销毁的,要处理大量运算、数据,又要能很好的显示界面和及时响应消息的优先选择多线程,因为像这些运算会消耗大量的CPU,常见的有算法处理和图像处理。还有一些操作允许并发而且有可能阻塞的, 也推荐使用多线程. 例如SOCKET, 磁盘操作等等。进程一般来说更稳定,而且它是内存隔离的,单个进程的异常不会导致整个应用的崩溃,方便调试,像很多服务器默认是使用进程模式的。
●线程之间是如何通信的一个是使用全局变量进行通信,还有就是可以使用自定义的消息机制传递信息。其实因为各个线程之间它是共享进程的资源的,所以它没有像进程通信中的用于数据交换的通信方式,它通信的主要目的是用于线程同步,所以像一些互斥锁啊临界区啊CEvent事件对象和信号量对象都可以实现线程的通信和同步。
●进程之间是如何通信的进程间的通信方式有PIPE管道,信号量,消息队列,共享内存,还可以通过 socket套接字进行通信。根据信息量大小的不同可以分为低级通信和高级通信,在选择上,如果用户传递的信息较少.或是需要通过信号来触发某些行为的,一般用信号机制就能解决,如果进程间要求传递的信息量比较大或者有交换数据的要求,那么就要使用共享内存和套接字这些通信方式。名词解释:管道其实是存在于内存中的一种特殊文件,它不属于文件系统,有自己的数据结构,根据使用范围还可分为无名管道和命名管道。共享内存是通过将共享的内存缓冲区直接附加到进程的虚拟地址空间中来实现的,它是利用内存缓冲区直接交换信息,不需要复制,很快捷、信息量大。消息队列缓冲是由系统调用函数来实现消息发送和接收之间的同步,它允许任意进程通过共享消息队列来实现进程间通信.但是信息的复制需要耗费大量CPU,所以不适用于信息量大或操作频繁的场合。
●线程同步和线程异步同步是指一个线程要等待另一个线程执行完之后才开始执行当前的线程。异步是指一个线程去执行,它的下一个线程不必等待它执行完就开始执行。一般一个进程启动的多个不相干线程,它们之间的相互关系就为异步,比如游戏有图像和背景音乐,图像是由玩家操作的 而背景音乐是系统循环播放,它们两个线程之间没什么关系各干各的,这就是线程异步。至于同步的话指的是多线程同时操作一个数据,这个时候需要对数据添加保护,这个保护就是线程的同步同步使用场景:对于多个线程同时访问一块数据的时候,必须使用同步,否则可能会出现不安全的情况,有一种情况不需要同步技术,那就是原子操作,也就是说操作系统在底层保证了操作要么全部做完,要么不做。异步的使用场景:当只有一个线程访问当前数据的时候。比如观察者模式,它没有共享区,主题发生变化后通知观察者更新,主题继续做自己的事情,不需要等待观察者更新完成后再工作。
●多线程同步和互斥有几种实现方法,分别适用什么情况线程同步的话有临界区,互斥量,信号量,事件。临界区适合一个进程内的多线程访问公共区域或代码段时使用。互斥量是可以命名的,也就是说它可以适用不同进程内多线程访问公共资源时使用。所以在选择上如果是在进程内部使用的话,用临界区会带来速度上的优势并且能够减少资源占用量。信号量与临界区和互斥量不同,它是允许多个线程同时访问公共资源的,它相当于操作系统的PV操作,它会事先设定一个最大线程数,如果线程占用数达到最大,那么其它线程就不能再进来,如果有部分线程释放资源了,那么其它线程才能进来访问资源。事件是通过通知操作的方式来保持线程同步。注意:互斥量,事件,信号量都是内核对象,可以跨进程使用。
●C++多线程有几种实现方法直接使用WIN32 API CreateThread,或者用C运行库_beginthread创建线程,MFC的话用AfxBeginThread. 还有就是运用第三方线程库,比如boost的thread等等。_beginthread和CreateThread的区别:_beginthread内部调用了CreateThread.如果你的程序只调用 Win32 API/SDK ,就放心用 CreateThread,如果要用到C++运行时库,那么就要使用_beginthreadex,因为C++运行库有一些函数里面使用了全局变量,beginthreadex 为这些全局变量做了处理,使得每个线程都有一份独立的“全局”量,在这种情况下使用CreateThread的话就会出现不安全的问题
●进程有哪几种状态,状态转换图,及导致转换的事件
●死锁概念:进程间进行通信或相互竞争系统资源而产生的永久阻塞,若无外力作用将永远处在死锁状态。产生原因:(1)系统资源不足;(2)进程运行推进顺序与速度不同也可能导致死锁;(3)资源分配不当;产生死锁四个必要条件:(1) 互斥条件:就是一个资源每次只能被一个进程使用。(2) 请求与保持条件:一个进程在请求其它资源而阻塞时,但是它对自己已获得的资源又保持不放。(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。预防死锁和避免死锁的方法:在系统设计、进程调度方面注意不让产生死锁的四个必要条件成立,确定资源的合理分配算法,避免进程永远占用系统资源,对资源分配要进行合理的规划。
●多线程中栈与堆是公有的还是私有的因为线程是共享进程的资源的,所以栈是私有的,堆是公有的。
●线程池的概念线程池就是一堆已经创建好的线程,最大数目一定,然后初始后都处于空闲状态,当有新任务进来时就从线程池中取出空闲线程处理任务,任务完成之后又重新放回去,当线程池中的所有线程都在任务时,只能等待有线程结束任务才能继续执行。
10.你了解面向对象设计的原则吗?(没听明白题目,请求解释下)你听过“开放封闭”吗?
七种设计原则简单归纳:
单一职责原则:专注降低类的复杂度,实现类要职责单一;
开放关闭原则:所有面向对象原则的核心,设计要对扩展开发,对修改关闭;
里式替换原则:实现开放关闭原则的重要方式之一,设计不要破坏继承关系;
依赖倒置原则:系统抽象化的具体实现,要求面向接口编程,是面向对象设计的主要实现机制之一;
接口隔离原则:要求接口的方法尽量少,接口尽量细化;
迪米特法则:降低系统的耦合度,使一个模块的修改尽量少的影响其他模块,扩展会相对容易;
组合复用原则:在软件设计中,尽量使用组合/聚合而不是继承达到代码复用的目的。
11.说说assert及其使用
assert是一个宏,其作用是计算表达式(条件)如果为假,先打印一个错误信息,再终止执行程序;缺点:使用assert会极大的影响程序的性能,增大额外开销程序调试结束后可以在#include前使用#define NDEBUG来禁用assert调用
使用断言的原则:
1.使用断言捕捉不应该发生的非法情况;(非法情况和错误情况不同,后者是必然存在并且一定要做出处理的)
2.使用断言对函数的参数进行确认(在函数的开始处检查参数的合法性)
3.在编写函数时就得考虑清楚打算做哪些假设,一旦做了假设就要使用断言对假设进行检查
4.一个assert只能检查一个假设,如果检查多个条件(建议使用多个断言)一旦断言失败无法判断是哪个条件失败
5.不能使用改变环境变量的语句,因为assert只在DEBUG下有效,如果真的这么写,会使程序在运行时遇到问题
断言assert只是在DEBUG版本下起作用的一个宏,用来调试代码,检查不应该发生的情况;在RELEASE版本下是自动忽略掉的;