C++ Primer记录_第六章

第六章 函数

  • 函数是一个命名了的代码块,我们通过调用函数执行和相应的代码。
  • 函数可以有0个或多个参数,而且(通常)会产生一个结果。
  • 可以重载函数,同一个名字对应几个不同的函数。

6.1 函数基础

  • 一个典型的函数定义包括以下部分:返回类型函数名字由0个或多个形参组成的列表以及函数体
// val的阶乘是val*(val-l)*(val -2) ... *((val -(val -1))* 1)
int fact(int val)
{
    int ret = 1; //局部变量,用于保存计算结果
    while (val > 1)
    {
        ret *= val--; //把ret和val的乘积赋给ret,然后将val减1
    }
    return ret; //返回结果
}
  • 我们可以通过调用运算符来执行函数。
int main()
{
    int j = fact(5); // j等于120,即fact(5)的结果
    cout << "5! is " << j << endl;
    return 0;
}
5! is 120
  • 函数的调用完成两项工作:一是用实参初始化函数对应的形参,二是将控制权转移给被调用函数。
  • 此时,主调函数的执行被暂时中断,被调函数开始执行。
  • 实参是形参的初始值。第一个实参初始化第一个形参,第二个实参初始化第二个形参,以此类推。
  • 尽管实参与形参存在对应关系,但是并没有规定实参的求值顺序,编译器能以任意可行的顺序对实参求值。
fact("hello");   //错误:实参类型不正确
fact();          //错误:实参数量不足
fact(42, 10, 0); //错误:实参数量过多
fact(3.14);      //正确:该实参能转换成int类型
  • 函数的形参列表可以为空,但是不能省略。
void f1(){/*...*/}      //隐式地定义空形参列表
void f2(void){/*...*/}  //显式地定义空形参列表
  • 形参列表中的形参通常用逗号隔开,其中每个形参都是含有一个声明符的声明,即使两个形参的类型一样,也必须把两个类型都写出来。
int f3(int v1,v2){/*...*/}      //错误
int f4(int v1,int v2){/*...*/}  //正确
  • 大多数类型都能用作函数的返回类型。
  • 一种特殊的返回类型是void, 它表示函数不返回任何值。

6.1.1 局部对象

  • 在C++语言中,名字有作用域,对象有生命周期
  • 名字的作用域是程序文本的一部分,名字在其中可见。
  • 对象的生命周期是程序执行过程中该对象存在的一段时间。
  • 同时局部变量还会隐藏在外层作用域中同名的其他所有声明中。
  • 局部静态对象在程序的执行路径上第一次经过对象定义语句时初始化,并且直到程序终止才被销毁。
#include <iostream>
using namespace std;

size_t count_calls()
{
    static size_t ctr = 0; //调用结束后,这个值仍然有效
    return ++ctr;
}
int main()
{
    for (size_t i = 0; i != 10; ++i)
    {
        cout << count_calls() << endl;
    }
    return 0;
}
1
2
3
4
5
6
7
8
9
10

6.1.2 函数声明

  • 函数的声明不包含函数体。
void print(vector<int>::const_iterator beg, vector<int>::const_iterator end);
  • 建议在头文件声明。

6.1.3 分离式编译

  • 随着程序越来越复杂,需要使用分离式编译
  • 声明写在.h文件中,定义在.cpp文件中。

6.2 参数传递

  • 每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。
  • 当形参是引用类型时,我们说它对应的实参被引用传递或者函数被传引用调用
  • 当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象,我们说这样的实参被值传递或者函数被传值调用

6.2.1 传值参数

  • 当初始化一个非引用类型的变量时,初始值被拷贝给变量。
  • 传值参数的机理完全一样,函数对形参做的所有操作都不会影响实参。
int n = 0; // int类型的初始变量
int i = n; // i是n的值的副本
i = 42;    // i的值改变;n的值不变
  • 指针的行为和其他非引用类型一样,当执行指针拷贝操作时,拷贝的是指针的值,但是我们可以间接地访问它所指的对象。
//该函数接受一个指针,然后将指针所指的值置为0
void reset(int *ip)
{
    *ip = 0; //改变指针ip所指对象的值
    ip = 0;  //只改变了ip的局部拷贝,实参未被改变
}
int i = 42;
reset(&i);                 //改变i的值而非i的地址
cout << "i=" << i << endl; //输出i=0

