書名: iLike職場大學生就業指導:C和C++方向作者名: 滿在龍 賀曉麗編著本章字數: 195字更新時間: 2019-01-01 07:03:13
第2章 C/C++程序設計基礎
如今,各種編程語言迅速發展,C語言以其簡單、快捷、高效的特點,得到了眾多開發者的青睞,成為計算機專業的必修課程。各大企業一般通過面試考量求職者的編程能力,如編程風格、語言運用熟練程度等。在面試過程中,無法讓求職者做項目考查其能力,但是可以通過對程序設計基本概念的理解,尤其對細節的考查,來了解求職者的編程風格和語言的熟練運用程度。因此,求職者要對基礎概念的細節加以注意。
2.1 數據類型
在C語言程序中,類型轉換遇到的比較多,主要為整數、浮點數與字符串的互相轉換,有時也會出現二進制、十六進制的數值與浮點數的轉換,這時實現起來就比較復雜一點。面試時,C語言的字符串問題對于一般的求職者來說沒有什么難度。但是一些細節問題,往往會被忽略,而這些細節問題大多是面試中涉及到的知識點。
【試題1】
在C++中,變量的聲明和定義有什么區別?
【答案】
在C++語言中,使用變量前必須定義或聲明。在同一作用域內,變量必須有且只能有一次定義,但是可以聲明多次。現在一般這樣描述變量的聲明和定義:把不需要分配存儲空間的稱之為聲明,而把分配存儲空間的稱之為定義。其實,定義也是聲明。變量的聲明可以分為以下兩種情況。
(1)需要建立存儲空間的聲明。
下面的代碼就在聲明變量時,創建了該變量的存儲空間。
int a=0;
(2)不需要建立存儲空間的聲明。
【解析】
這個試題考查了最基礎的概念,但是這個問題會被忽略。在編寫代碼時,初學者很少遇到變量的聲明和定義方面的錯誤,這是因為大家都知道在C/C++中,變量聲明和定義后才能使用。即使遇到這方面錯誤,也就是未賦值而使用的警告。因為其功能相似,差別較小,它們的區別一般都會被忽略,但是它們還是有些區別的。
現在常常把不需要分配存儲空間的稱之為聲明,而把分配存儲空間的稱之為定義。其實,定義也是聲明。下面的代碼只是聲明變量,并沒有創建該變量的存儲空間。這只是引用性聲明,告訴編譯系統這是引用其他地方定義的變量,不需要為其分配一個存儲空間。這種聲明可以有多次,只是表明變量的類型和名字。從這里可以看出,并非所有的變量聲明都是定義。
extern int a;
具有初始化的變量聲明時,聲明必須有定義才可以,因為變量初始化需要存儲空間來存儲變量值,只有定義才能分配存儲空間。如果聲明變量的同時進行了變量初始化,就是使用了關鍵字extern進行聲明,那么它也可以視為進行了定義。
下面的代碼在Visual C++ 6.0系統中編譯通過。
extern int a; extern int a=0;
全局變量的聲明可以有多次,即可以在函數之內,也可以在函數之外進行。
另外,使用關鍵字static聲明一個變量時,其作用有兩點。
(1)static聲明局部變量時,在整個程序的執行期內,該變量分配的空間始終都存在,其值在該作用域內一直可以用。
(2)static聲明外部變量時,該變量只限于本文件模塊內使用。
【試題2】
以下三條輸出語句分別輸出什么?
char str1[] = "abc"; char str2[] = "abc"; const char str3[] = "abc"; const char str4[] = "abc"; const char* str5 = "abc"; const char* str6 = "abc"; cout << ( str1==str2 ) << endl; cout << ( str3==str4 ) << endl; cout << ( str5==str6 ) << endl;
【答案】
0 0 1
【解析】
該題輸出的是比較的結果。
由C/C++編譯的程序占用內存可以分為以下幾部分。
(1)棧區(stack):編譯器自動分配和釋放,用于存放函數參數值、局部變量值等。
(2)堆區(heap):一般由程序分配和釋放。若程序在結束前沒有釋放,程序結束時可能由系統釋放回收。
(3)全局區(static):用于存放全局變量和靜態變量。程序結束后由系統釋放。
(4)常量區:常量存放于該區。字符串常量就是放在這里的。程序結束后由系統釋放。
(5)程序代碼區:用于存放可執行代碼。
str1為字符數組,其長度由系統依據其初始值設置。編譯系統在棧區為“abc”分配了一塊內存,這塊內存存儲的內容是可以被修改的,在棧上分配一個地址給str1并指向為“abc”分配的地址。同理,系統也為str2分配了內存空間。
str1和str2為字符數組,它們的值分別為各自存儲區首地址。因為str1和str2為不同數組,各有自己的存儲空間,因此,str1和str2的值不等。
str3和str4與str1、str2相同,都是字符數組,它們的值也分別為各自存儲區首地址。str3和str4由關鍵字const修飾,它們所指向的存儲空間的內容不能修改。str3和str4各有自己的存儲空間,因此,str3和str4的值不等。
編譯系統在遇到str5時,實際上先在常量區為“abc”分配了一塊內存,然后在棧上分配一個地址給str5并指向為“abc”分配的地址。str5實際上是常量區的地址,這個區的內容是不容被修改的。str6和str5都是字符指針,并不分配存儲區,系統對其優化后,會為它們分配指向常量區的同一常量“abc”地址的首地址,因此相等。
讀者可以使用下面的代碼輸出這些數組的地址值,查看一下結果。筆者在Visual C++ 6.0系統中測試,str5的值與str6的值相同。
char str1[] = "abc"; char str2[] = "abc"; const char str3[] = "abc"; const char str4[] = "abc"; const char* str5 = "abc"; const char* str6 = "abc"; cout << ( str1==str2 ) << endl; cout << ( str3==str4 ) << endl; cout << ( str5==str6 ) << endl; cout << "str1:" << (int)str1 << endl; cout << "str2:" << (int)str2 << endl; cout << "str3:" << (int)str3 << endl; cout << "str4:" << (int)str4 << endl; cout << "str5:" << (int)str5 << endl; cout << "str6:" << (int)str6 << endl;
【試題3】
以下代碼能夠編譯通過嗎,為什么?
unsigned int const n1 = 2; char sz1[n1]; unsigned int i = 0; unsigned int const n2 = i; char sz2[n2];
【答案】
不能。數組的長度需要由整型常量或整型常量表達式確定,而n2的值在編譯期間不能確定其值,無法指定數組的長度。
【解析】
這道題考查了數組的聲明問題。在不支持可變長數組的編譯系統中,數組的長度需要在編譯期間確定其值,也就是編譯期常量,如長度為一個整數或整型的常量表達式,也可以使用define來定義。如果編譯系統在編譯期間不能確定數組的長度,就無法創建該數組。
使用define定義常量時,編譯系統在編譯的時候將常量標識符放入符號表中,這就是編譯期常量。這時,系統并沒有為其分配內存,分配內存是運行時做的事情。下面的代碼就是定義一個常量。
#define MAX 128
在編譯時,所有的MAX都會使用128進行替換。
使用const變量也可以指定數組的長度。考題代碼第一條語句就聲明了一個常量n1。在C++中,編譯器盡量不為n1分配內存,而是在進行類型檢查之后將其值折疊到代碼里,也就是進行值替代,然后存放到符號表中。這與define定義常量的過程類似。編譯系統在編譯期間就可以確定數組的長度,也就可以通過編譯。
如果const定義的常量值太復雜,需要在運行期間確定其值,就不能使用該值指定數組的長度。n2也是const修飾的變量,但是其值為變量i的值,n2的值需要運行期間確定,編譯期間也就無法確定數組的長度。
【試題4】
下列哪兩個是等同的?
A) const int* a = &b; B) int * const a = &b; C) const int* const a = &b; D) int const* const a = &b;
【答案】
C和D。
【解析】
const與指針結合使用,有兩種情況。
(1)使指針所指為const,定義時將const寫在*的左邊。這種定義方式,使用const修飾的是*,也就是const修飾的指針所指向的變量,指針指向的變量為常量,其值不能修改,指針的地址可以改變。形式如下所示。
int a=2; int const* b= &a; const int * d=&a;
不能通過這類指針修改其內容。如不能通過b或者d修改a的內容,但是可以通過變量a修改a的內容,也就是說這類指針所指地址不必是const。由于指針本身并不是const,所以定義指針時不必初始化。下面兩種定義格式相同。
int const* c; int const* b= &a;
(2)指針為const,定義時將const寫在*的右邊。這種定義方式,使用const修飾的是指針,指針本身是常量,指針的地址是固定的,不能再指向其他地址,但是指針地址所指內容可以被更改。由于指針是const的,所以定義時必須初始化,如下所示。
int d = 1; int* const pi = &d;
從以上分析可以看出,A項表示*a是const,但指針a可變,但是*a不能修改。B項a是const,但是*a可以變化,也就是a所指地址的內容可以修改。C項和D項的a和*a都是const,常量和指針的值都不能改變。因此C和D兩者是相同的。
【試題5】
寫出下面定義所描述的意思。
a) int a; b) int *a; c) int **a; d) int a[10]; e) int *a[10]; f) int (*a)[10]; g) int (*a)(int); h) int (*a[10])(int); i) int *a(int);
【答案】
a):一個整型數變量。
b):一個指向整型數的指針變量。
c):一個指向整型數指針的指針變量。
d):一個有10個整型數元素的數組。
e):一個有10個元素的數組,每個元素是指向一個整型數的指針。
f):一個指向有10個整型數數組的指針。
g):一個指向函數的指針,該函數有一個整型參數并返回一個整型數。
h):這是一個函數指針數組。這個數組有10元素,每個元素指向有一個整型參數并返回一個整型數的函數。
i):這是一個指針函數,這個函數有一個整型數值參數。
【解析】
這道試題概括了大部分指針定義,可以考查求職者的基本功。如果用語言描述出其定義,讓求職者使用變量寫出其定義,更能考查求職者的基本功。
【試題6】
在C語言中,int、char和short類型數據在內存中所占用的字節數( )。
A)由用戶自己定義 B)均為2字節 C)是任意的 D)由所用機器的機器字長決定
【答案】
D
【解析】
在C語言中,int、char和short類型數據在內存中所占用的字節數由所用機器的機器字長決定。
【試題7】
以下代碼能夠編譯通過嗎?如果能,輸出結果是什么?
int/*This is a test code.*/i=10; int/*This is a test code.*/j=\ 5; int//This is a test \ code. k=6; /*This is */#define aaaa/* a//##// */13/* code/\/&%*/ cout<<"i="<<i<<endl; cout<<"j="<<j<<endl; cout<<"k="<<k<<endl; cout<<"aaaa="<<aaaa<<endl;
【答案】
i=10 j=5 k=6 aaaa=13
【解析】
C/C++語言里可以有兩種注釋方式:/* */和//。“/*”和“*/”可以對多行進行注釋,“/*”和“*/”必須對應,“/*”總是與離它最近的“*/”匹配,所以“/*”和“*/”內不能有嵌套。“//”可以對一行進行注釋,多行注釋可以使用接續符“\”進行連接。注釋用于解釋、標識代碼,編譯器在編譯代碼時,會使用空格代替注釋。
接續符“\”表示其后行接續到該行后。在編譯時,編譯器會將反斜杠剔除掉,將其后行的字符接續到前一行。接續符“\”之后不能有空格。如果有空格,編譯器會報錯。
注釋是為了保證代碼的可讀性,讓別人看懂代碼。注釋時,有些規則需要注意。下面列舉了一些注釋規則。
(1)注釋風格要統一,“/* */”和“//”都可以,但是要保證統一。
(2)注釋要簡單易讀,準確,不要有冗余。對簡單的代碼不要添加注釋。
(3)注釋有適當縮進,易于閱讀才好。
(4)文件開始要添加注釋。每個文件開始的注釋要包含版權、作者、功能等。
(5)類、函數前都要加注釋,要包含版權、作者、功能等。函數前的注釋還要包含函數參數、返回值等說明。
(6)對于全局數據(常量)、類數據成員要加注釋。變量名稱雖然可以說明變量的用途,但是難懂的變量還是需要添加注釋。
(7)代碼前可以添加注釋,比較難懂的行后可以添加注釋,但是不能在代碼的行下面添加注釋。
(8)當代碼有多重嵌套時,應當在每個嵌套段落的結束處加注釋,便于閱讀。
(9)盡量使用英文進行注釋。注釋語句也要注意標點、拼寫和語法,即使使用中文也不要出現錯別字。
【試題8】
寫出下面代碼的輸出結果。
int t1=5; float t2=12; int *p=&t1; t1=t2/*p; cout<<t1<<endl;
【答案】
編譯不通過。代碼“t1=t2/*p;”應該修改為下面的形式才能編譯通過。
t1=t2/(*p);
【解析】
C/C++語言里可以有兩種注釋方式:/* */和//。編譯器將代碼“t1=t2/*p;”中“/*”后代碼看作是注釋,而不是進行除法運算。“/*”后為除法,這段代碼肯定存在編譯不過的問題。這主要考查求職者對C/C++語言關鍵字的敏感程度。
【試題9】
指出下面代碼的錯誤。
typedef enum { table, chair, stool } Furniture_1 typedef enum Furniture { table, sofa = 0, bed, } Furniture_2; int main(void) { Furniture_2 a,b; a=table; b=-1; a=desk; printf("desk:%d\t%d\n",a,b); return 0; }
【答案】
typedef enum { table, chair, stool } Furniture_1; typedef enum Furniture { desk, sofa = 0, bed, } Furniture_2; int main(void) { Furniture_2 a,b; a=(enum Furniture) table; b=(enum Furniture) -1; a=desk; printf("desk:%d\t%d\n",a,b); return 0; }
【解析】
這道題考查了枚舉的聲明及枚舉類型變量的賦值。枚舉也是一種數據類型,可以像基本數據類型一樣對變量進行聲明和定義。枚舉類型的定義和變量的聲明可以分開進行,也可以同時進行。下面的代碼是分開定義枚舉類型和聲明變量。
enum Furniture_1 { table, chair, stool }; enum Furniture_1 a; enum //省略enum類型名稱 { table, chair, stool }a; //變量名稱
也可以使用typedef將枚舉類型定義別名,并利用該別名進行變量聲明。下面的代碼是正確的。
//Furniture_1是Furniture的別名。Furniture_1不是變量名稱 typedef enum Furniture { table, chair, stool }Furniture_1; Furniture_1 a; Furniture b;
但是同一程序內聲明和定義枚舉類型時,需要注意下面兩點。
(1)存在同名的枚舉類型。
(2)存在同名的枚舉成員。在這道試題中,Furniture_1與Furniture_2就存在同名的枚舉成員,這是不允許的。
對枚舉型的變量賦整數值時,需要進行類型轉換。
【試題10】
寫出下面代碼的輸出結果。
void main() { char *p; *p=-130; printf("%d",*p); }
【答案】
126
【解析】
這道題考查了兩方面的內容:
(1)負數在計算機內部的存儲形式。
(2)char類型。
下面是獲取-130在計算機內部的存儲形式。在計算機內,有符號數有3種表示法:原碼、反碼和補碼。原碼的最高位為符號位,“0”表示正,“1”表示負,其余位表示數值的大小;正數的反碼與其原碼相同,負數的反碼是對其原碼逐位取反,但符號位除外;正數的補碼與其原碼相同,負數的補碼是在其反碼的末位加1。
負數在計算機內以補碼形式進行存儲。130的二進制是10000010,其補碼是1111111101111110。因為C語言的char類型占用8位,其余位丟掉,所以*p的內容為01111110,其十進制為126。
2.2 類型轉換
在C/C++語言中,不同類型的數據運算時,需要進行類型轉換。類型轉換有兩種形式:自動轉換和強制轉換。自動轉換是系統根據不同類型的轉換規則,自動將兩個不同數據類型的運算對象轉換成同一種數據類型的過程;強制轉換則是允許代碼編寫者依據自己的意愿將一種數據類型強制轉換成另一種數據類型的過程。
【試題11】
寫出下面代碼的輸出結果。
char ch='\377'; int i; i=ch; printf("%d",i);
【答案】
-1
【解析】
這道題考查了char和int類型轉換的知識。char類型為unsigned char還是signed char,不同的編譯系統會有不同的結果。char類型變量通常為長度為1字節的存儲空間。字符型數據賦給整型變量時,分兩種情況,如下。
(1)對于無符號字符類型的整型變量,字符類型變量低八位不變,高位補零后賦值給整型變量。
(2)對于有符號字符類型的整型變量,若字符類型變量的符號位為零時,與無符號整型變量的轉換規則相同;若字節的符號位為1時,將高位全部置1后,低位數值不變賦值。
這樣的轉換規則,當其數值為正的字符變量轉換成整型變量時,其值不變,仍然為正數;當字符變量的值大于127,也就是符號位為1時,都將其看做為一個負數,并將其賦值給整型變量,這樣就保證了數值的相同。
'\377'為八進制形式,其十進制的值為255,255的二進制表示為11111111。當變量ch的數值為255時,其符號位為1,將其賦值給i時,高位補1,也就是變成了1111111111111111 (以16位演示,int的具體位數與系統有關),這是-1的補碼,轉換成有符號數值為-1。
【試題12】
寫出下面代碼的輸出結果。
unsigned int i1; unsigned short index1=0; long ncount=0; for(i1 = 0; i1 <index1-1; i1++) { ncount++; if(ncount>1000)break; } cout<<"first:"<<ncount<<endl; unsigned int i2=6; int ii ; int index2=-10; ncount=0; for(ii= 0; ii<index2+i2; ii++) { ncount++; if(ncount>1000)break; } cout<<"second:"<<ncount<<endl; unsigned int i3; unsigned int index3=0; ncount=0; for(i3 = 0; i3<index3-1; i3++) { ncount++; if(ncount>1000)break; } cout<<"third:"<<ncount<<endl;
【答案】
first:0 second:1001 third:1001
【解析】
這道題考查了類型隱式自動轉換。自動轉換需要根據不同類型的轉換規則進行轉換。轉換的基本原則是低精度類型向高精度類型轉換,如圖2-1所示。
在第一個循環中,index1是無符號短整型unsigned short。在進行index1-1運算時,由于類型不匹配,發生隱式類型轉換,index1將被轉換成有符號整型,轉換之后的index1還是0,因此index-1的結果就是-1。而i1的初始值為0,表達式0<-1不成立,立即退出循環。ncount的值仍然為初始值0,輸出的結果仍然是0。
在第二個循環中,index2是無符號長整型unsigned int,而i2為整型int。因此,當執行到語句index2+i2時,由于類型不匹配,i2自動轉換為unsigned int。i2轉換前的值為-10,也就是0xfffffff6,轉換后變成一個無符號整數,即4294967286。這是一個非常大的數值。因此,循環可以進行,ncount的值會大于1000,輸出1001。

