1. 程式人生 > >[C/C++筆試面試題] 程式設計基礎 - 位操作、函式、陣列篇

[C/C++筆試面試題] 程式設計基礎 - 位操作、函式、陣列篇

7 位操作

二進位制是現代計算機發展的基礎,所有的程式程式碼都需要轉換成最終的二進位制程式碼才能執 行。合理地進行二進位制的位操作,對於編寫優質程式碼,特別是嵌入式應用軟體開發非常關鍵。

7.1 一些結構宣告中的冒號和數字是什麼意思?

c語言的結構體可以實現位段,它的定義形式是在一個定義的結構體成員後面加上冒號, 然後是該成員所佔的位數。位段的結構體成員必須是int或者unsigned int型別,不能是其他型別。位段在記憶體中的儲存方式是由具體的編譯器決定的。

示例程式如下:

#include <stdio.h> 

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

int main()
{
    test t; 
    
    t.a = 1; 
    t.b = 3; 
    t.c = 1;
    printf("%d\n%d\n%d\n%d\n",t.a, t.b, t.c, sizeof(test)); 
    
    return 0;
}

輸出結果如下:

1
-1
-1
4

由於a佔兩位,而a被賦值為1,二進位制就是01,因此%d輸出的時候輸出1; b也佔了兩 位,賦值為3,二進位制也就是11,由於使用了%d輸出,表示的是將這個b作為有符號int型來輸出,這樣的話二進位制的11將會有一位被認為是符號位,並且兩位的b也會被擴充套件為int類 型,也就是4位元組,即32位。

其實a也做了這種擴充套件,只是擴充套件符號位的時候,由於數字在計算機中儲存都是補碼形式,因此擴充套件符號位的時候正數用0填充高位,負數則用1填充高位。因此對於a來說,輸出的時候被擴充套件為00000000 00000000 00000000 00000001,也就是 1,而b則擴充套件為11111111 11111111 11111111 11111111,也就是-1了,c的顯示也是這樣的。


7.2 如何快速求取一個整數的7倍?

可以先將此整數左移3位(相當於將數字 乘以8),然後再減去原值,即(x<<3)-x就獲得了x的7倍。此處需要注意的是,由於-的優先順序高於<<,所以不能去掉括號,否則結果不正確。


7.3 如何實現位操作求兩個數的平均值?

一般而言,求解平均數的方法就是將兩者相加,然後除以2,以變數x與y為例,兩者的平均數為 (x+y)/2。

但是採用上述方法,會存在一個問題,當兩個數比較大時,如兩者的和大於了機器位數能夠表示的最大值,可能會存在資料溢位的情況,而採用位運算方法則可以避免這一問題, (x&y)+((x^y)>>1)方式表達的意思都是求解變數x與y的平均數,而且位運算相比除法運算, 效率更高。

示例程式如下:

#include <stdio.h>

int main()
{
    int x = 2147483647, y = 2147483647;
    
    printf("%d\n",(x+y)/2); 
    printf("%d\n",(x&y)+((x^y)>>1)); 
    
    return 0;
}

在32位機器下,程式輸出結果如下:

-1

2147483647

程式的輸出正好驗證了這一演算法的可行性。


引申:如何利用位運算計算數的絕對值?

以X為負數為例來分析。因為在計算機中,數字都是以補碼的形式存放的,求負數的絕對值,應該是不管符號位,執行按位取反,末位加1操作即可。

對於一個負數,將其右移31位後會變成0xffffffff,而對於一個正數而言,右移31位則為 0x00000000,而0xffffffff^x + x = -1,因為 1011 ^ 1111 =0100 ,任何數與 1111 異或,其實質都是把x的0和1進行顛倒計算。如果用變數y表示x右移31位,(x^y)-y則表示的是x的絕對值。

程式示例如下:

#include<stdio.h> 

int MyAbs( int x)
{
    int y;
    y = x >> 31 ; //若x為正數,則y為0;若x為負數,則x為-1
    return (x^y)-y ;//此處還可以寫為(x+y)^y
}