6.2.2 传引用参数

  • 对于引用的操作实际上是作用在引用所引的对象上。
//该函数接受一个int对象的引用,然后将对象的值置为0
void reset(int &i) // i是传给reset函数的对象的另一个名字
{
    i = 0; //改变了i所引对象的值
}
int j = 42;
reset(j);                  // j采用传引用方式,它的值被改变
cout << "j=" << j << endl; //输出j=0
  • 拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型根本就不支持拷贝操作,函数只能通过引用形参访问该类型的对象。
//比较两个string对象的长度
bool isShorter(const string &s1,const string &s2)
{
    return s1.size()<s2.size();
}
  • 使用引用形参返回额外信息。
#include <iostream>
using namespace std;

//返回s中c笫一次出现的位置索引
//引用形参occurs负责统计c出现的总次数
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; //记录c第一次出现的位置
            }
            ++occurs; //将出现的次数加1
        }
    }
    return ret; //出现次数通过occurs隐式地返回
}

int main()
{
    string s = "openopen";
    string::size_type ctr = 0;
    auto index = find_char(s, 'o', ctr);
    cout << s << endl;
    cout << ctr << endl;
}
openopen
2

6.2.3 const形参和实参

  • 因为当用实参初始化形参时会忽略掉顶层const,换句话说,形参如果有顶层const,实参既可以传入const int也可以传入int,如果重载具有相同名字相同类型的变量形参,就会出现不确定性,以上为个人理解,书上写的不太懂。
const int ci = 42; //不能改变ci,const是顶层的
int i = ci;        //正确:当拷贝ci时,忽略了它的顶层const
int *const p = &i; // const是顶层,不能给p赋值
*p = 0;            //正确:通过p改变对象的内容是允许的,现在i变成了0

void fcn(const int i){ /*fcn能够读取i, 但是不能向i写值*/}
void fcn(int i){ /*...*/} //错误:重复定义了fcn(int)
  • 指针或引用形参与const,我们可以使用非常量初始化一个底层const对象,但是反过来不行,一个普通的引用必须用同类型的对象初始化。
int i = 42;
const int *cp = &i; //正确:但是cp不能改变i
const int &r = i;   //正确:但是r不能改变i
const int &r2 = 42; //正确:
int *p = cp;        //错误:p的类型和cp的类型不匹配
int &r3 = r;        //错误:r3的类型和r的类型不匹配
int &r4 = 42;       //错误:不能用字面值初始化一个非常量引用
void reset(int &i) // i是传给reset函数的对象的另一个名字
{
    i = 0;
}
int i = 0;
const int ci = i;
string::size_type ctr = 0;
reset(&i) {} //调用形参类型是int*的reset函数
reset(&ci);  //错误:不能用指向const int对象的指针初始化int*
reset(i);    //调用形参类型是int&的reset函数
reset(ci);   //错误:不能把苦通引用绑定到const对象ci上
reset(42);   //错误:不能把普通应用绑定到字面值上
reset(str);  //错误:类型不匹配,ctr是无符号类型
  • 尽量使用常量引用。

6.2.4 数组形参

  • 数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响。
    1. 不允许拷贝数组。
    2. 使用数组时通常会将其转换成指针。
//尽管形式不同,但这三个print函数是等价的
//每个函数都有一个const int*类型的形参
//尽管表现形式不同,但下面的三个函数是等价的:每个函数的唯一形参都是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]
  • 使用标记指定数组长度。
void print(const char *cp)
{
    if (cp) //若cp不是一个空指针
    {
        while (*cp) //只要指针所指的字符不是空字符
        {
            cout << *cp++; //输出当前字符并将指针向前移动一个位置
        }
    }
}
  • 标准库规范。
void print(const int *beg, const int *end)
{
    //输出beg到end之间(不含end)的所有元素
    while (beg != end)
    {
        cout << *beg++ << endl; //输出当前元素并将指针向前移动一个位置
    }
}

int j[2] = {0, 1};
// j转换成指向它首元素
//第二个实参是指向j的尾后元素的指针
print(begin(j), end(j)); // begin和end函数
  • 显式传递一个表示数组大小的形参。
// const int ia[]等价于const int* ia
// size表示数组的大小,将它显式地传给函数用于控制对ia元素的访问
void print(const int ia[], size_t size)
{
    for (size_t i = 0; i != size; ++i)
    {
        cout << ia[i] << endl;
    }
}
  • 数组形参和const,当函数不需要对数组元素执行写操作的时候,数组形参应该是指向const的指针。
  • 数组引用形参,形参可以是数组的引用。
