主页 分类 关于

C语 函数及编译预处理

C语学习笔记

函数的定义和调用

函数

函数定义的传统形式:


储存类型 数据类型 函数名 ( 形参表 )
形参类型说明语句序列
{
函数体
}

函数定义的现代形式:


储存类型 数据类型 函数名 ( 类型 参数 1, 类型 参数 2, ... )
{
函数体
}

关于函数定义的几点说明:

  • 一个源程序文件由一个或多个函数组成, 其中必有一个函数名为 main 的函数, 程序的执行是从 main 函数开始, 屌用其它函数后流程回到main() 函数, 在 main () 函数中结束整个程序的运行
  • 一个 C 程序由一个或多个源程序文件组成
  • 函数类型指出该函数返回值的类型, 有 int, float, char 等, 若函数无法返回值, 函数可以定义为空类型 void , 默认 int
  • 函数名符合标识的定义, 一般提倡函数名与函数内容有一定关系, 以增强程序的可读性
  • 函数的形参表可有可无, 无形参表的函数称无参函数, 但函数名后的 () 不能省略, 在调用无参函数时, 主调函数并不将数据传送给被调函数, 一般用来执行指定的操作
  • 有参函数可由一个或多个形参组成, 多个参数之间用逗号隔开, 在调用该类函数时, 主调函数可以将数据传递给被调用函数使用

函数说明与调用

  • 函数的使用与变量的使用类似, 使用前先定义其类型然后才能使用, 主调函数调用被调函数时, 在调用前应先对被调函数进行说明, 先说明后调用

函数说明的一般格式:


储存类型 数据类型 函数名 ( );
{
函数体
}

当函数类型为 int 型, 或被调函数定义在主调函数之前时, 可以省略被调函数的说明

编好一个函数后, 要由主调函数来调用才能发挥作用, 一个函数 ( 主调函数 ) 在执行过程中去执行另一个函数 ( 被调函数 ) ,称为函数调用, 当被调函数执行完毕后, 返回到主调函数调用处之后继续执行, 称为函数调用返回

C 语言中调用函数的一般格式:


函数名 ( 实参表 );

函数调用按其在程序中出现的位置来分, 可有如下三种调用方式

  1. 函数表达式

    • 概念: 函数出现在一个表达式中, 这种表达式称为函数表达式 这种表达式需要函数返回一个确定的值
  2. 函数参数

    • 概念: 把函数调用作为一个函数的实在参数
  3. 函数语句

    • 概念: 把函数调用作为一个语句, 不要求函数带回值, 只要求函数完成一定的操作

函数的返回值

  • 函数的返回值是通过函数中的 return 语句获得的, return 语句将被调函数中的一个确定值带回主调函数中去, 一个函数中可以有一个以上的 return 语句, 执行到哪一个 return 语句, 哪一个语句就会起作用
  • 函数的数据类型即为函数的返回值的类型, 若在定义函数时, 没有进行函数类型说明, 一律自动按 int 处理, 如果函数值的类型和return 语句中表达式值的类型不一致, 则以函数类型为准, 对于数据型数据, 可以自动进行类型转换, 即函数类型决定返回值的类型
  • 如果被调函数中没有 return 语句, 函数带回一个不确定的值, 为了明确表示不带回值, 可以用 void 说明无类型 ( 或称 “ 空类型 “ ), 为了减少程序出错, 保证正确调用, 凡不要求带回函数值的函数, 一般都定义 void 类型

变量的作用域

局部变量

  • 在一个函数内部定义的变量称为局部变量, 它只在本函数范围内有效, 也就是说只有在本函数内才能使用它们, 在此函数以外是不能使用这些变量的

