2019-10-10

C++的函数

标签(空格分隔): Cpp


函数是C\C++中重要的功能模块。这一部分主要总结C\C++中函数的主要规则和用法。

基本知识

使用函数必须提供:

  • 提供函数定义
  • 提供函数原型
  • 调用函数

定义函数

函数分为两类:有返回值和无返回值
无返回值定义:

void functionName(parameterList){
    statements(s)
    return;
}

有返回值定义:

typeName functionName(parameterList){
    statements
    return value;
}

通用的函数定义格式:

返回类型 函数名(参数列表){
    语句
    return 返回值;
}

函数定义注意事项:

  • 返回值可以是除数组以外任何类型(但是可以把数组放到结构体或类中返回)
  • 当返回值与返回类型不一致时,会发生强制类型转换
  • 参数列表为空和参数列表为void等效
  • 不指定参数列表时,使用(...)

返回值机制:
函数通过将返回值放到指定的内存位置,然后调用函数从指定的内存位置中获取返回值,在这个过程中,调用函数和被调函数必须就返回值的类型达成一致,这样才可以正确的获得返回值。如何达成一致?通过被调函数的原型,原型告诉了编译器这个函数的返回值是什么类型,要求分配指定类型的内存空间,调用函数也需按照此类型获取返回值。如果不一致,则可能发生强制类型转换,或者报错。

函数原型和函数调用

1、为什么需要原型?

  • 原型描述了函数到编译器的接口
    告知编译器,函数的返回类型,函数名称,参数列表等信息。此时,编译器也指定了函数的返回值要存放的位置。

2、原型的语法是什么?

  • 函数原型是一条语句,必须以分号结束。
    最简单的声明方式就是复制函数头,以分号结尾就可以了。原型中参数列表不必须有变量名,变量名就相当于是一个占位符,所以不必须与函数定义中的变量名相同。

3、原型的功能有哪些?

  • 降低程序出错的记录
  • 编译器正确处理函数返回值
  • 编译器检查参数数目是否正确
  • 编译器可以检查参数类型是否正确,可以帮助转换为正确的类型

这一阶段的函数原型检查属于静态类型检查**

函数参数和按值传递

被调函数用来接收参数的变量叫做形参,调用函数用来传递给被调函数的参数称为实参。
在函数声明中的变量(包括参数)是该函数私有的。也就是说,这些变量的作用范围在函数中,出了函数的范围,内存就会被释放。所以说,这些变量都是局部变量

函数与数组

int sum_arr(int arr[],int n)

在上一行代码中,arr不是数组,而是指针!但在函数中可以当做是数组使用。
只有(也就是说当且仅当)在函数头或函数原型中int arr[]和int arr是等效的,都表示arr是一个指针。*

函数如何使用指针来处理数组?

一般来说,数组名就是数组中第一个元素的地址。但是数组名又和数组的第一个元素的地址有一些不同之处,包括:

  • 数组声明使用数组名来标记存储位置
  • 对数组名使用运算符得到的是整个数组的长度(以字节为单位)
  • 对数组名使用地址云算符&时,返回整个数组的地址

也就是说,数组名不同于普通地址的地方在于,数组名包含了数组整体的特性。

int sum_arr(const int * begin, const int * end);

上一行代码介绍了另一种传递数组的方法,传递数组的开始指针和结束指针,遍历数组是可以用

for(int i = 0;(begin + i) != end; i++){}
或者
const int* pt;
for(pt = begin;pt != end; pt ++){}

这种用法常在STL中,叫做“超尾”。

指针和const

用const修饰指针有两种方式:

  • 让指针指向一个常量对象。
    例如:int const * pt = const int* pt = a。这样就禁止了利用指针pt来修改a的值,但是可以修改pt本身。
  • 将指针变量声明为常量。
    例如:int * const pt = a 。这样就是禁止更改pt指向的地址,但是可以通过pt修改a的值。

C++禁止将const的地址赋给非指向const类型的指针,但是可以通过const_cast进行强制类型转换
也就说,一定要保证,如果一个内存单元是const类型的,那么不管是通过变量名还是通过指针都不能更改这个内存单元的值。
要尽量使用const,好处有两点:1、可以避免无意中修改了原本不希望更改的值;2、接受const参数的函数,除了可以接受const的实参,还可以接受非const的实参。

函数和二维数组

难点:如何真确地声明指针?
例子:

