C++ Primer Plus学习(七)——函数简介


函数的基本知识

  • 创建函数时,必须完成如下工作:提供函数定义;提供函数原型;调用函数。
  • C++对于函数返回值的类型有一定的限制:不能是数组,但可以是其他任何类型——整数、浮点数、指针,甚至可以是结构和对象。
  • 函数原型不要求提供变量名,有类型列表就足够了。通常,在原型的参数列表中,可以包括变量名,也可以不包括。原型中的变量名相当于占位符,因此不必与函数定义中的变量名相同。
  • 函数原型的功能:
    1. 编译器正确处理函数返回值;
    2. 编译器检查使用的参数数目是否正确;
    3. 编译器检查使用的参数类型是否正确,如果不正确,则转换为正确的类型(如果可能的话)。在编译阶段进行的原型化被称为静态类型检查(static type checking)。

函数参数和按值传递

  • 用于接收传递值的变量被称为形参;传递给函数的值被称为实参。
  • 如果函数的两个参数的类型相同,则必须分别指定每个参数的类型,而不能像声明常规变量那样,将声明组合在一起;原型中的变量名不必与定义中的变量名相同,而且可以省略;提供变量名将使原型更容易理解,尤其是两个参数的类型相同时。
  • 对于(10 * 9) / (2 * 1)(10 / 2) * (9 / 1),前者将计算90 / 2,得到45;后者将计算为5 * 9,得到45。当数字非常大时,这种交替进行乘除运算的策略可以防止中间结果超出最大的浮点数。

默认参数

在C++中,定义函数时可以给形参指定一个默认的值,这样调用函数时如果没有给这个形参赋值(没有对应的实参),那么就使用这个默认的值。也就是说,调用函数时可以省略有默认值的参数。如果用户指定了参数的值,那么就使用用户指定的值,否则使用参数的默认值。

所谓默认参数,指的是当函数调用中省略了实参时自动使用的一个值,这个值就是给形参指定的默认值。

C++规定,默认参数只能放在形参列表的最后,而且一旦为某个形参指定了默认值,那么它后面的所有形参都必须有默认值。实参和形参的传值是从左到右依次匹配的,默认参数的连续性是保证正确传参的前提。

函数和数组

  • 在C++中,当(且仅当)用于函数头或函数原型中,int *arrint arr[]的含义才是相同的。它们都意味着arr是一个int指针。将指针(包括数组名)加1,实际上是加上了一个与指针指向的类型的长度(以字节为单位)相等的值。对于遍历数组而言,使用指针加法和数组下标时是等效的。

  • 传递常规变量时,函数将使用该变量的拷贝;但传递数组时,函数将使用原来的数组。实际上,这种区别并不违反C++按值传递的方法,函数仍传递一个值,这个值被赋给一个新变量,但这个值是一个地址,而不是数组的内容。

  • 将数组地址作为参数可以节省复制整个数组所需的时间和内存。

  • 对于处理数组的C++函数,必须将数组中的数据种类、数组的起始位置和数组中元素数量提交给它;传统的C/C++方法是,将指向数组起始处的指针作为一个参数,将数组长度作为第二个参数(指针指出数组的位置和数据类型);还有另一种给函数提供所需信息的方法,即指定元素区间(range),这可以通过传递两个指针来完成:一个指针标识数组的开头,另一个指针标识数组的尾部。

  • 写个例子加深一下各方面理解:

    #include <iostream>
    const int Max = 5;
    int fill_array(double ar[], int limit);
    void show_array(const double ar[], int n);  // do not change data
    void revalue(double r, double ar[], int n);
    
    int main()
    {
    	using namespace std;
    	double properties[Max];
    
    	int size = fill_array(properties, Max);
    	show_array(properties, size);
    
    	if (size > 0)
    	{
    		cout << "Enter revaluation factor: ";
    		double factor;
    		while (!(cin >> factor))   // bad input
    		{
    			cin.clear();
    			while (cin.get() != '\n')
    				continue;
    			cout << "Bad input; Please enter a number: ";
    		}
    		revalue(factor, properties, size);
    		show_array(properties, size);
    	}
    	cout << "Done.\n";
    	cin.get();
    	cin.get();
    	return 0;
    }
    
    int fill_array(double ar[], int limit)
    {
    	using namespace std;
    	double temp;
    	int i;
    	
    	for (i = 0; i < limit; i++)
    	{
    		cout << "Enter value #" << (i + 1) << ": ";
    		cin >> temp;
    		if (!cin)   // bad input
    		{
    			cin.clear();
    			while (cin.get() != '\n')
    				continue;
    			cout << "Bad input; input process terminated.\n";
    			break;
    		}
    		else if (temp < 0)  // signal to terminate
    			break;
    		/// 如果输入错误或者负值,则终止输入,返回当前输入值的数量;否则将输入值增加到数组中
    		ar[i] = temp;
    	}
    	return i;
    }
    
    void show_array(const double ar[], int n)
    {
    	using namespace std;
    	for (int i = 0; i < n; i++)
    	{
    		cout << "Property #" << (i + 1) << ": $";
    		cout << ar[i] << endl;
    	}
    }
    
    void revalue(double r, double ar[], int n)
    {
    	for (int i = 0; i < n; i++)
    		ar[i] *= r;
    }
    	
    
  • 指针和const

    1. 可以用两种不同的方式将const关键字用于指针。第一种方法是让指针指向一个常量对象,这样可以防止使用该指针来修改所指向的值,第二种方法是将指针本身声明为常量,这样可以防止改变指针指向的位置。

    2. 举个例子来说明一下:

      int age = 39;                          
      const int * pt = &age;
      

      pt的声明并不意味着它指向的值实际上就是一个常量,而只是意味着对pt而言,这个值是常量。例如,pt指向age,而age不是const。可以直接通过age变量来修改age的值,但不能使用pt指针来修改它。同时,该声明中的const只能防止修改pt的值(这里是39),而不能防止修改pt的值。也就是说,可以将一个新的地址赋给pt

      再比如:

      int sloth = 3;
      const int * ps = &sloth;         // a pointer to const int
      int * const finger = &sloth;    // a const pointer to int
      

      在最后一个声明中,关键字const的位置与以前不同,这种声明格式使得finger只能指向sloth,但允许使用finger来修改sloth的值。也就是,地址是常量。

      如果愿意的话,还可以声明指向const对象的const指针:

      double trouble = 2.98;
      const double * const stick = &trouble;
      
    3. 综上来讲,其实有四种组合关系:将常规变量的地址赋给常规指针;将常规变量的地址赋给指向const的指针;将const变量的地址赋给指向const的指针;将const的地址赋给常规指针。这里面,第四种是不可行的,其他都可行。

    4. 将指针参数声明为指向常量数据的指针有两条理由:a. 这样可以避免由于无意间修改数据而导致的编程错误;b. 使用const使得函数能够处理const和非const实参,否则将只能接受非const数据。如果条件允许,则应将指针形参声明为指向const的指针。

