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))
就是被替换的内容
- cube(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
是一个结构