函数参数

数据传递,函数调用的过程实际上就是将实参数据原样拷贝到形参当中
参数会出现在两个地方,分别是函数定义处和函数调用处,这两个地方的参数是有区别的
用void定义参数表示没有参数,不接收外部传输数据

形参(形式参数)

在函数定义中出现的参数可以看做是一个占位符,它没有数据,只能等到函数被调用时接收传递进来的数据,所以称为形式参数,简称形参
形参变量只有在函数被调用时才会分配内存,调用结束后,立刻释放内存,所以形参变量只有在函数内部有效,不能在函数外部使用。

实参(实际参数)

函数被调用时给出的参数包含了实实在在的数据,会被函数内部的代码使用,所以称为实际参数,简称实参
实参可以是常量、变量、表达式、函数等,无论实参是何种类型的数据,在进行函数调用时都必须有确定的值
实参和形参在数量上、类型上、顺序上必须严格一致\

函数调用

函数调用实质,程序执行流程转向由函数名指定的被调用函数
主要流程为:

  • 实参一一对应地传递给函数定义中的形参
  • 执行函数定义中的函数体
  • 执行结束,通过return语句将值返回到调用处
  • 程序执行流程返回调用处,执行后面的语句

传值,传址

函数参数传值调用过程:将函数调用语句中实参的一份副本传给函数的形参
c语言是以传值方式将参数值传递给被调用函数的,因此被调用函数不能直接修改主调函数中变量的值,例:

void Getnum(int num)         //实参n将值传给形参num
{
    num=10;                  //形参num的值发生改变,由于参数传递采用传值方式,因此该函数仅仅改变了num副本的值
    return;
}
int main()
{
    int n=20;                //实参赋值
    Getnum(n);               //返回主调函数,形参num的内存空间被释放掉,实参n的值没有发生改变
    printf("%d",n);          //输出结果:20
}

函数参数传地址调用过程:将变量的地址传递给函数的形参
指针参数使得被调用函数能够访问和修改主调函数中对象的值
形参和实参指向了同一个内存地址,对形参的操作同时影响了实参的值,从而实现了双向传递,例:

void Getnum(int *num)        //实参n将地址值传给指针形参num
{
    *num=10;                 //形参num对&n地址的值进行操作,通过指针间接访问指向的操作数
    return;
}
int main()
{
    int n=20;                 //实参赋值
    Getnum(&n);               //返回主调函数,此时实参n的值已经发生了变化
    printf("%d",n);           //输出结果:10
}

以上面示例程序中ReadInfo()也可以发现,如果函数中存在需要对读取学生人数进行修改等操作情况,就需要将形参设置为整型指针*num
另外如标准库函数scanf等的实现也借助了这种方法:
scanf将标识是否到达文件末尾的状态作为getint函数的返回值,同时使用一个指针参数存储转换后得到的整数并传回给主调函数
另外,指针在传入参数时实际上也是值传递,因此只能通过取址操作改变指针所指向空间中的值,对指针本身是无法进行操作的,例:

int strlen(char*s)
{
    int n;
    for(n=0;*s!='\0';s++)n++;
    return n;
}

其中执行s++运算不会影响到strlen函数调用者中的字符串,它仅对指针在strlen函数的私有副本进行自增运算

返回值

函数的返回值是指函数被调用之后,执行函数体中的代码所得到的结果,这个结果通过return语句返回
被调用函数通过return语句向调用者返回值,return语句后面可以跟任何表达式,表达式两边通常加一对圆括号,此处的括号式可选的
return只能用在函数中,用来返回处理结果,是提前结束函数的唯一办法\

若函数没有返回值,调用函数可以忽略返回值,此时通常用void定义返回值类型,并且return后不加表达式
当被调用函数执行到最后的花括号而结束执行时,控制流同样返回给调用者
return出来的数据的类型要和函数数据类型相同,必要时表达式将被转换为函数的返回值类型(return出来的数据实质上代表函数本身)
return语句可以有多个,可以出现在函数体的任意位置,但是每次调用函数只能有一个return语句被执行,所以只有一个返回值
某些情况下(多数编译环境是不支持的),如果函数从一个地方返回有返回值从另一个地方返回没有返回值,该函数并不非法,但是可能发生问题的征兆
任何情况下,如果一个函数没有成功返回一个值,则其值肯定是无用的
主函数main也可以返回一个值,该值通常代表状态,可以在调用该程序的环境中使用\

