[华为云在线课程][C语言基础][四][函数与程序结构][学习笔记]

举报
John2021 发表于 2022/10/12 12:01:02 2022/10/12
【摘要】 函数可以把大的任务分解成若干个较小的任务。一个设计得当的函数可以把程序中不需要了解的具体细节隐藏起来,使程序结构清晰,降低维护难度。 1.函数的基本知识函数的定义形式如下:返回值类型 函数名字(参数声明表){ 声明和语句}函数定义中的各构成部分都可以省略。最简单的函数:dummy() {},该函数不执行任何操作也不返回任何值。这种不执行任何操作的函数可以在程序开发期间用以保留位置(等待...

函数可以把大的任务分解成若干个较小的任务。一个设计得当的函数可以把程序中不需要了解的具体细节隐藏起来,使程序结构清晰,降低维护难度。

1.函数的基本知识

函数的定义形式如下:

返回值类型 函数名字(参数声明表)
{
    声明和语句
}

函数定义中的各构成部分都可以省略。最简单的函数:dummy() {},该函数不执行任何操作也不返回任何值。这种不执行任何操作的函数可以在程序开发期间用以保留位置(等待以后填充代码)。如果函数定义中省略了返回值类型,则默认为int类型。

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

被调用函数通过return语句向调用者返回值,return语句的后面可以跟任何表达式:return 表达式;。在必要时,表达式将被转换为函数的返回值类型。表达式两边通常加一对圆括号(可选)。

练习:

/*
编写函数strindex(s,t),返回字符串t在s中最右边出现的位置。
如果s中不包含t,则返回-1。
*/
#include <stdio.h>

int strindex(char s[], char t)
{
    int i;
    int count = -1;
    for (i = 0; s[i] != '\0'; i++)
    {
        if (s[i] == t)
        {
            count = i;
        }
    }
    return count;
}

typedef struct TEST
{
    char *data;
    char testchar;
    int expexted;
} TEST;

int main()
{
    TEST test[] =
        {
            {"Hello world", 'o', 7},
            {"This string is littered with iiiis", 'i', 32},
            {"No 'see' letters in here", 'c', -1}};
    size_t numtests = sizeof test / sizeof test[0];
    size_t i;

    char ch = 'o';
    int pos;

    for (i = 0; i < numtests; i++)
    {
        pos = strindex(test[i].data, test[i].testchar);
        printf("%s , %c.\n", test[i].data, test[i].testchar);
        printf("result: %d\n", test[i].expexted);
        printf("%sorrect (%d).\n", pos == test[i].expexted ? "C" : "Inc", pos);
        if (pos != -1)
        {
            printf("找到 %c\n", test[i].data[pos]);
        }
    }
    return 0;
}

2.外部变量

C语言程序可以看成由一系列的外部对象构成,这些外部对象可能是变量或函数。形容词external与internal相对的,internal用于描述定义在函数内部的函数参数及变量。外部变量定义在函数之外,因此可以在许多函数中使用。由于C语言不允许在一个函数中定义其他函数。

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

因为外部变量可以在全局范围内访问,就为函数之间的数据交换提供了一种可以代替函数参数与返回值的方式。任何函数都可以通过名字访问一个外部变量,当然这个名字需要通过某种方式进行声明。

外部变量相比内部变量具有更大的作用域和更长的生存期。自动变量只能在函数内部使用,从其所在的函数被调用时变量开始存在,在函数退出时变量也将消失。外部变量的值在一次函数调用到下一次函数调用之间保持不变。如果两个函数必须共享某些数据,而这两个函数互补调用对方,这时最方便的方式是把共享数据定义为外部变量,而不是作为函数参数传递。

3.作用域规则

名字的作用域指的是程序中可以使用该名字的部分。对于在函数开头声明的自动变量来说,其作用域是声明该变量名的函数。不同函数中声明的具有相同名字的各个局部变量之间没有任何关系。函数的参数也是如此,实际上可以将它看作是局部变量。

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

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

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

// 如果这两条语句放在所有函数的外部,那么这两条语句定义外部变量sp和val,并分配存储单元,同时这两条语句还可以作为该源文件中其余部分的声明。
int sp;
double val[MAXVAL];

// 下面这两条语句,为源文件的其余部分声明了一个int类型的外部变量sp以及一个double数据类型的外部变量val(该数组长度在其他地方确定),但这两个声明并没有建立变量或为它们分配存储单元。
extern int sp;
extern double val[];

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

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

// 假定函数push与pop定义在一个文件中,而变量val与sp在另一个文件中定义并被初始化,则需要通过以下操作将函数和变量绑定在一起
// file1
extern int sp;
extern double val[];
void push(double f) {...}
double pop(void) {...}
// file2
int sp=0;
double val[MAXVAL];

由于文件file1中的extern声明不仅放在函数定义的外面,而且还放在它们前面,因此适用于该文件中的所有函数。

4.头文件

对头文件的定义要对下面两个因素进行了折衷:

一方面期望每个文件只能访问它完成任务所需的信息;另一方面是现实中维护较多的头文件比较困难。

对于某些中等规模的程序,最好只用一个头文件存放程序中各部分共享的对象。较大的程序需要使用更多的头文件,需要精心组织。

5.静态变量

某些变量,仅供其所在的源文件中的函数使用,其他函数不能访问。用static声明限定外部变量与函数,可以将其后声明的对象的作用域限定为被编译源文件的剩余部分。通过static限定外部对象,可以达到隐藏外部对象的目的。

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

外部的static生命通常多用于变量,也可用于声明函数。通常情况下,函数名字是全局访问的。但如果把函数声明为static类型,则该函数名除了对该函数声明所在的文件可见外,其它文件都无法访问。

static可用于声明内部变量。static类型的内部变量同自动变量一样,是某个特定函数的局部变量,只能在该函数中使用,但它与自动变量不同的是,不管所在函数是否被调用会一直存在。static类型的内部变量是一种只能在某个特定函数中使用但一直占据存储空间的变量。

6.寄存器变量

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

//register声明如下:
register int x;
register char c;

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

f(register unsigned m,register long n)
{
    register int i;
    ......
}

无论寄存器变量实际上是不是存放在寄存器中,地址都是不能访问的。在不同的机器中,对寄存器变量的数目和类型的具体限制也是不同的。

7.程序块结构

C语言不允许在函数中定义函数。但是,在函数中可以以程序块结构的形式定义变量。变量的声明(包括初始化)除了可以紧跟在函数开始的花括号之后,还可以紧跟在任何其他标识复合语句开始的左花括号之后。以这种方式声明的变量可以隐藏程序块之外与之同名的变量,它们没有任何关系。

//例如
if(n>0){
    int i; //声明一个新的i
    for (i=0;i<n;i++){
        ...
    }
}

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

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

int x;
int y;
f(double x){
    double y;
}

函数f内的变量x引用的是函数的参数,类型为double;在函数f之外,x是int类型的外部变量。

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

8.初始化

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

定义变量时,可以在变量名后紧跟一个等号和一个表达式来初始化变量。

对于外部变量与静态变量来说,初始化表达式必须是常量表达式,且只初始化一次(从概念上讲是在程序开始执行前进行初始化)。对于自动变量与寄存器变量,则在每次进入函数或程序块时都将被初始化。

对于自动变量与寄存器变量来说,初始化表达式可以不是常量表达式:表达式中可以包含任意在此表达式之前已经定义的值,包括函数调用。

字符数组的初始化比较特殊:可以用一个字符串来代替用花括号括起来并用括号分隔的初始化表达式序列。例如:char pattern[] = "ould "; 等价于 char pattern[] = {'o','u','l','d'};,这种情况下,数组的长度是5(4个字符加上一个字符串结束符 ‘\0’ )。

9.递归

C语言中的函数可以递归调用,即函数可以直接或间接调用自身。一个较好能说明递归的例子就是快速排序。快速排序算法是C.A.R.Hoare于1962年发明的。对于一个给定的数组,从中选择一个元素,以该元素为界将其余元素划分为两个子集,一个子集中的所有元素都小于该元素,另一个子集中的所有元素都大于或等于该元素。对这样两个子集递归执行这一过程,当某个子集中的元素数小于2时,这个子集就不需要再次排序,终止递归。

/* swap: interchange v[i] and v[j] */
void swap(int v[],int i,int j)
{
    int temp;
    temp=v[i];
    v[i]=v[j];
    v[j]=temp;
}
/* sort v[left]...v[right] into increasing order */
void qsort(int v[],int left,int right)
{
    int i,last;
    void swap(int v[],int i,int j);
    /* do nothing if array contains */
    if(left>=right)
    {
        /* fewer than two elements */
        return;
    }
    /* move partition elem */
    swap(v,left,(left+right)/2);
    /* to v[0]  */
    last=left;
    /* partition */
    for(i=left+1;i<=right;i++)
    {
        if(v[i]<v[left])
        {
            swap(v,++last,i);
        }
    }
    /* restore partition elem */
    swap(v,left,last);
    qsort(v,left,last-1);
    qsort(v,last+1,right);
}

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

10.C预处理器

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

10.1.文件包含

文件包含指令(即#include指令)使得处理大量的#define指令以及声明更加方便。#include "文件名" / #include <文件名>的行都将被替换为由文件名指定的文件的内容。如果文件名用引号引起来,则在源文件所在位置查找;如果在该位置没有找到文件,或者如果文件名是用括号括起来的,则将根据相应的规则查找该文件。

10.2.宏替换

宏定义形式如下:#define 名字 替换文本。后续所有出现名字记号的地方都将被替换为替换文本。通常#define指令占一行,替换文本是#define指令行尾部的所有剩余部分内容,也可以把一个较长的宏定义分成若干行,这是需要在行末尾加上一个反斜杠符 \ 。

10.3.条件包含

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

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。