7. 函数
基本概念
返回值类型 函数名(参数类型1 参数1)
{
函数体;
return 返回值;
}
-
函数包括三个部分:
-
原型:
- 描述了函数到编译器的接口。也就是说他将函数返回值的类型以及参数的类型和数量告诉编译器。
- 原型中的变量名相当于占位符。可以包括,也可以不包括,也不必与函数定义中的变量名相同。
-
定义
- 定义函数的实现
-
调用
-
-
原型有啥用?
以
double cube(double x);为例-
确保编译器正确处理函数返回值。
-
当函数执行完,将返回值放在指定区域--CPU寄存器或内存中。然后调用函数(指调用这个函数的函数)将从这个位置取得返回值。 因为原型中指出了返回值的类型,所以编译器直到应该检索多少字节并直到如何解释他们。如果没有原型,编译器便无法得知。
-
-
确保编译器检查使用的参数数目是否正确
- 如果在调用函数时,没有传递参数,那么编译器将会指出错误。如果没有函数原型,那么编译器将会使
double z = cube()编译通过,编译器将找到cube()调用存放值的位置,并使用这里的值,这会导致不正确的结果。
- 如果在调用函数时,没有传递参数,那么编译器将会指出错误。如果没有函数原型,那么编译器将会使
-
确保编译器检查使用的参数类型是否正确,如不正确,则转换为正确的类型(如果可能的话)。
- 当有意义时,编译器才会进行类型转换。如
double z = cube(2);传入一个int值,编译器会自动转换类型。
- 当有意义时,编译器才会进行类型转换。如
-
-
一个函数的调用过程
EIP:指令指针,即指向下一条即将执行的指令的地址。EBP:基址指针,常用来指向栈底;ESP为栈指针,常用来指向栈顶。
-
传递参数
包括值传递、引用传递、指针传递
-
创建函数栈帧
将返回地址、保存的寄存器、参数、局部变量、栈指针(指向栈的顶部,最后一个入栈)保存到函数栈帧中,保存现场。
-
跳转并执行函数
PC跳转到函数的入口地址,开始执行函数体
-
保存返回值
将返回值存储到寄存器或者内存中,以便函数调用者使用
-
销毁函数栈帧
函数执行完毕并返回,销毁栈帧释放空间,回复现场。
-
函数和数组
使用函数来处理数组,一般需要将函数名和数组长度传递给函数。
// 当且仅当用于函数头或函数原型时,arr[] 和 *arr 的含义相同,都意味着arr是一个int指针。
// 所以在此时,下式恒等
// arr[i] == *(arr + i);
// &arr[i] == arr + i;
int sum_arr(int arr[], int n);
int sum_arr(int *arr, int n);
将数组用作参数,相当于传递的是数组的地址,这意味着对数组元素进行修改将会修改其原来的值。
如果要避免修改, 可以使用const关键字来限定。
int sum_arr(const int *arr, int n); // 使用const限定符,限制不能对数组本身进行修改
// 又一次的处理错误输入
// 仅输入一次
double factor;
while (!(cin >> factor))
{
cin.clear();
while(cin.get() != '\n')
continue;
cout << "Bad input! reinput!" << endl;
}
Q: 如何处理数组区间的函数
A: 通过传递两个指针完成,一个标识数组的开头,另一个标识超尾。
// 如传入参数为 (cookies, cookies + 4) 则是为处理 0 1 2 3 这四个元素。
int sum_arr(const int * begin, const int * end)
指针与const
有两种不同的方法来使用const修饰指针
-
让指针指向一个常量对象,来防止指针修改所指向的值。(防止修改第二块内存。目标内存内的值)
int age = 23; const int *pt = &age; // 这是表明pt指向了一个const int // 如何理解? // 定义语句的组成: 变量类型 变量名 = 值 // 所以此刻的 const 修饰的是 int, const 被视为变量类型的一部分 // * 表明这个变量是指针变量 *pt += 1; // 不允许 cin >> *pt; // 不允许 *pt = 20; // 不允许 age = 20; // 允许注意:c++不允许将const地址赋给非const指针
// 被允许 const float kGEarth = 9.80; const float = pe = & kGEarth; //不被允许 const float kGMoon = 1.63; float *pm = &kGMoon; -
将指针本身声明为一个常量,防止修改指针指向的位置。(防止修改第一块内存。指针内存储的目标内存地址)
int age = 23; int const *pt = &age; // 这表明声明了const *pt 指向了一个int类型 // 此时const修饰的是指针, 被视为指针的一部分 // 变量类型为int
二维数组
二维数组的话,还是使用[]比较好
// 函数原型
int sum(int arr[][], int sizex, int sizey);
int sum(int **arr, int sizex, int sizey); // 两次指针。
int sum(int arr[][4], int sizex); // 这是表明第二维的维度为4。
// 一些元素的使用
arr[2]; // 代表第三个元素,这个元素是一个一维数组,有4个元素。
arr[1][2]; // 第[2][3]个元素
函数与字符串
-
字符串的三种表示方式:
- char数组
- 用" "括起的字符常量。(其也会自己添加'\0')
- 被设置为字符串地址的char指针
-
使用函数操作字符串不需要传递数组的大小,因为字符串都是以'\0'结尾。
-
如何返回一个字符串?
不需要返回整个字符串,只需要返回字符串的地址。但我们知道,函数内的局部变量的生命周期只在函数的本次执行时存在,当函数执行结束后,函数栈销毁变量将不会存在。那么应该如何做呢?
-
直接传入需要存放的数组,借助已经在主函数中声明的指向字符串的指针
-
如何让他不自动销毁? 答案是new。使用new声明的变量,编译器不会帮助我们自动管理,其存放在堆区,需要通过delete自行销毁。那么可以在函数中使用new声明一个char指针。这样就在堆区开辟了一段空间用来存储字符串。
char * buildstr(char c, int n); int main() { char ch; int times; char * ps = buildstr(ch, times); // 保存传来的字符串地址 delete [] ps; // 释放空间 } // pstr在函数执行结束后所使用的内存会被释放,但注意这是释放的pstr这个在栈中的变量,而使用new在堆区存储的字符串并没有销毁。而pstr这个指针的值就是在堆区的地址。 char * buildstr(char c, int n) { char *pstr = new char[n+1]; // 使用new声明一个char * return pstr; // 返回char指针,指针也是一个变量,它存储一个地址,这个地址就是刚刚存字符串的地方 }
-
函数与结构
-
按值传递
-
按指针传递
struct polar { double x; double y; } void Show(polar pda); // 按值 void Show(cosnt polar * pda); // 按指针,不想修改就加上const限定符。 访问使用->箭头运算符,因为这不是结构的名称而是地址。
递归函数
void recurs(argumentlist)
{
statements1;
if(test)
recurs(arguments);
statements2;
}
当函数被调用时,函数按顺序执行,即调用recurs函数,statements1将会被执行。然后进行if判定,如果为真,那么将会调用recurs(),这是一次新的调用,你可以传递新的参数。然后在新的调用里面又会发生上述这些事情。直到有一次函数的if判定为假,那就开始return了,开始反向执行statements2(本就是返回调用函数的下一个语句,这就是程序运行的规则,并无例外,不是吗?)。
void countdown(int n)
{
cout << "这是正向调用 " << n << endl;
if(n > 0)
countdown(n-1);
cout << "这是反向调用 " << n << endl;
return;
}
// 当调用这个函数5次,那么输出为:
/*
这是正向调用 5
这是正向调用 4
这是正向调用 3
这是正向调用 2
这是正向调用 1
这是正向调用 0
这是反向调用 0
这是反向调用 1
这是反向调用 2
这是反向调用 3
这是反向调用 4
这是反向调用 5
*/
注意: 每次递归调用都是一次新的调用哦,其参数的地址是不一样的,比如此程序的"n"。
函数指针
-
啥叫函数指针?
- 函数指针和其他数据类型的指针一样,他是指向函数的指针。即声明了个指针,这个指针内存储的是函数的入口地址。
-
如果要把一个算法函数的地址传给另一个函数应该如何做?
-
获取被传递函数的地址
-
函数名就是函数的地址
// 假设存在函数think() process(think); // 传递函数地址 process(think()); // 传递函数返回值,相当于先调用函数think(),取得返回值,再将返回值传递给process()
-
-
声明一个函数指针,指向(存储函数入口地址)被调用函数。
-
如何声明一个函数指针?
声明基本类型的指针时,需要指定指针类型。声明函数指针时,需要指定指向函数的类型。
什么确定了函数的类型?返回值类型及函数的特征标。所以函数指针如此声明:
return_type (*pointer_name)(parameter_types); //ex: double pam(int); // 函数原型 double (*pf)(int); // 函数指针 就是将函数名声明为指针(*名),可以用这个指针指向(要指向的函数名) // 为提供正确的优先级,使用() 将*pf括起来,表明pf是一个指针,而(*pf)(int) 意味着pf是一个指向函数的指针
-
-
使用函数指针调用函数
有两种用法
-
将函数指针当作普通调用函数使用
double pam(int); double (*pf)(int); int main() { pf = pam; // 给指针赋值, 将函数入口地址给指针。 // 下面两个相同 pam(4); (*pam)(4); } -
将函数指针作为形参,用以传递函数
void estimate(int lines, double (*pf)(int)); // 将函数指针作为形参。那么可以将函数名传进来。 double pam(int); double bad(int); int main() { // 将函数名传递。 estimate(5, pam); estimate(9, bad); }
-
-
内联函数
inline double square(double x) {return x * x;}
Q: 如何编写一个内联函数?
A:
- 在其声明前添加关键字 inline
- 在其定义其添加关键字inline
而通常的做法是省略函数的原型(声明)。而直接将整个定义放在本应该提供函数原型的地方。
Q: 内敛函数如何工作?
A: 编译器将内联函数插入到每个调用它的地方。这省略了函数调用的时间,但需要更多的内存。
注意: 内敛函数不能递归。
引用变量
-
什么是引用变量?
- 引用是指引用已经定义好的变量,给已定义的变量的别名。他们指向相同的值和内存单元。所以对原变量进行操作相当于对原始变量进行操作。引用就是给已分配的内存单元一个别名。
-
如何声明一个引用变量?
-
使用&符号
int rats = 5; int & rodents = rats; // 声明rodents为变量rats的引用。 // 对任一操作将会影响两个变量。 rats++; // 此时rodents = 6;
-
-
引用变量与指针的区别
-
引用必须在声明时初始化,而指针不需要。
int *pt; // 被允许不初始化,只需要在解除引用之前给其分配地址 int a; int &b = a; int &c; // 不被允许 -
引用一旦与某个变量关联起来,就永远效忠。
int a, c; int &b = a; &b = c; // 不被允许 // 下面两式等价。 int & rodents = rats; int * const rodents = rats;
-
-
引用作为函数的参数
-
引用经常被当作函数的参数,使得函数中的变量名成为调用函数中的变量的别名。而注意,对引用变量操作就相当于对全变量操作(引用变量本就是将已经分配的地址添加别名。)
-
如果不想修改引用变量,那么应将引用变量声明为const
-
为什么要将引用作为函数的参数?
- 节省内存, 不用复制一遍,效率高
- 可以直接修改实参的值
-
-
将引用作为函数返回值
-
主要用于结构和类,用以返回一个最初传给函数的对象。
-
效率高,不用再创建一个临时的变量
-
返回引用实际上是被引用的变量的别名
-
返回const 类型的引用。为了使返回值不能被赋值
const free_throws & accumutlate(free_throws & target, const free_throws & source); // 这样使得下面的语句非法 accumulate(dup, five) = four; // 试图改变一个const值 // 但一般谁他喵的这么做。 // 一般将返回值声明为 const,是为了传入的参数本就是const, // 因为你不能让一个非const指向const对象
-
-
左值和非左值
-
左值:具有内存地址的值,可以在程序中找到并修改。
- 变量
- 数组元素
- 引用
-
右值:不具有持久性的内存地址,在本条语句完成后就会被销毁的临时值。
- 字面常量
- 临时对象
- 返回右值引用的函数调用
int x = 6; // x 具有内存地址,是左值。 6不具有,是临时的,是右值 int &y = x; // allow int &z1 = x * 6; // 错误, x * 6 是个临时的表达式,是一个右值 const int &z2 = x * 6; // 正确, x * 6是一个右值, 但常量引用可以绑定到临时值 int &&z3 = x * 6; // 正确,右值引用 int &&z4 = x; // 错误,x是一个左值-
右值引用有什么用?
- 移动语义(Move Semantics) :右值引用允许我们从临时对象“窃取”资源而不是进行深度复制,从而提高程序的性能。这在处理大型数据结构或需要频繁创建临时对象的情况下特别有用。
- 完美转发(Perfect Forwarding) :右值引用可以用于实现完美转发,即将参数以原样传递给其他函数,无论这些参数是左值还是右值。这在实现通用代码时非常有用,特别是在泛型编程中。
- 移动语义和右值引用的结合:通过使用移动语义和右值引用,我们可以设计更高效的容器和类,例如移动构造函数和移动赋值运算符,以提高资源管理的效率和性能。
-
默认参数
- 需要通过函数原型设置默认值,函数头不能带默认值
- 设置默认值需要从右至左添加默认值
- 默认参数使得能够通过不同数目的参数调用同一个函数
函数重载(函数多态)
-
什么是函数多态?
- 同名但不同功能的函数。通过函数的特征标所区分
-
如何进行函数多态?
通过相同函数名,不同特征标来表示不同的函数
- 参数类型
- 参数数目
- 参数顺序
注意:
-
不能通过传递引用和传递值来重载函数,因为值就是创建一个拷贝变量。
double cube(double x); double cube(double &x); // 传入x与这两个模型都匹配 -
但能够通过const与否来重载函数
void dribble(char * bits); void dribble(const char * bits); // const 被视为参数类型的一部分 与 非const 构成函数的重载 // 编译器将根据传递的实参是否为const类型来决定使用哪个函数 -
能够通过不同版本的引用参数来重载引用参数
void sink(double & r1); // 普通引用 void sink(const double & r2); // const 引用 void sink(double && r3); // 右值引用 int main() { double x = 55.5; const double y = 32.0; sink(x); // 普通引用 sink(y); // const引用 // 右值引用 如果没有定义右值引用,编译器将会调用const引用,因为const引用可以绑定右值。 //如果const应用也没有,将会报错 sink(x + y); } void sink(double & r1) { cout << "这是普通引用" << endl; } void sink(const double & r2) { cout << "这是const引用" << endl; } void sink(double && r3) { cout << "这是右值引用" << endl; } /* 输出: 这是普通引用 这是const引用 这是右值引用 */ -
编译器如何跟踪每一个重载函数?
-
答案是名称修饰。编译器根据函数中指定的特征标(形参类型、数目、顺序)对每个函数名进行加密
// 修饰前 long MyFunctionFoo(int, float); // 修饰后 随编译器不同 ?MyFunctionFoo@@YAXH
-
函数模板
函数模板
-
什么是函数模板
函数模板是通用的函数描述,通过将类型作为参数传递给模板,可以使编译器生成该类型的函数。,模板并不创建任何函数,只告诉编译器如何定义函数。
-
如何实现一个函数模板
// 使用关键字 template 表明要定义一个模板(或函数,或类) // 后面跟尖括号<>,其内部通过typename指出要泛化的类型, // 通过传入类型作为参数可以告诉编译器要生成的函数类型。 template <typename T> void Swap(T &a, T &b) { T temp; temp = a; a = b; b = temp; } // 也可以将类型参数作为返回值类型 template <typename T> T Swap(T &a, T &b) { ...; return a; } -
模板函数的函数头(函数声明)是什么?
// 注意 函数声明为template和函数名这两行 template <typename T> void Swap(T &b, T &b); // 函数名是什么? // 和普通函数一样 Swap() -
如何调用一个模板函数?
// 按照正常的函数调用,通过函数名 // 传入类型为int,编译器自动生成Swap的int版本函数 int x, y; Swap(x, y); // 传入类型为double,编译器自动生成Swap的double版本函数 double x, y; Swap(x, y); -
如何重载一个函数模板
// 与普通函数重载一样,使用不同的特征标 template <typename T> void Swap(T &b, T &b); template <typename T> void Swap(T &b, T &b, int n); -
可以传入多个类型吗?
// 可以 template <typename T1, typename T2> T1 max(T1 a, T2 b) { return b < a ? a : b; } ... auto m = max(4, 7.2); -
typename和class的相同与区别
-
相同:
- 在模板定义语法中,typename和class作用完全相同,都是指出后面的变量名是个类型
-
不同:
-
class可以用来声明一个类
-
typename可以用在嵌套依赖类型。typename可以指出后面的字符串是一个类型名称,而不是泛型内的其他东西(成员变量?函数)
// 我定义了一个类 // 并且在类中使用typedef 为int变量定义了一个别名 class MyArray { public: typedef int LengthType; // 为int定义了一个别名 ..... } // 如果我想在其他地方使用这个我在这个类中声明的int的别名呢? // 比如我要在模板函数中使用它 // 如果 template <typename T> void foo() { // 'typename' 用于告诉编译器 T::LengthType 是一个类型 typename T::LengthType* ptr; // 如果这条语句这样写 T::LengthType* ptr; 那么编译器可能无法确定这个LengthType是个什么东西。 }
-
-
-
decltype + auto 自动追踪函数返回值类型
// 问题1:想一下下面的函数模板中某变量应该的类型是什么? template <typename T1, typename T2> void ft(T1 x, T2 y) { ...; ?type? xyz = x + y; // xyz 的类型应该声明为什么? ...; } // 问题2:如果要根据传入的类型参数指定返回值的类型呢? template <typename T1, typename T2> ?type? gt(T1 x, T2 y) // 返回值类型应该如何指定呢? { return x + y; } // 使用decltype关键字可以在编译时以一个普通表达式对类型进行推导,可以解决问题1 // ex: int i = 4; decltype(i) a; //推导结果为int。a的类型为int。 // 重写问题1 template <typename T1, typename T2> void ft(T1 x, T2 y) { ...; decltype(x + y) xyz = x + y; // xyz 的类型应该声明为什么? ...; } // 使用auto + decltype 可以解决问题2 // 使用后置返回类型,进行返回类型的自动推导 template <typename T1, typename T2> auto gt(T1 x, T2 y) ->decltype(x + y) // ->后置返回类型 { return x + y; }// decltype 核对表 decltype(expression) var; /* 1. 如果expression 是没有被括号括起的标识符,则var与此标识符的类型一样,包括const等限定符 */ double x = 5.5; double &rx = x; const double *pd; decltype(x) w; // 声明一个与x类型相同的变量 int decltype(rx) u = x; // 定义一个与rx相同类型的变量 double& decltype(pd) pa; // 声明一个 const double指针 /* 2. 如果expression 是函数调用,那么var与函数返回值相同 */ long indeed(int); decltype (indeed(3)) m; // m为long。 这并不会调用函数,只是查看原型来获取返回类型 /* 3. 如果expression 是一个用括号括起的标识符,则var为指向其类型的引用, 注意是引用 */ double xx = 4.4; decltype ((xx)) r2 = xx; // double & decltype(xx) w = xx; // double /* 4. 如果前面都的不满足,则var的类型与expression的类型相同。 这主要用于右值 */ int j = 6; int &k = j; int &n = j; decltype(j+6) i1; // int decltype(100L) i2; // long decltype(k+n) i3; // int k和n是引用,但k+n这个表达式是求两个int的和,所以i3为int
显式具体化
-
显式具体化模板函数
C++对于给定的函数名,可以拥有非模板函数、模板函数和显式具体化模板函数以及他们的重载版本。显式具体化模板函数是指显式指出模板中参数的类型。其匹配优先级大于函数模板
-
显式具体化模板函数是为什么服务的?
为非通用类型服务。比如定义了一个结构,你想要交换这个结构中的内容或某些内容,那么普通通用模板无法完成这个事情,需要显式的再定义一个类型。
-
如何声明、定义、调用一个显式具体化模板函数
struct job { string name; int age; string job_name; } // 普通模板函数必然无法交换两个job结构类型的变量 // 显式具体化模板函数声明 template <> void Swap<job>(job &a, job &b); // 定义 template <> void Swap<job>(job &a, job &b) { ...; } // 调用 // 同其他函数一样。 job sue, jam; Swap(sue, jam);
实例化与具体化
-
实例化分为显式实例化和隐式实例化
-
隐式实例化
就是普通的函数调用,编译器根据参数自动推断变量类型
-
显式实例化
通过显式指定函数模板中参数类型的方式调用模板函数
// 单类型参数 template <typename T> void Swap(T &a, T &b); // 多类型参数 template <typename T1, typename T2> T1 max(T1 a, T2 b) int main() { int x, y; // 显式实例化 Swap<int>(x, y); max<int, double>)(x, y); // 多参数的可以不填满, 前面的指定,后面的进行自动推断 max<double>(x, y); // 会将第一个类型作为double传入 // 但是不能跳跃指定。 max<, double>(x, y); // 不被允许 } -
显式具体化
显式具体化就是不用模板函数,指定显式具体化模板函数
template <> void Swap<job>(job &a, job &b);
-
函数匹配优先级
-
如果参数完全匹配。则优先级为:
-
非模板函数 > 显式具体化模板函数 > 模板函数
-
但是可以显式的指出使用哪个
Swap<>(a, b); // 显式指出使用模板函数
-
-
提升转换。
- char、short转换为int,float转换为double
-
标准转换
- int 转为char, long 转为double
-
用户定义的转换,如类生命命中定义的转换
但一般谁他喵的这样定义,为什么要模板函数,就是因为非模板函数重写类型比较麻烦呗。
#include <iostream>
using namespace std;
// 模板函数 优先级:2
template <typename T>
void Swap(T &a, T &b);
// 非模板函数 优先级:0
void Swap(int &a, int &b);
// 显式具体化模板函数 优先级:1
template <>
void Swap<int>(int &a, int &b);
int main()
{
int a = 5, b = 6;
Swap(a, b);
cout << "a: " << a << "\t" << "b: " << b << endl;
return 0;
}
template <typename T>
void Swap(T &a, T &b)
{
T temp;
temp = a;
a = b;
b = temp;
}
template <>
void Swap<int>(int &a, int &b)
{
int temp;
temp = a;
a = b;
b = temp;
}
void Swap(int &a, int &b)
{
int temp;
temp = a;
a = b;
b = temp;
}
ref
函数的调用过程:blog.csdn.net/fu_zk/artic…
typename和class的区别:www.runoob.com/note/12729,写的也一般。
重载函数匹配顺序:blog.csdn.net/qq_28133013…
[]的优先级大于*
// 所以 [] 优先结合,除非使用()改变优先级
int * arr[10]; // 数组指针, 一个指针指向了数组
int (*arr)[10]; // 指针数组, 一个数组,里面全是指针