主页
文章
分类
系列
标签
简历
【C++ Primer(edition 5) 06】函数
发布于: 2021-9-13   更新于: 2021-9-13   收录于: Cpp
文章字数: 981   阅读时间: 5 分钟   阅读量:

一、函数基础

  • 函数定义:函数是一个命名了的代码块。
  • 编写函数:包括返回类型、函数名字和0个或者多个形参(parameter)组成的列表和函数体。
  • 调用运算符调用函数:调用运算符的形式是一对圆括号 (),作用于一个表达式,该表达式是函数或者指向函数的指针。
    • 圆括号内是用逗号隔开的实参(argument)列表。
    • 函数调用过程:
      • 1.主调函数(calling function)的执行被中断。
      • 2.被调函数(called function)开始执行。
  • 形参和实参:形参和实参的个数类型必须匹配上。
  • 返回类型void表示函数不返回任何值。函数的返回类型不能是数组类型或者函数类型,但可以是指向数组或者函数的指针。
  • 名字:名字的作用于是程序文本的一部分,名字在其中可见。

1、局部对象

  • 生命周期:对象的生命周期是程序执行过程中该对象存在的一段时间。
  • 局部变量(local variable):形参和函数体内部定义的变量统称为局部变量。它对函数而言是局部的,对函数外部而言是隐藏的。
  • 自动对象:只存在于块执行期间的对象。当块的执行结束后,它的值就变成未定义的了。
  • 局部静态对象static类型的局部变量,生命周期贯穿函数调用前后。

2、函数声明

  • 函数声明:函数的声明和定义唯一的区别是声明无需函数体,用一个分号替代。函数声明主要用于描述函数的接口,也称函数原型
  • 在头文件中进行函数声明:建议变量在头文件中声明;在源文件中定义。
  • 分离编译CC a.cc b.cc直接编译生成可执行文件;CC -c a.cc b.cc编译生成对象代码a.o b.oCC a.o b.o编译生成可执行文件。

二、参数传递

  • 形参初始化的机理和变量初始化一样。
  • 引用传递(passed by reference):又称传引用调用(called by reference),指形参是引用类型,引用形参是它对应的实参的别名。
  • 值传递(passed by value):又称传值调用(called by value),指实参的值是通过拷贝传递给形参。

1、传值参数

  • 当初始化一个非引用类型的变量时,初始值被拷贝给变量。
  • 函数对形参做的所有操作都不会影响实参。
  • 指针形参:常用在C中,C++建议使用引用类型的形参代替指针。

2、传引用参数

  • 通过使用引用形参,允许函数改变一个或多个实参的值。
  • 引用形参直接关联到绑定的对象,而非对象的副本。
  • 使用引用形参可以用于返回额外的信息
  • 经常用引用形参来避免不必要的复制。
  • void swap(int &v1, int &v2)
  • 如果无需改变引用形参的值,最好将其声明为常量引用。

3、const形参和实参

  • 形参的顶层const被忽略。void func(const int i);调用时既可以传入const int也可以传入int
  • 我们可以使用非常量初始化一个底层const对象,但是反过来不行。
  • 在函数中,不能改变实参的局部副本
  • 尽量使用常量引用。

4、数组形参

4.1 数组形参的定义

因为不能拷贝数组,所以无法以值传递的方式使用数组参数,因为数组会转换成指针,所以当为函数传递一个数组时,实际上传递的是指向数组首元素的指针

1
2
3
4
5
6
7
8
9
void print(const int *);
void print(const int[]);
void print(const int[10]); //这里的10只是期望数组含有多少元素,实际不一定

//三种print函数声明等价,形参都是const int* 类型

int i = 0 , j[2] = {0,1};
print(&i);
print(j);

4.2 数组形参的越界问题

要注意在函数中使用数组时,不能越界。管理指针形参有三种常用的技术。

1. 使用标记指定的数组长度 管理数据实参的第一个方法是要求数组本身包含一个结束标记,使用这种方法的典型示例是C风格字符串。C风格的是字符串是以\0为结束的,函数在处理C风格字符串时,遇到空字符停止

