【读书笔记:C++ primer plus 第六版 中文版】第7章 函数---C++的编程模块

转载请注明出处:http://blog.csdn.net/enyusmile/article/details/46604119

本章内容包括:

  • 函数基本知识
  • 函数原型
  • 按值传递函数参数
  • 设计处理数组的函数
  • 使用const指针参数
  • 设计处理文本字符串的函数
  • 设计处理结构的函数
  • 设计处理string对象的函数
  • ​调用自身的函数(递归)
  • 指向函数的指针

7.1 复习函数的基本知识

  • 要使用C++函数,必须完成如下工作:
    • 提供函数定义
    • 提供函数原型
    • 调用函数
  • 库函数是已经定义和编译号的函数,同时可以使用标准库头文件提供其原型,因此只需正确地调用这种函数即可.
  • 程序清单7.1 calling.cpp

7.1.1 定义函数

  • C++对于返回值的类型有一定的限制:不能是数组,但可以是其他任何类型—整数,浮点数,指针,甚至可以是结构和对象(有趣的是,虽然C++函数不能直接返回数组,但可以将数组作为结构或对象组成部分来返回).
  • 通常,函数通过将返回值复制到制定的CPU寄存器或内存单元中来将其返回.随后,调用程序将查看该内存单元.返回函数和调用函数必须就该内存单元中存储的数据的类型达成一致.
  • 函数原型将返回值类型告知调用程序,而函数定义命令被调用函数应返回什么类型的数据.

7.1.2 函数原型和函数调用

  • 程序清单7.2 protos.cpp
  • 程序说明
    1. 为什么需要原型
      • 原型描述了函数到编译器的接口,也就是说,它将函数返回值的类型(如果有的话)以及参数的类型和数量告诉编译器.
      • C++的编程风格是将main()方在最前面,因为它通常提供了程序的整体结构.
    2. 原型的语法
      • 函数原型是一条语句,因此必须以分号结束.
      • 函数原型不要求提供变量名,有类型列表就足够了,但在原型的参数列表中,可以包括变量名,也可以不包括.原型中的变量名相当于占位符,因此不必与函数定义中的变量名相同.
      • C++原型与ANSI原型:ANSI C借鉴了C++中的原型,但这两种语言还是有区别的.其中最重要的区别是,为与基本C兼容,ANSI C中的原型是可选的,但在C++中,原型是必不可少的.
    3. 原型的功能
      • 具体来说,原型确保以下几点:
        1. 编译器正确处理函数返回值
        2. 编译器检查使用的参数数目是否正确
        3. 编译器检查使用的参数类型是否正确.如果不正确,则转换为正确的类型(如果可能的话).
      • 在编译阶段进行的原型化被称为静态类型检查.可以看出,静态类型检查可捕获许多在运行阶段非常难以捕获的错误.

7.2 函数参数和按值传递

  • C++通常按值传递参数,这意味着将数值参数传递给函数,而后者将其赋给一个新的变量.
  • 出于简化的目的,C++标准使用参数来表示实参,使用参量来表示形参.

7.2.1 多个参数

  • 程序清单7.3 twoarg.cpp

7.2.2 另外一个接受两个参数的函数

  • 注意:有些C++实现不支持long double类型,如果所用的C++实现是这样的,请使用double类型.
  • 程序清单7.4 lotto.cpp

7.3 函数和数组

  • 程序清单7.5 arrfun1.cpp

7.3.1 函数如何使用指针来处理数组

  • 当指针指向数组的第一个元素时,本书使用数组表示法;而当指针指向一个独立的值时,使用指针表示法.例如:在C++中,当(且仅当)用于函数头或函数原型中,int *arr和int arr[]的含义才是相同的.
  • 记住,将指针(包括数组名)加1,实际上是加上了一个与指针指向的类型的长度(以字节为单位)相等的值.对于遍历数组而言,使用指针假发和数组下标是等效的.

7.3.2 将数组作为参数意味着什么

  • 传递常规变量时,函数将使用该变量的拷贝;但传递数组时,函数将使用原来的数组.实际上,这种却别并不违反C++按值传递的方法,sum_arr()函数仍传递了一个值,这个值被赋给一个新变量,但这个值是一个地址,而不是数组的内容.
  • 程序清单7.6 arrfun2.cpp
  • 注意:为将数组类别和元素数量高速数组处理函数,请通过两个不同的参数来传递它们.

7.3.3 更多数组函数示例

  1. 填充数组
  2. 显示数组及用const保护数组
    • 为防止函数无意中修改数组的内容,可在声明形参时使用关键字const
  3. 修改数组
  4. 将上述代码组合起来
    • 程序清单7.7 arrfun3.cpp
  5. 程序说明
    • 这种被称为自下而上的程序设计,因为设计过程从组件到整体进行.这种方法非常适合于OOP—它首先强调的是数据表示和曹总.而传统的过程性编程倾向于从上而下的程序设计,首先制定模块化设计方案,然后再研究细节.这两种方法都很有用,最终的产品都是模块化程序.
  6. 数组处理函数的常用编写方式

