十九、函數(二)

来源:https://www.cnblogs.com/piaolaipiaoqu/archive/2023/12/01/17871109.html
-Advertisement-
Play Games

十九、函數(二) 1、函數參數之接受不定量參數 1)普通函數不定量傳參用法 //接受不定量參數的函數 #include <cstdarg> //引入頭文件cstdarg int Add(unsigned count, ...) //第一個參數為參數的個數,第二個參數為三個. { int rt{}; ...


十九、函數(二)

1、函數參數之接受不定量參數

1)普通函數不定量傳參用法

//接受不定量參數的函數
#include <cstdarg>       //引入頭文件cstdarg
int Add(unsigned count, ...)  //第一個參數為參數的個數,第二個參數為三個.
{
	int rt{};
	char* c_arg; //聲明一個指針變數
	va_start(c_arg, count); //將參數數據指針賦值給c_arg
	for (int i = 0; i < count; i++) rt += va_arg(c_arg, int);
	va_end(c_arg);  //釋放指針
	return rt;
}

std::cout << Add(5, 1, 2, 3, 4, 5); //函數參賽數,需要依次傳入各個參數

2)示例:計算多個數的平均值

//通過不定量參數函數,求多個數的平均數
#include <iostream>
#include <cstdarg>

int Average(unsigned count, ...)
{
	va_list  arg;          //va_list 是一個char類型的指針,相當於char* arg;
	va_start(arg, count);  //第一個參數為接受數據的指針,第二個參數為參數的個數,目的是為了將參數的地址放到arg中。此處做了一個記憶體分配給arg
	int sum{};
	
	for (int i{}; i < count; i++)
	{
		//註:每調用一次va_arg()函數,都會將參數切換至下一個;va_arg()第一個參數為指針,第二個參數為參數的類型         
		sum += va_arg(arg, int);   //相當於把arg當成int類型的值解讀,每解讀一個,則切換為下一個
		std::cout << "arg地址:" << (int)arg << std::endl;  //本質還是利用了連續的記憶體空間
	}
	va_end(arg);  //釋放arg記憶體
	sum = sum / count;

	return sum;
}

int main()
{
	int x = Average(5, 221, 331, 202, 555, 776);
	std::cout << "平均數為:" << x << std::endl;
}


3)自己設計一個函數,計算多個數的平均值

//自己設計一個函數,計算多個數的平均值
#include <iostream>

struct Sarg
{
	int count; //統計參數的個數
	char* cMem; //參數的地址
};
int Avg(Sarg& y)
{
	int sum{};
	int* arg = (int*)y.cMem;
	for (int i = 0; i < y.count; i++)
	{
		sum += arg[i];
	}
	return sum / y.count;
}

void main()
{
	Sarg y;
	y.count = 5;
	y.cMem = (char*)new int[5]{ 221, 331, 202, 555, 776 };
	int x = Avg(y);
	std::cout << "平均數為:" << x << std::endl;

}

2、函數返回之返回指針和引用

1)項目設計:設計一個函數,能夠讓我們直接為c字元串賦值

//設計一個函數,能夠讓我們直接為c字元串賦值,如
char* str;
str = cstr("你好");
std::cout<<str;
//輸出你好
//直接使用強制類型轉化,將一個字元串進行賦值
#include <iostream>

int  main()
{
	char* str;
	str = (char*)"你好";  //強制類型轉化,"你好"是一個常量;強行的使得str指向了"你好"的地址
	//str沒有自己的記憶體空間,str只是"你好"字元串的一個副本
	std::cout << str << std::endl;
	//str[0]=0;  不允許修改值,因為指向的是一個常量的記憶體地址
}
//通過函數輸出字元串
#include <iostream>

//求字元串占用多少記憶體的函數
int clen(const char* str)   
{
	int i;
	for (i = 0; str[i]; i++);   //當字元串最後一位為0,表示字元串結束
	return ++i;
}
char* cstr(const char* str)
{
	//將字元串傳遞出去
	int len = clen(str);   //求出字元串長度
	//char strRt[0x20];  //錯誤,因為此處strRt為局部變數,沒有自己的記憶體空間,必須返回一個指針
	char* strRt = new char[len];
	memcpy(strRt, str, len);   //memcpy(目標,源,長度)
	return strRt;
}

