编译预处理与宏——C语言程序设计(十)

本文详细介绍了C语言中的全局变量、局部变量(包括静态局部变量)、初始化规则以及宏定义和预处理指令。讲解了全局变量的作用域、生命周期,以及被隐藏的情况。同时,探讨了静态局部变量的特点,以及返回指针的函数的潜在风险。此外,还深入讨论了宏的定义、使用和注意事项,包括无值宏、预定义宏、带参数的宏等。最后,文章提到了编译预处理在大型程序结构中的应用,以及头文件的作用和正确使用头文件的方法。
摘要由CSDN通过智能技术生成

C语言程序设计(十)

全局变量

  • 定义在函数外面变量全局变量
  • 全局变量具有全局的生存期和作用域
    • 它们与任何函数都无关
    • 在任何函数内部都可以使用它们
#include <stdio.h>

int  f(void);

int gAll = 12; //全局变量 gAll

int main(int argc, char const *argv[])  //里面参数可不写, 默认的就是里面的参数
{
    printf("in %s gAll=%d\n", __func__, gAll);  //__func__:字符串类型; 用于显示当前函数名,即函数名:main;
    f();
    printf("agn in %s gAll=%d\n", __func__, gAll);
    return 0;
}

int f(void)
{
	printf("in %s gAll=%d\n", __func__, gAll); //__func__:字符串类型; 用于显示当前函数名,即函数名: f;
    gAll += 2;
    printf("agn in %s gAll=%d\n", __func__, gAll);
    return gAll;
}

注: __func__ : 是两条下划线__

运行结果:

in main gAll=12
in f gAll=12
agn in f gAll=14
agn in main gAll=14

全局变量初始化

  • 没有做初始化的全局变量会得到0

    • 指针会得到NULL
  • 只能用编译时刻已知的值来初始化全局变量

    • 比如上面的int gAll = f(); 此时必须先调用f()函数才会赋值, 即得运行程序;但这样是不行的,会报错,只能在编译时刻来初始化全局变量

    • int gAll = 12;
      int g2 = gAll;
      //这样也是不行的, 不能把变量赋值给g2来进行初始化
      
    • const int gAll = 12; //gAll为常量
      int g2 = gAll;
      //这样是可以的, 但不建议这样进行初始化变量值; 它俩的位置是不能互换的, 不能先写int g2 = gAll;再声明const int gAll = 12; 因为编译器是从上到下扫描的.
      
  • 它们的初始化发生在main函数之前

    • 初始化不能写在main函数内, 因为只能在编译时刻来初始化全局变量,在main函数内,必须运行程序才执行

被隐藏的全局变量

  • 如果函数内部存在与全局变量同名的变量,则全局变量被隐藏
    • 函数内的变量, 即局部变量(或者叫 本地变量) 与全局变量同名 , 该函数内则优先使用函数内的变量
#include <stdio.h>

int a=1, b=1; //全局变量

void f1();

int main()
{
   printf("%d\n",a);
   printf("%d\n",b);
   f1();
   printf("%d\n",a);
   printf("%d\n",b);
   return 0;
}

void f1()
{
    int a = 3; //局部变量
    int c = 3; //局部变量
    printf("%d\n",a);
    printf("%d\n",c);

}

输出结果:

1
1
3
3
1
1

静态本地变量(静态局部变量)

  • 在本地变量定义时加上static修饰符就成为静态本地变量

  • 当函数离开的时候静态本地变量会继续存在并保持其值

  • 静态本地变量的初始化只会在第一次进入这个函数时做,以后进入函数时会保持上次离开时的值

  • 静态本地变量实际上是特殊的全局变量

  • 它们位于相同的内存区域

  • 静态本地变量具有全局的生存期,函数内的局部作用域

    • static在这里的意思是局部作用域(本地可访问)
#include <stdio.h>

int  f(void);

int gAll = 12; //全局变量 gAll

int main(int argc, char const *argv[])  //里面参数可不写, 默认的就是里面的参数
{
    f();
    return 0;
}

int f(void)
{
	int k = 0; //本地变量k
	static int all = 1;//静态本地变量all
	printf("&gAll=%p\n", &gAll);
	printf("&all =%p\n ",&all);
	printf("&k  =%p\n",&k);
    printf("in %s all=%d\n", __func__,all);
	all += 2;
    printf("agn in %s all=%d\n", __func__,all);
    return all;
}
&gAll=0000000000403010
&all =0000000000403014
 &k  =000000000061FDEC
in f all=1
agn in f all=3

注: gAll全局变量和all静态本地变量的内存地址是放在一块区域中的

