C语言程序设计学习笔记:P12-程序结构


一、全局变量

1.1 全局变量

全局变量

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

我们写段代码来看看全局变量的使用。我们在这段代码中定义了一个全局变量,同时在一个函数中修改它的值,最后看看这个全局变量的值是否改变。

#include <stdio.h>

int f(void);

int gAll = 12;

int main(int argc, char const *argv[])
{
    
    
	printf("in %s gAll=%d\n", __func__, gAll);
	f();
	printf("agn in %s gAll=%d\n", __func__, gAll);
	return 0;
}

int f(void)
{
    
    
	printf("in %s gAll=%d\n", __func__, gAll);
	gAll += 2;
	printf("agn in %s gAll=%d\n", __func__, gAll);
	return gAll;
}

运行,可以看出全局变量的值成功被修改。
在这里插入图片描述


全局变量初始化

• 没有做初始化的全局变量会得到0值
  • 指针会得到NULL值
• 只能用编译时刻已知的值来初始化全局变量
• 它们的初始化发生在main函数之前

注意
1、不要将两个全局变量放在一起。比如这里虽然给了全局变量gAll一个值,但是将其赋值给另外一个全局变量ge仍然不行。
在这里插入图片描述
报错情况如下:
在这里插入图片描述
2、如果函数内部存在与全局变量同名的变量,则全局变量被隐藏。比如我在函数f中定义了一个gAll,并让其加2,最后可以发现全局变量的值没有改变。
在这里插入图片描述

1.2 静态本地变量

静态本地变量

• 在本地变量定义时加上static修饰符就成为静态本地变量
• 当函数离开的时候,静态本地变量会继续存在并保持其值
• 静态本地变量的初始化只会在第一次进入这个函数时做,以后进入函数时会保持上次离开时的值

我们测试一下静态本地变量的使用。我在一个函数中定义了一个静态本地变量,并将其加2。我连续三次调用这个函数,看会发生什么。

#include <stdio.h>

int f(void);

int gAll = 12;

int main(int argc, char const *argv[])
{
    
    
	f();
	f();
	f();
	return 0;
}

int f(void)
{
    
    
	static int all = 1;
	printf("in %s all=%d\n", __func__, all);
	all += 2;
	printf("agn in %s all=%d\n", __func__, all);
	return all;
}

运行,可以发现all的值并没有调用一次函数就初始化一次,而是会保持上次在函数中的值。
在这里插入图片描述


静态变量的本质:

• 静态本地变量实际上是特殊的全局变量
• 它们位于相同的内存区域
• 静态本地变量具有全局的生存期,函数内的局部作用域
  • static在这里的意思是局部作⽤用域(本地可访问)

静态本地变量实际上是特殊的全局变量,我们来测试一下,看看静态本地变量和全局变量在内存中的位置。

#include <stdio.h>

int f(void);

int gAll = 12;

int main(int argc, char const *argv[])
{
    
    
	f();
	return 0;
}
int f(void)
{
    
    
	int k = 0;
	static int all = 1;
	printf("&gAll=%p\n", &gAll);
	printf("&all =%p\n", &all);
	printf("&k   =%p\n", &k);

	return all;
}

运行,可以发现它们是相邻的。
在这里插入图片描述

1.3 全局变量贴士

返回指针的函数

• 返回本地变量的地址是危险的
• 返回全局变量或静态本地变量的地址是安全的
• 返回在函数内malloc的内存是安全的,但是容易造成问题
• 最好的做法是返回传入的指针

关于全局变量的一些Tips:

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

二、编译预处理和宏

2.1 宏定义

编译预处理指令

• #开头的是编译预处理指令
• 它们不是C语言的成分,但是C语言程序离不开它们
• #define用来定义一个宏

我们来看一下如何定义一个宏,代码如下所示。C99之前的版本没有const,所以只能使用宏来替代。由于它是编译预处理指令,因此在预处理阶段会将所有的PI替换成3.14159。

#include <stdio.h>

#define PI 3.14159

int main(int argc, char const *argv[])
{
    
    
	printf("%f\n", 2*PI*3.14);
	return 0;

	return 0;
}

运行,可以发现PI替换了3.14159。

在这里插入图片描述


我们对宏的用法进行详细的测试:

#include <stdio.h>