int  main()
{
	char* str;
	str = cstr("你好");  //強制類型轉化,"你好"是一個常量;強行的使得str指向了"你好"的地址
	std::cout << str << std::endl;
}

註:返回指針時,一定不能返回一個局部變數

2)項目設計:游戲麟江湖新手村有6中怪物,要求設計一個函數來創建怪物,怪物結構如下:

typedef struct Role
{
    char* Name;
    int Hp;
    int maxHp;
    int Mp;
    int maxMp;
}*PROLE;
//性能損耗較大
#include <iostream>

typedef struct Role
{
    char* Name;
    int Hp;
    int maxHp;
    int Mp;
    int maxMp;
    int lv;
}*PROLE,ROLE;

int clen(const char* str)   
{
	int i;
	for (i = 0; str[i]; i++);   //當字元串最後一位為0,表示字元串結束
	return ++i;
}
char* cstr(const char* str)
{
	//將字元串傳遞出去
	int len = clen(str);   //求出字元串長度
	//char strRt[0x20];  //錯誤,因為此處strRt為局部變數,沒有自己的記憶體空間,必須返回一個指針
	char* strRt = new char[len];
	memcpy(strRt, str, len);   //memcpy(目標,源,長度)
	return strRt;
}

ROLE CreateMonster(const char* str, int Hp, int Mp)
{
	Role rt{ cstr(str),Hp,Hp,Mp,Mp,1 };
	return rt;  //將整個結構體的成員進行了返回,性能損耗較大
}
int main()
{
	ROLE role = CreateMonster("aoteman", 1500, 1500);  //實際項目中,不會使用結構體實體創建對象,因為性能損耗非常大
	std::cout << role.Name << std::endl;
	std::cout << role.Hp << "/" << role.maxHp << std::endl;
}
//上述代碼優化,函數返回指針
#include <iostream>

typedef struct Role
{
	char* Name;
	int Hp;
	int maxHp;
	int Mp;
	int maxMp;
	int lv;
}*PROLE, ROLE;

int clen(const char* str)
{
	int i;
	for (i = 0; str[i]; i++);   //當字元串最後一位為0,表示字元串結束
	return ++i;
}
char* cstr(const char* str)
{
	//將字元串傳遞出去
	int len = clen(str);   //求出字元串長度
	//char strRt[0x20];  //錯誤,因為此處strRt為局部變數,沒有自己的記憶體空間,必須返回一個指針
	char* strRt = new char[len];
	memcpy(strRt, str, len);   //memcpy(目標,源,長度)
	return strRt;
}

PROLE CreateMonster(const char* str, int Hp, int Mp)  
{
	PROLE rt = new Role{ cstr(str),Hp,Hp,Mp,Mp,1 };  //申請一個結構體rt類型大小的記憶體空間
	return rt;  //返回值是一個指針
}
int main()
{
	PROLE role = CreateMonster("aoteman", 1500, 1500);  //實際項目中,不會使用結構體實體創建對象,因為性能損耗非常大
	std::cout << role->Name << std::endl;
	std::cout << role->Hp << "/" << role->maxHp << std::endl;
}

//函數返回一個引用
#include <iostream>

typedef struct Role
{
	char* Name;
	int Hp;
	int maxHp;
	int Mp;
	int maxMp;
	int lv;
}*PROLE, ROLE;

int clen(const char* str)
{
	int i;
	for (i = 0; str[i]; i++);   //當字元串最後一位為0,表示字元串結束
	return ++i;
}
char* cstr(const char* str)
{
	//將字元串傳遞出去
	int len = clen(str);   //求出字元串長度
	//char strRt[0x20];  //錯誤,因為此處strRt為局部變數,沒有自己的記憶體空間,必須返回一個指針
	char* strRt = new char[len];
	memcpy(strRt, str, len);   //memcpy(目標,源,長度)
	return strRt;
}

