C++primer第6章函数


#pic_center =400x
系列文章:



6.1 函数基础

我们通过调用运算符来执行函数,调用运算符形式是一对圆括号,它作用于一个表达式,该表达式是函数或指向函数的指针。
函数的调用完成两项工作:一是用实参初始化函数对应的形参,而是将控制权转移给被调用函数,此时主调函数的执行被暂时中断,被调函数开始执行。
执行函数的第一步是隐式地定义并初始化它的形参。
实参是形参的初始值,实参和形参存在对应关系,但没有规定是实参的求值顺序,编译器能以任意可行的顺序对实参求值。
实参的类型必须和对应的形参类型匹配。
为了与C兼容,使用关键字void表示函数没有形参:
void f1(){} //隐式地定义空形参列表
void f1(void){} //显示定义空形参列表
函数返回类型
一种特殊的返回类型是void , 表示函数不返回任何值
函数的返回类型不能使数组类型或函数类型,但可以是指向数组或函数的指针

6.1.1 局部对象

自动对象
对于普通的局部变量对应的对象,当函数的控制路径经过变量定义语句时创建该对象,当达到定义所在的块末尾时销毁它。我们把只存在块执行期间的对象称为自动对象。
形参是自动对象
如果某个变量的定义本身含有初始值,则用初始值进行初始化,否则执行默认初始化,意味着有可能内置类型的未初始化局部变量将产生未定义的值。

局部静态对象
有时候有必要令局部变量的生命周期贯穿函数调用及之后的时间,可以将局部变量定义为static类型。
局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,直到程序终止才被销毁,在此期间及时对象所在的函数执行结束也不会对它有影响。
如统计函数被调用多少次

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-10
在程序流第一次经过ctr定义之前,ctr被创建初始化为0.每次调用将ctr加+返回。每次执行count_calls()函数时,变量ctr的值已经存在并等于上次退出时ctr的值。
如果局部静态变量没有显示初始化,则内置类型的局部静态变量初始化为0

6.1.2函数声明

函数只能定义1次,可以声明多次。
若一个函数从未使用过,这可以只有声明,没有定义
函数声明和定义类似,区别是声明无需函数体,用一个分号代替
函数的声明不包含函数体,所以无需形参的名字,但可以写上形参名字,更好理解函数的功能
函数的三要素(返回类型,函数名,形参类型)描述了函数的接口,说明库调用函数的全部信息,函数声明也称为函数原型

在有文件中进行函数声明
变量、函数都应该在头文件中声明,在源文件中定义

6.1.3 分离式编译

分离式编译允许把程序分割几个文件,每个文件独立编译
假设fact函数定义在fact.cc文件,声明在Chapter6.h头文件中。
另外,在名为factMain.cc的文件中创建main函数,main函数调用fact函数。
要生成可执行文件,必须告诉编译器我们用到的代码在哪里

CC factMain.cc fact.cc  //产生factMain.exe或a.out
CC factMain.cc fact.cc -o main //产生main或main.exe

如果修改其中一个源文件,只需要编译改动的文件

6.2参数传递

如果形参是引用类型,它将绑定到对应的实参上,否则将实参的值拷贝后赋值给形参。
当形参是引用类型,它对应的实参被引用传递或者函数被传引用调用,引用形参也是它绑定对象的别名即引用形参是它对应的实参的别名。
当实参 值被拷贝给形参时,形参和实参是两个独立的对象。这样的实参被值传递,或者函数被传值调用

6.2.1传值参数

传值参数: 函数对形参的所有操作都不会影响实参

指针形参
指针和其他非引用类型一样,执行指针拷贝操作时,拷贝是指针的值,拷贝后两个指针是不同的指针,可以通过在指针间接地访问它指的对象。

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

int i = 42;
reset(&i); //改变i的值而而非i的地址
cout << i << endl;  //i=0

6.2.2 传引用参数

对引用的操作实际上是作用在引用所引的对象上

void reset(int &i){
    i = 0; //改变i所引用对象的值
}
int j = 42;
reset(j); // j采用传引用方式,它的值被改变
cout << j << endl; //输出j = 0

使用引用避免拷贝
如比较两个string对象的长度,因为可能非常长,使用引用避免拷贝.
当函数无需修改引用形参的值时,最好使用常量引用

bool isShorter(const string &s1, const string &s2){
    return s1.size() < s2.szie();
}

6.2.3 const 形参和实参???

顶层的const作用于对象本身

const int ci = 42; //不能改变ci,const是顶层的
int i = ci; //正确:当拷贝ci时,忽略了它的顶层const
int * const p = &i; //const是顶层的,不能给p赋值
*p = 0; //正确:通过p改变对象的内容是允许的

