C语言程序设计笔记(浙大翁恺版) 第十二周:程序结构

按照中国大学MOOC上浙江大学翁恺老师主讲的版本所作,B站上也有资源。原课程链接如下:

https://www.icourse163.org/course/ZJU-9001

由于是大三抽空回头整理的,所以可能前五章会记的内容比较简略。此外,作为选学内容的A0:ACLLib的基本图形函数和链表两章也没有做。西电的考试是机试,理论上学到结构体就能够应付考试了,但为了以后的学习考虑建议全学。

 

其他各章节的链接如下:

C语言程序设计笔记(浙大翁恺版) 第一周:程序设计与C语言

C语言程序设计笔记(浙大翁恺版) 第二周:计算

C语言程序设计笔记(浙大翁恺版) 第三周:判断

C语言程序设计笔记(浙大翁恺版) 第四周:循环

C语言程序设计笔记(浙大翁恺版) 第五周:循环控制

C语言程序设计笔记(浙大翁恺版) 第六周:数据类型

C语言程序设计笔记(浙大翁恺版) 第七章:函数

C语言程序设计笔记(浙大翁恺版) 第八周:数组

C语言程序设计笔记(浙大翁恺版) 第九周:指针

C语言程序设计笔记(浙大翁恺版) 第十周:字符串

C语言程序设计笔记(浙大翁恺版) 第十一周:结构类型

C语言程序设计笔记(浙大翁恺版) 第十二周:程序结构

C语言程序设计笔记(浙大翁恺版) 第十三周:文件

其他各科笔记汇总

 

程序结构

全局变量

全局变量

定义在函数之外的变量,全局的生存期和作用域

 

 

全局变量

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

示例:

#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;
}
in main gAll=12
in f gAll=12
agn in f gAll=14
agn in main gAll=14

__func__是一个字符串,表达的是当前函数的名字

 

 

全局变量初始化

没有做初始化的全局变量会得到0值,指针会得到NULL

只能用编译时刻已知的值来初始化全局变量,它们的初始化发生在main函数之前

 

示例:

#include <stdio.h>

int f(void);

const int gAll = 12;
int g2 = gAll;

int main(int argc, char const *argv[])
{
    // ...
}

int f(void)
{
    // ...
}

并不建议用这种方式来初始化全局变量,全局变量的值不应该和另一个全局变量相联系

如果将const int gAll = 12;改为int gAll =12;编译会报错。编译器没有那么智能,它认为这里将一个变量的值赋给g2

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mzr1DqNH-1659891942610)(C语言程序设计.assets/image-20220801132447544.png)]
 

 

被隐藏的全局变量

如果函数内部存在与全局变量同名的变量,则全局变量被隐藏

示例:

#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)
{
    int gAll = 1;
    gAll += 2;
    {
        int gAll = 2;
        printf("in %s gAll=%d\n", __func__, gAll);
        gAll += 2;
        printf("agn in %s gAll=%d\n", __func__, gAll);
    }
    printf("in %s gAll=%d\n", __func__, gAll);
     
     return gAll;
}
in main gAll=12
in f gAll=2
agn in f gAll=4
in f gAll=3
agn in main gAll=12

 

静态本地变量

在本地变量定义时加上static修饰符就成为静态本地变量

当函数离开的时候,静态本地变量会继续存在并保持其值

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

 

示例:

#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;
}
in f all=1
agn in f all=3
in f all=3
agn in f all=5
in f all=5
agn in f all=7

 

 

静态本地变量实际上是特殊的全局变量,它们位于相同的内存区域

静态本地变量具有全局的生存期,函数内的局部作用域,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);
    
    printf("in %s all=%d\n", __func__, all);
    all += 2;
    printf("agn in %s all=%d\n", __func__, all);
    return all;
}
&gAll=0x3800c
&all =0x38010
&k   =0xbffc9d4c
int f all=1
agn in f all=3

gAllall在内存里是紧挨着存放的

 

后记

返回指针的函数

返回本地变量的地址是危险的,因为一旦离开函数本地变量就不存在了

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

返回在函数内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);
}

