【C++ Primer】函数

    函数是一个命名了的代码块,通过调用函数执行相应的代码,函数可重载。

 

一、函数基础

    一个典型的函数定义包含:返回类型、函数名字、零个或者多个实参、函数体。执行函数的第一步是用实参初始化形参。return有两项工作:1、返回值(如果有的话);2、将控制权从被调函数转移回主调函数

 

int fact(int val)
{
    int ret = 1;
    return ret > val ? ret : val;
}
int main()
{
    int j = fact(5);
    return 0;
}

    形参和实参:实参是形参的初始值;实参的类型必须与对应的形参类型匹配(或者可以实参可以隐式转换为形参),数量也必须匹配(除非提供了默认实参);

 

 

 

    【Note】:

    1)C++中的函数必须有返回类型,不需要返回值时可以指定其类型为void,只能有一个返回值。

    2)形参列表可以为空(显式(void)或者隐式()),但是不能省略,任意两个形参都不能同名。

    3)函数返回类型不能是数组和函数,但是可以返回指向数组或者函数的指针。

 

1、局部对象

    在C++中,名字有作用域,对象有生命周期形参和函数体内定义的变量统称为局部变量。仅在函数的作用域内可见,存储在栈上,函数结束时被销毁。因此不要返回局部对象的引用或者指针局部变量如果没有初始化则会产生未定义的值

    自动对象:只存在于代码块({...})执行期间的对象,执行时才分配内存。形参是一种自动对象。

    局部静态对象:某些时候,有必要令局部变量的声明周期贯穿函数调用之后的时间,可以将局部变量定义为static类型。static对象一直都在内存中,每次都被初始化为上次调用的值。内置类型的局部静态变量初始化为0只能使用常量表达式初始化局部静态变量

 

int global = 100;//局部静态变量,外部链接性(可以在其他文件中使用)。
static int a = 50;//局部静态变量,内部链接性(只能在本文件中使用)。
void fun()
{
    //局部静态变量,没有链接性(在其他地方被使用),一直在内存中。
    static int cnt = 0;//每次都被初始化为上次调用的值。
    //局部自动变量,没有链接性,运行时分配内存。
    int b = 1;
    return ++cnt;
}
int main()
{
    for (size_t i =0 ; i < 10 ; ++i)
	cout << fun() << endl;//计算函数被调用了多少次。
    return 0;
}

 

2、函数声明

 

    函数三要素:返回类型,函数名,形参列表(最好写上形参的名字)。含有函数声明的头文件(.h)应该被包含到定义函数的源文件(.cpp)中

 

二、参数传递

    形参初始化机理与变量初始化一样。

 

1、传值参数

    当初始化一个非引用的变量时,初始值被拷贝给变量。当指针作为参数时,拷贝的是指针的值。

 

void reset(*p)
{
	*ip = 0;//改变ip所指的值。
	ip = 0;//ip的局部拷贝被改变了,不会影响实参。
}

int i = 42;
reset(&i);//改变i的值。
cout << i << endl;//输出0。


2、传引用参数建议使用此方式

 

    拷贝大的类型对象或者容器对象比较低效,甚至有的类型不支持拷贝,此时只能通过引用传递参数。

 

void reset(int &i)
{
	i = 0;//改变i的值。
}

int i = 42;
reset(i);
cout << i << endl;//输出0。

    【Note】:

 

    1)如果函数无需改变形参的值,最好将其声明为常量引用

    2)如果函数需要返回“多个值”(例如返回字符出现的总次数及其位置),此时可以在函数形参列表中添加一个引用参数。

 

string::size_type find_char(const string &s,char c,string::size_type &occurs)
{
	/***/
}

 

 

3、const形参和实参

   当使用实参初始化形参时会忽略掉顶层const,因此const普通形参不可以重载

 

void fun(const string s,char c) { /***/ }
void fun(string s,char c) { /***/ }

    我们可以使用非常量初始化一个底层const对象,但是反过来不行;同时一个普通的引用必须使用同类型的对象初始化(我们不能把const对象、字面值常量传递给普通的引用形参)。因此const引用或者指针形参可以重载

 

 

void fun(const string &s,char c) { /***/ }
void fun(string &s,char c) { /***/ }

   【Note】:
    1)尽量使用常量引用,可以提高程序的效率

 

 

 

 

4、数组形参

    数组的两个特殊性质:不允许拷贝数组;使用数组名字时会将其转换为指针使用数值时注意不要越界

 

