C++primer 第六章 函数

第六章 函数

6.1函数基础

6.1.1局部对象

局部静态对象
有些时候需要使某些局部变量贯穿到函数始终,可以将局部变量定义成static 类型,局部静态对象在第一次执行的时候初始化,并且直到程序结束才销毁,局部静态变量可以理解为定义才函数中的全局变量,作用域在函数之中,但是变量的声明周期存在于整个程序。

#include <iostream>

using std::cin;
using std::cout;
using std::endl;

size_t count_calls() {
    static size_t ctr = 0;
    return ++ ctr;
}

int main() {
    for (size_t i = 0; i != 10; ++ i)
        cout << count_calls() << " ";
    return 0;
}
// 1 2 3 4 5 6 7 8 9 10

6.1.3分离式编译

假设将阶乘函数fact定义在fact.cc文件中,函数的声明在Chapter.h文件中,在factMain.cc文件的main函数中调用,要生成可执行文件必须告诉编译器我们用到的代码在哪。编译过程如下:
g++ factMain.cc fact.cc #生成 factMain.exe or a.out
g++ factMain.cc fact.cc -o main #生成 main or main.exe
如果修改其中一个源文件只需要重新编译改动的源文件,编译器采用分离式编译每个文件的机制,编译过程中会产生后缀名为.obj(windows) 或.o(Unix)的文件,后缀名的含义是该文件包含对象代码,然后编译器将对象文件链接生成可执行文件,编译详细过程如下:
g++ -c factMain.cc #生成 factMain.o
g++ -c fact.cc #生成 fact.o
g++ factMain.o fact.o #生成 factMain.exe or a.out
g++ factMain.o fact.o -o main #生成main or main.exe

6.2参数传递

6.2.1传值参数

当实参传递个一个非引用调用的形参时,初始值被拷贝给形参,形参的改变不影响实参的改变

指针形参
指针的行为和其他非引用类型一样,实参指针的值拷贝给形参,即将实参指针指向的地址拷贝给形参,两个指针是不同的指针,但是指向的对象相同。

#include <iostream>

void reset(int *p) {
    std::cout << "形参p的值是:" << p << "形参p所指向的对象的值是:" << *p << std::endl;
    *p = 0;
    p = 0;
}

int main(int argc, char const *argv[])
{
    int i = 42;
    int *q = &i;
    std::cout << "形参q的值是:" << q << "形参q所指向的对象的值是:" << *q << std::endl;
    reset(q);
    std::cout << "i的值是" << i << std::endl;
    return 0;
}

// 形参q的值是:0x7fffb1642bcc形参q所指向的对象的值是:42
// 形参p的值是:0x7fffb1642bcc形参p所指向的对象的值是:42
// i的值是0

6.2.2传引用参数

传引用参数就是形参对实参的绑定,因此形参相当于是被绑定的实参的一个名字。
使用引用避免拷贝
拷贝大的类类型对象或者容器对象比较低效,有的类类型根本不支持拷贝操作,这个时候只能通过引用形参访问该类的对象。如果函数无需改变引用形参的值,最好将其声明为引用。

6.2.3const形参和实参

顶层const作用于对象本身

const int ci = 42;  //不能改变ci,const是顶层的
int i = ci; //正确:当拷贝ci时,忽略顶层const
int * const p = &i; //const是顶层的,不能给p赋值
*p = 0; //正确,可以改变p所指向的内容的值

当实参初始化形参时会忽略掉顶层const,当形参有顶层const时传给它常量对象或非常量对象都是可以的
void fcn(const int i) { // fcn只能读I不能写i; }
void fcn(int i) {}; //错误,重复定义fcn(int)
C++支持函数重载但是两个同名函数的形参列表应该有明显区别, 因为第一个fcn的形参的顶层const被忽略掉,所以第二个fcn的形参和第一个一样。
指针或引用形参与const
形参的初始化方式和变量的初始化方式一样,可以用一个非常量初始化一个底层const对象,但是反过来不行。

int i = 42;
const int *cp = &i; //正确,当时cp不能改变i,可以改变cp的指向
const int &r = i;   //正确,但是r不能改变I
const int &r2 = 42; //正确
int *p = cp;    //错误,p是非常量,cp是常量类型不匹配
int &r3 = r;    //错误,r3类型和r不匹配
int &r4 = 42;   //错误不能使用一个字面值初始化一个非常量引用

void reset1(int *p);
void reset2(int &p);
int i = 0;
const int ci = i;
string::size_type ctr = 0;
reset1(&i);
reset1(&ci); //错误,不能用指向const int 对象的指针初始化int *
reset2(i);
reset2(ci); //错误,不能把普通引用绑定到const对象ci上
reset2(42); //错误:不能把普通应用绑定到字面值上
reset2(ctr);    //错误,类型不匹配

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;
}
//正确:find_char的第一个形参是对常量的引用
find_char("Hello World", 'o', ctr);