ROLE& CreateMonster(const char* str, int Hp, int Mp)   //返回一個引用
{
	PROLE rt = new Role{ cstr(str),Hp,Hp,Mp,Mp,1 };  //申請一個結構體rt類型大小的記憶體空間
	return *rt;  //  rt表示指針,*rt標誌指針的值。若此處是個控制在程式會報錯,因為引用必須初始化
}
int main()
{
	Role& role = CreateMonster("aoteman", 1500, 1500);  
	std::cout << role.Name << std::endl;           //引用需要使用實體調用結構體成員變數
	std::cout << role.Hp << "/" << role.maxHp << std::endl;
}

3)傳遞引用參數時的類型轉化

//傳遞引用參數時存在一個隱士的類型轉化
#include <iostream>

int Add1(int a, int b)
{
    return a + b;
}
int Add2(int& a, int& b)
{
    return a + b;
}
int main()
{
    float a = 200.0f;
    float b = 125.53f;
    std::cout << Add1(a, b) << std::endl;  //如果函數的參數不是引用,可以直接傳入其他類型的值
    std::cout << Add2(a,b) << std::endl;  //錯誤。如果函數的參數是引用,必須傳入對於引用類型的值,否則報錯
}

總結:如果函數的參數不是引用,可以直接傳入其他類型的值;如果函數的參數是引用,必須傳入對於引用類型的值,否則報錯

4)數組的引用

//int類型的引用定義
int a;
int & b=a;

//數組的引用定義
int c[100];
//int & d[100]=c;  //此寫法錯誤
int (&e)[100]=c;  //創建c的引用e,且e的數組長度必須和c的一致。e首先要是個引用,且e中有100個元素
//數組的引用用法
//缺點:若數組的元素不固定,則無法進行定義
#include <iostream>

void ave(int (&art)[5])   //傳入數組引用參數
{
	std::cout << sizeof(art) << std::endl;
	for (auto x : art)std::cout << x << std::endl;
}
int main()
{
	int a[5]{1,2,3,4,5};
	ave(a);
}

3、函數參數之右值引用

左值:有著明確的記憶體空間,可以往裡面寫入值,就叫做左值。如int c = 320,則c就是一個左值

右值:臨時空間存放的值,無法往裡面寫入值,就叫做右值。如上面的230+250。

//右值引用語法
int&& a = 320+230;   //右值引用指向的是臨時的值
//a = 1500; //錯誤,無法給右值引用進行傳值

右值引用可以解決上述問題,並且可以節省變數

#include <iostream>

void Add(int&& a)   //右值引用
{
	std::cout << a << std::endl;
}
int main()
{
	Add(320 + 250);  //如果函數的參數是一個引用,服務直接進行計算傳值
}

//右值引用示例
#include <iostream>
struct Role
{
	int Hp;
	int Mp;
};

Role CreateMonster()
{
	Role rt{ 100,200 };
	return rt;
}

void show(Role&& r1)   //使用右值引用,沒有再創建變數,而是直接接受CreateMonster()傳遞過來的rt
{
	std::cout << r1.Hp << std::endl;
	std::cout << r1.Mp << std::endl;
}

int main()
{
	show(CreateMonster());
}

4、函數的本質

1)分析函數彙編代碼時,先將調試方式設置為release,再打開項目屬性頁,將C/C++優化功能關閉

2)彙編代碼指令說明:

//部分彙編代碼指令說明:
push   x        //將x的內容方放到臨時變數的記憶體區域(棧)
call   x        //讓CPU去執行記憶體地址X處的代碼
ret           //讓CPU返回跳轉前的位置
//C++函數
#include <iostream>

int Add(int a, int b)
{
	return a + b;
}

int main()
{
	int c = Add(1, 2);
	std::cout << c;
}

