【c++】全面理解C++多態:虛函數表深度剖析與實踐應用

Alt

🔥個人主頁Quitecoder

🔥專欄c++筆記倉

Alt

朋友們大家好,通過本篇文章,來詳細理解多態的內容

目錄

  • `1.多態的定義及實現`
    • `1.1多態的構成條件`
    • `1.2虛函數的重寫`
    • `1.3 C++11 override 和 final`
    • `1.4重載、覆蓋(重寫)、隱藏(重定義)的對比`
  • `2.多態的原理`
    • `2.1虛函數表`
    • `2.2多態的原理`
    • `2.3單繼承的虛函數表`
  • `3.抽象類`
    • `3.1接口繼承與實現繼承`
    • `3.2靜態多態與動態多態`
    • `3.3例題`
  • `4.多繼承中的虛函數表`
    • `4.1菱形繼承和菱形虛擬繼承`
    • `4.2菱形虛擬繼承:`
  • `5.虛表的存儲位置`

1.多態的定義及實現

多態的基本概念:多態指的是對象可以通過指向它們的基類的引用或指針被操縱,同時還能保持其派生類部分的特性。將派生類對象當作基類對象來對待,這允許不同類的對象響應相同的消息以不同的方式,換句話說,同一個接口,使用不同的實例而執行不同操作

比如買票,普通人買票時,是全價買票;學生買票時,是半價買票

class Person {
public:virtual void BuyTicket() { cout << "買票-全價" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "買票-半價" << endl; }
};
void Func(Person& p)
{p.BuyTicket();
}
int main()
{Person ps;Student st;Func(ps);Func(st);return 0;
}

在這里插入圖片描述
普通人全價,學生半價

1.1多態的構成條件

多態是在不同繼承關系的類對象,去調用同一函數,產生了不同的行為。比如Student繼承了Person。Person對象買票全價,Student對象買票半價

那么在繼承中要構成多態還有兩個條件

  1. 必須通過基類的指針或者引用調用虛函數
  2. 被調用的函數必須是虛函數,且派生類必須對基類的虛函數進行重寫

在這里插入圖片描述
指向誰調用誰

void Func(Person  p)
{p.BuyTicket();
}

如果這樣調用,就不是指針或引用了,現在就不是多態
在這里插入圖片描述

1.2虛函數的重寫

虛函數:即被virtual修飾的類成員函數稱為虛函數

class Person {
public:virtual void BuyTicket() { cout << "買票-全價" << endl; }
};

虛函數的重寫(覆蓋):派生類中有一個跟基類完全相同的虛函數(即派生類虛函數與基類虛函數的返回值類型、函數名字、參數列表完全相同),稱子類的虛函數重寫了基類的虛函數

class Person {
public:virtual void BuyTicket() { cout << "買票-全價" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "買票-半價" << endl; }
};

虛函數重寫的三個例外

  1. 協變(基類與派生類虛函數返回值類型不同):
    派生類重寫基類虛函數時,與基類虛函數返回值類型不同。即基類虛函數返回基類對象的指針或者引用,派生類虛函數返回派生類對象的指針或者引用時,稱為協變。
class A {};
class B : public A {};
class Person {
public:virtual A* f() { return new A; }
};
class Student : public Person {
public:virtual B* f() { return new B; }
};
  1. 析構函數的重寫(基類與派生類析構函數的名字不同)
    如果基類的析構函數為虛函數,此時派生類析構函數只要定義,無論是否加virtual關鍵字,都與基類的析構函數構成重寫,雖然基類與派生類析構函數名字不同。雖然函數名不相同,看起來違背了重寫的規則,其實不然,這里可以理解為編譯器對析構函數的名稱做了特殊處理,編譯后析構函數的名稱統一處理成destructor

只有派生類Student的析構函數重寫了Person的析構函數,下面的delete對象調用析構函數,才能構成多態,才能保證p1和p2指向的對象正確的調用析構函數

class Person {
public:virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:virtual ~Student() { cout << "~Student()" << endl; }
};int main()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;
}