int data[3][4] = {{1,2,3,4},{9,8,7,6},{2,4,6,8}};
int total = sum(data,3);
/*如何编写sum函数的原型?*/
int sum(int(*p)[4],int size);
int sum(int p[][4],int size);

解析:
int(*p)[4]和int p[4]的区别,前者是说,p是一个指针,指向有4个元素的数组,每个元素为int;后者是说,p是一个数组有4个元素,每个元素是一个指针,每个指针是int。
int(
p)[4] == int p[][4];两者等价,sizeof(p)返回的是1个指针的内存大小,对p+1,会跳过4个int的内存地址。对于,int *p[4],sizeof(p)返回4个int *的内存大小,对p+1,会跳过1个int *的内存大小。

函数和C-风格字符串

将C-风格字符串作为参数的函数

有三种形式:

  • char数组
  • 字符串字面值
  • char* 指针

C-风格的字符串和char数组之间的一个重要区别是,字符串结尾有内置的结束字符。

函数与结构

结构体赋值,属于深拷贝;
结构体作为参数传递时,函数会生成一个原来结构体的副本,和普通类型的形实结合是一样的。

递归

包含一个递归调用的递归

递归调用的一般形式:

void recurs(argumentlist){
    statements1
    if(test)
        recurs(arguments)
    statements2
}

递归的运行过程,当进入recurs函数后,如果test为真,则再次调用recurs函数,一直执行到test为假,此时再执行statement2,此时,对于之前调用的recurs函数都依次执行statement2,然后结束函数。流程如下:

st=>start: start
op1_1=>operation: Into recurs No.1
op1_2=>operation: statements1 No.1
cond1=>condition: test No.1
op1_3=>operation: statements2 No.1
op2_1=>operation: Into recurs No.2
op2_2=>operation: statements1 No.2
cond2=>condition: test No.2
op2_3=>operation: statements2 No.2
op3_1=>operation: Into recurs No.3
op3_2=>operation: statements1 No.3
cond3=>condition: test No.3
op3_3=>operation: statements2 No.3
op4_1=>operation: Into recurs No.4
op4_2=>operation: statements1 No.4
cond4=>condition: test No.4
op4_3=>operation: statements2 No.4
op5_1=>operation: Into recurs No.5 ...
e=>end

st->op1_1->op1_2->cond1
cond1(yes)->op2_1->op2_2->cond2
cond1(no)->op1_3->e
cond2(yes)->op3_1->op3_2->cond3
cond2(no)->op2_3->op1_3->e
cond3(yes)->op4_1->op4_2->cond4
cond3(no)->op3_3->op2_3->op1_3->e
cond4(yes)->op5_1
cond4(no)->op4_3->op3_3->op2_3->op1_3->e

函数指针

函数指针基础知识

要使用函数指针,需要:

  • 获取函数的地址
  • 声明一个函数指针
  • 使用函数指针来调用函数
  • 获取函数地址
    要获取函数的地址,只要使用函数名就可以了,函数名就是一个函数的首地址。
  • 声明函数指针
    一般声明函数指针的方法:
    把函数原型中的函数名称更改为(pf),pf为指针名称,可以自定义*。声明一个函数指针,必须制定函数指针指向的函数类型。例如:
double pam(int);
double (*pf)(int) = pam;//声明pf指向函数pam
  • 使用指针调用函数
    如果一个函数指针指向了一个函数,那么这个指针就相当于是这个函数的一个别名,可以像使用原来函数那样,使用这个指针来调用函数,比如:
double pam(int);
double (*pf)(int);
pf = pam;
double x = pam(4); //相当于
double y = pf(5); //也相当于
double m = (*pf)(8);

在C++中,一个函数指针pf,可以使用pf调用函数,也可以使用(*pf)调用函数。

函数指针举例

// arfupt.cpp -- an array of function pointers
#include 
// various notations, same signatures
const double * f1(const double ar[], int n);
const double * f2(const double [], int);
const double * f3(const double *, int); //三个函数是一样的特征标,一样的返回类型

