c++primer笔记六、函数

6.1函数基础

一个典型的函数定义包括:返回类型、函数名字、0个或多个形参组成的列表、函数体。
函数执行的操作在语句块中说明,成为函数体。
通过圆括号()调用运算符来执行。

局部静态对象:定义成static
在函数调用时,只初始化一次,函数结束仍然有效。

函数声明

函数只能定义一次,但是可以声明多次。
函数声明无需函数体,用分号替代。
函数的三要素(返回类型,函数名,形参类型)描述了函数的接口,说明了调用该函数所需的全部信息。函数声明也称函数原型。

建议在头文件中声明,在源文件中定义。

分离式编译

允许把程序分割到几个文件,每个文件独立编译。

factMain调用fact,生产可执行文件
编译的过程如下:
$ CC factMain.cc fact.cc #generates factMain.exe
$ CC factMain.cc fact.cc -o #generates main.exe

如果修改了其中一个源文件,只需要重新编译改动的文件。
分离式编译产生一个.obj或.o文件,含义是该文件包含对象代码。

接下来编译器负责把对象文件链接在一起形成可执行文件
实际编译过程:

$ CC -c factMain.cc             #generates factMain.o
$ CC -c fact.cc                 #generates fact.o
$ CC factMain.o fact.o          #generates factMain.exe or a.out
$ CC factMain.o fact.o -o main  #geneartes main or main.exe

6.2参数传递

每次调用函数都会重新创建形参,并用传入的实参对形参进行初始化,即拷贝。
如果形参是引用类型,则实参被引用传递,函数被传引用调用。
如果形参是实参的拷贝,则成为值船体,函数被传值调用。

指针形参

执行指针拷贝操作时,拷贝的时指针的值,拷贝之后两个指针时不同的。

void reset(int *ip)
{
    *ip = 0;        //改变ip所指对象的值
    ip = 0;         //改变了ip的局部拷贝,实参未改变。
}

int i = 42;
reset(&i);      //i = 0;改变的是i的值,不会改变i的地址
传引用参数
void reset(int &i)
{
    i = 0;      //改变了i所引对象的值
}
int j = 42;
reset (j);      // j = 0;

拷贝大的类型或容器比较低效,有的类型(IO)还不支持拷贝,函数只能引用形参访问改类型对象。
如果函数要返回多个值,一个好方法就是给函数一个额外的引用参数,令其保存结果。

const形参和实参

首先要注意顶层const的使用

const int ci = 42;      //不能改变ci,const是顶层的
int i = ci;             //拷贝时可以忽略顶层const
int * const p = &i;     //const是顶层的,p无法再赋值
*p = 0;                 //通过p改变对象内容是允许的,i = 0

当用实参初始化形参时会忽略顶层const。
如果形参有const,传入的常量对象或者非常量对象都是可以的。

指针或引用参与const
void reset(int &i)
{
    i = 0;      //改变了i所引对象的值
}

int i = 0;
const int ci = i;
string::size_type ctr = 0;
reset(&i);          //可以
reset(&ci);         //错误,不能指向const
reset(i);           //可以
reset(ci);          //错误
reset(42);          //错误,不能把普通字面值作为引用
reset(ctr);         //错误,类型不匹配
尽量使用常量引用

如果把引用不设置未常量,容易误导人以为可以修改实参。
同时会限制参数,因此如果不需要修改就使用常量引用。

数组形参

数组有两个性质:1、不允许拷贝。2、使用数组时会转换成指针。
因此无法值传递使用数组参数,只能用类似形式。

//三个函数等价,每个函数都有const int*类型的形参
void print(const int*)
void print(const int[]])
void print(const int[10]])  //这里的维度表示期望含有的元素,实际不一定

调用时

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

因此一开始不知道数组的确切尺寸

1、使用标记指定数组长度

管理数组实参的一种方法是要求数组本身包含一个结束标记,如C风格字符串,最后有一个空字符表示结束。

适用于有明显结束标记的数组

void print(const char *cp)
{
    if(cp)              //CP不是空指针
        while(*cp)      //只有指针所指不是空字符
            count << *cp++  //打印
}
2、使用标准库规范

传递指向数组首元素和伪元素的指针。

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

调用时传入两个指针
int j [2] = {0,1};
print(begin(j),end(j));
3、显式传递数组大小的参数

实参专门定义一个数组大小的形参
只要size不超过实际数组大小,就是安全的。

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;
}

函数体只能作用于大小为10的数组
传递多维数组