全面变量

  • 全面变量和局部变量: 程序的编译单位是源程序文件, 一个源文件可以包含一个或若干个函数, 在函数内部定义的变量称为局部变量, 在函数外部定义的变量称为全局变量, 又称为外部变量, 全局变量可以为该文件中其它函数所共用, 它的有效范围为从定义变量的位置开始到本源文件
  • 全局变量在程序的全部执行过程中占用储存单元, 而不是在需要时才分配储存单元, 过多地使用全局变量, 会降低程序的清晰度和通用型, 因为人物往往难以清楚地判断出每个瞬时各个全局变量的值, 同时函数在执行时要依赖其所在的全局变量, 如果将一个函数移到另一个文件中, 还要将有关的全局变量及其值一起移过去, 因此建议非必要时不要使用全局变量

变量的储存类型

静态储存方式和动态储存方式

  • 从变量的作用域范围来分, 变量可以分为全局变量和局部变量, 从变量值存在的时间来分, 可以分为静态储存方式和动态储存方式
  • 静态存储方式是指在程序运行期间分配固定的存储空间, 而动态储存方式是指在运行期间根据需要进行动态分配储存空间
  • 在内存中供用户使用的存储空间是由程序区, 静态存储区和动态存储区三部分组成, 数据分别存放在静态存储区和动态存储区中, 全局变量存放在静态存储区中, 在程序开始时就给全局变量分配存储区, 程序执行完时才释放存储空间, 在程序执行过程中占用固定储存单元, 而不是动态分配和释放储存空间
  • 动态存储区主要存放函数的形式参数, 自动变量和函数使用时的现场保护和返回地址等, 对于这些数据, 在函数调用开始分配动态储存空间, 函数结束时释放这些空间, 如果一程序中两次调用同一个函数, 每次分配给函数中局部变量的储存地址可以是不相同

变量的储存类型

  • 一个变量和函数都存放两种属性: 一种是数据类型属性, 它说明变量占有储存空间的大小, 如读者已熟悉的整形, 实型, 字符型, 另一种是变量的储存类型, 主要有 auto ( 自动 ) 型, reginter ( 寄存器 ) 型, static ( 静态 ) 型和 extern ( 外部 ) 型四种
  1. auto ( 自动 ) 变量

    • auto 变量只用于定义局部变量, 储存在内存中的动态存储区

自动变量的定义形式:


auto 数据类型 变量名表;

  1. static ( 静态 ) 变量

    • static 型既可以定义全局变量, 又可以定义局部变量, 在静态储存区分配存储单元, 在整个程序运行期间, 静态变量自始至终占用被分配的储存空间

静态变量的定义形式:


static 数据类型 变量名表;

  • 注意

    • 静态局部变量是在编译时赋初值的, 即赋值一次, 在程序运行时它已有初值, 以后每次调用函数时不再重新赋值, 而只引用上次函数调用结果时的值
    • 若在定义静态局部变量时没有赋初值, 编译时自动赋初值 0 ( 对数值变量 ) 或空字符 ( 对字符变量 )
    • 定义全局变量时, 全局变量的有效范围是它所在的源文件, 其它源文件不能使用

  1. register ( 寄存器 ) 变量

    • 一般情况下, 变量的值是存放在内存中的, 如果某些变量要频繁使用, 同时为了提高变量的存取时间, 则将这些变量存放在寄存器中

寄存器变量的定义形式:


register 数据类型 变量名表;

  • 注意

    • 一个计算机系统中寄存器的数量是有限的, 因此不能定义太多的寄存器变量
    • 只有局部自动变量和形式参数可以定义为寄存器变量, 全部变量和静态储存变量不能定义为寄存器变量
    • 寄存器变量不能使用 & 运算符

  1. extern ( 外部 ) 变量

    • extern 变量称为外部变量, 就是全局变量, 是对同一类变量的不同提法, 全局变量是从作用域提出的, 外部变量是从其储存方式提出的, 表示它的生存期, 外部变量的定义必须在所有函数之外, 且只能定义一次
    • 若 extern 型变量的定义在后, 使用在前, 或者引用其它文件的 extern 型变量, 这时必须用 extern 对该变量进行外部说明

