22屆校招春招 c++開發 面試遇到的一些問題
c / c++面試筆試
1.c++的知道哪些C語言字串拷貝的方法
(1) 第一個strcpy()
使用標頭檔案:#include <string.h>
定義:char *strcpy(char *dest, const char *src); 引數: destinin:目標字元陣列; source:源字元陣列; 函式說明: strcpy()會將引數src 字串拷貝至引數dest 所指的地址。 用於對字串進行復制,識別到字串的結束符號‘\0’自動停止 返回值: 返回引數dest 的字串起始地址。 注意: 引數 dest 的記憶體空間要足夠大,否則拷貝可能會造成緩衝溢位。 strcpy() 在複製結束後會新增結束符\0,這點和strncpy()不同
(2) memcpy()
使用標頭檔案:C語言:#include <string.h> C++:#include<cstring>
定義:void memcpy(void *dest, const void *src, size_t n);
引數:
destinin:目標地址;
source:源地址;
n:複製的位元組長度。
函式說明:
memcpy()複製 src 所指的記憶體資料的 n 個位元組到 dest所指的記憶體地址上。也就是從源地址複製n 個位元組到目標地址
第一個和第二個指標都是void型且第二個指標不能被修改,第三個引數是需要拷貝的記憶體長度按位元組記。
返回值:返回指向 dest 的指標。返回的指標型別是void。
注意:
memcpy()並不限制被複制的資料型別,只是逐位元組地進行復制,任何資料型別都可以進行復制,例如字元陣列、整型、結構體、類等
memcpy() 會完整的複製 num個位元組,不會遇到‘\0’而結束,這點與 strcpy() 不同
dest 和 src所指的記憶體空間地址不能重疊
引數 dest 的記憶體空間要足夠大,起碼要大於等於 num個位元組
通常在複製字串時用strcpy,而需要複製其他型別資料時則一般用memcpy
(3)strncpy()
使用標頭檔案:#include <string.h> 定義:char *strncpy(char *dest, const char *src, size_t len); 引數: destinin:目標字元陣列; source:源字元陣列; len:複製的字串長度。 函式說明:strncpy()複製字串 src 的前 len 個位元組到 dest所指的記憶體地址上。 返回值:返回字串dest
注意:
-
strncpy()在複製結束後不會向dest結尾新增’\0’結束符 這個是很重要的一個點,要記住
-
如果source(源字元陣列)的長度>複製的字串數len,則只複製source(源字元陣列)的前len個字元,不會自動新增結束符\0
-
如果source(源字元陣列)的長度<複製的字串數len,則以NULL填充dest(目標字元陣列),直到複製完n個位元組
-
引數 dest 的記憶體空間要足夠大,起碼要大於等於 num個位元組
-
在使用strncpy()的時候,拷貝長度最好為strlen(src)+1,以保證最後的結束符\0也能被複制
(3) memmove()
使用標頭檔案:#include <string.h>
定義:void *memmove( void* dest, const void* src, size_t count );
引數:
destinin:目標地址;
source:源地址;
count:複製的位元組長度。
函式說明:memmove()複製 src 所指的記憶體資料的 n 個位元組到 dest所指的記憶體地址上。也就是從源地址複製n 個位元組到目標地址。
如果目標區域和源區域有重疊的話,memmove能夠保證源串在被覆蓋之前將重疊區域的位元組拷貝到目標區域中,但複製後源內容會被更改。但是當目標區域與源區域沒有重疊則和memcpy函式功能相同。
2.new 和 malloc的區別
(1). 申請的記憶體所在位置
new操作符從自由儲存區(free store)上為物件動態分配記憶體空間,
malloc函式從堆上動態分配記憶體。
自由儲存區是C++基於new操作符的一個抽象概念,凡是通過new操作符進行記憶體申請,該記憶體即為自由儲存區。而堆是作業系統中的術語,是作業系統所維護的一塊特殊記憶體,用於程式的記憶體動態分配,C語言使用malloc從堆上分配記憶體,使用free釋放已分配的對應記憶體。
那麼自由儲存區是否能夠是堆(問題等價於new是否能在堆上動態分配記憶體),這取決於operator new 的實現細節。自由儲存區不僅可以是堆,還可以是靜態儲存區,這都看operator new在哪裡為物件分配記憶體。
(2).返回型別安全性
new操作符記憶體分配成功時,返回的是物件型別的指標,型別嚴格與物件匹配,無須進行型別轉換,故new是符合型別安全性的操作符。
而malloc記憶體分配成功則是返回void 星 ,需要通過強制型別轉換將void*指標轉換成我們需要的型別。
(3).記憶體分配失敗時的返回值
new記憶體分配失敗時,會丟擲bac_alloc異常,它不會返回NULL;malloc分配記憶體失敗時返回NULL。
try
{
int *a = new int();
}
catch (bad_alloc)
{
...
}
如果你想順便了解下異常基礎,可以看http://www.cnblogs.com/QG-whz/p/5136883.html C++ 異常機制分析。
(4).是否需要指定記憶體大小
使用new操作符申請記憶體分配時無須指定記憶體塊的大小,編譯器會根據型別資訊自行計算,而malloc則需要顯式地指出所需記憶體的尺寸。
(5).是否呼叫建構函式/解構函式
使用new操作符來分配物件記憶體時會經歷三個步驟:
第一步:呼叫operator new 函式(對於陣列是operator new[])分配一塊足夠大的,原始的,未命名的記憶體空間以便儲存特定型別的物件。
第二步:編譯器執行相應的建構函式以構造物件,併為其傳入初值。
第三部:物件構造完成後,返回一個指向該物件的指標。
使用delete操作符來釋放物件記憶體時會經歷兩個步驟:
第一步:呼叫物件的解構函式。
第二步:編譯器呼叫operator delete(或operator delete[])函式釋放記憶體空間。
總之來說,new/delete會呼叫物件的建構函式/解構函式以完成物件的構造/析構。而malloc則不會。
(6).對陣列的處理
new對陣列的支援體現在它會分別呼叫建構函式函式初始化每一個數組元素,釋放物件時為每個物件呼叫解構函式。
-
注意delete[]要與new[]配套使用,不然會找出陣列物件部分釋放的現象,造成記憶體洩漏。
-
至於malloc,它並知道你在這塊記憶體上要放的陣列還是啥別的東西,反正它就給你一塊原始的記憶體,在給你個記憶體的地址就完事。所以如果要動態分配一個數組的記憶體,還需要我們手動自定陣列的大小:
(7).new與malloc是否可以相互呼叫
operator new /operator delete的實現可以基於malloc,而malloc的實現不可以去呼叫new。下面是編寫operator new /operator delete 的一種簡單方式,其他版本也與之類似:
(8).是否可以被過載
opeartor new /operator delete可以被過載。標準庫是定義了operator new函式和operator delete函式的8個過載版本:而malloc/free並不允許過載。
(9). 能夠直觀地重新分配記憶體
使用malloc分配的記憶體後,如果在使用過程中發現記憶體不足,可以使用realloc函式進行記憶體重新分配實現記憶體的擴充。realloc先判斷當前的指標所指記憶體是否有足夠的連續空間,如果有,原地擴大可分配的記憶體地址,並且返回原來的地址指標;如果空間不夠,先按照新指定的大小分配空間,將原有資料從頭到尾拷貝到新分配的記憶體區域,而後釋放原來的記憶體區域。
new沒有這樣直觀的配套設施來擴充記憶體。
(10). 客戶處理記憶體分配不足
在operator new丟擲異常以反映一個未獲得滿足的需求之前,它會先呼叫一個使用者指定的錯誤處理函式,這就是new-handler。對於malloc,客戶並不能夠去程式設計決定記憶體不足以分配時要幹什麼事,只能看著malloc返回NULL。
3. unix 的初始化和啟動
(1)unix系統的初始引導過程
unix從硬體加電到-->ROM BIOS(檢查硬體系統的配置,找到硬碟0號扇區)-->將0號扇區讀入記憶體
(2)unix系統的初始化
ROM BIOS --> mboot/pboot -->boot -->stand/unix -->init0 -->init1
init -- RC.sysint -- rc.x --login
在/etc/inittab 中獲取initdefault值進入預設的執行級
Unix的8個執行級
0 停止作業系統
1 單使用者
2 多使用者
3 加網路
4 多使用者配置環境
5韌體診斷,關閉所有服務,關機
6 重啟
S/s 安裝、root目錄外 其他目錄不安裝,執行系統內部程式,單使用者
Abc 偽執行級 執行使用者自定義出的程式系統的執行級不會發生變化
/etc/RC N N為上述1~6
-
檢查修復root檔案系統
-
設定內部時鐘
-
清理和安裝所有檔案系統
-
清理臨時檔案 /tnp和/var/tmp
-
設定網路埠、介面、路由
-
啟動日誌檔案排程程序syslogd
-
後臺作業排程程式cron
-
啟動必要的終端服務程序或應用特定的服務程序
-
使用者註冊程式
(3)unix的啟動順序
通過/boot/vm進行啟動 vmlinuz
init /etc/inittab
啟動相應的指令碼,並且開啟終端
rc.sysinit
rc.d(裡面的指令碼)
rc.local
啟動login登入介面 login
在使用者登入的時候執行sh指令碼的順序(每次登入的時候都會完全執行的)
/etc/profile.d/file
/etc/profile
/etc/bashrc
/root/.bashrc
/root/.bash_profile
4. linux系統初始化和啟動服務
linux啟動服務詳見:https://blog.csdn.net/chinaren0001/article/details/6901454?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_title~default-0.queryctrv2&spm=1001.2101.3001.4242.1&utm_relevant_index=3
Linux 系統啟動過程
linux啟動時我們會看到許多啟動資訊。
Linux系統的啟動過程並不是大家想象中的那麼複雜,其過程可以分為5個階段:
作業系統-->
/boot(MBR)-->
init程序-->
執行級別(在/etc/inittab檔案中)-->
初始化系統(/etc/RC.d/RC.sysint) -->
/etc/rc.d/rc x.d(根據載入的run level值載入對應目錄的程式,決定開啟哪些服務)-->
/sbin/minggetty(啟動六個虛擬控制檯)-->
使用者登入系統(login)
核心的引導。
執行init。
系統初始化。
建立終端 。
使用者登入系統。
(1) BIOS-->MBR-->KERNEL-->INIT程序(一切程序的起點)
-
當 電腦一開啟 電源時電腦就會進入BIOS(BIOS的工作主要是檢測一些硬體裝置);
-
檢測完後會進入MBR也就是boot loader(MBR位於硬碟的第一個扇區總共512bytes,其中前446bytes裡面的編碼是在選擇引導分割槽也就是決定要由哪個分割槽來引導);
-
載入系統的Kernel(核心),在Kernel裡主要是載入電腦裝置的驅動程式,以便可以控制電腦上的裝置,並且以只讀方式來掛載根目錄,也就是一 開始只能讀取到根目錄所對應的那個分割槽,所以/etc、/bin、/sbin、/dev、/lib這五個目錄必須同根目錄在一個分割槽中;
-
最後啟動init這個程式,所以init這個程式的程序編號為1,是Linux中第一個執行的程式;
init這個程式會根據 Run level來執行以下這些程式:
·/etc/rc.d/rc.sysinit;
·/etc/rc.d/rc 和 etc/rc.d/rc?.d/
·/etc/rc.d/rc.local
(2) 介紹 /etc/rc.d/rc.sysinit 這個程式主要做哪些工作(init初始化流程第二步)
- 啟動 udev ,也就是啟用熱插拔的裝置,例如:USB,並且也會啟動SELinux;
- 會把kernel的引數設定在/etc/sysctl.conf配置檔案裡;這個配置檔案下下單元詳細說明;
- 設定系統時間;
- 載入 keymaps 設定, keymap設定是在定義 鍵盤,這樣電腦開機時才能找到相對應的鍵盤設定;
- 啟用swap這個虛擬記憶體的分割槽;
- 設定主機名稱,主機名稱設定在 /etc/sysconfig/network 配置檔案中的 HOSTNAME= 項下;
- 檢查根目錄有沒有問題,並且重新掛載成為可讀可寫的狀態;
- 啟用RAID磁碟陣列,以及LVM的裝置;
- 啟用磁碟配額的功能,就是限制使用者最多可以使用多少硬碟空間;
- 檢查其它的檔案系統,並且把它們掛載進來;
- 最後會清除被修改過的locks及PID files,其實就是清除一些開機時的快取檔案,以及一些沒有用的資訊及檔案;
5.三次握手 和四次揮手
6.作業系統記憶體管理
分段 分頁 段頁式
7.new 的底層實現
operator_num malloc
8.stl的vector 和 map/set 無序的map怎麼排序
key 和 value 對鍵值和value的排序通過修改compare的函式
9. 多執行緒 的同步方式,多程序的 通訊方式
(1) 程序間通訊:
-
管道( pipe ):管道是一種半雙工的通訊方式,資料只能單向流動,而且只能在具有親緣關係的程序間使用。程序的親緣關係通常是指父子程序關係。
-
有名管道 (namedpipe) : 有名管道也是半雙工的通訊方式,但是它允許無親緣關係程序間的通訊。
-
高階管道(popen):將另一個程式當做一個新的程序在當前程式程序中啟動,則它算是當前程式的子程序,這種方式我們成為高階管道方式。
-
訊號量( semophore ) :訊號量是一個計數器,可以用來控制多個程序對共享資源的訪問。它常作為一種鎖機制,防止某程序正在訪問共享資源時,其他程序也訪問該資源。因此,主要作為程序間以及同一程序內不同執行緒之間的同步手段。
-
訊息佇列( messagequeue ) : 訊息佇列是由訊息的連結串列,存放在核心中並由訊息佇列識別符號標識。訊息佇列克服了訊號傳遞資訊少、管道只能承載無格式位元組流以及緩衝區大小受限等缺點。
-
訊號 ( sinal ) :訊號是一種比較複雜的通訊方式,用於通知接收程序某個事件已經發生。
-
共享記憶體( sharedmemory ) :共享記憶體就是對映一段能被其他程序所訪問的記憶體,這段共享記憶體由一個程序建立,但多個程序都可以訪問。共享記憶體是最快的 IPC 方式,它是針對其他程序間通訊方式執行效率低而專門設計的。它往往與其他通訊機制,如訊號兩,配合使用,來實現程序間的同步和通訊。
-
套接字( socket ) : 套解口也是一種程序間通訊機制,與其他通訊機制不同的是,它可用於不同及其間的程序通訊。
注意:臨界區則是一種概念,指的是訪問公共資源的程式片段,並不是一種通訊方式。
(2) 執行緒通訊:
- 互斥鎖提供了以排他方式防止資料結構被併發修改的方法。
- 讀寫鎖允許多個執行緒同時讀共享資料,而對寫操作是互斥的。
- 條件變數可以以原子的方式阻塞程序,直到某個特定條件為真為止。對條件的測試是在互斥鎖的保護下進行的。條件變數始終與互斥鎖一起使用。
- 訊號量機制(Semaphore):包括無名執行緒訊號量和命名執行緒訊號量
- 訊號機制(Signal): 類似程序間的訊號處理
提問:互斥鎖與訊號量的區別?
答:互斥鎖用於執行緒的互斥,訊號量用於執行緒的同步。這是互斥鎖和訊號量的根本區別,也就是互斥和同步之間的區別。同時互斥鎖的作用域僅僅在於執行緒,訊號量可以作用於執行緒和程序。
10 .虛擬函式 虛擬函式表 虛擬函式指標
(1)父類的解構函式不是虛擬函式會怎樣?
(2)所有的函式都可以定義為虛擬函式嗎?為什麼?
11.main函式執行前後會做什麼\
main函式執行前:
__start:
:
init stack;
init heap;
open stdin;
open stdout;
open stderr;
:
push argv;
push argc;
call _main; (呼叫 main)
呼叫main函式之前,
- 建立程序後,把控制權交給程式的入口函式,即為執行時庫的某個入口函式。
- glibc的入口函式是_start
- msvc(vc 6.0)的是mainCRTStartup
- 入口函式對執行庫和執行環境進行初始化,包括堆、I/O、執行緒、全域性變數構造
- 初始化堆,棧(設定棧指標)
- 全域性變數、物件和靜態變數、物件的空間分配和初始化。
- 初始化C/C++庫
- 引數壓棧 argc,argv ,獲取環境變數
- 開啟標準輸入,輸出、錯誤流
- 註冊解構函式,使用atexit註冊解構函式(註冊時在連結串列頭插入連結,main函式退出也從連結串列頭開始獲取連結串列函式,進行呼叫)
- 設定棧指標
- 初始化static靜態和global全域性變數,即data段的內容
- 將未初始化部分的全域性變數賦初值:數值型short,int,long等為0,bool為FALSE,指標為NULL,等等,即.bss段的內容
- 執行全域性構造器,估計是C++中建構函式之類的吧
- 將main函式的引數,argc,argv等傳遞給main函式,然後才真正執行main函式
main函式執行後:
-
全域性物件的解構函式會在main函式之後執行;
-
可以用_onexit 註冊一個函式,它會在main 之後執行;
呼叫main函式之後,
返回入口函式,進行清理工作
包括全域性變數析構、堆銷燬、關閉I/O
銷燬堆記憶體
關閉標準輸入、輸出、關閉錯誤流
呼叫exit系統API退出程序
用於註冊終止函式
atexit()
int atexit (void (*func) (void) )
c庫中的atexit()函式,當程式正常中止時,呼叫指定的func函式。當然,你可以在任何地方註冊終止函式,但它會在程式終止的時候被呼叫。
(終止函式func不接受任何引數)
_onexit()
12.c++ 記憶體 分割槽管理
https://blog.csdn.net/weixin_48953972/article/details/121402222
詳見
棧幀
每個函式發生呼叫時,都會有一塊棧空間,這塊棧空間稱為棧幀。
棧幀的結構如下:
這裡要注意的是:棧空間是從高地址到低地址分配的。
-
rbp:基址指標暫存器(reextended base pointer),其記憶體放著一個指標,該指標永遠指向系統棧最上面一個棧幀的底部。
-
rsp:棧指標暫存器,其記憶體放著一個指標,該指標永遠指向系統棧最上面一個棧幀的棧頂。
棧幀儲存了一個函式呼叫所需要的維護資訊:
- 函式的返回地址和引數。
- 臨時變數:包括函式的非靜態區域性變數以及編譯器自動生成的其他臨時變數。
儲存的上下文:包括在函式呼叫前後需要保持不變的暫存器。
總結一下,當函式A呼叫函式B時:
1. 將函式入參儲存要暫存器中。(在函式A的呼叫棧中)
2. 將函式B的返回地址壓入棧,即將callq <B>的下一行指令地址壓入棧,然後呼叫函式B。(在函式A的呼叫棧中)
3. 函式引數入棧。(在函式B的呼叫棧中)
4. 區域性變數入棧。(在函式B的呼叫棧中)
5. 執行一些運算指令。(在函式B的呼叫棧中)
6. 將返回結果儲存到暫存器eax中。(在函式B的呼叫棧中)
7. 從暫存器eax中獲得函式B的返回值。(在函式A的呼叫棧中)
13.c++string的構造,析構和賦值函式,拷貝函式
class String
{
public:
String(const char *str=NULL);//建構函式
String(const String &other);//拷貝建構函式
~String(void);//解構函式
String& operator=(const String &other);//等號操作符過載
void ShowString();
private:
char *m_data;//指標
};
String::~String()
{
delete[] m_data;//解構函式,釋放地址空間
}
String::String(const char *str)
{
if (str==NULL)//當初始化串不存在的時候,為m_data申請
{
m_data = new char[1];
*m_data='\0';
}else
{
int length = strlen(str);
m_data = new char[length+1];
strcpy(m_data,str);
}
}
String::String(const String &other)//拷貝建構函式
{
int length = strlen(other.m_data);
m_data = new char[length+1];
strcpy(m_data,other.m_data);
}
String& String::operator=(const String &other)
{
if (this==&other)
{
return *this;
}
delete []m_data;
int length = strlen(other.m_data);
m_data=new char[length+1];
strlen(m_data,other.m_data);
return *this;
}
14.交換函式數值"intintint&int&"
- int a 值傳遞 c++拷貝資料
- int *a 地址傳遞 可以通過地址修改
- int &a 引用傳遞 直接將這兩個引數傳過去 可以修改
- const int & a 不改變值的引用傳遞
15.給定一個連結串列,和一個數字K返回連結串列倒數第K個節點的值。後面又基於這個題的思想改進了幾個題,如一個連結串列,如何找到它的中間節點?找它的3/4節點呢?(這裡面有很多的解法,較優的用雙指標)
雙指標 i走k-1步 j開始走 中間節點 i走一步 j走兩步 四分之三 找到中間再來一次
16.vector 的size 可以等於 capacity 嗎?如果vector 儲存滿了,還能再插入元素嗎?為什麼?
不可以 可以 size是當前存的個數 capcity是可以存的
17.知道哪些排序演算法?手寫氣泡排序演算法,並對氣泡排序進行優化
//第一種 新增flag 設flag=0 當發生交換則置為1 檢測為0則return
//第二種 記錄上一次交換的位置 此時1~lastpos是無序的
//第三種 每次向前一個向後一個
18. 快速排序的優化
//第一種 序列長度達到一定大小時,使用插入排序
int pivotPos;
if (high - low + 1 < 10)
{
InsertSort(arr,low,high);
return;
}
if(low < high)
{
pivotPos = Partition(arr,low,high);
QSort(arr,low,pivotPos-1);
QSort(arr,pivotPos+1,high);
}
//第二種
尾遞迴優化
template <class T>
void QSort(T arr[],int low,int high)
{
int pivotPos;
if (high - low + 1 < 10)
{
InsertSort(arr,low,high);
return;
}
while(low < high)
{
pivotPos = Partition(arr,low,high);
QSort(arr,low,pivotPos-1);
low = pivotPos + 1;
}
}
//第三種聚集元素 三元選中的基準 聚集
19.TCP 和 IP 的區別
- ip是網路層點到點的協議 專注於原機到目標機 使用ip地址作為通訊地址 採用無連線經歷交付方式
- tcp是傳輸層程序到程序的協議 採用連線交付 套接字為
20.TCP 的特性
三次握手 四次揮手 全雙工 可靠 的 傳輸層協議
21.如何在一萬個字串中查詢某一個字串,說出演算法思路。
kmp字串匹配演算法 求得字首和字尾 的最大匹配
22.雜湊衝突怎麼解決
十字連結串列法 開放地址法(線性探測,平方探測,再雜湊)