c++ primer读书笔记——第6章 函数

第6章 函数

函数的定义与声明(如何传入参数 以及 如何返回结果)
重载函数&函数匹配
函数指针
函数是一个命了名的代码块

6.1 函数基础

函数组成:返回值类型 函数名 形参列表 函数体
调用运算符:形式是一对圆括号
函数调用:一是实参初始化形参,二是控制权转移给被调用函数
return语句:一是返回return语句中的值,二是将控制权归还给主调函数
规定实参数量应与形参数量一致,形参一定会被初始化
实参的类型必须与对应的形参类型匹配,不一定非要完全一致,能够类型转换也可以
函数的形参列表可以为空,但不能省略。
定义一个不带形参的函数,可以书写一个空的形参列表,也可以使用关键字void表示函数没有形参(与C语言兼容)
任意两个形参都不能同名,而且函数最外层作用域的局部变量也不能与函数形参同名
形参名是可选的,是否为形参命名取决于是否在函数体内使用该形参。不管形参命名与否,都需要为其提供实参,即形参一定会被初始化
函数的返回值类型不能是数组类型或者函数类型,但可以是指向数组或者函数的指针

6.1.1 局部对象

c++中,名字有作用域,变量有生命周期
形参和函数体内部定义的变量统称为局部变量
局部变量的生命周期依赖于定义的方式:自动对象、局部静态对象。自动对象始于变量定义语句,终于块末尾;局部静态变量始于变量定义语句,终于程序终止。
内置类型的未初始化的局部变量会产生未定义的值(默认初始化),内置类型的未初始化的局部静态变量初始化为0(值初始化)。
补充:值初始化:如果是内置类型,初始化为0;如果是类类型,执行类默认初始化

6.1.2 函数声明

函数只能定义一次,但可以声明多次。
函数声明与函数定义的区别:函数声明无需函数体,在后面加分号即可。因为声明中无函数体,所以无需写形参的名字,在函数声明中经常省略形参的名字,但加上形参名有助于更好的理解函数的功能。
函数声明又名函数原型,函数三要素(返回值类型、函数名、形参列表)描述了函数的接口,说明该函数所需的全部信息。
变量在头文件中声明,在源文件中定义;
函数在头文件中声明,在源文件中定义。
定义函数的源文件应该把含有函数声明的头文件包含进来,编译器负责验证函数声明和函数定义是否匹配
回顾:
①为了支持分离式编译,在多文件中共享代码,c++将声明与定义区分开。声明使得名字为程序所知,定义负责创建与名字关联的实体。定义只能有一次,但声明可以有多次。
②变量声明与变量定义的区别:变量声明规定了变量的类型与名字。定义除了规定了变量的类型与名字,还为变量申请了内存空间,可能会为变量赋初值。
如果想要声明一个变量而非定义它,可以在前面加extern,不要显式初始化。

6.1.3 分离式编译

分离式编译:允许将程序分割到几个文件中去,每个文件独立编译。只需要重新编译修改了的源文件。
经过分离式编译,把源文件变为对象代码,编译器把对象文件链接在一起形成可执行文件。

6.2 参数传递

每次调用函数都会重新创建它的形参,并用传入的实参对形参进行初始化
形参的类型决定了形参与实参的交互方式:
①形参是引用类型,形参绑定到对应的实参上; ——引用传递
②形参不是引用类型,将实参的值拷贝后赋值给形参。 ——值传递

6.2.1 传值参数

函数对形参做的所有操作不会影响实参。
指针形参:执行指针拷贝操作,两个指针是不同的指针,指向同一个对象。因为指针可以间接访问它所指的对象,所以通过指针可以修改它所指对象的值。
c++中建议用引用类型的形参代替指针。

6.2.2 传引用参数

使用引用形参, 允许函数改变一个或多个实参的值。
使用引用避免拷贝,也支持某些不支持拷贝的类类型的参数传递。
如果无需修改引用形参的值,最好将其设置为对常量的引用
使用引用形参返回额外信息,给函数传入额外的引用实参,使函数实际返回的值不只一次

6.2.3 const形参和实参

当实参初始化形参时会忽略掉顶层const,当形参是顶层const时,传给它常量或者非常量对象都可以。

void fcn(const int i); //fcn能够读取i,但是不能向i写值
void fcn(int i); //由于上一行函数忽略了顶层const,所以两个函数传入的fcn函数参数可以完全一样,因此第二个fcn是错误的

指针或引用形参与const
可以使用一个非常量初始化一个底层const对象,不能用一个底层const初始化一个非常量
回顾:
①关于引用:
引用必须被初始化
大多数情况下, 引用类型要与与之绑定的对象类型严格匹配
引用只能绑定在对象上,不能绑定在字面值或者表达式的计算结果上,所以引用类型的初始值必须是一个对象,但是初始化常量引用时允许以任何值作为初始值。

