C_函数 | 预处理指令(4/10)





博文所截笔记均来自:http://c.biancheng.net C语言中文网,并非用于营利,如有侵权,联系删除。





一、函数


函数不能嵌套定义

return 表达式  或者   return (表达式)

     return 语句可以有多个,可以出现在函数体的任意位置,但是每次调用函数只能有一个 return 语句被执行,所以只有一个返回值(少数的编程语言支持多个返回值,例如Go语言)。

    函数一旦遇到 return 语句就立即返回,后面的所有语句都不会被执行到了。从这个角度看,return 语句还有强制结束函数执行的作用。


分类:有参函数、无参函数、有返回值函数、无返回值函数


1、函数声明

① C语言代码由上到下依次执行,原则上函数定义要出现在函数调用之前,否则就会报错。但在实际开发中,经常会在函数定义之前使用它们,这个时候就需要提前声明。

② 所谓声明(Declaration),就是告诉编译器我要使用这个函数,你现在没有找到它的定义不要紧,请不要报错,稍后我会把定义补上。

③ 有了函数声明,函数定义就可以出现在任何地方了,甚至是其他文件、静态链接库、动态链接库等。



2、作用域——全局变量、局部变量

        形参变量要等到函数被调用时才分配内存,调用结束后立即释放内存。这说明形参变量的作用域非常有限,只能在函数内部使用,离开该函数就无效了。所谓作用域(Scope),就是变量的有效范围。

① C语言中所有的变量都有自己的作用域。

② 决定变量作用域的是变量的定义位置。

③ 分辨全局、局部很简单:

    定义在全部函数外部的变量为全局变量;

    定义在局部函数外部的变量为局部变量。

④ 每个C语言程序都包含了多个作用域,不同的作用域中可以出现同名的变量,C语言会按照从小到大的顺序、一层一层地去父级作用域中查找变量,如果在最顶层的全局作用域中还未找到这个变量,那么就会报错。








二、预处理指令


1、预处理的概念

        在实际开发中,有时候在编译之前还需要对源文件进行简单的处理。例如,我们希望自己的程序在 Windows 和 Linux 下都能够运行,那么就要在 Windows 下使用 VS 编译一遍,然后在 Linux 下使用 GCC 编译一遍。但是现在有个问题,程序中要实现的某个功能在 VS 和 GCC 下使用的函数不同(假设 VS 下使用 a(),GCC 下使用 b()),VS 下的函数在 GCC 下不能编译通过,GCC 下的函数在 VS 下也不能编译通过,怎么办呢?

        这就需要在编译之前先对源文件进行处理:如果检测到是 VS,就保留 a() 删除 b();如果检测到是 GCC,就保留 b() 删除 a()。

        这些在编译之前对源文件进行简单加工的过程,就称为预处理(即预先处理、提前处理)。


① 使用库函数之前,应该用#include引入对应的头文件。这种以#号开头的命令称为预处理命令。

预处理主要是处理以#开头的命令,例如#include <stdio.h>等。预处理命令要放在所有函数之外,而且一般都放在源文件的前面。
预处理是C语言的一个重要功能,由预处理程序完成。当对一个源文件进行编译时,系统将自动调用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。
编译器会将预处理的结果保存到和源文件同名的.i文件中,例如 main.c 的预处理结果在 main.i 中。和.c一样,.i也是文本文件,可以用编辑器打开直接查看内容。


举个栗子:

下面我们举个例子来说明预处理命令的实际用途。假如现在要开发一个C语言程序,让它暂停 5 秒以后再输出内容,并且要求跨平台,在 Windows 和 Linux 下都能运行,怎么办呢?

这个程序的难点在于,不同平台下的暂停函数和头文件都不一样:

  • Windows 平台下的暂停函数的原型是void Sleep(DWORD dwMilliseconds)(注意 S 是大写的),参数的单位是“毫秒”,位于 <windows.h> 头文件。

  • Linux 平台下暂停函数的原型是unsigned int sleep (unsigned int seconds),参数的单位是“秒”,位于 <unistd.h> 头文件。