某些情况下,函数的定义与声明必须一致,如果函数与调用的函数放在同一源文件,并且类型不一致,编译器会检测到该错误
但是如果函数是单独编译的,这种不匹配的错误则无法检测出来,最后结果值将毫无意义
如果没有函数原型,则函数将在第一次出现的表达式中被隐式声明,即如果一个没有声明过的名字出现在表达式中并且其后紧跟着一个圆括号时
则上下文认为该名字是一个函数名,该函数将被假定为int类型,且上下文不对参数做任何假定
如果函数声明中本身就不包含参数,则编译程序也不会对函数参数作任何假定,并且会关闭所有参数检查
(同样,目前大部分编译环境已经不支持)\

对空参数表的特殊处理是为了能使新式编译器能编译老的c程序,不过在新编写的程序中如果函数带有参数则应声明,如果没有参数则使用void声明
(空参数表说明函数不需要参数,并不代表就不可以传入参数,只不过传入的参数用不到而已,如int test();test(a);可以运行(只针对c,c++不行))
(而加入void参数就限定了函数无法传入任何类型的参数,使程序更严谨)\

例:返回两个整数中的较大的一个
int max(int a, int b)
{
    if(a<0|b<0)return;        //如果a或b小于0,则直接终止函数
    return (a>b) ? a : b;     //返回三目表达式
    printf("Function is performed\n");
}

变长参数表

在c中能以可移植的方式编写可处理变长参数表的函数

如:printf的正确声明格式为int printf(char*fmt,...);

其中省略号表示参数表中参数的数量和类型是可变的,并且省略号只能出现在参数表的尾部
标准头文件<stdarg.h>中包含一组宏定义,他们对如何遍历参数表进行了定义,该文件的实现因不同的机器而不同,但提供的接口是一致的
va_list类型通常用于声明一个指针变量,该变量依次引用各参数
宏va_start将参数指针初始化为第一个无名参数的指针,使用参数指针前该宏必须被调用一次
va_start参数表必须至少包括一个有名参数,va_start将最后一个有名参数作为起点
宏va_arg使用一个类型名来决定返回的对象类型、指针移动的步长,每次调用va_arg都将返回一个参数,并将参数指针指向下一个参数
最后,在va_arg返回之前必须调用宏va_end以完成一些必要的清理工作\

例:简化的printf函数simprintf()
void simprintf(char*fmt,...)
{
    va_list ap;                          //定义va_list类型的参数指针ap,依次指向每个无名参数
    char*p,*sval;int ival;double dval;   //指针p用于遍历模式字符串,ival,dval,sval用于接受输入流中的字符
    va_start(ap,fmt);                    //带参宏va_start将ap指向最后一个有名参数fmt的后一个参数,即第一个无名参数
    for(p=fmt;*p;p++)
    {                                    //遍历模式字符串
        if(*p!='%')
        {                                //如果没有遇到字符'%'则直接输出其中的内容并跳过switch
            putchar(*p);
            continue;
        }
        switch (*++p)
        {                                //移动*p,根据其值进行转换
            case 'd':
                ival=va_arg(ap,int);     //调用va_arg移动ap只指向下一个无名参数,同时返回int类型
                printf("%d",ival);       //输出ival
                break;
            case 'f':
                dval=va_arg(ap,double);  //同理,移动ap返回double,输出dval
                printf("%f",dval);
                break;
            case 's':
                for(sval=va_arg(ap,char*);*sval;sval++)putchar(*sval); //移动ap,返回char*遍历字符串后输出
                break;
            default:
                putchar(*p);             //其他情况则直接输出
                break;
        }
    }
    va_end(ap);                          //结束时的清理工作
}

命令行参数

main()也有参数,并且参数表元素个数和数据类型都是固定的(已经声明)
在支持c的环境中,可以在程序开始执行时将命令行参数传递给程序

main()函数原型:
int main(int argc,char *argv[]);
调用主函数main时它带有两个参数
第一个参数习惯上称为argc,用于参数计数,其值表示运行程序时命令行中参数的数目
第二个参数习惯上称为argv,用于参数向量,是一个指向字符串数组的指针,每个字符串对应一个参数,通常用多级指针处理这些字符串(指针数组)