把函数不会改变的形参定义成普通引用是错误的,同时使用普通引用也会极大地限制函数所能接受的实参类型。

6.2.4 数组形参

数组的特殊性质:不允许拷贝数组;使用数组时通常会转换成指针
因为不能拷贝数组,所以不能以值传递的形式使用数组参数。
因为数组会被转换成指针,当为函数传递一个数组时,实际传递的是指向数组首元素的指针

void print(const int*);
void print(const int[]);
void print(const int[10]);

以上三种形式等价,并且数组的大小对函数的调用没有影响
由于数组是以指针的形式传递的,所以函数并不知道数组的尺寸。可以显式传递一个表示数组大小的形参:

void print(const int[],size_t size);

数组引用形参:

void print(int (&arr)[10]);

&arr两端括号必不可少,如果没有括号,arr是包含10个整型引用的数组
传递多维数组:本质传递指向数组首元素的指针,多维数组首元素也是一个数组。数组第二维的大小(以及后面所有维)都是数组类型的一部分,不能省略。

void print(int (*matrix)[10],int rowSize);  //matrix指向含有10个整数的数组的指针
void print(int matrix[][10],int rowSize); //等价形式,以数组的语法传入参数

同样的,*matrix两端的括号不能少,如果去掉,matrix是包含10个指向整型的指针的数组
以数组的语法传入参数,由于转换成指向首元素的指针,所以只关心首元素的类型,不关心数组中元素的个数,即会忽略数组的第一个维度

6.2.5 main:处理命令行选项

情景:需要给main函数传递实参
命令行通过两个形参传递给main函数:

int main(int argc,char * argv[ ]) {...}

argv是一个数组,数组的每一个元素是指针,指针的类型是指向c风格字符串的指针,指针指向的字符串长度并不一定相等;argc表示数组中字符串的数量
由于

void print(const int*);
void print(const int[]);

等价,所以

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

与等价

6.2.6 含有可变形参的函数

如果所有实参的类型相同,数量不固定,可以传递一个名为initializer_list的标准库类型。initializer_list也是模板类型,其所有元素永远是常量值。使用列表初始化传值给initializer_list。含有initializer_list的函数也可以同时拥有其他形参。
省略符形参:传递可变数量的实参,只用于与c函数交互的接口程序,仅用于c和c++的通用类型,类对象不要通过省略符形参传递,只出现在形参列表的最后一个位置

void foo(param list,...);  //形参声明后的逗号可省略,指定了类型的形参执行正常的类型检查,省略符形参对应的实参无须类型检查
void foo(...);

6.3 返回类型和return语句

6.3.1 无返回值函数
6.3.2 有返回值函数
6.3.3 返回数组指针

数组不能被拷贝,所以数组不能作为实参传入,也不能作为返回值传出。但是数组可以被转化成指向首元素的指针,所以函数可以把数组的指针或者引用作为实参传入,作为返回值传出。
定义一个返回数组的指针或者引用的函数比较麻烦,可以借助类型别名

typedef int arrT[10];  //arrT是含有10个整数的数组
using arrT=int[10];  //等价形式,arrT是含有10个整数的数组
arrT * func(int i);   //func返回一个指向含有10个整数的数组的指针

不借助类型别名,直接声明一个返回数组指针的函数:

Type (* function(parameter_list))[dimension]

还可以使用尾置返回类型(c++11新特性)
将返回值类型跟在形参列表后面并以->开头,在本应该出现返回值类型的地方放置auto

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

如果知道函数返回的指针将指向哪个数组,可以使用decltype关键字。decltype不负责把数组类型转换成对应的指针,所以decltype(数组名)的结果是个数组,还需要在函数名前加*

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

6.4 函数重载

重载函数:函数名称相同,但形参列表不同,对返回值不做要求
回归:重写有什么要求吗?函数名相同,形参列表相同,返回值类型相同
main函数不能重载
由重载函数的要求,可以推断不允许两个函数除了返回类型外其他所有的要素都相同
(反证:如果存在仅返回值类型不同的函数,那么编译器在决定调用哪个函数时根本无法做出判断)
顶层const不影响参数传递(本质是顶层const不影响拷贝),形参有没有顶层const不影响。

void func(int i);
void func(const int i); //以上两种形式无法区分开

底层const会影响参数的传递,形参是否有底层const是有区别的。

void func(int *i);
void func(const int *i); //以上两种形式不同

补充:允许将指向非常量的指针或引用转换成指向常量的指针或引用
不允许将指向常量的指针或引用转换成指向非常量的指针或引用,不能试图删除底层const
const_cast在重载函数的情景中最有用

//比较两个string对象的长度,返回较短的那个引用
const string & shorterString(const string &s1,const string &s2)
{
     return s1.size() <=s2.size() ?s1:s2;
}

如果对两个非常量的string实参调用此函数,希望返回值是一个指向非常量的引用,但调用此函数结果仍是指向常量的引用,该如何改进呢?

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

