[C++ Primer] 第6章: 函数

参数传递

const形参和实参:
顶层const作用于对象本身, 和其他初始化过程一样, 当用实参初始化形参时会忽略掉顶层const, 换句话说, 形参顶层const被忽略掉了, 当形参有顶层const时, 传递给它常量对象或非常量对象都是可以的.

void fcn(const int i)    //fcn可以读取i, 但是不能向i写值
void fcn(int i)          //错误, 重复定义了fcn(int)

由于顶层const被忽略了, 所以上述两个函数的参数是完全一样的.
指针或引用形参与const:
我们可以使用非常量初始化一个底层const对象, 但是反过来不行, 即不能用const类型的对象初始化一个非const类型的对象.
尽量使用常量引用:
把函数不会改变的形参定义为普通引用是一种比较常见的错误, 这样会给人一种误导, 即函数可以修改它的实参的值. 此外使用引用而非常量引用也会极大地限制函数所能接受的实参的类型.
数组形参:
数组的两个性质:
1.不允许拷贝数组, 2.使用数组时(通常)会将其转换成指针. 这使得我们无法以值传递的方式使用数组形参, 当为函数传递一个数组时, 实际上传递的是指向数组的首元素的指针.

void print(const int*);   // 以下3条声明是等价的
void print(const int[]);
void print(const int[10]);// 维度只是表示我们期望有多少个元素, 实际上没有任何用处

使用数组形参通常有三种方式:
1.传递指向数组首元素和尾后元素的指针, 如begin(arr), end(arr)分别指向首元素和尾后元素.
2.显式传递一个表示数组大小的形参.
3.数组引用形参.

void print(const int *beg, const int *end); // 方式1
void print(const int ia[], size_t size); // 方式2
void print(int (&arr)[10]); // 方式3, 限制了函数只能作用于大小为10的数组

含有可变形参的函数:
为了编写能处理不同数量实参的函数, C++11新标准提供了两种主要的方法: 1.如果所有的实参类型相同, 可以传递一个名为initializer_list的标准库类型; 2.如果实参的类型不同, 我们可以编写一种特殊的函数, 也就是所谓的可变参数模板. 另外还有一种特殊的形参类型: 省略符, 可以用它传递可变数量的实参.
如果函数的实参数量未知但是全部实参的类型相同, 则我们可以使用initializer_list类型的形参. initializer_list是一种标准库类型, 用于表示某种特定类型的值的数组. initializer_list定义在同名头文件中.

函数 说明
initializer_list lst 默认初始化: T类型元素的空列表
initializer_list lst lst的元素数量和初始值一样多;lst的元素是对应初始值的副本, 列表中的元素是const, 注意用花括号
lst2 = lst 或 lst2(lst) 拷贝或赋值一个initializer_list对象不会拷贝列表中的元素;拷贝后原始列表和副本共享元素
lst.size() 列表中的元素数量
lst.begin() 返回指向lst中首元素的指针
lst.end() 返回指向lst尾元素之后的指针

注意: initializer_list对象中的元素永远是常量值, 不可改变!
可以使用以下形式编写输出错误信息的函数, 使其可以作用于可变数量的实参.

void error_msg(initializer_list<string> li)
{
    for(auto beg = li.begin(); beg != li.end(); ++beg)
        cout << *beg << "  ";
    cout << endl;
}

// 如果想向initializer_list形参中传递一个值的序列, 则必须把序列放在一对花括号里:
if(excepted != actual)
    error_msg({"functionX", excepted, actual}); // excepted和actual是string对象
else
    error_msg({"functionX", "okay"});

省略符形参:
省略符形参只能出现在形参列表的最后一个位置, 它的形式不外乎以下两种形式:

void foo(parm_list, ...);                    //逗号可以省略
void foo(...);

省略符形参应该仅仅用于C和C++通用的类型, 特别应该注意的是, 大多数类类型的对象在传递给省略符形参时都无法正确拷贝.

返回类型和return语句