*返回指针的函数

  • 返回本地变量的地址是危险的
  • 返回全局变量静态本地变量的地址是安全的
  • 返回在函数内 m a l l o c malloc malloc 的内存是安全的,但是容易造成问题
  • 最好的做法是返回传入的指针
#include <stdio.h>

int* f(void);
void g(void);

int main(int argc, char const *argv[])
{
    int *p = f();
    printf("*p=%d\n", *p);
    g();
    printf("*p=%d\n", *p);
    return 0;
}

int* f(void)
{
	int i = 12;
    return &i;
}

void g(void)
{
	int k = 24;
    printf("k=%d\n",k);
}

输出结果:

*p=12
k=24
*p=24

提示 :

  • 不要使用全局变量来在函数间传递参数和结果
  • 尽量避免使用全局变量
  • *使用全局变量静态本地变量函数是线程不安全的


编译预处理和宏

  • #开头的是编译预处理指令; 编译预处理在编译开始之前就已经开始处理了, 它的处理不在编译中.
  • 它们不是C语言的成分, 但是C语言程序离不开它们
  • #define 用来定义一个宏
#include <stdio.h>

//const double PI = 3.14159; //定义一个常量PI

#define PI 3.14159  //用PI来代替3.14159, 也相当于常量PI

int main(int argc, char const *argv[])
{
    //printf("%f\n". 2*3.14159*3.0);  
    printf("%f\n", 2*PI*3.0);//此时的PI就是上面的PI
 	 
    return 0;
}

#define

  • #define <名字><值>

  • 注意没有结尾的分号, 因为不是C的语句

  • 名字必须是一个单词, 值可以是各种东西

  • 在C语言的编译器开始编译之前, 编译预处理程序(cpp) 会把程序中的名字换成值

    • 完全的文本替换
  • 在Linux中可以查看编译后的文件代码 : 命令为 :gcc - save-temps


  • 宏 :是一种批量处理的称谓。计算机科学里的宏是一种抽象(Abstraction),它根据一系列预定义的规则替换一定的文本模式。

  • 如果一个宏的值中有其他的宏的名字, 也是会被替换的

  • 如果一个宏的值超过一行, 最后一行之前的行末需要加\

  • 宏的值后面出现的注释不会被当做宏的值的一部分

#include <stdio.h>


#define PI 3.14159  //用PI来代替3.14159
#define FORMAT "%f\n"
#define PI2 2*PI //pi*2
#define PRT printf("%f ", PI); \
 			printf("%f\n", PI2)   //换行需要加\ 

int main(int argc, char const *argv[])
{
    printf(FORMAT, 2*PI*3.0);//此时FORMAT就是"%f\n"
 	PRT; //此时PRI就是printf("%f ", PI); printf("%f\n", PI2)
    	 //注意后面的PRT带分号, 所以上面最后一句不用带分号,注意区别
    	 //或者直接上面都带分号, PRT后面就不用带; 但是为了区别define后面不带分号, 所以就不加分号,避免误导
    return 0;
}

输出结果:

18.849540
3.141590 6.283180

没有值的宏

  • #define _DEBUG
  • 这类宏是用于条件编译的, 后面有其他的编译预处理指令来检查这个宏是否已经被定义过了(就是如果这个宏的名字被定义过, 使用它就有值, 没有被定义过,使用它就没值)

预定义的宏

  • __LINE__ : 源代码文件当前所在的行号

  • __FILE__ : 源代码的文件名

  • __DATE__ : 编译时的日期 (月 日 年)

  • __TIME__ : 编译时的时间 (时 : 分 : 秒)

  • __STDC__ : 编辑器为ISO兼容实现时位十进制整型常量

__LINE__ 当前程序行的行号,表示为十进制整型常量
__FILE__ 当前源文件名,表示字符串型常量
__DATE__ 转换的日历日期,表示为Mmm dd yyyy 形式的字符串常量,Mmm是由asctime产生的。
__TIME__ 转换的时间,表示"hh:mm:ss"形式的字符串型常量,是有asctime产生的。(asctime貌似是指的一个函数)
__STDC__ 编辑器为ISO兼容实现时位十进制整型常量
__STDC_VERSION__ 如何实现复合C89整部1,则这个宏的值为19940SL;如果实现符合C99,则这个宏的值为199901L;否则数值是未定义
__STDC_EOBTED__ (C99)实现为宿主实现时为1,实现为独立实现为0
__STDC_IEC_559__ (C99)浮点数实现复合IBC 60559标准时定义为1,否者数值是未定义
__STDC_IEC_559_COMPLEX__ (C99)复数运算实现复合IBC 60559标准时定义为1,否者数值是未定义
__STDC_ISO_10646__ (C99)定义为长整型常量,yyyymmL表示wchar_t值复合ISO 10646标准及其指定年月的修订补充,否则数值未定义

