10.1.1 程序的編譯和執行
- 以#開頭的代碼都屬于預處理器處理的步驟
- #include? 將頭文件的內容包含進入當前源文件中
- #define? ?展開宏定義
- #ifdef? ? ? 處理條件編譯指令(#ifdef、ifndef、#if、#else、#elif、#endif)
- #other? ? 處理其他宏指令(#error、#warning、#line、#pragma)
預處理器? 其他功能如下:
- 處理預先定義的宏:例如__DATE__? ?__FILE__(其后均為兩條下劃線)
- 處理注釋:用一個空格代替連續的注釋
- 處理三元符
注意
- 編譯器對預先處理過的代碼進行詞法分析、語法分析和語義分析,將符合規則的程序轉化為等價的匯編代碼
- 匯編器將編譯器生成的匯編代碼翻譯成為計算機可以識別的機器指令,并生成目標文件。之所以不將源程序之間轉化成為機器指令,而分成了多級轉化,是為了在不同的階段應用不同的優化技術,并且這些優化技術已經很強。可以保證各個階段分別優化之后可以生成更為高效的機器指令
- 鏈接器 將所有的目標程序鏈接在一起,無論是靜態還是動態鏈接,最終都可以生成一個可以在機器上運行的可執行的程序。運行可執行程序就會得到執行的結果
10.1.2 金典面試題解析
簡述#include<>和#include“ ”之間的區別
- 主要目的都是為了 將指定文件中的內容引入到當前的文件,但是搜索被引入文件的時候,二者采用了不同的搜索策略
- #include<>:直接從編譯器指定的路徑處開始搜索
- #include“ ”:從程序所在的目錄進行搜索,如果搜索失敗,再從編譯器指定的路徑處開始搜索
- 用戶自定義的文件只能使用#include“ ”,如果系統文件使用#include“ ”,會在用戶目錄進行無意義的搜索嘗試,如果找不到,再按照編譯器指定的路徑處進行搜索
簡述#和##在define里面的作用
#include <iostream>
using namespace std;
#define PRINTCUBE(x) cout<<"cube("<< #x<<") ="<< (x)*(x)*(x) << endl;int main(){int y = 5;PRINTCUBE(5);PRINTCUBE(y);return 0;
}
// cube(5) =125
// cube(y) =125
- define里面使用了#x,因此#x是會被字符串化的宏參數
#include <iostream>
using namespace std;
#define LINK3(x,y,z) cout << x##y##z << endl;
int main(){
// LINK3("C","+","+");LINK3(3,5,0);return 0;
}
// 350
- #運算符會將其前后參數轉化成為字符串
- ##運算符會將前后的參數進行字符串的連接
assert 斷言的概念
- assert是一個帶參數的宏定義,不是一個函數,頭文件在assert.h文件里面
- 使用assert檢測條件表達式,如果表達式為假,表示檢測失敗,程序會向標準錯誤流stderr輸出一條錯誤的信息,再次調用函數abort函數終止程序的執行
- 頻繁使用assert會降低程序的性能,增加額外的開銷,一般是調試中使用宏,調試完成之后,在#include語句之前 使用#define DEBUG禁用assert宏定義
- assert雖然可以檢測多個條件,但是最好不要如此使用,因為一旦出錯,不知道是哪個條件導致的錯誤,因此沒有任何的意義
- 不要在assert里面修改變量的數值,因為assert只會在debug版本中使用,一旦使用了RELEASE版本,所有的assert都會失效。
- 斷言的目的是為了捕獲可控但是不應該發生的情況,不適合外部接口的函數檢測,因為外部輸入的參數是一個不可控的事件,應該使用if進行判定
- 斷言用于 內部函數,其參數檢測使用斷言;因為函數的使用者是程序開發者,錯誤的參數是可控并且不該發生的情況
- assert用于程序的DEBUG版本檢測條件的表達式,如果結果為假,則輸出診斷信息并且終止程序的執行
10.2.1? 知識點梳理
- 變量名使用小寫字母;常量、宏定義使用大寫字母
C++類型的轉換操作符
static_cast?
- static_cast 完全可以替代C風格的類型轉換實現基本的類型轉換。
- 此外,進行對象指針之間類型轉化的時候,可以實現父類指針和子類指針之間的轉換,但是無關的兩個類之間,無法實現相互轉化
- 如果父類指針指向一個一個父類對象,將其轉換成子類指針,雖然可以通過static_cast實現,但是這種轉換可能是不安全的
- 入股分類指針一開始就指向一個子類對象,將其轉換成子類指針,則不存在安全性問題
#include <iostream>class father{};
class children : public father{};int main(){father *f1 = new father;father *f2 = new children;children *c1 = static_cast<children *>(f1);//轉換成功,不安全children *c2 = static_cast<children *>(f2);//轉換成功,很安全return 0;
}
dynamic_cast?
- dynamic_cast只可以用于對象指針之間的類型轉換,可以實現父類和子類指針之間的轉換,還可以轉換引用,但是dynamic_cast不等同于static_cast
- dynamic_cast在將父類指針轉換為子類指針的過程中,會對其背后 的對象類型進行檢查,以保證類型的完全匹配,而static_cast不會這么做
- 只有當父類指針指向一個子類對象,并且父類中包含虛函數的時候,使用dynamic_cast將父類指針轉換成子類指針才會成功,否則返回空的指針,如果是引用則拋出異常
#include <iostream>class father{
public:virtual void HHH(){std::cout << "father" << std::endl;};
};
class children : public father{
public:void HHH() override{std::cout << "children" << std::endl;}void Test(){};
};int main(){father *f1 = new father;father *f2 = new children;//指針children *c1 = dynamic_cast<children *>(f1);//轉換不成功,Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)c1->HHH();children *c2 = dynamic_cast<children *>(f2);//轉換成功,很安全c2->HHH();//引用children &c3 = dynamic_cast<children &>(*f1);//轉換不成功,libc++abi.dylib: terminating with uncaught exception of type std::bad_cast: std::bad_castc3.HHH();children &c4 = dynamic_cast<children &>(*f2);//轉換成功c4.HHH();return 0;
}
const_cast
- const_cast 在轉換的過程中可以增加或者刪除const屬性,一般情況下,無法將常量指針直接賦值給普通指針,使用const_cast就可以移除常量指針的const屬性,實現const屬性到非const屬性的轉換
class Test{};int main(){const Test *t1 = new Test;Test *t2 = const_cast<Test *>(t1);return 0;
}
reinterpert_cast
- reinterpert_cast 可以將一種類型的指針 直接轉換成為 另外一種類型的指針,不管二者之間是否存在繼承關系;
- reinterpert_cast 實現指針和整數之間的轉換
- reinterpert_cast 還用在不同函數指針之間的轉換
class A{};
class B{};int main(){A *a = new A;B *a2b = reinterpret_cast<B *>(a);return 0;
}
靜態全局變量的概念
- 全局變量之前加上const關鍵字
- 定義和聲明放在源文件中,并且不可以使用extern將靜態全局變量導出
- 靜態全部變量的作用域僅僅限定于靜態全局變量所在的文件內部
- 普通的全局靜態變量的作用域是整個工程,在頭文件中使用extern關鍵字聲明普通全局變量,并且在源文件中定義
- 其他文件只需要引入全局變量定義的頭文件,就可以在當前文件中使用普通全局變量
- 如果在頭文件中聲明靜態全局變量,靜態全局變量在聲明的同時也會被初始化,如果沒有顯示的初始化,也會被初始化為默認數值,相當于在頭文件中完成了聲明和定義;而普通全局變量不可以直接定義在頭文件中
- 如果多個文件都使用#include包含了定義某個靜態全局變量的頭文件,那么靜態全局變量在各個源文件里面都有一份單獨的拷貝,而且初始數值相同,拷貝之間相互獨立;如果修改了某個靜態變量的數值,不會影響靜態全局變量的其他拷貝
參考鏈接
- C++ 全局變量 靜態全局變量 傻傻分不清
10.4.1?知識點梳理
- 宏函數省略了函數調用的過程,節省了函數調用的開銷;但是宏函數自身的缺陷,引出了內聯函數
- 編譯階段,編譯器在發現inline關鍵字的時候,會將函數體保存在函數名字所在的符號表內,在調用內聯函數的地方,編譯器直接在符號表中獲取函數名字和函數體,并且使用內聯函數的函數體替換函數的調用,從而節省了運行的時候函數調用的開銷
- inline出現在函數定義的時候,聲明使用inline是沒有用的;此外,函數使用者不需要知道函數是否是內聯函數,因此聲明不需要使用inline進行函數的修飾
class Rectangle{
public:Rectangle(uint,uint);int get_square();int get_girth(){return 2 * (length + width);}
private:uint length;uint width;
};Rectangle::Rectangle(uint length, uint width) : length(length),width(width){}
inline int Rectangle::get_square() {return length * width;
}
-
如果在函數聲明的同時給出函數的定義,編譯器會自動將函數識別為內聯函數;但是不推薦,最好實現函數的聲明和函數的分離;將函數的定義暴露給用戶是不恰當的
內聯函數和宏定義的區別
- 二者都將函數調用替換成完整的函數體,節省了頻繁的函數調用過程中產生的時間和空間的開銷,提高了程序的執行的效率
- 區別:內聯函數是個函數,可以像普通函數一樣進行調試;而宏定義僅僅是字符串的替換
- 宏定義展開是在預處理階段;內聯函數展開是在編譯階段;因此很多編譯階段的工作只對內聯函數有效,例如類型的安全檢查和自動類型的轉換
- 內聯函數作為類的成員函數的時,可以訪問類的所有成員,公有、私有、保護;this指針也可以隱式的正確使用,宏定義無法實現此功能
- 內聯函數需要注意代碼膨脹問題:代碼展開之后,所有使用內聯函數的地方,都會內嵌內聯函數的代碼,造成程序代碼體積的極度增長,因此使用內聯函數需要注意函數的體積
- 編譯器經過優化,如果程序員定義的內聯函數代碼體很大,編譯器也會拒絕將其展開,決絕將其作為內聯函數使用
宏展開錯誤
- 宏定義的缺陷,主要是指宏展開錯誤;主要原因是由于運算符號的優先級等原因導致的宏定義展開之后,語義與預設產生偏差
- 宏定義當中,最好將參數加上括號,宏定義的整體也要加上括號,從而保證邏輯的正確性
- 內聯函數可以被重載
- 構造函數可以定義成內聯函數:但是構造函數會隱式調用基類的構造函數,并且初始化成員變量,所以代碼展開會很大
sizeof使用
- sizeof運算符號 會獲取其操作數所占的內存空間的字節數,sizeof是一個單目運算符號,不是一個函數
- sizeof運算符的操作數可以是類型的名字,也可以是表達式;如果是類型的名字,則直接獲得該類型的字節數,這種操作一般用于給結構體分配空間時,計算大小;
- 如果是表達式,先分析表達式的結果類型,再確定所占的字節數,而不是對表達式實際進行計算
- 使用sizeof一般都是與內存分配或者計算數組長度等需求配合使用;使用sizeof(類型的名)作為分配空間的基數;
- ?uint *ptr = static_cast<uint *>(malloc(sizeof (uint) * 20));
- 計算數組的元素的個數的時候,使用sizeof運算符號加上數組元素的類型作為基數,例:int count = sizeof(array) / sizeof(double);
- 其中array是數組的名字,將數組的名字作為sizeof的操作數,可以獲得整個數組所占的空間;如果將array作為實參傳遞給子函數,子函數中形參對應的參數已經變為了指針,對指針使用sizeof只會獲取指針本身所占的字節數
void sub_func(double array[]){std::cout << sizeof (array) /sizeof (double) << std::endl; //輸出4/8=0 錯誤
}int main(){double array[20];sub_func(array);std::cout << sizeof (array) /sizeof (double) << std::endl; //輸出160/8=20return 0;
}
- 使用strlen計算字符串長度,sizeof是一個運算符,用于獲取類型表達式所占的內存的大小
- strlen是一個函數,用于計算字符串中字符的個數,其中字符串結束符號 \0? 不會被計算在內
數據對齊
- 數據對其是指處理結構體中的成員變量的時候,成員在內存中的起始地址編碼必須是成員類型所占的字節數的整倍;
- 例如int類型變量占用四個字節,因此結構體中int類型成員的起始地址必須是4的倍數,同理,double類型成員的起始地址必須是8的整數倍;由于結構體數據成員在內存存放的時候需要數據對齊,就會造成結構體中邏輯上相鄰的內存地址在物理上并不相鄰,兩個成員之間會出現多余的空間,書上稱之為補齊;
- 結構體成員使用數據對其的主要目的是為了加快數據的讀取速度,減少指令的周期,使得程序運行速度更快,這部分參考計算機組成原理
struct s1{char a;short b;int c;double d;
};
- 結構體的第一個成員a是char類型,起始地址為0,占用一個字節;笑一個地址為1,第二個成員的類型是short,占用兩個字節,起始地址必須是2的倍數,因此地址1 需要空出來一個位置,b的起始地址是2,下一個地址是4,第三個數據c是int類型,占用4個字節,當前地址正好為4,下一個地址為8;同理第四個數據成員是double,起始地址是8的倍數,占用8個地址,因此笑一個地址是16
- 因此結構體s1需要16個字節。由于s1中最大的double,因此計算結果必須是8的整數倍;因此結構體s1的sizeof的運算結果是16
struct s2{int c;double d;short b;char a;
};
- 第一個數據成員是int,起始地址是0,占據4個字節,下一個地址為4,第二個數據成員類型是double,占用了8個字節,起始地址必須是8的整數倍,因此空出4個字節,double的起始地址是8,占據8字節,下一個地址是16;c為short類型,占用兩個字節,當前地址是16,是2的倍數,short占據2個字節,下一個地址是18;第四個數據成員是char類型,起始地址是18,占據一個字節,下一個字節是19
- *? 結構體的計算結果必須是結構體內部占用內存空間最大成員類型的整數倍
- 因此,s2結構體的類型必須是s2的整數倍,因此,需要數據對齊,補充5個字節,因此s2結構體的sizeof運算結果是24
- 結構中成員成員包含 數組、其他結構體,在數據對齊的時候,需要以結構體最深層的基本數據類型為準
- 所謂的結構體中最深層的基本數據類型是指:如果結構體中的成員為復雜的數據類型,不應該以復雜的數據類型所占的空間作為數據對齊的標準,而是要繼續深入復雜的數據類型的內部,查看其所包含的基本數據類型所占的空間。如果結構體成員是一個數組,應該將數組的類型作為作為數據對齊的標準。如果結構體成員 是 其他結構體,應該將內層結構體中的基本數據類型成員作為外層結構體數據對齊的標準
內存分配
- C語言中,使用malloc函數動態申請內存空間,使用free函數將空間進行釋放
int *p = nullptr;p = reinterpret_cast<int *>(sizeof (int) * 10);free(p);p = nullptr;
- 使用malloc函數申請空間的時候,首先會掃描可用空間的鏈表,知道發現第一個大于申請空間的可用空間,返回可用空間的首地址,并且利用剩余空間的首地址和大小更新可用空間的鏈表
- 動態分配的空間來自堆空間,指針指向一個堆空間的地址,指針本身作為局部變量存儲在棧空間里面
- 使用malloc申請的空間必須使用free函數進行釋放
- 使用free函數釋放空間之后,最好將指針立即置空,這樣可以防止后面的程序對指針的錯誤操作
- 反復使用malloc和free函數申請和釋放空間之后,可用的空間鏈表會不斷更新,導致內存空間碎片化;必要的時候,調用malloc之后會進行空間的整理,將相鄰的較小的空間合并成為一個較大的可用的空間,從而滿足申請大的空間的需要
- 為了支持面向對象的技術,滿足操作類對象的需要;C++引入了new和delete兩個操作符,用于申請和釋放空間;
- new和delete不僅支持基本類型還支持自定義的類型,其申請和釋放的空間同樣在堆上
- 對于基本數據類型,new操作符號先分配空間,然后使用括內的數值初始化堆上的數據,并且返回空間的首個地址,delete操作符,直接釋放堆上的內存空間
- 對于自定義的數據類型,new首先分配空間,再根據括號內的參數調用類的構造函數初始化對象,delete操作符號首先調用類的析構函數再釋放對象在堆上的空間
- new完成了申請空間 和 初始化的工作
- delete完成了 釋放空間 和 調用對象析構函數的工作
簡述mallo/free 和 new/delete之間的區別
- malloc和free是C語言自帶的庫函數,通過函數調用訪問,需要傳遞參數并且接收返回數值;new和delete是C++提供的運算符號,有自己的規則和運算方式
- malloc和free只可以用于基本的數據類型;new和delete不僅可以應用于基本數據類型,還可以應用于自定義的數據類型;
- malloc和free返回的是void * 類型,程序需要顯示的轉換成所需要的指針類型;new直接指明了數據的類型,不需要類型的轉換
- malloc只負責申請內存空間,并且返回首地址;free只負責釋放空間,標識這段內存空間已經被占用;new完成了申請空間 和 初始化的工作;delete完成了 釋放空間 和 調用對象析構函數的工作
- new和delete已經涵蓋了malloc和free的功能,之所以保留malloc和free是為了解決兼容性問題,防止調用中出現C函數時候導致錯誤
delete和delete[]的區別
int *i = new int[5];delete i;int *j = new int[5];delete[] j;
- 數組元素是基本數據類型的時候,使用delete和使用delete[]釋放整個數組空間是等價的
- 對于基本數據空間,系統會根據數組長度和數據的類型計算出數組所占的空間,然后一次性釋放整個空間,因此不需要區分delete和delete[]
class Test{
private:char *text;
public:Test(int length = 100){text = new char[length];}~Test(){delete text;std::cout << "destructor!" << std::endl;}
};
int main(){Test *a = new Test[5];delete a;return 0;
}
- 使用new[]為數組分配空間的時候,如果數組的類型是自定義的類型,必須通過delete[]釋放整個數組空間,因為delete[]會逐個調用數組中每個對象的析構函數,只有這樣才可以將數組元素申請的資源釋放,從而將整個數組對象的空間完全釋放,不會造成數據的泄露。
- 使用delete釋放用戶自定義的數據類型的時候,只會調用數組中首個元素的析構函數,而delete[]在釋放空間的時候,會調用數組中所有元素的析構函數?
- 申請和釋放采用配對的形式;new和delete、new[]和delete[]配對使用
位運算
計算二進制數中1的個數
- 求二進制中1的個數,只需要逐個判斷,具體辦法就是指定位置上的元素和1進行&操作,其余位置上的元素和0進行&操作;這樣只會判斷指定位置上元素是0還是1,其余位置上的元素和0&后是0,排出干擾
- 也可以將數字每次右移一位,再和1進行按位與計算;也可以將1不斷左移,再與原先位置上的元素進行按位與操作,判斷是0還是1
二進制表示的整數,其中1的個數
int cnt = 0;while (num) {cnt += num&1;num >>= 1;}
- 參考鏈接
將二進制數倒數第M位的前N位取反
- 按位取反,一般是異或的問題。而且是和1進行異或操作。0和1異或的結果是1,1與1異或的結果是0,相當于取反。
步驟
- 將1左移N位(假設M=2,N=4:00000001 -> 00010000)
- 將上述結果-1? (00010000 - 1 = 00001111)
- 將上述結果左移N位 (N=2,00111100)
- 將上述結果和原數字進行異或
int getNum(uint num,uint m,uint n){int tmp = 1 << n;tmp -= 1;tmp = tmp << m;std::cout << tmp << std::endl;return tmp ^ num;
}int main(){int num = 255;std::cout << getNum(num,2,4) << std::endl;return 0;
}
驗證工具
-
在線進制轉換
一個數組中有兩種數出現了奇數次,其他數都出現了偶數次,怎么找到這兩個數 例子
- 相同的數字進行異或操作,其結果是0,異或操作非常適合去重
//C++
int get_single_dog(std::vector<int>num){int result = 0;for (auto i = num.cbegin();i != num.cend();i++) {result ^= *i;}return result;
}int main(){std::vector<int> num(5,4);int &m = num.at(1);m = 18;int &n = num.at(2);n = 18;int &a = num.at(3);a = 19;int &b = num.at(4);b = 19;for (auto i = num.cbegin();i != num.cend();i++) {std::cout << *i << std::endl;}
// std::cout << num.size() << std::endl;std::cout << get_single_dog(num) << std::endl;return 0;
}
參考鏈接
- 算法入門篇 一 時間復雜度
- vector容器用法詳解
找出人群中 三個單身狗中的一個
- 將三個元素 分成兩組,第一組里面有兩個元素,第二組里面有一個元素;分組之后,剩余元素都要出現偶數次;對第二組元素進行異或操作得到三個元素中的一個元素
- 將元素轉換為二進制,從元素的最末尾開始,先按照最末尾的1元素進行分組,如果不能實現“?第一組里面有兩個元素,第二組里面有一個元素 ”,那就按照倒數第二個1元素進行切分,如果實現不了,繼續此過程
- 反證法:如果找不到 第一組里面有兩個元素,第二組里面有一個元素,三個元素都會在一組里面,則表明這三個元素都相等,這個假設錯誤
例子
- 數組元素[2,5,7,6,4,5,7,2,8]
- 二進制轉換:2=0010;5=0101;7=0111;6=0100;4=0100;8=1000
- 第一步:按照二進制末尾最后一位進行分組:
- 組1:2=0010;6=0100;4=0100;2=0010;8=1000;
- 組2:5=0101;7=0111;5=0101;7=0111;
- 第二組5和7成對出現,沒有實現?三個元素 分成兩組,第一組里面有兩個元素,第二組里面有一個元素
- 第二步:按照二進制末尾倒數第二位進行分組:
- 組1:5=0101;5=0101;4=0100;8=1000;
- 組2:7=0111;6=0100;7=0111;2=0010;2=0010;
- 第一組異或不為0,意味著三個元素其中兩個分到第一組,另外一個元素分到了第二組
- 第二組只有這個元素出現了一次,其余都出現了偶數次
- 二進制的位數是有限的,32位操作系統最多進行32次分組檢測就可以得到最終的結果,因此時間復雜度是O(n)級
代碼
補充
main函數
- main函數之前會進行一些初始化的操作,main函數之后也會進行一部分 掃尾工作
- main函數分成無參和有參兩種類型
- int main()
- int main(int argc,char * argv[])
- 返回值都是int類型
- argc 是argument count的縮寫,整數類型,表示通過命令行輸入參數的個數
- argv 是argument value的縮寫,其中,argv[0]是程序的名字,其余元素為通過命令行輸入的參數
- mian函數之前會進行全局對象和靜態對象的初始化(構造),也會初始化基本數據類型的全局變量和靜態變量
class out_test{
public:out_test(){std::cout << "out_test begin!" << std::endl;}
};
class in_test{
private:static out_test inner_obj;
};out_test out_obj;
out_test in_test::inner_obj;int main(int argc,char * argv[]){std::cout << "main begin!" << std::endl;return 0;
}
- atexit函數是一個指向函數的指針,參數是函數的名字,可以使函數在atexit內部完成函數的注冊,經過注冊的函數會在main函數最后一條語句執行之前進行調用
- 函數的調用順序和注冊順序相反,函數注冊使用棧,注冊的時候,將函數指針入棧,調用的時候函數出棧
- 輸出:fun_3!? fun_2!? ?fun_1!
int main(int argc,char * argv[]){atexit(fun_1);atexit(fun_2);atexit(fun_3);std::cout << "main function!" << std::endl;return 0;
}
?