void fun(const int*);
void fun(const int[]);//传递数组时,实际上是传递指向首元素的指针。
void fun(const int[10]);//数组的大小对函数调用没有影响。

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

    管理数组的方式:标准库(begin,end)、显示传递一个表示数组大小的形参(size_t等)。

    传递多维数组,实际上传递的是指向首元素的指针

 

void fun(int (*matrix)[10]);

 

 

5、main:处理命令行选项

 

int main(int argc, char *argv[]) { /***/ }
int main(int argc, char **argv) { /***/ }

    第二个参数是数组,argv是一个指针数组(每个元素都是指针),它的第一个元素指向程序的名字或者一个空字符串,接下来的元素依次传递命令行提供的实参。第一个参数argc表示数组中字符串的数量。例如:

 

 

prog -d -o ofile data0

 

    argv[0]保存程序的名字,可选参数从argv[1]开始。

 

6、含有可变参数的函数

    为了能处理不同数量实参的函数,C++11提供了两种方法:

   1)如果所有实参数量未知,但是类型相同,可以传递一个名为initializer_list【C++11】的标准库类型(类似于容器)。

   2)如果实参类型不同,可以使用可变参数模板。

 

void error_msg(ErrCode e,initializer_list<string> il)//可以拥有其他形参。
{
	cout << e.msg() << ": ";
	for (const auto &elem : il)
		cour << elem << " ";
	cout << endl;
}

if(expected != actual)//expected和actual都是string对象。
{
	error_msg(ErrCode(42),{"FunctionX",expected,actual});//必须使用列表初始化。
}
else
{
	error_msg(ErrCode(0),{"FunctionX","ok"});
}

 

三、返回类型和return语句

 

1、无返回值函数

    返回值为void的函数,可以没有return语句(被隐式执行)。

 

2、有返回值函数

    return语句返回值的类型必须与函数的返回类型相同,或者能隐式地转换为函数的返回类型。只能通过一条有效的return语句退出。

    如果函数返回引用,则该引用仅仅是它所引用对象的别名,引用返回的是左值

 

char &fun(string &s,string::size_type ix)
{ 
    return s[ix];
}

string s("abc");
fun(s,0) = 'A';//如果是常量引用,就不能赋值。
cout << s ;//输出Abc。

    【Note】:

    1)不要返回局部对象的引用或者指针。函数结束时,局部对象所占用的空间也随之消失,所以局部对象的引用和指针都指向了不存在的空间,因此会发生内存错误。

    2)如果函数的返回类型不是void,则必须有返回值(main函数除外)

    3)main函数不能递归调用

 

3、返回数组指针

    因为数组不能被拷贝,所以函数不能返回数组。但是可以返回数组的指针或者引用。

 

#include <bits/stdc++.h>
using namespace std;
int odd[] = {1,3,5,7,9};//使用全局数组,存储在全局/静态存储区。
int even[] = {0,2,4,6,8};
auto func(int i) -> int(*)[5]//使用尾置类型返回
{
    return (i % 2) ? &odd : &even;
}
int main(int argc, char const *argv[])
{
	int (*arr)[5];//arr是一个指针,指向含有5个元素的数组。
	arr = func(3);//其实arr也是一个**类型。
	for (int i=0 ; i<5 ; ++i)
		cout << (*arr)[i] << " ";
	system("pause");
	return 0;
}

    任何函数的定义都能使用尾置类型返回【C++11】,这种形式对于返回类型比较复杂的函数最有效。比如返回数组的指针或者引用。
 

 

四、函数重载

    重载(overloaded)函数:同一个作用域内的几个函数名字相同,但是形参列表不同

    【Note】:

    1)main函数不能重载。

    2)最好只重载那些确实非常相似的操作。

    3)在C++的编译器中,如果出现了函数重载和函数带有默认参数(可以省略,所以会和函数重载冲突)时,编译是会出错的。

 

1、重载的几种方式

正确的重载:

    1)形参列表的类型或者数量不同:

 

int lookup(int a);
int lookup(double b);
int lookup(int a,double b);

    2)如果形参类型是指针或者引用,那么通过区分是常量对象或者非常量对象可以实现重载:

 

 

int lookup(const int &a);
int lookup(int &a);
int lookup(const int *);
int lookup(int *);

错误的重载:
    1)不允许除返回类型外其他的要素都相同,即返回类型不能作为判断依据(C++有时候会忽略返回类型):

 

 

bool lookup(int a);
int lookup(int a);

    2)形参的名字不能作为判断依据:

 

 

int lookup(const int &a);
int lookup(const int &);

    3)顶层const不能作为判断依据:

 

 

 

 

int lookup(int a);
int lookup(const int a);

     使用函数匹配来调用重载的函数,可能有三种结果:最佳匹配、无匹配、二义性调用。

 

 

 

 

 