int main()
{
    printf("%d\n", MyAbs(2)); 
    printf("%d\n", MyAbs(-2));
    
    return 0;
}

程式輸出結果:

2
2

上例中,在函式MyAbs中,對區域性變數y進行賦值時,由於是對x進行右移31位,如果 x為正數,則y=0;如果x為負數,則y = -1。


7.4 unsigned int i=3; printf("%u\n", i*-1)輸出為多少?

示例程式如下:

#include <stdio.h> 

int main()
{
    unsigned int i=3; 
    printf("%u\n",i*-l); 
    
    return 0;
}

程式輸出結果:

4294967293

在32位機器中,i*-l的值為4294967293。在32位機器中,無符號int的值域是[0,4294967295],有符號int的話,值域是[-2147483648,2147483647],兩個值域的個數都是4294967296個,即

[0,2147483647]U[2147483648,4294967295] = [0,4294967295]

有符號int的[-2147483648,-1]對應於無符號int的[2147483648,4294967295]區域,兩個區域的值是一一對映關係。所以,-1對應4294967295,-2對應4294967294, -3對應4294967293。


7.5 如何求解整型數的二進位制表示中1的個數?

求解整型數的二進位制表示中1的個數有以下兩種方法: 方法一,程式程式碼如下:

#include <stdio.h> 

int func(int x)
{
    int countx = 0; 
    while(x)
    {

        countx++; 
        x = x&(x-1);
    }

    return countx;
}

int main()
{
    printf("%d\n", func(9999)); 
    
    return 0;
}

程式輸出結果:

8

在上例中,函式func()的功能是將x轉化為二進位制數,然後計算該二進位制數中含有的1 的個數。首先以9為例來分析,9的二進位制為1001,8的二進位制為1000,兩者執行&操作之後 結果為1000,此時1000再與0111 (7的二進位制位)執行&操作之後結果為0。

為了理解這個演算法的核心,需要理解以下兩個操作:

(1) 當一個數被減1時,它最右邊的那個值為1的bit將變為0,同時其右邊的所有的bit 都會變成1。

(2) “&=”,位與並賦值操作。去掉已經被計數過的1,並將該值重新設定給n。這個演算法迴圈的次數是bit位為1的個數。也就說,有幾個bit為1,迴圈幾次,對bit為1比較稀疏的數來說,效能很好。例如,0x1000 0000迴圈一次就可以。

方法二,判斷每個數的二進位制表示中每一位是否為1,如果為1,就在count上加1,而迴圈的次數是常數,即n的位數。但該方法有一個缺陷,就是在1比較稀疏的時候效率會比較低。
程式示例如下:

#include <stdio.h> 

int func (int n)
{
    int count=0; 
    
    while (n)
    {
        count += n & Ox1u ;
        n >>= 1 ;
    }
    
    return count;
}

int main()
{
    printf("%d\n",func(9999)); 
    
    return 0;
}

程式輸出結果:

8


7.6 不能用sizeof()函式,如何判斷作業系統是16位還是32位的?

如果沒有強調不許使用sizeof, —般可以使用sizeof計算位元組長度來判斷作業系統的位 數,如在32位機器上,sizeof(int) = 4,而在16位機器上,sizeof(int)=2。除此之外,還有以下兩種方法。

方法一:一般而言,機器位數不同,其表示的數字的最大值也不同,根據這一特性,可以 判斷作業系統的位數。

例如,執行如下程式碼:

#include <stdio.h> 

int main()
{
    int i = 65536; 
    printf("%d\n",i); 
    int j = 65535; 
    printf("%d\n",j); 
    
    return 0;
}

由於16位機器下,無法表示這麼大的數,會出現越界情況,所以程式輸出為

0
1

