The C Programming Language——函数与程序结构

概述

C语言程序一般都由许多小的函数组成,而不是由少量较大的函数组成。

一个程序可以保存在一个或者多个源文件中。各个文件可以单独编译,并可以与库中已编译过的函数一起加载

ANSI 标准进一步明确了名字的作用域规则,特别要求每个外部对象只能定义一次初始化的适用范围也更加广泛了,自动数组与结构都可以进行初始化

C语言预处理器的功能也得到了增强


1、函数的基本知识

//该程序将输入中包含特定“模式”或字符串的各行打印出来
#include <stdio.h>
#define MAXLINE 1000

int getline(char line[], int max);
int strindex(char source[], char searchfor[]);

char pattern[] = "ould";    \\这里使用字符串字面值表示模式,这不是一中通用的机制

int main(int argc, char ** argv)
{
    char line[MAXLINE];
    int found = 0;

    while(getline(line, MAXLINE) > 0)
    {
        if(strindex(line, pattern) >= 0)
        {
            printf("%s", line);
            found++;
        }
    }
    return found;
}

//将输入的行保存在line中,并返回该行的长度
int getline(char line[], int max)
{
    int c, i;

    i = 0;
    while(--max > 0 &&(c = getchar()) != EOF && c != '\n')
        line[i++] = c;
    if(c == '\n')
        line[i++] = c;
    line[i] = '\0';
    return i;
}

