面试中常遇到的几个问题

image

引言

对于以技术出身的程序员来说,面试题的积累是非常有必要的。Abel在这整理几个iOS面试中常常会问到的几个问题。

1、#import与#include的区别,以及@class

  • 1.import会包含这个类的所有信息,包括实体变量和方法,而@class只是告诉编译器,其后面声明的名称是类的名称,至于这些类是如何定义的,暂时不用考虑,后面会再告诉你。
  • 2.在头文件中, 一般只需要知道被引用的类的名称就可以了。 不需要知道其内部的实体变量和方法,所以在头文件中一般使用@class来声明这个名称是类的名称。 而在实现类里面,因为会用到这个引用类的内部的实体变量和方法,所以需要使用#import来包含这个被引用类的头文件。
  • 3.在编译效率方面考虑,如果你有100个头文件都#import了同一个头文件,或者这些文件是依次引用的,如A–>B, B–>C, C–>D这样的引用关系。当最开始的那个头文件有变化的话,后面所有引用它的类都需要重新编译,如果你的类有很多的话,这将耗费大量的时间。而是用 @class则不会。
  • 4.如果有循环依赖关系,如:A–>B, B–>A这样的相互依赖关系,如果使用#import来相互包含,那么就会出现编译错误,如果使用@class在两个类的头文件中相互声明,则不会有编译错误出现。

所以,一般来说,@class是放在interface中的,只是为了在interface中引用这个类,把这个类作为一个类型来用的。 在实现这个接口的实现类中,如果需要引用这个类的实体变量或者方法之类的,还是需要import在@class中声明的类进来. 综上所述#include,#import与@class的区别可以做一下理解:

#include#import在引用一个类的时候会包含这个类的所有信息包括变量方法等,但是这样做会对编译效率造成影响.比如有100个类 都#import了ClassA,那么在编译的时候这100个类都会去对ClassA处理.又比如A被B引用,B被C引用,C被D引用…..此时如果 A被修改,那么后面的B,C,D…..都需要重新进行编译.

#import处理了重复引用的问题,还有一个用法会引起编译错误的就是在ClassA中#import ClassB 在ClassB中#import ClassA(交叉编译)那么在编译的时候也会出现未知错误。

2、堆和栈的区别?定义一个变量时,这个变量是在堆上还是在栈上?什么时候这个变量的内存会被释放?

预备知识—程序的内存分配

一个由C/C++编译的程序占用的内存分为以下几个部分

  • 1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其 操作方式类似于数据结构中的栈。
  • 2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
  • 3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的 全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另 一块区域。 – 程序结束后由系统释放。
  • 4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放
  • 5、程序代码区—存放函数体的二进制代码。

例子程序,这是一个前辈写的,非常详细

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//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);
  分配得来得1020字节的区域就在堆区。
  strcpy(p1, "123456"); 123456\0放在常量区,编译器可能会将它与p3所指向的"123456" 优化成一个地方。
}

堆和栈的理论知识

  • 申请方式

stack:

由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间 

heap:

需要程序员自己申请,并指明大小,在c中malloc函数 

如p1 = (char *)malloc(10);

在C++中用new运算符 如p2 = new char[10];

但是注意p1、p2本身是在栈中的。

  • 申请后系统的响应

    栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。

    堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时, 会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表 中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的 首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。 另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部 分重新放入空闲链表中。

  • 申请大小的限制

    栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。

    堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

  • 申请效率的比较:

    栈由系统自动分配,速度较快。但程序员是无法控制的。

    堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一块内存,虽然用起来最不方便。但是速度快,也最灵活。

  • 堆和栈中的存储内容

    栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。

    堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。

  • 存取效率的比较

    char s1[] = “aaaaaaaaaaaaaaa”;

    char *s2 = “bbbbbbbbbbbbbbbbb”;

    aaaaaaaaaaa是在运行时刻赋值的;

    而bbbbbbbbbbb是在编译时就确定的;

    但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。

    比如:

1
2
3
4
5
6
7
8
9
10
 #include 
  void main()
  {
      char a = 1;
      char c[] = "1234567890";
      char *p ="1234567890";
      a = c[1];
      a = p[1];
      return;
  }

对应的汇编代码

1
2
3
4
5
6
7
 10: a = c[1];
  00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
  0040106A 88 4D FC mov byte ptr [ebp-4],cl
  11: a = p[1];
  0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]
  00401070 8A 42 01 mov al,byte ptr [edx+1]
  00401073 88 45 FC mov byte ptr [ebp-4],al

第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到edx中,再根据edx读取字符,显然慢了。

  • 小结:

    堆和栈的区别可以用如下的比喻来看出:

    使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。

    使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。

