点击链接加入群【C语言】:http://jq.qq.com/?_wv=1027&k=2H9sgjG


今天终于做了一个决定,就是要开始写自己的博客了,其实是想将自己的学习心得发表出来,以帮助那些需要帮助的朋友,我的一个小小的梦想就是把这些文章整理成书进行发行。

说到这个技术话题,其实范围是很广的,许多朋友在进入IT行业的时候肯定是一头露水,不知道自己将来是做什么,或者为什么选择了IT这个行业,在这里我仅举几个例子,说大的范围如 移动、研发、云计算、服务器、硬件、软件、操作系统等等,说这些其实也只是一个简单的介绍,具体到某一个具体的分类,还有许多,大家可以把它想像成一棵树,分了几个大的枝干,每个枝干又会分出许多小枝干,每个小枝干又会分出小小枝干,直到最后长出一些叶子,而每个叶子 也就可以理解成一种IT人员的工作类别吧,关于IT,其实有许多工作行业,大家可以登录51JOB等网站进行查看,而我们现在讨论的仅仅是windows平台下 程序设计这一小块的某个小枝节,大家不要以为做IT的就是编程,这是一种极不正确的理解,所以认识到自己是位于什么位置是非常重要的。这其实就是一种概念的体现,叫做分层,也可以理解成分工,有话老话说的好呀,社会需要分工,的确是这样的。

那么我们研究的现在是WINDOWS下的C程序语言,其实这个主要是让大家更好理解一下什么是程序,以及程序员是做什么的。就像普通工作一样,程序员也是分为三六九等的,大家看招聘网站的时候,有的公司可能会写招聘 某某高级软件工程师 有的可能是中级,这就意味着大家要摆正自己的态度。这就像是工厂招人的时候 会有 普工、焊工等分类一样,普工待遇低,技工待遇高。许多人感觉只要把技术搞上去就行了,其实不然,在计算机这个领域,技术是一个非常复杂的话题,不要以为你可以学到家。这个可以通过在国内查找一些资料就可以看出来,当你工作中遇到某个问题的时候,需要查找一些相关资料,国内基本上找到的文章就那么几篇,而且都是简单介绍,基本没有什么实用价值。有点扯远了,还是回到这篇文章上来吧。其实我标题写个01意味着这是一个系列,看完这个系列,我可以保证你对C语言的认识达到一个新的高度,但是你需要有一些基本的语法常识。好了,我们开始吧。

在开始之前,我希望大家随便找一本书,看看C语言的历史,这是上世纪70年代的产物,我们现在仍然在用,其实程序设计在国外50年代的时候就已经开始了,在国内基本上得到90年代左右了,所以,我们落后于其它一些国家,也就是国内对底层的了解不够,以至于到现在国内一直没有自己的操作系统、CPU等,又扯远了。

一句话,我们知道CPU执行的0 1 ,那么今天的第一个话题就得先解决掉 01 。

我们应该知道,进制只是一种表示方法,我们只是习惯于用十进制来表示数据,如 152 、 521、652541 等,这些都是十进制,也就是逢十进一的机制,当我们遇到电气特性的时候,比如电压有高低之分,也就可以用01来表示这种电气特性,当然我们没有必要研究那么深入,只需要知道这些就可以,而CPU 只认识 二进制,也就是CPU 在执行的是N多个01的组合,有关进制的转换我不打算再做介绍,希望大家可以找一些资料自己攻破这个难题,如 14 表示成 1110 ,我下面要介绍的是这里面的一些数据关系。

先来讲一个小插曲,说古代有一个国王,抓到了几个罪犯,要判他们刑,但给他们一次机会,国王给他们出了一道题,国王拿出1000颗珍珠,和10个盒子,让罪犯把这1000颗珍珠放入这10个盒子中,而且正好放完,等罪犯放好之后,国王的要求就是 国王随便说一个 1 - 1000 的整数,如 516,罪犯要从 那 10个盒子中 选中 若干个,让这几个盒子中的珍珠总数=516。 就这么一个问题,我们需要好好的来看一下,至于这个问题怎么解我就不做解释了,只是把答案放出来,这个就是 一个 以2为比例的等比数列,也就是:1248163264128256489(1000-前几个数的和) 如,67=64+2+1 516=489+16+8+2+1 随便一个1-1000的整数都可以写出一个唯一的求和公式。

其实大家自己写出来这个等比数列的和就可以得到一个结论,就是 前N 项的和等于第N+1项的值-1 如,1+2 = 4-1 1+2+4+8=16-1 这个特性希望大家一定要记住。

