函数基础
函数返回类型
大多数类型都能用作函数的返回类型,一种特殊的返回类型是void就,这表示函数不返回任何值。函数返回类型不能是数组类型或者函数类型,但是可以返回指向数组或者函数的指针。
局部静态对象
有时候需要让局部变量的生命周期贯穿函数以及调用之后的时间,可以将局部变量定义为static类型从而获得这样的对象。用static修饰。
函数声明
和其它名字一样函数的名字也必须在声明之后使用。函数声明也叫做函数原型。
分离式编译
分离式编译就是把一个程序的各个部分存储在不同的文件内。每个文件单独编译。
# 一个例子
# 假设fact函数位于fact.cc的文件中,fact.h中。fact.cc应该包括fact.h。另外有一个factmain文件这个文件中包括了main函数,main函数中会调用fact函数
cc factmain.cc fact.cc # 产生main.ext或者a.out
cc factmain.cc fact.cc -o main # 执行之后回生成main或者main.ext(windows)
如果修改了其中的一个源文件,只需要重新编译那个改动了的文件即可。大多数编译器支持分离时编译。而这一过程通常会产生后缀.obj(windows)或者.o的文件,这个后缀名的含义是该文件的包含对象代码。
接下来编译器负责把对象链接在一起形成可执行的文件。编译的过程如下
cc -c factmain.cc # 产生factmain.o文件
cc -c fact.cc # 产生fact.o文件
cc factmina.o fact.o #产生factmain.ext或者a.out文件
cc factMain.o fact.o -o main# 产生main.ext或者main文件,这个文件可执行。
引用的参数传递
重载函数会忽略参数的顶层const因此int func(const int a);和int func(int a);不构成重载会报错
不能用字面值初始化一个非常量引用int &a=32;正确的应该:const int&a =32;
数组形参
数组的两个性质:1.不允许拷贝数组 2.使用数组时通常会把其转化为指针。
尽管不能以值的形式传递数组但是可以把形参写成类似数组的形式:
void print(const int*);
void print(const int[]); //可以看出来参数期待的是数组
void print(const int[10]); //指明了期望的数组的数量但是实际不一定。数组的大小跟函数的调用没啥关系
由于函数调用的时候不知道数组的确切元素个数,有一些方法可以解决这个问题:
- 使用标记指定数组长度:如c风格的字符串就有一个空字符来标记字符串的长度
- 使用标准库的规范:如
void print(cosnt int* beg,const int *end);//传入首和尾后指针即可
- 显示的传一个表示大小的实参:如
void print(int a[],const int length);
- 数组引用形参 c++允许将变量定义为数组的引用。因此可以
void print(int (&arr)[10]);//这个10不加会编译不过
并且需要严格的匹配好像。
传递多维数组
c++没有真正意义上的多维数组因此真正传递的是指向数组首个元素也就是里面一层数组的指针。
如果已经知道第二层数组的维度为10,就应该这样声明:void print(int (*p)[10]);//这里10也是必不可少的
也有一种等价的写法就是:void print(int p[][10]);
main处理命令行选项
我们可以向函数传递实参,来让main函数根据我们传入的参数处理不同的内容,就像pyhton的argparse一样。
int main(int argc,char *argv[]){...}
其中第一个参数表示参数的数量,第二个参数表示传入char*类型的数组。假如调用main -d -o data out.txt那么argv[0]="main",argv[1]="-d",argv[2]="-o"....
注意argv[0]保存了当前程序的名字。
可变形参的函数
可以通过两种方式实现:
- 如果所有的的实参类型相同,可以传递一个名为initializer_list的标准库类型;
- 如果实参的类型不同可以编写一种特殊的函数,也就是所谓的可变参数模板。
- 也有一种特殊的形参类型"…",可以用它传递可变数量的实参,不过主要用于和c兼容的接口程序
initializer_list
实参的数量未知但是类型都相同可以用这个。这也是一种模板类型,其基本的操作如下:
initializer_list<T> ist; //因为是模板类,所以传一个额外信息进去
initializer_list<T> ist={a,b,c...};
initializer_list<T> ist{a,b,c...}; //ist的元素数量和初始值一样多;其元素是对应初始值的副本,列表中的元素是const,ist中的元素也是const类型的
ist1(ist); //拷贝或者赋值一个initializer_list不会拷贝列表中的元素;拷贝后原始列表和副本共享元素。
ist2=ist;
ist.size();
ist.begin();ist.end();
//可以这个类的构造函数还是比较简陋的一般只能用{}来声称对象。
//initializer_list中的元素永远是const型,我们无法改变initializer_list对象中的元素的值
省略符形参
省略符形参为了便于c++程序访问某些c代码而设计的。这使用了名为varargs的
标准库功能,通常省略符形参不应用于其他目的。你的c编译器文档会描述如何使用varargs,并且省略后出现在最后一个参数。
函数返回值
不要返回局部的对象或者指针,这可能导致返回一个已经被销毁的对象的引用和指针。
函数返回类型决定了函数调用结果是否是左值。调用一个返回引用的函数得到左值,其他返回类型得到的是右值。
函数可以返回花括号包围的值的列表
vector<string> process(){
if (expected.empty())
return {};
else if(expected==actual){
return {"ab","c,d"};
}
else
return {"ef"};
}
//注意对于返回值为内置类型的情况下,花括号只能包括一个元素。是自定义类型的话按照这个类的定义来决定这个列表如何被使用。
主函数main的返回值
如果函数返回类型不是void那么就应该返回一个值。但是在main中可以没有return。一般使用int main,通过返回不同的变量可以得到程序执行的结果。
int main()
{
if (some_failure)
return EXIT_FALLURE; //定义在cstdlib头文件中
esle
return EIT_SUCESS; //应该就是表示0
}
返回数组指针
因为数组不能被拷贝,所以函数不能返回数组。但是可以返回数组指针或者引用。
要定义一个返回数组的指针或引用的函数比较麻烦,可使用类型别名简化任务。
typedef int arrT[10];
using arrT = int[10];
arrT* func(int i); //函数返回一个指向10个int元素的数组的指针。
typedef
//下面声明一个反比数组(元素为函数指针)的
Type (*function(parameter_list))[dimension];//由内而外的看
int (*func(int i))[10]; //首先p这个函数有一个int的参数,然后函数的调用结果可以进行解引用操作,然后解引用的结果是一个数组,数组元素的类型是int。
使用尾置返回类型
通过在本应该出现返回类型的地方放auto,然后使用->进行如下格式的返回:
auto func(int i) -> int(*)[10];
使用decltype来指定返回的类型
假设arr是一个包含10int元素的数组,就可以使用decltype(arr) * func(int i);
实现和上面一样的功能。
函数重载
main函数不能重载
不允许两个函数除了返回类型其他要素都相同,这不构成重载。
int func(int);和int func(int i);
不构成重载
如果形参是指针和引用,可以通过国区分指向常量或者非常量对象可以重载。如
int func(const int &i);和int func(int &i);//指针同理
重载与作用域
内层作用域声明的函数会让外层所有的同名函数隐藏,同样变量名也是如此。在c++中名字查找发生在类型检查之前。
特殊用途语言实参
默认实参
默认实参负责调用的时候为为填入的实参赋一个默认值。其声明放在最后。
函数的声明通常放在头文件中并且只声明一次,但是多次声明也是合法的。但是对于给定的作用域中一个形参只能被赋予一次默认形参,函数的后续声明只能为之间没有默认值的形参添加默认形参。
string screen(sz,sz,char='');
string screen(sz,sz,char='*'); //错误
string screen(sz,sz=2,char); //正确添加了默认形参
局部变量不能用作默认实参,除此之外只要表达式 的类型能转换为形参所需要的类型就能用作默认实参。如:
sz wd=80;
char def='';
sz ht();
string screen(sz=ht(),sz=wd,char=def); //函数声明,上面的默认参数都出现在函数外部。
内联函数和constexpr函数
和其他函数不一样,内联函数和constexpr函数可以再程序中多次定义,因为编译器想要展开函数只有函数声明是不够的还需要定义。但对于某个给定的内联函数或者constexpr函数来说多个定义必须完全一致。因此inline和constexpr函数通常会被定义到头文件中。
内联函数
内联(inline),就是会在每一个调用点上进行内联的展开(不同于字符串式的展开)。在函数前面家还是那个关键字inline就可以把它内联展开了。inline const string&();
内联一定要提前声明或者直接定义为inline类型不然可能未实现内联功能。
constexpr
constexpr函数是指能用于常量表达式的函数。定义的方式也是在最前面加上constexpr。内联函数的需要满足下面几点:
- 函数的返回类型和所有的形参类型都必须是字面值类型。
- 函数体中必须有且只有一条return语句
constexpr int new_sz(){return 43;} //定义constexpr函数
constexpr int foo = new_sz(); //定义一个constexpr变量
//便于展开常量表达式函数被隐式的展开为inline函数
我们允许constexpr的函数返回值并不是一个constexpr类型。如cosntexpr int new_sz(int a){return a*2;}
这时候如果参数a是常量表达式那么这个函数会返回常量表达式反之不是。
调试帮助
程序中可以有一些用来调试的代码,但是只会在开发程序中使用。一般用到两种预处理功能:assert和NDEBUG。
assert预处理宏
aseert宏使用一个表达式作为条件:assert(expression);
它回收下计算expr的值如果为false就会输出信息并终止程序的运行。否则什么都不做。
assert定义在cassert头文件中,因为是一种预处理宏,由预处理器管理而不是编译器,不需要namespace。宏名字在程序中必须唯一,很多其他头文件中可能包含了cassert因此,自己不用最后也不要随意使用assert作为变量以及函数的名字。
NDEBUG
assert的行为依赖于一个名为NDEBUG的预处理变量的状态的定义。如果定义了NDEBUG,那么assert直接失效。默认情况下没有定义。
我们可以手动的使用一个#define NDEBUG
,很多编译器也提供了一个命令行来让我们定义预处理变量如cc -D NDEUBG main.cc
合理利用预处理可以让我们更好的调试自己的代码如下:
void print(const int ia[],size_t size){
#ifndef NDEBUG
cerr<<__func__<<":array size is"<<size<<endl;
#endif
}
//其中__func__是编译器定义的一个局部静态变量,用于存放函数的名字
//除了编译器定义的__func__之外,预处理器也定义用于程序调试很有用的名字方便我们使用:
__FILE__ 存放文件名的字符串字面值
__LINE__ 存放文件名的字符串字面值
__file__ 存放文件名的字符串字面值
__file__ 存放文件名的字符串字面值
函数匹配
对于重载函数编译器会确定最佳的匹配,一定要有一个优与其他的选项与之匹配否则会报二义性错误。当有多个符合要求的重载函数时,这个脱颖而出的匹配函数要满足如下:
-
- 每个实参的匹配都不差与其他可行函数需要的匹配
- 至少有一个实参的匹配优与其他的匹配。
为了确定撇皮,编译器将实参到形参的类型匹配划分几个等级具体的排序如下:
- 精准匹配,包括如下情况:
- 实参和形参类型相同
- 实参从数组类型转化为对应的指针类型
- 向实参添加或者删除顶层const。
- 通过cosnt转换实现的匹配
- 通过类型提升实现的匹配
- 通过算数类型转换实现的匹配
- 通过类类型转化实现的匹配
函数指针
函数就是指向函数的指针。可以对下面的一个例子进行分析:
bool (*pf)(const string&);//首先从括号里往外看,从右往左看,首先pf是一个名字,紧跟着一个*说明pf是一个指针,然后往右边看是一个形参列表说明这个指针指向的是一个函数,然后往左看是一个bool说明这个指针指向的函数的返回值是boo类型
使用函数指针
pf=lengthCompare;//首先我们定义了一个函数并且使用一个指针指向它。
//然后我们可以直接使用这个指针进行调用而不用提前解引用指针:
bool b1 = pf("hello","google");
bool b2= (*pf)("hello","google"); //这个和上面的调用是等价的
指向不同函数类型的指针之间不存在转换规则。函数指针也可以被赋值位nullptr以及0来表示这个指针没有指向任何一个函数。
如果定义了重载函数的指针,编译器会根据指针的类型选择一个来进行精确匹配。PS:因为就算是重载函数每一个函数也都是不一样的,编译器只是会帮你决定调用哪个罢了,因此函数指针也只能指向其中的一个,所以这个指针并没有重载函数的功能。
函数指针形参
不可以定义函数类型的形参但是可以定义指向函数的指针。这个时候形参看起来是函数类型但是实际上可以当指针使用。
void useBigger(cosnt string &s1,const string &s2,bool pf(const string&,const string &));//这里第三个参数是一个函数的形式,但是它会自动转换为指向函数的指针.这和下面的是一样的
void useBigger(cosnt string &s1,const string &s2,bool (*pf)(const string&,const string &));//和上面是等价的
//这个时候我们就可以使用函数名来传入上面声明的函数了,函数名当实参传入后会自动转换为指针
useBigger(s1,s2,lengthCompare); //这里lenghCompare是一个函数名,会被自动转化为函数指针传入
返回指向函数的指针
和数组类似,虽然不能返回一个函数但是可以返回指向函数类型的指针,但是返回函数指针的时候需要显示的制定返回类型是指向函数的指,因为编译器不会自动的将函数返回类型当做对应的指针类型。
//简单的还是使用类型别名
using F = int(int* int); typedef int F(int* int);
using PF = int(*)(int * int); typedef int (*PF)(int*,int);
//然后使用这种类型作为函数的返回值
PF f1(int); //正确,函数返回一个函数指针
F f1(int); //错误,函数不能返回一个函数
F *f1(int); //正确,函数返回值是一个函数指针
//当然可以不使用类型别名
int (*f1(int))(int*,int); //同样是括号中的先看(注意如果左右都有括号,name先看左边括号再看右边括号)。首先(int)看不出来东西,要和左边的f1结合起来看说明f1是一个函数拥有一个int型的参数,然后对这个返回值进行解引用,然后往右边看有括号说明这个函数返回的解引用后的内容可以调用是一个参数为(int* int)的函数,然后往左边看这个说明解引用后的这个函数返回值是int类型。//这我都能讲清楚,夸一下自己
//也可以使用尾置指针来说明返回
auto f1(int) -> int (*)(int*,int); //这个挺简单明了的爱了爱了
//也可以使用decltype来简化返回
decltype(某个函数) * f1(int); //decltype(函数名)来或者这个函数的类型,然后再加上*说明返回这个函数的指针。