C++ Primer学习笔记之第六章--函数

来源:互联网 发布:淘宝店招怎么设置全屏 编辑:程序博客网 时间:2024/06/14 03:27

6.1 函数基础

1、一个典型函数的定义包括以下部分
(1)返回类型(return type)
(2)函数的名字
(3)由0个或多个形参(parameter)组成的列表
(4)函数体

2、函数的调用完成两项工作
(1)一:用实参初始化函数对应的形参
(2)二:将控制权转移给被调用函数
(3)此时,主调函数(calling function)执行被暂时中断,被调函数(called function)开始执行。

3、return语句完成两项工作
(1)一:返回return语句中的值(如果有则返回)
(2)二:将控制权从被调函数转回主调函数

6.1 节练习
练习6.4:编写一个与用户交互的函数,要求用户输入一个数字,计算生成该数字的阶乘,在main函数中调用该函数。

#include<iostream>#include<stdlib.h>#include<string>#include<vector>#include<stdexcept>using std::invalid_argument;using std::endl;using std::string;using std::cout;using std::cin;using std::vector;void factorial();int main(void){    //6.1test -->6.4    factorial();    return 0;}void factorial(){    long int fac=1;    long int n;    cout<<"Enter the number: ";    while(cin>>n)    {        try{            if(n<=0)                throw invalid_argument("Number must greater than 0!");        }catch (invalid_argument){            char c;            cout<<"Enter the number again?(y/n): ";            cin>>c;            if(cin||c=='y')            {                factorial();            }            else if(!cin||c=='n')                break;        }        for(;n>0;n--)            fac=fac*n;        cout<<"n!="<<fac<<endl;        fac=1;        cout<<"Enter the number: ";    }}

练习6.5:编写一个函数输出其实参的绝对值:

#include<iostream>#include<stdlib.h>#include<string>#include<vector>#include<stdexcept>using std::invalid_argument;using std::endl;using std::string;using std::cout;using std::cin;using std::vector;void factorial();void absolute();int main(void){    //6.1test -->6.5    absolute();    return 0;}void absolute(){    int n;    cout<<"Enter a number,and output the absolute value: ";    while(cin>>n)    {        if(n>0)            cout<<"The absolute number is: "<<n<<endl;        else            cout<<"The absolute number is: "<<-n<<endl;    cout<<"Enter a number,and output the absolute value: ";         }    cout<<"quit!"<<endl;}

6.1.1 局部对象

1、在C++中,名字有作用于,对象有生命周期(life)。
(1)名字的作用于是程序文本的一部分,名字在其中可见。
(2)对象的生命周期是程序执行过程中该对象存在的一段时间。

2、函数体是一个语句块,块构成一个新的作用域,我们可以在其中定义变量。
(1)形参和函数体内部定义的变量,统称局部变量(local value)
(2)局部变量可隐藏(hide)外层作用域中同名的其他所有声明。

3、自动对象(auto object):只存在于块执行期间的对象。

4、局部静态对象(local static object):在程序的执行路径第一次经过对象定义语句时初始化,并且指导程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。
(1)用关键字static可定义局部静态对象。

6.1.1练习
练习6.7:编写一个函数,当它第一次被调用时返回0,以后每次调用返回值加1

#include<iostream>#include<stdlib.h>#include<string>#include<vector>using std::endl;using std::string;using std::cout;using std::cin;using std::vector;int func_calledtimes();int main(void){    //6.1.1test -->6.7    for(int i=0;i<10;++i)        func_calledtimes();    return 0;}int func_calledtimes(){    static int times=0;    if(times==0)    {        cout<<"The function has been called "<<times<<" times."<<endl;        times++;        return 0;    }    else    {        cout<<"The function has been called "<<times<<" times."<<endl;        return times++;    }}

6.1.2 函数声明

1、函数的三要素:返回类型、函数名、形参类型描述了函数的接口,说明了该函数所需的全部信息。
(1)函数声明也称作为函数原型(function prototype)

