什麼樣的函數不能聲明爲虛函數?1)不能被繼承的函數。2)不能被重寫的函數。
1)普通函數
普通函數不屬於成員函數,是不能被繼承的。普通函數只能被重載,不能被重寫,因此聲明爲虛函數沒有意義。因爲編譯器會在編譯時綁定函數。
而多態體現在運行時綁定。通常通過基類指針指向子類對象實現多態。
2)友元函數
友元函數不屬於類的成員函數,不能被繼承。對於沒有繼承特性的函數沒有虛函數的說法。
3)構造函數
首先說下什麼是構造函數,構造函數是用來初始化對象的。假如子類可以繼承基類構造函數,那麼子類對象的構造將使用基類的構造函數,而基類構造函數並不知道子類的有什麼成員,顯然是不符合語義的。從另外一個角度來講,多態是通過基類指針指向子類對象來實現多態的,在對象構造之前並沒有對象產生,因此無法使用多態特性,這是矛盾的。因此構造函數不允許繼承。
4)內聯成員函數
我們需要知道內聯函數就是爲了在代碼中直接展開,減少函數調用花費的代價。也就是說內聯函數是在編譯時展開的。而虛函數是爲了實現多態,是在運行時綁定的。因此顯然內聯函數和多態的特性相違背。
5)靜態成員函數
首先靜態成員函數理論是可繼承的。但是靜態成員函數是編譯時確定的,無法動態綁定,不支持多態,因此不能被重寫,也就不能被聲明爲虛函數。
STL包括幾個部分:容器,算法(泛型算法),迭代器三個主要部分
vector的push_back()實現
產生N個不重複的隨機數(複雜度要底)
1.重複再隨機
這種方法是在[L,R]中先隨機一個數,如果這個數之前取過,那麼重複這個過程,直到取到不同的數爲止。
不過這種方法在n個數取m個隨機(m接近於n)時,時間複雜度將會很大。
這種方法的時間複雜度最壞爲O(無窮),但是空間複雜度很小爲O(1)。
2.隊列法
首先我們出師一個[L,R]的一維數組,然後在[L,R]中隨機一個下標作爲新產生的隨機數,然後將該隨機數與下標爲R的元素交換,然後再在[L,R-1]中隨機新的隨機數……以此遞歸,就可以在短時間內取到想要的隨機數列。
這種方法的時間複雜度爲O(m),但是空間複雜度會達到O(n)。
#include <iostream>
#include <vector>
#include<cmath>
#include<random>
using namespace std;
int main()
{
int N;
cin>>N;
vector<int> vec;
for(int i=0;i<N;i++)
{
vec.push_back(i);
}
for(int i = 0; i < N; i++)
{
int seed = rand()%(vec.size());
cout<<vec[seed]<<" ";
vec[seed]=vec.back();
vec.pop_back();
}
return 0;
}
數組合列表的區別
- Array數組可以包含基本類型和對象類型,ArrayList卻只能包含對象類型.
- Array大小是固定的。ArrayList大小是動態增長的。
- Array
malloc能分配的最大內存:1.2G
Linux下虛擬地址空間分配給進程的是3GB,Windows默認是2GB(操作系統爲32位)
虛函數表存儲在常量區,因爲虛函數表裏地址是不會改變的。
虛函數表指針的存儲位置是跟隨對象的存儲位置的,對象存在哪,虛函數表指針就存在哪。
C++內存分區
- 棧區(stack)— 由編譯器自動分配釋放 ,存放函數的參數值,局部變量的值等。其操作方式類似於數據結構中的棧。
- 堆區(heap) — 一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收 。注意它與數據結構中的堆是兩回事,分配方式倒是類似於鏈表,呵呵。
- 全局區(靜態區)(static)—,全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域, 未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域。 - 程序結束後有系統釋放
- 文字常量區 —常量字符串就是放在這裏的。 程序結束後由系統釋放
- 程序代碼區—存放函數體的二進制代碼。
//main.cpp
int a = 0; //全局初始化區
char *p1; //全局未初始化區
main()
{
int b; //棧
char s[] = "abc"; //棧
char *p2; 棧
char *p3 = "123456";// 123456在常量區,p3在棧上。
static int c =0; //全局(靜態)初始化區
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);
//分配得來得10和20字節的區域就在堆區。
strcpy(p1, "123456"); //123456放在常量區,編譯器可能會將它與p3所指向的"123456"優化成一個地方。
}
斷點調試原理:
調試斷點,依賴於父進程和子進程之間的通信,打斷點實際是在被調試的程序中,改變斷點附近程序的代碼,這個斷點使得被調試的程序,暫時停止,然後發送信號給父進程(調試器進程),然後父進程能夠得到子進程的變量和狀態。達到調試的目的。
修改斷點附近程序的指令地址爲0xcc,這個地址的指令就是int 3,含義是,是當前用戶態程序發生中斷,告訴內核當前程序有斷點,那麼內核中會向當前進程發送SIGTRAP信號,使當前進程暫停。父進程調用wait函數,等待子進程的運行狀態發生改變,這時子進程由於int 3中斷,子進程暫停,父進程就可以開始調試子進程的程序了。
虛繼承及實現原理https://blog.csdn.net/bxw1992/article/details/77726390
補充:
1、D繼承了B,C也就繼承了兩個虛基類指針
2、虛基類表存儲的是,虛基類相對直接繼承類的偏移(D並非是虛基類的直接繼承類,B,C纔是)
C++編譯系統在實例化D類時,只會將虛基類的構造函數調用一次,忽略虛基類的其他派生類(class B,class C)對虛繼承的構造函數的調用,從而保證了虛基類的數據成員不會被多次初始化。
對於虛基類的初始化是由最後的派生類中負責初始化。
在最後的派生類中不僅要對直接基類進行初始化,還要負責對虛基類初始化。
C++編譯系統只執行最後的派生類對基類的構造函數調用,而忽略其他派生類對虛基類的構造函數調用。從而避免對基類數據成員重複初始化。因此,虛基類只會構造一次。
虛繼承和虛函數是完全無相關的兩個概念。
虛繼承是解決C++多重繼承問題的一種手段,從不同途徑繼承來的同一基類,會在子類中存在多份拷貝。這將存在兩個問題:其一,浪費存儲空間;第二,存在二義性問題,通常可以將派生類對象的地址賦值給基類對象,實現的具體方式是,將基類指針指向繼承類(繼承類有基類的拷貝)中的基類對象的地址,但是多重繼承可能存在一個基類的多份拷貝,這就出現了二義性。
虛繼承可以解決多種繼承前面提到的兩個問題:
虛繼承底層實現原理與編譯器相關,一般通過虛基類指針和虛基類表實現,每個虛繼承的子類都有一個虛基類指針(佔用一個指針的存儲空間,4字節)和虛基類表(不佔用類對象的存儲空間)(需要強調的是,虛基類依舊會在子類裏面存在拷貝,只是僅僅最多存在一份而已,並不是不在子類裏面了);當虛繼承的子類被當做父類繼承時,虛基類指針也會被繼承。
實際上,vbptr指的是虛基類表指針(virtual base table pointer),該指針指向了一個虛基類表(virtual table),虛表中記錄了虛基類與本類的偏移地址;通過偏移地址,這樣就找到了虛基類成員,而虛繼承也不用像普通多繼承那樣維持着公共基類(虛基類)的兩份同樣的拷貝,節省了存儲空間。
在這裏我們可以對比虛函數的實現原理:他們有相似之處,都利用了虛指針(均佔用類的存儲空間)和虛表(均不佔用類的存儲空間)。
虛基類依舊存在繼承類中,只佔用存儲空間;虛函數不佔用存儲空間。
虛基類表存儲的是虛基類相對直接繼承類的偏移;而虛函數表存儲的是虛函數地址。
引入原因/純虛函數的作用
- 爲了方便使用多態特性,我們常常需要在基類中定義虛擬函數。
- 在很多情況下,基類本身生成對象是不合情理的。例如,動物作爲一個基類可以派生出老虎、孔雀等子類,但動物本身生成對象明顯不合常理。
爲了解決上述問題,引入了純虛函數的概念,將函數定義爲純虛函數(方法:virtual ReturnType Function()= 0;),則編譯器要求在派生類中必須予以重寫以實現多態性。同時含有純虛擬函數的類稱爲抽象類,它不能生成對象。這樣就很好地解決了上述兩個問題。
什麼時候需要重定義拷貝構造函數
1、這裏有個簡單的規則:如果你需要定義一個非空的析構函數,那麼,通常情況下你也需要定義一個拷貝構造函數。
2、有一個原則:一般來說你在類中進行了new操作,你就需要析構函數,在你需要析構函數的類中,一般需要加上挎貝構造函數和賦值函數。
3、拷貝構造函數,是一種特殊的構造函數,它由編譯器調用來完成一些基於同一類的其他對象的構建及初始化。其唯一的參數(對象的引用)是不可變的(const類型)。此函數經常用在函數調用時用戶定義類型的值傳遞及返回。拷貝構造函數要調用基類的拷貝構造函數和成員函數。如果可以的話,它將用常量方式調用,另外,也可以用非常量方式調用。
在C++中,下面三種對象需要調用拷貝構造函數(有時也稱“複製構造函數”):
- 一個對象作爲函數參數,以值傳遞的方式傳入函數體;
- 一個對象作爲函數返回值,以值傳遞的方式從函數返回;
- 一個對象用於給另外一個對象進行初始化(常稱爲複製初始化);
通常的原則是:①對於凡是包含動態分配成員或包含指針成員的類都應該提供拷貝構造函數;②在提供拷貝構造函數的同時,還應該考慮重載"="賦值操作符號。
如果自己定義了析構函數但是沒有重定義拷貝構造函數,則會使用默認的合成的拷貝構造函數。
默認的拷貝構造函數是傳值參數,只是簡單的將 形參的對象的成員變量,賦值給拷貝變量。
會造成對個對象可能指向相同的內存。兩個對象包含相同的指針值,析構時會被delete 兩次,發成錯誤。
含有指針的構造函數,自定義拷貝構造函數時需要深複製,即重新分配內存空間,將值拷過來。
sizeof(class)
- 類中的成員函數不佔空間,虛函數除外。只要有虛函數,就會存在虛函數表指針,就會佔4個字節。
- 空類佔一個字節
- 類中的static靜態成員變量不佔內存,靜態成員變量存儲在靜態區;並且靜態成員變量的初始化必須在類外初始化;
- 函數指針不佔字節;
重載和重寫的區別:
方法的重載和重寫都是實現多態的方式,區別在於前者實現的是編譯時的多態性,而後者實現的是運行時的多態性。重載發生在一個類中,同名的方法如果有不同的參數列表(參數類型不同、參數個數不同或者二者都不同)則視爲重載;重寫發生在子類與父類之間,重寫要求子類被重寫方法與父類被重寫方法有相同的參數列表,有兼容的返回類型,比父類被重寫方法更好訪問,不能比父類被重寫方法聲明更多的異常(里氏代換原則)。重載對返回類型沒有特殊的要求,不能根據返回類型進行區分。
覆蓋
覆蓋是指派生類中存在重新定義基類的函數,其函數名、參數列、返回值類型必須同父類中相應被覆蓋的函數嚴格一致,覆蓋函數和被覆蓋函數只有函數體不同,當基類指針指向派生類對象,調用該同名函數時會自動調用子類中的覆蓋版本,而不是父類中的被覆蓋函數版本。
函數調用在編譯期間無法確定,因虛函數表存儲在對象中,對象實例化時生成。因此,這樣的函數地址是在運行期間綁定。
覆蓋的特徵
不同的範圍(分別位於派生類和基類)
函數名字相同
參數相同
返回值類型相同
基類函數必須有virtual關鍵字。
重載和覆蓋的關係
覆蓋是子類和父類之間的關係,是垂直關係;重載是同一個類中方法之間的關係,是水平關係。
覆蓋只能由一對方法產生關係;重載是兩個或多個方法之間的關係。
覆蓋要求參數列表相同;重載要求參數列表不同。
覆蓋關係中,調用方法是根據對象的類型來決定的,重載關係是根據調用時的實參表與形參表來選擇方法體的。
重載
重載是指同名函數具有不同的參數表。
在同一訪問區域內聲明的幾個具有不同參數列表(參數的類型、個數、順序不同)的同名函數,程序會根據不同的參數列來確定具體調用哪個函數。
對於重載函數的調用,編譯期間確定,是靜態的,它們的地址在編譯期間就綁定了。
重載不關心函數的返回值類型。
函數重載的特徵
相同的範圍(同一個類中)
函數名字相同
參數不同
virtual關鍵字可有可無。
隱藏
隱藏是指派生類的函數屏蔽了與其同名的基類函數。
如果派生類的函數與基類的函數同名,但參數不同,則無論有無virtual關鍵字,基類的函數都被隱藏。
如果派生類的函數與基類的函數同名,並且參數也相同,但是基類函數沒有virtual關鍵字,此時基類的函數被隱藏。
隱藏的特徵
必須分別位於基類和派生類中
必須同名
參數不同的時候本身已經不構成覆蓋關係了,所以此時有無virtual關鍵字不重要
參數相同時就要看是否有virtual關鍵字,有就是覆蓋關係,無就是隱藏關係
原文鏈接:https://blog.csdn.net/weixin_40087851/article/details/82012624
C語言編譯運行的流程:
- 預處理:宏定義、文件包含、條件編譯、佈局控制
- 編譯:進行語法、詞法分析、語義分析,優化後生成彙編代碼文件
- 彙編: 彙編代碼轉換機器碼
- 鏈接:將源文件中用到的庫函數與彙編生成的目標文件.o合併生成可執行文件
虛函數和純虛函數
- virtual void function()=0; 純虛函數
- virtual void function(); 虛函數
純虛函數是在基類中聲明的虛函數,它在基類中沒有定義,但是要求任何派生類都要定義自己的實現方法。在基類中實現純虛函數的方法是在函數原型後加“=0”,含有純虛函數的類稱爲抽象類,它不能生成對象。
C++ 中數組作爲函數參數進行傳遞時,數組自定退化爲同類型的指針。
#include<iostream>
#include<cstdlib>
#include<cstdio>
#include<string>
using namespace std;
int getSize(int data[]) //C++ 中數組作爲函數參數進行傳遞時,數組自定退化爲同類型的指針
{
return sizeof(data);
}
int main()
{
int a[]={1,2,3,4,5};
int size1=sizeof(a);
cout<<size1<<endl; // 20=4*5
int *b=a;
int size2=sizeof(b);
cout<<size2<<endl; //指針大小爲 4
int size3=getSize(a);
cout<<size2<<endl; //數組退化爲指針,所以也是4
return 0;
}
IO模型 參考鏈接
- 數據準備階段:數據需要從硬件設備拷貝到內核緩衝區
- 數據拷貝階段:數據從內核緩衝區拷貝到用戶進程空間
- 同步:同步只能讓調用者去輪詢自己
- 異步:可以通知調用者IO可讀或可寫
- 阻塞:調用recv()函數時,整個進程或者線程就等待在這裏了,直到你recv的fd的所有信息都被send過來,這麼做好處就是保證所有信息都能夠完整的讀取了。缺點是進程或線程在此段時間裏,做不了其他事。
- 非阻塞:調用recv()函數後會立馬返回,進程或線程可以繼續執行下一步工作,不用等。
- 阻塞IO模型:調用recvfrom(),進程阻塞。直到數據準備好後,再進行數據拷貝
- 非阻塞IO模型:輪休調用recvfrom(), 數據準備階段不阻塞,當檢測到數據準備好後,開始進行數據拷貝(阻塞)。
- IO複用模型::本質上和非阻塞IO一樣,但是它可以同時在一個進程裏監視多個IO端口。調用select() 輪詢所有的Sockt當發現有數據準備好後,就調用recvfrom()函數進行數據拷貝
- 異步IO模型:
IO多路複用:單個線程,通過記錄跟蹤每個I/O流(sock)的狀態,來同時管理多個I/O流
select、poll、epoll的區別
select==>時間複雜度O(n)
- select會修改傳入的參數數組,對需要多次調用的函數不友好
- 有I/O事件發生了,卻並不知道是哪那幾個流(可能有一個,多個,甚至全部),我們只能無差別輪詢所有流,找出能讀出數據,或者寫入數據的流,select具有O(n)的無差別輪詢複雜度
- select能監視的端口有限。
- select 不是線程安全的,比如你把一個sock加入到select後,在其他線程中關閉了sock,select的結果將是不可預測的
- 需要維護一個用來存放大量fd的數據結構,這樣會使得用戶空間和內核空間在傳遞該結構時複製開銷大
(2)poll==>時間複雜度O(n)
poll本質上和select沒有區別,它將用戶傳入的數組拷貝到內核空間,然後查詢每個fd對應的設備狀態
- 它沒有最大連接數的限制,原因是它是基於鏈表來存儲的
- 輪詢方式檢測就緒事件,算法複雜度爲O(n)
(3)epoll==>時間複雜度O(1)
- epoll是線程安全的
- 不同於忙輪詢和無差別輪詢,epoll會把哪個流發生了怎樣的I/O事件通知我們。所以我們說epoll實際上是事件驅動(每個事件關聯上fd)
- 雖然連接數有上限,但是很大.沒有最大併發連接的限制,能打開的FD的上限遠大於1024(1G的內存上能監聽約10萬個端口);
- epoll通過內核和用戶空間共享一塊內存來實現的。
select,poll,epoll都是IO多路複用的機制。I/O多路複用就通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作。但select,poll,epoll本質上都是同步I/O,因爲他們都需要在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負責進行讀寫,異步I/O的實現會負責把數據從內核拷貝到用戶空間。
epoll跟select都能提供多路I/O複用的解決方案。在現在的Linux內核裏有都能夠支持,其中epoll是Linux所特有,而select則應該是POSIX所規定,一般操作系統均有實現
當我們定義一個數組a時,編譯器根據指定的元素個數和元素的類型分配確定大小(元素類型大小*元素個數)的一塊內存,並把這塊內存的名字命名爲a。名字a一旦與這塊內存匹配就不能改變。a[0],a[1]等爲a的元素,但並非元素的名字。數組的每一個元素都是沒有名字的。
這裏&a[0]和&a到底有什麼區別呢?a[0]是一個元素,a是整個數組,雖然&a[0]與&a的值一樣,但其意義不一樣。前者是數組元素的首地址,而後者是數組的首地址。以指針的形式訪問和以下標的形式訪問時,記住偏移量的單位是元素的個數而不是byte數,在計算新地址時千萬別弄錯了。
通過下面的例子來看:
#include<iostream>
using namespace std;
int main()
{
int a[5]={1,2,3,4,5};
int* ptr=(int*)(&a+1);
printf("%d,%d",*(a+1),*(ptr-1));
system("pause");
return 0;
}
答案:2,5
解析:
int* ptr=(int*)(&a+1);&a爲數組a的首地址,對指針加1操作,得到的是下一個元素的地址,而不是原有地址值直接加1,所以&a+1則爲&a的首地址加5*sizeof(int),顯然當前指針已經超過了數組的界限。將上一步計算出來的地址,強制轉換成int*類型,賦給ptr。
*(a+1);a,&a的值是一樣的,但意思不一樣。a是數組首元素的首地址,也就是[0]的首地址,a+1是數組下一個元素的首地址,即a[1],&a+1是下一個數組的首地址。所以輸出2
*(ptr-1);因爲ptr是指向a[5],並且ptr是int*類型,所以*(ptr-1)是指向a[4],輸出5。
C++四種強制類型轉換符:
static_cast<Type>(expression);
(1) 用於類層次結構中基類(父類)和派生類(子類)之間指針或引用的轉換;
——進行上行轉換(把派生類的指針或引用轉換成基類表示)是安全的;
——進行下行轉換(把基類指針或引用轉換成派生類表示)時,由於沒有動態類型檢查,所以是不安全的。
(2)用於基本數據類型之間的轉換。如int轉換爲char,int轉換爲enum,安全性由開發人員保證;
(3)把空指針(void*)轉換成目標類型的指針;
(4)把任何類型的表達式轉換成void類型。
dynamic_cast<Type>(expression); //支持運行時類型識別
- Type必須是一個類類型,並且要求該類中含有虛函數,否則編譯報錯。
- 如果 Type 是類指針類型,那麼expression也必須是一個指針;如果 type-id 是一個引用,那麼 expression 也必須是一個左值。
應用:類層次間的上行轉換和下行轉換,還可以用於類之間的交叉轉換。假設High和Low是兩個類,而ph,pl類型分別是High * 和Low * ,則僅當Low是High的可訪問基類(直接或間接)時下面語句纔將一個Low* 指針賦給pl: pl=dynamic_cast<Low*> ph. 否則該語句將空指針賦值給pl. - 作用:使得能夠在類層次結構進行向上轉換,而不予許其他轉換。
- 與static_cast相比,(1)dynamic_cast支持運行時類型識別,在下行轉換時比static_cast安全;(2)dynamic_cast支持類之間的交叉轉換,而static_cast不支持;(3)dynamic_cast要求轉換的類類型含有虛函數,而static_cast沒有這個限制。
reinterpret_cast<Type>(expression);
const_cast<Type>(expression);
- 將常量轉化爲非常量,但不能進行類型轉換。
- 去掉const,volatile屬性。
常見的C++關鍵字有哪些?各是什麼作用?
volatile
volatile關鍵字是一種類型修飾符,用它聲明的類型變量表示可以被某些編譯器位置的因素更改,比如:操作系統、硬件或者其它線程等。由於訪問寄存器的速度要快過RAM,所以編譯器一般都會作減少存取RAM的優化。遇到這個關鍵字聲明的變量,編譯器對訪問該變量的代碼就不再進行優化,從而可以提供對特殊地址的穩定訪問。
三大特性:
易變性:是在彙編層面反映出來的,就是兩條語句,下一條語句不會直接使用上一條語句對應的volatile變量的寄存器內容,而是重新從內存中讀取;
不可優化性:volatile所修飾的變量,編譯器不會對其進行各種激進的優化;
順序性:C/C++ 對volatile變量和非volatile變量之間的操作,是可能被編譯器交換順序的(交換後無變化,在有些地方這樣處理就會出問題),對volatile變量間的操作,編譯器不會交換順序;
explicit
C++中的explicit關鍵字只能用於修飾只有一個參數的類構造函數, 它的作用是表明該構造函數是顯示的, 而非隱式的, 跟它相對應的另一個關鍵字是implicit, 意思是隱藏的,類構造函數默認情況下即聲明爲implicit(隱式).
關鍵字用來修飾類的構造函數,表明該構造函數是顯式的。舉例說說明如下:
假設我們這樣定義了一個c++類
class MyClass
{
public:
MyClass( int num ){}
};
那麼如果構造函數MyClass前沒有關鍵字,下面的語句
MyClass obj = 10; //ok,convert int to MyClass
在編譯時(VC++ 6.0測試)是可以通過的,進行了一個隱式轉換,相當於執行了下面的語句
MyClass temp(10);
MyClass obj = temp;
但是如果我們在構造函數前面加上explicit 關鍵字,即
class MyClass
{
public:
explicit MyClass( int num ){}
};
就表明構造函數不能進行上面所說的隱式轉換
編譯時(VC++ 6.0)會給出error C2440: 'initializing' : cannot convert from 'const int' to 'class MyClass'
的錯誤報告!
inline
inline是爲了解決一些頻繁調用的小函數大量消耗棧空間的問題而引入的。一般函數代碼在10行之內,並且函數體內代碼簡單,不包含複雜的結構控制語句例如:while、switch,並且函數本身不是直接遞歸函數(自己調用自己)。
extern
extern(外部的)聲明變量或函數爲外部鏈接,即該變量或函數名在其它文件中可見。被其修飾的變量(外部變量)是靜態分配空間的,即程序開始時分配,結束時釋放。用其聲明的變量或函數應該在別的文件或同一文件的其它地方定義(實現)。在文件內聲明一個變量或函數默認
C和C++有什麼區別?
設計思想上:
C++是面向對象的語言,而C是面向過程的結構化編程語言
語法上:
C++具有封裝、繼承和多態三種特性
C++相比C,增加多許多類型安全的功能,比如強制類型轉換、
C++支持範式編程,比如模板類、函數模板等
C++11智能指針
介紹:智能指針主要用於管理在堆上分配的內存,它將普通的指針封裝爲一個棧對象。當棧對象的生存週期結束後,會在析構函數中釋放掉申請的內存,從而防止內存泄漏。C++ 11中最常用的智能指針類型爲shared_ptr,它採用引用計數的方法,記錄當前內存資源被多少個智能指針引用。該引用計數的內存在堆上分配。當新增一個時引用計數加1,當過期時引用計數減一。只有引用計數爲0時,智能指針纔會自動釋放引用的內存資源。對shared_ptr進行初始化時不能將一個普通指針直接賦值給智能指針,因爲一個是指針,一個是類。可以通過make_shared函數或者通過構造函數傳入普通指針。並可以通過get函數獲得普通指針。
智能指針的作用:普通指針new出的內存必須由程序員手動delete掉,否則會內存泄露。智能指針則是在將new獲得的地址(直接或間接的)賦值給智能指針對象,在智能指針過期時,自動調用析構函數使用deletel來釋放內存。方便內存管理。
智能指針類似於指針的類對象,下面介紹3中智能指針模板類。
- auto_ptr : 由C++98提出,被C++11摒棄。
採用所有權模式。
auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.”));
auto_ptr<string> p2;
p2 = p1; //auto_ptr不會報錯.
此時不會報錯,p2剝奪了p1的所有權,但是當程序運行時訪問p1將會報錯。所以auto_ptr的缺點是:存在潛在的內存崩潰問題!
- unique_ptr :“唯一”擁有其所指對象,同一時刻只能有一個unique_ptr指向給定對象(通過禁止拷貝語義、只有移動語義來實現)。
unique_ptr實現獨佔式擁有或嚴格擁有概念,保證同一時間內只有一個智能指針可以指向該對象。
採用所有權模式,還是上面那個例子
unique_ptr<string> p3 (new string ("auto")); //#4
unique_ptr<string> p4; //#5
p4 = p3;//此時會報錯!!
編譯器認爲p4=p3非法,避免了p3不再指向有效數據的問題。因此,unique_ptr比auto_ptr更安全。
另外unique_ptr還有更聰明的地方:當程序試圖將一個 unique_ptr 賦值給另一個時,如果源 unique_ptr 是個臨時右值,編譯器允許這麼做;如果源 unique_ptr 將存在一段時間,編譯器將禁止這麼做,比如:
unique_ptr<string> pu1(new string ("hello world")); unique_ptr<string> pu2; pu2 = pu1; // #1 not allowed unique_ptr<string> pu3; pu3 = unique_ptr<string>(new string ("You")); // #2 allowed
- share_ptr: 多個指針指向相同的對象,採用引用計數,每一個shared_ptr的拷貝都指向相同的內存。每使用他一次,內部的引用計數加1,每析構一次,內部的引用計數減1,減爲0時,自動刪除所指向的堆內存。shared_ptr 是爲了解決 auto_ptr 在對象所有權上的侷限性(auto_ptr 是獨佔的), 在使用引用計數的機制上提供了可以共享所有權的智能指針。
- .weak_ptr: 是一種不控制所指向對象生存期的智能指針,它指向一個由shared_ptr管理的對象。 將一個weak_ptr綁定到 一個share_ptr上,不會改變shared_ptr的引用計數。一旦最後一個指向對象的shared_ptr被銷燬,對象就會被釋放,即使有weak_ptr指向對象,對象也會被釋放。weak_ptr 字面意思:“弱共享對象” 也就是這麼來的。
我們創建一個weak_ptr時,要有一個shared_ptr來初始化它。
auto p=make_shared<int>(42) ; weak_ptr<int> wp(p); //wp弱共享p; p的引用計數未改變
if(shared_ptr<int> np=wp.lock()) { // 如果np 不爲空 則條件成立 }
指針和引用的區別:
1、指針有自己的一塊空間,而引用只是一個別名;
2、使用sizeof看一個指針的大小是4,而引用則是被引用對象的大小;
3、指針可以被初始化爲NULL,而引用必須被初始化且必須是一個已有對象的引用;
4、作爲參數傳遞時,指針需要被解引用纔可以對對象進行操作,而直接對引用的修改都會改變引用所指向的對象;
5、可以有const指針,但是沒有const引用;
6、指針在使用中可以指向其它對象,但是引用只能是一個對象的引用,不能 被改變;
7、指針可以有多級指針(**p),而引用至於一級;
8、指針和引用使用++運算符的意義不一樣;
9、如果返回動態內存分配的對象或者內存,必須使用指針,引用可能引起內存泄露。
stl::list 不支持隨機訪問迭代器
隨機訪問相當於重載[ ],list不支持常數時間的隨機訪問。
map的底層實現:
map本質是關聯類容器,
map內部自建一棵紅黑樹,這棵樹對數據有自動排序的功能,所以map內部所有數據都是有序的。
虛函數除了存函數地址還存了什麼?
堆和棧的區別:
- 申請方式不同:棧由操作系統自動分配回收,堆則需要程序員手動分配回收
- 申請效率:棧由系統分配,速度快,不會有內存碎片。堆由程序員分配,速度較慢,可能由於操作不當產生內存碎片。
- 申請大小限制不同:棧是由高地址向低地址擴展,是一塊連續的內存區域,棧頂的地址和棧的容量是系統預先規定好的。
堆則是由低地址向高地址擴展的,是不連續的內存區域。堆獲得的空間比較靈活也比較大。
內存碎片:
- 內部碎片是由於採用固定大小的內存分區,當一個進程不能完全使用分給它的固定內存區域時就產生了內部碎片,通常內部碎片難以完全避免;
- 外部碎片是由於某些未分配的連續內存區域太小,以至於不能滿足任意進程的內存分配請求,從而不能被進程利用的內存區域。
- 通常採用段頁式內存管理方式減少內存碎片的產生。
static作用:
- 隱藏:static全局變量和函數,對其他文件不可見。可以利用這個特性,在不同的文件定義同名函數和變量。
- 默認初始化爲0,:未初始化的全局變量和未初始化的靜態變量都存儲在BSS段,BSS段中所有字節默認值都是0x00.
- 保持局部變量內容的持久:static局部變量存儲在BSS段或數據段中,可以保持其上次的賦值。具有記憶性,退出該函數後,變量繼續存在,但是作用域任然與局部變量相同,所以退出函數後不可訪問。
類中的static需要注意的問題:
- 靜態成員變量必須在類定義體外部定義和初始化
- 靜態成員函數不能訪問非靜態成員函數和非靜態成員,可以訪問靜態成員及函數。非靜態函數則無限制。
- 靜態成員函數沒有this指針,因爲它不屬於任何對象。
- static成員函數不能聲明爲const, 畢竟將函數聲明爲const就是承諾不會通過該函數修改該函數所屬的對象。而static成員函數不屬於任何對象。
const修飾符
- const修飾指針時:
const int *ptr 常量指針,ptr不用初始化,ptr指向常量數據,不能通過ptr去修改 指向的常量,但是ptr指針自身可以被改變.
int * const ptr=&a 指針常量,ptr必須初始化,ptr本身不允許被修改,但是可以通過ptr修改 指向的數據。 - const修飾函數參數和返回值:參數爲指針或引用時,若不想函數對參數進行修改,則參數前加const, 返回值前加const也是保證返回不允許被修改。
- 類中 const成員常量,不能在類的聲明體中初始化,必須在構造函數裏初始化,因爲不同的對象可以有不同的 const 常量值,所以只能在構造函數初始化,而不能在聲明體或定義體裏初始化。
- 類裏的const,修飾成員函數,修飾成員函數參數,修飾對象。
new / delete 和 malloc / free 的區別
- malloc/free是C/C++語言的標準庫函數,new/delete是C++的運算符。
- new 可以自動計算空間大小,malloc不行
指針數組和數組指針
- 指針數組:是指一個數組裏面裝着指針,也即指針數組是一個數組; 定義形式: int *a[10];
- 數組指針:數組指針:是指一個指向數組的指針,它其實還是一個指針,只不過是指向數組而已;定義形式:int (*p)[10]; 其中,由於[]的優先級高於*,所以必須添加(*p).
結構體和聯合體的區別:
- struct和union都是由多個不同的數據類型成員組成, 但在任何同一時刻, union中只存放了一個被選中的成員, 而struct的所有成員都存在。在struct中,各成員都佔有自己的內存空間,它們是同時存在的。一個struct變量的總長度等於所有成員長度之和。在Union中,所有成員不能同時佔用它的內存空間,它們不能同時存在。Union變量的長度等於最長的成員的長度。
- 對於union的不同成員賦值, 將會對其它成員重寫, 原來成員的值就不存在了, 而對於struct的不同成員賦值是互不影響的。
抽象類和接口的區別:https://blog.csdn.net/qq_33098039/article/details/78075184
- 抽象類可以有構造方法,接口中不能有構造方法。
- 抽象類中可以有普通成員變量,接口中沒有普通成員變量
- 抽象類中可以包含靜態方法,接口中不能包含靜態方法
- 一個類可以實現多個接口,但只能繼承一個抽象類。
- 接口可以被多重實現,抽象類只能被單一繼承
- 如果抽象類實現接口,則可以把接口中方法映射到抽象類中作爲抽象方法而不必實現,而在抽象類的子類中實現接口中方法
抽象類:
- 抽象方法只作聲明,而不包含實現,可以看成是沒有實現體的虛方法
- 抽象類不能被實例化
- 如果一個類中有一個抽象方法,那麼當前類一定是抽象類;抽象類中不一定有抽象方法。
- 抽象類中的抽象方法,需要有子類實現,如果子類不實現,則子類也需要定義爲抽象的。
- 抽象派生類可以覆蓋基類的抽象方法,也可以不覆蓋。如果不覆蓋,則其具體派生類必須覆蓋它們
接口:
- 接口不能被實例化
- 接口只能包含方法聲明
- 接口的成員包括方法、屬性、索引器、事件
- 接口中不能包含常量、字段(域)、構造函數、析構函數、靜態成員
const和define的區別,以及const的優勢:
區別:
- 就起作用的階段而言: #define是在編譯的預處理階段起作用,而const是在 編譯、運行的時候起作用。
- 就起作用的方式而言: #define只是簡單的字符串替換,沒有類型檢查。而const有對應的數據類型,是要進行判斷的,可以避免一些低級的錯誤。
- 就存儲方式而言:#define只是進行展開,有多少地方使用,就替換多少次,它定義的宏常量在內存中有若干個備份;const定義的只讀變量在程序運行過程中只有一份備份。
- 從代碼調試的方便程度而言: const常量可以進行調試的,define是不能進行調試的,因爲在預編譯階段就已經替換掉了。
const優勢:
- const常量有數據類型,而宏常量沒有數據類型。編譯器可以對前者進行類型安全檢查。而對後者只進行字符替換,沒有類型安全檢查,並且在字符替換可能會產生意料不到的錯誤。
- 有些集成化的調試工具可以對const常量進行調試,但是不能對宏常量進行調試。
- const可節省空間,避免不必要的內存分配,提高效率