最近在學習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的函數指針參數正常運行。