//彙編代碼
int Add(int a, int b)
{
00F71000  push        ebp            //將ebp放如是臨時變數區,即棧區    
00F71001  mov         ebp,esp  //esp表示棧的位置,ebp=esp
	return a + b;
00F71003  mov         eax,dword ptr [ebp+8]  //將[ebp+8]記憶體地址中的值放入到eax寄存器
00F71006  add         eax,dword ptr [ebp+0Ch]  //eax=eax+[ebp+0Ch]記憶體地址中的值,即a和b的加法操作
}
00F71009  pop         ebp  
00F7100A  ret               //ret表示返回值跳轉前的位置
int main()
{
00F71010  push        ebp  
00F71011  mov         ebp,esp  
00F71013  push        ecx  
	int c = Add(1, 2);
00F71014  push        2    //push用戶給函數傳遞參數,即將2推送至棧區
00F71016  push        1    //先push最後一個參數
00F71018  call        00F71000  //call表示CPU跳轉到目標地址去指向,即此處CPU跳轉到00F71000地址
00F7101D  add         esp,8  
00F71020  mov         dword ptr [ebp-4],eax  
	std::cout << c;
00F71023  mov         eax,dword ptr [ebp-4]  
00F71026  push        eax  
00F71027  mov         ecx,dword ptr ds:[00F72038h]  
00F7102D  call        dword ptr ds:[00F72034h]  
}
00F71033  xor         eax,eax  
00F71035  mov         esp,ebp  
00F71037  pop         ebp  
00F71038  ret    //函數尾,有幾個ret就有幾個函數

3)函數的本質

​ 經過上面的分析,可知函數的本質是一段記憶體里的二進位數據,我們寫下的C++代碼會翻譯成對應的二進位數據,程式運行的時候會通過某種規則來載入到我們的記憶體里,一個程式一旦編譯(生成),這個程式的二進位數據就不會再發生變化

​ ①程式的生成:C++代碼=>二進位數據=>程式文件(硬碟)

​ ②程式的運行:程式文件(硬碟)=>載入到記憶體中

註:函數名的本質就是一個記憶體地址

#include <iostream>
#include <bitset>
int Add(int a, int b)
{
	return a + b;
}

int main()
{
	int c = Add(1, 2);
	std::cout <<"函數名的地址為:"<< Add << std::endl;;
	char* str = (char*)Add ;
	for (int i = 0; i < 30; i++)     //將函數的內容顯示出來
	{
		std::cout << std::bitset<8>(str[i]) << std::endl;  //函數的內容2進位表示
		//std::cout << std::hex<<(unsigned)str[i] << std::endl;   //函數的內容16進位表示
		//printf("%X\n", (unsigned char)str[i]);
	}
}

5、函數指針

1)函數指針聲明

//函數指針聲明語法
函數返回類型 (*函數指針變數名)(參數類型 參數名稱,......參數類型 參數名稱);

//示例
int (*pAdd)(int a,int b)
//函數指針簡單用法
#include <iostream>

int Add(int a, int b)
{
	return a + b;
}
int Add_X(int a, int b)
{
	return (a + b)/2;
}

int main()
{
	int (*pAdd)(int c, int d) {Add};  //申明一個函數指針,並將其初始化為函數Add的地址

	std::cout << pAdd(100, 200) << std::endl;
	std::cout << "函數指針大小為:"<<sizeof(pAdd(100, 200)) << std::endl;

	char (*pAdd_X)(int ,int ) { (char (*)(int,int))Add_X };  //如果函數的返回值類型和函數指針的返回值類型不同,需要進行強制類型轉化
	std::cout << pAdd_X(110, 20) << std::endl;
}

2)函數指針的類型的自定義

//通過typedef自定義函數指針的類型
#include <iostream>

//把(char (*)(int, int)類型定義為新的類型pFadd
typedef char(*pFadd)(int, int);  //聲明函數指針類型


int Add_X(int a, int b)
{
	return (a + b) / 2;
}

int main()
{
	pFadd pAdd_X = (pFadd)Add_X;   //pFadd就相當於(char (*)(int, int)
	std::cout << pAdd_X(110, 20) << std::endl;
}
//通過using自定義函數指針的類型
#include <iostream>

//把(char (*)(int, int)類型定義為新的類型pFadd
using pFadd =  char(*)(int, int);  //聲明函數指針類型

int Add_X(int a, int b)
{
	return (a + b) / 2;
}

int main()
{
	pFadd pAdd_X = (pFadd)Add_X;   //pFadd就相當於(char (*)(int, int)
	std::cout << pAdd_X(110, 20) << std::endl;
}

3)函數指針和指針函數

①函數指針本事是個指針,即一個可以指向特定類型函數的指針,如int (*pAdd)(int a,int b);

