Chinaunix首页 | 论坛 | 博客
  • 博客访问: 152037
  • 博文数量: 64
  • 博客积分: 2545
  • 博客等级: 少校
  • 技术积分: 692
  • 用 户 组: 普通用户
  • 注册时间: 2006-11-22 20:00
文章分类

全部博文(64)

文章存档

2011年(3)

2009年(51)

2008年(10)

我的朋友

分类: C/C++

2009-06-27 11:42:26

c/c++ 笔试题笔记

1.关键字volatile有什么含意?并给出三个不同的例子。


一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:
1) 并行设备的硬件寄存器(如:状态寄存器)
2) 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
3) 多线程应用中被几个任务共享的变量

回答不出这个问题的人是不会被雇佣的。我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。搞嵌入式的家伙们经常同硬件、中断、RTOS等等打交道,所有这些都要求用到volatile变量。不懂得volatile的内容将会带来灾难。
假设被面试者正确地回答了这是问题(嗯,怀疑是否会是这样),我将稍微深究一下,看一下这家伙是不是直正懂得volatile完全的重要性。
1)一个参数既可以是const还可以是volatile吗?解释为什么。
2); 一个指针可以是volatile 吗?解释为什么。
3); 下面的函数有什么错误:

int square(volatile int *ptr)
{
        return *ptr * *ptr;
}

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

int square(volatile int *ptr)
{
    int a,b;
    a = *ptr;
    b = *ptr;
    return a * b;
}

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

long square(volatile int *ptr)
{
    int a;
    a = *ptr;
    return a * a;
}

注:volatile可用来在多线程中同步。


2.中断(Interrupts)

11. 中断是嵌入式系统中重要的组成部分,这导致了很多编译开发商提供一种扩展—让标准C支持中断。具代表事实是,产生了一个新的关键字 __interrupt。下面的代码就使用了__interrupt关键字去定义了一个中断服务子程序(ISR),请评论一下这段代码的。

__interrupt double compute_area (double radius)
{
    double area = PI * radius * radius;
    printf("\nArea = %f", area);
    return area;
}

这个函数有太多的错误了,以至让人不知从何说起了:
1)ISR 不能返回一个值。如果你不懂这个,那么你不会被雇用的。
2) ISR 不能传递参数。如果你没有看到这一点,你被雇用的机会等同第一项。
3) 在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额处的寄存器入栈,有些处理器/编译器就是不允许在ISR中做浮点运算。此外,ISR应该是短而有效率的,在ISR中做浮点运算是不明智的。
4) 与第三点一脉相承,printf()经常有重入和性能上的问题。如果你丢掉了第三和第四点,我不会太为难你的。不用说,如果你能得到后两点,那么你的被雇用前景越来越光明了。

注:中断服务子程序越简洁越好。


3.动态内存分配(Dynamic memory allocation)

14. 尽管不像非嵌入式计算机那么常见,嵌入式系统还是有从堆(heap)中动态分配内存的过程的。那么嵌入式系统中,动态分配内存可能发生的问题是什么?
这 里,我期望应试者能提到内存碎片,碎片收集的问题,变量的持行时间等等。


4.写出判断ABCD四个表达式的是否正确, 若正确, 写出经过表达式中 a的值(3分)
int a = 4;
(A)a += (a++); (B) a += (++a) ;(C) (a++) += a;(D) (++a) += (a++);
a = ?
答:C错误,左侧不是一个有效变量,不能赋值,可改为(++a) += a;
改后答案依次为9,10,10,11

注:为什么++a是左值,而a++不是????


5.在C++ 程序中调用被 C 编译器编译后的函数,为什么要加 extern “C”声明?
答:函数和变量被C++编译后在符号库中的名字与C语言的不同,被extern "C"修饰的变
量和函数是按照C语言方式编译和连接的。由于编译后的名字不同,C++程序不能直接调
用C 函数。C++提供了一个C 连接交换指定符号extern“C”来解决这个问题。


6.函数模板与类模板有什么区别?
答:函数模板的实例化是由编译程序在处理函数调用时自动完成的,而类模板的实例化
必须由程序员在程序中显式地指定。


7.TCP/IP 建立连接的过程?(3-way shake)
答:在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接。
  第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状
态,等待服务器确认;
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个
SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
  第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1)
,此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

