C++ Primer小记 第六章 函数

第六章 函数

6.1 函数基础

函数的形参列表

​ 不带形参的函数,两种定义方法

void f1(){}			//隐式定义
void f2(void){}		//显式定义

6.2 参数传递

6.2.1 传值参数

指针形参

​ 指针的行为和其他非引用类型一样。当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。因为指针使我们可以间接的访问它所指的对象,所以通过指针可以修改它所指对象的值。

//Q:了解指针形参 
void reset(int *p){
	*p = 50;
	cout << "形参p的值:" << p << endl;
    p = 0;
    cout << "p=0执行后,形参p的值:" << p << endl;
}

void e1(){
	int i = 42;
    cout << "i的初始值:" << i << endl; 
    int *q = &i;
    cout << "实参q的值:" << q << endl;
    reset(q);
    cout << "函数运行过后i的值:" << i << endl; 
    cout << "函数运行过后实参q的值:" << q << endl; 
} 

以前很长一段时间都把指针形参等同于引用形参,这是错误的。要注意区分指针形参和引用形参的区别。指针形参还是值传递,引用形参是引用传递。

6.2.2 传引用参数

使用引用避免拷贝

​ 拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型根本不支持拷贝。当某种类型不支持拷贝操作时,函数只能通过引用形参(指针形参不行吗?)访问该类型的对象。

​ 如果函数无需改变引用形参的值,最好声明为常量引用***(const type &i)***。

使用引用参数返回额外信息

​ 想让函数返回多个值,引用形参为我们一次返回多个结果提供了有效的途径。下面总结一下返回多个值的方法。

  • 定义一个新的数据类型,比如XX类。返回这个类,这个类里面有你要的那些值
  • 返回的是某个对象,还想返回其他东西,则形参定义引用,然后通过引用改变外面那个值(实参),最后在函数外使用实参即可得到。
  • 返回类型void,全部使用指针或者引用来完成

6.2.3 const形参和实参

void fcn(const int i){}		//fcn能读取i,但不能像i写值
void fcn(int i){}			//错误,重复定义fcn(int)

​ 上述代码看起来好像是重载,但是利用实参初始化形参时,如果形参有顶层const,传给它的常量对象或者非常量对象都是可以的。那么,当主调函数在调用fcn的时候,用的实参都是非常量对象,那编译器就无法区分到底应该调用哪一个fcn了,因为两个都可以匹配。

6.2.4 数组形参

//一维
void print(const int*);
void print(const int[]);
void print(const int[10]);
void print(int (&arr)[10]);		//数组引用形参

​ 对于多维数组来说,数组的第二维(以及后面所有维度)的大小都是数组类型的一部分,不能省略。

//多维
void print(int (*matrix)[10],int rowSize);		//第二维是10个int的数组,其实这么看*(matrix[10])更好理解
void print(int matrix[][10],int rowSize);

6.2.5 main:处理命令行选项

prog -d -o ofile data0
int main(int argc,char *argv[]){}
int main(int argc,char **argv){}

​ 当实参传给main函数之后,argv的第一个元素指向程序的名字或者一个空字符串,接下来的元素依次传递命令行提供的实参。最后一个指针之后的元素值保证为0

​ 如上面命令行,argc为5

argv[0] = "prog";	//argv[0]保存程序的名字,而非用户输入
argv[1] = "-d";
argv[2] = "-o";
argv[3] = "ofile";
argv[4] = "data0";
argv[5] = 0;

6.2.6 含有可变形参的函数

initializer_list形参

​ 如果函数的实参数量未知但是全部实参的类型都相同,即可用它。注意包含头文件***<initializer_list>***。

​ initializer_list对象中的元素永远是常量值。无法改变initializer_list对象中元素的值。

//Q:initializer_list形参 -- 只有initializer形参 
void error_msg(initializer_list<string> li){
	for(const auto &elem : li){		//还可以使用迭代器来实现 
//		elem = "okay";				//试图改变initializer_list对象中的元素,编译报错。因为initializer_list对象中的元素是常量 
		cout << elem << endl;
	}
} 

