以下跳过大部分和C相同的函数知识。

函数基础知识

要使用C++函数,需要完成以下工作:

  • 提供函数定义;
  • 提供函数原型;(通常被隐藏在include文件中)
  • 调用函数;

    1. #include<array>
    2. #include<iostream>
    3. using namespace std;
    4. // 函数原型
    5. int caculate_area(int w, int h); // 可以省略w和h
    6. int main(){
    7. int w = 10;
    8. int h = 20;
    9. // 函数调用
    10. cout << caculate_area(w, h);
    11. }
    12. // 函数定义
    13. int caculate_area(int w, int h){
    14. return w * h;
    15. }

    注意事项:

  • C++对于返回值类型有一定的限制:不能是数组;(对于输入没有这样的限制)

  • 除了数组之外,其它任意类型:整数、浮点数、指针、结构体、对象等;
  • 当数组作为结构或者对象的组成部分时,仍旧可以返回!
  • int caculate_area(int, int);仍旧合法;

函数返回值时,首先将返回值复制到指定的CPU寄存器或内存单元中,随后调用程序将查看该内存单元。函数的原型将返回值类型告知调用程序,而函数定义命令被调用函数应该返回什么类型的数据。
当遇到多个返回语句时,第一个有效!

原型的意义

原型描述了函数到编译器的接口,也就是说,它将函数返回值的类型以及参数的类型和数量告诉编译器。
有了函数定义了,为什么不直接利用函数定义来获取这些信息呢?一个原因是这样做效率不高,另一个原因是函数定义可能存在于其它文件,C++容许将一个程序放在多个文件中,单独编译这些文件,最后再将他们组合起来(后面这个原因没有很懂)。
基于原型以上意义,我们不难推断,在数组作为参数进行传递时,被调函数中的参数数组不会重新开辟空间,也就是相对当一个const指针的传递(事实上的确是一个指针)。指针做参数,仍旧是值传递,只是传递的值时地址。

  1. #include<array>
  2. #include<iostream>
  3. using namespace std;
  4. // 函数原型
  5. int caculate_area(int w[], int n);
  6. int main(){
  7. int w[5] = {10, 10, 10, 10, 10};
  8. int n = 5;
  9. // 函数调用
  10. cout << caculate_area(w, n) << endl;
  11. cout << w[0] << endl;
  12. cout << w[1] << endl;
  13. return 0;
  14. }
  15. // 函数定义
  16. int caculate_area(int w[], int n){
  17. int total = 0;
  18. for(int i=0; i < n; i++){
  19. total += w[i];
  20. w[i] = total;
  21. }
  22. return total;
  23. }

以上代码会修改w数组的值!(同样非数组的类型,将引用作为参数,同样可以修改参数原值)