7.3.4 使用数组区间的函数

  • STL方法使用”超尾”概念来制定区间.也就是说,对于数组而言,标识数组结尾的参数将是指向最后一个元素后面的指针.
  • 程序清单7.8 arrfun4.cpp

7.3.5 指针和const

  • 可以用两种不同的方式将const关键字用于指针.第一种方法是让指针指向一个常量对象,这样可以防止使用该指针来修改所指向的值,第二种方法是将指针本身声明为常量,这样可以防止改变指针指向的位置.
  • 注意:如果数据类型本身并不是指针,则可以将const数据或非const数据的地址赋给指向const的指针,但只能将非const数据的地址赋给非const指针.
  • 尽可能使用const:将指针参数声明为指向常量数据的指针有两条理由
    1. 这样可以避免由于无意间修改数据而导致的编程错误.
    2. 使用const使得函数能够处理const和非const实参,否则将只能接受非const数据.
  • 如果条件允许,则应将指针形参声明为指向const的指针
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的值.中间的声明不允许使用ps来修改sloth的值,但允许将ps指向另一个位置.简而言之,finger和*ps都是const,而*finger和ps不是.

7.4 函数和二维数组
7.5 函数和C风格字符串
7.5.1 将C风格字符串作为参数的函数

  • 假设要将字符串作为参数传递给函数,则表示字符串的方式有三种:
    1. char数组
    2. 用引号括起的字符串常量(也称字符串字面值)
    3. 被设置为字符串的地址的char指针
  • 但上述3种选择的类型都是char指针(准确的说是char*).可以说是将字符串作为参数来传递,但实际传递的是字符串第一个字符的地址.这意味着字符串函数原型应将其表示字符串的形参声明为char*类型.
  • 程序清单7.9 strgfun.cpp

7.5.2 返回C风格字符串的函数

  • 程序清单7.10 strgback.cpp

7.6 函数和结构

  • 可以将一个结构赋给另外一个结构.同样,也可以按值传递结构,就像普通变量那样.在这种情况下,函数将只用原始结构的副本.另外,函数也可以返回结构.
  • 在C语言和C++中,都使用符号&来表示地址运算符.
  • 按值传递结构有一个缺点.如果结构非常大,则赋值结构将增加内存要求,降低系统运行的速度.处于这些原因(同时由于最初C语言不允许按值传递结构),许多C程序员倾向于床底结构的地址,然后使用指针来访问结构的内容.C++提供了第三种选择—按应用传递.

7.6.1 传递和返回结构

  • 程序清单7.11 travel.cpp

7.6.2 另一个处理结构的函数示例

  • 程序清单7.12 atrctfun.cpp
  • 注意:有些编译器仅当被明确指示后,才会搜索数学库.

7.6.3 传递结构的地址

  • 程序清单7.13 strctptr.cpp

7.7 函数和string对象

  • 程序清单7.14 topfive.cpp

7.8 函数与array对象

  • 在C++中,类对象是基于结构的,因此结构编程方面的有些考虑因素也适用于类.
  • 程序清单7.15 arrobj.cpp

7.9 递归

  • C++函数有一种有趣的特点—可以调用自己(然而,与C语言不同的是,C++不允许main()调用自己),这种功能被称为递归.

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

  • 程序清单7.16 recur.cpp

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

  • 在需要将一项工作不断分为两项较小的类似的工作时,递归非常有用.
    递归方法有时被称为分而治之策略.
  • 程序清单7.17 ruler.cpp
// ruler.cpp -- using recursion to subdivide a ruler
#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
    }
    // std::cin.get();
    return 0;
}
void subdivide(char ar[], int low, int high, int level)
{
    if (level == 0)
        return;
    int mid = (high + low) / 2;
    ar[mid] = '|';
    subdivide(ar, low, mid, level - 1);
    subdivide(ar, mid, high, level - 1); 
}
  • 如果要求的递归层次很多,这种递归方式将是一种糟糕的选择;然而,如果递归层次较少,浙江是一种精致而简单的选择.

7.10 函数指针

  • 与数据项相似,函数也有地址.函数的地址是存储其机器语言代码的内存的开始地址.这对用户来说不重要,对程序员却很有用.例如:可以编写将另一个函数的地址作为参数的函数.这样第一个函数将能够找到第二个函数,并运行它.与直接调用另一个函数相比,这种方法很笨拙,但它允许在不同的时间传递不同函数的地址,这意味着可以在不同的时间使用不同的函数.

