《程序員面試寶典》(第三版)筆記整理


      不怎樣的一本書,具體表現為:1)該詳細講解的地方,或者一筆帶過或者講得不全面或者講些不相關內容;2)該略過的地方,反而詳細起來;3)有一部分錯誤,如sizeof不計算static變量的大小之類的。雖說如此,收獲還是有的——知道了在筆試中常見的知識點。這里的筆記就是對我不熟悉或者理解不全面的知識點去Google和查書而來的。

C++的關鍵字
1. 使用extern "C"的理由
函數被C編譯器編譯后不帶參數信息,被C++編譯器編譯后會帶上參數信息。這是因為C++支持函數重載。所以函數被C編譯器編譯和被C++編譯器編譯是不同的。例如:void Zero(int lin),被C編譯后,可能得到_Zero,被C++編譯后,可能得到_Zero_int。那如果要在C++中使用被C編譯過的庫呢?使用extern "C"就可以!它會告訴C++編譯器,被它修飾的函數是以C語言的編譯方式編譯的。
 
2. const的作用
a. 聲明常量     
b. 聲明函數形參     -> 提高 自定義類型的調用效率。注意,如果是內部類型,如int,沒必要寫成const int&
c. 聲明函數返回值
d. 修飾類成員函數     -> 防止成員函數對 成員變量進行修改。注意,const成員函數內只能調用類的其他const成員函數
備注:如果要在const成員函數修改某個成員變量,那么可以給這個變量的聲明加上mutable
 
3. static的作用
a. 用於函數內部的局部變量時,能保證函數退出作用域后不回收其空間(即其值保持不變)
b. 用於全局變量時,使變量的作用域限制在一個文件內
c. 用於函數時,使函數只在一個文件內可見
d. 用於類成員變量時,代表該變量屬於類的(即所有對象共享這個變量)
e. 用於類成員函數時,代表該函數為整個類所有。注意,該函數不接收this指針,也意味着不能調用一般的成員函數或者變量
 
4. sizeof操作符
a. 作用:返回一個對象或者類型所占的內存
b. sizeof 不計算類/結構體的static成員 -> 因而可將a中提到所占的內存理解為存儲某個類型的對象所需要空間
c. 對空類使用sizeof會返回1     -> 占用內存為0,無法實例化,所以編譯器為了使空類也能實例化,就給其分配了1Byte
d. 如果類中有虛函數,那么最終結果要多加4Byte     -> 此時多出一個指針,指向虛函數表
e.g:
static int a;     //sizeof(a) = 4,因為這不是類內/結構體內的
 
class Zero {
int a;
static int b;
virtual void fun() {}
};    //sizeof(Zero) = 8
 
class Lin {
virtual void fun() {}
};     //sizeof(Lin) = 4,由d可知其為lin至少有4Byte,所以在這個例子中規則c不成立,因而最終結果為4Byte
 
class Child: public Zero {
     int a;
     virtual void fun() {}
};     //sizeof(Child) = 12
 
5. inline
a. inline只是一種建議,建議編譯器將指定的函數在被調用點展開,因此編譯器是可以忽略inline的(即不展開)
b. inline以代碼膨脹(復制)為代價,省去了函數調用的開銷,從而提高函數的執行效率
c. 每一處inline函數的調用都要復制代碼,這將使得程序的總代碼量增大,消耗更多的內存空間
d. inline必須與函數定義放在一起才能使函數成為內聯,僅將inline 放在函數聲明前面不起任何作用
e. 定義在類聲明之中的成員函數將自動地成為內聯函數
 
6. explicit
禁止單參數構造函數被用於隱式類型轉換
class Zero {
public:
     Zero(int i): lin(i) {}
private:
     int lin;
};
int Fun(Zero z) {}     //如果不用explicit的話,可以這樣調用Fun(1);
 
-------------------------------------------------------------------------------------------
C++的規則
1. 在聲明賦值語句中,變量先聲明,然后賦值
int i = 1;
int main() {
     int i = i;     //這里聲明的i 覆蓋了全局變量的i,之后的賦值就是局部變量的i 給自己賦值,因而其值是未定義的
     return 0;
}
 
2. 從右到左壓參數
int main() {
     int i = 1;
     printf("%d, %d\n", i, ++i);     //VS2010輸出2, 2
}
其它:
雖然壓參數的順序是固定的,但計算順序是編譯器相關的,因此最后的結果與編譯器相關。
 
3. 寫宏時要注意
a. 括號的使用
b. 不要使用分號
e.g: 寫一個找出兩者中較小的宏
#define MIN(a, b) ( (a) < (b) ? (a) : (b) )
 
4. 結構體對齊原則
 a. 數據成員對齊規則:結構(struct或union)的數據成員,第一個數據成員放在offset為0的地方,以后每個數據成員存儲的起始位置要從該成員大小的整數倍開始(e.g: int在32位機為4字節,則要從4的整數倍地址開始存儲)
