C++ 函数

《C++ Primer 4th》读书笔记

函数调用做了两件事情:用对应的实参初始化函数的形参,并将控制权转移给被调用函数。主调函数的执行被挂起,被调函数开始执行。

类似于局部变量,函数的形参为函数提供了已命名的局部存储空间。它们之间的差别在于形参是在函数的形参表中定义的,并由调用函数时传递函数的实参初始化。实参则是一个表达式。它可以是变量或字面值常量,甚至是包含一个或几个操作符的表达式。

形参的初始化与变量的初始化一样:如果形参具有非引用类型,则复制实参的值,如果形参为引用类型(第 2.5 节),则它只是实参的别名。

非引用形参

函数的形参可以是指针,此时将复制实参指针。与其他非引用类型的形参一样,该类形参的任何改变也仅作用于局部副本。事实上被复制的指针只影响对指针的赋值。如果函数形参是非 const 类型的指针,则函数可通过指针实现赋值,修改指针所指向对象的值。

指针形参是指向 const 类型还是非 const 类型,将影响函数调用所使用的实参。可以将指向 const 对象的指针初始化为指向非 const对象,但不可以让指向非 const 对象的指针向 const 对象。

C 语言中,具有 const 形参或非 const 形参的函数并无区别。

复制实参并不是在所有的情况下都适合,不适宜复制实参的情况包括:

• 当需要在函数中修改实参的值时。

• 当需要以大型对象作为实参传递时。对实际的应用而言,复制对象所付出的时间和存储空间代价往往过在。

• 当没有办法实现对象的复制时。

引用形参

引用形参直接关联到其所绑定的圣贤,而并非这些对象的副本。定义引用时,必须用与该引用绑定的对象初始化该引用。引用形参完全以相同的方式工作。

从 C 语言背景转到 C++ 的程序员习惯通过传递指针来实现对实参的访问。在 C++ 中,使用引用形参则更安全和更

自然。

在向函数传递大型对象时,需要使用引用形参,这是引用形参适用的另一种情况。

利用const 引用避免复制:如果使用引用形参的唯一目的是避免复制实参,则应将形参定义为 const 引用。应该将不需要修改的引用形参定义为 const 引用。普通的非 const 引用形参在使用时不太灵活。这样的形参既不能用 const 对象初始化,也不能用字面值或产生右值的表达式实参初始化。

如果函数具有普通的非 const 引用形参,则显然不能通过 const 对象进行调用。非 const 引用形参只能与完全同类型的非 const 对象关联。

传递指向指针的引用,形参int *&v1的定义应从右至左理解:v1 是一个引用,与指向 int 型对象的指针相关联。

vector和其他容器类型的形参

通常,函数不应该有 vector 或其他标准库容器类型的形参。调用含有普通的非引用 vector 形参的函数将会复制 vector 的每一个元素。

从避免复制 vector 的角度出发,应考虑将形参声明为引用类型。事实上,C++ 程序员倾向于通过传递指向容器中需要处理的元素的迭代器来传递容器:

数组形参

数组有两个特殊的性质,影响我们定义和使用作用在数组上的函数:一是不能复制数组;二是使用数组名字时,数组名会自动转化为指向其第一个元素的指针。因为数组不能复制,所以无法编写使用数组类型形参的函数。因为数组会被自动转化为指针,所以处理数组的函数通常通过操纵指向数组指向数组中的元素的指针来处理数组。

数组形参的定义

// three equivalent definitions of printValues

void printValues(int*) { /* ... */ }

void printValues(int[]) { /* ... */ }

void printValues(int[10]) { /* ... */ }

通常,将数组形参直接定义为指针要比使用数组语法定义更好。这样就明确地表示,函数操纵的是指向数组元素的指针,而不是数组本身。由于忽略了数组长度,形参定义中如果包含了数组长度则特别容易引起误解。

形参的长度会引起误解:编译器忽略为任何数组形参指定的长度。当编译器检查数组形参关联的实参时,它只会检查实参是不是指针、指针的类型和数组元素的类型时是否匹配,而不会检查数组的长度。

