embedded software classic interview questions

第一章 经典面试题

1.用#define声明一个常数,表明1年中有多少秒(忽略闰年问题)?

1
#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL

要点:
A.没有以分号结束;
B.懂得预处理器将为你计算常数表达式的值。即60 * 60 * 24 * 365而不是31536000.
C.考虑到了16位机将会溢出,巧妙运用了UL。

2.写一个“标准”宏MIN,这个宏输入两个参数并返回较小的一个?

1
#define MIN(A,B) ((A) <= (B) ? (A) : (B))

要点:
A.参数用括号括起来;
B.考察能否合理运用条件运算符;

3.Heap与Stack的差别?
答案:Heap是堆,Stack是栈。

Stack的空间由系统自动分配/释放,Heap上的空间手动分配/释放。
Stack空间有限,Heap是很大的自由存储区,malloc函数分配的内存空间即在堆上。

网上经典例子:

1
2
3
4
5
6
7
8
9
10
11
12
int a = 0; //全局初始化区 
char *p1;//全局未初始化区 
void main(void)
{
  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); //堆 
}

4.用变量a给出下面的定义
A.一个整型数?
答案:int a;

B.一个指向整型数的指针?
答案:int * a;

C.一个指向指针的的指针,它指向的指针是指向一个整型数?
答案:int ** a;

D.一个有10个整型数的数组?
答案:int a[10];

E.一个有10个指针的数组,该指针是指向一个整型数的?
答案:int * a[10];

F.一个指向有10个整型数数组的指针?
答案:int (* a)[10];

G.一个指向函数的指针,该函数有一个整型参数并返回一个整型数?
答案:int (* a)(int);

H.一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数?
答案:int (* a[10])(int);

5.关键字static的作用是什么?
A.在函数体,一个被声明为静态的变量在这一函数被调用过程中维持其值不变;
B.在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的局变量;
C.在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用;

6.关键字const有什么含意?
答案:const意味着”只读”。
如果回答”const意味着常数”,面试官会觉得你只是一个业余的人。

7.下面的声明都是什么意思?
const int a;
int const a;
const int *a;
int * const a;
const int * a;

答案:
A.前两个的作用是一样,a是一个常整型数;
B.第三个意味着a是一个指向常整型数的指针(也就是,整型数是不可修改的,但指针可以);
C.第四个意思a是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的);
D.最后一个意味着a是一个指向常整型数的常指针(也就是说,指针指向的整型数 是不可修改的,同时指针也是不可修改的)。

一个整型数 int a;
一个指向整型数的指针 int * a;
一个指向指针的指针,它指向的指针式指向一个整型数 int * a;
一个有10个整型数的数组 int a[10];
一个有10指针的数组,该指针是指向一个整型数 int * a[10];
一个指向有10个整型数数组的指针 int (
a)[10];
一个指向函数的指针,该函数有一个整型数参数并返回一个整型数 int (* a)(int);
一个有10个指针的数组,该指针指向一个函数,该函数有一个整型数参数并返回一个整型 int (* a[10])(int);

8.关键字volatile有什么含意?
答案:一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。

volatile表示被修饰的符号是易变的。告诉编译器不要随便优化我的代码!!
*一个硬件寄存器
*中断中用到的变量
*线程之间共享变量

1
2
3
4
5
volatile int a = 10;
while((a & 0x01) == 0);
#define P_UART_STATUS ((const volatile unsigned int *)0x88000000);
// volatile表示硬件会修改这个寄存器的内容
// const表示该寄存器只读,写无意义

9.sizeof和strlen的区别?

答案:
sizeof是运算符,在头文件中typedef为unsigned int,其值在编译时即计算好了,参数可以是数组、指针、类型、对象、函数等。
sizeof返回的值表示的含义如下:
数组——编译时分配的数组空间大小;
指针——存储该指针所用的空间大小(存储该指针的地址的长度,是长整型,应该为4)。

而strlen是函数,要在运行时才能计算。参数必须是字符型指针。当数组名作为参数传入时,实际上数组就退化成指针了。
它的功能是:返回字符串的长度。
该字符串可能是自己定义的,也可能是内存中随机的,该函数实际完成的功能是从代表该字符串的第一个地址开始遍历,直到遇到结束符NULL。
返回的长度大小不包括NULL。
strlen所作的仅仅是一个计数器的工作,它从内存的某个位置(可以是字符串开头,中间某个位置,甚至是某个不确定的内存区域)开始扫描,直到碰到第一个字符串结束符’\0’为止,然后返回计数器值(长度不包含’\0’)。

10.在某工程中,要求设置一绝对地址为0x67a9的整型变量的值为0xaa66。编译器是一个纯粹的ANSI编译器。写代码去完成这一任务?

1
2
3
int *p;
p = (int*)0x67a9;
*p = 0xaa66;

一个较晦涩的方法是:

1
  *(int * const)(0x67a9) = 0xaa66;

11.给定一个整型变量a,写两段代码,第一个设置a的bit 3,第二个清除a 的bit 3。在以上两个操作中,要保持其它位不变?

1
2
3
4
5
6
7
8
9
10
11
12
#define BIT3 (0x1 << 3)
static int a;

void set_bit3(void)
{
  a |= BIT3;
}

void clear_bit3(void)
{
  a &= ~BIT3;
}

12.下面函数错误吗?

1
2
3
4
int square(volatile int *ptr)
{
  return *ptr * *ptr;
}

答案:错误!

正确写法:

1
2
3
4
5
6
long square(volatile int *ptr)
{
  int a;
  a = *ptr;
  return a * a;
}

13.嵌入式交叉编译环境有哪两部分组成?简述交叉编译过程。

本地编译可以理解为,在当前编译平台下,编译出来的程序只能放到当前平台下运行。平时我们常见的软件开发,都是属于本地编译;
交叉编译可以理解为,在当前编译平台下,编译出来的程序能运行在体系结构不同的另一种目标平台上,但是编译平台本身却不能运行该程序。
以Linux-arm而言,我们在PC上开发的程序,只能够在Linux平台上能够运行,那我们的终极目的是能够在ARM产品上进行运行。对于这种跨平台而言,就需要交叉编译工具(arm-linux-gcc)进行编译,这样的代码才能够在ARM上进行运行。

至于为什么进行交叉编译?
Speed: 目标平台的运行速度往往比主机慢得多,许多专用的嵌入式硬件被设计为低成本和低功耗,没有太高的性能;
Capability: 整个编译过程是非常消耗资源的,嵌入式系统往往没有足够的内存或磁盘空间;
Availability: 即使目标平台资源很充足,可以本地编译,但是第一个在目标平台上运行的本地编译器总需要通过交叉编译获得;
Flexibility: 一个完整的Linux编译环境需要很多支持包,交叉编译使我们不需要花时间将各种支持包移植到目标板上。

交叉编译链就是为了编译跨平台体系结构的程序代码而形成的由多个子工具构成的一套完整的工具集,一般由编译器、连接器、解释器和调试器组成。
同时,它隐藏了预处理、编译、汇编、链接等细节,当我们指定了源文件(.c)时,它会自动按照编译流程调用不同的子工具,自动生成最终的二进制程序映像(.bin)。

14.互斥量和信号量的区别:

互斥量用于线程的互斥,信号量用于线程的同步。
这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别;

互斥量值只能为0/1,
信号量值可以为非负整数。
也就是说,
一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。
信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量是,也可以完成一个资源的互斥访问;

互斥量的加锁和解锁必须由同一线程分别对应使放用,
信号量可以由一个线程释放,另一个线程得到。

这里区分一下互斥和同步:
互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的;
同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。

15.Linux在某目录下查找某字符串通常如下哪个指令?
解答:Linux的find命令用来在指定目录下查找文件,可以使用-name选项来完成

1
2
find /home -name "*.txt"            //不忽略大小写
find /home -iname "*.txt" //忽略大小写

16.可用于中断下半段处理的处理机制有()。
A.tasklet
B.工作队列
C.软中断
D.双向链表

KEY:
ABC

解答:
上半部是不能中断的,仅仅是响应中断;下半部是可以中断的。
对于适时要求高的,必须放在上半部。
下半部的实现主要是通过软中断、tasklet、工作队列来实现的。

上半部的功能是响应中断。
当中断发生时,它就把设备驱动程序中中断处理例程的下半部挂到设备的下半部执行队列中去,然后继续等待新的中断到来。这样一来,上半部的执行速度就会很快,它就可以接受更多它负责的设备所产生的中断了。上半部之所以快,是因为它是完全屏蔽中断的,如果它没有执行完,其他中断就不能及时地处理,只能等到这个中断处理程序执行完毕以后。所以要尽可能多的对设备产生的中断进行服务和处理,中断处理程序就一定要快。

下半部的功能是处理比较复杂的过程。下半部和上半部最大的区别是可中断,而上半部却不可中断。下半部几乎完成了中断处理程序所有的事情,因为上半部只是将下半部排到了它们所负责的设备中断的处理队列中去,然后就不做其它的处理了。下半部所负责的工作一般是查看设备以获得产生中断的事件信息,并根据这些信息(一般通过读设备上的寄存器得来)进行相应的处理。下半部是可中断的,所以在运行期间,如果其它设备产生了中断,这个下半部可以暂时的中断掉,等到那个设备的上半部运行完了,再回头运行这个下半部。

下半部机制 上下文 复杂度 执行性能 顺序执行保障
软中断 中断 高(需要自己确保软中断的执行顺序及锁机制) 好(全部自己实现,便于调优) 没有
tasklet 中断 中(提供了简单的接口来使用软中断) 中 同类型不能同时执行
工作队列 进程 低(在进程上下文中运行,与写用户程序差不多) 差 没有(和进程上下文一样被调度)

