第七章 函数

使用函数的三步: 1 提供函数的定义 2 提供函数原型 3 调用函数

函数原型:

为什么要提供函数原型:

原型描述函数到编译器的接口,将函数返回值类型、参数类型、参数个数 告诉编译器,程序返回后将返回值放入寄存器或者内存,调用函数从这个位置取得返回值,编译器根据三要素去决定解析多少字节去获取返回值。并且利用函数原型,可以避免搜索函数定义,加快编译速度。

函数和数组:

数组作为参数时,形参形式为:int arr[]; 等价于 指针int* arr; 此时arr[] 并不能代表一个数组,应为编译器不知道它具有几个int成员

void start_fun_array_text_fun(int arr[], int size);

此种情况下形参只是一个指针,调用函数时,实参只会把数组第一个成员的地址复制给形参,不会传递整个数组。

参数 int arr[] 等价于 int* arr;
void start_fun_array_text_fun(int arr[], int size) {
  cout << "start_fun_arrmay_text_fun" << endl;
  cout << "arr[0] " << arr[0] << endl;
  cout << "arr[1] " << arr[1] << endl;
  cout << "arr[2] " << *(arr + 2) << endl;
  sizeof(arr);  // 结果为4,因为此时arr 为int* 型,指针大小为4 (某些系统不一样)
}

为了避免函数修改数组的值,如果函数不是特意修改修改值,建议都加上const 修饰

void start_fun_array_text_fun(const int arr[], int size);

void start_fun_array_text_fun(const int * arr, int size);声明参数指针arr 指向的值是const,不能被修改。 此时实参不需要用const 修饰。

const 修饰参数
int* const  p: 声明指针p本身是一个常量,不能被改变,
void start_fun_p_text_fun( int* const  p, int size) {
  cout << "start_fun_p_text_fun" << endl;
  int a =22;
  p = &a; //不合法,修改了指针常量的值
  *p = a; // 合法,修改指针常量 p 所指向地址保存的值
}


const int* p: 声明指针p指向的地址是一个常量对象,read only的,不能被改变,只读的
void start_fun_p_text_fun( const int* p, int size) {
  cout << "start_fun_p_text_fun" << endl;
  int a =22;
  p = &a; //合法,修改了指向常量的指针的值
  *p = a; // 不合法,此地址代表的内存是read only的,不能被修改。
}

int a = 10;
const int* p = &a;
-------------------
注意此种情况下,只是声明 对于p来说,p指向的常量,不能通过p修改;但a本身并不是常量,可以通过a修改值。

const int a = 10;
const int* p = &a;
--------------------
合法的

const int a = 10;
int* p = &a;
--------------------
此种是不合法的,常量必须通过常指针指向它,避免被修改。

两维数组:

二维数组a[m][n]

a[0] 是int* 类型,可以看成一维数组的数组名b; 
&a[0]与&b 就是整个一维数组的地址。即指向数组的指针
 int (*p1 )[2] = a;    // a为二维数组首个一维成员的地址,等价于 &a[0]。即指向数组指针

&a 为指向二维数组的指针 指向int (*)[m][n]型指针

所以 用指针指向二维数组要用数组指针,int(*p)[n];

void start_arr_2_text_fun() {
  cout << "start_arr_2_text_fun" << endl;
  int a[3][2] = 
  {{1, 2},
   {3, 4},
   {5, 6}};
int b[2] = {1, 2};
   cout << "a[0]" << a[0] << endl;
   cout << "a[1]" << a[1] << endl;

   int (*p )[2] = &a[0]; // a[0] 是int* 类型,可以看成一维数组的数组名b; 
                         // &a[0]与&b 就是整个一维数组的地址。即指向数组的指针
   int (*p1 )[2] = a;    // a为二维数组首个一维成员的地址,等价于 &a[0]。
    cout << "*p:" << *(*(p+2)+1) << endl; // 值为6
   cout << "*p:" << *(*(p+2)+1) << endl;

   int **p1 = a;
}

二维数组当做参数:

参数可以用数组指针,arr是一个指向长度为2的数组指针,line代表二维数组长度
实参必须是:int[n][2]
void start_fun_arrmay_2_text_fun(const int (*arr) [2], int line) {
  cout << "start_fun_arrmay_2_text_fun" << endl;
  cout << "arr[0][0] " << arr[0][0] << endl;
  cout << "arr[1][1] " << arr[1][1] << endl;
}

函数与结构体:

因为结构体名是代表整个结构,这个和数组有区别; 所以 结构体可以直接通过 = 赋值;st1 = st2; 数组是有个数区别的,结构没有,所以可以赋值;

所以 结构作为参数,传递的是整个结构体,而不是地址,在结构体成员较少的情况下可以用。

struct stime {
  int hour;
  int min;
} st1, st2;
按值传统参数
start_struct_fun_text(st1, st2);
void start_struct_fun_text(const stime t1, const stime t2) {
  stime sum;
  sum.hour = t1.hour + t2.hour;
  sun.min = t1.min + t2.min;
}

按指针传递

stime* stp;
void start_struct_fun_text(const stime* t1, const stime* t2) {
  stime sum;
  sum.hour = t1->hour + t2->hour;
  sun.min = t1->min + t2->min;
}

c++中类对象是基于结构的,所以结构的用法适用于类; string 对象用法可以像结构体一样使用。

由此可见C++ 中函数参数传递对象,都是传递的实参的拷贝,函数中并不会修改实参的值。

函数的递归调用:

就是函数自己调用自己,在有些场景非常有用

函数自己调用一次:

void start_recur_fun_text(int n) {
    cout << "start_recur_fun_text" << endl;
    cout << "before call n:" << n << " at address: " << &n << endl;
    if(n > 0) {
       start_recur_fun_text(n -1);
    }
    cout << "after call n:" << n << " at address: " << &n << endl;
}
输出结果:
start_recur_fun_text
before call n:3 at address: 0x62fc60
start_recur_fun_text
before call n:2 at address: 0x62fc20
start_recur_fun_text
before call n:1 at address: 0x62fbe0
start_recur_fun_text
before call n:0 at address: 0x62fba0
after call n:0 at address: 0x62fba0
after call n:1 at address: 0x62fbe0
after call n:2 at address: 0x62fc20
after call n:3 at address: 0x62fc60
此函数首先依次执行if 之前语句,函数调用自己依次建立调用栈,当n>0 不成立时,相反方向退出调用栈。
两次递归,可以解决类似二叉树问题
void start_recur_fun2_text() {
    int len = 66;
    char ruler[len] = {' '};
    ruler[len - 1] = '\0';
    int max = 64;
    int min = 0;
    int div = 6;

    ruler[min] = ruler[max] = '|';
    cout << ruler << endl;
    for (int i = 1; i <= div; i++) {
      start_recur_div_text(ruler, min, max, i);
      cout << ruler << endl;
      for(int j = 1; j < (len - 2); j++) {
        ruler[j] = ' ';
      }
    }
}

void start_recur_div_text(char ar[], int low, int high, int level) {
    if(level == 0) {
      return;
    }

    int mid = (high + low) / 2;
    ar[mid] = '|';
    start_recur_div_text(ar, low, mid, level -1);
    start_recur_div_text(ar, mid, high, level -1);
}




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

函数的指针:

函数的指针是存储函数机器语言代码的起始地址。

函数指针的实现,允许在不同的时间传递不同的函数地址,即在不同时间调用不同的函数。

函数名即代表函数的地址。

声明函数指针:

如果函数原型是 int fun_pointer(char);

对应的函数指针为: int  (*p) (char); // 注意和 int * P(char); 区别,后者是返回int* 型的函数p的声明。

int  (*p) (char);  对比原型,由于*p 替换成了 fun_pointer,由于fun_pointer是函数,所以*p也是函数,则p就是函数的指针。

可以使用auto代替声明类型:

int* (p1)(char)= f1;

auto p1 = f1;

指针函数的调用:

(*p)(5); 等价于p(5);

声明指针数组,数组中放入函数指针:

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

使用typedef

typedef int* (pfun)(char);

pfun p1;

函数与引用变量

引用是变量的别名,就是给变量增加了一个名字。引用主要用在函数的参数,通过引用,函数处理的是变量的原始数据,而不是参数的副本。

创建引用变量:

        int a = 10;

        int &b = a;//声明b是a的引用,b的类型是int&,说明b是指向int型的引用,指向的是a。

b和a的地址都是一样的,地址里的值肯定也是一样的,只是名字不一样。

引用必须在声明时候初始化,int& b;  b = a;//错误的

引用一旦被初始化,将不能改变值,即b和a终身绑定。

可以看做是int* const p = &a;的伪装表示。其中 *p和 b是相同的。

按引用传递参数:

        int fun(int & n){
             return n++;
        }

        调用:
        int a = 10;
        fun(10);
        //相当于隐式执行了 int& n = a;
        函数fun中执行的n, 就是实参a的别名,n的改动会影响到实参a;

可以用const修饰参数,程序将不能改变实参的值:

int fun(const int & n);

如果参数被const修饰,例如 int fun(const int & n); 则可以fun(a+2)这种形式调用,编译器将创建一个临时变量,如果没有被const修饰是不行的,因为函数潜在修改参数n的风险。