//正确:形参是数组的引用,维度是类型的一部分
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个整数的数组
  • 当将多维数组传递给函数时,真正传递的是指向数组首元素的指针。
// matrix指向数组的首元素,该数组的元素是由10个整数构成的数组
void print(int (*matrix)[10], int rowSize)
{ /*...*/
}
int *matrix[10];   // 10个指针构成的数组
int (*matrix)[10]; //指向含有10个整数的数组的指针
//等价定义
void print(int matrix[][10], int rowSize)
{ /*...*/
}

6.2.5 main:处理命令行选项

  • main函数平时定义的时候只有空形参列表int main(){}
  • main函数可以接受传递实参int main(int argc, char *argv []) { ... }等价于int main(int argc, char **argv) { ... }

6.2.6 含有可变形参的函数

  • 有时我们无法提前预知应该向函数传递几个实参。
  • 为了编写能处理不同数噩实参的函数,C++11新标准提供了两种主要的方法:
    1. 如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型。
    2. 如果实参的类型不同,可以编写一种特殊可变参数模板
  • initializer_list类型形参是一种标准库类型。
void error_msg(initializer_list<string> il)
{
    for (auto beg = il.begin(); beg != il.end(); ++beg)
    {
        cout << *beg << " ";
    }
    cout << endl;
}

//expected和actual是string对象
if (expected!=actual)
{
    error_msg({"functionX";expected,actual});
}
else
{
    error_msg({"functionX","okay"});
}
  • 含有initializer_list类型形参的函数也可以同时拥有其他形参。
void error_msg(ErrCode e, initializer_list<string> il)
{
    cout << e.msg() << ": ";
    for (const auto &elem : il)
    {
        cout << elem << " ";
    }
    cout << endl;
}

// expected和actual是string对象
if (expected != actual)
{
    error_msg(ErrCode(42),{"functionX";expected,actual });
}
else
{
    error_msg(ErrCode(0),{"functionX", "okay"});
}
  • 省略符形参是为了便于C++程序访问某些特殊的C代码而设置的。
void foo(parm_list,...);
void foo(...);

6.3 返回类型和return语句

  • return语句终止当前正在执行的函数并将控制权返回到调用该函数的地方。
  • 有两种形式:
    1. return;
    2. return expression;

6.3.1 无返回值函数

  • 没有返回值的return语句只能用在返回类型是void的函数中。
void swap(int &v1, int &v2)
{
    //如果两个值是相等的,则不需要交换,直接退出
    if (v1 == v2)
    {
        return;
    }
    //如果程序执行到了这里,说明还需要继续完成某些功能
    int tmp = v2;
    v2 = v1;
    v1 = tmp;
    //此处无须显示的return语句
}

6.3.2 有返回值函数

  • return语句的第二种形式提供了函数的结果,只要函数的返回类型不是void,则该函数内的每条return语句必须返回个值。
//因为含有不正确的返回值,所以这段代码无法通过编译
bool str_subrange(const string &str1, const string &str2)
{
    //大小相同:此时用普通的相等性判断结果作为返回值
    if (str1.size() == str2.size())
    {
        return str1 == str2; //正确:==运算符返回布尔值
    }
    //得到较短string对象的大小
    auto size = (str1.size() < str2.size()) ? str1.size() : str2.size();
    //检查两个string对象的对应字符是否相等,以较短的字符串长度为限
    for (decltype(size) i = 0; i != count; ++i)
    {
        if (str1[i] != str2[i])
        {
            return; //错误#1:没有返回值,编译器将报告这一错误
        }
    }
    //错误#2:控制流可能尚未返回任何值就结束了函数的执行
    //编译器可能检查不出这一错误
}
  • 返回一个值的方式和初始化一个变量或形参的方式完全一样。
//如果ctr的值大于1,返回word的复数形式
string make_plural(size_t ctr, const string &word, const string &ending)
{
    return (str > 1) ? word + ending : word;
}
* 同其他引用类型一样,如果函数返回引用,则该引用仅是它所引对象的一个别名。
//挑出两个string对象中较短的那个,返回其引用
const string &shorterString(const string &s1, cosnt string &s2)
{
    return s1.size() <= s2.size() ? s1 : s2;
}
  • 不要返回局部对象的引用或指针。