17.关于链表,描述正确的选项有()。
A.在运行时可以动态添加
B.物理空间不连续,空间开销更大
C.查找元素不需要顺序查找
D.可在任意节点位置插入元素

KEY:ABD

18.能在Linux内核态执行的是()。
A.缺页中断
B.时钟中断
C.命令解释
D.进程调度
KEY:ABD

解答:缺页中断和时钟中断都属于中断,在内核态执行。进程调度属于系统调用,在内核态执行,命令解释程序属于命令接口,在用户态执行。

19.整数数组清零:bzero(),memset()。

20.
小端:低位字节数据存储在低地址
大端:高位字节数据存储在低地址

例如:int a=0x12345678;(a首地址为0x2000)
0x2000 0x2001 0x2002 0x2003
0x12 0x34 0x56 0x78 大端格式

21.异步IO和同步IO区别

如果是同步IO,当一个IO操作执行时,应用程序必须等待,直到此IO执行完,
相反,异步IO操作在后台运行,IO操作和应用程序可以同时运行,提高系统性能,提高IO流量;

在同步文件IO中,线程启动一个IO操作然后就立即进入等待状态,直到IO操作完成后才醒来继续执行,
而异步文件IO中,线程发送一个IO请求到内核,然后继续处理其他事情,内核完成IO请求后,将会通知线程IO操作完成了。

22.c的关键字共32个

*数据类型关键字(12)
char,short,int,long,float,double,unsigned,signed,union,enum,void,struct

*控制语句关键字(12)
if,else,switch,case,default,for,do,while,break,continue,goto,return

*存储类关键字(5)
auto,extern,register,static,const

*其他关键字(3)
sizeof,typedef,volatile

23.评价下面的代码片断:

1
2
  unsigned int zero = 0;
  unsigned int compzero = 0xFFFF;

  /* 1’s complement of zero * /
  对于一个int型不是16位的处理器为说,上面的代码是不正确的。应编写如下:

1
  unsigned int compzero = ~0;

24.Norflash与Nandflash的区别
(1)、NAND闪存的容量比较大
(2)、由于NandFlash没有挂接在地址总线上,所以如果想用NandFlash作为系统的启动盘,就需要CPU具备特殊的功能,
如s3c2410在被选择为NandFlash启动方式时会在上电时自动读取NandFlash的4k数据到地址0的SRAM中。
(3)、NAND Flash一般地址线和数据线共用,对读写速度有一定影响。NOR Flash闪存数据线和地址线分开,
所以相对而言读写速度快一些。

25.反码:对原码除符号位外的其余各位逐位取反就是反码
补码:负数的补码就是对反码加1
正数的原码、反码、补码都一样

26.线程、互斥锁、信号量关键函数
pthread_t tid;
pthread_create(&tid,NULL,pthread_func,NULL);//创建线程
pthread_join(tid,NULL);//等待子线程结束,并回收资源
pthread_detach(tid);//与当前进程分离
pthread_exit(NULL);//退出调用线程
pthread_cancel(tid);//取消线程
pthread_mutex mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_init(&mutex,NULL);//初始化一个互斥锁
pthread_mutex_lock(&mutex);//对互斥锁上锁
pthread_mutex_unlock(&mutex);//对互斥锁解锁
sem_t sem;
sem_init(&sem,0,1);//创建信号量并初始化它的值
sem_wait(&sem);//信号量的值减1
sem_post(&sem);//信号量的值加1

27.ROM是只读存储器,掉电不丢失
RAM是读写存储器,掉电丢失

28.signed char 的取值范围-128~127.

29.已知strcpy函数的函数原型是:
char * strcpy(char * strDest, const char * strSrc)。其中,strDest是目的字符串,strSrc是源字符串。
不调用C++/C的字符串库函数,请编写函数strcpy

1
2
3
4
5
6
7
8
char *strcpy(char *strDest, const char *strSrc)
{
int i=0;
if(!(strDest && strSrc))
return;
while(strDest[i++] = *strSrc++);
return strDest;
}

30.关键字static的作用是什么
1)static用来修饰一个局部的变量的时候,
生命域是全局的
作用域是局部的

2)static用来修饰一个模块内的(某一个C的源程序文件)全局变量的时候
生命域不变
作用域减小,只在本模块内有效

3)static用来修饰一个函数的时候
作用域减小,只在本模块内有效

31.位转换

位 8 7 6 5 4 3 2 1
数 v8 v7 v6 v5 v4 v3 v2 v1

转换后:

位 8 7 6 5 4 3 2 1
数 v1 v2 v3 v4 v5 v6 v7 v8

1
2
3
4
5
6
7
8
9
10
11
12
unsigned char bit_reverse(unsigned char c)
{
  unsigned char buf = 0;
  int bit = 8;
  while(bit)
  {
  bit--;
  buf |= ((c & 1) << bit);
  c >>=1;
  }
  return buf;
}

32.字符串倒序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1)、inverted_order(char *p)
{
char *s1,*s2,tem;
s1=p;
s2=s1+strlen(p)-1;
while(s1<s2)
{
tem=*s1;
*s1=*s2;
*s2=tem;
s1++;
s2--;
}
}
1
2
3
4
5
6
7
8
9
10
11
2)、inverted_order(char *p)
{
int len = strlen(src);
char *des = (char *)malloc(len + 1);
char *s = &src[len -1];
char *d = des;
while(len-- != 0)
*d++ = *s--;
*d = 0;
free(des);
}

33.TCP通信建立和结束的过程?端口的作用
三次握手:

四次挥手:

端口是一个软件结构,被客户程序或服务进程用来发送和接收信息。一个端口对应一个16比特的数。服务进程通常使用一个固定的端口。
21端口:21端口主要用于FTP(File Transfer Protocol,文件传输协议)服务。 23端口:23端口主要用于Telnet(远程登录)服务,是Internet上普遍采用的登录和仿真程序。  
25端口:25端口为SMTP(Simple Mail TransferProtocol,简单邮件传输协议)服务器所开放,
主要用于发送邮件,如今绝大多数邮件服务器都使用该协议。  
53端口:53端口为DNS(Domain Name Server,域名服务器)服务器所开放,
主要用于域名解析,DNS服务在NT系统中使用的最为广泛。  
67、68端口:67、68端口分别是为Bootp服务的Bootstrap Protocol Server(引导程序协议服务端)和Bootstrap Protocol Client(引导程序协议客户端)开放的端口。  
69端口:TFTP是Cisco公司开发的一个简单文件传输协议,类似于FTP。  
79端口:79端口是为Finger服务开放的,主要用于查询远程主机在线用户、操作系统类型以及是否缓冲区溢出等用户的详细信息。880端口:80端口是为HTTP(HyperText Transport Protocol,超文本传输协议)开放的,这是上网冲浪使用最多的协议,主要用于在WWW(World Wide Web,万维网)服务上传输信息的协议。  
99端口:99端口是用于一个名为“Metagram Relay”(亚对策延时)的服务,该服务比较少见,一般是用不到的。  
109、110端口:109端口是为POP2(Post Office Protocol Version 2,邮局协议2)服务开放的,
110端口是为POP3(邮件协议3)服务开放的,POP2、POP3都是主要用于接收邮件的。 
111端口:111端口是SUN公司的RPC(Remote Procedure Call,远程过程调用)服务所开放的端口,主要用于分布式系统中不同计算机的内部进程通信,RPC在多种网络服务中都是很重要的组件。  
113端口:113端口主要用于Windows的“Authentication Service”(验证服务)。  
119端口:119端口是为“Network News Transfer Protocol”(网络新闻组传输协议,简称NNTP)开放的。  
135端口:135端口主要用于使用RPC(Remote Procedure Call,远程过程调用)协议并提供DCOM(分布式组件对象模型)服务。  
137端口:137端口主要用于“NetBIOS Name Service”(NetBIOS名称服务)。  
139端口:139端口是为“NetBIOS Session Service”提供的,主要用于提供Windows文件和打印机共享以及Unix中的Samba服务。  
143端口:143端口主要是用于“Internet Message Access Protocol”v2(Internet消息访问协议,简称IMAP)。  
161端口:161端口是用于“Simple Network Management Protocol”(简单网络管理协议,简称SNMP)。  
443端口:43端口即网页浏览端口,主要是用于HTTPS服务,是提供加密和通过安全端口传输的另一种HTTP。  
554端口:554端口默认情况下用于“Real Time Streaming Protocol”(实时流协议,简称RTSP)。  
1024端口:1024端口一般不固定分配给某个服务,在英文中的解释是“Reserved”(保留)。  
1080端口:1080端口是Socks代理服务使用的端口,大家平时上网使用的WWW服务使用的是HTTP协议的代理服务。  
1755端口:1755端口默认情况下用于“Microsoft Media Server”(微软媒体服务器,简称MMS)。  
4000端口:4000端口是用于大家经常使用的QQ聊天工具的,再细说就是为QQ客户端开放的端口,QQ服务端使用的端口是8000。  
5554端口:在今年4月30日就报道出现了一种针对微软lsass服务的新蠕虫病毒——震荡波(Worm.Sasser),该病毒可以利用TCP 5554端口开启一个FTP服务,主要被用于病毒的传播。  
5632端口:5632端口是被大家所熟悉的远程控制软件pcAnywhere所开启的端口。  
8080端口:8080端口同80端口,是被用于WWW代理服务的,可以实现网页浏览。

34.地址解析协议(ARP)的作用是将IP地址转换成物理地址;反地址解析协议(RARP)则负责将物理地址转换成IP地址。

35.已知数组table,用宏求元素个数。

1
  COUNT(table)  (sizeof(table)/sizeof(table[0]));

36.全局变量和局部变量的区别。

全局变量,储存在静态区.进入main函数之前就被创建.生命周期为整个源程序;
局部变量,在栈中分配.在函数被调用时才被创建.生命周期为函数内。

37.死锁的四个条件及处理方法。
(1)互斥条件:一个资源每次只能被一个进程使用。
(2)
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3)
不剥夺条件
:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

解决死锁的方法分为死锁的预防避免检测与恢复三种.

38.进程调度策略。
先进先出算法
最短CPU运行期优先调度算法
轮转法
多级队列方法

39.时间换空间、空间换时间的例子。

  冒泡排序 — 时间换空间
  快速排序,堆排序 — 空间换时间

40.static全局变量与普通的全局变量有什么区别?static局部变量和普通局部变量有什么区别?static函数与普通函数有什么区别?

全局变量(外部变量)的说明之前再加static就构成了静态的全局变量。全局变量本身就是静态存储方式,静态全局变量当然也是静态存储方式。这两者在存储方式上并无不同。
这两者的区别虽在于非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。
而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效。
从以上分析可以看出,把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。
把全局变量改变为静态变量后是改变了它的作用域,限制了它的使用范围。
static函数与普通函数作用域不同。仅在本文件。只在当前源文件中使用的函数应该说明为内部函数(static),内部函数应该在当前源文件中说明和定义。对于可在当前源文件以外使用的函数,应该在一个头文件中说明,要使用这些函数的源文件要包含这个头文件.

static全局变量与普通的全局变量有什么区别:
static全局变量只初使化一次,防止在其他文件单元中被引用

static局部变量和普通局部变量有什么区别:
static局部变量只被初始化一次,下一次依据上一次结果值

static函数与普通函数有什么区别:
static函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝

41.程序的局部变量存在于(堆栈)中,全局变量存在于(静态区)中,动态申请数据存在于(堆)中。

42.软件测试都有哪些种类?

人工测试:个人复查、抽查和会审
机器测试:黑盒测试(针对系统功能的测试 )和白盒测试 (测试函数功能,各函数接口)

43.堆栈溢出一般是由什么原因导致的?
没有回收垃圾资源。

44.写出float x 与“零值”比较的if语句。

1
if(x>0.000001&&x<-0.000001)

45.内存对齐问题的原因?

平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据;
性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐,因为为了访问未对齐的内存,处理器需要做两次内存访问,而对齐的内存访问仅需要一次。

46.比较一下进程和线程的区别?

(1)调度:
线程是CPU调度和分派的基本单位
进程是资源分配的最小单位

(2)拥有资源:
线程自己一般不拥有资源(除了必不可少的程序计数器,一组寄存器和栈),但他可以去访问其所属进程的资源,如进程代码,数据段以及系统资源(已打开的文件,I/O设备等)。

(3)系统开销:
在进程切换的时候,涉及到整个当前进程CPU环境的保存以及新被调度运行的进程的CPU环境的设置;
而线程切换只需要保存和设置少量寄存器的内容,并不涉及存储器管理方面的操作,从而能更有效地使用系统资源和提高系统吞吐量。
可见,进程切换的开销也远大于线程切换的开销。

(4)编程调试:
进程编程调试简单可靠性高,但是创建销毁开销大;
线程正相反,开销小,切换速度快,但是编程调试相对复杂。

(5)进程间不会相互影响;
线程一个线程挂掉将导致整个进程挂掉

进程是死的,只是一些资源的集合,真正的程序执行都是线程来完成的,程序启动的时候操作系统就帮你创建了一个主线程。
每个线程有自己的堆栈。

47.几个关于动态内存的相似案例
(1)

1
2
3
4
5
6
7
8
9
10
11
12
13
void getmemory(char *p)
{
p=(char *) malloc(100);
strcpy(p,"hello world");
}
int main( )
{
char *str=NULL;
getmemory(str);
printf("%s/n",str);
free(str);
return 0;
}

程序崩溃,getmemory中的malloc 不能返回动态内存, free()对str操作很危险.

(2)

1
2
3
4
5
6
7
8
9
10
11
void GetMemory(char *p)
{
 p = (char *)malloc(100);
}
void Test(void)
{
 char *str = NULL;
 GetMemory(str);
 strcpy(str, "hello world");
 printf(str);
}

请问运行Test函数会有什么样的结果?
答:程序崩溃。因为GetMemory并不能传递动态内存,Test函数中的 str一直都是 NULL。strcpy(str, “hello world”);将使程序崩溃。

(3)

1
2
3
4
5
6
7
8
9
10
11
void GetMemory2(char **p, int num)
{
 *p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}

请问运行Test函数会有什么样的结果?
答:(1)能够输出hello
(2)内存泄漏

(4)

1
2
3
4
5
6
7
8
9
10
11
char *GetMemory(void)
{
 char p[] = "hello world";
 return p;
}
void Test(void)
{
 char *str = NULL;
 str = GetMemory();
 printf(str);
}

请问运行Test函数会有什么样的结果?
答:可能是乱码。因为GetMemory返回的是指向“栈内存”的指针,该指针的地址不是 NULL,但其原现的内容已经被清除,新内容不可知。

(5)

1
2
3
4
5
6
7
8
9
10
11
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, “hello”);
free(str);
if(str != NULL)
{
strcpy(str, “world”);
printf(str);
}
}

请问运行Test函数会有什么样的结果?
答:篡改动态内存区的内容,后果难以预料,非常危险。因为free(str);之后,str成为野指针,if(str != NULL)语句不起作用。野指针不是NULL指针,是指向被释放的或者访问受限内存指针。造成原因:指针变量没有被初始化任何刚创建的指针不会自动成为NULL;指针被free或delete之后,没有置NULL;指针操作超越了变量的作用范围,比如要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。

48.进程间通信方式:管道、命名管道、消息队列、共享内存、信号、信号量、套接字。
(1) 管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。
进程的亲缘关系通常是指父子进程关系。
(2) 有名管道 (named pipe) :有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
(3) 信号量( semophore ) :信号量是一个计数器,可以用来控制多个进程对共享资源的访问。
它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。
因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
(4) 消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。
消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
(5) 信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
(6) 共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,
这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,
如信号量,配合使用,来实现进程间的同步和通信。
(7) 套接字( socket ) : 套接字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。

XX.线程间通信方式:临界区、互斥量、信号量、事件。
(1)临界区:通过多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问;
(2)互斥量(synchronized/lock):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所有可以保证公共资源不会被多个线程同时访问。
(3)信号量(semghare):为控制具有有限数量的用户资源而设计的,它允许多个线程在同一时刻去访问同一个资源,但一般需要限制同一时刻访问此资源的最大线程数目。
(4)事件/信号(wait/notify):通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。

49.宏和函数的优缺点?
(1) 函数调用时,先求出实参表达式的值,然后带入形参。而使用带参数的宏只是进行简单的字符替换。
(2) 函数调用是在程序运行时处理的,分配临时的内存单元;
而宏展开则是在编译时进行的,在展开时并不分配内存单元,不进行值的传递处理,也没有“返回值”的概念。
(3) 对函数中的实参和形参都要定义类型,二者的类型要求一致,应进行类型转换;
而宏不存在类型问题,宏名无类型,它的参数也是无类型,只是一个符号代表,展开时带入指定的字符即可。宏定义时,字符串可以是任何类型的数据。
(4) 调用函数只可得到一个返回值,而宏定义可以设法得到几个结果。
(5) 使用宏次数多时,宏展开后源程序长,因为每次展开一次都使程序增长,而函数调用不使源程序变长。
(6) 宏替换不占运行时间,只占编译时间;而函数调用则占运行时间(分配单元、保留现场、值传递、返回)。

50.c和c++的一些不同点(从语言本身的角度)
1)c++源于c,c++最重要的特性就是引入了面向对象机制,class关键字。
2)c++中,变量可以在任何地方声明;c中,局部变量只能在函数开头声明。
3)c++中,const型常量是编译时常量;c中,const常量只是只读的变量。
4)c++有&引用;c没有
5)c++的struct声明自动将结构类型名typedef;c中struct的名字只在结构标签名字空间中,不是作为一种类型出现
6)c语言的main函数可以递归调用;c++中则不可以
7)c中,void * 可以隐式转换成其他指针类型;c++中要求现实转换,否则编译通不过

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

52.线程同步的方法:信号量、条件变量、互斥锁。

53. 关键字volatile有什么含意?并给出三个不同的例子。
一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。
下面是volatile变量的几个例子:

1) 并行设备的硬件寄存器(如:状态寄存器)
2) 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
3) 多线程应用中被几个任务共享的变量

回答不出这个问题的人是不会被雇佣的。我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。
嵌入式系统程序员经常同硬件、中断、RTOS等等打交道,所用这些都要求volatile变量。不懂得volatile内容将会带来灾难。
假设被面试者正确地回答了这是问题(嗯,怀疑这否会是这样),我将稍微深究一下,
看一下这家伙是不是真正懂得volatile完全的重要性。

1) 一个参数既可以是const还可以是volatile吗?解释为什么。
2) 一个指针可以是volatile 吗?解释为什么。
3) 下面的函数有什么错误:

1
2
3
int square(volatile int * ptr) {
return * ptr * * ptr;
}

下面是答案:

1) 是的。一个例子是只读的状态寄存器。
它是volatile因为它可能被意想不到地改变。
它是const因为程序不应该试图去修改它。
2) 是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。
3) 这段代码的有个恶作剧。这段代码的目的是用来返指针ptr指向值的平方,但是,
由于
ptr指向一个volatile型参数,编译器将产生类似下面的代码:

1
2
3
4
5
6
int square(volatile int *ptr) {
int a,b;
a = *ptr;
b = *ptr;
return a * b;
}

由于* ptr的值可能被意想不到地该变,因此a和b可能是不同的。
结果,这段代码可能返不是你所期望的平方值!
正确的代码如下:

1
2
3
4
5
long square(volatile int *ptr) {
int a;
a = *ptr;
return a * a;
}

Volatile 关键字告诉编译器不要持有变量的临时性拷贝。一般用在多线程程序中,
以避免在其中一个线程操作该变量时,将其拷贝入寄存器。
请看以下情形:
A线程将变量复制入寄存器,然后进入循环,反复检测寄存器的值是否满足一定条件(它期待B线程改变变量的值。在此种情况下,当B线程改变了变量的值时,已改变的值对其在寄存器的值没有影响。所以A线程进入死循环。
volatile 就是在此种情况下使用。

54.操作系统的内存分配一般有哪几种方式,各有什么优缺点?
定长和变长。
变长:内存时比较灵活,但是易产生内存碎片。
定长:灵活性差,但分配效率较高,不会产生内存碎片

55.全局变量可不可以定义在可被多个.C文件包含的头文件中?为什么?
答:可以,在不同的C文件中以static形式来声明同名全局变量。可以在不同的C文件中声明同名的全局变量,前提是其中只能有一个C文件中对此变量赋初值,此时连接不会出错

56.ICMP(ICMP协议对于网络安全具有极其重要的意义)功能主要有:
· 侦测远端主机是否存在。
· 建立及维护路由资料。
· 重导资料传送路径。
· 资料流量控制。

57.列举几种进程的同步机制(比较其优缺点,待完善。)
1.原子操作
2.信号量机制
3.自旋锁
4.管程
5.会合
6.分布式系统

58.进程死锁的原因
资源竞争及进程推进顺序非法

59.死锁的处理
1.鸵鸟策略
2.预防策略
3.避免策略
4.检测与解除死锁

60.类的静态成员和非静态成员有何区别?
类的静态成员每个类只有一个
非静态成员每个对象一个

61.软件测试有哪些种类?
黑盒:针对系统功能的测试
白合:测试函数功能,各函数接口

62.Windows消息调度机制:指令堆栈、指令队列、消息队列

63.程序的局部变量存在于(栈)中,全局变量存在于(静态区)中,动态申请数据存在于(堆)中。

64.不使用库函数,编写函数int strcmp(char * source, char * dest) 相等返回0,不等返回-1。

答案1:

1
2
3
4
5
6
7
8
9
10
int strcmp(char *source, char *dest)
{
assert((source!=NULL) && (dest!=NULL));
int i , j;
for( i=0; source[i]==dest[i]; i++);
if (source[i]=='\0' && dest[i]=='\0')
return 0;
else
return -1;
}

答案2:

1
2
3
4
5
6
7
8
9
int strcmp(char *source, char *dest)
{
while ((*source != '\0') && (*source == *dest) && (*dest !== '\0'))
{
source++;
dest++;
}
return ((*source)-(*dest))?-1:0;
}

65.写一函数int fun(char * p)判断一字符串是否为回文,是返回1,不是返回0,出错返回-1。

答案1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int fun(char *p)
{
if(p==NULL)
return -1;
else
{
int length = 0;
int i = 0;
int judge = 0;
length = strlen(p);
for(i=0; i<length/2; i++)
{
if(p[i]!=p[length-1-i])
{
judge = 0;
break;
}
}
if (judge == 0)
return 0;
else
return 1;
}
}

答案2:

1
2
3
4
5
6
7
8
9
10
11
12
13
int fun(char *p)
{
int len = strlen(p)-1;
char *q = p + len;
if(!p)
return -1;
while(p < q)
{
if((*p++)!=(*q--))
return 0;
}
return 1;
}

66.异步IO和同步IO区别
如果是同步IO,当一个IO操作执行时,应用程序必须等待,直到此IO执行完,
相反,异步IO操作在后台运行,IO操作和应用程序可以同时运行,提高系统性能,提高IO流量;

在同步文件IO中,线程启动一个IO操作然后就立即进入等待状态,直到IO操作完成后才醒来继续执行,
而异步文件IO中,线程发送一个IO请求到内核,然后继续处理其他事情,内核完成IO请求后,将会通知线程IO操作完成了。

67. 效率最高的算法
(1)a=b * 2;
(2)a=b/4;
(3)a=b%8;
(4)a=b/8 * 8+b%4;
(5)a=b * 15。

答案:
(1) a=b * 2 -> a=b<<1;
(2) a=b/4 -> a=b>>2;
(3) a=b%8 -> a=b&7;
(4) a=b/8 * 8+b%4 -> a=((b>>3)<<3)+(b&3);
(5) a=b * 15 -> a=(b<<4)-b。

68.内存管理MMU的作用

  • 内存分配和回收
  • 内存保护
  • 内存扩充
  • 地址映射

69.不调用C++/C的字符串库函数,请编写函数strcpy()函数
答案:

1
2
3
4
5
6
7
8
char *strcpy(char *strDest, const char *strSrc)
{
int i = 0;
if(!(strDest && strSrc))
return;
while (strDest[i++] = *strSrc++);
return strDest;
}

70.关键字static的作用是什么

1)static用来修饰一个局部的变量的时候,
生命域是全局的
作用域是局部的

2)static用来修饰一个模块内的(某一个C的源程序文件)全局变量的时候
生命域不变
作用域减小,只在本模块内有效

3)static用来修饰一个函数的时候
作用域减小,只在本模块内有效

71.堆和栈的区别

(1)存储内容不同
栈:在函数调用时,栈中存放的是函数中(最底下是函数调用后的下一条指令)的各个参数(局部变量)。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员分配。

(2)管理方式上不同
栈:由系统自动分配并释放空间。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间,当对应的生存周期结束后栈空间被自动释放。
堆:需要程序员指定大小手动申请和手动释放,在C语言中使用malloc函数申请,使用free函数释放。

(3)空间大小不同
栈:获取空间较小。在Windows下一般大小是1M或2M,当剩余栈空间不足时,分配失败overflow。
堆:获得空间根据系统的有效虚拟内存有关,比较灵活、大。

(4)能否产生碎片不同
栈:不会产生碎片,空间连续。
堆:采用的是链表的存储方式,会产生碎片。

(5)生长方向不同
栈: 向低地址扩展的数据结构,是一块连续的内存区域。
堆: 向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表空闲内存地址来存储的,自然不连续,而链表的遍历方向是由低地址向高地址。

(6)分配方式不同
栈:有2种分配方式:静态分配和动态分配,静态由编译器完成,例如局部变量;动态由malloc函数实现,由编译器进行释放。
堆: 都是动态分配的,没有静态分配的堆。

(7)分配效率不同
栈:由系统自动分配,速度较快。但程序员无法控制。
堆:由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。

72.Volatile与Register的区别

volatile是易变的,不稳定的意思,volatile是关键字,是一种类型修饰符,用它修饰的变量表示可以被某些编译器未知的因素更改,比如操作系统、硬件或者其他线程等,遇到这个关键字声明的变量,编译器对访问该变量的代码不在进行优化,从而可以提供对特殊地址的稳定访问。

状态要经常变化的,为了防止我们编译优化而导致的存取的数据不同步的问题,这时我们就需要用到volatile。
那具体到什么场景下需要用到volatile关键字呢?

1、并行设备的硬件寄存器(如:状态寄存器);
2、一个中断服务子程序中会访问到的非自动变量();
3、多线程应用中被几个任务共享的变量;

上面提到了非自动变量,这里进一步对几种变量做一番解释:
自动变量:是在函数内部定义和使用的变量,它是局部变量。
非自动变量:有两种,一种是全局变量,一种是静态变量。

register这个关键字请求编译器尽可能的将变量存在CPU内部寄存器中,而不是通过内存寻址访问,以提高效率。注意是尽可能,不是绝对。你想想,一个CPU 的寄存器也就那么几个或几十个,你要是定义了很多很多register 变量,它累死也可能不能全部把这些变量放入寄存器吧。

73.并发(concurrency)和并行(parallelism)
并发(concurrency):指宏观上看起来两个程序在同时运行,比如说在单核cpu上的多任务。但是从微观上看两个程序的指令是交织着运行的,你的指令之间穿插着我的指令,我的指令之间穿插着你的,在单个周期内只运行了一个指令。这种并发并不能提高计算机的性能,只能提高效率。

并行(parallelism):指严格物理意义上的同时运行,比如多核cpu,两个程序分别运行在两个核上,两者之间互不影响,单个周期内每个程序都运行了自己的指令,也就是运行了两条指令。这样说来并行的确提高了计算机的效率。所以,现在的cpu都是往多核方面发展。


第二章 好的编程习惯

  1. 变量的声明和初始化的位置应该放在一起
  2. 遵循一致的命名约定。
  3. 描述性变量/方法名称。例如不常见的缩写Sort_PT()可能会令人困惑:PT是什么意思?如果您不能在第二个检查中回答,请优化名称以Sort_PostType()获得更好的理解。
  4. 缩进和换行符小写。
  5. 不要把代码放在一个脚本中。根据主要功能将代码分离成不同的文件。
  6. 太大的switch-case,要考虑使用函数指针数组优化。
  7. 在运算符和变量之间加上空格。

参考文献