What & How & Why

函数

C++ Primer 笔记 第六章


函数基础

函数由四部分组成:函数的返回类型,函数名,函数接收的参数列表 (parameter list),还有函数体。我们通过调用运算符(一对括号)来对函数进行调用,调用返回的类型即是函数的返回类型。
函数调用分为三步:

  1. 初始化。函数从调用函数的位置获取对应数量的 argument,然后用 argument 对 parameter进行初始化 (隐式转换)。
  2. 执行函数。
  3. 当遭遇 return 关键字时,函数调用结束,函数返回值(如果有的化),并将程序控制权交回调用函数的主体。

Parameters & Arguments

Argument 用于对函数 Parameter 的初始化。函数的 parameter 和 arguments 有如下的匹配要求:

  • 初始化类型的要求:相同类型 & 可以通过类型转换得到相同的类型
  • 初始化数量的要求:用于初始化的 arugments 与函数 parameter 数量相同

函数按顺序对应初始化的 argument 和 paramater,但并不保证先初始化哪个 argument。

Parameter list

Parameter List 有如下的要求:

  • 可以为空,但不能省略(必须有括号)。可以使用 void 关键字作为填充:

void f1(){ /* ... */ };
void f2(void){ /* ... */ } // explicit void parameter list

  • 每个 parameter 必须单独声明

int f3(int v1, v2) { /* ... */ } // error
int f4(int v1, int v2) { /* ... */ } // ok

  • parameter 不能重名,必须有名字。
函数返回类型
  • 不返回任何类型的时候,函数的返回类型是 void
  • 返回值不能是函数或者数组,但可以是指向函数或数组的指针

局部变量

两个前置概念:

  • 变量由两部分组成:名字(Name)和作用域Scope)。名字拥有作用域
  • Object 有生命周期(Lifetime

两个后续概念:

  • 名字的作用域范围(The scope of a name):从名字可见开始到对应 scope 结束。
  • 对象的生命周期 (The lifetime of an object):对象存在的的时间(在程序执行的过程中)

由于函数的创建会定义一个新的 scope,因此我们将所有定义于函数内部的变量统称为局部变量(本地变量,Local variables)。局部变量的生命周期取决于函数的 scope。

自动对象

自动对象(Automatic Object)对应局部变量,指在 block 内创建的对象。其生命周期到 block 结束。当 block 结束后,任何创建于 block 中的对象都将变为未定义。

自动对象的初始化由 arguments 或者 block 内的 Initializer 负责初始化,比如 parameter 就是一种自动对象,由 argument 初始化。

再次提醒: build-in type 在函数中的默认初始化是 Undefined Beahvior.

局部静态对象

什么是局部静态对象(Local static obejct)?

  • 创建于函数中的对象,初始化于函数第一次执行的时候。
  • 生命周期从创建开始,直到函数不再执行为止。

局部静态对象应用的场景是?

某些函数可能会被反复调用,但这些函数可能需要一个全局的、存在于所有函数调用期间的对象(通常用于记录、累积等等),比如下面的计数器,总量就是通过局部静态对象保存的:

size_t count_calls()
{
	static size_t ctr = 0; // value will persist across calls
	return ++ctr;
}
int main()
{
	for (size_t i = 0; i != 10; ++i)
	cout << count_calls() << endl;
	return 0;
}
局部静态变量的默认初始化?

build-in type 的默认初始化为 0.

函数的声明

函数也是对象,所以函数在使用之前也必须声明。函数的声明:

  • 不需要写 parameter (但是强烈推荐写上,方便理解)
  • 不需要写函数的 Body 部分。
  • 可以存在多次函数声明

写法:

type function_name (/* parameter list here is not necessary but recommonded */);

Function Declarations Go in Header Files

怎么样使用头文件来帮助函数的声明?

<html>

<img src=“/_media/programming/cpp/cpp_primer/declaration_and_header.svg” width=“400”>

</html>

为什么要这么做?

统一接口:只需要在特定的 Header 中声明函数,需要使用的时候包含该 Header 即可。修改同理,只需要修改函数的实现文件和 Header 中的声明。

实现函数的原文件中也应该包含声明了这些函数的 header。编译器可以通过这种方式来验证函数的声明是否一致。

分离式编译

对应上述的要求,C++ 使用分离式编译(Separate Compliation)来处理。在分离式编译中,每个文件可以进行单独编译,最后通过链接组合到一起。

编译链接多个源文件

假设 :

  • fact() 函数定义于 fact.cc 文件中
  • fact() 函数声明于 Chapter6.h
  • fact() 函数需要在 factMain.cc 文件中使用

$ g++ factMain.cc fact.cc #generates factMain.exe or a.out
$ g++ factMain.cc fact.cc -o main #generates main.exe or main

单独编译并链接的情况

如果修改了某个源文件,重新编译修改的文件,再手动链接即可。使用 -c 标签会产生对应的 object 文件,windows 以 .obj 为后缀,Unix 以 .o 为后缀:

$ g++ -c factmain.cc 
$ g++ -c main.cc # "c" flag means generate object file.
$ g++ factmain.o main.o -o main # link object files and generate a executable file.
以上的 g++ 是编译器的名字,替换成当前使用的编译器名字即可。

参数传递

Parameter 的类型决定了参数传递(Argument passing)的方式:

  • 如果是引用,则将 Parameter 视作 Argument 的 alias;函数处理的对象是 Argument
  • 如果是传值,则 Parameter 是 Argument 的拷贝,函数处理的对象是 Parameter

这两种方式分别称为引用传递Passed by reference)和值传递Passed by value)。

值传递

值传递中,Parameter 通过拷贝的形式得到 Argument 的值,两者处于不同的内存空间,修改其中一个的值不会影响另外一个。

指针的传递属于值传递

指针的传递属于值传递。传递后会得到两个不同的、但指向同一个位置的指针。因此,改变指针指向的内容会影响 Argument 指向的内容:

<html>

<img src=“/_media/programming/cpp/cpp_primer/passed_by_value.svg” width=“400”>

</html>

void reset (int *ip) {
    *ip = 0; //changes tghe value of the object which ip points
}
至于 Argument (指针本身)并不会收到影响。

在 C++ 中, 推荐使用引用访问函数外部的数据。

