深入理解:函数和函数模板的作用与应用
本篇要学习的内容和知识结构概览
函数的参数及其传递方式
1. 函数参数传递方式
传值:
传变量值: 将实参内存中的内容拷贝一份给形参, 两者是不同的两块内存
传地址值: 将实参所对应的内存空间的地址值给形参, 形参是一个指针, 指向实参所对应的内存空间
传引用:
形参是对实参的引用, 形参和实参是同一块内存空间
2. 对象作为函数参数, 也就是传变量值
将实参对象的值传递给形参对象, 形参是实参的备份, 当在函数中改变形参的值时, 改变的是这个备份中的值, 不影响原来的值
像这样:
void fakeSwapAB(int x , int y) {
int temp = x;
x = y;
y = temp;
}
int a = 5;
int b = 8;
cout << "交换前: " << a << ", " << b << endl;
// 传变量值
fakeSwapAB(a, b);
cout << "交换后: " << a << ", " << b << endl;
3. 对象指针作为函数参数, 也就是传地址值
形参是对象指针, 实参是对象的地址值, 虽然参数传递方式仍然是传值方式, 因为形参和实参的地址值一样, 所以它们都指向同一块内存, 我们通过指针更改所指向的内存中的内容, 所以当在函数中通过形参改变内存中的值时, 改变的就是原来实参的值
像这样:
void realSwapAB(int * p, int * q) {
int temp = *p;
*p = *q;
*q = temp;
}
int a = 5;
int b = 8;
cout << "交换前: " << a << ", " << b << endl;
// 传地址值
realSwapAB(&a, &b);
cout << "交换后: " << a << ", " << b << endl;
对于数组, 因数组名就是代表的数组首地址, 所以数组也能用传数组地址值的方式
void swapArrFirstAndSecond(int a[]) {
int temp = a[0];
a[0] = a[1];
a[1] = temp;
}
int main(int argc, const char * argv[]) {
int a[] = {2, 3};
cout << "交换前: " << a[0] << ", " << a[1] << endl;
swapArrFirstAndSecond(a);
cout << "交换后: " << a[0] << ", " << a[1] << endl;
return 0;
}
4. 引用作为函数参数, 也就是传地址(注意: 这里不是地址值)
在函数调用时, 实参对象名传给形参对象名, 形参对象名就成为实参对象名的别名. 实参对象和形参对象代表同一个对象, 所以改变形参对象的值就是改变实参对象的值
像这样:
void citeSwapAB(int & x, int & y) {
int temp = x;
x = y;
y = temp;
}
int a = 5;
int b = 8;
cout << "交换前: " << a << ", " << b << endl;
// 传引用
citeSwapAB(a, b);
cout << "交换后: " << a << ", " << b << endl;
优点: 引用对象不是一个独立的对象, 不单独占内存单元, 而对象指针要另外开辟内存单元(内存中放实参传过来的地址), 所以传引用比传指针更好用.
5. 默认参数
不要求程序在调用时必须设定该参数, 而由编译器在需要时给该参数赋默认值.
规则1. 当程序需要传递特定值时需要显式的指明. 默认参数必须在函数原型中说明.
如果函数在main函数后面定义, 而在声明中设置默认参数, 在定义中不需要设置默认参数
像这样:
// 在main函数前声明函数, 并设置默认参数
void PrintValue(int a, int b = 0, int c = 0);
int main(int argc, const char * argv[]) {
// 调用函数
PrintValue(5);
return 0;
}
// 在main函数后定义函数, 不需要设置默认参数
void PrintValue(int a, int b, int c) {
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
如果函数在main函数前面定义, 则在定义中设置默认参数
像这样:
// 在main前定义函数, 需要设置默认参数
void PrintValue(int a, int b = 0, int c = 0) {
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
int main(int argc, const char * argv[]) {
// 调用函数
PrintValue(5);
return 0;
}
规则2: 默认参数可以多于一个, 但必须放在参数序列的后部.
像这样:
可以有一个默认参数:
void PrintValue(int a, int b, int c = 0);
可以是有多个默认参数:
void PrintValue(int a, int b = 0, int c = 0);
不可以在中间设置默认参数:
void PrintValue(int a, int b = 0, int c);
规则3. 如果一个默认参数需要指定一个特定值时, 则在此之前的所有参数都必须赋值
// 调用函数 第一种: 三个参数全部有特定值
PrintValue(5, 8, 9);
// 调用函数 第二种: 我们给第二个参数设特定值, 它前面所有参数必须赋值, 所以可以
PrintValue(5, 8);
/*
调用函数 第三种: 当一个默认参数有特定值时, 它前面所有的参数都必须赋值,
我们给第三个默认参数设特定值 也就是说第一, 二个参数也必须赋值 所以不可以
*/
// PrintValue(5, , 9);
6. 使用const保护数据
用const修饰要传递的参数, 该函数只能使用参数, 而无权修改参数, 以提高系统的自身安全.
像这样:
// 拼接字符串的函数
void catStr(const string str) {
string str2 = str + " Ray!";
// 函数内部不能修改const修饰的形参, 所以不能这么使用
// str = "Hi";
cout << str2 << endl;
}
int main(int argc, const char * argv[]) {
// 实例化一个字符串
string str = "Hello";
// 调用函数
catStr(str);
return 0;
}
函数返回值
C++函数返回值类型可以是除数组和函数以外的任何类型
当返回值是指针或引用对象时, 需要注意函数返回值所指的对象必须存在, 因此不能将函数内部的局部对象作为函数返回值, 因为函数内, 局部变量或者对象在函数运行完毕后内存就释放啦
1. 返回引用的函数
函数可以返回一个引用, 目的是为了让该函数位于赋值运算符的左边
格式: 数据类型 & 函数名(参数列表);
像这样:
// 全局数组
int arr[] = {2, 4, 6, 8};
// 获得数组下标元素
int & getValueAtIndex(int i) {
return arr[i];
}
int main(int argc, const char * argv[]) {
cout << "更改前: " << arr[2] << endl;
// 调用函数, 并且用于计算或者重新赋值
getValueAtIndex(2) = 10;
cout << "更改后: " << arr[2] << endl;
return 0;
}
2. 返回指针的函数
返回值是存储某种数据类型数据的内存地址, 这种函数称为指针函数
格式: 数据类型 * 函数名(参数列表);
像这样:
// 返回指针的函数
int * getData(int n) {
// 根据形参, 申请内存空间
int * p = new int[n];
// 给申请下来的内存空间赋值
for (int i = 0; i < n; i++) {
p[i] = i + 10;
}
// 返回这段内存空间的首地址
return p;
}
int main(int argc, const char * argv[]) {
// 调用函数, 并接收返回值, 不要忘记释放函数中分配的内存
int * p = getData(5);
// 打印指针所指向的内存中的内容
for (int i = 0; i < 5; i++) {
cout << p[i] << endl;
}
return 0;
}
3. 返回对象的函数
格式: 数据类型 函数名(参数列表);
像这样:
// 返回对象的函数
string sayHello(string s) {
// 我们拼接好一个字符串, 给str
string str = "Hello " + s;
// 并把str这个对象返回
return str;
}
int main(int argc, const char * argv[]) {
// 调用函数, 接收函数返回的对象
string str = sayHello("Ray");
cout << str << endl;
return 0;
}
4. 函数返回值作为函数参数
如果函数返回值作为另一个函数的参数, 那么这个返回值必须与另一个函数的参数类型一致
像这样:
// 求最大值的函数
int getMax(int x, int y) {
return x > y ? x : y;
}
int main(int argc, const char * argv[]) {
// 先求8, 9返回最大值; 返回值再跟5比较, 返回最大值
int maxValue = getMax(5, getMax(8, 9));
cout << maxValue << endl;
return 0;
}
内联函数
1. 内联函数的概念
使用关键字inline声明的函数称为内联函数, 内联函数必须在程序中第一次调用此函数的语句出现之前定义, 这样编译器才知道内联函数的函数休, 然后进行替换
像这样:
// 判断输入的字符是否为数字
inline bool isNumber(char c) {
if (c >= '0' && c <= '9') {
return true;
} else {
return false;
}
}
int main(int argc, const char * argv[]) {
// 声明字符c
char c;
// 从键盘输入字符
cin >> c;
// 进行判断, 这里的isNumber(c), 在程序编程期间就会被isNumber()函数体所替换, 跟宏一样一样的
// 如果函数体特别大, 替换的地方特别多, 就增加了代码量
if (isNumber(c)) {
cout << "输入了一个数字" << endl;
} else {
cout << "输入的不是一个数字" << endl;
}
return 0;
}
2. 注意:
在C++中, 除具有循环语句, switch语句的函数不能说明为内联函数外, 其它函数都可以说明为内联函数.
3. 作用:
使用内联函数可以提高程序执行速度, 但如果函数体语句多, 则会增加程序代码量.
函数重载和默认参数
1. 函数重载
一个函数名具有多种功能, 具有多种形态, 称这种我为多态性, 一个名字, 多个函数
函数重载要满足的条件:
参数类型不同或者参数个数不同
像这样:
// 求和的函数 2两个整型参数
int sumWithValue(int x, int y) {
return x + y;
}
// 求和的函数 3两个整型参数
int sumWithValue(int x, int y, int z) {
return x + y + z;
}
// 求和的函数 2个浮点型参数
double sumWithValue(double x, double y) {
return x + y;
}
// 求和的函数 3个浮点型参数
double sumWithValue(double x, double y, double z) {
return x + y + z;
}
int main(int argc, const char * argv[]) {
// 两个整型变量求和
int sumValue1 = sumWithValue(8, 9);
// 三个整型变量求和
int sumValue2 = sumWithValue(8, 9, 10);
// 两个浮点型变量求和
double sumValue3 = sumWithValue(1.2, 2.3);
// 三个浮点型变量求和
double sumValue4 = sumWithValue(1.2, 2.3, 3.4);
cout << sumValue1 << endl;
cout << sumValue2 << endl;
cout << sumValue3 << endl;
cout << sumValue4 << endl;
return 0;
}
2. 函数重载与默认参数
当函数重载与默认参数相结合时, 能够有效减少函数个数及形态, 缩减代码规模.
这样我们每种数据类型只保留一个函数即可完成我们的功能, 直接少了两个函数.
像这样:
// 整型参数求和
int sumWithValue(int x = 0, int y = 0, int z = 0) {
return x + y + z;
}
// 浮点型参数求和
double sumWithValue(double x = 0, double y = 0, double z = 0) {
return x + y + z;
}
int main(int argc, const char * argv[]) {
// 两个整型变量求和
int sumValue1 = sumWithValue(8, 9);
// 三个整型变量求和
int sumValue2 = sumWithValue(8, 9, 10);
// 两个浮点型变量求和
double sumValue3 = sumWithValue(1.2, 2.3);
// 三个浮点型变量求和
double sumValue4 = sumWithValue(1.2, 2.3, 3.4);
cout << sumValue1 << endl;
cout << sumValue2 << endl;
cout << sumValue3 << endl;
cout << sumValue4 << endl;
return 0;
}
如果使用默认参数, 就不能对参数个数少于默认个数的函数形态进行重载, 只能对于多于默认参数个数的函数形态进行重载.
像这样
// 求和的参数, 并且使用默认参数, 最多三个整型参数求和
int sumWithValue(int x = 0, int y = 0, int z = 0) {
return x + y + z;
}
// 像这样是不行的, 不能对参数个数少于默认个数的函数形态进行重载
//int sumWithValue(int x, int y) {
// return x + y;
//}
// 像这样是可以的, 当调用时传入4个整型参数时就会调用该参数
int sumWithValue(int x, int y, int z, int t) {
return x + y + z + t;
}
int main(int argc, const char * argv[]) {
// 求和, 只给两个特定值
int sumValue1 = sumWithValue(8, 9);
// 求和, 给三个特定值
int sumValue2 = sumWithValue(8, 9, 10);
// 求和, 有4个整型参数
int sumValue3 = sumWithValue(8, 9, 10, 11);
cout << sumValue1 << endl;
cout << sumValue2 << endl;
cout << sumValue3 << endl;
return 0;
}
函数模板
从而上面可以看出, 它们是逻辑功能完全一样的函数, 所提供的函数体也一样, 区别仅仅是数据类型不同, 为了统一的处理它们, 引入了函数模板.
现在我们的函数从4个缩减成一个, 但是我们的功能没有减少, 反而增加了. 比如我们可以计算char, float类型
1. 什么是函数模板
在程序设计时没有使用实际存在的类型, 而是使用虚拟的参数参数, 故其灵活性得到加强.
当用实际的类型来实例化这种函数时, 就好像按照模板来制造新的函数一样, 所以称为函数模板
格式: 一般用T来标识类型参数, 也可以用其它的
Template <class T>
像这样:
// 定义模板
template <class T>
// 定义函数模板
T sumWithValue(T x, T y) {
return x + y;
}
int main(int argc, const char * argv[]) {
// 调用模板函数
int sumValue1 = sumWithValue(3, 5);
// 调用模板函数
double sumValue2 = sumWithValue(3.2, 5.1);
cout << sumValue1 << endl;
cout << sumValue2 << endl;
return 0;
}
当用用函数模板与具体的数据类型连用时, 就产生了模板函数, 又称为函数模板实例化
2. 函数模板的参数
函数模板名<模板参数>(参数列表);
我们可以将参数列表的数据强制转换为指定的数据类型
像这样
int sumValue2 = sumWithValue<int>(3.2, 5.1);
我们将参数列表里的数据强制转换为int类型, 再参与计算
也可以样:
double sumValue2 = sumWithValue(3.2, (double)5);
我们也可以将参数列表里的单个参数进行强制类型转换, 再参与计算
不过我们一般不会加上模板参数.
3. 使用关键字typename
用途就是代替template参数列表中的关键字class
像这样
template <typename T>
只是将class替换为typename, 其它一样使用.
强烈建议大家使用typename, 因为它就是为模板服务的, 而class是在typename出现之前使用的, 它还有定义类的作用, 不直观, 也会在一些其它地方编译时报错.
总结:
可能对于初学者来说, 函数有点不是很好理解, 包括我当初也是, 不要想得过于复杂, 其实它就是一段有特定功能的代码, 只不过我们给这段代码起了个名字而已, 这样就会提高代码的可读性和易维护性.
本系列文章会持续更新! 大家踊跃的留下自己的脚印吧!
????????????????????????????????
上一篇: 必知!常用设计模式大集合
下一篇: 《诗经》中的莲:内心的独白
推荐阅读
-
逆滤波与维纳滤波的应用于Python运动模糊和退化模型的点扩散函数
-
深入理解JSP第四部分:EL表达式的探索与应用,包括各类数据的获取、内置对象的使用、运算功能实现、数据回显、自定义函数和fn方法库的应用
-
理解并掌握getuid、geteuid、getgid和getegid函数的作用
-
使用 LISTAGG 和 OVER PARTITION BY:函数的详解与应用
-
理解Oracle的listagg函数与Mysql的group_concat在实际应用中的差异与相似之处
-
理解Oracle中的wm_concat和listagg函数:使用方法与异同点
-
组织群体数据与类型的方法:函数模板和类模板的应用
-
深入理解:函数和函数模板的作用与应用
-
如何在Python中轻松创建、调用和理解def函数,以及参数传递的实际操作与示例代码
-
【摩尔线程+Colossal-AI强强联手】MusaBert登上CLUE榜单TOP10:技术细节揭秘 - 技术实力:摩尔线程凭借"软硬兼备"的技术底蕴,让MusaBert得以从底层优化到顶层。其内置多功能GPU配备AI加速和并行计算模块,提供了全面的AI与科学计算支持,为AI推理和低资源条件下的大模型训练等场景带来了高效、经济且环保的算力。 - 算法层面亮点:依托Colossal-AI AI大模型开发系统,MusaBert在训练过程中展现出了卓越的并行性能与易用性,特别在预处理阶段对DataLoader进行了优化,适应低资源环境高效处理海量数据。同时,通过精细的建模优化、领域内数据增强以及Adan优化器等手段,挖掘和展示了预训练语言模型出色的语义理解潜力。基于MusaBert,摩尔线程自主研发的MusaSim通过对比学习方法微调,结合百万对标注数据,MusaSim在多个任务如语义相似度、意图识别和情绪分析中均表现出色。 - 数据资源丰富:MusaBert除了自家高质量语义相似数据外,还融合了悟道开源200GB数据、CLUE社区80GB数据,以及浪潮公司提供的1TB高质量数据,保证模型即便在较小规模下仍具备良好性能。 当前,MusaBert已成功应用于摩尔线程的智能客服与数字人项目,并广泛服务于语义相似度、情绪识别、阅读理解与声韵识别等领域。为了降低大模型开发和应用难度,MusaBert及其相关高质量模型代码已在Colossal-AI仓库开源,可快速训练优质中文BERT模型。同时,通过摩尔线程与潞晨科技的深度合作,仅需一张多功能GPU单卡便能高效训练MusaBert或更大规模的GPT2模型,显著降低预训练成本,进一步推动双方在低资源大模型训练领域的共享目标。 MusaBert荣登CLUE榜单TOP10,象征着摩尔线程与潞晨科技联合研发团队在中文预训练研究领域的领先地位。展望未来,双方将携手探索更大规模的自然语言模型研究,充分运用上游数据资源,产出更为强大的模型并开源。持续强化在摩尔线程多功能GPU上的大模型训练能力,特别是在消费级显卡等低资源环境下,致力于降低使用大模型训练的门槛与成本,推动人工智能更加普惠。而潞晨科技作为重要合作伙伴,将继续发挥关键作用。