當我們通過基類的指針來刪除一個派生類的對象時,如果基類的析構函數沒有被聲明為虛擬的(virtual),將會發生對象的不完全析構。這意味著只有基類的析構代碼會被執行,而派生類的析構邏輯不會調用,可能導致資源泄露或其他問題。

在給定的代碼中,Person 類的析構函數被聲明為虛擬的:

virtual ~Person() { cout << "~Person()" << endl; }

這意味著任何從 Person 派生的類,像 Student,都應該提供析構函數的一個覆蓋版本:

virtual ~Student() { cout << "~Student()" << endl; }

delete p2; 被執行的時候(其中 p2 是一個基類 Person 類型的指針,指向一個 Student 對象),Student 的析構函數首先會被調用(子類),然后是 Person 的析構函數(基類)

因此,重寫基類的虛擬析構函數確保了當通過基類指向派生類對象的指針進行 delete 操作時,能夠按照正確的順序調用派生類和基類的析構函數

  1. 派生類可以不寫virtual
class Person {
public:virtual void BuyTicket() { cout << "買票-全價" << endl; }
};
class Student : public Person {
public:void BuyTicket() { cout << "買票-半價" << endl; }
};

在重寫基類虛函數時,派生類的虛函數在不加virtual關鍵字時,雖然也可以構成重寫(因為繼承后基類的虛函數被繼承下來了在派生類依舊保持虛函數屬性),但是該種寫法不是很規范,不建議這樣使用

1.3 C++11 override 和 final

C++對函數重寫的要求比較嚴格,但是有些情況下由于疏忽,可能會導致函數名字母次序寫反而無法構成重載,而這種錯誤在編譯期間是不會報出的,只有在程序運行時沒有得到預期結果才來debug會得不償失,因此:C++11提供了override和final兩個關鍵字,可以幫助用戶檢測是否重寫

  1. final:修飾虛函數,表示該虛函數不能再被重寫
class Car
{
public:virtual void Drive() final {}
};
class Benz :public Car
{
public:virtual void Drive() { cout << "Benz-舒適" << endl; }
};

在這里插入圖片描述

用final修飾的類叫做最終類,不能被繼承

class Car final{
public:virtual void Drive() {}
};
class Benz :public Car {
public:virtual void Drive()  { cout << "Benz-舒適" << endl; }
};

在這里插入圖片描述

  1. override: 檢查派生類虛函數是否重寫了基類某個虛函數,如果沒有重寫編譯報錯
class Car {
public:virtual void Drive() {}
};
class Benz :public Car {
public:virtual void Drive() override { cout << "Benz-舒適" << endl; }
};

在這里插入圖片描述

1.4重載、覆蓋(重寫)、隱藏(重定義)的對比

重載發生在同一作用域內。當兩個或者更多的函數擁有相同的名字,但是**參數列表不同(參數類型、參數個數或者參數順序不同)**時,這些函數被稱為重載函數。

class MyClass {
public:void func() void func(int i)void func(double d) 
};

重寫僅在基類和派生類之間發生,且只針對虛函數。當派生類定義一個與基類中虛函數簽名完全相同的函數時(即函數名、參數列表和返回類型相同),派生類函數會覆蓋(重寫)基類中對應的虛函數。這是多態的基礎,使得在運行時可以通過基類的指針或引用調用派生類的函數實現

示例:

class Base {
public:virtual void func() { /* ... */ }
};class Derived : public Base {
public:void func() override { /* ... */ } // 覆蓋(重寫)基類中的func
};

隱藏也是在類的繼承關系中發生,但它和是否為虛函數無關。在派生類中定義了一個新的函數,如果這個函數的名字與基類中的某個函數的名字相同,但是參數列表不同,那么它會隱藏(也稱為重定義)所有與它同名的基類函數,不論基類中同名函數參數列表如何

示例:

