C++面試沖刺筆記1:虛函數的基本工作原理

C++面試沖刺筆記1:虛函數的基本工作原理

前言

? 筆者最近開始投簡歷,出于應對之后快速的面試流程需求,這里準備的是將常見的C++八股文進行總結,從而方便自己進行學習,檢查和評估。

什么是虛函數

? 虛函數,本質上還是函數,為什么是虛的呢?虛函數本質上是由 virtual 修飾的成員函數,但“虛”的真正含義指的是:它的調用行為并非在編譯階段決定,而是在運行階段通過動態綁定機制決定。舉一個例子感受一下:

class BaseClass {
public:virtual void aVirtualFunction() = 0; // 這是一個最極端的純虛函數  
};class DerivedClass : public BaseClass {
public:void aVirtualFunction() override; // 這是子類的一個實現
};

? 如果你熟悉,你就會說:很簡單,當我們寫上代碼:

BaseClass* pSon = new DerivedClass();
pSon->aVirtualFunction();

? 的時候,我們就會高興的發現,盡管我們在書寫的時候寫的是BaseClass*,但是實際上,由于我們的賦值對象是BaseClass的派生類DerivedClass,這個時候,aVirtualFunction被瞧瞧的替換成了子類的實現而不是父類的實現。**這就是一種運行時的多態!**大類中的子類行為可以被共同表達為一個抽象的行為,而子類則各自為政,具象化這個抽象的父類行為(或者是覆蓋父類的行為,這就是類的覆蓋了,我們后面討論類的時候慢慢聊)。

虛函數的實現本質

? 我們現在聊一聊虛函數的運作本質是如何支持的。很簡單,寫過嵌入式C比較大型項目的朋友都知道,我們經常搞函數回調,也就是說,在運行的時候動態的跳轉道給定的地址執行代碼。在C++中,我們可以聯想到——上述的這些本質上可以看作是類中的一個動態的存儲著函數指針的成員變量,發生繼承的時候,我們就把子類的指針賦值給我們的對象上,這樣,發生調用的時候,我們就會直接調用這個函數指針指向的函數。很好!實際上大致的確如此,我們把這些潛在的虛函數排列成一張表格。也就是經典的虛函數表。虛函數表是用一個虛函數表指針指向的。所以說,指向虛函數表成員的大小就是一個指針的大小,這是毋庸置疑的。**這就是你調試一個帶有虛函數的類的時候,你看到調試欄中的_vptr或者_vtable**對象了。

免責聲明:

  • _vptr 是編譯器生成的,名稱和布局是實現相關的(如 MSVC 和 GCC 不一樣),也就是說,不同的編譯器對此的實現不一致,所以別亂搞Hack把代碼的可移植性搞丟了
  • 不是標準定義的,因此依賴它做 portable 編程是不可取的,但了解它對理解原理很重要。

一些其他重要的事情

  • 派生類中重寫時,可以省略 virtual,虛性自動繼承。

  • 可以用 override 強制編譯器檢查函數簽名是否匹配,避免拼寫或參數錯誤

  • 純虛函數=0)表示“子類必須重寫”,容許定義抽象類,也就是無法實例化的接口 。這個小trick可以用在庫設計中,強迫客戶程序員重寫父類的代碼

  • 虛析構函數用來確保通過基類指針刪除子類對象時,析構鏈能正確觸發。所以,任何帶有虛函數的類,都最好將自己的析構函數寫上大大的virtual,否則會翻車。

    struct A {~A() { std::cout << "A析構\n"; }
    };
    struct B : A {~B() { std::cout << "B析構\n"; }
    };A* p = new B();
    delete p; // 只會調用A的析構!
    

    這個時候你需要做的是:

    struct A {virtual ~A() { std::cout << "A析構\n"; }
    };
    
  • 靜態函數不能是虛函數:它不屬于實例,沒 this 指針,無法參與動態綁定

到這里就結束?No No No

? 那可太沒意思了兄弟們,大伙都知道我這個人的性子,寫著點是沒有啥值得說的,否則就成了博客灌水了。我們還有很多其他的話題。

多重繼承下的虛函數表結構(主次 vtable)

? 我們知道,C++是一個允許多重繼承的語言,請看下圖:

class HolyShitComplexClass : 	public BaseA, // is BaseApublic BaseB, // is BaseBpublic BaseC, // is Also BaseC
{...
};