#define PI 3.14159
#define FORMAT "%f\n"
#define PI2 2*PI   //宏可以包含另外一个宏
#define PRT printf("%f ", PI); \
			printf("%f\n", PI2) //一行写不下需要使用'\'换行接着写

int main(int argc, char const *argv[])
{
    
    
	printf(FORMAT, PI2*3.0);
	PRT;
	return 0;

	return 0;
}

运行,结果如下:
在这里插入图片描述

对于define,有以下总结

• #define <名字> <值>
• 注意没有结尾的分号,因为不是C的语句
• 名字必须是一个单词,值可以是各种东西
• 在C语言的编译器开始编译之前,编译预处理程序(cpp)会把程序中的名字换成值
  • 完全的文本替换
• gcc --save-temps 保存编译过程中的临时文件

对于宏,有以下总结

• 如果一个宏的值中有其他的宏的名字,也是会被替换的
• 如果一个宏的值超过一行,最后一行之前的行末需要加\
• 宏的值后面出现的注释不会被当作宏的值的一部分

没有值的宏

• #define _DEBUG
• 这类宏是用于条件编译的,后面有其他的编译预处理指令来检查这个宏是否已经被定义过了

预定义的宏

__LINE__   源代码文件当前所在行的行号
__FILE__   源代码文件的文件名
__DATE__   编译时候的日期
__TIME__   编译时候的时间
__STDC__

我们对预定义的宏进行测试

#include <stdio.h>

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

}

运行,结果如下。
在这里插入图片描述

2.2 带参数的宏

带参数的宏

#define cube(x) ((x)*(x)*(x))
宏可以带参数

我们写段代码来看看带参数的宏的详细使用情况。

#include <stdio.h>
#define cube(x) ((x)*(x)*(x))

int main(int argc, char const *argv[])
{
    
    
	int i = 5;
	printf("%d\n", cube(i));
	return 0;

}

运行,可以看出结果正确。
在这里插入图片描述


错误的写法

错误定义的宏
#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[])
{
    
    
	//正常情况下应该是7*57.29578,结果为400左右
	printf("%f\n", RADTODEG1(5+2));
	//正常情况下应该是180/57.29578,结果为3
	printf("%f\n", 180/RADTODEG1(1));
	return 0;

}

运行一下,可以看出结果明显错误。
在这里插入图片描述
在运算时,实际上的执行的操作为:

printf("%f\n", (5+2 * 57.29578));
printf("%f\n", 180/(1) * 57.29578);

因此,带参数的宏的原则为:

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

带多个参数的宏

• 带参数的宏可以有多个参数
• #define MIN(a,b) ((a)>(b)?(b):(a))
• 也可以组合(嵌套)使用其他宏

带参数的宏的使用情况

• 在大型程序的代码中使用非常普遍
  • 占据空间大,但是执行效率高
• 可以非常复杂,如“产生”函数,
  • 在#和##这两个运算符的帮助下
• 存在中西方文化差异
  • 中国程序员使用较少,国外程序员使用的多。
• 部分宏会被inline函数替代
  • inline会做参数类型检查

小测验

1、假设宏定义:

#define DOUBLE(x) 2*xDOUBLE(1+2)的值是

答案:4

2、假设宏定义如下:

#define TOUPPER(c) ('a'<=(c)&&(c)<='z'?(c)-'a'+'A':(c))
设s是一个足够大的字符数组,i是int型变量,则以下代码段的输出是:
strcpy(s, "abcd");
i = 0;
putchar(TOUPPER(s[++i]));

答案:D

三、大程序结构

3.1 多个源代码文件

在编写程序时,有时候会碰到以下情况:

• main()里的代码太长了
• 一个源代码文件太长了

这时我们就会将一些函数剥离出来,写在一个新的源代码文件中(注意:这些源代码文件必须在同一个工程下)。我们写段代码看看,代码中我将max函数放在了另外一个源文件max.c中,同时在main函数所在的helloword.c文件中调用max函数。

#include <stdio.h>
int main(int argc, char const *argv[])
{
    
    
	printf("%d", max(10,12));
	return 0;
}

我们运行一下,结果正确运行的过程如下图所示:
在这里插入图片描述

3.2 头文件

在上面我们写的代码中,调用max函数返回的结果正确,然而这是有问题的。

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