就是系统内置的宏, 不需要我们再去定义就可以直接使用

两个_ 为一个 __

#include <stdio.h>

int main(int argc, char const *argv[])
{
	printf("%s : %d\n", __FILE__, __LINE__);
	printf("%s , %s\n", __DATE__, __TIME__);
	return 0;
}  

运行结果:

E:\C语言代码\C语言程序设计\练习10.cpp : 5
Jul 20 2020 , 22:08:35

带参数的宏

像函数的宏

  • #define cube(x) ((x)*(x)*(x))

    • cube(x) 相当于函数; ((x)*(x)*(x))就是被替换的内容
  • 宏可以带参数

#include <stdio.h>

#define cube(x) ((x)*(x)*(x))
int main(int argc, char const *argv[])
{
    printf("%d\n", cube(5)); //相当于printf("%d\n", (5)*(5)*(5));
    
    int i;
    scanf("%d", &i);
    printf("%d\n", cube(i)); //相当于printf("%d\n", (i)*(i)*(i));
        
    return 0;    
        
}

运行结果

125
3
27

错误定义的宏

  • #define RADTODEG(x)(x*57.29578)

  • #define RADTODEG(x) (x)*57.29578

#include <stdio.h>

#define RADTODEG1(x)(x*57.29578)
#define RADTODEG2(x) (x)*57.29578

int main(int argc, char const *argv[])
{
    printf("%f\n", RADTODEG1(5+2)); //相当于printf("%f\n", (5+2 *57.29578));
    
    printf("%f\n", 180/RADTODEG2(1)); //相当于printf("%f\n", 180/(1)*57.29578);
        
    return 0;    
        
}

输出结果:

119.591560
10313.240400

  • 因为宏只是进行了替换, 并不严格按照函数的规则运行

带参数的宏的原则

  • 一切都要有括号
    • 整个值要括号
    • 参数出现的每个地方都要括号
  • 正确的应该是#define RADTODEG(x) ((x)*57.29578)

带参数的宏

  • 可以带多个参数

    • #define MIN(a,b) ((a)>(b)?(b):(a)) //a>b就是a; a<b就是b
  • 也可以组合(嵌套)使用其他宏


  • 在大型程序中的代码中使用非常普遍
  • 可以非常复杂, 如"产生"函数
    • 在#和##这两个运算符的帮助下
  • 存在中西方文化差异
  • 部分宏会被inline函数替代

这种带参数的宏,代替函数的效率会高很多, 但是代码会很大, 这是牺牲空间来换取时间的代价. 这样的宏可以做的很复杂,宏不会做类型检查


注意 : 宏后面最好不要使用分号, 它不是C的语句, 避免使用错误

例如 :

#define PRETTY_PRINT(msg) printf(msg);   //加了分号, 就意味着后面的替换也有分号, 如果后面使用这个宏时,再加分号就出错.

if(n<10)
    PRETTY_PRINT("n is less than 10");  //后面的分号就多余了
else
    PRETTY_PRINT("n is at least 10");  //后面的分号就多余了

其他编译预处理指令

  • 条件编译
  • error

大程序结构

多个.c文件

  • main()里面的代码太长了适合分成几个函数

  • 一个源代码文件太长了适合分成几个文件

  • 两个独立的源代码文件不能编译形成可执行文件

  • 由于大程序代码比较多, 一个.c文件如果放下整个项目的代码, 行数将会十分庞大, 修改代码或调试代码就十分不方便, 查找也很繁琐; 这时需要分而治之, 将一个.c文件分成多个.c文件, 这样就能减少单个文件的代码量

  • 但分成多个文件需要将他们关联起来才能组成一个大程序

建项目或工程

Dev C++codeblock建工程或项目; 这样可以将多个.c文件链接起来

在Dev C++中建项目

  • 在Dev C++中新建一个项目

请添加图片描述

  • 选终端应用程序 Console Application

请添加图片描述

  • 选择语言类型, 设定项目名称

请添加图片描述

  • 选择保存的文件路径

请添加图片描述

  • ok 建立了一个项目

请添加图片描述

举个栗子:

单个.c文件

#include <stdio.h>

int max(int a, int b);

int main(int argc, char *argv[]) {
	int a=5;
	int b=6;
	printf("%d\n", max(a,b));
	 
	return 0;
}

int max(int a , int b)
{
	return a>b?a:b;
}

分成两个.c文件

