灯火互联
管理员
管理员
  • 注册日期2011-07-27
  • 发帖数41778
  • QQ
  • 火币41290枚
  • 粉丝1086
  • 关注100
  • 终身成就奖
  • 最爱沙发
  • 忠实会员
  • 灌水天才奖
  • 贴图大师奖
  • 原创先锋奖
  • 特殊贡献奖
  • 宣传大使奖
  • 优秀斑竹奖
  • 社区明星
阅读:8265回复:1

[C++技术]C++的那些事:函数全解析

楼主#
更多 发布于:2014-05-06 10:57
一、函数的结构
image
 
 
 
函数在C++中可能出现在三种地方,一是函数的定义,它包括了如上图的结构;二是函数的声明,它与函数的定义相比,没有了函数体部分;三则是函数的调用。当然,不同的函数定义可以还会稍有不同,比如类的成员函数、内联函数等。这里我们主要讨论函数的调用时需要注意的一些问题。
 
 
 
二、参数传递
 
我们将函数定义或声明里的参数叫形参,而在调用函数时传入的参数叫实参。那么根据形参类型的不同,有几下形式的参数传递。
 
 
 
1,非引用形参
 
1)普通的内置类型
 
 
 
普通非引用类型的参数通过复制对应的实参实现形参的初始化。当用实参的副本初始化形参时,函数并没有访问调用所传递的实参的本身,因此函数不可能改实参的值。比如下面的交换两个数的程序:
 
 
 
复制代码
 
void swap(int v1, int v2)
 
{
 
    int temp = v1;
 
    v2 = v1;
 
    v1 = temp;
 
}
 
 
 
swap(a, b);// 调用swap
 
复制代码
 
上面程序中,实参为a与b,但是在调用时,v1与v2接受的是a与b的副本,所以实际上a与b的值没有变化。
 
 
 
2)指针形参
 
 
 
函数的形参可以是指针,此时将复制实参指针,其实这类跟1)原理类似,函数内并无法改变实参的指针值。只是函数可以通过复制到的地址改变实参指针所指向的值。
 
 
 
复制代码
 
void swap(int* v1, int* v2)
 
{
 
    int temp = *v2;
 
    *v2 = *v1;
 
    *v1 = temp;
 
}
 
int main()
 
{
 
    int a = 10,b = 20;
 
    int *p1 = &a,*p2 = &b;
 
    swap(p1,p2);
 
    return 0;
 
}
 
复制代码
 
上面程序中定义的swap的形参为指针类型,main中调用swap,实际上swap并不能改变p1与p2的值,只是改变了它们所指向的值。
 
 
 
3)const 形参
 
 
 
对于普通的非引用类型用const修饰实际上是没有意义的,因为本来函数就不会改变实参的值。像下面的定义,实际中编译器会忽略const的定义,而将其视为int型。
 
 
 
void fcn(const int i);
 
2,引用形参
 
1)在上面的程序中我们看到,如果想交换两个变量的值,通过调用普通的非引用类型形参的函数,并不能实现。用它们的指针可以,同时我们也可以用引用。
 
 
 
复制代码
 
void swap(int& v1, int& v2)
 
{
 
    int temp = v2;
 
    v2 = v1;
 
    v1 = temp;
 
}
 
int main()
 
{
 
    int a = 10,b = 20;
 
    swap(a,b);
 
    return 0;
 
}
 
复制代码
 
在实际调用swap时,v1与v2实际相当于a与b的另一个名字。
 
 
 
2)在有的时候我们需要向函数传递大型对象,需要使用引用形参,如果直接使用复制实参的形式可以,但是它的效率太低了,甚至有些对象是无法复制的。但是使用引用形参时,我们不希望函数改变了实参传入的值,我们就可以使用const来限定形参。下面程序用来判断哪个字符串更长,明显我们不希望函数会改变字符串的内容,我们就可以用const引用型的形参。
 
 
 
bool isLonger(const string &s1, const string &s2)
 
{
 
    return s1.size() > s2.size();
 
}
 
所以,如果使用引用形参的惟一的目的是避免复制实参时,则应将形参定义为const引用。
 
 
 
3)在使用引用形参函数时,有两点值得注意:
 
 
 
不要用const限定的实参或字面值来调用非const引用形参函数。因为这样函数内,可以改变实参的值,这不合法。
 
 
 
非const引用形参只能与完全同类型的非const对象关联。
 
 
 
4)传递指向指针的引用
 
 
 
如下有下面的程序:
 
 
 
复制代码
 
void swap(int* &v1, int* &v2)
 
{
 
    int* temp = v2;
 
    v2 = v1;
 
    v1 = temp;
 
}
 
int main()
 