外部变量的定义形式:


extern 数据类型 变量名表;

函数间的数据传递

传值方式

  1. 实参向形参传递数据是单向的, 且按顺序一一对应, 实参可以是变量, 常量, 函数调用和表达式, 但必须有确定值, 在调用时将实参的值赋给形参的变量

  2. 在被定义的函数中, 必须指定形参类型, 调用时要求实参与形参类型一致

  3. 形参, 实参各占独立的储存空间, 形参在函数被使用时, 系统为其动态分配临时储存空间, 函数返回时, 释放储存空间, 因此实参与形参可以同名, 也可以不同民, 而且形参数值发生变化时, 实参值不变

  4. 形参属于局部变量

地址复制方式

  • 地址复制方式又叫传址方式, 它是把地址常量 ( 而不是数据 ) 传送给被调用函数的形参, 采用地址传递方式, 可以很好地解决数组中大量数据在函数间传递的问题, 在这种方式中, 一般用数组名或指针作为形参接收实参数组首地址, 这样使得形参与实参数组 ( 或指针 ) 首地址相同, 所以在被调函数中, 如果修改了数组元素值, 调用函数后实参数组元素值也发生相应变化, 可见, 用地址传递可实现被调函数返回多值给主调函数

利用参数返回结果

  • 当函数被调用时, 其处理结果可以返回值的形式传递给调用函数, 如果要求返回多个结果值时, 还可以利用参数返回处理结果
  • 当使用地址复制方式传递参数时, 被调用函数可以改变调用函数种的数据, 利用参数返回处理结果就是根据该特征来实现的

利用函数返回值传递数据

  • 从被调函数传递数据给主函数, 一般采用函数的返回值来实现, 返回值是被调函数执行返回主调函数的一个值, 它通过 return 语句来实现

利用全局变量传递数据

  • 利用全局变量进行函数间的数据传递, 不但简单, 而且程序的运行效率高, 但是, 如果函数间使用过多的全局变量, 就增加了函数间的联系, 降低了函数的独立性

函数嵌套调用

  • C 语言规定不允许在定义一个函数中再定义一个函数, 也就是一个函数内不能包含另一函数, 虽然 C 语言不能嵌套定义函数, 但可以嵌套调用函数

函数递归调用

  • 在调用一个函数的过程中直接或间接地调用函数本事, 称为函数的递归调用, C 语言的特点之一就在于允许函数的递归调用
  • 递归函数要避免死循环, 在编写递归调用语句的前面写上终止递归的条件

递归的流程:


if ( 条件 ) 递归调用
else ...

编写递归函数必须要清楚两个问题

  • 递归程序算法, 即如何实现其递归
  • 递归调用的结束条件

内部函数和外部函数

内部函数

  • 如果一个函数只能被其所在的源文件中的函数调用, 称此函数为内部函数, 内部函数的储存类型为 static
  • 用 static 定义的函数又称为静态函数, 该函数只能被 file.c 中的 main 函数调用, 其它程序文件是不能调用的

内部函数的格式:


static 类型标识符 函数名 ( 形式参数表 )

外部函数

  • 若将函数的储存类型定义为 extern 型, 则此函数能被其它源文件的函数调用, 称此函数为外部函数

外部函数的格式:


extern 类型标识符 函数名 ( 形式参数表 )

编译预处理

  • 在前面各章中已多次使用过以 “ # “ 开头的预处理命令, 如文件包含命令 #include, 宏定义命令 #define 等, 在源程序中这些命令都放在函数之外, 而且一般都放在源文件的前面, 在源程序进行编译时的第一遍扫描, 首先对这些以 “ # “ 开头的命令进行预先处理, 称为编译预处理, 处理完毕后自动进入对源程序的编译
  • 在 C 语言程序中使用预处理功能, 可以改善程序的设计环境, 提高程序的通用性, 可读性, 可修改性, 可调试性, 可移植性和方便性, C语中的预处理命令有宏定义, 文件包含和条件编译三类, 在此重点介绍宏定义和文件包含两类预处理命令