圖2-1 隱式自動轉換規則
index3為無符號長整型unsigned int,在進行減1時,1將被轉換成無符號長整型。因此,index3-1的結果就是0xffffffff,即4294967295。因此,循環可以進行,ncount的值會大于1000,輸出1001。
在編寫代碼時,語句和表達式通常只使用一種類型的變量和常量。如果使用了混合類型,最好強制進行類型轉換,否則系統會自動進行類型轉換,帶來潛在的危險。
下面是比較容易出錯的幾種運算符。
● →、[]、()的優先級高于*。
*p.next的實際運行結果是*(p.next),而不是(*p).next;同樣,*p[10]的運行結果是*(p[10]),*p(10)的實際結果是*(p(10)),fp是個函數,返回值為指針類型。
● 算術運算符與移位、比較、邏輯運算符。
a<< 4 + b的實際運行結果是a<<(4 + b)。
● == 和!=高于位運算符和賦值運算符,但是低于其他比較運算符。
● 逗號運算符最低。
【試題13】
寫出下面代碼的輸出結果。
short aa; unsigned long bb; bb=32768; aa=bb; cout<<"aa="<<(int)aa<<endl; char a=512; unsigned char b=-128; cout<<"a="<<(int)a<<endl; cout<<"b="<<(int)b<<endl;
【答案】
aa=-32768 a=0 b=128
【解析】
這道題考查了高精度類型向低精度類型轉換的知識。a為短整型,一般為16位;bb為無符號長整型,一般為32位。bb的值為32768,使用二進制可以表示為1000000000000000。無符號長整型數據賦值給短整型變量時,系統會自動截取低16位傳給短整型變量,短整型變量的第一位當做符號位。因此,aa的值為1000000000000000,即a為-0。在計算機內部存儲時,使用補碼形式存儲,-0也就是-32768。無符號和有符號整型互相賦值時,二進制數各位值不變,有符號整型數把首位當做符號。
512的二進制為1000000000。char類型變量長度一般為8位,a的二進制形式為00000000,其十進制為0。
-128的二進制形式為111110000000。unsigned char類型變量的長度仍然為8位,b的二進制為10000000,也就是二進制的128。
2.3 結構體、聯合體和sizeof
結構體可以看做由基本數據類型構成的并用一個標識符來命名的各種變量的組合。當然結構體可以含有不同的基本數據類型,也可以含有結構體和聯合體的復合數據類型,結構體是其中所有變量的集合。聯合體與結構體類似,但是其中的成員共用一個內存地址,只保存一個數據,但是可以按照不同類型來讀取。
【試題14】
寫出下面代碼的輸出結果。
Struct { short t1; short t2; }TestA; Struct { long t1; short t2; }TestB; int main(int argc, char* argv[]) { cout<<"TestA:"<<sizeof(TestA)<<endl; cout<<"TestB:"<<sizeof(TestB)<<endl; }
【答案】
下面是在32位機Visual C++ 6.0下運行的結果。
TestA:4 TestB:8
【解析】
這道題考查了內存字節對齊的知識點。結構體是其中所有變量的集合,其變量存放為依次存放。不同的編譯系統會采用不同的字節對齊方式,這里以Windows系統下的Visual C++字節對齊方式介紹。
結構體TestA中有2個short類型變量,長度都是2字節。該結構所有成員中最大對齊單元就是2,這兩個變量可以以2字節對齊,則sizeof(TestA)為4,這也是2的整數倍。
TestB中t1的長度為4字節,t2長度為2字節。為了保證讀取和傳送效率,編譯系統需要對字節進行對齊,該結構所有成員中最大對齊單元就是4,則t1取4字節對齊,t2需要補空2字節,保證結構體大小是4的整數倍,則sizeof(B)為8。
C++編譯器為了使CPU的性能達到最佳,會對struct的內存結構進行優化。默認的情況下,編譯器會對struct的結構進行數據對齊,以提高讀取效率。現代計算機中內存空間都是按照字節劃分的,從理論上講可以訪問內存的任何地址。編譯系統在內存空間上存放數據,不一定是按照其占有字節數順序的、一個接一個存放,而是將數據按照一定的規則在空間上存放,這就是對齊。
對齊有自然對齊和指定對齊。
(1)自然對齊。
結構體是一種復合數據類型,其構成元素既可以是基本數據類型的變量,如int、long、float等,也可以是一些復合數據類型的變量,如數組、結構體等。在編譯時,編譯器會自動對成員變量進行對齊,默認對齊方式就是自然對齊(natural alignment)。自然對齊時,編譯器按照結構體成員中長度最大的成員進行對齊,為結構體的每個成員分配空間。各個成員在內存中存放的順序就是它們被聲明的順序,第一個成員的地址和整個結構的地址也就相同。例如,32位的計算機的數據傳輸值是4字節,它的默認對齊字節就是4字節,這樣也可以保證傳輸數據的完整性。
在對齊時,變量存放的起始地址相對于結構的起始地址的偏移量與變量類型相關。下面是常見數據類型的偏移量(32位機器)。
● char:偏移量必須為sizeof(char)即1的倍數。
● int:偏移量必須為sizeof(int)即4的倍數。
● float:偏移量必須為sizeof(float)即4的倍數。
● double:偏移量必須為sizeof(double)即8的倍數。
● short:偏移量必須為sizeof(short)即2的倍數。
結構體的成員變量依據在結構中聲明的順序依次申請空間,同時按照上面的對齊方式調整位置,空缺的字節由編譯器自動填充。在Visual C++ 6.0中,整個結構體大小需要為結構字節邊界數的倍數,也就是該結構中占用最大空間的類型所占用的字節數的倍數。這表明,最后一個成員申請空間后,如果不夠上述要求,編譯系統就會自動補缺字節。
(2)指定對齊。
指定對齊就是改變默認對齊條件,按照指定的對齊條件對結構體成員進行對齊。可以通過下面的方法來改變默認的對齊條件。
● 偽指令#pragma pack (n):指示編譯器按照n字節對齊。如果n大于結構體中最大成員的長度,則編譯器仍按照結構體中最大成員的長度進行對齊。
● 偽指令#pragma pack ():指示編譯器取消自定義字節對齊方式。
【試題15】
寫出下面代碼的輸出結果。
Struct { short t1; short t2; }TestA; Struct { long t1; short t2; short t3; }TestB; Struct { short t2; long t1; short t3; }TestC; int main(int argc, char* argv[]) { cout<<"TestA:"<<sizeof(TestA)<<endl; cout<<"TestB:"<<sizeof(TestB)<<endl; cout<<"TestC:"<<sizeof(TestC)<<endl; }
【答案】
下面是在32位機Visual C++ 6.0下運行的結果。
TestA:4 TestB:8 TestC:12
【解析】
這道題和上道題差不多,TestB增加了一個成員變量t3,還增加了一個結構體TestC。TestB中t1的長度為4字節,t2長度為2字節,t3長度為2字節,t1、t2和t3依次順序存放。為了保證讀取和傳送效率,編譯系統需要對字節進行對齊,該結構所有成員中最大對齊單元就是4,則取4字節對齊,t2和t3共有4字節,3個成員共有8字節保證結構體大小是4的整數倍,則sizeof(B)為8。
而TestC結構體成員的存放順序是t2、t1和t3。該結構所有成員中最大對齊單元是4,則取4字節對齊,而t2只有2字節,其后為占有4字節的t1,它需要補空2字節;同樣,t3也需要補空2字節。這就是TestC需要占用12字節。
【試題16】
指出下面代碼的錯誤之處。
union un { int n; float a; }unt; struct { char c[6]; }aa; struct { int c[6]; }bb; struct { un c; }cc; struct st { char c[6]; int i[4]; un b; }dd; struct { char c[4]; int i[4]; un b; }ee; int main(int argc, char* argv[]) { struct st * sp=ⅇ cout<<sizeof(unt)<<endl; cout<<sizeof(aa)<<endl; cout<<sizeof(bb)<<endl; cout<<sizeof(cc)<<endl; cout<<sizeof(dd)<<endl; cout<<sizeof(ee)<<endl; cout<<sizeof(sp)<<endl; cout<<sizeof(*sp)<<endl; }
【答案】
4 6 24 4 28 24 4 24
【解析】
這道題還是考查結構體和聯合體結構大小的知識。聯合體的大小就是其成員長度最大所占的空間大小。所以sizeof(unt)大小就是float型變量的長度4。
變量dd對齊字節應為4字節,其第一個成員c占用6字節,i占用16字節,c需要補充2字節。這樣,dd的長度為28字節。
sizeof(sp)是對指針sp求值,sp指針占用4字節,所以其值為4。而sizeof(*sp)是對結構體ee變量求值,結果應為24字節。在面試時,一定注意sizeof()是對指針求值還是對其指向的內容求值。這些細節需要注意。
【試題17】
寫出下面代碼的輸出結果。
struct s_BTestB { char t ; char k; }; int main(int argc, char* argv[]) { //0x1234二進制形式為1 0010 0011 0100 int mm =0x1234; //將mm的值賦給結構體BTestB s_BTestB BTestB=*((s_BTestB *)& mm); cout<<(unsigned char)(BTestB.t)<<";"<<(int)(BTestB.k) <<endl; //將結構體BTestB的值賦給mm mm=*((unsigned short *)&BTestB); cout<<mm<<endl; return 0; }
【答案】
52;18 4660
【解析】
這道題考查了結構體成員變量順序存儲的知識。因為結構體成員變量順序存儲,所以可使用其內存空間存放數值。0x1234的二進制形式為1001000110100,而結構體BTestB的空間為2字節,可以存放0x1234的值,其結構如下。
BTestB 15 14 13 12 |11 10 9 8 | 7 6 5 4 |3 2 1 0 BTestB.t | 0 0 1 1 |0 1 0 0 BTestB.k 0 0 0 1 |0 0 1 0 |
所以BTestB.t的值為52,BTestB.k的值為18。
【試題18】
寫出下面代碼的輸出結果。
struct s_BTestA { char t:4; char k:6; unsigned short i:7; }; int main(int argc, char* argv[]) { //0x1234二進制形式為1 0010 0011 0100 int mm =0x1234; //將mm的值賦給結構體BTestA s_BTestA BTestA=*((s_BTestB *)& mm); cout<<(unsigned char)( BTestA.t)<<";"<<(int)( BTestA.k) <<";"<<(BTestA.i) <<endl; //將結構體BTestA的值賦給mm mm=*((unsigned short *)&BTestA); cout<<mm<<endl; cout<<sizeof(BTestA)<<endl; return 0; }
【答案】
4;18;0 4660 4
【解析】
這道題主要考查結構體中位域的使用。有時結構體中的成員元素存儲信息時,并不需要占用一個完整的字節,而只需一個或者幾個二進制位。例如存放一個BOOL值時,用一個二進制位就可以存放TRUE和FALSE兩種狀態。為了節省存儲空間和方便使用,C/C++語言支持位域。位域就是把一個字節中的二進位分為幾個不同的區域,并為每個區域指定域名(也可以無域名)和每個區域的位數。程序可以對域名進行操作。
(1)位域的定義和位域變量。
位域定義與結構體定義相仿,其形式如下。
struct 位域結構名 { 類型說明符 位域名:位域長度; 類型說明符 位域名:位域長度; …… };
(2)位域的說明。
一個位域不能跨2字節,只能存儲在同一個字節中。如果需要多個字節,就不需要使用位域來聲明變量,可以直接使用基礎類型變量。不同位域的存放順序和其聲明順序一致,也是一個接一個順序存放。如果一個字節所剩位不夠存放另一位域時,應從下一字節起存放該位域。下面的代碼就占用了2字節。
struct BTestA { char t:4; char k:6; };
也可以指定某位域從下一個單元開始存放,下面的例子就指定k從下一個字節存放。在這里,t占第一字節的4位,后4位填0表示不使用,k從第二字節開始,占用4位。
struct BTestA { char t:4; char :0;/*空域*/ char k:4; /*從下一字節開始存放*/ };
由于位域不允許跨2字節,也就是說位域的長度不能大于一字節的長度。
這道題中,t和k的位數和超過了一個字節,它們分別占用一字節空間。同樣的,k和i的位數和也超過了一個字節,i也重新分配內存空間。i是unsigned short類型,雖然占用7位,但是系統也會為它分配2字節空間(作者使用的是32位Visual C++ 6.0,以下同)。所以sizeof(BTestA)的值為4,而不是3。
因為結構體成員變量順序存儲,所以可使用其內存空間存放數值。0x1234的二進制形式為1001000110100,而結構體BTestA的空間為4字節,可以存放0x1234的值,其前兩個字節的結構如下。由于mm為int類型,占用4字節,后兩個字節內容均是0,也全部填充到BTestA的后兩個字節中,這里不再繪出。
BTestB 15 14 13 12 |11 10 9 8 | 7 6 5 4 |3 2 1 0 BTestB.t | 0 0 1 1 |0 1 0 0 BTestB.k 0 0 0 1 |0 0 1 0 |
所以BTestB.t的位數是4,只能計算其4位的值,所以其值為4;同樣的,BTestB.k的值為18,BTestB.i的值為0。
【試題19】
寫出下面程序的輸出結果。
typedef struct { int a:2; int b:2; int c:1; }TestC; int main(int argc, char* argv[]) { int kk=0; TestC t =*((test *)&kk); t.a = 1; t.b = 3; t.c = 1; kk=*(( short *)&t); cout<< t.a <<endl; cout<< t.b<<endl; cout<< t.c<<endl; cout<< kk<<endl; return 0; }
【答案】
1 -1 -1 29
【解析】
這道題主要考查結構體中位域的使用。t.a占用2位,其二進位為01,又是有符號數值,所以其值為1。t.b占用2位,其二進位為11,又是有符號數值,所以其值為-1。t.c占用2位,其二進位為1,又是有符號數值,所以其值為-1。
在定義t時,使用kk對其初始化,也就是說t的內存空間存放的是0。最后把t的內存空間數據賦值給kk。kk的二進位為011101,也就是29。
【試題20】
寫出下面程序的輸出結果。
#define OFFSET(s,e) (int)&(((struct s*)0)->e) struct Test_addr { int a; char b; double c; char d; }; int main(int argc, char* argv[]) { struct Test_addr s; int offset = OFFSET(Test_addr,a); cout<<offset<<endl; offset = OFFSET(Test_addr,b); cout<<offset<<endl; offset = OFFSET(Test_addr,c); cout<<offset<<endl; offset = OFFSET(Test_addr,d); cout<<offset<<endl; cout<<sizeof(Test_addr)<<endl; return 0; }
【答案】
0 4 8 16 24
【解析】
這道題輸出了結構體成員變量的偏移量和結構體大小。這是考查求職者對sizeof()和結構體大小理解的另外一種方法。對結構體成員變量的偏移量的獲取方法,這段代碼使用了一個小技巧。這個小技巧在宏OFFSET里體現。宏將地址0強制轉換成指定結構體類型,該結構體第一個成員變量的地址就是0,該成員相對于結構體地址的偏移量也是0。同理,第二個成員地址也是其相對于結構體地址的偏移量。這里,沒有對地址0進行操作,還是安全的。
其實,讀者可以將強制轉換地址設置成其他數值。但是求偏移量時,需要減去這個值。上面的代碼可以修改成以下形式,仍然可以獲得正確的結果。
#define OFFSET(l,s,e) (int)&(((struct s*)l)->e)- l struct Test_addr { int a; char b; double c; char d; }; int main(int argc, char* argv[]) { struct Test_addr s; int offset = OFFSET(0x2000,Test_addr,a); cout<<offset<<endl; offset = OFFSET(0x2000,Test_addr,b); cout<<offset<<endl; offset = OFFSET(0x2000,Test_addr,c); cout<<offset<<endl; offset = OFFSET(0x2000,Test_addr,d); cout<<offset<<endl; cout<<sizeof(Test_addr)<<endl; return 0; }
【試題21】
下面代碼的輸出結果是什么?
int aa = 0; cout<<sizeof(aa=12)<<endl; cout<<aa<<endl;
【答案】
4 0
【解析】
上面的答案在Visual C++ 6.0環境下運行獲得。最新的C++標準支持sizeof()在運行時計算,但大多數沒有完全實現C++標準的編譯器,仍然在編譯階段處理sizeof()。由于sizeof()不能被編譯成機器碼,其參數也不能被編譯,所以直接被替換成類型。在Visual C++ 6.0環境下,sizeof()為編譯階段處理,語句“aa=12”也就是被替換成aa的類型,輸出4。對aa的賦值并沒有執行,所以aa的值仍然為0。
【試題22】
下面代碼的輸出結果是什么?
class CTest{}; class CTestA { int s; double d; char c; }; class CTestB { int s; double d; static int e; char c; }; class CTestC { int s; char c; double d; }; int main(int argc, char* argv[]) { cout<<"sizeof(CTest):"<<sizeof(CTest)<<endl; cout<<"sizeof(CTestA):"<<sizeof(CTestA)<<endl; cout<<"sizeof(CTestB):"<<sizeof(CTestB)<<endl; c cout<<"sizeof(CTestC):"<<sizeof(CTestC)<<endl; CTest a; CTestA a1; CTestB b1; c CTestC c1; cout<<"sizeof(a) :"<<sizeof(a)<<endl; cout<<"sizeof(a1) :"<<sizeof(a1)<<endl; cout<<"sizeof(b1) :"<<sizeof(b1)<<endl; c cout<<"sizeof(c1) :"<<sizeof(c1)<<endl; return 0; }
【答案】
在32位Visual C++ 6.0中測試結果如下。
sizeof(CTest):1 sizeof(CTestA):24 sizeof(CTestB):24 sizeof(CTestC):16 sizeof(a) :1 sizeof(a1) :24 sizeof(b1) :24 sizeof(c1) :16
【解析】
類CTest是空類,但是編譯器輸出的結果為1。類聲明后,可以實例化。編譯系統為實例化的類,也就是對象,在內存中都會分配一個地址,用以標識該對象。為此,編譯器往往會給空類一個字節,所以sizeof(CTest)為1。
類一般由非靜態成員變量、static成員變量、函數、虛函數等構成。static成員變量獨立于該類的任意對象,它是只與類關聯的,由該類的所有對象共享訪問,并不與該類的對象關聯。對于非靜態成員變量,每個類對象都有其復制,只有在對象創建時才存在。static成員變量類似于全局變量,分配在全局數據區,并不在類中占用內存空間,無論類是否被實例化,它都已存在。使用sizeof()對類計算,是獲取類所占用的內存空間,static成員變量并不占用類內存空間,只是在類中聲明,所以sizeof()所求值不包含static成員變量所占用的內存空間。
這就是CTestA和CTestB的sizeof()結果是一樣的原因。
在C++中,非虛函數的聲明是在編譯/連接時使用,運行期使用重定位地址跳轉調用非虛函數,不占用存儲空間。函數代碼在代碼區存放,也沒有存放在類的內存空間中。所以,對類進行sizeof()運算,也不包含非虛函數所占的內存空間。
sizeof(CTestB)和sizeof(CTestC)的結果不同,是因為內存字節對齊造成的。這在講解struct時已經介紹過,不再重復。該題不同類所占內存大小不同,從中看出類中成員變量的定義順序會影響到內存的利用率,這也是跟編譯器的對齊方式有關。
【試題23】
下面代碼的輸出結果是什么?
class CTest{}; class CTestA{int s;}; class CTestB{}; class CTestC:public CTestA{ virtual void fun()=0; }; class CTestD:public CTest{ virtual void fun()=0; }; class CTestE:public CTestB,public CTestC{}; int main(int argc, char* argv[]) { cout<<"sizeof(CTest):"<<sizeof(CTest)<<endl; cout<<"sizeof(CTestA):"<<sizeof(CTestA)<<endl; cout<<"sizeof(CTestB):"<<sizeof(CTestB)<<endl; cout<<"sizeof(CTestC):"<<sizeof(CTestC)<<endl; cout<<"sizeof(CTestD):"<<sizeof(CTestD)<<endl; cout<<"sizeof(CTestE):"<<sizeof(CTestE)<<endl; CTestA a1; CTestB b1; cout<<"sizeof(a1) :"<<sizeof(a1)<<endl; cout<<"sizeof(b1) :"<<sizeof(b1)<<endl; return 0; }
【答案】
在32位Visual C++ 6.0中測試結果如下。
sizeof(CTest):1 sizeof(CTestA):4 sizeof(CTestB):1 sizeof(CTestC):8 sizeof(CTestD):4 sizeof(CTestE):8 sizeof(a1) :4 sizeof(b1) :1
【解析】
虛函數可以重載,以改寫虛擬函數,實現一些特定需求或改變系統的默認處理。C++支持多態性。為了實現多態性,C++編譯器也提供動態聯編。動態聯編,也稱之為晚捆綁,就是在運行階段,才將函數的調用與對應的函數體連接的一種方式。
編譯器編譯時,發現一個虛函數,會為該類創建一個虛函數表vtable。該表通常包含該類和其父類的所有虛函數的地址。如果該類重載了其父類的虛函數,表vtable中指向其父類虛函數的對應表項則指向重載后的此函數;如果沒有重載,該函數仍然為虛函數,其指針并未改變。
編譯器并非將該虛函數表直接放入該類的內存空間中,而是在該類中隱含插入一個指針指向虛函數表的指針vptr。在32位系統下,指針為4字節。
調用該類的函數時,編譯器會將vptr指向對應的vtable。調用基類的構造函數時,指向基類的指針此時已經變成指向具體類的this指針,借此this指針即可得到正確的vtable,也就實現了多態性。這才能真正與函數體進行連接,實現動態聯編。
所以,在對含有虛函數的類進行sizeof()運算時,會多出一個vptr指針大小的值。在32位系統下,也就是多出4字節。
這就是CTestC和CTestB的sizeof()結果是一樣的原因。
對子類進行sizeof()運算結果為父類和子類所占內存空間。sizeof(CTestC)的值為CTestA所占內存空間加上虛函數表指針的和。CTestA所占內存空間為4,虛函數表的指針為4,因此sizeof(CTestC)的值為8。如果CTestA的成員變量s的類型為char類型,則sizeof(CTestA)為1。但是sizeof(CTestC)的值仍然為8。這是因為CTestC的內存空間需要字節對齊,虛函數表指針為4字節,則CTestA所占空間也需要補齊4字節。
CTestD繼承自CTest類。CTestD需要復制CTest到其空間內,CTest為空類,因此也不需要占用內存空間。雖然sizeof(CTest)的結果為1,但是在CTestD內,需要為其分配一個字節,用以標識該類的對象。
下面是sizeof()的總結。
(1)基本類型。
基本類型的長度由系統決定,不同硬件系統、操作系統或者編譯器得到的結果可能是不同的。下面是獲取基本數據類型長度的代碼。
cout<<"sizeof(CTestA )"<<sizeof(a1)<<endl; cout<<"sizeof(CTestB )"<<sizeof(b1)<<endl; cout<<"sizeof(bool)="<<sizeof(bool)<<endl; cout<<"sizeof(char)="<<sizeof(char)<<endl; cout<<"sizeof(short)="<<sizeof(short)<<endl; cout<<"sizeof(long)="<<sizeof(long)<<endl; cout<<"sizeof(int)="<<sizeof(int)<<endl; cout<<"sizeof(float)="<<sizeof(float)<<endl; cout<<"sizeof(double)="<<sizeof(double)<<endl;
下面是在32位機器、Visual C++ 6.0環境下得到對基本類型進行sizeof()運算的結果。
sizeof(bool)=1; sizeof(char)=1; sizeof(short)=2; sizeof(long)=4; sizeof(int)=4; sizeof(float)=4; sizeof(double)=8;
(2)數組、指針和引用。
對數組進行sizeof()運算,需要注意有時數組退化成指針,例如數組作為參數以指針形式傳給函數,函數內對其參數進行sizeof()運算,就是對指針進行運算。下面的代碼就是將數組處理為一個指針,對s進行sizeof()運算,不是對數組運算,而是對指針運算,結果為4。
int func(char s [15]) { cout<<"fun:sizeof(s):"<<sizeof(s)<<endl; return 1; }
如果對func()進行運算,如sizeof(func(s)),則結果將依據func()的返回值決定。如上面的func()函數返回int類型,sizeof(func(s))=4。
下面代碼為對不同數組變量進行sizeof()運算。
char* s = "abcd"; //對指針進行運算,指針長度為4。 cout<<"sizeof(s)="<<sizeof(s)<<endl; //對字符*s進行運算,*s為s所指的第一個字符。 cout<<"sizeof(*s)="<<sizeof(*s)<<endl; //s1為數組,其長度由編譯器依據其初始值字符串長度決定,即初始值字符串長度加1。 char s1[] = "abcd"; cout<<"sizeof(s1)="<<sizeof(s1)<<endl; //對字符*s1進行運算,*s1為s1所指的第一個字符。 cout<<"sizeof(*s1)="<<sizeof(*s1)<<endl; //s2為含有100個元素的字符數組。 char s2[100] = "abcd"; cout<<"sizeof(s2)="<<sizeof(s2)<<endl; //s3為含有100個元素的整型數組,每個元素占用4字節。 int s3[100] ; cout<<"sizeof(s3)="<<sizeof(s3)<<endl; double s4; double* s5=&s4; //s6為引用變量,對引用變量進行sizeof()運算時,結果等于所引用的變量的sizeof()結果。 double& s6=s4; cout<<"sizeof(s4)="<<sizeof(s4)<<endl; //s5為指針。 cout<<"sizeof(s5)="<<sizeof(s5)<<endl; cout<<"sizeof(s6)="<<sizeof(s6)<<endl;
下面是上面的代碼在32位機器、Visual C++ 6.0環境下得到對基本類型進行sizeof()運算的結果。
sizeof(s)=4 sizeof(*s)=1 sizeof(s1)=5 sizeof(*s1)=1 sizeof(s2)=100 sizeof(s3)=400 sizeof(s4)=8 sizeof(s5)=4 sizeof(s6)=8
(3)結構。
Sizeof()對類和結構運算時,其處理情況相同。對結構運算時,其值就是非靜態數據成員所占內存和,還需要加上字節對齊所占用的內存空間。空的結構sizeof()值為1。這里不再舉例。
(4)無父類的類。
對無父類的類進行sizeof()運算,其值原則上等于非靜態成員變量的size之和。如果有虛函數,則需要加上虛函數表指針所占用的字節。在32位系統中,虛函數表指針占用4字節。通過上面的例子,可以得到這樣一個共識,定義成員變量的順序可能會影響到類的sizeof()運算結果。這是由字節對齊造成的。
對空類進行sizeof運算,結果為1。
(5)派生類。
對派生類進行sizeof()運算,需要加上其基類的size。基類所占空間還要與派生類的成員變量字節對齊。
這里順便介紹strlen函數和sizeof的區別,總結如下。
(1)sizeof是運算符,strlen是函數。
(2)sizeof獲取變量所占用的空間,可以對不同數據類型進行運算;strlen獲取字符串或字符串指針所指字符串的長度,其參數為char型,且字符串必須是以 '\0 '結尾的。
(3)sizeof運算符的結果類型是size_t,也就是unsigned int類型。
(4)sizeof是運算符,在編譯時就可進行運算;strlen是函數,在運行時才能計算。
(5)因為sizeof是運算符,對類型進行sizeof運算,sizeof后必須加括弧;如果是變量名可以不加括弧。
(6)數組作為strlen的參數不會退化成指針,但是傳遞給sizeof就有可能退化為指針。
2.4 運算符
C/C++提供了非常靈活的語法,但是也會給用戶帶來不便。運算符是編程中最常用的,但是一些細節往往會被忽視,出現一些預料不到的結果。這也成了面試中經常出現的問題。
【試題24】
下面代碼的輸出結果是什么?
int m=1,x=2,y,z; x==(m<x)?m:x; y=(y=z=4); z=x&&y; x=y&z; printf("x=%d\n",x); printf("y=%d\n",y); printf("z=%d\n",z);
【答案】
x=0 y=4 z=1
【解析】
代碼“x==(m<x)?m:x;”的意思是左側變量和右側表達式的值比較,左側x的值并未變化。右側表達式“(m<x)?m:x”意思是如果m小于x,則返回m值;否則返回x值。這行代碼并未改變任何變量的值。冒號兩側變量類型要一致,否則會出錯。下面的代碼在Visual C++ 6.0中會編譯不過。
cout<<( m<x?1:’2’) << endl;
代碼“y=(y=z=4);”意思是把4賦值給z,然后再賦值給y,最后再賦值給左側的變量y,因此y的變量就是4。
代碼“z=x&&y;”是將x和z與運算的結果賦值給z。與運算就是如果x和y為真,則結果為真,返回1;否則,為假,返回0。因為x為2,z為4,因此返回1,z的值最后為1。
代碼“x=y&z;”意思是將y和z的按位與結果賦值給x。按位與就是將y和z的二進制形式值的每一位進行與運算,然后將按位與后的數值返回給x。按位與的結果如表2-1所示。
表2-1 按位與結果表

【試題25】
使用位運算符實現一個十六進制表示的正整數值的高低位交換。例如,一個正整數值為0xABCD,高低位交換后的數值是0xDCBA。
【答案】
int HLT( int n) { //獲取n的字節數。 int k=sizeof(n); //保存交換后的數值。 int r=0; //保存移位后的臨時數值。 int t=0; //保存第一位非0數的位置。 int f=0; /* 一個十六進制數值,每個十六進制位對應4位二進制位。 每個字節8位,因此每個字節可以保存2位十六進制數值。 要處理的十六進制位數為字節數的2倍。 從數值的高位進行處理。 */ for(int i=2*k-1;i>=0;i--) { //獲取待處理的4位二進制,并將其左移到低4位。 t=n>>i*4; //與0x000f進行按位與,獲取低4位的數值。 t&=0x000f; //尋找高位中第一個非0數值的位置。 if(t==0 && f==0)continue; //當t不為0時,f記錄i的值。 if(f==0)f=i; //t右移相應位置。 t<<=(f-i)*4; //將t與r相加。 r |= t; } return r; } int main() { int a = 0xABCD; a=HLT(a); printf("%x\n",a); return 0; }
【解析】
這道題考查了移位操作的知識點。1位十六進制數可以使用4位二進制數表示。在這道題中,需要一次移位4位二進制數,也就是1位十六進制數。這道題實現流程如下。
(1)獲取十六進制數的數的長度。
(2)從高位開始處理十六進制數。
(3)將待處理的高位數(共4位二進制位)右移至低4位。
(4)判斷該數是否為0,且未碰到非0位,返回(2)繼續執行(查找第一位非0數,因為只對十六進制的有效數進行交換)。
(5)如果數值非0,設置第一位非0數值位置。
(6)將該數左移相應位置,并與r相加。
(7)重復步驟(2)~(6),直至處理完所有位。
(8)返回r。
【試題26】
寫出下面代碼的輸出結果。
int main() { int x=0,y=0; for( ;y<=1 && !x++;y++) {} printf("%d,%d\n",x,y); return 0; }
【答案】
2,1
【解析】
這道題主要考查運算符!和++的優先級。因為!和++運算符具有相同的優先級,結合性是從右向左運算,故++運算先于!運算。請讀者注意,運算符~的級別比較高,高于++等運算符。
【試題27】
寫出下面代碼的輸出結果。
int main() { int b = 3; int arr[] = {6,7,8,9,10}; int *ptr = arr; *ptr=*ptr+++b; printf("%d,%d",*ptr,*ptr++); return 0; }
【答案】
在Visual C++ 6.0環境中運行結果如下。
7,7
【解析】
該段程序考查++、+、*運算符的優先級,以及printf的計算順序。所有的優先級中,只有三個優先級是從右至左結合的,它們是單目運算符(!、~、++、--、+、-、*、(type))、條件運算符(三目運算符?:)、賦值運算符(+=、-=、*=、/=、%=、&=、^=、|=、<<=、>>=)。其他都是從左至右結合。
++運算符和*運算符具有相同優先級,但結合性是自右向左,先計算++然后才能運算*。但是代碼“*ptr=*ptr+++b;”的執行結果可能與編譯器相關。這是因為ptr在執行后加1,有些編譯器是在整行代碼運算結束后加1,有些是在加b結束后加1。這兩種不同的處理方式導致的結果會不同。在Visual C++ 6.0環境中運行時,是整行代碼執行后加1。
printf()語句的計算順序是自右向左,也就是先進行計算*ptr++,再計算*ptr。在計算*ptr++時,是獲取*ptr值后ptr加1,還是輸出后加1,這也要看編譯器的處理方式。在Visual C++ 6.0環境中運行時,是整行代碼執行后加1,所以才有上面的運行結果。
所以在寫代碼時,不要寫這樣的代碼,以防出現不可預料的結果。
【試題28】
寫出下面代碼的輸出結果。
int main() { int a,b,d=241; a=d/10%9; b= (-2)&&3; d=(-2)&3; printf("%d,%d,%d",a,b,d); return 0; }
【答案】
在Visual C++ 6.0環境中運行結果如下。
6,1,2
【解析】
該段程序考查/和%優先級。/和%屬于同一級優先級,左結合順序。在代碼“a=d/10%9;”中,先計算/,才能計算%。
&&運算符是進行與運算,這是一個兩目運算符,只要兩側數值為true,結果就為true。因為非0數值都會被認為是true,因此結果為true,輸出格式為整數,顯示為1。讀者可以測試一下對小數進行與運算的結果。
【試題29】
new和malloc的區別是什么?在C++中,new是否可以不分配內存,而創建指定的對象呢?
【答案】
malloc是一個庫函數,是已經編譯好的代碼,編譯器無法改變它。調用malloc時,編譯器從堆中為其分配內存,需要調用free函數釋放為其分配的內存。malloc函數不能初始化對象。
而new是運算符,在編譯器控制范圍之內。調用new時,通常需要以下3個步驟。
(1)調用operator new分配內存。
(2)調用構造函數生成類對象,初始化對象。
(3)返回相應指針。
使用new時,需要調用delete釋放申請的內存,并調用對象的析構函數釋放內存,而free不會調用對象的析構函數。
可以使用new在已經申請的內存空間上建立對象,就是placement new。
【解析】
在C++中,有關new的形式可以有3種:new、operator new、placement new。下面簡要介紹一下這3種形式。
new操作符和delete操作符對應,就是對內存進行申請和釋放,不能被重載。調用new時,通常需要以下3個步驟實現內存分配。
(1)調用operator new分配內存。
(2)調用構造函數生成類對象,初始化對象。
(3)返回相應指針。
如果想實現與new不同的內存分配方法,可以通過重載的operator new實現。operator new就像operator+一樣,是可以重載的。同樣的,operator delete也是可以重載的。如果類中沒有重載operator new,那么系統就會調用全局的::operator new實現內存的分配。
placement new也是operator new的一個重載版本。如果需要在已經分配的內存中創建一個對象,就可以通過placement new實現。它允許在一個已經分配好的內存中構造一個新的對象,原型如下:
void *operator new( size_t, void *p ) throw() { return p; }
其中,void *p指向一個已經分配的內存緩沖區的首地址。執行placement new時,參數size_t常被忽略,由系統指定。下面的代碼在申請的內存空間buf上,創建兩個task對象。
class task ; char * buf = new [10*sizeof(task )]; task *pt= new (buf) task[2];
placement new的使用方法和其他普通的new有所不同。通常的使用步驟如下。
(1)提前分配緩存。
下面為類task的對象申請內存空間。
class task ; char * buf = new [10*sizeof(task )];
(2)分配對象。
在已分配的緩存區調用placement new來構造一個對象。
//task *pt= new (buf) task[2]; task *pt= new (buf) task;
(3)使用對象。
對對象的使用,可以按照普通方式使用。
// pt[0]->fun();//fun()為類task的方法。 pt->fun();//fun()為類task的方法。
(4)對象析構。
在使用完對象后,和普通對象的使用方法一樣,必須調用析構函數。下面就是調用的析構函數。
pt->~task();
(5)釋放空間。
在使用完對象后,如果申請的空間不再使用,可以把空間釋放掉。釋放后的空間不能再被使用。如果還要將該空間分配給其他新對象,就不能釋放空間。
delete [] buf;
可見,使用placement new就可以不重新創建內存空間,而創建指定的對象。
2.5 預處理
預處理是C/C++語言的一種特殊而又重要的功能,它由預處理程序負責完成。當對一個源文件進行編譯時,在第一遍掃描(也就是詞法掃描和語法分析)前,系統將自動調用預處理程序對源程序中的預處理部分進行處理,處理完畢后才對源程序進行編譯。預處理包括3種形式:宏(#include)、文件包含(#define)和條件編譯(# ifdef、# ifndef等)。
【試題30】
下面代碼的輸出結果是什么?
#define XX(x) x+x*x int main(int argc, char* argv[]) { int x=2; x=3*XX(x); printf("%d",x); return 0; }
【答案】
10
【解析】
上面例子定義了一個宏。把代碼“x=3*XX(x);”的宏展開,代碼就變成了下面的形式。
x=3*x+x*x;
變量x的值是3,最后x的值變為10。這道題很簡單,但是有些考生會做錯。原因不外乎兩種:馬虎和對宏代換理解不夠。馬虎就是忘記了細節,忽略了宏代換之后的表達式的形式;對宏代換理解不夠,就是對宏概念理解不正確導致的結果。其實這兩種原因都是對宏代換理解的問題。宏定義就是用宏名表示一個字符串,宏展開時以該字符串代換宏名。宏代換就是簡單的代換,因此,宏定義時必須十分注意,保證在宏代換之后不發生錯誤。兩邊的括號最好不要少。
定義宏時,要注意以下幾點。
(1)宏中可以包含常數、字符,也可以是表達式。但是宏定義不要添加不必要的符號,如分號。預處理程序也會把分號作為宏的一部分進行替換。
(2)宏定義允許嵌套其他宏,也就是定義宏時,可以使用已經定義的宏名。在展開宏時,預處理程序進行代換。
(3)宏名習慣上用大寫字母表示。
(4)宏定義可以表示數據類型,也可以指定數值的類型。常用的宏定義就是使用宏定義一個常數,表示1年中有多少秒。UL就是告訴預處理程序,這個常數是UL類型。這樣寫出的表達式比較直觀,可使維護者一看就知道這個宏的意義。預處理程序計算該常數的值,沒有降低程序的效率。
#define SEC_PERYEAR (60 * 60 * 24 * 365)UL
(5)編譯時,預處理程序對定義的宏不做語法檢查。只有展開宏之后,編譯系統對展開宏的代碼編譯時,才能發現宏定義的錯誤。
(6)宏定義中,可以帶有參數,宏名和形參表之間不能有空格。如果出現空格,系統將會認為是無參數宏定義。
(7)調用帶有參數的宏時,宏才展開,參數也是符號代換,不存在傳值。宏定義時的形式參數(宏定義中的參數)是不分配內存的,也不必在定義宏時指定參數類型。
宏定義中的形參只是標識符,對應的實參(宏調用中的參數)可以為變量,也可以為表達式。因此,定義宏時,最好也為宏參數加上括號。
【試題31】
在C/C++語言中,文件包含的命令行一般有以下兩種形式:
#include "文件名" #include <文件名>
簡要說明一下這兩種形式的區別。
【答案】
使用尖括號的文件包含表示在包含目錄中去查找包含的文件,不是在源文件目錄中去查找。包含目錄是由用戶在環境中設置的文件目錄。使用雙引號的文件包含則表示首先在當前源文件目錄中查找包含的文件,如果沒有找到則到包含目錄中去查找。
【解析】
C/C++提供了很多頭文件,這些頭文件包含了各個標準庫函數的函數原型。程序調用庫函數時,需要包含該函數原型所在的頭文件。文件包含也是C/C++預處理程序的一個重要功能。文件包含的功能是把指定的文件插入該代碼行位置,并取代該代碼行,使指定的文件和當前源程序文件組成一個源文件。在對其編譯時,編譯系統將為其生成一個目標文件。文件包含允許嵌套。
【試題32】
寫出下面代碼的輸出結果。
#define PI 3.14 int main(int argc, char* argv[]) { int r=10; #ifdef PI printf("s=%d",(int)(PI*r*r)); #else printf("s=%d",int)(3*r*r)); #endif }
【答案】
s=314
【解析】
預處理程序提供了條件編譯功能,使編譯系統按照不同的條件編譯不同的程序部分,產生不同的目標代碼文件。條件編譯一般的語法格式如下:
#ifdef 標識符 程序段1 #else 程序段2 #endif
如果標識符已使用#define命令定義,則對程序段1編譯;否則,對程序段2進行編譯。該格式中的#else也可以沒有,格式如下:
#ifdef 標識符 程序段 #endif
另外,也可以使用#ifndef形式指定編譯條件,格式如下:
#ifndef 標識符 程序段1 #else 程序段2 #endif
如果標識符未被#define命令定義,則對程序段1編譯;否則,對程序段2進行編譯。這正好與#ifdef預編譯命令的功能相反。
【試題33】
如何定義當前源文件的文件名及源文件的當前行號。
【答案】
cout << __FILE__ << endl; cout<<__LINE__ << endl ;
【解析】
__FILE__和__LINE__是編譯系統預定義的宏,分別輸出當前源文件的文件名和當前所在的行號。標準的C/C++除了支持這兩個宏外,還支持如下兩個宏。
__DATE__:表示編譯時的日期,字符串格式。
__TIME__:表示編譯時的時間,字符串格式。
【試題34】
寫出下面代碼的輸出結果。
#include "stdafx.h" #include <iostream> #include "stdio.h" using namespace std; int main(int argc, char* argv[]) { cout<<__LINE__ << endl ; #line 100 cout<<__LINE__ << endl ; return 0; }
【答案】
在Visual C++ 6.0下的運行結果如下。
7 100
【解析】
#line是預處理程序提供的預處理指令,用于改變當前所在的行號和文件名稱。#line命令的基本格式如下:
#line num["filename"]
其中,num為所設置的行號,[]內的文件名可以省略。
#line 100就是省略了文件名,改變當前行號為100。編譯系統在編譯源代碼時,會產生一些中間文件。使用該指令,可以使中間文件名固定,有利于進行調試。
另外,預處理程序還提供有#pragma預處理指令。該指令是設定編譯器的狀態,或設置編譯器執行特定的動作。常用的#pragma指令如下。
(1)#pragma message。
該指令在編譯時,在輸出窗口輸出指定的信息,其格式如下:
#pragma message(msg)
其中,msg為待輸出的文本信息。
下面的代碼在定義了宏PI時,輸出提示信息。
#ifdef PI #Pragma message(“macro PI defined!”) #endif
(2)#pragma once。
該指令一般加在頭文件的開始處,保證頭文件被編譯一次。
(3)#pragma warning。
#pragma warning的形式比較多,主要有如下幾種。
● #pragma warning(disable: n)
該指令將某個警報設置為失效,n為警報的序號。
● #pragma warning(default: n)
該指令將某個警報設置為默認,n為警報的序號。
● #pragma warning(error:n)
該指令將某個警報信息設置為錯誤,n為警報的序號。
● #pragma warning( push)
存儲當前警報信息設置。
● #pragma warning(push, n)
存儲當前警報信息設置,并設置報警級別為n。n為1~4的自然數。
● #pragma warning( pop )
恢復之前壓入堆棧的警報信息設置。pop和push是對應的,該指令使push和pop之間的任何警報信息設置都失效。
例如,下面代碼將不顯示4507警告信息。
#pragma warning(disable:4507)
(4)#pragma comment。
#pragma comment(comment-type [,"commentstring"])
其中,comment-type是一個預定義的標識符,指定注釋的類型,應該是compiler、exestr、lib、linke之一;commentstring是一個為comment-type提供附加信息的字符串。
comment-type的類型簡單解釋如下。
● compiler:將編譯器的版本或名字放入一個對象文件。
● lib:可將庫文件加入到工程中。
#pragma comment(lib, "user32.lib")
該指令用來將user32.lib庫文件加入到本工程中。
● linker:指定一個連接選項。
(5)#pragma pack。
該指令的格式如下。
● #pragma pack (n):該指令指示編譯器將按照n個字節對齊。
● #pragma pack ():該指令指示編譯器將取消自定義字節對齊方式。#pragma pack (n)和#pragma pack ()之間的代碼按n個字節對齊。
如果指定的對齊字節數n超過了系統默認的字節對齊字節數,系統將按照默認的字節數進行對齊。只有n小于系統默認的字節數,指定的對齊方式才有效。
2.6 其他
本小節的試題以編程題為主。這些試題考查了面試者的綜合能力。
【試題35】
編寫一個程序,以小數形式輸出一個分數。用戶輸入分母和分子,表示一個分數形式的數值,用戶輸入小數位數后,程序以小數形式輸出分子的計算結果。程序需要滿足以下條件:
(1)用戶輸入分母、分子和輸出位數;
(2)保證輸入的數值為大于0的整數。
【答案】
#include <stdio.h> #include <string.h> #include <stdlib.h> void GetFloat(int x,int y,char * str,int nnum) { //保存轉換后的數值。 char tmp[30]; //獲取x除以y的整數部分,并將其轉換成字符串保存在str中。 strcat(str,itoa(x/y,tmp,10)); //加入小數點。 strcat(str,"."); //計算小數部分的長度。字符串結束標志“\0”占用一個數組元素。 int num=nnum-strlen(str)-1; //獲取x除以y的余數。 x%=y; //i表示已經獲取的小數數目。 int i=0; //獲取小數,循環條件是獲取的小數數目不夠num,并且余數不為0。 //對余數擴大10倍,求其對y的商,就是當前小數的數值部分。 while(i<num && (x!=0)) { x*=10; //獲取x除以y的整數部分,并將其轉換成字符串保存在str中。 strcat(str,itoa(x/y,tmp,10)); //獲取x除以y的余數。 x%=y; //小數數目增1。 i++; } return; } int main(int argc, char* argv[]) { int x; int y; int nnum; //獲取用戶輸入的分子。如果小于0則重復等待用戶輸入正確的值。 do { printf("input integer x(>=0): "); scanf("%d", &x); }while(!(x>=0)); //獲取用戶輸入的分母。如果小于0則重復等待用戶輸入正確的值。 do { printf("input integer y(>0): "); scanf("%d", &y); }while(!(y>0)); do { printf("input integer nnum(>0): "); scanf("%d", &nnum); }while(!(nnum>0)); //申請保存結果的內存空間。 char* str=new char[nnum]; //初始化內存空間。 memset(str,0,nnum); GetFloat(x,y,str,nnum); printf("%s\n",str); delete[] str; return 0; }
【解析】
這道題實現起來比較簡單,但也會給一些求職者帶來麻煩。造成麻煩的原因是獲取求分數的辦法。筆者給出的答案是將余數擴大10倍除以分母,并將商作為小數一位的方法,實現求小數。如果余數為0,則終止求小數。
這道題還是有很多細節問題需要注意,如果程序要實現的完美,還是要下點功夫。
【試題36】
編寫一個程序,輸出由字母組成的“字母塔”。 例如:輸入C,則輸出:
A ABA ABCBA
【答案】
int main(int argc, char* argv[]) { char c,*d; //獲取用戶輸入的字母。字母需要在字母“A”和“Z”(或者字母“a”和“z”)之間; //否則,程序將強行要求用戶再次輸入,直至輸入正確為止。 do { printf("input char(<='z' and >='a'): "); scanf("%c", &c); d=strupr(&c); c=*d; }while(! (c>='A' && c<='Z')); //計算輸出的行數。行數依據用戶輸入的字母確定。 int l=c-'A'+1; for(int i=0;i<l;i++) { //輸出每行開始字符左側的空格。 for(int j=0;j<l-i-1;j++) printf("%c",' '); //輸出每行字符。 for(int k=0;k<=i;k++) printf("%c",'A'+k); //輸出每行最后一個字符右側的空格。 for(int m=i-1;m>=0;m--) printf("%c",'A'+m); printf("\n"); } return 0; }
【解析】
這道題的設計難點主要有以下3個:
(1)輸出的行數;
(2)每行字符串左側和右側的空格數;
(3)每行輸出的字符及數目。
解決了這3個問題,該程序實現起來還是比較簡單的。
【試題37】
編寫一個程序:當用戶輸入小數時,程序輸出該小數對應的分數。例如,輸入0.125,則程序輸出1/8;輸入1.375,則程序輸出1+3/8。
【答案】
/* 輾轉相除法求x與y最大公約數,并將x和y轉換成分數形式。 x:轉換后的分子。 y:轉換后的分母。 ch:保存轉換后的分數形式。 */ bool getcd(long int x,long int y,char ch[]) { int m,n; m=x; n=y; long int r,t; //如果m小于n,則m和n交換,保證m大于n。 if(m<n) { t=m; m=n; n=t; } //獲取m對n的余數。 r=m%n; //求公約數,直至r為0。 while(r!=0) { //此時n大于m,將n賦值給m,保證m大于n。 m=n; n=r; //獲取m除以n的余數。 r=m%n; } //將x和y分別除以n,獲取分子和分母。 x=(long)x/n; y=(long)y/n; int i=0; int j=0; m=x; n=y; //獲取x和y的位數,以便申請足夠內存空間存放其字符串形式。 //將數值除以10,直至為0,則獲取該數值的位數。 while(m!=0) { m/=10; //計位數數目。 i++; } while(n!=0) { n/=10; j++; } //比較兩個數的位數,并存入i中。 if(j>i)i=j; //使用m和n的最大位數申請內存空間,以便能存放分子和分母。 //多申請一個空間用于保存字符串結束標志“\0”。 char* p=new char[i+1]; //如果x大于y,則獲取該分數的整數部分,并存入ch。 if(x>y) { m=x/y; x=x%y; itoa(m,p,10); strcpy(ch,p); strcat(ch," "); } itoa(x,p,10); strcat(ch,p); itoa(y,p,10); strcat(ch,"/"); strcat(ch,p); delete [] p; return true; } int main(int argc, char* argv[]) { float x,m; long int g,n; int i=0; //接受用戶的輸入,要求x大于0。 do { cout<<"input a decimal fraction(x>0):"; cin>>x; }while(x<=0); m=x; n=1; /* 將x(m)擴大成整數m作為分子,將擴大倍數(n)作為分母。 求m和n的最大公約數,并將m和n除以最大公約數,就得到分數形式。 */ while(m-long(m)!=0) { m=m*10.0; n=n*10; i++; } char*p=new char[(i+2)*2]; getcd(long(m),n,p); cout<<"Result fraction: "<<p<<endl; delete [] p; return 0; }
【解析】
這道題的設計難點主要是小數轉換分數的方法。讀者如果沒有碰到過小數轉換成分數的例子,會感覺到無從下手。本題通過將小數擴大成整數作為分子,將擴大倍數作為分母,求兩者的最大公約數,兩者除以最大公約數就是分子和分母。
這道題也是從一個側面考察了求最大公約數的方法。該例使用輾轉相除法獲取最大公約數。
【試題38】
編寫一個程序,求1……n的和,要求不能使用乘除法、循環語句(如for、while等)、條件判斷語句(如if、else、switch、case、A?B:C等)。
【答案】
以下程序在Visual C++6.0上運行通過。
#include <stdio.h> int GetSum1(int n) { int l; //n為0的時候,執行下面的語句,返回0。 !n && (l=n); //n不為0時,執行下面的語句,遞歸調用。 n && (l=n+GetSum1(n-1)); return l; } int main(int argc, char* argv[]) { printf("%d\n",GetSum1(100)); return 0; }
【解析】
這道題的設計難點主要是不能使用乘除法、循環語句、條件判斷語句等關鍵字。這就使得不能使用通常的辦法來實現。實現這道題的辦法很多,所給答案使用的是遞歸實現循環。
遞歸可以實現循環的功能,但是遞歸需要終止遞歸的判定條件。因為這道題不能使用條件判斷語句,設置遞歸終止條件就需要費一番周折。所給答案利用邏輯與運算實現了判斷的功能。在邏輯與運算中,如果前一個條件為假,系統將不判斷后一個條件的真假,直接返回假;如果前一個條件為真,才會判斷后一個條件是否為真。利用這個特點,通過下面的語句實現依據n值不同執行不同的語句的功能。
n=0:!n為真,系統判斷執行(l=n)語句,l的值也就是為0;n不等于0的時候,不執行(l=n)語句。
!n && (l=n);
n!=0:n為真,執行判斷后一個條件,進入遞歸調用;n為0時,不執行后一個條件。
n && (l=n+GetSum1(n-1));
這只是一種實現辦法。使用類也可以實現這道題的功能。類具有靜態成員變量。類的靜態成員變量與類的對象無關,可以作為類創建對象的計數器使用。利用這個特點,可以使用類的靜態成員變量記錄累加和。
下面是使用類靜態成員變量實現累加功能。
#include <stdio.h> class SUM { public: SUM() { //使N增1,并求和。 //每創建一個SUM對象,就執行該語句一次:N的值增1,Sum值增加。 //N的值也可作為類對象的計數器使用。 Sum += ++N; } static int GetSum() { //獲取累加和。 return Sum; } private: //N為類靜態成員變量,用以保存當前累加的數。 static int N; // Sum為類靜態成員變量,用以保存當前累加的和。 static int Sum; }; //初始化類SUM的靜態成員變量。 int SUM::N = 0; int SUM::Sum = 0; //獲取和。 int GetSum(int n) { //創建100個對象,就對1…N求和。 SUM *a = new SUM[n]; delete []a; a = 0; return SUM::GetSum(); } int main(int argc, char* argv[]) { printf("%d\n",GetSum(100)); return 0; }
通過類的虛函數也可以實現遞歸。繼承類對象調用虛函數時,會依據對象調用不同類的虛函數。依據這個特點,可以實現判斷功能:當n為0時,調用基類的虛函數;當n不為0時,調用派生類的虛函數。
下面是使用虛函數實現累加功能。
/*
聲明兩個類:基類CSUMA和派生類CSUMB。
兩個類都有虛函數Sum():基類的Sum()返回0;派生類的Sum()遞歸調用本身,實現累加。
*/ class CSUMA { public: //定義虛函數。 virtual int Sum (int n) { return 0; } }; //聲明一個具有兩個元素的全局數組,保存CSUMA對象的指針。 CSUMA* Arr[2]; //聲明一個派生類。 class CSUMB: public CSUMA { public: //定義虛函數,實現遞歸調用。 virtual int Sum (int n) { /* 當n為0時,!!n的值為0,調用數組Arr[0]所保存對象的虛函數。因為Arr[0]所 保存對象為CSUMA,調用CSUMA的虛函數; 當n不為0時,!!n的值為1,調用數組Arr[1]所保存對象的虛函數。因為Arr[0] 所保存對象為CSUMB,調用CSUMB的虛函數,遞歸調用本身,實現累加。 */ return Arr[!!n]->Sum(n-1)+n; } }; int GetSum2(int n) { //聲明兩個對象。 CSUMA a; CSUMB b; //將兩個對象保存到數組Arr中。 Arr[0] = &a; Arr[1] = &b; int value = Arr[1]->Sum(n); return value; } int main(int argc, char* argv[]) { printf("%d\n",GetSum2(100)); return 0; }
另外,也可以通過模板實現累加的功能。
【試題39】
編寫一個C語言程序,輸入一個表示數的字符串,把該字符串轉換成數字并輸出,并將數字轉換成大寫金額的字符串輸出。如輸入“123”,輸出123和“壹佰貳拾叁”。
程序中出現的字符串轉換成數字,不能使用atoi()、strtol()等函數。
【答案】
#include <iostream> using namespace std; #define MAX 0xffffff /* 將字符串轉換為數字。 str為待轉換字符串; nJ為字符串所表示數字的進制。 */ float StrToInt(const char* str,int nJ=10) { //限制進制范圍。 if(nJ>16) return 0; if(nJ<2) return 0; long int num = 0; double fnj=nJ; double fnum=0; if(str != NULL) { const char* digit = str; //標識當前處理的是小數部分還是整數部分:false標識整數,true標識小數。 bool bfloat=false; //下面判斷字符串所表示數字是正數還是負數:minus為1表示正數;minus為-1表示負數。 int minus = 1; if(*digit == '+') digit++; else if(*digit == '-') { digit++; minus = -1; } //處理剩余字符。 while(*digit != '\0') { //處理字符。 if((*digit >= '0' && *digit <= '9') || (*digit >= 'A' && *digit <= 'F') || (*digit >= 'a' && *digit <= 'f')) { int m; //將字符轉換成十進制數值。 if( *digit >= 'A' && *digit <= 'F') m=(*digit - 'A'+10); else if(*digit >= 'a' && *digit <= 'f') m=(*digit - 'a'+10); else m=*digit - '0'; //如果數值超出進制表示范圍,則返回。 if(m>=nJ) return 0; //處理整數部分。 if(!bfloat) { num = num * nJ +m; //超出最大值,則返回。 if(num > MAX) { num = 0; break; } } else { //將小數部分轉換成十進制數。 fnum+= (*digit - '0')*fnj; //fnj要適當變小。 fnj*=1/((float)nJ); } digit ++; } //處理小數點。 else if(*digit == '.') { if(fnj>=nJ) { fnj=1/((float)nJ); bfloat=true; digit++; } else { fnum=0; break; } } //處理非法字符。 else { num = 0; break; } } //如果處理結束,則將數值乘以符號值。 if(*digit == '\0') { if(bfloat) fnum=num+fnum; else fnum=num; fnum = fnum*minus; } } if(fnum==((int)fnum)) return (int)fnum; else return fnum; } /* 將指定數轉換成漢字大寫格式。 num為待轉換的數,這里只處理整數; nL為num的位數; pdes保存轉換后的漢字。 */ void GetChinese(__int64 num,int nL,char* pdes) { char *pNum="零壹貳叁肆伍陸柒捌玖"; char pUnit[][3]={"","拾","佰","仟","萬","億"}; //用于保存臨時的轉換結果。 char p[3]; //k用于保存每位對應的權值,如第2位的權值是10,第3位的權值為100。 __int64 k=1; for(int i=0;i<nL;i++) { //獲取當前處理位上的數值。 int l=(num%(10*k))/k; /* 如“23”,讀作“貳拾叁”。2后需要加“權”,而個位則不需要。因此,i為0時,也 就是處理個位時,直接將數保存在pdes。 */ if(i>=1) { //處理億和萬以上數值時,需要單獨處理。i為8時,處理億以上的數。 if(i>=8) { //i為8時,獲取“億”。 if(i==8) strcpy(p,pUnit[5]); else //如果是十億、百億以上的數,只需要獲取除8的余數, //就可以獲取其對應的位。 strcpy(p,pUnit[i%8]); } else if(i>=4) { //i為4時,獲取“萬”。 if(i==4) strcpy(p,pUnit[4]); else //如果是十萬、百萬、千萬以上,小于億的數,只需要獲取除4的余數, //就可以獲取其對應的位。 strcpy(p,pUnit[i%4]); } else strcpy(p,pUnit[i]); //將獲取的位值復制到pdes中。pdes保存的是正常讀順序的逆序。 strcat(pdes,p); } //將數值保存到pdes。 strncat(pdes,pNum+l*2,2); k*=10; } //將pdes保存的字符順序逆轉,就是正常讀的順序。 int m=strlen(pdes)-2; pdes[strlen(pdes)]='\0'; for( i=0;i<m/4;i++) { //相應字符交換,漢字占用兩個字符,一次交換兩個字符。 strncpy(p,pdes+i*2,2); strncpy(pdes+i*2,pdes+m-i*2,2); strncpy(pdes+m-i*2,p,2); } p[0]='\0'; /* 下面將字符串中的“零”去掉。如“壹拾零萬零仟叁佰零拾零”,就需要轉換成“壹拾萬叁佰”。 如果“零”字符后是“萬”或者是“億”,使用空格代替“零”; 如果“零”字符后是“仟”、“佰”、“拾”等,則使用空格代替。 然后將空格去掉,實現轉換完畢。 */ //每個漢字占用兩個字符位置,這里針對漢字操作,m/2就是漢字字符數。 for( i=0;i<m/2;i++) { //獲取當前漢字字符。 strncpy(p,pdes+i*2,2); //如果是“零”,則進行處理。 if(strcmp(p,"零")==0) { //設置下一個漢字字符的位置。 int n=i*2+2; char p1[3]=""; //如果沒有到達字符串尾端,則繼續處理。 if(n<m) { //獲取下一個漢字字符。 strncpy(p1,pdes+n,2); //如果不是“萬”和“億”,則全部使用空格替換。 if(!((strcmp(p1,"萬")==0) || (strcmp(p1,"億")==0))) { p1[0]=' '; p1[1]=' '; strncpy(pdes+i*2,p1,2); strncpy(pdes+n,p1,2); } //如果是“萬”和“億”,則使用空格替換“零”。 else { p1[0]=' '; p1[1]=' '; strncpy(pdes+i*2,p1,2); } } else { //處理“個位數”。 p1[0]=' '; p1[1]=' '; strncpy(pdes+i*2,p1,2); } } } //下面的代碼將空格清除掉。 //i保存當前處理字符位置。 i=0; //n為字符前移個數。 int n=0; while(i<strlen(pdes)) { if(pdes[i]==' ') { //記錄i當前位置。 int mn=i; //查找到下一個非空字符。 while(pdes[++i]==' '); //計算前移個數。 n=i-mn+n; //將下一個空格前的字符前移。 while(i<strlen(pdes) && pdes[i]!=' ') { pdes[i-n]=pdes[i]; i++; } } else i++; } pdes[i-n]='\0'; } int main(int argc, char* argv[]) { float fnum=StrToInt("123.123",8); cout<<fnum<<endl; char p[107]=""; GetChinese(123456700089,12,p); cout<<"123456700089:"; cout<<p<<endl; return 0; }
【解析】
這道題考查面試者的基本功。該題實現起來并不難,但是要考慮周全,卻也不簡單。在實現字符轉換數字時,需要注意以下幾點。
(1)正負符號。
如果第一個字符是“+”號,則不需要做任何操作;如果第一個字符是“-”號,則說明這個數是負數,需要將得到的數值變成負數。
(2)非法字符。
非法字符主要為進制外的字符,不同的進制有不同的字符序列,需要針對不同進制進行判斷。進制的范圍也需要限制。
(3)字符轉換成數值的實現方法。
在字符轉換成數值時,每掃描到一個字符,將該字符轉換成數值,并將之前得到的數字乘以10再加上當前數值。
(4)數值的溢出。
在轉換成漢字大寫字符時,需要注意以下幾點。
(1)數字與漢字大寫字符的對應。
這種對應比較容易實現。所給答案就是將漢字大寫字符保存為數組,大寫字符與下標對應。知道數字,就可以獲取對應的漢字大寫字符。
(2)對應“權”。
除了個位不需要加“權”外,其他位都需要加“權”。如123,1后要加“佰”,2后要加“拾”。如果這個數字很大,如123456,1后要加“拾”,而不是“拾萬”。
本題所給答案的實現辦法是將對應的位使用數字標識,如個位使用0標識,十位使用1標識,十萬位就用5標識。5除以4(4是萬的位標識)的余數所對應的“權”就是十萬位的權。對于億以上的數,也采取類似操作。
(3)清除“零”。
這樣處理后的字符串與人們正常讀的順序還是有差別的。如果數值里有很多零,轉換后的字符就會出現很多“零”,如“零仟零佰”之類的。這需要清除這些“零”,并且清除相應的“權”。
(4)“零萬”、“零億”中清除“零”。
如果“零萬”、“零億”出現時,就要謹慎一些。這是因為“萬”和“億”不能清除,只能清除“零”,否則,轉換的結果會不正確。
【試題40】
編寫一個程序,不使用第三方參數交換兩個參數的值。
【答案】
#include <stdio.h> void TranXY( int &x, int &y) { x=x+y; y=x-y; x=x-y; } int main(int argc, char* argv[]) { int x=60; int y=50; printf("x=%d\n",x); printf("y=%d\n",y); TranXY(x,y); printf("TranXY:x=%d\n",x); printf("TranXY:y=%d\n",y); return 0; }
【解析】
一般的數據交換辦法是借用一個臨時變量實現,這道題要求不借助臨時變量,可能出乎一些面試者的意料。這道題考查面試者對基本編程能力的運用。如果面試者只記住一些常用的方法而不能靈活運用,在工作中也很難開拓局面。
這道題實現起來并不難,還可以有以下兩種辦法實現。
/* 使用異或運算實現交換。 異或運算符合交換律。一個數與其本身進行異或后,結果為0。利用這個特點,可以實現數據的交換。 */ void TranXY1( int &x, int &y) { //x保存x和y的異或結果。 x^=y; /* y與x進行異或,實際是x與y異或后再與y進行異或。 異或運算符合交換律、結合律。y與x進行異或可以轉換成x與y和y的異或結果進行異或。 因為一個數與其本身進行異或后,結果為0。任何數與0進行異或,結果還是本身。 所以y與x的異或結果為x。 */ y^=x; //同樣的,x與y(此時的y的內容是原來x沒有進行運算前的內容)的結果為y。 x^=y; } /* 通過x和y的加減運算進行交換。 這個方法是在運算時同時賦值,而沒有使用一個臨時變量。 */ void TranXY2( int &x, int &y) { x = x+y-(y=x) ; }
2.7 總結
有關C/C++程序設計基礎知識的試題都是對多個知識點的綜合考查,很少有對某個知識點進行考查。這也就說明招聘單位已經默認應聘者對知識點都已經掌握,只是通過考查了解應聘者靈活運用能力和對知識點掌握的程度。這就要求應聘者不但要掌握這些知識點,而且要深入、靈活地掌握這些知識點。應聘者可以多讀一些程序代碼,特別是一些“大牛”寫的經典程序代碼。通過閱讀大量代碼,應聘者的經驗和能力會有很大的提高。但是通過閱讀大量代碼的方法提高能力,需要時間;如果應聘者時間有限,可以通過做有關的面試題來增加經驗,增加熟練程度,這可以在較短時間內提高應聘者的能力和經驗。只要應聘者注意細節,考慮周全,靈活應用,肯定能順利過關。