class Base {
public:void func() { /* ... */ }void func(int i) { /* ... */ }
};class Derived : public Base {
public:void func(double d) { /* ... */ } // 隱藏了基類的func()// 注意:現在Base的func()和func(int)都被隱藏,只能通過Derived的對象訪問新的func(double)
};

在繼承的類中隱藏了基類中的同名函數(不論是重載還是同簽名的函數),如果想要調用被隱藏的函數,需要顯式地指明作用域:

Derived obj;
obj.Base::func(); // 顯式調用Base類中被隱藏的func()
obj.Base::func(42); // 顯式調用Base類中被隱藏的func(int)
obj.func(3.14); // 調用Derived類中的func(double)

兩個基類和派生類的同名函數,不構成重寫就是隱藏

2.多態的原理

2.1虛函數表

class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;
};

sizeof(Base)是多少?
答案是8,我們進行測試觀察:

在這里插入圖片描述
除了_b成員,還多一個__vfptr放在對象的前面,對象中的這個指針我們叫做虛函數表指針(v代表virtual,f代表function)一個含有虛函數的類中都至少都有一個虛函數表指針,因為虛函數的地址要被放到虛函數表中,虛函數表也簡稱虛表
用內存窗口觀察:
在這里插入圖片描述
它是占八個字節的

2.2多態的原理

class Person {
public:virtual void BuyTicket() { cout << "買票-全價" << endl; }private:int _i = 1;
};class Student : public Person {
public:virtual void BuyTicket() { cout << "買票-半價" << endl; }int _j = 2;
};void Func(Person* p)
{p->BuyTicket();
}int main()
{Person Mike;Func(&Mike);Student Johnson;Func(&Johnson);return 0;
}

在這里插入圖片描述

這里的指向父類調父類,指向子類調子類是怎么實現的呢? 我們進行調試

在這里插入圖片描述
在這里插入圖片描述

Johnson首先繼承了父類的部分,有虛表和虛表指針,這兩個虛表指針不一樣,他們指向內容不一樣,一個指向父類的Buyticket,另一個指向子類的

p是指向mike對象時,p->BuyTicket在mike的虛表中找到虛函數Person::BuyTicket
p是指向johnson對象時,p->BuyTicket在johson的虛表中找到虛函數是Student::BuyTicket

這樣就實現出了不同對象去完成同一行為時,展現出不同的形態

反過來思考我們要達到多態,有兩個條件,一個是虛函數覆蓋,一個是對象的指針或引用調用虛函數。反思一下為什么

滿足多態條件,這里的調用生成的指令就會指向對象的虛表中找對應的虛函數調用

滿足多態以后的函數調用,不是在編譯時確定的,是運行起來以后到對象的中取找的。不滿足多態的函數調用時編譯時確認好的

	p->BuyTicket();
009924E1  mov         eax,dword ptr [p]  
009924E4  mov         edx,dword ptr [eax]  
009924E6  mov         esi,esp  
009924E8  mov         ecx,dword ptr [p]  
009924EB  mov         eax,dword ptr [edx]  
009924ED  call        eax  
009924EF  cmp         esi,esp  
009924F1  call        __RTC_CheckEsp (09912B2h)

滿足多態的情況下

  • p中存的是mike對象的指針,將p移動到eax中

  • [eax]就是取eax值指向的內容,這里相當于把mike對象頭4個字節(虛表指針)移動到了edx

  • [edx]就是取edx值指向的內容,這里相當于把虛表中的頭4字節存的虛函數指針移動到了eax

  • call eax中存虛函數的指針。這里可以看出滿足多態的調用,不是在編譯時確定的,是運行起來以后到對象的中取找的

同類型共用一個虛表

Person Mike;
Func(&Mike);Person p1;
Func(&p1);

在這里插入圖片描述
現在如果不滿足多態呢?

我將父類進行修改

class Person {
public:void BuyTicket() { cout << "買票-全價" << endl; }
private:int _i = 1;
};
	p->BuyTicket();
005B24E1  mov         ecx,dword ptr [p]  
005B24E4  call        Person::BuyTicket (05B149Ch)