处理的是数组的数组,首元素本身是一个数组,指针是一个指向数字的指针。

void print(int (*matrix)[10], int rowSize)

等价定义
void print(int matrix[][10], int rowSize)
main:处理命令行选项

有时需要给main传递实参,一种常见的情况是用户通过设置一组选项来确定函数要执行的操作。
例如:假定main函数位于可执行文件prog内,我们可以向程序传递下面的选项:

prog -d -o ofile data0

命令行通过两个(可选的)形参传递给main函数

int main(int argc, char *argv[])

第一个形参表示数组中字符串的数量。
第二个形参是一个数组,它的元素是指向C风格字符串的指针。

也可以定义成
int main(int argc, char **argv0)
其中argv指向char*  

当实参传递给main后,argv的第一个元素指向程序的名字或者一个空字符串。接下来的元素一次传递命令行提供的实参。最后一个指针之后的元素值保证为0.

上述命令行为例
argv[0] = "prog";
argv[1] = "-d";
argv[2] = "-o";
argv[3] = "ofile";
argv[4] = "data";
argv[5] = 0;
含有可变形参的函数

为了编写能处理不同数量实参的函数,C++ 11有两种主要方法:
1、如果所有实参类型相同,可以传递一个名为initializer_list的标准库函数。
2、如果实参类型不同,可以编写一种特殊函数,即可变参数模板。

C++还有一种特殊的形参类型(省略符),可以传递可变数量的实参。
这种功能一般只用于与C函数交互的接口程序。

initializer_list形参

表示某种特定类型的值的数组。
提供的操作有:

initializer_list<T> lst //默认初始化;T类型元素的空列表
initializer_list<T> list{a,b,c}//lst的元素数量和初始值一样多;lst的元素是对应初始值的副本;列表中的元素是const
lst2(lst) ;// 拷贝或赋值一个initializer_list对象不会拷贝列表中的元素;
lst2 = lst; //拷贝后,原始列表和副本共享元素
lst.size(); //数量
lst.begin();    //首元素指针
lst.end();  //尾元素下一个位置的指针。

和vector一样,initializer_list也是模板类型,需要说明所含元素类型。不过initializer_list对象中的元素永远是常量,无法修改。

initializer_list<int> li;

如下编写输出错误信息的函数,使其可以作用于可变函数的实参:

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

如果想向initializer_list形参中传递一个值,要用花括号。

error_msg({"functionX", expected, actual})

含有initializer_list形参的函数也可以同时拥有其他形参。

省略符形参

是为了c++程序访问某些特殊的C代码设置的,这些代码使用了varargs的标准库。
通常省略符形参不应用其他目的。
只能出现在形参列表最后一个位置

void foo(parm_list, ...);

6.3、返回类型和return语句

无返回值函数

只能用在void函数中。void的函数最有一句后面会隐式地执行return。
一般用法是中间位置提前退出,类似break。

有返回值函数

只要函数类型不是void,则必须返回一个对应的值。
函数返回的是一个拷贝的临时对象。
函数也可以返回引用

const string &shorterString(const string &s1. const string &s2)
{
    retrun s1.size() <= s2.size() ? s1 : s2;
}
不要返回局部对象的引用

函数完成后,所占用的存储空间就释放了,因此局部变量的引用就不在有效。

引用返回左值

调用一个返回引用的函数得到左值,其他返回类型得到右值。

char &get_val(string &str, string::size_type ix)
{
    return str[ix];
}
get_val(s, 0) = 'A';

如果返回的是常量引用就不能赋值。

列表初始化返回值

c++11允许返回花括号包围的值的列表。

vector<string> process()
{
    //expected和actual是string
    if(expected.empty())
        return{};
    else if (expected == actual)
        return {"functionX", "okay"};
    else
        return {"functionX", expected, actual}
}
主函数main的返回值

允许main函数没有return(有一个隐藏的retrun 0)
main函数的返回值看作状态指示器,返回0表示成功,其他表示失败。
为了使返回值与机器无关,cstdlib头文件定义了两个预处理变量。

int main()
{
    if(some_failure)
        return EXIT_FAILURE;
    else
        return EXIT_SUCCESS;
}
递归

函数调用自身就是递归。
递归函数必须有一个路径是不包含递归调用的,否则就会死循环。

返回数组指针

数组不能拷贝,因此函数不能返回数组,只能返回指针或引用。

声明一个返回数组指针的函数
int (*func(int i))[10];
使用尾置返回类型

c++简化方法,使用尾置返回类型
fun函数返回指针,指针指向了10个整数的数组