3、进程和线程的区别

  • 通俗的解释

    一个系统运行着很多进程,可以比喻为一条马路上有很多马车,不同的进程可以理解为不同的马车,而同一辆马车可以有很多匹马来拉——这些马就是线程,假设道路的宽度恰好可以通过一辆马车,道路可以认为是临界资源,那么马车成为分配资源的最小单位(进程),而同一个马车被很多匹马驱动(线程)——即最小的运行单位,每辆马车马匹数>=1,所以马匹数=1的时候进程和线程没有严格界限,只存在一个概念上的区分度,马匹数>1的时候才可以严格区分进程和线程

  • 专业的解释:

    简而言之,一个程序至少有一个进程,一个进程至少有一个线程.

    线程的划分尺度小于进程,使得多线程程序的并发性高。另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。

    线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

    从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

    进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.

    线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.

    一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行

    进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。如果有兴趣深入的话,我建议你们看看《现代操作系统》或者《操作系统的设计与实现》。对就个问题说得比较清楚。 +++++++++++++++++++++++++++++++++++++++++++++++

    进程概念

    进程是表示资源分配的基本单位,又是调度运行的基本单位。例如,用户运行自己的程序,系统就创建一个进程,并为它分配资源,包括各种表格、内存空间、磁盘空间、I/O设备等。然后,把该进程放人进程的就绪队列。进程调度程序选中它,为它分配CPU以及其它有关资源,该进程才真正运行。所以,进程是系统中的并发执行的单位。

    在Mac、Windows NT等采用微内核结构的操作系统中,进程的功能发生了变化:它只是资源分配的单位,而不再是调度运行的单位。在微内核系统中,真正调度运行的基本单位是线程。因此,实现并发功能的单位是线程。

    线程概念

    线程是进程中执行运算的最小单位,亦即执行处理机调度的基本单位。如果把进程理解为在逻辑上操作系统所完成的任务,那么线程表示完成该任务的许多可能的子任务之一。例如,假设用户启动了一个窗口中的数据库应用程序,操作系统就将对数据库的调用表示为一个进程。假设用户要从数据库中产生一份工资单报表,并传到一个文件中,这是一个子任务;在产生工资单报表的过程中,用户又可以输人数据库查询请求,这又是一个子任务。这样,操作系统则把每一个请求――工资单报表和新输人的数据查询表示为数据库进程中的独立的线程。线程可以在处理器上独立调度执行,这样,在多处理器环境下就允许几个线程各自在单独处理器上进行。操作系统提供线程就是为了方便而有效地实现这种并发性

    引入线程的好处

    (1)易于调度。

    (2)提高并发性。通过线程可方便有效地实现并发性。进程可创建多个线程来执行同一程序的不同部分。

    (3)开销少。创建线程比创建进程要快,所需开销很少。。

    (4)利于充分发挥多处理器的功能。通过创建多线程进程(即一个进程可具有两个或更多个线程),每个线程在一个处理器上运行,从而实现应用程序的并发性,使每个处理器都得到充分运行。

    ++++++++++++++++++++++++++++++++++++++++++++++++

    进程和线程的关系:

    (1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。

    (2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。

    (3)处理机分给线程,即真正在处理机上运行的是线程。

    (4)线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。

    线程是指进程内的一个执行单元,也是进程内的可调度实体.

    与进程的区别:

    (1)调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位

    (2)并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行

    (3)拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源.

    (4)系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。

    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    进程间的通信方式:

    1.管道(pipe)及有名管道(named pipe):

    管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。

    2.信号(signal):

    信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致的。

    3.消息队列(message queue):

    消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息。

    4.共享内存(shared memory):

    可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。

    5.信号量(semaphore):

    主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段。

    6.套接字(socket);

    这是一种更为一般得进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。

    ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    线程之间的同步通信:

    1.信号量 二进制信号量 互斥信号量 整数型信号量 记录型信号量

    2.消息 消息队列 消息邮箱

    3.事件event

    互斥型信号量:必须是同一个任务申请,同一个任务释放,其他任务释放无效。同一个任务可以递归申请。(互斥信号量是二进制信号量的一个子集)

    二进制信号量:一个任务申请成功后,可以由另一个任务释放。(与互斥信号量的区别)

    整数型信号量:取值不局限于0和1,可以一个任务申请,另一个任务释放。(包含二进制信号量,二进制信号量是整数型信号量的子集)

    二进制信号量实现任务互斥:

    打印机资源只有一个,a b c三个任务共享,当a取得使用权后,为了防止其他任务错误地释放了信号量(二进制信号量允许其他任务释放),必须将打印机房的门关起来(进入临界段),用完后,释放信号量,再把门打开(出临界段),其他任务再进去打印。(而互斥型信号量由于必须由取得信号量的那个任务释放,故不会出现其他任务错误地释放了信号量的情况出现,故不需要有临界段。互斥型信号量是二进制信号量的子集。)

    二进制信号量实现任务同步:

    a任务一直等待信号量,b任务定时释放信号量,完成同步功能

    记录型信号量(record semaphore):

    每个信号量s除一个整数值value(计数)外,还有一个等待队列List,其中是阻塞在该信号量的各个线程的标识。当信号量被释放一个,值被加一后,系统自动从等待队列中唤醒一个等待中的线程,让其获得信号量,同时信号量再减一。 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    同步和互斥的区别:

    当有多个线程的时候,经常需要去同步这些线程以访问同一个数据或资源。例如,假设有一个程序,其中一个线程用于把文件读到内存,而另一个线程用于统计文件中的字符数。当然,在把整个文件调入内存之前,统计它的计数是没有意义的。但是,由于每个操作都有自己的线程,操作系统会把两个线程当作是互不相干的任务分别执行,这样就可能在没有把整个文件装入内存时统计字数。为解决此问题,你必须使两个线程同步工作。

    所谓互斥,是指散布在不同进程之间的若干程序片断,当某个进程运行其中一个程序片段时,其它进程就不能运行它们之中的任一程序片段,只能等到该进程运行完这个程序片段后才可以运行。如果用对资源的访问来定义的话,互斥某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的

    所谓同步,是指散步在不同进程之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。如果用对资源的访问来定义的话,同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源





Comments