建项目

请添加图片描述

main.c

#include <stdio.h>


int main(int argc, char *argv[]) {
	int a=5;
	int b=6;
	printf("%d\n", max(a,b));
	 
	return 0;
}

max.c

int max(int a , int b)
{
	return a>b?a:b;
}

结果都一样

在Codeblocks中建项目

  • 在codeblocks中建项目

请添加图片描述

  • 选择Console application

请添加图片描述

  • 下一步

请添加图片描述

  • 选C

请添加图片描述

  • 设置工程标题, (工程即项目, 名字不一样,意思一样)
  • 设置好工程的文件路径

请添加图片描述

  • 都勾选上调试配置
  • 方便后面的调试工作

请添加图片描述

  • ok 建项目成功

请添加图片描述

  • 往项目里添加文件

请添加图片描述

  • 选源文件source

请添加图片描述

  • 选择刚刚建工程文件的路径
  • 并且勾上构建目标

请添加图片描述

  • 最终添加的结果

请添加图片描述

  • 在codeblock中是构建并运行 , 就会执行代码程序

请添加图片描述


项目

  • 在Dev C++中新建⼀一个项目,然后把几个源代码文件加入进去
  • 对于项目,Dev C++的编译会把一个项目中所有的源代码文件都编译后,链接起来
  • 有的IDE(集成开发环境) 有分开的编译和构建两个按钮,前者是对单个源代码文件编译,后者是对整个项目做链接

编译单元

  • 一个.c文件是一个编译单元
  • 编译器每次编译只处理一个编译单元

头文件

函数原型

  • 如果不给出函数原型,编译器会猜测你所调用的函数的所有参数都是int,返回类型也是int
  • 编译器在编译的时候只看当前的一个编译单元,它不会去看同一个项目中的其他编译单元以找出那个函数的原型
  • 如果你的函数并非如此,程序链接的时候不会出错
  • 但是执行的时候就不对了
  • 所以需要在调用函数的地方给出函数的原型,以告诉编译器那个函数究竟长什么样

header(头文件)

  • 把函数原型放到一个头文件(以.h结尾)中,在需要调用这个函数的源代码文件(.c文件)中#include这个头文件,就能让编译器在编译的时候知道函数的原型

  • 就是把函数声明放到头文件中, 来说明函数的信息

#include

  • #include是一个编译预处理指令,和宏一样,在编译之前就处理了
  • 它把那个文件的全部文本内容原封不动地插入到它所在的地方
  • 所以也不是一定要在.c文件的最前面#include

#include的小细节

是用“”还是<>

例如:

#include <stdio.h>
#include "max.h"

//一般系统的头文件用<> ; 自己定义的头文件用"" 
//<>表示是到系统指定的路径下去查找, 而""是到当前代码文件的路径下找
//因此不要用错, 否者编译器就会找不到该文件
  • #include有两种形式来指出要插入的文件

    • “”要求编译器首先在当前目录(.c文件所在的目录)寻找这个文件,如果没有,到编译器指定的目录去找
    • <>让编译器只在系统指定的目录去找
  • 编译器自己知道自己的标准库的头文件在哪里

  • 环境变量和编译器命令行参数也可以指定寻找头文件的目录

#include的误区

  • #include不是用来引入库的, 原因是它只是导入头文件, 而头文件里只是一些函数的声明, 并没有函数的源代码
  • 例如: stdio.h里只有printf的原型,printf的源代码在另外的地方,某个.lib(在Windows系统中的拓展名)或.a(在Unix系统中的拓展名)中
  • 现在的C语言编译器默认会引入所有的标准库
  • #include <stdio.h>只是为了让编译器知道printf函数的原型,保证你调⽤用时给出的参数值是正确的类型

头文件的作用

  • 在使用和定义这个函数的地方都应该#include这个头文件
  • 说白了头文件.h只是把它里面的内容原封不动地插入到.c文件中, 它不能进行编译的, 因为它编译也不会产生代码, 它的作用只是告诉编译器,它声明的函数原型
  • 一般的做法就是任何.c都有对应的同名的.h,把所有对外公开函数的原型全局变量的声明都放进去
  • 头文件一定要放在.c文件的开头的原因就是在编译.c文件之前,必须先将.h文件插入到.c文件中,才会再开始编译。如果把头文件放在程序中间或者末尾的某个地方就一定会报错, 因为编译器一旦发现前面的没有导入头文件, 就会开始编译, 编译过程中发现头文件的东西没有插入到源代码文件中,就会发生报错。一旦开始编译就不会再去插入头文件的内容了。

例如这样子

请添加图片描述


