c++函式引數和返回值

Backsword發表於2023-05-19

c++函式引數和返回值

c++一直以來是一個關注效率的程式碼,這樣關於函式的引數傳遞和返回值的接收,是重中之重。下文提供了一些個人的見解。

函式儲存位置

函式引數在編譯期展開,目前各平臺的編譯期均有不同。

名稱 儲存位置
函式名稱和邏輯 程式碼段儲存
函式引數和返回值 棧中或者暫存器(64位會有6個暫存器使用)
new malloc 的變數

函式引數入棧順序

微軟有幾種編譯期屬性,用來定義函式引數的順序和堆疊。

關鍵字 堆疊清理 引數傳遞
__cdecl 呼叫方 在堆疊上按相反順序推送引數(從右到左)
__clrcall 不適用 按順序將引數載入到 CLR 表示式堆疊上(從左到右)。
__stdcall 被呼叫方 在堆疊上按相反順序推送引數(從右到左)
__fastcall 被呼叫方 儲存在暫存器中,然後在堆疊上推送
__thiscall 被呼叫方 在堆疊上推送;儲存在 ECX 中的 this 指標
__vectorcall 被呼叫方 儲存在暫存器中,然後按相反順序在堆疊上推送(從右到左)

所以直接在函式引數上,呼叫表示式和函式來回去值的話,非常危險

初始化列表

class Init1
{
public:

    void Print()
    {
        std::cout << a << std::endl;
        std::cout << b << std::endl;
        std::cout << c << std::endl;
    }

    int c, a, b;
};

A這個類,可以透過 A a{1,2,3}; 來初始化物件。
看著很美好,但是有幾個問題需要注意。
引數是的入棧順序是跟著類的屬性的順序一致, 當前是 c, a, b;

int i = 0;
Init1 a = {i++, i++, i++};
a.Print();

當我如此呼叫的時候,得到的返回值是 1 2 0
i++的執行順序是從左到右,跟函式呼叫順序無關。 另外不能有 建構函式

	class Init1
	{
	public:
		Init1(int ia, int ib, int ic)
		{
			std::cout << "construct" << std::endl;
			a = ia;
			b = ib;
			c = ic;
		}
		Init1(const Init1& other)
		{
			std::cout << "copy " << std::endl;
			a = other.a;
			b = other.b;
			c = other.c;
		}

		void Print()
		{
			std::cout << a << std::endl;
			std::cout << b << std::endl;
			std::cout << c << std::endl;
		}

		int c, a, b;
	};

當我新增了建構函式的時候。 用下面程式碼測試。會得到兩種結果

void Test_InitilizeList()
{
	int i = 0;
	//Init1 a = { i++, i++, i++ }; // 0 1 2 
	Init1 a(i++, i++, i++); // 2 1 0 
	a.Print();
}

函式的返回值

函式返回值的宣告週期在函式體內。

用引數引用來返回

class Result
{
public:
int result;
};
void GetResult(Result& result) ...

優點:

  • 效率最高,因為返回值的物件在函式體外構造,可以一直套用, 可以一處構造,一直使用。
  • 安全,可以定義物件,並不用new或者malloc, 沒有野指標困擾。
    缺點:
  • 程式碼可讀性低,不夠優美
  • 無法返回nullptr. 一般在 Result 中定義一個; 用來表示一個空物件。
  • 容易賦值到一個臨時物件中,當呼叫GetResult({1}) 會賦值到一個 臨時的 Result 物件中,拿不到返回值。正常來說也不會這樣做。

返回一個引數指標

class Result
{
public:
int result;
};
Result* GetResult() ...

優點:

  • 簡潔明瞭
  • 引數傳遞快速
    缺點:
  • 指標如果在 函式內 static 需要考慮多執行緒。 如果是 new 出來的,多次呼叫效率不高
  • 指標無法重複使用,(可以用 std::share_ptr 增加物件池來解決問題。但會引入新的複雜度。)
  • 需要考慮釋放的問題

返回一個物件

class Result
{
public:
int result;
};
Result GetResult() ...

優點:

  • 沒有記憶體洩露的風險
  • 簡潔明瞭
    缺點:
  • 但有個別編譯期最佳化選項問題,會導致一次構造兩次複製, 第一次是函式體內物件向返回值複製,第二次是 返回值複製給外面接收引數的。
  • 開啟編譯期最佳化選項,並且是 在 return Result 的時候構造返回物件,才能最佳化。

總結

一般如果是 簡單結構體,用 返回一個臨時物件的方式解決。
如果使用 返回一個引數指標,一般改成返回一個id,用一個manager來管理記憶體機制。或者 共享記憶體,記憶體池來解決記憶體洩露後續的問題
用 引數引用來返回的話,一般會這麼定義 int GetResult(Result& result) 函式返回值,用來返回狀態碼,真正的資料,放到 result 中。

函式的幾種變體

inline 函式

  • inline 函式是行內函式,是編譯期最佳化的一種手段,一般是直接展開到呼叫者程式碼裡,減少函式堆疊的開銷。
  • inline 標識只是建議,並不是一定開啟內聯。
  • 函式比較複雜或者遞迴有可能編譯期不展開。
  • dll 匯出的時候,可以不用加匯出標識,會直接匯出到目標處。
  • inline 在msvc的平臺,只要實現標頭檔案中,加不加內聯是一樣的. (警告頂級調到最高/Wall, 不加inline標識的函式會提示,未使用的行內函式將被刪除。)
  • inline 函式比全域性函式更快,但是全域性函式無法定義在標頭檔案中(會報多重定義函式。)所以一般用class 包一層 static inline 函式,用來寫工具類。

函式物件

class A {
public :
    int value;  
    int operator() (int val) {
        return value + val;
    }
}

上述程式碼是一個函式物件,過載operator()得到一個函式物件。
int a = A{10}(1) 會返回11, 顯示構造了一個A{value=10}的物件,然後呼叫過載函式operator(), 返回 10 + 1 = 11
上述程式碼因為是在標頭檔案實現的,所以編譯期會自動把operator()函式當成inline函式,執行效率很高。

lambda 函式

lambda 其實就是一個函式物件,會在編譯期展開成一個函式物件體。

相關文章