【C++ Primer Plus】第7章 函数——C++的编程模块

7.1 函数基本知识

7.1.1 有返回值的函数

  1. 如果声明的返回类型为double,而函数返回一个int表达式,则该int值将被强制转换为 double类型。
  2. 返回类型:不能是数组,但可以是其他任何类型——整 数、浮点数、指针,甚至可以是结构和对象!(有趣的是,虽然C++函 数不能直接返回数组,但可以将数组作为结构或对象组成部分来返回。
  3. 函数在执行返回语句后结束。如果函数包含多条返回语句(例如, 它们位于不同的if else选项中),则函数在执行遇到的第一条返回语句后结束。

7.1.2 函数原型(函数声明)

为什么需要原型?
原型描述了函数到编译器的接口,也就是说,它将函数返回值的类型(如果有的话)以及参数的类型和数量告诉编译器。

函数原型的语法:
double add(double a, double b); 或者double add(double, double);
原型中的变量名不必与定义中的变量名相同,而且可以省略:原型中的变量名相当于占位符,因此不必与函数定义中的变量名相同。

函数原型的作用:

  1. 编译器正确处理函数返回值;
  2. 编译器检查使用的参数数目是否正确;
  3. 编译器检查使用的参数类型是否正确。如果不正确,则转换为正确的类型(如果可能的话)。

7.2 按值传递函数参数

cube(side)执行的操作将不会影响main( )中的side数据,因为 cube( )使用的是side的副本,而不是原来的数据。
如果函数的两个参数的类型相同,则必须分别指定每个参数的类型,而不能像声明常规变量那样,将声明组合在一起。int add(int a, int b);
(10 * 9)/(2 * 1)(10 / 2)*(9 / 1),前者将计算90/2,得到45,后者将计算为5*9,得到45。这两种方法得到的结果相同,但前者的中间值(90)大于后者。因子越多,中间值的差别就越大。当数字非常大时,交替进行乘除运算的策略可以防止中间结果超出最大的浮点数。

7.3 函数和数组

7.3.1 带数组参数的函数

int sum_arr(int arr[], int n); // 指针变量arr,第一个参数传递数组名作为第一个元素的地址;第二个参数传递数组的长度

  1. arr实际上并不是数组,而是一个指针!
  2. 在编写函数的其余部分时,可以将arr看作是数组。
  3. 接受数组名(地址)作为参数,访问的是原始数组,而不是其副本。
  4. 为将数组类型和元素数量告诉数组处理函数,请通过两个不同的参数来传递它们,而不要试图使用方括号表示法来传递数组长度。

arr[i] == *(ar + i) // values in two notations
&arr[i] == ar + i // addresses in two notations
记住,将指针(包括数组名)加1,实际上是加上了一个与指针指向的类型的长度(以字节为单位)相等的值。对于遍历数组而言,使用指针加法和数组下标是等效的。

#include <iostream>

const int ArSize = 8;
int sum_arr(int arr[], int n);  // 指针变量arr

int main()
{
    
    
    int cookies[ArSize] = {
    
    1,2,4,8,16,32,64,128};
    std::cout << cookies << " = array address, ";   // 有些C++实现以十进制而不是十六进制格式显示地址,还有些编译器以十六进制显示地址时,会加上前缀0x。
    std::cout << sizeof cookies << " = sizeof cookies\n";   // 32, sizeof cookies是整个数组的长度
    int sum = sum_arr(cookies, ArSize); // cookies是数组名,而根据C++规则,cookies是其第一个元素的地址,因此函数传递的是地址.
                                        // 接受数组名参数的函数访问的是原始数组,而不是其副本
    std::cout << "Total cookies eaten: " << sum << std::endl;
    sum = sum_arr(cookies, 3);          // a lie, 通过第二个参数获知数组中的元素数量
    std::cout << "First three eaters ate " << sum << " cookies.\n";
    sum = sum_arr(cookies + 4, 4);      // another lie, 提供假的数组起始位置,也可以使用 &cookies[4]
    std::cout << "Last four eaters ate " << sum << " cookies.\n";
    return 0;
}
// return the sum of an integer array
int sum_arr(int arr[], int n)   // 必须显式传递数组长度, arr指针本身并没有指出数组的长度
{
    
    
    int total = 0;
    std::cout << arr << " = arr, ";
    // some systems require a type cast: unsigned (arr)
    std::cout << sizeof arr << " = sizeof arr\n";   // 4, sizeof arr只是指针变量的长度
    for (int i = 0; i < n; i++)
        total = total + arr[i];
    return total;
}

out:

0xe7793ffb50 = array address, 32 = sizeof cookies
0xe7793ffb50 = arr, 8 = sizeof arr
Total cookies eaten: 255
0xe7793ffb50 = arr, 8 = sizeof arr
First three eaters ate 7 cookies.
0xe7793ffb60 = arr, 8 = sizeof arr
Last four eaters ate 240 cookies.

7.3.2 用const保护数组

为防止函数无意中修改数组的内容,可在声明形参时使用关键字const:
void show_array(const double ar[], int n); // 使用const意味着show_array( )不能修改传递给它的数组中的值。

7.3.3 使用数组区间的函数

指定元素区间(range),这可以通过传递两个指针来完成:一个指针标识数组的开头,另一个指针标识数组的尾部。

#include <iostream>
const int ArSize = 8;
int sum_arr(const int * begin, const int * end);
int main()
{
    
    
    using namespace std;
    int cookies[ArSize] = {
    
    1,2,4,8,16,32,64,128};
    // some systems require preceding int with static to
    // enable array initialization
    int sum = sum_arr(cookies, cookies + ArSize);
    cout << "Total cookies eaten: " << sum << endl;
    sum = sum_arr(cookies, cookies + 3);        // first 3 elements
    cout << "First three eaters ate " << sum << " cookies.\n";
    sum = sum_arr(cookies + 4, cookies + 8);    // last 4 elements
    cout << "Last four eaters ate " << sum << " cookies.\n";
    return 0;
}
// return the sum of an integer array
int sum_arr(const int * begin, const int * end) 
{
    
    
    const int * pt;
    int total = 0;
    for (pt = begin; pt != end; pt++)
        total = total + *pt;
    return total;
}

7.3.4 使用const指针参数

第一种方法是让const指针指向一个const对象,这样可以防止使用该指针来修改所指向的值,
第二种方法是将指针本身声明为const,这样可以防止改变指针指向的位置。

// 1、能将常规变量的地址赋给指向const的指针
int age = 39;
const int * pt = &age;	// const在前,不能使用pt指针来修改age的值,但是可以将一个新地址赋给pt
age = 20;				// 可以直接通过age变量来修改age的值
int sage = 80;
pt = &sage;				// const在前,不能使用pt指针来修改指向的值,但是可以将一个新地址赋给pt

int sloth = 3;
const int * ps = &sloth; 		// a pointer to const int,不允许使用ps来修改sloth的值,但允许将ps指向另一个位置
int * const finger = &sloth; 	// a const pointer to int,finger只能指向sloth,但允许使用finger来修改sloth的值

// 2、能将const变量的地址赋给指向const的指针
const float g_earth = 9.80;
const float * pe = &g_earth; // VALID

// 3、不能将const的地址赋给常规指针

7.3.4 函数和二维数组

int sum(int ar2[][4], int size);	// ar2是指针而不是数组, 它指向由4个int组成的数组
int sum(int (*ar2)[4], int size);	// int (*ar2)[4]将声明一个指向由4个int组成的数组的指针

// data是一个数组名,该数组有3个元素。第一个元素本身是一个数组,有4个int值组成。
int data[3][4] = {
    
    {
    
    1,2,3,4}, {
    
    9,8,7,6}, {
    
    2,4,6,8}};	// data的类型是指向由4个int组成的数组的指针。

int total = sum(data, 3);			// 函数调用

int sum(int ar2[][4], int size)		// 函数定义
{
    
    
    int total = 0;
    for (int r = 0; r < size; r++)
        for (int c = 0; c < 4; c++)
            total += ar2[r][c];		// 将ar2看作是一个二维数组的名称使用,ar2[r][c] == *(*(ar2 + r) + c)
    return total;
}

ar2[r][c] == *(*(ar2 + r) + c) 	// same thing
ar2 							// pointer to first row of an array of 4 int
ar2 + r 						// pointer to row r (an array of 4 int)
*(ar2 + r) 						// row r (an array of 4 int, hence the name of an array,
                                // thus a pointer to the first int in the row, i.e., ar2[r]
*(ar2 +r) + c 					// pointer int number c in row r, i.e., ar2[r] + c
*(*(ar2 + r) + c	 			// value of int number c in row r, i.e. ar2[r][c]

7.4 函数和C-字符串

将字符串作为参数来传递,但实际传递的是字符串第一个字符的地址。
这意味着字符串函数原型应将其表示字符串的形参声明为 char *类型。
可以使用const 来禁止对字符串参数进行修改。

7.4.1 将C-风格字符串作为参数的函数

字符串作为参数传递给函数,则表示字符串的方式有三种:

  1. char数组;
  2. 用引号括起的字符串常量(也称字符串字面值);
  3. 被设置为字符串的地址的char指针。
// 使用一个函数来计算特定的字符在字符串中出现的次数。

#include <iostream>
using namespace std;

unsigned int c_in_str(const char * str, char ch);

int main(void)
{
    
    
    char mmm[15] = "minimum";
    char *wail = "ululate";
    unsigned int ms = c_in_str(mmm, 'm');
    unsigned int us = c_in_str(wail, 'u');

    cout << ms << " m characters in " << mmm << endl;
    cout << us << " u characters in " << wail << endl;

    return 0;
}

unsigned int c_in_str(const char * str, char ch)
{
    
    
    unsigned int count = 0;

    while (*str)    // quit when *str is '\0'
    {
    
    
        if (*str == ch)
            count++;
        str++;      // move pointer to next char
    }
    return count;
}

7.4.2 返回C-风格字符串的函数

返回字符串的地址

// 定义了一个名为buildstr( )的函数,该函数返回一个指针。
// 该函数接受两个参数:一个字符和一个数字。
// 函数使用new创建一个长度与数字参数相等的字符串,然后将每个元素都初始化为该字符。
// 最后,返回指向新字符串的指针。

#include <iostream>
char * buildstr(char c, int n); // prototype
int main()
{
    
    
    using namespace std;
    int times;
    char ch;
    cout << "Enter a character: ";
    cin >> ch;
    cout << "Enter an integer: ";
    cin >> times;
    char *ps = buildstr(ch, times);
    cout << ps << endl;
    delete [] ps;               // free memory
    ps = buildstr('+', 20);     // reuse pointer
    cout << ps << "-DONE-" << ps << endl;
    delete [] ps;               // free memory
    return 0;
}
// builds string made of n c characters
char * buildstr(char c, int n)
{
    
    
    char * pstr = new char[n + 1];	// 包含n个字符的字符串,需要能够存储n + 1个字符的空间
    pstr[n] = '\0';             // terminate string
    while (n-- > 0)				// 在最后一轮循环开始时,n的值为1。先比较再--
        pstr[n] = c;            // fill rest of string, 最后一个pstr[0]
        // 变量pstr的作用域为buildstr函数内,因此该函数结束时,pstr(而不是字符串)使用的内存将被释放。
    return pstr;	// 返回字符串的地址
}

out:

Enter a character:V
Enter an integer:46
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
++++++++++++++++++++-DONE-++++++++++++++++++++

7.5 函数和结构

结构名只是结构的名称,要获得结构的地址,必须使用地址运算符&
结构作为参数传递给函数,则表示结构的方式有三种:

  1. 按值传递结构。(当结构比较小时)
  2. 传递结构的地址,然后使用指针来访问结构的内容。
  3. 按引用传递。

7.5.1 按值传递结构和返回结构

例1:开发用于表示时间值的结构,然后再开发一个函数,它接受两个这样的结构为参数,并返回表示参数的和的结构。

// 开发用于表示时间值的结构,然后再开发一个函数,它接受两个这样的结构为参数,并返回表示参数的和的结构。 
// 要将两个时间相加,应首先将分钟成员相加。然后通过整数除法 (除数为60)得到小时值,通过求模运算符(%)得到剩余的分钟数。

// 1、定义结构
struct travel_time
{
    
    
    int hours;
    int mins;
};

// 2、返回两个这种结构的总和的sum( )函数的原型
travel_time sum(travel_time t1, travel_time t2);	// 返回值的类型应为travel_time,两个参数也应为这种类型。
// 开发用于表示时间值的结构,然后再开发一个函数,它接受两个这样的结构为参数,并返回表示参数的和的结构。
// 要将两个时间相加,应首先将分钟成员相加。然后通过整数除法 (除数为60)得到小时值,通过求模运算符(%)得到剩余的分钟数。

#include <iostream>
using namespace std;
struct travel_time
{
    
    
    int hours;
    int mins;
};
const int Mins_per_hr = 60;

travel_time sum(travel_time t1, travel_time t2);
void show_time(travel_time t);

int main(void)
{
    
    
    travel_time day1 = {
    
    5, 45};
    travel_time day2 = {
    
    4, 55};
    travel_time trip = sum(day1, day2);
    cout << "Two-day total: ";
    show_time(trip);
    return 0;
}

travel_time sum(travel_time t1, travel_time t2)
{
    
    
    travel_time total;
    total.mins = (t1.mins + t2.mins) % Mins_per_hr;
    total.hours = t1.hours + t2.hours + (t1.mins + t2.mins) / Mins_per_hr;
    return total;
}

void show_time(travel_time t)
{
    
    
    cout << t.hours << " hours, " << t.mins << " minutes\n";
}

例2:定义两个结构,用于表示两种不同的描述位置的方法,然后开发一个函数,将一种格式转换为另一种格式,并显示结果。

// 定义两个结构,用于表示两种不同的描述位置的方法,然后开发一个函数,将一种格式转换为另一种格式,并显示结果。
// 1、建立直角坐标系的结构
struct rect
{
    
    
    double x; // horizontal distance from origin
    double y; // vertical distance from origin
};

// 2、建立极坐标系得结构
struct polar
{
    
    
    double distance; // distance from origin
    double angle; // direction from origin
};

// 3、显示极坐标的函数
// 将弧度值转换为角度值。这意味着需要将弧度值乘以180/——约为57.29577951
// show polar coordinates, converting angle to degrees
void show_polar (polar dapos)	// 将一个polar结构传递给该函数时,该结构的内容将被复制到dapos结构中
{
    
    
    using namespace std;
    const double Rad_to_deg = 57.29577951;
    cout << "distance = " << dapos.distance;
    cout << ", angle = " << dapos.angle * Rad_to_deg;
    cout << " degrees\n";
}

// 4、编写一个将直角坐标转换为极坐标的函数。
#include <cmath>
polar rect_to_polar(rect xypos) // 该函数接受一个rect参数,并返回一个polar结构
{
    
    
    polar answer;
    answer.distance = sqrt( xypos.x * xypos.x + xypos.y * xypos.y);	// 根据毕达哥拉斯定理,使用水平和垂直坐标来计算距离
    answer.angle = atan2(xypos.y, xypos.x);	// atan2( )函数可根据x和y的值计算角度
    return answer; // returns a polar structure
}
// strctfun.cpp -- functions with a structure argument
#include <iostream>
#include <cmath>
// structure declarations
struct polar
{
    
    
    double distance; // distance from origin
    double angle; // direction from origin
};
struct rect
{
    
    
    double x; // horizontal distance from origin
    double y; // vertical distance from origin
};
// prototypes
polar rect_to_polar(rect xypos);
void show_polar(polar dapos);
int main()
{
    
    
    using namespace std;
    rect rplace;
    polar pplace;
    cout << "Enter the x and y values: ";
    while (cin >> rplace.x >> rplace.y) // slick use of cin
    {
    
    
        pplace = rect_to_polar(rplace);
        show_polar(pplace);
        cout << "Next two numbers (q to quit): ";
    }
    // 如果程序在输入循环后还需要进行输入,则必须使用 cin.clear( )重置输入,然后还可能需要通过读取不合法的输入来丢弃它们。
    cout << "Done.\n";
    return 0;
}
// convert rectangular to polar coordinates
polar rect_to_polar(rect xypos)
{
    
    
    using namespace std;
    polar answer;
    answer.distance = sqrt( xypos.x * xypos.x + xypos.y * xypos.y);
    answer.angle = atan2(xypos.y, xypos.x);
    return answer; // returns a polar structure
}
// show polar coordinates, converting angle to degrees
void show_polar (polar dapos)
{
    
    
    using namespace std;
    const double Rad_to_deg = 57.29577951;
    cout << "distance = " << dapos.distance;
    cout << ", angle = " << dapos.angle * Rad_to_deg;
    cout << " degrees\n";
}

7.5.2 传递结构的地址

重新编写show_polar( )函数,使用指向结构的指针。需要修改三个地方:

  1. 调用函数时,将结构的地址(&pplace)而不是结构本身(pplace)传递给它;
  2. 将形参声明为指向polar的指针,即polar *类型。由于函数不应该修结构,因此使用了const修饰符;
  3. 由于形参是指针而不是结构,因此应间接成员运算符(->),而不是成员运算符(句点)。
struct polar	// 结构定义
{
    
    
    double distance; 	
    double angle; 		
};

polar pplace;	// 结构声明

show_polar(&pplace);	// 函数调用

void show_polar(const polar * pda)	// 函数定义
{
    
    
    using namespace std;
    const double Rad_to_deg = 57.29577951;
    cout << "distance = " << pda->distance;
    cout << ", angle = " << pda->angle * Rad_to_deg;
    cout << " degrees\n";
}
// strctptr.cpp -- functions with pointer to structure arguments
#include <iostream>
#include <cmath>
// structure templates
struct polar
{
    
    
    double distance; // distance from origin
    double angle; // direction from origin
};
struct rect
{
    
    
    double x; // horizontal distance from origin
    double y; // vertical distance from origin
};
// prototypes
void rect_to_polar(const rect * pxy, polar * pda);
void show_polar (const polar * pda);
int main()
{
    
    
    using namespace std;
    rect rplace;
    polar pplace;
    cout << "Enter the x and y values: ";
    while (cin >> rplace.x >> rplace.y)
    {
    
    
        rect_to_polar(&rplace, &pplace); // pass addresses
        show_polar(&pplace); // pass address
        cout << "Next two numbers (q to quit): ";
    }
    cout << "Done.\n";
    return 0;
}
// show polar coordinates, converting angle to degrees
void show_polar (const polar * pda)
{
    
    
    using namespace std;
    const double Rad_to_deg = 57.29577951;
    cout << "distance = " << pda->distance;
    cout << ", angle = " << pda->angle * Rad_to_deg;
    cout << " degrees\n";
}
// convert rectangular to polar coordinates
// 函数不返回一个新的结构,而是修改调用函数中已有的结构。
// 因此,虽然第一个参数是const指针,但第二个参数却不是。
void rect_to_polar(const rect * pxy, polar * pda)
{
    
    
    using namespace std;
    pda->distance =
            sqrt(pxy->x * pxy->x + pxy->y * pxy->y);
    pda->angle = atan2(pxy->y, pxy->x);
}

7.6 函数和string对象

例:声明一个string对象数组,并将该数组传递给一个函数以显示其内容:

// 声明一个string对象数组,并将该数组传递给一个函数以显示其内容:
#include <iostream>
#include <string>
using namespace std;
const int SIZE = 5;
void display(const string sa[], int n);
int main()
{
    
    
    string list[SIZE]; 	// 数组list的每个元素都是一个string对象
    cout << "Enter your " << SIZE << " favorite astronomical sights:\n";
    for (int i = 0; i < SIZE; i++)
    {
    
    
        cout << i + 1 << ": ";
        getline(cin,list[i]);
    }
    cout << "Your list:\n";
    display(list, SIZE);
    return 0;
}
void display(const string sa[], int n)	// 形参sa是一个指向string对象的指针
{
    
    
    for (int i = 0; i < n; i++)
        cout << i + 1 << ": " << sa[i] << endl;
}

7.7 函数和array对象

// 1、声明一个array对象
const int Seasons = 4;
const std::array<std::string, Seasons> Snames = {
    
    "Spring", "Summer", "Fall", "Winter"};

// 2、声明一个用array对象做参数的函数
void show(std::array<double, 4> da); 	// da an object
void fill(std::array<double, 4> * pa); 	// pa a pointer to an object
//arrobj.cpp -- functions with array objects (C++11)
#include <iostream>
#include <array>
#include <string>
// constant data
const int Seasons = 4;
const std::array<std::string, Seasons> Snames = {
    
    "Spring", "Summer", "Fall", "Winter"};
// function to modify array object
void fill(std::array<double, Seasons> * pa);
// function that uses array object without modifying it
void show(std::array<double, Seasons> da);
int main()
{
    
    
    std::array<double, Seasons> expenses;
    fill(&expenses);
    show(expenses);
    return 0;
}
void fill(std::array<double, Seasons> * pa)
{
    
    
    using namespace std;
    for (int i = 0; i < Seasons; i++)
    {
    
    
        cout << "Enter " << Snames[i] << " expenses: ";
        cin >> (*pa)[i];
    } 
}
void show(std::array<double, Seasons> da)
{
    
    
    using namespace std;
    double total = 0.0;
    cout << "\nEXPENSES\n";
    for (int i = 0; i < Seasons; i++)
    {
    
    
        cout << Snames[i] << ": $" << da[i] << endl;
        total += da[i];
    }
    cout << "Total Expenses: $" << total << endl;
}

out:

Enter Spring expenses:20
Enter Summer expenses:12
Enter Fall expenses:11.1
Enter Winter expenses:23

EXPENSES
Spring: $20
Summer: $12
Fall: $11.1
Winter: $23
Total Expenses: $66.1

7.8 调用自身的函数(递归)

C++不允许main( )调用自己。

7.8.1 包含一个递归调用的递归

// 如果recurs( )进行了5次递归调用,则第一个statements1部分将按函数调用的顺序执行5次,
// 然后statements2部分将以与函数调用相反的顺序执行5次。
// 进入5层递归后,程序将沿进入的路径返回。
void recurs(argumentlist) 
{
    
    
    statements1
    if (test)	// test最终将为false,调用链将断开。
        recurs(arguments);
    statements2
}
// 注意,每个递归调用都创建自己的一套变量,
// 因此当程序到达第5次调用时,将有5个独立的n变量,其中每个变量的值都不同。
// 在Counting down阶段和Kaboom阶段的相同层级,n的地址相同.

#include <iostream>
void countdown(int n);
int main()
{
    
    
    countdown(4); // call the recursive function
    return 0;
}
void countdown(int n)
{
    
    
    using namespace std;
    cout << "Counting down ... " << n << " (n at " << &n << ")" << endl;
    if (n > 0)
        countdown(n-1); // function calls itself
    cout << n << ": Kaboom!" << " (n at " << &n << ")" << endl;
}

out:

Counting down ... 4 (n at 0xc848bffc60)
Counting down ... 3 (n at 0xc848bffc30)
Counting down ... 2 (n at 0xc848bffc00)
Counting down ... 1 (n at 0xc848bffbd0)
Counting down ... 0 (n at 0xc848bffba0)
0: Kaboom! (n at 0xc848bffba0)
1: Kaboom! (n at 0xc848bffbd0)
2: Kaboom! (n at 0xc848bffc00)
3: Kaboom! (n at 0xc848bffc30)
4: Kaboom! (n at 0xc848bffc60)

7.8.2 包含多个递归调用的递归

在需要将一项工作不断分为两项较小的、类似的工作时,递归非常有用。

// 递归函数subdivide( )使用一个字符串,该字符串除两端为 | 字符外,其他全部为空格。
// main函数使用循环调用 subdivide( )函数6次,每次将递归层编号加1,并打印得到的字符串。
// 这样,每行输出表示一层递归。
#include <iostream>
const int Len = 66;
const int Divs = 6;
void subdivide(char ar[], int low, int high, int level);
int main()
{
    
    
    char ruler[Len];
    int i;
    for (i = 1; i < Len - 2; i++)
        ruler[i] = ' ';
    ruler[Len - 1] = '\0';
    int max = Len - 2;
    int min = 0;
    ruler[min] = ruler[max] = '|';
    std::cout << ruler << std::endl;
    for (i = 1; i <= Divs; i++)
    {
    
    
        subdivide(ruler,min,max, i);
        std::cout << ruler << std::endl;
        for (int j = 1; j < Len - 2; j++)
            ruler[j] = ' '; // reset to blank ruler
    }
    return 0;
}
void subdivide(char ar[], int low, int high, int level)
{
    
    
    if (level == 0) // 使用变量level来控制递归层,当level为0时,该函数将不再调用自己。
        return;
    int mid = (high + low) / 2;
    ar[mid] = '|';
    subdivide(ar, low, mid, level - 1);
    subdivide(ar, mid, high, level - 1);    // 函数调用自身时,将把level减1
}

out:

|                                                               |
|                               |                               |
|               |               |               |               |
|       |       |       |       |       |       |       |       |
|   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||

7.9 函数和指针

7.9.1 函数指针

函数的地址是存储其机器语言代码的内存的开始地址。

  1. 获取函数的地址; 使用函数名
  2. 声明一个函数指针;
  3. 使用函数指针来调用函数。
double pam(int);	// 1、函数名pam是这个函数的地址

double (*pf)(int);	// 2、声明一个函数指针pf,(*pf)扮演的角色与函数名相同
pf = pam;			// 2、正确地声明pf后,便可以将相应函数的地址赋给它
                    // pam( )的特征标和返回类型必须与pf相同

double x = pam(4); 		// 3、使用函数名来调用函数
double y = (*pf)(5); 	// 3、使用函数指针来调用函数方法1
double y = pf(5); 		// 3、使用函数指针来调用函数方法2

void estimate(int lines, double (*pf)(int));	// 4、声明一个使用函数指针做参数的函数
estimate(50, pam);		// 5、函数调用时传递函数pam的地址作为参数

为何pf和(*pf)等价呢?
一种学派认为,由于pf是函数指针,而 *pf 是函数,因此应将(*pf)( )用作函数调用。另一种学派认为,由于函数名是指向该函数的指针,指向函数的指针的行为应与函数名相似,因此应将pf( )用作函数调用使用。

// 两次调用estimate( )函数,一次传递betsy( )函数的地址,另一次则传递pam( )函数的地址。
// 在第一种情况下,estimate( )使用betsy( )计算所需的小时数;
// 在第二种情况下,estimate( )使用pam( )进行计算。

#include <iostream>
double betsy(int);
double pam(int);
// second argument is pointer to a type double function that
// takes a type int argument
void estimate(int lines, double (*pf)(int));
int main()
{
    
    
    using namespace std;
    int code;
    cout << "How many lines of code do you need? ";
    cin >> code;
    cout << "Here's Betsy's estimate:\n";
    estimate(code, betsy);
    cout << "Here's Pam's estimate:\n";
    estimate(code, pam);
    return 0;
}
double betsy(int lns)
{
    
    
    return 0.05 * lns;
}
double pam(int lns)
{
    
    
    return 0.03 * lns + 0.0004 * lns * lns;
}
void estimate(int lines, double (*pf)(int))
{
    
    
    using namespace std;
    cout << lines << " lines will take ";
    cout << (*pf)(lines) << " hour(s)\n";
}

out:

How many lines of code do you need?100
Here's Betsy's estimate:
100 lines will take 5 hour(s)
Here's Pam's estimate:
100 lines will take 7 hour(s)

7.9.2 函数指针、指针函数、函数指针数组

double (*pf)(int); 	// (*pf)(int)意味着pf是一个指向函数的指针
double *pf(int); 	// *pf(int)意味着pf( )是一个返回指针的函数
// 1、指针函数原型
// 下面这些指针函数原型表示方法本质上都是一样的:
// *ar 与 ar[] 含义相同,且在函数原型中,可以省略标识符
const double * f1(const double ar[], int n);
const double * f2(const double [], int);
const double * f3(const double *, int);
const double * f1(const double ar[], int n){
    
    } // 定义时不能省略标识符

// 2、声明一个指针,指向这个指针函数
const double * (*p1)(const double *, int) = f1;	// 并初始化
auto p2 = f2;	// 使用C++11的自动类型推断功能

// (*p1) (av, 3)和p2(av, 3)都调用指向的函数(这里为f1()和f2()),并将av和3作为参数。
// 因此,显示的是这两个函数的返回值。返回值的类型为const double *(即double值的地址), 
// 因此在每条cout语句中,前半部分显示的都是一个double值的地址。后半部分是值。
cout << (*p1)(av,3) << ": " << *(*p1)(av,3) << endl;
cout << p2(av,3) << ": " << *p2(av,3) << endl;

// 3、用一个函数指针数组使用这三个指针函数
// 运算符[]的优先级高于*,因此*pa[3]表明pa是一个包含三个指针的数组。
// pa中每个指针都指向(将const double *和int作为参数,并返回一个const double *)的函数。
const double * (*pa[3])(const double *, int) = {
    
    f1,f2,f3};
auto pb = pa;	// 数组名是指向第一个元素的指针,因此pa和pb都是指向函数指针的指针。

// pa[i]和pb[i]都表示数组中的指针,因此可将任何一种函数调用表示法用于它们
const double * px = pa[0](av,3);
const double * py = (*pb[1])(av,3);
// 要获得指向的double值,可使用运算符*
double x = *pa[0](av,3);
double y = *(*pb[1])(av,3);

// 4、创建指向整个数组的指针
// 由于数组名pa是指向函数指针的指针,因此指向数组的指针将是这样的指针,即它指向指针的指针。
(*pd)[3]	// pd是一个指针,它指向一个包含三个元素的数组
const double *(*(*pd)[3])(const double *, int) = &pa;
// 既然pd指向数组,那么*pd就是数组,而(*pd)[i]是数组中的元素,即函数指针
// 简单函数调用:使用(*pd)i来调用函数,而*(*pd)i是返回的指针指向的值。
// 使用指针调用函数的语法:使用(*(*pd)[i])(av,3)来调用函数,而*(*(*pd)[i]) (av,3)是指向的double值。

以上四种玩法pa和&pa之间的差别:

  1. pa是数组第一个元素的地址,即&pa[0],是单个指针的地址。
  2. &pa是整个数组(即三个指针块)的地址。
  3. 从数字上说,pa和&pa的值相同,但它们的类型不同:
  4. 差别1:pa+1为数组中下一个元素的地址,而&pa+1为数组pa后面一个12字节内存块的地址(这里假定地址为4字节)。
  5. 差别2:要得到第一个元素的值,只需对pa解除一次引用,但需要对&pa解除两次引用。**&pa == *pa == pa[0]

7.9.3 使用typedef进行简化

  1. 预处理器创建类型别名:#define A B 用 A 替代 B
  2. 关键字 typedef 创建类型别名:typedef A B; 用 B 替代 A
// 将别名当做标识符进行声明,并在开头使用关键字typedef
typedef const double *(*p_fun)(const double *, int); 	// p_fun now a type name
p_fun p1 = f1; 	// p1 points to the f1() function

// 然后使用这个别名来简化代码
p_fun pa[3] = {
    
    f1,f2,f3}; 	// pa an array of 3 function pointers
p_fun (*pd)[3] = &pa; 		// pd points to an array of 3 function pointers

7.10 总结

  1. 函数是C++的编程模块。要使用函数,必须提供定义和原型,并调用该函数。函数定义是实现函数功能的代码;函数原型描述了函数的接口:传递给函数的值的数目和种类以及函数的返回类型。函数调用使得程序将参数传递给函数,并执行函数的代码。
  2. 在默认情况下,C++函数按值传递参数。这意味着函数定义中的形参是新的变量,它们被初始化为函数调用所提供的值。因此,C++函数通过使用拷贝,保护了原始数据的完整性。
  3. C++将数组名参数视为数组第一个元素的地址。从技术上讲,这仍然是按值传递的,因为指针是原始地址的拷贝,但函数将使用指针来访问原始数组的内容。当且仅当声明函数的形参时,下面两个声明才是等价的:typeName arr[];typeName * arr;。这两个声明都表明,arr是指向typeName的指针,但在编写函数代码时,可以像使用数组名那样使用arr来访问元素:arr[i]。即使在传递指针时,也可以将形参声明为const指针,来保护原始数据的完整性。由于传递数据的地址时,并不会传输有关数组长度的信息,因此通常将数组长度作为独立的参数来传递。另外,也可传递两个指针(其中一个指向数组开头,另一个指向数组末尾的下一个元素),以指定一个范围,就像STL使用的算法一样。
  4. C++提供了3种表示C-风格字符串的方法:字符数组、字符串常量和字符串指针。它们的类型都是char*(char指针),因此被作为char*类型参数传递给函数。C++使用空值字符(\0)来结束字符串,因此字符串函数检测空值字符来确定字符串的结尾。
  5. C++还提供了string类,用于表示字符串。函数可以接受string对象作为参数以及将string对象作为返回值。string类的方法size( )可用于判断其存储的字符串的长度。
  6. C++处理结构的方式与基本类型完全相同,这意味着可以按值传递结构,并将其用作函数返回类型。然而,如果结构非常大,则传递结构指针的效率将更高,同时函数能够使用原始数据。这些考虑因素也适用于类对象。
  7. C++函数可以是递归的,也就是说,函数代码中可以包括对函数本身的调用。
  8. C++函数名与函数地址的作用相同。通过将函数指针作为参数,可以传递要调用的函数的名称。

7.11 复习题

// 2
double doctor(const char *str); // e doctor( )接受一个字符串参数(不能修改该字符串),并返回一个double值。
char * plot(map * pa);          // g plot( )将map结构的指针作为参数,并返回一个字符串。

// 3 编写一个接受3个参数的函数:int数组名、数组长度和一个int值,并将数组的所有元素都设置为该int值。
void function3(int arr[], int size, int value)
{
    
    
    for (int i=0; i < size; i++)
        arr[i] = value;
}

// 4 编写一个接受3个参数的函数:指向数组区间中第一个元素的指针、指向数组区间最后一个元素后面的指针以及一个int值,
// 并将数组中的每个元素都设置为该int值。
void function4(int *begin, int *end, int value)
{
    
    
    for (int *pt = begin, pt != end; pt++)
        *pt = value;
}
int arr[10];
function4(arr, arr+3, 100);	// 函数调用

// 5 编写将double数组名和数组长度作为参数,并返回该数组中最大值的函数。该函数不应修改数组的内容。
double function5(const double arry[], int size)
{
    
    
    double temp = arry[0];
    for (int i=1; i < size; i++)
        if (temp < arry[i])
            temp = arry[i];
    return temp;
}

// 6 为什么不对类型为基本类型的函数参数使用const限定符?
// 因为传递的是实参的副本,不改变实参本身的值。

// 7 C++程序可使用哪3种C-风格字符串格式?
char site[7] = {
    
    'R', 'U', 'N', 'O', 'O', 'B', '\0'};
char str[] = "hello world"; // 字符数组
char *pt = "hello world";   // 字符指针

// 8 编写一个函数将字符串中所有的c1都替换为c2,并返回替换次数。
int replace(char * str, char c1, char c2)
{
    
    
    int count = 0;
    while (*str)
    {
    
    
        if ((*str) == c1)
        {
    
    
            *str = c2;
            count++;
        }
        str++;
    }
    return count;
}

// 11 函数judge( )的返回类型为int,它将这样一个函数的地址作为参数:将const char指针作为参数,并返回一个int值。
int judge(int (*pf)(const char *)); // judge()参数是一个函数指针

// 12
struct applicant {
    
    
    char name[30];
    int credit_ratings[3];
};
// a.编写一个函数,它将application结构作为参数,并显示该结构的内容。
void function12a(application a)
{
    
    
    cout << a.name << endl;
    for (int i=0; i<3; i++)
        cout << a.credit_ratings[i] << endl;
}
// b.编写一个函数,它将application结构的地址作为参数,并显示该参数指向的结构的内容。
applicant pplace;	    // 结构声明
show_polar(&pplace);    // 函数调用
void  function12b(const applicant * pa)	// 函数定义
{
    
    
    cout << pa->name << endl;
    for (int i=0; i<3; i++)
        cout << pa->credit_ratings[i] << endl;
}

// 13 函数f1()和f2()的原型如下:
void f1(applicant * a);
const char * f2(const applicant * a1, const applicant * a2);

void (*p_f1)(applicant * a);            // *p_f1 现在是函数指针
typedef void (*p_f1)(applicant * a);    // *p_f1 现在是函数指针类型,和f1函数的类型一样
p_f1 p1=f1;                             // 将p1声明为指向f1的指针
p_f1 ap[5];                             // 将ap声明为一个数组,它包含5个类型与p1相同的指针;

const char * (*p_f2)(const applicant * a1, const applicant * a2);
typedef const char * (*p_f2)(const applicant * a1, const applicant * a2);
p_f2 p2=f2;
p_f2 (*pa)[10];     // 将pa声明为一个指针,它指向的数组包含10个类型与p2相同的指针。

9、表达式*"pizza"的含义是什么?“taco” [2]呢?

字符常量的行为与数组名相同
"pizza"为第一个元素的地址,因此使用*运算符将得到第一个元素的值,即字符p
"taco"为第一个元素的地址,“taco”[2]为第二个元素的值,即字符c。

10、C++允许按值传递结构,也允许传递结构的地址。如果glitz是一个结构变量,如何按值传递它?如何传递它的地址?这两种方法有何利弊?

  1. 要按值传递它,只要传递结构名glitz即可。要传递它的地址,请使用地址运算符&glitz。
  2. 按值传递将自动保护原始数据,但这是以时间和内存为代价的。按地址传递可节省时间和内存,但不能保护原始数据,除非对函数参数使用了const限定符。
  3. 另外,按值传递意味着可以使 用常规的结构成员表示法,但传递指针则必须使用间接成员运算符。

7.12 编程练习

// 5. 定义一个递归函数,接受一个整数参数,并返回该参数的阶乘。
// 前面讲过,3的阶乘写作3!,等于3*2!,依此类推;而0!被定义为 1。通用的计算公式是,如果n大于零,则n!=n*(n−1)!。
// 在程序中对该函数进行测试,程序使用循环让用户输入不同的值,程序将报告这些值的阶乘。

#include <iostream>
using namespace std;
long long recurs(int n);
int main(void)
{
    
    
    cout << "Please enter a number for factorial:";
    int num;
    long long result;
    while (cin>>num)
    {
    
    
        result = recurs(num);
        cout << num << "! = " << result << endl;
        cout << "Please enter a number against:";
    }
    return 0;
}

long long recurs(int n)
{
    
    
    if (n == 0)
        return 1;
    else
        return n * recurs(n-1);
}

// 7. 修改程序清单7.7中的3个数组处理函数,使之使用两个指针参数来表示区间。
// fill_array( )函数不返回实际读取了多少个数字,而是返回一个指针,该指针指向最后被填充的位置;
// 其他的函数可以将该指针作为第二个参数,以标识数据结尾。

#include <iostream>
const int Max = 5;
// function prototypes
double *fill_array(double *begin, double *end);
void show_array(double *begin, double *end);
void revalue(double r, double *begin, double *end);
int main()
{
    
    
    using namespace std;
    double properties[Max];
    double *pa = fill_array(properties, properties+Max);
    show_array(properties, pa);
    if (pa - properties > 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, pa);
        show_array(properties, pa);
    }
    cout << "Done.\n";
    cin.get();
    return 0;
}
double *fill_array(double *begin, double *end)
{
    
    
    using namespace std;
    double temp;
    double *pt;
    for (pt = begin; pt != end; pt++)
    {
    
    
        cout << "Enter value #" << (pt - begin) + 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;
        else
            *pt = temp;
    }
    return pt;
}
// the following function can use, but not alter,
// the array whose address is ar
void show_array(double *begin, double *end)
{
    
    
    using namespace std;
    for (double *pt = begin; pt != end; pt++)
    {
    
    
        cout << "Property #" << (pt - begin) + 1 << ": $";
        cout << *pt << endl;
    }
}
// multiplies each element of ar[] by r
void revalue(double r, double *begin, double *end)
{
    
    
    for (double *pt = begin; pt != end; pt++)
        *pt *= r;
}

猜你喜欢

转载自blog.csdn.net/qq_39751352/article/details/126671080