2、含有函声明的头文件应该被包含到定义函数的源文件中。

6.1.3 分离式编译

1、大多数编译器提供了分离式编译每个文件的机制,这一过程通常会产生一个后缀名是.obj(Windows)或.o(Unix)的文件。

6.2 参数传递

1、形参初始化机制与变量初始化一样。

2、形参是引用类型时,我们说它对应的实参被引用传递(pass by reference)或函数被传引用调用(called by reference)。

3、当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。

6.2.1 传值参数

1、指针形参可改变实参的值。

2、使用引用避免拷贝
(1)拷贝较大类类型对象或容器比较低效
(2)甚至有的类类型(包括IO类型在内根本不支持拷贝操作)

3、如函数无须改变引用形参的值,最好将其声明为常量引用(const &)

6.2.1 节练习
练习6.10:编写一个函数,使用指针形参交换两个整数的值。

#include<iostream>#include<stdlib.h>#include<string>#include<vector>using std::endl;using std::string;using std::cout;using std::cin;using std::vector;void change(int *,int *);int main(void){    //6.2.1test -->6.10    int a=10,b=13;    cout<<"a="<<a<<endl        <<"b="<<b<<endl;    change(&a,&b);    cout<<"a="<<a<<endl    <<"b="<<b<<endl;    return 0;}void change(int *a1,int *a2){    int tmp=*a1;    *a1=*a2;    *a2=tmp;}

6.2.4 数组形参

1、数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响,这两个性质分别是:
(1)不允许拷贝数组
(2)使用数组时(通常)会将其转换为指针

2、管理数组指针形参的三种方法
(1)使用标记指定数组长度

void print(const char *cp){    if(cp)           //cp不是一个空指针    while(*cp)       //只要指针所指的字符不是空字符    cout<<*cp<<endl;}

(2)使用标准库规范

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

(3)显示传递一个表示数组大小的形参

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

(4)传递多维数组

void print(int (*matrix)[10],int rowSize){    /*..........*/}

6.2.5 main:处理命令行选项

1、main函数可以定义为:

int main(int argc,char *argv[]){}//等价定义int main(int argc,char **argv){}(1)argc是指命令行输入参数的个数(2)argv存在所有的命令行参数

6.2.6 含有可变形参的函数

1、initializer_list形参
(1)如果函数的实参数量未知但全部实参的类型都相同,我们可以使用initializer_list类型的形参。

(2)initializer_list是一个标准库类型(定义在同名的头文件中),用于表示某种特定类型的值的数组。

2、initializer_list提供的操作

1)initializer_list<T> lst; //默认初始化,T类型元素的空列表(2)initializer_list<T>{a,b,c,d...};//lst的元素数量和初始值一样名,lst的元素是对应初始值的副本,列表的元素是常量。(3)lst2(lst);    lst2=lst;           //拷贝赋值一个initializer_list对象不会拷贝列表中的元素,原始列表和副本列表共享元素(4)lst.size();              //列表中的元素数量(5)lst.begin();         //返回指向lst中首元素的指针(6)lst.end();               //返回指向lst中尾元素下一位置的指针

3、initializer_list和vector是模版类型。
(1)与vector不同,initializer_list对象中的元素永远是常量,无法改变initalizer_list对象中元素的值。

4、例:

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

6.2.6 节练习
练习6.27:编写一个函数,它的参数是initializer_list类型的对象,函数的功能是计算列表中所有元素的和

#include<iostream>#include<stdlib.h>#include<string>#include<vector>//#include "fuc.c"#include<initializer_list>#include<stdexcept>using std::initializer_list;using std::invalid_argument;using std::endl;using std::string;using std::cout;using std::cin;using std::vector;void sum(initializer_list<int>); int main(void){    initializer_list<int> li{1,2,3,4,5,6,7,8,9,10};    sum(li);    return 0;}void sum(initializer_list<int> a){    int sum=0;    for(auto il=a.begin();il!=a.end();++il)        sum+=*il;    cout<<"sum="<<sum<<endl;}