它在編譯鏈接時就確定了

2.3單繼承的虛函數表

來看下面的類:

class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a = 1;
};class Derive :public Base {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }virtual void func4() { cout << "Derive::func4" << endl; }
private:int b = 2;
};
int main()
{Base b;Derive d;return 0;
}

在這里插入圖片描述
我們發現Derive少了兩個虛表指針,它只有重寫的func1和繼承的func2,沒有func3,func4,這里是監視窗口的問題

Derive 類的虛表中,會有以下指向虛函數的指針:

  1. 指向 Derive::func1 的指針 (重寫了 Base::func1
  2. 指向 Base::func2 的指針 (繼承自 BaseDerive 沒有重寫)
  3. 指向 Derive::func3 的指針 (Derive 新增的虛函數)
  4. 指向 Derive::func4 的指針 (Derive 新增的虛函數)

我們通過內存來確認:

在這里插入圖片描述
我們不是很確認后面兩個地址就是func3和func4的地址

那么我們如何查看d的虛表呢?下面我們使用代碼打印出虛表中的函數

這里我們用到函數指針數組來實現:

虛函數表的本質就是函數指針數組

void(*p[10])();

這個就定義了一個函數指針數組,我們用typedef來進行優化一下:

typedef void(*VFPTR)();
VFPTR p2[10];

我們定義一個打印虛表的函數

void PrintVFT(VFPTR* vft)
{for (size_t i = 0; i < 4; i++){printf("%p->", vft[i]);VFPTR pf = vft[i];(*pf)();//pf();}
}

依次取虛表中的虛函數指針打印并調用。調用就可以看出存的是哪個函數

函數寫好后,關鍵是我如何取到它的地址?

Derive d;
int ptr = (int)d;  

上面是不支持轉換的,只有有關聯的類型才能互相轉換

但是,指針可以隨意轉換

VFPTR* ptr = (VFPTR*)(*((int*)&d));
  1. &d 取得 d 對象的地址。
  2. (int*)&dd 對象的地址轉換為 int* 類型的指針。這里假定 int 大小足夠存儲指針
  3. *((int*)&d) 對轉換后的指針進行解引用,得到的是 d 對象內存起始處的值。由于在C++中,一個包含虛函數的對象在內存起始地址處通常存儲著指向虛表的指針,因此這步操作實際上獲取的是指向 Derive 虛表的指針
  4. (VFPTR*)int 類型的值強制轉換為 VFPTR* 類型,也就是指向函數指針的指針。
  5. 最終,ptr 就是指向 Derive 類的虛表的指針

因此,VFPTR* ptr 就是指向目標對象 d 的虛表的指針。之后調用 PrintVFT(ptr); 就可以遍歷虛表中的每個條目并調用對應的函數(這里的函數都是通過函數指針 VFPTR 調用的)

在這里插入圖片描述

3.抽象類

在虛函數的后面寫上 =0 ,則這個函數為純虛函數包含純虛函數的類叫做抽象類(也叫接口類)抽象類不能實例化出對象。派生類繼承后也不能實例化出對象,只有重寫純虛函數,派生類才能實例化出對象。純虛函數規范了派生類必須重寫,另外純虛函數更體現出了接口繼承

class Car
{
public:virtual void Drive() = 0;
};

在這里插入圖片描述
某種意義上說,抽象類強制派生類去完成重寫

class Benz :public Car
{
public:virtual void Drive(){cout << "Benz-舒適" << endl;}
};
class BMW :public Car
{
public:virtual void Drive(){cout << "BMW-操控" << endl;}
};

3.1接口繼承與實現繼承

普通函數的繼承是一種實現繼承,派生類繼承了基類函數,可以使用函數,繼承的是函數的實現。虛函數的繼承是一種接口繼承,派生類繼承的是基類虛函數的接口,目的是為了重寫,達成多態,繼承的是接口。所以如果不實現多態,不要把函數定義成虛函數

3.2靜態多態與動態多態

  1. 靜態綁定又稱為前期綁定(早綁定),在程序編譯期間確定了程序的行為,也稱為靜態多態,比如:函數重載
  2. 動態綁定又稱后期綁定(晚綁定),是在程序運行期間,根據具體拿到的類型確定程序的具體行為,調用具體的函數,也稱為動態多態

3.3例題

下面函數輸出結果是什么?

class A
{
public:virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }virtual void test() { func(); }
};class B : public A
{
public:void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};int main(int argc, char* argv[])
{B* p = new B;p->test();return 0;
}
A: A->0  B: B->1  C: A->1  D: B->0  E: 編譯出錯  F: 以上都不正確

正確答案是B

B 繼承自類 A 并且 復寫了 A 中的虛函數 func

首先,復寫(覆蓋)的本質是派生類提供基類虛函數的一個新的實現。基類中的虛函數定義了一個接口,而派生類通過覆蓋這個虛函數,提供了這個接口的特定實現

當創建了派生類 B 的實例,并通過它調用 test() 時,過程如下:

  1. test() 是在基類 A 中定義的,因此它會調用 func 時使用 A 中定義的默認參數,即 1
  2. 由于 func 是虛函數,并且我們實際上是在操作 B 類的對象,因此調用的是 B 類中覆蓋的 func 版本。
  3. 被調用的 B 類的 func 輸出 “B->”,然后使用傳遞給它的參數值,此時是基類的默認參數值 1

綜上所述,輸出是 B->1

要明白一個重要的細節:虛函數的默認參數是靜態綁定的,而非動態綁定。也就是說,虛函數的默認參數會在編譯時根據函數的靜態類型決定,而函數的動態類型會決定在運行時實際調用哪個版本的覆蓋函數。這意味著即使 B::func 定義了一個默認值 0,在 A::test 中調用 func() 時,由于它在編譯時是視為 A 類型的函數調用,所以使用的是 A::func 定義的默認參數 1。這就是為什么是 B->1 而不是 B->0

4.多繼承中的虛函數表

class Base1 {
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1;
};
class Base2 {
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int b2;
};
class Derive : public Base1, public Base2 {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1;
};

在這里插入圖片描述
這里有兩個虛表指針,繼承了兩個父類,兩個父類的虛表不能合在一起,這里對兩張虛表都進行了重寫,那么這里func3放在哪個虛表中了呢,是都放呢還是只放一個呢?

我們可以用上面的打印虛表的函數進行打印

void PrintVTable(VFPTR vTable[])
{cout << " 虛表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d個虛函數地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}
void test()
{Derive d;VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);PrintVTable(vTableb1);VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));PrintVTable(vTableb2);
}