auto func(int i) -> int(*)[10]
使用decltype

如果知道函数返回的指针将指向哪个数组,就可以使用decltype来声明返回类型。

int odd[] = {1,3,5,7,9};
int even[] = {0,2,4,6,8};
decltype(odd) *arrPtr(int i)
{
    return (i % 2) ? &odd : &even;
}

arrPtr使用decltype表示它的返回类型是指针,并且所指对象与odd类型一致。因为odd是数组,所以arrPtr返回一个指向含有5个整数的数组的指针。

6、4函数重载

统一作用域内的几个函数名字相同但是形参列表不同,称之为重载。

有顶层const的形参和没有的无法区分

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

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

Record lookup(Phone*);  //函数作于指向Phone的指针
Record lookup(const Phone*);    //函数作用于指向常量的指针   
const_cast和重载

当函数的实参不是常量时,得到的结果是一个普通的引用,就可以使用const_cast

string &shorterString(string &s1, string &s2)
{
    auto &r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2));
    return const_cast<string&>(r);
}

先把实参强制转成const的引用,然后调用函数,返回对const string的引用,再将其转换回一个普通的string&,是安全的。

调用重载的函数

把函数调用与一组重载函数中的某一个关联起来,叫函数匹配,也叫重载确定。
调用重载函数有3种可能的结果:
1、找到一个最佳匹配的函数
2、找不到任何一个匹配的,编译器发出无匹配的错误
3、有多于一个函数可以匹配也会发生错误,称为二义性调用

重载和作用域

函数只会调用最近的作用域中的重载函数。

6、5特殊用途语言特性

默认实参

定义成如下

string screen(sz ht = 24; sz wid = 80; char backgrnd = ' ')

一旦每个形参被赋予了初值,剩下的都必须有默认值。
实际调用时只能省略尾部的实参。

window = screen(66)
window = screen('?')        /实际调用screen('?', 80,' ')
默认实参声明

通常放在头文件,且一个函数只声明一次。
每个形参只能被赋予一次默认值,如果后续声明为了添加默认实参,也是允许的。

默认实参初始值

局部变量不能作为默认实参,除此之外只要表达式的类型能转换成形参所需要的类型,该表达式就能作为默认实参。

sz wd = 80;
char def = '';
sz ht();
string screen(sz = ht(); sz = wd; char = def);

如果:
void f2()
{
    def = '*;           //更新了传递的值
    sz wd = 100;        //隐藏了外层的wd,但与传递的wd无关
    window = screen();  //调用screen(ht(), 80, '*');
}
内联函数和constexpr

调用函数有一系列工作,会比较慢

内联函数可以避免函数调用的开销

在函数返回类型前加上inline,声明成内联函数

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

一般用于优化规模较小,流程直接,频繁调用的函数。很多编译器不支持内联递归函数。

constexpr函数

指能用于常量表达式的函数。
定义时候的约定:
1、函数的返回类型及所有形参类型都是字面值类型
2、函数体中必须有且只有一个return

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

在编译中直接替换成结果值,constexpr函数被隐式地指定为内联函数。
constexpr函数体内也可以包含其他语句,只要这些语句在运行时不执行任何操作。

把内联函数和constexpr函数放在头文件

内联函数和constexpr函数可以多次定义,但是它的多个定义必须完全一致。

调试帮助

程序可以包含一下用于调试的代码,在发布时要屏蔽,用到两个预处理功能。assert和NDEBUG

assert

是一种预处理宏,即一个预处理变量。使用一个表达式作为条件

assert(expr);

如果表达式为假,assert输出信息并终止程序;否则什么也不做。
定义在cassert头文件中,名字由预处理器管理,因此不需要提供using声明。
常常用于检查“不能发生”的条件。

NDEBUG

assert的行为依赖于NDEBUG的预处理变量的状态。如果定义了NDEBUG,则assert什么也不做。默认状态下没定义。

可以使用#define定义NDEBUG,从而关闭调试状态。
很多编译器都提供了一个命令行选项是我们可以预处理变量

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

这条命令等价于在开头写#define NDEBUG

还可以使用NDEBUG编写自己的调试代码,如果NDEBUG没定义,将指向#ifndef和#endif之间的代码

void print(const int io[], sizt_t size)
{
#ifndef NDEBUG
    cerr << __func__ << ": array size is" << size << endl;
#endif
}

__func__输出当前函数的名字。
编译器为每个函数都定义了一些名字