每个参数字符串在输入时通过空格隔开

例:arg[0]指向程序本身(类似于数组),arg[n]指向命令行参数的第n个字符串常量。argc和argv是默认的参数名,也可以另取名字
    最简单的例子是程序echo,它将命令行参数回显在屏幕上的一行中,命令行各参数之间用空格隔开
    因此,命令echo programming is fun
    将打印输出:programming is fun
    此时argc=4,argv[0]="echo.exe",argv[1]="programming",argv[2]="is",argv[3]="fun";

按照c语言的约定,argv[0]的值是启动该程序的程序名,因此argc的值至少为1,如果argc=1则表示程序名后没有命令行参数
因此第一个可选参数为argv[0],最后一个可选参数为argv[argc-1]
ANSI标准要求argv[argc]的值必须是一个空指针\

回显程序的简单实现:
int main()
{
    while(--argc>0)cout<<*++argv<<(argc>1?' ':''); 或者打印语句也可以写成printf(argc>1?"%s ":"%s",*++argv)
    cout<<endl;    argv是一个指向参数字符串数组起始位置的指针,++argv可以使它刚开始就指向argv[1],跳过存储程序名的argv[0]
    return 0;
}

在GUI界面以前,计算机操作界面都是字符式的命令行界面(DOS,Linux,UNIX)
在命令行界面中输入:文件名 命令行参数 这样的格式可以执行文件
其中多个命令行参数字符串使用空格分隔(如果参数中本身带有空格则放在""或’'中)

命令行参数示例程序:模式查找程序的命令行参数模拟
#include<iostream>
using namespace std;
int grepmain(int argc,char*argv[])
{
    char gline[MAXLINE];
    int found=0;
    if(argc!=2)cout<<"Usge:find pattern"<<endl;
    else while(getline_ptr(gline,MAXLINE)>0)
        if(strstr(line,argv[1])!= nullptr)
        {
            cout<<gline;
            found++;
        }
    return found;
}

标准库函数strstr(s,t)返回一个指针,该指针指向字符串t在字符串s中第一次出现的位置,如果字符串t没有在字符串s中出现则返回空指针NULL
该函数声明在头文件<string.h>
为进一步解释指针结构,可以假定允许程序带两个可选参数,其中一个表示打印除匹配模式之外所有行,另一个参数表示每个打印文本行前加相应行号
UNIX系统中的c程序有一个公共的约定:以负号开头的参数表示一个可选标志或参数
假定以-x(代表“除…之外”)表示打印所有与模式不匹配的文本行,用-n(代表“行号”)表示打印行号

如:find -x -n 模式 表示打印所有与模式不匹配的行,并在每个打印行的前面加上行号

可选参数应该允许以任意次序出现,同时程序的其余部分应该与命令行中参数的数目无关,并且如果可选参数能组合使用,将给使用者带来更多的便利\

如:find -nx 模式
#include<iostream>
using namespace std;
int grepmain_nx(int argc,char*argv[])
{
    char gline[MAXLINE];
    long lineno=0;
    int except=0,number=0,found=0;
    while(--argc>0&&**++argv=='-')while(*++*argv)
    switch (**argv)
    {
        case 'x':except=1;
                break;
        case 'n':number=1;
                break;
        default:
            cout<<"find:illegal option"<<" -"<<**argv;
            argc=0,found=-1;
            break;
    }
    if(argc!=1)cout<<"Usge:find -x -n pattern"<<endl;
    else while(getline_ptr(gline,MAXLINE)>0)
    {
        lineno++;
        if((strstr(gline,*argv)!= nullptr)!=except)
        {
            if(number)cout<<lineno;
            cout<<gline;
            found++;
        }
    }
    return found;
}

注意,++argv是一个指向参数字符串的指针,**++argv是它的第一个字符,另一种有效表达方式是(++argv)[0]
如果在上述表达式中不加圆括号变成*argv[0]的话,实际上是*(argv[0]),相当于argv[0][1]或argv
实际上在内层循环中就使用了
argv或++argv[0],其目的是遍历一个特定的字符串
实际上很少有人会使用比这更复杂的指针表达式了,如果遇到这种情况可以考虑分为两三步来理解会更直观一些