C程序设计语言- 函数与程序结构-3.1----函数的基本知识、外部变量、作用域规则、头文件

前言:最近悠闲,继续出C系列第三章--函数与程序结构。


1.函数的基本知识

函数的定义形式如下:

返回值类型  函数名(参数声明表)

{

                声明和语句

}



尽管我们可以把所有的代码都放在主程序main中,但是更好的做法是,利用其结构把每一部分设计成一个独立的函数。下面我们贴出

代码:

#include <stdio.h>
#define MAXLINE 1000 //maximum input line length

int getline(char line[],int max);
int strindex(char source[],char searchfor[]);
char pattern[] = "ould";
main(){
  char line[MAXLINE];
  int found=0;

  while(getline(line,MAXLINE)>0){
	  if (strindex(line, pattern) >= 0) {
            printf("%s", line);
            found++;
      }
     
       return found;
  }
}
  //getline: get line into s, return length
  int getline(char s[],int lim){
      int c;

	  int i=0;
	  while (--lim > 0 && (c=getchar()) != EOF && c != '\n')
      s[i++] = c;
      if (c == '\n')
      s[i++] = c;
      s[i] = '\0';
      return i;
  }
  
  //strindex: return index of t in s, -1 if none 
  int strindex(char s[], char t[]){
         int i, j, k;
         for (i = 0; s[i] != '\0'; i++) {
         for (j=i, k=0; t[k]!='\0' && s[j]==t[k]; j++, k++)
         if (k > 0 && t[k] == '\0')
         return i;
         }
       return -1;
  }


2.外部变量


下面我们看一个例子:编写一个具有+、-、*、/四则运算功能的计算机程序。为了更容易实现,我们在计算机中使用逆波兰表示法代替普通的中缀表达式。

用逆波兰表示法设计思路:

while(下一个运算符或操作数不是文件结束指示符)

            if(是数)

                   将该数压入到栈中

            else if(是运算符)

                    弹出所需数目的操作数

                    执行运算

                    将结果压入到栈中

             else if(是换行符)

                    弹出并打印栈顶的值

             else 

                   出错

    下面上代码:

首先main.c

#include <stdio.h>
#include <stdlib.h> /* for atof() */
#define MAXOP 100 /* max size of operand or operator */
#define NUMBER '0' /* signal that a number was found */
int getop(char []);
void push(double);
double pop(void);
/* reverse Polish calculator */
main()
{
          int type;
          double op2;
          char s[MAXOP];
          while ((type = getop(s)) != EOF) {
             switch (type) {
              case NUMBER:
              push(atof(s));
              break;
              case '+':
              push(pop() + pop());
              break;
              case '*':
              push(pop() * pop());
              break;
              case '-':
              op2 = pop();
              push(pop() - op2);
              break;									
              case '/':
              op2 = pop();
              if (op2 != 0.0)
              push(pop() / op2);
              else
              printf("error: zero divisor\n");
              break;
              case '\n':
              printf("\t%.8g\n", pop());
              break;
              default:
              printf("error: unknown command %s\n", s);
              break;
           }
     }
          return 0;
}
其次getop.c

#include <ctype.h>
#include<stdio.h>

#define NUMBER '0'
int getch(void);
void ungetch(int);
/* getop: get next character or numeric operand */
int getop(char s[])
{
         int i, c;
         while ((s[0] = c = getch()) == ' ' || c == '\t')

         s[1] = '\0';
         if (!isdigit(c) && c != '.')
         return c; /* not a number */
         i = 0;
         if (isdigit(c)) /* collect integer part */
         while (isdigit(s[++i] == getch()))

         if (c == '.') /* collect fraction part */
         while (isdigit(s[++i] = c = getch()))

         s[i] = '\0';
         if (c != EOF)
         ungetch(c);
          return NUMBER;
}

再次pop.c

#include<stdio.h>
#define MAXVAL 100 /* maximum depth of val stack */
int sp = 0; /* next free stack position */
double val[MAXVAL]; /* value stack */
/* push: push f onto value stack */
void push(double f)
{
if (sp < MAXVAL)
val[sp++] = f;
else
printf("error: stack full, can't push %g\n", f);
}
/* pop: pop and return top value from stack */
double pop(void)
{
if (sp > 0)
return val[--sp];
else 
printf("error: stack empty\n");
return 0.0;
}


这样一个简单的计算机的四则运算编写完成。

             

3.作用域规则

程序中名字的作用域,通俗的讲,就是这个名字在程序中的使用范围。在C语言中,作用域的规则相对比较简单,主要分为 全局作用域 和 局部作用域 两种。

一个变量如果定义在函数中,那么这个变量的作用域就是一个局部作用域,在函数外面不能访问这个变量。在别的函数中定义同名的变量,也不会出现名字冲突,因为它们都处于各自的局部作用域中。

外部变量或函数的作用域是从声明的位置开始,到所在文件的末尾。如果需要在别的文件中引用不在这个文件中定义的外部变量,或者在这个变量定义之前使用,可以通过关键字 extern 进行声明。

extern 修饰符的作用是,告知该变量已经在别的地方定义了。外部变量和函数默认具有全局作用域,所以可以在程序的任何位置进行引用。