{
 
    int a = 10,b = 20;
 
    int* p1 = &a, *p2 = &b;
 
    swap(p1,p2);
 
    return 0;
 
}
 
复制代码
 
上面的程序依然不能改变a与b的值,但是它改变了p1与p2的值,现在p1指向了b,而p2指向了a。
 
 
 
3,其他类型的形参
 
1)vector和其他类型的形参:一般在这种类型作为形参时,为了避免复制应该考虑形参声明为引用类型。C++程序员倾向于传递容器中需要处理的元素的迭代器来传递容器。
 
 
 
2)数组形参:由于数组不能复制,所以不能直接编写数组类型的形参函数,一般通过传递指向数组的元素的指针来处理数组。值得注意的是在通过引用传递数组时,在调用函数时形参与实参的类型要匹配。
 
 
 
复制代码
 
void printValues(int (&ar)[10]);
 
int main()
 
{
 
    int i = 0, j[2] = { 0, 1 };
 
    int k[10] = {0,1,2,3,4,5,6,7,8,9};
 
    printValues(i);  //error int不能初始化 int(&)[10]
 
    printValues(j);  //error int[2] 不能初始化 int(&)[10]
 
    printValues(k);   // ok
 
    return 0;
 
}
 
复制代码
 
二、函数的返回值
 
1)没有返回值
 
 
 
很多函数并没有返回值,尤其是现在C++风格,习惯于把需要的结果作为引用形参。这类型函数一般没有return语句,有时候有return是使函数中途中断执行。
 
 
 
2)返回非引用类型
 
 
 
这种情况在函数调用处,程序会用一个临时变量复制函数的返回值。
 
 
 
3)返回引用
 
 
 
当函数返回引用类型时,并没有复制返回值。相反,返回的是对象本身。
 
 
 
在返回引用这种情况下,注意不要返回局部变量的引用,因为局部变量在函数体内定义,当函数执行完后就销毁了,所谓的引用也就没有意义了。同理,不要返回指向局部变量的指针。
 
 
 
三、重载函数
 
出现在相同作用域中的两个函数,如果具有相同的名字而形参不同,则称为重载函数。
 
 
 
1)注意区分函数重载与重复声明
 
 
 
有些看起来不同的形参,本质是相同的。下面代码中的都是重复声明的例子
 
 
 
复制代码
 
typedef double newDouble;
 
int func(double i);
 
int func(newDouble i);  // 没有新类型
 
 
 
int func1(int, int = 1); //只是提供默认参数
 
int func1(int ,int);
 
 
 
int func2(int);
 
int func2(const int);  //对于普通非引用形参用cosnt修饰是没有意义的
 
复制代码
 
2)重载与作用域
 
 
 
局部声明的函数,将屏蔽所有全局作用的同名函数。下面例子显示,即使全局作用的函数更加匹配调用的实参类型,但是仍然调用的是局部的函数。
 
 
 
复制代码
 
void print(int);
 
int main()
 
{
 
    void print(double);
 
    print(42);
 
    return 0;
 
}
 
复制代码
 
上面程序中,将调用void print(double)函数,虽然42是int型。
 
 
 
3)重载确定的三个步骤
 
 
 
如果定义了众多的函数重载,将存在函数调用到底与哪个重载函数相匹配的问题。我们通过下面的示例代码来说明问题:
 
 
 
void f();  // 1
 
void f(int);// 2
 
void f(double);// 3
 
void f(int, int);// 4
 
void f(double, double);// 5
 
第一步:确定候选函数
 
 
 
假如我们调用f(4.2),那么先找到同名函数,并且在作用域内可见,上面例子中5个函数都满足。
 
 
 
第二步:选择可行的函数
 
 
 
必须满足2个条件:一是函数形参与该调用实参个数相同;第二,每个实参的类型必须与对应的类型匹配,或者可以被隐式转换为对应的形参类型。这里我们再调用f(4.2)时,排除了1、4、5号函数,只剩下2与3。其中2号函数可以通过类型转换来满足。
 
 
 
第三步:寻找最佳匹配
 
 
 
在经过第二步确定后,剩下2与3函数,那么2需要进行类型转换,显然3是最佳匹配了。
 
 
 
但是如果这样调用f(42,4.2)。这时候就会出现二义性,编译器将提示。
 
 
 
还有一种要注意的就是有默认参数的函数,比如我们定义6号函数为void f(double,int =1);那么在调用f(4.2)时就会有二义性。
 
 
 
可基于函数的引用形参是指向const对象还是指向非const对象实现函数重载。
 
 
 
bool isLonger(const string &s1, const string &s2)
 
{
 
    return s1.size() > s2.size();
 
}
 
四、函数指针
 