函数值是如何被返回的:
返回一个值的方式和初始化一个变量或形参的方式完全一样: 返回的值用于初始化调用点的一个临时量, 该临时量就是函数调用的结果. 同其他引用类型一样, 如果函数返回引用, 则该引用仅是它所引用对象的一个别名.
不要返回局部变量的引用或指针: 函数完成后, 局部对象所占用的存储空间也随之被释放掉. 返回局部变量的引用导致函数调用结束后引用绑定一个不再可用的内存空间, 同理返回局部对象的指针也是错误的!
要想确保返回值的安全, 就要查看该对象是否在函数调用之前就已经存在?!

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

引用返回左值:
调用一个返回引用的函数得到左值, 其他返回类型则得到右值. 特别是我们可以为返回类型是非常量引用的函数的结果赋值:

char &get_val(string &str, string::size_type ix)
{
    return str[ix];
}
int main()
{
    string s("a value");
    get_val(s, 0) = 'A'; // 将s[0]的值改为'A'
    return 0;
}

列表初始化返回值:
C++11新标准规定, 函数可以返回花括号包围的值的列表. 此列表用来对表示函数返回的临时量进行初始化, 如果列表为空, 则执行值初始化.

vector<string> process()
{
    return {"functionX", "OK"};
}

如果函数返回的是内置类型, 则花括号包围的列表最多包含一个值, 而且该值所占空间不应该大于目标类型的空间.
返回数组指针:
由于数组不能被拷贝, 所以函数不能返回数组, 但是函数可以返回数组的指针或引用. 使用类型别名可以简化定义.

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

声明一个返回数组指针的函数:

Type (*function(parameter_list))[dimension];

Type表示元素的类型, dimension表示数组的大小, (*function(parameter_list))两端的括号必须存在, 如果没有这对括号, 函数的返回类型将是指针的数组.

int (*func(int i))[10]; //返回值是一个指针, 该指针指向含有10个int的数组

使用尾置返回类型:
尾置返回类型对于返回类型比较复杂的函数最有效.

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

使用decltype:
如果知道函数返回的指针指向那个数组, 就可以使用decltype关键字声明返回的类型.

int odd[] = {1, 3, 5, 7};
int even[] = {0, 2, 4, 6};
decltype(odd) *arrPtr(int i)
{
    return (i % 2)?&odd : &even;
}

decltype并不负责把数组类型转换为对应的指针, 所以decltype的结果是个数组, 要想表示arrPtr返回指针还必须在函数声明时加一个*符号.

函数重载

重载和const形参:
顶层const不影响传入函数的对象, 一个拥有顶层const的形参无法和另外一个没有顶层const的形参区分开来, 顶层const表示的是对象或指针本身是个常量:

void lookup(int);
void lookup(const int);        //重复声明
void lookup(int *);
void lookup(int * const);      //重复声明

如果形参是某种类型的指针或引用, 则通过区分其指向的是常量对象还是非常量对象可以实现重载, 此时const是底层的:

void lookup(int&);
void lookup(const int&);        //函数重载
void lookup(int *);
void lookup(const int *);       //函数重载

const不能转换成其他类型, 而非const可以转换成const, 当我们传递一个非常量时, 编译器会优先选用非常量版本.
const_cast和重载:
当通过const实现函数重载是, 可以使用非const形参的函数调用常量形参的函数减少重复代码

const string &shorterString(const string &s1, const string &s2)
{
    return s1.size() <= s2.size() ? s1 : s2;
}
string &shorterString(string &s1, string &s2)
{
    // 形参必须强制转换成const, 否则会递归调用自己
	auto &r = shorterString(const_cast<const string&>(s1),
                            const_cast<const string&>(s2));
    return const_cast<string &>(r);
}

内联函数和constexpr函数:
内联函数可以避免函数调用的开销. 内联只是向编译器发出一个请求, 编译器可以忽略这个请求.
constexpr函数是指能用于常量表达式的函数, 定义constexpr函数要遵循几项约定: 函数的返回类型及所有形参的类型都得是字面值类型, 而且函数体中必须有且只有一条return语句.

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

执行该初始化任务时, 编译器把对constexpr函数的调用替换成其结果值. 为了能在编译过程中随时展开, constexpr函数被隐式地指定为内联函数.
constexpr函数体内也可以包含其他语句, 只要这些语句在运行时不执行任何操作就行. 如constexpr函数中可以有空语句, 类型别名以及using声明.
constexpr函数不一定返回常量表达式

