C++函数

总列表

函数基础

    先简要说明一下定义:函数是一个命名了的代码块(可以说这个定义十分精简容易理解了)。一个函数可以传入多个参数,并且(通常来说)会产生一个结果(通过return)。
    调用函数时,跟在函数后面的一对圆括号实际上是调用运算符,表示在此处调用函数。

形参列表

    形参列表是函数定义时,设定函数在被调用时,应该传入的实参的数量以及类型。

函数形参列表可以是空的,但是不能省略(从而表示它是一个函数)
void func1(){/*...*/}//隐式的定义一个空形参列表
void func2(void){/*...*/}//显示的定义一个空形参列表

    定义形参列表时,形参之间即使是相同的变量类型,也必须要单独声明——每个形参变量都要单独写出类型

int func3(int index, size){/*...*/}//错误
int func4(int index, int size){/*...*/}//正确

    形参之间是不能同名的,并且,在函数调用时,此函数的外层作用域中的局部变量名不可与形参同名!
    注意形参的命名是不必要的,如下的定义时允许的:

int func5(int , int index);//可见它第一个形参存在,但是没有命名

    函数第一个形参是存在的,只不过我们没有给它命名,同样的,在函数体内,我们也无法使用这个形参。但是无论我们是不是要在函数体中使用这个形参,在实际调用这个函数时,传入的实参都要与形参的数量(一定)以及类型(尽量)保持一致。

局部对象

自动对象(最普通的那种,出作用域销毁)

    形参和函数体内部创建的变量,都属于这种自动对象
    C++中的对象都是由其特定的作用域的(生命周期),函数体是一个语句块,语句块中定义的变量称为局部变量,出了语句块其生命周期结束。语句块内的局部变量会隐藏在外层作用域中的同名其他所有声明。

int func1(int i, int j)
{
	int sum = 10;
	//sum = i+j;
	return sum;
}

int main(void)
{
	int sum = 100;
	int any = func1(1, 1);
}
如上所示,将会返回10给any而不是100,称之为“隐藏”外层作用域中其他所有的声明。

    注意形参和函数内部定义变量的区别:形参不允许与外部作用域中的局部变量同名;但是函数内部定义的局部变量可以与外部作用域中的变量同名,只不过函数内部只会使用函数体内创建的局部变量。

局部静态对象

    是这样的:局部静态对象的生命周期很长:在你第一次调用函数时开始,贯穿整个程序直到结束。

int func1(void)
{
	static int sum = 0;//没有显式的初始化则被隐式的初始化为0
	sum += 1;
	return sum;
}
int main(void)
{
	for(int i=0; i<10; i++)
		cout << func1() << endl;
	return 0;
}

    以上的函数调用将会打印1~10十个数字,在函数被第一调用时,创建这个局部静态对象(并执行初始化),之后的调用不会再执行创建以及初始化了,而是直接进行之后的累加,但是上次的累加值会保留, 因为函数执行完毕后,被创建的局部变量并没有被销毁。(虽然没有被销毁但不代表它有全局变量的属性,即你在函数外部还是不能访问这个变量)。

函数声明

    函数的使用必须在声明之后,与变量一样,函数的定义只有一次,但声明可以有多次!一般我们使用头文件的方法,将声明都写到头文件中,之后用包含头文件的方法来省去不同文件中多次的声明。
    函数可以只有声明没有定义!前提是你永远不会使用到这个函数。

.cpp文件
void func_any(bool arg0, int arg1, char arg2)
{
	//......
}
.h文件
void func_any(bool, int, char)
可以像如上这样,在函数声明时不加以对形参的命名,合法的但是不建议

    因为函数的三要素:返回类型函数名形参类型说明了函数调用所需的全部信息,故像以上一样,函数的声明仅顾全以上的三要素即可,对于函数的形参名可以不加以说明。但加上形参名一般可以方便我们更好的去理解参数的意义,故还是建议加上。

分离式编译(这里先不详细展开,用到后我会补充)

参数传递

    每次函数调用的时候,形参会被重建,用传入的实参对形参进行初始化。参数的传入也分几种,分别是:传值参数传引用参数传指针参数

传值参数(包括传指针)

    实参的值拷贝后赋值给形参。这样,对形参进行的任何改变都不会影响到实参。
    对于指针形参,传入的指针值也会被拷贝,通过这个指针指向的地址,我们可以间接的访问和改变“实参”(对象)的值。

