经典问题11: 位运算与嵌入式编程相关问题

来源:互联网 发布:as400软件开发工程师 编辑:程序博客网 时间:2024/05/17 20:01

-------------------------------------------------------------------
经典问题11: 位运算与嵌入式编程相关问题
-------------------------------------------------------------------

    (1)面试题:求下列程序的输出结果。
      1    #include <stdio.h>
     2    
     3    int main()
     4    {
     5        printf("%f /n",5);
     6        printf("%d /n",5.01);
     7        printf("%d/n",5.25);
     8        return 0;
     9    }
-------------
结果:
$ ./a.out
-0.995594
1889785610
0
$ ./a.out
-1.272499
1889785610
0
$ ./a.out
-0.012507
1889785610
$ ./a.out
-0.467943 (每次执行不一样)
1889785610
0
--------------
分析:
第一个,   
    5被认为整型,4个字节(32位)所以在内存表现为   
           05 00 00 00 [] [] [] []
           |little-endian—低到高|
           00 00 00 00 [] [] [] []
          |big-endian—高到低--|
    这个数,在小端机器上,现在让你按照浮点数来解释0x[] [] [] [] 00 00 00 05==随机很小的数,就可以解释我每次运行不一下,因为每次运行分配的空间不一样,所以,后面的随机四个数(阶码部分在里面)就不一样; 在大端机器上,现在让你按照浮点数来解释0x 00 00 00 05 [] [] [] [],阶码是0,说明这是个非常很小的负数(近似为0)!    
    
  第二个,   
    5.01被编译器认为是Double型,8字节在内存表现为   
                0a   d7   a3   70   3d   0a   14   40   
    现在这个数按照整型来解释   即:                 
                0a   d7   a3   70         ==         1889785610
知识点:(1)浮点数的存储格式

=====================================
    (2)面试题:In C++,there are four types of Casting Operators,Please enumerate and explain them especially the
    difference.
    答案:
1)static_cast 数制转换。
2)dynamic_cast 用于执行向下转换和在继承之间的转换。
3)const_cast 去掉const。
4)reinterpret_cast 用于执行并不安全的implementation_dependent类型转换。
------
    知识点:
1)reinterpret_cast
该函数将一个类型的指针转换为另一个类型的指针.
这种转换不用修改指针变量值存放格式(不改变指针变量值),只需在编译时重新解释指针的类型就可做到.
reinterpret_cast 可以将指针值转换为一个整型数,但不能用于非指针类型的转换.
例:
//基本类型指针的类型转换
double d=9.2;
double* pd = &d;
int *pi = reinterpret_cast<int*>(pd);  //相当于int *pi = (int*)pd;

//不相关的类的指针的类型转换
class A{};
class B{};
A* pa = new A;
B* pb = reinterpret_cast<B*>(pa);   //相当于B* pb = (B*)pa;

//指针转换为整数
long l = reinterpret_cast<long>(pi);   //相当于long l = (long)pi;


2)const_cast

该函数用于去除指针变量的常量属性,将它转换为一个对应指针类型的普通变量。反过来,也可以将一个非常量的指针变量转换为一个常指针变量。这种转换是在编译期间做出的类型更改。
例:
const int* pci = 0;
int* pk = const_cast<int*>(pci);  //相当于int* pk = (int*)pci;

const A* pca = new A;
A* pa = const_cast<A*>(pca);     //相当于A* pa = (A*)pca;

出于安全性考虑,const_cast无法将非指针的常量转换为普通变量。


3)static_cast

该函数主要用于基本类型之间和具有继承关系的类型之间的转换。
这种转换一般会更改变量的内部表示方式,因此,static_cast应用于指针类型转换没有太大意义。
例:
//基本类型转换
int i=0;
double d = static_cast<double>(i);  //相当于 double d = (double)i;

//转换继承类的对象为基类对象
class Base{};
class Derived : public Base{};
Derived d;
Base b = static_cast<Base>(d);     //相当于 Base b = (Base)d;


4)dynamic_cast

它与static_cast相对,是动态转换。
这种转换是在运行时进行转换分析的,并非在编译时进行,明显区别于上面三个类型转换操作。
该函数只能在继承类对象的指针之间或引用之间进行类型转换。进行转换时,会根据当前运行时类型信息,判断类型对象之间的转换是否合法。dynamic_cast的指针转换失败,可通过是否为null检测,引用转换失败则抛出一个bad_cast异常。
例:
class Base{};
class Derived : public Base{};

//派生类指针转换为基类指针
Derived *pd = new Derived;
Base *pb = dynamic_cast<Base*>(pd);

if (!pb)cout << "类型转换失败" << endl;

//没有继承关系,但被转换类有虚函数
class A(virtual ~A();)   //有虚函数
class B{}:
A* pa = new A;
B* pb  = dynamic_cast<B*>(pa);