//严重错误:这个函数试图返回局部对象的引用
const string &manip()
{
    string ret;
    //以某种方式改变一下ret
    if (!ret.empty())
    {
        return ret; //错误:返回局部对象的引用!
    }
    else
    {
        return "Empty"; //错误:"Empty"是一个局部临时量
    }
}
  • 和其他运算符一样,调用运算符也有优先级和结合律。
  • 调用运符符的优先级与点运符符和箭头运算符相同,并且也符合左结合律。
//调用string对象的size成员,该string对象是由shorterString函数返回的
auto sz = shorterString(sl, s2).size();
  • 引用返回左值,调用一个返回引用的函数得到左值,其他返回类型得到右值。
char &get_val(string &str, string::size_type ix)
{
    return str[ix]; // get_val假定索引值是有效的
}

int main()
{
    string s("a value");
    cout << s << endl;   //输出a value
    get_val(s, 0) = 'A'; //将s[0]的值改为A
    cout << s << endl;   //输出A value
    return 0;
}
  • 如果返同类型是常量引用,我们不能给调用的结果赋值。
const string &shorterString(const string &s1, cosnt string &s2)
{
    return s1.size() <= s2.size() ? s1 : s2;
}
shorterString("hi", "bye") = "X"; //错误:返回值是个常量
  • C++11新标准规定,列表初始化返回值可以返回花括号包围的值的列表。
vector<string> process()
{
    //...
    // expected和actual是string对象
    if (expected.empty())
    {
        return {}; //返回一个空vector对象
    }
    else if (expected == actual)
    {
        return {"functionX", "okay"}; //返回列表初始化的vector对象
    }
    else
    {
        return {"functionX", expected, actual};
    }
}
  • 允许主函数main没有return语句直接结束。控制到达了main函数的结尾处而且没有return语句,编译器将隐式地插入一条返回0的return语句。
  • 如果一个函数调用了它自身,不管这种调用是直接的还是间接的,都称该函数为递归函数。
//计算val的阶乘,即1*2*3...*val
int factorial(int val)
{
    if (val > 1)
    {
        return factorial(val - 1) * val;
    }
    return 1;
}

6.3.3 返回数组指针

  • 因为数组不能被拷贝,所以函数不能返回数组,不过,函数可以返回数组的指针或引用。
typedef int arrT[10]; // arrT是一个类型别名,它表示的类型是含有10个整数的数组
using arrT = int[10]; // arrT的等价声明
arrT *func(int i);    // fun返回一个指向含有10个整数的数组的指针

int arr[10];          // arr是一个含有10个整数的数组
int *p1[10];          // p1是一个含有10个指针的数组
int (*p2)[10] = &arr; // p2是一个指针,它指向含有10个整数的数组
  • 返回数组指针的函数形式如下所示:
    1. Type表示元素的类型。
    2. dimension表示数组的大小。
    3. (*function(parameter_list))两端的括号必须存在,就像我们定义p2时两端必须有括号一样。
  • func(int i)表示调用func函数时需要一个int类型的实参。
  • (*func(int i))意味着我们可以对函数调用的结果执行解引用操作。
  • (*func(int i))[10]表示解引用func的调用将得到一个大小是10的数组。
  • int (*func(int i))[10]表示数组中的元素是int类型。
Type (*function(parameter_list))[dimension]
int (*func(int i))[10];
  • 使用队尾表示法,任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效,比如返回类型是数组的指针或者数组的引用。
//func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组
auto func(int i) -> int (*)[10];
  • 使用decltype,如果找们知道函数返回的指针将指向哪个数组,就可以使用关键字声明返回类型。
int odd[] = {1, 3, 5, 7, 9};
int even[] = {0, 2, 4, 6, 8};
//返回一个指针,该指针指向含有5个整数的数组
decltype(odd) *arrPtr(int i)
{
    return (i % 2) ? &odd : &even; //返回一个指向数组的指针
}

6.4 函数重载

  • 如果同一个作用域内的几个函数名字相同形参列表不同,我们称之为重载函数。
void print(const char *cp);
void print(const int *beg, const int *end);
void print(const int ia[], size_t size);