constexpr size_t scale(size_t cnt){ return new_sz()*cnt; }    //如果arg是常量表达式, 则scale(arg)也是常量表达式
int arr[scale(2)];  //正确, scale(2)是常量表达式
int i = 2;
int a2[scale(i)];  //错误, scale(i)不是常量表达式, 不能用作数组的维度

如果我们用一个非常量表达式调用scale函数, 如scale(i), 则返回值将是一个非常量表达式.
调试帮助:
基本思想: 程序可以包含一些用于调试的代码, 但这些代码只在开发时使用. 当程序编写完成准备发布时, 要先屏蔽掉调试代码. 这种方式用到两项预处理功能: assert和NDEBUG
assert是一种预处理宏, 有预处理器而非编译器管理, 因此可以直接使用无需提供using声明. 定义在cassert头文件中.

assert(expr);

对expr其值, 当表达式为假, assert输出信息并终止程序, 当表达式为真则什么也不做. assert宏常用于检查不能发生的条件.
NDEBUG预处理变量
assert变量依赖于一个名为NDEBUG的预处理变量的状态, 如果定义了NDEBUG, 则assert什么也不做. 默认状态下没有定义NDEBUG, 此时assert将执行运行时检查.

#define NDEBUG  // 定义NDEBUG, 关闭调试状态

通过命令行选项定义预处理变量, 等价于上面的宏定义

$ CC -D NDEBUG main.c   #微软编译器使用 /D

也可以使用NDEBUG定义自己的调试代码

void print(const int ia[], size_t size)
{
#ifndef NDEBUG
    cerr << __func__ << ": array size is " << size << endl;
#endif
// ... ...
}

函数指针:
函数指针指向函数而非对象.

bool lengthcompare(const string &, const string &);
// 函数指针定义
bool (*pf)(const string &, const string &);   //pf指向一个函数, 该函数的两个参数是const string的引用, 返回值是bool类型
// 赋值
pf = lengthcompare;
pf = &lengthcompare;    //等价地赋值语句: 取地址是可选的
// 使用
bool b1 = pf(“hello”, “world”);
bool b2 = (*pf)(“hello”, “world”);   //一个等价地调用.

重载函数指针

void ff(int *);
void ff(unsigned int);
void (*pf1)(int *) = ff; // pf1指向ff(int *)
void (*pf2)(unsigned int) = ff; // pf2指向ff(unsigned int)

函数指针形参

// 第3个参数是函数类型, 将自动转化为指向函数的指针
void useBigger(const string &s1, const string &s2,
               bool pf(const string &, const string &));
// 等价的声明, 显示的将形参定义成指向函数的指针
void useBigger(const string &s1, const string &s2,
               bool (*pf)(const string &, const string &));
// 可以直接把函数作为实参使用, 此时它会自动转换成指针
useBigger(s1, s2, lengthcompare);

使用typedef和decltype简化函数指针的使用
注意: 当我们将decltype作用域某个函数时,它返回函数类型而非指针类型,因此我们显式地加上*以表明我们需要返回指针,而非函数本身.

// Func和Func2是函数类型
typedef bool Func(const string &, const string &);
typedef decltype(lengthcompare) Func2;   // 等价声明
// FuncP和FuncP2是指向函数的指针
typedef bool (*FuncP)(const string &, const string &);
typedef decltype(lengthcompare) *FuncP2; // 等价声明
// useBigger等价声明
void useBigger(const string &s1, const string &s2, Func); // 编译器自动将Func表示的函数类型转换成指针.
void useBigger(const string &s1, const string &s2, FuncP2);

返回指向函数的指针:
必须将返回类型写成指针形式,编译器不会自动地将函数返回类型当成对应的指针类型来处理. 声明一个返回函数指针的函数,最简单的办法是使用类型别名.

using F  = int(int *, int);     //F是函数类型,不是指针
using pF = int(*)(int*, int);   //pf是指针类型

PF f1(int); // 正确, f1返回指向函数的指针
F  f1(int); // 错误, 不能返回函数类型, 只能返回函数指针类型
F *f1(int); // 正确
// 等价的声明
int(*f1(int))(int*, int);
auto f1(int) -> int(*)(int*, int); //尾置返回类型
posted @ 2017-07-28 19:29  moon1992  阅读(223)  评论(0编辑  收藏  举报