不对外公开的函数

  • 关键字 static : 静态;

  • 在函数前面加上static这个关键字就使得它成为只能在所在的编译单元(即一个.c文件)中被使用的函数

  • 在全局变量前面加上static关键字就使得它成为只能在所在的编译单元(即一个.c文件)中被使用的全局变量

声明

变量的声明

  • int i; 是变量的定义
  • extern int i; 是变量的声明, 将别处(.c文件)的变量用到这个.c文件中

举个栗子

max.h文件

double max(double a, double b);
extern double gAll; //声明gAll的类型; 相当于把gAll变成全局变量

main.c文件

#include <stdio.h>
#include "max.h"

int main(void) {
	double a=5;
	double b=6;
	printf("%f\n", max(a,b));
	printf("%f\n", max(a,gAll)); //已经声明过gAll, 必须声明才能使用
	return 0;
}

max.c文件

double gAll = 11;
double max(double a , double b)
{
	return a>b?a:b;
}

声明和定义

  • C语言中声明和定义是不一样的;

    • 普通的写下一个变量是变量的定义; 在变量前面加extern就是变量的声明
    • 声明不能初始化, 定义才能初始化
  • 声明是不产生代码的东西(即编译后不产生代码)

    • 函数原型

    • 变量声明

    • 结构声明

    • 宏声明

    • 枚举声明

    • 类型声明

    • inline函数

  • 定义是产生代码的东西 (即编译后会产生代码)

    • 函数
    • 全局变量

放在头文件里的声明

  • 只有声明可以被放在头文件中
    • 是规则不是法律
    • 尽管定义放在头文件里不会报错, 但很容易会出错, 十分不提倡这种做法
    • 所以系统自带的头文件不会有定义的, 即没有函数的源码, 只是声明
  • 否则会造成一个项目中多个编译单元里有重名的实体
    • 某些编译器允许几个编译单元中存在同名的函数,或者用weak修饰符来强调这种存在

重复声明

  • 同一个编译单元里,同名的结构不能被重复声明
  • 如果你的头文件里有结构的声明,很难这个头文件不会在一个编译单元里被#include多次
  • 所以需要“标准头文件结构”

举个栗子(错误的例子):

main.h文件

#include "max.h" 

max.h文件

int max(int a, int b);
extern int gAll;
 

struct Node{
	int value;
	char* name;
}; 

main.c文件

#include <stdio.h>
#include "main.h"
#include "max.h"
 


int main(int argc, char *argv[]) {
	int a=5;
	int b=6;
	printf("%d\n", max(a,b));
	printf("%d\n", max(a,gAll)); 
	return 0;
}

max.c文件

int max(int a , int b)
{
	return a>b?a:b;
}

编译运行结果…

请添加图片描述

  • 结构体被重复定义, 很多时候都会忽略, 当然这个程序比较小, 找出来很容易, 但是当大程序拥有几千上万条代码时,就十分困难了.

  • 有什么办法避免重复定义呢?这时引入了一个标准头文件结构

标准头文件结构

  • 运用条件编译保证这个头文件在一个编译单元中只会被#include一次
  • 在Visual Stdio中可以用#pragma once也能起到相同的作用,但是不是所有的编译器都支持

因此上述代码的修改为:

max.h文件改为

#ifndef __MAX_H__   //编译预处理文件 ; 意思是如果没有定义__MAX_H__这个宏, 就进入该判断语句下面 
#define __MAX_H__   //定义一个宏 , 命名方式为了方便, 命名和max.h类似, 全部大写, 用_代表. 
					//名字可以自定义 , 尽量起一个避免重复命名的宏的名字 
//下面都是定义宏的内容
int max(int a, int b);
extern int gAll;
 

struct Node{
	int value;
	char* name;
}; 

#endif  //如果定义了就结束该判断语句; 这样避免重复定义宏 

编译运行正常 ! ! !

请添加图片描述

格式为:

#ifndef __宏的名字__
#define __宏的名字__

<定义的内容>

#endif  
  • #号开头的都是编译预处理指令

  • 宏的命名可以为其他方式, __命名__这样命名不会和其他的宏冲突, 而且也不会和系统自定义的宏冲突.

  • 类似于if条件判断语句, 如果命名重复, 就会再次include到编译单元中, 就跳过该插入.

*前向声明

#ifndef __LIST_HEAD__
#define __LIST_HEAD__

struct Node;

typedef struct _list{
	struct Node* head;
    struct Node* tail;
} List;

#endif
  • 因为在这个地方不需要具体知道Node是怎样的,所以可以用struct Node来告诉编译器Node是一个结构
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值