6.3 返回类型和return语句

1、return语句形式:

1return;(2return expression;

2、返回一个值的方式和初始化变量和形参的方式完全一样;
返回的值用于初始化调用点的临时量,该临时量是函数调用的结果

3、不要返回局部对象的引用或指针。

4、返回类类型的函数和调用运算符
(1)如果函数返回指针、引用或类的对象,我们就能使用函数调用的结果访问结果对象的成员。
例:auto sz=shorterString(s1,s2).size();

5、引用返回左值
(1)调用一个返回引用的函数得到左值,其它返回类型得到右值
(2)例:

char & get_val(string &str,string::size_type ix){    return str[ix];}int main(){    string s("a value");    get_value(s,0)='A';}

6、列表初始化
(1)C++11规定,函数可以返回{}包围的值的列表
(2)例:

vector<string> process(){    return {"abc","def"};}

6.3.3 返回数组指针

1、返回数组指针的函数形式:
(1)

Type (*function(parameter_list))[dimetion]1、Type表示数组元素的类型2、dimension表示数组的大小3、(*function(parameter_list))最外层两端的括号必须存在。

(2)例: int (*func(int i))[10];
<1>逐层理解其含义:
①func(int i):表示调用func函数时需要一个int类型的实参。
②(*func(int i)):意味着我们可以对函数调用结果执行解引用操作
③(*func(int i))[10]:表示解引用func的调用将得到一个大小是10的数组
④int (*func(int i))[10]:表示数组中的元素是int类型

2、使用尾置返回类型
(1)C++11中有一种建华上述声明的方法,就是使用尾置返回类型(trailing return type)

(2)任何函数的定义都能使用尾置返回,但这种形式对于返回类型比较复杂的函数最有效。
如:返回类型是数组的指针或数组的引用,函数指针等。

(3)尾置返回类型跟在形参列表后并以一个->符号开头

(4)例:func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组

auto func(int i)->int(*)[10];

3、使用decltype
(1)例:

int odd[]={1,3,5,7,9};int even[]={0,2,4,6,8};decltype(odd) *arrPtr(int i){    return (i%2)?&odd:&even;           //返回一个指向数组的指针。}

(2)arrPtr使用关键字decltype表示它返回一个指针,并且该指针所指的对象与odd类型一致,类型含有5个整型的数组。
注意:decltype并不负责把数组类型转换为指向数组的指针,所以必须在函数声明时加一个符号*

6.3.3 节练习
练习6.36:编写一个程序,使其返回数组的引用并且该数组包含10个string对象。不要使用尾置返回类型,decltype或者类型别名。

#include<iostream>#include<stdlib.h>#include<string>#include<vector>#include<initializer_list>#include<stdexcept>using std::initializer_list;using std::invalid_argument;using std::endl;using std::string;using std::cout;using std::cin;using std::vector;string (& return_str_arr(string (* str)[10] ) ) [10];int main(void){    //6.3.3test -->6.36    string st[10]={"Hello","Arch","CPrimer"};    string * stp=st;    stp=return_str_arr(&st);    cout<<"st type is string [10]:"<<endl;    for(size_t j=0;j<10;j++)        cout<<st[j]<<endl;    cout<<"stp type is also string [10]:"<<endl;    for(size_t j=0;j<10;j++)        cout<<stp[j]<<endl;    return 0;}string (& return_str_arr(string (*str)[10]) ) [10]{    for(size_t i=0;i<10;++i)        (*str)[i]="Hey.";    return *str;}

练习6.37:为6.36的函数再写三个声明,一个使用类型别名,一个使用尾置返回类型,最后一个使用decltype关键字

//(1)使用类型别名版本:using str_arr_ref=string(&)[10];//等价定义于typedef string(&str_arr_ref)[10] ;str_arr_ref return_str_arr(string (*str)[10]){    for(size_t i=0;i<10;++i)        (*str)[i]="Hey.";    return *str;}//(2)使用尾置返回类型//声明部分为:auto return_str_arr(string (*str)[10])->string(&)[10];//定义部分:auto return_str_arr(string (*str)[10])->string(&)[10]{    for(size_t i=0;i<10;++i)        (*str)[i]="Hey.";    return *str;}//(3)decltype关键字声明及定义string st[10]={"Hello","Arch","CPrimer"};    decltype(st) & return_str_arr( string (*str)[10] )     {        for(size_t i=0;i<10;++i)            (*str)[i]="Hey.";        return *str;    }

6.4 函数重载

1、如果同一作用域内几个函数的名字相同,但形参列表不同,我们称之为重载(overloaded)函数。

2、main函数不能重载

3、重载和const形参
(1)顶层const不影响传入函数的对象,一个拥有顶层const的形参无法和另外一个没有顶层const的形参区分开来。

(2)例:

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

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

Record lookup(Account &);       //函数作用于Account的引用Record lookup(const Account &); //作用于常量引用(重载函数)Record lookup(Account *);Record lookup(const Account *);//重载函数,作用于指向常量的指针。

4、const_cast和重载
(1)有一个选出最短字符串的函数,该版本为:

const string & shorterString(const string &s1,const string &s2){    return s1.size()<=s2.size()?s1:s2;}//该函数版本即使实参不是常量,但返回仍是const string的引用。

(2)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_cast转换可以去掉底层const,并且这一过程安全。

(3)const_cast版本函数执行流程:将实参强制转换为对const的引用(事实上这个实参强制仍然是绑定这一个普通的引用,而非常量引用),然后调用shorterString函数的const版本。const版本返回const string的引用(事实上该引用为普通引用)。然后被强制转换为普通引用。

5、调用重载函数
(1)函数匹配(function match):把函数调用与一组重载函数中的某一个关联起来的过程。
函数匹配也叫做重载确定(overload resolution)

(2)调用重载函数时有三种可能的结果
<1>编译器找到一个实参最佳匹配(best match)的函数,并生成调用该函数的代码。
<2>找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配(no match)的错误。
<3>有多余一个函数可以匹配,但每一个都不是最佳选择。此时也将发生错误,称为二义性调用(ambiguous call)

6.4.1 重载与作用域

1、重载对作用于的一般性质并没有什么影响。但在内层作用于中声明名字,它将隐藏外层作用于中同名的实体。

2、在不同作用域中无法重载函数名。

3、在C++中,名字查找发生在类型检查之前。

6.5 特殊用途语言特性

6.5.1 默认实参

1、在函数的很多调用它们都被赋予一个相同的值,我们把这个反复出现的值称为函数的默认实参(default argument)

2、例:

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

6.5.2 内联函数和constexpr函数

1、内敛函数可比main函数调用的花销
(1)将函数指定为内敛函数(inline),通常是将它在每个调用点“内联地”展开。

(2)例:

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

2、内联说明只是想编译器发出一个请求,编译器可以选择忽略这个请求。

3、一般来说,内联几只用于优化规模较小,流程直接,调用频繁的函数。


4、constexpr函数
(1)constexpr函数是指能用于常量表达式的函数。

5、constexpr函数遵循几项约定:
(1)函数返回类型所有形参的类型都得是字面值类型

(2)并且函数体中必须有且只有一条return语句

(3)例:

constexpr int new_sz(){return 42;};constexpr int foo=new_sz();        //正确,foo是一个常量表达式

6、constexpr函数不一定返回常量表达式。

7、把内联函数放在头文件(头文件最后加#ifndef)中
(1)和其它函数一样,内联函数和constexpr函数可以在程序中多次定义。
原因:编译器要想展开函数声明和定义。

6.5.3 调试帮助

1、assert 预处理宏
(1)assert是一种预处理宏(preprocessor marco)
所谓预处理宏其实是一个预处理变量。assert红使用一个条件表达式作为它的条件:

(2)格式

assert(expr);

(3)首先对expr求值,如果表达式为假(0),assert输出信息并终止程序的执行。
若expr为真(非0),assert什么都不做。

(4)assert定义在cassert头文件中。

2、assert宏常用于检查“不能发生的条件”。
例:

assert(word.size()>threshold);

3、NDEBUG预处理变量
(1)定义NDEBUG能不执行运行时检查

4、C++编译器定义的_ _func_ _之外,预处理器还定义了4个对于程序调试很有用的名字:

1、 __FILE__     :存放文件名的字符串字面值2、 __LINE__     :存放当前行号的整型字面值3、 __TIME__     :存放文件编译时间的字符串字面值4、 __DATE__     :存放文件编译日期的字符串字面值

6.5.3 节练习
练习6.47:改写一下程序,使其有条件地输出与执行过程有关的条件。例如,每次调用时输出数组元素的大小。分别在打开和关闭调试器的情况下执行该程序:

修改的程序:int &get(int *array,int index){ return arry[index];}int main(){int ia[10];for(int i=0;i!=10;++i)    get(ia,i)=i;}//启动assert调试:include<iostream>#include"stdlib.h"#include<string>#include<vector>#include<cassert>using namespace std;using std::string;using std::endl;using std::cout;using std::cin;int &get(int *array,int index){ return array[index];}int main(){    int ia[10];    for(int i=0;i!=10;++i)        {            get(ia,i)=i;            assert(ia[i]<5);            cout<<ia[i]<<endl;        }    return 0;}//关闭assert预处理宏,在头文件#include<cassert>前定义NDEBUG预处理变量即可#include<iostream>#include"stdlib.h"#define NDEBUG#include<string>#include<vector>#include<cassert>using namespace std;using std::string;using std::endl;using std::cout;using std::cin;int &get(int *array,int index){ return array[index];}int main(){    int ia[10];    for(int i=0;i!=10;++i)        {            get(ia,i)=i;            assert(ia[i]<5);            cout<<ia[i]<<endl;        }    return 0;}

6.6 函数匹配

1、候选函数(candidate function)有两个特征
(1)与被调用的函数同名
(2)声明在调用点可用

2、从候选函数中选出能被这组实参调用的函数称为可行函数(viable function)

6.6.1 实参类型转换

一、实参类型和形参类型的转换划分为几个等级:

1、精准匹配,包括三种情况:
(1)实参类型和形参类型相同
(2)实参从数组类型或函数类型转换成对应的指针类型
(3)向实参添加顶层const或者从实参中删除顶层const

2、通过const转换实现的匹配

3、通过类型提升实现的匹配(如整型提升)

4、通过算数类型转换或指针转换实现的匹配

5、通过类类型转换实现的匹配

6.7 函数指针

1、函数指针指向的是函数而非对象
函数指针指向某种特定类型
函数的类型由它的返回类型和形参类型共同决定,与函数名无关

(1)例:

bool lengthCompare(const string &,const string &);要想声明一个可以指向该函数的指针,只需用 指针替换函数名 即可bool (*pf)(const string &,const string &);

2、使用函数指针
(1)将leengthCompare的地址赋给pf

1、  pf=lengthCompare;           //pf指向名为lengthCompare的函数,函数名自动转换为指针2、  pf=&lengthCompare;          //等价的赋值语句,取地址符可选

(2)直接使用指向函数的指针调用该函数,无须提前解引用指针:

1bool b1=pf("hello","goodbye");         //调用lengthCompare函数2bool b2=(*pf)("hello","goodbye");      //一个等价调用

3、在指向不同函数类型的指针不存在转换规则。

4、函数指针形参
(1)函数虽然不能定义函数类型的形参,但形参可以是指向函数的指针。
例:

void userBigger(const string & s1,const string &s2,                bool pf(const string &,const string &));//另一个等价声明void userBigger(const string & s1,const string &s2,                bool (*pf)(const string &,const string &));

(2)我们可以把函数名作为实参使用,此时它会自动转换成指针:

useBigger(s1,s2,lengthCompare);1、用typedef定义类型别名和decltype能简化以上使用typedef bool Func(const string &,const string &);typedef decltype(lengthCompare) Func2;      //等价类型//Func和Func2函数类型typedef bool (*FuncP)(const string &,const string &);typedef decltype(lengthCompare) *Func2P;        //等价类型//FuncP和Func2P是函数指针类型注意:decltype()返回的是函数类型,需加*才能得到指针。//useBigger的等价声明void useBigger(const string &,const string &,Func);     //编译器自动将Func函数名转换为指向函数的指针。void useBigger(const string &,const string &,Func2P);

5、返回指向函数的指针
(1)函数虽然不能返回函数类型,但能返回指向函数类型的指针

(2)编译器不会自动地将函数返回类型当成对应指针类型处理,所以我们必须把返回类型写成指针形式。

(3)声明一个返回函数指针的函数,最简单的变法是使用类型别名

1using F=int(int *,int);           //F为函数类型   using PF=int(*)(int *,int);      //PF为函数指针类型2、  PF f1(int);         //正确,PF指向函数的指针。    F f1(int);          //错误,F是函数类型,f1不能返回一个函数    F *f1(int)          //正确:显示地指定返回类型是指向函数的指针    //等价于    int (*f1(int)) (int *,int );3、用尾置返回类型的方式声明一个返回函数指针的函数    auto f1(int)->int (*) (int*,int);  

6、可将auto和decltype用于函数指针类型
例:

string::size_type sumLength(const string &,const string &);decltype(sumLength) *getFcn(const string &);


6.7 节练习
练习 6.54:编写函数的声明,令其接受两个int形参并且返回类型也是int;然后声明一个vector对象,令其元素是指向该函数的指针。

int funcint(int a=1,int b=2){    return a+b;}typedef int (*funcp) (int,int);int main(){    std::vector<funcp> vfucp;    funcp first=funcint;    vfucp.push_back(first);    //int a=(*vfucp[0])(1,23);      int a=vfucp[0](1,23);    cout<<a<<endl;    return 0;}

练习6.55:编写4个函数,分别对两个int值执行+、-、*、/运算,在vector对象中保存这些函数指针。

#include<iostream>#include"stdlib.h"#include<string>#include<vector>#include<stdexcept>using namespace std;using std::string;using std::endl;using std::cout;using std::cin;int sum(int a=1,int b=2){    return a+b;}int subtract(int a=1,int b=2){    return a-b;}int multiply(int a=1,int b=2){    return a*b;}int getalong(int a=1,int b=2){    try{        if(b==0)            throw runtime_error("b must not equal to 0!");    }catch(runtime_error err){            cout<<err.what();            cout<<"Exit"<<endl;            return 0;    }    return a/b;}typedef int (*funcp) (int,int);int main(){    std::vector<funcp> vfucp;    vfucp.push_back(sum);    vfucp.push_back(subtract);    vfucp.push_back(multiply);    vfucp.push_back(getalong);    for(auto vfp=vfucp.begin();vfp!=vfucp.end();++vfp)        cout<<(*vfp)(2,0)<<endl;    return 0;}

术语表总结

1、Assert是一个预处理宏,作用于一条表示条件的表达式。当未定义预处理变量NDEBUG时,assert对条件求值,条件为假,输出一条错误信息并终止当前程序的执行。

2、链接(link):是一个变异过程,负责把若干个对象文件连接起来形成可执行程序。

3、尾置返回类型(trailing return type)在参数列表后面指定返回类型。

0 0
原创粉丝点击