C++Primer第五版【笔记】——第六章 函数

1 基础

1.1 范围和生命期

  • 一个名字的范围是指该名字在程序中的作用域,即可见范围。
  • 一个对象的生命期是指在程序执行时,对象存在的持续时间。
一个 全局对象的生命期从程序创建时开始,到程序终止时结束。 局部对象的生命期开始于其定义的位置,当程序控制路径越过其所在的作用范围时,生命期结束。加上 static声明的局部对象,其初始化发生在程序第一次执行到该对象的定义之前。其生命期在函数调用结束后仍然持续。

void go()
{
	static int i = 1;
	++i;
}

int main() {
	go();	
	go();
	return 0;
}
当程序执行到第2行 '{' 时,变量i初始化为1,第一个go()调用后i为2,进入第二个go()调用时,i = 2。

1.2 函数声明

函数的声明不带函数体,即将大括号换成分号:

void fun(int, int);
在函数声明中参数的名字是不必要的。函数声明包括三部分: 返回值函数名参数类型。这三部分描述了函数接口。函数声明也称为 函数原型
一般将函数的声明放在头文件中,定义放在源文件中。源文件需要包含有函数声明的头文件。

2 参数传递

函数调用时的参数传递有两种方式:1. 传值; 2. 传引用。对于占用内存很大的对象,传递引用可以避免大量的空间开销。

2.1 尽可能使用const引用

使用const引用或字面值初始化一个普通的引用是错误的,该规则对于参数的传递也适用。
void go(int &i)
{	
}

int main() {
	int i = 1;
	const int &ri = i;
	go(ri);	// error
	go(10); // error
	return 0;
}
加上const
void go(const int &i)
{	
}

int main() {
	int i = 1;
	const int &ri = i;
	go(ri);	// ok
	go(10); // ok
	return 0;
}

2.2 传递数组

由于数组的特殊性,在使用数组时,会自动转换成指针。即:
int a[10];
int *p = a;
cout << a[5] << endl; // 等价于 *(p+5)
在传递数组时,实际上传递的是指向数组的指针。
void go(int *pa);
void go(int a[]);   // 强调传递的是数组
void go(int a[10]); // 
...
int a0 = 0;
int a1[2] = {0,1};
int a2[15] = {0,1};
go(&a0); // ok
go(a1);  // ok
go(a2);  // ok
这三种定义方法在某种程度上是等价的,因为在调用go()时,编译器只会检查实参的类型是否是int,即检查数组元素的类型。所以上面的调用都是合法的,但是数组范围的可控制则需要很小心。看下面的示例:
void go(int a[10])
{
	for (int i=0; i<10; ++i)
		cout << a[i] << endl;	
}

int main() {
	
	int a1[2] = {0,1};
	int a2[15] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
	go(a1);
	go(a2);
	return 0;
}
go(a1)调用之后,输出的十个数,后面八个是未定义的,因为数组a1只定义了两个元素;
go(a2)调用后,输出数组a2的前10个元素。如果将函数稍作修改,让其输出15个元素,这也未尝不可
void go(int a[10])
{
	for (int i=0; i<15; ++i)
		cout << a[i] << endl;	
}
go(a2)调用后,输出a2的15个元素。你可能会以为后五个元素是未定义的,因为形参是 int a[10]。但是,前面已经指出,它和 int *pa的定义方法等价,因为数组就是指针。

2.2.1 传递数组的时候如何获取数组的长度信息?

  1. 对于C-风格字符串,我们可以利用末尾的NULL符号判断;
  2. 使用标准库的风格,指定数组的开始位置和结束位置;
    void go(int *beg, int *end)
    {
    	while (beg != end) {
    		cout << *beg++ << endl;		
    	}
    }
    我们知道end指向的是数组的超出末端位置,不能对其引用。
    利用新规则,我们可以很方便的得到这两个指针
    int a[10] = {};
    go(begin(a), end(a));
  3. 将数组长度作为形参传递。

2.2.2 数组的引用

可以将形参定义为数组的引用:
void go(int (&ra)[10]); //(&ra)的括号不能省
因为数组的大小是其类型的一部分,所以调用go时传递的实参也必须严格的是int [10],显然这会限制其使用。

2.2.3 传递多维数组

严格的讲,C++中没有多维数组。所谓的多维数组实际上是指 数组的数组。定义方法如下:
void goMul(int (*paa)[5], int crow)
{
	for (int row=0; row < crow; ++row) {
		for (int col=0; col < 5; ++col)
			cout << paa[row][col] << " ";
		cout << endl;
	}
}

