【C++基础总结】函数

C++基础总结:函数

一、函数基础

  • 函数调用过程:
    • 将控制权转移给调用函数,函数隐式地定义并初始化它的形参;
    • 当执行到return语句或者执行完函数内全部语句后返回结果。
  • 实参和形参:实参是形参的初始值
  • 函数的返回类型
    • 大多数类型都可以作为函数的返回类型。void也可以,它表示不返回任何值。
    • 函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。
  • 局部静态对象
    • 局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序最终才被销毁。
    • 局部静态对象只能被初始化一次,不会被多次初始化!如果没有显示初始值,它将执行值默认初始化,内置类型的局部静态变量初始化为0
    • 示例:
    int count_calls()
    {
    	static int count = 0;
    	return ++count;
    }
    
    int main()
    {
    	for(int i = 0; i < 6; ++i)
    		std::cout <<count_calls() << ", ";
    }
    
    输出结果:
    1, 2, 3, 4, 5, 
    

二、参数传递

  • 形参初始化的原理和变量初始化一样。
  • 实参可以通过引用传递或者值传递给形参初始化。

1. 传值参数

  • 通过值传递的形参是实参的拷贝,两者独立,不会相互影响
  • 指针形参
    • 当执行指针拷贝操作时,拷贝的是指针的值。拷贝后,两个指针是不同的指针。所以,我们可以通过指针形参访问或修改实参指向的值,如果修改形参的指向,实参指向并不会因此改变。

2. 传引用参数

  • 拷贝大的类类型对象或者容器对象时效率低,甚至有的类类型根本不支持拷贝操作,此时我们可以使用引用形参访问该类型的对象。
  • 如果函数无须改变引用形参的值,最好将其声明为常量引用。
  • 内置类型初始化一般会比引用效率高,所以内置类型无须使用常量引用,可以使用引用或传值参数。
  • 使用引用形参是方便返回多个结果的有效途径。

3. const形参和实参

  • 某变量或字面值是否可以用作参数传递,可以考虑其是否可以用于变量初始化的值。
  • 我们要尽可能的使用常量引用代替普通引用。因为普通引用会给使用不需要修改参数函数的使用者一种误导,并且普通引用会限制可传入参数类型。我们不能把const对象、字面值或者需要类型转换的对象传递给普通的引用形参。

4. 数组形参

  • 数组有连个特性,分别是:不允许拷贝数组以及使用数组时(通常)会将其转换成指针。
  • 因为不能拷贝数组,所以无法使用值传递的方式使用数组参数。
  • 因为数组会被转换成指针,所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
  • 尽管不能以值传递的方式传递数组,但我们可以把形参写成类似数组的形式:
    //尽管形式不同,但是下面三个函数是等价的
    void print(const int*);
    void print(const int[]);		//函数的用途是作用于一个数组
    void print(const int[10]);		//这里的维度表示我们期望数组含有多少元素,实际并不一定
    
    尽管表现形式不同,但三个函数等价:每个函数的唯一形参都是const int*类型。
    int i =0, j[2] = {0, 1};
    print(&i);  //正确:&i的类型是int*
    print(j);  //正确:j转换成int*并指向j[0]
    
  • 为防止数组指针越界,管理指针形参有三种常用技术:
    • 使用标记指定数组长度
      这种方法是在数组结尾有一个结束标志,典型就是C风格字符数组。字符数组最后跟着一个空字符,函数在处理C风格字符串时遇到空字符停止:
      	void print(const char* cp)
      	{
      		if(cp)								//若cp不是空指针
      			while(*cp)					//若指针所指字符不是空字符
      				cout << *cp++;			//输出当前字符并指向下一个字符
      	}
      
      这种方式适用于有明显结束标记的数组,但对于int这种数组就不太有效了。
    • 使用标准库规范
      第二种方法来自标准库技术,是传入数组首元素和尾后元素的指针。
      	void print(const int* beg, const int* end)
      	{
      		//输出beg和end之间(不含end)的所有元素
      		while(beg != end)
      			cout << *beg++ << endl;  //输出当前元素并将指针向前移动一个位置
      	}
      
      可以通过调用标准库的begin和end函数,代码安全地获取数组首指针和尾后指针
      	int j[2] = {0, 1};
      	print(std::begin(j), std::end(j));
      
    • 显式传递一个表示数组大小的形参
      第三种方法是专门定义一个表示数组大小的形参,在C程序和旧的C++程序中常常使用这个方法。
      	//const int ia[]等价于const int* ia
      	//size表示数组的大小,将它显式地传给函数用于控制对ia元素的访问
      	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));
      
  • 数组引用形参
    C++语言允许将变量定义成数组的引用,所以,形参也可以是数组的引用。引用形参绑定到对应的实参上,也就是绑定到数组上:
    void print(int (&arr)[10])		//&arr两端的括号必不可少,后边的10与传入数组指针形参不同,这个具有实际意义,即数组长度必须为10
    {
    	for(auto elem : arr)
    		cout << elem << endl;
    }
    
  • 传递多维数组
    • 和所有数组一样,当将多为数组传递给函数时,真正传递的是指向数组首元素的指针。
    //matrix指向数组的首元素,该数组的元素是由10个整数构成的数组
    void print(int (*matrix)[10], int rowSize){/* ... */}
    等价于
    void print(int matrix[][10], int rowSize){/* ... */}
    matrix看起来是一个二维数组,实际上形参是指向含有10个整数的数组的指针
    
    int *matrix[10];		//10个指针构成的数组
    int (*matrix)[10];		//指向含有10个整数的数组的指针
    