8.动态连接库的两种方式?
答:调用一个DLL中的函数有两种方法:
1.载入时动态链接(load-time dynamic linking),模块非常明确调用某个导出函数
,使得他们就像本地函数一样。这需要链接时链接那些函数所在DLL的导入库,导入库向
系统提供了载入DLL时所需的信息及DLL函数定位。
2.运行时动态链接(run-time dynamic linking),运行时可以通过LoadLibrary或Loa
dLibraryEx函数载入DLL。DLL载入后,模块可以通过调用GetProcAddress获取DLL函数的
出口地址,然后就可以通过返回的函数指针调用DLL函数了。如此即可避免导入库文件了

9.引用与指针有什么区别?
    1) 引用必须被初始化,指针不必。
    2) 引用初始化以后不能被改变,指针可以改变所指的对象。
    3) 不存在指向空值的引用,但是存在指向空值的指针。

10.IP地址的编码分为哪俩部分?
     IP地址由两部分组成,网络号和主机号。不过是要和“子网掩码”按位与上之后才能区分哪些是网络位哪些是主机位。

11.字符串常量 Vs 字符数组字符串
 

char *RetMemory(void)
{
     char *p = "hello world";
     char s[] = "hello world";
     int i;
     printf("p:%p, s:%p, i:%p\n", p, s, &i);
     return p;
}

p:0046C04C, s:0012FEF0, i:0012FEEC

注:字符串常量和字符数组字符串存储的位置不一样,字符数组是局部变量,函数结束后就不存在。


??12. 位域

typedef struct
  {
     int a:2;
     int b:2;
     int c:1;
  }test;

  test t;
  t.a = 1;
  t.b = 3;
  t.c = 1;

  printf("%d",t.a);
  printf("%d",t.b);
  printf("%d",t.c);



t.a为01,输出就是1
t.b为11,输出就是-1
t.c为1,输出也是-1
3个都是有符号数int嘛。
这是位扩展问题
01
11
1
编译器进行符号扩展


??13. 求1000!的未尾有几个0(用素数相乘的方法来做,如72=2*2*2*3*3);
求出1->1000里,能被5整除的数的个数n1,能被25整除的数的个数n2,能被125整除的数的个数n3,
能被625整除的数的个数n4.
1000!末尾的零的个数=n1+n2+n3+n4;


??14优先级反转问题在嵌入式系统中是一中严重的问题,必须给与足够重视。
a) 首先请解释优先级反转问题
b) 很多RTOS提供优先级继承策略(Priority inheritance)和优先级天花板策略(Priority ceilings)用来解决优先级反转问题,请讨论这两种策略。

答:高优先级任务需要等待低优先级任务释放资源,而低优先级任务又正在等待中等优先级任务的现象叫做优先级反转。
  优先级继承策略(Priority inheritance):继承现有被阻塞任务的最高优先级作为其优先级,任务退出临界区,恢
复初始优先级。
  优先级天花板策略(Priority ceilings):控制访问临界资源的信号量的优先级天花板。
  优先级继承策略对任务执行流程的影响相对教小,因为只有当高优先级任务申请已被低优先级任务占有的临界资源
这一事实发生时,才抬升低优先级任务的优先级。


15. downcasting

class A{
public:
        void print1(){
                printf("1\n");
        }
        virtual void print2(){
                printf("2\n");
        }
};

class B : public A{
public:
        void print1(){
                printf("3\n", s);
        }
        virtual void print2(){
                printf("4\n", s);
        }
};

void print(){
    A* ap = new A();
    ap->print1();
    ap->print2();

    delete ap;
    ap = new B();
    ap->print1();
    ap->print2();

    delete ap;
    B* bp = (B*)new A();
    bp->print1();
    bp->print2();
}
以上程序的输出为:
1
2
1
4
3
2
这里面最后这个B* bp = (B*)new A();我是一点都不理解, 我原以为程序在这里报异常, 谁知却可以运行出让人目瞪口呆的结果出来.
我 之所以有将报异常的印象, 是因为<>上说专用于类类别转换的dynamic_cust会自动判断dynamic_cust是否成功, 对于指针来说如果结果是不能转换, 则指针变为NULL, 对于&引用则会抛异常出来.  而(B*)与dynamic() 的作用其实是相同的, 所以我以为二者会有相同的外在表现.