而在32位機器下,則會正常輸出,程式輸出為 65536 65535。之所以會有區別,是因為在16位機器下,能夠表示的最大數為65535,所以會存在最高位溢位的情況。當變數的值為65536時,輸出為0;當變數的值為65535時,輸出為-1。而在 32位機器上,則不會出現溢位的情況,所以輸出為正常輸出。

 

方法二:對0值取反,不同位數下的0值取反,其結果不一樣。例如,在32位機器下, 按位取反運算,結果為11111111111111111111111111111111。執行如下程式碼:

#include <stdio.h> 

int main()
{
    unsigned int a =〜0; 
    
    if( a>65536)
        printf("32 位\11");
    else
        printf(”16 位\11"); 
        
    return 0;
}

程式輸出為:

32位


7.7 嵌人式程式設計中,什麼是大端?什麼是小端?

大端模式,是指資料的高位元組儲存在記憶體的低地址中,而資料的低位元組儲存在記憶體的高地址中。小端模式,是指資料的高位元組儲存在記憶體的高地址中,而資料的低位元組儲存在記憶體的低地址中。如下圖所示:

引申:如何判斷計算機處理器是大端還是小端?

方法一:可以通過指標地址來判斷,由於在32位計算機系統中,int佔4個位元組,char佔1個 位元組,所以可以採用如下做法實現該判斷。示例程式如下:

#include <iostream>
#include <stdio.h>

using namespace std;

int fun()
{
    int num = 0x12345678;

    // (char*)&num獲得num的起始地址,*((char*)&num)是num的起始地址所指向的值。
    return (*((char*)&num) == 0x12 )?1:0; // 本機返回1,為大端 返回0,為小端
}

int main()
{
    cout << fun() << endl;
}

方法二:聯合體union的存放順序是所有成員都從低地址開始存放。

#include <iostream>
#include <stdio.h>

using namespace std;

//判斷系統是大端還是小端:通過聯合體,因為聯合體的所有成員都從低地址開始存放
int fun()
{
    union test
    {
        int i;
        char c;
    };

    test t;
    t.i = 1;

    //如果是大端,則t.c為0x00,則t.c!=1,返回0 是小端,則t.c為0x01,則t.c==1,返回1
    return (t.c==1);
}

int main()
{
    cout << fun() << endl;
}


7.8 怎麼樣寫一個接受可變引數的函式?

C語言中支援函式呼叫的引數為變參形式。例如,printf()這個函式,它的函式原型是int printf( const char* format, ...),它除了有一個引數format固定以外,後面跟的引數的個數和型別都是可變的,可以有以下多種不同的呼叫方法:

(1) printf("%d",i);

(2) printf("%s",s);

(3) printf("the number is %d ,string is:%s", i, s);

printf()函式是一個有著變參的庫函式,在C語言中,程式設計師也可以根據實際的需求編寫 變參函式。如下程式示例程式碼,實現了一個變參函式add2(),該函式實現多引數求和運算。

#include <stdio.h>

//num使用int型別也可以,這裡只是為了節省記憶體
int add2(char num,...)
{
    int sum = 0;
    int index = 0;

    int *p = (int *)&num+1;
    for (; index<(int)num; index++)
    {
        sum += *p++;
    }

    return sum;
}

int main()
{
    int i = 1;
    int j = 2;
    int k = 3;

    printf("%d\n",add2(3,i,j,k));

    return 0;    
}

程式輸出結果:

6


8 函式

函式是程式的基本組成單位,利用函式,不僅能夠實現程式的模組化,而且簡單、直觀, 能極大地提高程式的易讀性和可維護性,所以將程式中的一些計算或操作抽象成函式以供隨時呼叫,理解函式的執行原理以及應用是一個優秀程式設計師應該具備的基本能力。

 

8.1 函式模板/模板函式、類模板/模板類?

(1) 函式模板/模板函式。

函式模板是對一批模樣相同的函式的說明描述,它不是某一個具體的函式;而模板函式則是將函式模板內的“資料型別引數”具體化後得到的過載函式(就是由模板而來的函式)。簡單地說,函式模板是抽象的,而模板函式則是具體的。

函式模板減少了程式設計師輸入程式碼的工作量,是C++中功能最強的特性之一,是提高軟體程式碼重用性的重要手段之一。函式模板的形式一般如下所示:

 template <模板型別形參表>

<返回值型別> <函式名>(模板函式形參表)
{
    //函式體
}

其中 <模板函式形參表> 的型別可以是任何型別,包括基本資料型別和類型別。需要注意的是,函式模板並不是一個實實在在的函式,它是一組函式的描述,它並不能直接執行,需要例項化為模板函式後才能執行,而一旦資料型別形參例項化以後,就會產生一個實實在在的模板函數了。

(2) 類模板/模板類。

類模板與函式模板類似,將資料型別定義為引數,描述了程式碼類似的部分類的集合,具體化為模板類後,可以用於生成具體的物件。

template <型別引數表> 

class<類名>
{
    //類說明體
}; 

template <型別形參表>
<返回型別> <類名>< 型別名錶 >::<成員函式1>(形參表)
{
    //成員函式定義體
}

其中 <型別形參表> 與函式模板中的 <型別形參表> 意義類似,而類模板本身不是一個真實的類,只是對類的一種描述,必須用型別引數將其例項化為模板類後,才能用來生成具體的對 象。簡而言之,類是物件的抽象,而類模板就是類的抽象。

具體而言,C++中引入模板類主要有以下5個方面的好處:

1) 可用來建立動態增長和減小的資料結構。