void e3(){
	string i = "i is false", j = "j is true";
	if(i == j){
		error_msg({"i == j",i,j,"okay"});
	}else{
		error_msg({"i != j",i,j});
	}
}

//Q:initializer_list形参 -- 还拥有其他形参 
void error_msg(int code,initializer_list<string> li){
	cout << "Err_code is " << code << endl;
	for(auto begin = li.begin(),end = li.end(); begin != end ; ++begin){
		cout << *begin << endl;
	}
}

void e4(){
	string i = "i is false", j = "j is true";
	if(i == j){
		error_msg(520,{"i == j",i,j,"okay"});
	}else{
		error_msg(404,{"i != j",i,j});
	}
}

省略符形参

​ 一般用于与C函数交互的程序接口。这些代码使用了名为varargs的C标准库功能。

​ 省略符形参只能出现在形参列表的最后一个位置,他的形式无外乎两种:

void foo(param_list,...){}
void foo(...){}

​ 在第一种形式中,形参声明逗号后面是可选的。

​ 省略号的优先级别最低,所以在函数解析时,只有当其它所有的函数都无法调用时,编译器才会考虑调用省略号函数的。

由于还涉及到一些新的类型和方法书上没有细说,以后再仔细研究

6.3 返回类型和return语句

​ return有两种形式。

return ;
return expression;

6.3.1 无返回值函数

​ 没有返回值的函数只有void返回值类型的函数。这种函数最后无需显式的写return语句,因为在这类函数的最后一句后面隐式的执行return。

​ 当然,void返回值类型的函数也可以显式的写 return expression 类型的语句。但是,此时return语句的expression必须是另一个返回void的函数(这句其实调用了一个函数)。强行令void函数返回其他类型的表达式将产生编译错误。

6.3.2 有返回值函数

主函数main的返回值

​ 6.3.1里面说过如果函数返回值类型不是void,那它必须返回一个值。除了main函数。如果main函数没有return语句直接结束,编译器将隐式的插入一条返回0的return语句。

​ 为了使返回值和机器无关,***cstdlib***头文件定义了两个预处理变量。

int main(){
    if(flag){
        return EXIT_FAILURE;	//定义在cstdlib头文件中
    }else{
        return EXIT_SUCCESS;	//定义在cstdlib头文件中
    }
}

递归

​ main函数不能调用他自己。

6.3.2 返回数组指针

使用类型别名的方法来定义

typedef int arrT[10];		//或者 using arrT = int[10];
arrT* func();				//返回一个指向含有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类型。

使用尾置返回类型

​ 尾置返回类型跟在形参列表后面并以一个 -> 符号开头。为了表示函数真正的返回类型跟在形参列表之后,我们在本应该出现返回类型的地方放置一个auto

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

​ 注意这个带括号的解引用符,表示func函数返回的是一个指针,并且改指针指向了含有10个整数的数组。

使用decltype

​ 如果我们知道函数返回的指针将指向哪个数组,就可以使用decltype关键字声明返回类型。

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

​ 这里涉及到一个知识点,***decltype***对于数组类型的解析,并不会把数组类型转换成对应的指针。所以*decltype的结果是一个数组。因此需要一个来表示返回的是数组指针。

6.4 函数重载

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

main函数不能重载。

​ 对于重载函数来说,他们应该在形参数量或形参类型上有所不同。

重载和const形参

​ 前面说过如果形参是顶层const,实参可以是常量也可以是非常量。因此,像下面这种情况:

int findNum(Phone);
int findNum(const Phone);

不是重载,因为const是顶层const,实参可以是常量也可以是非常量。还有下面这种情况:

int findNum(Phone*);
int findNum(Phone* const);

​ Phone* const 说明指针是个常量。是顶层const。因此这也不是重载。

​ 当然如果形参是底层const,这样构成重载。因为如果实参是常量,不能传给非底层const的形参,因为可以传的话就相当于可以用形参来改变常量,不符合逻辑。而实参不是常量的话,看起来形参是不是底层const都可以。但是一旦有这种重载,编译器就会区分,如果是非常量编译器会优先选择非常量版本,如果是常量只能选择常量版本。

