不怎樣的一本書,具體表現為: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成員函數
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. 定義在類聲明之中的成員函數將自動地成為內聯函數
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. 結構體的總大小,必須是內部最大成員的整數倍,不足的要補齊
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;
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. 程序代碼區: 存放函數的二進制代碼
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) |