当用实参初始化形参时会忽略顶层const
void fcn(const int i){ /* fcn能读取i,但不能向i写值 */}
调用fcn函数时,既可以传入const int也可以传入int
忽略顶层const可能会产生意想不到结果:

void fcn(const int i){}
void fcn(int i){} //错误:重复定义fcn(int)

指针或引用形参与const
可以使用非常量初始化一个底层const对象,但反过来不行
引用必须同类型对象初始化

int i = 42;
const int *cp = &i; //正确,但cp不能改变i,可以间接定义其他i的引用改变i
const int &r = i;
const int &r2 = 42; //正确
int *p = cp; // 错误:p和cp类型不匹配
int &r3 = r; //错误
int &r4 = 42; //错误:不能用字面值初始化非常量引用

参数传递

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(ctr);//错误:类型不匹配, ctr是无符号
void reset(int &i){}

想要调用reset只能使用int型对象,不能使用字面值、求值结果为int的表达式、需要转换的对象或者const int类型
指针也是只能使用int*

尽量使用常量引用
把函数不会改变的形参尽量定义为常量引用,这样做的好处:1不会误导即函数可以改变市场的值 2是像int*这样的形参不能接受const修饰、字面值或需要类型转换的对象

6.2.4 数组传参

不能拷贝数组-无法值传递使用数组参数
使用数组时会将数组转为指针-为函数传递一个数组,实际传递的指针
不能以值传递方式传递数组,但可以把形参写的类似数组形式:

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

//编译器处理print函数的调用时,只需要检查传入的参数是否是const int*类型
int i = 0, j[2] = {0, 1};
print(&i); //正确:&i的类型是int*
print(j); //正确: j转换为int*并指向j[0]

如果传给rint函数是一个数组,则实参自动转换为指针,数组的大小对函数的调用没有影响,
一开始函数不知道数组的确切大小,调用者需要提供信息,管理指针形参有三种常用的技术。
使用比较指定数组长度
要求数组本身包含一个结束标记,这种方法典型地例子就是C风格的字符串,最后一个字符跟着空字符。
这种方法适合那些有明显结束标记且标记不会和普通数据混淆

使用标准库规范
传递数组首元素的指针和尾后元素的指针

void print(const int *beg, const int *end){
    while(beg != end)
        cout<< *beg++ << endl;
}
int j[2] = {0, 1};
print(begin(j), end(j));

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

//const int ia[] 等价于const int* ia;
void print(const int ia[], size_t size)
int j[] = {0, 1};
print(j, end(j)-begin(j))

6.2.5

使用引用传参返回额外信息
一个函数只能返回一个值,而使用引用形参可以一次返回多个结果。

// 返回第一次出现c 的位置,并且返回共出现几次c 
string::size_type find_char(const string &s, char c, string::size_type &occurs){
    ...
    return ret;//位置
}
auto index = find_char(s, 'O', ctr);

数组形参和const
当函数不需要对数组元素执行写操作的时候,数组形参应该是指向const的指针,当函数确实需要改变元素值的时候才把形参定义为指向非常量的指针。

数组引用形参
形参可以是数组的引用

void print(int (&arr)[10]){
    for(quto elem: arr)
        cout << elem << endl;
}

6.3返回类型和return语句

6.3.2有返回值

值是如何被返回的
返回临时变量,临时变量是函数调用的结果
如果是引用,返回引用,不会真正拷贝对象

不要返回局部对象的引用或指针
函数完成后,它所占用的存储空间会被释放,因此函数终止,函数内的局部变量的引用将不在有效的内存区域。

const string &manip(){
    string ret;
    if(!ret.empty())
        return ret; //错误:返回局部变量的引用
    else
        return "Empty"; //错误: "Empty",字符串字面值会转换为一个局部临时string,是一个局部临时变量
}

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

返回类类型的函数和调用运算符

调用运算符的优先级和点运算符、箭头运算符相同。符合左结合律。
如果函数返回指针、引用或类的对象,可以使用函数调用的结果访问结果对象的成员。

引用返回左值
函数的返回类型决定函数调用是否是左值。
调用一个返回引用的函数将得到左值,其他返回类型将得到右值。
可以像使用其他左值那样使用返回引用的函数调用,特别的可以为返回类型是非常量引用的函数的结果赋值

char &get_val(string &str, string::size_type ix){
    return str[ix];
}
get_val(s, 0) = 'a'; //将s[0]的值改为A

列表初始化返回值
C++11新标准规定,函数可以返回花括号包围值的列表。可以使用列表对表示函数返回的临时变量进行初始化

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

如果函数返回的是内置类型,则花括号包围的列表最多包含一个值,而且该值所占空间不应该大于目标类型的空间。
如果返回的是类类型,由类本身定义初始值如何使用。