尽量使用常量引用
把函数不会改变的形参定义成引用是一种比较常见的错误,会给函数的调用者带来误导,即函数可以修改它的实参的值,而且使用非常量引用会限制函数多能接受的实参类型。

//将find_char改成非常量引用
string::size_type find_char(string &s, char c, string::size_type &occurs)

整个时候find_char只能接受string对象,下面这种调用编译器会报错
find_char("Hello World", 'o', ctr);
如果其他函数的形参定义成常量引用在调用find_char函数时也会报错

bool is_sentence(const string &s) {
    string::size_type ctr = 0;
    return find_char(s, '.', ctr) == s.size() - 1 && ctr == 1;
}

6.2.4数组形参

数组的两个特殊性质使得在定义和使用作用在数组上的函数有影响,两个性质是:不允许拷贝数组和使用数组时通常会将其转换成指针。因为不能拷贝数组所以我们无法以值传递的方式使用数组参数,数组会被转换成指针当我们为函数传递一个参数时传递的是指针。
可以把形参写成类似数组的形式,以下三种写法等价:

void print(const int *);
void print(const int []);
void print(const int [10]); //这里面的维度只能说明期望数组含有多少个元素,实际上不一定

以上三种写法都是const int*类型,调用时编译器只检查传递的函数是否是const int*类型

int i = 0, j[2] = {0, 1};
print(&i);  //正确,&i的类型是int *
print(j);   //正确,j转换成int *并指向j[0]

因为数组是以指针的形式传递给形参的,所以一开始函数并不知道数组的尺寸,可以通过一下三种方式获得数组的尺寸:
使用标记指定数组长度
数组本身含有结束标记,比如C风格的字符串

void print(const char *cp) {
    if (cp) {
        while(*cp)
            cout << *cp++;
    }
}

使用标准库规范
传递数组首元素和尾后元素的指针,可以按照如下方法进行:

void print(const int *beg, const int *end) {
    while(beg != end)
        cout << *beg++ << endl;
}

显示传递一个表示数组大小的实参

void print(const int a[], size_t size) {
    for (size_t i = 0; i != size; ++i)
        cout << a[i] << endl;
}

数组引用形参

形参可以是数组的引用,引用形参绑定到对应的实参上,绑定到数组上

//形参是数组的引用,维度是类型的一部分
void print(int (&arr)[10]) {
    for (auto elem : arr)
        cout << elem << endl;
}

f(int &arr[10]);    //错误,arr声明成了引用数组
f(int (&arr)[10]);  //正确,arr是具有10个整数的整型数组的引用

数组引用作为实参要求实参的大小必须和形参相同

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);   //正确

传递多维数组

多维数组可以理解成是一维数组,其中每个元素又是一个数组,即数组的数组。和一维数组一样传递的数组名实际上是传递数组首元素的地址,化成传递多维数组,就要想办法传递多为数组中各个数组的首元素地址。可以用传递指向数组的指针来实现

//martix指向数组的首元素,该数组的元素是由10个整数构成的数组
void print(int (*martix)[10], int rowsize){ /*.....*/ }
void print(int martix[][10], int rowSize) { /*.....*/ }

int *martix[10];    //从右向左都,一个大小为10的数组,然后数组名是一个指针,类型是Int,这是一个由10个Int型的指针构成的数组
int (*martix)[10];  //从括号向外看,一个指针,指针指向一个大小为10的数组,数组中数据类型为int,指向含有10个整数的数组的指针

6.2.5main:处理命令行选项

int main(int argc, char const *argv[])
{
    
    return 0;
}

编译器有时候需要给main传递实参,用户通过设定一组选项来确定函数所要执行的操作,例如可以向程序传递下面的选项:
prog -d -o offile data0
命令行通过两个argcchar const *argv[],其中argv是指向C风格字符串的指针,argc表示数组中字符串的数量
所以通过以上命令行可知argc = 5

argv[0] = "prog";
argv[1] = "-d";
argv[2] = "-o";
argv[3] = "offile";
argv[4] = "data0";
argv[5] = 0;

6.2.6含有可变形参的函数

C++11提供了两种办法用来处理含有可变形参的函数:

  1. 如果所有实参的类型相同,可以传递一个名为initializer_list的标准库类型
  2. 如果实参类型不同可以编写可变参数模板

initializer_list
initializer_list所存在的标准库在其同名的头文件中,支持的操作如下

函数名 功能
initalizer_list<T> lst; 默认初始化,T类型元素的空列表
initalizer_list<T> lst{a, b, c...} lst的元素数量和初始值一样多,初始值的类型都是const
lst2(lst)\ lst2 = lst 拷贝或者赋值
lst.size() 列表中元素的数量
lst.begin() 返回指向lst首元素的指针
lst.end() 返回指向lst尾元素下一个位置的指针