int main() {
	int aa[2][5] = {
		1, 2, 3, 4, 5,
		6, 7, 8, 9, 10
	};
	goMul(aa, 2);
	return 0;
}

2.2.4 main函数参数

int main(int argc, char **argv); 或者 int main(int argc, char *argv[])
argv 是保存char*类型的指针数组,即保存的C-风格字符串。argc表示字符串数。系统将命令行参数传递给main,argv中的第一个字符串是程序名,或者为空串。可选参数从argv[1]开始。

2.3 可变参数

有时候我们事先不知道要传递多少个参数,像printf库函数就是一个例子。新标准有两种方法实现可变参数方程。

2.3.1 initializer_list 参数

如果所有的参数类型相同,我们可以使用initializer_list模板。
Operations on initializer_list s
initializer_list lst; 默认初始化,元素类型为T的空列表
initializer_list lst{a,b,c...}; lst的元素个数可以和initializers一样多;元素是对于的
initializers的副本。列表中的元素是const的
lst2(lst)
lst2 = lst
对initializer_list拷贝或赋值不会复制列表中的元素。复制后
副本和原列表共享元素
lst.size() 列表中的元素数
lst.begin()
lst.end()
返回lst中第一个元素和过一个最后元素的指针。

2.3.2 省略号参数

来源自C语言库中的 varargs.printf是一个很好的例子: int printf(const char* _Format, ...);

3.函数返回值

返回值为非void的函数必须有返回值。使用 return val;语句返回一个合适的值。
当函数返回一个值的时候,是将返回值赋给一个临时变量,该变量就作为函数调用的结果。
[注意]返回局部对象的引用或指针会导致严重的错误:因为当函数调用结束后,局部对象将被释放,引用为定义的对象是危险的。
如果函数的返回一个引用类型,则该函数调用可以作为 左值;否则为右值。
int &swapXY(int &x, int &y)
{
	int temp;
	temp = x;
	x = y;
	y = temp;
	return x;
}
int main() {
	int x = 10, y = 5;
	swapXY(x,y) = 0;	
	return 0;
}
返回列表
返回列表类似于列表初始化的规则,使用列表来填充临时返回变量。
string ss(bool state)
{
	if (state)
		return {"Ok"};
	else
		return {"Error", "Exit"};
}
返回指向数组的指针
  1. 使用数组别名
    typedef int arr[10];
    using arr = int[10];
    
    arr* func(int i);
  2. 直接定义
    Type ( *function(parameter_list) ) [dimension]
    int (*func(int i)) [10];
  3. 使用auto
    auto func(int i) -> int(*)[10];
  4. 使用decltype
    int arr[10] = {1,2,3};
    
    decltype(arr) *func(int i)
    {
    }

4 重载

对于一个函数,与其函数名相同,而参数类型或参数个数不同的函数是其重载函数。
如果判断两个函数是否相同是关键,下面列举的都是相同的函数声明:
int overLoad(int a);   // 原函数
int overLoad(int);   // 变量名可以忽略
typedef int INT;
int overLoad(INT a);   // int 和 INT是相同的类型
float overLoad(int a); // error: 只有返回值不同
int overLoad(const int); // top-level const对于可以传递的实参的类型没有影响

4.1 const_cast与重载

有时候我们可能同时需要一个函数的const版本和非const版本,这时const_cast就很有用了。
const int &go(const int &a)
{
	return a;
}

int &go(int &a)
{
	return const_cast(go(const_cast(a)));
}
非const版本的实现调用了const版本,这是一种不错的重载函数实现方法。因为 同名的重载函数一般都完成相同的任务,利用其中一个去实现其他的,这样在函数需要修改时,就很方便了
当定义好重载函数以后,需要考虑的就是函数匹配问题。即编译器需要根据实参的类型去匹配应该调用哪个函数。一般可以根据参数个数和类型决定。但是当一些参数可以通过类型转换而与一些函数发生联系时,就不容易匹配了。

4.2 专用特性

4.2.1 默认参数

如果某个参数在大多数情况下的值是固定的,而又希望可以另行指定值。我们可以为参数提供默认的初始化值。
void go(int, int = 10, int = 5);
void go(int i0, int i1 = 10, int i2 = 5)
{
}
如果一个参数具有默认值,那么其后的所以参数也需要提供默认值。这种规定可以使我们在调用函数时, 全部或部分省略有默认值的参数。
go(10);
go(10,20);
go(10,20,30);
【技巧】在设计函数时,将最不常使用默认值得参数放在前面,最常使用默认值的放在后面。