我们将其用指数形式表示出来


2^01
2^12
2^24
2^38
2^416
2^532
2^664
2^7128
2^8256
2^9512
2^101024
2^112048
2^124096



上面的这些数值大家也要背下来,其实并不难背吧,只需要记一些比较关键的就行了,如2^10=1024 那么2^9=? 除以2就是了,但这些数值一定要背会。

下面我们就可以用它来做一些简单的测试了,如2^0+2^1+...+2^64 = 多少? 结果 2^65-1 , 想不明白?你应该再看一下上面的知识了。

有了这些东西后,我们就可以来讲一些知识了,在计算机中,我们讲的是32位系统及32位的编译系统,最好就是用 XP系统下的VC6.0 ,我建议大家都用这个环境进行学习,

并不是因为它有多好,而是因为...,大家以后就会明白的。

c语言里有一个关键字 叫 sizeof ,它可以用来测试一种类型占用的字节数的大小。如 sizeof(int) = 4 sizeof(char)=1,这里就引出一个问题,字节数,什么意思,何解呢?

其实是这样的,我们知道我们的程序是需要被操作系统加载到内存中才能被CPU访问到我们程序中的数据和指令,那么内存这个东西就显得很重要了,我们可以在我的电脑上右击,选择属性,可以查看到自己的内存容量 ,如,2GB, 我们先来解释一下G是什么,它就像中国的K一样,1Kg=1000g 把g约掉,1K=1000 ,其实就是这么回事,1G=1024M,1M=1024K,1K=1024,明白了吧,我们在描述重量的时候用的是g这个单位,也就是说B才是内存容量的基本单位,GB只能算是一个扩展单位,那么再来讨论一下B,B就是字节的意思,1B=8b,b就是0/1的意思,也就是说,8个0/1的组合就是一个字节,内存的基本单位是字节,这里我还要再强调一下什么是单位,就是最小的了,没有特殊需要不要再拆分的意思,所以,我们以后会经常和B打交道,很少和b打交道,既然知道了我们的内存容量了,如2GB,512MB,或者3GB,或者4GB,那么我们总是可以把它换算成 xB,x代表一个整数,意思是我们现在有了很多多个字节的意思,那么怎么管理这么多个字节?这个问题可能不怎么好解决,我先来举一些例子,中国人多吧,13Y人口,政府怎么管理这么多人呢?***ID。那么多的汽车、火车、飞机怎么管理,车牌与航班号。宾馆有几十个房间怎么管理,房间ID。类似的例子太多太多,那么多企业,工商局怎么管理,那么多玩家,游戏运营商怎么管理,那么多数据包,TCP协议怎么管理等等。这些例子可以给我们一思路,我们只要给他们贴一个标签就行了吧,或者叫为每1个字节 赋一个ID 就行了吧,而我们的 CPU 在通过地址总线(可以理解为32根电线,每根上可以有高电压(5V)和低电压(接近0V)两种状态)访问的内存的时候 正好提供了这样32个状态信号,也就是32个0/1的一个排列,这样我们为我们每个字节都定义一个这样的ID就行了,这个ID就是传说中的 物理地址了啊。32个0/1的排列,意味着它的大小是4B,也就是在32位系统及32位编译环境中 我们的地址都是4B,有的人可能不理解为什么我说32位环境及32位编译环境呢?用过TurboC的人可能会知道,TurboC里的整数是2B,而在VC中是4B,所以要有这两个限定条件。举个例子,假如我们有 512MB(536870912B)的物理内存,我们就可以像下面这样子进行编号


0000 0000 0000 0000 0000 0000 0000 0000第1个字节
0000 0000 0000 0000 0000 0000 0000 0001第2个字节
0000 0000 0000 0000 0000 0000 0000 0010第3个字节
0000 0000 0000 0000 0000 0000 0000 0011第4个字节
0000 0000 0000 0000 0000 0000 0000 0100第5个字节
........
........
0001 1111 1111 1111 1111 1111 1111 1110第536870911个字节
0001 1111 1111 1111 1111 1111 1111 1111第536870912个字节



希望上面的数据不是很难理解,如果上面的东西你理解了,那么左边一列我们可以将其称为物理地址,大家可以总结出来的一条理论是 一个地址对应一个字节(很重要)

现在讲一下 2 -8 -16 这三种进制之间的转换吧,

我们知道 3 个2进制可以表示的范围是 0-7 ,4个2进制可以表示的范围是 0-15

1个8进制可以表示的范围是 0-7 , 1个16进制可以表示的范围是 0-15