int findNum(Account&);
int findNum(const Account&);

​ 构成重载,因为第一个不是常量引用,第二个是常量引用,底层const。记住,对于引用来说,不管const怎么放,都是底层const。

int findNum(Account*);
int findNum(const Account*);

​ 同上一个类似,因为const Account* 是指向常量的指针,是底层const。

const_cast 和重载

​ 通过下面的例子来了解用法。前面提过const_cast,是用来显式转换的,只能改变运算对象的底层const。

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);
}

​ 其实上面两个函数,因为形参列表不同且第一个函数const不是顶层const,所以是重载函数。这里主要是展示当需要普通string的时候,如何通过普通的非常量实参,去调用函数。并且返回的也是非常量实参。

​ 利用const_cast来实现,当在第二个函数中调用第一个函数时,利用其把非常量转换成常量,最后返回一个常量引用。注意这个r其实是对s1或s2的一个引用,只是在这里是常量引用,这是符号逻辑的。由于第二个函数最后需要的是非常量引用,因此可以利用const_cast再次转换,然后返回。

6.5 特殊用途语言特性

6.5.1 默认实参

​ 一旦某个形参被赋予了默认值,它后面所有形参都必须有默认值。

int find(int ht = 24 , int wd = 80 , char backgrnd = '');

​ 想要覆盖backgrnd的默认值,必须为ht和wd提供实参。

默认实参声明

​ 同一个函数可以多次被声明,但是在给定作用域中一个形参只能被赋予一次默认实参。换句话说,函数的后续声明只能为之前那些没有默认值的形参添加默认实参,并且该形参右侧的所有形参都必须有默认值(一旦某个形参被赋予了默认值,它后面所有形参都必须有默认值)。

​ 假如给定

int find(int a,int b,char c = '');

​ 我们不能修改一个已经存在的默认值:

int find(int a,int b,char c = '*');		//错误,重复声明

​ 但是可以按照如下形式添加默认实参:

int find(int a = 24,int b = 80,char c);

默认实参初始值

局部变量不能作为默认实参

//Q:默认实参初始值案例
//A:局部变量不能作为默认实参,其他可以,但是要注意位置。 
int ht(){
	return 55;
}

int wd = 100;
char def = 'a';

int myFind(int ht = ht(), int wd = wd, char def = def){
	cout << " ht = " << ht		
			<< " wd = " << wd
			<< " def = " << def << endl;
}

void e5(){
	wd = 200;			//wd值被改变,由于改变的是全局变量wd,且在调用之前改变的,所以默认实参也就改变了 
	char def = 'b';		//局部变量def被改变,并不影响默认实参,局部变量不能作为默认实参 
	myFind();
} 

6.5.2 内联函数和constexpr函数

内联函数可以避免函数调用的开销

​ 在函数的返回类型前面加上关键字***inline***,这样就可以将它声明成内联函数了。内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求。一般来说内联函数用于规模小、流程直接、频繁调用的函数。所以如果函数声明为内联函数,但是规模较大,编译器很可能只是把他当成普通函数来处理。

constexpr函数

​ 定义constexpr函数的方法和其他函数类似,不过要遵循几项约定:

  • 函数的返回类型及所有形参的类型必须是字面值类型(算术类型,引用和指针都属于字面值类型)

  • 函数中必须有且仅有一条***return***语句

    ​ constexpr被隐式的指定为内联函数。

    ​ 我们允许constexpr函数的返回值并非一个常量。

    constexpr int scale(int cnt){
        return 42 * cnt;
    }
    

    ​ 当实参是常量表达式的时候,他的返回值也是常量表达式,反之则不然

    int arr[sacle(2)];		//正确,scale(2)是常量表达式
    int i = 2;
    int a[scale(i)];		//错误,scale(i)不是常量表达式
    

    ​ 记得前面之前说过,constexpr函数就是为了编译的时候就得到结果。而且得到常量的结果。但是constexpr函数不一定返回常量表达式。上面的例子,如果需要的是常量表达式,那就得返回常量表达式,否则就会报错。

    内联函数和constexpr函数放在头文件

    ​ 和其他函数不同,内联函数和constexpr函数在程序中可以多次定义。但是多个定义必须完全一致。所以内联函数和constexpr函数通常定义在头文件中。

    ​ 在这里突然有点疑问。其他函数只能定义一次,内联函数和constexpr函数可以多次定义但必须完全一致,那和一次定义有什么区别吗?为什么这里强调通常把他们定义在头文件中。

    ​ 想了一个解释,因为内联函数和constexpr函数一般来说要较小的函数,所以放在头文件中比较合适。而一般的函数由于要用到很多东西,有时还要使用到对应文件的数据,所以在程序中定义较合适。