不同的平台下必须调用不同的函数,并引入不同的头文件,否则就会导致编译错误,因为 Windows 平台下没有 sleep() 函数,也没有 <unistd.h> 头文件,反之亦然。这就要求我们在编译之前,也就是预处理阶段来解决这个问题。请看下面的代码:

#include <stdio.h>

//不同的平台下引入不同的头文件
#if _WIN32  //识别windows平台
#include <windows.h>
#elif __linux__  //识别linux平台
#include <unistd.h>
#endif

int main() {
    //不同的平台下调用不同的函数
    #if _WIN32  //识别windows平台
    Sleep(5000);
    #elif __linux__  //识别linux平台
    sleep(5);
    #endif

    puts("http://c.biancheng.net/");

    return 0;
}

#if、#elif、#endif 就是预处理命令,它们都是在编译之前由预处理程序来执行的。这里我们不讨论细节,只从整体上来理解。


对于 Windows 平台,预处理以后的代码变成:

#include <stdio.h>
#include <windows.h>

int main() {
    Sleep(5000);
    puts("http://c.biancheng.net/");

    return 0;
}

对于 Linux 平台,预处理以后的代码变成:

#include <stdio.h>
#include <unistd.h>

int main() {
    sleep(5);
    puts("http://c.biancheng.net/");

    return 0;
}

        在不同的平台下,编译之前(预处理之后)的源代码都是不一样的。这就是预处理阶段的工作,它把代码当成普通文本,根据设定的条件进行一些简单的文本替换,将替换以后的结果再交给编译器处理。



关于预处理命令

#include <stdio.h>
int main(){
    #if _WIN32
        system("color 0c");
        printf("http://c.biancheng.net\n");
    #elif __linux__
        printf("\033[22;31mhttp://c.biancheng.net\n\033[22;30m");
    #else
        printf("http://c.biancheng.net\n");
    #endif

    return 0;
}

#if、#elif、#else 和 #endif 都是预处理命令,整段代码的意思是:如果宏 _WIN32 的值为真,就保留第 4、5 行代码,删除第 7、9 行代码;如果宏 __linux__ 的值为真,就保留第 7 行代码;如果所有的宏都为假,就保留第 9 行代码。

② 这些操作都是在预处理阶段完成的,多余的代码以及所有的宏都不会参与编译,不仅保证了代码的正确性,还减小了编译后文件的体积。

③ 这种能够根据不同情况编译不同代码、产生不同目标文件的机制,称为条件编译。条件编译是预处理程序的功能,不是编译器的功能。

④ 条件编译需要多个预处理命令的支持



2、#include  —— 文件包含命令

① #include叫做文件包含命令,用来引入对应的头文件(.h文件)。#include 也是C语言预处理命令的一种。

②  #include 的处理过程很简单,就是将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件,这与复制粘贴的效果相同。

③ 用法:

#include 的用法有两种,如下所示:

#include <stdHeader.h>
#include "myHeader.h"

使用尖括号< >和双引号" "的区别在于头文件的搜索路径不同:

  • 使用尖括号< >,编译器会到系统路径下查找头文件;

  • 而使用双引号" ",编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找(功能更为强大)。

④ 我个人的习惯是使用尖括号来引入标准头文件,使用双引号来引入自定义头文件(自己编写的头文件),这样一眼就能看出头文件的区别。

⑤ 关于 #include 用法的注意事项:

  • 一个 #include 命令只能包含一个头文件,多个头文件需要多个 #include 命令。

  • 同一个头文件可以被多次引入,多次引入的效果和一次引入的效果相同,因为头文件在代码层面有防止重复引入的机制,具体细节我们将在《防止C语言头文件被重复包含》一文中深入探讨。

  • 文件包含允许嵌套,也就是说在一个被包含的文件中又可以包含另一个文件。