//返回searchfor在source中的位置,若未找到则返回-1
int strindex(char source[], char searchfor[])
{
    int i, j, k;

    for(i = 0; source[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;
}

不执行任何操作的函数有时很有用,它可以在程序开发期间用以保留位置(留待以后填充代码)。如果函数定义中省略了返回值类型,则默认为int(强烈建议使用最标准的写法)

程序可以看成是变量定义函数定义的集合。函数之间的通信可以通过参数、函数返回值以及外部变量进行。函数在源文件中出现的次序可以是任意的。只要保证每一个函数不被分离到多个文件中,源程序就可以分成多个文件

r e t u r n 表达式:return后可以跟任何表达式。在必要时,表达式将被转换(自动转换)为函数的返回值类型。当return后面没有表达式时,函数将不向调用者返回值(只返回控制权)。在任何情况下,如果函数没有成功地返回一个值,则它的“值”肯定是无用的

这里所说的加载即链接


2、返回非整型值的函数

//将字符串转换为相应的双精度浮点数
#include <stdio.h>

#define MAXLINE 100

int main(int argc, char ** argv)
{
    double sum, atof(char []);
    char line[MAXLINE];
    int getline(char line[], int max);

    sum = 0;
    while(getline(line, MAXLINE) > 0)
        printf("\t %g\n", sum += atof(line));

    return 0;
}

double atof(char s[])
{
    double val, power;
    int  i, sign;

    for(i = 0; isspace(s[i]); i++)
        ;
    sign = (s[i] == '-') ? -1 : 1;
    if(s[i] == '+' || s[i] == '-')
        i++;
    for(val = 0.0; isdigit(s[i]); i++)
        val = 10.0 * val + (s[i] - '0');
    if(s[i] == '.')
        i++;
    for(power = 1.0; isdigit(s[i]); i++)
    {
        val = 10.0 * val + (s[i] - '0');
        power *= 10.0;
    }
    return sign * val / power;
}

函数的声明与定义必须一致。如果函数与调用的它的函数放在同一源文件中,并且类型不一致,编译器就会检测到该错误(没有顺序要求)。但是,如果函数是单独编译的,这种不匹配的错误就无法检测出来(前提是没有使用函数原型

如果没有函数原型,则函数将在第一次出现的表达式中被隐式声明——如果先前没有声明过的一个名字出现在某个表达式中,并且其后紧跟一个左圆括号,那么上下文就会认为该名字是一个函数名字,该函数的返回值将被假定为int类型。无论表达式中的圆括号中是否含有实参,上下文并不会对其参数作任何假设,并且关闭所有的参数检查

如果函数带有参数,则要声明它们;如果没有参数,则使用void进行声明(这里有兼容性的问题)。坚持使用函数原型,使用最标准的写法

强制类型转换可以防止有关的警告信息


3、外部变量

C语言程序可以看成由一系列的外部对象构成,这些外部对象可能是变量或函数

外部变量定义在函数之外,因此可以在许多函数中使用。由于C语言不允许在一个函数中定义其他函数,因此函数本身是“外部的”

默认情况下,外部变量与函数具有下列性质:通过同一个名字对外部对象的所有引用(即使这种引用来自于单独编译的不同函数),实际上都是引用同一个对象(标准中把这一性质称为外部链接

还可以定义只能在某一源文件中使用的外部变量与函数(使用static关键字来进行隐藏)

因为外部变量可以在全局范围内访问,所以任何函数都可以通过名字访问一个外部变量,当然这个名字需要通过某种方式进行声明(如何合适的声明是很重要的)

外部变量的用途还表现在它们与内部变量相比具有更大的作用域和更长的生存期

逆波兰表示法中缀表示法

在逆波兰表示法中,所有运算符都跟在操作数的后面。该表示法不需要圆括号,只要知道每个运算符需要几个操作数就不会引起歧义

//逆波兰计算器
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>

#define MAXOP 100   //操作数或运算符的最大长度
#define NUMBER '0'  //标识找到一个操作数

int getop(char []);
int getch(void);
void ungetch(int);
void push(double);
double pop(void);

int main(int argc, char ** argv)
{
    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: unknow command %s\n", s);
                break;
        }
    }

    return 0;
}

#define MAXVAL 100  //栈的最大深度

int sp = 0; //下一个空闲栈位置
double val[MAXVAL]; //值栈

//压栈函数
void push(double f)
{
    if(sp < MAXVAL)
        val[sp++] = f;
    else
        printf("error: stack full, cant't push %g\n", f);
}

//弹出并返回栈顶的值
double pop(void)
{
    if(sp > 0)
        return val[--sp];
    else
    {
        printf("error: stack empty\n");
        return 0.0;
    }
}

//获取下一个运算符或操作数
int getop(char s[])
{
    int i, c;

    while((s[0] = c = getch()) == ' ' || c == '\t')
        ;
    s[1] = '\0';
    if(!isdigit(c) && c != '.')
        return c;
    i = 0;
    if(isdigit(c))
        while(isdigit(s[++i] = c = getch()))
            ;
    if(c == '.')
        while(isdigit(s[++i] = c = getch()))
            ;
    s[i] = '\0';
    if(c != EOF)
        ungetch(c);
    return NUMBER;
}

#define BUFSIZE 100

char buf[BUFSIZE];  //用于反取字符操作的缓冲区
int bufp = 0;   //buf中的下一个空闲位置

//取一个字符
int getch(void)
{
    return (bufp > 0) ? buf[--bufp] : getchar();
}

//反取一个字符
void ungetch(int c)
{
    if(bufp >= BUFSIZE)
        printf("ungetch: too many characters\n");
    else
        buf[bufp++] = c;
}

程序中经常会出现这样的情况:程序不能确定它已经读入的输入是否足够,除非超前多读入一些输入


4、作用域规则

构成C语言程序的函数与外部变量可以分开进行编译。一个程序可以存放在几个文件中,原先已编译过的函数可以从库中进行加载

  • 如何进行声明才能确保变量在编译时被正确声明?
  • 如何安排声明的位置才能确保程序在加载时各部分能正确连接?
  • 如何组织程序中的声明才能确保只有一份副本?
  • 如何初始化外部变量 ?

名字的作用域指的是程序中可以使用该名字的部分

函数参数就是该函数的自动变量(局部)

外部变量或函数的作用域从声明(定义也有声明的作用)它的地方开始,到其所在的文件的末尾结束

如果要在外部变量的定义之前使用该变量,或者外部变量的定义与变量的使用不在同一个源文件中,则必须在相应的变量声明中强制性地使用关键字extern

将外部变量的声明与定义严格区分开来很重要。变量声明用于说明变量的属性(主要是变量的类型),而变量定义除此之外还将引起存储器的分配(外部变量的定义有声明的作用)

在一个源程序的所有源文件中,一个外部变量只能在某个文件中定义一次,而其他文件可以通过extern声明来访问它(定义外部变量的源文件中也可以包含对该外部变量的extern声明,函数也可以这样)。外部变量的定义中必须指定数组的长度,但extern声明则不一定要指定数组的长度

外部变量的初始化只能出现在其定义中

如果要在同一个文件中先使用、后定义外部变量,则需要在文件的开始处加extern声明

extern关键字只能用于声明外部对象


5、头文件

之所以要将源程序分割成多个文件,主要是考虑在实际的程序中,它们分别来自于单独编译的库

我们一般将一些共享的部分放在头文件中(一般来说是一些函数的声明宏定义外部变量的定义或声明等)

  • 我们期望每个文件只能访问它完成任务所需的信息
  • 维护较少的头文件个数

头文件只有在被包含进入源文件中,才会被编译


6、静态变量

用static声明限定外部变量与函数,可以将其后声明的对象的作用域限定为被编译源文件的剩余部分(即通过static限定外部对象,可以达到隐藏外部对象的目的)

要将对象指定为静态存储,可以在正常的对象声明之前加上关键字static作为前缀

外部的static声明通常多用于变量(也可用于声明函数)

通常情况下,函数名字是全局可访问的,对整个程序的各个部分而言都可见。但是,如果把函数声明为static类型,则该函数名除了对该函数声明所在的文件可见外(对包含该声明所在头文件的源文件也可见),其他文件都无法访问(隐藏)

static类型的内部变量是一种只能在某个特定函数中使用但一直占据存储空间的变量

static关键字可以用在声明处,也可以用在定义出(并没有完全搞清楚)。好像对于变量一般是用在定义处(进行了初始化);对于函数一般是用在声明处


7、寄存器变量

register声明告诉编译器,它所声明的变量在程序中使用频率较高。其思想是,将register变量放在机器的寄存器中,这样可以使程序更小、执行速度更快。但编译器可以忽略此选项

register声明只适用于自动变量以及函数的形式参数

实际使用时,底层硬件环境的实际情况对寄存器变量的使用会有一些限制。每个函数中只有很少的变量可以保存在寄存器中,且只允许某些类型的变量。但是,过量的寄存器声明并没有什么害处,这是因为编译器可以忽略过量的或不支持的寄存器变量声明。另外,无论寄存器变量实际上是不是存放在寄存器中,它的地址都是不能访问的。在不同的机器中,对寄存器变量的数目和类型的具体限制也是不同的


8、程序块结构

在函数中可以以程序块结构的形式定义变量。变量的声明(包括初始化)除了可以紧跟在函数开始的花括号之后,还可以紧跟在任何其他标识复合语句开始的左花括号之后。以这种方式声明的变量可以隐藏程序块外与之同名的变量,它们之间没有任何关系,并在与左花括号匹配的右花括号出现之前一直存在

每次进入程序块时,在程序块内声明以及初始化的自动变量都将被初始化。静态变量只在第一次进入程序块时被初始化一次

自动变量(包括形式参数)也可以隐藏同名的外部变量与函数(确定可以隐藏函数?????)

在一个好的程序设计风格中,应该避免出现变量名隐藏外部作用域中相同名字的情况,否则,很可能引起混乱和错误


9、初始化

初始化指的是在声明变量的同时对其进行赋值操作

不进行显式初始化的情况下,外部变量和静态变量都将被初始化为0,而自动变量和寄存器变量的初值则没有定义(即初值为无用的信息)

对于外部变量与静态变量来说,初始化表达式必须是常量表达式,且只初始化一次(从概念上讲是在程序开始执行前进行初始化)。

对于自动变量与寄存器变量,则在每次进入函数或程序块时都将被初始化,它们的初始化表达式可以不是常量表达式:表达式可以包含任意在此表达式之前已经定义的值,包括函数调用

考虑到变量声明中的初始化表达式容易被人忽略,且距使用的位置较远,我们一般使用显式的赋值语句(完成初始化)

数组的初始化可以在声明的后面紧跟一个初始化表达式列表,初始化表达式列表用花括号括起来,,各初始化表达式之间通过逗号分隔。当省略数组的长度时,编译器将把花括号中初始化表达式的个数作为数组的长度

如果初始化表达式的个数比数组元素数,则对外部变量、静态变量和自动变量来说,没有初始化表达式的元素将被初始化为0(任何类型的数组都是这样)。如果初始化表达式的个数比数组元素数多,则是错误的。不能一次将一个初始化表达式指定给多个数组元素,也不能跳过前面的数组元素而直接初始化后面的数组元素

字符数组的初始化比较特殊:可以用一个字符串来代替用花括号括起来并用逗号分隔的初始化表达式序列(注意’\0’)


10、递归

C语言中的函数可以递归调用,即函数可以直接或间接调用自身

递归的例子

//将数字作为字符串打印的一种解决方法
#include <stdio.h>

void printd(int n)
{
    if(n < 0)
    {
        putchar('-');
        n = -n;
    }
    if(n / 10)
        printd(n / 10);
    putchar(n % 10 + '0');
}

快速排序的一种实现

//递增排列
void qsort(int v[], int left, int right)
{
    int i, last;
    void swap(int v[], int i, int j); //函数声明,但是不建议使用这种,建议使用函数原型

    if(left >= right)
        return;
    swap(v, left, (left + right) / 2);
    last = left;
    for(i = left + 1; i <= right; i++)
        if(v[i] < v[left])
            swap(v, ++last, i);
    swap(v, left, last);
    qsort(v, left, last - 1);
    qsort(v, last + 1, right);
}

void swap(int v[], int i, int j)
{
    int temp;

    temp = v[i];
    v[i] = v[j];
    v[j] = temp;
}

递归并不节省存储器的开销,因为递归调用过程中必须在某个地方维护一个存储处理值的栈。递归的执行速度并不快,但递归代码比较紧凑,并且比相应的非递归代码更易于编写与理解

树是一种递归定义的数据结构

如果需要在函数中显式声明外部变量,需要使用extern关键字;如果要在函数中声明函数则不需要使用extern关键字。如果是跨文件(不跨文件也是可以的,但这就失去了extern的意义了)声明外部对象,则需要用extern关键字

加了extern关键字的函数前置声明应该还是函数原型


11、C预处理器

预处理器是编译过程中单独执行的第一个步骤。两个最常用的预处理指令是:#include指令(用于在编译期间把指定文件的内容包含进当前文件中)和#define指令(用任意字符序列替代一个标记)

# include 文件名 的行都将被替换为由文件名指定的文件的内容。如果文件名用引号引起来,则在源文件所在位置查找该文件;如果在该位置没有找到文件,或者如果文件名是用尖括号括起来的,则将根据相应的规则查找该文件,这个规则同具体的实现有关(一般来说是环境变量)。被包含的文件本身也可包含#include指令

在大的程序中,#include指令是将所有声明捆绑在一起的较好的方法。它保证所有的源文件都具有相同的定义与变量声明,这样可以避免出现一些不必要的错误。很自然,如果某个包含文件的内容发生了变化,那么所有依赖于该包含文件的源文件都必须重新编译

宏替换中的替换文本可以是任意的。通常情况下,#define指令占一行,替换文本是#define指令行尾部的所有剩余部分内容,但也可以把一个较长的宏定义分成若干行,这时需要在待续的行末尾加上一个反斜杠符\

#define指令定义的名字的作用域从其定义点开始,到被编译的源文件的末尾处结束。

宏定义中也可以使用前面出现的宏定义。替换只对记号进行,对括在引号中的字符串不起作用

宏定义也可以带参数(宏调用

要适当使用圆括号以保证计算次序的正确性

getchar与putchar函数在实际中常常被定义为宏,这样可以避免处理字符串时调用函数所需的运行时开销<ctype.h>头文件中定义的函数也常常是通过宏实现的

可以通过#undef指令取消名字的宏定义

宏定义中#和##的使用(书上所说太拗口)

预处理属于编译过程

可以使用条件语句对预处理本身进行控制,这种条件语句的值是在预处理执行的过程中进行计算。这种方式为在编译过程中根据计算所得的条件值选择性地包含不同代码提供了一种手段

#if语句对其中的常量整型表达式(有一些细节性的要求)进行求值,若该表达式的值不等于0,则包含其后的各行,直到遇到#endif、#elif、或#else语句为止。

#if语句中可以使用表达式defined(名字),该表达式的值遵循下列规则:当名字已经定义时,其值为1;否则,其值为0

可以使用这种方法来避免多次重复包含同一文件

如果多个头文件能够一致地使用这种方式,那么,每个头文件都可以将它所依赖的任何头文件包含进来,用户不必考虑和处理头文件之间的各种依赖关系

C语言专门定义了两个预处理语句#ifdef与#ifndef,它们用来测试某个名字是否已经定义

猜你喜欢

转载自blog.csdn.net/weixin_39918693/article/details/80586780