void reset(int *ip)
{
	*ip = 0;
	ip = 0;
}
int main(void)
{
	int num = 100;
	int *loc = &num;
	reset(loc);
	return 0;
}

    需要注意的是,传入的指针也是拷贝过去的,你可以改变指针指向的对象,但是对指针本身是不可以通过传指针值的方式改变的。于是执行完以上的reset()函数后,loc值是没有改变的,num被reset为0。

传引用参数

    引用是绑定到对象上的,引用本身并不是对象,但是当你改变引用时,实际上是在对其绑定的对象进行改变。于是,通过传引用的方法,允许再函数内对传入的实参进行修改。

void reset(int &i){i = 0;}
int j = 100;
reset(j);
通过这种方式调用传入j,注意j并不是一个引用,而是一个对象但是无妨,可以这样理解:
传入参数时,i被初始化为绑定到对象j上的引用。这样就很容易理解了。

    关于参数传递初始化的过程需要注意,通过以上的例子你就明白了传引用,传入的实参的确是要一个确定的对象,因为只有确定的对象才可以被形参(引用类型)绑定。而进行值传递是,必须是特定的值、对象或指针。这样也就不难理解为什么传值参数可以用字面值实参而传引用对象不能使用字面值,必须是对象。

使用传引用以避免拷贝操作

    这是为何要使用传引用参数的好处之一:传值即拷贝,但一些数据太大,拷贝是低效的;或者一些数据类型(可能是自定义的)根本就不支持拷贝操作。所以使用传引用参数来避免拷贝。如果我们本身又想避免(其实就是根本不需要)对这些数据进行改变,那么我们就将这些形参定义为常量引用

使用传引用以“返回”额外的信息

    是这样:虽然我们只能通过return语句显式的返回一个对象,但是可以通过改变传入的参数来使这些传入的参数作为返回信息的储存容器,因为传引用可以改变实参,所以这提供了一种(隐式的)“返回”额外信息的方法。(当然传值中的传指针参数也是一个道理)
    使用过arm系列单片机的同学也可以参考其math库——arm_math.h,其中的fft函数库中的求傅里叶变换函数的output参数就是这以种方式返回一个额外的数组信息的(只不过C语言只支持传指针)。

const形参和实参

    形参的初始化和一般变量对象的初始化相同,对于形参的顶层const属性会被忽略掉(仅在初始化阶段),所以当形参被定义为const时,传入的实参是普通对象、字面值、const对象都是可以的。但注意如果定义的对象是普通的形参变量,则不允许传入一个const对象。
    以下通过对象的初始化进行说明:

int index = 66;

const int *cp = &i;//正确:但是不可以通过cp来改变i
const int &r = i;//正确:但是不可以通过r来改变i
const int &r2 = 66;//正确

int *p = cp;//错误,类型不匹配
int &r3 = r;//错误,类型不匹配
int &r4 = 66;//错误,类型不匹配
不过你将有const关键字和没有const关键字的定义顺序交换一下就是合法的

    以下通过函数调用的举例进行说明:

int i = 0;
const int ci = i;
reset(&i);//合法,这里我们调用的是传指针参数的reset()函数
reset(&ci);//错误,不能用指向const int的对象指针初始化int*
reset(i);//合法,这里我们调用的是传引用参数的reset()函数
reset(ci);//错误,不能将普通引用绑定到const对象上
reset(66);//错误,引用怎么能绑定一个不是对象的字面值呢?!

    一般来说,我们推荐将传引用都定义成const类型,这样它就可以接受任意类型的实参(无论是不是const类型或字面值都可),前提是我们不会改变这个将要传入的实参;但是,如果我们想通过传引用的方式返回额外的信息(刚刚提到过),需要对传引用的实参进行改变,则不能加const关键字!

数组形参

    C/C++中规定数组是无法直接拷贝的,在想专递数组参数时只能考虑使用传指针的方式(数组首地址)。传递数组实际上是传递数组中首元素的指针。
    如下是定义传数组参数的几个示例

注意:尽管定义(声明)的形式不同,但实际上以下的定义(声明)是等价的,调用时,
编译器只会检测传入的参数是不是int*类型
void print(const int*);
void print(const int[]);
void print(const int[10]);
虽然最后一个声明说明了数组的长度,但实际长度并不一定与其相等,这里只是一个预估,
实际可以比这个长或短。
函数传参数组遍历长度管理