引用传递

引用传递的实际效果是什么?

实际上允许函数修改 Argument 本身,比如下例:

void reset(int &i) {
    i = 0; // change the value to which i refers
}
为什么要使用引用传递?

引用传递的优势与作用在于:

  • 在函数内对 argument 进行修改只能通过引用实现。
  • 相比引用传递,值传递需要拷贝,额外占用空间和时间。比较典型的例子是 string。使用引用传递会大大的提高效率:

bool is_shorter(const string &s1, const string &s2) {
	return s1.size() < s2.size();
}

  • 有些类型根本无法被拷贝(某些类,IO)
  • 引用传递可以传递额外的信息:

通常情况下函数只能返回一个值,而使用引用传递能够有效的返回多个值:比如下例,我们希望在某个 string 中找到某个字母,并返回其第一个匹配的位置,以及出现在该 string 中的次数:

string::size_type find_char(const string &s, char c, 
							string::size_type &occurs) {
	auto ret = s.size();
	occurs = 0;
	for (decltype(ret) i = 0; i != s.size(); ++i) {
		if (s[i] == c) {
			if (ret == s.size())
				ret = i;
			++occurs;
		}
	}
	return ret;
}
上述函数返回的是第一个匹配字母所在的下标信息;而传递进去用于计数的变量 occurs 则直接被函数修改了。这是函数使用引用传递修改函数外对象的另一种使用方法。

Ref: Function pass by value vs. pass by reference

const in passing

带有 const 的值传递是怎么被处理的?

首先需要明确的有两点:

  • 值传递是一个拷贝的过程
  • 应用到上的 const,是 top-level const,是确保值不被改变的 const。

由于在对其他变量进行初始化 / 赋值的时候,带有 top-const 属性的变量会被自动忽略掉;因此通过值传递初始化 low-const parameter 的时候,无论是 const 还是 non-const 的 argument,均可初始化成功。

有没有什么 top-const 带来的副作用?

函数重载的时候需要特别注意 top-level const 会被自动忽略的情况:

void fcn(const int i);
void fcn(int i); //error, redefines
上例中,在初始化过程中,const int i 的 top-const 属性已经被忽略掉了,因此编译器判定上述两个函数是同一个。

poiner / reference parameter and const

明确两点以下两点后:

  • 这里讨论的 是带有 Low-const 属性的 parameter,特指 reference to const & pointer to const
  • 对 low-const 对象的初始化需要 initializer 也具有 low-const 属性,或是能通过类型转换得到 low-const 属性

可以得出结论:

  • low-const 的 parameter 可以接收 low-const / non-const 的 argument
  • non-const 的 parameter 不能接收 low-const 的 argument

一些例子:

int i = 0;
const int ci = i; //ok, top-level ignored 
string::size_type ctr = 0;

void reset(int *ip) {....}
void reset(int &i) {....}

reset(&i); //ok, plain pointer
reset(&ci); //error, a pointer to const can't initialize int*
reset(i); //ok, call the version of int&
reset(ci); //error, a reference to const can't initialize int&
reset(42); //error, a literal can't initialize int&
reset(ctr); //error, reference binding requires matached type.

Use Reference to const When Possible

什么时候使用 reference to const 作为 parameter?

通常情况下,使用 reference to const 作为 parameter 表示了不会进行对 parameter 修改的意愿。这种意愿是面向一切数据的:无论传递的是 non-const 或是 low-const 的数据。

使用 plain reference 取代 reference to const 会有什么后果?

  • 首先,plain reference 会使调用者误解传递的参数是需要修改的。
  • 其次,plain reference 无法接收指定的数据,比如带 low-const 的参数,或者 Literal。这个问题在函数本身被其他函数调用的时候显的更为严重。

比如书中的例子,将接收 const string& 类型参数的 find_char 函数改为接收 plain reference 的参数:

string::size_type find_char(string &s, char c,
string::size_type &occurs);
则引发的问题有:

  • 自身的问题,无法接收 literial string

find_char("Hello World", 'o', ctr); //error, plain reference can't be initialized by a literal

  • 被其他函数调用的问题,无法接收来自上游函数的 reference to const 参数:

bool is_sentence(const string &s)
{
	// if there's a single period at the end of s, then s is a sentence
	string::size_type ctr = 0;
	return find_char(s, '.', ctr) == s.size() - 1 && ctr ==1; //error, s is a const string&, find_char can't take s as an argument
}
解决的方法?

  • 改变函数中参数的 const
  • 如果无法改变函数,则定义原有数据的副本,将该副本交由函数处理。

Array Parameters

Array 具有两个特性:

  1. 不能直接复制
  2. 访问 Array 一般通过指向数组第一个元素的指针来访问

因此,函数想获得一个数组类型的参数,是不能通过值传递的方式来得到的,但可以通过传递指针来得到。

定义数组的 parameter 有三种等价的方式:

void print(int arr[]);
void print(int arr[10]);
void print(int *arr);
上面所有的方法最终都将转化为 const int* 类型的指针。值得注意的是第二个例子,尽管 parameter 中注明了数组的大小,但该转换过程中只会用到数组名函数是没有办法通过单个数组参数来知道数组的大小的

传递数组长度的几种解决方法

解决上述问题有三种方式:

  • 对于数组自带结束标志符的情况,可以通过对数组元素的判断来获取数组的长度。比如 C-string,可以通过判断当前元素是否为 /0

void print(const char *cp) {
	if (cp) //cp is not a nullptr
		while(*cp) //as long as cp is not nullpter
			cout << *cp++;
}

  • 第二种方法是使用 beginend 两个函数,使用类似 STL 迭代器的原理传递信息:

//define
void print(const int *beg, const int *end)
{
// print every element starting at beg up to but not including end
while (beg != end)
cout << *beg++ << endl; // print the current element
// and advance the pointer
}
//call
int j[2] = {0, 1};
print(begin(j), end(j));

  • 第三种方式是显式的传入数组的长度,定义第二个 parameter 为数组的大小:

//define
void print(const int ia[], size_t size){
	for (size_t i = 0; i != size; ++i) {
	cout << ia[i] << endl;
	}
}
//call
int j[2] = {1,2};
print(j, end(j) -begin(j);
上述三种方法均加上了 const 修饰,表示了不希望通过指针修改数组的意愿。跟引用一样,如果我们不想对数组进行修改,那么就加上 const。

使用引用传递数组

当然数组的引用也可以作为 parameter:

void print(int (&arr)[10]) {
	for (auto elem : arr)
	cout << elem << endl;
}

数组的引用写法必须带上括号,比如 int (arr&)[10]。不带括号会被诠释为包含 10 个 int& 的数组。因为引用不是对象,不能作为元素,因此不带括号的写法是非法的。

与指针不同,由于数组引用的维度 10 是定义数组引用的一部分,因此是可以拿来使用的。但同时,维度的确定导致了 argument 的维度必须与其一致。比如上述的 print 函数,要求维度为 10 的数组引用:

int i = 0, j[2] = {0, 1};
int k[10] = {0,1,2,3,4,5,6,7,8,9};
print(&i); // error: argument is not an array of ten ints
print(j); // error: argument is not an array of ten ints
print(k); // ok: argument is an array of ten ints

多维数组的传递

结论:Parameter 是多维数组的情况必须指定子数组的长度。

多维数组同样是使用指针进行传递。与一维数组不同的是,多维数组的指针指向的元素也是数组,因此需要指定该数组的长度。写法如下:

int (*martix)[10]; //pointer to an array with 10 int elements
int (*martix)[][10]; // equivlent to the first defination

CLI Options in main

main 函数实际上也有两个参数(其中一个是数组),用于传递一些可选的命令,比如:

prog -d -o ofile data0; #prog is the name of the program
实际上的 main 函数是写成下面这样的:
int main(int argc, char const *argv[])
{
	/* code */
	return 0;
}
这两个参数:

  • argc 决定了传递 string 的数量
  • *argv[] 决定了传递的内容,本意是传递 c-string 的数组,这里相当于传递指向 string 数组的指针。

实际的工作过程中,取决于命令行是否有额外的可选命令,argv[] 的第一个元素指向 nullptr 或者程序名。按上面的例子来说:

argv[0] = "prog"; // or argv[0] might point to an empty string
argv[1] = "-d";
argv[2] = "-o";
argv[3] = "ofile";
argv[4] = "data0";
argv[5] = 0;
argv 保证最后一个元素为 0,对应的 argc 值为 5

个数变化的参数

应用场景是什么?

需要用单一函数处理多种情况。每种情况都有数量不同的 arugment 需要传递。

解决方案是什么?

  • argument 是同类型的,使用 initializer_list 参数。
  • argument 是不同类型的,使用 variadic template(本小节不 cover)。

initializer_list 是什么?

initializer_list 是一个类似于数组的 STL 容器。与数组一样,initializer_list 可以指定类型:

#include<initializer_list>
initializer_list<string> ls;
initializer_list<int> li;
使用 initializer_list 之前需要包含头文件 <initializer_list>。除此之外,initializer_list 要求所有的元素都是 const,初始化以后不许再更改。

initializer_list 支持的操作?



如何创建 initializer_list parameter?

initializer_list 单独作为 parameter:
void error_msg(initializer_list<string> il)
{
	for (auto beg = il.begin(); beg != il.end(); ++beg)
		cout << *beg << " " ;
	cout << endl;
}
和别的 parameter 一起使用:
void error_msg(ErrCode e, initializer_list<string> il)
{
	cout << e.msg() << ": ";
	for (const auto &elem : il)
		cout << elem << " " ;
	cout << endl;
}
如何调用 initializer_list ?

单独调用 initializer_list:
if (expected != actual)
	error_msg({"functionX", expected, actual});
else
	error_msg({"functionX", "okay"});
与其他元素一起调用:
if (expected != actual)
	error_msg(ErrCode(42), {"functionX", expected, actual});
else
	error_msg(ErrCode(0), {"functionX", "okay"});

Ellipsis Parameters
Ellipsis parameters are in C++ to allow programs to interface to C code that uses a C library facility named varargs. Generally an ellipsis parameter should not be used for other purposes. Your C compiler documentation will describe how to use varargs.

返回类型 & Return 语句

Return 语句的作用:

  • 终结当前函数的执行
  • 跳转到函数被调用的地方

Return 语句的两种形式:

return;
return expression;

没有 return 值的函数

应用场景是什么?

只适用于返回类型是 void 的函数。

意义是什么?

  • 如果函数中 return 语句的存在(或是 return 语句没有生效),那么意味着函数会完整的执行到最后一行。
  • 如果处于函数中部,则代表中断当前函数的执行(类似于 break):

void swap(int &v1, int &v2) {
	//if the values are the same, no need to swap, just return
	if (v1 == v2)
		return;
	//otherwise doing the swap
	int temp = v2;
	v2 = v1;
	v1 = temp;
	//no explicit return necessary
}

void 类型的函数也可以使用 return + expression 的写法,前提是返回值必须是另一个返回 void 的函数。返回其他类型的表达式都会导致编译错误。

带 return 值的函数

应用场景是什么?

return expression 这种形式应用于:除 void 类型函数以外的一切函数。任何带 return 类型的函数必须返回一个值

返回值的要求是什么?

  • 返回值类型必须与函数定义的返回类型一致
  • 或者可以通过隐式转换变得与函数返回类型一致

C++ 确保正确的返回值吗?

C++ 不能保证返回值结果的正确性,但可以检测返回值类型的正确性;换句话说,C++ 会尽可能让函数从正常的 return 语句处结束,比如下面的例子:

bool str_subrange(const string &str1, const string &str2)
{
	// same sizes: return normal equality test
	if (str1.size() == str2.size())
		return str1 == str2; // ok: == returns bool
        // find the size of the smaller string; conditional operator, see § 4.7 (p. 151)
	auto size = (str1.size() < str2.size())
	? str1.size() : str2.size();
	for (decltype(size) i = 0; i != size; ++i) {
		if (str1[i] != str2[i])
		return; // error #1: no return value; compiler should detect this error
}
// error #2: control might flow off the end of the function without a return
// the compiler might not detect this error
}
第一种情况:无返回值的 return 语句,C++ 能确保其无法通过编译。

第二种情况:实际上还是在强调有返回类型的函数必须返回对应的值。对于循环来说,如果循环中有 return,且该 return 可以正确返回函数需要的返回值,那么就没有问题。但同时,我们需要考虑到循环中不执行 return 语句的情况;因此在循环结束后提供 return 语句作为默认返回是必要的,尤其在这种错误很可能不会被编译器查出来的情况下。

Ref: Using a return statement outside of a loop and one inside of it in a function

在含有 return 语句的循环体结束后,也需要提供 return 语句。否则该行为是 run-time undefined,编译器也很可能查不出该错误。

值是怎么被返回的

函数的返回值与 parameter 的初始化采用的是一样的策略,分为值返回引用返回

  • 如果是返回普通的值,函数会利用返回值对一个临时对象(调用时产生)进行初始化,然后将该临时对象作为函数调用的结果。这个初始化的过程也是复制的过程
  • 如果是返回引用,那么返回值则是引用绑定的对象,没有复制操作的参与。
不要返回局部变量的引用或者指针

为什么不能?

当函数调用结束的时候,用于存储函数的内存将会被释放。这意味着任何指向局部变量的指针、引用,在函数调用结束后都将无效化

// disaster: this function returns a reference to a local object
const string &manip()
{
	string ret;
// transform ret in some way
if (!ret.empty())
	return ret; // WRONG: returning a reference to a local object!
else
	return "Empty"; // WRONG: "Empty" is a local temporary string
}
需要注意的是 string literal 的例子。这种情况下需要分情况:

  • string literal 不能以引用的形式返回,但可以复制的形式返回。
  • string literal 可以以 const char * 类型的指针返回。

解释一下第二点,书上有一段是这么说的:

The string literal is converted to a local temporary string object.
  • 首先要明确的是,“Empty” 是 literal string。C++ 中的 literal string 是 statically allocate 的,因此其生存周期并不受函数的生命周期影响。
  • 其次,C++ 中的 Literal string 是 const char* 类型的。因此第二种情况实际上是以 const char* 的形式返回了一个全局对象,并没有什么问题。
  • 问题出在 const string &manip()。如果强制按 STL string 类型返回,那么 “Empty” 的类型会从 const char* 隐式转换为 std::string。std::string 定义的变量在函数中属于局部变量,因此函数结束时,std::string “Empty” 就不存在了。因此返回该 string 的引用就是 Undefined 的行为。

Refs:

返回 class 类型和 call operator

call operator 的两个特性允许我们在返回类指针的时候同时进行调用:

  • call operator 满足左结合律
  • call operator 的优先级与成员访问运算符 . / 解应用+成员访问运算符 相同。

因此,如果返回的是指向 class 的指针,就可以直接访问类成员:

auto sz = shorterString(s1, s2).size();

引用返回值是左值

函数以 plain reference 返回的情况下会得到一个左值

char &get_val(string &str, string::size_type ix)
{
	return str[ix]; // get_val assumes the given index is valid
}
int main()
{
string s("a value");
	cout << s << endl; // prints a value
get_val(s, 0) = 'A'; // changes s[0] to A
	cout << s << endl; // prints A value
return 0;
}

List初始化返回值

C++11 标准中我们可以通过 list 的形式来返回多个返回值。该返回属于值返回,使用 List 内容对临时对象进行初始化后返回临时对象。如果列表为空,则进行默认值初始化Value initialized)。