int j[2] = {0, 1};
print("Hello World");        //调用print(const char*)
print(j, end(j) - begin(j)); // void print(const int ia[],size_t size);
print(begin(j), end(j));     // void print(const int *beg,const int *end);
  • main函数不能重载。
  • 重载函数使得可以定义一组函数,更据不同的调用方式调用不同的函数。
Record lookup(const Account &); //根据Account查找记录
Record lookup(const Phone &);   //根据Phone查找记录
Record lookup(const Name &);    //根据Name查找记录
Account acct;
Phone phone;
Record r1 = lookup(acct);  //调用接受Account的版本
Record r2 = lookup(phone); //调用接受Phone的版本
  • 对于重载的函数来说,它们应该在形参数量或形参类型上有所不同。
  • 假设有两个函数,它们的形参列表一样但是返回类型不同,那么它是错误的。
Record lookup(const Account &);
bool lookup(const Account &); //错误:与上一个函数相比只有返回类型不同
  • 判断两个形参的类型是否相异。
Record lookup(const Account &acct);
Record lookup(const Account &); //省略了形参的名字
typedef Phone Telno;
Record lookup(const Phone &);
Record lookup(const Telno &); // Telno和Phone的类型相同
  • 重载和const形参,顶层const不影响传入函数的对象。
Record lookup(Phone);
Record lookup(const Phone); //重复声明了Record lookup(Phone)
Record lookup(Phone *);
Record lookup(Phone *const); //重复声明了Record lookup(Phone*)

//对于接受引用或指针的函数来说,对象是常量还是非常量对应的形参不同
//定义了4个独立的重载函数
Record lookup(Account &);       //函数作用于Account的引用
Record lookup(const Account &); //新函数作用于常量引用
Record lookup(Account *);       //新函数,作用于指向Account的指针
Record lookup(const Account *); //新函数,作用于指向常量的指针
  • 尽管函数重载能在一定程度上减轻我们为函数起名字、记名字的负担,但是最好只重载那些确实非常相似的操作。
  • const_cast和重载。
const string &shorterString(const string &s1, cosnt string &s2)
{
    return s1.size() <= s2.size() ? s1 : s2;
}

string &shorterString(string &s1, string &s2)
{
    auto &r = shorterString(const_cast<const string &>(sl), const_cast<const string &>(s2));
    return const_cast<string &>(r);
}
//为什么不这么写?
string &shorterString(string &s1, string &s2)
{
    return s1.size() <= s2.size() ? s1 : s2;
}
  • 函数匹配是指定义了一组重载函数后,我们需要以合理的实参调用它们。
  • 在这个过程中我们把函数调用与一组重载函数中的某一个关联起来,函数匹配也叫做重载确定
  • 当调用重载函数时有三种可能的结果:
    1. 编译器找到一个与实参最佳匹配的函数,并生成调用该函数的代码。
    2. 找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配的错误信息。
    3. 有多于一个函数可以匹配,但是每一个都不是明显的最佳选择。此时报错称二义性调用

6.4.1 重载与作用域

  • 如果我们在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体。
string read();
void print(const string &);
void print(double); //重载print函数
void fooBar(int ival)
{
    bool read = false; //新作用域:隐藏了外层的read
    string s = read(); //错误:read是一个布尔值,而非函数
    //不好的习惯:通常来说,在局部作用域中声明函数不是一个好的选择
    void print(int); //新作用域:隐藏了之前的print
    print("Value:"); //错误:print(const string &)被隐藏掉了
    print(ival);     //正确:当前print(int)可见
    print(3.14);     //正确:调用print(int);print(double)被隐藏掉了
}
void print(const string &);
void print(double); //重载print函数
void print(int);    //重载print函数
void fooBar2(int ival)
{
    print("Value:"); //调用print(const string &)
    print(ival);     //调用print(int)
    print(3.14);     //调用print(double)
}

6.5 特殊用途语言特性

  • 三种函数相关的语言特性:
    1. 默认实参。
    2. 内联函数和constexpr函数。
    3. 程序调试过程中常用的一些功能。

6.5.1 默认实参

  • 在函数的很多次调用中它们都被赋予一个相同的值,此时,我们把这个反复出现的值称为函数的默认实参
  • 调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。
typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char backgrand = ' ');
string window;
window = screen();             //等价于screen(24,80,' ')
window = screen(66);           //等价于screen(66,80,' ')
window = screen(66, 256);      //等价于screen(66,256,' ')
window = screen(66, 256, '#'); //等价于screen(66,256,'#')
window = screen(, , '?');      //错误:只能省略尾部的实参
window = screen('?')           //调用screen('?',80,' ')
  • 对于函数的声明来说,通常的习惯是将其放在头文件中,并且一个函数只声明一次,但是多次声明同一个函数也是合法的。
//表示高度和宽度的形参没有默认值
string screen(sz, sz, char = ' ');
string screen(sz, sz, char = '*');     //错误:重复声明,不能修改一个已经存在的默认值
string screen(sz = 24, sz = 80, char); //正确:添加默认实参
  • 局部变量不能作为默认实参。
  • 只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参。
// wd、def和ht的声明必须出现在函数之外
sz wd = 80;
char def = ' ';
sz ht();
string screen(sz = ht(), sz = wd, char = def);
string window = screen(); //调用screen(ht(),80,' ')
void f2()
{
    def = '*';          //改变默认实参的值
    sz wd = 100;        //隐藏了外层定义的wd,但是没有改变默认值
    windown = screen(); //调用screen(ht(),80,'*')
}

6.5.2 内联函数和constexpr函数

  • 使用函数一般比求等价表达式的值要慢一点,所以可以将规模较小的操作定义为内联函数
  • 将函数指定为内联函数,通常就是将它在每个调用点上“内联地”展开。
const string &shorterString(const string &s1, cosnt string &s2)
{
    return s1.size() <= s2.size() ? s1 : s2;
}
//调用如下
cout << shorerString(s1, s2) << endl;
//编译过程中内联展开成以下形式
cout << (s1.size() < s2.size() ? s1 : s2) << endl;
  • 在函数返回类型前加上关键字inline,这样可以声明成内联函数。
inline const string &shorterString(const string &s1, cosnt string &s2)
{
    return s1.size() <= s2.size() ? s1 : s2;
}

6.5.3 调试帮助

  • C++有时会用到一种类似头文件保护的技术,以便有选择地执行调试代码。
  • assert是一种预处理宏,所谓预处理宏其实是一个预处理变量,它的行为有点类似内联函数。
  • assert由预处理器管理而非编译器管理,所以使用时都不用命名空间声明。
//首先对expr求值,如果表达式为假,assert输出信息并终止程序的执行。如果表达式为真,assert什么也不做。
assert(expr);
  • NDEBUG预处理变量,assert的行为依赖于NDEBUG的状态。
  • 如果定义了NDEBUG,则assert什么也不做。
  • 默认状态下没有定义NDEBUG,此时assert将执行运行时检查。
  • 很多编译器都提供了一个命令行选项使我们可以定义预处理变量。
gcc -D NDEBUG main.c
#define NDEBUG//等价于上面
  • 也可以使用NDEBUG编写自己的条件调试代码,条件编译
void print(const int ia[], size_t size)
{
#ifndef NDEBUG //如果定义了NDEBUG,这些代码将被忽略掉
    //__func__是编译器定义的一个局部静态变量,用于存放函数的名字
    cerr << __func__ << ": array size is" << size << endl;
#endif
    //...
}
  • 预处理器几个定义了于程序调试很有用的系统宏
__fun__//存放函数的名字
__FILE__//存放文件名的字符串字面值
__LINE__//存放当前行号的整型字面值
__TIME__//存放文件编译时间的字符串字面值
__DATE__//存放文件编译日期的字符串字面值
  • 可以使用这些常量在错误消息中提供更多信息:
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;
}

6.6 函数匹配

  • 当几个重载函数的形参数量相等以及某些形参的类型可以由其他类型转换得来时要进行函数匹配
  • 第一步是选定本次调用对应的重载函数集,集合中的函数称为候选函数
  • 第二步考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数
void f();
void f(int);
void f(int, int);
void f(double, double = 3.14);
f(5.6); //调用void f(double,double)
//f(int)是可行的,因为实参类型double能转换成形参类型int
//f(double, double)是可行的,因为它的第二个形参提供了默认值,而第一个形参的类型正好是double,与函数使用的实参类型完全一致。
  • 第三步是从可行函数中选择与本次调用最匹配的函数,实参类型与形参类型越接近,它们匹配得越好,所以精确匹配比需要类型转换的匹配更好。
  • 当实参的数量有两个或更多时,函数匹配就比较复杂了,如下,因为每个可行函数各自在个实参上实现了更好的匹配,编译器最终将因为这个调用具有二义性而拒绝其请求。