2) 它是型別無關的,因此具有很高的可複用性。

3) 它在編譯時而不是執行時檢查資料型別,保證了型別安全。

4) 它是平臺無關的,可移植性強。

5) 可用於基本資料型別。


8.2 C++函式傳遞引數的方式有哪些?

當進行函式呼叫時,要填入與函式形式引數個數相同的實際引數,在程式執行過程中,實際引數(簡稱實參)就會將引數值傳遞給相應的形式引數(簡稱形參),然後在函式中實現對資料的處理和返回。C++函式傳遞引數的方式一般有以下4種:

(1) 值傳遞

當進行值傳遞時,就是將實參的值複製到形參中,而形參和實參不是同一個儲存單元,所以函式呼叫結束後,實參的值不會發生改變。程式示例如下:

#include <iostream> 
using namespace std; 

void swap(int a,int b)
{
    int temp; 
    temp=a;
    a=b;
    b=temp;
    cout<<a<<","<<b<<endl;
}

int main()
{
    int x=l; 
    int y=2; 
    swap(x,y);
    cout<<x<<", "<<y<<endl; 
    
    return 0;
}
程式的輸出結果:
2,1
1,2

也就是說,在進行函式呼叫的時候只交換了形參的值,而並未交換實參的值,形參值的改變並沒有改變實參的值。

 

(2) 指標傳遞

當進行指標傳遞時,形參是指標變數,實參是一個變數的地址,呼叫函式時,形參(指標變數)指向實參變數單元。這種方式還是“值傳遞”,只不過實參的值是變數的地址而已。而在函式中改變的不是實參的值,而是實參地址所指向的變數的值。

程式示例如下:

#include <iostream> 
using namespace std; 

void swap(int *a,int *b)
{
    int temp; 
    temp=*a;
    *a=*b;
    *b=temp;
    cout<<*a<<","«*b«endl;
}

int main()
{
    int x=l; 
    int y=2; 
    
    swap(&x,y);     
    cout<<x<<","<<y<<endl; 
    
    return 0;
}
程式的輸出結果:
2,1
2,1

也就是說,在進行函式呼叫時,不僅交換了形參的值,而且交換了實參的值。

 

(3) 引用傳遞

實參地址傳遞到形參,使形參的地址取實參的地址,從而使形參與實參共享同一單元的方式。

程式程式碼示例如下:

#include <iostream> 
using namespace std; 

