C++ Primer 笔记 第六章
函数由四部分组成:函数的返回类型,函数名,函数接收的参数列表 (parameter list),还有函数体。我们通过调用运算符(一对括号)来对函数进行调用,调用返回的类型即是函数的返回类型。
函数调用分为三步:
Argument 用于对函数 Parameter 的初始化。函数的 parameter 和 arguments 有如下的匹配要求:
函数按顺序对应初始化的 argument 和 paramater,但并不保证先初始化哪个 argument。
Parameter List 有如下的要求:
void
关键字作为填充:
void f1(){ /* ... */ };
void f2(void){ /* ... */ } // explicit void parameter list
int f3(int v1, v2) { /* ... */ } // error
int f4(int v1, int v2) { /* ... */ } // ok
void
。两个前置概念:
两个后续概念:
由于函数的创建会定义一个新的 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;
}
局部静态变量的默认初始化? 函数也是对象,所以函数在使用之前也必须声明。函数的声明:
写法:
type function_name (/* parameter list here is not necessary but recommonded */);
怎么样使用头文件来帮助函数的声明?
<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)的方式:
这两种方式分别称为引用传递(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
}
为什么要使用引用传递?
bool is_shorter(const string &s1, const string &s2) {
return s1.size() < s2.size();
}
通常情况下函数只能返回一个值,而使用引用传递能够有效的返回多个值:比如下例,我们希望在某个 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
则直接被函数修改了。这是函数使用引用传递修改函数外对象的另一种使用方法。
带有 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 属性已经被忽略掉了,因此编译器判定上述两个函数是同一个。
明确两点以下两点后:
可以得出结论:
一些例子:
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.
什么时候使用 reference to const 作为 parameter?
通常情况下,使用 reference to const 作为 parameter 表示了不会进行对 parameter 修改的意愿。这种意愿是面向一切数据的:无论传递的是 non-const 或是 low-const 的数据。
使用 plain reference 取代 reference to const 会有什么后果?
比如书中的例子,将接收 const string&
类型参数的 find_char
函数改为接收 plain reference 的参数:
string::size_type find_char(string &s, char c,
string::size_type &occurs);
则引发的问题有:
find_char("Hello World", 'o', ctr); //error, plain reference can't be initialized by a literal
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
}
解决的方法?
Array 具有两个特性:
因此,函数想获得一个数组类型的参数,是不能通过值传递的方式来得到的,但可以通过传递指针来得到。
定义数组的 parameter 有三种等价的方式:
void print(int arr[]);
void print(int arr[10]);
void print(int *arr);
上面所有的方法最终都将转化为 const int*
类型的指针。值得注意的是第二个例子,尽管 parameter 中注明了数组的大小,但该转换过程中只会用到数组名。函数是没有办法通过单个数组参数来知道数组的大小的。
解决上述问题有三种方式:
/0
:
void print(const char *cp) {
if (cp) //cp is not a nullptr
while(*cp) //as long as cp is not nullpter
cout << *cp++;
}
begin
和 end
两个函数,使用类似 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));
//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
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 需要传递。
解决方案是什么?
initializer_list
参数。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,初始化以后不许再更改。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 ?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 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 expression;
应用场景是什么?
只适用于返回类型是 void
的函数。
意义是什么?
return
语句的存在(或是 return 语句没有生效),那么意味着函数会完整的执行到最后一行。
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 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 语句。否则该行为是 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 的例子。这种情况下需要分情况:
解释一下第二点,书上有一段是这么说的:
The string literal is converted to a local temporary string object.
const char*
类型的。因此第二种情况实际上是以 const char*
的形式返回了一个全局对象,并没有什么问题。const string &manip()
。如果强制按 STL string 类型返回,那么 “Empty” 的类型会从 const char*
隐式转换为 std::string
。std::string 定义的变量在函数中属于局部变量,因此函数结束时,std::string “Empty” 就不存在了。因此返回该 string 的引用就是 Undefined 的行为。Refs:
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;
}
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 返回值需要相同的类型:
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)。递归需要保证某次调用不参与调用自身,否则会无限递归。
数组不能被复制,因此函数返回数组的方式是使用指针。
两种简化数组定义的方式:
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 是必须的。
C++ 11 中提供了一种新的 trailing return type 来简化函数的声明。结构如下:
auto function(parameter list) -> array type;
一个例子,函数接收一个 int,返回一个指向包含有10个 int 元素数组的指针:
auto func(int i) -> int(*) [10];
另外一种声明返回数组指针函数的方式是使用 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
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 不是同一种参数:record lookup(Account&);
record lookup(const Account&); //new function that takes a reference to const
如果 argument 是 non-const, 编译器会倾向于选择 non-const 版本的函数重载版本。
假设如下场景:
尽管带有 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);
}
//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)的策略。在匹配的过程中通常会有三种情况出现:
匹配的机制请参考函数匹配。
重载与 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
}
为什么会有这样的影响?所以导致的结果就是,只要在本地找到了对应的名字声明,即便这个函数不能用,编译器也不会去寻找 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 可以应用于一些常用的、很少需要修改的变量,比如固定大小的画纸的长宽等等。
怎么写?
using sz = string::size_type;
stromg screen(sz ht = 24, sz wd = 80, char bacgrnd = ' ');
调用带有 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 的 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 的要求是?
为什么不能使用局部变量作为 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 函数?
普通函数在调用的过程中会有额外的开销,包括但不限于:
如果被调用的函数是短小的(通常就一行),但被调用的非常频繁的函数,使用 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 函数的递归。
constexpr 函数的要求是什么?
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 函数只能定义一次吗?
不像一般的函数,inline / constexpr 函数可以定义多次。
为什么可以?
可以看出,通过 extern
来共享 inline / constexpr 函数是行不通的;允许每个文件中独立存在 inline / constexpr 函数是有必要的。当然,随之而来的是另外一个要求:所有 inline / constexpr 函数的定义必须相同。
有没有更好的办法共享 inline / constexpr 函数 ?
将 inline / constexpr 的函数定义放到 header 中即可。通常只将普通函数的声明放到 header 中,是因为有函数重定义的风险;而放置 inline constexpr 函数并没有这样的顾虑。
arrert 是什么?
assert
是一个预处理宏(Preprocessor marco)。预处理宏是一个预处理变量(Preprocessor variable),行为类似于 inline 函数。
assert 的用法与功能是?
assert 定义于 cassert
头文件中。由于 assert 是预处理宏,因此不需要指定命名。用法如下:
assert(expression);
其主要功能是用于检查一些“不应该”发生的情况;比如某个词的长度不应该超过为它设置的上限:
assert(word.size() > threshold);
assert 通过执行目标表达式来得到结果。如果目标表达式返回 false
,则 assert 会发送相关信息并停止整个程序的执行;反之则不会进行任何操作。
NDEBUG是什么?
NDEBUG 同样是一个预处理变量。我们可以将 NDEBUG 视作 assert 的总开关:如字面意义,当 NDEBUG 被定义,则 assert 不工作(no debug!)。
NDEBUG 如何使用?
#define 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
1.寻找 Candidate Functions
只要名字与被调用函数相同的,都是 Candidate Function。
2.寻找 Viable Functions
Viable function 需要满足:
parameter 的数量匹配过程中,带有 deafault argument 的 parameter 比较灵活,比如带 2 个 parameter 的函数,其中有一个有 default argument,那么调用者无论是带两个 parameter 还是 一个 parameter,都符合函数数量匹配的要求。
3.寻找 Best Match
最佳匹配基于匹配等级来衡量,同时也分两种情况处理:
只需要匹配单个 parameter 的情况:
这种情况下,优先选择匹配等级高的作为最佳匹配对象。
存在匹配多个 parameter 的情况:
编译器会依次检查 parameter 来决定最佳匹配。如果有函数同时满足下列条件:
那么这个函数则为最佳匹配。如果这样的函数存在不止一个,那么该调用则是二义性的(ambiguous call),将导致编译错误。
一个二义性例子的典型:
void f();
void f(int);
void f(int, int);
void f(double, double = 3.14);
f(2.56, 42)
整个过程如下:
到此为止,两个版本都有匹配等级更高的 parameter 存在,因此导致了二义性。
Argument Type Matching Rank(匹配等级) 按以下的规则进行划分:
Rank level 1:最高等级的匹配,如果匹配 argument 与 parameter 得到以下结果:
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, 因此导致了二义性。
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)的类型由函数的返回值类型和 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 有两种写的方式,区别在于带不带解引用操作符。但无论带不带,两种写法都等价:
//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 简化函数指针的类型时,与一般的写法有些差别:
//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);
针对函数 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 的用法并未在书中提出。后面的理解为猜测,根据 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