f(42,2.56)

6.6.1 实参类型转换

  • 为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个等级,具体排序如下所示:
    1. 精确匹配,包括以下情况:
    • 实参类型和形参类型相同。
    • 实参从数组类型或函数类型转换成对应的指针类型。
    • 向实参添加顶层const或者从实参中删除顶层const。
    1. 通过const转换实现的匹配。
    2. 通过类型提升实现的匹配。
    3. 通过算术类型转换或指针转换实现的匹配。
    4. 通过类类型转换实现的匹配。
  • 小整型一般都会提升到int类型或更大的整数类型,即使实参是一个很小的整数值,也会直接将它提升成int类型,此时使用short版本反而会导致类型转换。
void ff(int);
void ff(short);
ff('a'); // char提升成int; 调用f(int)
  • 字面值3.14的类型是double,它既能转换成long也能转换成float,因为存在两种可能的算数类型转换,所以该调用具有二义性。
void manip(long);
void manip(float);
manip(3.14); //错误:二义性调用
  • 如果重载函数的区别在于它们的引用类型的形参是否引用了const,或者指针类型的形参是否指向const,则当调用发生时编译器通过实参是否是常量来决定选择哪个函数。
Record lookup(Account &);       //函数的参数是Account的引用
Record lookup(const Account &); //函数的参数是一个常量引用
const Account a;
Account b;
lookup(a); //调用lookup(const Account&)
lookup(b); //调用Record lookup(Account&);

6.7 函数指针

  • 函数指针指向的是函数而非对象。
  • 和其他指针一样,函数指针指向某种特定类型。
  • 函数的类型由它的返回类型和形参类型共同决定,与函数名无关。
//比较两个string对象的长度
bool lengthCompare(const string &, const string &);
//pf指向一个函数,该函数的参数是两个const string的引用,返回值是bool类型。
bool (*pf)(const string &, const string &); //未初始化
//声明一个名为pf的函数,该函数返回bool*
bool *pf(const string &, const string &);
  • 当我们把函数名作为一个值使用时,该函数自动地转换为指针。
bool (*pf)(const string &, const string &);
pf = lengthCompare;                          // pf指向名为lengthCompare的函数
pf = &lengthCompare;                         //等价的赋值语句:取地址符是可取的
bool b1 = pf("hello", "goodbye");            //调用lengthCompare
bool b2 = (*pf)("hello", "goodbye");         //一个等价的调用
bool b3 = lengthCompare("hello", "goodbye"); //另一个等价的调用
string::size_type sumLength(const string &, const string &);
bool cstringCompare(const char *, const char *);
pf = 0;              //正确:pf不指向任何函数
pf = sumLength;      //错误:返回类型不匹配
pf = cstringCompare; //错误:形参类型不匹配
pf = lengthCompare;  //正确:函数和指针的类型精确匹配
  • 重载函数的指针,编译器通过指针类型决定选用哪个函数,指针类型必须与重载函数中的某一个精确匹配。
void ff(int *);
void ff(unsigned int);
void (*pf1)(unsigned int) = ff; // pf1指向ff(unsigned)
void (*pf2)(int) = ff;          //错误:没有任何一个ff与该形参列表匹配
double (*pf3)(int *) = ff;     //错误:ff和pf3的返回类型不匹配
  • 函数指针形参
bool lengthCompare(const string &, const string &);
//笫三个形参是函数类型,它会自动地转换成指向函数的指针
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 &));
//自动把函数lengthCompare转换成指向该函数的指针
userBigger(s1, s2, lengthCompare);
// 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) *Func2; //等价的类型
// useBigger的等价声明,其中使用了类型别名
void useBigger(const string &, const string &, Func);//编译器自动将Func表示的函数类型转换成指针
void useBigger(const string &, const string &, FuncP2);
  • 返回指向函数的指针。
using F = int(int *, int);      // F是函数类型,不是指针
using PF = int (*)(int *, int); // PF是指针类型
PF f1(int);                     //正确:PF是指向函数的指针,f1返回指向函数的指针
F f1(int);                      //错误:F是函数类型,f1不能返回一个函数
F *f1(int);                     //正确:显式地指定返回类型是指向函数的指针
  • 由内向外的顺序解读表达式。
int (*f1(int))(int *, int);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Flame老唐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值