全稱是 Resource Acquisition Is Initialization , 即「資源獲取即初始化」,其核心是把資源和物件的生命週期系結:物件建立獲取資源,物件銷燬釋放資源。這就是的資源也有了生命週期,有了自動回收的功能。lock_guard 都利用了 RAII機制 機製來實現。
防止記憶體泄露的方式有 RAII、智慧指針。
大端就是高位元組在高地址,低位元組在低地址。
小端就是低位元組在高地址,高位元組在低地址。
// 大端小端區分
bool isBigEndian() {
union NUM {
int a;
char b; // 如果是大端 b 就是最高位 ,小端就是最低位
}num;
num.a = 0x1234;
if(num.b == 0x12)
{
return true;
}
return false;
}
大端小端轉換
//無符號整型16位元
uint16_t bswap_16(uint16_t x) {
return ((x & 0x00ff) << 8) | (x & 0xff00) >> 8) ;
}
//無符號整型32位元
uint32_t bswap_32(uint32_t x) {
return ((x & 0xff000000) >> 24)| ((x & 0x00ff0000) >> 8) | \
((x & 0x0000ff00) << 8) | ((x & 0x000000ff) << 24) ;
}
main.cc
翻譯成一個ASCII碼的中介軟體檔案 main.i
main.i
檔案 翻譯成一個 ASCII 彙編檔案 main.s
.obj、.o、.a、.ko
) :執行彙編器,將 main.s
翻譯成一個可重定位目標檔案main.o
.exe、.elf、.axf
等) : 執行鏈接器,將main.o
中使用到的目標檔案組合起來,建立一個可執行的檔案根本區別:是在編譯期還是在是執行期完成鏈接、裝入動作。鏈接的主要內容就是把各個模組之間相互參照的部分都處理好,
使得各個模組之間能夠正確地銜接
靜態庫:之所以叫做靜態庫,是因爲在鏈接階段,當使用鏈接器將由彙編生成的目標檔案構造成一個可執行的輸出檔案時,它只是複製靜態庫中裡這個即將生成的可執行檔案所參照到的目標模組。靜態庫特點總結:
靜態庫對函數庫的鏈接是放在編譯時期完成的。
程式在執行時與函數庫再無瓜葛,移植方便。
浪費空間和資源,因爲所有相關的目標檔案與牽涉到的函數庫被鏈接合成一個可執行檔案。
動態庫:動態庫在程式編譯時並不會被連線到目的碼中,而是在程式執行是才被載入。不同的應用程式如果呼叫相同的庫,那麼在記憶體裡只需要有一份該共用庫的範例,規避了空間浪費問題。動態庫在程式執行是才被載入,也解決了靜態庫對程式的更新、部署和發佈頁會帶來麻煩。使用者只需要更新動態庫即可,增量更新
動態庫把對一些庫函數的鏈接載入推遲到程式執行的時期。
可以實現進程之間的資源共用。(因此動態庫也稱爲共用庫)
將一些程式升級變得簡單。
甚至可以真正做到鏈接載入完全由程式設計師在程式程式碼中控制(顯示呼叫)。
static全域性變數與普通的全域性變數有什麼區別:static全域性變數只初使化一次,防止在其他檔案單元中被參照;
static區域性變數和普通區域性變數有什麼區別:static區域性變數只被初始化一次,下一次依據上一次結果值;
程式的區域性變數存在於(堆疊)中,全域性變數存在於(靜態區 )中,動態申請數據存在於( 堆)中。
extern全域性變數(用extern修飾的變數只是說明該變數在其他地方定義,所以在其他地方一定要用明確的定義如int a,並且不能用static修飾)、static全域性變數和static區域性變數的生存期都是「永久」,區別只是可見域不同。extern全域性變數可見區域是工程,static全域性變數可見區域是檔案,而static區域性變數的可見區域是塊。
阻止編譯器爲了提高速度將一個變數快取到暫存器內而不寫回。
阻止編譯器調整操作volatile
變數的指令順序。
注意:即使 volatile
能夠阻止編譯器調整順序, 也無法阻止CPU動態排程換序(reorder
)
斷言主要用於檢查邏輯上不可能的情況。例如,它們可用於檢查程式碼在開始執行之前所期望的狀態,或者在執行完成後檢查狀態。與正常的錯誤處理不同,斷言通常在執行時被禁用。
assert 是個宏而非函數,如果條件返回錯誤,則會拋出異常,最後會呼叫 abort 終止程式,發送的是 SIGABRT,可以通過宏 NODEBUG 來關閉 assert,但是需要設定在原始碼的開頭。
#define assert(expr) \
(static_cast <bool> (expr) \
? void (0) \
: __assert_fail (#expr, __FILE__, __LINE__, __ASSERT_FUNCTION))
extern
void __assert_fail (const char *__assertion,
const char *__file,
unsigned int __line,
const char *__function)
{
__THROW __attribute__ ((__noreturn__));
}
在判斷失敗時會呼叫 __assert_fail 來拋出異常,在C++中異常的底層是用 abort 實現的。
new:不僅僅是分配記憶體,還包括了物件型別轉換以及初始化
malloc
返回 void*
型別,並且在記憶體不足時,返回NULL
指針
當開闢的空間小於 128K
時,呼叫brk()
函數,malloc
的底層實現是系統呼叫函數 brk()
當開闢的空間大於 128K
時,mmap()
系統呼叫函數來在虛擬地址空間中(堆和棧中間,稱爲「檔案對映區域」的地方)找一塊空間來開闢
operator new
底層也是由malloc
實現。 malloc
底層是由slab
實現。
對於POD
型別物件,使用new
建立的物件,也是可以使用free
來釋放銷燬物件,原因就是在於這個物件是POD
型別。沒有其他資源需要釋放,或者檔案描述符需要關閉,因此可以不呼叫解構函式而使用free
來替代delete
。儘管可以,但是不推薦,這樣的程式碼並不健壯。
new
在C++裡是可以用 malloc + placement
替代的,或者說等效於這兩個。
// 更好的展示 malloc 與 new 的區別與聯繫
class Foo {
public:
Foo(int a=0, int b=0): a(a),b(b) { }
~Foo() { std::cout<<"dtor"<<std::endl; }
private:
int a;
int b;
};
int main(int argc, char const *argv[])
{
// new = placement new + malloc
Foo* foo = static_cast<Foo*>(::malloc(sizeof(Foo)));
new(foo)Foo(1,1);
// delete
foo->~Foo();
::free(foo);
return 0;
}
// 執行結束,無記憶體泄露
除了new
和malloc
,還有什麼方式可以在堆中分配記憶體麼,以及怎麼釋放?
mmap
,munmap
多型的三個條件:繼承,重寫(override
),基礎類別參照指向派生類物件。
靜態多型
過載,在編譯時期就可以確定
模板技術:比如 CRTP
,它是使用子類來作爲基礎類別的模板參數,在基礎類別中呼叫子類的方法。
template<typename Derived>
class Base{
// 其他忽略
Derived& convert()
{
return static_cast<Derived&>(*this);
}
};
這個也從側面說明static_cast
可以讓子類轉換爲父類別,要使得保證安全,前提是轉換後呼叫子類的方法中沒有使用子類獨有的數據成員
動態多型
執行時才確定呼叫的是哪個函數。核心關鍵在於虛擬函式:子類重寫基礎類別的虛方法,定義指向子類物件的基礎類別指針。這個基礎類別指針的行爲直到執行時才能 纔能確定呼叫的是子類還是基礎類別的方法,這就是多型。
實現原理是:虛擬函式表和虛擬函式指針vptr
。詳情
overload
)和重寫(override
)過載:允許多個同名函數,而這些函數的參數列表不同,函數模板這麼實現的,在編譯期間就能確定。
C++函數過載底層實現原理是C++利用 name mangling
技術,來改變函數名,區分參數不同的同名函數。編譯器通過函數名和其參數型別識別過載函數。不能僅僅基於不同的返回型別而實現函數過載,是因爲經過 name mangling
後得到的函數名與返回值型別是無關的。
void func(int a) { }
int func(int a, int b) { return a+b; }
int main(int argc, char const *argv[])
{
return 0;
}
比如,如上的程式碼。在經過編譯後,得到的符號表如下:
$ objdump -t main.o
0000000000000000 g F .text 000000000000000e _Z4funci
000000000000000e g F .text 0000000000000018 _Z4funcii
0000000000000026 g F .text 0000000000000016 main
其中, 字首 __z
是規定,4
是函數名的字元個數,i
是第一個函數的參數型別int
,ii
是第二個函數的參數型別int, int
。由此可見也是與返回值無關的。
重寫override
:是指子類重新定義父類別的方法,子類的函數名和參數列表以及返回值必須與父類別的完全一致。對於虛擬函式的重寫,c++11中新定義了一個關鍵詞override
,就是爲了讓子類在重寫父類別的虛擬函式方法時,如何參數列表發生更改可以讓編譯器報錯。
static
static
變數都是在全域性數據區分配記憶體,宣告週期直到程式執行結束.
四類static
static
變數和static
函數:都已經被匿名名稱空間取代,作用是不能外部檔案使用static
變數:在數據區.data
分配記憶體,首次初始化以後,以後呼叫都不會再初始化,作用域僅侷限於函數,生命週期直到程式執行結束.data
爲什麼要引入static
需要一個數據物件爲整個類而非某個物件服務,同時又不能破壞類的封裝特性,因此將靜態成員隱藏在類的內部,提供靜態成員函數介面,因爲共用,可以節省記憶體。
空類有六個成員函數
class Empty {
public:
Empty();
Empty(const Empty& );
~Empty();
Empty& operator=(consy Empty& );
Empty* operator& (); // 取地址運算子
const Empty* operator& () const; // const 型別取地址運算子
};
建構函式可以是虛擬函式嗎?解構函式可以是虛擬函式嗎?
虛擬函式對應一個vtbl
,這個vtbl
實際是儲存在物件的記憶體空間.如果建構函式是虛的,物件的構造就需要vtbl
來呼叫,而虛擬函式表又是在物件構造後才能 纔能建立,因此建構函式不能是虛擬函式.
而解構函式在使用多型的繼承中一般都是虛解構函式.爲的是能正確選擇解構函式.
c++
的深拷貝如何理解
在類中有指針時並且內部分配資源.經過淺拷貝後,最終會造成資源一次分配,多次釋放.造成系統崩潰.
main函數執行之前,主要就是初始化系統相關資源:
static
變數和global
全域性變數,即.data
段的內容short
,int
,long
等爲0
,bool
爲FALSE
,指針爲NULL
等等,即.bss
段的內容main
之前呼叫建構函式argc
,argv
等傳遞給main
函數,然後才真正執行main
函數main函數執行之後:
atexit
註冊一個函數,它會在main 之後執行; int main(int argc, char const *argv[]){
const char* str = "name";
sizeof(str); // 取的是指針str的長度,是8
strlen(str); // 取的是這個字串的長度,不包含結尾的 \0。大小是4
return 0;
}
char* strcpy(char* dest, const char* src);
char* strncpy(char* dest, const char* src, size_t n);
void* memcpy (void* dest, const void* src, size_t n);
前面兩個函數是以字元爲單位,而mmemcpy
是以位元組爲單位。
strcpy
和memcpy
主要有以下3方面的區別。
strcpy
只能複製字串,而memcpy
可以複製任意內容,例如字元陣列、整型、結構體、類等。strcpy
不需要指定長度,它遇到被複制字元的串結束符'\0'
才結束,所以容易溢位。memcpy
則是根據其第3個參數決定複製的長度,而且如果字串數據中包含'\0'
,只能用memcpy
。strncpy
:在複製字串時,memcpy
更加類似於strncpy
。
strncpy
和memcpy
很相似,只不過它在一個終止的空字元處停止。當 n > strlen(src)
時,dst[strlen(len)] ~ dst[n-1]
都是\0
;當 n<=strlen(src)
時,複製前src
的n個字元。這裏隱藏了一個事實,就是dst
指向的記憶體一定會被寫n個字元。
memcpy
需要注意的是:
dest
指針要分配足夠的空間,也即大於等於 n
位元組的空間。如果沒有分配空間,會出現斷錯誤。dest
和 src
所指的記憶體空間不能重疊(如果發生了重疊,使用 memmove()
會更加安全)。動手實現 memcpy
:
void* myMemcpy(void* dst, const void* src, size_t n) {
if(dst ==nullptr || src==nullptr) return nullptr;
if(src == dst) retrun src;
char* pdst = static_cast<char*>(dst);
const char* psrc = static_cast<const char*>(src);
// 發生重疊時,從後向前複製
if(psrc < pdst && pdst < psrc + n)
{
for(int i=n-1; i >=0; --i) pdst[i] = psrc[i];
}
else
{
for(int i=0; i < n; ++i) pdst[i] = psrc[i];
}
return pdst;
}
// 使用
int main(int argc, char const *argv[])
{
char buf[12]={0};
char str[] = "hello world cpp";
myMemcpy(str, str+6, 9);
std::cout<<str<<std::endl;
return 0;
}
都是是指向無效記憶體區域(這裏的無效指的是"不安全不可控")的指針,存取行爲將會導致未定義行爲。
野指針
野指針,指的是沒有被初始化過的指針
int main(void) {
int* p; // 未初始化
std::cout<< *p << std::endl; // 未初始化就被使用
return 0;
}
因此,爲了防止出錯,對於指針初始化時都是賦值爲 nullptr
,這樣在使用時編譯器就會直接報錯,產生非法記憶體存取。
懸空指針
懸空指針,指針最初指向的記憶體已經被釋放了的一種指針。
int main(void) {
int * p = nullptr;
int* p2 = new int;
p = p2;
delete p2;
}
此時 p和p2就是懸空指針,指向的記憶體已經被釋放。繼續使用這兩個指針,行爲不可預料。需要設定爲p=p2=nullptr
。此時再使用,編譯器會直接保錯。
避免野指針比較簡單,但懸空指針比較麻煩。c++引入了智慧指針,C++智慧指針的本質就是避免懸空指針的產生。
#include <stdlib.h>
void *malloc(size_t size);
void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size);
void free(void *ptr);
malloc
最常用的分配記憶體函數,分配size
個位元組。分配的記憶體是未初始化的,如果size==0
,要麼返回NULL
,要麼返回一個獨一無二的指針,能被free
釋放。
realloc
:用來改變已有記憶體區的大小,而不改變內容。新的大小爲參數size
,即newsize。
ptr==NULL
:realloc(NULL,size)
就相當於malloc(size)
size==0
:realloc(ptr, 0)
就相當於free(ptr)
ptr==NULL && size==0
:危險。newsize > oldsize
,那麼增加的記憶體是未初始化的,原來的記憶體內容保持不變,即[ptr, ptr+oldsize)
內部不變,[ptr+oldsize, ptr+newsize)
是初始化的內容。如果newsize < oldsize
,尾部的內容就被切去,釋放,只是剩下前面。
因此 realloc
之後不能給這個記憶體區初始化.
calloc
分配nmemb
個元素,每個元素大小是size
個位元組的連續記憶體。
nmemb*size
的連續記憶體區0
。如果size==0
,要麼返回NULL
,要麼返回一個獨一無二的指針,能被free
釋放。
爲了減小編譯依賴加快編譯速度和生成二進制檔案的大小,C/C++ 專案中一般在 *.h 檔案對於指針型別(包括智慧指針) 儘量使用前置宣告,而不是直接包含對應類的標頭檔案。例如:
//Test.h
//在這裏使用A的前置宣告,而不是直接包含A.h檔案
class A;
class Test
{
public:
Test();
~Test();
private:
A*
};
g++ 編譯器是預設開啓 copy elison
選項的。如果要關閉這個選項,使用 -fno-elide-constructors
。copy elision
主要包括以下兩項內容:
1. 返回值優化
即通過將返回物件所佔空間的直接構造到他們本來要複製/移動到的物件中去,依次來避免拷貝/移動操作。返回值優化包括具名返回值優化 NRVO
與 匿名返回值優化 URVO
,區別在於返回值是具名的區域性變數(NRVO
)還是無名的臨時物件(URVO
)
URVO
與 徹底 Copy elision
class Copyable {
public:
Copyable() { std::cout<<"default ctor"<<std::endl; }
Copyable(const Copyable& rhs) = delete;
Copyable(Copyable&& rhs) = delete;
};
Copyable return_urvo_value() {
return Copyable{}; // since c++17 ok
}
int main(int argc, char const *argv[]) {
auto x = return_urvo_value();
return 0;
}
上述程式碼在C++17中是可以編譯通過的。在 C++17 之前,並沒有明確的提出在什麼情況下,可以徹底進行 Copy Elision
(這裏的徹底的指的是包括不進行檢查是否有可用的 copy/move 建構函式)。在C++17中,對於匿名物件(or 臨時物件)不論是傳遞參數,還是以返回值返回時,都不會呼叫拷貝/移動構造。因此上面的這段程式碼在C++17是可以正常編過的,而在C++14會編譯出錯。
$ g++ main.cc -std=c++14 -o main && ./main
main.cc: In function ‘Copyable return_urvo_value()’:
main.cc:29:19: error: use of deleted function ‘Copyable::Copyable(Copyable&&)’
29 | return Copyable{};
| ^
main.cc:24:3: note: declared here
24 | Copyable(Copyable&& rhs) = delete;
| ^~~~~~~~
main.cc: In function ‘int main(int, const char**)’:
main.cc:34:31: error: use of deleted function ‘Copyable::Copyable(Copyable&&)’
34 | auto x = return_urvo_value();
| ^
main.cc:24:3: note: declared here
24 | Copyable(Copyable&& rhs) = delete;
| ^~~~~~~~
自然,只要將上面程式碼中的如下兩行註釋掉,即可正常編譯,並且 Copyable
的建構函式都是隻被呼叫一次,即copy elision
起作用了。 注意:Copyable
的複製/移動建構函式必須同時可存取。
Copyable(const Copyable& rhs) = delete;
Copyable(Copyable&& rhs) = delete;
因此,在C++17以前,對於 urvo
不在乎是否返回的物件的複製/移動建構函式是否存在或者可存取,copy elision
都能生效。而在 C++14
之前,返回的物件可以沒有複製/移動建構函式,但是必須可以存取。
nrvo
在 nrvo
時,返回物件的複製/移動建構函式必須可存取。否則編譯不過。
class Copyable {
public:
Copyable() { std::cout<<"default ctor"<<std::endl; }
Copyable(const Copyable& rhs) = delete;
Copyable(Copyable&& rhs) = delete;
};
Copyable return_urvo_value() {
return Copyable{};
}
Copyable return_nrvo_value() {
Copyable local;
return local;
}
int main(int argc, char const *argv[]) {
auto x = return_urvo_value();
auto y = return_nrvo_value();
return 0;
}
如上程式碼,即使是C++17也會編譯失敗,必須將如下兩行程式碼註釋掉,使得 Copyable
物件的複製/移動建構函式可存取。copy elision
才能 纔能生效:Copyable
的預設建構函式只調用一次。
// Copyable(const Copyable& rhs) = delete;
// Copyable(Copyable&& rhs) = delete;
2. 右值拷貝優化
右值拷貝優化,當某一個類的臨時物件以值傳遞給該類的另一個物件時,也可以直接利用該臨時物件的來避免拷貝/移動操作。在上面的基礎上,加上如下的程式碼:
void pass_by_value(Copyable rhs) {
}
int main(int argc, char const *argv[]) {
auto x = return_urvo_value();
auto y = return_nrvo_value();
pass_by_value(Copyable());
return 0;
}
最終的輸出也是呼叫預設三次建構函式:
$ g++ main.cc -std=c++11 -o main && ./main
default ctor
default ctor
default ctor
到此,copy elision
基本分析結束。如果想檢視沒有copy elision
作用下的輸出,開啓-fno-elide-constructors
。
對於一些沒有拷貝/移動構造的物件,如 unique_ptr
、 atomic
等。現在我們能夠定義一個工廠函數,即使沒有複製或移動建構函式都可以返回一個物件。例如,以下通用工廠函數:
template <typename T, typename... Args>
T make_instance(Args&& ... args)
{
return T{ std::forward<Args>(args)... };
}
int main()
{
int i = make_instance<int>(42);
// std::unique_ptr 實現了 移動建構函式,因此可以編譯成功
auto up = make_instance<std::unique_ptr<int>>(new int{ 42 });
// 禁止了複製建構函式,但是也沒有實現移動建構函式,因此要到 C++17 才能 纔能編譯過
auto ai = make_instance<std::atomic<int>>(42);
return 0;
}
std::vector 與 std::list
對於如下程式碼,使用 std::bind 系結類的成員函數並呼叫。
class Foo {
public:
Foo()=default;
void add(const int& lhs, const int& rhs)
{
std::cout<< (lhs + rhs)<<std::endl;;
}
};
int main(int argc, char const *argv[])
{
Foo foo1;
// 系結並且執行
std::bind(&Foo::add, &foo1, 1, 2)();
return 0;
}
最後內部執行的程式碼如下:
template<typename _Res, typename _MemFun, typename _Tp, typename... _Args>
constexpr _Res
__invoke_impl(__invoke_memfun_deref, _MemFun&& __f, _Tp&& __t, _Args&&... __args)
{
return ((*std::forward<_Tp>(__t)).*__f)(std::forward<_Args>(__args)...);
}
其中
_t
,型別是 Foo*&
函數物件指針,指向的foo1
物件。_f
是函數指針,指向的就是add
成員函數__invoke_memfun_deref
:是用來標記是哪種把系結方式,比如上述程式碼中的系結物件成員函數因此,最終,呼叫可以簡化爲如下:
foo1.add(1,2);
整個std::bind
最終的執行程式碼如下:
// 直接傳入函數呼叫: std::bind(func, arg1, arg2); 或者靜態成員函數
// 很明顯,這裏沒有類物件
template<typename _Res, typename _Fn, typename... _Args>
constexpr _Res
__invoke_impl(__invoke_other, _Fn&& __f, _Args&&... __args)
{ return std::forward<_Fn>(__f)(std::forward<_Args>(__args)...); }
// 上面介紹過
template<typename _Res, typename _MemFun, typename _Tp, typename... _Args>
constexpr _Res
__invoke_impl(__invoke_memfun_deref, _MemFun&& __f, _Tp&& __t, _Args&&... __args)
{
return ((*std::forward<_Tp>(__t)).*__f)(std::forward<_Args>(__args)...);
}
// 下面 下麪幾種沒見過呼叫
template<typename _Res, typename _MemFun, typename _Tp, typename... _Args>
constexpr _Res
__invoke_impl(__invoke_memfun_ref, _MemFun&& __f, _Tp&& __t, _Args&&... __args)
{ return (__invfwd<_Tp>(__t).*__f)(std::forward<_Args>(__args)...); }
template<typename _Res, typename _MemPtr, typename _Tp>
constexpr _Res
__invoke_impl(__invoke_memobj_ref, _MemPtr&& __f, _Tp&& __t)
{ return __invfwd<_Tp>(__t).*__f; }
template<typename _Res, typename _MemPtr, typename _Tp>
constexpr _Res
__invoke_impl(__invoke_memobj_deref, _MemPtr&& __f, _Tp&& __t)
{ return (*std::forward<_Tp>(__t)).*__f; }
Lambda
lambda可以理解爲是仿函數的語法糖。
int main(int argc, char const *argv[])
{
auto add = [](int a, int b) { return a+b; };
int c = add(1,2);
return 0;
}
對於上面的lambda
函數,在gdb偵錯,經過編譯的到的函數型別:
(gdb) s
<lambda(int, int)>::operator()(int, int) const (__closure=0x7fffffffe6d3, a=1, b=2) at main.cpp:24
24 auto add = [](int a, int b) { return a+b; };
lambda
可以看作是匿名的函數物件,並且 lambda
表達式預設是 const
屬性。
class Foo {
public:
Foo()
{
[](){std::cout<<"123"<<std::endl; }();
}
};
int main(int argc, char const *argv[])
{
Foo foo;
return 0;
}
在類中呼叫lambda
表達式,編譯出來的型別如下:
Foo::Foo()::{lambda()#1}::operator()() const
而實際上,lambda與operator本質上也是一樣的,如下程式碼:
class Foo {
public:
Foo()
{
// 以下兩個設計等價
[this](const char* str){this->print(str); }("lambda");
Unmaed(this)("operator");
}
void print(const char* str) {
std::cout<<str<<std::endl;
}
private:
struct Unmaed
{
Unmaed(Foo* foo): foo_(foo) { }
void operator()(const char* str) const
{
foo_->print(str);
}
Foo* foo_;
};
};
lambda
可以過載,但是 std::bind
無法區別過載
void f(int) {}
void f(double) {}
auto g = [] { f(1); }; // OK
auto g = std::bind(f, 1); // 錯誤
auto g = std::bind(static_cast<void(*)(int)>(f), 1); // OK
爲此必須指定對應的函數指針型別。lambda
閉包類的 operator()
採用的是能被編譯器內聯的常規的函數呼叫。而std::bind
採用的是一般不會被內聯的函數指針呼叫,這意味着 lambda
比 std::bind
執行得更快。
std::bind
的參數,系結的是 std::bind
,而不是std::bind
內部管理的函數void f(std::chrono::steady_clock::time_point t, int i)
{
std::this_thread::sleep_until(t);
std::cout << i;
}
auto g = [](int i)
{
f(std::chrono::steady_clock::now() + std::chrono::seconds(3), i);
};
g(1); // 3秒後列印1
// 用std::bind實現相同效果,但存在問題
auto h = std::bind(f,
std::chrono::steady_clock::now() + std::chrono::seconds(3),
std::placeholders::_1);
h(1); // 3秒後列印1,但3秒指的是呼叫std::bind後的3秒,而非呼叫f後的3秒
計算時間的表達式作爲實參被傳遞給std::bind
,因此計算髮生在呼叫std::bind
的時刻,而非呼叫其系結的函數的時刻。
在 c++14
中,完全沒有理由使用 std::bind
,c++11
由於特性受限,存在兩個使用場景:
c++11
缺少的移動捕獲operator()
是模板時,如果將此函數作爲參數使用,用 std::bind
系結才能 纔能接受任意型別參數struct X {
template<typename T>
void operator()(const T&) const;
};
X x;
auto f = std::bind(x, _1); // f可以接受任意參數型別
// c++14 做法
X a;
auto f = [a](const auto& x) { a(x); };
std::bind
傳入的參數預設情況下是 「值傳遞」,想要使用參照傳遞需要std::ref
。詳細可以參考下面 下麪的程式碼:
class Foo {
public:
Foo()
{
std::cout<<"default"<<std::endl;
}
Foo(const Foo& rhs)
{
std::cout<<"ctor"<<std::endl;
}
};
void add(const Foo& lhs, const Foo& rhs) {
}
int main(int argc, char const *argv[])
{
std::cout<<std::boolalpha;
Foo foo1;
Foo foo2;
std::cout<<"bind: pass by value"<<std::endl;
auto func = std::bind(add, foo1, foo2);
std::cout<<"bind: pass by ref"<<std::endl;
auto func = std::bind(add, std::ref(foo1), std::ref(foo2));
std::cout<<"lambda "<<std::endl;
[&foo1, &foo2]{ add(foo1, foo2);}();
return 0;
}
上面的程式碼輸出:
$ g++ -g -O0 main.cc -o main && ./main
default
default
bind: pass by value
ctor
ctor
bind: pass by ref
lambda
可以看到std::bind
在預設情況下,是依靠值傳遞,使用了std::ref
來包裹傳入參數纔是使用參照傳遞。用 gdb
偵錯,可以跟蹤到發生構造 Foo
物件的位置:
template<typename _UHead>
constexpr _Head_base(_UHead&& __h) : _Head(std::forward<_UHead>(__h)) { }
// 整個呼叫鏈如下:
#0 Foo::Foo (this=0x7fffffffe068, rhs=...) at main.cc:13
#1 0x0000555555555a8c in std::_Head_base<1ul, Foo, true>::_Head_base<Foo&> (this=0x7fffffffe068, __h=...) at /usr/include/c++/9/tuple:87
#2 0x00005555555559e8 in std::_Tuple_impl<1ul, Foo>::_Tuple_impl<Foo&> (this=0x7fffffffe068, __head=...) at /usr/include/c++/9/tuple:349
#3 0x00005555555558f3 in std::_Tuple_impl<0ul, Foo, Foo>::_Tuple_impl<Foo&, Foo&, void> (this=0x7fffffffe068, __head=...)
at /usr/include/c++/9/tuple:218
#4 0x0000555555555815 in std::tuple<Foo, Foo>::tuple<Foo&, Foo&, true> (this=0x7fffffffe068, __a1=..., __a2=...) at /usr/include/c++/9/tuple:969
#5 0x00005555555556fc in std::_Bind<void (*(Foo, Foo))(Foo const&, Foo const&)>::_Bind<Foo&, Foo&>(void (*&&)(Foo const&, Foo const&), Foo&, Foo&)
(this=0x7fffffffe060, __f=@0x7fffffffe010: 0x5555555551ea <add(Foo const&, Foo const&)>) at /usr/include/c++/9/functional:467
#6 0x0000555555555571 in std::bind<void (&)(Foo const&, Foo const&), Foo&, Foo&> (__f=
@0x5555555551ea: {void (const Foo &, const Foo &)} 0x5555555551ea <add(Foo const&, Foo const&)>) at /usr/include/c++/9/functional:812
#7 0x00005555555552b7 in main (argc=1, argv=0x7fffffffe198) at main.cc:30
左值參照,也就是「常規參照」,不能系結到要轉換的表達式,字面常數,或返回右值的表達式。而右值參照恰好相反,可以系結到這類表達式,但不能系結到一個左值上。
右值參照就是必須系結到右值的參照,通過&&獲得。右值參照只能系結到一個將要銷燬的物件上,因此可以自由地移動其資源。
返回左值的表達式包括返回左值參照的函數及賦值,下標,解除參照和前置遞增/遞減運算子;
返回右值的包括返回非參照型別的函數及算術,關係,位和後置遞增/遞減運算子。可以看到左值的特點是有持久的狀態,而右值則是短暫的
void func(int&& index, int idx) {
if(idx > 3) return;
func(index++, ++idx); // Ok
func(++index, ++idx); // Error
std::cout<< std::is_rvalue_reference<decltype(index)>::value <<std::endl;
}
在上面的程式碼中,++index
產生的是左值,而 index++
產生的是右值。因此上面的可以編譯成功,下面 下麪的編譯不過。
【注意】:已經命名的右值,編譯器會認爲是左值。
變數都是左值,即使變數是右值參照型別
int&& ref1 = 1; // ok
int&& ref2 = ref1; // error
因爲 ref2
是右值參照型別的變數,不能將其系結到左值ref1
上。ref1
與 ref2
是左值,因爲他們都是變數,但是變數型別是右值參照型別,即這兩個變數只能系結到右值上。
基礎類別和子類之間轉換:
static_cast
的使用,當且僅當型別之間可隱式轉化時,static_cast
的轉化纔是合法的。有一個例外,那就是類層次間的向下轉型,static_cast
可以完成類層次間的向下轉型,但是向下轉型無法通過隱式轉換完成。
向上轉換安全:子類指針轉換成父類別指針是安全的;
向下轉換不安全:父類別指針轉換成子類指針是不安全的。
static_cast
不能進行無關型別(如非基礎類別和子類)指針之間的轉換。
class Base{ };
class Derived : public base{ /**…*/ };
Base* B = new Base;
Derived* D = static_cast<Drived*>(B); // 不安全
```
爲什麼不安全?
D指向本質上還是B的物件模型,D指向的記憶體模型中可能存在B沒有的成員變數。如果 `D->foo()` 中使用了 `D` 的成員變數,那麼這個函數呼叫就是不安全的。因此,向下轉換是安全的。
static_cast
還可以在左值和右值之間顯示地轉換。雖然不能隱式地將左值轉換爲右值,但是可以使用static_cast
顯示地將左值轉換爲右值。
基本數據型別轉換: enum
, int
, char
, float
等。安全性問題由開發者來保證。
把空指針轉換成目標型別的空指針
int* iptr = static_cast<int*>(::malloc(sizoef(int)));
把任何型別的表達式轉換成void型別:static_cast<void>(iptr)
static_cast
不能去掉型別的const、volitale
屬性(用const_cast
)
隱式轉換都建議使用 static_cast
進行標明和替換
專門用於將多型基礎類別的指針或參照強制轉換爲派生類的指針或參照,而且能夠檢查轉換的安全性。對於不安全的指針轉換,轉換結果返回 nullptr 指針。
使用特點:
基礎類別必須要有虛擬函式,因爲dynamic_cast
是執行時型別檢查,需要執行時型別資訊,而這個資訊是儲存在類的虛擬函式表中,只有一個類定義了虛擬函式,纔會有虛擬函式表
對於下行轉換,dynamic_cast
是安全的(當型別不一致時,轉換過來的是空指針),而static_cast
是不安全的(當型別不一致時,轉換過來的是錯誤意義的指針,可能造成踩記憶體,非法存取等各種問題), reinterpreter_cast
下行轉換是可以轉換,但是不安全。
相同基礎類別不同子類之間的交叉轉換,轉換結果是是 nullptr
class Base
{
public:
virtual void fun() { }
};
class Drived : public base {
public:
int i;
};
Base *Bptr = new Drived();//語句0
Derived *Dptr1 = static_cast<Derived*>(Bptr); //語句1;
Derived *Dptr2 = dynamic_cast<Derived*>(Bptr); //語句2;
此時語句1和語句2都是安全的,因爲此時 Bptr
確實是指向的派生類的記憶體模型,所以兩個型別轉換都是安全的。Dptr1
和 Dptr2
可以盡情存取 Drived
類中的成員,絕對不會出問題。但是如果此時語句0更改爲如下表達:
Base* Bptr = new Base(); `
那麼 Bptr
指向的是Base
物件記憶體模型。因此語句1是不安全的,因爲如果存取子類的數據成員,其行爲將是未定義。而語句2返回的是 nullptr
,更加直觀的告訴使用者不安全。
用於進行各種不同類型的指針之間、不同類型的參照之間以及指針和能容納指針的整數型別之間的轉換。轉換時執行的是逐 byte
複製的操作。
reinterpret_cast
是從底層對數據僅僅進行重新解釋,但沒有進行二進制的轉換,依賴具體的平臺,可移植性差;reinterpret_cast
可以將整型轉換爲指針,也可以把指針轉換爲陣列;reinterpret_cast
可以在指針和參照裡進行肆無忌憚的轉換;// RAII 技術封裝
class RefCount {
public:
RefCount() : reference_{0}
{ }
~RefCount() {
this->decrementRef();
}
void IncrementRef()
{
++reference_;
}
bool decrementRef()
{
if (--reference_ == 0) {
delete this;
return true;
}
return false;
}
int64_t use_count() {
return reference_;
}
private:
std::atomic<int64_t> reference_;
};
template <typename T>
class SharedPtr
{
public:
SharedPtr() : ptr_(nullptr) {}
explicit SharedPtr(T* ptr)
: ptr_(ptr),
ref_(new RefCount)
{
if (ref_) ref_->IncrementRef();
}
SharedPtr(const SharedPtr& other)
: ptr_(other.ptr_),
ref_(other.ref_)
{
if (ref_) ref_->IncrementRef();
}
SharedPtr(SharedPtr&& other) noexcept {
ptr_ = other.ptr_;
ref_ = other.ref_;
other.ptr_ = nullptr;
other.ref_ = nullptr;
}
SharedPtr& operator=(const SharedPtr& other) {
if (this == &other || *this == other)
return *this;
reset();
ptr_ = other.ptr_;
ref_ = other.ref_;
ref_->IncrementRef();
return *this;
}
SharedPtr& operator=(SharedPtr&& other) noexcept {
if (this == &other || *this == other) return *this;
reset();
ptr_ = other.ptr_;
ref_ = other.ref_;
other.ptr_ = nullptr;
other.ref_ = nullptr;
return *this;
}
~SharedPtr()
{
if (ref_) this->decrementRef();
}
T& operator*() const { return *ptr_; }
T* operator->() const { return ptr_; }
explicit operator bool() const { return !!ptr_; }
T* get() const { return ptr_; }
void reset() {
if (ptr_) {
this->decrementRef();
// ptr_ = nullptr;
}
}
void decrementRef() {
if(ref_ && ptr_) {
if(ref_->decrementRef()) {
delete ptr_;
ptr_ = nullptr;
}
}
}
int64_t use_count() {
return ref_->use_count();
}
bool unique() {
return use_count() == 1;
}
void swap(SharedPtr & other) {
std::swap(ptr_, other.ptr_);
std::swap(ref_, other.ref_);
}
friend inline bool operator==(SharedPtr const& lhs, SharedPtr const& rhs) {
return lhs.ptr_ == rhs.ptr_;
}
friend inline bool operator!=(SharedPtr const& lhs, SharedPtr const& rhs) {
return lhs.ptr_ != rhs.ptr_;
}
friend inline bool operator<(SharedPtr const& lhs, SharedPtr const& rhs) {
return lhs.ptr_ < rhs.ptr_;
}
private:
T* ptr_;
RefCount* ref_;
};
int main(int argc, char const *argv[]) {
SharedPtr<int> iptr (new int);
SharedPtr<int> iptr2(iptr);
SharedPtr<int> iptr3(std::move(iptr));
SharedPtr<int> iptr4 = iptr2;
SharedPtr<int> iptr5 = std::move(iptr3);
std::cout<<iptr5.use_count()<<std::endl; // 3
return 0;
}
序列式容器
序列式容器會失效的原因是因爲其儲存都是連續的,因此刪除或者插入一個元素都有可能導致其他元素的迭代器失效。
vector
erase
會導致刪除節點之後的全部失效push_back
時,之前的end()
操作得到的迭代器失效insert/push_back
導致capacity()
改變,那麼之前的first()/end()
得到的迭代器會失效insert
一個元素,如果空間沒有分配,那麼插入節點之前的迭代器位置有效,之後的失效。
簡而言之:導致記憶體分配的全會失效,導致元素移動的會區域性失效
deque
關聯式容器
std::unorder_map 解決衝突方式是 拉鍊法(陣列的每個元素都連着一個鏈表?):將所有產生衝突的關鍵字所對應的數據全部儲存在同一個線性鏈表中(bucket)。這個方法解決數據儲存位置發生衝突的雜湊表,整個儲存結構如圖 1 所示。
其中 p_i表示儲存的各個鍵值對。
當使用無序容器儲存鍵值對時,會先申請一整塊連續的儲存空間,但此空間並不用來直接儲存鍵值對,而是儲存各個鏈表的頭指針,各鍵值對真正的儲存位置是各個鏈表的節點。STL
標準庫預設選用vector
容器儲存各個鏈表的頭指針。STL
標準庫中,將圖 1 中的各個鏈表稱爲桶 bucket,每個桶都有自己的編號(從 0 開始)。當有新鍵值對儲存到無序容器中時,整個儲存過程分爲如下幾步:
key
的值帶入設計好的雜湊函數,會得到一個雜湊值: H = hash(key);H % n
),該結果即表示應將此鍵值對儲存到的桶的編號;其他解決衝突的方法
開放地址法:
其中m
是雜湊表的表長,d
是一個增量,當產生衝突時,選擇以下三種方法一種獲取d的值,然後計算,直到計算出的hash 值不存在衝突。
再雜湊法rehasp
當通過雜湊函數求得的雜湊地址同其他關鍵字產生衝突時,使用另一個雜湊函數計算,直到衝突不再發生
上述的拉鍊法解決了雜湊衝突的問題,但是當插入元素很多,產生了嚴重雜湊衝突時,就會導致某個鏈表長度越來越長,進而導致雜湊表的查詢就會退化爲鏈表,效率降低爲O(n)的時間複雜度。
雜湊表儲存結構還有一個重要的屬性,稱爲負載因子load factor
,用於衡量容器儲存鍵值對的空/滿程度:即負載因子越大,意味着容器越滿,即各鏈表中掛載着越多的鍵值對,這無疑會降低容器查詢目標鍵值對的效率;反之,負載因子越小,容器肯定越空,但並不一定各個鏈表中掛載的鍵值對就越少。
負載因子的計算方法爲:負載因子 = 容器儲存的總鍵值對 / 桶數
STL
中預設情況下,無序容器的最大負載因子爲 1.0。如果操作無序容器過程中,使得最大複雜因子超過了預設值,則容器會自動增加桶數,並重新進行雜湊,以此來減小負載因子的值。需要注意的是,此過程會導致容器迭代器失效,但指向單個鍵值對的參照或者指針仍然有效。
因此當插入元素過多,使得負載因子的大於1.0,就會產生rehash行爲,來改善即將下降的效率。
STL中的 unordered_map 的 rehash 策略
一下程式碼節選自g++的STL庫:
// __n_bkt is current bucket count, __n_elt is current element count,
// and __n_ins is number of elements to be inserted. Do we need to
// increase bucket count? If so, return make_pair(true, n), where n
// is the new bucket count. If not, return make_pair(false, 0).
std::pair<bool, size_t>
_M_need_rehash(size_t __n_bkt, size_t __n_elt, size_t __n_ins) noexcept {
if (__n_elt + __n_ins >= _M_next_resize) {
long double __min_bkts = (__n_elt + __n_ins) / (long double)_M_max_load_factor;
if (__min_bkts >= __n_bkt)
// 這個是需要rehash 時返回的策略
return std::make_pair(true,
_M_next_bkt(std::max<size_t>(__builtin_floor(__min_bkts) + 1,
__n_bkt * _S_growth_factor)));
_M_next_resize = __builtin_floor(__n_bkt * (long double)_M_max_load_factor);
return std::make_pair(false, 0);
}
return std::make_pair(false, 0);
}
size_t _M_next_bkt(size_t __n) noexcept {
const auto __max_width = std::min<size_t>(sizeof(size_t), 8); // 8 個位元組
const auto __max_bkt = size_t(1) << (__max_width * __CHAR_BIT__ - 1); // 2 ^ 63
size_t __res = __clp2(__n); // 計算大於等於 n 的最小的2的冪
if (__res == __n) __res <<= 1;
if (__res == 0) __res = __max_bkt;
// Set next resize to the max value so that we never try to rehash again
// as we already reach the biggest possible bucket number.
// Note that it might result in max_load_factor not being respected.
if (__res == __max_bkt)
_M_next_resize = size_t(-1);
else
_M_next_resize = __builtin_ceil(__res * (long double)_M_max_load_factor);
return __res;
}
其中,在這個類的前面有定義:
- _M_max_load_factor 初始化爲 1.0
- static const size_t _S_growth_factor = 2;
整個擴容的策略大致是按照2倍的策略增長,但是並不嚴格按照。在MSVC中按照的是8倍的擴充策略。
STL
中的 雜湊表 是執行緒不安全的(其實 STL
庫都是執行緒不安全的)。 比如兩個執行緒同時向 std::unordered_map 中插入數據,當發生rehash時,如果不加鎖,可能導致兩個執行緒都會產生rehash。
如何優化 多執行緒讀寫操作?這裏要借鑑下java的分段鎖。
紅黑樹的規則
條件1和2說明,紅色節點均是內部節點,其父節點及左、右孩子節點必然存在;另外條件3說明,紅節點之父必爲黑色,因此樹中任一通路都不含相鄰的紅節點
紅黑樹的效率爲什麼比AVL樹高?如果只有查詢操作哪種樹的效率高?
紅黑樹的效率高,是因爲不需要像AVL
樹那樣,爲了維護高度平衡性,而不斷地動態調整以維護左右子樹高度差不超過1.紅黑樹降低了對平衡度的要求,以減少每次插入/刪除操作時的動態調整次數.但是就查詢效率而言,是不如AVL
樹的.
void *Memcpy(void *dst, const void *src, size_t1 size)
{
char *psrc; //源地址
char *pdst; //目標地址
if(NULL == dst || NULL == src)
{
return NULL;
}
if((src < dst) && (char *)src + size > (char *)dst) //源地址在前,對應上述情況2,需要自後//向前拷貝
{
psrc = (char *)src + size - 1;
pdst = (char *)dst + size - 1;
while(size--)
{
*pdst-- = *psrc--;
}
}
else //源地址在後,對應上述第一種情況,直接逐個拷貝*pdst++ = *psrc++即可
{
psrc = (char *)src;
pdst = (char *)dst;
while(size--)
{
*pdst++ = *psrc++;
}
}
return dst;
}
答:自己動手實現memcpy()時就需要考慮地址重疊的情況。我們來看個簡單的例子。有一個5個元素的陣列,不妨設爲int arr = {1,2,3,4,5};考慮2種情況:
1)源地址是arr[2],目標地址是arr[0],自前向後拷貝3個元素後arr爲{3,4,5,4,5}
2)源地址是arr[0],目標地址是arr[2],自前向後拷貝3個元素後arr爲{1,2,1,2,3}
第一種情況,由低地址向高地址逐個將源地址的元素拷貝到目標地址就行,容易;
第二種情況需要注意,如果是按第一種情況由低地址拷貝到高地址,需要分3個步驟把arr[0]=1,arr[1]=2,arr[2]=3三個元素逐個拷貝,重點在於第一步是將arr[0]拷貝到arr[2]的位置,這樣就會把原來的arr[2]=3改爲arr[2]=1,覆蓋了原來的值,因此在這種情況,我們需要自後向前拷貝,也就是高地址向低地址拷貝。也就是第一步將arr[2]放到arr[4],第二步將arr[1]放到arr[3],第一步將arr[0]放到arr[2].
解析:
1、固定陣列
在函數體內分配(不帶static)是在棧
中的
2、固定陣列是全域性變數和帶static字首的區域性陣列是在全域性數據
的
3、固定陣列在類中分配是在堆中的
4、動態陣列
(通過malloc或者new出來的空間)不管在函數體中、類中、全域性變數都是在堆
中
解析:
1)首先char與int都分爲signed與unsigned型別,預設情況下都是signed型別。
2)從長位元組數據型別轉換爲短位元組數據型別,會產生截斷:
如從4位元組的int型別轉換成1個位元組的char型別,則取int數據的最低的一個位元組,將這個位元組的數據賦給char型數據,且是有符號的
,即首位爲符號位;而如果是從int轉換成unsigned char型別,則整個一個位元組都是數據,沒有符號位。
1)從短位元組型別轉換爲長位元組型別,從char轉換爲int:則在前面的三個位元組補符號位,即補上0xffffff
(char的首位爲1),或0x000000
(char的首位爲0)。從unsigned char轉換爲int,則前面補上0x000000
.
解析:
在C語言中,static作用:「改變生命週期」 或者 「改變作用域」。有以下特性:
1)static區域性變數:區域性變數爲動態儲存,即指令執行到定義處才分配記憶體,將一個變數宣告爲函數的區域性變數,使其變爲靜態儲存方式(靜態數據區),那麼這個區域性變數在函數執行完成之後不會被釋放,而是繼續保留在記憶體中。
2)static全域性變數:全域性變數即定義{}
外面,其本身就是靜態變數,編譯時就分配記憶體,這隻會**改變其連線方式,**使其只在本檔案內部有效,而其他檔案不可連線或參照該變數。
3)static函數:對函數的連線方式產生影響,使得函數只在本檔案內部有效,對其他檔案是不可見的。這樣的函數又叫作靜態函數。使用靜態函數的好處是,不用擔心與其他檔案的同名函數產生幹擾,另外也是對函數本身的一種保護機制 機製。如果想要其他檔案可以參照本地函數,則要在函數定義時使用關鍵字extern,表示該函數是外部函數,可供其他檔案呼叫。另外在要參照別的檔案中定義的外部函數的檔案中,使用extern宣告要用的外部函數即可。
到了C++的時候,static多了幾個其他的作用:
4)static類成員變數
:表示這個成員爲全類所共有,對類的所有物件只有一份拷貝,可以藉助類名直接存取。
5)static類成員函數
:表示這個函數爲全類所共有,而且只能存取靜態成員變數,因爲這個函數不接收this指針。
解析:主要有以下區別
1)用``#define MAX 255`定義的常數是沒有型別的(不進行型別安全檢查,可能會產生意想不到的錯誤),所給出的是一個立即數,編譯器只是把所定義的常數值與所定義的常數的名字聯繫起來,define所定義的宏變數在預處理****階段的時候進行替換,在程式中使用到該常數的地方都要進行拷貝替換;
用``const float MAX = 255`;定義的常數有型別****(編譯時會進行型別檢查)名字,存放在記憶體的靜態區域中,在編譯時確定其值。在程式執行過程中const變數只有一個拷貝,而#define所定義的宏變數卻有多個拷貝,所以宏定義在程式執行過程中所消耗的記憶體要比const變數的大得多;
2)用define定義的常數是不可以用指針變數去指向的,用const定義的常數是可以用指針去指向該常數的地址的;
3)用define可以定義一些簡單的函數(宏替換隻作替換,不做計算,不做表達式求解),const是不可以定義函數的.
4)宏定義的作用範圍僅限於當前檔案。 而預設狀態下,const物件只在檔案內有效,當多個檔案中出現了同名的const變數時,等同於在不同檔案中分別定義了獨立的變數。 如果想在多個檔案之間共用const物件,必須在變數定義之前新增extern關鍵字(在宣告和定義時都要加)。
volatile關鍵詞的作用是影響編譯器編譯的結果,用volatile宣告的變數表示該變數隨時可能發生變化,與該變數有關的運算,不要進行編譯優化,以免出錯。
解析:嵌入式問的不多,當作瞭解。
回憶一下小時候查字典的步驟,索引和字典目錄的概念是一致的。字典目錄可以讓我們不用翻整本字典就找到我們需要的內容頁數,然後翻到那一頁就可以。索引也是一樣,索引是對記錄按照多個欄位進行排序的一種展現。對錶中的某個欄位建立索引會建立另一種數據結構,其中儲存着欄位的值,每個值還包括指向與它相關記錄的指針。這樣,就不必要查詢整個數據庫,自然提升了查詢效率。同時,索引的數據結構是經過排序的,因而可以對其執行二分查詢,那就更快了
先說優點:
1)大大加快數據的檢索速度,這也是建立索引的最主要的原因
2)加速表和表之間的連線,特別是在實現數據的參考完整性方面特別有意義。
3)在使用分組和排序子句進行數據檢索時,同樣可以顯著減少查詢中分組和排序的時間。
再說缺點:
1)建立索引需要耗費一定的時間,但是問題不大,一般索引只要build一次
2)索引需要佔用物理空間,特別是聚集索引,需要較大的空間
3)當對錶中的數據進行增加、刪除和修改的時候,索引也要動態的維護,降低了數據的維護速度,這個是比較大的問題。
解析:這是本人在面試CVTE遇到的面試題。
1、支援隨機存取迭代器的(連續儲存空間)vector、deque(雙向存取vector)使用STL的sort函數。
2、不支援隨機存取迭代器的(鏈式非連續儲存空間)list(雙向鏈表)、slist(單向鏈表forward_list),不能使用STL的sort函數,因此都會在類中定義sort()成員函數,使用物件名呼叫即可。
3、關係型容器中基於紅黑樹的set、multiset、map、multimap,本身就有自動從大到小排序的功能。所以不需要sort函數。
4、stack、queue沒有迭代器,各元素的出入口特定,不能進行排序。
5、基於雜湊表的(hash)unordered_set/multiset/map/multimap,都是未排序的,當然因爲計算hash再儲存的特性,也不需要進行排序。
解析:建立連線的時候,伺服器在LISTEN狀態下,收到建立連線請求的SYN報文後,把ACK和SYN放在一個報文裡發送給用戶端。
而關閉連線時,伺服器收到對方的FIN報文時,僅僅表示對方不再發送數據了但是還能接收數據,而自己也未必全部數據都發送給對方了,所以己方可以立即關閉,也可以發送一些數據給對方後,再發送FIN報文給對方來表示同意現在關閉連線,因此,己方ACK和FIN一般都會分開發送,從而導致多了一次。
解析:MSL(Maximum Segment Lifetime),TCP允許不同的實現可以設定不同的MSL值。
第一,保證用戶端發送的最後一個ACK報文能夠到達伺服器,因爲這個ACK報文可能丟失,站在伺服器的角度看來,我已經發送了FIN+ACK報文請求斷開了,**用戶端還沒有給我迴應,**應該是我發送的請求斷開報文它沒有收到,於是伺服器又會重新發送一次,而用戶端就能在這個2MSL時間段內收到這個重傳的報文,接着給出迴應報文,並且會重新啓動2MSL計時器。
第二,防止類似與「三次握手」中提到了的「已經失效的連線請求報文段」出現在本連線中。用戶端發送完最後一個確認報文後,在這個2MSL時間中,就可以使本連線持續的時間內所產生的所有報文段都從網路中消失。這樣新的連線中不會出現舊連線的請求報文。