所以我们经常会做如下的转换

如 2进制数据 如十进制数415242 转成2进制是(可以用电脑自带的计算器) 1100101011000001010 ,很显然 它不够32位,这就像我们写十进制数一样,在前面加0是没有意义的,所以将前面的0省略掉了,我们一般需要将它补齐

先来看转成8进制,先将数据分成3个3个的,从右边开始分,左边不够的被0,分完后如下

2进制001 100 101 011 000 001 010 现在我们只需要将每3个对应的8进制数据写出来即可了

8进制 1  4  5  3  0  1  2   1453012 这就是我们的结果了

再来看转成16进制,先将数据分成4个4个的,从右边开始分,左边不够的被0,分完后如下

2进制  0110 0101 0110 0000 1010现在我们只需要将每4个对应的16进制数据写出来即可了

16进制 6  5   6   0   A   6560A 这就是我们的结果了


那么如果你看到这里也没有什么问题,那么恭喜你,你已经进入C语言的大门了, 我没有说假话,You must trust me!


总结一下,我们现在学会了哪些,知道内存的基本单元是字节,知道系统管理内存字节们(请允许我加上们,表示复数)是通过编号的方式了,知道了 进制之间的转换方式了。


那么我们来看一下其它的一些概念

我们知道计算机是用来进行计算用的,那么数据是要存储在内存中才能被CPU访问到,但是,数据在内存中到底是怎么表示的呢?先来讨论最简单的数据,最简单的就是char型数据,我们知道用sizeof关键字可以统计出它占一个字节的内存空间,那么我们就拿这一个字节来说事,首先要纠正许多人的一个错误认识,我们知道在内存中的某个地址上的数据就是那个数值,但是它是什么意义我们却是无从知晓的。举个例子,我们举例的地址是 0X0012FF7C(其实就是2进制),如下


地址数值
0X0012FF7C41
0X0012FF7D5A
0X0012FF7E60
0X0012FF7F6B



就像上面的数据,是连续的四个内存地址,其值分别是十六进制 的 41 5A 60 6B ,但是它们是什么意义,我们却是不知道的。

当我们在 程序中定义一个char 型变量的时候,其实就是在申请了 1B 的内存,我们可以为它赋值,如 char c = 'A' ; 我们申请了1B,将这1B 赋以 'A' 对应的16进制。 还有一点要说明 char 和 unsigned char 是两种数据类型,是不一样的。

我们先来看 char 型 ,占1B 对应 8b

xxxx xxxx x=0/1

上面的表达式表示的就是通用二进制表示形式

我们知道,给定一个数值,我们可以写出其2进制,如 18 = 16 + 2  0001 0010 这就是它的表示形式,我们可以将这个数值放入内存中,但是这是不是一种通用的方式呢,换句话,用它们进行计算能符合计算公式吗?还有负数怎么表示呢?

先来看 -18 如何表示,计算机前辈们首先想到一个解决办法,我们可以把最高位(最左边的位) 拿出来,用来表示这个二进制表示的是一个正数还是一个负数,如

0000 0010 表示 +2

1000 0010 表示 -2

从上面,我们就可以看出来,这样子牺牲了数值的表示范围,因为 最高位不能用来表示 2^7 了,但这并不重要,重要的是它违背了计算规则,我们来看一下将两个数据进行相加 的结果


0001 0010+18
+1001 0010-18
=1010 0100-36


这个计算结果,按照我们上面的定义,首先是负数(最高位为1),数值(后面7位)为36 也就是 -36 ,所以说,它违背了计算规则。

看来计算机前辈走的第一步是不正确的,那么如何解决这个问题呢?

我们的目的是得到0 ,那么我们如何得到0呢?

我们知道


1111 1111-1
+ 0000 0001+1
=10000 0000注1


注1:这里,其存储长度为8b,最左边的1自然丢弃,这可能会是电气特性(我们不需要关心) ,这样我们就得到0了

通过上面的例子,我们只要能构造出一个 1111 1111 ,然后再加个1 就可以得到0 了。那么我们就要想怎么得到1111 1111 ,很显然,这一步是很简单的,来看下面的式子


0001 0010+18
+ 1110 1101将+18的各个b取反(就是将0换成1,将1换成0)
= 1111 1111



整合一下上面的等式


0001 0010+18
+ 1110 1101取反 得到的是 反码
+ 0000 0001是1
= 0000 0000是0



从上面这个等式可以得出 -18 了吧,-18 的表示应该是 将+18 按位取反再加1(得到补码),这样我们就可以统一了它满足了我们的计算公式。