以之前的多个 parameter 初始化为例,使用 return List 返回的方式可以替代使用 initializer_list 的方式:

vector<string> process()
{
// . . .
// expected and actual are strings
if (expected.empty())
	return {}; // return an empty vector
else if (expected == actual)
	return {"functionX", "okay"}; // return list-initialized vector
else
	return {"functionX", expected, actual};
}
list 返回值需要相同的类型:

  • 如果返回值build-in 类型,list 是不允许 narrowing conversion 的
  • 如果返回值是类类型,那么取决于类如何进行初始化
main函数的返回值

main 函数的返回值是什么样的?

与其他的函数不同,main() 函数并不需要返回指定的返回值。如果程序运行到最后也没有 return,那么编译器会隐式的为 main() 函数加上 return 0

main 函数的返回值有什么意义?

通用的是,0代表程序成功执行;其他的值多半表示执行的失败;具体情况要视机器类型而定。

如何使返回值摆脱机器依赖?

C++ 提供了定义于 cstdlib 头文件中的两个 preprocessor 变量来代替数字作为成功和失败的返回值:

int main()
{
if (some_failure)
	return EXIT_FAILURE; // defined in cstdlib
else
	return EXIT_SUCCESS; // defined in cstdlib
}

Recursion

函数调用其自身的方法称之为递归Recursion)。递归需要保证某次调用不参与调用自身,否则会无限递归。

返回数组指针

数组不能被复制,因此函数返回数组的方式是使用指针。

使用 type alias 简化定义

两种简化数组定义的方式:

typedef int arrT[10];
using arrT = int[10];
arrT* func(int i); // arrT* means return a pointer to which points 10 int elements

声明返回值为数组指针的函数

通常情况下将函数与 parameter 视作一个变量名整体。之后的声明规则与数组声明是一样的:

type (* func(parameter list)) [dimension]
与数组指针 / 引用声明一致,函数外的 parenthesis 是必须的。

Trailing return type

C++ 11 中提供了一种新的 trailing return type 来简化函数的声明。结构如下:

auto function(parameter list) -> array type;
一个例子,函数接收一个 int,返回一个指向包含有10个 int 元素数组的指针:
auto func(int i) -> int(*) [10];

使用 decltype

另外一种声明返回数组指针函数的方式是使用 decltype。

应用条件?

已知存在数组对象的情况下,才能使用 decytype 去推断。
除此之外,decltype 返回的是数组类型本身,因此必须在前面加上 * 强调函数的返回值是指向数组的指针。

int odd[] = {1,3,5,7,9,11,13,15,17,19};

//arr is a function that returns a pointer to which points 10 int elements
//the return value should be an address(&odd)
decltype(odd) *arr(int i) {
    return &odd;
}

重载函数

什么是重载的函数(Overloaded Function)?

重载函数指函数名相同,但 parameter list 不同的函数,且这些函数都应处于同一个 scope。

重载函数的主要意义?

为不同类型 & 数量的 Parameter 提供类似的操作