原型的功能

  • 原型确保编译器正确处理函数返回值;
  • 原型确保编译器检测使用的参数数目是否正确;
  • 原型确保编译器检查使用的参数类型是否正确,如果不正确,则将进行类型转换。

    函数和数组

    可以参考上面的代码,值得注意的就是原型中的数组表示:typeName varName[];并且此变量是一个指针,由于指针的[]索引,其和数组看不出差别;
    以下两句等价:
    int caculate_area(int w[], int n);
    int caculate_area(int* w, int n);

  • 对数组进行sizeof得到的是整个数组块的大小,对指针sizeof则是一个类型长度;

  • 函数调用传递数组实际上是传递指针,sizeof满足指针特性;
  • 对于不需要修改的数组,前面最好加上const关键字(例如上面第一个原型中的int w[]);
  • 对于数组的传递,不仅需要传递数组的首地址,还需要传递元素个数;

    • 一个替代的方式是传递一个首地址和尾地址的方式;
      1. int caculate_area(const int* begin, const int* end){
      2. int total = 0;
      3. const int* pt = NULL; // 申明指向常量的指针,不是指针常量
      4. for(pt=begin; pt!=end;pt++){
      5. total += *pt;
      6. }
      7. return total;
      8. }

      指针和const

      指针指向常量

      注:此处的const指针指的是指向const变量的指针,不是指指针值是常量的指针!
      1. const int number = 10;
      2. const int* np = &number;
      注意事项:
  • 利用const关键字修饰指针并非一定是指向常量,上述代码中number如果不是常量,np仍旧可以用const修饰;

  • 只是修饰之后不能通过np改变被指向变量的值;
  • const指针不能指向const变量;

    1. const int number = 10;
    2. int* np = &number; // Invalid

    也就是说:

  • const指针可以指向非const变量;

  • 非const指针不能指向const变量;(指向之后则可以进行修改,与const不符)
  • const指针可以指向const变量;
  • 强制类型转换可以突破限制;

    常量指针

    注:此处常量指针才是指针值不能改变的指针!
    常量指针在定义时必须被初始化,因为指针值不能修改!

    1. int number = 100;
    2. int * const np = &number;

    上述代码表示,np是一个常值指针,它的值不能修改,只能指向number,也就是说const的位置,实际影响变量的特性。由此还可以定义指向常量的常量指针:

    1. const double pi = 3.1415926;
    2. const double* const pPI = &pi;

    由此可见,这些特征完全是可以推出来的!

    函数和二维数组

    首先应该知道,二维数组由一维数组组成。

    1. int data[3][4] = {{1,2,3,4},{2,3,4,5},{3,4,5,6}};
    2. int sum = sum_data(data, 3);
    3. // 或者 sum_data(data, 3, 4)

    那么函数的原型如何定义呢?
    对于data来说,data[0],data[1],data[2]都是一个包含4个元素的一维数组。显然,data可以看做是一个一维数组,数组里面的元素还是一维数组。根据函数不能将数组作为参数,所以形参必须是指针类型(因为data传递的是地址),并且指针指向的元素是包含4个元素的一维数组,由此有:int (*ar2)[4],式中ar2是一个指向4个元素的一维数组的指针。

    1. int sum_data(int (*ar2)[4], int size);

    其中的int (*ar2)[4]又可以等价于int ar2[][4](意义相同,形式不同,但是后者更明了!)

    错误理解

    显然如果不限定二维数组的列数,那么可以有:_int (*ar2)[]_也可以写作_int *(*ar2)_

    1. int sum_data(int **ar2, int row, int col);

    以上的写法是错误的,因为在参数传递过程中,传递的是data的首地址,如果不指定data元素的类型(即4个元素的一维数组),那么如何知道ar2 + 1为多少呢?也即是说,int ** ar2没有提供足够充足的信息!
    那么如何理解int** p呢?—> 指向整型指针的指针!(复合指针)也就是说*p是一个整型指针,那么p是指向整型指针的指针!

    高维数组

    1. int data[3][3][3] = {{{1,2,3},{1,2,3},{1,2,3}},
    2. {{1,2,3},{1,2,3},{1,2,3}},
    3. {{1,2,3},{1,2,3},{1,2,3}}};
    4. int sum = sum_data(data, xxx);

    data[i]是一个指针,类型为整型;data[i][j]同样是指针,类型同样为整型;如果但看data则,它是一个数组,包含3个元素,每个元素都是一个二维数组。对比二维数组,易知:

    1. int sum_data(int (*data)[3][3], int size);
    2. // 等价于
    3. int sum_data(int data[][3][3], int size);

    其中,还可以利用一级指针通过计算偏移量来进行访问(数组空间是连续的,线性;这种方式的好处是:不是针对特定size的数组,更具有通用性!!!):

    1. //通过一级指针,计算偏移量来遍历二维数组:
    2. void printDoubleDimensionalArray(int *array,int rows,int cols)
    3. {
    4. int i,j;
    5. for(i=0;i<rows;i++)
    6. {
    7. for(j=0;j<cols;j++)
    8. {//通过一级指针,计算偏移量来遍历二维数组:
    9. printf("%4d",*(array+i*cols+j));
    10. }
    11. printf("\n");
    12. }
    13. }

    符号的不同组合方式将产生完成不同的类型!要小心!

    函数和C-style字符串

    字符串表示方式:

  • char数组(必须以\0结尾);

  • 引号括起的字符串常量;
  • char指针;

将字符串作为参数,和数组一样都是传递的地址。值得注意的是,char数组只有以\0结尾才能被称为是字符串,而\0(编码值为0)又可以作为遍历字符串的结束标志!

函数和结构

结构作为参数编写函数时比数组作为参数编写函数简单很多,因为结构可以作为参数进行传递(其主要原因是数组不能互相进行赋值操作)。

  1. struct person
  2. {
  3. std::string name;
  4. int age;
  5. };
  6. person choose_person(person p1, person p2); // 函数原型
  7. person per1 = {"dhh", 20};
  8. person per2 = per1;
  9. per2.name = "zyr";
  10. cout << "person 1:" << per1.name << endl;
  11. cout << "person 2:" << per2.name << endl;
  12. person choose_person(person p1, person p2){
  13. //TODO
  14. }

与数组不同,结构名和地址无关,如果需要获取结构体的地址,需要利用取地址运算符&
结构体完全可以向普通变量那样使用,作为参数,作为函数返回值,但是当结构体较大时,结构体的复制等会增加内存消耗,降低运行速度,所以可以采用指针进行参数传递。
此处不再赘述函数和结构的案例等。值得注意的是:

  • 结构指针访问结构成员:sp -> member 或者 (*sp).member
  • 结构访问结构成员:struct.member

    函数和string对象

    string对象和C-style的字符串用途几乎相同,但是它和结构更为相似。对象之间可以进行赋值(会生成副本)。 ```cpp

    include

    include

    using namespace std; const int SIZE = 3; void show_strings(std::string sa[], int n);

int main(){ std::string sa[SIZE]; cout << “Enter “<< SIZE << “strings!” << endl; for(int i=0;i<SIZE;i++){ cout << i << “: “; getline(cin, sa[i]); } show_strings(sa, SIZE); }

void show_strings(std::string sa[], int n){ cout << “display below:” << endl; for(int i=0; i<SIZE;i++){ cout << i << “: “; cout << sa[i] << endl; } }

  1. <a name="A8YND"></a>
  2. ## 函数和array对象
  3. 和string差不多
  4. ```cpp
  5. const std::array<std::string, 2> Names = {"yangtao", "songjiazhuang"}