现在,我想,我们应该知道补码是怎么得来的了,那么,我再告诉你一个秘密,计算机在内存中存储整型(整数类型 如char,short,int,long)的时候就是用这种方式存储的哦。

好了,现在,我现在再给一个例子,上面我们讨论的是8位的,现在我们来讨论一个32位的,-615427545,随便敲的一个负数

1、先求正数对应的二进制100100101011101010110111011001,然后划分成4个4个的,左边不够的补0


0010 0100 1010 1110 1010 1101 1101 1001下面按位取反
1101 1011 0101 0001 0101 0010 0010 0110再加1
+ 0000 0000 0000 0000 0000 0000 0000 0001
1101 1011 0101 0001 0101 0010 0010 0111再换成16进制
D  B  5  1  5   2  2  7DB 51 52 27


DB 51 52 27这就是四个字节(连续的两个16进制表示1个字节),在内存中存储的形式。

紧接着问题也出现了,这四个字节,如果放在起始地址为 0X0012FF7C 的位置上,怎么保存呢?

方式A


地址数值
0X0012FF7CDB
0X0012FF7D51
0X0012FF7E52
0X0012FF7F27


方式B


地址数值
0X0012FF7C27
0X0012FF7D52
0X0012FF7E51
0X0012FF7FDB


上面是两种存储方法,这东西并不是我们去实现,也不是我们去管理,而是由硬件决定的,不同的硬件平台会有不同的表示方法,我们所用的基本上是方式B,这种方式有一个名字,叫做小端模式,那么可想而知,方式A就叫大端模式了,那么我们怎么去记忆这些呢?其实我们只需要记住小端模式就可以了,有一个很好记的公式:高高低低,什么意思呢?地址是从0 到 一个很大的正数,那么也就有了大小,如上面,0X12FF7C 相对0X0012FF7F是小地址,也叫低地址,那么0X12FF7F就叫高地址了,DB 51 52 27这4个字节,也有高低之分,位于高位的叫高地址,也就是DB,位于低位的叫低地址,也就是27,那么你也就基本可以理解了,高地址存放高字节数据,低地址存放低字节数据,好理解吧,等我们以后学了指针,再来讨论如何判定一个平台是采用的什么存储方式。其实现在可以先说一下思想,我们就定义这样一个整数,还是刚才那个整数,如果我们可以取到 0X0012FF7C 这个字节对应的值,暂时定义为X,我们就可以做出判断了吧,如果X等于0X27 ,那么就是小端,如果X等于0XDB,那么就是大端喽,当然我们一般没必要定义这么大一个数据,一般用1来做这种判断就可以了,我们现在所不会的只是怎么取出来 X,这要我们学习指针后再来讨论了。如果是1的话,我们可以写出它的大小端存储形式


小端模式

地址数值
0X0012FF7C01
0X0012FF7D00
0X0012FF7E00
0X0012FF7F00


大端模式

地址数值
0X0012FF7C00
0X0012FF7D00
0X0012FF7E00
0X0012FF7F01

是不是很简单,很好理解吧。


理解了上面的知识点了吧,我们再来讨论一个问题,范围,如int 的范围,unsigned char 的范围

int 是有符号的,那么最高位就是符号位,我们知道,右边31位中 如果某一位的值为1,那么它对应的值就是 2^n,但是符号位代表的是多少呢?其实跟右边31位是一样的,只不过代表的值是负的,什么不理解?那么我来解释


0000 0000 0000 0000 0000 0000 0000 10001代表 2^3
0000 0000 0000 0001 0000 0000 0000 00001代表2^16
1000 0000 0000 0000 0000 0000 0000 0000注2


注2:这里的1代表 - 2^31 ,如果这是unsigned int 呢?那么它就代表 2^31 很容易吧


那么我们来讨论int的取值范围
最大值:int有正负之分, 首先可以确定的是最高位为 0 ,表示正数,后面的每个位都是 >=0的值,那么肯定是当所有位为1的时候,此值最大,如下

0111 1111 1111 1111 1111 1111 1111 1111 其值是多少?要用上面的公式哦, 2^31-1 不理解?重头再来一遍吧

最小值:肯定是负数哦,最高位为1,后面的每个位都是 >=0的值,一个负数加上任何正数都会使本值变大,所以后面 31 都应该为 0,如下

1000 0000 0000 0000 0000 0000 0000 0000 其值是多少,不用解释了吧 -2^31

上面可以得到32位有符号整数范围[ -2^31 , 2^31-1] , 换成16位,8位我想你也可以求出来吧