f函数返回一个int的指针,这里返回本地变量i的指针。编译运行会警告返回堆栈里的本地变量i的地址是有问题的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GKiKykKF-1659891942612)(C语言程序设计.assets/image-20220801143043856.png)]

虽然*p=12,但此时i已经不存在,也就是不再受控了,这个本地变量的地址会被继续分配做他用,无法保证这个12所在的那个地方还能够继续保持12的值

尽管没有对*pi再做任何处理,但调用g函数后*p=24。实际上会发现如果在fg里分别打印出ik的地址是一样的

 

 

tips

尽量避免使用全局变量,不要使用全局变量来在函数间传递参数和结果

使用全局变量和静态本地变量的函数是不可重入的,这些函数对于多线程环境是不安全的

 

编译预处理和宏

宏定义

编译预处理指令

#开头的是编译预处理指令

它们不是C语言的成分,比如#include用来引入头文件,include就不是C语言的关键字,也不是只有C语言在使用这些编译预处理指令,其他语言也可以使用。但是C语言程序离不开它们

#define用来定义一个宏

 

 

#define

#define <名字> <值>

注意没有结尾的分号,因为不是C的语句

名字必须是一个单词,后面的所有东西都是值

在C语言的编译器开始编译之前,编译预处理程序(cpp)会把程序中的名字换成值,做完全的文本替换

 

示例:

#include <stdio.h>

// const double PI = 3.14159;
#define PI 3.14159

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

如果是GCC,编译时增加--save-temps选项保存编译过程中的临时文件,最终除了最终要运行的a.out之外,还多了.i.o.s文件

程序编译过程中产生文件的顺序是.c —> .i —> .s —> .o —> .out.c源代码经过编译预处理,执行完所有编译预处理指令得到.i,C编译器编译.i产生.s汇编代码文件,.s做汇编得到.o目标代码文件,.o再经过链接形成.out可执行文件

tail查看.i的结尾几行,可以看到进行了文本替换

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cV2gPlfR-1659891942613)(C语言程序设计.assets/image-20220802101106303.png)]

 

 

如果一个宏的值中有其他的宏的名字,也是会被替换的

如果一个宏的值超过一行,最后一行之前的行末需要加\

值可以很复杂,也可以带空格和各种标点符号,但宏的值后面出现的注释不会被当作宏的值的一部分

 

示例:

#include <stdio.h>

#define 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, PI2*3.0);
    PRT;
    return 0;
}
18.849540
3.141590 6.283180

"FORMAT"不会被替换,编译器不会对字符串里的单词做替换

 

 

没有值的宏

#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;
}

在这里插入图片描述

 

带参数的宏

像函数的宏

#define cube(x) ((x)*(x)*(x))

宏可以带参数

 

示例:

#include <stdio.h>

#define cube(x) ((x)*(x)*(x))

int main(int argc, char const *argv[])
{
    int i;
    scanf("%d", &i);
    printf("%d\n", cube(i+2));
    return 0;
}

 

 

错误定义的宏

示例:

#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", 180/RADTODEG2(1));
    return 0;
}
119.591560
10313.240400

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tHqn2LDG-1659891942616)(C语言程序设计.assets/image-20220802143029219.png)]
 

 

带参数的宏的原则

一切都要括号:整个值要括号,参数出现的每个地方都要

#define RADTODEG1(x) (x * 57.29578)#define RADTODEG2(x) (x) * 57.29578改为#define RADTODEG1(x) ((x) * 57.29578)

 

 

带参数的宏的原则

可以带多个参数。 如:#define MIN(a, b) ((a)>(b)?(b):(a))

也可以组合(嵌套)使用其他宏

 

 

带参数的宏

在大型程序的代码中使用非常普遍,可以牺牲空间换取效率

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

部分带参数的宏会被inline函数替代,它是函数但是没有函数调用时的额外开销,也会进行参数类型检查

 

 

其他编译预处理指令

  • 条件编译
  • error

 

大程序结构

多个源代码文件

多个.c文件

main()里的代码太长了适合分成几个函数

一个源代码文件太长了适合分成几个文件

 