__FILE__        //存放文件名的字符串字面值
__LINE__        //存放当前行号的整型字面值
__TIME__        //存放文件编译时间的字符串字面值
__DATE_-        //存放文件编译日期的字符串字面值

6、6函数匹配

有时候重载函数太多,不容易选择。

确定候选函数和可行函数

函数匹配第一步是选本次调用对应的重载函数集。
两个特征:1、同名 2、声明在调用点可见。
第二步考察实参,然后选出可行函数。
两个特征:1、形参与调用提供的实参个数相等。2、类型也相同
第三步:寻找最佳匹配。逐一检查提供的实参类型,寻找最匹配的可行函数。

void f(int);
void f(double, double = 3.14);
f(3.14);        //调用第二个
含有多个形参的函数匹配

如果有且只有一个函数满足下列条件则匹配成功:
1、函数的每个实参匹配都不劣于其他可行函数需要的匹配。
2、至少有一个实参的匹配优于其他可行函数的匹配。
如果没有函数可以,就是错误的,会报错。

void f(int, int);
void f(double, double = 3.14);
f(3, 3.14)      //直接报错,有二义性。
实参类型转换

为了确定最近匹配,编译器将实参类型到形参类型的转换分了几个等级:
1、精确匹配(类型相同、转换成对应指针、添加顶层const或删除顶层const)
2、通过const转换实现的匹配
3、类型提升
4、算数转换或指针转换
5、通过类类型转换

需要算数提升和转换的匹配

如果有两个函数,优先算术提升

void ff(int);
void ff(short);
ff('a');        //提升为int调用int的

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

void manip(long);
void manip(float);
manip(3.14);        //错误,二义性调用
函数匹配和const实参

如果重载函数的区别在于引用类型的形参是否引用了const,或者指针类型的形参是否指向const,那么就会根据实参是否是变量来决定使用哪个。
如果传入const对象,所以唯一可行的函数是常量引用函数。
如果传入普通对象,两个函数都可调用,优先使用非常量版本的函数。

6、7函数指针

指向的是函数而非对象。

bool (*pf)(const string &, const string &);

pf是一个指向函数的指针,该函数的参数是两个const string引用,返回值是bool。

使用函数指针

把函数名作为一个值使用时,函数自动转换成指针

pf = lengthCompare;        //pf指向这个函数
pf = &LengthCompare;    //等价的,取地址符时可选的

调用时可以直接使用,不需要解引用

boo1 b1 = pf("Hello", "goodbye");
boo1 b2 = (*pf)("Hello", "goodbye");
bool b3 = lengthCompare("Hello", "goodbye");
//三者等价

函数指针可以赋值为nullptr或者0,表示没有指向函数。
指向时函数的返回类型,形参类型必须精确匹配。

重载函数的指针

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

void ff(int*);
void ff(unsigned int);
void (*pf1)(unsigned int) = ff;
void (*pf2)(int) = ff;
void (*pf3)(int*) = ff;
函数指针形参

可以直接把函数作为实参使用,自动转换成指针

void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &));
useBigger(s1, s2, lengthCopmare);

直接使用函数指针类型很冗长,用类别别名和decltype可以简化函数指针

typedef bool Func(const string&, const string&);
typedef decltype(lengthCompare) Func2;      //等价类型,是函数类型


typedef bool (*Func2)(const string&, const string&);
typedef decltype(lengthCompare) *Func2;      //等价类型,是指向函数的指针类型

然后可以如下重新声明函数
void useBigger(const string &s1, const string &s2, Func);   //编译器自动将Func表示的函数类型转换成指针。
void useBigger(const string &s1, const string &s2, Func2);
返回指向函数的指针

一种方法是使用别名

using F = int(int*, int);       //F是函数
using PF = int(*)(int* ,int);    //F是指针

PF fl(int);     //正确,PF是指针,fl返回指向函数的指针
F fl(int);      //错误,F是函数,不能返回一个函数
F *fl(int);     //正确,显式返回

完整的如下:
int (*fl(int))(int*, int);
fl是个函数,fl返回一个指针,指针指向的函数返回int。

使用尾置返回
auto fl(int) -> int (*)(int*, int);
将auto和decltype用于函数指针类型

如果明确知道返回的函数是哪一个,就使用decltype简化。
将decltype作用于某个函数时,本身返回函数类型而非指针,因此要加上*

size_type sumLength(const string&, const string&);
decltype(sumLength) * getFun(const string &);
原文地址:https://www.cnblogs.com/aqq2828/p/14073372.html