而实际上(B*)这种C语言的强制转换是缺乏这种是否可转的检查的, 因此程序能运行出结果出来, 但实际上这种结果只是全部可能的异常情况中看起来最正常的一种, 该段程序其它的可能包括导致程序崩溃等等.
后来我把那一段强制转换改成 :B* bp = dynamic_cast(new A());  之后, 发现程序运行时报野指针错误, "非法的对0x00000000的访问". 然后我在强制转换后加了一个判断
if(bp == NULL){
    printf("cust not allowed!\n" );
}
这才明显地看出来, dynamic_cast确实把该指针批向NULL了.   也就是说, 考试题中的代码实际上是不合法的.

另外我又更进一步做了如下的试验,
class C{
public:
        void print1(){
                printf("5\n");
        }
        virtual void print2(){
                printf("6\n");
        }
};

C* pc = (C*) new A();    //这种写法比考试题更无耻
pc->print1();
pc->print2();
程序居然还可以运行, 而且结果输出为:
5
2
唉, 看到这种结果我真是觉得有些无法解释. 应该是跟指向函数表的vptr虚函数指针有关系 <>中对此有深入描述, 看来确实有必要读一下这本书了.......

20080707 注释:看过more effective c++ item24之后,对最后这个问题又有了新的理解。这种对指针的转型B* bp = (B*) new A(); 对于这当前的程序来说,完全是可以正常运行的,但也属于打擦边球,如果B对象中多一个 int i; 那么这个程序运行的结果就不可预测了。
       先说为什么现成这个程序可以正常运行。对于B来说print1非virtual函数,则编译器会直接根据指针bp的类型B*将其与B::print1() 连接起来。而对于print2, 则会根据vptr 虚函数指针去虚函数表去寻找准确的函数,然后动态绑定时发现bp所指的对象其实是A,因此就把bp->print2()指向对 A::print2()的调用。对于后台的与A完全无关的C类也可以强制转换并调用,其实道理也是完全一样的。
        再说为什么随遍添加几个类成员就可能让程序运行结果不可预测。A* ap = new A(), 导致在堆上分配了4个字节,这四个字节存储了vptr。而如果B中有一个int成员变量的话,则sizeof(B)=8, 而B* bp = (B*) ap导致进行了一个强制转换,指向堆上4个字节A对象的指针被强制标记为指向堆上8个字节B对象的指针。这样一来,程序就把4个字节上方4个节也也私自 (相对于光明正大的new 或者malloc操作)据为已有,当程序试图操作这本不属于他的4个字节时,其结果是不可预料的。另外当前这个程序能否正常运行也依赖于编译器的实现,这 个“非法的“程序由于不符合C++语法,因此无法保证它可以在全部依照C++标准而实现的编译器下编译并运行。从dynamic_cast操作的结果来 看,C++对于此种强制转换也是拒绝的,因为cast的结果是一个NULL指针。



16. 函数指针


  首先要理解以下三个问题:

  (1)C语言中函数名直接对应于函数生成的指令代码在内存中的地址,因此函数名可以直接赋给指向函数的指针;

  (2)调用函数实际上等同于"调转指令+参数传递处理+回归位置入栈",本质上最核心的操作是将函数生成的目标代码的首地址赋给CPU的PC寄存器;

  (3)因为函数调用的本质是跳转到某一个地址单元的去执行,所以可以"调用"一个根本就不存在的函数实体,晕?请往下看:

  请拿出你可以获得的任何一本大学《原理》教材,书中讲到,186 CPU启动后跳转至绝对地址0xFFFF0(对应C语言指针是0xF000FFF0,0xF000为段地址,0xFFF0为段内偏移)执行,请看下面的代码:

typedef void (*lp) ( ); /* 定义一个无参数、无返回类型的 */
/* 函数指针类型 */
lp lpReset = (lp)0xF000FFF0; /* 定义一个函数指针,指向*/
/* CPU启动后所执行第一条指令的位置 */
lpReset(); /* 调用函数 */

在以上的程序中,我们根本没有看到任何一个函数实体,但是我们却执行了这样的函数调用:lpReset(),它实际上起到了"软重启"的作用,跳转到CPU启动后第一条要执行的指令的位置。

  记住:函数无它,唯指令集合耳;你可以调用一个没有函数体的函数,本质上只是换一个地址开始执行指令!
阅读(275) | 评论(0) | 转发(0) |
0

上一篇:C/C++ interview1

下一篇:interview C/C++3

给主人留下些什么吧!~~