如果对无继承关系或者没有虚函数的对象指针进行转换、基本类型指针转换以及基类指针转换为派生类指针,都不能通过编译。

=====================================
    (4)面试题:给定一个整形变量a,写两段代码,第一个设置a的bit3,第二个清楚bit3
.在以上两个操作中,要保持其他位不变。
    最佳解决方案:
     1    #define BIT3 (0X1<<3)
     2    
     3    static int a;
     4    
     5    void set_bit3()
     6        a|=BIT3;
     7        }
     8    void clear_bit3()
     9        a&=~BIT3;
    10        }
=====================================
-------------------------------------------------------------------
经典问题: 位运算与嵌入式编程 ---嵌入式编程
-------------------------------------------------------------------
=====================================
    (1)面试题:Interrupts are an important part of embedded systems. Consequently,many
compliler vendors offer an extension to standard C to support interrupts.Typically, the keword is _interrupt.The
following rutine (IST). Point out the errors in the code.

     1    interrupt double compute_area
     2        (double radius)
     3    {
     4        double area = PI*radius*radius;
     5        printf("/nArea = %f",area);
     6        return area;
     7    }
-----------
答案:(基于机器裸奔时候情况,有操作系统情况,ISR也会有参数和返回值)
    1)ISR不能返回一个值。
    2)ISR不能传递参数。
    3)在许多处理器/编译器中,浮点一般都是不可重入的。
    4)printf()经常有重入和性能是上的问题,所以一般不使用printf()。
=====================================
1.In embedded system,we usually use the keyword “volatile”,what does the keyword
mean?
答案:
一般说来,volatile用在如下的几个地方:
  1、中断服务程序中修改的供其它程序检测的变量需要加volatile;
  2、多任务环境下各任务间共享的标志应该加volatile;
  3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;
  4、并行设备的硬件寄存器(如:状态寄存器);
  5、一个中断服务子程序中会访问到的非自动变量;
  6、多线程应用中被几个任务共享的变量;
  另外,以上这几种情况经常还要同时考虑数据的完整性(相互关联的几个标志读了一半被打断了重写),在1中可以通过关中断来实现,2中可以禁止任务调度,3中则只能依靠硬件的良好设计了。

  volatile 的含义
  volatile总是与优化有关,编译器有一种技术叫做数据流分析,分析程序中的变量在哪里赋值、在哪里使用、在哪里失效,分析结果可以用于常量合并,常 量传播等优化,进一步可以死代码消除。但有时这些优化不是程序所需要的,这时可以用volatile关键字禁止做这些优化,volatile的字面含义是 易变的,它有下面的作用:
 1 )不会在两个操作之间把volatile变量缓存在寄存器中。在多任务、中断、甚至setjmp环境下,变量可能被其他的程序改变,编译器自己无法知道,volatile就是告诉编译器这种情况。
 2) 不做常量合并、常量传播等优化,所以像下面的代码:
volatile int i = 1;
if (i > 0) ...
if的条件不会当作无条件真。
  3) 对volatile变量的读写不会被优化掉。如果你对一个变量赋值但后面没用到,编译器常常可以省略那个赋值操作,然而对Memory Mapped IO的处理是不能这样优化的。
  有人说volatile可以保证对内存操作的原子性,这种说法不大准确,其一,x86需要LOCK前缀才能在SMP下保证原子性,其二,RISC根本不能对内存直接运算,要保 证原子性得用别的方法,如atomic_inc。
  对于jiffies,它已经声明为volatile变量,我认为直接用jiffies++就可以了,没必要用那种复杂的形式,因为那样也不能保证原子性。
  你可能不知道在Pentium及后续CPU中,下面两组指令
inc jiffies
;;
mov jiffies, %eax
inc %eax
mov %eax, jiffies
作用相同,但一条指令反而不如三条指令快。

=====================================
    (2)面试题:Linux程序的内存结果以及堆和栈的区别
知识点:(两点)
一、程序的内存空间
一个典型的Linux C程序内存空间由如下几部分组成:
代码段(.text)。这里存放的是CPU要执行的指令。代码段是可共享的,相同的代码在内存中只会有一个拷贝,同时这个段是只读的,防止程序由于错误而修改自身的指令。
初始化数据段(.data)。这里存放的是程序中需要明确赋初始值的变量,例如位于所有函数之外的全局变量:int val=100。需要强调的是,以上两段都是位于程序的可执行文件中,内核在调用exec函数启动该程序时从源程序文件中读入。
未初始化数据段(.bss)。位于这一段中的数据,内核在执行该程序前,将其初始化为0或者null。

例如出现在任何函数之外的全局变量:int sum;
堆(Heap)。这个段用于在程序中进行动态内存申请,例如经常用到的malloc,new系列函数就是从这个段中申请内存。
栈(Stack)。函数中的局部变量以及在函数调用过程中产生的临时变量都保存在此段中。

