C程序结构

程序结构

一、全局变量
I、定义

  在函数外面的变量叫全局变量。

II、特点
  1. 全局变量具有全局的生存期和作用域。

  2. 它们与任何函数都无关。

  3. 在任何函数内都可以使用它们。

#include <stdio.h>
int ball=2;
int f(void);
int main()
{
    printf("in %s ball =%d\n",__func__,ball);
    f();//__func__返回函数的名字
    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;
}
  1. 没有做初始化的全局变量会得到0值,指针会得到NULL。

  2. 只能用编译时刻已知的值来初始化全局变量。

  3. 它们的初始化发生在main函数前面。

int gall=12;
int g2=gall;
//错误 编译器比较笨 一看以为你把一个全局变量赋给了一个全局变量
const int gall=12;
int g2=gall;
//ok gall是常量
  1. 如果函数内部存在与全局变量同名的变量,则全局变量被隐藏
#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;//外面的ball被隐藏了
    printf("in %s ball =%d\n",__func__,ball);
    // ball+=2;
    printf("again in %s ball =%d\n",__func__,ball);
    return ball;
}
二、静态本地变量
I、定义

  在本地变量定义时加上static关键字就成为静态本地变量。

II、特点
  1. 当离开函数的时候,静态本地变量会继续存在并保持其值。

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

#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
  1. 静态本地变量是一种特殊的全局变量
#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
//发现全局变量gall和静态本地变量all放在一起的,而k在另外一个地方

  它们位于相同的内存区域,静态本地变量具有全局的生存期,函数内的局部作用区域,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;
}
//vscode直接不让这样运行了...

  返回全局变量或者静态本地变量的地址是安全的。

  • 返回在函数内malloc的内存是安全的,但是容易造成问题。
  • 最好的做法是返回传入的指针
  • 由于函数如果返回静态本地变量或全局变量的地址,那么此函数为不可从入的,会导致线程不安全(丰田汽车的案子),因此不要使用全局变量在函数之间传递参数,尽量避免使用全局变量。
四、宏
I、初识宏
1.编译预处理指令
  • #开头的是编译预处理指令。
  • 它们不是C语言的成分,但是C语言离不开它们。//include不是C语言的关键字,也不是只有c会使用这些编译预处理指令,其他语言也可以使用。#可以念punch,给…打孔。
  • 用#define来定义一个宏。
#include <stdio.h>
// const double PI=3.14159;//C99才有了这种做法
//更老的做法
#define PI 3.14159
//这条指令把一个符号定义为了一个东西,我们称为定义了一个宏。
//编译之前会把所有程序里头的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);//ok
printf("FORmat",2*PI*3.0);//编译器不会管""里头的东西

2.c文件变成out文件的过程

我们的c文件变成out文件的过程:

.c->.i->.s->.o->.out

四个箭头的含义:

  1. 把所有编译预处理指令执行一遍。
  2. 由编译器把i.变成汇编代码文件.s。
  3. 汇编变成目标代码文件.o。
  4. 经过链接和其他东西连在一起(项目)。
II、宏的定义
  • #define <宏的名字> <宏的值>
  • 注意没有结尾的分号,因为不是C的语句。
  • 名字必须是一个单词,值可以是各种东西。
  • 在C语言的编译器开始编译之前,编译预处理程序(cpp)会把程序中的名字换成值。
  • 这个换是一个完全的文本替换。
1.注意事项
  • 如果一个宏中有其他宏的名字,也是会被替换的。
  • 如果一个宏的值超过一行,最后一行之前的行末需要加\。
  • 宏的值后面出现的注释不会被当做宏的值的一部分。

小实验:

#include <stdio.h>
#define PI 3.14159
#define PI2 2*PI//2*3.14159
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//2*3.14159
#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//删除N的定义,并选中以下程序段
    //程序段
#endif
IV、预定义的宏
__LINE__//当前被使用处的行号 表达为十进制常量
__FILE__//源代码文件的文件名 表达为字符串型常量
__DATE__//编译时的日期,表示为Mmm dd yyyy 形式的字符串常量,Mmm是由asctime产生的。
__TIME__//编译时的时间,转换的时间,表示"hh:mm:ss"形式的字符串型常量,是有asctime产生的。(asctime貌似是指的一个函数)
__STDC__//编辑器为ISO兼容实现时位十进制整型常量 看不懂

小实验

#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));
    //被替换成了printf("%d",((i)*(i)*(i)));
    return 0;
}
1.错误定义的宏:
#define RADTODEG1(x) (x*57.29578)
#define RADTODEG2(x) (x)*57.29578
//这样如果整段复制后 变成7/RADTODG2(5)=7/(5)*57.29578
//和我们想要的计算完全不一样啊喂
//RADTODEG1(2+2) 变成(2+2*57.29578)
//一样的问题
2.带参数的宏的定义原则
  • 一切都要括号

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

  • 可以带多个参数

    • #define MIN(a,b) ((a)>(b)?(b):(a))
      //(逻辑语句)?执行语句1:执行语句2等价于
      //如果逻辑语句成立 那么执行执行语句1,否则执行执行语句2
      
  • 也可以组合(嵌套)使用其他宏或函数 如

    #define PRINTSTR(msg) printf(msg)
    PRINTSTR("人生一世有几个知己");
    
  • 在大型程序的代码中使用非常普遍。

  • 在#和##这两个运算符的帮助下,宏可以非常复杂,甚至可以产生函数。

  • 西方程序员一般比较爱用宏,中国程序员用的相对少一点。

  • 宏有一个问题,因为不需要类型,所以都是不检查数据类型的,如果数据类型使用出错了没人会告诉你,inline函数做到了带参数的宏的很多功能并且会检查数据类型。

五、多个源代码文件
I、动机
  • main()里面的代码太长了适合分成几个函数。
  • 一个源代码文件太长了适合分成几个文件
  • 两个独立的源代码文件不能编译形成可执行的程序。
  • 我们要做什么才能把两个源代码文件形成一个可以运行的程序呢?
II、在vscode里头创建c项目的方法
  1. 安装插件C/C++ Project Generator

  2. 创建一个文件夹,用vscode打开,然后ctrl+shift+p,搜索Create C project,然后就会生成了一个项目。

  3. 目前已确定的事情,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;
//这个关键字extern相当于告诉编译器在这个项目的某个地方有个全局变量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;
	// scanf("%d %d",&a,&b);
	printf("%d \n",max(a,gall));
	return 0;
}

这样会把max.h中的东西重复声明,出现问题。

为了解决这个问题,我们要引入一种叫标准头文件声明的东西

max.h
#ifndef _MAX_H_//如果没有定义宏_MAX_H_
#define _MAX_H_//就定义宏_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也能起到相同的作用,但是不是所有的编译器都支持。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值