函数:由返回值、函数名、参数列表、函数体组成。
函数这里主要考虑传参问题
1.传参、赋值、返回值这三个是同一个问题:都是赋值问题
int Sum(int a, int b)
{
return a + b;
}
int c = Sum(1,2);
如上述代码:
我们调用Sum时,传实参1,2给形参进行初始化,就是赋值的问题
同样Sum的返回值赋值给c
所以我们在此讨论赋值的问题
再说下两个概念:顶层const和底层const
顶层const:修饰的是类型对象
底层const:对于指针和引用来说的,修饰的是指针或引用指向的对象
所以赋值时顶层const可以忽略,底层const不能忽略(可以从宽(非const)到严(const)进行赋值)
从宽到严:要求上从宽泛到严格
三种情况:
1.值拷贝:基本的内置类型属于这一类:int,float,string,long,char等
这种情况会忽略顶层const
int a = 3;
const int b = a; //非const的类型能给const的类型赋值
int c = b; //const的类型也能给非const的类型赋值
const int d = b;
其实我们想想也明白,因为是值拷贝,所以赋值用的是副本,不是原来的对象,所以有无const都不是问题
2.引用:引用时对象的一个别名
赋值时,遵循有宽到严的原则,这里的const是底层const,所以是不能忽略
int r = 1;
int& r1 = r;
const int& r2 = r; //正确:非const(宽)给const(严)赋值
const int& r3 = r2; //正确:类型完全一致,赋值更没问题
int& r4 = r2; //错误: const(严)给非const(宽)赋值
3.指针:和引用类似,但可能有两个const
int p = 3;
int* p1 = &p;
const int* p2 = p1; //正确:从宽到严,和引用类似,这里是底层const
int* p3 = p2; //错误:不能从严到宽
const int p4 = 3;
int* p5 = &p4; //错误:不能从严到宽,这里是底层const
const int* p6 = &p4; //正确:类型一致
const int* const p6 = &p4; //正确:顶层const忽略(第二个const,修饰指针)
const int* p7 = p6; //正确:同上
const int* const p8 = p7; //正确:同上
上面修饰指针的const是顶层const,会自动忽略,因为指针的赋值也是值拷贝
p8 = p7;
就是把p7指针的值拷贝一份,把副本赋值给p8, p7、p8是不同的指针,但指向同一个对象。
所以赋值的问题理解了,传参和返回值就懂了
定义函数时,如果不需要修改参数的值,可以加上const,const和非const的类型都能用来传参
2.数组形参
数组有两个特殊性质:
1.不允许拷贝数组
2.使用数组时(通常)会将其转换成指针
因为不能拷贝数组,所以我们无法以值传递的方式使用数组参数
因为数组会被转换成指针,所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针
尽管不能以值传递的方式传递数组,但我们可以把形参写成类似数组的形式:
void print(const int*);
void print(const int[]);
void pirnt(const int[10]);
因为数组是以指针的形式传递给函数的,所以一开始函数不知道数组的确切尺寸,调用者应该为此提供一些额外信息。
1.使用标记指定数组长度:如下,空字符
void print(const char *cp)
{
if(cp) //若cp不是一个空指针
while(*cp) //只有指针所指的字符不是空字符
cout<<*cp+;
}
2.使用标准库规范:指定首尾指针
void print(const int *beg,const int *end)
{
while(beg!=end)
cout<<*beg++<<endl;
}
3.显示传递一个表示数组大小的形参
void print(const int arr[], size_t size)
{
for(size_t i=0;i!=size;++i)
{
cout<<arr[i]<<endl;
}
}
int a[] = {0,1};
print(a,end(a)-begin(a));
数组引用形参
void print(int (&arr)[10])
{
for(auto elem:arr)
cout<<elem<<endl;
}
&arr两端的括号不能少
f(int &arr[10]) //错误:将arr声明成了引用的数组:数组里存的是引用
f(int (&arr)[10]) //正确:arr是具有10个整数的整型数组的引用
传递多维数组
C++实际上没有真正的多维数组,多维数组是数组的数组
当将多维数组传递给函数时,真正传递的是指向数组首元素的指针,因为我们处理的是数组的数组
所以首元素本身就是一个数组,指针就是一个指向数组的指针。数组第二维的大小都是数组类型的
一部分,不能省略。
void print(int (*matrix)[10], int rowSize){ }
*matrix两端的括号不能少
等价于
void print(int matrix[][10],int rowSize){ }
main:处理命令行选项
int main(int argc, char *argv[]){ }
第二个形参argv是一个数组,它的元素指向C风格字符串的指针;第一个形参argc表示数组中字符串的数量。
因为第二个形参是数组,所以main函数也可以定义成:
int main(int argc, char **argv){ }
argv的第一个元素指向程序的名字或者一个空字符串,接下来的元素依次传递命令行提供的实参
最后一个指针之后的元素值保证为0
命令行输入:prog -d -o ofile data0
当输入上述命令后,argc应该等于5,argv应该包含如下的C风格字符串:
argv[0] = "prog";
argv[1] = "-d";
argv[2] = "-o";
argv[3] = "ofile";
argv[4] = "data0";
argv[5] = "0";
注:当时用argv中的实参时,一定要记得可选的实参从argv[1]开始;
argv[0]保存程序的名字,而非用户输入
含有可变形参的函数
为了编写处理不同数量的实参的函数,C++11提供了两种主要的方法:
1.如果实参类型相同,可以传递一个名为initializer_list的标准库类型
2.如果实参的类型不同,编写一种特殊的函数:可变参数模板(后面讲)
3.C++还有一种特殊的形参类型(即省略符),用它传递可变数量的实参,不过这种功能一般只用于与C函数交互的接口程序。
initializer_list对象中的元素永远是常量值,无法改变里面对象中元素的值
void error_msg(initializer_list<string> il)
{
for(auto beg = il.begin(); beg != il.end(); ++beg)
cout<<*beg<<" ";
cout<<endl;
}
使用
error_msg("abc","123");
3.返回类型和return语句
值是如何被返回的:返回一个值的方式和初始化一个变量或形参的方式完全一样
不要返回局部对象的引用或指针,可以返回局部对象的拷贝
函数完成后,它所占用的存储空间也随之被释放掉,函数终止意味着局部变量的引用将不再指向有效的内存区域
这里插一句:
对于有垃圾回收机制的语言,比如C#,不存在这个问题,因为引用对象并不是在函数结束后就被释放掉了,
而是有垃圾回收机制管理,所以在C#中可以返回局部对象的引用(地址,这里的引用更像是C++的指针)。
而C++中是没有垃圾回收机制的,需要自己管理,不同点在这。
列表初始化返回值
C++11规定,函数可以返回花括号包围的值的列表,此值对表示函数返回的临时量进行初始化
vector<string> process()
{
string s="b";
if()
return {}; //返回一个空vector对象
else if()
return {"a","b"}; //返回列表初始化的vector对象
else
return {"a",s};
}
主函数main的返回值
如果控制到达了main函数的结尾处而且没有return语句,编译器将隐式地插入一条返回0的return语句。
main函数的返回值可以看做是状态指示器,返回0表示执行成功,返回其他值表示执行失败,其中非0值的具体含义依机器而定,cstdlib头文件定义了两个预处理变量,可以使用这两个变量分别表示成功与失败
EXIT_FAILURE 和 EXIT_SUCCESS
返回数组指针
因为数组不能拷贝,所以函数不能返回数组,不过函数可以返回数组的指针或引用
定义一个返回一个数组的指针或引用的函数比较烦琐,可以用一些方法简化,最直接的方法是使用类型别名
typedef int 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个整数的数组
和这些声明一样,如果我们想定义一个返回数组指针的函数,则数组的维度必须跟在函数名字后面
然而,函数的形参列表也跟在函数名字后面,且形参列表应该优先于数组的维度
形式: type (*function(parameter_list))[dimension]
int (*func(int i))[10];
理解含义:
func(int i)表示调用函数时需要一个int类型的实参
(*func(int i))意味着我们可以对函数的结果执行解引用操作
(*func(int i))[10]表示解引用func的调用将得到一个大小是10的数组
int (*func(int i))[10]表示数组中的元素是int类型
使用尾置返回类型
C++11还有一种可以简化上述func的声明方法,使用尾置返回类型
任何函数都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效
形式:auto func(int i) -> int(*)[10];
使用decltype
如果知道函数返回的指针将指向哪个数组,就可以使用decltype关键字声明返回类型
int odd[] = {1,3,5,7,9};
int even[] = {0,2,4,6,8};
decltype(odd) *arrPtr(int i)
{
return (i%2) ? &odd : &even;
}
decltype(odd) * 表示返回一个指向含有5个整数的数组的指针
decltype并不负责把数组类型转换成对应的指针,所以decltype的结果是个数组,
要想表示arrPtr返回指针还必须在函数声明时加一个*符合
4.函数重载
同一个作用域内几个函数名相同但形参列表不同,称之为重载函数。
不允许两个函数除了返回类型外其他所有的要素都相同
顶层const不影响传入函数的对象,一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来
void lookup(int);
void lookup(const int); //重复声明
void lookup(int*);
void lookup(int* const); //重复声明,const修饰指针,是顶层const
如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时的const是底层的
void lookup(int&); //新函数
void lookup(const int&); //新函数:作用于常量引用
void lookup(int*); //新函数
void lookup(const int*); //新函数:作用于指向常量的指针
因为const对象只能传递给const形参,非常量可以转换成const
所以当传入的参数是非常量时,上面四个函数都能用,编译器会优先选择非常量的函数
const_cast和重载
const_cast在重载函数的情景中最有用
const string &shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
上面的函数的参数和返回类型都是const string的引用,我们可以对两个非常量的string实参调用这个函数
但结果返回是const string的引用,当我们想当它的实参不是常量时,得到的结果是一个普通的引用,使用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);
}
这个版本中,首先将它的实参强制转换成对const的引用,然后调用shorterString函数的const版本,
const版本返回对const string的引用,这个引用事实上绑定在了某个初始的非常量实参上,因此
我们可以再将其转换回一个普通的string&,是安全的
5.特殊用途语言特性
默认实参
某些函数有这样一种形参,在函数的很多次调用中它们都被赋予一个形同的值,此时我们把这个反复出现的值称为函数的默认实参
void fun(int a,int b=0); //调用时可以省略给b传参
默认实参声明:
可以多次声明同一个函数,在给定的作用域中一个形参只能被赋予一次默认实参
void fun(int a,int b=0);
void fun(int a,int b=1); //错误,重复声明
void fun(int a=1,int b); //正确
默认实参初始化:
局部变量不能作为默认实参,初次之外,只要表达式的类型能转换成形参需要的类型,该表达式就能作为默认实参
//a,fun声明必须出现在函数之外
int a=1;
int fun();
void fun2(int i = a, int j = fun());
调用:fun2();
用作默认实参的名字在函数声明所在的作用域内解析,而这些名字的求值过程发生在函数调用时
void fun3()
{
int a = 3; //隐藏外层定义的a,但是没有改变默认值,这里的a和传递给fun2的a没有任何关系
fun2(); //调用fun2(1,fun());
}
内联函数和constexpr函数
内联函数可以避免函数调用的开销
将函数指定为内联函数(inline),通常就是将它在每个调用点上“内联地”展开
inline int fun(int a,int b)
{
return a+b;
}
cout<<fun(1,2)<<endl;
cout<<1+2<<endl; //展开后
内联机制用于优化规模较小、流程直接、频繁调用的函数,很多编译器都不支持内联递归函数。
constexpr函数:
constexpr函数是指能用于常量表达式的函数。定义constexpr函数和其他函数类似,但要遵循几项约定:
函数的返回类型及所有形参的类型都是字面值类型,而且函数体中必须有且只有一条return语句
constexpr int fun(){ return 3;};
constexpr int a = fun();
执行该初始化任务时,编译器把对constexpr函数的调用替换成其结果值,为了能在编译过程中国随时展开
constexpr函数被隐式地指定为内联函数。
允许constexpr函数的返回值并非一个常量:
constexpr int fun2(int count){ return fun() * count;}
当fun2的实参时常量表达式时,它的返回值也是常量表达式,反之则不然。
int arr[fun2(2)]; //正确,fun2(2)是常量表达式
int i=2; //i不是常量表达式
int arr2[fun2(i)]; //错误:fun2(i)不是常量表达式
把内联函数和constexpr函数放在头文件内
和其他函数不一样,内联函数和constexpr函数可以在程序中多次定义,但对于某个给定的内联函数或者constexpr函数来说,它的多个定义必须完全一致,基于这个原因,内联函数和constexpr函数通常定义在头文件中
调试帮助
C++程序员有时会用到一种类似于头文件保护的技术,以便有选择地执行调试代码。
基本思想是:程序可以包含一些用于调试的代码,但是这些代码只在开发程序时使用,当应用程序编写完成准备发布时,
要先屏蔽掉调试代码。这种方法用到两项预处理功能:assert和NDEBUG
assert预处理宏:
assert是一种预处理宏,所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数。
assert宏使用一个表达式作为它的条件:
assert(expr);
首先对expr求值,如果表达式为假(即0),assert输出信息并终止程序的执行。
如果表达式为真(即非0),assert什么也不做。
assert宏常用于检查“不能发生”的条件。例如,一个对输入文本进行操作的程序,可能要求所有给定单词的长度都大于
某个阈值,此时,程序可以包含一条如下所示的语句:
assert(word.size()>threshold);
NDEBUG预处理变量:
assert的行为依赖于一个名为NDEBUG的预处理变量的状态。
如果定义了NDEBUG,则assert什么也不做,默认状态下是没有定义NDEBUG,此时assert将执行运行时检查。
我们可以使用一个#define语句定义NDEBUG,从而关闭调试状态。
同时,很多编译器都提供了一个命令行选项使我们可以定义预处理变量:
$ CC -DNDEBUG main.C # use /D with the Microsoft compiler
这条命令的作用等价于在main.c文件的一开始写#define NDEBUG
处理用于assert外,也可以使用NDEBUG编写自己的条件调试代码。
void print(const int ia[], size_t size)
{
#ifndef NDEBUG
//_ _func_ _(前后两个下划线,每空格)是编译器定义的一个局部静态变量,用于存放函数的名字
cerr<<__func__<<":array size is "<<size<<endl;
#endif
}
编译器为每个函数都定义了__func__,它是const char的一个静态数组,用于存放函数的名字。
还有4个对于程序调试很有用的名字:
__FILE__:存放文件名的字符串字面值
__LINE__:存放当前行号的整型字面值
__TIME__:存放文件编译时间的字符串字面值
__DATE__:存放文件编译日期的字符串字面值
6.函数匹配
优先最合适函数,具体细节看书,这一块理解原理就行
7.函数指针
函数指针指向的是函数而非对象。和其他指针一样,函数指针指向某种指向某种特定类型,函数的类型由它的返回类型和形参类型共同决定,与函数名无关
bool lengthCompare(const string &,const string &);
改函数的类型是:bool(const string &,const string &),想要声明一个可以指向该函数的指针,只需用指针替换函数名即可。
bool (*pf)(const string &, const string &); //为初始化
解释:从声明的名字开始观察,pf前面有个*,因此pf是指针,右侧是形参列表,表示pf指向的是函数;再观察左侧,发现函数
的返回值类型是bool,因此,pf就是一个指向函数的指针,其中该函数的参数是两个const string的引用,返回值是bool类型
*pf的两端的括号必不可少,如果不写这对括号,则pf是一个返回值为bool指针的函数。
bool *pf(const string &,const string &);
写的明显些:
bool* pf(const string &,const string &);
使用函数指针:
当我们把函数名作为一个值使用时,该函数自动地转换成指针。
pf = lengthCompare; //上面已经声明了pf
pf = &lengthCompare; //等价,取地址符是可选的
我们能直接使用指向函数的指针调用该函数,无须提前解引用指针:
bool b1 = pf("hello","goodbye"); //调用legthCompare函数
bool b2 = (*pf)("hello","goodbye"); //等价
bool b3 = lengthCompare("hello","goodbye"); //等价
在指向不同函数类型的指针间不存在转换规则,但我们可以为函数指针赋一个nullptr或者值为0的整型常量表达式,
表示该指针没有指向任何一个函数:
pf = 0;
pf = nullptr;
重载函数的指针:
当我们使用重载函数时,上下文必须清晰地界定到底应该选用哪个函数。
void fun(int*);
void fun(unsigned int);
void (*pf1)(unsigned int) = fun; //pf1指向fun(unsigned)
指针类型必须与重载函数中的一个精确匹配
void (*pf2)(int) = fun; //错误:没有任何一个fun与该形参列表匹配
double (*pf3)(int*) = fun; //错误:fun和pf3的返回类型不匹配
函数指针形参:
和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针,此时,形参看起来是函数原型,实际上确实当成指针使用:
//第三个形参是函数类型,它会自动地转换成指向函数的指针
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); //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; //等价的类型
注:decltype返回函数类型,不会将函数类型自动转换成指针类型,因为decltype的结果是函数类型
所以只有在结果前面加上*才能得到指针。
可以使用如下的形式重新声明useBigger:
void useBigger(const string &,const string &,Func);
void useBigger(const string &,const string &,FuncP2);
返回指向函数的指针:
和数组类似,虽然不能返回一个函数,但是能返回指向函数类型的指针。然而我们必须把返回类型写成指针形式,编译器
不会自动将函数返回类型当成对应的指针类型处理。
和往常一样,要想声明一个返回函数指针的函数,最简单的办法是使用类型别名:
using F = int (int*,int); //F是函数类型,不是指针
using FP = int(*)(int*,int); //FP是指针类型
注:和函数类型的形参不一样,返回类型不会自动地转换成指针,必须显示地将返回类型指定为指针:
FP f1(int); //正确
F f1(int); //错误:F是函数类型,f1不能返回一个函数
F *f1(int); //正确:显示指定返回类型是指向函数的指针
直接声明f1:
int (*f1(int))(int*,int);
按照由内往外的顺序阅读这条声明语句:
f1有形参列表,所以f1是个函数;f1前面有*,所以f1返回一个指针;进一步观察,指针的类型本身也包含形参列表,
因此指针指向函数,该函数的返回类型是int
还可以使用尾置返回类型的方式声明一个返回函数指针的函数:
auto f1(int) -> int (*)(int*,int);
将auto和decltype用于函数指针类型:
如果我们知道返回函数的是哪一个,就能使用decltype简化书写函数指针返回类型的过程。
例如:假定有两个函数,它们的返回类型都是string::size_type,并且各有两个const string &类型的形参,此时
我们可以编写第三个函数,它接受一个string类型的函数,返回一个指针,该指针指向前两个函数中的一个:
string::size_type sumLength(const string&,const string&);
string::size_type LargerLength(const string&,const string&);
//根据其形参的取值,getFun函数返回指向sumLength或者largerLength的指针
decltype(sumLength) *getFun(const string &);
声明getFun唯一注意的地方是,牢记当我们将decltype作用于某个函数时,它的返回类型而非指针类型。
因此,我们显式地加上*以表面我们需要返回指针,而非函数本身。
其实函数指针和C#的委托有点像,可以用函数指针写出C#委托的效果