1
2
3
4
5
6
void print(const char *cp)
{
	if(cp)                    //非空指针
		while(*cp)            //非空字符
			count << *cp++;
}

2.使用标准库规范 管理数组实参的第二种技术是传递指向数组首元素和尾后元素的指针

1
2
3
4
5
6
7
8
void print(const int *beg , const int *end)
{
	while(beg != end)
		cout <<*beg++<<endl;
}

int j[2]={0,1};
print(begin(j),end(j));

3.显示传递一个表示数组大小的形参 第三种管理方法,就是专门定义一个表示数组大小的形参

1
2
3
4
5
6
7
8
9
void print(const int ia[], size_t size)
{
	for(size_t i=0 ; i != size ; ++i){
		cout<<ia[i]<<endl;
	}
}

int j[] = {0,1};
print(j,end(j)-begin(j));

4.3 数组引用形参

1
2
3
4
5
6
7
8
f(int &arr[10]);    //arr是一个数组,每一个元素都引用变量
f(int (&arr)[10]);  //arr是引用变量,指向一个含有10个元素的数组

int i = 0 , j[2] = {0,1};
int k[10] = {0,1,2,3,4,5,6,7,8,9};
f(&i);   //错误
f(j);    //错误
f(k);    //正确

4.4 传递多维数组

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

5、main处理命令行选项

  • int main(int argc, char *argv[]){...}
  • 第一个形参代表参数的个数;第二个形参是参数C风格字符串数组。

6、可变形参

如果函数的实参数量未知但是全部实参的类型都相同,可以使用initializer_list类型的形参,这是一种标准库类型,用于表示某种特定类型的值的数组。定能一在同名头文件中。

initializer_list提供的操作(C++11):

操作 解释
initializer_list<T> lst; 默认初始化;T类型元素的空列表
initializer_list<T> lst{a,b,c...}; lst的元素数量和初始值一样多;lst的元素是对应初始值的副本;列表中的元素是const
lst2(lst) 拷贝或赋值一个initializer_list对象不会拷贝列表中的元素;拷贝后,原始列表和副本共享元素。
lst2 = lst 同上
lst.size() 列表中的元素数量
lst.begin() 返回指向lst中首元素的指针
lst.end() 返回指向lst中微元素下一位置的指针

initializer_list使用demo:

1
2
3
4
5
6
7
8
void err_msg(ErrCode e, initializer_list<string> il){
    cout << e.msg << endl;
    for (auto bed = il.begin(); beg != il.end(); ++ beg)
        cout << *beg << " ";
    cout << endl;
}

