[C/C++筆試面試題] 程式設計基礎 - 變數、字元、編譯篇
10 變數
變數是一段有名字的連續儲存空間,它是程式中資料的臨時存放場所。
10.1 全域性變數和靜態變數有什麼異同?
相同:都在靜態儲存區分配空間,生命週期與程式生命週期相同。
區別:全域性變數的作用域是整個程式,它只需要在一個原始檔中定義,就可以作用於所有的原始檔。而靜態變數只在定義其的原始檔內有效。
10.2 變數定義與變數宣告有什麼區別?
定義(definition) 為變數分配儲存空間,還可以為變數指定初始值。而宣告(declaration)是指向程式表明變數的型別和名字。 定義也是宣告,定義變數的同時也聲明瞭它的型別和名字。可以通過使用extern關鍵字宣告變數名而不定義它,它所說明的並非自身,而是描述其他地方建立的物件,可以多次出現, 如 extern int my_array[]。
“定義也是宣告”,這說明定義包括宣告,對於int a來說,它既是定義又是宣告,對於 extern int a來說,它是宣告不是定義。一般為了敘述方便,把建立儲存空間的宣告稱定義,而 不把建立儲存空間的宣告稱為宣告。
10.3 不使用第三方變數,如何交換兩個變數的值?
#include <iostream> using namespace std; #define swap1(x,y) x=x+y,y=x-y,x=x-y #define swap2(x,y) x^=y,y^=x,x^=y int main() { int a=1,b=2; //方法1 - 算數法 swap1(a,b); cout << "a=" << a << " b=" << b << endl; //方法2 - 異或法 a=1,b=2; swap2(a,b); cout << "a=" << a << " b=" << b << endl; }
10.4 C語言中各種變數的預設初始值是什麼?
全域性變數放在記憶體的全域性資料區,如果在定義的時候不初始化,則系統將自動為其初始化,數值型為0,字元型為NULL,即0,指標變數也被賦值為NULL。靜態變數的情況與全域性變數類似。而非靜態區域性變數如果不顯示初始化,那麼其內容是不可預料的,將是隨機數,會很危險,對系統的安全造成非常大的隱患。
11 字元
字串是程式語言的基礎,對於字串的處理一直是程式設計的重要組成部分,所以在面試筆試中,經常會考查字串處理函式的機理。掌握常見的字串處理函式原理,並能自己實現其原型,對於應對程式設計師面試筆試非常重要。
11.1 不使用C/C++字串庫函式,如何自行編寫strcpy()函式?
char * strcpy(char *strDest, const char *strSrc)
{
assert((strDest!=NULL) && (strSrc!=NULL));
char *address = strDest;
while( (*strDest++ = *strSrc++) != '\0');
return address ;
}
返回型別為char *主要是為了實現鏈式表示式。例如:
int length=strlen(strcpy(strDest, "hello world"));
strcpy(strDest, strcpy(strDest1, strSrc));
可以將strSrc複製到strDest1與strDest中,也就是說,可以將函式的返回值做為另一個函式的引數。
11.2 如何把數字轉換成字串?
C語言中常用到字串與數字之間的相互轉換,常見的此類庫函式有atof (字串轉換成浮點數)、atoi (字串轉換成整型數)、atol (字串轉換成長整型數)、itoa (整型數轉換成字串)、ltoa (長整型數轉換為字串)等。
為了考查求職者對基本功的掌握,在程式設計師的面試筆試中仍然經常會見到此類題目,讓求職者自定義此類函式的實現,此類題目雖然難度不大,但是需要認真仔細對待。
以自定義Myatoi()與Myitoa()函式為例,分別實現自定義字串轉換為整型數函式與自定義整型數轉換為字串函式。以下為自定義Myatoi()函式的實現以及測試程式碼:
#include<stdio.h>
int Myatoi(char *str)
{
if(str = NULL)
{
printf("Invalid Input");
return -1;
}
while(*str = '') //跳過開頭的空格字元
{
str++;
}
//'0xA1'是不列印字元,一般是佔兩個位元組的漢字
while((*str==(char)0xA1) && (*(str+1)==(char)0xAA))
{
str += 2;
}
int nSign = (*str = '-')?-1:1;//確定符號位
if(*str='+' || *str='-')
{
*str++;
}
int nResult = 0;
while(*str>='0' && *str<='9')
{
nResult = nResult*10 + (*str-0);
*str++;
}
return nResult * nSign;
}
int main()
{
printf(""%d\n",Myatoi("12345"));
return 0;
}
程式輸出結果:
12345
以下為自定義Myitoa函式的實現以及測試程式碼:
#include <stdio.h>
char *Myitoa(int num)
{
char str[1024];
int sign = num,i = 0,j = 0;
char temp[11];
//如果為負數,則轉換為其絕對值
if(sign<0)
{
num = -num;
};
//數字轉換為倒序的字元陣列
do
{
temp[i] = num%10 + '0';
num/=10;
i++;
}while(num>0);
//字元陣列加上“符號”
if(sign<0)
{
temp[i++] = '-';
}
//轉換為字串
temp[i] = '\0';
//將字串反轉
i—-;
while(i>=0)
{
str[j] = temp[i];
j++;
i--;
}
str[j] ='\0';
return str;
}
int main()
{
printf("%s\n",Myitoa(-12345));
return 0;
}
程式輸出結果:
12345
11.3 如何自定義記憶體複製函式memcpy()?
memcpy是C語言中的記憶體複製函式,它的函式原型為void memcpy(void dest, const void*src, size_t n)。它的目的是將src指向地址為起始地址的連續n個位元組的資料複製到以dest指向地址為起始地址的空間內,函式返回指向destin的指標。需要注意的是,src和dest所指記憶體區域不能重疊,同時,與strcpy相比,memcpy遇到’\0’不結束,而是一定會複製完n個位元組。
自定義記憶體複製函式示例如下:
#include <stdio.h>
void *MyMemCpy(void *dest, const void *src, size_t count)
{
char *pdest = static_cast<char*>(dest);
const char *psrc = static_cast<const char*>(src);
if((pdest>psrc) && (pdest<(psrc+count))) //這種情況:MyMemCpy(str+1, str+0, 9);
{
for(size_t i=count-l; i!=-l; —-i)
pdest[i] = psrc[i];
}
else
{
for(size_t i=0; i<count; ++i) //這種情況:MyMemCpy(str, str+5, 5);
pdest[i] = psrc[i];
}
return dest;
}
int main()
{
char str[] = "0123456789";
MyMemCpy(str+1, str+0, 9);
printf("%s\n",str);
MyMemCpy(str, str+5, 5);
printf("%s\n",str);
return 0;
}
12 編譯
編譯是程式執行過程中的一個重要步驟,瞭解程式的編譯過程以及編譯方式都對程式的開發起著至關重要的作用。通過對編譯過程的理解,有助於程式設計師編寫邏輯清晰、髙效的程式碼, 有助於減少程式中存在的潛在Bug。
12.1 編譯和連結的區別是什麼?
在多道程式環境中,要想將使用者原始碼變成一個可以在記憶體中執行的程式,通常分為3個步驟:編譯、連結、載入。
(1) 編譯:將預處理生成的檔案,經過詞法分析、語法分析、語義分析以及優化後編譯成若干個目標模組。可以理解為將高階語言翻譯為計算機可以理解的二進位制程式碼,即機器語言。
(2) 連結:由連結程式將編譯後形成的一組目標模組以及它們所需要的庫函式連結在一起,形成一個完整的載入模型。連結主要解決模組間的相互引用問題,分為地址和空間分配, 符號解析和重定位幾個步驟。 連結一般分為靜態連結、載入時動態連結以及執行時動態連結3種。
(3) 載入:由載入程式將載入模組載入記憶體。
編譯和連結是為將使用者程式從硬碟上調入記憶體並將其轉換成可執行程式服務的。以C/C++語言為例,把原始檔編譯成中間程式碼檔案,在windows下面為.obj檔案,在 UNIX、Linux F面就是.o檔案,即Object File,該動作被稱為編譯。然後再把大量的object File合成可執行檔案,這個動作稱為連結。
編譯時,編譯器需要的是語法正確,函式與變數的宣告正確。而一般來說,每個原始檔都應該對應於一箇中間目標檔案(.o檔案或是.obj檔案)。連結時,主要是連結函式和全域性變數, 所以可以使用這些中間目標檔案(.o檔案或是.obj檔案)來連結應用程式。連結就是那些目標檔案之間相互連結自己所需要的函式和全域性變數,而函式可能來源於其他目標檔案或庫檔案。
12.2 如何判斷一段程式是C編譯程式還是C++編譯程式編譯的?
如果編譯器在編譯cpp檔案,那麼_cplusplus就會被定義,如果是一個C檔案在被編譯, 那麼_STDC_就會被定義。_STDC_是預定義巨集,當它被定義後,編譯器將按照ANSIC標準來編譯C語言程式。
所以,可以採用如下程式示例判斷:
#ifdef _cplusplus
#define USING_C 0
#else
#define USING_C 1
#endif
#include <stdio.h>
int main()
{
if(USING_C)
printf("C\n");
else
printf("C++\n");
return 0;
}
12.3 C++程式中呼叫被C編譯器編譯後的函式,為什麼要加extern "C"?
C++語言是一種面向物件程式語言,支援函式過載,而C語言是面向過程的程式語言, 不支援函式過載,所以函式被C++編譯後在庫中的名字與C語言的不同。如果宣告一個C語 言函式float f(int a,char b), C++的編譯器就會將這個名字變成像_f_int_char之類的東西以支援函式過載。然而C語言編譯器的庫一般不執行該轉換,所以它的內部名為_f,這樣聯結器將無法解釋C++對函式f()的呼叫。
C++提供了 C語言 (替代連線說明)符號extern “C”來解決名字匹配問題,extern後跟一個字串來指定想宣告的函式的連線型別,後面是函式宣告。
extern "C" float f(int a,char b);
該語句的目的是告訴編譯器f()是C連線的,這樣C++就不會轉換函式名。應該到庫中找名字_f而不是找_f_int_char。 C++編譯器開發商已經對C標準庫的標頭檔案作了extern “C”處理,所以可以用include直接引用這些標頭檔案。
12.4 兩段程式碼共存於一個檔案,編譯時有選擇地編譯其中的一部分,如何實現?
可以通過以下兩種方法實現:
(1) 在原始碼中使用條件編譯語句,然後在程式檔案中定義巨集的形式來選擇需要的編譯程式碼。
(2) 在原始碼中使用條件編譯語句,然後在編譯命令的命令中加入巨集定義命令來實現選擇編譯。