1,如何定义一个指针为函数类型?我们知道一个函数的类型是由它的返回类型和形参类型共同决定,而与函数名无关。所以在定义一个指向函数的指针,必须包含形参表与返回值这些信息。
 
 
 
下面来看一个比较两个字符串长度的函数:
 
 
 
bool lengthCompare(const string&, const string&);
 
那么这个函数的类型即bool(const string&, const string&) 。如果要想定义一具指向这种类型函数的指针,则可以如下定义:
 
 
 
bool (*pf) (const string&, const string&);
 
注意上式中*pf外面的括号不可以省略,不然pf就成了一个函数的定义,这个函数返回一个指向bool类型的指针。
 
 
 
2,当我们把函数名作为一个值使用时,该函数自动地转换成指针,所以我们可以这样对函数指针pf初始化。
 
 
 
pf = lengthCompare;
 
pf = &lengthCompare;
 
上面两种方法是等价的。
 
 
 
那么在使用函数指针时,我们可以解引用,也可以不解引用。
 
 
 
bool b1 = pf("hello","goodbye");
 
bool b2 = (*pf)("hello","goodbye");
 
3,在给函数指针赋值的时候,一定要注意函数类型的完全匹配,但是我们可以给指向任意函数类型的指针赋一个nullptr或值为0的整型常量表达式,表示该指针没有指向任何一个函数。
 
 
 
4,如果定义了指向重载函数的指针,在使用这个指针时并不是根据形参来确定所调用的函数,而是根据指针的具体函数类型。即,编译器根据指针类型决定选用哪个函数,指针类型必须与重载函数中的某一个精确匹配。
 
 
 
复制代码
 
void ff(int*);
 
void ff(int);
 
void ff(unsigned int);
 
 
 
void (*pf)(unsigned int) = ff; // pf指向ffunsigned int)
 
int num = 42;
 
pf(num); // num将转换为unsigned类型
 
复制代码
 
5,函数指针作为另一个函数的形参。
 
 
 
有的时候,我们需要将一个函数作为一个参数传递给别一个参数,比如定义一个函数用来返回两个对象中较大的那个,那么我们需要将一个比较函数作为参数传递。
 
 
 
const string& BigString(const string& s1, const string& s2, bool pf(const string& ,const string&));
 
const string& BigString(const string& s1, const string& s2, bool (*pf)(const string& ,const string&));
 
上面定义的函数原型显得有点冗长,我们可以定义函数类型,来简化上面的代码:
 
 
 
// 定义函数类型
 
typedef bool Func(const string&, const string&);
 
typedef decltype(lengthCompare) Func;
 
// 定义指针类型
 
typedef bool (*Func)(const string&, const string&);
 
typedef decltype(lengthCompare)* Func;
 
有了上面的定义,我们就可以简单BigString的定义了:
 
 
 
const string& BigString(const string& ,const string, Func);
 
6,函数返回值为一个函数指针。
 
 
 
我们知道,函数并不能返回一个函数,但是可以返回一个指向函数的指针。
 
 
 
最简单的方法,我们用类型别名定义一种函数类型:
 
 
 
using F = int(int*,int); // F是一个返回int,接受一个指向int类型的指针和一个int类型,F是一种函数类型
 
using PF = int(*) (int*, int); // PF是一个函数指针类型
 
下面我们来定义返回函数指针的函数:
 
 
 
PF f(int); // f是一个函数,它返回一个函数指针
 
F f(int); // 错误:F是一个函数类型
 
F* f(int); // OK
 
当然我们也可以直接定义f:
 
 
 
int (*f(int)) (int*,int);
 
由内向外观察:首先f有一个形参表(int),所以f是一个函数,然后f的的左边有一个*,说明f返回的是一个指针。进一步发现,指针的类型本身也包含形参表,因此指针指向函数,该函数的返回类型是int。
 
 
 
我们还可以用C++11中的尾置返回类型来声明一个返回函数指针的函数:
 
 
 
auto f(int) -> int (*)(int*, int);
 
如果我们需要返回的函数类型有一个函数实例,那么我们可以用decltype来说明函数的类型:
 
 
 
int func(int*,int);
 
decltype(func)* f(int);
 
注意上面代码中decltype(func)返回的是一个函数类型,我们需要在后面加上*,说明一个函数指针类型。

喜欢0 评分0
Yuki520
禁止发言
禁止发言
  • 注册日期2022-07-04
  • 发帖数4
  • QQ2663812151
  • 火币5枚
  • 粉丝0
  • 关注0
沙发#
发布于:2022-07-04 10:51
用户被禁言,该主题自动屏蔽!

回复(0) 喜欢(0)     评分
游客

返回顶部