err_msg(ErrCode(0), {"functionX", "okay});
  • 所有实参类型相同,可以使用 initializer_list的标准库类型。
  • 实参类型不同,可以使用可变参数模板
  • 省略形参符: ...,便于C++访问某些C代码,这些C代码使用了 varargs的C标准功能。

三、返回类型和return语句

1、无返回值函数

没有返回值的 return语句只能用在返回类型是 void的函数中,返回 void的函数不要求非得有 return语句。

2、有返回值函数

return语句的返回值的类型必须和函数的返回类型相同,或者能够隐式地转换成函数的返回类型。

2.1 值是如何被返回的

![[c++ 临时对象以及引用的一些知识]] 返回的值用于初始化调用点的一个临时量(除了指针和引用),该临时量就是函数调用的结果。

函数返回的是一个string对象,意味着返回值将被拷贝到调用点,因此,该函数返回word的副本或者一个未命名的临时string对象

1
2
3
4
5
6
7
string make_plural(size_t ctr , const string &word,
								const string &ending)
{
	return (ctr > 1) ? word+ending : word;
}

string word = make_plural(0,word,"s");

如果函数返回引用,该引用仅是它所引用对象的一个别名

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

1
2
3
4
5
6
7
8
9
// 严重错误:这个函数试图返回局部对象的引用
const string &mainp()
{
	string ret ; 
	if(!ret.empty())
		return ret;
	else 
		return "Empty";
}

返回局部对象的引用是错误的;同样,返回局部对象的指针也是错误的。 一旦函数完成,局部对象被释放,指针将指向一个不存在的对象。要想确保返回值的安全,不妨提问:引用所引(指针所指向)的是在函数之前已经存在的哪个对象?

⚠️不要,而不是不能 ![[C++中返回临时变量指针问题]]

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

1
2
//调用string对象的size成员,该string对象是由shorterString函数返回的
auto sz = shorterString(s1,s2).size();

2.4 引用返回左值

函数的返回类型决定函数调用是否是左值。==调用一个返回引用的函数得到左值==;其他返回类型得到右值。

2.5 列表初始化返回值

函数可以返回花括号包围的值的列表。(C++11) 此处的列表也用来对表示函数返回的临时量进行初始化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
vector<string> process()
{
	//string expected , actual
	if(expected.empty())
		return {};
	else if(expected == actual)
		return {"functionX" ,"okay"};
	else
		return {"fuctionX",expected,actual};
}

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

2.6 主函数main的返回值

main函数可以没有return语句直接结束 如果结尾没有return,编译器将隐式地插入一条返回0的return语句。返回0代表执行成功。main的返回值可以看作状态指示器,0表示成功,其他表示失败,其中非0的具体含义根据机器定夺,如果期望返回值与机器无关,则可调用cstdlib头文件中定义的两个预处理变量

1
2
3
4
5
6
7
int main()
{
	if(some_failure)
		return EXIT_FAILURE;
	else
		return EXIT_SUCCESS;
}

其为预处理变量,所以不用在前面添加std::

2.7 返回数组指针

因为数组不能被拷贝,所以函数不能返回数组。不过,函数可以返回数组的指针或引用。虽然从语法上来说,要想定义一个返回数组的指针或引用 的函数比较烦琐,但是有一些方法可以简化这一任务,其中最直接的方法是使用类型别名:

1
2
3
typedef int arrT[10];  //arrT是包含10个整数的数组
using arrT = int[10];  //等价声明 
arrT* func() {...}     //func返回一个指向含有10个整数的数组指针

如果不使用类型别名

1
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类型

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

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

尾置返回类型 使用尾置返回类型 (trailing return type)。任何函数的定义都能使用,但是这种形式对返回类型复杂的函数最有效,比如返是数组的指针或者数组的引用。尾置返回类型跟在形参列表后面并以->符号开头。为了表示函数真正的返回类型跟在形参列表之后,在本应该出现返回类型的地方放置一个auto:

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

四、函数重载

1、重载概念

  • 重载:如果同一作用域内几个函数名字相同但形参列表不同,我们称之为重载(overload)函数。
  • main函数不能重载。
  • 重载和const形参
    • 一个有顶层const的形参和没有它的函数无法区分。 Record lookup(Phone* const)Record lookup(Phone*)无法区分。
    • 相反,是否有某个底层const形参可以区分。 Record lookup(Account*)Record lookup(const Account*)可以区分。
  • 重载和作用域:若在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体,在不同的作用域中无法重载函数名。

2、函数匹配

  • 重载函数匹配的三个步骤:1.候选函数;2.可行函数;3.寻找最佳匹配。
  • 候选函数:选定本次调用对应的重载函数集,集合中的函数称为候选函数(candidate function)。
  • 可行函数:考察本次调用提供的实参,选出可以被这组实参调用的函数,新选出的函数称为可行函数(viable function)。
  • 寻找最佳匹配:基本思想:实参类型和形参类型越接近,它们匹配地越好。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
string read();
void print(const string &);
void print(double);
void fooBar(int val)
{
	bool read = false;
	string s = read(); //WRONG
	void print(int);
	print("Value:");   //WRONG
	print(val);
	print(3.14);
}

五、特殊用途语言特性

1、默认实参

  • string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');
  • 一旦某个形参被赋予了默认值,那么它之后的形参都必须要有默认值。

2、内联(inline)函数

  • 普通函数的缺点:调用函数比求解等价表达式要慢得多。
  • inline函数可以避免函数调用的开销,可以让编译器在编译时内联地展开该函数。
  • inline函数应该在头文件中定义。
1
2
cout << shorterString(s1,s2)<<endl;   //inline
cout << (s1.size() < s2.size() ? s1 : s2) <<endl;   //编译时

3、constexpr函数

  • 指能用于常量表达式的函数。编译器会把所有调用constexpr函数的地方替换成结果值,被隐式的指定成内联函数
  • 函数的返回类型及所有形参类型都要是字面值类型,并且有且仅有一条return语句
1
2
3
4
5
6
7
8
constexpr int new_sz() {return 42;}
constexpr int foo = new new_sz();      //foo是一个常量表达式

//返回值可以不是常量
constexpr size_t scale(size_t cnt){return new_sz()*cnt;}
int arr[scale(2)];
int i =2 ;
int a2[scale(i)];  //WRONG
  • constexpr函数应该在头文件中定义。

4、调试帮助

4.1 assert

预处理宏(preprocessor macro) assert(expr); 如果expr为假(0),输出信息并终止程序的执行 如果为真(1),什么也不做 定义在cassert头文件中,预处理器而不是编译器管理

4.2 NDEBUG

assert的行为依赖于一个名为NDEBUG的预处理变量,该变量定义,则不是执行,未定义,则执行检查

定义NDEBUG的方法

  1. 利用#define NDEBUG 关闭assert
  2. 利用编译器参数控制调试状态: CC -D NDEBUG main.c可以定义这个变量NDEBUG

利用NDEBUG编写条件调试代码 如果定义了NDEBUG,忽略掉调试代码 如果未定义NDEBUG,执行 #ifndef#endif之间的代码

1
2
3
4
5
void print(){
    #ifndef NDEBUG
        cerr << __func__ << "..." << endl;
    #endif
}

预处理器定义的一些变量

变量名 含义
__FILE__ 存放文件名的字符串字面量
__LINE__ 存放当前行号的整型字面值
__FILE__ 存放文件编译时间的字符串字面值
__FILE__ 存放文件编译日期的字符串字面量

六、函数指针

1、函数指针

指向函数的指针bool (*pf)(const string &, const string &); 注:两端的括号不可少。 将函数名作为一个值使用时,该函数自动转换为指针

1
2
bool *pf = func;
bool *pf = &func;  //&可选

2、函数指针形参

形参中使用函数定义或者函数指针定义效果一样。

1
2
3
4
5
6
7
8
void useBigger(const string &s1 , const string &s2,
			   bool pf(const string & , const string &));

void useBigger(const string &s1 , const string &s2,
			   bool (*pf)(const string & , const string &));

//调用
useBigger(s1,s2,func);

过于冗余的形参,可以使用类型别名或者decltype来简化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//func1 , func2 , funcU1 都是函数类型
typedef bool func1(const string & , const string &);
typedef decltype(lengthCompare) func2;
using funcU1 = bool(const string & , const tring &);
//func3 , func4 , funcU2是指向函数的指针
typedef bool(*func3)(const string & , const string &);
typedef decltype(lengthCompare) *func4;
using funcU2 = bool(*)(const string & , const tring &);
//更加简洁的形式
void useBigger(const string &s1 , const string &s2, func1);
void useBigger(const string &s1 , const string &s2, func3);
void useBigger(const string &s1 , const string &s2, funcU1);
void useBigger(const string &s1 , const string &s2, funcU2);

这里直接给函数名字作为形参,会自动转为函数指针

3、返回指向函数的指针

直接声明

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

类型别名 和形参不同,返回类型不会自动识别函数名为函数指针类型,所以要明确的定义

1
2
3
4
5
using F = int(int *, int*);
using Fp = int(*)(int *,int*);

Fp f1(int);
F f2(int);      //WRONG

尾置返回类型

1
auto f1(int)->int(*)(int *,int*);

4、重载函数的指针

定义指向重载函数的指针必须精准匹配

1
2
3
4
5
6
7
void ff(int *);
void ff(unsigned int);

void (*pf1)(unsigned int) = ff;   //pf1指向ff(unsigned int)

void (*pf2)(int) = ff;   //WRONG
double (*pf3)(int *) =ff; //WRONG