void swap(int &a,int &b) 
{
    int temp; 
    temp=a; 
    a=b; 
    b=temp;
    cout<<*a<<","<<*b<<endl;
}

int main()
{
    int x=l; 
    int y=2; 
    swap(x,y);
    cout<<x<<","<<y<<endl; 
    
    return 0;
}

程式的輸出結果:

2,1
2,1

(4) 全域性變數傳遞

這裡的“全域性”變數並不見得就是真正的全域性的,所有程式碼都可以直接訪問的,只要這個變數的作用域足夠這兩個函式訪問就可以了,比如一個類中的兩個成員函式可以使用一個成員變數實現引數傳遞,或者使用static關鍵字定義,或者使用namespace進行限制等,而這裡的成員變數在這種意義上就可以稱為“全域性”變數。


8.3 過載與覆蓋有什麼區別?

過載是指函式不同的引數表,對同名函式的名稱做修飾,然後這些同名函式就成了不同的函式(至少對於編譯器來說是這樣的)。在同一可訪問區域內被宣告的幾個具有不同引數列 (引數的型別、個數、順序不同)的同名函式,程式會根據不同的引數列來確定具體呼叫哪個函式。對於過載函式的呼叫,在編譯期間就已經確定,是靜態的,它們的地址在編譯期間就綁定了,與多型無關。注意,過載不關心函式的返回值型別。

double calculate(double);
float calculate(double);

上面的兩個同名函式只是返回值不同,所以不能構成過載。

成員函式被過載的特徵如下:

(1) 相同的範圍(在同一個類中)。

(2) 函式名字相同。

(3) 引數不同。

(4) virtual關鍵字可有可無。

 

覆蓋是指派生類中存在重新定義基類的函式,其函式名、引數列、返回值型別必須同父類中的相對應被覆蓋的函式嚴格一致,覆蓋函式和被覆蓋函式只有函式體不同,當派生類物件呼叫子類中該同名函式時會自動呼叫子類中的覆蓋版本,而不是父類中的被覆蓋函式版本,它和多型真正相關。當子類重新定義了父類的虛擬函式後,父類指標根據賦給它的不同的子類指標, 動態地呼叫屬於子類的該函式,這樣的函式呼叫在編譯期間是無法確定的(呼叫的子類的虛擬函式的地址無法給出)。因此,這樣的函式地址是在執行期繫結的。

覆蓋的特徵如下:

(1) 不同的範圍(分別位於派生類與基類)。

(2) 函式名字相同。

(3) 引數相同。

(4) 基類函式必須有virtual關鍵字。

 

過載與覆蓋的區別如下:

(1) 覆蓋是子類和父類之間的關係,是垂直關係;過載是同一個類中方法之間的關係,是水平關係。

(2) 覆蓋只能由一個方法,或只能由一對方法產生關係;方法的過載是多個方法之間的關係。

(3) 覆蓋要求引數列表相同;過載要求引數列表不同。

(4) 覆蓋關係中,呼叫方法體是根據物件的型別(物件對應儲存空間型別)來決定的,重 載關係是根據呼叫時的實參表與形參表來選擇方法體的。

程式示例如下:

#include <iostream> 
using namespace std;

class Base 
{
public:
    void f(int x)
    {
        cout<<"Base::f(int)"<<x<<endl;
    }
    
    void f(float x)
    {
        cout<<"Base::f(float)"<<x<<endl;
    }
    
    virtual void g(void)
    {
        cout<<"Base::g(void)"<<endl;
    }
};

class Derived:public Base
{
public:
    virtual void g(void)
    {
        cout<<"Derived::g(void)"<<endl;
    }
};

int main()
{
    Derived d;
    Base *pb = &d; 
    pb->f(42);
    pb->f(3.14f);
    Pb->g(); 
    
    return 0;
}

程式輸出結果:

Base::f(int) 42 
Base::f(float) 3.14 
Derived: :g(void)

