C/C++關於普通函數,成員函數,靜態成員函數,函數指針的理解

最近在學習C++11的多線程技術,對std::thread構造函數需要傳入的第一個參數的類型產生的疑惑,在C++11中增加了可調用對象(Callable Objects)的概念,包括以下幾種:

  • 函數指針
  • 重載operator()運算符的類對象
  • lambda表達式
  • std::function

std::thread的第一個參數要求的類型就是Callable Objects,讓我產生疑惑就是第一種——函數指針,因爲在一些書籍裏我看到了很多不同的寫法。比如:

// 第一種
void hello()
{
    printf("thread hello!!!\n");
}
std::thread t1(hello);

// 第二種
class thread_test
{
public:
    thread_test() = default;

    void run()
    {
        printf("thread_test running !!!\n");
    }
};
thread_test obj_test;
std::thread t2(&thread_test::run, &obj_test);

那普通函數、成員函數、靜態成員函數以及在函數名前加"&"到底有什麼區別呢?

(以下僅是些個人理解,我不是彙編大神,不能從更本質的角度分析,如有錯誤請多包涵)

首先是說普通函數、成員函數、靜態成員函數三者的區別:

從內存本質上來說,三者其實沒太大區別,都位於代碼段。成員函數只是比普通函數多了一層類的外殼,調用的時候需要與實際的類關聯。普通函數更像是靜態成員函數,兩者傳入參數的方式比較一致,而非靜態成員函數的區別就是的第一個參數是默認的this指針(當然不需要顯式的調用)。

無論是靜態或者是非靜態成員函數,本質上都是屬於類的,而不是具體對象。從設計模式的角度其實很好理解,因爲這些成員函數對於每個實際對象其實都是一樣的,而且是在class設計實現階段就定死不會變化的,所以只需要在內存代碼段保留一份即可。而類中對於每個對象可能不同的部分,比如成員變量、虛函數表等,這些可能在程序執行過程中發生動態變化,所以需要和具體對象綁定在一起,有多少個對象,就有多少個成員變量、虛函數表。

總結如下:

成員函數是和類綁定在一起的,有且僅有一份,位於代碼段。

成員變量、虛函數表等不同對象可能不同值的部分,和具體所屬的對象綁定在一起,位於棧上或堆上(new)。

 

再者聊聊普通函數、成員函數、靜態成員函數、函數名前加"&"以及函數指針的關係

函數名可以直接賦值給函數指針,這類似於數組名可以直接賦值給指針的用法。函數名和數組名從本質上來說都不是指針類型,執行賦值操作實際是編譯器幫我們做了隱性的轉換,它會默認幫我們在函數名或者數組名前加上取地址符"&"。比如上面的例子,函數名hello和&hello從數值上來說是一樣的,其實這是很匪夷所思的,函數指針不應該函數hello存放的地址嗎,怎麼加不加"&"一樣呢?從個人角度來說,加上取地址符更符合我的理解。很多時候,越簡練的程序理解起來就越困難,這就是代價吧。

成員函數和靜態成員函數也類似,給函數指針賦值或者當做函數指針參數傳入時,函數名前加不加"&"都可以。用一個實際的程序例證下。

#include <iostream>

void hello()
{
    printf("thread hello!!!\n");
}

class test
{
public:
    test() = default;

    void run()
    {
        printf("test running !!!\n");
    }

    static void static_run()
    {
        printf("test static running !!!\n");
    }
};

int main()
{
    printf("hello = 0x%x\n", hello);
    printf("&hello = 0x%x\n\n", &hello);

    printf("test::run = 0x%x\n", test::run);
    printf("&test::run = 0x%x\n\n", &test::run);

    printf("test::static_run = 0x%x\n", test::static_run);
    printf("&test::static_run = 0x%x\n\n", &test::static_run);

    getchar();
    return 0;
}

實際運行結果:

hello = 0x401080
&hello = 0x401080

test::run = 0xffffcba0
&test::run = 0xffffcba0

test::static_run = 0x401840
&test::static_run = 0x401840

可以看到,普通函數、靜態成員函數、非靜態成員函數,函數名與函數名前加"&"的值本質上是一樣的。

創建thread線程時,函數名前需要加取地址符嗎?

答案是加不加都可以。理由同上,C++編譯器會自動爲我們添加取地址符,特殊的是非靜態成員函數至少需要一個參數(this指針),所以創建線程時必須得有一個實際的對象爲我們提供這個this地址,否則編譯是通不過的。Talk is cheap, show me the code 上代碼驗證。

#include <iostream>
#include <thread>

void hello()
{
    printf("thread hello!!!\n");
}

class thread_test
{
public:
    thread_test() = default;

    void run()
    {
        printf("class thread_test running !!!\n");
    }

    static void static_run()
    {
        printf("class thread_test static running !!!\n");
    }
};

int main()
{

    thread_test obj_test;

    std::thread t1(hello);
    std::thread t2(&hello);
    t1.join();
    t2.join();
    std::cout << std::endl;

    std::thread t3(thread_test::static_run);
    std::thread t4(&thread_test::static_run);
    t3.join();
    t4.join();
    std::cout << std::endl;
    
    std::thread t5(thread_test::run, &obj_test);
    std::thread t6(&thread_test::run, &obj_test);
    t5.join();
    t6.join();

    getchar();
    return 0;
}

實際運行結果:

thread hello!!!
thread hello!!!

class thread_test static running !!!
class thread_test static running !!!

class thread_test running !!!
class thread_test running !!!

可以看到普通函數、靜態成員函數、非靜態成員函數三者函數名前,無論加不加"&"都可以作爲std::thread的函數指針參數正常運行。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章