int main()
{
    using namespace std;
    double av[3] = {1112.3, 1542.6, 2227.9};

    // pointer to a function
    /*
    p1是一个指针,指向函数,这个函数的特征标是(const double *, int),返回类型是const double* 
    */
    const double *(*p1)(const double *, int) = f1;
    auto p2 = f2;  // C++0x automatic type deduction
    // pre-C++0x can use the following code instead
    /*
    p2是一个指针,指向函数,函数的特征标是(const double *, int),返回类型是const double* 
    */
    // const double *(*p2)(const double *, int) = f2;
     cout << "Using pointers to functions:\n";
    cout << " Address  Value\n";
    /* 
    (*p1)(av,3)相当于p1(av,3)相当于f1(av,3)
    *(*p1)(av,3)相当于*(p1(av,3))相当于*(f1(av,3)),意思是:函数的返回值是一个const double*,取这个指针指向的地址的值。
    */
    cout <<  (*p1)(av,3) << ": " << *(*p1)(av,3) << endl;
    cout << p2(av,3) << ": " << *p2(av,3) << endl;

    // pa an array of pointers
    // auto doesn't work with list initialization
    /*
    pa是一个有三个元素的数组,数组的元素是指针,指针的类型是指向函数的指针,指向的那个函数的特征标是(const double *, int),返回类型是const double *
    */
    const double *(*pa[3])(const double *, int) = {f1,f2,f3};
    // but it does work for initializing to a single value
    // pb a pointer to first element of pa
    auto pb = pa;
    // pre-C++0x can use the following code instead
    /*
    pb是一个指针,指向的是一个指针,被指向的指针指向一个函数,这个函数的特征标是(const double *, int),返回类型是const double *。
    */
    // const double *(**pb)(const double *, int) = pa;
    cout << "\nUsing an array of pointers to functions:\n";
    cout << " Address  Value\n";
    for (int i = 0; i < 3; i++)
        cout << pa[i](av,3) << ": " << *pa[i](av,3) << endl;
    cout << "\nUsing a pointer to a pointer to a function:\n";
    cout << " Address  Value\n";
    for (int i = 0; i < 3; i++)
        cout << pb[i](av,3) << ": " << *pb[i](av,3) << endl;

    // what about a pointer to an array of function pointers
    cout << "\nUsing pointers to an array of pointers:\n";
    cout << " Address  Value\n";
    // easy way to declare pc 
    auto pc = &pa; 
     // pre-C++0x can use the following code instead
    /*
    pc是一个指针,指向一个有3个元素的数组,这个数组的元素是指针,指向的是函数,函数的特征标是(const double *, int),返回类型是const double *
    pc是指向3个元素的数组,因此*pc就是那个有3个元素的数组,那么(*pc)[0]就是n
    */
    // const double *(*(*pc)[3])(const double *, int) = &pa;
   cout << (*pc)[0](av,3) << ": " << *(*pc)[0](av,3) << endl;
    // hard way to declare pd
    const double *(*(*pd)[3])(const double *, int) = &pa;
    // store return value in pdb
    const double * pdb = (*pd)[1](av,3);
    cout << pdb << ": " << *pdb << endl;
    // alternative notation
    cout << (*(*pd)[2])(av,3) << ": " << *(*(*pd)[2])(av,3) << endl;
    // cin.get();
    return 0;
}

// some rather dull functions

const double * f1(const double * ar, int n)
{
    return ar;
}
const double * f2(const double ar[], int n)
{
    return ar+1;
}
const double * f3(const double ar[], int n)
{
    return ar+2;
}

感谢auto

在C++11后,C++中增加了auto关键字,可以自动进行类型推断。但是auto只能用于单值初始化,而不能用于初始化列表,例如:

const double *( *pa[3])(const double *,int) = {f1,f2,f3};
//不能使用auto pa[3] = {f1,f2,f3};

这个时候就不可以用auto进行类型推断。

题外话

在C++中,当知道了一个内存地址保存的是什么类型时,那么所有关于这个内存地址的操作,都需要遵循这个内存地址的类型所规定的操作。

C++内联函数

使用方法(一下二选一):

  • 在函数声明前加上关键字inline
  • 在函数定义前加上关键字inline

注意事项:

  • 内联函数不能递归
  • 声明为内联函数,但是编译器不一定实现成内联函数

内联函数比宏更好用!

引用变量

引用变量相当于一个变量的别名,但是这个别名有什么用呢?
引用变量主要用途是用作函数的形参,通过使用引用变量作为形参,那么函数就是使用的传入的实参,而不是实参的副本。
引用变量必须在声明时进行初始化!!
引用变量和指针:引用变量更像是指针常量,指向某一个变量后,便不能再被更改。

int a = 10;
int & b = a; //b是a的别名

将引用用作函数的参数

