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