C++題目

1、內存管理

1.內存模型

棧:在執行函數時,函數內局部變量的存儲單元都可以在棧上創建,函數執行結束時這些存儲單元自動被釋放。

堆:就是那些由new分配的內存塊,其釋放由程序員控制(一個new對應一個delete)。

自由存儲區:堆是操作系統維護的一塊內存,自由存儲區是C++中通過new和delete動態分配和釋放對象的抽象概念,和堆比較像,但不等價。

常量存儲區:存儲常量,不允許修改。

代碼區:存放函數體的二進制代碼。

2.內存泄漏

內存泄漏是指由于疏忽或錯誤造成了程序未能釋放掉不再使用的內存的情況;比如,new申請資源后沒有delete,子類繼承父類時,父類析構函數不是虛函數。

檢測:一些常見的工具插件,如ccmalloc、Dmalloc、Leaky、Valgrind等等。

解決辦法:智能指針

3.內存對齊

什么是內存對齊?

在C語言中,結構體是一種復合數據類型,其構成元素既可以是基本數據類型(如int、long、float等)的變量,也可以是一些復合數據類型(如數組、結構體、聯合體等)的數據單元。在結構體中,編譯器為結構體的每個成員按其自然邊界(alignment)分配空間。各個成員按照它們被聲明的順序在內存中順序存儲,第一個成員的地址和整個結構體的地址相同。

為了使CPU能夠對變量進行快速的訪問,變量的起始地址應該具有某些特性,即所謂的“對齊”,比如4字節的int型,其起始地址應該位于4字節的邊界上,即起始地址能夠被4整除,也即“對齊”跟數據在內存中的位置有關。如果一個變量的內存地址正好位于它長度的整數倍,他就被稱做自然對齊。

為什么要內存對齊?

需要字節對齊的根本原因在于CPU訪問數據的效率問題。假設上面整型變量的地址不是自然對齊,比如為0x00000002,則CPU如果取它的值的話需要訪問兩次內存,第一次取從0x00000002-0x00000003的一個short,第二次取從0x00000004-0x00000005的一個short然后組合得到所要的數據,如果變量在0x00000003地址上的話則要訪問三次內存,第一次為char,第二次為short,第三次為char,然后組合得到整型數據。

而如果變量在自然對齊位置上,則只要一次就可以取出數據。一些系統對對齊要求非常嚴格,比如sparc系統,如果取未對齊的數據會發生錯誤,而在x86上就不會出現錯誤,只是效率下降。

2、C++基礎語法

1.在main執行之前和之后執行的代碼

main函數執行之前,主要就是初始化系統相關資源:

  1. 設置棧指針

  2. 初始化靜態static變量和global全局變量,即.data段的內容

  3. 將未初始化部分的全局變量初始化,short、int、long等為0,bool為false,指針為NULL等

  4. 全局對象初始化,在main之前調用構造函數,這是可能會執行前的一些代碼

  5. 將main函數的參數argc、argv等傳遞給main函數,然后允許main函數

main函數執行之后

  1. 全局對象的析構函數執行

  2. 可以使用atexit注冊一個函數,它會在main之后執行

2.簡述C++從代碼到可執行二進制文件的過程

一個C++程序從源代碼到可執行程序有四個過程:預處理,編譯,匯編,鏈接。

  1. 預編譯:這個過程主要的處理操作如下:

    (1) 將所有的#define刪除,并且展開所有的宏定義

    (2) 處理所有的條件預編譯指令,如#if、#ifdef

    (3) 處理#include預編譯指令,將被包含的文件插入到該預編譯指令的位置。

    (4) 過濾所有的注釋

    (5) 添加行號和文件名標識。

  2. 編譯:這個過程主要的處理操作如下:

    (1) 詞法分析:將源代碼的字符序列分割成一系列的記號。

    (2) 語法分析:對記號進行語法分析,產生語法樹。

    (3) 語義分析:判斷表達式是否有意義。

    (4) 代碼優化:

    (5) 目標代碼生成:生成匯編代碼。

    (6) 目標代碼優化:

  3. 匯編:這個過程主要是將匯編代碼轉變成機器可以執行的指令。

  4. 鏈接:將不同的源文件產生的目標文件進行鏈接,從而形成一個可以執行的程序。鏈接分為靜態鏈接和動態鏈接。

    靜態鏈接:是在鏈接的時候就已經把要調用的函數或者過程鏈接到了生成的可執行文件中,就算你在去把靜態庫刪除也不會影響可執行程序的執行;生成的靜態鏈接庫,Windows下以.lib為后綴,Linux下以.a為后綴。每當庫函數的代碼修改了,都需要重新進行編譯鏈接形成可執行程序。但是運行速度快。

    動態鏈接:是在鏈接的時候沒有把調用的函數代碼鏈接進去,而是在執行的過程中,再去找要鏈接的函數,生成的可執行文件中沒有函數代碼,只包含函數的重定位信息,所以當你刪除動態庫時,可執行程序就不能運行。生成的動態鏈接庫,Windows下以.dll為后綴,Linux下以.so為后綴。更新方便,但是每次都需要進行鏈接,性能會有一定損耗。

3.引用和指針的區別

指針:指針相當于一個變量,但是他和不同變量不一樣,它存放的是其他變量在內存中的地址。指針名指向了內存的首地址。在32位平臺下,無論指針類型是什么,sizeof p = 4,在64位平臺下,sizeof p 都是8。指針在初始化后可以改變指向。

引用:引用是原來的變量實質上是一個東西,是原來變量的別名,引用只能有一級,引用不能為NULL而且在定義時必須初始化,在初始化之后不可再改變,sizeof引用得到的是引用所指向變量的大小。

在匯編層面,一些編譯器把引用當成指針操作,因此引用也會占用空間,是否占用空間,應該結合編譯器分析。

當把指針作為參數進行傳遞時,也是將實參的一個拷貝傳遞給形參,兩者指向的地址相同,但不是同一個變量,在函數中改變這個變量的指向不影響實參,而引用卻可以。

在傳遞函數參數時,什么時候用指針,什么時候用引用?

  • 需要返回函數局部變量的內存時候用指針,使用指針傳參需要開辟內存,用完要記得釋放指針內存,不然會內存泄漏,而返回局部變量的引用是沒有意義的。

  • 對棧空間大小敏感的時候(比如遞歸)使用引用。使用引用傳遞不需要創建臨時變量,開銷要更小。

  • 類對象作為參數傳遞時使用引用,這是C++類對象傳遞的標準方式。

4.指針數組、數組指針、函數聲明和函數指針

?int *p[10]; // 指針數組,數組內有10個元素,每個元素都是指向int類型的指針int (*p)[10]; // 數組指針,是一個指針,指向一個int類型的數組int*p(int a); // 函數聲明,返回值是int*類型的,參數是int類型int (*p)(int a); // 函數指針,指向一個返回值為int,參數為int的函數

5.說說new和malloc的區別,以及各自底層實現原理

  1. new是操作符,而malloc是函數。

  2. new在調用的時候先分配內存,在調用構造函數,釋放的時候調用析構函數;而malloc沒有構造函數和析構函數。

  3. malloc需要給定申請內存的大小,返回的指針需要強轉;new會調用構造函數,不用指定內存的大小,返回指針不用強轉。

  4. new可以被重載;malloc不行

  5. new分配內存更直接和安全。

  6. new發生錯誤拋出異常,malloc返回null。

malloc底層實現:當開辟的空間小于 128K 時,調用 brk()函數;當開辟的空間大于 128K 時,調用mmap()。malloc采用的是內存池的管理方式,以減少內存碎片。先申請大塊內存作為堆區,然后將堆區分為多個內存塊。當用戶申請內存時,直接從堆區分配一塊合適的空閑快。采用隱式鏈表將所有空閑塊,每一個空閑塊記錄了一個未分配的、連續的內存地址。

new底層實現:關鍵字new在調用構造函數的時候實際上進行了如下的幾個步驟:

  1. 首先調用operator new的標準庫函數,分配足夠大的原始為類型化的內存,創建一個新的對象

  2. 將構造函數的作用域賦值給這個新的對象(因此this指向了這個新的對象)

  3. 執行構造函數中的代碼(為這個新對象添加屬性)

  4. 返回新對象

6.說說內聯函數和宏函數的區別

  1. 宏定義不是函數,但是使用起來像函數。預處理器用復制宏代碼的方式代替函數的調用,省去了函數壓棧退棧過程,提高了效率;而內聯函數本質上是一個函數,內聯函數一般用于函數體的代碼比較簡單的函數,不能包含復雜的控制語句,while、switch,并且內聯函數本身不能直接調用自身。

  2. 宏函數是在預編譯的時候把所有的宏名用宏體來替換,簡單的說就是字符串替換 ;而內聯函數則是在編譯的時候進行代碼插入,編譯器會在每處調用內聯函數的地方直接把內聯函數的內容展開,這樣可以省去函數的調用的開銷,提高效率

  3. 宏定義是沒有類型檢查的,無論對還是錯都是直接替換;而內聯函數在編譯的時候會進行類型的檢查,內聯函數滿足函數的性質,比如有返回值、參數列表等

inline函數一般用于比較小的,頻繁調用的函數,最好定義在頭文件,而不僅僅是聲明,因為編譯器在處理inline函數時,需要在調用點內聯展開該函數,所以僅需要聲明函數是不夠的。

7.說說const和define的區別

const用于定義常量;而define用于定義宏,而宏也可以用于定義常量。都用于常量定義時,它們的區別有:

  1. const生效于編譯的階段;define生效于預處理階段。

  2. const定義的常量,在C語言中是存儲在內存中、需要額外的內存空間的;define定義的常量,運行時是直接的操作數,并不會存放在內存中。

  3. const定義的常量是帶類型的;define定義的常量不帶類型。因此define定義的常量不利于類型檢查。

8.C++中const和static關鍵字的作用

static

  1. 定義全局靜態變量和局部靜態變量:在變量前面加上static關鍵字。初始化的靜態變量會在數據段分配內存,未初始化的靜態變量會在BSS段分配內存。直到程序結束,靜態變量始終會維持前值。只不過全局靜態變量和局部靜態變量的作用域不一樣;

  2. 定義靜態函數:在函數返回類型前加上static關鍵字,函數即被定義為靜態函數。靜態函數只能在本源文件中使用;

  3. 在變量類型前加上static關鍵字,變量即被定義為靜態變量。靜態變量只能在本源文件中使用

  4. 在c++中,static關鍵字可以用于定義類中的靜態成員變量:使用靜態數據成員,它既可以被當成全局變量那樣去存儲,但又被隱藏在類的內部。類中的static靜態數據成員擁有一塊單獨的存儲區,而不管創建了多少個該類的對象。所有這些對象的靜態數據成員都共享這一塊靜態存儲空間。

  5. 在c++中,static關鍵字可以用于定義類中的靜態成員函數:與靜態成員變量類似,類里面同樣可以定義靜態成員函數。只需要在函數前加上關鍵字static即可。如靜態成員函數也是類的一部分,而不是對象的一部分。所有這些對象的靜態數據成員都共享這一塊靜態存儲空間,不具有this指針。靜態成員函數在類內定義,必須在類外初始化。不能訪問類對象的非static成員變量和非static成員函數。

const

  1. cosnt常量在定義時必須初始化,之后無法更改;

  2. const形參可以接受const和非const類型的實參;

  3. const成員變量:不能在類定義外部初始化,只能通過構造函數初始化列表進行初始化,并且必須有構造函數,不同類對其const數據成員可以不同,所以不能再類中聲明初始化。

  4. const成員函數:const對象不可以調用非const成員函數,非const對象都可以調用;不可以改變非mutable數據的值。

9.final和override關鍵字

override

當在父類中使用了虛函數時候,可能需要在某個子類中對這個虛函數進行重寫:如果不使用override將虛函數的名字寫錯了,這時候override的作用就出來了,它指定了子類的這個虛函數是重寫的父類的,如果名字錯誤的話,編譯器是不會通過的。

?class A{virtual void foo();};?class B : public A{virtual void f00(); // 新增的函數virtual void foo() override;};

final

當不希望某個類被繼承時,或不希望某個虛函數被重寫時,可以在類名和虛函數后面添加final關鍵字,添加final關鍵字后被繼承或重寫時,編譯器會報錯。

10.volatile、mutable和explicit關鍵字

1、volatile

volotile關鍵字是一種類型修飾符,用它聲明的類型變量表示可能被某些編譯器未知的因素修改。因此編譯后的程序每次需要存儲或讀取這個變量的時候,都會直接從變量地址中讀取數據。如果沒有volatile關鍵字,則編譯器可能優化讀取和存儲,可能暫時使用寄存器中的值,如果這個變量由別的程序更新了的話,將出現不一致的現象。多線程應用中被幾個任務共享的變量應該加 volatile;

2、mutable

mutable是為了突破const的限制而設置的。被mutable修飾的變量,將永遠處于可變的狀態,即使在一個const函數中。我們知道,如果類的成員函數不會改變對象的狀態,那么這個成員函數一般會聲明成const的。但是,有些時候,我們需要在const的函數里面修改一些跟類狀態無關的數據成員,那么這個數據成員就應該被mutalbe來修飾。

3、explicit

它用來修飾只有一個參數的類構造函數,以表明該構造函數是顯式的,而非隱式的。當使用explicit修飾構造函數時,它將禁止類對象之間的隱式轉換,以及禁止隱式調用拷貝構造函數。

?class Foo {?public:?Foo(int x) { /* ... */ }?};?void bar(Foo f) {?// ...?}?int main() {?bar(42); // 隱式轉換,調用Foo(42)?}?// 在這里,`bar`函數需要一個`Foo`對象,但傳入了一個`int`,編譯器會隱式調用`Foo(int)`構造函數。如果這不符合預期,加上`explicit`?class Foo {?public:?explicit Foo(int x) { /* ... */ }?};?void bar(Foo f) {?// ...?}?int main() {?bar(42); // 錯誤:不能隱式轉換int到Foo?bar(Foo(42)); // 正確:顯式轉換?}

11.說說什么是野指針和懸空指針,怎么產生的,如何避免?

  1. 概念:野指針就是沒有被初始化的指針,懸空指針就是最初指向的內存已經被釋放的一種指針。

  2. 產生原因:釋放內存后指針不及時置空(野指針),依然指向了該內存,那么可能出現非法訪問的錯誤。這些我們都要注意避免。

  3. 避免辦法:

    (1)初始化置NULL

    (2)申請內存后判空

    (3)指針釋放后置NULL

    (4)使用智能指針

12.C++的頂層const和底層const

  • 頂層const:指的是const修飾的變量本身是一個常量,無法修改。

  • 底層const:指的是const修飾的變量所指向的對象是一個常量,指的是所知變量。

?int i = 10;const int ii = 11; // 頂層const: 對象 ii 本身是常量const int *a = &i; // 底層const: 指針對象 a 本身不是常量,而所存地址指向的對象是常量int const *b = &i; // 底層const: 與上一條語句寫法不同,但都是表示指向常量的指針,即指針常量int* const c = &i; // 頂層const: 指針對象 c 本身是常量,即常量指針const int* const d = &i; // 左側const是底層const,右側const是頂層constconst int& e = i; // 底層const: 引用所存地址指向的對象是常量
  • 常量指針:即指針本身是常量,表示指針存儲的地址不可被修改

  • 指針常量:即指向常量的指針,表示其所指向的對象不可被修改

?int i = 10;int* const a = &i; //頂層 const => 常量指針const int* b = &i; //底層 const => 指針常量const int* const c = &i; //指向常量對象的常量指針,既有底層 const 又有 頂層 const

13.如何用代碼判斷大小端存儲?

大端存儲:字數據的高字節存儲在低地址中。

小端存儲:字數據的低字節存儲在低地址中。

在socket編程中,往往需要將操作系統所用的小端存儲的IP地址轉換為大端存儲,這樣才能進行網絡傳輸。

?#include <iostream>using namespace std;int main(){int a = 0x1234;//由于int和char的長度不同,借助int型轉換成char型,只會留下低地址的部分char c = (char)(a);if (c == 0x12)cout << "big endian" << endl;else if(c == 0x34)cout << "little endian" << endl;}

14.malloc、realloc、calloc的區別

?void *malloc(unsigned int num_size);int *p = malloc(20*sizeof(int));?// 省去了人為空間計算,malloc申請的空間的值是隨機初始化的,calloc申請的空間的值是初始化為0的void *calloc(size_t n, size_t size);int *p = calloc(20, sizeof(int));?// 給動態分配的空間分配額外的空間,用于擴充容量void realloc(void *p, size_t new_size);

15.C++函數調用的壓棧過程

當函數從入口函數main函數開始執行時,編譯器會將我們操作系統的運行狀態,main函數的返回地址、main的參數,main函數中的變量、進行依次壓棧。當main函數開始調用func函數時,編譯器會將main函數的運行狀態進行壓棧,再將func函數的返回地址,func函數的參數從右到左,func定義變量依次壓棧;當func函數調用f函數時,編譯器此時會將func函數的運行狀態進行壓棧,再將返回地址,f函數的參數從右到左,f函數定義變量依次壓棧,先執行f后,在執行func最后執行main。

16.全局變量和局部變量有什么區別?

生命周期不同:全局變量隨主程序創建而創建,隨主程序銷毀而銷毀;局部變量在局部函數內部,甚至局部循環體等內部存在,退出就不存在。

使用方式不同:通過聲明后全局變量在程序的各個部分都可以用到;局部變量分配在堆棧區,只能在局部使用。

操作系統和編譯器通過內存分配的未知可以區分兩者:全局變量分配在全局數據端并且在程序開始運行的時候被加載。局部變量則分配在堆棧里面。

17.C++中struct和class的區別

相同點:

  • 兩者都擁有成員函數、公有和私有部分

  • 任何可以使用class完成的工作,同樣可以使用struct完成

不同點

  • 兩者中如果不對成員指定公私有,struct默認是公有的,class默認是私有的

  • class默認private繼承,而struct默認是public繼承

3、面向對象

1.面向對象的三大特性

面向對象的三大特性是封裝、繼承、多態。

  1. 封裝:將數據和操作數據的方法進行有機結合,隱藏對象的屬性和實現細節,僅對外公開接口來和對象進行 交互。

  2. 多態:用父類型別的指針指向其子類的實例,然后通過父類的指針調用實際子類的成員函數。實現多態,有二種方式,重寫,重載。

  3. 繼承:可以使用現有類的所有功能,并在無需重新編寫原來的類的情況下對這些功能進行擴展。三種繼承方式:

繼承方式private繼承protected繼承public繼承
基類的private成員不可見不可見不可見
基類的protected成員變為private成員仍為protected成員仍為protected成員
基類的public成員變為private成員變為protected成員仍為public成員仍為public成員

2.C++中的重載、重寫的區別

重載:重載是指在同一范圍定義中的同名成員函數才存在重載函數。主要特點是函數名相同,參數類型和數目所不同,不能出現參數個數和類型均相同,僅僅依靠返回值不同來區分的函數。重載和函數成員是否是虛函數無關。

重寫:重寫是指在派生類中覆蓋基類中的同名函數,重寫就是重寫函數體,要求基類函數必須是虛函數,運行時根據對象的實際類型來調用相應的函數。如果對象類型是派生類,就調用派生類的函數;如果對象類型是基類,就調用基類的函數。

  1. 用virtual關鍵字申明的函數叫做虛函數,虛函數肯定是類的成員函數。

  2. 存在虛函數的類都有一個一維的虛函數表叫做虛表,類的對象有一個指向虛表開始的虛指針。虛表是和類對應的,虛表指針是和對象對應的。

  3. 多態性是一個接口多種實現,是面向對象的核心,分為類的多態性和函數的多態性。

  4. 重寫用虛函數來實現,結合動態綁定。

  5. 純虛函數是虛函數再加上 = 0。

  6. 抽象類是指包括至少一個純虛函數的類。

純虛函數:virtual void fun()=0。即抽象類必須在子類實現這個函數,即先有名稱,沒有內容,在派生類實現內容。

重載與重寫的區別:

  • 重寫是父類和子類之間的垂直關系,重載是不同函數之間的水平關系

  • 重寫要求參數列表相同,重載則要求參數列表不同,返回值不要求

  • 重寫關系中,調用方法根據對象類決定,重載根據調用時實參表與形參表的對應關系來選擇函數體。

3.說說C++中構造函數有幾種,分別有什么作用?

C++中的構造函數可以分為四類:默認構造函數初始化構造函數拷貝構造函數移動構造函數

1、默認構造函數初始化構造函數。在定義類的對象的時候,完成對象的初始化工作。

?class Student{public://默認構造函數Student(){num=1001;age=18; ? ? ? }//初始化構造函數Student(int n,int a):num(n),age(a){}private:int num;int age;};int main(){//用默認構造函數初始化對象S1Student s1;//用初始化構造函數初始化對象S2Student s2(1002,18);return 0;}

有了有參數的構造函數,編譯器就不提供默認的構造函數了。

2、拷貝構造函數

#include "stdafx.h"#include "iostream.h"?class Test{int i;int *p;public:Test(int ai,int value){i = ai;p = new int(value);}~Test(){delete p;}Test(const Test& t){this->i = t.i;this->p = new int(*t.p);}};//復制構造函數用于復制本類的對象int main(int argc, char* argv[]){Test t1(1,2);Test t2(t1);//將對象t1復制給t2。注意復制和賦值的概念不同return 0;}
賦值構造函數默認實現的是值拷貝(淺拷貝)3、移動構造函數用于將其他類型的變量,隱式轉換為本類對象。下面的代碼,將int的r轉換為Student類型的對象,對象的age為r,num = 1004Student(int r){int num=1004;int age= r;}

4.淺拷貝和深拷貝的區別

淺拷貝

淺拷貝只是拷貝一個指針,并沒有新開辟一個地址,拷貝的指針和原來的指針指向同一塊地址,如果原來的指針指向的資源釋放了,那么再釋放淺拷貝的指針的資源就會出現錯誤。

深拷貝

深拷貝不僅拷貝值,還開辟出一塊新的空間用來存放新的值,即使原先的對象被析構掉,釋放內存了也不會影響到深拷貝得到的值。在自己實現拷貝賦值的時候,如果有指針變量的話是需要自己實現深拷貝的。

5.如果有一個空類,它會默認添加哪些函數?

  1. 無參的構造函數

  2. 拷貝構造函數

  3. =運算符重載函數

  4. 析構函數

6.如何阻止一個類被實例化?

  1. 將類定義為抽象基類或者將構造函數私有化

  2. 不允許類外部創建類對象,只能在類內部創建對象

7.如何禁止程序自動生成拷貝構造函數?

  1. 為了組織編譯器默認生成拷貝構造函數和拷貝賦值函數,我們需要手動重寫這兩個函數,某些情況下,為了避免調用拷貝構造函數和拷貝賦值函數,我們可以把它們設置為private,防止被調用。

  2. 類的成員函數和firend友元函數還是可以調用private函數,如果這個private函數只聲明不定義,則會產生一個連接錯誤,所以我們定義一個基類,在基類中將拷貝構造函數和拷貝賦值函數設置成private,那么派生類中編譯器中將不會自動生成這兩個函數,且由于在基類中該函數是私有的,因此,派生類將阻止編譯器執行相關的操作。

8.C++中為什么拷貝構造函數必須傳引用不能傳值?

假設拷貝構造函數是按值傳遞參數的話,會發生什么情況呢?比如,如果有一個類MyClass,拷貝構造函數寫成MyClass(MyClass other),那么在調用這個構造函數的時候,就需要傳遞一個實參的副本。也就是說,當傳入一個對象時,需要調用拷貝構造函數來創建參數other,而這個參數本身又是一個傳值參數,這就會導致無限遞歸調用拷貝構造函數。

在C++標準中,明確指出拷貝構造函數的參數必須是引用類型,否則會導致編譯錯誤。這是因為編譯器檢測到這種情況,會阻止用戶這樣定義,避免運行時的問題。

9.類成員初始化方式?構造函數的執行順序?為什么用成員初始化列表會快一些?

  1. 賦值初始化,通過在函數體內進行賦值初始化;列表初始化,在冒號后使用初始化列表進行初始化。這兩種方式的主要區別在于:對于在函數體中初始化,是在所有的數據成員被分配內存空間后才進行的。列表初始化是給數據成員分配內存空間時就進行初始化,就是說分配一個數據成員只要冒號后有此數據的賦值表達式,那么分配了內存空間后在進入函數體之前給數據成員賦值,就是說初始化這個數據成員此時函數體還未執行。

  2. 一個派生類構造函數的執行順序如下:虛擬基類的構造函數(多個虛擬基類則按照繼承的的順序執行構造函數);基類的構造函數(多個普通基類也按照繼承的順序執行構造函數);類類型的成員對象的構造函數(按照成員對象在類中的定義順序);派生類自己的構造函數。

  3. 方法一是在構造函數中做賦值的操作,而方法二是做純粹的初始化操作。我們都知道,C++的賦值是會產生臨時對象的。臨時對象的出現會降低程序的效率。

10.簡述下向上轉型和向下轉型

  1. 子類轉換為父類:向上轉型,使用dynamic_cast(expression),這種轉換相對來說安全不會有數據的丟失。

  2. 父類轉換為子類:向下轉型,可以使用強制類型轉換,這種轉換是不安全的,會導致數據的丟失,原因是父類的指針或者引用的內存中可能不包含子類的成員的內存。

11.簡述一下C++中的多態

由于派生類重寫基類方法,然后用基類引用或指針指向派生類對象,調用方法時候會根據所指對象的實際類型來調用相應的函數,如果對象類型是派生類,就調用派生類的函數,如果對象是基類,就調用基類的函數,這就是多態。

?
#include <iostream>using namespace std;?class Base{public:virtual void fun(){cout << " Base::func()" <<endl;}};?class Son1 : public Base{public:virtual void fun() override{cout << " Son1::func()" <<endl;}};?class Son2 : public Base{?};?int main(){Base* base = new Son1;base->fun();base = new Son2;base->fun();delete base;base = NULL;return 0;}// 運行結果// Son1::func()// Base::func()

子類1繼承并重寫了基類的函數,子類2繼承基類但沒有重新基類的函數,從結果分析子類 體現了多態性,那么為什么會出現多態性,其底層的原理是什么?

這里需要引出虛表和虛基表指針的概念。

虛表:虛函數表的縮寫,類中含有virtual關鍵字修飾的方法時,編譯器會自動生成虛表。

虛表指針:在含有虛函數的類實例化對象時,對象地址的前四個字節存儲的指向虛表的指針。

實現多態的過程:

  1. 編譯器在發現基類中有虛函數時,會自動為每個含有虛函數的類生成一份虛表,該表是一個一維數組,虛表里保存了虛函數的入口地址

  2. 編譯器會在每個對象的前四個字節中保存一個虛表指針,即vptr,指向對象所屬的虛表。在構造時,根據對象的類型區初始化虛表指針,從而讓vptr指向正確的虛表,從而在調用虛函數時,能找到正確的函數。

  3. 所謂的合適時機,在派生類定義對象時,程序運行會自動調用構造函數,在構造函數中創建虛表并對虛表初始化。在構造子類對象時,會先調用父類的構造函數。此時,編譯器只看到了父類,并為父類對象初始化虛表指針,令他指向父類的虛表;當調用子類的構造函數時,為子類對象初始化虛表指針,令它指向子類的虛表。

  4. 派生類的虛表生成:a.先將基類中的虛表內容拷貝一份到派生類虛表中 b.如果派生類重寫了基類中某個虛函數,用派生類自己的虛函數覆蓋虛表中基類的虛函數 c.派生類自己新增加的虛函數按其在派生類中的聲明次序增加到派生類虛表的最后。

  5. 當派生類對基類的虛函數沒有重寫時,派生類的虛表指針指針指向的相當于基表指針,此時調用的是基類的虛函數,當派生類對基類的虛函數重寫時,派生類的虛表指針指向的是自身的虛表,調用這個函數。

12.靜態綁定和動態綁定

靜態綁定: 靜態綁定是在編譯時確定調用的函數或方法,它是通過函數或方法的名稱、參數數量、類型和順序來匹配確定的。對于非虛擬函數和靜態成員函數,默認情況下都是靜態綁定。例如,在以下代碼中:

?
class Base {public:void display() {std::cout << "Base class" << std::endl;}};class Derived : public Base {public:void display() {std::cout << "Derived class" << std::endl;}};int main() {Base baseObj;Derived derivedObj;baseObj.display(); ? ? ?// 靜態綁定,輸出 "Base class"derivedObj.display(); ? // 靜態綁定,輸出 "Derived class"}

動態綁定: 動態綁定是指在運行時確定調用的函數或方法,它是通過虛擬函數和指針/引用來實現的。虛擬函數是在基類中聲明為虛擬的成員函數,在派生類中進行重寫。通過使用基類的指針或引用調用虛擬函數時,實際調用的是派生類中重寫的函數。例如,在以下代碼中:

?class Base {public:virtual void display() {std::cout << "Base class" << std::endl;}};class Derived : public Base {public:void display() {std::cout << "Derived class" << std::endl;}};int main() {Base* basePtr;Derived derivedObj;basePtr = &derivedObj;basePtr->display(); ? // 動態綁定,輸出 "Derived class"}

13.基類的虛函數表存放在內存的什么區?虛表指針vptr的初始化時間?

C++中的虛函數表位于只讀數據段,也就是C++內存模型中的常量區;而虛函數位于代碼段,也就是C++內存模型的代碼區。

由于虛表指針和虛函數密不可分,對于有虛函數或繼承于擁有虛函數的基類,對該類進行實例化時,在構造函數執行時會對虛表指針進行初始化,并且存在對象內存布局的最前面。

14.構造函數為什么不能為虛函數?析構函數為什么要虛函數?

構造函數為什么不能為虛函數

  1. 從存儲空間角度:虛函數對應一個指向vtable,這個表的地址是存儲在對象的內存空間的。如果將構造函數設置為虛函數,就需要到vtable 中調用,可是對象還沒有實例化,沒有內存空間分配,如何調用。(悖論)

  2. 從使用角度:虛函數主要用于在信息不全的情況下,能使重載的函數得到對應的調用。構造函數本身就是要初始化實例,那使用虛函數也沒有實際意義呀。所以構造函數沒有必要是虛函數。虛函數的作用在于通過父類的指針或者引用來調用它的時候能夠變成調用子類的那個成員函數。而構造函數是在創建對象時自動調用的,不可能通過父類的指針或者引用去調用,因此也就規定構造函數不能是虛函數。

  3. 從實現上看,vbtl 在構造函數調用后才建立,因而構造函數不可能成為虛函數。從實際含義上看,在調用構造函數時還不能確定對象的真實類型(因為子類會調父類的構造函數);而且構造函數的作用是提供初始化,在對象生命期只執行一次,不是對象的動態行為,也沒有太大的必要成為虛函數。

析構函數為什么要虛函數

虛析構:將可能會被繼承的父類的析構函數設置為虛函數,可以保證當我們new一個子類,然后使用基類指針指向該子類對象,釋放基類指針時可以釋放掉子類的空間,防止內存泄漏。如果基類的析構函數不是虛函數,在特定情況下會導致派生來無法被析構。

  1. 用派生類類型指針綁定派生類實例,析構的時候,不管基類析構函數是不是虛函數,都會正常析構

  2. 用基類類型指針綁定派生類實例,析構的時候,如果基類析構函數不是虛函數,則只會析構基類,不會析構派生類對象,從而造成內存泄漏。為什么會出現這種現象呢,個人認為析構的時候如果沒有虛函數的動態綁定功能,就只根據指針的類型來進行的,而不是根據指針綁定的對象來進行,所以只是調用了基類的析構函數;如果基類的析構函數是虛函數,則析構的時候就要根據指針綁定的對象來調用對應的析構函數了。

C++默認的析構函數不是虛函數是因為虛函數需要額外的虛函數表和虛表指針,占用額外的內存。而對于不會被繼承的類來說,其析構函數如果是虛函數,就會浪費內存。因此C++默認的析構函數不是虛函數,而是只有當需要當作父類時,設置為虛函數。

15.請你回答一下 C++ 類內可以定義引用數據成員嗎?

c++類內可以定義引用成員變量,但要遵循以下三個規則:

  1. 不能用默認構造函數初始化,必須提供構造函數來初始化引用成員變量。否則會造成引用未初始化錯誤。

  2. 構造函數的形參也必須是引用類型。

  3. 不能在構造函數里初始化,必須在初始化列表中進行初始化。

16.哪些函數不能是虛函數?

  1. 構造函數,構造函數初始化對象,派生類必須知道基類函數干了什么,才能進行構造;當有虛函數時,每一個類有一個虛表,每一個對象有一個虛表指針,虛表指針在構造函數中初始化。

  2. 內聯函數:內聯函數表示在編譯階段進行函數體的替換操作,而虛函數意味著在運行期間進行類型確定,所以內聯函數不能是虛函數;

  3. 靜態函數,靜態函數不屬于對象屬于類,靜態成員函數沒有this指針,因此靜態函數設置為虛函數沒有任何意義。

  4. 友元函數,友元函數不屬于類的成員函數,不能被繼承。對于沒有繼承特性的函數沒有虛函數的說法。

  5. 普通函數,普通函數不屬于類的成員函數,不具有繼承性,因此普通函數沒有虛函數。

17.說說 C++ 中什么是菱形繼承問題,如何解決

? /**Animal類對應于圖表的類A**/?class Animal { /* ... */  }; // 基類{int weight;?public:?int getWeight() { return weight;};?};?class Tiger : public Animal { /* ... */ };?class Lion : public Animal { /* ... */  }?class Liger : public Tiger, public Lion { /* ... */ }int main( ){Liger lg ;/*編譯錯誤,下面的代碼不會被任何C++編譯器通過 */int weight = lg.getWeight(); ?}

在我們的繼承結構中,我們可以看出Tiger和Lion類都繼承自Animal基類。所以問題是:因為Liger多重繼承了Tiger和Lion類,因此Liger類會有兩份Animal類的成員(數據和方法),Liger對象"lg"會包含Animal基類的兩個子對象。

所以,你會問Liger對象有兩個Animal基類的子對象會出現什么問題?再看看上面的代碼-調用"lg.getWeight()"將會導致一個編譯錯誤。這是因為編譯器并不知道是調用Tiger類的getWeight()還是調用Lion類的getWeight()。所以,調用getWeight方法是不明確的,因此不能通過編譯。

我們給出了菱形繼承問題的解釋,但是現在我們要給出一個菱形繼承問題的解決方案。如果Lion類和Tiger類在分別繼承Animal類時都用virtual來標注,對于每一個Liger對象,C++會保證只有一個Animal類的子對象會被創建。看看下面的代碼:

?class Tiger : virtual public Animal { /* ... */ };?class Lion : virtual public Animal { /* ... */ }

18.說說什么是虛繼承,解決什么問題,如何實現?

虛繼承是解決C++多重繼承問題的一種手段,從不同途徑繼承來的同一基類,會在子類中存在多份拷貝。這將存在兩個問題:其一,浪費存儲空間;第二,存在二義性問題,通常可以將派生類對象的地址賦值給基類對象,實現的具體方式是,將基類指針指向繼承類(繼承類有基類的拷貝)中的基類對象的地址,但是多重繼承可能存在一個基類的多份拷貝,這就出現了二義性。虛繼承可以解決多種繼承前面提到的兩個問題。

虛繼承底層實現原理與編譯器相關,一般通過虛基類指針和虛基類表實現,每個虛繼承的子類都有一個虛基類指針(占用一個指針的存儲空間,4字節)和虛基類表(不占用類對象的存儲空間)(需要強調的是,虛基類依舊會在子類里面存在拷貝,只是僅僅最多存在一份而已,并不是不在子類里面了);當虛繼承的子類被當做父類繼承時,虛基類指針也會被繼承。

實際上,vbptr指的是虛基類表指針,該指針指向了一個虛基類表,虛表中記錄了虛基類與本類的偏移地址;通過偏移地址,這樣就找到了虛基類成員,而虛繼承也不用像普通多繼承那樣維持著公共基類(虛基類)的兩份同樣的拷貝,節省了存儲空間。

19.說說C++中虛函數與純虛函數的區別

  1. 虛函數和純虛函數可以定義在同一個類中,含有純虛函數的類被稱為抽象類,而只含有虛函數的類不能被稱為抽象類。

  2. 虛函數可以被直接使用,也可以被子類重載以后,以多態的形式調用,而純虛函數必須在子類中實現該函數才可以使用,因為純虛函數在基類有聲明而沒有定義。

  3. 虛函數和純虛函數都可以在子類中被重載,以多態的形式被調用。

  4. 虛函數和純虛函數通常存在于抽象基類之中,被繼承的子類重載,目的是提供一個統一的接口。

  5. 虛函數的定義形式:virtual{};純虛函數的定義形式:virtual { } = 0;在虛函數和純虛函數的定義中不能有static標識符,原因很簡單,被static修飾的函數在編譯時要求前期綁定,然而虛函數卻是動態綁定,而且被兩者修飾的函數生命周期也不一樣。

4、STL

5、C++新特性

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/74399.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/74399.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/74399.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

天地圖InfoWindow插入React自定義組件

截至2025年03月21日天地圖的Marker不支持添加Label; 同時Label和Icon是不支持自定義HTMLElement只支持String&#xff1b;目前只有InfoWindow支持自定義HTMLElement; 效果圖 React核心api import ReactDOM from react-dom/client const content document.createElement(div);…

Java并發編程面試匯總

Java并發編程 一、 基礎概念1. 進程與線程的區別是什么&#xff1f;2. 創建線程的幾種方式&#xff1f;3. 線程的生命周期&#xff08;狀態&#xff09;有哪些&#xff1f;4. 什么是守護線程&#xff08;Daemon Thread&#xff09;&#xff1f;5. 線程優先級&#xff08;Priori…

【STM32】第一個工程的創建

目錄 1、獲取 KEIL5 安裝包2、開始安裝 KEIL52.1、 激活2.2、安裝DFP庫 3、工程創建4、搭建框架5、開始編寫代碼 1、獲取 KEIL5 安裝包 要想獲得 KEIL5 的安裝包&#xff0c;在百度里面搜索“KEIL5 下載”即可找到很多網友提供的下載文件&#xff0c;或者到 KEIL 的官網下載&a…

動態規劃~01背包問題

01背包問題 經典的0 - 1背包問題的解決方案。 二維數組的版本 代碼功能概述 0 - 1背包問題指的是有 n 個物品和一個容量為 m 的背包&#xff0c;每個物品有對應的體積 v[i] 和價值 w[i]&#xff0c;需要從這些物品里挑選若干個放入背包&#xff0c;讓背包內物品的總價值達到最…

深入理解Java享元模式及其線程安全實踐

引言 在軟件系統中&#xff0c;當需要處理海量細粒度對象時&#xff0c;直接創建大量實例可能會導致內存消耗激增和性能下降。享元模式&#xff08;Flyweight Pattern&#xff09;通過共享對象內部狀態&#xff0c;成為解決這類問題的經典方案。然而在多線程環境下&#xff0c…

1、mysql基礎篇--概述

關系型數據庫&#xff08;RDBMS&#xff09; 概念特點&#xff1a;數據模型&#xff1a; 概念 建立在關系模型基礎上&#xff0c;有多張表相互連接的二維表組成的數據庫 特點&#xff1a; 1、使用表存儲&#xff0c;格式統一&#xff0c;便于維護 2、使用sql語言操作&#…

如何提升庫存系統的高并發和穩定性:算法與設計模式

庫存系統是企業運營的核心模塊&#xff0c;尤其是在電商、零售和供應鏈管理中&#xff0c;系統的高并發和穩定性直接影響訂單處理的準確性和效率。面對海量訂單、復雜的庫存管理需求&#xff0c;如何在高并發環境下確保庫存數據的準確性和系統的穩定性&#xff1f;本文將從架構…

【多線程】synchronized底層實現的方式

前言 在java 開發中對于鎖的應用非常的常見&#xff0c;如果對于什么時候該用什么鎖&#xff0c;以及鎖實現的原理有所不知道的&#xff0c;或者面試過程中面試官問你不知道怎么回答的&#xff0c;歡迎來看下面的文章 1、synchronized和ReentrantLock的區別 2、synchronized的…

Pytorch中Tensorboard的學習

1、Tensorboard介紹 TensorBoard 是 TensorFlow 開發的一個可視化工具&#xff0c;用于幫助用戶理解和調試機器學習模型的訓練過程。盡管它最初是為 TensorFlow 設計的&#xff0c;但通過 PyTorch 的 torch.utils.tensorboard 模塊&#xff0c;PyTorch 用戶也可以方便地使用 Te…

ETL 自動化:提升數據處理效率與準確性的核心驅動力

在數字化轉型的浪潮中&#xff0c;數據已成為企業戰略資產&#xff0c;高效處理數據的能力直接關系到企業的競爭力。ETL&#xff08;Extract, Transform, Load&#xff09;自動化作為數據處理領域的關鍵技術&#xff0c;正逐漸成為企業在數據時代脫穎而出、實現高效運營與精準決…

std::endl為什么C++ 智能提示是函數?

在使用vscode 的C智能提示后&#xff0c;輸入endl 后&#xff0c;提示的卻是std::endl(basic_ostream<CharT, Traits> &os), 感覺比較奇怪&#xff0c;各種代碼里都是直接用的std::endl 啊&#xff0c; 這里怎么變成函數了呢&#xff1f; 在 C 中&#xff0c;std::en…

簡潔、實用、無插件和更安全為特點的WordPress主題

簡站WordPress主題是一款以簡潔、實用、無插件和更安全為特點的WordPress主題&#xff0c;自2013年創立以來&#xff0c;憑借其設計理念和功能優勢&#xff0c;深受用戶喜愛。以下是對簡站WordPress主題的詳細介紹&#xff1a; 1. 設計理念 簡站WordPress主題的核心理念是“崇…

數據結構篇:空間復雜度和時間復雜度

目錄 1.前言&#xff1a; 1.1 學習感悟 1.2 數據結構的學習之路(初階) 2.什么是數據結構和算法 2.1 數據結構和算法的關系 2.2 算法的重要性 2.3 如何衡量算法的好壞 3.時間復雜度 3.1 時間復雜度的概念 3.2 大O的漸進表示法 O() 4.空間復雜度 5. 常見的時間復雜度和…

node-ddk,electron,截屏封裝(js-web-screen-shot)

node-ddk 截屏封裝(js-web-screen-shot) https://blog.csdn.net/eli960/article/details/146207062 也可以下載demo直接演示 http://linuxmail.cn/go#node-ddk 感謝/第三方 本截屏工具, 使用的是: js-web-screen-shot https://www.npmjs.com/package/vue-web-screen-shot…

泰坦軍團攜手順網旗下電競連鎖品牌樹呆熊 共創電競新紀元

在電競行業的浪潮中&#xff0c;品牌之間的戰略合作愈發成為推動市場前行的重要動力。最近&#xff0c;電競顯示器領域領軍品牌泰坦軍團高層領導出席順網旗下電競連鎖品牌樹呆熊十周年盛典。會議現場&#xff0c;雙方高層領導宣布泰坦軍團與樹呆熊正式達成戰略合作伙伴關系。 在…

HandyJSON原理

HandyJSON 的優勢 JSON(JavaScript Object Notation) 是一種輕量級的數據交換格式, 應用廣泛. 在 App 的使用過程中, 服務端給移動端發送的大部分都是 JSON 數據, 移動端需要解析數據才能做進一步的處理. 在解析JSON數據這一塊, 目前 Swift 中流行的框架基本上是 SwiftyJSON, …

信號的產生和保存

信號的產生 信號就是操作系統對用戶操作做出的反應&#xff0c;但它的本質就是往操作系統寫入信號&#xff0c;這是由操作系統的結構決定的。通過修改比特位來告訴操作系統接收信號和傳了幾號信號。 也正是因為我們身為用戶無法親自修改內核數據&#xff0c;所以我們需要通過操…

在C++ Qt中集成Halcon窗口并實現跨平臺兼容和大圖加載

目錄 1. Halcon窗口嵌入Qt Widget 2. 處理大圖加載 3. 多線程優化顯示 4. 跨平臺兼容性 1. Halcon窗口嵌入Qt Widget 將Halcon的HWindow控件嵌入到Qt的QWidget容器中,利用系統原生句柄實現跨平臺。 #include <HalconCpp.h> #include <QWidget>class HalconWi…

深度學習技術與應用的未來展望:從基礎理論到實際實現

深度學習作為人工智能領域的核心技術之一&#xff0c;近年來引起了極大的關注。它不僅在學術界帶來了革命性的進展&#xff0c;也在工業界展現出了廣泛的應用前景。從圖像識別到自然語言處理&#xff0c;再到強化學習和生成對抗網絡&#xff08;GAN&#xff09;&#xff0c;深度…

藍光三維掃描技術:汽車零部件檢測的精準高效之選

——汽車方向盤配件、保險杠塑料件、鈑金件檢測項目 汽車制造工業的蓬勃發展&#xff0c;離不開強大的零部件制造體系作支撐。汽車零部件作為汽車工業的基礎&#xff0c;其設計水平、制造工藝、質量控制手段逐漸與國際標準接軌&#xff0c;對于零部件面差、孔位、圓角、特征線…