main 函数无法被重载。

定义重载函数

重载函数的要求 parameter 的类型不同或者数量不同

//different types
Record lookup(const Account&); //Acocount is a long type
Record lookup(const Name&); // Name is a std::string type
//different parameter quantities
void print(const char *cp);
void print(const int *beg, const int *end);
函数只有返回类型的不同不算做重载
Record lookup(const Account&);
bool lookup(const Account&); // error: only the return type is different
Parameter 只有名字不同不算重载:
//below are the same function
Record lookup(const Account &acct); //parameter is named acct
Record lookup(const Account&); // parameter names are ignored
Type alias 不算重载:
typedef Phone Telno;
Record lookup(const Phone&);
Record lookup(const Telno&); // Telno and Phone are the same type

重载和 const

top-const 的 parameter 与 non-const 的 parameter 同一种参数:

由于值传递初始化会忽略 top-const,编译器无法区分 parameter 是否是 top-const。因此,带有 top-const 和 non-const parameter 的函数是同一种函数:

record lookup(phone);
record lookup(const phone); //redeclares record lookup(phone)
record lookup(phone*);
record lookup(phone* const); //record lookup(phone*)
low-const 的 parameter 与 non-const 的 parameter 不是同一种参数:

编译器可以分辨 refer / point to const 与 refer / point to non-const 的区别,因此可以重载:
record lookup(Account&);
record lookup(const Account&); //new function that takes a reference to const
如果 argument 是 non-const, 编译器会倾向于选择 non-const 版本的函数重载版本

const_cast和重载

假设如下场景:

  • 我有一个可以接收 low-const parameter,并返回 low-const parameter 的函数。
  • 我想基于这个函数实现一个 接收 non-const parameter 并返回 non-const parameter 的函数。

尽管带有 low-const parameter 的函数可以接收 non-const parameter,但经过隐性转换,最终参与函数运算的还是 low-const parameter。为了得到 non-const 的返回值,需要使用 const_cast 去掉 low-const 属性:

//original function:
const string& func(const string& s1, const string& s2) {
     statments....
     return const string& s3;
}
//The author's implementation
string& func(string& s1, string& s2) {
    //this function return a reference to const
    auto &r = func(const_cast<string&> (s1), const_cast<string&> (s2));
    //convert reference to const to plain reference 
    return const_cast<string&> (r);
}
注:个人认为这个实现方法的意义真的不是很大。如果想要传递 non-const 得到 non-const,用普通的 string& 实现一遍就可以。如果要确保不可修改,用 low-const 的版本就可以:
//low-const in, low-const out
const string &shorterString(const string &s1, const string &s2) {
	return s1.size() <= s2.size() ? s1 : s2;
}

//non-const in, non-const out
string &shorterString( string &s1, string &s2) {
 	return s1.size() <= s2.size() ? s1 : s2;
}
只要正确区分 low-const 和 non-const 的 argument,编译器是一定能完成重载的:
string s1 {"Hello"};
string s2 {"world"};

const string& sr1 = s1;
const string& sr2 = s2;

cout << (shorterString(s1, s2) = "!") << endl; //complie passed. shorterString(s1, s2) returns a plain ref
cout << (shorterString(sr1, sr2) = "!") << endl; //complie failed. shorterString(sr1, sr2) returns a ref to const

书上的例子是在谈 low-level const 的参数会被视做重载的函数。对于普通版本的 shorterString(),如果不对其调用同名函数 shorterString 进行 const_cast,那么该调用将调用普通版本的 sharterString,也就是其自身,从而造成无限递归。

看完第七章后,该方法的实际意义是:

  • 将功能函数独立出来复用,避免代码重复
  • 使用专门的接口处理常量参数与变量参数,任何修改不会涉及功能函数

调用重载函数

编译器在调用重载函数的时候回遵循函数匹配Function Matching or Overload Resolution)的策略。在匹配的过程中通常会有三种情况出现:

  • Best match:编译器找到了最合适的重载函数。
  • No match:编译器找不到匹配结果,编译器报错。
  • ambiguous call: 编译器找到了不止一个合适的重载函数。

匹配的机制请参考函数匹配

重载和scope

重载与 scope 有什么关系?

重载不影响 scope,因为重载必须在同一个 scope 才会生效。

受 scope 影响的是什么?

Name。需要强调的是,这里的 Name 不分类型和种类;一个重要的例子就是,变量的 Name 屏蔽外部的同名函数。

scope 是如何影响 Name 的?

scope 内声明的 name,一定会屏蔽外部的 name. 即便是不一样的类型。比如下面的例子,函数内部的 bool 变量 read,屏蔽了外部的 read() 函数:

string read();
void foo() {
    bool read;
    string s = read(); // error, read is a bool type
}
为什么会有这样的影响?

这与编译器寻找 name 的声明方式有关:

  1. 当调用函数的时候,编译器会首先寻找本地的(对应 scope 内)的声明。
  2. 如果找到了对应的名字,编译器就会忽略其他 scope 中所有同名的声明

所以导致的结果就是,只要在本地找到了对应的名字声明,即便这个函数不能用,编译器也不会去寻找 scope 外的替代品了:

void print(const string &); //print() at outter scope
void print(double); //print() at outter scope, overloaded
void foo {
    void print(int); //print() at inner scope, hides all print() outside the scope
    print("hello"); //error. inner print() only accepts int
}

C++ 的 name lookup 会优先于 type checking。

特殊用法

Default argument

在哪使用 default argument?

default argument 可以应用于一些常用的、很少需要修改的变量,比如固定大小的画纸的长宽等等。

怎么写?

using sz = string::size_type;
stromg screen(sz ht = 24, sz wd = 80, char bacgrnd = ' ');

调用带 default argument 的函数

调用带有 default argument 的函数时,default argument 从右到左填充未填写的 argument:


如果需要覆盖 default argument,那么需要从左到右依次填入 argument,否则会报错:

string screen(int h = 24, int w = 80, char background = '*');
string window;
window = screen(); //ok, all default argument;
window = screen(66, 256); //argument list is { 66, 256, '*'}
window = screen(, , '?'); //error, can omit only trailing argument

所以在一般设计中为了遵循这个规则,一般把常改的 argument 放到最左边,最不常改的放到最右边