不带参数的宏定义

  • 宏定义是指用一个标识符 ( 名字 )来代替一个文本串

不带参数的宏定义的格式:


#define 标识符 文本串 // 标识符称为 宏名

  • 作用: 将宏名的值定义为指定的文本串, 即在本程序后面的命令行中, 凡出现宏名的地方, 在预处理时都用指定的文本串替换, 在预处理时将宏名替换成指定的文本串的过程称为 “宏展开”, 这里#define就是宏定义命令
  • 说明

    1. 宏名为了与变量名区别, 一般用大写字母来表示

    2. 宏定义是用宏名代替一个文本串, 文本串无论是数字符还是字母字符都只作简单的替换, 不作正确性检查

    3. 宏定义不是 C 语句, 不必在行尾加分号, 如果加分号, 预处理时会将分号当作字符一同代入

    4. #define命令出现在程序中函数的外面, 宏名的有效范围是: 从定义位置开始到本文件结束通常 #define 命令写在文件的开头

    5. 可以用 #undef 命令终止宏定义的作用域

    6. 在宏定义时, 可以引用已定义的宏名

    7. 程序中用双引号括起来的字符串内的字符, 即使与宏名相同, 也不进行替换

    8. 宏名与变量名的含义不同, 只做字符替换, 不分配存储单元, 因此其值也不能改变

带参数的宏定义

  • 宏定义还可以像函数一样带参数
  • 带参数的宏在引用时必须给出实参, 在宏替换时由左到右逐个字符进行替换, 遇到与 形参相同的字符时, 用实参替换, 直到文本串中的所有字符被替换完

带参数的宏定义的格式:


#define 宏名 ( 参数表 ) 文本串

  • 作用: 定义一个带参数的宏
  • 说明

    • 宏名与括号之间不能有空格
    • 宏调用时, 实参的个数必须与形参的个数相同
    • 带参数的宏替换, 也只是将文本串中的形参字符用实参替换, 不做语法检查
    • 规范的宏替换格式可以减少不必要的错误发生, 对于宏定义不仅应在参数两侧加括号, 也应在整个字符串外加括号
  • 宏调用和函数调用有相似之处, 但二者有本质的不同

    • 在函数调用中, 实参和形参都要定义类型, 而且类型要一致, 而宏调用时, 参数不存在类型问题, 宏名无类型, 它的参数也无类型, 只是一个符号代表, 展开时代入指定的字符即可
    • 使用宏次数多时, 宏展开后的源程序就会变长, 因为每展开一次都会使程序增长, 而函数调用不使源程序增长
    • 宏替换不占运行时间, 而函数调用则占运行时间( 给形参分配存储单元, 保留现场, 值传送, 返回 ) 利用好宏可以使程序简化

文件包含

  • 所谓包含, 是指在一个源文件中, 用 #include 命令将另一个源文件的全部内容包含进来, 即装入 #include 命令所处的位置上, 使其成为一个程序
  • 说明

    • 包含命令中的文件名可以用双引号括起来, 也可以用尖括号括起来
* 一个 #include 命令只能指定一个被包含文件, 若有多个文件要包含, 则需用多个  #include 命令

+ 文件包含允许嵌套, 即在一个被包含的文件中可以包含另一个文件
  • 二者的区别

    • 用尖括号时, 系统直接到存放 C 库函数头文件所在的目录中查找要包含的文件, 这种方式称为标准方式
    • 用双引号时, 系统先在用户当前目录中寻找要包含的文件, 若找不到, 再按标准方式查找
    • 一般情况下, 如果要包含库函数, 用尖括号可节省时间, 如被包含文件是用户自己编写的文件, 则用双引号









作者: 我叫史迪奇
本文来自于: https://sdq3.link/C-Pretreatment.html博客内容遵循 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 协议