1 函数基础
1.1 函数定义1.2 函数调用1.3 形参与实参1.4 返回类型1.5 局部对象
1.5.1 自动对象1.5.2 局部静态对象 2 参数传递
2.1 传值2.2 传址
2.2.1 传指针2.2.2 传引用2.2.3 为什么要使用传址 3 参数传递中的 const
3.1 指针或引用形参与const3.2 什么时候使用常量引用
1 函数基础 1.1 函数定义基础,略。
1.2 函数调用基础,略。需要注意的是,return语句的作用除了给主调函数传递被调函数的返回值,另一层意义在于将程序的控制权交还给主调函数。
1.3 形参与实参实参是形参的初始值,其值用来初始化形参对象。实参与形参按对应进行初始化,第一个实参初始化第一个形参,第二个实参初始化第二个形参。并不规定实参的求值顺序,编译器可以按照任意可行的顺序对实参求值。实参的类型必须与对应的形参类型匹配,除非可以完成(隐式的)类型转换。
范例:
// 定义一个名为fact的函数,其参数列表接收一个int类型对象,用来计算val的阶乘,并将结果以一个int类型返回
int fact(int val) {
int ret = 1; //局部变量,生命周期为函数体内,用于保存阶乘计算结果
while (val > 1) {
ret += val--; //把ret和val的乘积赋给ret,然后将val减1
}
return ret; //将ret的值作为结果返回给主调
}
int main() {
fact("hello"); //❌,实参类型不正确且不能完成隐式类型转换
fact(3.14); //正确,该实参能隐式转换为int类型,执行调用时,隐式地截取小数部分转成int类型,调用等价于fact(3);
}
形参列表:
1.4 返回类型显示定义空参列表的方式:在参数列表内写void,如int func(void) {…}形参可以仅指定类型而不给名字,但未命名的形参无法被使用,这种未命名的形参代表在函数体内不会使用它形参是否命名不影响调用时提供的实参数量,即使某个形参未命名,调用时也必须为它提供一个实参。
1.5 局部对象void返回类型,代表函数不返回任何值,结束调用时仅交还控制权限。有返回值的函数通常用于计算某个值,无返回值的函数通常用来执行某些动作。函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。
在C++中,名字(程序文本中的命名符号)有作用域,对象(内存中真实的存储实体)有生命周期。
函数的形参和函数体内部定义的变量统称为局部变量。
1.5.1 自动对象从名字的角度上来说:它们对于函数来说是“局部的”。它们的名字仅在函数的作用域内可见,并且在函数内部,它们会隐藏外层作用域中同名的名字。从对象的角度上来说:局部变量(对象)的生命周期取决于其定义的方式(分为自动对象和局部静态对象)。与之相对的是,在所有函数体(包括main函数)之外定义的对象,存在与程序的整个执行过程中,这种对象在程序启动时被创建,直到程序结束才会被销毁。
自动对象是只存在于函数块执行期间的对象。也就是普通局部变量所对应的对象,当函数执行路径经过函数块内的变量定义语句时创建该对象,到达定义所在的块末尾时销毁它。当块执行结束后,块中创建的自动对象的值为未定义。形参也是一种自动对象。
1.5.2 局部静态对象局部静态对象的作用是:绕开【函数外定义对象,并通过函数改变其值】的方法,使得函数内定义的对象可以在函数执行完调用后仍能在函数外使用。通过将局部变量定义前加static修饰符获得这样的局部静态对象,其生命周期贯穿函数调用及调用之后。局部静态对象在程序的执行路径第一次经过其对象定义语句时初始化,直到整个程序终止才销毁。
范例:
int val = 10; //这里的val是定义在所有函数外的(此程序中的)全局量,在整个程序结束后才会销毁
int func() {
int val = 0; //该函数中定义了一个名为val的局部变量,val这个名字仅在函数内可见,对于函数func来说,其内部使用到的所有val代表的都是这里定义的这个val,而不是在func以外定义的那个val
return ++val;
}
int count1 = 0; //count1是定义在所有函数外的(此程序中的)全局量
int count_call1(int count) {
return ++count;
}
int count_call2() {
static int ctr = 0; //定义一个局部静态变量ctr,其代表的对象在该函数调用结束后仍可访问
return ++ctr;
}
int count_call3() {
static int ctr = 0; //这里的局部静态变量ctr从对象视角来说,与count_call2中定义的局部静态变量ctr是两个不同的对象(两块不同的内存),从名字视角来说,这里的ctr名字仅在count_call3函数内有效,count_call2函数中的ctr名字也只在count_call2函数内有效,因此命名不冲突
ctr = 0;
return ++ctr;
}
int main() {
cout << val << endl; //输出10,这里的val是全局量val,任何函数都可以无需参数传递而直接使用全局量
cout << func() << endl; //输出1,调用func函数,func函数内定义了一个名为val(仅在函数func内有效)的局部量,初始值为0,该函数将其值加1后把值返回给此调用处
int count2 = 0; //定义了一个在main函数作用域内有效的局部量count2,初始化值为0
for (int i=0; i!=3; ++i) {
cout << "======" << endl;
cout << count_call1(count1) << endl; //在main函数中使用全局量count1的值初始化count_call1函数的形参(count),注意这里的count_call1使用的是值传递,因此循环中的每一次调用count_call1都会重新初始化其形参(即每一次都先将count_call1函数内部的count设为0再加1返回)
cout << count_call1(count2) << endl; //在main函数中使用main的局部量count2的值初始化count_call1函数的形参,也是值传递
cout << count_call2() << endl; //调用count_call2函数,第一次调用时,其函数内定义局部静态变量ctr的语句会初始化一个局部静态变量ctr,初始值为0,之后循环中每次调用到该函数时,当再执行到定义局部静态变量的语句时,就不再擦除重新定义,也就是说static的作用是,只初始化一次,之后该对象一直存在,直到整个程序结束,不会再次被初始化
cout << count_call3() << endl; //调用count_call3函数相对于count_call2的区别是,在函数中执行了对局部静态对象ctr的赋值操作,这会使得循环中每次调用count_call3函数时都要进行重新对ctr赋值的操作,这再次表明,初始化和赋值的区别
}
//循环部分的输出结果为:
return 0;
}
2 参数传递
每次调用函数都会重新创建它的形参,并用传入的实参对形参进行初始化。形参的类型决定了形参与实参的交互方式,因此,根据形参类型的不同,划分出了两种参数传递方式:
2.1 传值如果形参是非引用类型,那么在调用函数时,程序会将实参的值拷贝给形参,也就是说,此时形参和实参时两个互相独立的对象。我们说这样的实参被值传递,或者说该函数被传值调用。
在传值调用下,函数对形参做的所有操作都不会影响到实参。
函数的传址调用总的含义是通过调用该函数,改变其传入的实参对象本身。由于C++相对于C而言,除指针外,还增加了引用这种复合类型。因此C++函数的传址调用也可以用传指针和传引用两种方式完成。
2.2.1 传指针请特别注意,指针类型的形参也是非引用类型的形参,因此其行为和其他非引用类型的参数传递一样,也是值拷贝。当执行参数拷贝传递时,拷贝的是指针的值。拷贝之后,实参指针和形参指针是两个不同的指针对象,但是,他们指向的却都是同一个对象(因为参数是按指针的值拷贝传递的),因此我们也可以通过形参指针来间接访问它所指向的对象,修改其指向对象的值。
范例:
void func(int *p) {
*p = 0; //改变形参指针p所指对象的值
p = 0; //使形参指针p置空(不指向任何东西),传入的实参指针本身不会被改变(仍指向原来所指的对象)
}
int main() {
int i = 10;
int *ip = &i; //ip指向i这个对象
func(ip); //将ip指针的值(指向)拷贝给func函数中的形参指针p,现在p具有ip的值(指向),即ip和p是两个独立的指向i的指针。func1的操作通过形参指针p修改了i的值为0,并将形参指针p置空。则执行完该语句后,i的值为0,形参指针p置空(函数调用结束后被销毁),实参指针ip仍然存在并仍指向i
//综上,func函数通过传递一个指向i的指针来间接改变其外部对象i的值,这对于i来说相当于传址调用,但对于传入的指针ip本身来说是传值调用
return 0;
}
2.2.2 传引用
如果函数的形参类型是引用,那么这种传递方式将使得函数对其形参的操作直接改变实参本身,这种传递即传址。
范例:
#includeusing namespace std; int func1(int var) { //传值函数 return ++var; } int func2(int &var) { //func3用传入的int类型引用的值来初始化这里的形参(int类型的引用var),相当于把var绑定在了传入的实参引用绑定的那个对象上 return ++var; //将var引用绑定的对象的值+1后返回 } int func3(int *var) { //func3用传入的int类型指针的值来初始化这里的形参(int类型的指针var) return ++(*var); //将var指针解引用后的结果+1后返回 } int main() { int num = 1; int &r = num; int *p = # cout << num << endl; //1,输出num的初始值 cout << func1(num) < 2.2.3 为什么要使用传址 函数使用传址调用而不是传值调用主要有两个作用:
避免调用时对大型对象的整体拷贝使得函数可以对外返回额外信息,而不是仅能返回return后的一个返回值
范例:使用传址来向函数外部传递额外信息
int add(int a, int b, int &flag) { flag++; //通过flag传址修改外部实参来向外传递信息 return a + b; //返回计算结果本身 } int main() { int count = 0; int &r = count; int num1 = 2, nums2 = 4; cout << add(nums1, nums2, r) << endl; //求出num1+num2的结果,并通过修改r绑定的对象值来向外传递另一个内部信息,即add的调用次数 cout << r << endl; }3 参数传递中的 const牢记一点:形参的初始化方式和其他变量的初始化方式相同。和其他初始化过程一样,用实参的值初始化形参对象时,会忽略掉顶层const。也就是说,当参数列表中形参类型带有顶层 const 时,传给它常量对象或非常量对象都可以。有时这种忽略会导致错误的函数重载,特别要注意的是以下例子中的情况:
void func(const int i) { //函数能够读取形参i,但不能向形参i写值 ... } void func(int i) { //重载错误,重复定义了func,对于C++来说两个函数没有区别,因为第一个参数列表声明形参类型为const int的函数也可以接收一个int传递 ... } int main() { int a = 1; const int b = 2; func(a); //正确,可以用int实参初始化const int形参,拷贝时忽略顶层const func(b); //正确,实参形参类型一致 }3.1 指针或引用形参与const由于形参的初始化方式和其他变量的初始化方式相同。回顾通用的初始化规则:
可以使用非常量来初始化底层const对象,但反过来不行一个普通的引用必须用同类型的对象来初始化
将同样的规则类比到实参对形参的初始化上可以得到如下规则:
int i = 1; const int ci = i; void reset(int *p) { //该函数接受一个int类型指针,将其值(指向)拷贝给形参指针,然后通过解引用形参指针获得原实参指针所指的对象,将其值置为0 *p = 0; //相当于传址调用的传值调用,改变了p所指对象(实参指针所指对象)的值 } void reset(int &r) { //该函数接受一个int对象的引用,然后将对象的值置为0 r = 0; //传址调用,改变了r所绑定的那个对象(实参)的值 } reset(&i); //调用形参类型是int *版本的reset函数,i的值被修改为0 reset(&ci); //❌,该函数想调用传指针版本的reset,&ci返回的实参是一个指向const int类型的指针,但传指针版本的reset函数声明的形参类型是一个指向int类型的指针,也即不能用指向const int类型的指针(实参)初始化指向int类型的指针(形参),即不能用底层const对象初始化非常量对象 reset(i); //调用形参类型是int &版本的reset函数,i的值被修改为0 reset(ci); //❌,该函数想调用传引用版本的reset,ci是一个const int类型的引用,但传引用版本的reset函数声明的形参类型是一个int类型的引用,不能通过实参对形参的初始化将一个普通引用(形参r)绑定在const对象(ci)上 reset(42); //❌,该函数想调用传引用版本的reset,但不能把普通引用(形参r)绑定到字面值(实参42)上 void func(const int &var) { cout << var << endl; } func(10); //正确,func的形参var是对常量的引用,C++允许用字面值初始化常量引用特别注意,在以上范例中:
3.2 什么时候使用常量引用想调用传引用版本的reset,只能传入int类型的对象,不能使用字面值、求值结果为int的表达式、需要转换的对象或者const int类型的对象。想要调用传指针版本的reset,只能传入*int 类型的对象。当函数的形参声明为常量引用时,调用该函数允许传入字面值作为实参,因为可以用字面值初始化常量引用。
除非是为了2.2.3中的第二个原因(为了使得函数可以对外返回额外信息,而不是仅能返回return后的一个返回值),否则在声明函数用来传引用的形参时,尽量使用常量引用。
把函数不会改变的形参定义成普通的非常量引用会存在两个弊端:给函数的调用者带来一种误导,即认为:函数可以修改它实参的值(传址)会极大地限制函数所能接受的实参类型(如上例,不能把const对象、字面值或者需要类型转换的对象传递给普通的引用形参)



