C语言学习笔记
多文件
多文件的形式及其作用
- 一个工程里面有多个文件,互相包含依赖,这样就组成了多文件
- 一定要有主文件也就是有主函数的文件。
- 头文件是对自己写的函数的声明,也就是定义在源文件,声明在头文件,调用再主文件mian.c,然后再源文件内要写
#include "lan.h"
这样就完成了包含。 - 头文件和源文件的个数是任意的。
- 分多个文件的意义是什么:让文件的名字把函数的类表示出来,更加方便理解、读取、调用,还能方便管理,维护……总之,好处多多。
#include的作用
#include "lan.h"
包含头文件,include是预处理指令,他们的特点是在程序编译的时候(预编译期)起作用,运行时和他没有任何关系。最终,预处理指令形成了声明的作用。- 实际上#include的工作方式是将
lan.h
或者系统给的头文件conio.h,stdio.h
这样的头文件中所有内容全部替换过来,也是一种傻瓜式替换。从这个角度上来说,头文件是可有可无的,但是它的作用是大大减少了文件的大小,增强了代码的可读性。 - 所以一般,我们下载的源代码,每个源文件都对应这个一个头文件,这样我们只需要写一个
#include
就行了,完全不用手写任何声明,很方便。 - 主函数中函数的调用,是如何找到源文件的:程序运行时会把所有函数加载进内存,所以是通过函数地址找到函数的,而不是去源文件中执行。
双引号和尖括号的区别
- 我们自己做的头文件用
" "
,系统的头文件用< >
- 根本区别:查找路径不一样。双引号是先在当前工程路径下查找,如果没找到,尖括号会在默认路径下找(编译器环境目录下找),包含默认路径。
相对路径和绝对路径
- 通过完整路径可快速打开文件,省时。
- 这里提到的完整路径就是绝对路径
例如:#inclucde"G: \\c3.A”
.这样通过绝对路经来找文件。 - 相对路径:是系统为了方便书写,设置的一种方法。
- 注意:在写路径时一定要写板文斜杠、因为
\\
才表示\
,否则会被认为转义字符。 - 相对路经的其它用法:
../
进入父级文件夹。用法:"../include\\c3.h“
。也可重复使用:..\..\
.\
当前文件夹(没必要写) - 注意路径的知识不是c语言专属。
头文件重复包含
- 不止是函数可以在头文件中声明,结构体,联合,枚举等需要声明的部分都可以。
- 头文件重复包含的原理也是在其它头文件中调用了声明过的函数、结构体等。
- 如果相同的名字的部分被重复定义,如果是在一个文件中是会报错的,但如果在不同头文件中,且定义一模一样就不会出现任何问题。但如果结构体成员、函数体不完全一样也在出现问题。也就是说作用域是单个文件。
- 但是要注意主函数中不能出现重定义,这经常是头文件重复包含带来的问题。
头文件重复包含解决办法
-
解决办法1:
#ifndef d3 #define d3 头文件内容 #endif
-
这些代码是专门用来解决头文件重复包含的,d3是随便起的名字,正因为随便起,一般使用该头文件的文件名
-
解决办法2:把所有的公共使用的函数、结构体等定义在一个头文件中,只有源文件
main.c
中调用,或者只有在该头文件(通常叫common.h)使用解决办法1。
头文件互相包含
- 正常情况下,如果出现头文件互相包合的情况,系.统(编绎器)会提示致命错误
fatal error
。 - 解决方法:同上。这里充分表现了
define
的作用。 - 注意特殊情况:函数的定义不要写在头文件,会被认为是重复声明。结构体在两个头文件中相互调用时,即使解决了互相包含的问题,也无法解决错误,这是结构体自身声明和定义的关系。这是结构体自身声明和定义的关系决定的。结构体互相嵌套的解决办法是使用指针(地址),
struct HH* g;
存储类说明符
- 自动变量:或者叫局部变量/栈区变量,定义在
{}内的代码段
,比如在函数内,函数形参、if
、while
、for
、switch
、do while
、的结构内。结构体花括号成员不是局部变量的定义。 - 种类:
int a;int a[4];int *p;struct AAA a[3];
注意malloc的空间不是变量。 - 特性:声明周期:所在花括号结束时,该变量会被自动释放。
作用域:所在花括号,除了花括号,名字就没用了。
所以叫局部变量,仅在局部起作用。 - 注意:关键词
auto
默认的就是auto变量,不写跟写了时相同作用的,所以函数不能返回局部变量的地址(注意返回值是地址的函数要用int* fun()
来声明,这样会报警告)。同一个作用域内变量不能重名,但是不同的作用域内就可以重名。作用域嵌套:在小作用域内小的起作用覆盖大的,没有任何关系。 - ★静态存储区与全局变量:特点:
1、内存区域:静态区/静态全局区,静态存储区。
2、内存特点:全被自动初始化为0,生命周期与程序共存亡,运行时,在加载资源阶段分配空间。也就是说,静态存储的数据在编译一次之后,只要程序不关闭,就可以保持内部数据不变。 - 全局变量:自动初始化为0,包括指针、基本数据类型、结构体,全局变量和局部变量重名,在局部位置,局部变量有效。
1、生命周期与程序共存亡
2、作用域说所有文件函数都可见,跟函数一样,只能出现一个定义,但是声明可以出现多个。
3、初始化一定要用常量,函数变量都不行。
4、全局变量与多文件关系:如果全局变量的声明放在了头文件里,其他函数就能轻松调用。
extern 介绍
- 修饰函数/变量声明,表示是外部变量,实体在别处也就是表示语处使用的是全局变量或函数
- 修饰全局变量和函数的声明时,都是加不加均可。
- 注意声明局部的全局变量(在局部声明全局)。只能进行声明、不能定义
- 其优点在于使代码逻辑更清晰,别人一看,就知道这个a是会商量/外部变量。
static静态全局变量
- 作用域;只在所在文件内有效,别的文件用不了。地址和数据都不会一样。
- 静态局部变量:在扇部的块里声明
static int a;
同时也不能用变量对其进行初始化。作用域和普通的局部变量相同。它和普通的局部变量的主要区别是静态局部变量只各进行一次声明及初始化、也就是说上一次运行的结果是下一次的初始化。加果用断点来进行测试,会发现断点直接跳过了那一行。 - 静态函数:作用域仅在当前文件内有效、其也文体不认识,和全局变量的作用域一样。
寄存器变量register
- 定义方式与上相同、特点是不能取地址和不能修饰全局变量。
- CPU取数据处理:1、寄存器,与CPU速度相当空间比较小,在kb级别 。2、高速缓存(一级二级三级),高速缓存比寄存器慢,但空间可以达到MB 3、物理内存,内存比缓存慢,但是空同可以达到GB级别,当前个人电脑一般都是4G、8G、16G。4.硬盘(距固态和磁盘),硬盘这个速度更慢,但是价格也更便宜、空间也很大。3.其他外设,usb外接存储器(移动硬盘、u盘等),光盘,软盘等,这个就更慢了。
因此这个寄存器变量就是指示系统将此变量存在寄存器中,但是由于这个寄存器可用空间非带珍贵,系统不一定会让我们使用。 - 以上存储类标识符,一共有五个:
typedef,auto,extern,static,register。
每个变量最多用一个存储类标识符进行修饰。
const 简介
- 一般边叫常量修饰符,作用是被const修饰的变量修饰为常量,不可再被修改(通常认为只读)
- 在声明时一定要初始化,否则后面就修改不了了。写法可以是
const int a;
也可以是int const a;
- 本质是不能通过a的名字对其进行修改,我们仍可通过地址来修改。写法:
int p=(int *)&a;
注意C++这样写也会报错。 - 修饰数组:与上述类似。
- 修饰指针:这一块是const的难点及重点,首先,有不同的形式,不同的形式的意义不一样
1、const int *p = &a;
这样的形式,意思是*p是只读变量,*p不能再被修改,但是p指向的地址可以修改。
2、int const *p = &a;
和上述完全相同
3、int* const p;
这样的方式*p是可以操作改变的,但是p就无法重新指向了。
4、const int* const p
这样的方式导致p和*p都无法修改。 - const还可以修饰函数参数、函数返回值:
const int fun(const int* p);
这样的修饰意味着参数不希望在函数内被修改,返回值也不希望被修改。 char *p = "hello,lancibe";
我们知道,常量字符串无法进行修改,但是如果我们程序中对他进行了修改,编译器并不会报错,但是在运行过程中会出现问题,所以我们通常会在char前面加上const,这样来表示常量字符串不能被修改,在编译时就能解决问题。
volatile和restrict
-
volatile修饰的变量被定义为易变变量,也就是说使用频率较高,系统会将该变量放入寄存器中或者高速缓存,以增加对该变量的读取速度,从而增加程序的执行速度。
-
restrict修饰可优化的操作,只能用于指针,表示该指针是访问对应空间的唯一,且初始的访问方式,使用它修饰了之后,
int* restrict p = (int*)malloc(sizeof(int)*); p[0] = 0; p[0] += 3; p[0] += 5;
-
这样编译器就会自动优化为
p[0] = 0+3+5;
不进行多次运算。
内存分区
- 栈区:局部变量,特点:默认为1M,自动申请,自动释放,局部作用域的特点。
- 堆区:malloc的空间,程序员申请,程序员释放,不是放的话也是与程序员共存亡。
- 静态全局区:储存全局变量,static变量。特点是会自动初始化为0;生命周期与程序共存亡;作用域是整个多文件。
- 常量区:例如
12
,'c'
,"qwe"
,12.3
。特点是只读,该区域内的数据不能被修改;空间也是系统申请和释放。生命周期:字符串常量与程序共存亡,数据常量是立即数存储,一般不占用额外的存储空间,也就是说使用完之后空间就被释放掉。 - 代码区:该区只装代码,特点是只读,且更加严格的由系统管理。
命令行参数
- 形式
int main(int argc, char * argv[])
这是标准命令行参数(都是可以用其他的来替换的)。参数一是命令行参数的个数,我们实际传递的参数是从下标一开始的(意思是总有一个该程序自己,argv[0]的数据就是该程序的绝对地址)。参数二是字符串数组,意思是每一个参数(元素)都是一个字符串,所以这里用一个字符串数组来装所有传递进来的参数。 - 使用:可以通过循环(i<argc)来打印出各个传递进来的参数
- 参数如何传递进来?就像使用不同程序来打开同一个文件时一样,可以通过控制台传递:1、将exe文件拖入控制台(cmd),后面接参数,参数之间用空格隔开,这里控制台内显示的exe的绝对路径也是一个参数
argv[0]
2、控制台又称命令行,是用来输入指令的,所以这里传递进来的参数,就叫做命令行参数。
也可以通过编译器传递,工具属性->调试->命令行参数,然后直接编译运行即可,这样做的好处是能便于调试。
随机数
- 应用:各种游戏、程序中需要用到随机数的地方,比如贪吃蛇的食物产生位置,俄罗斯方块下一个方块的样子,棋牌游戏的发牌……但是其算法本身不用去研究,非常复杂。
- 下列函数的头文件:
<stdlib.h>
,整个随机数系统由至少两个函数组成 - 第一:随机数种子:
void srand(unsigned int seed)
,这个函数产生随机数的种子,为了保证每次运行程序是产生的随机数不同,我们可以调用时间函数time(void);
,这就是调用该函数的写法,其头文件是<time.h>
。总的写法srand(unsigned int time(void));
- srand函数只需要调用一次,因为每次调用下面的函数时,都会新生成一个种子
- 第二:产生随机数函数:
int rand(void)
这个函数的作用是将其参数通过一定的算法来算到一个特定的数,调用一次产生一个,通过返回值返回。因此每次产生的随机数不同。 - 随机产生一个指定一组数中的一个:这一部分就是数学思想的灵活运用
不写srand函数的话,种子默认为1。
随机产生指定范围的一个数:产生0~79的随机数:a = rand()%80;
随机产生100~999的随机数:a = rand()%1000+100;
随机产生一组指定的数中的一个,通过数组将其一个一个的记录下来,然后随机产生下标。