default argument 的声明

default argument 的声明的限制是?

函数中,带 default argument 的 parameter 只能在同一个 scope 中声明一次

string screen(sz, sz, char = ' ');
string screen(sz, sz, char = '*'); // error: redeclaration
string screen(sz = 24, sz = 80, char); // ok: adds default
为什么存在只能声明一次的限制?

default argument 实际上对对应的 parameter 进行了初始化。任何进行了初始化的声明,都是定义。同一个函数只能定义一次。因此,default argument 只能填充一次对应的 argument。

Default Argument Initializers

Default Argument 的要求是?

  • Local variable 不能作为 default argument
  • default argument 类型需要与 parameter 一致

为什么不能使用局部变量作为 default argument?

被 default argument 使用的名字在使用其的函数的 scope 中可见;而其值在函数被第一次调用的时候确定。因此,我们无法使用该函数内部的同名局部变量对 default argument 进行修改:

// the declarations of wd, def, and ht must appear outside a function
sz wd = 80;

char def = ' ';
sz ht();
string screen(sz = ht(), sz = wd, char = def);
string window = screen(); // calls screen(ht(), 80, ' ');

void f2()
{
    def =   '*';   // changes the value of a default argument
    sz wd = 100; // hides the outer definition of wd but does not change the default
    window = screen(); // calls screen(ht(), 80, '*')
}

Inline / constexpr 函数

Inline(内联)函数

为什么要使用 inline 函数?

普通函数在调用的过程中会有额外的开销,包括但不限于:

  • 寄存器的保存与恢复
  • argument 的复制
  • 程序转向新的位置

如果被调用的函数是短小的(通常就一行),但被调用的非常频繁的函数,使用 inline 函数可以节约这部分开销。

inline 函数的具体作用?

inline 函数会将函数真正核心的内容在编译期展开,省去了在 run-time 进行函数调用的开销,比如下面的函数:

const string &
shorterString(const string &s1, const string &s2) {
return s1.size() <= s2.size() ? s1 : s2;
}
普通的调用是这样的:
cout << shorterString(s1, s2) << endl;
但如果被 inline 修饰,那么很可能是这样的:
cout << (s1.size() < s2.size() ? s1 : s2) << endl;
inline 函数的局限性?

简单来说,inline 函数会将函数的核心内容在编译期展开,因此会额外增加编译的时间与执行程序的大小。因此,需要尽量避免对较大的函数进行 inline。

不仅如此,对于编译器来说,关键字 inline 只会被认作是程序员对其提出的一个“建议”,编译器会根据自身的判断来决定是否将一个函数判定为 inline 函数。

注意:很多编译器不支持 inline 函数的递归。

Ref: 被知乎大佬嘲讽后的一个月,我重新研究了一下内联函数

constexpr 函数

constexpr 函数的要求是什么?

  • 返回值必须是 Literal type
  • parameter 必须是 Literal type
  • 函数体必须有且只有一个 return 语句

constexpr 函数的内容要求是什么?

需要确保 constexpr 函数内部的所有 statement 都不会在 run-time 时期有任何行为,比如空语句、type alias 等等。

constexpr 函数的返回值能确保是 const expression 吗?

不能。constexpr 函数不要求返回 constexpr expression。具体来说,如果函数的 arugment 是 non-const,那么返回值也是 non-const:

constexpr size_t scale(size_t cnt) {return new_sz() * cnt};
int arr[scale(2)];//ok, 2 is an const expression, so scale(2) returns the same type
int i = 2;
int a2[scale(i)]; //error, scale(i) returns a non-const

constexpr 函数与 constexpr 变量代表的意义不一样。constexpr variable 与 const variable 类似,都隐含了常量的意思;但 constexpr 函数更强调的是在编译期完成计算的意愿(C++11 默认 Constexpr 会为函数添加 const,但14以后就不是了)。

inline / constexpr in header

inline / constexpr 函数只能定义一次吗?

不像一般的函数,inline / constexpr 函数可以定义多次

为什么可以?

  • C++ 支持分离式编译
  • 编译器需要在编译期将 inline / constexpr 函数其展开执行,因此只有函数的声明是不够的。

可以看出,通过 extern 来共享 inline / constexpr 函数是行不通的;允许每个文件中独立存在 inline / constexpr 函数是有必要的。当然,随之而来的是另外一个要求:所有 inline / constexpr 函数的定义必须相同。

有没有更好的办法共享 inline / constexpr 函数 ?

将 inline / constexpr 的函数定义放到 header 中即可。通常只将普通函数的声明放到 header 中,是因为有函数重定义的风险;而放置 inline constexpr 函数并没有这样的顾虑。

调试辅助

The assert Preprocessor macro

arrert 是什么?

assert 是一个预处理宏(Preprocessor marco)。预处理宏是一个预处理变量Preprocessor variable),行为类似于 inline 函数。

assert 的用法与功能是?

assert 定义于 cassert 头文件中。由于 assert 是预处理宏,因此不需要指定命名。用法如下:

assert(expression);
其主要功能是用于检查一些“不应该”发生的情况;比如某个词的长度不应该超过为它设置的上限:
assert(word.size() > threshold);
assert 通过执行目标表达式来得到结果。如果目标表达式返回 false,则 assert 会发送相关信息并停止整个程序的执行;反之则不会进行任何操作。

使用 assert 的限制?

  • 使用 assert 可能存在的最大限制是其名字本身。由于预处理变量在程序中是唯一的,因此我们应该避免任何以 assert 命名的变量、函数以及其他实体。
  • 其次是功能性的限制。assert 只能作为一种调试程序的辅助手段,程序应该拥有自身的逻辑 / 错误校验。
NDEBUG 预处理变量

NDEBUG是什么?

NDEBUG 同样是一个预处理变量。我们可以将 NDEBUG 视作 assert 的总开关:如字面意义,当 NDEBUG 被定义,则 assert 不工作(no debug!)。

NDEBUG 如何使用?

  • 第一种用法是使用预处理符开启 NDEBUG:

#define NDEBUG

  • 第二种用法是使用编译器命令来开启 NDEBUG:

CC -D NDEBUG main.C # use /D with the Microsoft compiler

  • 第三种用法是与 #ifndef / #endif 配合使用。使用的逻辑是,如果 NDEBUG 没有被开启(定义),那么 #ifndef-#endif 代码段之间的代码就会被检查(我们需要 debug)。配合几个编译器定义的变量可以达到更好的效果:

void print(const int ia[], size_t size) {
	#ifndef NDEBUG
	cerr <<__func__ << ":array size is " << size <<endl;
	#endif
	//...
}
还有哪些常用的编译器变量?
__func__ //function name, string literal
__FILE__ //file name, string literal
__LINE__ //current line number, int literal
__TIME__ //the time the file was complied, string literal
__DATE__ //the date the file was complied, string literal
这些变量记录一些调试的环境情况,使调试信息更加完整。比如下面的例子:
if (word.size() < threshold)
	cerr << "Error: " << _ _FILE_ _
	<< " : in function " << _ _func_ _
	<< " at line " << _ _LINE_ _ << endl
	<< " Compiled on " << _ _DATE_ _
	<< " at " << _ _TIME_ _ << endl
	<< " Word read was \"" << word
	<< "\": Length too short" << endl;
可能的输出结果:
Error: wdebug.cc : in function main at line 27
Compiled on Jul 11 2012 at 20:50:03
Word read was "foo": Length too short

Function Matching

重载函数匹配的机制

1.寻找 Candidate Functions

只要名字与被调用函数相同的,都是 Candidate Function。

2.寻找 Viable Functions

Viable function 需要满足:

  • parameter 的数量与被调用函数匹配
  • parameter 的类型与被调用函数匹配

parameter 的数量匹配过程中,带有 deafault argument 的 parameter 比较灵活,比如带 2 个 parameter 的函数,其中有一个有 default argument,那么调用者无论是带两个 parameter 还是 一个 parameter,都符合函数数量匹配的要求。

3.寻找 Best Match

最佳匹配基于匹配等级来衡量,同时也分两种情况处理:

只需要匹配单个 parameter 的情况:

这种情况下,优先选择匹配等级高的作为最佳匹配对象。

存在匹配多个 parameter 的情况:

编译器会依次检查 parameter 来决定最佳匹配。如果有函数同时满足下列条件:

  • 候选者自身,对(被调用函数的)所有的对应的 argument 的匹配等级,都不低于其他的竞争者
  • 候选者自身,至少一个 argument 的匹配等级,高于他的竞争者

那么这个函数则为最佳匹配。如果这样的函数存在不止一个,那么该调用则是二义性的(ambiguous call),将导致编译错误。



一个二义性例子的典型:

void f();
void f(int);
void f(int, int);
void f(double, double = 3.14);
f(2.56, 42)
整个过程如下:

  1. 第一轮检查函数名,所有函数都是 candidate function
  2. 第二轮检查 parameter 的数量与对应类型,void f(int, int) / void f(double, double = 3.14) 版本入选为 viable function
  3. 第三轮 best match,按位检查 parameter
    1. 第一位 argument 是 double, 导致 void f(int, int) 版本有一个 parameter 的匹配等级低于另外的竞争者
    2. 第二位 argument 是 int, 导致 void f(double, double = 3.14) 版本有一个 parameter 的匹配等级低于另外的竞争者

到此为止,两个版本都有匹配等级更高的 parameter 存在,因此导致了二义性。

Argument Type Conversions

匹配等级的划分

Argument Type Matching Rank(匹配等级) 按以下的规则进行划分:

Rank level 1:最高等级的匹配,如果匹配 argument 与 parameter 得到以下结果:

  • 两者完全相等。
  • 两者之间有隐性转换,转换是数组/函数转化为指针的转换。
  • 两者之间有隐性转换,转换是 Top_level const 的添加或者删除。

Rank level 2:argument 和 parameter 进行了non-const 到 low-const 的转换
Rank level 3:argument 和 parameter 之间发生了整型的类型提升
Rank level 4:argument 和 parameter 之间发生了算术类型的转换(除开整型类型的提升),或者指针到别的类型转换
Rank level 5:argument 和 parameter 进行了类的类型转换

匹配需要类型提升的情况

如果调用过程中存在参数的类型提升,那么调用中比 int 的小的 argument,都将调用 parameter 类型为 int 的函数:

void ff(int);
void ff(short);
ff('a'); // call ff(int)
如果想调用指定大小 parameter 的函数,比如调用上例的 ff(short),argument 必须是 short 类型才可以。也就是说,必须存在完全匹配的调用,才可以无视类型提升的转换:
vod ff(int);
void ff(short);
short si = 10;
ff(si); //call ff(short)

匹配需要算术类型转换的情况

与类型提升不同,算术类型的互换具有同等优先级

void manip(long);
void manip(float);
manip(3.14); // error: ambiguous call
上例中,double 可以转换为 float 和 long, 因此导致了二义性。

匹配带有 low-const 的 argument

C++ 会根据 argument 的 constness 来选取最佳匹配。low_level const parameter 的最佳匹配是 const to reference / pointer; 普通的 argument 则会优先匹配 plain argument。

需要注意的是,存在 plain argument 到 low_level const 的转换; parameter 为 low_level const 的函数可以使用 plain argument,但该匹配等级为 2, 不是最佳匹配。

Record lookup(Account&); // function that takes a reference to Account
Record lookup(const Account&); // new function that takes a reference to const account
const Account a;
Account b;
lookup(a); // calls lookup(const Account&)
lookup(b); // calls lookup(Account&)

Pointer to functions

函数指针的定义

指向函数的指针(Pointer to functions)的类型由函数的返回值类型parameter 类型组成:

bool lentgh_comp(const string &, const string &); //function
bool (*pf) (const string &, const string&); //pointer points to a function that takes two string & and return a bool
括号不可丢弃,否则定义的是返回指针的函数:
bool *pf (const string &, const string&); //function named pf that returns a pointer to bool

函数指针的一般使用

函数名会转化为指针

当函数名被作为值使用的时候,函数会被转化为指向它的指针(与数组类似):

pf = length_comp; //length_comp is converted to a pointer that points itsself
pf = &length_comp;// equivalent operation, but not necessary
从例子中可以看到不需要取地址也可以获得指向函数的指针。

函数的指针可以用于调用函数

