文章详情

  • 游戏榜单
  • 软件榜单
关闭导航
热搜榜
热门下载
热门标签
php爱好者> php文档>c/c++ 笔试题

c/c++ 笔试题

时间:2010-06-04  来源:280552108

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();我是一点都不理解, 我原以为程序在这里报异常, 谁知却可以运行出让人目瞪口呆的结果出来.
我之所以有将报异常的印象, 是因为<<more effective c++>>上说专用于类类别转换的dynamic_cust会自动判断dynamic_cust是否成功, 对于指针来说如果结果是不能转换, 则指针变为NULL, 对于&引用则会抛异常出来.  而(B*)与dynamic<B*>() 的作用其实是相同的, 所以我以为二者会有相同的外在表现.

而实际上(B*)这种C语言的强制转换是缺乏这种是否可转的检查的, 因此程序能运行出结果出来, 但实际上这种结果只是全部可能的异常情况中看起来最正常的一种, 该段程序其它的可能包括导致程序崩溃等等.
后来我把那一段强制转换改成 :B* bp = dynamic_cast<B*>(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虚函数指针有关系 <<inside C++ object model>>中对此有深入描述, 看来确实有必要读一下这本书了.......

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)因为函数调用的本质是跳转到某一个地址单元的code去执行,所以可以"调用"一个根本就不存在的函数实体,晕?请往下看:

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

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

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

  记住:函数无它,唯指令集合耳;你可以调用一个没有函数体的函数,本质上只是换一个地址开始执行指令!
相关阅读 更多 +
排行榜 更多 +
辰域智控app

辰域智控app

系统工具 下载
网医联盟app

网医联盟app

运动健身 下载
汇丰汇选App

汇丰汇选App

金融理财 下载