b. 結構體作為成員:如果一個結構里有某些結構體成員,則結構體成員要從其內部最大元素大小的整數倍地址開始存(e.g: struct a里存有struct b,b里有char,int,double等元素,那b應該從8的整數倍開始存儲)
c. 結構體的總大小,必須是內部最大成員的整數倍,不足的要補齊
備注:設計結構體,最好把占用空間小的類型排在前面,占用空間大的類型排在后面,這樣可以相對節約一些對齊空間
struct Zero {
char b;
int a;     //a的起始位置要按4字節對齊,所以b之后要補3個字節
short c;
};     //sizeof(Zero) = 12
struct OreZ {
char b;
short c;     //c的起始位置要按2字節對齊,所以b之后要補1個字節
int a;
}     //sizeof(Orez) = 8
 
5. 結構體位制
a. 位段成員的類型僅能夠為unsigned或者int, 並且指定的位數不能超過類型的長度
b. 存儲規則:如果下一個位段能存放在之前的存儲單元中(存儲后長度不超過類型長度),則存儲,否則,存儲到新的存儲單元中。因為位段不能跨單元存儲
struct Zero {
     unsigned short a : 4;
     unsigned short b : 5;
     unsigned short c : 7;     //剛好16,符合unsigned short的長度,所以存放在同一單元中
}zero;
int main() {
     zero.a = 2;
     zero.b = 3;
     int i = *((short*)&zero);
     printf("%d", i);     //輸出50(VS2010測試結果),這說明先聲明的變量在低位
     return 0;
}
 
6. C++風格類型轉換
a. const_cast<type>(varible): 去掉變量的const或volatile屬性
b. static_cast<type>(varible): 類似C的強制轉換,但功能上有所限制(如:不能去掉const屬性)     -> 常用於基本類型轉換,void指針與目標指針轉換
c. dynamic_cast<type>(varible): 有條件轉換,運行時進行類型安全檢查(如果失敗則返回NULL)     -> 用於基類與子類之間的類型轉換(必須要有虛函數)
d. reinterpret_cast<type>(varible): 僅僅重新解釋二進制內容    -> 常用於不同類型的指針轉換
class Base {
public:
     virtual void fun() {}
};
class Zero: public Base {
public:
     virtual void fun() {}
};
int main() {
     Base *pb = new Base;
     (dynamic_cast<Zero*>(pb)) -> fun();     //由於實際指向的是基類,所以此轉換失敗,返回NULL,導致運行時錯誤
     delete pb;
     return 0;
}
 
7. 指針與引用的差異
a. 初始化:指針可以不初始化;引用必須在定義時初始化
b. 非空性:指針可以為NULL,因而指針可能是無效的;引用不能為空,因而引用總是有效的
c. 內存占用:指針要占用內存,引用不占     -> 引用只是一個邏輯概念,通常在編譯時就優化掉了
d. 可修改性:指針可以重新指向其它變量;引用不可以
《Effective C++》:在一般情況下,引用和指針是一樣的,但是根據條款23:在返回一個對象時,盡量不要用引用,而是用指針
 
8. 指針與句柄(handle)
指針標記一個內存的地址
句柄是Windows用來標識被應用程序建立或使用的對象(窗口,菜單,內存等)的唯一整數。從構造上看,句柄是一個指針,但其不指向存儲對象的內存位置,而是一個包含了對象所在的內存位置的結構(結構的復雜度由所指的對象決定)
 
9. 指針與淺拷貝
淺拷貝在復制指針時,只是單純地將指針指向同一內存,並不對所指向的內容進行復制。因而,當成員變量有指針時,最好考慮是否要寫深拷貝函數和賦值函數
class Zero {
public:
     Zero(): ptr(NULL) {}
     ~Zero() { if( ptr )     delete[] ptr; }
     char* ptr;
};
 
int main() {
     Zero z;
     z.ptr = new char[100];
     strcpy(z.ptr, "zero");
     vector<Zero> * vz = new vector<Zero>();
     vz->push_back(z);
     delete vz;
     return 0;
}   //退出main時會出現運行時錯誤
由於Zero沒有定義拷貝函數,所以有一個默認的淺拷貝函數。在main函數中,delete vz已經釋放過一次ptr指向的內存,然后在離開main的作用域時z的析構函數啟動,再次釋放該內存
 
10. 迭代器失效
在容器中增加/刪除元素時,可能會使部分或者全部的迭代器失效
 
11. C++編譯器默認產生的成員函數
構造函數,析構函數,拷貝函數,賦值函數
 
12. 類中靜態成員變量與常量
a. 靜態成員變量一定要在類外初始化
b. 成員常量一定要在初始化列表初始化
c. 靜態成員常量一定要初始化,初始化時可以直接在聲明時寫上,也可以像一般的靜態成員變量那樣寫
class Zero {
public:
     Zero():lin2(1) {}
private:
     static int lin;
     const int lin2;
     static const int lin3 = 1;
};
int Zero::lin = 0;
 
13. 初始化列表的初始化順序根據成員變量的 聲明順序執行
class Zero {     //不要寫這樣的代碼
public:
     Zero(int i):lin2(i), lin1(lin2) {}     //此時先初始化lin1,再初始化lin2,因而最后的結果是lin1的值是隨機數,lin2的值是i
private:
     int lin1;
     int lin2;
};
 