? 這個所謂的HolyShitComplexClass是三個類的子集,我們這個時候會問,欸!這么多的父類,我們的虛函數表怎么排列呢?答案是這樣的,按照我們繼承的順序,依次排放我們的虛函數表指針,這也就是說,在多重繼承中,每個基類子對象通常都有獨立的 vptr 和對應的 vtable(主表與次表),使得各繼承路徑可獨立動態綁定

[vptr_A][A_fields][vptr_B][B_fields][vptr_C][C_fields][Derived_fields]

? 你看,這樣排列的,我要是重寫了A中的一個虛函數的行為,咱們就改A函數表中對應的函數指針指向,其他的如法炮制!

虛函數與非虛函數混合時的內存布局

? 更多的時候,我們往往是類中同時含有虛函數和非虛數據成員,筆者的CCIMXDesktop項目中就有大量的這樣的例子:

#ifndef APPCARDWIDGET_H
#define APPCARDWIDGET_H#include <QWidget>class DesktopToast;
class QLabel;
namespace Ui {
class AppCardWidget;
}/*** @brief AppCardWidget is a lightweight widget used to post messages to a DesktopToast.** This is an abstract base class representing an application card UI component.* It is responsible for handling pre-launch work and posting messages via toast notifications.*/
class AppCardWidget : public QWidget {Q_OBJECTpublic:Q_DISABLE_COPY(AppCardWidget);AppCardWidget() = delete;/*** @brief Constructs an AppCardWidget.* @param toast Pointer to the DesktopToast object used to show messages.* @param parent Optional parent widget.*/explicit AppCardWidget(DesktopToast* toast, QWidget* parent = nullptr);~AppCardWidget();/*** @brief Set the current icon for the app card.** This function allows derived classes to customize the app card icon* by providing a QPixmap.** @param icons The pixmap to be used as the icon.*/virtual void setCurrentIcon(const QPixmap& icons);/*** @brief Abstract method to invoke pre-launch operations.** Derived classes should implement this to perform necessary* preparations before the system starts or the app card becomes active.*/virtual void invoke_preLaunch_work() = 0;/*** @brief operate_comment_label*/virtual void operate_comment_label() = 0;/*** @brief invoke_textlabel_stylefresh*/void invoke_textlabel_stylefresh();protected:/*** @brief Abstract method to post messages to the bound DesktopToast.** Derived classes implement this to send notifications or status updates* through the toast system.*/virtual void postAppCardWidget() = 0;/*** @brief setHelperFunction: plainly set the text for shown* @param what*/virtual void setHelperFunction(const QString& what);virtual void setupSelfTextLabelStyle(QLabel* selfTextLabel) = 0;DesktopToast* binding_toast; ///< Pointer to the toast widget used for posting messages.Ui::AppCardWidget* ui; ///< UI object generated from the Qt Designer form.public:/*** @brief Event filter to handle user interaction events.* @param watched The QObject being watched.* @param event The event being filtered.* @return true if the event was handled, otherwise false.*/bool eventFilter(QObject* watched, QEvent* event) override;
};
#endif // APPCARDWIDGET_H

? 你看,這個類中,我們就混合了虛函數和非虛函數,那么問題來了,我們的編譯器有沒有比較具體的排列呢?有的。

  • 通常 vptr 放在對象首部(首成員);
  • 緊接其后是非虛成員,保持自然對齊;
  • 所有虛函數都在單一 vtable 中按聲明順序排列 。
+---------------------+--------------------------------------------+
| 內存區域            | 內容描述                                   |
+=====================+============================================+
|      虛表指針       | 指向 AppCardWidget 的虛函數表               |
+---------------------+--------------------------------------------+
|                     |                                            |
|     成員變量        | DesktopToast* binding_toast                |
|                     | (綁定的 toast 對象指針)                     |
|                     +--------------------------------------------+
|                     | Ui::AppCardWidget* ui                      |
|                     | (UI 組件指針)                              |
+---------------------+--------------------------------------------+
|     繼承部分        | QWidget 基類的所有成員數據                 |
+---------------------+--------------------------------------------+
|     Qt 特性         | Q_OBJECT 宏添加的元對象系統數據            |
|                     +--------------------------------------------+
|                     | Q_DISABLE_COPY 宏禁用拷貝功能              |
+---------------------+--------------------------------------------+虛表指針指向的是->
+-----+---------------------------------------------+-----------+
| 偏移 | 函數簽名                                   | 類型      |
+=====+=============================================+===========+
|  0  | ~AppCardWidget()                           | 析構函數  |
+-----+---------------------------------------------+-----------+
|  1  | setCurrentIcon(const QPixmap&)             | 虛函數    |
+-----+---------------------------------------------+-----------+
|  2  | invoke_preLaunch_work()                    | 純虛函數  |
+-----+---------------------------------------------+-----------+
|  3  | operate_comment_label()                    | 純虛函數  |
+-----+---------------------------------------------+-----------+
|  4  | postAppCardWidget()                        | 純虛函數  |
+-----+---------------------------------------------+-----------+
|  5  | setHelperFunction(const QString&)          | 虛函數    |
+-----+---------------------------------------------+-----------+
|  6  | setupSelfTextLabelStyle(QLabel*)           | 純虛函數  |
+-----+---------------------------------------------+-----------+
|  7  | eventFilter(QObject*, QEvent*)             | 虛函數    |
+-----+---------------------------------------------+-----------+