数组实参,和其他类型一样,数组形参可定义为引用或非引用类型。大部分情况下,数组以普通的非引用类型传递,此时数组会悄悄地转换为指针。在传递数组时,实参是指向数组第一个元素的指针,形参复制的是这个指针的值,而不是数组元素本身。函数操纵的是指针的副本,因此不会修改实参指针的值。然而,函数可通过该指针改变它

所指向的数组元素的值。通过指针形参做的任何改变都在修改数组元素本身。不需要修改数组形参的元素时,函数应该将形参定义为指向 const 对象的指针。

数组形参可声明为数组的引用。如果形参是数组的引用,编译器不会将数组实参转化为指针,而是传递数组的引用本身。在这种情况下,数组大小成为形参和实参类型的一部分。编译器检查数组的实参的大小与形参的大小是否匹配:

// ok: parameter is a reference to an array; size of array is fixed

void printValues(int (&arr)[10]) { /* ... */ }

int main()

{

int i = 0, j[2] = {0, 1};

int k[10] = {0,1,2,3,4,5,6,7,8,9};

printValues(&i); // error: argument is not an array of 10 ints

printValues(j); // error: argument is not an array of 10 ints

printValues(k); // ok: argument is an array of 10 ints

return 0;

}

// ok: parameter is a reference to an array; size of array is fixed

void printValues(int (&arr)[10])

{

for (size_t i = 0; i != 10; ++i) {

cout << arr[i] << endl;

}

}

&arr 两边的圆括号是必需的,因为下标操作符具有更高的优先级:

多维数组的传递

和其他数组一样,多维数组以指向 0 号元素的指针方式传递。多维数组的元素本身就是数组。除了第一维以外的所有维的长度都是元素类型的一部分,必须明确指定:

// first parameter is an array whose elements are arrays of 10 ints

void printValues(int (matrix*)[10], int rowSize);

上面的语句将 matrix 声明为指向含有 10 个 int 型元素的数组的指针。

int *matrix[10]; // array of 10 pointers

int (*matrix)[10]; // pointer to an array of 10 ints

我们也可以用数组语法定义多维数组。与一维数组一样,编译器忽略第一维的长度,所以最好不要把它包括在形参表内:

// first parameter is an array whose elements are arrays of 10 ints

void printValues(int matrix[][10], int rowSize);

这条语句把 matrix 声明为二维数组的形式。实际上,形参是一个指针,指向数组的数组中的元素。数组中的每个元素本身就是含有 10 个 int 型对象的数组。

传递给函数的数组的处理

有三种常见的编程技巧确保函数的操作不超出数组实参的边界。

第一种方法是在数组本身放置一个标记来检测数组的结束。

第二种方法是传递指向数组第一个和最后一个元素的下一个位置的指针。

第三种方法是将第二个形参定义为表示数组的大小。

含有可变形参的函数

在无法列举出传递给函数的所有实参的类型和数目时,可以使用省略符形参。

void foo(parm_list, ...);

void foo(...);

第一种形式为特定数目的形参提供了声明。在这种情况下,当函数被调用时,对于与显示声明的形参相对应的实参进行类型检查,而对于与省略符对应的实参则暂停类型检查。

大部分带有省略符形参的函数都利用显式声明的参数中的一些信息,来获取函数调用中提供的其他可选实参的类型和数目。因此带有省略符的第一种形式的函数声明是最常用的。

Return

如果返回类型不是引用,在调用函数的地方会将函数返回值复制给临时对象。当函数返回非引用类型时,其返回值既可以是局部对象,也可以是求解表达式的结果。

当函数返回引用类型时,没有复制返回值。相反,返回的是对象本身。

// find longer of two strings

const string &shorterString(const string &s1, const string &s2)

{

return s1.size() < s2.size() ? s1 : s2;

}

理解返回引用至关重要的是:千万不能返回局部变量的引用。当函数执行完毕时,将释放分配给局部对象的存储空间。此时,对局部对象的引用就会指向不确定的内存。

// Disaster: Function returns a reference to a local object

const string &manip(const string& s)

{

string ret = s;

// transform ret in some way

return ret; // Wrong: Returning reference to a local object!

}

返回引用的函数返回一个左值。因此,这样的函数可用于任何要求使用左值的地方.给函数返回值赋值可能让人惊讶,由于函数返回的是一个引用,因此这是正确的,该引用是被返回元素的同义词。如果不希望引用返回值被修改,返回值应该声明为 const:

const char &get_val(...

千万不要返回指向局部对象的指针

函数的返回类型可以是大多数类型。特别地,函数也可以返回指针类型。和返回局部对象的引用一样,返回指向局部对象的指针也是错误的。一旦函数结束,局部对象被释放,返回的指针就变成了指向不再存在的对象的悬垂指针

函数声明

函数也必须在被调用之前先声明。函数的声明也可以和函数的定义分离;一个函数只能定义一次,但是可声明多次。

函数声明由函数返回类型、函数名和形参列表组成。形参列表必须包括形参类型,但是不必对形参命名。这三个元素被称为函数原型,函数原型描述了函数的接口。函数原型为定义函数的程序员和使用函数的程序员之间提供了

接口。在使用函数时,程序员只对函数原型编程即可。

函数声明中的形参名会被忽略,如果在声明中给出了形参的名字,它应该用作辅助文档:

void print(int *array, int size);

函数也应当在头文件中声明,并在源文件中定义。

默认实参

如果有一个形参具有默认实参,那么,它后面所有的形参都必须有默认实参。函数调用的实参按位置解析,默认实参只能用来替换函数调用缺少的尾部实参。设计带有默认实参的函数,其中部分工作就是排列形参,使最少使用默认实参的形参排在最前,最可能使用默认实参的形参排在最后。

默认实参可以是任何适当类型的表达式:

string::size_type screenHeight();

string::size_type screenWidth(string::size_type);

char screenDefault(char = ' ');

string screenInit(

string::size_type height = screenHeight(),

string::size_type width = screenWidth(screenHeight()),

char background = screenDefault());

既可以在函数声明也可以在函数定义中指定默认实参。但是,在一个文件中,只能为一个形参指定默认实参一次。下面的例子是错误的:

// ff.h

int ff(int = 0);

// ff.cc

#include "ff.h"

int ff(int i = 0) { /* ... */ } // error

通常,应在函数声明中指定默认实参,并将该声明放在合适的头文件中。

局部对象

名字的作用域指的是知道该名字的程序文本区。对象的生命期则是在程序执行过程中对象存在的时间。

默认情况下,局部变量的生命期局限于所在函数的每次执行期间。只有当定义它的函数被调用时才存在的对象称为自动对象。自动对象在每次调用函数时创建和撤销。

一个变量如果位于函数的作用域内,但生命期跨越了这个函数的多次调用,这种变量往往很有用。则应该将这样的对象定义为 static(静态的)。static 局部对象确保不迟于在程序执行流程第一次经过该对象的定义语句时进行初始化。这种对象一旦被创建,在程序结束前都不会撤销。当定义静态局部对象的函数结束时,静态局部对象不会撤销。在该函数被多次调用的过程中,静态局部对象会持续存在并保持它的值。

size_t count_calls()

{

static size_t ctr = 0; // value will persist across calls

return ++ctr;

}

int main()

{

for (size_t i = 0; i != 10; ++i)

cout << count_calls() << endl;

return 0;

}

这个程序会依次输出 1 到 10(包含 10)的整数。

内联函数

调用函数比求解等价表达式要慢得多。在大多数的机器上,调用函数都要做很多工作;调用前要先保存寄存器,并在返回时恢复;复制实参;程序还必须转向一个新位置执行。

将函数指定为 inline 函数,(通常)就是将它在程序中每个调用点上“内联地”展开。假设我们将 shorterString 定义为内联函数,则调用:

cout << shorterString(s1, s2) << endl;

在编译时将展开为:

cout << (s1.size() < s2.size() ? s1 : s2)

<< endl

在函数返回类型前加上关键字inline,从而消除了把 shorterString 写成函数的额外执行开销。

// inline version: find longer of two strings

inline const string &

shorterString(const string &s1, const string &s2)

{

return s1.size() < s2.size() ? s1 : s2;

}

一般来说,内联机制适用于优化小的、只有几行的而且经常被调用的函数。大多数的编译器都不支持递归函数的内联

内联函数应该在头文件中定义,这一点不同于其他函数。

类的成员函数

和任何函数一样,成员函数也包含下面四个部分:

• 函数返回类型。

• 函数名。

• 用逗号隔开的形参表(也可能是空的)。

• 包含在一对花括号里面的函数体。

前面三部分组成函数原型。函数原型必须在类中定义。但是,函数体则既可以在类中也可以在类外定义。

编译器隐式地将在类内定义的成员函数当作内联函数

class Sales_item {

public:

// operations on Sales_item objects

double avg_price() const;

bool same_isbn(const Sales_item &rhs) const

{ return isbn == rhs.isbn; }

// private members as before

private:

std::string isbn;

unsigned units_sold;

double revenue;

};

this指针的引入

每个成员函数(static 成员函数外)都有一个额外的、隐含的形参 this。在调用成员函数时,形参 this 初始化为调用函数的对象的地址。

total.same_isbn(trans);

就如编译器这样重写这个函数调用:

Sales_item::same_isbn(&total, trans);

在这个调用中,函数 same_isbn 中的数据成员 isbn 属于对象 total。

理解跟在 Sales_item 成员函数声明的形参表后面的 const 所起的作用:const 改变了隐含的 this 形参的类型。用这种方式使用 const 的函数称为常量成员函数。由于 this 是指向 const 对象的指针,const 成员函数不能修改调用该函数的对象。

在类的定义外面定义成员函数必须指明它们是类的成员,使用作用域操作符指明函数 avg_price 是在类 Sales_item 的作用域范围内定义的:

double Sales_item::avg_price() const

{

if (units_sold)

return revenue/units_sold;

else

return 0;

}

构造函数是特殊的成员函数,与其他成员函数不同,构造函数和类同名,而且没有返回类型。构造函数通常应确保其每个数据成员都完成了初始化。通常将类的声明放置在头文件中。大多数情况下,在类外定义的成员函数则置于源文件中

// default constructor needed to initialize members of built-in type

Sales_item(): units_sold(0), revenue(0.0) { }

在冒号和花括号之间的代码称为构造函数的初始化列表。构造函数的初始化列表为类的一个或多个数据成员指定初值。它跟在构造函数的形参表之后,以冒号开关

编译器创建的默认构造函数通常称为合成的默认构造函数,它将依据如同变量初始化的规则初始化类中所有成员。合成的默认构造函数一般适用于仅包含类类型成员的类。而对于含有内置类型或复合类型成员的类,则通常应该定义他们自己的默认构造函数初始化这些成员。

重载函数

出现在相同作用域中的两个函数,如果具有相同的名字而形参表不同,则称为重载函数。

如果两个函数声明的返回类型和形参表完全匹配,则将第二个函数声明视为第一个的重复声明。如果两个函数的形参表完全相同,但返回类型不同,则第二个声明是错误的。

有些看起来不相同的形参表本质上是相同的:

// each pair declares the same function

Record lookup(const Account &acct);

Record lookup(const Account&); // parameter names are ignored

typedef Phone Telno;

Record lookup(const Phone&);

Record lookup(const Telno&); // Telno and Phone are the same type

Record lookup(const Phone&, const Name&);

// default argument doesn't change the number of parameters

Record lookup(const Phone&, const Name& = "");

// const is irrelevent for nonreference parameters

Record lookup(Phone);

Record lookup(const Phone); // redeclaration

形参与 const 形参的等价性仅适用于非引用形参。有 const 引用形参的函数与有非 const 引用形参的函数是不同的。类似地,如果函数带有指向 const 类型的指针形参,则与带有指向相同类型的非 const 对象的指针形参的函数不相同。

一般的作用域规则同样适用于重载函数名。如果局部地声明一个函数,则该函数将屏蔽而不是重载在外层作用域中声明的同名函数。由此推论,每一个版本的重载函数都应在同一个作用域中声明。一般来说,局部地声明函数是一种不明智的选择。函数的声明应放在头文件中。

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

重载确定的三个步骤

函数重载确定的第一步是确定该调用所考虑的重载函数集合,该集合中的函数称为候选函数。候选函数是与被调函数同名的函数,并且在调用点上,它的声明可见。

第二步是从候选函数中选择一个或多个函数,它们能够用该调用中指定的实参来调用。因此,选出来的函数称为可行函数。可行函数必须满足两个条件:第一,函数的形参个数与该调用的实参个数相同;第二,每一个实参的类型必须与对应形参的类型匹配,或者可被隐式转换为对应的形参类型。

函数重载确定的第三步是确定与函数调用中使用的实际参数匹配最佳的可行函数。这个过程考虑函数调用中的每一个实参,选择对应形参与之最匹配的一个或多个可行函数。这里所谓“最佳”的细节将在下一节中解释,其原则是实参类型与形参类型越接近则匹配越佳。因此,实参类型与形参类型之间的精确类型匹配比需要转换的匹配好。

含有多个形参的重载确定。如果有且仅有一个函数满足下列条件,则匹配成功:

  1. 其每个实参的匹配都不劣于其他可行函数需要的匹配。

  2. 至少有一个实参的匹配优于其他可行函数提供的匹配。

如果在检查了所有实参后,仍找不到唯一最佳匹配函数,则该调用错误。编译器将提示该调用具有二义性。

为了确定最佳匹配,编译器将实参类型到相应形参类型转换划分等级。转换等级以降序排列如下:

  1. 精确匹配。实参与形参类型相同。

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

  3. 通过标准转换实现的匹配。

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

函数指针是指指向函数而非指向对象的指针。像其他指针一样,函数指针也指向某个特定的类型。函数类型由其返回类型以及形参表确定,而与函数名无关

// pf points to function returning bool that takes two const string references

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

*pf 两侧的圆括号是必需的:

// declares a function named pf that returns a bool*

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

typedef简化函数指针的定义

函数指针类型相当地冗长。使用 typedef 为指针类型定义同义词,可将函数指针的使用大大简化:

typedef bool (*cmpFcn)(const string &, const string &);

该定义表示 cmpFcn 是一种指向函数的指针类型的名字。

在引用函数名但又没有调用该函数时,函数名将被自动解释为指向函数的指针。假设有函数:

// compares lengths of two strings

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

除了用作函数调用的左操作数以外,对 lengthCompare 的任何使用都被解释为如下类型的指针:

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

直接引用函数名等效于在函数名上应用取地址操作符:

cmpFcn pf1 = lengthCompare;

cmpFcn pf2 = &lengthCompare;

函数指针只能通过同类型的函数或函数指针或 0 值常量表达式进行初始化或赋值。

指向函数的指针可用于调用它所指向的函数。可以不需要使用解引用操作符,直接通过指针调用函数:

cmpFcn pf = lengthCompare;

lengthCompare("hi", "bye"); // direct call

pf("hi", "bye"); // equivalent call: pf1 implicitly dereferenced

(*pf)("hi", "bye"); // equivalent call: pf1 explicitly dereferenced

函数指针形参

函数的形参可以是指向函数的指针。这种形参可以用以下两种形式编写:

// third parameter is a function type and is automatically treated as a pointer tofunction

void useBigger(const string &, const string &, bool(const string &, const string &));

// equivalent declaration: explicitly define the parameter as a pointer to function

void useBigger(const string &, const string &, bool (*)(const string &, const string &));

返回指向函数的指针

int (*ff(int))(int*, int);

使用 typedef 可使该定义更简明易懂:

// PF is a pointer to a function returning an int, taking an int* and an int

typedef int (*PF)(int*, int);

PF ff(int); // ff returns a pointer to function

允许将形参定义为函数类型,但函数的返回类型则必须是指向函数的指针,而不能是函数。

具有函数类型的形参所对应的实参将被自动转换为指向相应函数类型的指针。但是,当返回的是函数时,同样的转换操作则无法实现:

// func is a function type, not a pointer to function!

typedef int func(int*, int);

void f1(func); // ok: f1 has a parameter of function type

func f2(int); // error: f2 has a return type of function type

func *f3(int); // ok: f3 returns a pointer to function type


C++ 语言允许使用函数指针指向重载的函数:

extern void ff(vector<double>);

extern void ff(unsigned int);

// which function does pf1 refer to?

void (*pf1)(unsigned int) = &ff; // ff(unsigned)


指针的类型必须与重载函数的一个版本精确匹配。
原文链接: https://www.cnblogs.com/1zhk/p/5011369.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/225217

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月13日 下午12:43
下一篇 2023年2月13日 下午12:43

相关推荐