1、带有特定结束符的数组
    这种数组常常以特定的结束符作为数组结束的标记,比如以下的C风格字符串,以“\0”结束:

void print(const char* str)
{
if(str)//若指针非空
	while(*str)//若指针不是空字符
	{
		cout << *str << endl;
		str++;
	}
}

2、使用首尾指针进行传参

void print(const int *begin const int *end)
{
	while(begin!=end)
	{
		cout<<*begin<<endl;
		begin++;
	}
}

    调用时一般可以使用如下方法

int *Array[] = {1021,2233,34,222,.....};
print(Array, Array+sizeof(Array)/sizeof(*Array));
传入首地址 以及 首地址加上数组长度

3、直接显式的传递一个表示数组大小的参数

void print(const int[] xxx, size_t size)
{
	for(size_t index; index < size; ++index)
		cout << xxx[index] << endl;
}

    **注意:**以上均使用const限定符来传入数组,当需要改变数组内元素时,则去掉const限定符。

数组引用
void print(int (&arr)[10])//注意括号必不可少,且数组维度10是引用类型的一部分
{
	for(auto elem : arr)
		cout << elem << endl;
}

    很明显,既然维度也是类型的一部分,那么这种方式必然限制了数组的传入:只能是同维度的数组。

int i = 0;
int j[2] = {1,2};
int k[] = {0,1,2,3,4,5,6,7,8,9};
print(i);//错误,实参不是含有10个整数的数组
print(j);//错误,实参不是含有10个整数的数组
print(k);//正确
多维数组传参

    C++语言中没有真正意义上的多维数组,“多维数组”只不过是数组的数组(元素是数组的数组,数组嵌套)。同样的,传入数组也是传入指针,如下示例:

void print(int (*matrix)[10], int dimension_size)
{/*...*/};
指向数组的首元素,元素是一个有10int型元素的数组,以下定义与其等价
void print(int matrix[][10], int dimension_size)
{/*...*/};

    以上定义的函数中,可以看到,数组的第一维是可以省略的。除此之外,多维数组后面的维度都要明确说明size,因为这实际上关系到元素的类型,必须明确。

main处理命令行选项(这个之后用到再详细说明,会附加链接)

含有可变形参的函数(之后详解)

    主要介绍initializer_list形参。由于一些情况下我们也不清楚应该对函数传入多少个参数,这种列表为其提供了方便,可以让使用者传入相同类型、不确定个数的参数。

initializer_list:标准库模板类型,以下是对其操作

initializer_list<T> lst;//默认初始化为空表
initializer_list<T> lst{a,b,c,d,...};
//初始化一张表,lst中的元素数量和初始定义(到时候传入的)的一样多,列表中的元素都是const
//关于调用方法以及定义,使用时进行说明,没有使用无法体会到真谛!

返回类型以及return语句

    return语句的意义在于:终止当前正在执行的函数,并将控制权交给正在调用当前函数的地方。其两种形式如下
    return;
    return expression;

    注意:void函数内也可以return,一般情况下,在void函数的最后,它会隐式的执行return;不需要我们书写,但如果需要中途断出,也是可以加入return语句的,但注意只可以使用以上的第一种用法。(其实也可以使用第二种用法,返回的expression必须也得是一个void类型的函数。否则将产生编译错误)

bool tuto_6_30(const string &str1, const string &str2)
{
	if (str1.size() == str2.size())
		return str1 == str2;
	auto size = (str1.size() < str2.size()) ? str1.size() : str2.size();
	for (decltype(size) index = 0; index != size; ++index)//取较短的长度进行遍历
		if (str1[index] != str2[index])//比较
			return false;//这步没有返回值也会报错

	return true;//走到这步说明都相等
	//最后没有return 会报错,非常重要,在含有循环语句的循环后也要有return,如果没有则认为程序错误!!
	//(很多编译器不报这个错误,但是必须要理解)
}

