好久不见,甚是想念。书接上回,继续前进!
关键字static-最名不副实的关键字
对extern声明的小小补充
当我要对一个函数进行声明的时候可不可以像如下情况:
extern int v_gal=100;
对这个变量进行了赋值,这是不可以的,因为声明并没有开辟空间,=100这是开辟空间或者初始化
所以我们得出结论:
所有的变量声明的时候不能设置初始值
再来将讲一点:
我们这里是main.c这个源文件去调用test.c里的show()函数,倘若还有test1.c,test2.c,test3.c要去调用这个show()函数呢,所以:
单纯地使用源文件,组织项目结构的时候,项目越大越复杂的时候,维护成本会变得越来越高
这样也就诞生了头文件 .h
.h:头文件,组织项目结构的时候,减少大型项目的维护成本
接下来是对维护成本的解释:
还是拿刚刚的例子来举例,如果说我们要把上面的v_gal改成v_gal2,那么我们是不是要把所有的源文件声明的位置进行改动,如果说漏掉一个,,都会导致程序运行错误,这里还好只有四个源文件,但是如果是四十个,或者四百个呢,你试想一下。
综上,我们都在回答一个问题:为什么要有头文件
有关头文件的补充说明:
.h基本上都是要被多个源文件包含的,可能有一个问题,头文件被重复包含的问题,会导致运行效率降低
解决方案:
在头文件开头写上#pragma once(**PS:**在VS2022中会默认加上#pragma once)
头文件包含以下内容:
-
C头文件
-
所有的变量的声明
-
所有的函数的声明
-
#define ,类型typedef, struct
在C语言中,包含头文件的两种形式:
#include <stdio.h>这种是C语言库里面要包含的头文件
#include"test.h"是包含自定义的头文件
这个地方我们要特别注意的是函数的声明,因为在链接的过程中,test.c和main.c会进行匹配看是否有show()函数,即使说你已经定义了该函数,但是你没有声明的时候,编译器会忽视,所以要进行声明
书写如下:
不向里面添加函数体,道理和声明变量一个道理
注意:
变量声明必须带上extern!!!
虽然在实际应用的时候编译器不会报错,但是你会无法区分是开辟空间还是声明变量
函数声明建议带上extern,为什么是建议呢。因为区别在于有没有函数体,编译器在识别的时候如果没有看到函数体会默认是函数的声明
几个小问题:
1.全局变量可以跨文件访问吗?可以
2.函数可以跨文件访问吗?可以
在具体的应用场景中,有没有可能我们不想让全局变量或者函数跨文件访问,指向本文件内部被访问
请出我们真正的主角-------static
看上面这幅图片,LNK表示的是链接的意思,这里是链接错误
static int g_val =100;//全局变量
结论1==:static修饰全局变量,该变量只能在本文件内被访问,不能被外部其他文件直接访问(可以被间接访问,如被函数调用)
static修饰函数,函数只能在本文件内被访问,不能在外部其他文件访问
为什么要这样做呢?
原因是static维护项目,提供安全保证
如果说把项目全部分装出去,那样别人就可以任意篡改,不具有安全性,static对其进行了很好的封装,相当于给对方私人定制。
第二个问题:static修饰全局变量会改变生命周期还是作用域?
答案是:作用域,只要它是全局变量,那么它就会随着程序的运行从开始到最后,但是被static修饰之后只能在本文件内被访问,所以说是改变了作用域
说完全局变量,那么再来谈谈局部变量
来看下面一段代码:
#include <stdio.h>
static void fun()
{
int i = 0;
i++;
printf("i=%d\n", i);
}
int main()
{
for (int i = 0; i < 10; i++)
{
fun();
}
return 0;
}
运行结果:
如果是平常的情况来说的话,应该是依次递增的结果才是,局部变量被static修饰了之后发生了这样神奇的变化
原因如下:
1.局部变量,具有局域临时性
2.函数调用开辟空间并初始化,函数结束释放空间
在上面的代码基础上进行了些修改:
#include <stdio.h>
static void fun()
{
static int i = 0;//i的初始化动作,永远只会初始化一次,第一次!
//初始化相当于定义,定义只有一次
//定义的话,相当于表白心意,有且只有一次
i++;
printf("i=%d\n", i);
}
int main()
{
for (int i = 0; i < 10; i++)
{
fun();
}
return 0;
}
这同样让我们大吃一惊,我们根据这样的既定事实,推导出一个结论:i在fun运行的过程中并没有被释放
由此基础上,再来得出一个结论:static修饰局部变量,更改的是局部变量的生命周期,改为全局的生命周期,作用域不同
回到前面,全局变量为什么可以跨文件访问?
因为有一定规模的项目,一定是多文件的,多个文件之间,后续一定要进行数据的“交互”(#include “test.h”,main.c),如果不能跨文件,“交互”成本会比较高
为什么临时变量具有临时性,全局变量具有全局性?
C语言地址空间是内存吗?
答案:不是
在任何有关C语言的书中是找不到相关知识的,这是操作系统的知识,有关进程地址空间
sizeof关键字-最冤枉的关键字
sizeof关键字(操作符),求特定类型对应开辟空间的大小
来看下面几行代码:
int a = 10;
printf("%d\n", sizeof(a));//1
printf("%d\n", sizeof(int));//2
printf("%d\n", sizeof a);//3,基本事实,说明了什么呢?说明了sizeof不是函数,是关键字或者操作符
printf("%d\n", sizeof int);//4
上面这四行里面,第四行为错的,sizeof是关键字,int 是变量类型,两个不能并列出现
C中为何要有类型:本质对内存合理化划分,按需索取
类型为什么在C中有这么多种:应用场景不同,解决应用场景对应的方法不同,需要的空间大小不同
本质:用最小的成本,解决多样化场景的问题
上面的两个问题也就回答了变量的定义,是什么,为什么,怎么办
需要注意的是:局部变量,不初始化,内容是随机值
对于命名的规范要求有以下几点:
1.见名知意
2.大小驼峰
3.数字字母下划线
整型的存储
联系之前,变量的创建是要在内存中开辟空间,空间的大小是根据不同类型决定的
那么,数据在所开辟的内存是如何存储的?
有符号数
首先要了解几个概念:原码,反码,补码
看下面几行代码:
int main()
{
//计算机内存储的整型必须为补码
int a=10;
//任何数据在计算机中,必须要转化成二进制,为什么?计算机只认识二进制
//符号位(0,1)+数据位
//有符号数且为正数,原码=反码=补码
//0000 0000 0000 0000 0000 0000 0000 1010
int b=-20;
//有符号数且为负数
//1000 0000 0000 0000 0000 0000 0001 0100 原码
//1111 1111 1111 1111 1111 1111 1110 1011 反码
//1111 1111 1111 1111 1111 1111 1110 1100 补码
}
需要注意的是符号位需要参与计算
该怎样把补码转为原码呢?
有两种方法:
方法1:
补码-1,再按位取反变为原码
方法2:
将补码变为反码,反码+1变为原码
上面这两种方法,理解上推荐方法1,但是实际应用更喜欢方法二
方法二是通过计算机硬件完成的,可以用一条计算机硬件电路,完成转化
补充:整型存储的本质
unsigned int a=10;
unsigned int b=-120;//这并不会报错
数据保存到空间里,空间里只能存储二进制,那么也就意味着负数保存到空间里,先转化成二进制
可能就要问了,前面加上这个,什么时候起到效果?
是在读取的时候具有意义!类型决定了如何解释空间内部保存的二进制序列
再来看下面几行:
unsigned int b=-120;
printf("%u\n",b);
printf("%d\n",b);
这里需要了解两个概念:
变量存的过程:字面数据必须先转为补码,在放入空间当中。所以,所谓符号位,完全看变量是否携带±号,和变量是否有符号无关
变量取得的过程:取数据一定要看变量本身类型,然后才决定要不要看最高符号位。如果不需要,直接二进制转为十进制。如果需要,则需要转为原码,然后才能识别。(当然,最高符号位在哪,又要明确大小端)
看一个数据的时候:
1.先看自身类型决定
a.先看符号位
b.确定原反补
二进制快速转化口诀:
1->20
10->21
100->22
1000->23
…
1后面跟n个比特位,就是2n
拿67举例,67=64+2+1
可以无脑写出32个比特位,然后拆26+21+20,从右往左数,对应的写1
0000 0000 0000 0000 0000 0000 0100 0011
大小端
那么什么叫做取值范围?
所谓的特定数据类型,能表示多少个数据类型,取决于,多个比特位对应的排列组合的顺序
从上面这张图来看,怎么表示数字0呢?
0,有且只有一种表示方案,0000 0000 还有一种是1000 0000,根据上面说的二进制转十进制的口诀,不难发现只有128和-128两种可能,根据第一位为符号位,很容易确定出是-128,所以上面的
接下来深入讨论一下为什么一定是-128(根据上面符号位来看,只是表象)
根据上面这幅图,变量的初始化会先开辟空间,然后十进制的数转成二进制,存储在空间中是补码,但是char是一个字节,也就是八个比特位,1 1000 0000 有九位,就会发生截断,截断后面八位,所以是-128
在原先的基础上又有些许变化,不能够根据1000 0000 来推断出其补码,原因是本身就是错的,被截断了,但是可以得到一种结论:
1000 0000->-128
所有的计算全部被转化成了加法,依靠的是补码
为什么要转化成加法呢?
因为这种是在CPU内进行的,全转化为加法,只要设计一套硬件电路就够了
一道练习题:
#include <stdio.h>
#include <string.h>
#include <Windows.h>
int main()
{
char a[1000];
for(int i=0;i<1000;i++)
{
a[i]=-1-i;
}
printf("%d\n",strlen(a));
}
再看一道:
#include <stdio.h>
#include <Windows.h>
int main()
{
int i=-20;
unsigned int j=10;
printf("%d\n",i+j);
printf("%u\n",i+j);
system("pause");
return 0;
}
变形:
#include <stdio.h>
#include <Windows.h>
int main()
{
unsigned int i;
for(int i=9;i>=0;i--)
{
printf("%u\n",i);
Sleep(1000);
}
return 0;
}
if-else组合
什么是语句?**
分号;隔开的就是一条语句
如:
printf("%d\n",a);
什么是表达式?**
用各种操作符把变量连起来,形成有意义的式子
第一个结论:if(0)可以用于注释代码,
但是不推荐,因为if(0)里面的代码也会进入到编译阶段,成本会大一点,如果有人把0改成1的话,运行效率会变低
第二个结论:C语言中,0为假,非0为真
第三个结论:if条件的执行流程,1.先执行()中的表达式,得到真假结果 2.条件 判定功能 3 .进行 分支功能
Bool类型
C语言有没有bool类型?
C99之前,主要是C90是没有的,目前大部分书,都是认为没有的。因为书籍一般会落后于行业。
但是C99引入_Bool类型(在新增头文件stdbool.h中,被重新用宏写成了bool,为了保证C/C++兼容性)
后面万一要用到bool,推荐C99,不要用微软的,因为微软的可移植性差,
总结:
1.优先使用C90,就是我们之前以及后面一直用的方式
2.万一非得使用bool,推荐C99标准,不推荐MS定义
有以下三种书写方式:
//1
int flag=0;
if(flag==0)
{
//条件相当于int x==0
//不推荐
printf("1\n");
}
//2
if(flag==false)
{
//必须要在C99的环境下才行,也就是要包含头文件<stdbool.h>
}
//3
if(flag)
{
//推荐
//直观反映出flag=bool
printf("3\n");
}
结论==:bool类型,直接判定,不用操作符和特定值进行比较
这里又产生了一个疑问:
在if中,结论所说的写法是否符合if本身语法?
符合的
浮点类型
浮点数在内存中存储,并不是像我们想的那样,是完整存储的,在十进制化为二进制,有可能存在精度损失
注意点:
1.浮点数在进行比较的时候,绝对不能使用==来进行比较
2.浮点数本身有精度损失,进而导致各种结果有细微的差别
解决方案:
浮点值的比较
浮点数和0比较**:
1.浮点数存储的时候,是有精度损失的
2.浮点数是不能进行==比较的
3.if(fabs(a-b)<DBL_EPSION){}
4.要不要<= ,细节 不要
注意点:0 \0 NULL都是一个意思,表示0
如何理解强制类型转换?
强制类型转换:不改变内存中的数据,只改变对应的类型
真实的转换:改变内存中的数据,只改变对应的类型
指针变量与“零”值进行比较
指针变量与“零”值进行比较,用if语句如何写?
有以下三种写法
int*p=NULL;
(1)if(p==0) if(p!=0)
(2)if§ if(!p)
(3)if(NULL==p) if(NULL!=p)
其中最推荐写法**(3)**
写法(1)如果去掉int*p=NULL,无法区分p是指针还是整型;写法(2)很容易理解成是一个BOOL类型,如果p=0,那么就不会执行了
写法(3)还有一个妙处,我们平常在实际应用中不怎么接触到==,如果把NULL提前,如果发现漏写=,会有提醒报错的
else到底与哪个if配对?
来看下面一段代码:
int main()
{
int x=1;
int y=1;
if(10==x)
if(11==y)
printf("1234\n");
else
printf("5678\n");
return 0;
}
结果如上
else会与最近的if配对,但是上面两个if里面的条件都是不对的,所以程序结束
在上面这几行代码的基础上做一点修改
int main()
{
int x=10;
int y=1;
if(10==x)
if(11==y)
printf("1234\n");
else
printf("5678\n");
return 0;
}
结果如上,因为if(10== x)满足,所以进入条件,但是if(11==y)不满足,所以进入else中
注意一个问题:
int main()
{
int flag=0;
if(flag);
printf("Hello World");
}
非常的奇怪,明明if里面的条件为0,在C语言里面认为是假的,为什么还能够打印出来呢,原因就在;上,因为语句看到;就是结束了,然后if语句没有加上花括号,所以只会和最近的一个语句配对,正好是分号,对下面的一行代码没有影响