通过调用运算符来执行函数,调用运算符为()
。
函数调用完成两项工作:第一、实参初始化函数对应的形参;第二、将控制权转移给被调函数,也就是说,主调函数的执行被暂时中止,被调函数开始执行。
函数结束完成两项工作:第一、返回return语句中的值;第二、控制权从被调函数转移回主调函数。
名字有作用域、对象有生命期。
作用域是指名字发挥作用的区域,通常而言,一组花括号就是一个作用域。以以下代码为例,两个变量名称相同,但表示的对象不同,他们分别属于不同的作用域,同时,外部也无法访问循环作用域中的a与i。
生命期是指对象在内存中存在的时间。
int a=1;
for(i=0;i<6;i++){
int a=i;
cout << a << endl;
}
形参和函数体内部定义的变量统称为局部变量,仅在函数的作用域内可见。主函数中的变量也是局部变量,只有在主函数之外定义的才属于全局变量。
在所有函数体之外定义的对象存在于程序的整个执行过程中。此类对象在程序启动时被创建,直到程序结束才会销毁局部变量的生命周期依赖于定义的方式。
把只存在于块执行期间的对象称为自动对象。当块执行结束后,块中创建的自动对象的值就变成未定义的了。
形参是一种自动对象。如果局部变量对应的自动对象本身含有初始值,就用这个初始值进行初始化;否则,如果变量定义本身不含初始值,执行默认初始化,即产生未定义的值。
局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,并直到程序终止时才被销毁,在此期间即使对象所在函数结束执行也不会对它有影响。
如果没有显式的初始值,内置类型的局部静态变量的初始化为0。
形参初始化的机理和变量初始化一样。
如果形式参数是引用类型,它将绑定到对应的实参上;否则,将实参的值拷贝后赋给形式参数。
当形式参数是引用类型时,称对应的实参被引用传递,或函数被传引用调用。
当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。称对应的实参为值传递或者函数被传值调用。
初始值拷贝给变量,此时,对变量的改动不会影响初始值。
指针形参可以访问函数外部的对象,但是在C++中建议使用引用类型。
当形参是const时,需要注意关于顶层const的讨论。
当形参有顶层const时,传给他常量或者非常量都是可以的。
当形参没有顶层const,传给他顶层const也是可以的。
void fcn(const int i) /*fcn能够读取i,但是不能向i写值*/
复习:
int i = 0;
int *const p1 = &i; /*顶层const*/
const int ci = 42; /*顶层const*/
const int *p2 = &i; /*底层const*/
const int &r = i; /*底层const*/
拷贝时,底层const作为被拷贝者时,要求拷贝者是底层const,底层const作为拷贝者时,要求被拷贝者也是底层const;
初始化时,只有当底层const作为初始化者时,对于被初始化者是有要求的。
可以使用非常量初始化一个底层const,但是反过来不行。
如果无需改变引用对象的值,那么尽量使用常量引用。
数组有两个特殊的性质:
在传递数组时,实现的是值传递中的指针传递。
传递数组的形式有
void print(const int *i);
void print(const int []);
void print(const int [10]);
void print(const int a[]);
void print(const int a[10]);
定义为顶层constant指针。
void print(int (&arr)[10]);
另外,需要强调的是,引用数组和数组引用是有区别的:
int (&arr)[10] /*引用数组*/
int &arr[10] /*数组引用*/
无法预知应该向函数传递几个实参。
如果所有实参类型相同,可以传递一个名为initializer_list的标准库类型;
如果实参类型不同,可以编写一种特殊的函数,也就是可变参数模版。
initializer_list类型的操作:
initializer_list<T> lst; /*默认初始化:T类型元素的空列表*/
initializer_list<T> lst{a,b,c,...}; /*1st的元素时对应初始值的脚本,列表中的元素是const*/
lst2(lst);list2 = lst; /*拷贝或赋值一个initializer_list对象不会拷贝列表中的元素;拷贝后,原始列表和副本共享元素*/
lst.size() /*列表中的元素数量*/
lst.begin() /*返回指向lst中首元素的指针*/
lst.end() /*返回指向lst中尾元素下一位置的指针*/
与vector不同的是,initializer_list对象中的元素永远是常量值。
在传递多个值时,需要用花括号表示。
void error_msg(initializer_list<string> i1){
cout << i1.size();
}
error_msg({"functionX","expected","actual"});
没有返回值的return语句只能用在返回类型是void的函数中。
void类型的函数不要求有return语句,自身默认会补充一句。
含有return语句的循环后面应该也有一条return语句,如果没有的话该程序就是错误的。很有编译器无法发现此类错误。
返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
务必不能返回局部对象的引用或指针。局部变量被释放,指针指向了不再有效的内存区域。引用的道理也是如此。
函数的返回类型决定函数的调用是否是左值。调用一个返回引用的函数得到左值,其他返回类型得到右值。
char &get_val(string &str,string::size_type ix){
return str[ix];
}
int main(){
string s("a value");
get_val(s,0) = 'A'; // 将s[0]变为‘A’
}
感觉就是初始化过程char &get_val=str[ix]
。
针对vector、initializer_list这样的类型,函数可以返回花括号包围的值的列表。
vector<string> process(){
return {"functionX","okay"};
}
函数不能返回数组,但是可以返回数组的指针或引用。
int *p1[10]; //含有10个指针的数组;
int (*p2)[10]; //指向数组的指针
数组指针的函数可以如下书写:
Type (*function(parameter_list))[dimension] //这与数组指针的写法很类似
尾位置返回类型。任何函数的定义都能使用,用于交代返回类型。
auto func(int i) -> int(*)[10]
如果同一作用域内的几个函数名字相同但形参列表不同,称之为重载函数。
void print(const char *cp);
void print(const int *beg,const int *end);
void print(const int ia[],size_t size);
编译器会根据传递的实参类型或数量推断所使用的函数。
main函数不能重载。
不允许两个函数除了返回类型外其他所有要素都相同。
一个有顶层const的形参无法和另一个没有顶层const的形参区分开。
Record lookup(Phone);
Record lookup(const Phone); //这两个声明是等价的
而底层const可以起到区分的作用。
Record lookup(Phone *);
Record lookup(const Phone *)
const对象只能传递给const形参;非常量都可以传递,但是首先传递给非常量。
如果在内层作用域中声明名字,它将隐藏外层作用域中的同名实体。
在不同作用域中无法重载函数名。
调用默认实参时,省略实参就行。
string screen(sz ht = 24,sz wid = 80,char backgrnd = '');
string window;
window = screen();
函数调用时,实参按照其位置进行解析,默认实参负责填补函数调用缺少的尾部实参,也就是说,想覆盖backgrnd的默认值需要为ht和wid提供实参。
必须在函数的声明中添加默认值,需要注意的是一个函数只能声明一次,如果要多次声明,那么形参只能被赋予一次默认值,后续的声明只能为没有默认值的形参添加默认值。
局部变量不能作为默认值。
一般函数在调用时比求等价表达式的值要慢一些。调用前要先保存寄存器,并在返回时恢复,可能需要拷贝实参,程序转向一个新的位置继续执行。
内联函数可以避免函数调用的开销。
inline int add(int a,int b){
return a + b;
}
在函数定义前加上inline则完成了内联函数的使用。
内联机制适用于规模较小、流程直接、调用频繁的函数。
内联不允许出现循环和switch。
常量函数是指能用于常量表达式的函数。
定义constexpr函数的方法与其他函数类似,有几项规定需要遵守:
constexpr int new_sz() { return 42; }
constexpr int foo = new_sz;
编译器把对constexpr函数的调用替换成结果值。为了能在编译过程中随时展开,constexpr函数被隐式指定为内联函数。
constexpr size_t scale(size_t cnt){return new_sz()*cnt;}
// 如果arg是常量表达式,则scale(arg)返回的也是常量表达式,反之则是变量。
结合上述例子来看,constexpr函数给了函数一个成为常量表达式的机会。
常量表达式是指值不会改变并且能在编译过程就能得到计算过程的表达式。
字面值就属于常量表达式,用常量表达式初始化的const对象也是常量表达式。
const int max_files = 20;
const int limit = max_files + 1;
int staff_size =27; //不是常量表达式
const int sz = get_size(); //不是常量表达式
在复杂的系统中,很难分辨一个初始值是不是常量表达式,即使要求数据类型为const,但初始化值不是常量表达式的话,则其也不是常量表达式。
通过声明变量为constexpr类型,由编译器来验证变量的值是否是一个常量表达式。
声明为constexpr
的变量一定是一个常量,而且必须用常量表达式初始化。
常量表达式的值需要在编译时就得到计算,所以对声明constexpr时用到的类型必须有所限制。因为这些类型比较简单,称之为“字面值类型”。
算数类型、引用、指针都属于字面值类型。自定义类、IO库、string类型都不属于字面值类型。
指针和引用都能定义为constexpr类型,对于指针来说初始值必须是nullptr或者0,或者是存储与某个固定地址的对象。
一般来说,函数体内定义的变量并非存放在固定地址中,因此constexpr不能指向这样的变量,而定义于函数体之外的则可以。
constexpr仅对指针有效,对指针所指的对象无关。
const int *p = nullptr; // 指向整型常量的指针
constexpr int *p = nullptr; // 指向整数的常量指针
constexpr把它所定义的对象置为了顶层const。
constexpr可以指向常量,也可以指向非常量,地址是常量就行。
当重载函数的形参数量相等以及某些形参的类型可以由其他类型转换得到时,选择合适的重载函数就十分困难。
函数匹配的第一步是选定本次调用对应的重载函数集。集合中的函数称之为候选函数。候选函数具备两个特征:
void f();
void f(int);
void f(int,int);
void f(double,double=3.14);
f(5.6);
上述例子中候选函数有六个。
函数匹配的第二步考察本次调用提供的实参,然后从候选函数中选出被这组实参调用的函数,这些新选出的函数被称为可行函数。可行函数有两个特征:
上述例子中,函数2与函数4都是可行函数。如果没可行函数,编译器将报告无匹配函数的错误。
函数匹配的第三步是从可行函数中选择与本次调用最匹配的函数。它的基本思想是,实参类型和形参类型接近且越匹配越好。
当实参数量有两个或更多时,函数匹配比较复杂。如果传入的实参为(42,2.56)
时,可行函数为f(int,int)
和f(double,double)
。如果有且仅有一个函数满足以下条件:
如果不存在这样一个唯一的函数,那么报错。
函数指针指向的是函数而非对象。
函数指针和返回指针的函数的定义区别和数组指针和包含指针的数组的定义区别是一致的。
bool (*pf)(const string &);
// pf指向一个函数,该函数的参数是一个const string的引用
pf = lengthCompare; //pf指向名为lengthCompare的函数
pf = &lengthCompare; //等价赋值
bool b1 = pf("hello");
bool b2 = (*pf)("hello");
bool b3 = lengthCompare("hello"); //上述三个调用是等价的
在指向不同函数类型时,不存在转换规则。
void ff(int *);
void ff(unsigned int);
void (*pf1)(unsigned int) = ff; // 由函数指针的参数决定指向哪个重载函数。
void useBigger(bool pf(const string &));
void useBigger(bool (*pf)(const string &));
传参时,有:
useBigger(lengthCompare);
useBigger(pf)
和数组一样,不能返回函数,但是能返回指向函数类型的指针。把返回类型写成指针形式即可。
函数类型可以如下书写:把函数名当作变量,结构与函数指针类似:
int(*)(int*,int*) f1(int);
int (*f1(int))(int*,int*)