函数
在调用函数的时候,会进行两步。
- 将形参实例化。
- 将控制权交给被调函数。
在调用时,形参与实参的个数必须对应,而在此过程中,若类型不同,这将进行类型转换。
形参可以不被命名,如果它在函数中不需要被使用,但是不代表程序不应该传入实参与它对应。(虽然并不知道这有什么用)
名字有作用域,对象有生命周期。
当局部变量在其自身的作用域的时候,会隐藏其外层作用域的同名变量。但是在作用域结束之后会,取消隐藏,并且其值不变,就好像没有被隐藏过一样。
局部变量的生命周期取决于它的定义方式。
自动对象
会在离开作用域之后自动销毁的对象。
并不是所有的局部变量都是自动对象,静态局部变量就不是,因为它出作用域并不会销毁。
静态局部变量
使用static修饰词修饰。
在离开作用域时,不会被销毁,只会在程序结束的时候被销毁。
函数声明
没有函数体的对函数的定义就是函数声明。类似于变量。声明时形参无需名字。函数声明也被成为函数原型。
函数声明应该都写在头文件中,而非源文件,并且在源文件中引用头文件。
参数传递
- 值传递:传递的参数是一个拷贝
- 引用传递:传递的参数是对原本实参的一个引用
由于值传递需要拷贝,而拷贝需要花时间且有时类没有拷贝方法,此时可以用引用来进行传递。但是,此时改变它的值会影响到原本的实参,因此若是不需要进行修改,则应加上const,使用常量引用。
函数只能返回一个对象,虽然可以使用结构体,但是也可以通过传入引用来作为一种返回变量的方式。
除非是常量引用或者常量指针,不然常量并不能作为形参,因为不能向常量写值。
使用常量引用还能扩大函数的使用范围,使其当传入参数为常量时也仍然能使用(字面值是常量)。
数组传参
对于数组传参,无论形参写成什么形式,是指针亦或是数组,其所接收的是一个头指针。
为了保证数组不越界,最好传入一些表示数组大小的形参,例如在数组结尾增加标记,尾后指针,数组大小等。
可以传递对数组的引用,但是也限制了数组的大小。
由于多维数组实际上是一个一维的指针数组,因此可以通过传递指针数组的方式来传递多维数组。
多维数组也可以通过a[][大小]的方式定义,但编译器会自动省略第一个维度,将其当成是一个指针数组,因此它的第二维的大小必须定义,对于更多维并没有进行测试。
main
main可以有两个从外部运行程序时传入的参数。
- int argc,表示字符串的个数
- char *argv[],一个字符串数组,实际参数,它的0号字符串是这个可执行文件的名字(这并非是在运行时输入的,而是在程序编译的时候就已经存储的),从1号字符串开始才是正式的参数。
可变参数
当实参的类型相同时,可以使用initializer_list。
其类似于vector,但是它的元素数量无法改变,与初始化时元素数量相同,且其元素都是常量。默认初始化时元素个数为0。若进行赋值,它会与原变量共享相同的元素,而非进行拷贝,相当于是引用。
一般只作为形参使用,在传入参数时使用列表初始化。
省略符
一般只用于访问特殊的C代码。
用法:(parm_list,…)或(…)
并不会对省略符内的实参进行类型检查。
函数返回
当函数的返回值是void时,除了return;还可以return一个返回值是void的函数。
一个非void返回类型的函数的return必须有返回值,但是若是隐式返回大部分编译器并不会检查。
整数类型默认返回1,小数类型dev上返回一个非常接近0的数。
由于函数在结束后其所占的内存空间会被释放,因此不能返回其局部变量的引用或者指针。
当返回值是一个非常量引用时,便是左值。
返回值可以使用列表初始化,通过这种方式可以返回vector。
main函数当失败退出的时候返回的非0值会根据机器而不同。可以使用cstdlib中的EXIT_SUCCESS和EXIT_FAILURE两个预处理变量来免除因机器不同而产生的差异,SUCCESS是0,FAILURE是1。
main按书中是不可以调用自己的,但Dev中可以。
复杂类型的返回有四种方式:
- 当类型比较繁琐麻烦的时候最好使用类型别名,如在使用指向数组的指针的时候。由于函数不能返回数组,所以通过返回指向数组的指针来间接返回数组。
- 不使用类型别名来返回数组指针十分的麻烦,格式:
- 这种方式非常的复杂,因此我们可以使用尾后返回类型的方式来处理。此处的type若使用数组指针的话就是int (*)[10]。
- 也可以使用decltype来处理,但要注意的是decltype不会把数组转成指针,因此需要加上解引用号。
函数重载
当使用相同的函数名而不同的形参时,就是用了函数重载。
main函数不能被重载。
形参不同有以下几点要注意的:
- 形参类型,包括类型,声明符。但是类型别名并不代表类型不同。
- 底层const会被当做不同,但是顶层不会。
可以通过const_cast来将常量函数重载为非常量。
作用域
函数内部也可以定义函数。当在函数内部重载外部函数的时候,就并不是重载,而是覆盖隐藏,因为函数内部是局部作用域,因此会隐藏外部作用域的同名。
在这种情况下,调用函数就不能在名字查找时查找到外部的函数,即便外部有类型匹配的函数,也并不会去调用,只会调用局部作用域内的函数,即便会报错。
默认实参
可以给形参指定默认参数,但是当一个形参被声明了默认形参,其右侧定义的剩下的形参也必须要声明默认形参。
在调用的时候只需从某一个形参开始省略,其右侧的所有形参也将使用默认形参。
可以通过多次声明来增加默认形参,但是不允许在这一过程中更改已有的默认形参的值。
默认形参可以被赋值为变量或者函数。当变量或者函数的返回值改变的时候,其默认的值也会改变。
但是当在内作用域内重新定义一个用作默认参数的变量或者函数时,其新赋的值并不会改变默认形参的值,它隐藏了外部的同名,但是默认形参使用的仍然是外部形参的。
内联函数
函数比普通的表达式执行的速度与消耗更慢也更大,因此当函数内容较少时可以添加关键字inline将函数转为内联函数,此时再编译时会将所有调用函数的地方替换为函数的内容,但是inline只是一个建议,而非强制,编译器可以选择忽略。
constexpr函数
用于作为常量初始化时的一部分时,最好使用constexpr修饰。
函数的形参必须是常量,函数的返回值也必须是常量,但是返回值可以是一个表达式,但是要保证当形参为常量时,其返回值也必须为常量值。
由于在编译时会自动将函数替换为其结果,因此constexpr函数会被隐式声明为inline。
由于constexpr定义必须一致,因此一般会直接在头文件中定义,而非在源文件定义。
assert
assert是一种预处理宏,定义于cassert头文件。
用法assert(expr)。
当expr为假时,就会终止程序。
一般用于检查“不能发生”的情况。
assert使用无需using声明,因为宏是预处理器处理而非编译器。
NDEBUG(Not DEBUG)
NDEBUG是一个预处理变量。
可以在编译程序的时候指定是否定义NDEBUG。
当NDEBUG没有被定义时,assert会正常工作,并且可以通过
#ifndef NDEBUG在内部写一些只有在调试时需要的调试代码,例如输出程序错误等。
在其中有四个非常有用用于调试的预处理变量:
函数匹配的规则
- 寻找所有同名的可见的函数。
- 从中去除所有不能匹配的,即即便类型转换也不能匹配的函数。
- 在剩下的函数中进行最优匹配。
当没有找到匹配,则会报错。
最优匹配
若有且仅有一个函数满足下列条件则匹配成功:
即在其他情况下相平的情况下有一点更优。
若没有,则报二义性错。
当使用重载函数的时候最好不要强制类型转换。
优的顺序
-
精确匹配
类型相同,数组/函数转指针,对顶层const操作
-
对底层const操作
-
小整数到大整数
-
算数类型转换/指针转换
-
类类型转换
函数指针
一个函数指针的类型只由形参和返回类型决定。当要声明的时候,只需要将函数名部分改为指针即可,但必须加括号。
直接使用**=进行赋值**。在使用时可以直接将指针名作为函数别名,。
当函数指针指向一个重载函数时,会根据函数指针的形参表来进行精准匹配。
函数可以使用函数指针来传入函数作为实参。格式与声明时相同,但最好还是使用类型别名与decltype来简化使用,也更加易于理解。
与数组一样,函数也可以用指针来返回。
函数会在传参的时候自动转换为函数指针。