main() { ... }
int sp = 0;
double val[MAXVAL];
void push(double f) { ... }
double pop(void) { ... }

在上面的代码中,sp和val的作用域是从它们定义的位置开始的,所以可以在push和pop函数中引用这两个变量。而在main函数中,不能直接引用这两个变量。如果需要引用在后面定义的变量,可以使用关键字 extern 声明需要使用的变量。在上面的代码中,如果需要在main中使用sp,则可以这样声明:


extern int sp;

声明和定义的区别:声明只是告知这个变量的类型,而定义一个变量,除了告知变量的类型,还会为变量分配存储空间。

在一个程序中,外部变量的定义只能有一处,而外部变量的声明可以出现在任何需要引用这个变量的文件中。通过在文件中使用 extern 关键字声明一个变量,可以使用在别的地方声明的变量。

static修饰符

在C语言中,static修饰符用于定义静态变量。static除了可以修饰变量外,还可以修饰函数。

对于外部变量和函数,static修饰符会限制它们的作用域,使它们只能在定义它们的文件中使用,作用域不能传播到文件外。

使用static修饰符,提供了一种隐藏名字的方式,可以将名字限定在一个文件中,防止名字冲突。


static char buf[BUFSIZE];
static int bufp = 0;
int getch(void) { ... }
void ungetch(int c) { ... }

在上面的代码中,bufp和buf都只能在函数getch和ungetch中使用,而不能在别的文件中使用,即使在别的文件中使用了extern声明。

static修饰符除了可以修饰外部变量和函数外,也可以作用于局部变量。通过static修饰的局部变量,在函数中的存储空间将不会再函数结束后销毁,而是一直存在直到程序结束。当程序再次被调用时,仍旧可以访问这个区域的内存。

4.头文件

来自一个爱思考的人:

让我们的思绪乘着时间机器回到大学一年级。C原来老师正在讲台上讲着我们的第一个C语言程序: Hello world!

例程-1 

文件名 First.c

main()

{

     printf(“Hello world!”);

}   

看看上面的程序,没有.h文件。是的,就是没有,世界上的万物都是经历从没有到有的过程的,我们对.h的认识,我想也需要从这个步骤开始。这时确实不需要.h文件,因为这个程序太简单了,根本就不需要。那么如何才能需要呢?让我们把这个程序变得稍微复杂些,请看下面这个,

文件名 First.c

 例程-2

 printStr()

{

     printf(“Hello world!”);

}

main()

{

printStr();

}


还是没有, 那就让我们把这个程序再稍微改动一下.

 文件名 First.c

 例程-3

main()

{

printStr();

}

 printStr()

{

     printf(“Hello world!”);

}

等等,不就是改变了个顺序嘛, 但结果确是十分不同的. 让我们编译一下例程-2和例程-3,你会发现例程-3是编译不过的.这时需要我们来认识一下另一个C语言中的概念:作用域.

我们在这里只讲述与.h文件相关的顶层作用域, 顶层作用域就是从声明点延伸到源程序文本结束, 就printStr()这个函数来说,他没有单独的声明,只有定义,那么就从他定义的行开始,到first.c文件结束, 也就是说,在在例程-2的main()函数的引用点上,已经是他的作用域. 例程-3的main()函数的引用点上,还不是他的作用域,所以会编译出错. 这种情况怎么办呢? 有两种方法 ,一个就是让我们回到例程-2, 顺序对我们来说没什么, 谁先谁后都可以,只要能编译通过,程序能运行, 就让main()文件总是放到最后吧. 那就让我们来看另一个例程,让我们看看这个方法是不是在任何时候都会起作用.

文件名 First.c


   play2()
 {

 ……………….

           play1();

 ………………..

 
 }

   play1()

{
    ……………..

           play2();       
    ………………
 }

 main()

{

    play1();

}

也许大部分都会看出来了,这就是经常用到的一种算法, 函数嵌套, 那么让我们看看, play1和play2这两个函数哪个放到前面呢?

 这时就需要我们来使用第二种方法,使用声明.

文件名 First.c

例程-4

play1();

play2();

play2()

{

 ……………….

 play1();

 ………………..
}


    play1()
 {
     …………………….

 play2();

 ……………………
 }

 

main()

{

play1();

}

经历了我的半天的唠叨, 加上四个例程的说明,我们终于开始了从量变引起的质变, 这篇文章的主题.h文件快要出现了。

一个大型的软件项目,可能有几千个,上万个play, 而不只是play1,play2这么简单, 这样就可能有N个类似 play1(); play2(); 这样的声明, 这个时候就需要我们想办法把这样的play1(); play2(); 也另行管理, 而不是把他放在.c文件中, 于是.h文件出现了.

 

文件名 First.h

play1();

play2();

 

文件名 First.C

例程-4

#include “first.h”

play2()

{

 ……………….

 play1();

 ………………..

}


    play1();

{

……………………..

     play2();

  ……………………

}

 main()

{

play1();

}