上例中,函式 Base::f(int)與 Base::f(float)相互過載,而 Base::g(void)被 Derived::g(void)覆蓋。


8.4 隱藏與覆蓋有什麼區別?

隱藏是指派生類的函式遮蔽了與其同名的基類函式,規則如下:

(1) 如果派生類的函式與基類的函式同名,但是引數不同,則不論有無virtual關鍵字, 基類的函式都將被隱藏。

(2) 如果派生類的函式與基類的函式同名,並且引數也相同,但是基類函式沒有virtual 關鍵字,此時基類的函式被隱藏。

在呼叫一個類的成員函式時,編譯器會沿著類的繼承鏈逐級地向上查詢函式的定義,如果找到了就停止查找了。所以,如果一個派生類和一個基類都存在同名(暫且不論引數是否相同)的函式,而編譯器最終選擇了在派生類中的函式,那麼就說這個派生類的成員函式“隱藏”了基類的成員函式,也就是說它阻止了編譯器繼續向上查詢函式的定義。

隱藏的特徵如下:

(1) 必須分別位於派生類和基類中。

(2) 必須同名。

(3) 引數不同的時候本身已經不構成覆蓋關係了,所以此時是否是virtual函式已經不重要了。

當引數相同的時候就要看是否有virtual關鍵字了,有的話就是覆蓋關係,沒有的時候就是隱藏關係了。

示例程式如下:

#include <iostream> 
using namespace std;

class Base
{
public:
    virtual float f(float x)
    {
        cout<<"Base::f(float) "<<x<<endl;
    }

    void g(float x)
    {
        cout<<"Base::g(float) "<<x<<endl;
    }

    void h(float x)
    {
        cout<<"Base::h(float) "<<x<<endl;
    }
};

class Derived:public Base
{
public:
    virtual float f(float x)
    {
        cout<<"Derived::f(float) "<<x<<endl;
    }

    void g(int x)
    {
        cout<<"Derived::g(int) "<<x<<endl;
    }   

    void h(float x)
    {
        cout<<"Derived::h(float) "<<x<<endl;
    }
};

int main()
{
    Derived d;
    Base *pb = &d;
    Derived *pd = &d;
    pb->f(3.14f);
    pd->f(3.14f);
    pb->g(3.14f);
    pd->h(3.14f);
    
    return 0;
}

程式輸出結果:

Derived::f(float) 3.14 
Derived::f(float) 3.14 
Base::g(float) 3.14 
Derived::h(float) 3.14

上例中,函式 Derived: :f(float)覆蓋了 Base: :f(float),函式 Derived: :g(int)隱藏了 Base: :g(float);函式Derived::h(float)隱藏了 Base::h(float)。


8.5 是否可以通過絕對記憶體地址進行引數賦值與函式呼叫?

同一個數可以通過不同的方式表達出來,對於函式的訪問,變數的賦值除了直接對變數賦值以外,還可以通過絕對記憶體地址進行引數賦值與函式呼叫。

(1) 通過地址修改變數的值。

int x;
printf("%d\n",&x);
int *p = (int *)0x0012ff60;
*p = 3;
printf("%d\n",x);

程式首先輸出變數x所在地址為十六進位制的0x12ff60 (本來應該為8位的十六進位制數,高位為0則省略掉),然後定義一個指標變數,讓它指向該地址,通過指標變數的值來修改變數x的值。(實際測試發現每次編譯器給x分配的地址都不同)

示例程式碼如下:

int *ptr=(int*)0xa4000000;
*ptr=Oxaabb;
printf("%d\n",*ptr);

以上程式會崩潰,因為這樣做會給一個指標分配一個隨意的地址,很危險,所以這種做法是不允許的。(必須是一個確定的地址)

 

(2) 通過地址呼叫函式的執行。

#include <iostream> 
using namespace std;

typedef void(*FuncPtr)();
void p()
{
    printf("MOP\n");
}

