6.1 函数基础
1.函数是一个命名了的代码块,我们通过调用函数执行相应的代码。函数可以有0个或者多个参数,而且(通常)会产生一个结果。可以重载,也就是说,同一个名字可以对应几个不同的名字。
2.一个典型的函数定义包括以下部分:返回类型,函数名字,由0个或多个形参组成的列表以及函数体。其中,形参以逗号隔开,形参的列表位于一对圆括号之内。函数执行的操作在语句块中说明,该语句块成为函数体。
3.我们通过调用运算符来执行函数。调用运算符的形式是一对圆括号,它作用于一个表达式,该表达式是函数或者指向函数的指针:圆括号之内是一个用括号给的实参列表,我们用实参初始化函数的形参。调用表达式的类型就是函数的返回类型。
void func(int a, char c) // 形参
{
}
int main()
{
func(1, 'a'); // 实参,与形参的类型、数量相匹配
return 0;
}
4.名字有作用域,对象有生命周期。
名字的作用域是程序文本的一部分,名字在其中可见。
对象的生命周期是程序执行过程中该对象存在的一段时间。
局部变量:形参和函数体内部定义的变量统称为局部变量。仅在函数的作用域内可见,同时,局部变量还会隐藏在外层作用域中同名的其他所有声明中。
5.自动对象:我们把只存在于块执行期间的对象称为自动对象。当块执行结束后,块中创建的自动对象的值就变成未定义的了。形参就是一种自动对象。
6.局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。
int count()
{
static int cnt=0;
return ++cnt;
}
//函数会自动统计被调用多少次
//对于局部静态对象,若cnt没有显式初始化,那么cnt会默认初始化
①内置基本类型的局部静态变量如果没有显示地初始化,就会被初始为 0,且只会初始化一次
②局部静态变量会被存储到存储器的全局量区,生命周期贯穿函数调用以及之后的实际直到程序终止才会被销毁
③局部变量的生命周期超出函数的作用域范围,但是局部变量只能在其所在函数的作用域范围内使用
#include <iostream>
using namespace std;
int counter()
{
static int static_cnt = 0;
int stack_i = ++static_cnt;
return stack_i;
}
int main()
{
for (int i = 0; i < 5; ++i) {
cout << counter() << endl;
}
return 0;
}
7.函数声明
声明(函数原型,)不包含函数体(返回值类型还是需要的),且形参列表可以只保留参数的类型而省略参数名称,最后需要以";"结束。函数在被调用之前一定要被声明或被定义。
// 函数的声明,必须要写在函数第一次被调用之前。
// 这部分的代码可放到头文件中,用的时候include进来就可以了(分离式编译)
int func(char, int);
// 只要函数在被调用前声明了,那函数的定义可以写在声明之后的任意的位置,
// 如这里的func就可以在声明之后、main函数之前定义;
// 如果func没有在调用前声明,则必须要在被调用前定义(相当于把main函数后面的func那段代码放在这里实现)
int main() {
return func('a', '1'); // 函数的调用,虽然func在main后定义,但是因为之前对func进行了声明,所以编译器知道这个函数的三要素是啥
}
/*
* 函数的定义的参数列表中各参数的类型、数量以及位置等需要和声明时的相匹配
*/
int func(char c, int i)
{
// do something
return 0;
}
8.分离式编译:允许我们把程序分割到几个文件中,每个文件独立编译。
6.2 参数传递
1.当形参是引用类型时,我们说它对应的实参被引用传递或者函数被传引用调用。
当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被值传递或者函数被传值调用。
- 值传递传的是变量的拷贝,而不是原始的值
- 在修改指针参数指向的内存空间存储的数据时,应使用操作符*
- 操作引用参数实际操作的是参数所引用的对象,使用引用传递可以避免拷贝,比如说如果vector对象中存储了大量的元素,而其作为参数传入时不需要进行修改,则可以传递引用,这样就避免了拷贝其中的大量元素,提高效率
- 使用引用参数可以让函数返回额外的信息
-
函数的顶层const形参会被忽略掉,重载时需注意
#include <iostream>
using namespace std;
/**
* 传进来的是main函数中变量a和b的值的拷贝 c1, c2
* 交换的也是值的拷贝,并没有改变a和b内存空间中的数据,
* 所以此函数并不能交换a和b的值
*/
void swap1(char c1, char c2)
{
char tmp = c1;
c1 = c2;
c2 = tmp;
}
/**
* 传进来的是main函数中变量a和b的内存地址的拷贝 cp1, cp2
* 交换的也是内存地址的拷贝,并没有改变a和b内存空间中的数据,
* 所以此函数并不能交换a和b的值
*/
void swap2(char *cp1, char *cp2)
{
cout << "in swap2: *cp1=" << *cp1 << ", *cp2=" << *cp2 << endl;
char *tmp = cp1;
cp1 = cp2;
cp2 = tmp;
cout << "out swap2: *cp1=" << *cp1 << ", *cp2=" << *cp2 << endl;
}
/**
* 传进来的是main函数中变量a和b的内存地址的拷贝 cp1, cp2
* 然后通过操作符*来更新内存地址中的数据,所以此函数可以成功交换a和b的值
*/
void swap3(char *cp1, char *cp2)
{
cout << "in swap3: *cp1=" << *cp1 << ", *cp2=" << *cp2 << endl;
// 取c1指向的内存空间中的数据赋值给临时变量tmp
char tmp = *cp1;
// 取cp2指向的内存空间中的数据赋值到cp1指向的内存空间中,覆盖原有数据
*cp1 = *cp2;
// 将tmp的值赋值到cp2指向的内存空间中,覆盖原有数据
*cp2 = tmp;
cout << "out swap3: *cp1=" << *cp1 << ", *cp2=" << *cp2 << endl;
}
/**
* 传进来的是main函数中变量a和b的引用cr1, cr2
* cr1, cr2直接操作的是其引用的对象,所以此函数可以成功交换a和b的值
*/
void swap4(char &cr1, char &cr2)
{
char tmp = cr1;
cr1 = cr2;
cr2 = tmp;
}
/**
* 交换指针,过程是一样的,只是会这里需要传指针的指针进来
*/
void swap_pointer(char **cp1, char **cp2)
{
char *tmp = *cp1;
*cp1 = *cp2;
*cp2 = tmp;
}
int main()
{
char a = 'A';
char b = 'B';
cout << "origin: a=" << a << ", b=" << b << endl << endl;
swap1(a, b);
cout << "after swap1: a=" << a << ", b=" << b << endl << endl;
swap2(&a, &b);
cout << "after swap2: a=" << a << ", b=" << b << endl << endl;
swap3(&a, &b);
cout << "after swap3: a=" << a << ", b=" << b << endl << endl;
// 重置 a, b 为swap3交换之前的原值
a = 'A';
b = 'B';
swap4(a, b);
cout << "after swap4: a=" << a << ", b=" << b << endl << endl;
// 重置 a, b 为swap4交换之前的原值
a = 'A';
b = 'B';
char *pa = &a;
char *pb = &b;
printf("before swap_pointer: pa=%p, pb=%p\n", pa, pb);
swap_pointer(&pa, &pb);
printf("after swap_pointer: pa=%p, pb=%p\n", pa, pb);
return 0;
}
2.当形参是const时,当用实参初始化形参时,顶层const被忽略,也就是说传给它常量对象和非常量对象就是可以的。
3.如果函数无需改变引用形参的值,最好将其声明为常量引用。
int L(int *A){...}
int R(int &B){...}
int i=0;const int j=0;
L(&i)
L(&j) //错误,不能用指向常量的指针初始化变量指针
R(i)
R(j) //错误,不能把普通引用绑定到常量引用上
R(42) //错误,不能把普通引用绑定到字面值上
4.数组形参
数组具有两个性质:①不允许拷贝数组,②使用数组时通常会将其转化为指针。
因此,为函数传递数组,不能使用值传递(因为不能拷贝数组),所以实际是传递了指向数组首元素的指针。
当我们想把数组作为函数的实参时候,有以下三种方式:
//以下三种定义方法等价,每个函数的唯一形参都是const int*类型的
void print(const int *a);//声明为指针
void print(const int a[]);//声明为不限维度的数组
void print(const int a[10]);//这里维度表示我们期望数组含有多少元素,实际不一定
- 1
- 2
- 3
- 4
- 5
①:不控制指针的边界:void print1(const int *p)
②:调用者指定数组的维度:void print2(const int *p,const int sz)//sz为数组维度
③:c++新规定的begin ,end函数限定数组的边界void print3(const int *beg,const int *end)
数组引用形参写法如下:
void print(int (&arr)[10])
//这样可以限制以下几种非法写法
int i=0,j[2]={1,2};
print(&i);//错误
print(j);//错误
- 1
- 2
- 3
- 4
- 5
传递高维数组,例如m[20][10]
void print(int (*matrix)[10])
//正确,指向数组的首元素(10个int构成的数组)
void print(int *matrix[10])//错误
void print(int matrix[][10])//正确
- 1
- 2
- 3
- 4
main函数传递数组:
int main(int argc,char *argv[])//第2个参数是个数组,是指向c风格字符串的指针,第1个形参表示数组中字符串的数量
或者:int main(int argc,char **argv) //当运行文件时,例如 ./ex hello world /* 此时,argc==3 argv[0]="./ex" argv[1]="hello" argv[2]="world" argv[3]=0 */
当使用argv[]中的实参时,一定记得可选的实参从argv[1]开始,因为argv[0]保存的是程序的名字,而非用户输入
5.为了处理不同数量实参的函数,但是实参类型都相同,initializer_list是一种标准库类型,用于表示某种特定类型的值的数组:
initializer_list:当我们要传递不同数量的实参(实参类型都相同)时,我们可以使用initializer_list
,与vector
类似,但值都为常量,无法更改。
使用时包含头文件:#include<initializer_list>
initializer_list提供的操作:
initializer_list<T>1st: 默认初始化;T类型元素的空列表
initializer_list<T>1st{a,b,c...}: 1st的元素数量和初始值一样多;1st的元素是对应初始值的副本;列表中的元素是const
1st2(1st): 拷贝或赋值一个initializer_list对象不会拷贝列表中的元素;拷贝后,原始列表和副本共享元素
1st2 = 1st: 同上
1st.size(): 列表中的元素数量
1st.begin(): 返回指向1st中首元素的指针
1st.end(): 返回指向1st中尾元素下一位置的指针
#include <initializer_list>
void error(inializer_list<string>ls){...}
if(opt==1)error({"Ass","We","Can"});//三个参数
else error({"Thankyou","Sir"});//两个参数
6.3 返回类型和return语句
1.没有返回值的return语句只能用在返回类型时void的函数中。
只要函数的返回类型不是void,则该函数内的每条return语句必须返回一个值。return语句返回值的类型必须与函数的返回类型相同,或者能隐式的转换成函数的返回类型。
动态参数列表:void func(string msg, ...); 常见的应用场景:日志的打印
c++11新标准允许返回花括号包围的值列表:
vector<string> test()
{
return {"hello", "world"};
}
2.如果一个函数调用了它自身,不管这种调用时直接的还是间接的,都称该函数为递归函数。
递归,调用函数自身,main函数不能调用它自己。应用场景:虽然很多书的例子都是用的阶乘或者是斐波拉数列,但是实际工作中除了就是做算法的人,因为涉及到性能的问题,平时应该都很少用递归,我自己也只在遍历文件目录的时候用过递归。
// 伪代码
void list_dir(string dir)
{
files = traverse_current_dir(dir); // 遍历当前目录下的文件或者目录
for path in files:
print path
if path is directory: // 如果文件是目录,则调用自身遍历这个目录
list_dir(path);
}
返回左值的引用:调用一个返回引用的函数得到左值
int &get_val(int a[]){return a[0];}
int main()
{
get_val(x)=10;//函数调用放在左侧,是因为函数返回的是引用的函数,是左值
}
- 1
- 2
- 3
- 4
- 5
初始化返回值
vector<int> P{return {1,2,3};}
- 1
main
函数返回值
#include <cstdlib>
int main()
{
return EXIT_SUCCESS;//EXIT_SUCCESS==0
return EXIT_FAILURE;//EXIT_FAILURE!=0,但具体值依机器定
//这两个均为预处理变量
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
因为数组不能被拷贝,所以函数不能返回数组,但是函数可以返回数组的指针或引用
//方法一:使用别名
typedef int arr10[10];//using arr10=int[10];
arr10 *func(int x)
//方法二:直接声明
int (*(func(int x)))[10] //对比声明指向数组的指针 int (*p)[10],必须有数组维度
//方法三:尾置返回
auto func(int x) -> int (*)[10]//箭头后面表示真正返回的类型
//方法四:decltype
int odd[]={1,3,5},even[]={2,4,6};
decltype(odd) *func(int x)//返回一个大小为3的数组的指针
6.4 函数重载
1.如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为重载函数。
不允许两个函数除了返回类型外其他所有的要素都相同。
顶层const
与非顶层const
无法区分开,所以不允许重载,所以const int和int一样,不算重载。
但是形参是指针或引用等底层引用时,可以重载
int lookup(int &x) //1
int lookup(const int &x) //2
int lookup(int *x) //3
int lookup(const int *x) //4
//以上四种均不相同
lookup(y)
//当y为常量,只有函数2可以调用
//当y为变量,优先函数1,函数1不存在时调用函数2
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
不同作用域下函数的重载
void print(string &s);
void print(double a);
int main()
{
void print(int a);
print("XTX");//错误,print(string)被覆盖
print(3.14);//错误,print(double)被覆盖
print(3);//正确,调用print(int)
}
2.函数匹配是指一个过程,在这个过程中我们把函数调用与一组重载函数中的某一个关联起来,函数匹配也叫做重载确定。
3.调用重载函数时有三种可能的结果:
编译器找到一个与实参最佳匹配的函数,并生成调用该函数的代码。
找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配的错误信息。
有多于一个函数可以匹配,但是每一个都不是最佳选择。此时也将发生错误,称为二义性调用。
int get_user(int id);
int get_user(string name, string mail);
int main()
{
get_user(100); // 调用形参列表是 (int) 的
get_user("cat", "littlefawn@163.com"); // 调用形参列表是 (string, string)的
return 0;
}
6.5 特殊用途语言特性
1.某些函数有这样一种形参,在函数的很多次调用中它们都被赋予一个相同的值,此时,我们把这个反复出现的值称为函数的默认实参。调用含有默认实参的函数时,可以包含该实参,可以省略该实参。
一旦某个形参被赋予了默认值,它后面所有形参都必须有默认值。
尽管多次声明可行,但是对于有默认实参的函数声明,之后同一函数声明只能对没有默认值的形参添加默认值,已有默认值不变
string screen(int a,int b,int c=10);
string screen(int a,int b,int c=5);//错误!
string screen(int a=1,int b=3,int c);//正确
在声明时候使用的默认参数不能是局部变量。
局部变量不能作为默认实参,否则,只要表达式的类型能转化为默认实参,就能作为默认实参
int wd=80;
int f(int x=wd);
int x=f();//调用f(80)
void f2()
{
int wd=100;
int y=f();//调用f(80),外层定义隐藏,但是没改变默认值
}
2.内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求。
一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数,很多编译器都不支持内联递归函数。
#include <iostream>
using namespace std;
// 在函数声明的返回值之前加上关键字 inline即可
inline string short_str(const string &str1, const string &str2) {
return str1.size() > str2.size() ? str1 : str2;
}
int main()
{
string s1 = "Hi";
string s2 = "world";
// short_str会被展开为:()
cout << short_str(s1.size() > s2.size() ? s1 : s2) << endl;
return 0;
}
3.constexpr函数是指能用于常量表达式的函数。
函数返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句。
#include <iostream>
using namespace std;
constexpr int MIN_TO_SEC(int minutes)
{
return minutes * 60;
}
int main()
{
const int one_hour_minutes = 60;
cout << one_hour_minutes << " minutes = " << MIN_TO_SEC(one_hour_minutes) << " seconds" << endl;
return 0;
}
constexpr int scale(int x){return 2*x;}
int arr[scale(3)],i=10;//正确
int arr[scale(i)];//错误,scale(i)不是常量表达式,编译器报错
把内联函数(inline)和constexpr函数放在头文件中。函数声明也应该放在头文件中。
4.调试帮助 assert
:assrt(a),如果a为假,assrt输出信息并且终止程序,若为真,则什么也不做。#include<cassert>
,由预处理器管理,不需用std::
,不需要提供using声明。NDEBUG
:当我们定义NDEBUG
时,assert
函数将不执行,否则assert
函数将进行调试
其他常用函数:
_ _func_ _ 输出当前调试的函数的名字
_ _FILE_ _ 存放文件名的字符串字面值
_ _LINE_ _ 存放当前行号的整型字面值
_ _TIME_ _ 存放文件编译时间的字符串字面值
_ _DATE_ _ 存放文件编译日期的字符串字面值
6.6 函数匹配
1.函数匹配的第一步是选定本次调用对应的重载函数集,集合中的函数称为候选函数。候选函数具备两个特征:一是与被调用的函数同名,二是其声明在调用点可见。
第二步考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数。可行函数也有两个特征:一是其形参数量与本次调用提供的实参数量相等,二是每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。
函数匹配的原则:
void f();
void f(int);
void f(int,int);
void f(double,double=3.14);
f(5.6) //调用f(double,double =3.14)
/*
1.寻找同名且调用点可见
2.找出所有可行函数
3.若存在最优匹配,寻找最优,即实参与形参类型越接近,结果越好
*/
f(2,2.56)//错误!
/*
寻找最优匹配,有且仅有一个可行函数满足下列条件才可行
1.每个实参匹配均不劣于其他可行函数
2.至少一个优于其他
对于2,f(int,int)更优,对于2.56,f(double,double=3.14)更优,因此,调用具有二义性,错误!
*/
2.为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个风机,具体排序如下所示:
a.精确匹配,包括以下情况:
实参类型和形参类型相同
实参从数组类型或函数类型转换成对应的指针类型
向实参添加顶层const或者从实参中删除底层const
b.通过const转换实现的匹配
c.通过类型提升实现匹配
d.通过算术类型转换
e.通过类类型转换实现匹配
6.7 函数指针
1.函数指针指向的是函数而非对象。和其他指针一样,函数指针指向某种特定类型。函数的类型由它的返回类型和形参类型共同决定,与函数名无关。
bool judge(int x);
bool (*J)(int x);//未初始化的函数指针
J=judge;
J=&judge;//两种写法等价,均为将judge与J绑在一起
//若定义重载函数,则函数指针的形参必与某一个函数形参精确匹配
J(1);(*J)(1);judge(1);//三种等价调用方法
//函数指针也可以作为形参,以下两种等价定义
void dy(bool u(int x));
void dy(bool (*u)(int x));
//可以直接调用函数,自动转为指针
dy(judge);
//可以用decltype,typedef,using简化定义
typedef bool Func(int x);
/*
等价定义
typedef decltype(judge) Func;
using Func=bool(int);
*/
void dy(Func u);
void dy(Func *u);
//若需要返回一个函数指针,我们还可以这样
auto ddy(int x)-> int (*)(int);
个函数形参精确匹配
J(1);(*J)(1);judge(1);//三种等价调用方法
//函数指针也可以作为形参,以下两种等价定义
void dy(bool u(int x));
void dy(bool (*u)(int x));
//可以直接调用函数,自动转为指针
dy(judge);
//可以用decltype,typedef,using简化定义
typedef bool Func(int x);
/*
等价定义
typedef decltype(judge) Func;
using Func=bool(int);
*/
void dy(Func u);
void dy(Func *u);
//若需要返回一个函数指针,我们还可以这样
auto ddy(int x)-> int (*)(int);
2.当我们使用重载函数时,上下文必须清晰地界定到底应该选用哪个函数。如果定义了指向重载函数的指针,编译器通过指针类型决定选用哪个函数,指针类型必须与重载函数中的某一个精确匹配。
#include <iostream>
using namespace std;
string short_str(const string &str1, const string &str2) {
return str1.size() > str2.size() ? str1 : str2;
}
string short_str(const string &str1) {
return str1;
}
// 参数p为函数指针
void show(const string &str1, const string &str2, string (*p)(const string &, const string &)) {
cout << p(str1, str2) << endl;
}
// 返回指向函数的指针
using SSP = string (*)(const string &);
SSP get_function()
{
return &short_str;
}
int main()
{
string (*ssp)(const string &, const string &);
ssp = &short_str; // 如果只有一个short_str时,只需要直接赋值即可
cout << ssp("Hi", "world") << endl;
string (*ssp2)(const string &) = &short_str; // 存在重载函数时,上下文需明确指定应选用哪个函数
cout << ssp2("Hi") << endl;
show("my", "god", &short_str);
cout << get_function()("good") << endl;
return 0;
}