7.10.1 函数指针的基础知识

  1. 获取函数的地址
    1. 使用函数名即可获取函数的地址.如:如果think()是一个函数,则think就是该函数的地址.
    2. 要将函数作为参数进行传递,必须传递函数名.一定要区分传递的是函数的地址还是函数的返回值.
  2. 声明函数指针
    1. 提示:通常,要声明指向特定类型的函数的指针,可以首先编写这种函数的原型,然后用(*pf)替换函数名.这样pf就是这类函数的指针.
    2. 为提供正确的运算符优先级,必须在声明中使用括号将*pf括起.
      使用函数指针时,比较棘手的是编写原型,而传递地址则非常简单.
  3. 使用指针来调用函数
    1. 历史与逻辑:真实非常棒的语法!为何pf和(*pf)等价呢?一种学派认为,由于pf是函数指针,而*pf是函数,因此应将(*pf)()用作函数调用.另一种学派认为,由于函数名是指向该函数的指针,指向函数的指针的行为应与函数名相似,因此硬件pf()用作函数调用使用.C++进行了折中—这两种方式都是正确的,或者至少是允许的,虽然他们在逻辑上是互相冲突的.在认为折中折中粗糙之前,应该想到,容忍逻辑上无法自圆其说的观点正在人类思维活动的特点.

7.10.2 函数指针示例
程序清单7.18 fun_ptr.cpp

// fun_ptr.cpp -- pointers to functions
#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);
    // cin.get();
    // cin.get();
    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";
}

7.10.3 深入探讨函数指针

  • 程序清单arfupt.cpp
// arfupt.cpp -- an array of function pointers
#include <iostream>
// various notations, same signatures
const double * f1(const double ar[], int n);
const double * f2(const double [], int);
const double * f3(const double *, int);
int main()
{
    using namespace std;
    double av[3] = {1112.3, 1542.6, 2227.9};
    // pointer to a function
    const double *(*p1)(const double *, int) = f1;
    auto p2 = f2;  // C++0x automatic type deduction
    // pre-C++0x can use the following code instead
    // const double *(*p2)(const double *, int) = f2;
     cout << "Using pointers to functions:\n";
    cout << " Address  Value\n";
    cout <<  (*p1)(av,3) << ": " << *(*p1)(av,3) << endl;
    cout << p2(av,3) << ": " << *p2(av,3) << endl;
    // pa an array of pointers
    // auto doesn't work with list initialization
    const double *(*pa[3])(const double *, int) = {f1,f2,f3};
    // but it does work for initializing to a single value
    // pb a pointer to first element of pa
    auto pb = pa;
    // pre-C++0x can use the following code instead
    // const double *(**pb)(const double *, int) = pa;
    cout << "\nUsing an array of pointers to functions:\n";
    cout << " Address  Value\n";
    for (int i = 0; i < 3; i++)
        cout << pa[i](av,3) << ": " << *pa[i](av,3) << endl;
    cout << "\nUsing a pointer to a pointer to a function:\n";
    cout << " Address  Value\n";
    for (int i = 0; i < 3; i++)
        cout << pb[i](av,3) << ": " << *pb[i](av,3) << endl;
    // what about a pointer to an array of function pointers
    cout << "\nUsing pointers to an array of pointers:\n";
    cout << " Address  Value\n";
    // easy way to declare pc 
    auto pc = &pa; 
     // pre-C++0x can use the following code instead
    // const double *(*(*pc)[3])(const double *, int) = &pa;
   cout << (*pc)[0](av,3) << ": " << *(*pc)[0](av,3) << endl;
    // hard way to declare pd
    const double *(*(*pd)[3])(const double *, int) = &pa;
    // store return value in pdb
    const double * pdb = (*pd)[1](av,3);
    cout << pdb << ": " << *pdb << endl;
    // alternative notation
    cout << (*(*pd)[2])(av,3) << ": " << *(*(*pd)[2])(av,3) << endl;
    // cin.get();
    return 0;
}
// some rather dull functions
const double * f1(const double * ar, int n)
{
    return ar;
}
const double * f2(const double ar[], int n)
{
    return ar+1;
}
const double * f3(const double ar[], int n)
{
    return ar+2;
}
  • 显示的地址为数组av中double值的存储位置.
  • 指向函数指针数组的指针并不少见.实际上,类的虚方法实现通常都采用了这种技术.所幸的是,这些细节由编译器处理.
  • 感谢auto:C++11的目标之一是让C++更容易使用,从而让程序员将主要经理放在设计而不是细节上.程序清单7.19演示了这一点.自动类型推断功能表明,编译器的角色发生了改变.在C++98中,编译器利用其知识帮助您发现错误,而在C++11中,编译器利用其知识帮助您进行正确的声明.存在一个潜在的缺点.自动类型推断确保变量的类型与赋给它的初值的类型一致,但您提供的初始的类型可能不对:auto pc = *pa;上述声明导致pc的类型与*pa一致,在程序清单7.19中,后面使用它时假定其类型与&pa相同,这将导致编译错误.

7.10.4 使用typedef进行简化
7.11 总结
7.12 复习题
7.13 编程练习

附件:本章源代码下载地址

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值