int main()
{
    void (*ptr)();
    P();
    printf("%x\n",p); 
    ptr = (void (*)())0x411Of0; 
    ptr();//函式指標執行 
    ((void (*)( ))0x4110f0)();
    ((FuncPtr)0x4110f0)(); 
    
    return 0;
}

程式輸出結果:

MOP
4110f()
MOP
MOP
MOP

首先定義一個ptr的函式指標,第一次通過函式名呼叫函式,輸出MOP,列印函式的入口地址,函式的入口地址為4110f()。然後給函式指標ptr賦地址值為p的入口地址,呼叫ptr,輸 出MOP。接著的過程不通過函式指標直接執行,仍然使用p的入口地址呼叫,輸出為MOP。最後是通過typedef呼叫的直接執行。

函式名稱、程式碼都是放在程式碼段的,因為是放在程式碼段,每次會跳到相同的地方,但引數會壓棧,所以函式只根據函式名來獲取入口地址,與引數和返回值無關。無論引數和返回值如何不同,函式入口地址都是一個地方。


8.6 預設建構函式是否可以呼叫帶參建構函式?

預設建構函式不可以呼叫單引數的建構函式。程式示例如下:

class A
{
public:
    A()
    {
        A(0);
        Print();
    }

    A(int j):i(j)
    {
        printf("Call A(int j)\n");
    }

    void Print()
    {
        printf("Call Print()!\n");
    }

    int i;
};

int main()
{
    A a;
    cout<<a.i<<endl; 
    return 0;
}

程式輸出結果:

Call A(int j)
Call Print()
-858993460

以上程式碼希望預設建構函式呼叫帶參建構函式,可是卻未能實現。因為在預設建構函式內部呼叫帶參的建構函式屬使用者行為而非編譯器行為,它只執行函式呼叫,而不會執行其後的初 始化表示式。


8.7 C++中函式呼叫有哪幾種方式?

由於函式呼叫經常會被巢狀,在同一時刻,堆疊中會儲存多個函式的資訊,每個函式又佔用一個連續的區域,一個函式佔用的區域常被稱為幀(frame),編譯器是從高地址開始使用堆疊的,在多執行緒(任務)環境,CPU的堆疊指標指向的儲存器區域就是當前使用的堆疊。切換執行緒的一個重要工作,就是將堆找指標設為當前執行緒的堆疊棧頂地址。

當一個函式被呼叫時,程序核心物件為其在程序的地址空間的堆疊部分分配一定的棧記憶體給該函式使用,函式堆疊用於:

(1) 在進入函式之前,儲存“返回地址”和環境變數。返回地址是指該函式結束後,從進入該函式之前的那個地址繼續執行下去。

(2) 在進入函式之後,儲存實參或實參複製、區域性變數。

函式原型:[連線規範]函式型別[呼叫約定]函式名引數列表{……}

呼叫約定:呼叫約定是決定函式實參或實參複製進入和退出函式堆疊的方式以及函式堆疊釋放的方式,簡單地講就是實參或實參複製入棧、出棧、函式堆疊釋放的方式。在Win32下 有以下4種呼叫:

(1) _cdecl:它是C/C++的預設呼叫方式。實參是以引數列表從右依次向左入棧,出棧相反,函式堆疊由呼叫方來釋放,主要用在那些帶有可變引數的函式上,對於傳送引數的記憶體棧是由呼叫者來維護的。

(2) _stdcall:它是_ API的呼叫約定,其實COM介面等只要是申明定義介面都要顯 示指定其呼叫約定為_stdcall。實參以引數列表從右依次向左入棧,出棧相反。函式堆疊是由被呼叫方自己釋放的。但是若函式含有可變引數,那麼即使顯示指定_stdcall,編譯器也會自動把其改變成__cdecl。

(3) _thiscall:它是類的非靜態成員函式預設的呼叫約定,其不能用在含有可變引數的函式上,否則編譯會出錯。實參以引數列表從右依次向左入棧,出棧相反。但是類的非靜態成員函式內部都隱含有一個this指標,該指標不是存放在函式 堆疊上,而是直接存放在CPU暫存器上。

