c++ misc

05 February 2021

Pointer陷阱

Q1
int B = 2;
void func(int *p){p = &B;}
int main(){
     int A = 1, C = 3;
     int *ptrA = &A;
     func(ptrA);
     printf("%d\n", *ptrA);
     return 0;
}

output: 1

原因: func在calling convention中是會複製一份指標變數,而他只到的地方跟ptrA是同樣的地方,等於現在有兩個指標變數都指向變數A的位址,但在func做的是是把裡面的這個指標變數指向B這個位址,但此時ptrA仍然是指向變數A

改法: 如果希望func確切把ptrA指向的地方改為B, 那就必須徹徹底底的把ptrA的指向位址改向B而不是複製一個指標來做這件事,因此改法是先把func的參數改成**p, 然後傳入&ptrA, 這樣等於func吃一個指向指標位址的指標變數,所以*p就會是ptrA,我們就可以操控ptrA要指向哪裡

int B = 2;
void func(int **p){ *p = &B; }
int main(){
     int A = 1, C = 3;
     int *ptrA = &A;
     func(&ptrA);
     printf("%d\n", *ptrA);
     return 0;
}

output: 2

Reference:
medium
hackmd

Call by reference

主要是利用reference operator來做的, 為C++特有的operator, 實作細節應該是利用alias 讓兩個變數名稱指向同個記憶體位置

int &M = n;
代表M的位址指向n的位址, 這裡的&是reference operator
這樣M的值修改也會跟著修改n的值, 所以函式傳遞參數時也是使用reference operator

相對的
int m = &n; 則是address operator
這裡m的值會是n的記憶體位置的整數值

reference operator的幾個特性:

  1. 再initialize時一定要指到某個變數,不能像pointer一樣init時為NULL
  2. init完後指到的位置就不能再變
  3. 在for loop or function call時很好用, 能避免大物件的copy

Reference:
geekforgeek

function pointer

宣告一個指標指向function的instance

void func1(int a, int b)

void *(funcptr)(int,int)  這等於是一個function pointer叫做funcptr 他會指到接收兩個int當參數, 回傳void的function

funcptr = &func1

著名例子就是qsort這種可以定義最後comparison function時, function實作的參數就會用到function pointer

void qsort(void* base, size_t n, size_t size, int (*cmp)(const void*, const void*));
有一個function pointer叫做cmp指到接收兩個參數,回傳int的function

另一個常見的usecase就是multithread中的callback function, 在要平行化的function宣告一個function pointer, 這個function pointer接收的function就是在平行化的function做完後執行這個callback function 他可以根據使用者傳入的function執行不同的function

最後一種就是refactor時常用的, 避免過多的if else, 用function pointer更簡潔

加碼: 閱讀複雜的pointer的句子方法

void ** (*d) (int &, char*) -> 一個function pointer d, 接收一個reference int, 跟char pointer 回傳一個兩層pointer

reference: 閱讀複雜pointer的句子

Constructor & Destructor

用於class, struct建立時預先跑的一段function來初始化class, struct(通常是初始化變數值). 基本上struct跟class是一樣的東西, 可以用variable, member function, contructor, destructor,但struct所有member function, variable都是public, 沒有private, 此外他也沒有繼承的功能

struct trie{
  struct trie *next[26];
  int val;
  trie() val(0){
    for(int i = 0 ; i <26 ; i++){
      this->next[i] = NULL;
    }
  }
  ~trie(){
    for(int i = 0 ; i < 26 ; i++){
      if(this->next[i] != NULL) delete this->next[i];
    }
  }
}

constructor就是一個沒有回傳值的function, function name要是class name或struct name, constructor宣告是可以overload的, 就是可以有複數個constructor, 只要他的paramter不同即可.

Destructor就是在建立這個class/struct的function結束時, 會呼叫destructor來刪除物件, 如果物件本身都是靜態記憶體配置(沒有用到malloc, new) 那其實有沒有destructor通常是沒差, 主要是有動態配置記憶體, function結束,用完時要把它清掉, 不然就memory leak. 如果一個function裡有多個constructor呼叫,那function結束時, destructor呼叫的順序會是反序(stack FILO的方式, 最晚建立的物件會最先被呼叫desctructor)

