經典問題11: 位運算與嵌入式編程相關問題

-------------------------------------------------------------------
經典問題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中此關鍵字的含義相同)。
=====================================

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章