当把引用当做是函数的参数时,这种传参数方式叫做按引用传递。
按引用传递时,函数处理的是实参本身,而不是副本。

引用的属性和特别之处

按引用传递参数时,函数对于形参的改变,也会导致实参的改变。如果不想造成实参的改变,那么在按引用传递时,应使用常量引用。
一般来说,如果参数类型是基本数据类型,最好使用按值传递;如果数据比较大(如结构或类的对象时),最好使用按引用传递。
按引用传递时,对函数传入的参数必须是左值,不能是右值。

临时变量、引用参数和const

一般来说,普通变量不能和引用变量进行类型转换。但是在向函数传参数时,部分情况会发生类似的类型转换,就是在传入的实参满足下边条件时,会生成一个临时变量,传入函数。这些临时变量只在函数调用期间存在,此后编译器可以随意将其删除。
在引用参数带有const关键字时:

  • 实参的类型正确,但不是左值
  • 实参的类型不正确,但可以转换成正确的类型。
    左值是可以被引用的数据对象。非左值包括字面常量和包含多项的表达式。现在,常规变量和const变量都是左值,因为可以通过地址访问他们。而const变量属于不可修改的左值。
    函数的引用参数使用const的好处:
  • 使用const可以避免无意中修改数据
  • 使用const可以使函数既可以处理const的实参,还可以处理非const的实参
  • 使用const引用使函数能够正确生成并使用临时变量

右值引用 &&

返回引用时,要注意,不能返回那种在函数返回后不存在的内存单元的引用。要避免这样的问题,两招:

  • 返回一个作为参数传递给函数的引用
  • 返回用new新建的存储空间。(别忘了在函数外delete)
accumulate(dup,five) = four;

这条语句成立的前提是,accumulate函数返回的位置是一个可以被修改的内存单元,也就是说,返回值是一个左值,那么这条语句就成立。但是常规(非引用)返回类型是右值---不能通过地址访问的值,因为这种返回值位于临时内存单元中。

何时使用引用参数

使用引用参数的原因:

  • 程序员能够修改调用函数中的对象
  • 通过传递引用而不是整个数据对象,可以提高程序的运行速度。

什么时候使用按引用传参,什么时候使用按指针传参,什么时候使用按值传参?
对于使用传递的值,而不作修改的函数:

  • 如果数据对象很小,则按值传递
  • 如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向const的指针
  • 如果数据对象是较大的结构,则使用const指针或者const引用,以提高程序效率。
  • 如果数据对象是类对象,则使用const引用

对于修改调用函数中数据的函数:

  • 如果数据对象是内置数据类型,则使用指针。
  • 若数据对象是数组,则只能使用指针
  • 如果数据对象是结构,则使用引用或指针
  • 如果数据对象是类对象,则使用引用

默认参数

如何设置默认值?必须通过函数原型!添加默认参数时,必须是从右到左添加,顺序不能变。
只有原型指定了默认值,函数定义与没有默认参数时一样。

函数重载

函数重载的关键是函数的参数列表,参数列表又称为函数的特征标,包括三部分,参数的数目类型排列顺序。只有这三者同时相同时,才可以说两个函数的特征标相同。(特征标不包含函数的返回类型哟!)
C++中的函数重载,可以允许函数名称相同,但是特征标必须不同。C++把类型的引用和类型本身视为同一个特征标!
在函数重载时,如果传入参数与所有函数的特征标都不完全相同时,会对实参进行类型转换,前提是只有一个函数可以用来作为类型转换的目标,如果有多个时,就会发生错误。例如:

void fun(string,double);
void fun(string,long);
int a = 10;
fun("chongzai",a);//发生错误,因为有两个可以接受的函数,发生了二义性。

const是可以用来区分特征标的。函数可以重载,是因为C++编译器执行了名称修饰。

函数模板

函数模板特性也被称为参数化类型。模板定义:

template 
void 函数名(AnyType a,AnyType b){

}
  • template关键字和typename(或者class)是必需的。
  • <>
  • 模板不创建任何函数,只是告诉编译器如何定义函数
  • 函数模板不能缩短可执行程序

重载的模板

可以像常规函数重载那样,定义重载的模板。

模板的局限性

template 
void f(T a,T b){
    statements
}

局限性主要体现在,模板所假设的类型T可能不能满足模板中statements所用到的部分操作,这是就会出错。比如,如果T为structe类型,statements中有 a + b等等。
解决的方法有两个:
1、重载操作符
2、为特定对象提供具体化的模板定义
这一章主要讲解方法二。