放在同一目录下两个独立的源代码文件不能编译形成可执行的程序

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5gvaXff5-1659891942617)(C语言程序设计.assets/image-20220802145738291.png)]

”Id returned 1 exit status“中ld指的是链接器,这句话并不是真正出错的原因,而是告知你编译没有完成的结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6nnnRGmx-1659891942619)(C语言程序设计.assets/image-20220802145843195.png)]

编译max.c会告知找不到main

 

 

示例:

对于这样的IDE,为了将这两个文件放在一起,需要先新建一个项目

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bZMfBctO-1659891942620)(C语言程序设计.assets/image-20220802150529771.png)]
 

现在可以选择为“Console Application”终端应用程序,C项目并起项目名,确定后找到放main.c和max.c地方保存

在这里插入图片描述
在这里插入图片描述
 

Dev C++会自动做一个main.c,由于已经有main.c,这里直接关闭不需要保存

在项目管理里去掉main.c,这个main.c是我们删掉不要的那个

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5mZLm9IY-1659891942625)(C语言程序设计.assets/image-20220802151734946.png)]
 

然后添加main.c和max.c到项目里再编译执行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FvzJ08JD-1659891942626)(C语言程序设计.assets/image-20220802151856834.png)]
在这里插入图片描述

 

 

项目

在Dev C++中新建一个项目,然后把几个源代码文件加入进去

对于项目,Dev C++的编译会把一个项目中所有的源代码文件都编译后,链接起来产生一个可执行文件

Dev C++是一个比较特别的IDE,绝大多数其他IDE(如Visual Studio, Code::Blocks)哪怕只有一个.c文件都必须要求先建一个项目,而Dev C++允许不建项目。但是不建项目时就只能是一个.c文件,如果有多个.c文件就必须建项目

从编译器的角度,一个.c是一个编译单元,编译器同一时间一次只能处理一个编译单元,它只会对一个编译单元做编译,编译完后形成.o目标代码文件,然后由链接器把它们链接在一起。所以有的IDE,如Visual Studio有分开的编译(Compile)和构建(Build)两个按钮,前者是对单个源代码文件编译产生.o文件,后者把这些.o文件链接起来形成一个可执行程序。Dev C++只有一个编译按钮

 

 

头文件

如果在main.c注释掉max函数原型声明,编译仍然会通过,编译器编译main.c时会猜测max函数的参数类型和返回类型。但如果猜测的和实际max.c里定义的max函数不一致,虽然main.c和max.c各自编译完成,但链接器把它们链接起来后执行时会得到错误的结果

为了保证在main.c这边对max函数的使用和在max.c这边所定义的max函数一致需要有头文件作为媒介

头文件

把函数原型放到一个头文件(以.h结尾中),在需要调用这个函数的源代码文件(.c文件)中#include这个头文件,就能让编译器在编译的时候知道函数的原型

 

示例:

再新建一个源代码文件添加到项目中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j3dyTARk-1659891942629)(C语言程序设计.assets/image-20220802163816566.png)]

 

在里面放原来在main.c里的max函数原型,保存为max.h

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GcbPFV7Q-1659891942631)(C语言程序设计.assets/image-20220802164021183.png)]

 

在main.c和max.c里#include "max.h"

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qFHqy13N-1659891942632)(C语言程序设计.assets/image-20220802164134223.png)]
在这里插入图片描述
这样编译就会出错,因为编译器能发现max.c里对max函数的定义和max.h里声明的不一致

 

 

#include

#include是一个编译预处理指令,和宏一样,在编译之前就处理了

它把那个文件的全部文本内容原封不动地插入到它所在的地方

所以也不是一定要在.c文件的最前面#include

 

示例:

在命令行编译链接多个.c文件
在这里插入图片描述
 

先只编译.c文件,-c表示只编译不链接,不去试图产生.out

在这里插入图片描述
在这里插入图片描述
.i文件里#开头的是注释

表示在main.c的第2行放进max.h,然后下面是max.h第一行的内容double max(double a, double b);。所以#include就是把头文件里所有的文本插入到#include所在行

 

 

""还是<>