递归

递归:自己调用自己。利用递归时可能会造成冗余计算 -> 斐波那契数列的递归算法;利用递归时,应该进行一定的分析,以防冗余计算!

  1. def get_fibo(int n){
  2. // 递归出口
  3. if(n == 0 || n == 1)
  4. return 1;
  5. // 递归调用
  6. else
  7. return get_fibo(n-1) + get_fibo(n-2);
  8. }

函数指针

函数地址是存储其机器语言代码的内存开始地址。利用函数的地址,可以编写一个函数,它以另一个函数的地址作为参数,这样前者就可以找到后者并运行它。当然,通常情况下,还是直接调用函数比较方便。以函数地址为参数,适合不同情况下调用不同函数的情况。
函数指针调用函数:

  • 获取函数的地址;
  • 申明一个函数指针;
  • 使用函数指针来调用函数;

    1、函数地址获取

    获取函数地址很简单,直接用函数名即可。例如上面的get_fibo()函数,get_fibo函数名就是该函数的地址(与数组类似)。
    由此有如下总结:

  • 数组和函数的名称即是地址;

  • 其它类型名称则是名称;

    2、声明函数指针

    声明指向某种数据类型的指针时,必须指定指针指向的类型。同样,声明函数指针时,也必须指定指针指向的函数类型。这意味着声明应指定函数的返回类型以及函数的特征标(参数列表)。换言之,声明应像函数原型那样指出有关函数的信息。

    1. double pam(int); // 函数原型
    2. double (*pf)(int); // 定义指针
    3. pf = pam; // pf指向pam函数

    由于pam是函数,与之对应的(*pf)也是函数,从而pf是函数指针。
    注意事项:

  • (*pf)括号不能掉;

    • 没了括号:double *pf (int);表示的是pf是返回指针类型的函数;
  • pfpam的特征标必须相同;(参数列表和返回值类型必须相同)

    1. double pam(int); // 定义原型
    2. double (*pf)(int);// 定义指针
    3. pf = pam;
    4. double x = pam(4);
    5. double y = (*pf)(4); // 也容许直接用:pf(4)来调用函数
    6. if(x==y){
    7. cout << "OK!" << endl;
    8. }
    9. double pam(int a){
    10. return a;
    11. }

    注意事项:

  • 严格来说(*pf)才是函数,但是在C++中允许利用pf(4)的形式去使用指针;

  • 也可以在声明函数指针时进行初始化:double (*pf)(int) = pam;

    1. double estimate(int lines, double (*pf)(int)); //函数指针作为参数

    3、函数指针数组

    只有当函数的特征标相同时才能构成数组!

    1. const double* (*pa[3])(const double *, int) = {f1, f2, f3};

    值得注意的是:

  • []的优先级高于,所以`pa[3]`首先说明pa是一个数组,包含三个元素,然后指明元素的类型为指针(pa是指针数组);

    • 所以是*pa[3]的形式!
  • 相反如果是:(*pa)[3],则首先说明pa是指针,然后表示(*pa)是包含3个元素的数组,那么pa就是数组指针,该指针指向的数组元素必须为3;(数组指针)
  • 注意C++中数组是可以越界访问的!例如:

    1. int a[2] = {1,2};
    2. cout << a[3] << endl;
    1. #include<array>
    2. #include<iostream>
    3. using namespace std;
    4. double add(double, double);
    5. double add1(double, double);
    6. void calculate(double, double, double (*ap)(double, double));
    7. int main(){
    8. double (*ap)(double, double) = add; // ap是指针
    9. calculate(2.1, 2.2, ap);
    10. calculate(2.1, 2.2, add1); // add1是函数名,实际上是函数地址值
    11. }
    12. double add(double x, double y){
    13. return x + y;
    14. }
    15. double add1(double x, double y){
    16. return (x + y)*10;
    17. }
    18. void calculate(double x, double y, double (*ap)(double, double)){
    19. cout << (*ap)(x, y) << endl;
    20. }

    auto进行简化

    1. const double* (*pa[3])(const double *, int) = {f1, f2, f3};

    上述代码不能使用:auto pa = {f1, f2, f3}

  • auto只能用于单值初始化,而不能用于初始化列表;

    • 像:auto pc = pa则是合理的;
  • **&pa==*pa==pa[0]

    typedef进行简化

    typedef可用于别名的创建,例如给double创建别名real
    其可以减少代码的输入量!
    1. typedef double real;
    将别名当做标识符进行声明:
    1. typedef const double *(*p_fun)(const double *, int); // p_fun现在是类型名了
    2. p_fun p1 = f1;