阿里C++二面面經
公眾號:阿Q技術站
來源:https://www.nowcoder.com/feed/main/detail/fc4a48403b534aafa6a6bce14b542c4e?sourceSSR=search
1、智能指針?
-
std::shared_ptr
:-
原理:
std::shared_ptr
是基于引用計數的智能指針,用于管理動態分配的對象。它維護一個引用計數,當計數為零時,釋放對象的內存。 -
使用場景:適用于多個智能指針需要共享同一塊內存的情況。例如,在多個對象之間共享某個資源或數據。
-
std::shared_ptr<int> sharedInt = std::make_shared<int>(42); std::shared_ptr<int> anotherSharedInt = sharedInt; // 共享同一塊內存
-
-
std::unique_ptr
:-
原理:
std::unique_ptr
是獨占式智能指針,意味著它獨占擁有所管理的對象,當其生命周期結束時,對象會被自動銷毀。 -
使用場景:適用于不需要多個指針共享同一塊內存的情況,即單一所有權。通常用于資源管理,例如動態分配的對象或文件句柄。
-
std::unique_ptr<int> uniqueInt = std::make_unique<int>(42); // uniqueInt 的所有權是唯一的
-
-
std::weak_ptr
:-
原理:
std::weak_ptr
是一種弱引用指針,它不增加引用計數。它通常用于協助std::shared_ptr
,以避免循環引用問題。 -
使用場景:適用于協助解決
std::shared_ptr
的循環引用問題,其中多個shared_ptr
互相引用,導致內存泄漏。 -
std::shared_ptr<int> sharedInt = std::make_shared<int>(42); std::weak_ptr<int> weakInt = sharedInt;
-
-
std::auto_ptr
(已廢棄):-
原理:
std::auto_ptr
是C++98標準引入的智能指針,用于獨占地管理對象。但由于其存在潛在的問題,已在C++11中被廢棄。 -
使用場景:在C++98標準中,可用于獨占性地管理動態分配的對象。不推薦在現代C++中使用。
-
std::auto_ptr<int> autoInt(new int(42)); // 已廢棄
-
2、棧和堆的區別?
- 分配方式:
- 棧:棧是一種自動分配和釋放內存的數據結構,它遵循"后進先出"(LIFO)原則。當你聲明一個局部變量時,該變量存儲在棧上。函數的參數和局部變量也存儲在棧上。棧的分配和釋放是自動的,由編譯器管理。
- 堆:堆是一種手動分配和釋放內存的數據結構。在堆上分配內存需要使用
new
或malloc
等函數,釋放內存則需要使用delete
或free
。堆上的內存不會自動釋放,必須手動管理。
- 存儲內容:
- 棧:棧主要存儲局部變量、函數參數和函數調用的上下文。它的存儲生命周期通常是有限的,當超出其作用域時,內存會自動釋放。
- 堆:堆主要用于存儲動態分配的對象和數據結構。它的存儲生命周期沒有那么明確,需要手動釋放。
- 生命周期:
- 棧:棧上的變量生命周期與其作用域(通常是一個函數的執行)相對應。一旦超出作用域,棧上的變量將自動銷毀。
- 堆:堆上的內存生命周期由程序員控制。在程序員顯式釋放內存之前,內存將一直存在。
- 分配速度:
- 棧:由于棧上的內存分配和釋放是自動管理的,通常比堆更快。
- 堆:堆上的內存分配和釋放需要較多的開銷,通常比較慢。
- 大小限制:
- 棧:棧的大小通常受到限制,因為它由操作系統管理,可以很小,通常在幾MB以內。
- 堆:堆的大小可以較大,受到系統資源的限制,通常比棧要大得多。
- 數據訪問:
- 棧:棧上的數據訪問速度較快,因為它是線性存儲,訪問局部變量通常只需要一次尋址操作。
- 堆:堆上的數據訪問速度較慢,因為它是散亂存儲,需要進行額外的尋址操作。
3、c++和c的不同?
- C是面向過程的語言,而C++是面向對象的語言。
- C和C++動態管理內存的方法不一樣,C是使用malloc/free函數,而C++除此之外還使用new/delete關鍵字。
- C++的類是C里沒有的,但是C中的struct是可以在C++中正常使用的,并且C++對struct進行了進一步的擴展,使得struct在C++中可以和class有一樣的作用。而唯一和class不同的地方在于struct成員默認訪問修飾符是public,而class默認的是private。
- C++支持重載,而C語言不支持。
- C++有引用,C沒有。
- C++全部變量的默認鏈接屬性是外鏈接,而C是內鏈接。
- C 中用const修飾的變量不可以用在定義數組時的大小,但是C++用const修飾的變量可以。
4、用const的目的?
- 防止修改變量的值: 將變量聲明為
const
后,編譯器會確保該變量的值在初始化后不能被修改。這有助于在程序中創建更加穩定和可維護的代碼。
const int maxAttempts = 3;
// maxAttempts = 4; // 錯誤,無法修改常量
- 指定函數參數為只讀: 在函數定義中,使用
const
可以指定某個參數是只讀的,防止在函數內部修改參數的值。
void printMessage(const std::string& message) {// message += "!"; // 錯誤,無法修改只讀參數std::cout << message << std::endl;
}
- 確保成員函數不修改對象狀態: 在成員函數聲明和定義中使用
const
關鍵字,可以確保該成員函數不會修改調用對象的狀態。這種方法被稱為常量成員函數。
class MyClass {
public:void modifyState(); // 普通成員函數void queryState() const; // 常量成員函數,不修改對象狀態
};
- 指定常量指針或常量引用: 在指針或引用聲明中使用
const
可以指定指針指向的對象是常量,或者引用的對象是常量。
const intptrToConst; // 指向常量的指針
int constconstPtr; // 同樣是指向常量的指針
- 避免不必要的拷貝: 在函數參數傳遞和返回值中使用
const
可以避免不必要的拷貝,提高性能。
5、指針和數組的區別?
- 概念
數組:存儲連續多個相同類型的數據;
指針:變量,存的是地址
- 賦值
同類型的指針變量可以相互賦值,數組不行,只能一個一個元素的賦值或拷貝
- 存儲方式
數組:連續內存空間。
指針:靈活,可以指向任意類型的數據。指向的是地址空間的內存。
- sizeof
數組的sizeof求的是占用的空間(字節)。
在32位平臺下,無論指針的類型是什么,sizeof(指針名)都是4;在64位平臺下,無論指針的類型是什么,sizeof(指針名)都是8。
- 傳參
作為參數時,數組名退化為常量指針。
6、重載和重寫的區別?
重載(Overloading):
- 定義:在同一個作用域內,允許存在多個同名的函數,但是這些函數的參數列表必須不同(包括參數的個數、類型、順序等)。
- 目的:通過相同的函數名來處理不同類型的參數,提高代碼的靈活性。
- 發生條件:函數名相同,但參數列表不同。
int add(int a, int b) {return a + b;
}double add(double a, double b) {return a + b;
}
重寫(Overriding):
- 定義: 在派生類中重新實現(覆蓋)其基類的虛函數。發生在繼承關系中,子類重新定義基類的虛函數,實現子類自己的版本。
- 目的: 支持多態性,允許基類的指針或引用在運行時指向派生類對象,并調用相應的派生類函數。
- 發生條件: 子類繼承自父類,子類中的函數與父類中的虛函數具有相同的函數簽名。
class Shape {
public:virtual void draw() const {// 具體的實現}
};class Circle : public Shape {
public:void draw() const override {// Circle 版本的實現,覆蓋了基類的虛函數}
};
總結:
- 重載是指在同一作用域中定義多個同名函數,通過參數列表的不同來區分;
- 重寫是指派生類重新實現(覆蓋)其基類的虛函數,以支持多態性。
7、定義指針時要注意的問題?
- 初始化:指針在定義時最好立即初始化,可以為其賦予
nullptr
(C++11 及以上)或NULL
,或者指向有效的內存地址。未初始化的指針具有不確定的值。
int* ptr = nullptr; // 推薦使用 nullptr 初始化指針
- 懸空指針:當指針指向的內存被釋放后,如果不將指針置為
nullptr
,該指針就成了懸空指針。使用懸空指針可能導致未定義行為。
int* ptr = new int;
delete ptr;
// ptr 現在是懸空指針
- 野指針: 指針指向未知的內存地址,可能是未初始化的指針或者指向已釋放的內存。使用野指針可能導致程序崩潰或不可預測的行為。
int* ptr; // 未初始化的指針
*ptr = 42; // 野指針
- 空指針解引用:嘗試解引用空指針會導致未定義行為。在解引用指針之前,應該確保指針不為
nullptr
。
int* ptr = nullptr;
// *ptr; // 錯誤,解引用空指針
- 指針的生命周期:指針在超出其作用域后不再有效,但如果指針指向的是動態分配的內存,需要手動釋放以防止內存泄漏。
void foo() {int* ptr = new int;// 使用 ptrdelete ptr; // 釋放動態分配的內存
} // ptr 超出作用域,但內存已經釋放
- 指向棧上的內存:當指針指向棧上的內存時,應該確保在指針超出作用域之前,該內存仍然有效。
int* func() {int x = 42;return &x; // 錯誤,返回指向棧上的內存地址
} // x 超出作用域,指向的內存已經無效
- 空指針與野指針: 空指針(
nullptr
)表示指針不指向任何有效的內存地址,而野指針是指指針的值是一個不確定的地址。合理使用空指針,并盡量避免野指針。
8、c++內存分配?
- 棧區(Stack):用于存儲局部變量和函數調用的信息。棧是一種后進先出(LIFO)的數據結構。每當進入一個新的函數,系統會為其分配一個棧幀,用于存儲局部變量、參數和函數調用的返回地址等信息。當函數執行完成,對應的棧幀會被銷毀。
- 堆區(Heap):用于動態分配內存。程序員通過
new
運算符從堆上分配內存,通過delete
運算符釋放堆上的內存。堆上的內存分配和釋放需要程序員手動管理,確保在不再使用時及時釋放,以防止內存泄漏。 - 全局區/靜態區(Global/Static Area):用于存儲全局變量和靜態變量。全局變量存儲在全局數據區,靜態變量存儲在靜態數據區。這些變量在程序啟動時被分配,直到程序結束時才會釋放。
- 常量區(Constant Area):用于存儲常量字符串和全局常量。這部分內存是只讀的,程序運行期間不能修改。
- 代碼區(Code Area):用于存儲程序的執行代碼。在程序運行時,代碼區是只讀的。
9、new/delete和malloc/free的聯系及區別?
new
和 delete
是 C++ 中用于動態內存分配和釋放的運算符,而 malloc
和 free
是 C 語言中對應的庫函數。
聯系:
- 目的相同:
new
和malloc
都用于在堆上動態分配內存,而delete
和free
用于釋放動態分配的內存。 - 使用方式:
new
和delete
是 C++ 中的運算符,可以直接使用,而malloc
和free
是 C 語言中的庫函數,需要包含頭文件<cstdlib>
。
區別:
- 類型安全:
new
和delete
是類型安全的,它們會調用對象的構造函數和析構函數。malloc
和free
是基于void*
,不會調用構造和析構函數,因此不是類型安全的。 - 大小參數:
new
和delete
不需要顯式指定要分配的內存大小,它們會根據類型自動計算。而malloc
和free
需要顯式指定分配或釋放的內存大小。 - 操作對象:
new
和delete
主要用于操作對象,而malloc
和free
可以用于分配任意大小的內存塊。 - 對NULL的處理:
new
在分配失敗時會拋出std::bad_alloc
異常,而malloc
在分配失敗時返回NULL
。 - 適用范圍:
new
和delete
是 C++ 中的運算符,而malloc
和free
是 C 標準庫中的函數。在 C++ 中,推薦使用new
和delete
,因為它們更符合面向對象的編程思想。
10、c++是類型安全的語言嗎(面試官提到了動態聯編和靜態聯編)?
C++ 是一種相對而言更加類型安全的編程語言。類型安全是指在編譯時和運行時,程序對數據類型的使用都是合法的,不會發生未定義行為。C++ 在設計上考慮了類型安全,并提供了一些機制來減少類型相關的錯誤。
- 靜態聯編(Static Binding):在編譯階段,編譯器將函數調用與具體的函數實現關聯起來,這被稱為靜態聯編。C++ 是靜態類型語言,因此大部分的聯編工作在編譯時完成。這有助于在編譯期發現一些類型相關的錯誤,提高了類型安全性。
- 動態聯編(Dynamic Binding):在運行時,通過虛函數和多態性實現動態聯編。C++ 支持運行時多態,允許在父類的指針或引用上調用子類的虛函數。這種機制在一定程度上提高了靈活性,但也引入了動態聯編的概念。
- 強類型:C++ 是一種強類型的語言,即在編譯時對類型的檢查比較嚴格,不同類型之間的操作需要進行明確的類型轉換。
- 靜態類型檢查:C++ 是一種靜態類型檢查語言,這意味著變量的類型在編譯時就已經確定,不會發生隱式的類型轉換錯誤。
- 面向對象的封裝:C++ 支持面向對象編程,通過類的封裝特性可以將數據和操作封裝在一起,防止未授權的訪問和修改。
- 模板和泛型編程:C++ 提供了模板和泛型編程的支持,允許程序員編寫與類型無關的代碼,提高了代碼的通用性和類型安全性。
11、main函數前會有其他函數語句被執行嗎? 在標準的 C++ 程序中,main
函數是程序的入口點,程序從main
函數開始執行。在main
函數執行之前,不會有其他普通函數被自動調用。然而,有一些特殊情況可能導致main
函數執行前調用其他函數或執行其他代碼。
- 全局對象的構造:在 C++ 中,全局變量和靜態變量的構造函數會在
main
函數執行之前調用。這意味著如果你有全局對象,它們的構造函數將在main
函數執行前執行。
#include <iostream>class GlobalObject {
public:GlobalObject() {std::cout << "GlobalObject constructed!" << std::endl;}
};GlobalObject globalVar; // 全局變量,構造函數會在 main 函數執行前調用int main() {std::cout << "Inside main function!" << std::endl;return 0;
}
例子中,GlobalObject
類的構造函數會在 main
函數執行前被調用。
- 特殊初始化函數:在一些特殊的嵌入式系統或特定編譯器中,可能存在一些特殊的初始化函數,這些函數可能在
main
函數之前執行。
12、虛函數實現?
- 虛函數表(vtable):對于每個包含虛函數的類,編譯器會在該類的對象中添加一個指向虛函數表的指針。虛函數表是一個數組,其中存儲了類的虛函數的地址。每個類有一個對應的虛函數表。
- 虛函數指針(vptr):對象中的虛函數指針指向虛函數表。在對象的構造過程中,虛函數指針被設置為指向類的虛函數表。
- 動態綁定:當通過基類指針或引用調用虛函數時,實際調用的是對象的實際類型的虛函數。這種調用方式被稱為動態綁定。編譯器通過虛函數指針找到對象的虛函數表,然后在表中查找對應虛函數的地址。
看個例子:
#include <iostream>class Base {
public:virtual void show() {std::cout << "Base::show()" << std::endl;}
};class Derived : public Base {
public:void show() override {std::cout << "Derived::show()" << std::endl;}
};int main() {Base baseObj;Derived derivedObj;Base* basePtr = &baseObj;Base* derivedPtr = &derivedObj;// 調用虛函數,實際執行 Derived::show()basePtr->show();derivedPtr->show();return 0;
}
例子中,Base
類有一個虛函數 show
,而 Derived
類覆蓋了這個虛函數。在 main
函數中,通過基類指針調用虛函數,實際執行的是對象的實際類型的虛函數。這就是虛函數實現動態綁定的基本原理。
13、TLS握手?
- 客戶端向服務端發起第一次握手請求,告訴服務端客戶端所支持的SSL的指定版本、加密算法及密鑰長度等信息。
- 服務端將自己的公鑰發給數字證書認證機構,數字證書認證機構利用自己的私鑰對服務器的公鑰進行數字簽名,并給服務器頒發公鑰證書。
- 服務端將證書發給客戶端。
- 客服端利用數字認證機構的公鑰,向數字證書認證機構驗證公鑰證書上的數字簽名,確認服務器公開密鑰的真實性。
- 客戶端使用服務端的公開密鑰加密自己生成的對稱密鑰,發給服務端。
- 服務端收到后利用私鑰解密信息,獲得客戶端發來的對稱密鑰。
- 通信雙方可用對稱密鑰來加密解密信息。
14、手撕算法冒泡排序
基本思想是通過重復遍歷要排序的數列,一次比較兩個元素,如果它們的順序錯誤就交換它們,直到沒有需要交換的元素為止。
思路:
- 從第一個元素開始,依次比較相鄰的兩個元素。
- 如果順序不對,就交換這兩個元素的位置。
- 繼續遍歷整個數組,執行相同的操作。
- 一輪遍歷結束后,最大的元素就會沉到數組末尾。
- 重復上述步驟,但不包括已經排序好的元素,直到整個數組有序。
參考代碼:
#include <iostream>
#include <vector>void bubbleSort(std::vector<int>& arr) {int n = arr.size();for (int i = 0; i < n - 1; ++i) {// 每一輪遍歷,把最大的元素放到末尾for (int j = 0; j < n - i - 1; ++j) {// 如果前面的元素比后面的大,交換它們的位置if (arr[j] > arr[j + 1]) {std::swap(arr[j], arr[j + 1]);}}}
}int main() {// 測試數據std::vector<int> arr = {64, 34, 25, 12, 22, 11, 90};// 打印排序前的數組std::cout << "排序前的數組:";for (int num : arr) {std::cout << num << " ";}// 調用冒泡排序函數bubbleSort(arr);// 打印排序后的數組std::cout << "\n排序后的數組:";for (int num : arr) {std::cout << num << " ";}return 0;
}
了解了一些密碼學的相關知識,后面就屬于聊天了。面試官依舊人很好,給予了很積極的反饋,說了一下筆試的問題,給了一些建議。