C++ 知识整理 函数
在读《c++primer》的过程中,遇到一些知识点,做一下简单的备忘
c++中函数接受的参数是引用类型的情况下,传递引用的限制非常的严格:
举个例子:
例如,定义函数swap(),实现两个数字值的交换:
#include <iostream> #include <cstddef> // inlude ptrdiff_t #include <vector> using namespace std; void swap(int& a, int& b); // 定义函数交换两个数的值 int main(int argc, char *argv[]) { int a1 = 2; int a2 = 3; cout << "Before swap a=" << a1 << " and b=" << a2 << endl; swap(a1, a2); cout << "After swap a=" << a1 << " and b=" << a2 << endl; cout << "-------------------------------" << endl; long b1 = 5; long b2 = 6; cout << "Before swap a=" << b1 << " and b=" << b2 << endl; swap(b1, b2); cout << "After swap a=" << b1 << " and b=" << b2 << endl; return 0; } void swap(int& a, int& b) { int temp = a; a = b; b = temp; }
运行结果:
书中提到,在第二种情况下,早期c++规则比较宽松,这样的操作不能实现两个数值的交换,因为实参和形参的类型不是严格的匹配,所以编译器将创建两个临时的变量,将long类型的变量转化为int,再赋值给临时变量,所以实际上swap()中进行操作的是临时变量,所以并没有实现变量的值交换。
所以总结是: 如果接受引用参数的函数的意图是修改作为参数传递的变量,则创建临时变量将阻止这种意图的实现,所以应该避免这种临时变量的创建
(但是再我的c++编译器下,还是成功实现了数值交换)
将引用用于结构和类:
为了提高程序的效率,在应用结构和类的时候,传递和返回参数的形式一般会使用引用,而不是按值传递,因为按值传递要栈用额外的空间和时间。再返回引用时需要注意一些问题:
例如:
// 定义一个结构 struct free_throw { string name; int age; int grade; } const free_throw& copy(free_throw& ft) { free_throw cp; // cp是一个临时变量 cp = ft; return cp; // 返回一个临时变量的引用 }
上面的函数返回一个临时变量的引用,但是临时变量在函数执行完毕后已经销毁,所以这个引用是不存在的。同理,函数也应该避免返回临时变量的指针。
解决方法:
1. 返回作为参数传递给函数的引用:
const free_throw& copy(free_throw& ft) { free_throw *pt; // cp是一个指针变量 *pt = ft; // pt指向ft return *pt; // 返回传入参数的引用 }
2. 使用new分配新的内存空间,返回内存的地址
但是这种方法隐含了对new()的调用,使得很容易忘记使用delete释放内存
但是可以使用智能指针来解决这种隐含的情况:
函数重载:
函数签名,函数特征标(function signature), 函数参数列表
函数重载中需要注意的条目:
1. 编译器将类型的引用和类型本身视为同一个特征标:
例如:
double cube(double x); double cube(double& x); // invalid
例如上面的情形就是不合法的函数重载
2. 函数匹配时,将不区分const和非const变量
// const与非const的区别 const char p1[20] = "Hello the piano"; char p2[20] = "Hello the macro"; //函数重载 void dribble(char* p); void dribble(const char* p); // 函数重载 void dabble(char* bits); void drive(const char* p); dribble(p1); // 匹配const参数的函数 dribble(p2); // 匹配非const参数的函数 // 非const赋给const是合法的 // const赋给非const是不合法的 dabble(p1); // invalid 参数不匹配 drive(p2); // 参数匹配
名称修饰:
c++通过名称修饰来跟踪每一个重载函数。他根据函数型原型中指定的形参类型对每个函数名进行加密。
例如:
long myFunc(int, float)被编译器加密为?myFunc@@YAXH
函数模板:
函数模板是通用的函数描述,它使用泛型来定义函数,其中的泛型可以用具体的类型来替代。由于类型是用参数表示的,因此模板特性也被称为参数化类型。
实际上,模板并不创建函数,而只是告诉编译器如何定义函数。在实际使用时只需要按照实际传入的参数创建函数。
重载的模板:
可以向像重载常规函数那样重载模板定义。
模板的局限性:
某一些类型的操作可能没有定义,例如,对于结构体,没有+-*/的操作。
解决方案:
1. c++允许对一些运算符进行重载,重载运算符即可
2. 为特定类型提供具体化的模板定义
具体化机制介绍:
第三代具体化:(c++98 具体化方法)
1. 对于给定的函数名,可以有非模板函数,模板函数,显示具体化模板函数以及他们的重载
2. 显式具体化的原型和定义以template< >开头,并通过名称来指出类型。
3. 具体化优先于常规模板,非模板函数优先于具体化和常规模板, 即 非模板函数 > 显式具体化 > 模板函数
举一个具体的例子:
// no template function prototype void swap(job& a, job& b); // template function prototype template <typename T> void swap(T& a, T& b); // explicit speciallization 显式具体化 template <> void swap<job>(job& a, job& b);
例如,要实现swap函数,对两个结构体进行交换操作:
#include <iostream> using namespace std; // template function prototype template <typename T> void swap(T& a, T& b); struct job { char name[40]; double salary; int floor; }; // explicit speciallization 显式具体化 template <> void swap<job>(job& a, job& b); int main() { cout.precision(2); // 设置输出精度 cout.setf(ios::fixed, ios::floatfield); int n1 = 10; int n2 = 20; cout << "n1 =" << n1 << " n2 " << n2 << endl; swap(n1, n2); cout << "n1 =" << n1 << " n2 " << n2 << endl; p1 = {"zhangsna", 45.36, 89 } ; p2 = { "wangwu", 45.3, 90 } swap(p1, p2); return 0; } // function defination template<typename T> void swap(T& a, T&b) { T temp; temp = a; a = b; b = temp; // 结构体允许这样做 } template <> void swap<job>(job& a, job& b) { double temp1; int temp2; temp1 = a.salary; a.salary = b.salary; b.salary = temp1; temp2 = a.floor; a.floor = b.floor; b.floor = temp2; }
函数模板的发展:
模板带来的问题:
1. 在编写模板时,并非总能知道在声明中使用哪种类型:
例如:
template<typename T1, typename T2> void ft(T1 x, T2 y) { ... ?type? sum = x+y; .... }
那么sum因该是什么类型呢,这个与传入的参数的类型是有关的。因此在c++98中没有办法声明sum的类型。
2.decltype关键字
c++11通过decltype关键字来提供解决方案
int x; decltype(x) y; // 定义y的类型与x相同
所以,上面的函数可以改写为:
template<typename T1, typename T2> void ft(T1 x, T2 y) { ... decltype(x+y) sum; sum = x+y; .... }
decltype的实际运作原理:
假设有如下声明:
decltype(expression) var;
为了确定类型,编译器会遍历一个核对表:
第一步: 如果expression是一个没有用括号括起来的标识符,和var的类型和标识符的类型相同,包括const等限定符
例如:
double x = 9.0; double y = 7.9; double& rx = x; const double* pd; decltype(x) w; // w double decltype(rx) u = y; // u double& decltype(pd) v; // v double*
第二步:如果expression是函数调用,则var的类型和函数返回值的类型相同
long indeed(int); decltype (indeed(3)) y; // y long
这一操作并不会实际调用函数,编译器只是通过查看函数的原型来获取返回值的类型。
第三步:如果expression为一个左值,则var为其引用类型
例如:
double xx = 4.4; decltype ((xx)) r2 = xx; // r2 double& decltype (xx) w = xx; // w double
第四步:如果前面的都不满足,则var的类型与expression的类型相同
int j = 4; int& m = j; int& n = j; decltype(j+6) w; // w int decltype(m+n) u; // u int; 虽然mn都是引用,但是m+n是两个int的和,所以u的类型为int
同时,如果需要多次申明,可结合和typedef使用:
template<typename T1, typename T2> void ft(T1 x, T2 y) { ... typedef decltype(x+y) xytype; xytype sum; xytype a1; sum = x+y; .... }
decltype无法解决的问题:
具有返回值的模板函数:
例如:
template<typename T1, typename T2> ?type? gt(T1 x, T2 x) { ... return x+y; ... }
无法预先知道参数x,y的类型,所以返回值的类型无法确定,此时还不能使用decltype(x+y)来说明返回值类型,因为此时参数x,y还不再作用域内,必须在声明参数后使用decltype:
C++提供了后置返回类型来解决这种问题:
使用auto:
auto h(int x, float y)->double { ... //function body; ... }
auto在这里相当于一个占位符,表示后置返回类型提供的类型。
所以上述问题的解决方案可以为:
template<typename T1, typename T2> auto gt(T1 x, T2 x) -> decltype(x+y) { ... return x+y; ... }
现在参数x,y位于作用域内,所以可以使用它们。
----------------------------------------------------------------------------------------------------------
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)