调用函数可以直接使用指针名调用,调用过程中,解应用可以省略:

bool b1 = pf("hello", "world"); //using pointer to call the function, no dereference needed
bool b2 = (*pf)("hello", "world"); //equivalent call, derefenence is not necessary
bool b3 = length_comp("hello", "world"); //equivalent call

函数指针之间没有类型转换

如下例子,sumLength 指向的函数类型不能通过其他类型的指针转换得到:

string::size_type sumLength(const string&, const string&);
bool cstringCompare(const char*, const char*);
pf = 0;              // ok: pf points to no function
pf = sumLength;      // error: return type differs
pf = cstringCompare; // error: parameter types differ
pf = lengthCompare;  // ok: function and pointer types match exactly
可以看出,只有在返回类型parameter 的数量和类型完全匹配的情况下,才可以进行函数指针的赋值。当然使用 0 或者 nullptr 初始化函数指针是个例外:
pf = 0; // ok
pf = nullptr; // ok

指向重载函数的指针

由于不同类型函数指针之间不存在类型转换,因此函数指针与重载函数的对应关系是一一对应的:

void ff(int*);
void ff(unsigned int);
void (*pf1) (unsgined int) = ff;// ok
void (*pf2) (int) = ff; // error, parameter type not match
double (*pf3) (int*) = ff; // error, return type not match

函数指针作为 parameter 使用

如何定义?

函数指针作为 parameter 有两种写的方式,区别在于带不带解引用操作符。但无论带不带,两种写法都等价

//the 3rd parameter is a function type(converted to pointer)
void useBigger(const string& s1, const string& s2, bool pf(const string&, const string&));
//explicitly define a pointer to function as a parameter
void useBigger(const string& s1, const string& s2, bool (*pf) (const string&, const string&));
如何使用?

直接使用函数名即可。函数名将会被自动转化为函数指针:
// automatically converts the function lengthCompare to a pointer to function
useBigger(s1, s2, lengthCompare);

使用 typedef 简化函数指针 parameter

使用 typedef 简化函数指针的类型时,与一般的写法有些差别:

//common 
typedef oldtype alias_type;

//in functions
return_type (pointer_to_func_name*) (parameter_list); //declaration of a pointer to function
typedef alias_type(*pointer_name) (parameter list); //typedef for pointer to functions
由于存在函数名到函数指针的自动转换,因此两种类型的函数指针表示都可以使用 typedef,比如:
// Func and Func2 have function type
typedef bool Func(const string&, const string&);
// FuncP have pointer to function type
typedef bool(*FuncP)(const string&, const string&);

除此之外,typedef 也可以配合 decltype 使用:

  • decltype 的形式不用变形,以正常的形式做定义
  • decltype 也可以应用于之前的两种函数指针表现类型上。但有一点需要注意:由于 decltype 会直接返回函数的类型,如果需要显式的定义函数指针类型,那么需要加上 * 标识符:

// Func and Func2 have function type
typedef bool Func(const string&, const string&);
typedef decltype(lengthCompare) Func2; // equivalent type

// FuncP and FuncP2 have pointer to function type
typedef bool(*FuncP)(const string&, const string&);
// equivalent type
// '*' indicates FuncP2 is a pointer 
typedef decltype(lengthCompare) *FuncP2;
typedef 定义过的名字可以直接使用,与之前使用函数名作为 parameter 时的情况等价:
//equivalent declarations
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 &));
               
void useBigger(const string&, const string&, Func); 
void useBigger(const string&, const string&, Funcp2);

函数指针的返回

函数必须以指针的形式返回:

与函数作为参数不同,编译器在返回函数类型的时候,不会自动将函数类型视作指向它的指针类型。因此,解引用符是无法省略的

直接声明返回类型为函数指针的函数

type (*function_name(function paramter_list))(parameter_list of a pointer to function);
以书上的例子来讲一下这个写法。首先是一个函数:
int A(int*, int);
如果我们需要将这个函数 A 作为另外一个 B 的返回值,那么必须写成指针的形式:
int (*PA) (int*, int); // pointer points to A, named PA
PA ,也就是函数 A 的返回类型即为:
int (*) (int*, int);
现在假设我们需要写一个函数 B 的声明。如果希望 B 接收一个 int 参数,并返回 A 类型的函数,那么函数 B 的声明应该写成:
int (*B(int))(int*, int);

使用 Type alias 简化上述声明

针对函数 A,有几种方式可以简化该种返回类型的写法:

使用 using:

using F = int (int*, int); //F is a function type
using PF = int(*) (int*, int); // PF is a pointer type
注意下面的第二个错误,再次强调函数的返回值不会被编译器自动转换为指针,因此让 函数 f1 返回一个函数类型是非法的:
PF f1(int);// ok, f1 is a pointer type
F f1(int);// error, f1 is a function type
F *f1(int);// ok, f1 is a pointer type
使用 auto 与 trail type 的组合:
auto f1(int) -> int(*) (int*, int);// trailing is the return type of A.

https://cdecl.org/ 神器网站,可以帮助理解复杂的声明。

auto 与 decltype 的其他应用

书中可能存在没有完成的部分,auto 的用法并未在书中提出。后面的理解为猜测,根据 stack overflow 上的解答来的。

假设我们拥有两个函数,除了名字不同之外,返回类型与 parameter 的类型都相同;这种情况下,我们可以新建一个函数,根据该函数接收到的 argument 来返回指向其中某个具体函数的指针:

string::size_type sum_length(const string&, const& string);
string::size_type large_length(const string&, const& string);
//using a string parameter to decide which pointer to function should be return
decltype(sum_length) *getFcn(const string&);
这段代码的实际意义在于,我们可以通过指定的 string 变量来返回对应的函数。假设我们有 getFcn() 定义如下:
decltype(sum_length) *getFcn(const string& s)
{
    if(s.size() > 100)
        return sum_length;
    else
        return large_length;
}
通过调用 getFcn() 就可以根据 string 长度来调用不同的函数,最终得到结果:
string str1, str2;

// getFcn(str1) returns the pointer to the function we wish to call
// (str1, str2) is the parameter list to the chosen function
// len will be the final result
auto len = getFcn(str1)(str1, str2);

ref:Using auto or decltype for Function Pointer Types in C++ primer