主函数main的返回值
如果函数的返回类型是不void,那么它必须有返回值。例外就是main,没有return ,编译器将隐式地插入一条返回0的return语句。
main函数的返回值可以看做状态指示器,返回0表示执行成功,返回其他值失败,非0状态和机器有关,为了使返回值与机器无关,cstdlib文件定义了两个预处理变量,用这两个变量分别表示成功或失败: EXIT_FAILURE, EXIT_SUCCESS

6.3.3返回数值指针

数组不能拷贝,所以函数不能返回数组,但函数可以返回数组的指针或引用
返回数组的指针或引用比较繁琐,简单的方法是使用类型别名

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

声明一个返回数组指针的函数
要想在声明func时,不使用类型别名,必须牢记被定义的名字后面数组的维度:

int arr[10]; //arr是一个含有10个整数的数组
int *p1[10]; //p1是一个含有10个指针的数组
int (*p2)[10]; //p2是一个指针,它指向含有10个整数的数组

定义一个返回数组指针的函数,则数组的维度必须跟在函数名字之后,函数的形参列表也跟在函数名字后面且形参列表先于数组维度,形式如下:
Type (*function(parameter_list))[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新标准可以简化上述func声明方法,使用尾置返回类型
任何函数的定义都能使用尾值返回,对于像返回数组的指针或数组的引用这类复杂度返回类型最有效。
尾置返回类型在形参列表后面以一个->开头。
为了表示函数真正的返回类型跟在幸灾列表之后,我们在本应该出现 返回类型的地方放置一个auto:
// 返回一个指针,该指针指向含有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; //返回一个指向数组的指针
}
使用decltype表示指针所指对象与odd类型已知,odd是数组,所以arrPtr返回一个纸箱含有5个整数的数组的指针。
但decltype不负责将数组类型转换为指针,所以decltype的结果是数组, 要想表示arrPtr返回指针还必须在函数声明是加一个*

6.3.

6.4函数重载

函数名字相同,但形参列表不同(形参数量和类型)
下面是几个形参列表相同

Record lookup(const Account&);
bool lookup(const Account&); //形参相同

Record lookup(const Account &acct);
bool lookup(const Account&); //省略了形参名字,形参的名字仅起到帮助记忆功能,并不会影响形参列表


typedef Phone Telno;
Record lookup(const Phone&);
Record lookup(const Telno&); //Phone和Telno类型相同

const形参和重载
顶层const不影响传入函数的对象。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来:

 Record lookup(Phone);
 Record lookup(const Phone); //两者声明一样
 Record lookup(Phone*);
 Record lookup(Phone* const); //声明一样

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

对于接受引用或指针的函数,对象是常量还是非常量对应的形参不同
Record lookup(Account&); 函数作用于Account引用
Record lookup(const Account&); //新函数,作用于常量引用
Record lookup(Account*); //新函数作用于指向Account的指针
Record lookup(const Account*); 新函数作用于指向常量的指针

const_cast和重载
const_cast在重载函数情形中最有用-4.11.3

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

我们希望当形参不是常量时,得到的结果是一个普通的引用

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

6.4.1 重载与作用域

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

string read();
void print(const string &);
void print(double);
void fooBar(int ival){
    bool read = false;
    string s = read(); //错误:read是bool值
    // 通常来说在局部作用域声明函数不是一个好的选择
    void print(int);
    print("Value: "); //错误: print(const string &)被屏蔽掉了
    print(ival); //正确:当前print(int)可见
    print(3.14); //正确:调用print(int); print(double)被屏蔽掉了
}

当调用print函数,编译器会先寻找对该函数名的声明,找到接受int值的局部声明。一旦在当前作用于找到所需要的名字,编译器会忽略外层作用域中同名实体。剩下的工作就是检查函数调用是否有效。
但是如果把上述的print声明放在同一个作用域中,编译器能看到所有的print函数。

6.5特殊用途语言特性

6.5.1 默认实参

typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char backfrnd = ' ');
注意点1:调用默认实参的函数时,可以包含该实参,也可以省略该实参。
注意点2:一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值

使用默认实参调用函数

string window;
window = screen(); //等价于screen(24, 80, ' ')
window = screen(66);/等价于screen(60, 80, ' ')
window = screen(66, 256);/等价于screen(60, 256, ' ')
window = screen(66, 256, '#');/等价于screen(60, 256, '#')
window = screen( ,  , '?');//错误:只能省略尾部的实参
window = screen('?');//正确,到调用的是screen('?', 80, ' ')  ?被转为int

默认实参声明

