最开始我们使用UNIX或LINUX系统编程时经常使用没有图形界面的编译器来把写好的代码编译成obj文件,这时候我们使用命令行来编译一份源代码文件,我们需要在终端里输入类似“prog -d -o oflie data0”的命令行来进行命令行控制。
现在我们看到的main函数一般都是int main(),括号里面什么也不写,我们也可以给main传递上述的那个命令行参数。形如:int main(int argc,char *argv[]) main可以什么参数也不接受,也可以接受一个int和一个指向字符串的指针这两个参数。main没有第三种形式了。
int argc是表示后面的argv一共指向几个字符串用的。char *argv[] 里面的每一个字符串都顺序对应着命令行的参数。这些参数的字符串数组的第一个元素应该是可执行文件的名字或者空参数,最后一个字符串的值必须为0。有关命令行的更多选项和argv参数的具体用法,可以参照对应的编译器文档。
到现在我们定义的函数都是固定参数的,但是有时候我们无法预知向函数传递几个参数,又想使用一个函数接受这种变化,我们就可以使用C++指定的两种方法来定义含有可变形参的函数。
initializer_list类似vector,是一种容器,接纳一种同样类型的元素。initializer_list定义在<initializer_list >中,我们可以把任意数量,同样类型的参数传递给这个容器使函数能够处理多个元素。
initializer_list 容器名1(已被定义的initializer_list 容器的容器名2 )//使容器1的内容和容器二一致,两个容器共享容器二里面的元素。不会形成拷贝,(也可以用initializer_list 容器名1=initializer_list 容器名2)
当我们声明一个接受 initializer_list类型的函数 void fa(initializer_list<int> list1);的时候,我们需要使用大括号来调用这个函数,形如fa({2,3,4});这样我们就向initializer_list传递了一个值的序列。
首先,如果要用省略符的方式处理不定参数的函数要包含头文件:#include <stdarg.h> (C语言中)或者#include <cstdarg>(C++中)。 然后利用va_list类型和va_start、va_arg、va_end 3个宏读取传递到函数中的参数值。用省略符处理不定参数的函数基于C语言的方法,在C++中不建议使用。(使用了C语言标准库功能varargs)。
知识点10:P202,6.3,返回值(英文版226页)
在void返回值的语句最后会隐式地有return;语句,这时函数什么也不返回。函数可以返回一个引用作为左值。也可以返回一个花括号括起来的列表,来初始化vector等类型。main函数的return语句可以不写,编译器会带为隐式补充。
知识点11:P205,6.3.3,返回数组指针(英文版229页)
虽然我们不能直接让函数返回一个数组,但是我们可以设定函数返回一个指针的类型。函数会返回数组的指针。返回数组指针的函数定义语句如下所示:数组元素类型 (*函数名 (参数列表))[ 数组大小 ]当然,返回一个临时量或者局部对象的引用/指针都是错误的行为,如果你在函数里普通地定义了一个数组,那么这个数组的生命周期在函数返回时就结束了,会被内存中释放,因此可能需要用static使这个数组静态。静态对象只是延长了对象的生命周期,但是无论如何在函数内部定义的对象在外部都无法访问,除非使用返回指针的方法。
一条double (*func(int a))[10]这种语句来说明函数接受一个int a形参并且返回一个带有10个元素的double数组,这种语句在写法上比较乱,因此C++提供了尾置返回的方法让程序员不必要非要迁就编译器的理解能力,上一条语句等价于这样:auto func(int a)->double(*)[10] 我们使用->符号把返回值类型的描述放在了参数列表后面并和函数声明分离开让函数看起来不那么乱。
我们也可以使用decltype语句返回数组指针。decltype后面的括号可以括起一个现有的数组推导数组类型。
我们再手动加*得到数组指针类型的返回值。在已有int a[10];的情况下,我们可以使用decltype(a) *fn(int b)这种形式定义一个返回指向数组的指针的返回值类型。
知识点11:P207,6.4,函数重载(英文版233页)
我们可以定义一组功能类似,函数名一致,但是接受的参数类型或数量不同的函数。定义多个这种函数就叫做函数重载。函数重载可以提供给我们用一个函数名处理多种参数形式的情况。
定义重载函数要能重传入的参数里区别出实质不同的重载函数,如函数A的定义为int fa(const int a);和函数B int fa(int b);这两个函数函数名一样,形参类型不同,但仍然无法作为重载函数。因为我们传入一个int值时,fa不知道应该执行第一种还是第二种。所以只有参数顶层const属性不同的几个函数不是重载函数。
当然,对于底层const,比如参数列表为const int *a的函数和参数列表为int a的函数能被看出不同,因为对于一个传入的const常量指针,这个实参只能初始化const int *a,不能被初始化int a。当同时有这两种形式的重载函数时,当传入一个非常量,IDE会优先选择为它匹配形参为int a版本的普通变量形参函数。
知识点12:P209,6.4,const_cast和函数(英文版234页)
这里主要介绍const_cast类型强制转换是如何在函数中被使用的。在第四章第一次接触const_cast的时候我们提到过这个常被用于函数里。这里我们就看看怎么使用。
之前说过,向函数传递参数时最好传递const型参数使其能够接受多种参数,这里我们可以在函数体内使用const再把参数变回普通的变量,这样就可以返回一个非const值了。
知识点13:P210,6.4,重载和作用域(英文版234页)
声明变量时,变量的作用域就在块里,声明函数也一样,而且里层的作用域会隐藏外部的作用域。例子如下:
{
int a=0; //这里是a的外层作用域
{
double a=1.2; //外边已经有a了,这里又声明了一个a,因此这个a的作用域覆盖了前面的int a;
cout<<a<<endl; //输出的会是1.2
}
}
函数声明也一样,
{
int fa(int b); //这里是函数fa的外层作用域
double fa(double b); //重载了函数fa使它能够接受double
{
double fa(string & c); //外边已经有fa了,这里又声明了一个fa,因此这个fa的作用域覆盖了前面的;
fa(2.3); //错误,原型为double fa(double b)的函数声明作用域被double fa(string & c);覆盖,匹配不到函数
}
}
知识点14:P211,6.5.1,默认实参(英文版236页)
有时候一些函数我们每次调用它总会向它传递一些特殊的值。我们可以声明带有默认实参的函数。默认实参如果没有明确说明,默认实参会被自动当做函数的初始值传递进去。
形如int fn(int a,int b=2,double c=3.3)这样定义函数头的方式就给了b和c默认的实参,注意,当一个形参被给了默认实参,它后面的所有参数都要有默认实参才行。
当我们想使用默认实参的时候,只要调用函数的时候使用这种对应的实参就行了,默认实参会用来填补缺少的尾部实参,上面的定义的函数如果这么调用:fn(1,2);double c的值会被自动设为3.3。书写这种函数时要尽量保证要经常用到的默认实参放在参数列表的更后面一点,这样才合理。
可以只在函数声明里标注默认实参不在函数定义里这样写,结果仍然将是正确的。void fn(int = 1, int = 2, int =3);这种函数声明语句省略了形参的名字,不过也是可以的。
局部变量不能做默认实参,默认实参的定义在函数体之外。另外,默认实参是可以在名字的作用域内通过名字更改的。
知识点15:P213,6.5.2,内联函数(英文版238页)
有时我们要频繁调用一个优化规模小,流程直接,频繁被调用的函数,定义函数时我们可以在返回值类型前面加上关键字inline使它成为内联函数,减少运行时的开销。
知识点16:P214,6.5.2,constexpr函数(英文版239页)
这是一种能够被用在常量表达式的函数,但是函数的返回值类型和形参类型必须都是字面值。函数体中必须有且只有一条return语句,constexpr函数被隐式的指定为内连函数。const函数中也可以有类型别名,使用作用域声明等不执行操作的其他语句。这里没有赋值,没有构建对象。同时constexpr可以返回计算后的结果。如constexpr int fn(int a){return a+22;},这条定义是正确的,前提是调用函数这个函数fn时,传入的实参是一个常量。比如fn(3);
知识点17:P215,6.5.3,调试帮助(英文版240页)
程序员在写程序时可能涉及到一些调试中的代码,这些代码只在开发程序时使用,当即将发布程序的时候,要暂时屏蔽掉正在调试中的代码。C++提供了assert和NDEBUG两个预处理功能屏蔽测试代码。
assert这个宏定义在cassert头文件中,assert使用一个表达式作为它的条件,形如assert(expr);首先对expr或者表达式求值,如果结果为真(非0),那么assert什么都不做。如果结果为假(表达式值为0),那么assert输出信息并且终止程序的执行。
assert经常用于处理不能发生的条件,如果你写了一段代码,代码没测试越界,你就可以用assert,当它越界了我们就结束程序的执行。
NDEBUG宏定义可以影响assert的行为,这个默认是没被定义的。当我们宏定义了NDEBUG,就屏蔽掉了assert的功能。
此外,IDE还提供了__FILE__(这里是两个英文下划线,这个存放文件名) 、__func__(这个存放所在的函数名) 、 __LINE__(这个存放所在的行数) 、__TIME__(这个存放调试的时间) 、 __DATE__(这个存放调试的日期) 这五种静态数组来提供错误信息。
知识点18:P217,6.6,函数匹配(英文版242页)
程序员定义重载函数之后就可以使用它们了,挑选到底使用哪个版本的函数是一个过程,这个过程叫做函数匹配。
1、函数匹配的第一步是在调用时先找与与调用函数同名的函数名。且调用点在函数作用域内。这一步筛选出的函数叫做候选函数。
2、函数匹配的第二步是从候选函数中选择出能够被本次函数调用的实参传入的函数,函数名一致的前提下还要求函数的形参个数和实参一致,实参能够转化成(或者就是)形参规定的类型。这一步筛选出的函数叫做可行函数。
3、寻找最佳匹配。当有int fn(int a);和int fn(double a,double b=1.0)时,我们调用函数fn形如fn(3.4);显然这两种函数都是可行函数,这是我们再寻找最佳的匹配,因为fn(3.4);对应fn(double,double=1.0);的话无需转化,因此是最佳匹配。当有多个最佳匹配的时候函数将停止调用。
为了划分最佳匹配的各种情况,编译器将实参类型到形参类型的转换划分为几个等级,具体排序如下所示:
1.精确匹配:
精确匹配可以包含以下情况:数组名转化成数组指针的匹配,函数类型转换成函数指针的匹配,实参类型与形参类型相同。另外,像实参添加顶层const或者忽略实参赋值给形参的顶层const也属于精确匹配。
2.通过指针的转换把非常量指针转换成常量指针。
3.通过类型提升实现的匹配。
4.通过算数类型转换或指针转换实现的匹配
5.通过类类型转换实现匹配(类类型转换还没有讲)
要注意小整数字面值会被自动转换成int,而带小数点的字面值会被默认转换成doube。
知识点19:P221,6.7,函数指针(英文版248页)
声明一条函数指针的语句如下: int (*PtrOfFunc)(参数列表),其中PtrOfFunc就是指向函数的指针。我们可以把函数名赋值给定义的函数指针的名字。
返回函数指针的形参定义为 double(*fn(int a)) (int d,char b);这里声明的函数是fn,函数的形参是int a,返回值是函数指针类型的,返回的函数指针对应的函数的返回类型是double,参数是int d,char b。
和处理数组一样,我们也可以使用尾置返回来返回一个函数指针,尾置返回函数指针的声明是auto fn(int a)->double (*)(int d,char b);尾置返回适合用来返回复杂的类型比如数组,函数指针等等。
遇到double(*fn(int a)) (int d,char b);这种复杂的表达式,应该以定义的变量名为中心,从里往外一层层往外扩展。这个函数的定义语句里面,fn就是其中的变量名,看它右侧,有(int a),这(int a)是一个形参列表。因此得出结论fn的本质是一个函数,再看左侧,*代表这个函数返回一个指针,这个指针的类型在更外层(double (*) (int d,char b))型。当然这种声明/定义容易让人心累,所以这种情况下使用auto fn(int a)->double (*)(int d,char b)是不错的选择。如果这样还是觉得太长了,可以使用typdef,USING等重命名语句加上decltype推导。比如tpyedef double func (int d,char b);这样的语句之后,func就是一个函数类型。
也可以使用tpyedef decltype(fn) func2;这条语句等价于上面的语句。对于using语句,using Func2 = double (int d,char b);即可。可见typedef和using的替换原则是不同的,在涉及到复杂类型的时候,类似数组,函数指针,tpyedef的替换名要和被替换的类型一起被声明。