6.7 函数指针

​ 函数的类型由它的返回类型和形参类型共同决定,与函数名无关。

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

​ 该函数类型是***bool (const string &,const string &)***。声明一个指向函数的指针,只需要用指针替换函数名即可,bool ( ***** ***pf)(const string &,const string &)***。

使用函数指针

//赋值
pf = lengthCompare;
pf = &lengthCompare;		//和上面是等价的,取地址符是可选的

//使用,无需提前解引用
bool b1 = pf("hello","goodbye");
bool b2 = (*pf)("hello","goodbye");			//等价调用
bool b3 = lengthCompare("hello","goodbye");	//另一个等价调用

注意,不同函数类型的指针不存在转换规则。使用上述的***pf***,只能用于***bool (*** ***** ***pf)(const string &,const string &)***类型的函数。

//Q:函数指针类型及调用的案例
//A:函数指针只能指向同类型的函数,不能转换。 
void func1(int a){
	cout << "func1被调用" << endl;
} 

void func2(int b){
	cout << "func2被调用" << endl;
}

void e6(){
	void (*p)(int) = func1;	//定义函数指针,且指针初始化指向func1 
	p(1);					//通过函数指针的方式调用函数,没有用解引用符 
	p = func2;				//给函数指针重新赋值,这是同类型赋值 
	(*p)(2);					//通过函数指针的方式调用函数,用解引用符 
}

重载函数的指针

​ 注意一点,使用重载函数的指针,必须清楚的界定用哪个函数。因为重载函数形参列表不同,使用重载函数的类型不同,则重载函数的指针就不同。

函数指针形参

​ 函数指针可以作为形参。

void userFuncPointer(void (*pf)(int),int x){		
	pf(x);
} 

​ 当然上面那么写比较麻烦,可以使用别名替代。如下:

//Q:函数指针形参案例
//Func 和 Func2 是函数类型 
typedef void Func(int);
typedef decltype(func1) Func2;	//等价的类型 
//FuncP 和 FuncP2 是指向函数的类型
typedef void (*FuncP)(int);
typedef decltype(func1)* FuncP2;  //等价的类型 
void userFuncPointer(FuncP2 pf,int x){		//FuncP2换成上面任意一种都可以 
	pf(x);
} 

void e7(){
	userFuncPointer(func1,1);
	userFuncPointer(func2,2);
} 

​ 注意一点,decltype返回函数类型,此时不会将函数类型自动转换成指针类型,需要自己加指针,如FuncP2。

返回指向函数的指针

​ 注意是返回指向函数的指针,不能返回函数。不使用类型别名的情况写函数***f1***,返回的是一个函数指针。

int (*f1(int))(int*,int);

​ 从内向外看

  • ***f1(int)***是一个函数。

  • *f1前面有一个。说明要返回指针

  • 这个指针有形参列表,说明返回的是函数指针

  • 最开头的那个***int***,说明函数指针指向的函数,返回类型是***int***

    上述写法较麻烦,可以使用别名。

using F = int(int*,int);		//F是函数类型,不是指针
using PF = int(*)(int*,int);	//PF是函数指针

​ 注意返回的是函数指针,使用返回类型应该是 PF 或者 F***** ,不是***F***。

将auto和decltype用于函数指针类型

​ 注意一点,decltype返回函数类型,不是函数指针

上一篇:C++Primer(第五版 )第二章 变量和基本类型 章节编程练习答案


下一篇:C++ Primer Plus 第十三章知识点(一)