第5~8章 函数
建议下载下来用Typora软件阅读markdown文件
一、函数
函数—C++的编程模块(要提高编程效率,可更深入地学习STL和BOOST C++提供的功能)
- 1.提供函数定义 function definition
- 2.提供函数原型 function prototype
- 3.调用函数 function call
1 | Void functionName(parameterlist) |
- parameterlist:指定了传递给函数的参数类型和数量
- void:没有返回值,对于有返回值的函数,必须有返回语句return
- 1.返回值类型有一定的限制:不能是数组,但可以是其他任何类型—整数,浮点数,指针,甚至可以是结构和对象。
- 2.函数通过将返回值复制到指定的CPU寄存器或内存单元中来将其返回。
1.为什么需要原型
原型描述了函数到编译器的接口:
它将1.函数返回值类型(如果有的话)
以及2.参数的类型
和3.数量告诉编译器。(在原型的参数列表中,可以包含变量名,也可以不包含。原型中的变量名相当于占位符,因此不必与函数中的变量名相同)
- 确保:编译器正确处理1,编译器检查2,3
2.函数参数传递和按值传递
- 用于接收传递值的变量被称为形参(parameter),传递给函数的值被称为实参(argument)。
- 值传递:调用函数时,使用的是实参的副本,而不是原来的数据。
- 在函数中声明的变量(局部变量(自动变量))(包括参数)是该函数私有的,函数调用时:计算机将为这些变量分配内存;函数结束时:计算机将释放这些变量使用的内存。
3.函数和数组
1 | int sum_arr(int arr[],int n);//arr=arrayname.n=size |
a.数组与指针
- 在C++中,当且仅当用于函数头或者函数原型中,
int * arr
和int arr[]
的含义才是相同的。它们意味着arr是一个int指针。 - 然而,数组表示法(int arr[])提醒用户,arr不仅指向int,还指向int数组的第一个int。
b.const保护数组(输入数组原数据不能改变)
void show_array(const double ar[],int n);
//声明形参时使用const关键字
- 该声明表明,指针or指向的是常量数据。这意味着不能使用or修改数据。这并不意味着原始数据必须是常量,只意味着不能使用该指针来修改原始数据。
- 如果该函数要修改数组的值,声明ar时不能使用const
c.如何使用数组作为参数传递给函数
对于处理数组的C++函数,必须将数组中的
1.数据类型
2.数组的起始位置
3.和数组元素中的数量提交给他
- 传统的C/C++方法是,将指向数组起始处的指针作为一个参数,将数组长度作为第二个参数(指针指出数组位置和数据类型)
- 第二种方法:指定元素区间(range)
通过传递两个指针来完成:一个指针表示数组的开头,另外一个指针表示数组的尾部。例子:
1 | int sum_arr(const int *begun,const int *end) |
4.函数与C风格字符串
假设要将字符串(实际传递的是字符串第一字符的地址)作为参数传递给函数,则表示字符串的方式有三种:
- 1.char数组T
- 2.用引号的字符串常量
- 3.被设置为字符串的地址的char指针。
5.函数和结构
涉及函数时,结构变量的行为更接近基于基本的单值变量
- 1.按值传递–>如果结构非常大,则复制结构将增加内存要求,且
使用的是原始变量的副本
- 2.传递结构的地址,然后使用指针来访问结构的内容
1 | rect rplace; |
调用函数时,将结构的地址(&pplace)而不是结构本身(pplace)传递给它;将形参声明为指向polar的指针,即polar*
类型。由于函数不应该修改结构,因此使用了const修饰符,由于形参是指针不是结构,因此应使用间接成员运算符(->),而不是成员运算符(.)。
- 3.按引传递用,传指针和传引用效率都高,一般主张是引用传递代码逻辑更加紧凑清晰。
递归—C++函数有一种有趣的特点–可以调用自己(除了main())
1.包含一个递归调用的递归
1 | void recurs(argumentlist) |
如果调用5次recurs就会运行5次statement1,运行1次statement2.
2.包含多个递归调用的递归
1 | void recurs(argumentlist) |
3.从1加到n
1 | class Solution |
6.函数指针
函数也有地址—存储其机器语言代码的内存的开始地址
- 获取函数的地址,只要使用函数名(后面不跟参数)即可。
例如think()是个函数
1
2
3
4
5
6
7
8
9
10
11process(think);//传递的是地址
thought(think());//传递的是函数返回值
//使用
double pam(int);//原始函数声明
double (*pf)(int);//函数指针声明
pf=pam;//使用指针指向pam函数
double x=pam(4);//使用函数名调用pam()
double y=(*pf)(5);//使用指针调用pam()
//也可以这样使用函数指针
double y=pf(5);
- 获取函数的地址,只要使用函数名(后面不跟参数)即可。
- 进阶
下面函数原型的特征表和返回类型相同1
2
3
4
5
6
7
8
9const double *f1(const double ar[],int n);
const double *f2(const double [],int );
const double *f3(const double *,int );
//声明一个指针可以指向f1,f2,f3
const double * (*p1)(const double *,int );//返回类型相同,函数的特征标相同
//声明并初始化
const double * (*p1)(const double *,int )=f1;
//也可以使用自动类型推断
auto p2=f2;
- 进阶
- 使用for循环通过指针依次条用每个函数
例子:声明包含三个函数指针的数组,并初始化
- 使用for循环通过指针依次条用每个函数
const double * (*pa[3])(const double *,int)={f1,f2,f3};
问:为什么不使用自动类型推断?auto
答:因为自动类型推断只能用于单值初始化,而不能用初始化列表。
但可以声明相同类型的数组 auto pb=pa;
使用:
1 | const double *px=pa[0](av.3);//两种表示法都可以 |
- 除了auto外,其他简化声明的工具,typedef进行简化
点云库里常常用到,如:typedef pcl::PointNormal PointNT
1 | typedef const double * (*p_fun)(const double *,int ); |
二、函数探幽
C++11新特性
- 函数内联
- 按引用传递变量
- 默认参数值
- 函数重载(多态)
- 模板函数
1.内联函数
c++内联函数–>提高程序运行速度:常规函数与内联函数的区别在于,C++编译器如何将它们组合到程序中
- 常规函数调用过程:
- 执行到函数调用指令程序在函数调用后立即存储该指令地址,并将函数参数复制到堆栈中(为此保留的代码),
- 跳到标记起点内存单元,
- 执行函数代码(也许将返回值放入寄存器中),
- 然后跳回地址被保存的指令处。
来回跳跃并记录跳跃位置意味着以前使用函数时,需要一定的开销。
- 情况:函数代码执行时间很短—内联调用就可以节省非内联调用的大部分时间(节省时间绝对值并不大)
- 代价:需要占用更多的内存:如果程序在是个不同地方调用一个内联函数,则该函数将包含该函数代码的10个副本
- 使用:
在函数声明前加上关键字inline;
在函数定义前加上关键字inline;
通常的做法是省略原型,将整个定义(即函数头和所有代码),放在本应提供原型的地方。
- 内联函数不能递归
- 如果函数占用多行(假设没有冗长的标识符),将其作为内联函数不太合适.
内联与宏
C语言使用预处理语句#define
来提供宏—内联代码的原始实现
1 | # define SQUARE(X) X*X |
- 这不是通过传递参数实现的,而是通过文本替换实现的—X是”参数”的符号标记。所以宏不能按值传递
故有时候会出现错误
1 | c=10; |
2.按引用传递变量
引用变量–>是复合类型int & rodents =rats;
其中int &是类型,该声明允许将rats和rodent互换—他们指向相同的值和内存单元。
- 必须在声明引用变量时进行初始化
- 引用更接近const指针(指向const数据的指针),必须在创建时进行初始化,一旦与某个变量关联起来就一直效忠于它。
1
2
3
4
5int & rodents=rats;
//实际上是下述代码的伪装表示
int * const pr=&rats;
//引用rodents扮演的角色与*pr相同。
//*pr值是个地址,且该地址恒等于&rat-->rats的地址
引用的属性与特别之处
应该尽可能使用const
C++11新增了另外一种引用—右值引用。这种引用可指向右值,是使用&&声明的:
第十八章将讨论如何使用右值引用来实现移动语义(move semantics),以前的引用(使用&声明的引用)现在称为左值引用
- 右值引用是对临时对象的一种引用,它是在初始化时完成的,但右值引用不代表引用临时对象后,就不能改变右值引用所引用对象的值,仍然可以初始化后改变临时对象的值
- 右值短暂,右值只能绑定到临时对象。所引用对象将要销毁或没有其他用户
- 初始化右值引用一定要用一个右值表达式绑定。
例子:
1 | double &&rref=std::sqrt(36.00);//在左值引用中不成立,即使用&来实现也是不允许的 |
将引用用于结构
引用非常适合用于结构和类(C++用户定义类型)而不是基本的内置类型。
声明函数原型,在函数中将指向该结构的引用作为参数:
void set_pc(free_throws & tf);
如果不希望函数修改传入的结构。可使用const;void display(free_throws & tf);
返回引用:free_throws &accumlate(free_throws& traget,free_throws& source);为何要返回引用?如果accumlate()返回一个结构,如:dup=accumlate(team,five) 而不是指向结构的引用。这将把整个结构复制到一个临时位置,再将这个拷贝复制给dup。但在返回值为引用时,直接把team复制到dup,其效率更高,复制两次和复制一次的区别。
应避免返回函数终止时,不在存在的内存单元引用。为避免这种问题,最简单的方法是,返回一个作为参数传递给函数的引用。作为参数的引用指向调用函数使用的数据,因此返回引用也将指向这些数据。
1
2
3
4
5
6
7free_throws& accumlate(free_throws& traget,free_throws& source)
{
traget.attempts+=source.attempts;
traget.mode+=source.mode;
set_pc(target);
return target;
}另一种方法是用new来分配新的存储空间
1
2
3
4
5
6
7const free_throws& clone(&three)
{
free_throws * pt;//创建无名的free_throws结构,并让指针pt指向该结构,因此*pt就是该结构,在不需要new分配的内存时,应使用delete来释放它们。
//auto_ptr模板以及unique_ptr可帮助程序员自动完成释放
* pt=ft;
return *pt;//实际上返回的是该结构的引用
}
将引用用于对象
和结构同理
对象继承和引用
使得能够将特性从一个类传递给另外一个类的语言被称为继承
ostream–>基类 ofstream–>派生类
基类引用可以指向派生类对象,而无需强制类型转换
时使用引用参数
使用引用参数到主要原因有两个:
(1)程序员能够修改调用函数中的数据对象。
(2)通过传递引用而不是整个数据对象,可以提高程序的运行速度。
当数据对象较大时(如结构和类对象),第二个原因最重要。这些也是使用指针参数的原因。这是有道理的,因为引用参数实际上是基于指针的代码的另一个接口。那么什么时候应该使用引用,什么时候应该使用指针呢?什么时候应该按值传递呢?下面是一些指导原则:
对于使用传递到值而不做修改到函数:
(1)如果数据对象很小,如内置数据类型或小型结构,则按值传递。
(2)如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向const的指针。
(3)如果数据对象是较大的结构,则使用const指针或const引用,以提高程序的效率。这样可以节省复制结构所需要的时间和空间。
(4)如果数据对象是类对象,则使用const引用。类设计的语义常常要求使用引用,这是C++新增这项特性的主要原因。因此,传递类对象参数的标准方式是按引用传递。
对于修改调用函数中数据的函数:
(1)如果数据对象是内置数据类型,则使用指针。如果看到诸如fixit(&x)这样的代码(其中x是int),则很明显,该函数将修改x。
(2)如果数据对象是数组,则只能使用指针。
(3)如果数据对象是结构,则使用引用或指针。
(4)如果数据对象是类对象,则使用引用。
当然,这只是一些指导原则,很可能有充分到理由做出其他的选择。例如,对于基本类型,cin使用引用,因此可以使用cin>>n,而不是cin>>&n。
3.默认参数值—当函数调用中省略了实参时自动使用的一个值
如何设置默认值?**必须通过函数原型
char* left(const char* str,int n=1);
原型声明
定义长这样 char * left(const char* str,int n){…}
对于带参数列表的函数,必须从左向右添加默认值:下面代码错误,int j应该也设默认值
1 | int chico(int n,int m=6,int j);//fault |
- 通过默认参数,可以减少要定义的析构函数,方法以及方法重载的数量
4.函数重载
- 默认参数让你能够使用不同数目的参数调用的同一个函数。
- 而函数多态(函数重载)让你能够使用多个同名函数。
- 仅当函数基本上执行相同的任务,但使用不同形式的数据时,才应用函数重载
- C++使用名称修饰(名称矫正)来跟踪每一个重载函数
未经过修饰:long MyFunction(int,float);
名称修饰(内部转换):?MyFunctionFoo@@YAXH
—>将对参数数目和类型进行编码
重载与多态的区别
- 重载:是指允许存在多个同名方法,而这些方法的参数不同(特征标不同)。重载的实现是:编译器根据方法不同的参数表,对同名方法的名称做修饰,对于编译器而言,这些同名方法就成了不同的方法。他们的调用地址在编译器就绑定了。重载,是在编译阶段便已确定具体的代码,对同名不同参数的方法调用(静态联编)
- C++中,子类中若有同名函数则隐藏父类的同名函数,即子类如果有永明函数则不能继承父类的重载。
- 多态:是指子类重新定义父类的虚方法(virtual,abstract)。当子类重新定义了父类的虚方法后,父类根据赋给它的不同的子类,动态调用属于子类的方法,这样的方法调用在编译期间是无法确定的。(动态联编)。对于多态,只有等到方法调用的那一刻,编译器才会确定所要调用的具体方法。
重载与覆盖的区别
- 重载要求函数名相同,但是参数列列表必须不不同,返回值可以相同也可以不不同。
覆盖要求函数名、参数列列表、返回值必须相同。 - 在类中重载是同一个类中不同成员函数之间的关系
在类中覆盖则是⼦子类和基类之间不同成员函数之间的关系 - 重载函数的调用是根据参数列表来决定调用哪一个函数 覆盖函数的调用是根据对象类型的不不同决定调用哪一个
- 在类中对成员函数重载是不不能够实现多态 在子类中对基类虚函数的覆盖可以实现多态
5.模板函数—通用的函数描述
用于函数参数个数相同的类型不同的情况,如果参数个数不同,则不能那个使用函数模板
函数模板自动完成重载函数的过程。只需要使用泛型和具体算法来定义函数,编译器将为程序使用特定的参数类型生成正确的函数定义
函数模板允许以任意类型的方式来定义函数。例如,可以这样建立一个交换模板
1
2
3
4
5
6
7
8template <typename AnyType>
void Swap(AnyType &a,AnyType &a)
{
AnyType temp;
temp=a;
a=b;
b=temp;
}模板不会创建任何函数,而只是告诉编译器如何定义函数
C++98没有关键字typename,使用的是
template<class AnyType>void Swap(AnyType &a,AnyType &a){...}
函数模板不能缩短可执行程序,最终仍将由两个独立的函数定义,就像以手工方式定义了这些函数一样。最终的代码不包含任何模板,只包含了为程序生成的实际函。使用模板的寒除湿,它使生成多个函数定义更简单,更可靠更常见的情形是将模板放在头文件中,并在需要使用模板的文件中包含头文件
重载的模板
对多个不同类型使用同一种算法(和常规重载一样,被重载的模板的函数特征标必须不同)。
1 | template <typename T> |
模板的局限性:编写的模板很可能无法处理某些类型
如1.T为数组时,a=b不成立;T为结构时a>b不成立
解决方案:
- C++允许重载运算符,以便能够将其用于特定的结构或类
- 为特定类型提供具体化的模板定义
显式具体化(explicit specialization)
提供一个具体化函数定义,其中包含所需的代码,当编译器找到与函数调用匹配的具体化定义时,将使用该定义,不再寻找模板。
- 该内容在代码重用中有不再重复。
重载解析(overloading resolution)—编译器选择哪个版本的函数
对于函数重载,函数模板和函数模板重载,C++需要一个定义良好的策略,来决定为函数调用哪一个函数定义,尤其是有多个参数时
过程:
- 创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数。
- 使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式的转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。例如,使用float参数的函数调用可以将该参数转换为double,从而与double形参匹配,而模板可以为float生成一个实例。
- 确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错。
最佳到最差的顺序:
- 完全匹配,但常规函数优先于模板
- 提升转换(例如,char和shorts自动转换为int ,float自动转换为double)。
- 标准转换(例如,int转换为char,long转换为double)。
- 用户定义的转换,如类声明中定义的转换。
完全匹配:完全匹配允许的无关紧要转换
从实参到形参 | 到实参 |
---|---|
Type | Type & |
Type & | Type |
Type[] | * Type |
Type(argument-list) | Type( * )(argument-list) |
Type | const Type |
Type | volatile Type |
Type* | const Type |
Type* | volatile Type |
*** |