另一點值得注意的是desctructor沒有overload,也就是一個class只會有一個descturctor,他也不吃任何參數。但destructor可以override,也就是它可以是virtual function, 每個class可以有自己的desctructor然後是繼承自base class, 這樣也比較好,避免明明是在derived class卻呼叫到base class desctructor的窘境。

Class裡的static function是就算class object還沒有建立還是能夠使用的function,但靜態函數不能夠存取class內的非靜態成員資料,一個原因當然是使用static function時不一定有物件建立了

class A{
  static int getA(){...};
}

A::getA()  使用class static function時要使用::這個operator來使用

Class裡的static variable是所有物件的共用變數而不屬於任一個物件,所以他可以被大家存取,static variable也不能使用this, 因為他不屬於物件的一部分

class A{
  static int c;
}
A::c = 4; 可以在不用建立class object時就初始化或修改值

Constructor

copy constructor基本上就是對每個element呼叫memcpy, 對於一般data type沒什麼問題,但對於指標,呼叫memcpy基本上只複製指標這個東西和他指向的位置,並不是複製他的memory data, 所以導致shallow copy. Shallow copy除了非預期的行為外,也很容易在destructor時發生double free的問題,因為兩個object instance內的member都指向同個memory data.

Default and Delete

C++11多了兩個keyword: default and delete, 主要用於constructor部分

一般而言,如果一個class沒有宣告constructor, 編譯器會自動幫忙產生三個constructor, default constructor, copy constructor, assign constructor。 而所謂的default constructor的部分,如果user有宣告其他的constructor,就不會有他,因此如果user希望能夠保留這個default constructor, 就要加=default的字串,這樣就可以確保編譯器還是會加這個constructor。

delete關鍵字則相反,當我不希望compiler幫我建立這些constructor時使用,例如某個class希望他是non-copyable, 那就希望不要有copy constructor,所以就會用delete關鍵字來避免編譯器自動產生constructor

class A{
  A() = default;
  A(int a);
  A(const A&) = delete;
}

Reference: cnblog

Initialize list

在constructor建立物件時,一種init class member的方式,除了語法糖以外,他的主要功能有以下

class A::A(int a) : b(a);

Default Initialize and Value Initialize

Reference:
Value Initialize
Default Initialize
StackOverflow Paranthesis after object declare

c++建立物件以兩種方式: default initialize跟value initialize Default initialize指建立好空間給物件,因此這種使用上要注意,建立的物件事uninit的 value initialize則會把物件的data member也改成指定值

Default Initialize

B b;
B = new B();

這兩種方式建立的物件都是uninit的

default contructor還有一種情況會發生: 被contructor的initialize list忽略的class member也會被default initialize

Value Initialize

B b();
B b{};
B b = b();
B b = new B();

這種方式建立的物件就會value init, 如果沒限定就是zero init, 把值設定成0

friend class/method

friend function代表該function雖然宣告在class內,但跟static一樣該function不屬於該class, 外部的class或其他人都可以直接使用該function, 而friend function一個很大用處是雖然他不屬於class, 但他可以存取class object內的所有成員(public, private…都可以)

class A{
  int fires;
  friend int getfires(Fires obj){return obj.fires;}
};

class B{
  void print(A &obj){
    cout<< getfires(obj)
  }
}

int main(){
  A f;
  getfires(obj)
}

friend class, 如果class A是class B的friend, 則class B可以使用class A的任何成員(public,private…)

class A{
  friend class B;
}
class B{}
  1. friend是單向的, 也就是上述例子class B可以使用class A的任何成員, 但class A不能使用class B的任意成員
  2. friend的關係是不可轉移的,例如class A是class B的friend, class B是class C的friend, 但class C不能夠直接存取class A的內容
  3. friend關係不能被繼承,如果class A是class B的friend,繼承class B的class不能使用class A的成員

Virtual Function

Virtual Function是為了實現C++裡的OOP多型的指令。

多型的概念是在於對於有繼承關係的Class, 可以對於某些function, derived class跟base class有不同的實作跟行為。 做法就是在base class把想要多型(根據class有不同實作的function)宣告virtual, 然後再derived class就可以override這些function

使用Virtual Function方法是宣告一個base class pointer, 然後這個pointer會根據指向的derived class種類來run-time link virtual function, 所以pointer指到derived class A,那這個pointer呼叫virtual function時他就會使用到class A的function實做.

