程序结构
一、全局变量
I、定义
??在函数外面的变量叫全局变量。
II、特点
-
全局变量具有全局的生存期和作用域。 -
它们与任何函数都无关。 -
在任何函数内都可以使用它们。
#include <stdio.h>
int ball=2;
int f(void);
int main()
{
printf("in %s ball =%d\n",__func__,ball);
f();
printf("again in %s ball =%d\n",__func__,ball);
return 0;
}
int f(void)
{
printf("in %s ball =%d\n",__func__,ball);
ball+=2;
printf("again in %s ball =%d\n",__func__,ball);
return ball;
}
-
没有做初始化的全局变量会得到0值,指针会得到NULL。 -
只能用编译时刻已知的值来初始化全局变量。 -
它们的初始化发生在main函数前面。
int gall=12;
int g2=gall;
const int gall=12;
int g2=gall;
- 如果函数内部存在与全局变量同名的变量,则全局变量被隐藏
#include <stdio.h>
int ball=2;
int f(void);
int main()
{
printf("in %s ball =%d\n",__func__,ball);
f();
printf("again in %s ball =%d\n",__func__,ball);
return 0;
}
int f(void)
{
int ball=2000;
printf("in %s ball =%d\n",__func__,ball);
printf("again in %s ball =%d\n",__func__,ball);
return ball;
}
二、静态本地变量
I、定义
??在本地变量定义时加上static关键字就成为静态本地变量。
II、特点
-
当离开函数的时候,静态本地变量会继续存在并保持其值。 -
静态本地变量的初始化只会在第一次进入这个函数的时候做,以后进入函数时会保持上次离开的值。
#include <stdio.h>
int f(void);
int main()
{
f();
f();
f();
return 0;
}
int f(void)
{
static int all=1;
printf("in %s all =%d\n",__func__,all);
all+=2;
printf("again in %s ball =%d\n",__func__,all);
return all;
}
in f all =1
again in f ball =3
in f all =3
again in f ball =5
in f all =5
again in f ball =7
- 静态本地变量是一种特殊的全局变量
#include <stdio.h>
int f(void);
int gall=12;
int main()
{
f();
return 0;
}
int f(void)
{
static int all=1;
int k=0;
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("again in %s ball =%d\n",__func__,all);
return all;
}
&gall=0000000000403010
&all=0000000000403014
&k=000000000061FDEC
??它们位于相同的内存区域,静态本地变量具有全局的生存期,函数内的局部作用区域,static在这里的意思是局部作用域(本地可访问,只在这个函数里可以访问它)
三、返回指针的函数
??**返回本地变量的地址是危险的。**因为本地变量离开函数以后这个变量就不存在了。
#include <stdio.h>
int* f(void)
{
int i=12;
return &i;
}
void g(void)
{
int k=24;
printf("k=%d\n",k);
}
int main()
{
int* p=f();
printf("*p=%d\n",*p);
g();
printf("*p=%d\n",*p);
return 0;
}
??返回全局变量或者静态本地变量的地址是安全的。
- 返回在函数内malloc的内存是安全的,但是容易造成问题。
- 最好的做法是返回传入的指针
- 由于函数如果返回静态本地变量或全局变量的地址,那么此函数为不可从入的,会导致线程不安全(丰田汽车的案子),因此不要使用全局变量在函数之间传递参数,尽量避免使用全局变量。
四、宏
I、初识宏
1.编译预处理指令
- #开头的是编译预处理指令。
- 它们不是C语言的成分,但是C语言离不开它们。//include不是C语言的关键字,也不是只有c会使用这些编译预处理指令,其他语言也可以使用。#可以念punch,给…打孔。
- 用#define来定义一个宏。
#include <stdio.h>
#define PI 3.14159
int main(int argc,const char* argv[])
{
printf("%f\n",2*PI*3.0);
return 0;
}
#define FORMAT "%f\n"
printf(FORMAT,2*PI*3.0);
printf("FORmat",2*PI*3.0);
2.c文件变成out文件的过程
我们的c文件变成out文件的过程:
.c->.i->.s->.o->.out
四个箭头的含义:
- 把所有编译预处理指令执行一遍。
- 由编译器把i.变成汇编代码文件.s。
- 汇编变成目标代码文件.o。
- 经过链接和其他东西连在一起(项目)。
II、宏的定义
- #define <宏的名字> <宏的值>
- 注意没有结尾的分号,因为不是C的语句。
- 名字必须是一个单词,值可以是各种东西。
- 在C语言的编译器开始编译之前,编译预处理程序(cpp)会把程序中的名字换成值。
- 这个换是一个完全的文本替换。
1.注意事项
- 如果一个宏中有其他宏的名字,也是会被替换的。
- 如果一个宏的值超过一行,最后一行之前的行末需要加\。
- 宏的值后面出现的注释不会被当做宏的值的一部分。
小实验:
#include <stdio.h>
#define PI 3.14159
#define PI2 2*PI
int main(int argc,const char* argv[])
{
printf("%f\n",PI2*3.0);
return 0;
}
#include <stdio.h>
#define PI 3.14159
#define PI2 2*PI
#define PRT printf("%f\n",PI);\
printf("%f\n",PI2)
int main(int argc,const char* argv[])
{
PRT;
return 0;
}
III、没有值的宏—条件编译
#define _DEBUG
??这类宏是用来做条件编译的。
??**条件编译是指预处理器根据条件编译指令,有条件地选择源程序代码中的一部分代码作为输出,送给编译器进行编译。**主要是为了有选择性地执行相应操作,防止宏替换内容(如文件等)的重复包含。
常见条件编译指令:
-
#if
-
#elif
-
#else
-
#endif
-
#ifdef
-
#ifndef
1.常见语句1 #if-#else-#endif
#if 条件表达式
程序段1
#else
程序段2
#endif
功能为:如果#if后的条件表达式为真,则程序段 1 被选中,否则程序段 2 被选中。
2.常见语句2 #ifndef-#define-#endif
#ifndef 标识符
#define 标识符 替换列表
#endif
功能为:一般用于检测程序中是否已经定义了名字为某标识符的宏,如果没有定义该宏,则定义该宏,并选中从 #define 开始到 #endif 之间的程序段;如果已定义,则不再重复定义该符号,且相应程序段不被选中。
??该条件编译指令的一个重要应用是防止头文件重复包含。
??如果f.c源文件中包含f1.h和f2.h两个头文件,而f1.h头文件及f2.h头文件均包含x.h头文件.为了防止头文件重复包含,我们可以在x.h头文件里头,使用以下条件编译指令。
#ifndef _HEADNAME_H_
#define _HEADNAME_H_
#endif
??这样当头文件x.h第一次被包含的时候,由于没有检测到对应的符号(宏名),我们会定义该头文件对应的符号(宏),其值为系统默认。并且,该条件便一直领选中#endif之前的头文件内容;如果当x.h头文件再次被包含时,由于检测到了已存在以该头文件名对应的宏,则忽略该编译指令之间的所有代码,从而避免了重复包含。
3.常见语句.3 #if-#elif-#else-#endif
#if 条件表达式1
程序段 1
#elif 条件表达式2
程序段 2
#else
程序段3
#endif
功能为:先判断条件1的值,如果为真,则程序段 1 被选中编译;如果为假,而条件表达式 2 的值为真,则程序段 2 被选中编译;其他情况,程序段 3 被选中编译。
4.常见语句.4 #ifdef-#endif
#ifdef 标识符
程序段
#endif
功能为:如果检测到已定义该标识符,则选择执行相应程序段被选中编译;否则,该程序段会被忽略。
例子:
#ifdef N
#undef N
#endif
IV、预定义的宏
__LINE__
__FILE__
__DATE__
__TIME__
__STDC__
小实验
#include <stdio.h>
int main(int argc,const char* argv[])
{
printf("%s:%d\n",__FILE__,__LINE__);
printf("%s,%s\n",__DATE__,__TIME__);
return 0;
}
输出
Sep 17 2021,00:16:35
V、带参数的宏
#define cube(x) ((x)*(x)*(x))
??定义了一个宏,名字叫做cube(x),值为((x)乘(x)乘(x)),与函数的定义不同,我们定义这个带参数的宏没有规定x的类型。
实验:
#include <stdio.h>
#define cube(x) ((x)*(x)*(x))
int main(int argc,const char* argv)
{
int i;
scanf("%d",&i);
printf("%d",cube(i));
return 0;
}
1.错误定义的宏:
#define RADTODEG1(x) (x*57.29578)
#define RADTODEG2(x) (x)*57.29578
2.带参数的宏的定义原则
-
一切都要括号
-
#define RADTOEG(x) ((x)*57.29578) -
可以带多个参数
-
也可以组合(嵌套)使用其他宏或函数 如 #define PRINTSTR(msg) printf(msg)
PRINTSTR("人生一世有几个知己");
-
在大型程序的代码中使用非常普遍。 -
在#和##这两个运算符的帮助下,宏可以非常复杂,甚至可以产生函数。 -
西方程序员一般比较爱用宏,中国程序员用的相对少一点。 -
宏有一个问题,因为不需要类型,所以都是不检查数据类型的,如果数据类型使用出错了没人会告诉你,inline函数做到了带参数的宏的很多功能并且会检查数据类型。
五、多个源代码文件
I、动机
- main()里面的代码太长了适合分成几个函数。
- 一个源代码文件太长了适合分成几个文件
- 两个独立的源代码文件不能编译形成可执行的程序。
- 我们要做什么才能把两个源代码文件形成一个可以运行的程序呢?
II、在vscode里头创建c项目的方法
-
安装插件C/C++ Project Generator -
创建一个文件夹,用vscode打开,然后ctrl+shift+p,搜索Create C project,然后就会生成了一个项目。 -
目前已确定的事情,str文件夹里放c程序,include文件夹里放头文件。
III、头文件
??把函数原型放到一个头文件(以.h结尾)里头,在需要调用这个函数的源代码文件(.c文件)中#include这个头文件,就能让编译器在编译的时候知道函数的原型。
??这个头文件就好像一个合同,是max提供给别人的一个合同,它承诺我的max原型是这个样子,你要用我的max就#include <max.h>.
IV、#include
- #include是一个编译预处理指令,和宏一样,在编译之前就处理了。
- 它把那个文件的全部文本内容原封不动的插入它所在的地方。
- 所以也不一定是要在.c文件的最前面#include
- #include有两种形式来指出要插入的文件
- ""要求编译器首先在当前目录(.c文件所在目录)去寻找这个文件,如果没有,到编译器指定的目录去找
- <>让编译器只在指定的目录去找
- 所以一般自己的头文件用""调用C标准库的头文件用<>
- 编译器自己知道自己的标准库的头文件在哪里
V、#include的误区
- #include不是来引入库的,它其实只做了一件很平凡的事情,告诉了编译器一些函数的原型。
- 这些函数的源码在另外的地方,在某个lib(Windows)或.a(Unix)中
- 现在的C语言编译器默认会引入所有的标准库,所以我们有时候忘写了#include <stdlib.h>仍然可以使用malloc函数,只不过编译器会猜malloc函数的原型,猜成了int* malloc(malloc)有时候就猜对了
- 在使用和定义这个函数的地方都应该#include这个有函数原型的头文件。
- 一般的做法就是任何的.c都有对应的同名的.h,把所有对外公开的函数的原型和全局变量的声明都放进去。
- 全局变量可以在多个.c之间共享
VI、不对外公开的函数和全局变量
??不对外公开的意思是只在此编译单元使用。
??假如说我们有个函数放在某个.c里头我们不希望别的.c用,但是我们希望在这个.c里头别的函数能用,可以在这个函数前面加上关键字static.
- 在函数前面加上static就使得它成为只能在所在编译单元中被使用的函数。
- 在全局变量前面加上static就使得它成为只能在所在编译单元中被使用的全局变量。
VII、声明
??假如在项目中的某个编译单元里头有个全局变量,在别的编译单元想要使用这个全局变量,应该怎么做呢?
??假设我们的max.c函数中定义了一个全局变量
#include <max.h>
int gall=2;
double max(double a,double b)
{
return a>b?a:b;
}
??我们应该在main里面有个声明 说在项目的某处有那么一个全局变量。
??写法是这样的,在max.h文件里这样写
double max(double a,double b);
extern int gall;
变量的声明:
-
int i;是变量的定义
-
extern int i;是变量的声明
在C中,声明是不产生代码的东西
(编译器看到声明后不会为此产生代码而是会记下来)
- 函数原型
- 变量声明
- 结构声明
- 宏声明
- 枚举声明
- 类型声明
- inline函数
定义是产生代码的东西:函数 全局变量
我们的规则:只有声明可以被放在头文件中
问题:头文件重复包含(重复声明)
max.h
int max(int a,int b);
extern int gall;
struct Node{
int data;
char* name;
};
min.h
#include "max.h"
main.c
#include <stdio.h>
#include "max.h"
#include "min.h"
int main()
{
int a=15;
int b=65;
printf("%d \n",max(a,gall));
return 0;
}
这样会把max.h中的东西重复声明,出现问题。
为了解决这个问题,我们要引入一种叫标准头文件声明的东西
max.h
#ifndef _MAX_H_
#define _MAX_H_
int max(int a,int b);
extern int gall;
struct Node{
int data;
char* name;
};
#endif
我们利用没有值的宏的条件编译完成了这件事情。
标准头文件结构:保证这个头文件在一个编译单元只能被#include一次
#ifndef __LIST_HEAD__
#define __LIST_HEAD__
#include "node.h"
typedef struct{
Node* head;
Node* tail;
}List;
#endif
visual studio中#pragma once也能起到相同的作用,但是不是所有的编译器都支持。
|