本專欄文章持續更新,新增內容使用藍色表示。
C++面向對象的三大特性:封裝,繼承,多態
(1)封裝是將數據和函數組合到一個類里。主要目的是隱藏內部的實現細節,僅暴露必要的接口給外部。通過封裝,可以控制類成員的訪問級別(例如:public、protected 和 private)。
(2)繼承是派生類獲得基類的屬性和方法。提高代碼復用性。public、protected、private。
(3)多態是同一個函數在不同的對象上表現形式不同。主要通過重載和重寫實現的,重載在編譯階段完成,屬于靜態的多態,重寫是利用虛函數和動態綁定實現,屬于動態多態。
【補充】重載是利用名稱修飾技術來改函數名,區分參數列表不同的參數。
解釋虛函數與虛函數表
(1)虛函數是C++中實現運行時多態的機制。重寫是利用虛函數和動態綁定實現的,在基類函數前加上virtual,派生類中重寫,運行時就會根據對象的實際類型去調用。
(2)虛函數表:每個有虛函數的類都會維護一個虛函數表(vtable),存儲了該類所有虛函數的指針。調用虛函數時,程序通過vptr找到虛函數表,再通過表中的函數指針調用正確的函數實現。
【補充】對象創建時,編譯器會在對象內存布局的最前面加一個vptr指針,指向該類的虛函數表。調用虛函數時,程序通過vptr找到虛函數表,再通過表中的函數指針調用正確的函數實現。
【補充】虛函數表在內存中包含:
指向類型信息的指針(RTTI),該類所有虛函數的實際地址,可能包含繼承鏈中父類的虛函數地址,派生類的虛函數表會繼承基類的虛函數表內容,并用派生類重寫的函數地址覆蓋對應的表項。純虛函數在虛函數表中的位置會被保留,但指向一個特殊的"未實現"處理函數。
你對重載運算符了解多少?
首先只能重載已有的運算符,并且運算符的優先級、操作數個數、結合律和原來的一致。
其次是重載方式分為成員運算符和非成員運算符,區別是成員運算符少一個參數。
【補充】下標運算符、箭頭運算符必須是成員運算符
重載、重寫與隱藏
(1)重載是指相同作用域(命名空間、類)內有相同的函數名,但是參數列表不同。它根據參數不同來調用不同的函數。
【補充】返回值不能作為重載的區分條件
(2)重寫是指在派生類中重新定義基類中的方法。在需要改變或擴展基類方法時使用。
【補充】基類中被重寫的函數有virtual修飾,派生類在重寫時要有相同的名稱、參數列表、返回類型,只有函數體內不同。
(3)隱藏是指派生類的函數屏蔽了與其同名的基類函數。注意只要同名函數,不管參數列表是否相同,基類函數都會被隱藏。
【面試】在隱藏或者重寫的情況下,如何調用基類的函數?
答:使用作用域解析符::,類名::函數名(),或者通過基類指針或引用調用。
malloc、new,free、delete的異同
malloc和free是C語言中的庫函數,而new和delete是C++ 運算符,會調用它的構造和析構函數,而且運算符可以被重載。不過new和delete的底層會調用malloc和free。
【補充】malloc和free需要指定大小,返回的是void*類型,需要進行強制類型轉換。
【補充】new的底層會調用operator new,而它又會使用malloc分配內存,而malloc的底層是系統調用。delete 先調用析構函數釋放對象資源,再通過 operator delete釋放內存,調用free 直接歸還內存給系統。
malloc 1KB和1MB 有什么區別?
malloc() 并不是系統調用,而是 C 庫里的函數,用于動態分配內存。它的源碼里默認定義了一個閾值:
如果用戶分配的內存小于閾值,則通過 brk() 系統調用從堆分配內存。
如果用戶分配的內存大于閾值,則通過 mmap() 系統調用在文件映射區域分配內存;
字節序問題
大端字節序:高位字節存儲在低地址處,低位字節存儲在高地址處
低位字節序:低位字節存儲在低地址處,高位字節存儲在高地址處
【補充】網絡字節序使用的是大端字節序
Explicit關鍵字
用于防止隱式轉換。防止編譯器自動執行預期外的類型轉換,提高代碼的安全性。
define、const、inline、typedef的區別
define,發生在預處理階段,用于定義宏、常量,但是定義的常量沒有類型檢查,作用域是全局作用域,不受命名空間限制。
Const發生在編譯階段,定義的常量是帶類型的,會存儲在內存中,有利于類型轉換。
Inline內聯函數,在函數聲明前加上 inline 關鍵字。
typedef 發生在編譯階段處理的,有更嚴格的類型檢查,用于為現有類型創建別名。
【補充】定義為內聯函數不一定會進行內聯,就像mysql的索引優化一樣,編譯器內部會進行判斷,選擇最優的結果。優點是類型安全、可調試、可優化,缺點是函數體被復制多次,占用更多的代碼段空間,某些情況下會導致代碼膨脹,所以只適用于簡單代碼。
C++類對象的初始化
(1)基類初始化
如果當前類繼承自一個或多個基類,它們將按照聲明順序進行初始化,有虛繼承和一般繼承的情況下,優先虛繼承。
(2)成員變量初始化
按照在類定義中的聲明順序進行初始化(只與聲明順序有關)。
(3)執行構造函數
深copy與淺copy
淺拷貝是復制指針值,也就是地址,這會導致新舊對象共享同一塊內存。而深拷貝不僅復制指針,還會創建新的內存空間去拷貝數據,每個對象都有自己獨立的副本。
【補充】默認情況下,C++使用淺拷貝,效率高,但是有安全隱患:比如一個對象釋放了內存,另一個對象的指針就會變成懸空的,訪問會導致未定義行為;其次,當多個對象析構時,會多次釋放同一塊內存,造成程序崩潰。
【補充】深拷貝,需要自定義拷貝構造函數和賦值運算符重載。拷貝構造函數中,要為指針成員分配新的內存,并拷貝原始對象的數據;賦值運算符重載中,除了拷貝數據外,還需要先釋放對象原有的內存,處理自賦值的情況。
使用智能指針,shared_ptr共享所有權,unique_ptr獨占所有權。
Static類成員函數和類靜態變量
static 函數是靜態成員函數,它與類本身相關,而不是與類的對象。可以將其視為在類作用域下的全局函數。
【補充】靜態函數沒有 this 指針,不能訪問任何非靜態成員變量。
C++11新特性
(1)auto關鍵字編譯器自動推斷變量類型。
(2)基于范圍的for循環,簡化容器遍歷語法
(3)智能指針:引入內存管理類,減少內存泄漏:
std::unique_ptr:獨占所有權的智能指針,
std::shared_ptr:共享所有權的智能指針,
std::weak_ptr:不增加引用計數的智能指針。
(4)移動語義和右值引用,提高資源利用效率。
(5)Lambda表達式,支持匿名函數。
(6)nullptr更安全的空指針常量。
(7)委托構造函數,構造函數可以調用同類其他構造函數。
右值引用
右值引用簡單來說,就是可以延長某塊內存的存活時間。一般情況下,變量生命周期結束,它就被銷毀了,比如,函數中的臨時變量,在函數退出之后銷毀。如果想要延長使用時間,就可以使用右值引用,此時生命周期變為右值引用的生命周期。
【補充】右值引用構造函數(移動構造函數),復用另外一個對象中的資源(堆內存)。移動構造對資源進行了轉移,而普通的拷貝構造函數會使得多個對象的指針指向同一塊內存。
【補充】左右值不是等號的左右,關于左值有個關鍵字是lvalue(locator value),字面意思是可通過地址定位到這個數據,所以左值是可以進行取地址操作。右值是rvalue(read value),只讀的數據。所以一般能進行取地址操作的是左值,不能的是右值。
【補充】不管是左值引用還是右值引用都是別名,不占用內存空間。實現方面右值引用比左值引用多寫一個取地址符。語法上我們只能通過右值去初始化右值引用。同類型的左值、右值引用,右值常量引用等都可以用來初始化常量左值引用(const T&)。
智能指針
(1)std::unique_ptr (獨占指針)
特點:獨占所有權,同一時間只能有一個unique_ptr指向特定對象
不能復制,但可以通過std::move轉移所有權,離開作用域時自動釋放所管理的內存,零開銷(與原始指針相比幾乎沒有額外成本)
std::unique_ptr<MyClass> ptr(new MyClass());// 或者更好的方式(C++14起):auto ptr = std::make_unique<MyClass>();
(2)std::shared_ptr (共享指針)???????? 可以使用std::make_shared
特點:共享所有權,多個shared_ptr可以指向同一個對象
使用引用計數機制跟蹤資源,當最后一個shared_ptr被銷毀時釋放資源,需要額外內存,有少量額外開銷
(3)std::weak_ptr (弱指針)
特點:不增加引用計數,不會阻止對象被銷毀
用于解決shared_ptr的循環引用問題,必須轉換為shared_ptr才能訪問對象
static_cast、dynamic_cast、?const_cast、?reinterpret_cast類型轉換
(1)static_cast在編譯時執行類型轉換,在進行指針或引用類型轉換時,需要自己保證合法性。如果想要運行時類型檢查,可以使用dynamic_cast進行安全的向下類型轉換
(2)dynamic_cast在C++中主要應用于父子類層次結構中的安全類型轉換。
它在運行時執行類型檢查,因此相比于static_cast,它更加安全
dynamic_cast <new_type> (expression)
(3)const_cast
new_type 必須是一個指針、引用或者指向對象類型成員的指針。當需要使用const對象調用非const成員函數時,可以使用const_cast刪除對象的const屬性
const_cast <new_type> (expression)
(4)?reinterpret_cast
reinterpret_cast用于在不同類型之間進行低級別的轉換。
reinterpret_cast <new_type> (expression)
Lamada表達式
Lambda 表達式是一種匿名函數,可以在需要的地方內聯定義函數,不用單獨聲明命名函數。作為回調函數時,常用。
[capture](parameters) -> return_type {// 函數體}
捕獲列表 (capture):定義哪些外部變量可以在 lambda 體內使用
參數列表 (parameters):與普通函數參數類似
返回類型 (return_type):可選的,編譯器通常可以自動推導
函數體:包含 lambda 要執行的代碼
map、set是怎么樣實現的,紅黑樹是怎么樣能夠同時實現這兩種容器?為什么使用紅黑樹?
(1)底層都是以紅黑樹的結構實現,插入刪除等操作都在對數級別O(log n)完成的,比較高效;
(2)通過定義了模版特化和仿函數,使用同一棵紅黑樹:基于模板的RBTree<Key, Value, KeyOfValue, Compare>。但是不同節點數據:set:Value = Key,直接存值,map:Value = pair<const Key, T>,存鍵值對。通過仿函數KeyOfValue從Value中提取比較鍵,set用Identity:直接返回值,map用SelectFirst。
(3)因為map和set要求是自動排序的,紅黑樹能夠實現這一功能,而且時間復雜度比較低。
了解RAII嗎?
RAII全稱是“Resource Acquisition is Initialization”,“資源獲取即初始化”, 資源生命周期與對象生命周期綁定,簡單來說就是對象構造時獲取資源,析構時自動釋放資源。智能指針(unique_ptr、shared_ptr、weak_ptr)是RAII的典型實現。
如有問題或建議,歡迎在評論區中留言~