目录
一、值传递(传值函数)
1.BayMer程序1 计算三个数的和
2.文字说明
二、地址传递
1.BayMer程序2 交换两个数
2.文字说明
三、模板函数
1.BayMer程序3 多类型大小比较器
2.文字说明
四、引用传递
1.Baymer程序4 利用引用传递计算三位数乘法
2.文本说明
五、返回值
1.以值的方式返回
2.以地址的方式返回(错误做法)
3.以引用的方式返回
六、函数重载
七、全文巩固习题
#include
int add(int a, int b, int c)
{
return a + b + c;
}
int main()
{
using namespace std;
int num1 = 1, num2 = 2, num3 = 3;
int sum = add(num1, num2, num3);
cout << "1+2+3=" << sum << endl;
return 0;
}
在Baymer程序1中,主函数中的num1、num2、num3为实参,add函数中的a、b、c为形参。在运行时,实参复制给形参。复制过程中调用了形参所属数据类型的拷贝构造函数。在类型允许隐式转换的前提下,若实参与形参的数据类型不同,将会发生类型转换。
此处简述类型转化。上述代码中,若将add函数的形参a改为double类型,则num1赋值给a时,会发生隐式类型转换。但在有些情况下,如a仍是为string类型,num1无法转换为string类型。
因为add函数中的形参a、b、c是对num1、num2、num3的值拷贝,a、b、c与num1、num2、num3指向的内存空间各不相同。当add函数运行结束后,各形参调用相应数据类型的析构函数来释放形式参数。当一个函数运行结束时,形参的值不会被复制给对应的实参。因此,此时,若对a、b、c进行修改,主函数中的num1、num2、num3不会发生任何变化。即,值传递不会修改实参的值。
#include
void swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
int main()
{
using namespace std;
int num1 = 10;
int num2 = 20;
swap(&num1, &num2);
cout << "The value of num1 and num2 after runing swap:" << endl;
cout << "num1 = " << num1 << endl;
cout << "num2 = " << num2 << endl;
return 0;
}
在主函数中,调用swap函数时,将num1及num2的地址复制给swap函数的指针参数。此时a与num1、b与num2指向同一块内存空间。因而,swap中对a、b所指向的内存空间的改值操作,必定会导致主函数中对应变量的变化。
该方式需要对指针进行操作,解引用(取*操作)让操作变得相对繁琐。且对指针的操作,可能引发不可预知的错误。因此,对实参所指向的内存空间进行操作时,并不推荐使用地址传递,而更推荐引用传递(下下部分说明)。
#include
template
void compare(T a,T b)
{
if(a > b)
{
cout << "Larger" << endl;
}
else if(a < b)
{
cout << "Smaller" << endl;
}
else
{
cout << "Equal" << endl;
}
}
int main()
{
using namespace std;
//整型比较
int num1 = 5;
int num2 = 8;
compare(num1, num2);
//浮点型比较
double d1 = 1.5;
double d2 = 6.8;
compare(d1, d2);
//字符型比较
char c1 = 'a';
char c2 = 'z';
compare(c1, c2);
return 0;
}
假设,我们需要对5和8进行比较、1.5和6.8进行比较、‘a’和‘z’进行比较。为每个类型编写一个比较函数的话,整个程序就过于冗余。与其对每一种可能的形参类型编写一个对应函数的新版本,不如编写一段通用代码。这也就是模板函数。
在BayMer程序3中,编写了一个用于比较大小的通用类型模板函数。在整型比较时,编译器将模板函数中的T替换为int。同理,在double型比较时,编译器将模板函数中的T替换为double,以此类推。将compare编写为模板函数后,我们就不必了解形参的类型了(用户自定义类型可能无法进行直接比较)。
#include
template
T mul(T& a, T& b, T& c)
{
return a * b * c;
}
template
T mulConst(const T& a, const T& b, const T& c)
{
return a * b * c;
}
int main()
{
using namespace std;
int num1 = 1;
int num2 = 2;
int num3 = 3;
int ret = mul(num1, num2, num3);
cout << "ret = " << ret << endl;
ret = mulConst(num1, num2, num3);
cout << "ret = " << ret << endl;
return 0;
}
在Baymer程序1中,由于函数add为值传递方式,因此在传入a、b、c时需要调用其对应类型的拷贝构造函数,返回a、b、c相加的结果时,需要调用一次拷贝构造函数和a、b、c三个变量对应类型的析构函数。若此时函数的参数是数组,需要传递的参数是1000*1000的矩阵,传入参数时需要调用100万次拷贝构造函数,返回时要调用100万次析构函数。这显然是很浪费实践和空间的。
在上述情况下,若将a、b、c转换为引用类型(如代码中的mul函数所示),此时传入a、b、c的值时,并不会调用拷贝构造函数,函数执行结束后也不会调用对应类型的析构函数。此时的a、b、c相当于给传入num1、num2、num3取别名,它们指向的内存空间与num1、num2、num3,类似于地址传递,但使用它们的时候,并不需要解引用操作(即取*操作)。
上述的mul函数还存在一定问题。若主函数只希望mul函数进行乘法操作,但却被mul函数中的某个操作修改了数值。怎么避免这种情况呢?这就需要常量引用参数了,在引用参数前加上const关键字,就构成了常量引用参数。此时,mulConst中,若有操作想试图修改参数数值,编译器将会报错。
参数有值传递、地址传递、引用传递。那返回值是否有对应的值返回、地址返回、引用方式返回呢?
#include
using namespace std;
class MyType
{
public:
MyType(int num)
{
this->m_num = num;
cout << "构造函数" << endl;
}
MyType(const MyType& myType)
{
this->m_num = myType.m_num;
cout << "拷贝构造函数" << endl;
}
~MyType()
{
cout << "析构函数" << endl;
}
int m_num;
};
MyType addOne(MyType num)
{
num.m_num += 1;
return num;
}
int main()
{
MyType num1(5);
addOne(num1);
cout << "after add one: " << num1.m_num << endl;
return 0;
}
程序运行结果:
构造函数
拷贝构造函数
拷贝构造函数
析构函数
析构函数
after add one: 5
析构函数
我们从运行结果分析整个程序的运行过程:在创建num1时必定调用MyType的构造函数。调用addOne函数时,num1复制给num,因此调用了拷贝构造函数。在函数完成加1操作后,程序在栈区开辟一个临时的内存空间用于存储返回值,num复制给临时返回值,因此调用了拷贝构造函数。在addOne函数运行结束后,调用了临时返回值的析构函数和num的析构函数。主函数输出运行结束后,调用num1的析构函数。
由上述分析可知,值返回过程中会在栈区开辟临时内存空间。
#include
using namespace std;
int* add(const int& a, const int& b)
{
int sum = a + b;
return ∑
}
int main()
{
int num1 = 1;
int num2 = 2;
int* ret = add(num1, num2);
cout << "ret = " << *ret << endl;
cout << "ret = " << *ret << endl;
cout << "ret = " << *ret << endl;
return 0;
}
程序运行结果:
ret = 3
ret = 2047187336
ret = 2047187336
我们从运行结果分析整个程序运行过程:num1、num2被引用传递至add函数中后,add函数在栈区为sum开辟一个内存空间,并保存num1和num2的结果。函数运行结束后,释放函数中所有局部变量的地址空间。之后,再将sum的地址返回给主函数。
在函数运行结束后,sum的地址空间已经被释放。此时,对sum原地址的访问本就是非法访问,可能导致程序错误或崩溃。第一次对ret进行解引用能输出正确结果,是因为编译器对结果进行了保留。后续再对ret进行解引用,就无法得到正确结果了。
#include
using namespace std;
const int& addConst(const int& a, const int& b)
{
int sum = a + b;
return sum;
}
int& add(const int& a, const int& b)
{
int sum = a + b;
return sum;
}
int main()
{
int num1 = 1;
int num2 = 2;
int ret = add(num1, num2);
cout << "ret = " << ret << endl;
const int constRet = addConst(num1, num2);
cout << "constRet = " << constRet << endl;
return 0;
}
我们从运行结果分析整个程序运行过程:将num1、num2传递给add函数后,add函数在栈区为sum开辟内存空间,并存储num1和num2的和。在函数运行结束前,将sum以引用的方式返回,主函数使用ret进行接收。retConst运行方式类似。
在函数运行结束前,使用的引用返回方式等价于"int& ret = sum",即sum的地址空间现在被ret所指向,内存空间不会被释放。
#include
using namespace std;
// ---1号---
int add(int& a, int& b)
{
cout << "int& a, int& b" << endl;
return a + b;
}
// ---2号---
int add(const int& a, const int& b)
{
cout << "const int& a, const int& b" << endl;
return a + b;
}
// ---3号---
int add(const int& a, const int& b, const int& c)
{
return a + b + c;
}
// ---4号---
int add(const int& a, const double& b)
{
return a + b;
}
// ---5号---
int add(const double& b, const int& a)
{
return a + b;
}
// ---6号---
template
Ret add(const T1& a, const T2& b)
{
cout << "模板函数" << endl;
return a + b;
}
int main()
{
int num1 = 1;
int num2 = 2;
cout << "num1 + num2" << add(num1, num2) << endl;
return 0;
}
构成函数重载的条件,除了要满足类名相同外,还要满足下面的任意一条:①参数个数不同(如上述代码的3号和4号函数)②参数个数相同时,参数的类型不同(如上述代码的2号和4号函数)③参数个数和类型相同时,参数顺序不同(如上述代码的4号和5号函数))⑤参数类型相同个数相同顺序相同时,有const修饰和无const修饰也能构成重载(如上述代码的1号和2号函数)。
注意:在模板函数和已经确定类型的函数都能够被执行时,优先调用确定类型的函数。