Destructor也可以使用virtual function, 這樣在derived class就可以使用自己的destructor然後也會呼叫到base class的desctructor, 不然如果只有base class有destructor, 然後derived class沒有的話,會呼叫到base class的destructor, 那就有可能導致潛在的memory leak問題, 例如derived class自己擁有的variable使用了new指令, 而base class如果沒有這個變數的話,只呼叫base class constructor會造成memory leak, 這些new的變數不會被清掉。

class X{
  virtual void run(){
    puts("X");
  }
}

class A:X{
  void run(){
    puts("A");
  }
}

int main(){
  X *ptr;
  A sample;
  ptr = &sample;
  ptr->run()  // output A
}

pure virtual function的宣告方式如下, 即在function declaration地方直接後面接 = 0, 這樣代表這個class是一個interface的概念。即有pure virtual function的class不能夠建立object, 而繼承他的class就必須去實作這個function。

class A{
  virtual int print()=0;
}

Virtual Function的特色就是這些function是在run-time link到code section而不是compile time link, 因為他是看指到哪個 實作概念上就是對於這些function會在symbol table上一開始先不填上code section, 而是在跑起來時,根據指到的derived class再填入function的位址。而實際上就是class在建立時會有一個欄位叫做vptr,他會指向這個class的vtable, vtable就是紀錄各個virtual function他指向的code section

此外在程式中, 並沒有class, struct的概念, 他們都是變數, 以primitive type存在運行程式的記憶體中, 所以例如class, struct的建立其實都是編譯時會給他們default的一個function(這個function在code section), 這個function在gdb上看名稱是亂碼, 但基本上就是struct, class的constructor, 自己宣告的constructor同理, 只是皆不同參數, gdb上看也是亂碼函式, 而class的method其實就是function, 所以也是存在code section, 只是method不同於其他function是他編譯時會自動補上第一個參數, this來指名呼叫這個method的object位址, 然後就可以通過this來操控object裡的變數

Reference: vptr,vtable</br> cpp virtual table</br> where does class and method store

Exception

C++有一個stdexcept的header可以用, 裡面定義了一些常見的exception(如logic_error, out_of_range, overflow_error, runtime_error等等)

這些exception class都是繼承自exception這個base class,裡面有一個what的virtual function, 讓各個exception class能夠specify自己的details, 所以可以catch(exception e)然後呼叫e.what()來看細節

Stack unwinding: 用於exception handling, 基本上就是throw exception到catch中間可能會有多層function call(try/catch block跟throw的function中間可能很多層function call), 一旦發生thro exception, throw的function block到catch的function block之間的function, 他們在stack的資料都會被清掉,有點像是function return的感覺,但他實際上是只有清掉這些function在stack上的資料,並沒有真的正常的執行throw exception後面的程式碼,也沒有進行return. 但正常的stack中有的class,他們的 constructor, desctructor會被呼叫, 所以在class object裡面new的東西,理論上發生stack unwinding還是會call desctructor然後delete這些dynamic allocation.

guidlines:

  1. 如果某些錯誤從來就不應該出現,使用assert,例如發生這個錯誤就是要噴出一些明顯的訊息和讓程式terminate, 那就直接用assert就好. exception應該在程式正確執行時仍然可能發生的錯誤, 如input值範圍, file not found, out of memory.
  2. throw exception by value, caught them by reference
  3. 不要再function後面加Exception keywords和說明可能的exception
  4. Exception handle要很小心,stack unwinding可能會導致memory leak, 如果多層function call,裡面某層發生exception的function是由某個外層的function catch, 那中間這些function進行stack unwinding,中間的function如果有new, delete東西, 很容易會沒call到delete導致memory leak. 又例如function allocate的東西後throw exception, 就很有可能沒執行到delete導致memroy leak, 或者catch沒處理好garbage collection, 沒free掉function裡的resource直接跳到其他地方.
  5. keep resource simple, 使用smart pointer來allocate resource
  6. 要值得注意的是constructor如果發生exception, desctructor不會被invoke, 不要讓exception escape from desctructor, exception這裡發生一定要在這裡做完

Reference:
cplusplus
microsoft最佳使用exception的時機
stack unwinding during exception

特殊變數

