www.久久久久|狼友网站av天堂|精品国产无码a片|一级av色欲av|91在线播放视频|亚洲无码主播在线|国产精品草久在线|明星AV网站在线|污污内射久久一区|婷婷综合视频网站

當前位置:首頁 > 公眾號精選 > C語言與CPP編程
[導讀]從存儲空間角度,虛函數(shù)對應一個虛函數(shù)表,而指向虛函數(shù)表的虛函數(shù)指針是存儲區(qū)對象內(nèi)存內(nèi)的。如果構(gòu)造函數(shù)是虛函數(shù),則需要通過虛函數(shù)表來調(diào)用,而對象還沒有構(gòu)造出來,無法找到虛函數(shù)表。

shared_ptr 指針的實現(xiàn)

template<typename T> class Shared_ptr {private: T *ptr; int *use_count; // 用指針保證指向同一塊地址public: Shared_ptr() { ptr = nullptr; use_count = nullptr; cout << "Created" << endl; } Shared_ptr(T *p){ ptr = p; use_count = new int(1); cout << "Created" << endl; } Shared_ptr(const Shared_ptr &other) { ptr = other.ptr; ++(*other.use_count); use_count = other.use_count; cout << "Created" << endl; } Shared_ptr& operator=(const Shared_ptr &other) { if(this == &other) return *this; (*other.use_count)++; if(ptr && --(*use_count) == 0) { delete ptr; delete use_count; } ptr = other.ptr; use_count = other.use_count; return *this; } T& operator*() { // 返回指針的引用 return *ptr; } T* operator->() { return ptr; } int get_use_count() { if(use_count == nullptr) return 0; return *use_count; } ~Shared_ptr() { if(ptr && --(*use_count) == 0) { delete ptr; delete use_count; cout << "Destroy" << endl; } }};

構(gòu)造函數(shù)不可以是虛函數(shù)

  1. 從存儲空間角度,虛函數(shù)對應一個虛函數(shù)表,而指向虛函數(shù)表的虛函數(shù)指針是存儲區(qū)對象內(nèi)存內(nèi)的。如果構(gòu)造函數(shù)是虛函數(shù),則需要通過虛函數(shù)表來調(diào)用,而對象還沒有構(gòu)造出來,無法找到虛函數(shù)表。
  2. 從使用角度,虛函數(shù)主要用于信息不全的情況下,使子類重寫的函數(shù)能得到對應的調(diào)用。構(gòu)造函數(shù)本身就是要初始化對象,所以用虛函數(shù)沒有意義。

平衡樹(AVL)、伸展樹(Splay)、紅黑樹

  1. 平衡樹:
    1. 優(yōu)點:查找、插入、刪除最壞復雜度都是 O(logN)。操作簡單。
    2. 缺點:插入、刪除的旋轉(zhuǎn)成本不菲。刪除操作后,最多旋轉(zhuǎn) O(logN) 次,若頻繁進行插入、刪除操作,得不償失
  2. 伸展樹
    1. 優(yōu)點:局部性強: 1)剛剛訪問過的節(jié)點,可能在不久后再次被訪問到。2) 將要訪問的節(jié)點,可能在不久之前剛剛被訪問的節(jié)點附近。
    2. 缺點:不能保證單次最壞情況的出現(xiàn),不適用于敏感場合。復雜度不好分析,均攤 O(logN)。
  3. 紅黑樹
    1. 優(yōu)點:查找、插入、刪除的復雜度都是 O(logN)。插入之后最多旋轉(zhuǎn) 2 次,刪除之后最多旋轉(zhuǎn) 1 次能夠再次達到平衡狀態(tài)。
    2. 缺點:左右子樹高度相差比 AVL 大。

AVL 和 紅黑樹比較

AVL 是嚴格的平衡樹,因此在插入/刪除節(jié)點時,根據(jù)不同的情況,旋轉(zhuǎn)的次數(shù)比紅黑樹多。

紅黑樹是弱平衡的,用非嚴格的平衡換取插入/刪除節(jié)點時旋轉(zhuǎn)次數(shù)的降低。

兩者都是平衡樹,那么查找的時候,查找效率就會和樹的高度有關(guān),所以 AVL 會比紅黑樹更優(yōu)。

有了 AVL 為什么還要紅黑樹

雖然 AVL 解決的二叉查找樹退化成鏈表的情況,但是平衡樹要求每個節(jié)點左子樹和右子樹高度相差最多為 1。這個要求太嚴格了,導致插入/刪除的時候需要不斷的旋轉(zhuǎn)來達到這個要求。而紅黑樹不會頻繁的破壞規(guī)則,不用頻繁的調(diào)整,紅黑樹是一種不大嚴格的平衡樹,但是換來了效率的提高,是一種折中的方案。

紅黑樹

面試常問:什么是紅黑樹?

wiki紅黑樹

紅黑樹性質(zhì)

  1. 節(jié)點一定是紅色或黑色。
  2. 根節(jié)點是黑色。
  3. 每個葉子節(jié)點都是黑色的空節(jié)點(NIL)。
  4. 每個紅色節(jié)點的兩個子節(jié)點都是黑色。
  5. 從任一節(jié)點到其每個葉子節(jié)點的所有路徑上都包含相同的黑色節(jié)點數(shù)。

這些性質(zhì)強制了紅黑樹從根到葉子的最長路徑不會超過最短路徑的兩倍。性質(zhì) 4 導致了路徑不能有兩個相連的紅色節(jié)點,最短的可能路徑都是黑色節(jié)點,最長的可能路徑有交替的紅色和黑色節(jié)點。因為根據(jù)性質(zhì) 5 所有最長的路徑都有相同數(shù)目的黑色節(jié)點,這就表明了沒有路徑能多于任何其他路徑的兩倍長。

紅黑樹的擴展

可以給每個階段加上 size 域,表示以節(jié)點 x 為根的子樹的節(jié)點數(shù)。

size[x] = size[x->left]+size[x->right]

通過 size 就可以獲得比 x 小的節(jié)點數(shù)目和第 x 小的節(jié)點。

i++ 和 ++i 的區(qū)別

++i 返回對象的引用,i++ 必須產(chǎn)生一個臨時對象保存更改前對象的值并返回,所以導致在大對象的時候產(chǎn)生的比較大的復制開銷,效率較低。

// ++iint& int::operator++() { *this += 1; return *this;}// i++const int int::operator++(int) { int old_value = *this; ++(*this); return old_value;}

拷貝構(gòu)造函數(shù)

Node a;Node b(a);Node c = a;

這里的 b 和 c 都是一開始是不存在的,是通過 a 對象來構(gòu)造和初始化的。

拷貝構(gòu)造函數(shù)重載形式:

Node (const Node &other) { 
}

如果用戶沒有自定義拷貝構(gòu)造函數(shù)并且用到了拷貝構(gòu)造函數(shù),則編譯器會生成默認的拷貝構(gòu)造函數(shù)。

在 C++ 中,這三種情況下拷貝構(gòu)造函數(shù)會被使用:

  1. 一個對象以值傳遞的形式傳入函數(shù)內(nèi)。
  2. 一個對象以值傳遞的形式從函數(shù)返回。
  3. 一個對象通過另一個對象初始化。

優(yōu)點:可以很容易的復制對象。

缺點:對象的隱式拷貝是 C++ 中是錯誤和性能問題的來源之一。它也降低了代碼的可讀性,并使得對象子程序中的傳遞和改變變得難以跟蹤。

賦值函數(shù)

Node a;Node b;b = a;

這里的 b 已經(jīng)存在的,在通過 a 賦值給 b。

賦值函數(shù)重載形式:

Node& operator=(const Node &other) {
}

拷貝構(gòu)造函數(shù)和賦值函數(shù)區(qū)別

  1. 拷貝構(gòu)造函數(shù)是在對象初始化時,分配一塊空間并初始化,而賦值函數(shù)是對已經(jīng)分配空間的對象進行賦值操作。
  2. 實現(xiàn)上,拷貝構(gòu)造函數(shù)是構(gòu)造函數(shù),通過參數(shù)的對象初始化產(chǎn)生一個對象。賦值函數(shù)則是把一個對象賦值給另一個對象,需要先判斷兩個對象是否是同一個對象,若是則什么都不做,直接返回,若不是則需要先釋放原對象內(nèi)存,在賦值。(可以參考 shared_ptr 實現(xiàn))

總結(jié):

  • 對象不存在,沒有通過別的對象來初始化,就是構(gòu)造函數(shù)。
  • 對象不存在,通過別的對象來初始化,就是拷貝構(gòu)造函數(shù)。
  • 對象存在,通過別的對象來初始化,就是賦值函數(shù)。

虛函數(shù)和內(nèi)聯(lián)函數(shù)

內(nèi)聯(lián)函數(shù)通常就是將它在調(diào)用處 "內(nèi)斂的" 展開,消除反復調(diào)用的額外開銷,但是如果函數(shù)太長,會使代碼臃腫,反而降低效率。

虛函數(shù)可以是內(nèi)聯(lián)函數(shù),無論是顯式還是隱式,inline 都只是一個申請,最終由編譯器決定是否內(nèi)聯(lián)。所以可以用 inline 修飾虛函數(shù),但虛函數(shù)表現(xiàn)多態(tài)性時,不可以內(nèi)聯(lián),只有當知道調(diào)用哪個類的函數(shù)時,才可以是內(nèi)聯(lián)。

空類的大小

在 C++ 中規(guī)定類的大小不為 0,空類大小為 1,當類不包含虛函數(shù)和非靜態(tài)成員時,其對象大小也為 1。若存在虛函數(shù),則需要存儲一個虛函數(shù)指針大小,在 32 位上為 4 字節(jié)。

結(jié)構(gòu)體字節(jié)對齊

class A {};class B{public: A x;};sizeof(B) = 1
class B{public: inline virtual fun() { }};sizeof(B) = 4
class B{public: A x; inline virtual fun() { }};sizeof(B) = 8

可以發(fā)現(xiàn)最后一個的 sizeof 并不是單純的 1+4=5,而直接變成了 8,因為存在結(jié)構(gòu)體的字節(jié)對齊規(guī)則。

結(jié)構(gòu)體字節(jié)對齊的根本原因:1) 移植性更好,某些平臺只能在特定的地址訪問特定的數(shù)據(jù)。2) 提高存取數(shù)據(jù)的速度,CPU 通常按塊讀取數(shù)據(jù)會更加快速。

結(jié)構(gòu)體字節(jié)對齊原則:

  1. #pragma pack
    1. 結(jié)構(gòu)內(nèi)部各成員首地址必然是自身大小的整數(shù)倍。
    2. sizeof 最終結(jié)果必然是結(jié)構(gòu)內(nèi)部最大成員的整數(shù)倍,不夠補齊
  2. #pragma pack(n)
    1. 結(jié)構(gòu)內(nèi)部各成員首地址必然是 min(n, 自身大小) 的整數(shù)倍。
    2. sizeof 最終結(jié)果必然是 min(n, 結(jié)構(gòu)內(nèi)部最大成員) 的整數(shù)倍,不夠補齊。
#includeusing namespace std;
class A { char a; int b; short c;};class B { int a; char b; short c;};
int main() { cout << sizeof(A) << endl; // 12 cout << sizeof(B) << endl; // 8 return 0;}

造成不同結(jié)果的原理在于:

  • 對于 class A 來說,內(nèi)部字節(jié)為
a b c
1*** 1111 11**
  • 對于 class B 來說,內(nèi)部字節(jié)為
a b c
1111 1* 11

有 static、virtual 之類的一個類的內(nèi)存分布

  • static 修飾成員變量
    • 靜態(tài)成員變量在 全局存儲區(qū) 分配內(nèi)存,本類的所有對象共享,在還沒生成類對象之前也可以使用。
  • static 修飾成員函數(shù)
    • 靜態(tài)成員函數(shù)在 代碼區(qū) 分配內(nèi)存。靜態(tài)成員函數(shù)和非靜態(tài)成員函數(shù)的區(qū)別在于非靜態(tài)成員函數(shù)存在 this 指針,而靜態(tài)成員函數(shù)不存在,所以靜態(tài)成員函數(shù)沒有類對象也可以調(diào)用。
  • virtual
    • 虛函數(shù)表存儲在常量區(qū),也就是只讀數(shù)據(jù)段
    • 虛函數(shù)指針存儲在對象內(nèi),如果對象是局部變量,則存儲在棧區(qū)內(nèi)。

使用宏定義求結(jié)構(gòu)體成員偏移量

#include#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE*)0)->MEMBER)/*(TYPE*)0 將零轉(zhuǎn)型成 TYPE 類型指針((TYPE*)0->MEMBER) 訪問結(jié)構(gòu)體中的成員&((TYPE*)0->MEMBER) 取出數(shù)據(jù)成員地址,也就是相對于零的偏移量(size_t) & ((TYPE*)0)->MEMBER) 將結(jié)果轉(zhuǎn)換成 size_t 類型。*/
struct Node { char a; short b; double c; int d;};
int main() { printf("%d\n", offsetof(Node, a)); printf("%d\n", offsetof(Node, b)); printf("%d\n", offsetof(Node, c)); printf("%d\n", offsetof(Node, d)); return 0;}

size_t 在可以理解成 unsigned\ \ int,在不同平臺下被 typedef 成不同類型。

C++ 中哪些函數(shù)不可以是虛函數(shù)

  1. 普通函數(shù)(非成員函數(shù)):普通函數(shù)不屬于類成員,不能被繼承。普通函數(shù)只能被重載,不能被重寫,因此聲明成虛函數(shù)沒有意義。
  2. 構(gòu)造函數(shù):構(gòu)造函數(shù)本來就是為了初始化對象而存在的,沒有定義為虛函數(shù)的必要。而且對象還沒構(gòu)造出來,不存在虛函數(shù)指針,也無法成為虛函數(shù)。
  3. 內(nèi)聯(lián)成員函數(shù):內(nèi)聯(lián)函數(shù)是為了在代碼中直接展開,減少調(diào)用函數(shù)的代價,是在編譯的時候展開。而虛函數(shù)需要動態(tài)綁定,這不可能統(tǒng)一。只有當編譯器知道調(diào)用的是哪個類的函數(shù)時,才可以展開。
  4. 靜態(tài)成員函數(shù):對于所有對象都共享一個函數(shù),沒有動態(tài)綁定的必要。
  5. 友元函數(shù):C++ 不支持友元函數(shù)的繼承,自然不能是虛函數(shù)。

手撕堆排序

[堆排序的思路以及代碼的實現(xiàn)

](https://blog.csdn.net/qq_36573828/article/details/80261541)

令 k = log_2n

初始化堆復雜度 O(N) 。分析:假設(shè)每一次都到葉子的父節(jié)點


排序重建堆復雜度 。分析:假設(shè)每一次都到葉子節(jié)點。


void adjust(vector<int>& nums, int i, int n) { while(2*i+1 < n) { int j = 2*i+1; if(j+11]) j++; if(nums[i] > nums[j]) break; swap(nums[i], nums[j]); i = j; }}
vector<int> sortArray(vector<int>& nums) { if(nums.size() <= 1) return nums; int n = nums.size(); for(int i=n/2-1; i>=0; i--) adjust(nums, i, n); // 初始化堆 for(int i=n-1; i>0; i--) { // 排序重建堆 swap(nums[i], nums[0]); adjust(nums, 0, i); } return nums;}

main 函數(shù)前后還會執(zhí)行什么

全局對象的構(gòu)造在 main 函數(shù)之前完成,全局對象的析構(gòu)在 main 函數(shù)之后完成。

const 和 define

  1. define 在預編譯階段起作用,const 在編譯、運行的時候起作用。
  2. define 只是簡單的替換功能,沒有類型檢查功能,const 有類型檢查功能,可以避免一些錯誤。
  3. define 在預編譯階段就替換掉了,無法調(diào)試,const 可以通過集成化工具調(diào)試。

inline 和 define

  1. inline 在編譯時展開,define 在預編譯時展開。
  2. inline 可以進行類型安全檢查,define 只是簡單的替換。
  3. inline 是函數(shù),define 不是函數(shù)。
  4. define 最好用括號括起來,不然會產(chǎn)生二義性,inline 不會。
  5. inline 是一個建議,可以不展開,define 一定要展開。

inline 函數(shù)的要求

  1. 含有遞歸調(diào)用的函數(shù)不能設(shè)置為 inline
  2. 循環(huán)語句和 switch 語句,無法設(shè)置為 inline
  3. inline 函數(shù)內(nèi)的代碼應很短小。最好不超過5行

聲明和定義

  • 變量定義:為變量分配空間,還可以為變量指定初始值。
  • 變量聲明:向程序表明變量的類型和名字,但不分配空間??梢酝ㄟ^ extern 關(guān)鍵字來聲明而不定義,extern 告訴編譯器變量在別的地方定義了。
  1. 定義也是聲明,聲明不是定義。例如:
  • extern int i 聲明且不定義 i 變量, int i 聲明且定義了 i 變量。
  • 聲明有初始值時,為當成定義
    • extern int i=10 此時看成定義了 i 變量。
  • 函數(shù)的聲明和定義,區(qū)別在于是否帶有花括號。
  • 面向過程和面向?qū)ο?/span>

    面向過程就是分析出解決問題所需要的步驟,然后用函數(shù)把步驟一步一步的實現(xiàn)。優(yōu)點在于性能高。

    面向?qū)ο缶褪菢?gòu)成問題事務(wù)分解成各個對象,建立對象的目的不是為了完成一個步驟,而是為了描述某個對象在整個解決問題的步驟中的行為。優(yōu)點在于易維護、易復用、易擴展,使系統(tǒng)更加靈活。

    堆區(qū)和自由存儲區(qū)

    基本上所有的 C++ 編譯器默認使用堆來實現(xiàn)自由存儲,也就是缺省的 new/delete 是通過 malloc/free 方式來實現(xiàn)的,這時候可以說他從堆上分配內(nèi)存,也可以說他從自由存儲區(qū)分配內(nèi)存。但程序員可以通過重載操作符,改用其他內(nèi)存實現(xiàn)自由存儲。

    堆區(qū)是操作系統(tǒng)維護的一塊內(nèi)存,而自由存儲區(qū)是 C++ 中用于 new/delete 動態(tài)分配和釋放對象的 抽象概念。

    堆區(qū)和棧區(qū)


    堆區(qū) 棧區(qū)
    管理方式 由程序員控制 系統(tǒng)主動分配
    空間大小 空間很大,可以達到4G 默認1M
    碎片問題 頻繁的 malloc/free 會造成大量碎片 不會存在這個問題
    生長方向 向著內(nèi)存地址增加的方向 向著內(nèi)存地址減小的方向
    分配效率 速度比較慢 速度比較快

    #ifndef 和 #endif 的作用

    在大的軟件工程中,可能多個文件包含同一個頭文件,當這些文件鏈接成可執(zhí)行文件的時候,就會造成大量的 "重定義" 錯誤,可以通過 #ifndef 來避免這個錯誤。

    #ifndef _A_H#define _A_H
    #endif

    這樣就可以保證 a.h 被定義一次。

    explicit 關(guān)鍵字

    類的構(gòu)造函數(shù)存在隱式轉(zhuǎn)換,如果想要避免這個功能,就可以通過 explicit 關(guān)鍵字來將構(gòu)造函數(shù)聲明成顯式的。

    菱形繼承

    class A { public: void fun() { cout << "!!!" << endl; }};class B : public A{};class C : public A{};class D : public B, public C {};

    int main() { // freopen("in", "r", stdin); D d; d.fun();; return 0;}

    像上面的繼承關(guān)系,當繼承關(guān)系形成菱形時,D 中保存了兩份 A,在調(diào)用 d.fun() 時就會出現(xiàn)調(diào)用不明確的問題。

    這種情況由兩種解決方法:

    1. 使用域限定訪問的函數(shù)。也就是將 d.fun 修改成 d.B::fun 或者 d.C::fun。
    2. 第二種方法就是使用虛繼承。虛繼承解決了從不同途徑繼承來的同名數(shù)據(jù)成員在內(nèi)存中有不同的拷貝造成數(shù)據(jù)不一致的問題,將共同基類設(shè)置成虛基類,這時從不同路徑繼承過來的同名數(shù)據(jù)成員在內(nèi)存中就只有一個拷貝。操作方法就是在 B 和 C 的繼承處加上 virtual 修飾。

    虛繼承底層實現(xiàn)一般通過虛基類指針和虛基類表實現(xiàn)。每個虛繼承的子類都有一個虛基類指針和一個虛基類表,虛表中記錄了虛基類與本類的偏移地址。通過偏移地址,這樣就找到了虛基類成員,節(jié)省了存儲空間。

    weak_ptr 指向的對象被銷毀

    首先 weak_ptr 是一種不控制對象生存周期的智能指針,它指向一個 shared_ptr 管理的對象。一旦最后一個指向?qū)ο蟮?shared_ptr 被銷毀,那么無論是否有 weak_ptr 指向該對象,都會釋放資源。

    weak_ptr 不能直接指向?qū)ο?,需要先調(diào)用 lock,而 lock 會先判斷該對象是否還存在。

    如果存在 lock 就會返回一個指向該對象的 shared_ptr,并且該對象的 shared_ptr 的引用計數(shù)加一。

    如果在 weak_ptr 獲得過程中,原本的所有 shared_ptr 被銷毀,那么該對象的生命周期會延長到這個臨時 shared_ptr 銷毀為止。

    lambda 表達式

    lambda 表達式定義一個匿名函數(shù),并且可以捕獲一定范圍內(nèi)的變量,基本格式如下:

    [capture](params) mutable ->ReturnType {statement}
    • [capture]:捕獲列表,可以捕獲上下文的變量來供 lambda 函數(shù)使用
      • [var]:值傳遞的方式捕獲 var
      • [&var]:引用傳遞的方式捕獲 var
      • [=]:值傳遞的方式捕獲父作用域的所有變量。
      • [&]:引用傳遞的方式捕獲父作用域的所有變量。
      • [this]:值傳遞的方式捕獲當前 this 指針。
    • (params):參數(shù)列表,和普通函數(shù)參數(shù)列表一致,如果不傳參數(shù)可以和 () 一起忽略。
    • mutable 修飾符號,默認情況下,lambda 表達式是一個 const 函數(shù),可以用 mutable 取消他的常量性。若使用 mutable 修飾,參數(shù)列表不可省略。
    • **->**ReturnType:返回值類型。若是 void,可以省略。
    • {statement}:函數(shù)主體,和普通函數(shù)一樣。

    lambda 表達式優(yōu)點在于代碼簡潔,容易進行并行計算。缺點在于在非并行計算中,效率未必有 for 循環(huán)快,并且不容易調(diào)試,對于沒學過的程序員來說可讀性差。

    #includeusing namespace std;
    int main() { vector<int> g{9, 2, 1, 2, 5, 6, 2}; int ans = 1; sort(g.begin(), g.end(), [](int a, int b) ->bool{return a>b;}); for_each(g.begin(), g.end(), [&ans](int x){cout << x << " ", ans = ans*x;}); cout << "\nmul = " << ans << endl; return 0;}/*9 6 5 2 2 2 1mul = 2160*/

    存在全局變量和局部變量時,訪問全局變量

    可以通過全局作用符號 :: 來完成

    int a = 10;
    int main() { // freopen("in", "r", stdin); int a = 20; cout << a << endl; cout << ::a << endl; return 0;}

    全局變量的初始化的順序

    同一文件中的全局變量按照聲明順序,不同文件之間的全局變量初始化順序不確定。

    如果要保證初始化次序的話,需要通過在函數(shù)使用靜態(tài)局部變量并返回來實現(xiàn)。

    class FileSystem{...};FileSystem & tfs(){ static FileSystem fs;//定義并初始化一個static對象 return fs;}

    淺拷貝和深拷貝

    • 淺拷貝:源對象和拷貝對象共用一份實體,僅僅是引用的變量名不同。對其中任意一個修改,都會影響另一個對象。
    • 深拷貝:源對象和拷貝對象相互獨立。對其中一個對象修改,不會影響另一個對象。
    • 兩個對象指向同塊內(nèi)存,當析構(gòu)函數(shù)釋放內(nèi)存時,會引起錯誤。

    從這個例子可以看出,b 通過默認拷貝函數(shù)進行初始化,然而進行的是淺拷貝,導致對 a 進行修改的時候,b 的存儲值也被修改。

    struct Node { char* s; Node(char *str) { s = new char[100]; strcpy(s, str); } /* 手動實現(xiàn)拷貝構(gòu)造函數(shù) Node(const Node &other) { s = new char[100]; strcpy(s, other.s); } */};
    int main() { // freopen("in", "r", stdin); Node a("hello"); Node b(a); cout << b.s << endl; strcpy(a.s, "bad copy"); cout << b.s << endl; return 0;}

    正確的寫法應該自己寫一個拷貝函數(shù),而不是用默認的,應該盡量的避免淺拷貝。

    如何控制一個類只能在堆或棧上創(chuàng)建對象

    在 C++ 中創(chuàng)建對象的方法有兩種,一種是靜態(tài)建立,一個是動態(tài)建立。

    • 靜態(tài)建立由編譯器為對象分配內(nèi)存,通過調(diào)用構(gòu)造函數(shù)實現(xiàn)。這種方法創(chuàng)建的對象會在棧上。
    • 靜態(tài)建立由用戶為對象分配內(nèi)存,通過 new 來實現(xiàn),間接調(diào)用構(gòu)造函數(shù)。這種方法創(chuàng)建的對象會在堆上。

    只能從堆上分配對象:

    當建立的對象在棧上時,由編譯器分配內(nèi)存,因此會涉及到構(gòu)造函數(shù)和析構(gòu)函數(shù)。那么如果無法調(diào)用析構(gòu)函數(shù)呢?也就是說析構(gòu)函數(shù)是 private 的,編譯器會先檢查析構(gòu)函數(shù)的訪問性,由于無法訪問,也就防止了靜態(tài)建立。

    但這種方法存在一種缺點,就是把析構(gòu)函數(shù)設(shè)成 private 后,如果這個類要作為基類的話,析構(gòu)函數(shù)應該設(shè)成虛函數(shù),而設(shè)成 private 后子類就無法重寫析構(gòu)函數(shù),所以應該把析構(gòu)函數(shù)設(shè)成 protected。然后額外設(shè)置一個接口來 delete。

    class Node {public: Node(){}; void Destroy() { delete this; }protected: ~Node(){};};

    此時解決了靜態(tài)建立的過程,但使用時,通過 new 創(chuàng)建對象,通過 Destroy 函數(shù)釋放對象,為了統(tǒng)一,可以把構(gòu)造函數(shù)和析構(gòu)函數(shù)都設(shè)成 protected,重寫函數(shù)完成構(gòu)造和析構(gòu)過程。

    class Node {public: static Node* Create() { return new Node(); } void Destroy() { delete this; }protected: Node(){}; ~Node(){};};

    只能從棧上分配對象:

    同樣的道理,只需要禁止通過動態(tài)建立對象就可以實現(xiàn)在棧上分配對象,所以可以重載 new 和 delete 并設(shè)為 private,使用戶只能靜態(tài)建立對象。

    class Node {public: Node(){}; ~Node(){};private: void* operator new(size_t t){} void operator delete(void* p){}};

    memcpy 和 memmove 的實現(xiàn)

    memcpy 可以直接通過指針自增賦值,但要求源地址和目的地址無重合。

    void mymemmove1(void* s, const void* t, size_t n) {char *ps = static_cast<char*>(s);const char *pt = static_cast<const char*>(t);while(n--) {*ps++ = *pt++;}}

    如果源地址和目的地址存在重合,會因為地址的重合導致數(shù)據(jù)被覆蓋,所以要通過 memmove 來實現(xiàn),需要從末尾往前自減賦值。

    為了加快速度還可以使用 4 字節(jié)賦值的方式

    // 直接按字節(jié)進行 copyvoid mymemmove1(void* s, const void* t, size_t n) { char *ps = static_cast<char*>(s); const char *pt = static_cast<const char*>(t); if(ps<=pt && pt<=ps+n-1) { ps = ps+n-1; pt = pt+n-1; while(n--) { *ps-- = *pt--; } } else { while(n--) { *ps++ = *pt++; } }}
    // 加快速度,每次按 4 字節(jié)進行 copyvoid mymemmove2(void *s, const void *t, size_t n) { int *ts = static_cast<int*>(s); const int *tt = static_cast<const int*>(t); char *ps = static_cast<char*>(s); const char *pt = static_cast<const char*>(t); int x = n/4, y = n%4; if(ps<=pt && pt<=ps+n-1) { ps = ps+n-1; pt = pt+n-1; while(y--) { *ps-- = *pt--; } ps++, pt++; ts = reinterpret_cast<int*>(ps); tt = reinterpret_cast<const int*>(pt); ts--, tt--; while(x--) { *ts-- = *tt--; } } else { while(y--) { *ps++ = *pt++; } ts = reinterpret_cast<int*>(ps); tt = reinterpret_cast<const int*>(pt); while(x--) { *ts++ = *tt++; } }}

    通過重載 new/delete 來檢測內(nèi)存泄漏的簡易實現(xiàn)

    講每次 new 產(chǎn)生的內(nèi)存記錄,并在 delete 的時候刪去記錄,那么最后剩下的就是發(fā)生內(nèi)存泄漏的代碼。

    #include using namespace std;
    class TraceNew {public: class TraceInfo { private: const char* file; size_t line; public: TraceInfo(); TraceInfo(const char *File, size_t Line); ~TraceInfo(); const char* File() const; size_t Line(); }; TraceNew(); ~TraceNew(); void Add(void*, const char*, size_t); void Remove(void*); void Dump();private: map<void*, TraceInfo> mp;} trace;
    TraceNew::TraceInfo::TraceInfo() {}
    TraceNew::TraceInfo::TraceInfo(const char *File, size_t Line) : file(File), line(Line) {}
    TraceNew::TraceInfo::~TraceInfo() { delete file;}
    const char* TraceNew::TraceInfo::File() const { return file;}
    size_t TraceNew::TraceInfo::Line() { return line;}
    TraceNew::TraceNew() { mp.clear();}
    TraceNew::~TraceNew() { Dump(); mp.clear();}
    void TraceNew::Add(void *p, const char *file, size_t line) { mp[p] = TraceInfo(file, line);}
    void TraceNew::Remove(void *p) { auto it = mp.find(p); if(it != mp.end()) mp.erase(it);}
    void TraceNew::Dump() { for(auto it : mp) { cout << it.first << " " << "memory leak on file: " << it.second.File() << " line: " << it.second.Line() << endl; }}
    void* operator new(size_t size, const char *file, size_t line) { void* p = malloc(size); trace.Add(p, file, line); return p;}
    void* operator new[](size_t size, const char *file, size_t line) { return operator new(size, file, line);}
    void operator delete(void *p) { trace.Remove(p); free(p);}
    void operator delete[](void *p) { operator delete(p); }
    #define new new(__FILE__,__LINE__)
    int main() { int *p = new int; int *q = new int[10]; return 0;}
    /*0xa71850 memory leak on file: a.cpp line: 900xa719b8 memory leak on file: a.cpp line: 91*/

    垃圾回收機制

    之前使用過,但現(xiàn)在不再使用或者沒有任何指針再指向的內(nèi)存空間就稱為 "垃圾"。而將這些 "垃圾" 收集起來以便再次利用的機制,就被稱為“垃圾回收”。

    垃圾回收機制可以分為兩大類:

    1. 基于引用計數(shù)的垃圾回收器
    • 系統(tǒng)記錄對象被引用的次數(shù)。當對象被引用的次數(shù)變?yōu)?0 時,該對象即可被視作 "垃圾" 而回收。但難以處理循環(huán)引用的情況。
  • 基于跟蹤處理的垃圾回收器
    • 標記-清除:對所有存活對象進行一次全局遍歷來確定哪些對象可以回收。從根出發(fā)遍歷一遍找到所有可達對象(活對象),其它不可達的對象就是垃圾對象,可被回收。
    • 標記-縮并:直接清除對象會造成大量的內(nèi)存碎片,所以調(diào)整所有活的對象縮并到一起,所有垃圾縮并到一起,然后一次清除。
    • 標記-拷貝:堆空間分為兩個部分 From 和 To。剛開始系統(tǒng)只從 From 的堆空間里面分配內(nèi)存,當 From 分配滿的時候系統(tǒng)就開始垃圾回收:從 From 堆空間找出所有的活對象,拷貝到 To 的堆空間里。這樣一來,F(xiàn)rom 的堆空間里面就全剩下垃圾了。而對象被拷貝到 To 里之后,在 To 里是緊湊排列的。接下來是需要將 From 和 To 交換一下角色,接著從新的 From 里面開始分配。

    通過位運算實現(xiàn)加減乘除取模

    • 加法操作

    對于每一位而言,在不考慮進位的情況下,可以得到

    0+0 = 0\\
    0+1 = 1\\
    1+0 = 1\\
    1+1 = 0

    顯然,上面的情況符合 異或 操作且只有第四種情況發(fā)生了進位,進位情況符合 操作。在所有發(fā)生進位處,應該在更高的一位處加一,這個值可以通過 左移 操作實現(xiàn)。那么就可以得到

    x+y = x \oplus y + (x \& y)<<1

    可以發(fā)現(xiàn),后面的式子變成了一個新的加法式,那么只要遞歸計算即可。當 (x & y)<<1 == 0 時,就消除了加法式。

    ll add(ll x, ll y) { ll newx = x^y; ll newy = (x&y)<<1; if(newy == 0) return newx; return add(newx, newy);}
    • 減法操作

    減法操作可以看成


    同樣可以通過加法式得到

    ll sub(ll x, ll y) { return add(x, add(~y, 1));}
    • 乘法操作

    假設(shè) y=1010,則可以關(guān)注于二進制上的 1 位,那么可以將 x*y 做出拆解

    \begin{aligned} xy &= x1010\ &= x1000 + x0010
    \end{aligned}

    而這個當乘數(shù)只有一個 1 時,可以通過二進制的左移操作實現(xiàn)。

    ll mul(ll x, ll y) { ll ans = 0; int flag = x^y; x = x<0 ? add(~x, 1) : x; y = y<0 ? add(~y, 1) : y; for(int i=0; (1ll< if(y&(1ll< ans = add(ans, x< } } return flag<0 ? add(~ans, 1) : ans;}
    • 除法操作

    和乘法操作思想一樣,枚舉答案每一位是否為 1,通過左移來得到乘積并減去。先從大的開始找,如果有一位是 1,那么就在答案將這一位設(shè)成 1。

    ll divide(ll x, ll y) { ll ans = 0; int flag = x^y; x = x<0 ? add(~x, 1) : x; y = y<0 ? add(~y, 1) : y; for(int i=31; i>=0; i--) { if(x >= (1ll*y< ans |= (1ll< x = sub(x, 1ll*y< } } return flag<0 ? add(~ans, 1) : ans;}
    • 取模操作

    已經(jīng)得到了除法的結(jié)果,那么取模操作也很容易實現(xiàn)了

    ll mod(ll x, ll y) { x = x<0 ? add(~x, 1) : x; y = y<0 ? add(~y, 1) : y; return sub(x, mul(y, divide(x, y)));}

    為什么子類對象可以賦值給父類對象而反過來卻不行

    • 子類繼承于父類,它含有父類的部分,又做了擴充。如果子類對象賦值給父類變量,則使用該變量只能訪問子類的父類部分。
    • 如果反過來,這個子類變量如果去訪問它的擴充成員變量,就會訪問不到,造成內(nèi)存越界。

    為什么 free 時不需要傳指針大小

    free 要做的事是歸還 malloc 申請的內(nèi)存空間,而在 malloc 的時候已經(jīng)記錄了申請空間的大小,所以不需要傳大小,直接傳指針就可以。

    手寫鏈表實現(xiàn) LRU

    class LRU {private: struct Node { int val; Node *pre, *suf; Node() { pre = suf = nullptr; } Node(int _val) { val = _val; pre = suf = nullptr; } }; int size; int capacity; Node *head; unordered_map<int, Node*> mp; Node* find(int val) { if(mp.count(val)) { return mp[val]; } else { return nullptr; } } void del(Node *node) { if(node == nullptr) return ; node->pre->suf = node->suf; node->suf->pre = node->pre; mp.erase(node->val); if(node == head) head = head->suf; size--; } void add(Node *node) { if(head == nullptr) { head = node; head->suf = head; head->pre = head; mp[node->val] = node; } else { node->suf = head; node->pre = head->pre; head->pre = node; node->pre->suf = node; mp[node->val] = node; head = node; } size++; }public: LRU() { mp.clear(); head = nullptr; size = capacity = 0; } void reverse(int _capacity) { capacity = _capacity; } void insert(int val) { Node *node = new Node(val); Node *selectnode = find(val); del(selectnode); if(size == capacity) { del(head->pre); } add(node); } void view() { Node *p = head; do { cout << p->val << " "; p = p->suf; } while(p != head); cout << endl; }}lru;

    推薦閱讀:

    C++ 萬字長文第一篇---拿下字節(jié)面試

    免責聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!

    本站聲明: 本文章由作者或相關(guān)機構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內(nèi)容真實性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時聯(lián)系本站刪除。
    換一批
    延伸閱讀

    9月2日消息,不造車的華為或?qū)⒋呱龈蟮莫毥谦F公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

    關(guān)鍵字: 阿維塔 塞力斯 華為

    加利福尼亞州圣克拉拉縣2024年8月30日 /美通社/ -- 數(shù)字化轉(zhuǎn)型技術(shù)解決方案公司Trianz今天宣布,該公司與Amazon Web Services (AWS)簽訂了...

    關(guān)鍵字: AWS AN BSP 數(shù)字化

    倫敦2024年8月29日 /美通社/ -- 英國汽車技術(shù)公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時1.5...

    關(guān)鍵字: 汽車 人工智能 智能驅(qū)動 BSP

    北京2024年8月28日 /美通社/ -- 越來越多用戶希望企業(yè)業(yè)務(wù)能7×24不間斷運行,同時企業(yè)卻面臨越來越多業(yè)務(wù)中斷的風險,如企業(yè)系統(tǒng)復雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務(wù)連續(xù)性,提升韌性,成...

    關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

    8月30日消息,據(jù)媒體報道,騰訊和網(wǎng)易近期正在縮減他們對日本游戲市場的投資。

    關(guān)鍵字: 騰訊 編碼器 CPU

    8月28日消息,今天上午,2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會開幕式在貴陽舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

    關(guān)鍵字: 華為 12nm EDA 半導體

    8月28日消息,在2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會上,華為常務(wù)董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語權(quán)最終是由生態(tài)的繁榮決定的。

    關(guān)鍵字: 華為 12nm 手機 衛(wèi)星通信

    要點: 有效應對環(huán)境變化,經(jīng)營業(yè)績穩(wěn)中有升 落實提質(zhì)增效舉措,毛利潤率延續(xù)升勢 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務(wù)引領(lǐng)增長 以科技創(chuàng)新為引領(lǐng),提升企業(yè)核心競爭力 堅持高質(zhì)量發(fā)展策略,塑強核心競爭優(yōu)勢...

    關(guān)鍵字: 通信 BSP 電信運營商 數(shù)字經(jīng)濟

    北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺與中國電影電視技術(shù)學會聯(lián)合牽頭組建的NVI技術(shù)創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會上宣布正式成立。 活動現(xiàn)場 NVI技術(shù)創(chuàng)新聯(lián)...

    關(guān)鍵字: VI 傳輸協(xié)議 音頻 BSP

    北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會上,軟通動力信息技術(shù)(集團)股份有限公司(以下簡稱"軟通動力")與長三角投資(上海)有限...

    關(guān)鍵字: BSP 信息技術(shù)
    關(guān)閉
    關(guān)閉