initalizer_list中的元素永远都是常量值不能改变

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

如果想向initalizer_list形参中传递一个值的序列,必须把序列放在一对花括号内

//expected和actual都是string对象
if (expected != actual)
    error_msg({"functionX", expected, actual});
else
    error_msg({"functionX", "okay"});

6.3返回类型和return语句

6.3.1无返回值函数

没有返回值的return语句只能用在返回类型是void的函数,这类函数最后一句后面会隐式的加上return;语句.

6.3.2有返回值函数

值是如何被返回的
返回一个值的方式和初始化一个变量和形参的方式一样:返回值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。

string make_plural(size_t ctr, const string &word, const string &ending) {
    return (ctr > 1) ? word + ending : word;
}

返回类型是string,返回值将被拷贝到调用点,函数返回的是word的副本或者一个未命名的临时string对象,对象的内容是word+ending

//选出两个字符串中较短的那个返回其引用
const string &shorterString(const string &s1, const string &s2) {
    return s1.size() <= s2.size() ? s1 : s2;
}

和其他引用类型一样,函数返回的引用是其所引用对象的别名,如以上例子所示不管是调用函数还是返回结果都不会真正拷贝string对象

不要返回局部对象的引用或指针
函数完成后所占用的内存空间随之释放掉,因此函数终止意味着局部变量的引用将不再指向有效的内存区域

const string &mainp() {
  string ret;
  if (!ret.empty())
    return ret; //错误,返回局部对象引用
  else
    return "Empty"; //错误,"Emtpy"是一个局部变量
}

//编译器输出的错误信息
//   In function ‘const string &mainp()’: case.cc : 9 : 10 : warning
//       : reference to local
//   variable ‘ret’ returned[-Wreturn - local - addr] string ret;
//   ^~~case.cc : 13 : 12 : warning
//       : returning reference to
//             temporary[-Wreturn - local - addr] return "Empty";

引用返回左值
调用一个返回引用的函数得到左值,其他类型返回右值,可以为返回类型是非常量的引用的函数的结果赋值。

#include <iostream>
#include <string>

using std::cin;
using std::cout;
using std::endl;
using std::string;

char &get_val(string &str, string::size_type ix) { return str[ix]; }

int main(int argc, char const *argv[]) {
  string s("a value");
  cout << s << endl;
  get_val(s, 0) = 'A';
  cout << s << endl;
  return 0;
}
// a value
// A value

列表初始化返回值
C++11新标准函数可以返回花括号包围的值的列表

vector<string> process() {
    //...
    //expected 和 actual 是 string对象
    if (expected.empty())
        return {};
    else if (expected == actual)
        return {"functionX", "okay"};
    else 
        return {"functionX", expected, okay};
}

6.3.3返回数组指针

因为数组不能背拷贝,所以函数不能返回数组。函数可以返回数组的指针或引用。可以采用类型别名的方法来代替定义数组的指针或引用

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

声明一个返回数组指针的函数
声明func时不使用类型别名,必须牢记被定义的名字后面数组的维度
返回数组指针的函数形式:
Tpye (*function(parameter_list) [dimension])
Type 表示元素的类型,dimension表示数组的大小
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类型

使用尾置返回类型
C++11中使用尾置返回类型。任何函数类型都可以使用尾置返回类型,但是对于返回类型比较复杂的函数最有效,比如返回类型是数组的指针或引用。尾置返回类型跟在形参列表后面并以一个->符号开头,为了表示函数真正的返回类型跟在形参列表之后可以在本应该出现返回类型的地方放置auto

//func接受一个int类型的实参,返回一个指针,指针指向含有10个整数的数组
auto func(int i) -> int (*)[10];

采用尾置返回类型表述更直观

使用decltype
如果我们知道函数返回的指针指向哪个数组可以使用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
print(j, end(j) - begin(j));    //调用void print(const int ia[], size_t size)
print(begin(j), end(j));

定义重载函数

Record lookup(const Account&);  //根据Account查找记录
Record lookup(const Phone&);    //根据PHone查找记录
Record lookup(const Name&); //根据Name查找记录
Account acct;
Phone phone;
Record r1 = lookup(acct);   //调用接受acct的版本
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形参和没有顶层const的形参无法区分

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

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

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

//一下四个函数都是不同的
Record lookup(Account&);
Record lookup(const Account&);
Record lookup(Account*);
Record lookup(const Account*);

const_cast和重载
const_cast只能改变运算对象的底层const,只能用来改变表达式的常量属性

const char *pc;
char *p = const_cast<char*>(pc);
const string &shorterString(const string &s1, const string &s2) {
    return s1.size() <= s2.size() ? s1 : s2;
}