上面的函数先将实参强制转换成了对const的引用,然后调用了shorterString的const版本,const版本返回对常量的引用,再通过强制转换变成指向非常量的string。
误区:之前以为const_cast函数只能去掉底层const。
正确:const_cast既可以为指向非常量的指针或引用添加底层const,也可以去掉底层const
调用重载函数时可能有三种结果:
①找到与实参最佳匹配的函数,生成调用该函数的代码
②找不到任何一个函数与调用的实参匹配,编译器发出无匹配的错误信息
③有多于一个函数可以匹配,但每一个都不是最佳选择,会发生二义性调用错误

6.4.1 重载与作用域

如果我们在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体。
在不同作用域无法实现函数重载。

string read;
void print(const string &);
void print(double);  //重载print函数
void fooBar(int val)
{
  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)被隐藏掉了
}

6.5 特殊用途语言特性

6.5.1 默认实参

一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。
默认实参负责填补函数调用缺少的尾部实参(靠右侧位置)

6.5.3 调试帮助

功能:类似头文件保护机制,有选择地执行调试代码
assert预处理宏:
形式:assert(expr);
对expr求值,如果表达式为false,assert输出错误信息并终止程序;如果是true,什么也不做
一般expr是大多数情况都符合的条件
assert在头文件cassert中,把assert当做关键字,不要定义同名实体

NDEBUG预处理变量:assert的行为依赖NDEBUG的状态。如果用#define定义了NDEBUG,表明关闭调试状态,则assert什么也不做;如果没有定义NDEBUG,表示在调试状态,assert将执行运行时检查

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

#ifndef NDEBUG
   //__func__是c++编译器定义的局部静态变量,用于存放函数名
   cerr<<__func__<<":array size is"<<size<<endl;
#endif

预处理器定义了另4个名字:

//__FILE__ 存放文件名的字符串字面值
//__LINE__存放当前行号的整型字面值
//__TIME__存放文件编译时间的字符串字面值
//__DATE__存放文件编译日期的字符串字面值
#ifndef NDEBUG
   cerr<<“Error:”<<__FILE__
   <<"in function:"<<__func__
   <<"at line"<<__LINE__
   <<  Compiled on:<<__DATE__
   <<"at"<<__TIME__<<endl;
#endif

6.6 函数匹配

6.7 函数指针

指向函数的指针
用处:当需要函数作为形参或者返回值时。和数组一样,函数不能直接作为形参或返回值,只能用指向函数的指针作为形参或者返回值。
指向函数的指针的类型取决于函数的返回值和形参列表,声明指向函数的指针时用指针代替函数名

bool lengthCompare(const string &,const string &); //函数
bool (*ptr)(const string &,const string &); //指向函数的指针,括号不能省

给函数指针赋值:

ptr=lengthCompare; //直接把函数名赋值给指向函数的指针
//加上取地址符也可以
ptr=&lengthCompare;

使用指向函数的指针调用该函数:

bool b1=ptr("hello","goodbye"); //直接使用指向函数的指针调用函数,需要传入形参
bool b2=(*ptr)("hello","goodbye"); //也可以将指向函数的指针解引用,等价形式的调用
bool b3=lengthCompare("hello","goodbye"); //以上两种形式和使用函数名调用是等价的

和指向变量的指针一样:
①指向不同函数类型的指针不存在转换规则(只要返回值类型和形参列表不同,就不能进行相互转换)
②指向函数的指针可以被赋值为nullptr

函数指针作为形参
实参和形参都可以直接是函数类型,会被自动转换成指向函数的指针

void useBigger(const string &s1,const string &s2,bool func((const string &,const string &)); //直接把函数作为形参,其会被自动转化成指向函数的指针
void useBigger(const string &s1,const string &s2,bool (*ptr)((const string &,const string &)); //直接传入指向函数的指针作为形参
//调用函数时,可以直接把函数名作为实参传递,其会被自动转换成指向函数的指针
useBigger(s1,s2,lengthCompare);

借助类型别名简化函数指针的书写

bool lengthCompare(const string &s1,const string &s2);
typedef bool Func(const string &s1,const string &s2); //Func是函数类型
typedef decltype(lengthCompare) Func2; //Func2是函数类型,decltype作用于某个函数时,返回函数类型而不是指针类型
typedef bool (*Funpc)(const string &s1,const string &s2); //Funpc是指向函数的指针类型
typedef decltype(lengthCompare) *Funcp2; //Funcp2是指向函数的指针类型

函数指针作为返回值
和形参不同,编译器会把函数转换成指向函数的指针,编译器不会自动将函数返回类型当成对应指针类型处理
,必须把返回值类型写成指针类型。
使用类型别名:

using F=int (int*,int);  //F是函数类型
using Fp=int* (int*,int);  //Fp是指针类型
//必须显式地将返回类型定义成指针
Fp f1(int);
F *f1(int);
//尾置返回值类型的方法
auto f1(int)->int* (int*,int);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值