②指針函數是指一個返回指針的函數,如int* xAdd(int a,int b);

//函數指針類型也可以被當作函數參數
#include <iostream>

using pRole = int(*)(int hp, int mp);   //自定義一個函數指針類型

int Test(int a,int b,pRole x)   //傳入一個函數指針類型
{
	return x(a, b);
}

int Add(int a, int b)
{
	return a + b;
}

int main()
{
	pRole pAdd{ Add };  //聲明一個函數指針
	std::cout << Test(100, 200, pAdd);  //將100,200傳入函數指針
}

6、從函數的角度認識棧

1)棧和棧的意義

​ 我們都知道,變數的本質是對應的記憶體空間,因此每個變數都需要獨立的記憶體空間,問題是,在實際開發過程中,一個函數可能會被反覆調用,如果每次都分配記憶體空間,那麼系統開銷將非常大,如果為這樣的變數都分配固定的記憶體空間,又非常的浪費記憶體資源,所以才有了棧的感念,棧的本質是一段提前分配好的記憶體空間,主要就是用來存放臨時變數!這樣我們只需要管理好棧的讀寫就可以避免頻繁的記憶體分配和不必要的記憶體浪費!

​ 棧是連續的記憶體空間。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • SQL Server中的存儲過程 什麼是存儲過程? 存儲過程是一段預先編寫好的 SQL 代碼,可以保存在資料庫中以供反覆使用。它允許將一系列 SQL 語句組合成一個邏輯單元,併為其分配一個名稱,以便在需要時調用執行。存儲過程可以接受參數,使其更加靈活和通用。 存儲過程語法 創建存儲過程的語法如下: ...
  • 歡迎來到袋鼠雲08期產品功能更新報告!在瞬息萬變的市場環境中,我們深知客戶的需求與期待,因此,我們及時推出袋鼠雲最新產品更新及優化,包括數據治理中心、Hive SQL 性能優化、新插件等,助力企業在數字世界中勇往直前。 以下為袋鼠雲產品功能更新報告08期內容,更多探索,請繼續閱讀。 離線開發平臺 新 ...
  • Apache Paimon是一個流式數據湖平臺。致力於構建一個實時、高效的流式數據湖平臺。這個項目採用了先進的流式計算技術,使企業能夠實時處理和分析大量數據。Apache Paimon 的核心優勢在於它對於大數據生態系統中流式處理的支持,尤其是在高併發和低延遲方面表現出色。 目前業界主流數據湖存儲格 ...
  • 最近聽說好多App都被下架處理了,隱私合規管理特別嚴格。隔壁王老闆公司旗下的一款App就被通報了,說是嵌入的第三方SDK違規收集用戶個人信息。 還記得,在2021年的315晚會,上海、北京有幾家公司都被報道,其SDK均在未經用戶授權,竊取用戶個人信息。涉案App有 50多款,嚴重侵害了用戶權益,播出 ...
  • 前段時間,一個資訊類APP(以下稱“某APP”)的負責人急匆匆找到網安雲,直言其負責的APP最近收到很多用戶投訴,說他們的信息被泄露了,屢遭電銷騷擾。由於電銷太過猖狂,導致很多用戶都到應用市場給他們發差評,對品牌形象塑造和業務發展影響極大! 同時,他們也收到了本地通信管理局的限期整改通知書,責令他們 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 前言 有個朋友說前端技能大家大部分都會,就是部署項目這一塊經驗都比較稀缺,一直很想學一下。所以在這裡寫一篇簡單的從零開始部署前端項目的全過程,感興趣的掘友們或者想自己搭建項目部署的可以看一下這篇。 環境搭建 首先我們需要進行環境搭建主要就 ...
  • TS中的類系統對比起JS完善了許多,知識點包括但不限於可訪問性、繼承類、實現介面、訪問器、泛型、抽象類。 ...
  • acwing week2 基礎演算法3總結 總結點1:雙指針演算法 //常用模版框架 for (int i = 0, j = 0; i < n; i ++ ) { while (j < i && check(i, j)) j ++ ; } 常見問題分類: (1) 對於一個序列,用兩個指針維護一段區間 ( ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...