(4) _fastcall:快速呼叫。它們的實參並不是存放在函式堆疊上,而是直接存放在CPU暫存器上,所以不存在入棧、出棧、函式堆疊釋放。

需要注意的是,全域性函式或類靜態成員函式,若沒指定呼叫,約定預設是_cdecl,或是IDE 設定的。


8.8 什麼是可重入函式?C++中如何寫可重入函式?

可重入函式是指能夠被多個執行緒“同時”呼叫的函式,並且能保證函式結果正確性的函式。

在c語言中編寫可重入函式時,儘量不要使用全域性變數或靜態變數,如果使用了全域性變數或靜態變數,就需要特別注意對這類變數訪問的互斥。一般採用以下幾種措施來保證函式的可重入性:訊號量機制、關排程機制、關中斷機制等方式。

需要注意的是,不要呼叫不可重入的函式,當呼叫了不可重入的函式時,會使該函式也變 為不可重入的函式。一般驅動程式都是不可重入的函式,因此在編寫驅動程式時一定要注意重入的問題。


9 陣列

陣列的下標問題、越界問題一直是程式設計師經常忽視的問題,但與陣列相關的問題卻不僅限於此:多維陣列的預設賦值、下標越界、行儲存、列儲存等,本節將詳細對陣列的這些問題進行逐一分析。

 

9.1 int a[2][2]= { {1}, {2,3} },則a[0][1]的值是多少?

二維陣列的初始化一般有兩種方式,第一種方式是按行來執行(如int array[2][3]= { {0,0,1}, {1,0,0} };),而第二種方式是把數值寫在一塊(如int array[2][3]= { 0,0,1,1,0,0 };)。

若只對部分元素進行初始化,陣列中未賦值的元素自動為賦值為0,所以a[0][1]的值是0。


9.2 a是陣列,(int*)(&a+1)表示什麼意思?

表示int型別的陣列指標,若a為陣列a[5],則 (int*)(&a+1) 為a[5]。

示例程式如下:

#include <stdio.h> 

int main()
{
    int a[5]={1,2,3,4,5};
    int b[ 100];
    int *ptr=(int*)(&a+1); 
    printf("%d\n%d\n",*(a+1),*(ptr-1)); 
    printf("sizeof(b)=%d\n",sizeof(b)); 
    printf("sizeof(&b)=%d\n",sizeof(&b)); 
    
    return 0;
}

程式輸出結果:

2
5
sizeof(b)=400
sizeof(&b)=4

&a是陣列指標,是一個指向int (*)[5]的指標,所以&a+1的地址是&a地址再加5*sizeof(int),它的運算單位是int(*)[5]。經過型別轉換後,ptr相當於int *[5],也就是指向了一個含有5個元素的陣列。

ptr-1的單位是ptr的型別,因此ptr-1的位置剛好是a[4],它在記憶體中的分佈位置是和 &a+1相鄰的。但是ptr與(&a+l)型別是不一樣的,所以ptr-1只會減去sizeof(int*)。

值得注意的是,a和&a的地址是一樣的,但意思不一樣,a是陣列首地址,也就是a[0]的地址;&a是 物件(陣列)首地址;a+1是陣列下一元素的地址,即a[1];而&a+1是下一個物件的地址, 即 a[5]。

9.3 不使用流程控制語句,如何打印出1 ~ 1000的整數?

採用建構函式與靜態構造變數結合的方法實現。首先在類中定義一個靜態成員變數,然後在建構函式裡面列印該靜態變數的值,並對靜態變數進行自增操作,同時在主函式裡面定義一個類陣列,程式程式碼示例如下:

class print
{
public:
    static int a;

    print()
    {
        printf("%d\n",print::a);
        a++;
    }
};
int print::a = 1;

int main()
{
    print tt[100]; 
    
    return 0;
}