Volatile

因為compiler會對程式編譯做一定的優化, 例如for迴圈可能會把它unfold或者把迴圈加法變成乘法, 這種行為在multithread環境會有潛在性的問題, 或者是kernel exception/interrupt handler會修改到的變數, 那這時候某些變數也要用volatile避免優化導致變數值跟預期中的不同, 因此volatile的變數就是告訴compiler不要對這個變數的行為做優化, 避免再multithread環境出問題

Reference: liquid0118

Restrict

用來做指標的優化, 如果確定指標是唯一指向data的指標, 不會有其他pointer、變數來存取這個data, 那可以把pointer宣告restrict來提升效能, 優化包括在for loop裡面就可以把restrict pointer的運算unfold, 因為不用擔心其他程式碼會來修改到這份資料, 所以也可以做instruction reordering

explicit

主要用於只有一個argument的constructor, 用途在於避免C++ 自己進行implicit compile導致可能的錯誤。

如果參數型態錯誤的話,C++ compiler會自動幫user做一次參數轉型。但參數型態錯誤這可能是一個bug,而compiler沒抓出來,這樣導致原本不應該傳進去的參數被傳進去,導致constructor建立物件會有問題。

如果constructor宣告explicit的話,這種參數型態錯誤,就會變成compile error而不會自動轉型。

struct Foo {
    // Single parameter constructor, can be used as an implicit conversion.
    // Such a constructor is called "converting constructor".
    Foo(int x) {}
};
struct Faz {
    // Also a converting constructor.
    Faz(Foo foo) {}
};

// The parameter is of type Foo, not of type int, so it looks like
// we have to pass a Foo.
void bar(Foo foo) {};

int main() {
    // However, the converting constructor allows us to pass an int.
    bar(42);
    // Also allowed thanks to the converting constructor.
    Foo foo = 42;
    // Error! This would require two conversions (int -> Foo -> Faz).
    Faz faz = 42;
}

Reference: What does the explicit keyword mean

Why do we need header file?

What is header file?

header file跟cpp檔最大的差異在pre-processing階段就會被處理,在這個階段,macro會被展開,因此header file裡面寫的東西都會被複製一份到include他的cpp檔,因此建議header file都以declaration為主,不然implement在這裡的話內容太多,每個cpp檔都會複製同樣的implementation是不必要的,很佔空間,除非是像template這種東西才能夠寫在裡面。

Declarations and Definition

C++的entity (variable, function…)分成declaration跟definition,任何entity在使用前一定要先declare.

void func();  //forward declaration
int main(){
  func(); //如果沒有forward declare,會報錯
}
void func(){...}

在現在大型project實行modularize的情況下,要在file A使用file B的東西,一個方法就是透過forward declaration, 或者是寫在.h檔裡面,讓compiler把.h裡面的declaration複製進來此cpp

Extern變數: 大型專案內會有多個compile unit(即每個cpp檔都是一個compile unit, 最後透過linker把這些產生的obj檔連成exe), 而當symbol是定義在另一個compile unit時, linker在link多個obj檔時會沒辦法resolve那個symbol產生error, 因此這時候就要對那個變數使用extern的關鍵字,跟compiler說當前這個變數是宣告在其他的scope或compile unit, 讓linker去其他compile unit找。

Definition的意思是某些entity(structure, class, function),他們會需要有更多的細節, compiler才能夠產出machine code來達到預期的目的,而這些細節稱為definition.

microsoft docs:Declaration and definition

ODR (One Definition Rule)

一個Entity在一個程式裡只能有一個definition, 不然會出錯,最常見是function裡面定義了某些structure, class,如果一個執行檔裡多個檔案同時include此header file, 就會產生error, 因為等於同一個structure在這個執行檔裡被定義多次(多個檔案include header, header把structure,class的definition複製到每個檔案中),此時就要用#ifndef,#endif,或者#pragma once,在header中加入這些東西可以避免他被複製多次到執行檔中導致重複定義。

ODR cpprefernce

Back to the question

為什麼要header file: 因為c++設計上使用entity前需要declare, 而又需要modularize的狀況下,就產生的.h檔這種做法,Modularization,讓程式能夠拆小,並且重複使用程式碼,只inlude會用到的函數而不是所有的程式碼都在單一檔案