「在头文件中定义定义函数和全局变量」这种认知是原则性的错误!不管是标准头文件,还是自定义头文件,都只能包含变量和函数的声明,不能包含定义,否则在多次引入时会引起重复定义错误。



3、#define —— 宏定义命令


 #define 叫做宏定义命令它也是C语言预处理命令的一种。所谓宏定义,就是用一个标识符来表示一个字符串,如果在后面的代码中出现了该标识符,那么就全部替换成指定的字符串。


#define N 100就是宏定义,N为宏名,100是宏的内容(宏所表示的字符串)。在预处理阶段,对程序中所有出现的“宏名”,预处理器都会用宏定义中的字符串去代换,这称为“宏替换”或“宏展开”。


 宏定义的一般形式为:

#define  宏名  字符串

#表示这是一条预处理命令,所有的预处理命令都以 # 开头。宏名是标识符的一种,命名规则和变量相同。字符串可以是数字、表达式、if 语句、函数等。宏定义不是说明或语句,在行末不必加分号,如加上分号则连分号也一起替换。

这里所说的字符串是一般意义上的字符序列,不要和C语言中的字符串等同,它不需要双引号。

程序中反复使用的表达式就可以使用宏定义,例如:

#define M (n*n+3*n)

习惯上宏名用大写字母表示,以便于与变量区别。但也允许用小写字母。


需要注意的是,在宏定义中表达式(n*n+3*n)两边的括号不能少,否则在宏展开以后可能会产生歧义——经典面试题


 宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。如要终止其作用域可使用#undef命令。

 代码中的宏名如果被引号包围,那么预处理程序不对其作宏代替。

 宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名,在宏展开时由预处理程序层层代换。例如:

#define PI 3.1415926
#define S PI*y*y    /* PI是已定义的宏名*/


⑦ 

可用宏定义表示数据类型,使书写方便。例如:

#define UINT unsigned int

在程序中可用 UINT 作变量说明:

UINT a, b;

应注意用宏定义表示数据类型和用 typedef 定义数据说明符的区别。宏定义只是简单的字符串替换,由预处理器来处理;而 typedef 是在编译阶段由编译器处理的,它并不是简单的字符串替换,而给原有的数据类型起一个新的名字,将它作为一种新的数据类型。

宏定义虽然也可表示数据类型, 但毕竟只是简单的字符串替换。在使用时要格外小心,以避出错。


⑧ 允许宏带有参数。在宏定义中的参数称为“形式参数”,在宏调用中的参数称为“实际参数”,这点和函数有些类似。

#include <stdio.h>
#define MAX(a,b) (a>b) ? a : b
int main(){
    int x , y, max;
    printf("input two numbers: ");
    scanf("%d %d", &x, &y);
    max = MAX(x, y);
    printf("max=%d\n", max);
    return 0;
}

同样地,为了避免引用时的错误,对于带参宏定义不仅要在参数两侧加括号,还应该在整个字符串外加括号。

#include <stdio.h>
#define SQ(y) ((y)*(y))
int main(){
    int a,sq;
    printf("input a number: ");
    scanf("%d", &a);
    sq = 200 / SQ(a+1);
    printf("sq=%d\n", sq);
    return 0;
}

happysneaker.com



宏定义命令也能定义参数,那么和参数的区别呢?

① 带参数的宏和函数很相似,但有本质上的区别:宏展开仅仅是字符串的替换,不会对表达式进行计算;

② 宏在编译之前就被处理掉了,它没有机会参与编译,也不会占用内存(所以叫做预处理);

③ 而函数是一段可以重复使用的代码,会被编译,会给它分配内存,每次调用函数,就是执行这块内存中的代码。





Web安全技术分享
请先登录后发表评论
  • 最新评论
  • 总共0条评论