4.2.2 inline函数

inline机制用来优化一些小的、很直接的且调用频繁的函数。inline函数在编译时将会被展开,避免了函数调用带来的额外开销。
比较x,y,z的大小,返回最大值:
inline int maxXYZ(int x, int y, int z)
{
	return x > y ? 
	      (x > z ? x : z):
	      (y > z ? y : z);
}

4.2.3 constexpr函数【C++11】

constexpr函数是可以在 常量表达式中使用的函数。constexpr函数与其他函数相比有一些 限制返回值类型和每个参数的类型必须是字面值(literal type),函数体中只能包含一个return语句
constexpr函数默认是内联函数。constexpr函数的函数体可以包含一些在运行时不产生动作的语句,比如:空语句,类型别名,using声明等。constexpr函数可以返回一个非常量的值,但是调用函数的实参必须是常量表达式。
【注】inline函数和constexpr函数的定义一般都放在头文件中。

4.3 调试助手

assert是一个预处理宏。定义:
#define assert(_Expression) (void)( (!!(_Expression)) 
        || (_wassert(_CRT_WIDE(#_Expression), _CRT_WIDE(__FILE__), __LINE__), 0) )
该宏用来测试一个表达式的值是否为真。如果为假,则会中断程序的执行,并提示出错的地方。
assert一般用于程序的调试,它与NDEBUG标示符相关联,如果该标示符定义了,则assert会失效,即不进行检查。所以assert不能替代程序运行时的逻辑检查或错误检查。
C++预处理器定义了几个有用的变量方便调试:
  • __FILE__ string literal containing the name of the file
  • __LINE__ integer literal containing the current line number
  • __TIME__ string literal containing the time the file was compiled
  • __DATA__ string literal containing the data the file was compiled

5 函数匹配

先看一个例子:
void go();
void go(int);
void go(int, int);
void go(double, double = 1.0);
...
go(3.14);
函数调用会和 go(double, double = 1.0);匹配。
第一步是确认 候选函数。候选函数是指当前可见的(在之前声明过)与调用函数同名的函数。
第二步是从候选函数中选取 可行函数。参数个数必须相同,实参类型必须和形参类型一致或者可以类型转换为一致。在上面的例子中,可以排除没有参数,和有两个int形参的候选函数。有两个double形参的函数因为含有默认初始化,所以可以使用单独一个参数调用。
第三步是从可行函数中选取 最佳匹配函数。衡量匹配度的标准是实参类型与形参类型接近程度。5.6当然与double类型更接近,不需要类型转换,所以上面的例子中应该与go(double, double = 1.0);匹配。
如果有多于一个的参数,则匹配会更复杂。此时选取的最佳匹配函数应该满足:
  1. 每个参数的匹配情况都不必其他函数差;
  2. 至少一个参数的匹配比其可行他函数更佳。
如果没有函数满足上面两点,则会产生歧义,即调用出错。比如:go(5, 5,1);就是一个错误的调用。
为了确定最佳匹配,编译器对类型转换进行了排名:
  1. 准确匹配。包括:形参类型和实参类型相同;实参是从数组或函数类型转换成对应的指针类型;在实参中加入或丢掉顶级const。
  2. 通过const转换匹配
  3. 通过类型提升匹配
  4. 通过算术或指针转换匹配
  5. 通过类类型转换匹配

6 指向函数的指针

一个函数的类型由它的返回值和参数决定,函数名不是类型的一部分。所以我们可以定义指向函数的指针:
bool cmp(const void *a, const void *b);
bool (*pf)(const void *a, const void *b) = cmp;
函数指针必须赋值为与其类型相同的函数,不能指向不同类型的函数。当然,函数指针可以指向重载函数,可以作为函数的参数或返回值。由于函数指针的声明比较复杂,我们可以利用新标准的decltype和auto关键字来简化。
typedef decltype(cmp) FUNC_TYPE; // 
函数指针作为函数返回值:
FUNC_TYPE *getCmpFunc(); // decltype返回的是函数类型而不是函数指针
using PFUNC_TYPE = bool (*)(const void *, const void *);
PFUNC_TYPE getCmpFunc();
auto getCmpFunc() -> bool (*)(const void *, const void *);




你可能感兴趣的:(c++,C++技术学习,C++,函数,inline,assert,函数指针)