CRTP 靜態多態

? 這個就談不上虛函數的內容了,但是放在這里原因很簡單,我們談論到運行時多態的時候,必然要跟CRTP 靜態多態做做對比。

? 在很多時候,我們可能實際上,不太需要的是運行時多態,什么意思?

BaseClass* pSon = new DerivedClass();
pSon->aVirtualFunction();

? 這個代碼顯然我們并不需要運行時多態,理由非常簡單,因為我們知道pSon在這個邏輯流中指向的是一個具體的子類DerivedClass,而不是其他奇奇怪怪的東西。基于這個理念,我們發現一些場景中完全不需要所謂的運行時多態,我們在寫代碼的時候就預料到這里一定不會出現運行才能裁決的事情,啥叫運行時裁決呢?

void DesktopMainWindow::invoke_appcards_init() {/* sequencely invoke the work */showToast("AppCards Are Initing...");// each_app_cards是一個父類,這個父類表達這個對象是一個AppCards,但是具體是啥,如何提前派發初始化的工作,由子類自己的invoke_preLaunch_work裁決。for (const auto& each_app_cards : std::as_const(this->app_cards)) {each_app_cards->invoke_preLaunch_work();}showToast("AppCards Init Finished!");
}

? 這個卡片可能實從用戶提供的插件動態庫,將來的我創建的,但是現在我完全不知道他們的具體的行為,但是我可以保證他們肯定至少是AppCards,這個時候就要到運行的時候檢索類對象的元信息找出她到底是誰,然后調用具體的類對invoke_preLaunch_work的實現

? 所以,CRTP是啥呢?答案是:Curiously Recurring Template Pattern,奇異遞歸模板模式。這個玩意的基本起手框架是這樣的。。。

template <typename Derived>
class Base {
public:void interface() {// 調用派生類實現的方法static_cast<Derived*>(this)->implementation();}// 可提供默認實現(可選)void implementation() {std::cout << "Base implementation\n";}
};class Derived : public Base<Derived> {
public:void implementation() {std::cout << "Derived implementation\n";}
};

? 這很有意思,我們實際上利用模板,在編譯的時候就轉發給了我們指定的編譯器確定的Derived類。這就是CRTP。我們可以看到,這里在語法上,Base和Derived可以說是毫無關系,我們在代碼編寫的時候才會耦合到一起。

  • 這個可是和傳統虛函數的動態多態不同,CRTP 在 編譯期就完成了多態行為的分發(通過 static_cast 強轉為派生類),沒有虛函數表、沒有間接跳轉;
  • 不會因為虛函數造成 cache miss 或影響內聯優化;
  • 可以實現“接口繼承”或“行為注入”。

? 所以CRTP在現代C++中還是有不少的身影的(我沒用到,因為我評估我的項目是否可以有潛在的提升的時候發現真用不上)。

? 感謝GPT,他給了我一個總結表格:

特性CRTP(靜態多態)虛函數(動態多態)
多態分發時機編譯期運行時
是否使用 vtable
性能開銷無額外開銷,可內聯優化存在虛函數調用開銷
類型靈活性編譯期固定,類型必須已知支持運行時多態
擴展性模板層級難以抽象出接口更適合框架級接口

🧱 CRTP 限制和注意事項

? CRTP是存在問題的,我們一個一個了解:

  • 編譯復雜度增加:模板膨脹、編譯錯誤難排查,我想大家都被該死的模板報錯折磨過(好像新clang對模板的報錯稍微友善了點?但是排查模板的報錯屬于是非常的不友好了)
  • 不可跨 DLL 邊界使用:CRTP 依賴編譯期展開,這就是因為它本身就是一個依賴模板的靜態多態技術,屬于是代價了。
  • 無法通過指針存儲基類對象:除非用 Base<Derived>* 這樣的具體類型;
  • 類型綁定固定:CRTP 的“接口”只能綁定一個特定派生類,缺乏動態擴展能力;

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

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

相關文章

Spring Boot 事務失效問題詳解:原因、場景與解決方案

在 Spring Boot 開發中&#xff0c;事務管理是保證數據一致性和完整性的核心機制。然而&#xff0c;許多開發者在使用 Transactional 注解時&#xff0c;可能會遇到事務失效的問題&#xff0c;導致數據異常或業務邏輯錯誤。本文將深入分析 Spring Boot 中事務失效的常見原因&am…

Python-文件操作-StringIO和BytesIO-路徑操作-shutil模塊-csv,ini序列化和反序列化-argparse使用-學習筆記

序 欠4年前的一份學習筆記&#xff0c;獻給今后的自己。 文件操作 馮諾依曼體系架構CPU由運算器和控制器組成 運算器&#xff0c;完成各種算數運算、邏輯運算、數據傳輸等數據加工處理 。 控制器&#xff0c;控制程序的執行 存儲器&#xff0c;用于記憶程序和數據&#xff0c;例…

LLM的表征做減法的是什么,自然語言是一個矩陣,怎么進行減法的

LLM的表征做減法的是什么,自然語言是一個矩陣,怎么進行減法的 有個假設:就是最后一個詞語融合了前面詞語的信息 減法操作主要用于提取模型內部表征中的"誠實性"概念向量。具體來說,這是通過對比誠實和不誠實場景下的模型隱藏狀態實現的。 import torch from t…

Java創建型模式---單例模式

單例模式基礎概念單例模式是一種創建型設計模式&#xff0c;其核心思想是確保一個類僅有一個實例&#xff0c;并提供一個全局訪問點來獲取這個實例。在 Java 中實現單例模式主要有以下關鍵點&#xff1a;私有構造函數 - 防止外部通過new關鍵字創建實例靜態實例變量 - 類內部持有…

詳解Kafka重平衡機制詳解

Kafka 的重平衡機制&#xff08;Rebalance&#xff09;是確保消費者組內成員動態變化&#xff08;如新成員加入、現有成員退出或崩潰、訂閱主題分區數變化&#xff09;時&#xff0c;分區所有權能合理、公平地重新分配的核心機制。其目標是保證所有分區都有消費者處理&#xff…

代碼詳細注釋:文件IO在用戶管理系統中的應用實踐:C語言實現用戶名查重與密碼確認與支持日志記錄的終端用戶認證解決方案的注冊登錄系統

代碼/* 作業增強版注冊登錄系統 - 帶日志和安全性增強功能 */ #include <stdio.h> // 標準輸入輸出函數(printf, scanf等) #include <stdlib.h> // 標準庫函數(exit, malloc等) #include <string.h> // 字符串處理函數(strcmp, strcspn等) #inc…

Go與JS無縫協作:Goja引擎實戰之錯誤處理最佳實踐

引言&#xff1a;當Go邂逅JavaScript 在現代軟件開發中&#xff0c;跨語言協作已成為提升效率的關鍵。想象一下&#xff1a;用Go的高性能處理核心邏輯&#xff0c;同時用JavaScript的靈活性實現動態規則——這不再是夢想。Goja&#xff0c;這個純Go語言實現的JavaScript引擎&am…

繼承與多態:面向對象編程的兩大支柱

引言&#xff1a;為什么必須掌握繼承與多態&#xff1f; 在Java開發中&#xff0c;繼承與多態是構建可擴展、易維護系統的基石&#xff1a; 繼承&#xff1a;實現代碼復用&#xff0c;建立清晰的類層次結構多態&#xff1a;提升代碼靈活性&#xff0c;實現"編寫一次&#…

2025使用VM虛擬機安裝配置Macos蘋果系統下Flutter開發環境保姆級教程--上篇

前言 我們在學習Flutter開發的過程中&#xff0c;永遠都跳不過去的一個問題就是如何在MAC下開發并打包Flutter工程項目&#xff0c;但MAC開發首先要解決的問題就是我們一般技術人員的電腦都是WINDOWS操作系統&#xff0c;專門配置一臺MAC的話成本又是不得不考慮的因素&#xf…

250708-Svelte項目從Debian遷移到無法聯網的RHEL全流程指南

&#x1f4cc; 背景 在 Debian 上使用以下命令創建了一個 Svelte 項目&#xff1a; npm install -g sv npx sv create my-svelte-demo cd my-svelte-demo npm install npm run dev現在需要將該項目遷移到一臺 無法聯網的 RHEL 9.4 服務器 上運行&#xff0c;出現如下報錯&…

力扣 hot100 Day39

118. 楊輝三角 給定一個非負整數 numRows&#xff0c;生成「楊輝三角」的前 numRows 行。 class Solution { public:vector<vector<int>> generate(int numRows) {vector<vector<int>> res(numRows);for (int i 0; i < numRows; i) {res[i].resi…

HuggingFists: 無代碼處理復雜PDF

有過使用LLM搭建RAG或其它類知識系統的朋友一定會對文檔數據的復雜多樣性有著深刻的理解。各行各業的磁盤中都沉睡了數年到數十年的各類文檔信息&#xff0c;包括&#xff1a;Doc、Docx、PPT、PDF、XLS、PNG、JPEG等各類格式。利用LLM激活這些數據價值的首要工作就是能夠正確的…

Vue 3 框架實現理念、架構與設計哲學深度解析

第一部分&#xff1a;Vue 3 的起源&#xff1a;架構演進與設計哲學 Vue 3 的誕生并非一次簡單的版本迭代&#xff0c;而是一場深刻的架構革命。它的出現是前端技術演進、應用規模擴張以及對更高性能和可維護性追求的必然結果。要全面理解 Vue 3 的各項實現理念&#xff0c;必須…

SQL Server使用存儲過程導出數據到Excel實現方式

在SQL Server數據庫管理中,存儲過程作為預編譯的T-SQL語句集合,能顯著提升數據操作效率與安全性。將數據導出到Excel的需求廣泛存在于報表生成、數據遷移等場景。本文詳細解析四種通過存儲過程實現數據導出的技術方案,涵蓋代碼實現、適用場景及優化策略,為不同業務需求提供…

OpenGL 2. 著色器

#include <glad/glad.h> #include <GLFW/glfw3.h> #include <iostream> #include <stdexcept>// 函數聲明 void framebuffer_size_callback(GLFWwindow* window, int width, int height); void processInput(GLFWwindow* window); void checkShaderCom…

【c++】容器擴容導致的類實例資源被錯誤釋放

BUG記錄 表現為新實例被存入前&#xff0c;容器內部的舊實例的析構被意外調用 因為 std::vector 在容量不足時&#xff0c;會自動擴容&#xff0c;把舊元素「搬」到新內存&#xff0c;然后析構舊內存上的那些對象。然后由于LKMotorController 類里沒有正確處理移動語義&#xf…

TypeScript 集成

下面&#xff0c;我們來系統的梳理關于 Vue TypeScript 深度集成 的基本知識點&#xff1a;一、TypeScript 與 Vue 集成概述 1.1 為什么需要 TypeScript 類型安全&#xff1a;編譯時類型檢查&#xff0c;減少運行時錯誤代碼智能&#xff1a;強大的IDE智能提示和自動補全可維護…

npm proxy

背景 前端項目下載依賴時經常會出現timeout的情況&#xff0c;此時有三種解決方案。 切換鏡像源。 適用于對依賴版本要求不嚴格的情況。延長超時時間。設置npm proxy。一些生產環境對依賴版本有著嚴格要求&#xff0c;并且指定了依賴的下載地址&#xff08;如下圖&#xff09;&…

TVS管工作原理是什么?主要的應用場景都有哪些?

什么是TVS管&#xff1f; TVS&#xff08;Transient Voltage Suppressors&#xff09;&#xff0c;即瞬態電壓抑制器&#xff0c;也被稱為雪崩擊穿二極管&#xff0c;是一種二極管形式的高效能保護器件&#xff0c;常用來防止端口瞬間的電壓沖擊造成后級電路的損壞。 TVS 有單…

分布式微服務系統架構第156集:JavaPlus技術文檔平臺日更-Java線程池使用指南

title: java線程池使用 author: 哪吒 date: 2023-06-15點擊勘誤issues&#xff0c;哪吒感謝大家的閱讀Java線程池使用指南1. 線程池基礎使用1.1 創建線程池的方式方式一&#xff1a;使用Executors工具類&#xff08;不推薦&#xff09;// 1. 固定大小線程池 ExecutorService fi…