有符号整数位数最小值最大值
32-2^312^31-1
16-2^152^15-1
8-2^72^7-1


那么无符号的呢?无符号意味着32个位都表示数值,所以最小为 32个0 ,最大32个1(值:2^32-1 还是公式哦)


无符号整数位数最小值最大值
3202^32-1
1602^16-1
802^8-1



在上面的讲述中,我用了一个词,物理地址,不知道大家在阅读的时候有没有注意到,为什么要加上物理两个字,这是因为还有另一个地址,叫逻辑地址,那么逻辑地址是什么意思?我们需要先来讨论一下操作系统,我们的操作系统是在运行许多程序,你听着歌可以浏览网页,可以打字,可以聊天,可以看视频,当然操作系统的内核代码也可由CPU来执行,如果将用户写的代码和操作系统的代码都直接爆露给用户,那么我们的应用程序一旦出现问题,你应该遇到过程序弹出错误对话框的情况吧,它所占用的内存数据可能会导致别的物理内存空间里的数据出现错误,这就导致其它应用程序也崩溃,最严重的是导致操作系统的内核代码被错误执行,遇到过系统蓝屏的情况吧,一般是一些硬件驱动等与内核通信错误导致的,所以,我们的系统现在都是运行于 一种称为 保护模式的 模式下,那么保护模式保护什么呢? 保护操作系统内核代码不被用户程序错误的访问,保护程序A的数据不被程序B异常访问。那么操作系统现在就全权接受了这个任务,也就是我们的应用程序运行在自己的逻辑地址空间中,那么这两种地址之间是什么关系呢?其实逻辑逻辑讲的就是一种思维上的存在,它的范围(0 --- 4G-1),它实际上还是用的物理内存,只是操作系统爆露给用户的是一个别的地址,我们来看一下下面的数据


物理内存地址


A段(对应于物理内存中的某一段连续的内存空间)(0X00561000---0X00562000)
B段(对应于物理内存中的某一段连续的内存空间)(0X00578000---0X00579000)
C段(对应于物理内存中的某一段连续的内存空间)(0X00581000---0X00582000)
D段(对应于物理内存中的某一段连续的内存空间)(0X00564000---0X00566000)
E段(对应于物理内存中的某一段连续的内存空间)(0X00583000---0X00588000)
F段(对应于物理内存中的某一段连续的内存空间)(0X00661000---0X00662000)
G段(系统内核使用,地址我们就不指定了)由系统定义


程序A逻辑地址空间

0

1

2

....

....

4G-1


程序B逻辑地址空间

0

1

2

....

....

4G-1

程序A和程序B都需要物理内存,但是这些内存是由操作系统负责管理的,也就是A可能用的是A,C,E段,B可能用的是B,D段,但是A中A,C,E这些段的地址是逻辑性的,是由CPU的MMU采用某种算法映射到A中的,同样B也是,但是A与B的空间具有独立性,是不相关的,因为再返回物理内存中,对应的是由操作系统管理的ABCDE段,也就是说在A中是访问不到在B中的数据的,这样就起到了对程序数据的最基本的保护了,其实,在这4G的地址空间中,真正可以由用户访问的也只有2GB,当然可以启用大地址,大家可以百度一下3GB大地址,一般服务器可能会用,但是现在的计算机配置已经很高了,基本上不怎么使用,这2GB的数据是低2GB,那么高2GB的地址空间用来干嘛呢?是操作系统运行代码的映射,也就是大家可以理解成上面的G段映射到AB 的高2GB 地址空间了,用户是无法访问的。到这里把物理地址与逻辑地址做了一个基本介绍,但是这个介绍是非常简略的,也就是只提供一个模型,模型大家都理解的吧,我就不多解释了。

说了那么久的程序,到底什么是程序呢?程序是指令与数据的集合,我们写的就是指令,指令来处理各种数据,也就是算法,那么这些只是小程序,大的程序,一般称为解决方案,知道为什么许多公司要你们提供解决方案嘛,说的就是这个东西,一个解决方案可以包含多个项目,每个项目完成一部分工作,现在VS系列(VISUAL STUDIO 2003 2005 2008 2010 2012 2013) 都是这种模式。我们暂时不用讨论那么复杂的,只讨论一些简单的就行了。

一个程序被操作系统加载到内存之后,系统主要是为它分配内存,然后将物理内存地址映射成逻辑地址空间,我们以后在讨论的话,要记住都是在逻辑地址空间中的哦。


如果上面的东西你可以解理,那么你是非常强的。01里暂时就介绍这么多。