第一章 C/C++程式基礎 一、一般賦值語句: 考察一般賦值語句的概念和方法。 1.程式: 2.答案: 3.分析: 代碼行.12中的“&”表示位與計算,即將y和z轉換為二進位數字10和10,進行位與計算。1&1=1,其餘都是0,故結果為二進位10。所以答案為2,x=2。 代碼行.16中的“|”表示位 ...
第一章 C/C++程式基礎
一、一般賦值語句:
考察一般賦值語句的概念和方法。
1.程式:
1 #include<stdio.h>
2
3 int main(void)
4 {
5 int x=3,y,z;
6
7 x*=(y=z=4); printf("x=%d\n",x);
8
9 z=2;
10 x=(y=z); printf("x=%d\n",x);
11 x=(y==z); printf("x=%d\n",x);
12 x=(y&z); printf("x=%d\n",x);
13 x=(y&&z); printf("x=%d\n",x);
14
15 y=4;
16 x=(y|z); printf("x=%d\n",x);
17 x=(y||z); printf("x=%d\n",x);
18
19 x=(y==z)? 1:(y<z)? 2:3;
20 printf("x=%d\n",x);
21
22 return 0;
23 }
2.答案:
x=12
x=2
x=1
x=2
x=1
x=6
x=1
x=5
x=3
3.分析:
代碼行.12中的“&”表示位與計算,即將y和z轉換為二進位數字10和10,進行位與計算。1&1=1,其餘都是0,故結果為二進位10。所以答案為2,x=2。
代碼行.16中的“|”表示位或計算,方法同上。0|0=0,其餘都是1,故結果為二進位110。所以答案為6,x=6。
4.小結:
一般賦值語句,記住符號的優先順序即可。另外記住一些特殊的運算符,如位計算符、三目運算符。
(圖片來自網路資源)
二、C++域操作符:
1.程式:
1 #include<stdio.h>
2
3 int value=0;
4
5 void printvalue()
6 {
7 printf("value=%d\n",value);
8 };
9
10 int main()
11 {
12 int value=0;
13
14 value=1;
15 printf("value=%d\n",value);
16
17 ::value=2;
18 printvalue();
19
20 return 0;
21 }
2.答案:
(C++中輸出)
value=2 //局部變數value
value=1 //全局變數value
PS:C語言下無法編譯通過。
3.分析:
這段程式包含著兩個value變數。其中一個是在main函數前就聲明的全局變數,另一個是在main函數內部聲明的局部變數。這兩者作用域是不同的(局部變數只可以作用魚定義它的函數內部)。
在main函數中代碼行.15中輸出的是局部變數value的值(value=1)。這是因為在main函數中的局部變數value引用優先。在C++中可以通過域操作符“::”來直接操作全局變數(代碼行.17改變了全局變數value的值)。在之後的代碼行.18中調用printvalue函數輸出全局變數value的值(value=2)。
三、i++與++i哪個效率更高:
其實,單純考慮首碼自增運算和尾碼自增運算是沒有意義的。首先考慮內建數據類型的情況:如果自增運算表達式的結果沒有被使用,而只是用於增加一員操作數,那麼兩者沒有任何區別。主要需要查看自定義數據類型的情況。
1.程式:
1 #include<stdio.h>
2
3 int main()
4 {
5 int i=0;
6 int x=0;
7
8 i++;
9 ++i;
10 x=i++;
11 x=++i;
12
13 return 0;
14 }
經過VC++6.0編譯,得到如下彙編代碼:
;Line 5
mov DWORD PTR _i$[ebp],0
Line 6
mov DWORD PTR _x$[ebp],0
Line 8
mov eax,DWORD PTR _i$[ebp]
add eax,1
mov DWORD PTR _i$[ebp],eax
Line 9
mov ecx,DWORD PTR _i$[ebp]
add ecx,1
mov DWORD PTR _i$[ebp],ecx
Line 10
mov edx,DWORD PTR _i$[ebp]
mov DWORD PTR _x$[ebp],edx
mov eax,DWORD PTR _i$[ebp]
add eax,1
mov DWORD PTR _i$[ebp],eax
Line 11
mov ecx,DWORD PTR _i$[ebp]
add ecx,1
mov DWORD PTR _i$[ebp],ecx
mov edx,DWORD PTR _i$[ebp]
mov DWORD PTR _x$[ebp],edx
2.答案:
內建數據類型,效率沒有區別。
自定義數據類型,++i效率更高。
3.分析:
代碼行.8-9生成的彙編代碼幾乎完全一致。
代碼行.10-11生成的彙編代碼只是在加1的先後順序上有所區別,效率也完全一致。
由此說明,考慮內建數據類型時,它們的效率差別不大。
那麼再考慮自定義數據類型(主要指類)的情況,由於首碼式(++i)可以返回對象的引用,而尾碼式(i++)必須返回對象的值,從而導致在大對象的時候產生較大的複製開銷,引起效率降低。故自定義數據類型時,首碼式遞增/遞減效率更高。
4.小結:
表示分析中的最後一段,估計很多如我一般的萌新都感覺不是很懂。所以經過百度和查閱後,給大家一個翻譯後的解釋。
(以下代碼來自博客:為什麼(i++)不能做左值,而(++i)可以) // 首碼形式:
int& int::operator++()
//這裡返回的是一個引用形式,就是說函數返回值也可以作為一個左值使用
{
//函數本身無參,意味著是在自身空間內增加1的
*this += 1; // 增加
return *this; // 取回值
}
//尾碼形式:
const int int::operator++(int)
//函數返回值是一個非左值型的,與首碼形式的差別所在。
{
//函數帶參,說明有另外的空間開闢
int oldValue = *this; // 取回值
++(*this); // 增加
return oldValue; // 返回被取回的值
}
故++i與i++兩者的具體實現函數的不同造成在引用時,兩者所占據的資源不同。
四、選擇編程風格良好的條件選擇語句:
1.程式:
A.布爾型:假設布爾變數名字為flag,它與零值比較的標準if語句如下:
第一種:
1 if(flag==TRUE)
2 if(flag==FALSE)
第二種:
1 if(flag)
2 if(!flag)
B.整型:假設整型變數的名字為value,它與零值比較的標準if語句如下:
第一種:
1 if(value==0)
2 if(value!=0)
第二種:
1 if(value)
2 if(!value)
C.浮點型:假設浮點變數的名字為x,它與零值比較的標準if語句如下:
第一種:
1 if(x==0.0)
2 if(x!0.0)
第二種:
1 if((x>=-EPSINON)&&(X<=EPSINON))
2 if((X<-EPSINON)||(X>EPSINON))
PS:其中EPSINON為允許的誤差(精度)。
D.指針型:指著變數p與0的比較如下:
第一種:
1 if(p==NULL)
2 if(p!=NULL)
第二種:
1 if(p==0)
2 if(p!=0)
2.答案:
A:後者風格較為良好。
B:前者風格較為良好。
C:後者風格較為良好。
D:前者風格較為良好。
3.分析:
A:根據布爾類型的語義,零值為“假”(記為FALSE),任何非零值都是“真”(記為TRUE)。TRUE的值並沒有統一的標準。因此,不可將布爾變數直接與TURE、FALSE進行比較。
B:後者風格容易令人誤解value為布爾變數。所以應該將整型變數用“==”與“!=”直接與0比較。
C:浮點型變數都有精度限制。一定要極力避免將浮點變數通過“==”或“!=”與數字進行比較,應該設法轉化為精度與“>=”、“<=”間的比較。(論證在C教程上)
D:指針變數的零值為“空”(記為NULL)。儘管NULL的值與0相同,但是兩者意義不同。用p與NULL顯式比較,強調p是指針。
五、有符號變數與無符號變數的值的轉換:
(有符號變數和無符號變數的區別:http://blog.csdn.net/gogokongyin/article/details/39758289)
1.程式:
1 #include<stdio.h>
2
3 char getChar(int x,int y)
4 {
5 char c;
6 unsigned int a=x;
7
8 (a+y>10)? (c=1):(c=2);
9 return c;
10 }
11
12 int main(void)
13 {
14 char c1=getChar(7,4);
15 char c2=getChar(7,3);
16 char c3=getChar(7,-7);
17 char c4=getChar(7,-8);
18
19 printf("c1=%d\n",c1);
20 printf("c2=%d\n",c2);
21 printf("c3=%d\n",c3);
22 printf("c4=%d\n",c4);
23
24 return 0;
25 }
2.答案:
c1=1
c2=2
c3=2
c4=1
3.分析:
在getChar函數中,有兩個整型的輸入參數x和y。在函數內,把參數x的值轉換為無符號(unsigned)整型後再與y相加。之後再與10比較。
註意:在表達式中存在有符號類型和無符號類型時,所有的操作數都自動隱式轉換成無符號類型。
代碼行.14中,傳入的參數分別為7和4,兩個數相加後為11,因此c1返回1。
代碼行.15中,傳入的參數分別為7和3,兩個數相加後為10,因此c2返回2。
代碼行.16中,傳入的參數分別為7和-7,在signed下,-7為7的原碼00000000 00000111的反碼11111111 11111000加1,得到其補碼11111111 11111001。在signed下,整數通過16位開頭第一位區別正負,0為整數,1為負數。餘下的15位才是真正的數欄位。但是unsigned下,是沒有負數的,也就是說整整16位都是數欄位。所以signed下的-7,在unsigned下轉化為65529。那麼7和65529相加後為65536,這個值得大小正好溢出(unsigned int數據範圍為0-65535共計65536個數字)(實際得到的結果為0)。因此c3返回2。
PS:我只按照16位解釋,將位數增加到32位也是沒有區別的。
代碼行.17中,傳入的參數分別為7和-8,實際轉化方式如上,最終相加得到的值為65535。因此c4返回1。
4.小結:
其實這個程式難點在於兩處:一方面,要知道有符號、無符號的概念與區別;另一方面,知道原碼、反碼、補碼的計算。
當然,你十分熟悉這個過程,一眼就看出來也行。通過7+(-7)=0,知道-7為是否溢出的負數上限,看出負數中[-65535,-8]滿足條件,[-7,-1]會導致溢出(正數大家都可以一眼看出)。
六、不使用任何中間變數如何將a、b的值進行轉換:
表示這個問題我在剛學編程是就想過,因為我們老師說轉換兩者值必須中間變數,然後我就想出通過加、減(下麵程式第二個方法)寫出代碼,給老師看去了。
表示我“打”老師臉最開心的幾次。淡定ing。
1.程式:
1 #include<stdio.h>
2
3 void swap1(int& a,int& b)
4 {
5 int temp=a; //使用局部變數temp完成交換
6 a=b;
7 b=temp;
8 };
9
10 void swap2(int& a, int& b)
11 {
12 a=a+b; //使用加減運算完成交換
13 b=a-b;
14 a=a-b;
15 };
16
17 void swap3(int& a,int& b)
18 {
19 a^=b; //使用異或運算完成交換
20 b^=a;
21 a^=b;
22 };
23
24 int main(void)
25 {
26 int a1=1,b1=2;
27 int a2=3,b2=4;
28 int a3=5,b3=6;
29 int a=2147483647,b=1;
30
31 swap1(a1,b1); //測試使用臨時變數進行交換的版本
32 swap2(a2,b2); //測試使用加減運算進行交換的版本
33 swap3(a3,b3); //測試使用異或運算進行交換的版本
34
35 printf("after swap...\n");
36 printf("a1=%d,b1=%d\n",a1,b1);
37 printf("a2=%d,b2=%d\n",a2,b2);
38 printf("a3=%d,b3=%d\n",a3,b3);
39
40 swap2(a,b);
41 printf("a=%d,b=%d\n",a,b);
42
43 return 0;
44 }
2.答案:
after swap...
a1=2,b1=1;
a2=4,b2=3;
a3=6,b3=5;
a1=1,b1=2147483647
3.分析:
首先,swap1函數就不談了。通過一個中間變數temp來達到交換目的。其次,swap2函數就是通過a,b的和與差來進行轉化,也就是需要對數學有一定的基礎和靈性。
之後,就是一個小重點(沒辦法,學校C課程對位運算提到原碼,補碼,反碼),那就是通過位運算中的異或運算來實現數值轉換。
4.小結:
其實從某種角度上來說第三種方法時前兩種方法的綜合。一方面,這個方法存在中間變數,那就是代碼行.19中的最終結果a。另一方面,這個方法將這個中間變數保存在了兩個變數中的變數a。之所以沒說這個方法和第二種一致,是因為它只用了兩個變數中的一個變數存儲中間變數就是實現了最終目的。所以,我很喜歡它。
七、標準頭文件的結構:
考察標準頭文件中一些通用結構的理解。
1.程式:
1 #ifndef _INCvxWorksh
2 #define _INCvxWorksh
3 #ifdef _cplusplus
4 extern "C" {
5 #endif
6 /*...*/
7 #ifdef _cplusplus
8 }
9 #endif
10 #endif /* _INCvxWorksh */
2.答案:
代碼行.1,2,10中代碼的作用是防止頭文件被重覆引用。
代碼行.3中代碼的作用是表示當前使用的是C++編譯器。
代碼行.4-8中的extern"C"是C++編譯器提供的與C連接交換指定的符號,用來解決名字匹配問題。
3.分析:
代碼行.1,2,10中代碼是為了防止頭文件的重覆引用。因為常常一個CPP文件有多個頭文件,而頭文件之間又是可以相互引用的,這就可能造成頭文件的重覆引用,當頭文件被重覆引用時,編譯器就會報錯,顯示頭文件被重覆定義。
那麼避免重覆引用頭文件有兩種方法:
1.使用 ifndef/define/endif 結構產生預處理塊
優點:由於這個語句是語言相關的,可移植性好。
缺點:由於只有打開了某個頭文件,編譯器才能根據保護巨集確定是否引用過了。所以效率相對較低。但是這種方法是通過保護巨集名來確定某個頭文件是否被引用過,那麼巨集名重覆,就出現需要的頭文件明明就在那裡,編譯器卻說找不到這個頭文件(因為那個頭文件因為保護巨集名重覆而被屏蔽了)。
2.使用 #pragma once
優點:由編譯器提供保證,同一個文件不會被包含兩次(這裡的包含指的是物理地址,即文件的路徑地址)。所以效率較高。
缺點:由於頭文件的屏蔽是編譯器通過頭文件物理地址的記錄來實現的。所以如果某一個頭文件有多個複製的副本,依舊會造成頭文件重覆引用的問題。
另外這個方法只能在微軟開發工具上實現。很多編譯器沒有,或者有這個方法的編譯器打算移除。
(PS:上述部分資料引用於http://blog.csdn.net/zhl30041839/article/details/37728237)
代碼行.3表明瞭當前使用的編譯器為C++編譯器。如果需要表明為C編譯器,則語句可以表示為“#indef _STDC_”。
代碼行.4-8中 extern "C"包含著雙重意義。這句話主要由兩個部分“extern”與“C”組成。
前者是C/C++的一個重要關鍵字,用於表明其修飾的變數和函數作用範圍。extern表明其聲明的變數和函數可以在本模塊或其他模塊中使用。通常,在模塊的頭文件中對本模塊提供給其他模塊引用的函數和全局變數以關鍵字extern聲明。在模塊的頭文件中對本模塊提供給其它模塊引用的函數和全局變數以關鍵字extern聲明。例如,如果模塊B欲引用該模塊A中定義的全局變數和函數時只需包含模塊A的頭文件即可。這樣,模塊B中調用模塊A中的函數時,在編譯階段,模塊B雖然找不到該函數,但是並不會報錯;它會在連接階段中從模塊A編譯生成的目標代碼中找到此函數。
(PS:上述部分資料引用於http://www.cnblogs.com/skynet/archive/2010/07/10/1774964.html)
後者表示其修飾的變數和函數是按照C語言的方式編譯和連接的。由於C++支持函數重載,而過程式語言C則不支持,故C++和C語言的函數編譯是存在不同的。
例如:“void foo(int x,int y);”與“void foo(int x,float y);”在C++語言也許會被編譯為“_foo_int_int”與“_foo_int_float”(C++通過這種機制來實現函數重載),但在C語言編譯中都是“_foo”。這就會造成在C中連接C++編譯的函數符號時,就會因此找不到匹配的符號而發生連接錯誤。如果添加了extern“C”聲明後,模塊編譯會生成foo的目標代碼時,就不會對其名字進行特殊處理,而採用C語言的方式處理(也就是不會添加參數表信息)。
八、#include<head.h>與#include"head.h"的區別:
1.答案:
尖括弧<>表明其中的文件head.h是一個工程或者標準頭文件。所以編譯器會在查找過程中優先檢查預定義的目錄。當然,我們可以通過設置搜索路徑環境變數或命令行選項來修改這些目錄。
引號""表明其中的文件head.h是有用戶提供的頭文件。所以編譯器在查找過程中會優先從當前文件目錄(或文件名指定的其他目錄)中尋找該文件,然後再在標準位置尋找文件。
九、C++中main函數執行完後還需要執行的語句:
1.程式:
1 #include<stdlib.h> //使用atexit()函數必須包含頭文件stdlib.h
2 #include<stdio.h>
3
4 void fn1(void);
5 void fn2(void);
6
7 int main(void)
8 {
9 atexit(fn1); //使用atexit註冊fn1()函數
10 atexit(fn2); //使用atexit註冊fn2()函數
11 printf("main exit...\n");
12 return 0;
13 }
14
15 void fn1()
16 {
17 printf("calling fn1()...\n"); //fn1()函數列印內容
18 }
19
20 void fn2()
21 {
22 printf("calling fn2()...\n"); //fn2()函數列印內容
23 }
運行結果:
main exit...
calling fn2()...
calling fn1()...
2.答案:
可以通過atexit()函數(at-exit)來註冊程式正常終止時要被調用的函數,並且在main()函數結束時,調用這些函數順序和註冊它們的順序相反。
3.分析:
我們常常需要在程式結束時解決一些類似資源釋放的操作,但是程式退出的方法很多(有興趣的可以看看WIN32編程中提及的),所以我們需要一個一種與程式退出方式無關的方式,來進行那些在程式退出時進行的必要操作。
atexit()函數就是這樣一個滿足條件的函數,它通過註冊程式正常終止時需要被調用的函數。其函數原型為:
1 int atexit (void (*)(void));
在一個程式中最多可以通過atexit()函數註冊32個必要處理函數。不過這些必要處理函數的調用順序和它們的註冊順序恰好相反。
(PS:有時也有一些有關進程關閉的語句會令這個函數無法使用,具體可以參照百度百科-atexit)