#include有两种形式来指出要插入的文件

""要求编译器首先在当前目录(.c文件所在的目录)寻找这个文件,如果没有,到系统指定的目录去找。<>让编译器只在系统指定的目录去找

所以一般自己的.h文件用"",而标准库的<>

 

 

编译器自己知道自己的标准库的头文件在哪里

对于Unix系统来说,可以在"/usr/include/"目录下找到各种.h文件

查看stdio.h,可以在里面找到printf的原型声明,但找不到源代码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g4tQt432-1659891942639)(C语言程序设计.assets/image-20220802220910765.png)]
 
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4vUh9hjy-1659891942641)(C语言程序设计.assets/image-20220802221042762.png)]
 

而Window标准库的头文件在如下目录

在这里插入图片描述
 

环境变量和编译器命令行参数也可以指定寻找头文件的目录

 

 

#include的误区

#include不是用来引入库的

stdio.h里只有printf的原型,printf的代码,printf编译好后产生的目标代码文件在另外的地方,某个.lib(Windows)或.a(Unix)中

现在的C语言编译器默认会引入所有的标准库,把所有标准库的代码都和源代码链接在一起去掉用不到的形成一个可执行文件

#include <stdio.h>只是为了让编译器知道printf函数的原型,保证你调用时给出的参数值是正确的类型

 

 

头文件

在使用和定义这个函数的地方都应该#include这个头文件

使用这个函数的地方有头文件,编译器能够检查函数调用时给出的参数是否正确。定义这个函数的地方包含头文件,编译器会检查对外宣称的函数原型和实际的函数定义是否一致

一般的做法就是任何.c除了不需要给出原型的main都有对应的同名的.h,把所有对外公开的函数的原型和全局变量的声明都放进去

 

 

不对外公开的函数

有函数放在某个.c里,不希望别人使用,但希望当前.c里别的函数能够使用

在函数前面加上static就使得它成为只能在所在的编译单元中被使用的函数

在全局变量前面加上static就使得它成为只能在所在的编译单元中被使用的全局变量

 

声明

变量的声明

项目当中某个源代码文件里定义一个全局变量,在别的地方想要使用需要在头文件中声明告知编译器在项目的某处有该全局变量,然后包含该头文件

 

int i;是变量的定义

extern int i;是变量的声明

声明不能初始化,初始化是定义的工作

 

 

声明和定义

声明是不产生代码的东西

  • 函数原型
  • 变量声明
  • 结构声明
  • 宏声明
  • 枚举声明
  • 类型声明
  • inline函数

定义是产生代码的东西

 

 

头文件

规则尚只有声明可以被放在头文件中,否则会造成一个项目中多个编译单元里有重名的实体,某些编译器允许几个编译单元中存在同名的函数,或者用weak修饰符来强调这种存在

 

 

重复声明

同一个编译单元里,同名的结构不能被重复声明

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

 

示例:

在一个.c文件中不能重复出现结构声明,编译时会说重复定义

在这里插入图片描述
 

把这部分结构的声明放到max.h里。如果还有另外一个min.h,在min.中包含max.h

在这里插入图片描述
在这里插入图片描述
 

然后在main.c里面又包含min.h。由于include做的是头文件文本插入,最终同样会导致结构重复声明

在这里插入图片描述
在这里插入图片描述

 

需要引入标准头文件结构,在max.h里加入几句话。#ifndef _MAX_H_是条件编译指令,编译预处理时如果当前编译单元没有定义宏_MAX_H_就定义宏_MAX_H_,如果已经定义了#ifndef _MAX_H_#endif之间的内容就不会出现在.i文件里

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZZrpLv7b-1659891942648)(C语言程序设计.assets/image-20220803144732641.png)]

 

 

标准头文件结构

运用条件编译和宏,保证这个头文件在一个编译单元中只会被#include一次

Visual Studio的#program once也能起到相同的作用,但是不是所有的编译器都支持

 

示例:

#ifndef __LIST_HEAD__
#define __LIST_HEAD__

#include "node.h"

typedef struct _list {
    Node* head;
    Node* tail;
} List;

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值