string screen(sz, sz, char='');
string screen(sz, sz, char='*');//错误:重复声明
string screen(sz=24, sz=80, char); //正确:添加默认实参

6.5.2 内联函数 和cosntexpr函数

调用函数一般比求等价表达式的值要慢写,大多数机器上调用函数包含一系列工作:调用前要保存寄存器,并返回时恢复;可能需要拷贝实参;程序转一个新的位置继续执行。
内联函数可以避免函数调用的开销
将函数指定为内联函数inline,通常在它的每个调用点上“内联地”展开

cout << shorterString(s1, s2) << endl;
编译过程展开类似于下面的形式
cout << (s1.size() < s2.size()? s1: s2) <<endl;

在shorterString函数的返回类型前面加上关键字inline就可以声明为内联函数了。
内联说明是向编译器发出一个请求,编译器可以回略该请求。
一般而言,内联机制用于优化规模小、流程直接、频繁调用的函数,很多编译器都不支持内联递归函数

constexpr函数
回顾:
常量表达式:值不会改变且在编译过程得到计算结果的表达式。
允许将变量声明为constexpr类型,由编译器验证变量的值是否是一个常量表达式。
声明为constexpr的变量一定是一个常量,且必须用常量表达式初始化。

constexpr函数的约定:

  • 函数的返回类型以及所有形参类型都是字面值类型
  • 函数体必须有且只有一条return语句
constexpr int new_sz() { return 42; }
constexpr int foo = new_sz(); //正确 foo是一个常量表达式

执行该任务是,编译器把对constexpr函数的调用结果替换为其结果值
为了能在编译过程中随时展开,constexpr函数被隐式地指定为内联函数

6.5.3

6.6函数匹配

void f();
void f(int);
void f(int, int);
void f(double, double = 3.14);
f(5.6); //调用f(double, double)
f(42, 2.56)// 看一个参数调用int,int合适,看第二个调用方double double合适,所以编译器无法【判断】,报告二义性

6.6.1 实惨类型转换

为了确定嘴角匹配。编译器将实参类型到形参类型的转换划分为几个等级。

  1. 精确匹配

    • 实参类型和形参类型相同
    • 实参从数组类型或函数类型转换为对应的指针类型
    • 向实参添加顶层const或从实参删除顶层const
  2. 通过const转换的匹配

  3. 通过类型提升实现的匹配

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

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

需要类型提升和算术类型转换的匹配
小整型一般会提升到int型或更大的整数类型
假设有两个函数,一个接受int, 另一个接收short,则只有当提供short类型的值才会选择short版本。
所有的算术类型转换级别都一样,int向unsigned int 和int 向double转换级别一样

void manip(long);
void manip(float);
manip(3.14); //错误:二义性调用

函数匹配和const实参
如果重载函数的区别在于它们的引用类型的形参是否引用了const或者指针类型的形参是否指向const,则调用发生时编译器通过形参是否是常量来决定选择哪个函数:

Record lookup(Account&); //参数是Account的引用
Record lookup(const Account&); //函数的参数是一个常量引用
const Account a;
Account b;
lookup(a); //调用lookup(const Account&)
lookup(b); //调用lookup(Account&)

lookup(Account&)不能接受const Account a, 因为普通引用不能绑定到const对象上
lookup(const Account&) 可以接受const Account a, Account b;但b更精准
指针类似

6.7 函数指针

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

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

该函数类型是 bool (const string &, const string &)
要想声明一个指向该函数的指针,只需将函数名替换为指针即可
bool (*pf)(const string &, const string &); //未初始化

从声明的名字观察: pf前有*,说明是pf是指针, 右侧是形参列表,表示p指向的是函数; 左侧是bool,函数返回类型是bool、 因此pf是一个指向函数的指针,其中该函数的参数是两个const string引用,返回bool型.
pf两端的括号必不可少,否则pf是一个返回值为bool指针的函数,即返回bool

使用函数指针

当我们把函数名作为一个值使用时,该函数自动转换为指针

pf = lengthCompare; //pf指向名为lengthCompare的函数
pf = &lengthComparel //等价的赋值语句:取地址符是可选的

可以直接使用指向函数的指针调用该函数,无需提前解引用指针;

bool b1 = pd("Hello", 'goodbye'); //调用lengthCompare函数
bool b2 = (*pf)("Hello", 'goodbye'); //等价调用
bool b3 =  lengthCCompare("Hello", 'goodbye'); // 等价调用

可以为函数指针赋值nullptr或者值为0的整形常量表达式,表示指针没有指向任何一个函数:

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; //指向ff(unsigned int)
void (*pf2)(int) = ff; // 错误: 没有任何一个ff与该形参列表匹配
double (*pf2)(int*) = ff; //错误 返回类型不匹配
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值