如果我将max函数的参数类型和返回类型都改成double,然后在主函数中去调用它并向它传递两个int类型的参数,看看结果会怎样。
main函数代码

#include <stdio.h>

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

max函数代码

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

运行,可以发现结果明显错误。
在这里插入图片描述

那我们怎样保证在main函数中对max函数的使用和max函数的定义是一致的呢?这时我们需要一个中间媒介:头文件。把函数原型放到一个头文件(以.h结尾)中,在需要调用这个函数的源代码文件(.c文件)中#include这个头文件,就能让编译器在编译的时候知道函数的原型。
main函数代码

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

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

max.h文件代码

double max(double a, double b);

max.c文件代码

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

我们运行并看看程序的结构,可以看出结果正确。
在这里插入图片描述


关于#include,有以下注意的地方:

• #include是一个编译预处理指令,和宏一样,在编译之前就处理了
• 它把那个文件的全部文本内容原封不动地插入到它所在的地方
  • 所以也不是一定要在.c文件的最前面#include
• #include有两种形式来指出要插入的文件
  • “”要求编译器首先在当前目录(.c文件所在的目录)寻找这个文件,如果没有,到编译器指定的目录去找
  • <>让编译器只在指定的目录去找
• 编译器自己知道自己的标准库的头文件在哪里
• 环境变量和编译器命令行参数也可以指定寻找头文件的目录

关于include,有一些误区,需要注意:

• #include不是用来引入库的
• stdio.h里只有printf的原型,printf的代码在另外的地方,某个.lib(Windows)或.a(Unix)中
• 现在的C语言编译器默认会引入所有的标准库
• #include <stdio.h>只是为了让编译器知道printf函数的原型,保证你调用时给出的参数值是正确的类型

使用头文件时,需要注意:

• 在使用和定义这个函数的地方都应该#include这个头文件
• 一般的做法就是任何.c都有对应的同名的.h,把所有对外公开的函数的原型和全局变量的声明都放进去

不对外公开的函数

假如你有一些放在.c中的函数不想别人使用,但是这个.c文件中的其它函数能用,你的做法如下:
• 在函数前面加上static就使得它成为只能在所在的编译单元中被使用的函数
• 在全局变量前面加上static就使得它成为只能在所在的编译单元中被使用的全局变量

3.3 声明

如果在某个源代码文件中有一个全局变量,现在要在另外一个源代码文件中使用,我们该怎么做呢?举个例子,现在我在max.c中有个全局变量gAll,现在想在main函数中使用它,这时我们需要在max.h中加上一些东西。
max.h文件

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

max.c文件

int gAll = 12;

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

main函数

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

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

运行,可以看到结果正确,成功读到了值为12的这个gAll。
在这里插入图片描述


变量的声明和定义

• int i;是变量的定义
• extern int i;是变量的声明
• 声明是不产生代码的东西
  • 函数原型
  • 变量声明
  • 结构声明
  • 宏声明
  • 枚举声明
  • 类型声明
  • inline函数
• 定义是产生代码的东西

头文件

• 只有声明可以被放在头文件中
  • 是规则不是法律,也可以放定义,但是极不推荐这样做
• 否则会造成一个项目中多个编译单元里有重名的实体
  • 某些编译器允许几个编译单元中存在同名的函数,或者用weak修饰符来强调这种存在	 

重复声明

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

我们来测试一下重复声明的情况。我们在max.h中声明一个结构,同时新建一个min.h文件,在min.h中#include “max.h”。最后在主函数中#include “max.h” 和 #include “min.h”。
max.h文件

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

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

min.h文件

#include "max.h"

max.c文件

int gAll = 12;

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

主函数

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

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

我们运行,可以看出报错:结构体被重复声明。
在这里插入图片描述


要解决这个问题,需要使用一个东西:标准头文件结构

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

我们写出代码测试,在max.h中加上宏。如果没有定义过这个宏,就执行下面的。如果定义过,就不执行。
max.h文件

#ifndef MAX_H
#define MAX_H

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

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

因此,在主函数中,虽然我们#include “min.h”,但是由于MAX_H已经定义了,不会再重复包含一次了。
在这里插入图片描述
我们也可以使用#pragma once,在Visual Studio中是支持的,同时每次新建一个头文件都会默认给你加上。而gcc是不支持的。

猜你喜欢

转载自blog.csdn.net/InnerPeaceHQ/article/details/121687436