二、堆和栈的区别
2.1申请方式
stack:
由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间
heap:
需要程序员自己申请,并指明大小,在c中malloc函数
如p1 = (char *)malloc(10);
在C++中用new运算符
如p2 = (char *)malloc(10);
但是注意p1、p2本身是在栈中的。  

2.2 申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时, 会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大 小,系统会自动的将多余的那部分重新放入空闲链表中。
2.3申请大小的限制
栈:在Windows下,栈是向低地址扩展的数据结 构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是 一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

2.4申请效率的比较:
栈由系统自动分配,速度较快。但程序员是无法控制的。
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈,是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。
2.5堆和栈中的存储内容
栈: 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
2.6存取效率的比较 :
char s1[] = "aaaaaaaaaaaaaaa";
char *s2 = "bbbbbbbbbbbbbbbbb";
aaaaaaaaaaa是在运行时刻赋值的;
而bbbbbbbbbbb是在编译时就确定的;
但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。
比如:
#include <stdio.h>
void main()
{
    char a = 1;
    char c[] = "1234567890";
    char *p ="1234567890";
    a = c[1];
    a = p[1];
    return;
}
对应的汇编代码
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读取字符,显然慢了。

2.7小结:
堆和栈的区别可以用如下的比喻来看出:
使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。 使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。
=====================================
    (2)面试题:一个参数可以既是const又是volatile吗?一个指针可以是volatile吗?解释为什么?
答案:
    第一个问题:是的。一个例子就是只读的状态寄存器。它是volatile ,因为它可能被随意想不到的地方改变;它又是const,因为程序不应该试图去修改它。
    第二个问题:是的。尽管这并不常见。一个例子是当一个中断服务程序修改一个指向一个buffer的指针时。
=====================================
    (3)面试题:评价下面的代码片断,找出其中的错误。
unsigned int zero = 0;
unsigned int compzero = 0xFFFF;
/*1's complement of zero */

分析:
对于一个int型不是16位的处理器为说,上面的代码是不正确的。应编写如下:
unsigned int compzero = ~0;
unsigned int compzero = 0xFFFF;
只写了2个字节,16位的才符合
32位的可以写:
unsigned int compzero = 0xFFFFFFFF;

但unsigned int compzero = ~0;更安全,不管有多少位,直接取反,把所有的0都变成1。    
=====================================
-------------------------------------------------------------------
经典问题: 位运算与嵌入式编程 --- static
-------------------------------------------------------------------
=====================================
    (1)面试题:关键字static的作用是什么?
答案:
1) 在函数体内,static变量的作用域为该函数体,该变量的内存只被分配一次,因此,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。
2) 在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变量。
3) 在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用。
4)在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝。
5)在类中的static成员函数属于整个类所拥有,这个函数不接受this指针,因而只能访问类的static成员变量。
----------
知识点:
    用static声明的函数和变量小结
static 声明的变量在C语言中有两方面的特征:
  1)、变量会被放在程序的全局存储区中,这样可以在下一次调用的时候还可以保持原来的赋值。这一点是它与堆栈变量和堆变量的区别。
  2)、变量用static告知编译器,自己仅仅在变量的作用范围内可见。这一点是它与全局变量的区别。

Tips:
    A.若全局变量仅在单个C文件中访问,则可以将这个变量修改为静态全局变量,以降低模块间的耦合度;
    B.若全局变量仅由单个函数访问,则可以将这个变量改为该函数的静态局部变量,以降低模块间的耦合度;
    C.设计和使用访问动态全局变量、静态全局变量、静态局部变量的函数时,需要考虑重入问题;
    D.如果我们需要一个可重入的函数,那么,我们一定要避免函数中使用static变量(这样的函数被称为:带“内部存储器”功能的的函数)
      E.函数中必须要使用static变量情况:比如当某函数的返回值为指针类型时,则必须是static的局部变量的地址作为返回值,若为auto类型,则返回为错指针。
    函数前加static使得函数成为静态函数。但此处“static”的含义不是指存储方式,而是指对函数的作用域仅局限于本文件(所以又称内部函数)。使用内部函数的好处是:不同的人编写不同的函数时,不用担心自己定义的函数,是否会与其它文件中的函数同名。
扩展分析:
     术语static有着不寻常的历史.起初,在C中引入关键字static是为了表示退出一个块后仍然存在的局部变量。

     随后,static在C中有了第二种含义:用来表示不能被其它文件访问的全局变量和函数。为了避免引入新的关键字,所以仍使用static关键字来表示这第二种含义。最后,C++重用了这个关键字,并赋予它与前面不同的第三种含义:表示属于一个类而不是属于此类的任何特定对象的变量和函数(与Java中此关键字的含义相同)。
=====================================