函数

函数---C++的编程模块(要提高编程效率,可更深入地学习STL和BOOST C++提供的功能

  • 1.提供函数定义 function definition
  • 2.提供函数原型 function prototype
  • 3.调用函数 function call
Void functionName(parameterlist)
{
statement(s)
teturn;
}
  • parameterlist:指定了传递给函数的参数类型和数量
  • void:没有返回值,对于有返回值的函数,必须有返回语句return
  • 1.返回值类型:不能是数组,但可以是其他任何类型---整数,浮点数,指针,甚至可以是结构和对象。
  • 2.函数通过将返回值复制到指定的CPU寄存器或内存单元中来将其返回。

为什么需要原型

原型描述了函数到编译器的接口,它将1.函数返回值类型(如果有的话)以及2.参数的类型和3.数量告诉编译器。(在原型的参数列表中,可以包含变量名,也可以不包含。原型中的变量名相当于占位符,因此不必与函数中的变量名相同)

  • 确保:编译器正确处理1,编译器检查2,3

函数参数传递和按值传递

  • 用于接收传递值的变量被称为形参(parameter),传递给函数的值被称为实参(argument)。
  • 值传递:调用函数时,使用的是实参的副本,而不是原来的数据。
  • 在函数中声明的变量(局部变量(自动变量))(包括参数)是该函数私有的,函数调用时:计算机将为这些变量分配内存;函数结束时:计算机将释放这些变量使用的内存。

函数和数组

int sum_arr(int arr[],int n);//arr=arrayname.n=size
int sum_arr(int arr[],int n);//arr=arrayname.n=size
//两者是等价的
  • const保护数组(输入数组原数据不能改变) void show_array(const double ar[],int n);//声明形参时使用const关键字
  • 该声明表明,指针or指向的是常量数据。这意味着不能使用or修改数据。这并不意味着原始数据必须是常量
  • 如果该函数要修改数组的值,声明ar时不能使用const

  • 1.对于处理数组的C++函数,必须将数组中的

1.数据类型 2.数组的起始位置 3.和数组元素中的数量提交给他

  • 传统的C/C++方法是,将指向数组起始处的指针作为一个参数,将数组长度作为第二个参数(指针指出数组位置和数据类型)

  • 2.第二种方法:指定元素区间(range) 通过传递两个指针来完成:一个指针表示数组的开头,另外一个指针表示数组的尾部。例子:

int sum_arr(const int *begun,const int *end)
{
const int *pt;
int total=0;
for(pt=begin;pt!=end;pt++)
total=toatl+*pt;
return total;
}
int cookies[ArSize]= {1,2,4,8,16,32,64,128};
int sum=sum_arr(cookies,cookies+ArSize);

函数与C风格字符串

假设要将字符串(实际传递的是字符串第一字符的地址)作为参数传递给函数,则表示字符串的方式有三种:

  • 1.char数组
  • 2.用字符串常量
  • 3.被设置为字符串的地址的char指针。

函数和结构

涉及函数时,结构变量的行为更接近基于基本的单值变量

  • 1.按值传递-->如果结构非常大,则复制结构将增加内存要求,且使用的是原始变量的副本
  • 2.传递结构的地址,然后使用指针来访问结构的内容
rect rplace;
polar pplace;
void rect_to_polar(const rect*pxy,polar*pda)
{
...
}
rect_to_polar(&rplace,&pplace);

调用函数时,将结构的地址(&pplace)而不是结构本身(pplace)传递给它;将形参声明为指向polar的指针,即polar*类型。由于函数不应该修改结构,因此使用了const修饰符,由于形参是指针不是结构,因此应使用姐姐成员运算符(->),而不是成员运算符(.)。

  • 3.按引传递用,传指针和传引用效率都高,一般主张是引用传递代码逻辑更加紧凑清晰。

递归---C++函数有一种有趣的特点--可以调用自己(除了main())

1.包含一个递归调用的递归

void recurs(argumentlist)
{
statement1
if(test)
recurs(arguments)
statement2
}

如果调用5次recurs就会运行5次statement1,运行1次statement2.

2.包含多个递归调用的递归

void recurs(argumentlist)
{
if(test)
return;
statement;
recurs(argumentlist1);
recurs(argumentlist2);
}

3.从1加到n

class Solution
{
public:
int Sum_Solution(int n){
    int ans=n;
    ans&&(ans+=Sum_Solution(n-1));
    return ans;
}
};
//&&就是逻辑与,逻辑与有个短路特点,前面为假,后面不计算。

函数指针

函数也有地址---存储其机器语言代码的内存的开始地址

    1. 获取函数的地址,只要使用函数名(后面不跟参数)即可。

例如think()是个函数

process(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. 进阶 下面函数原型的特征表和返回类型相同
const double *f1(const double ar[],int n);
const double *f2(const dopuble [],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;
    1. 使用for循环通过指针依次条用每个函数

例子:声明包含三个函数指针的数组,并初始化

const double * (*pa[3])(const double *,int)={f1,f2,f3};

问:为什么不使用自动类型推断?auto

答:因为自动类型推断只能用于单值初始化,而不能用初始化列表。

但可以声明相同类型的数组 auto pb=pa;

使用:

const double *px=pa[0](av.3);//两种表示法都可以
const double *py=pb[1](av.3);
//创建指向整个数组的指针。由于数组名pa是指向函数指针的指针
auto pc=&pa;//c++11
//等价于
const double * (*(*pd[3]))(const double *,int)=&pa;//C++98
  • 除了auto外,其他简化声明的工具,typedef进行简化 点云库里常常用到,如:typedef pcl::PointNormal PointNT
typedef const double * (*p_fun)(const double *,int );
p_fun p1=f1;

函数探幽

C++11新特性

  1. 函数内联
  2. 按引用传递变量
  3. 默认参数值
  4. 函数重载(多态)
  5. 模板函数

内联函数

c++内联函数-->提高程序运行速度:常规函数与内联函数的区别在于,C++编译器如何将它们组合到程序中

  • 常规函数调用过程:

    1. 执行到函数调用指令程序在函数调用后立即存储该指令地址,并将函数参数复制到堆栈中(为此保留的代码),
    2. 跳到标记起点内存单元,
    3. 执行函数代码(也许将返回值放入寄存器中),
    4. 然后跳回地址被保存的指令处。来回跳跃并记录跳跃位置意味着以前使用函数时,需要一定的开销。
  • 情况:函数代码执行时间很短---内联调用就可以节省非内联调用的大部分时间(节省时间绝对值并不大)

  • 代价:需要占用更多的内存:如果程序在是个不同地方调用一个内联函数,则该函数将包含该函数代码的10个副本

  • 使用:在函数声明前加上关键字inline;在函数定义前加上关键字inline;

通常的做法是省略原型,将整个定义(即函数头和所有代码),放在本应提供原型的地方。

  • 内联函数不能递归
  • 如果函数占用多行(假设没有冗长的标识符),将其作为内联函数不太合适.

内联与宏

C语言使用预处理语句#define来提供宏---内联代码的原始实现

# define SQUARE(X) X*X
  • 这不是通过传递参数实现的,而是通过文本替换实现的---X是"参数"的符号标记。所以宏不能按值传递

故有时候会出现错误

c=10;
d=SQUARE(C++);is replaced by d=C++*c++=11X12=122

按引用传递变量

引用变量-->是复合类型 int & rodents =rats;其中int &是类型,该声明允许将rats和rodent互换---他们指向相同的值和内存单元。

  • 必须在声明引用变量时进行初始化
  • 引用更接近const指针(指向const数据的指针),必须在创建时进行初始化,一旦与某个变量关联起来就一直效忠于它。
int & rodents=rats;
//实际上是下述代码的伪装表示
int * const pr=&rats;
//引用rodents扮演的角色与*pr相同。
//*pr值是个地址,且该地址恒等于&rat-->rats的地址

引用的属性与特别之处

应该尽可能使用const

C++11新增了另外一种引用---右值引用。这种引用可指向右值,是使用&&声明的:

第十八章将讨论如何使用右值引用来实现移动语义(move semantics),以前的引用(使用&声明的引用)现在称为左值引用

    1. 右值引用是对临时对象的一种引用,它是在初始化时完成的,但右值引用不代表引用临时对象后,就不能改变右值引用所引用对象的值,仍然可以初始化后改变临时对象的值
    1. 右值短暂,右值只能绑定到临时对象。所引用对象将要销毁或没有其他用户
    1. 初始化右值引用一定要用一个右值表达式绑定。

例子:

double &&rref=std::sqrt(36.00);//在左值引用中不成立,即使用&来实现也是不允许的
double j=15.0;
double&& jref=2.0*j+18.5;//同样使用左值引用是不能实现的。

将引用用于结构

引用非常适合用于结构和类(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,其效率更高,复制两次和复制一次的区别。
  • 应避免返回函数终止时,不在存在的内存单元引用。为避免这种问题,最简单的方法是,返回一个作为参数传递给函数的引用。作为参数的引用指向调用函数使用的数据,因此返回引用也将指向这些数据。
free_throws& accumlate(free_throws& traget,free_throws& source)
{
traget.attempts+=source.attempts;
traget.mode+=source.mode;
set_pc(target);
return target;
}
  • 另一种方法是用new来分配新的存储空间
const 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。

默认参数值---当函数调用中省略了实参时自动使用的一个值

如何设置默认值?必须通过函数原型

char* left(const char* str,int n=1);原型声明

定义长这样 char * left(const char* str,int n){...}

对于带参数列表的函数,必须从左向右添加默认值:下面代码错误,int j应该也设默认值

int chico(int n,int m=6,int j);//fault
  • 通过默认参数,可以减少要定义的析构函数方法以及方法重载的数量

函数重载

    1. 默认参数让你能够使用不同数目的参数调用的同一个函数。
    1. 而函数多态(函数重载)让你能够使用多个同名函数。
    1. 仅当函数基本上执行相同的任务,但使用不同形式的数据时,才应用函数重载
    1. C++使用名称修饰(名称矫正)来跟踪每一个重载函数

未经过修饰:long MyFunction(int,float);

名称修饰(内部转换):?MyFunctionFoo@@YAXH--->将对参数数目和类型进行编码

重载与多态的区别

  • 重载:是指允许存在多个同名方法,而这些方法的参数不同(特征标不同)。重载的实现是:编译器根据方法不同的参数表,对同名方法的名称做修饰,对于编译器而言,这些同名方法就成了不同的方法。他们的调用地址在编译器就绑定了。**重载,是在编译阶段便已确定具体的代码,对同名不同参数的方法调用(静态联编)
  • C++中,子类中若有同名函数则隐藏父类的同名函数,即子类如果有永明函数则不能继承父类的重载。
  • 多态:是指子类重新定义父类的虚方法(virtual,abstract)。当子类重新定义了父类的虚方法后,父类根据赋给它的不同的子类,动态调用属于子类的方法,这样的方法调用在编译期间是无法确定的。(动态联编)。对于多态,只有等到方法调用的那一刻,编译器才会确定所要调用的具体方法。

重载与覆盖的区别

  1. 重载要求函数名相同,但是参数列列表必须不不同,返回值可以相同也可以不不同。 覆盖要求函数名、参数列列表、返回值必须相同。
  2. 在类中重载是同一个类中不同成员函数之间的关系 在类中覆盖则是⼦子类和基类之间不同成员函数之间的关系
  3. 重载函数的调用是根据参数列表来决定调用哪一个函数 覆盖函数的调用是根据对象类型的不不同决定调用哪一个
  4. 在类中对成员函数重载是不不能够实现多态 在子类中对基类虚函数的覆盖可以实现多态

模板函数---通用的函数描述

  • 用于函数参数个数相同的类型不同的情况,如果参数个数不同,则不能那个使用函数模板
  • 函数模板自动完成重载函数的过程。只需要使用泛型和具体算法来定义函数,编译器将为程序使用特定的参数类型生成正确的函数定义
  • 函数模板允许以任意类型的方式来定义函数。例如,可以这样建立一个交换模板
template <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){...}
  • 函数模板不能缩短可执行程序,最终仍将由两个独立的函数定义,就像以手工方式定义了这些函数一样。最终的代码不包含任何模板,只包含了为程序生成的实际函。使用模板的寒除湿,它使生成多个函数定义更简单,更可靠更常见的情形是将模板放在头文件中,并在需要使用模板的文件中包含头文件

重载的模板

对多个不同类型使用同一种算法(和常规重载一样,被重载的模板的函数特征标必须不同)。

template <typename T>
void Swap(T& a,T& b);
template <typename T>
void Swap(T* a,T* b,int n);
  • 模板的局限性:编写的模板很可能无法处理某些类型

如1.T为数组时,a=b不成立;T为结构时a>b不成立

  • 解决方案:
  1. C++允许重载运算符,以便能够将其用于特定的结构或类
  2. 为特定类型提供具体化的模板定义

显式具体化(explicit specialization)

提供一个具体化函数定义,其中包含所需的代码,当编译器找到与函数调用匹配的具体化定义时,将使用该定义,不再寻找模板。

  • 该内容在代码重用中有不再重复。

重载解析(overloading resolution)---编译器选择哪个版本的函数

对于函数重载,函数模板和函数模板重载,C++需要一个定义良好的策略,来决定为函数调用哪一个函数定义,尤其是有多个参数时

过程:

  1. 创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数。
  2. 使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式的转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。例如,使用float参数的函数调用可以将该参数转换为double,从而与double形参匹配,而模板可以为float生成一个实例。
  3. 确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错。

最佳到最差的顺序:

  1. 完全匹配,但常规函数优先于模板
  2. 提升转换(例如,char和shorts自动转换为int ,float自动转换为double)。
  3. 标准转换(例如,int转换为char,long转换为double)。
  4. 用户定义的转换,如类声明中定义的转换。

完全匹配:完全匹配允许的无关紧要转换

从实参到形参到实参
TypeType &
Type &Type
Type[]* Type
Type(argument-list)Type( * )(argument-list)
Typeconst Type
Typevolatile Type
Type*const Type
Type*volatile Type