string &shorterString(string &s1, string &s2) {
    //将表达式转换成常量属性调用const shorterString
    auto &r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2));
    //r的类型是一个常量引用
    return const_cast<string&>(r);  //类型转换成非常量引用
}

6.5特殊语言特性

6.5.1默认实参

在函数调用中它们都被赋予一个相同的值,此时我们把这个反复出现的值称为函数的默认实参

typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char background = ' ');

一旦某个形参被赋予默认值,它后面的所有形参都必须具有默认值

使用默认实参调用函数

string window;
window = screen();  //等价于screen(24, 80, ' ')
window = screen(66);    //等价于screen(66, 80, ' ')
window = screen(66, 256);   //screen(66, 256, ' ')
window = screen(66, 256, '#')

6.5.2内联函数和constexpr函数

内联函数可避免函数调用时的开销
将函数定义成内联函数,在该函数的调用点上内联的展开从而避免不必要的开销,例如:
cout << shorterString(s1, s2) << endl;
在编译过程中展开成如下形式:
cout <<(s1.size() < s2.size() ? s1 : s2>) << endl;
函数前面加上inline关键字就可以定义成内联函数

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

constexpr函数

constexpr函数是指能用于常量表达式的函数,函数的返回类型及所有形参的类型都是字面值类型,而且函数体必须只有一条return语句
constexpr int new_sz() { return 42; }
constexpr int foo = new_sz();
允许constexpr函数的返回值并非一个常量

//如果arg时常量表达式,则scale(arg)也是常量表达式
constexpr size_t scale(size_t cnt) { return new_sz() * cnt };

当scale的实参是常量表达式时,它的返回值也是常量表达式

int arr[scale(2)];  //正确,scale(2)是常量表达式
int i = 2; 
int a2[scale(i)];   //错误,scale(i)不是常量表达式

内联函数和constexpr函数一般定义在头文件中

6.5.3调试帮助

NDEBUG预处理变量
默认状况下程序中是没有定义NDEBUG,可以使用一个#define语句定义过NDEBUG关闭调试状态。也可以使用命令行来定义预处理变量:
cc -d D NDEBUG main.cc
这条命令的作用等价于在main.cc的文件的一开始写#define NDEBUG

可以使用NDEBUG编写自己的条件调试代码。如果NDEGBUG未定义,将执行#ifndef和#endif之间的代码,如果定义了NDEGBUG,这些代码将被忽略掉

void print(const int a[], size_t type) {
#ifndef NDEGBUG
  std::cerr << __func__ << ":array size is " << type << endl;

#endif  // NDEGBUG

  std::cerr << ":the location is " << __FILE__ << endl;
}

__func__输出当前调试的函数名字
__FILE__输出存放文件名的字符串字面值
__LINE__存放当前行号的字符串字面值
__TIME__存放文件编译时间的字符串字面值
__DATE__存放文件编译日期的字符串字面值

6.7函数指针

函数指针指向的是函数,函数指针由它的返回类型和形参类型共同决定和函数名无关。

bool lengthCompare(const string &, const string &);

//pf指向一个函数
bool (*pf)(const string &, const string &);

函数指针的理解方法:先看声明的名字,(*pf)声明了一个指针,右侧是形参列表表明pf指向的是函数,返回的类型是Bool,所以pf是一个指向函数的指针,返回值为bool类型,如果去掉括号就是指针的函数

使用函数的指针
把函数指针的函数名作为一个值来使用,函数自动的转化为指针。

pf = lengthCompare;     //pf指向名为lengthCompare的函数
pf = &lengthCompare;    //等价赋值语句,&可选

bool b1 = pf("hello","world");  //调用lengthCompare
bool b2 = (*pf)("hello","world");   //等价调用
bool b3 = lengthCompare("hello", "world");  //等价调用

string::size_type sumLength(const string&, const string &);
bool cstringCompare(const char*, const char*);
pf = 0; //指向空指针
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;//错误,没有形参列表与之匹配
double (*pf3)(int *) = ff;  //没有返回值类型与之匹配

函数指针形参
和数组类似,不能定义函数类型的形参,但是形参可以是指向函数的指针

//第三个形参为函数类型会自动转化成函数指针
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);   //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) *FuncP2;    //等价类型

void useBigger(const string &s1, const string &s2, 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); //正确,PF是指向函数的指针,f1返回指向函数的指针
F f1(int);  //错误,F是函数,f1无法返回函数
F *f1(int); //正确

//直接声明
int (*f1(int)) (int *, int);
//采用尾置的形式
auto f1(int) -> int (*)(int *, int);

posted on 2021-05-31 14:24  翔鸽  阅读(99)  评论(0编辑  收藏  举报