显式具体化

具体化函数定义---显示具体化。当编译器找到与函数调用匹配的具体化定义时,就不再寻找模板了。
具体化的方法和具体化的特点:

  • 对于给定的函数名,可以有非模板函数、模板函数和显式具体化模板函数以及他们的重载版本
  • 显式具体化的原型和定义应以template<>开头,并通过名称来指出类型
  • 对于编译器,选择重载的优先级是:非模板函数>具体化模板函数>模板函数
void Swap(job& job&) //非模板

template 
void Swap(T&,T&) //模板

template <> void Swap(job& job&); //具体化模板

实例化和具体化

实例化:编译器使用模板为特定类型生成函数定义时,得到的是模板实例
实例化可以隐式实例化,也可以显示实例化。隐式实例化是编译器通过函数的参数,自动推断函数实例化的定义的,显示实例化是通过主动说明函数的定义。
显示具体化,是告诉编译器,在生成特定函数定义的时候,需要使用函数具体化的函数模板,而不是普通函数模板。
隐式实例化,显式实例化和显式具体化统称为具体化。相同之处是,他们表示的都是使用具体类型的函数定义,而不是通用描述。
template---显式实例化
template <> ----显示具体化

通用模板、显示具体化、显示实例化、隐式实例化的区别:
通用模板:就是一个模板函数,可以用来实现范式。“基础款模板” 是模板
显示具体化:在通用模板的基础上,针对某一种类型专门实现一种模板,当模板函数需要实例化为这个类型的函数时,要求编译器使用显示具体化模板,而不是通用模板。因此,显示具体化也是一种模板是模板
显示实例化:在函数声明中,进行显示实例化,那么编译器遇到这个声明时,根据模板实现函数的定义。这是显示要求编译器定义一种指定类型的函数。是定义
隐式实例化:当编译器遇到一个和函数模板名称相同的函数语句时,编译器根据这个语句中参数的类型,自动根据模板生成一个定义,无需指定类型,编译器根据参数,自动推断类型。是定义

编译器选择使用哪个函数版本

函数重载不紧可以在不同的函数间,也可以在不同的函数模板间,也可以在函数模板和函数之间进行重载
因此,产生一个问题,编译器应该选择使用哪一个函数版本呢?
重载解析过程:

  • 第一步:创建候选函数列表。只要函数名称相同就可以
  • 第二步:使用候选函数列表创建可行函数列表。要求函数的特征标相同,会发生隐式类型转换,也就是说这一步要求所有的函数放到程序的那个位置都可以执行
  • 第三部:确定最佳的可行函数。在所有的可行函数中,找到一个编译器进行最少操作就可以执行的最佳,就是说,这个函数越不需要编译器帮助就越好

确定最佳函数的顺序

  • 大等级:
    完全匹配 > 提升转换 > 标准转换 > 用户定义的转换

  • 同等级时:
    常规函数 > 具体化模板 > 通用模板
    :如果在同等级时,还有两个相同优先级别的函数,那么重载会报错!!错误为二义性

  • 三种转换

  • 无关紧要要的转换,也就是说,这些形实参的转换可以忽略不计,相当于是完全匹配
    | 从实参 | 到形参 | 备注 |
    | ----------- | ------ | ---- |
    | Type | Type& | 引用 |
    | Type& | Type |
    | Type[] | Type* | 数组 |
    | Type(argument-list)| Type(*)(argumente-list)| 函数 |
    | Type | const Type| 常量 |

| Type | volatile Type| |
| Type* | const Type | 指针 |
| Type* | volatile Type| |

多个参数的函数进行匹配的原则:
一个函数要比其他函数都合适,其所有参数的匹配程度都必须不比其他函数差,同时至少有一个参数的匹配程度比其他函数都高。

C++11特性

decltype关键字:
decltype可以通过语句推断类型。例如:

int x;
decltype(x) y; //让y的类型为x的类型

decltype(x+y) xpy;  //让xpy的类型为语句x+y的结果的类型

后置返回类型

auto h(int x, float y) --> double;
//auto是一个占位符,->double称为后置返回类型。
//结合decltype就可以解决使用模板时不知道返回值什么类型的问题了。例如:
template
auto gt(T1 x, T2 y) -> decltype(x + y){
    ...
    return x + y;
}

在此输入正文

你可能感兴趣的:(2019-10-10)