這里第一個虛表已經講過,找第二個虛表先強轉為char,再進行字節相加*
在這里插入圖片描述

func3放入第一個虛表中
在這里插入圖片描述

4.1菱形繼承和菱形虛擬繼承

class A
{
public:virtual void func1() { cout << "A::func1" << endl; }int _a;
};
class B : public A
//class B : virtual public A
{
public:virtual void func2() { cout << "B::func2" << endl; }int _b;
};class C : public A
//class C : virtual public A
{
public:virtual void func3() { cout << "C::func3" << endl; }int _c;
};class D : public B, public C
{
public:virtual void func4() { cout << "D::func4" << endl; }int _d;
};int main()
{D d;cout << sizeof(d) << endl;	return 0;
}

在這里插入圖片描述
在這里插入圖片描述
菱形繼承與多繼承相似,d里面的虛函數放在B的虛表中

4.2菱形虛擬繼承:

class B : virtual public A
class C : virtual public A

在這里插入圖片描述
在這里插入圖片描述
這里除了虛表指針,還有上篇文章講解的存儲偏移量的虛基表指針

int main()
{D d;cout << sizeof(d) << endl;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}

在這里插入圖片描述
在這里插入圖片描述
菱形虛擬繼承,每個類都有一個虛函數,這里ABC都有自己的虛表,但是BC的虛函數不能放在A的虛表中,因為這里虛基類A是共享的