应尽可能的适用const 修饰参数。

函数返回值为引用类型,

int & fun(const int & n) {
    return n;
}

此函数将返回引用参数n,返回的是形参的引用,如果返回值是int 而不是int&,将返回n的拷贝。

int a = fun(b);相当于将b的值复制给了a。 编译器会先把n的值直接复制给a。

如果是int fun(const int & n);int a = fun(b);此种情况下,编译器会先创建一个临时位置,将
n的值传递给临时变量,然后再将临时变量的值传递给变量a,
所以使用引用返回类型,节约了一次拷贝。

const声明返回的引用:
const int & fun(const int & n);此种情况下,返回的引用将不能被直接修改,fun(a) = 10;时
候错误的,不能当做左值; 如果不用const修饰,将是合法的,因为返回的是实参的引用。

引用使用的场景:

1 如果数据对象很小,按值传递。

2 如果数据对象是数组,使用指针,是唯一的选择。

3 如果数据对象是类对象,使用const引用,传递对象标准方式是按引用传递。

4 如果数据对象是结构,使用指针或引用,并尽量使用const。

默认参数

char* left(const char* str, int n = 1, int b = 2);

值得注意的是,默认参数必须从右向左设备,也就是说,如果想设置哪个参数为默认参数,则其右边所有参数必须是默认参数。

函数的重载:

函数的重载关键是看函数的参数列表,即函数特征标是否相同,包括参数列表顺序,参数类型。

int fun(int n);
int fun(int& n); 
这两个函数被视为是相同的,不能被重载,因为调用时候形式一样,fun(a);

函数返回值不同,参数列表相同,是不能被重载的。

int fun(int n);
double fun(int n); 
不能被重载,要看参数列表

函数模板

使用泛型定义函数,可将类型作为参数传递给函数模板。

模板格式:

template<Typename T1, Typename T2, Typename T3....>

 返回值类型 函数名(参数列表){}

模板原型:

template <typename T>
void my_swap(T &a, T &b);

template <class T>
void my_swap(T &a, T &b);

也可以用用class 代替typename

和普通函数一样模板函数需要定义,和原型的声明。

编译器再遇到模板函数的调用时候,会将实际使用的类型替换模板类型,生成一个实际使用类型的函数。

值的注意的是,模板并不创建任何函数,只是告诉编译器如何创建函数,遇到模板调用时,编译器将替换typename,生成对应的函数。

#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <string>
#include <vector>
#include <fstream>
#include "stlApp/slt_eng.h"

using namespace std;

template <typename T>
void my_swap(T &a, T &b);

struct stime {
  int hour;
  int min;
};

int main()
{
    cout << "Start main funtion!" << endl;
    int a = 10;
    int b = 100;
    my_swap(a, b); //编译器会生成一个 void my_swap(int &, int &);的函数定义
    double da = 1.1;
    double db = 2.2;
    my_swap(da, db); //编译器会生成一个 void my_swap(double &, double &);的函数定义
    system("pause");    // 防止运行后自动退出,需头文件stdlib.h
    return 0;
}

template <typename T>
void my_swap(T &a, T &b) {
  T temp;
  temp = a;
  a = b;
  b = temp;
}

函数模板重载

函数模板也可以像正常函数一样重载:

template <typename T>
void my_swap(T &a, T &b);

template <typename T>
void my_swap(T* a, T* b);
  1. //函数模板不允许自动类型转换,必须严格匹配类型

  2. //普通函数可以进行自动类型转换

  1. 模板具体化函数定义:
    显式具体化的原型和定义以template<>开头,并通过名称来指出类型。
    具体化有线于常规模板
    template <> bool myComplete(Persion p1, Persion p2) {
     ...
    }
    也可以这样写:
    template <> bool myComplete<Persion>(Persion p1, Persion p2);
    函数调用时候,参数如果是Persion,将优先使用具体换模板,myComplete(p1, p2);

所以对于给定的函数名,有非模板函数,模板函数,显式具体化模板函数。

实例化和具体化:

实例化就是定义一个模板后,在程序中使用该模板,传入一个特定的类型,如my_swap(a, b); 编译器会隐式实例化一个my_swap函数实例。

显示具体化就是,利用显式具体化函数定义,template <> bool myComplete<Persion>(Persion p1, Persion p2); 编译器在定义时候就会实例化函数myComplete,

也可以在函数调用时候显示具体化: my_swap<int>(a, b)

这里有个问题搞不明白:显式具体化是干嘛用的,什么场景使用,和普通函数看起来没什么区别

参考文档:

C++函数模板 - 百度文库

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值