C++ - 函数

  • 参数传递

1、函数传参应尽量使用引用传递避免使用值传递。

因为拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型在内)根本不支持拷贝操作。所以使用引用参数较为明智。

2、如果函数无需改变引用形参的值,最好声明为常量引用。

使用引用而非常量引用会极大限制函数所能接受的实参类型。如不能把const对象、字面值或需要类型转换的对象传递给普通引用形参。

3、数组形参

因为不允许拷贝数组和使用数组时通常会将其转换成指针,所以无法以值传递的方式使用数组参数。我们可以把形参写成类似数组的形式,但实际上传递的是指向数组首元素的指针。

//下面三个print函数是等价的,且都有一个const int*类型的形参
void print(const int*);
void print(const int[]);
void print(const int[10]);    //这里的维度表示期望数组有多少元素,实际不一定

int i = 0, j[2] = {0, 1};
print(&i);    //正确:&i的类型是int*
print(j);    //正确:j转换成int*并指向j[0]

如果我们传给print函数的是一个数组,则实参自动转换成指向数组首元素的指针,数组的大小对函数的调用没有影响,但是使用数组时必须确保不会越界
因为数组以指针形式传递,所以一开始函数并不知道数组的具体尺寸,调用者应提供一些额外信息。有三种常用技术:
使用标记指定数组长度(要求数组本身包含一个结束标记)
使用标准库规范(传递指向数组首元素和尾后元素的指针)
显示传递一个表示数组大小的形参
数组引用形参

void print(int (&arr)[10]){    //形参是数组的引用,维度是类型的一部分
    for(auto elem : arr)
        cout << elem <<endl;
}
int i = 0, j[2] = {0, 1};
int k[10] = {0,1,2,3,4,5,6,7,8,9};
print(&i);    //错误:实参不是含有10个整数的数组
print(j);    //错误:实参不是含有10个整数的数组
print(k)    //正确:实参是含有10个整数的数组

注意&arr两端的括号必不可少。int (&arr)[10]是具有10个整数的数组的引用,而int &arr[10]将arr声明成了引用的数组。

  • 含有可变形参的函数

两种主要方法
1、可变参数模板(函数重载)
2、如果实参类型相同,可以传递一个名为initializer_list的标准库类型

  • 返回类型

1、无返回值的return语句只能用在返回类型是void的函数中。
2、返回类型是void的函数可以使用return expression形式的返回语句,但expression必须是另一个返回void的函数。强行令void函数返回其他类型的表达式将产生编译错误。
3、在含有return语句的循环后面应该也有一条return语句,没有的话该程序就是错误的。
4、不要返回局部对象的引用或指针

const string &manip(){
    string ret;
    if(!ret.empty())
        return ret;        //错误:返回局部对象的引用
    else
        return "Empty";    //错误:"Empty"是一个局部临时量

5、返回数组指针

(一)使用类型别名

typedef int arrT[10];    //arrT是一个类型别名,表示含有十个整数的数组
using arrT = int[10];    //arrT的等价声明
arrT* func(int i);    //func返回一个指向含有十个整数的数组的指针


(二)声明一个返回数组指针的函数

type (*function(parameter_list))[dimension]
type表示元素类型,dimension表示数组大小,parameter_list是形参列表
int (*func(int i))[10];
逐步分析:
func(int i)表示调用func函数时需要一个int型实参
(*func(int i))表示可以对函数调用的结果执行解引用操作
(*func(int i))[10]表示解引用func的调用会得到一个大小是10的数组
int (*func(int i))[10]表示数组中的元素是int型


(三)使用尾置返回类型
尾置返回类型跟在形参列表后面并以一个->符号开头,原本返回类型的地方放置一个auto

auto func(int i) -> int(*)[10];


(四)使用decltype

int odd[] = {0,1,2,3,4,5,6,7,8,9};
decltype(odd) *func(int i);

  • 函数重载

(一)有顶层const的形参无法和另一个没有顶层const的形参区分开来。

int func(int);
int func(const int);    //重复声明了int func(int)
int func(int*);
int func(int* const);    //重复声明了int func(int*)

上面每组的两个声明是等价的。
但如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时const是底层的

int func(int&);    //作用于int的引用
int func(const int&);    //新函数,作用于常量引用
int func(int*);    //新函数,作用域指向int的指针
int func(const int*);    //新函数,作用域指向常量的指针


(二)const_cast在重载函数中的使用

const string &shorterString(const string &s1, const string &s2){
    return s1.size() <= s2.size() ? s1 : s2;
}

此时函数的参数和返回类型都是const string的引用类型。可以对两个非常量的string实参调用此函数,但返回的结果仍然是const string的引用。因此需要一种新的shorterString函数,当其参数不是常量时,得到的结果时一个普通的引用。

string string &shorterString(string &s1, string &s2){
    auto &r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2));
    return const_cast<string&>(r);
}


(三)重载与作用域

如果在内层作用域中声明名字,它将隐藏外层总用于声明的同名实体,在不同作用域中无法重载函数名。

