第十七篇 -- 研究下函数
基于C++宝典学习。关于前面的函数组成,定义,声明,调用就不讲了。这里从指针和引用参数部分讲起。
函数已传值的方式传递参数,传递的只是实参的值,而不是实参本身。这样做一方面会有效率的问题,因为对于大的数据类型有一次赋值过程;另一方面在函数中也并不能改变实参的值,因为函数中只是构建了实参的一个副本。用指针和引用作为参数,可以弥补上述不足。
一、指针参数
在函数的参数列表中,可以使用指针类型的参数。传递给指针参数的实参可以是一个指针变量,也可以是一个变量的地址。例如,下面函数的参数就是一个int类型的指针:
void function(int *);
调用函数function时也必须传入一个int型的指针变量。例如:
int *p; //定义一个int型指针变量 ..... //某些操作改变了p的值 function(p); //以p作为实参,调用函数function int iVal = 0; //定义整型变量 function(&iVal); //获取整型变量的地址,并以指针的形式传递给函数
使用指针作为参数可以弥补值传递的不足:
1. 提高了传递参数的效率
2. 函数可以修改实参指针所指变量的值
对于比较大的数据类型,例如定义了很多数据成员的结构体,假设是100个整型数成员。如果将这样的结构体变量作为参数来传递,则在函数内部也要构建同样的一个结构体变量,并将其成员一次赋值。这样在内存中就会有两份同样的结构体变量,占据了800(200 * 4)个字节的内存。这无论在空间和时间上都是巨大的浪费。类也是如此。其实在C++中,结构体与类的差别很小。
如果用指针传递,则可以消除值传递带来的空间和时间上的浪费。这主要是因为指针的值是一个地址值(在32位系统中是一个4字节的整型数),参数传递时只需要传递这个地址值即可。
提示:用指针作为参数,遵从的也是值传递的原则。实际传递的值是指针的值,而不是指针所指变量的值。
例如,对于SomeStruct这样一个大的结构体,用指针作为参数可以写为:
void function(SomeStruct *p);
而用SomeStruct变量作为参数调用function函数时可以写为:
SomeStruct a; //定义一个SomeStruct变量 ...... //定义变量a的操作 SomeStruct *p = &a; //定义一个SomeStruct类型的指针,并指向上述变量(获取a的地址值) function(p); //用SomeStruct类型的指针作为实参,调用function函数
这样在调用函数时,函数就可以只构造一个指针变量的副本,而不用构造整个结构体的副本,大大节省了内存,而且省去了给每个数据成员赋值的时间,也大大提高了时间效率。用指针作为参数还有一个好处,就是函数可以修改指针实参所指变量的值。这是因为通过指针参数传递的是变量的地址,在函数内可以通过这个地址获取变量,从而也就可以修改变量的地址。例如:
void function(int *p) //定义函数function { *p = 123; //获取指针形参所指变量,并给该变量赋值 } ...... int a = 0; //定义整型变量a,并初始化为0 function(&a); //将变量a的地址传递给function函数 cout << a; //输出变量a的值
上述程序将输出123,而不是0。这就是因为在函数内部将变量a的值修改了。而如果参数int类型的,则达不到这种效果。下面的例子定义一个函数,交换两个变量的值。由于使用普通类型的参数不能修改实参的值,所以可以用指针作为参数。
void swap(int *a, int *b) { int temp = *a; //用一个临时变量保存a指针所指变量的值 *a = *b; //将b指针所指变量的值赋给a指针所指的变量 *b = temp; //将临时变量的值赋给b指针所指的变量 } int main(int argc, char *argv[]) { cout << "——指针参数——" << endl; //输出提示信息 cout << "请输入两个整型数:" << endl; int a = 0, b = 0; //保存用户输入的变量 cin >> a; //输入 cin >> b; swap(&a, &b); //调用交换函数 cout << "交换后:" << a << "\t" << endl; //输出交换后的值 system("PAUSE"); //等待用户反应 return EXIT_SUCCESS; }
在swap函数的定义中,用指针作为参数,避免了使用普通类型的参数,从而达到了交换变量值的目的。其实看到这里,或许会产生疑问,之前学过引用,那个也可以改变变量的值,和指针好像啊,对不对,然后回头去看了下引用,引用其实是一个变量实体的别名。指针可以重新赋值指向新的对象,引用在初始化之后就不可以改变指向对象了。然后呢,通过冒泡排序,使用这两种方法,以示区别。compare引用,swap指针
#include "pch.h" #include <iostream> using namespace std; #define len 10 //define array length equal 10 //function prototype void Method_one(); void Method_two(); void compare(int &a, int &b); void swap(int *a, int *b); int break_node = 0;//when break_node is true, jump the loop int main() { //Bubble sort //Method_one(); Method_two(); } void Method_one() { //define an array which length is 10 int array_sort[len]; //input array value cout << "Please input " << len << " number: " << endl; for (int i = 0; i < len; i++) cin >> array_sort[i]; //start sort for (int m = len; m > 0; m--) { for (int j = 0; j < m - 1; j++) { compare(array_sort[j], array_sort[j + 1]); } if (break_node) { break_node = 0; } else {//while no change data in inner loop, jump out the external loop break; } } //output sorted array(high-->low) cout << "Sorted array: " << endl; for (int k = 0; k < len; k++) { cout << array_sort[k] << " "; } cout << endl; } void Method_two() { int length = 0; cout << "Please input the length of array : " << endl; cin >> length; cout << "Please input " << length << " values" << endl; int *pArr = new int[length]; //input array value for (int i = 0; i < length; i++) cin >> pArr[i]; //start sort for (int m = length; m > 0; m--) { for (int j = 0; j < m - 1; j++) { compare(pArr[j], pArr[j + 1]); } if (break_node) { break_node = 0; } else {//while no change data in inner loop, jump out the external loop break; } } //output sorted array(high-->low) cout << "Sorted array: " << endl; for (int k = 0; k < length; k++) { cout << pArr[k] << " "; } cout << endl; delete pArr; } void compare(int &a, int &b) { if (a >= b); else { //int temp; //temp = a; //a = b; //b = temp; //break_node++; swap(&a, &b); break_node++; } } //change two variable position void swap(int *a, int *b) { int temp = *a; *a = *b; *b = temp; }
二、数组参数
用数组作为函数的参数,即函数的形参是数组。例如:
void function(int a[10]);
上述的函数声明表示function函数接受一个整型数组作为参数。但是,用数组作为函数参数时,数组的长度是没有意义的,也就是说上述的声明同以下的声明是等效的:
void function(int a[]);
void function(int a[100]); //数组长度没有意义,所以a[10]同a[100]是等效的
所以在定义函数时,不能依赖于数组参数中数组的长度。实际上,编译器会自动将数组参数作为指针进行解释,这个指针指向一块儿连续的内存。而这样的一个指针中不会保存长度信息,所以函数声明时,数组参数的长度是没有意义的。为了弥补这个缺点,可以在参数列表中在附加一个参数,用以传递数组的长度,例如:
void function(int a[], int n);
其中第二个参数n就是数组的长度。
说明:定义函数时数组参数作为指针使用,而这个指针就指向数组实参的首地址,也就是数组中第一个元素的地址。
正因为在定义函数时,数组参数被当做指针,所以数组参数也可以用指针参数表示:
void function(int *p);
这个声明同function(int a[])的声明是等价的。一般来讲,如果用数组作为函数的参数,则函数调用时应当将一个数组作为实参。因为数组参数在函数定义时被当做指针,所以也可以将指针作为实参。至于这个指针到底 指向什么样的内存,则是由调用者确定的。用数组作为实参,则传递给函数的是数组名,调用过程如下:
int a[] = {1, 2, 3}; //定义数组
function(a); //以数组名作为实参,调用函数function
或者也可以先取得数组的首地址,作为指针传递给函数:
int a[] = {1, 2, 3}; //定义数组
int *p = a; //获取数组首地址,也可以写成int p = &a[0];
function(p); //以指向数组首地址的指针作为实参,调用函数function
因为数组参数在定义函数时被当做指针使用,传递的值是数组的首地址。所以在函数内如果修改了数组参数中某个元素的值,也就修改了对应的数组实参中元素的值。C++程序中常用的排序函数就利用了数组参数的这个特性,它在函数中对数组参数进行排序,函数运行完后,数组中的元素就是排序后的结果。
三、函数中的变量
1. 多个源文件共享全局变量
全局变量的作用域虽然是整个程序,但在使用时仍然有特殊的要求。假设有两个源文件file1.cpp和file2.cpp,其中file1.cpp中定义了一些全局变量,如果file2.cpp中的函数要使用这些全局变量,则必须在使用前声明。声明的方法是:
extern 类型 全局变量名;
其中extern关键字表明这个全局变量是在别的文件中定义的,需要在本文件中使用。例如,假设在file1.cpp中定义了如下的全局变量:
/////////////////////////////////////////////////////////////////////// ////////file1.cpp 源文件1 int gVal1 = 0; double gVal2 = 0.0;
则在file2.cpp中要使用上述两个变量时,必须做如下的声明:
//////////////////////////////////////////////////////////////////// /////////file2.cpp extern int gVal1 = 0; extern double gVal2 = 0.0;
当心:在全局变量前加上extern关键字只是用来声明一个全局变量,而不是定义全局变量。如果只有extern声明,没有定义,则全局变量仍然不存在。编译器会报告“某某全局变量没有定义”的错误。
2. 静态变量
静态变量分两种,一种是函数内的静态变量,一种是全局的静态变量,其特点是变量定义时带有static关键字。例如:
static int gVar; //在函数外,定义全局的静态变量 void function() { static int iVar; //在函数内,定义函数内的静态变量 ... }
函数内的静态变量也称为局部静态变量,其作用域只限于函数内部,别的函数不能访问。局部静态变量的生命周期同全局变量一样,在整个程序运行期间都有效。例如:
void function() { static int iVal = 0; cout <<iVal++ << ','; } ... for(int i = 0; i < 3; i++) { function(); }
上述程序运行时将输出“0,1,2”,而不是“0,0,0”。这是因为iVal是一个局部静态变量,其生命周期在程序运行期间一直有效,所以函数function每一次运行结束后,并没有销毁iVal。而且,函数function每一次访问到的iVal的值都是上一次函数运行的结果。例如,function第一次运行后,iVal的值是1,第二次运行访问iVal的值就是1,同样第三次访问到的值就是2。
基于这个特性,可以利用局部静态变量保存每一次函数运行时的状态,以供函数再次被调用时使用。虽然全局变量也可以做到这一点,但是任何函数都可以访问,不利于控制。而局部静态变量只有本函数能够访问,可以有效的限制其作用域。例如,某些严格的安全系统对用户试图登录的次数有限制,可以用静态变量记录这个次数,超过限定次数后则阻止用户继续尝试,用全局变量则给了其他函数修改变量的机会,不符合安全性的要求。
#include "pch.h" #include <iostream> using namespace std; void login(); int main() { for (int i = 0; i < 4; i++) { login(); //调用登录函数 } return 0; } void login() { static int sLogNum = 0; //记录登录次数的静态变量 sLogNum++; //递增登录次数 if (sLogNum > 3) { cout << "登录次数已超过3次,不允许登录!" << endl; //输出禁止信息 } else { cout << "登录成功" << endl; } }
3. 全局静态变量
同一般全局变量类似,全局静态变量也是在函数外部定义的变量,只是定义之前带有static关键字。例如:
static int gVar;
跟一般全局变量不同的是:全局静态变量的作用域仅限于定义这个变量的源文件,而不是整个程序。例如,假设程序中有file1.cpp和file2.cpp两个源文件,其中file1.cpp中定义了全局静态变量gVar,则在file2.cpp中试图访问gVar时,会遇到一个编译错误:“变量gVar未定义”,这就是因为gVar是定义在file1.cpp中的全局静态变量,只能在file1.cpp中访问。