5. main:处理命令行选项

  • 通常,我们使用空参主函数,但我们也可以给主函数传入参数
int main(int argc, char* argv[]){...}

第一个形参是数组长度,第二个形参是C风格字符串数组,argv指向char*,所以也可以写成:

int main(int argc, char** argv){...}

当我们使用argv中的参数时,一定要记得可选的实参从argv[1]开始,argv[0]保存程序的名字,而非用户输入

6. 含有可变形参的函数

C++11标准提供了两种主要方法:如果所有实参类型相同,可以传递一个名为initializer_list的标准库类型;如果实参的类型不同,我们可以使用可变参数模板,此方法在后续的模板章节中总结。除此之外,C++还提供了一种特殊的形参类型,即省略符形参。

initializer_list形参

  • initializer_list是一种标准库类型,用于表示某种特定类型的值的数组。initializer_list类型定义在同名的头文件中。

  • 和vector一样,initializer_list也是一种模板类型,定义对象时,必须说明列表中所含元素的类型。

  • 和vector不同的是,initializer_list对象中的元素永远是常量值,无法改变initializer_list对象中元素的值

  • 示例:

    	void error_msg(initializer_list<string> il)
    	{
    		for(auto beg = il.begin(); beg != il.end(); ++beg)
    			cout << *beg << " ";
    	}
    
    	error_msg({"functionx", "okay"});		//通过一对花括号传入序列
    
  • initializer_list提供的操作

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

省略符形参
省略符形参是为C++便于访问某些特殊的C代码而设置的,这些代码使用了名为varargs的C标准库功能。通常,省略符形参不应用于其他目的。

省略符形参应该仅仅用于C和C++通用的类型。特别注意的是,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。

省略符形参只能出现在形参列表的最后一个位置

void foo(parm_list, ...);
void foo(...);

我们可以添加cstdarg文件,使用其中定义的函数方法访问省略符形参

#include <cstdarg>

void VarArgFunc(int i, ...)
{
	va_list pArg = nullptr;		//创建参数指针
	va_start(pArg, i)			//初始化参数指针
	int a = va_arg(pArg, int);	//获取第一个参数
	char b = va_arg(pArg, char);	//获取第二个参数
	va_end(pArg);		//清空参数指针
}

三、返回类型和return语句

1. 有返回值函数

  • 返回一个值的方式和初始化一个变量和形参的方式完全一样:返回的值用于初始化调用点的一个临时变量,该临时量就是函数调用的结果。
  • 不要返回局部对象的引用或指针!所以,我们返回的引用或指针需要是函数调用之前就已经存在的变量。
  • 调用一个返回引用的函数会得到左值,其它返回类型得到右值。
  • C++11新标准规定,函数可以返回花括号包围的值的列表。
  • main函数可以没有显式返回值,main函数的返回值可以看作是状态指示器。返回0表示执行成功,返回其他值表示执行失败。为了使返回值与机器无关,cstdlib头文件定义了两个预处理变量,我们可以使用这两个变量表示成功与失败:EXIT_FAILURE和EXIT_SUCCESS
  • 如果一个函数调用了自身,无论是直接还是间接,都称该函数为递归函数。如果函数出现不断调用自身直到程序栈空间耗尽为止,我们称其为循环递归。另外,main函数不能调用自己。

2. 返回数组指针

  • 因为数组不能拷贝,所以函数不能返回数组。但是,函数可以返回数组的指针或引用。
    使用类型别名
typedef int arrT[10];		//arrT是一个类型别名,表示的类型是含有10个整数的数组
using arrT = int[10];		//与上条等价
arrT* func(int i);			//func返回一个指向含有10个整数的数组的指针

声明一个返回数组指针的函数

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

下面的函数声明:
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的新标准,常用语比较复杂的返回类型,比如数组的指针或数组的引用。

下例为函数的声明:

auto func(int i) -> int(*)[10];	//返回类型放到形参之后,原来放置返回类型的地方使用auto

四、函数重载

同一作用域内,函数名相同,形参列表不同的函数称之为重载函数。

main函数不能重载。

重载和const形参
拥有顶层const的形参无法与另一个没有顶层const的形参区分:

Record lookup(Phone);
Record lookup(const Phone);		//重复声明了Record lookup(Phone)

Record lookup(Phone*);
Record lookup(Phone* const);	//重复申明了Record lookup(Phone*)

底层const可以重载:

Record lookup(Account&);		//函数作用于Account引用
Record lookup(const Account&);	//新函数,作用于常量引用

Record lookup(Account*);		//新函数,作用于指向Account的指针
Record lookup(const Account*);	//新函数,作用于指向常量的指针

const_cast和重载

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

五、特殊用途语言特性

1. 默认实参

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

2. 内联函数和constexpr函数

调用函数一般比求等价表达式的值慢一些。大多数机器上,函数调用过程为:调用前要先保存寄存器,并在返回时恢复;可能需要拷贝实参;程序转向一个新的位置继续执行。

内联函数可避免函数调用的开销
在函数声明前加inline,可以将其声明成内联函数。内联函数是向编译器发送请求,将其在编译时直接展开到函数调用处,当然,编译器也可以忽略这个请求。

很多编译器都不支持内联递归调用。

constexpr函数
constexpr函数是指能用于常量表达式的函数。定义constexpr函数需要遵循约定:

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

如果arg是常量表达式,则scale(arg)也是常量表达式:

constexpr size_t scale(size_t cnt){return new_sz() * cnt;}

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

constexpr函数不一定返回常量表达式。

内联函数和constexpr函数通常定义在头文件中。

3. 调试帮助

程序可以包含一些用于调试的代码,但是这些代码只在开发程序时使用,当应用程序开发完成后,要先屏蔽调试代码。这种方法用到两项预处理功能:assert和NDEBUG。

assert预处理宏

  • assert是一种预处理宏,预处理宏是一个预处理变量,行为类似于内联函数。
  • assert宏定义在cassert头文件中。
  • 可以像下面代码一样,检查表达式输出结果,如果表达式为假(0),assert输出信息并中止程序执行;如果为真(非0),程序继续执行:assert(expr);

NDEBUG预处理变量
assert的行为依赖于一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG,则assert什么也不做。默认状态下,没有定义NDEBUG,此时assert将检查运行时检查。

六、函数指针

函数指针指向的是函数,而非对象。函数的类型由他的返回类型和形参类型共同决定,与函数名无关。要想声明一个可以指向函数的指针,只需要用指针替代函数名即可:

bool (*pf)(const string&, const string&);		//未初始化

*pf两端的括号必不可少。如果不写括号,则pf是一个返回值为bool*的函数

使用函数指针
当函数名作为值使用时,该函数自动地转换成指针。

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

我们还可以直接使用指向函数的指针调用该函数,无须提前解引用指针:

bool b1 = pf("hello", "goodbye");			//调用lengthCompare函数
bool b2 = (*pf)("hello", "goodbye");		//一个等价的调用
bool b3 = lengthCompare("hello", "goodbye");//另一个等价的调用

函数指针形参
和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。此时,形参看起来是函数类型,实际却是指针:

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, lengthCompare);

类型别名和decltype可以简化函数指针的代码:

//Func和Func2是函数类型
typedef bool Func(const string&, const string&);
typedef decltype(lengthCompare) Func2;			//等价的类型
//FuncP和FuncP2是指向函数的指针
typedef bool(*FuncP)(const string&, const string&);
typedef decltype(lengthCompare) *FuncP2;		//等价的类型

可以使用以下形式声明useBigger:

void useBigger(const string&, const string&, Func);
void useBigger(const string&, const string&, Func2);

返回指向函数的指针
和数组类似,虽然不能返回一个函数,但是可以返回指向函数类型的指针。
要想声明一个返回函数指针的函数,最简单的方法是使用类型别名:

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

我们必须显式地将返回类型指定为指针:

PF f1(int);		//正确:PF是指向函数的指针,f1返回指向函数的指针
F f1(int);		//错误:F是函数类型,f1不能返回一个函数
F* f1(int);		//正确:显式地指定返回类型时指向函数的指针

我们也能直接声明f1:

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

也可以使用尾置返回类型的方式:

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

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

string::size_type sumLength(const string&, const string&);
string::size_type largerLength(const string&, const string&);
//getFcn函数返回指向sumLength或者largerLength的指针
decltype(sumLength)* getFcn(const string&);

当我们将decltype作用于某个函数时,它返回函数类型而非指针类型。因此,我们显式地加上*以表明我们需要返回指针,而非函数本身。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值