string read();
void print(const string &);
void print(double);    //重载print函数
int main(){
    bool read = false;    //在内层作用域中声明read,隐藏了外层的read
    string s = read();    //错误:在当前作用域中read是布尔值,而非函数
    void print(int);    //隐藏了外层的print
    print("hello");    //错误:print(const string &)被隐藏掉了
    int i = 0;
    print(i);        //正确:当前print(int)可见
    print(3.14);    //正确:调用当前print(int),print(double)被隐藏掉了
}

  • 默认实参

可以为一个或多个形参定义默认值,一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值
想要使用默认实参只要在调用函数时忽略该实参就可以了。函数调用时实参按其位置解析,默认实参负责填补缺少的尾部实参(靠右侧的),所以要想覆盖某个形参的默认值必须提供其左侧的所有形参

typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');    //为每个形参都提供了默认实参
string window;
window = screen();        //等价于screen(24, 80, ' ')
window = screen(48);    //等价于screen(48, 80, ' ')
window = screen(48,160);    //等价于screen(48, 160, ' ')
window = screen(48,160,'#');    //等价于screen(48, 160, '#')
window = screen(, , '?');    //错误:只能忽略尾部的实参
window = screen('?');    //等价于screen('?', 80, ' ')

通常,应该在函数声明中指定默认实参,并将该声明放在头文件中,且一个函数只声明一次,尽管多次声明同一个函数也是合法的。但是在给定的作用域内一个形参只能被赋予一次默认实参,且该形参右侧的所有形参必须都有默认值。

string screen(sz, sz, char = ' ');    //表示高度和宽度的形参没有默认值
string screen(sz, sz, char = '*');    //错误:重复声明
string screen(sz = 24, sz = 80, char);    //正确:添加默认实参

  • 内联函数

把规模较小的操作定义成函数有很多好处,像6(二)中的shorterString函数,方便阅读、可以重复利用……
但使用shorterString函数也存在一个潜在的缺点:调用函数比求等价表达式的值要慢一些。
在函数的返回类型前加上关键字inline,就将函数指定为内联函数了,通常就是将它在每个调用点“内联地”展开。

inline const string &shortString(const string &s1, const string &s2){
    return s1.size() <= s2.size() ? s1 : s2;
}

再调用shortString时,如

cout << shortString(s1, s2) << endl;

将在编译过程中展开成类似于这样的形式

cout << s1.size() <= s2.size() ? s1 : s2 << endl;

  • constexpr函数

在学习const限定符时已经接触过constexpr变量,constexpr函数是指能用于常量表达式的函数。定义constexpr函数需遵循几项规定:函数的返回值类型和所有形参的类型都得是字面值类型,且函数体中必须只有一条return语句

constexpr int new_sz() { return 1; }
constexpr int foo = new_sz();    //正确:foo是一个常量表达式

执行该初始化任务时,编译器把对constexpr函数的调用替换成其结果值,为了能在编译过程中随时展开,constexpr函数被隐式地指定为内联函数。
constexpr函数不一定返回常量表达式,且被允许。

//当scale实参是常量表达式时,返回值也是常量表达式;反之则不是
constexpr size_t scale(size_t cnt){
    return new_sz() * cnt;
}
int arr[scale(2)];    //正确:scale(i)是常量表达式
int i = 2;
int arr[scale(i)];    //错误:scale(i)不是常量表达式

内联函数和constexpr函数通常定义在头文件中

  • 调试帮助

assert预处理宏

assert(expr);
对expr求值,如果为假,assert输出信息并终止程序的执行;如果为真,assert什么也不做。
assert宏定义在cassert头文件中,因为预处理由预处理器而非编译器管理,故可无需提供using声明直接使用。

NDEBUG预处理宏

assert的行为依赖于一个名为NDEBUG的预处理变量的状态,如果定义了NDEBUG,则assert什么也不做。默认状态下没有定义NDEBUG,此时assert将执行运行时检查。
可以使用一个#define语句定义NDEBUG:#define NDEBUG,从而关闭调试状态。

  • 函数匹配

如果检查了所有实参之后没有任何一个函数脱颖而出,则该调用是错误的,编译器将报告二义性调用的信息。
为确定最佳匹配,编译器将实参类型到形参类型的转换划分几个等级,具体如下:
(一)精准匹配:
实参类型与形参类型相同;
实参从数组类型或函数类型转换成对应的指针类型;
向实参添加顶层const或从实参中删除顶层const。
(二)通过const转换实现的匹配
(三)通过类型提升实现的匹配
(四)通过算术类型转换或指针转换实现的匹配
(五)通过类类型转换实现的匹配

  • 函数指针

函数指针是指向函数的指针。

bool lenthCompare(const string &, const string &);
bool (*pf)(const string &, const string &);    //未初始化
//pf两端的括号必不可少。如果没有这对括号,pf就是一个返回值为bool指针的函数。
pf = lenthCompare;        //pf指向名为lengthCompare的函数
pf = &lenthCompare;    //等价的赋值语句,取址符是可选的
bool b1 = pf("hello","goodbye");    //调用lengthCompare函数
bool b2 = (*pf)("hello","goodbye");    //一个等价调用
bool b3 = lenthCompare("hello","goodbye");    //另一个等价调用
string::size_type sumLength(const string&, const string&);
bool cstringCompare(const char*, const char*);
pf = 0;        //正确:pf不指向任何函数
pf = sumLength;    //错误:返回类型不匹配
pf = cstringCompare;//错误:形参类型不匹配


 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值