14. 構造函數不可以為virtual,析造函數有時必須為virtual
虛函數在運行時動態綁定,動態綁定需要知道動態類型才能進行綁定。而一個類的對象在沒有運行構造函數前,其動態類型不完整,因而無法進行動態綁定
delete指向子類對象的基類指針時,如果析構函數不是virtual,則只會調用基類的析構函數,從而造成內存泄漏
 
15. C++編譯的程序占用的內存
a. stack(棧區): 由OS自動分配釋放空間,主要用來存放函數的參數,局部變量等
b. heap(堆區): 一般由程序員分配釋放空間, 若程序員不釋放,程序結束時可能由OS回收
c. static(全局/靜態區): 存放全局變量和靜態變量。值得注意的是,初始化和未初始化的這兩種變量是分開存放的。
d. 文字常量區: 存放常量字符串
e. 程序代碼區: 存放函數的二進制代碼
 
-------------------------------------------------------------------------------
雜項
1. 不用循環,判斷一個數X是否為2的N的次方(N >= 0)
X & (X - 1)     //最后結果為0,則是2的N次方
分析:2,4,8的二進制式為10, 100, 1000,當其與1, 3, 7的二進制式相與時,其結果為0
 
2. 不用判斷語句,找出兩個數中的最大值
((a + b) + abs(a - b)) / 2
 
3. 不用中間變量,交換兩者的值
a = a ^ b;     //得到a與b的哪些位不相同(即一共有多少位不同)
b = a ^ b;     //去掉b的不同位,換上a的不同位,從而得到a
a = a ^ b;
 
4. 寫一個宏FIND,求結構體struc里某個變量相對struc的偏移量
#define FIND(struc, e) (size_t)&(((struc*)0)->e)
解析:(struc*)0將0強制轉換為struc*的指針類型,然后再取(struc*)0的成員e的地址,從而得到e離結構體首地址的偏移量
 
5. 數組名再取地址
int a[] = {1, 2, 3};
int *ptr = (int*)(&a + 1);     //&a相當於得到二維數組指針,因而&a + 1相當於移動一行
printf("%d\n", *(ptr - 1));    //輸出3
 
6. 指針的移動
int main() {
     int *pi = NULL;
     pi += 15;
     printf("%d\n", pi);     //輸出60
     return 0;
}
 
7. 整數超范圍
如果是有符號數,那么最大正整數 + 1 等於最小負整數
如果是無符號數,那么最大正整數 + 1 等於零
bool IsOdd(int i) {
     return (i & 1) == 1;    //不要寫成(i % 2) == 1,因為這樣寫判斷負數時會出問題
}
int main() {
     for(int i = 0xFFFFFFFF; i <= 0x7FFFFFFF; ++i) {     //這會陷入死循環,因為最大正整數 + 1 等於最小負數
          cout<<i<<" is "<<IsOdd(i) ? "odd" : "even"<<endl;
     }
     return 0;
}
 
8. 基類指針與子類指針
class Base {
     int a;
};
class Zero: public Base {
     int b;
};
int main() {
     Zero *pz = new Zero();
     Base *pb = dynamic_cast<Base*>(pz);
     (pz == pb) ? (cout<<"Yes\n") : (cout<<"No\n");     //VS2010輸出"Yes"
     (int(pz) == int(pb)) ? (cout<<"Yes\n") : (cout<<"No\n");      //VS2010輸出"Yes"
     return 0;
}
 
9. strcpy的寫法
char* strcpy(char* dst, const char* src) {     //返回char*是為了方便鏈式調用
     assert( (src != NULL) && (dst != NULL) );     //特別注意檢查
     char* tmp = dst;
     while( *dst++ = *src++ );    //++和*的優先級一樣,而雙者均為右結合,所以*dst++相當於*(dst++)
     return tmp;
}
 
10. 概率題
int main() {
     int cnt = 0;
     for(int i = 0; i < 10000; ++i) {
          int x = rand();
          int y = rand();
          if( x * x + y * y < RAND_MAX * RAND_MAX )     ++cnt;     //實質上是1/4圓與正方形的面積的比較
     }
     printf("%d\n", cnt);     //結果約為PI/4 * 10000
}
 
11. 以下程序在編譯時,哪句會出錯
struct Zero {
     void Lin() {}
};
int main() {
     Zero z();     //這樣寫會被編譯器認為是聲明一個函數,而不是調用構造函數
     z.lin();     //這句會出錯,因為此時的z被認為是未聲明的變量
     return 0;
 
12. 各種排序總結
算法 穩定性 時間復雜度 空間復雜度 備注
選擇排序 F O(n^2) O(1)  
插入排序 T O(n^2) O(1) 當序列有序時,時間復雜度為O(n)
冒泡排序 T O(n^2) O(1) 當序列有序時,時間復雜度為O(n)
希爾排序 F O(nlogn) O(1) 具體的時間復雜度與所選的增量序列有關
歸並排序 T O(nlogn) O(n)  
堆排序 F O(nlogn) O(1)  
快速排序 F O(nlogn) O(logn) 當序列有序時,時間復雜度惡化為O(n^2)
桶排序 T O(n) O(k)  

 

      


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM