0%

函数

值传递和引用传递

与值传递相比,引用传递的优势主要体现在三个方面:一是可以直接操作引用形参所引的对象;二是使用引用形参可以避免拷贝大的类类型对象或容器类型对象;三是使用引用形参可以帮助我们从函数中返回多个值。

数组形参

数组有两个性质: 不允许数组拷贝,使用数组时通常会被转换为指针
数组以指针的形式传给函数,所以一开始函数并不知道数组的确切尺寸,要解决这个问题有三种常用的技术:

  • 使用标记指定数组长度,函数在处理 C 风格字符串时遇到空字符停止,但是这种方法只适合字符这种有结束标记,想 int 这种所有取值都合理的不适用。
  • 使用标准库范围
    示例代码:
    1
    2
    int j[2] = {0, 1};
    print(begin(j), end(j));
  • 显示传递一个表示数组大小的形参
    1
    2
    3
    4
    5
    void print(const int ia[], size_t size){
    for(size_t i = 0; i != size; ++i){
    cout <<ia[i] << endl;
    }
    }
    数组引用形参,维度是类型的一部分,void print(int (&arr)[10]),&arr 两端的括号必不可少,没有括号的话就变成了引用的数组。但是这样只能将函数作用于大小为 10 的数组。

当我们想把数组作为函数的形参时,有三种可供选择的方式:一是声明为指针,二是声明为不限维度的数组,三是声明为维度确定的数组。

可变形参

  • initializer_list
    和 vector 一样,initializer_list 也是一种模板类型
    和 vector 不一样的是,initializer_list 对象中的元素永远是常量值,我们无法改变 initializer_list 对象中元素的值。
    因为 initializer_list 对象的元素永远是常量值,所以我们不可能通过设定引用类型来更改循环控制变量的内容。只有当 initializer_list 对象的元素类型是类类型或容器类型(比如 string)时,才有必要把范围 for 循环的循环控制变量设为引用类型。
  • 省略符形参
    省略符形参应该仅仅用于 C 和 C++ 通用的类型。特别应该注意的是,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。
    省略符形参只能出现在形参列表的最后一个位置,它的形式无外乎以下两种:
    1
    2
    void foo(parm_list, ...); // , 可以省略,省略符形参所对应的实参无须类型检查
    void foo(...);

返回数组指针

分四种情况,比较复杂:

  • 类型别名

    1
    2
    3
    typedef int arrT[10]; // arrT 是一个类型别名
    using arrT = int[lO]; // 等价于上一行
    arrT* func(int i);
  • 声明一个返回数组指针的函数

    1
    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 类型。

  • 使用尾置返回类型

    1
    auto func(int i) —> int(*)[10];
  • 使用 decltype

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

    return (i % 2) ? &odd : &even;
    }

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

函数重载

顶层不可重载,一个拥有顶层 const 的形参无法和另一个没有顶层 const 的形参区分开来

1
2
3
4
5
Record lookup(Phone);
Record lookup(const Phone); // 重复声明

Record lookup(Phone*);
Record lookup(Phone* const); // 重复声明

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

1
2
3
4
5
Record lookup(Account&);
Record lookup(const Account&);

Record lookup(Account*);
Record lookup(const Account*);

函数调用的底层实现

在大多数机器上,一次函数调用其实包含着一系列工作:调用前要先保存寄存器,并在返回时恢复;可能需要拷贝实参;程序转向一个新的位置继续执行。

内联函数

将函数指定为内联函数(inline),通常就是将它在每个调用点上 “内联地” 展开。

1
2
3
4
cout <<shorterString(sl, 52) << endl;
// 将在编译过程中展开成类似于下面的形式
cout <<(sl.size() < 32.size() ? 31 : 52) << endl;
// 从而消除了 shorterString 函数的运行时开销。

声明内联函数

1
2
3
4
5
inline const string &
shorterString<const string &s1, const string &s2)
(
return sl.size() <= s2.size() ? sl : s2;
)

constexpr 函数

constexpr 函数(constexpr function)是指能用于常量表达式的函数。

函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条 return 语句:

1
2
constexpr int new_sz() { return 42;}
constexpr int foo = new_sz();

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

头文件保护技术

所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数。

1
assert(expr);

首先对 expr 求值,如果表达式为假(即 0),assert 输出信息并终止程序的执行。如果表达式为真(即非 0),assert 什么也不做。

assert 的行为依赖于一个名为 NDEBUG 的预处理变量的状态。如果定义了 NDEBUG,则 assert 什么也不做。默认状态下没有定义 NDEBUG,此时 assert 将执行运行时检查。

打开和关闭调试
1
CC -D NDEBUG main.C it use /D with the Microsoft compiler

这条命令的作用等价于在 main.c 文件的一开始写 #define NDEBUG。

类型提升

即使实参是一个很小的整数值,也会直接将它提升成 int 类型

1
2
3
void ff(int);
void ff(short);
ff('a');

所有算术类型的转换级别都一样

1
2
3
void manip(long);
void manip(float);
manip(3.14); // 错误,二义性调用

类型匹配

1
2
3
4
5
6
7
8
int calc(char*, char*);
int calc(char* const, char* const); // 顶层 const,无法区分这个两个函数

int calc(char*, char*);
int calc(const char*, const char*); // 底层 const,可以区分

int calc(int&, int&);
int calc(const int&, const int&); // 可以区分

函数指针

1
2
3
4
5
bool lengthCompare(const string &, const string &);
// 声明一个函数指针指向函数
bool (*pf) (const string &, const string &);
pf = lengthCompare;
pf = &lengthCompare; // 等价于上一行
使用函数指针
1
2
3
bool bl = pf("hello", "goodbye");
bool b2 = (*pf) ("hello", "goodbye"); // 等价
bool b3 = lenqthCompare("hello", "qoodbve"); // 等价

可以为函数指针赋一个 nullptr 或者值为 0 的整型常量表达式,表示该指针没有指向任何一个函数。

重载函数 的指针,指针类型必须与重载函数中的某一个精确匹配。

1
2
3
4
void ff(int*);
void ff(unsigned int);

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

decltype 返回函数类型,此时不会将函数类型自动转换成指针类型。因为 decltype 的结果是函数类型,所以只有在结果前面加上*才能得到指针。

1
2
typedef decltype(lengthCompare)* FuncPZ; // * 号位置可变
void useBigger(const string&, const string&, FuncPZ);

函数类型的形参会自动转换成指针,返回类型不会。

返回指向函数的指针
1
2
int (*fl(int))(int*, int);
auto fl(int) -> int (*)(int*, int); // 等价。尾置返回类型

按照由内向外的顺序阅读这条声明语句:我们看到 f1 有形参列表,所以 f1 是个函数;f1 前面有 *,所以 f1 返回一个指针;进一步观察发现,指针的类型本身也包含形参列表,因此指针指向函数,该函数的返回类型是 int。

示例

编写函数的声明,令其接受两个 int 形参并且返回类型也是 int;然后声明一个 vector 对象,令其元素是指向该函数的指针。

1
2
int func(int, int);
vector<decltype(func)* > vF;

const 函数

C++ 中的 const 函数指的是在类中声明的成员函数,该函数承诺不会修改类的成员变量,因此可以在函数声明和定义中使用 const 关键字来表示这一点。使用 const 修饰的成员函数被称为 const 成员函数。

const 成员函数可以被声明为常量对象调用,这意味着该对象的成员变量的值不能被修改。如果试图在 const 成员函数中修改成员变量的值,则会导致编译错误。

1
2
3
4
5
6
7
8
class MyClass {
int value;
public:
int getValue() const {
return value; // 返回值不改变成员变量的值,因此可以使用 const 关键字
}
};