各位有可能会说,这位janders大虾也太罗嗦了,上面这些我也知道, 你还讲了这么半天, 请原谅, 如果说上面的内容80%的人都知道的话,那么我保证,下面的内容,80%的人都不完全知道. 而且这也是我讲述一件事的一贯作风,我总是想把一个东西说明白,让那些刚刚接触C的人也一样明白.

上面是.h文件的最基本的功能, 那么.h文件还有什么别的功能呢? 让我来描述一下我手头的一个项目吧.

 这个项目已经做了有10年以上了,具体多少年我们部门的人谁都说不太准确,况且时间并不是最主要的,不再详查了。是一个通讯设备的前台软件, 源文件大小共 51.6M, 大大小小共1601个文件, 编译后大约10M, 其庞大可想而知, 在这里充斥着错综复杂的调用关系,如在second.c中还有一个函数需要调用first.c文件中的play1函数, 如何实现呢?

 例程-5

first.c 文件

 play1();

s

econd.c文件

 

***()

{

…………….

Play1();

……………….

}

在second.h文件内声明play1函数,怎么能调用到first.c文件中的哪个play1函数中呢? 是不是搞错了,没有搞错, 这里涉及到c语言的另一个特性:存储类说明符.

C语言的存储类说明符有以下几个, 我来列表说明一下

  

 说明符
  用    法
 
 Auto
  只在块内变量声明中被允许, 表示变量具有本地生存期.
 
 Extern
  出现在顶层或块的外部变量函数与变量声明中,表示声明的对象具有静态生存期, 连接程序知道其名字.
 
 Static
  可以放在函数与变量声明中,在函数定义时,只用于指定函数名,而不将函数导出到链接程序,在函数声明中,表示其后边会有定义声明的函数,存储类型static.在数据声明中,总是表示定义的声明不导出到连接程序.
 

无疑, 在例程-5中的second.h和first.h中,需要我们用extern标志符来修饰play1函数的声明,这样,play1()函数就可以被导出到连接程序, 也就是实现了无论在first.c文件中调用,还是在second.c文件中调用,连接程序都会很聪明的按照我们的意愿,把他连接到first.c文件中的play1函数的定义上去, 而不必我们在second.c文件中也要再写一个一样的play1函数.

但随之有一个小问题, 在例程-5中,我们并没有用extern标志符来修饰play1啊, 这里涉及到另一个问题, C语言中有默认的存储类标志符. C99中规定, 所有顶层的默认存储类标志符都是extern . 原来如此啊, 哈哈. 回想一下例程-4, 也是好险, 我们在无知的情况下, 竟然也误打误撞,用到了extern修饰符, 否则在first.h中声明的play1函数如果不被连接程序导出,那么我们在在play2()中调用他时, 是找不到其实际定义位置的 .

那么我们如何来区分哪个头文件中的声明在其对应的.c文件中有定义,而哪个又没有呢?这也许不是必须的,因为无论在哪个文件中定义,聪明的连接程序都会义无返顾的帮我们找到,并导出到连接程序, 但我觉得他确实必要的. 因为我们需要知道这个函数的具体内容是什么,有什么功能, 有了新需求后我也许要修改他,我需要在短时间内能找到这个函数的定义, 那么我来介绍一下在C语言中一个人为的规范:

在.h文件中声明的函数,如果在其对应的.c文件中有定义,那么我们在声明这个函数时,不使用extern修饰符, 如果反之,则必须显示使用extern修饰符.这样,在C语言的.h文件中,我们会看到两种类型的函数声明. 带extern的,还不带extern的, 简单明了,一个是引用外部函数,一个是自己生命并定义的函数.

最终如下:

Second.h 文件

Extern play1();


上面洋洋洒洒写了那么多都是针对函数的,而实际上.h文件却不是为函数所御用的. 打开我们项目的一个.h文件我们发现除了函数外,还有其他的东西, 那就是全局变量.

 

在大型项目中,对全局变量的使用不可避免, 比如,在first.c中需要使用一个全局变量G_test, 那么我们可以在first.h中,定义 TPYE G_test. 与对函数的使用类似, 在second.c中我们的开发人员发现他也需要使用这个全局变量, 而且要与first.c中一样的那个, 如何处理? 对,我们可以仿照函数中的处理方法, 在second.h中再次声明TPYE G_test, 根据extern的用法,以及c语言中默认的存储类型, 在两个头文件中声明的TPYE G_test,其实其存储类型都是extern, 也就是说不必我们操心, 连接程序会帮助我们处理一切. 但我们又如何区分全局变量哪个是定义声明,哪个是引用声明呢?这个比函数要复杂一些, 一般在C语言中有如下几种模型来区分:

 

1、初始化语句模型

顶层声明中,存在初始化语句是,表示这个声明是定义声明,其他声明是引用声明。C语言的所有文件之中,只能有一个定义声明。

按照这个模型,我们可以在first.h中定义如下TPYE G_test=1;那么就确定在first中的是定义声明,在其他的所有声明都是引用声明。

2、省略存储类型说明

在这个模型中,所有引用声明要显示的包括存储类extern,而每个外部变量的唯一定义声明中省略存储类说明符。

这个与我们对函数的处理方法类似,不再举例说明。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值