2、重载与作用域

 

#include <bits/stdc++.h>
using namespace std;
string read();
void print(const string &);
void print(double);
void foobar(int val)
{
	bool read = false;//隐藏了外层的read。
	string s = read();//错误:read是bool是类型。
	void print(int);//在局部作用域内声明函数不是一个明智的选择。
	print("value");//错误:print(const string &)被隐藏掉了。
	print(val);//正确:当前print(int)可见。
	print(3.14);//正确:调用print(int),print(double)被隐藏掉了。
}

    【Note】:
    1)在C++语言中,名字查找发生在类型检查之前。

 

 

 

    2)如果我们在内层作用域中声明名字,将隐藏外层作用域中声明的同名实体

    3)在局部作用域内声明函数不是一个明智的选择。

    4)函数可以嵌套声明,可以嵌套调用,但是不能嵌套定义

 

五、特殊用途语言特性

1、默认参数

    在某些函数中有这样一种形参,在函数的很多次调用中,它们都被赋予相同的值,把反复出现的值称为默认实参

 

using string::size_type sz;
string screen(int x, int y, sz ht = 24, char bg = ' ');

screen(1,2);//等价于screen(1,2,24,' ');

    【Note】:

 

 

 

    1)一旦某个形参被赋予了默认值,后面所有的形参都必须有默认值。所以尽量把默认实参放在后面

    2)只能省略尾部的实参。

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

    4)通常,应该在函数声明中指定默认参数,并将该声明放在合适的头文件中。

 

2、内联函数

    将函数指定为内联函数,通常就是将它在每个调用点上“内联地”展开(非拷贝)。

 

inline const string &ShortString(const string &s1,const string &s2)
{
	return s1.size() <= s2.size() ? s1 : s2;	
}

    内联机制用于优化规模较小、流程直接、频繁调用的函数
 

 

3、constexpr函数

    constexpr函数是指能用于常量表达式的函数,函数的返回类型及所有形参的类型都必须是字面值类型

    constexpr函数在编译期就能获得结果,编译器把对constexpr函数的调用替换成其结果值。constexpr不一定返回常量表达式。尽量把一些简单、容易得到结果的函数作为constexpr函数。

 

constexpr int new_sz() { return 42; }
constexpr int foo = new_sz();

    把内联函数和constexpr函数放在头文件中。

 

 

 

 

 

4、assert预处理宏

    assert是一种预处理宏,assert使用一个表达式作为其条件。

 

assert(expr);

 

    assert()是一个调试程序时经常使用的宏,在程序运行时它计算括号内的表达式,如果表达式为FALSE (0), 程序将报告错误,并终止执行。如果表达式不为0,则继续执行后面的语句。这个宏通常原来判断程序中是否出现了明显非法的数据,如果出现了终止程序以免导致严重后果,同时也便于查找错误

以下是使用断言的几个原则:

(1)使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。
(2)使用断言对函数的参数进行确认,每个assert只检验一个条件。
(3)在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?”一旦确定了的假定,就要使用断言对假定进行检查。
(4)一般教科书都鼓励程序员们进行防错性的程序设计,但要记住这种编程风格会隐瞒错误。当进行防错性编程时,如果“不可能发生”的事情的确发生了,则要使用断言进行报警。
 

六、函数匹配

 

void print();
void print(double);
void print(int val);
void print(const string &);

    函数匹配的步骤:

    1)选择候选函数:其与被调函数同名,而且其名在调用点可见。

    2)选择可行函数:其形参数量和类型与本次调用提供的实参相对应。

    3)寻找最佳匹配:如果没有的话,报告错误。

    【Note】:

    调用重载函数时要尽量避免强制类型转换

 

七、函数指针

    函数指针指向的是函数而非对象。虽然不能返回函数,但是可以返回指向函数的指针。

 

#include <bits/stdc++.h>
using namespace std;
int add(int a,int b){return a+b;}
int multiply(int a,int b){return a*b;}
int subtract(int a,int b){return a-b;}
int divide(int a,int b){return b!=0 ? a/b : 0;}
bool L_M(const string &s1,const string &s2)
{
	return s1.size() > s2.size() ? true : false;
}
int main()
{
	auto pf = &L_M;//pf是指向函数的指针。
	cout << pf("ab","abc") << endl;
	typedef int(*p)(int a,int b);//使用typedef定义指向函数的指针。
	vector<p> vec{add,multiply,subtract,divide};
	for (auto f : vec)
	{
		cout<<f(2,3)<<endl;
	}
	return 0;
}

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

~青萍之末~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值