值的返回方式

    实际上值的返回和参数传递是相同的,都是先会对原值进行拷贝,之后用拷贝后的值进行赋值/初始化操作。return后面的语句表达式会先创建一个临时变量,之后用表达式的值为临时变量赋值,之后返回。
    如果是引用,则创建的临时对象是表达式中引用的别名(另一个副本),实际上返回的还是原来的那个对象(和返回原引用没什么区别)。
    **注意:**局部对象的值是可以被返回的,但局部对象的指针或引用不能返回,因为函数结束后,其占用的内存空间被释放掉,相对应的指针或引用也就失去了意义。

函数返回左值

    只有当为返回为引用时,函数得到的是左值。 我们可以简单理解为:函数是可以被赋值的

char &get_val(string &str, string::size_t ix){return str[ix]};
函数的作用是返回输入字符串的第ix+1个字符的引用,以上是传引用类型,左值返回
int main(void)
{
	string sss = "Glaurung, the Worm of Greed";
	cout << sss << endl;
	get_val(sss, 0) = 'g';
	cout << sss << endl;
	return 0;
}
在main函数中调用get_val(),作用是可以将sss字符串中的第一个字符改为'g'
注意,仅当返回引用的函数可以这么用,并且此引用不能是const类型;
返回值(包括返回指针)的函数,返回的都是右值,不允许这样调用。

递归

    递归就是函数调用它本身,与迭代(就是循环)要区分清楚。下面仅给出一个阶乘的示例。

int factorial(int val)//计算val的阶乘
{
	if(val > 1)
		return factorial(val-1)*val;
	else
		return 1;
}

    关于递归的学问可就大了,很多算法都有用到,之后这部分会单独列出一个新的页面进行说明。

返回数组

    数组不能直接被返回,但可以通过返回其指针或引用的方式来返回一个数组。常用的方法就是通过定义类型别名:

通过定义类型别名
typedef int arr_t[10];
using arr_t = int[10];
以上是两个类型别名的等价定义,表示arr_t的类型是一个包含10个整数的数组
arr_t *func(int i);//声明了一个返回10个整数数组指针的函数 
不使用类型别名的方式

    不用类型别名也可,在之前字符串、向量、数组的章节有过详细介绍说明,如下使用方式的模板以及示例:
*Type ( function( parameter_list ) )[dimension]

int function(int i);//返回一个int型变量的函数
int (*function(int i));//返回一个int型指针的函数,可以对它解引用来返回一个值
int (*function(int i))[10];//返回一个有10个int型变量组成的数组的指针
尾置返回类型

    这个是我认为最能接收,最好理解的一种用法:

int function(int i) -> int(*)[10];
解释为:
function()函数,传入参数是一个整型变量;
返回一个指针,该指针指向一个含有10个整数的数组
使用decltype

    如果我们在定义函数之前,就明白函数要指向哪个数组(或哪些数组中的一个),那么可以使用以下的方式:

int any_arr_1[] = {1,2,3,4,5};
int any_arr_2[] = {6,7,8,9,0};
decltype(any_arr_1) *arr_ptr(int i){return (!i)?any_arr_1:any_arr_2};

any_arr_1和any_arr_2是同一类型,维度相同的数组,可以这样做来指定返回类型。

待续未完

待解决问题 / 待补充问题

1、说:函数形参不能与其外层作用域中的局部变量同名,那么在当前cpp文件中声明过的全局变量可以和形参同名吗?
2、书本中提到:形参名是不必要的,但我们在函数体中无法使用未命名的形参。有时确实会定义在函数体中未用到的形参,此时我们可以不给它命名,但这样的形参存在的意义是什么呢?你能举例说明吗?
3、局部对象第一个代码块:对于在当前cpp文件中的全局变量呢?函数体内部将会如何处理?报错?还是用内部定义的局部变量?还是使用全局变量?
4、只有声明没有定义的函数,这种函数永远也不会使用到,那么意义何在呢?
5、说了很多关于“意义何在”的问题探讨,或许我想其本身就不存在什么意义不意义的,只是编译器允许这样做罢了?如果我真的找不到其他解释了,这条可能算是一种“合理解释”吧!
6、关于传参数的顶层const和底层const的问题你还是没搞太明白!!!第一部分的基础还要从头过一遍。
7、数组引用的第二个代码段,如何编写一个函数,使其形参是数组引用,但是可以传入任意长度的数组?
8、关于列表返回值,返回一个序列?如何承接?
9、关于尾置返回类型:你只给出了声明,实际定义的之后函数体要放到什么地方呢,自己使用一次试试看。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值