函数和C-风格字符串

  • 将字符串作为参数时意味着传递的是地址,但可以使用const来禁止对字符串参数进行修改。
  • C-风格字符串有内置的结束字符,这意味着不必将字符串长度作为参数传递给函数,而函数可以使用循环依次检查字符串中的每个字符,直到遇到结尾的空值字符为止。

函数和结构

涉及到函数传递时,结构变量的行为更接近于基本的单值变量:

  • 可以将一个结构赋给另一个结构;
  • 可以按值传递结构,就像普通变量那样;在这种情况下,函数将使用原始结构的副本;函数也可以返回结构;但这样有一个缺点,如果结构非常大,则复制结构将增加内存要求,降低系统运行的速度;
  • 也可以传递结构的地址,用指针来访问结构的地址;与数组名就是数组第一个元素的地址不同的是,结构名只是结构的名称,要获得结构的地址,必须使用地址运算符&
  • 另外,C++还使用&表示引用变量,按引用传递也是特别重要的一种方式。

递归

函数指针

总结

函数是C++的编程模块。要使用函数,必须提供定义和原型,并调用该函数。函数定义是实现函数功能的代码;函数原型描述了函数的接口; 传递给函数的值的数目和种类以及函数的返回类型,函数调用使得程序将参数传递给函数,并执行函数的代码。

在默认情况下,C++函数按值传递参数。这意味着函数定义中的形参是新的变量,它们被初始化为函数调用所提供的值。因此,C++函数通过使用拷贝,保护了原始数据的完整性。

C++将数组名参数视为数组第一个元素的地址。从技术上讲,这仍然是按值传递的,因为指针是原始地址的拷贝,但函数将使用指针来访问原始数组的内容。当且仅当声明函数的形参时,下面两个声明才是等价的:

typeName arr[];
typeName * arr;

这两个声明都表明,arr是指向typeName的指针,但在编写函数代码时,可以像使用数组名那样使用arr来访问元素:arr[i]。即使在传递指针时,也可以将形参声明为const指针,来保护原始数据的完整性。由于传递数据的地址时,并不会传输有关数组长度的信息,因此通常将数组长度作为独立的参数来传递。另外,也可传递两个指针(其中一个指向数组开头,另一个指向数组末尾的下一个元素),以指定一个范围,就像STL使用的算法一样。

C++提供了3种表示C-风格字符串的方法:字符数组、字符串常量和字符串指针。它们的类型都是char*char指针),因此被作为char*类型参数传递给函数。C++使用空值字符(\0)来结束字符串,因此字符串函数检测空值字符来确定字符串的结尾。

C++还提供了string类,用于表示字符串。函数可以接受string对象作为参数以及将string对象作为返回值。string类的方法size()可用于判断其存储的字符串的长度。

C++处理结构的方式与基本类型完全相同,这意味着可以按值传递结构,并将其用作函数返回类型。然而,如果结构非常大,则传递结构指针的效率将更高,同事函数能够使用原始数据。这些考虑因素也适用于类对象。

C++函数可以是递归的,也就是说,函数代码中可以包括对函数本身的调用。

C++函数名与函数地址的作用相同。通过将函数指针作为参数,可以传递要调用的函数的名称。


版权声明:本文为weixin_41480034原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。