子類有虛函數,繼承的父類有虛函數就有虛表,子類對象中就不需要單獨建立虛表

在這里插入圖片描述
但是菱形虛擬繼承就需要自己建立虛表,不能往父類中放

在這里插入圖片描述

再看下面的代碼:

class A {
public:A(const char* s) { cout << s << endl; }~A() {}
};class B :virtual public A
{
public:B(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};class C :virtual public A
{
public:C(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};class D :public B, public C
{
public:D(const char* s1, const char* s2, const char* s3, const char* s4):B(s1, s2)  //A  B, C(s1, s3)  //A  C, A(s1)      //A{// Dcout << s4 << endl;}
};
int main() {D* p = new D("class A", "class B", "class C", "class D");delete p;return 0;
}

當創建一個派生類的對象時,構造函數會按照特定的順序執行,確保所有的基類和成員變量都被正確初始化。在多繼承和虛繼承的情況下,這個順序變得更加復雜。上面代碼涉及到虛繼承,這意味著基類 A 只會有一個實例,即使它被多次包含在派生類層次結構中,在 BC

D(const char* s1, const char* s2, const char* s3, const char* s4):B(s1, s2)  //A  B, C(s1, s3)  //A  C, A(s1)      //A{

D 的構造函數,我們發現它首先調用 B 的構造函數,然后是 C 的構造函數,最后調用 A 的構造函數。然而,在虛繼承的情況下,共享的基類(在該例子中是 A)只會被初始化一次,而且是由最底層的派生類(D)來初始化。無論 BC 在其構造函數中怎么嘗試初始化 A,它們的嘗試都會被忽略

根據上述規則,執行 new D("class A", "class B", "class C", "class D"); 的過程如下:

  1. 首先,最底層的派生類 D 的構造器被調用。
  2. 因為 A 是通過虛繼承被 BC 繼承的,所以 D 的構造器負責初始化 A。這里將輸出 “class A”
  3. 接下來,D 的構造器調用 B 的構造函數。雖然 B 試圖先調用 A 的構造函數,但這個調用會被忽略,因為 A 已經被初始化了。然后,B 的構造器繼續執行并輸出 “class B”
  4. C 的構造函數也會被調用,但同樣,其對 A 構造函數的調用被忽略,并且 C 的構造器繼續執行,輸出 “class C”
  5. 最后,在 D 的構造函數中的代碼執行之前,所有基類都已經初始化完成。最后輸出 “class D”。
class A
class B
class C
class D

所以,盡量不要寫菱形虛擬繼承,坑點十分多

5.虛表的存儲位置

我們可以通過下面的代碼來推斷虛表在哪存儲的:

class Person {
public:virtual void BuyTicket() { cout << "買票-全價" << endl; }
};class Student : public Person {
public:virtual void BuyTicket() { cout << "買票-半價" << endl; }
};
void tese()
{int i = 0;static int j = 1;int* p1 = new int;const char* p2 = "xxxxxxxx";printf("棧:%p\n", &i);printf("靜態區:%p\n", &j);printf("堆:%p\n", p1);printf("常量區:%p\n", p2);Person p;Student s;Person* p3 = &p;Student* p4 = &s;printf("Person虛表地址:%p\n", *(int*)p3);printf("Student虛表地址:%p\n", *(int*)p4);
}

在這里插入圖片描述
可以推斷出存儲位置在常量區

本節內容到此結束!!感謝大家閱讀!!

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

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

相關文章

wireshark協議大致過濾規則

參考鏈接&#xff1a;真保姆鏈接 1、比較操作符 等于 &#xff01;不等于 >大于 <小于 >大于等于 <小于等于 2、協議類型 直接在Filter框中直接輸入協議名即可。注意&#xff1a;協議名稱需要輸入小寫。 tcp&#xff0c;只顯示TCP協議的數據包列表udp&#xff0c…

鴻蒙內核源碼分析 (內核啟動篇) | 從匯編到 main ()

這應該是系列篇最難寫的一篇&#xff0c;全是匯編代碼&#xff0c;需大量的底層知識&#xff0c;涉及協處理器&#xff0c;內核鏡像重定位&#xff0c;創建內核映射表&#xff0c;初始化 CPU 模式棧&#xff0c;熱啟動&#xff0c;到最后熟悉的 main() 。 內核入口 在鏈接文件…

在k8s中安裝Grafana并對接Prometheus,實現k8s集群監控數據的展示

&#x1f407;明明跟你說過&#xff1a;個人主頁 &#x1f3c5;個人專欄&#xff1a;《Grafana&#xff1a;讓數據說話的魔術師》 &#x1f3c5; &#x1f516;行路有良友&#xff0c;便是天堂&#x1f516; 目錄 一、引言 1、Grafana簡介 2、Grafana的重要性與影響力 …

強化訓練:day9(添加逗號、跳臺階、撲克牌順子)

文章目錄 前言1. 添加逗號1.1 題目描述2.2 解題思路2.3 代碼實現 2. 跳臺階2.1 題目描述2.2 解題思路2.3 代碼實現 3. 撲克牌順子3.1 題目描述3.2 解題思路3.3 代碼實現 總結 前言 1. 添加逗號 ??2. 跳臺階 ??3. 撲克牌順子 1. 添加逗號 1.1 題目描述 2.2 解題思路 我的寫…

【Vue】vue中動態樣式綁定

在Vue中&#xff0c;可以使用動態樣式綁定來根據數據的變化來動態修改元素的樣式。動態樣式綁定可以通過以下幾種方式實現&#xff1a; 對象語法 <template><div :style"dynamicStyles"></div> </template><script> export default {…

STM32學習和實踐筆記(28):printf重定向實驗

1.printf重定向簡介 在C語言中printf函數里&#xff0c;默認輸出設備是顯示器&#xff0c;如果想要用這個函數將輸出結果到串口或者LCD上顯示&#xff0c;就必須重定義標準庫函數里中printf函數調用的與輸出設備相關的函數。 比如要使用printf輸出到串口&#xff0c;需要先將f…

linux 任務管理(臨時任務定時任務) 實驗

目錄 任務管理臨時任務管理周期任務管理 任務管理 臨時任務管理 執行如下命令添加單次任務&#xff0c;輸入完成后按組合鍵Ctrl-D。 [rootopenEuler ~]# at now5min warning: commands will be executed using /bin/sh at> echo "aaa" >> /tmp/at.log at&g…

什么是 PL/SQL

PL/SQL 是 Oracle 公司開發的一種過程化擴展 SQL 語言&#xff0c;它結合了 SQL 語句和過程化編程的特點&#xff0c;允許開發者在一個塊&#xff08;block&#xff09;中編寫聲明、條件語句、循環等&#xff0c;使得數據庫編程更加靈活和強大。PL/SQL 常用于 Oracle 數據庫系統…

bash腳本 報錯:/bin/bash^M:解釋器錯誤: 沒有那個文件或目錄

bash腳本 報錯&#xff1a;/bin/bash^M&#xff1a;解釋器錯誤: 沒有那個文件或目錄 出現這個問題是因為該腳本文件在windows下編輯過 在windows下&#xff0c;每一行的結尾是\n\r&#xff0c;而在linux下文件的結尾是\n&#xff0c;那么你在windows下編輯過的文件在linux下打…

J-STAGE (日本電子科學與技術信息集成)數據庫介紹及文獻下載

J-STAGE (日本電子科學與技術信息集成)是日本學術出版物的平臺。它由日本科學技術振興機構&#xff08;JST&#xff09;開發和管理。該系統不僅包括期刊&#xff0c;還有論文集&#xff0c;研究報告、技術報告等。文獻多為英文&#xff0c;少數為日文。目前網站上所發布的內容來…

零基礎學Java第十三天之日期類

日期時間類 1、Date 1、理解 表示特定的瞬間&#xff1a;Date對象表示從"epoch"&#xff08;即1970年1月1日 00:00:00 GMT&#xff09;開始計算的毫秒偏移量。不包含時區信息&#xff1a;原始的Date類不直接處理時區。它只是一個時間點&#xff0c;沒有與時區關聯。…

使用Vue調用ColaAI Plus大模型,實現聊天(簡陋版)

首先去百度文心注冊申請自己的api 官網地址&#xff1a;LuckyCola 注冊點開個人中心 查看這個文檔自己申請一個ColaAI Plus定制增強大模型API | LuckyColahttps://luckycola.com.cn/public/docs/shares/api/colaAi.html來到vue的頁面 寫個樣式 <template><Header …

ICode國際青少年編程競賽- Python-5級訓練場-綜合練習6

ICode國際青少年編程競賽- Python-5級訓練場-綜合練習6 1、 for i in range(3):Dev.step(2 * (i 1))Dev.turnLeft()while Flyer[2 - i].disappear():wait()Dev.step(2 * (i 1))Dev.turnRight()while Dev.x ! Item[i].x:wait()2、 for i in range(3):Dev.step(2 * i 1)while …

用Python的pynput庫成為按鍵記錄高手

哈嘍&#xff0c;大家好&#xff0c;我是木頭左&#xff01; 揭秘鍵盤輸入&#xff1a;pynput庫的基本介紹 無論是為了安全審計、數據分析還是創建熱鍵操作&#xff0c;能夠記錄和處理鍵盤事件都顯得尤為關鍵。這就是pynput庫發揮作用的地方。pynput是一個Python庫&#xff0c…

Java 對象序列化

序列化&#xff1a;把對象轉化為可傳輸的字節序列過程稱為序列化。 反序列化&#xff1a;把字節序列還原為對象的過程稱為反序列化 序列化的作用是方便存儲和傳輸&#xff0c;細節可參考如下文章&#xff1a; 序列化理解起來很簡單 - 知乎序列化的定義 序列化&#xff1a;把對…

echarts map地圖添加背景圖

給map地圖添加了一個陰影3d的效果&#xff0c;添加一張背景圖&#xff0c;給人感覺有3d的效果 具體配置如下&#xff1a; html代碼模塊&#xff1a; <div class"echart_img" style"position: fixed; visibility: hidden;"></div><div id&q…

Autoware內容學習與初步探索(一)

0. 簡介 之前作者主要是基于ROS2&#xff0c;CyberRT還有AutoSar等中間件完成搭建的。有一說一&#xff0c;這種從頭開發當然有從頭開發的好處&#xff0c;但是如果說絕大多數的公司還是基于現成的Apollo以及Autoware來完成的。這些現成的框架中也有很多非常好的方法。目前作者…

【Java的抽象類和接口】

1. 抽象類 1.1 抽象類概念 在面向對象的概念中&#xff0c;所有的對象都是通過類來描繪的&#xff0c;但是反過來&#xff0c;并不是所有的類都是用來描繪對象的&#xff0c;如果 一個類中沒有包含足夠的信息來描繪一個具體的對象&#xff0c;這樣的類就是抽象類。 以上代碼中…

Leaflet系列——【一】初識Leaflet與Leaflet視圖操作

初識Leaflet&#xff08;vue3 &#xff09; 前言&#xff1a;當你熟悉了openlayer、mapbox、cesium等一些GIS框架之后&#xff0c;對于我們開發來說其實他們的本質就是往瓦片上面疊加圖層、【點、線、面、瓦片、geoJson、熱力圖、圖片、svg等等】都是一層層的Layer圖層&#xf…

MySQL中的多表設計

由于業務之間的相互關聯&#xff0c;所以各個表結構之間也存在著各種聯系 基本分為三種&#xff1a; 一對多 多對多 一對一 外鍵語法 create table 表名&#xff08; 字段名 數據類型&#xff0c; ... [constraint] 外鍵名稱 foreign key &#xff08;外鍵字段名&#…