1. 面向對象的三大特征
(1)封裝:隱藏對象的內部狀態,只暴露必要的接口。
#include <iostream>
#include <string>// 定義一個簡單的類 Person
class Person {
private: // 私有成員,外部不可直接訪問std::string name;int age;public: // 公共方法,外部可以訪問// 構造函數,用于初始化對象Person(std::string n, int a) {name = n;age = a;}// 公共方法,用于設置年齡void setAge(int a) {if (a > 0 && a < 150) { // 簡單的年齡驗證age = a;} else {std::cout << "Invalid age!" << std::endl;}}// 公共方法,用于獲取姓名std::string getName() {return name;}// 公共方法,用于獲取年齡int getAge() {return age;}
};int main() {// 創建 Person 對象Person p1("Alice", 30);// 嘗試直接訪問私有成員(編譯錯誤)// std::cout << p1.name << std::endl;// 使用公共方法設置年齡p1.setAge(35);// 使用公共方法獲取姓名和年齡std::cout << "Name: " << p1.getName() << ", Age: " << p1.getAge() << std::endl;return 0;
}
(2)繼承:無需修改原有類的情況下對功能實現拓展
#include <iostream>
#include <string>// 定義一個基類 Animal
class Animal {
protected: // 受保護的成員,子類可以訪問std::string name;public:// 構造函數,初始化 Animal 的名稱Animal(std::string n) : name(n) {}// 公共方法,用于輸出 Animal 的聲音void makeSound() const {std::cout << "Animal " << name << " makes a sound" << std::endl;}
};// 定義一個派生類 Dog,繼承自 Animal
class Dog : public Animal {
private:std::string breed;public:// 構造函數,初始化 Dog 的名稱和品種Dog(std::string n, std::string b) : Animal(n), breed(b) {}// 重寫父類的 makeSound 方法void makeSound() const {std::cout << "Dog " << name << " barks" << std::endl;}// 新的方法,用于輸出 Dog 的品種void showBreed() const {std::cout << "Dog " << name << " is of breed " << breed << std::endl;}
};int main() {// 創建 Animal 對象Animal a("Generic");// 調用 Animal 的方法a.makeSound();// 創建 Dog 對象Dog d("Buddy", "Labrador");// 調用 Dog 的方法d.makeSound();d.showBreed();// 使用基類指針指向派生類對象,演示多態Animal* animalPtr = &d;animalPtr->makeSound(); // 調用的是 Dog 的 makeSound 方法return 0;
}
(3)多態:同一個函數名在不同對象中有不同的行為。通過時下接口重用增強可拓展性
多態分為2類,靜態多態和動態多態。
靜態多態:又稱編譯時多態。通過函數重載和運算符重載實現。
動態多態:又稱運行時多態,通過虛函數和繼承實現。
虛函數:在基類中聲明函數為虛函數,派生類可以覆蓋這些虛函數,從而實現不同的行為。在運行時,根據對象的實際類型決定調用哪個版本的函數,這種機制稱為動態綁定或后期綁定。
// 靜態多態
#include <iostream>class MathTool {
public:// 計算整數平方int calculateSquare(int number) {return number * number;}// 計算浮點數平方double calculateSquare(double number) {return number * number;}
};int main() {MathTool tool;// 靜態多態示例:根據參數類型自動選擇合適的方法std::cout << "整數5的平方是: " << tool.calculateSquare(5) << std::endl; // 調用整數版本std::cout << "浮點數2.5的平方是: " << tool.calculateSquare(2.5) << std::endl; // 調用浮點數版本return 0;
}// 動態多態
#include <iostream>// 基類,含有虛函數
class Animal {
public:virtual ~Animal() {} // 虛析構函數,確保通過基類指針刪除派生類對象時能正確調用派生類的析構函數virtual void makeSound() {std::cout << "Some animal makes a sound." << std::endl;}
};// 派生類1
class Dog : public Animal {
public:void makeSound() override {std::cout << "Dog barks." << std::endl;}
};// 派生類2
class Cat : public Animal {
public:void makeSound() override {std::cout << "Cat meows." << std::endl;}
};int main() {Animal* animalPtr; // 基類指針// 動態分配內存給派生類對象animalPtr = new Dog();animalPtr->makeSound(); // 運行時動態決定調用Dog的makeSound()animalPtr = new Cat();animalPtr->makeSound(); // 運行時動態決定調用Cat的makeSound()delete animalPtr; // 由于基類析構函數為虛函數,可以安全刪除派生類對象return 0;
}
2. 多態的實現原理
(1)靜態多態
原理:函數名修飾。編輯器會根據函數的簽名(包括參數類型、數量和順序)對內部的函數名稱進行編碼,生成一個唯一的標識符。這個編碼后的名稱包含了足夠的信息,使得編譯器能夠準確地區分不同的重載版本,即使它們的外部名稱相同。
編譯過程:
????????預編譯:把頭文件中的函數聲明拷貝到源文件,避免編譯過程中語法分析找不到函數定義。
? ? ? ? 編譯:語法分析,同時進行符號匯總
? ? ? ? 匯編:生成函數名到函數地址的映射,方便之后通過函數名照到函數定義從而執行函數。
? ? ? ? 鏈接:講過個文件的符號表匯總合并
早綁定:編譯器編譯時就已經確定對象調用的函數的地址。靜態多態依賴于早綁定。
(2)動態多態
原理:虛函數重寫。當一個類中聲明了至少一個虛函數時,編譯器會為該類生成一個虛函數表。這是一個存儲虛函數指針的數組,每個指針指向類中相應虛函數的實現。含有虛函數的類實例在內存中除了包含數據成員外,還會有一個指向其所屬類的虛函數表的指針(通常稱為vptr)。這個vptr是在對象創建時由編譯器自動初始化的。在派生類中,當你定義了一個與基類中虛函數同名且簽名相同的函數時,這就是虛函數的重寫。派生類的虛函數表中,對應的條目會存儲派生類中該函數的地址,而非基類的。
虛函數重寫:基類函數上加上virtual關鍵字,在派生類重寫虛函數。運行時會根據對象的類型調用相應的函數。如果對象的類型是基類,那么調用基類的函數,如果對象的類型是派生類誒,則調用派生類的函數
晚綁定:程序運行時才確定對象調用的函數的地址。動態多態依賴于晚綁定。C++中,晚綁定是通過virtual
關鍵字實現的。
3. 怎么解決菱形繼承
因為c++有多重繼承的特性,導致一個子類可能繼承多個基類。這些基類可能繼承自相同的基類,從而造成了菱形繼承。
菱形繼承的問題主要是數據冗余并且造成二義性。
二義性:當一個派生類(我們稱之為D
)從兩個不同的基類(比如B1
和B2
)繼承,而這兩個基類又都繼承自同一個基類(A
),那么在D
中直接訪問從A
繼承來的成員時,編譯器無法確定應該使用B1
繼承的版本還是B2
繼承的版本。這種情況下,編譯器會報錯,指出存在二義性。
數據冗余:如果不使用虛擬繼承(virtual
關鍵字),每個派生類(B1
和B2
)都會包含基類A
的一個完整副本。因此,當D
繼承自B1
和B2
時,它將擁有兩份A
的成員,導致數據冗余。這不僅浪費存儲空間,還可能導致邏輯上的混亂,尤其是在修改這些成員時,可能會忘記更新所有副本,從而引發一致性問題。
使用虛繼承可以解決菱形繼承問題。在派生類對共同基類進行虛繼承時,編譯器會確保只有一份共同基類的實例,而不是每個中間類都有自己的一份。
4. 關鍵字override、final的作用
子類繼承基類的虛函數之后,可能存在這樣的問題:子類不希望這個函數會自己的子類被進一步重寫;子類想重寫一個新函數,但是錯誤的重寫了基類虛函數;子類本意是重寫基類的虛函數但是簽名不一致導致重新構建了一個虛函數;子類希望自己絕后!但是沒有辦法從語法上完成這件事。
override可以指定子類的一個虛函數復寫基類的一個虛函數;并且保證該重寫的虛函數與子類的虛函數有相同的簽名。
final可以指定某個虛函數不能在派生類中覆蓋,或者某個類不能被派生。加在類前面就可以阻塞類的進一步派生。
5. C++類型推導的用法
C++類型推導主要有三個方面:auto、decltype、模板類型推導
(1)auto:用于推導變量的類型,通過強制聲明一個變量的初始值,編譯器會通過初始值進行推導類型
(2)?decltype
用于推導表達式的類型,編譯器只分析表達式類型而不參與運算
?6. C++11 中function、lambda、bind之間的關系
std::function是一個抽象了函數參數和函數返回值的類模板。它把任意函數包裝成了一個對象,該對象可以保存傳遞以及復制。也可以進行動態綁定,只需要修改該對象,實現類似多態的效果
#include <iostream>
#include <functional>// 函數對象(仿函數)
struct Adder {int operator()(int a, int b) const {return a + b;}
};// 普通函數
int subtract(int a, int b) {return a - b;
}// 成員函數
struct Calculator {int multiply(int a, int b) {return a * b;}
};int main() {// 使用 std::function 聲明不同類型的可調用對象std::function<int(int, int)> func1 = Adder(); // 函數對象std::function<int(int, int)> func2 = subtract; // 普通函數Calculator calc;std::function<int(Calculator&, int, int)> func3 = &Calculator::multiply; // 成員函數// 調用并輸出結果std::cout << "Adder result: " << func1(10, 5) << std::endl;std::cout << "Subtract result: " << func2(10, 5) << std::endl;std::cout << "Multiply result: " << func3(calc, 10, 5) << std::endl;return 0;
}
lambda表達式:一種方面的創建匿名函數的語法糖
原理:編譯的時候把表達式轉變為一個函數對象,然后根據表達式慘呼列表重載operate ()
demo:
// 簡單表達式
auto sayHello = []{ std::cout << "Hello, World!\n"; };
sayHello();// 捕獲外部變量
int x = 10, y = 20;
auto incrementAndPrint = [x, &y]() {x++; // 按值捕獲,不會改變外部xy++; // 按引用捕獲,會改變外部ystd::cout << "x: " << x << ", y: " << y << "\n";
};
incrementAndPrint(); // 輸出:x: 11, y: 21
std::cout << "After: x=" << x << ", y=" << y << "\n"; // 輸出:x=10, y=21// 指定返回類型
int x = 10, y = 20;
auto incrementAndPrint = [x, &y]() {x++; // 按值捕獲,不會改變外部xy++; // 按引用捕獲,會改變外部ystd::cout << "x: " << x << ", y: " << y << "\n";
};
incrementAndPrint(); // 輸出:x: 11, y: 21
std::cout << "After: x=" << x << ", y=" << y << "\n"; // 輸出:x=10, y=21
std::bind:用來通過綁定函數以及函數參數的方式生成函數對象的模板函數,提供占位符,實現靈活的參數綁定
#include <iostream>
#include <functional>// 通用加法函數
int add(int x, int y) {return x + y;
}int main() {// 使用std::bind固定add的第一個參數為5auto addFive = std::bind(add, 5, std::placeholders::_1);// 現在addFive是一個新的可調用對象,只需要一個參數std::cout << "Adding 5 to 3 gives: " << addFive(3) << '\n'; // 輸出: 8std::cout << "Adding 5 to 10 gives: " << addFive(10) << '\n'; // 輸出: 15return 0;
}
總結:function 用來描述函數對象的類型;lambda 表達式用來生成函數對象(可以訪問外部變量的匿名函數);bind 也是用來生成函數對象(函數和參數進行綁定生成函數對象);
這是一條吃飯博客,由挨踢零聲贊助。學C/C++就找挨踢零聲,加入挨踢零聲,面試不挨踢!