本系列文章为浙江大学翁恺C语言程序设计学习笔记,前面的系列文章链接如下:
C语言程序设计学习笔记:P1-程序设计与C语言
C语言程序设计学习笔记:P2-计算
C语言程序设计学习笔记:P3-判断
C语言程序设计学习笔记:P4-循环
C语言程序设计学习笔记:P5-循环控制
C语言程序设计学习笔记:P6-数据类型
C语言程序设计学习笔记:P7-函数
C语言程序设计学习笔记:P8-数组
C语言程序设计学习笔记:P9-指针
C语言程序设计学习笔记:P10-字符串
C语言程序设计学习笔记:P11-结构类型
全局变量
• 定义在函数外面的变量是全局变量
• 全局变量具有全局的生存期和作用域
• 它们与任何函数都无关
• 在任何函数内部都可以使用它们
我们写段代码来看看全局变量的使用。我们在这段代码中定义了一个全局变量,同时在一个函数中修改它的值,最后看看这个全局变量的值是否改变。
#include
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,最后可以发现全局变量的值没有改变。
静态本地变量
• 在本地变量定义时加上static修饰符就成为静态本地变量
• 当函数离开的时候,静态本地变量会继续存在并保持其值
• 静态本地变量的初始化只会在第一次进入这个函数时做,以后进入函数时会保持上次离开时的值
我们测试一下静态本地变量的使用。我在一个函数中定义了一个静态本地变量,并将其加2。我连续三次调用这个函数,看会发生什么。
#include
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
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;
}
返回指针的函数
• 返回本地变量的地址是危险的
• 返回全局变量或静态本地变量的地址是安全的
• 返回在函数内malloc的内存是安全的,但是容易造成问题
• 最好的做法是返回传入的指针
关于全局变量的一些Tips:
• 不要使用全局变量来在函数间传递参数和结果
• 尽量避免使用全局变量
• 丰田汽车的案子
• 使用全局变量和静态本地变量的函数是线程不安全的
编译预处理指令
• #开头的是编译预处理指令
• 它们不是C语言的成分,但是C语言程序离不开它们
• #define用来定义一个宏
我们来看一下如何定义一个宏,代码如下所示。C99之前的版本没有const,所以只能使用宏来替代。由于它是编译预处理指令,因此在预处理阶段会将所有的PI替换成3.14159。
#include
#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
#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
int main(int argc, char const *argv[])
{
printf("%s:%d\n", __FILE__,__LINE__);
printf("%s,%s\n", __DATE__,__TIME__);
return 0;
}
带参数的宏
#define cube(x) ((x)*(x)*(x))
宏可以带参数
我们写段代码来看看带参数的宏的详细使用情况。
#include
#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
#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*x
则DOUBLE(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
在编写程序时,有时候会碰到以下情况:
• main()里的代码太长了
• 一个源代码文件太长了
这时我们就会将一些函数剥离出来,写在一个新的源代码文件中(注意:这些源代码文件必须在同一个工程下)。我们写段代码看看,代码中我将max函数放在了另外一个源文件max.c中,同时在main函数所在的helloword.c文件中调用max函数。
#include
int main(int argc, char const *argv[])
{
printf("%d", max(10,12));
return 0;
}
在上面我们写的代码中,调用max函数返回的结果正确,然而这是有问题的。
• 如果不给出函数原型,编译器会猜测你所调用的函数的所有参数都是int,返回类型也是int
• 编译器在编译的时候只看当前的一个编译单元,它不会去看同一个项目中的其他编译单元以找出那个函数的原型
• 如果你的函数并非如此,程序链接的时候不会出错
• 但是执行的时候就不对了
• 所以需要在调用函数的地方给出函数的原型,以告诉编译器那个函数究竟长什么样
如果我将max函数的参数类型和返回类型都改成double,然后在主函数中去调用它并向它传递两个int类型的参数,看看结果会怎样。
main函数代码
#include
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
#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 只是为了让编译器知道printf函数的原型,保证你调用时给出的参数值是正确的类型
使用头文件时,需要注意:
• 在使用和定义这个函数的地方都应该#include这个头文件
• 一般的做法就是任何.c都有对应的同名的.h,把所有对外公开的函数的原型和全局变量的声明都放进去
不对外公开的函数
假如你有一些放在.c中的函数不想别人使用,但是这个.c文件中的其它函数能用,你的做法如下:
• 在函数前面加上static就使得它成为只能在所在的编译单元中被使用的函数
• 在全局变量前面加上static就使得它成为只能在所在的编译单元中被使用的全局变量
如果在某个源代码文件中有一个全局变量,现在要在另外一个源代码文件中使用,我们该怎么做呢?举个例子,现在我在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
#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
#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是不支持的。