C语言学习笔记
最近、花几天时间学习了C语言,不难、指针概念卡我大半天,水平也只是看得懂基本代码。打算玩一个操作系统(汇编+C)来提高自己的C语言水平。
万物都是容器,容器的符号是U,对象就是单个容器的泛称。一切皆对象,具有某些相同属性特征的对象归纳成类。对象(Object)是类(Class)的一个实例(Instance),类是对象的模板。如果将对象比作房子,那么类就是房子的蓝图。我们以自然语言去描述世界,而计算机是用各种数据结构容器去描述世界。一个茶杯容器可以装茶水、可乐、油、米等等无数不同种类的东西,而计算机世界的容器及种类却简单太多、太多了,只有装二进制数据的位容器。最简单的位容器只是1位(bit):0或1,这一位二进制容器可以用0表达为假、1表示为真,等等。其次、就有8位的b(称为字节byte)、16位的z(称为半字z)、32位的w(称为字word)、64位的dw(称为双字Double Word)、128位的、256位的、等等位容器。数据可以用x个二进制位的位容器BUx来表示。对象具有状态、方位、时间等属性,每个属性是用数据值或说位容器BUx或带方向的动态向量集合(V[V0,V1,…,Vj]表示有j个元素的位容器向量)来描述,最后构造了描述对象属性的数据表。对象还有操作,用于改变对象的属性,操作就是对象的行为。操作也可以说是方法或函数,方法就是一段指令;一条机器指令码或是用一个数据字w表示,所以、方法就是一个指令字数组。由一系列方法构成的指令数据表就描述了对象的行为。所以,一个对象就是由属性数据表和方法数据表来描述。
(给变量起易记名字):
typedef int8_t s8; // 8位有符号整数,最高位为1、是负数,反之为正数。8位二进制最多表示256种状态2^8 = 256,-128---127。
typedef int16_t s16; // 16位有符号整数,-32768--32767。typedef为创建别名,c标准名称是int16_t,别名s16(起短名字可以少打符号)。
typedef int32_t s32; // 32位有符号整数(Signed number),表示范围:-2^31(- 2147483648)---(2^31 - 1) (2147483647)
typedef int64_t s64; // 64位有符号整数(Signed number),表示范围:-2^63---(2^63 - 1)
typedef const int32_t sc32; // const是只读变量的意思,sc32意思就是32位有符号整数只读变量。
typedef const int16_t sc16;
typedef const int8_t sc8;
typedef const int64_t sc64;
typedef __IO int32_t vs32; // __IO表示 变量是随时被改变的(volatile),不要进行编译优化,以免出错。如外设寄存器等情形的定义。
typedef __IO int16_t vs16;
typedef __IO int8_t vs8;
typedef __I int32_t vsc32; // _I表示只读权限的32位有符号整数常量类型。
typedef __I int16_t vsc16;
typedef __I int8_t vsc8;
typedef uint32_t u32; // 32位无符号整数int(integer),表示范围:0--2^32(4294967296)。
typedef uint16_t u16; // 或者用于16位字符表示wchar_t,如Unicod字符编码UTF_16。
typedef uint8_t u8; // 或者用于8位字符表示char,如ASCII 字符編碼7位(最高位为0)。
typedef const uint32_t uc32; // 32位无符号整数常量int(integer),表示范围:0--2^32(4294967296)。
typedef const uint16_t uc16;
typedef const uint8_t uc8;
typedef const uint64_t uc64;
typedef __IO uint32_t vu32;
typedef __IO uint16_t vu16;
typedef __IO uint8_t vu8;
typedef __I uint32_t vuc32;
typedef __I uint16_t vuc16;
typedef __I uint8_t vuc8;
typedef float f32; // 单精度浮点数,表示范围:+-3.4028234 * 10^38。
typedef double f64; // 双精度浮点数,表示范围:+-1.7976931348623157 * 10^308。
位容器通常以大量字节集中于“内存”,CPU是用内存地址(如32位地址线A31–A0可表达2^32个单元、每个单元是一个字节b。)来标识一块内存区域。令A0=0,也可以有单元为半字;A1A0 = 00,也可以有单元为字;…也可以有单元为8字r(32字节、表示为行row)、或有扇区s(512字节、磁盘单位)、或有页page(8个扇区、4Kb)等等。单元内容表述的数据可以是不定的、可变的,我们用变量来标识内存单元,给变量起个易记的名字(标识符identifier)、就是变量名。机器码中,是不会出现变量名的;出现的是逻辑相对地址,给出地址、读或写单元的内容;变量名是给我们程序员易于记忆和编写代码而使用的;简单说,变量就是指其对应地址的数据内容。
◆寄存器:用于自动变量,如方法里的局部变量。ARM的用户进程可用:R0—R7(8个32位通用寄存器)。函数调用时用R0–R3,返回时R0、R1。
◆栈:存放临时的基本类型数据、方法的局部变量。
栈是会自动清除或覆盖数据的,适合自动变量。对堆内存的操作和对栈内存的操作速度是一样的,比寄存器操作稍微慢点,即使是动态内存分配和释放不过是约1us。栈中的数据大小和生命周期是可以确定的。通常一个进程内只有一个用户栈,所有的对象(包括线程对象)都是共用一个栈。栈的大小,是全局变量;默认是2个页,256行;如果对象数目多或很多线程,就可设大些。对象消息处理代码块内只是一些少量的自动变量,普通对象完成消息处理后就会自动清除,相对来说不占空间;而线程对象可能会因等待消息,从而暂时让出CPU,但并不让出占据栈内的那部分空间;就可能会同时有多个线程占据栈空间,解决办法就是增大栈空间。栈溢出或许会造成死机,因为溢出后、会破坏其它进程的数据,造成混乱。是故,用户进程的大数组等等、较大的位容器变量就不要放入栈中,以免溢出。这种情形,还是得操作系统去防护(监测用户进程PC和SP)。
◆堆:存放用动态产生的数据
堆内存用来存放由动态创建的对象和数组等动态变量。 在堆中分配的内存,由内存管理员来管理或由用户代码释放。动态定义向量或说动态数组u32 A[N]是可以的,可能会使用到连续的数据块或数据块链表;但内存总是有限的,还不如用数据流,只是开辟一个数据块窗吧,数据流可以无限。动态内存的生存期由程序员决定,使用灵活,但如果分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏。频繁地分配和释放不同大小的空间将会产生内存碎块。优化取决于好的“内存分配算法”,linux采用的是“Buddy(伙伴)算法”来有效的分配与回收页面块;我看了代码和介绍,只能算半懂;如果我连续申请n多个3页(3*4Kb)内存,会产生好多1Kb的空洞碎片,而且、这算法代码也太复杂了一些。
在代码块中以表声明、或以向量、或数组等等声明的变量都放到堆区,都当作是动态对象,系统会返回分配的开始地址。由于动态变量的地址,编译器无法预先定义,所以是不会把动态变量翻译成地址的。在堆中产生了一个动态数组或动态对象后,还可以在相应对象属性表中定义一个特殊的变量;让对象属性表中这个变量的取值最终找到数组或对象在堆内存中的首地址,对象属性表中的这个变量就成了数组或对象的引用变量。 引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用对象属性中的引用变量来访问堆中的数组或对象。引用变量是普通的变量,定义时在对象属性表中分配;引用变量在程序运行到其作用域之外后被释放(还在的,但如果释放内存,会变为空指针),而动态数组和动态对象本身在堆中分配。动态数组和动态对象在没有引用变量指向它的时候变为垃圾,不能再被使用,但仍然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走(释放掉)。所以,用户对象退出时调用析构方法,而析构方法要包含释放对象内存代码。
◆常量池:存放常量 (一般放在方法(代码)表区、通常放在FLASH区)
数组是相同类型元素(元素可以是C基本类型或者结构体、数组等等)的位容器集合,结构体是不同类型元素(元素可以是C基本类型或者结构体、数组等等)的位容器集合。u32 A;// 无符号32位整形变量A也可以视为一个32位的位图变量。更大的位图容器可表示为字数组,如256位图的A变量:u32 A[8];// 无符号32位整形数组变量A也可以视为一个32X8 = 256位的位图容器变量。要创建一个数组,你必须首先定义数组变量所需的类型和元素个数(长度len),数组下标是从0开始的,故其下标索引最大值为len - 1。需注意下标运算不要超过其最大值,会让程序崩溃的。在代码块中,也可以有“非常量表达式”的可变长度数组。字符串是连续的字符,最后是“\0”空字符,字符串长度就是字符的数量。字符串被存储在元素类型是char或char_t
的数组中,数组的长度需大于字符串长度。多维数组实际上是数组的数组,char screen[10][40][80]; // 一个三维数组;数组screen包含10个元素,从
screen[0]到screen[9];每个元素都有40个一维数组,然后每个一维数组都有80个字符,数组总共有10X40X80 = 32000个字符。想要读写三维数组内的某个char元素,必须指定3个索引下标值,如screen[1][2][3] = ‘z’; 。结构体struct内每个元素都有不同的位置,而联合union每个元素都共享内存位置。
文件是复杂数据容器集合的一种组织形式,通常由描述文件属性集的文件头结构和文件体元素数组组成,文件也可以只有文件头,在类Unix系统中一切都是文件。文件头是由一些属性字段元素组成的结构,通常有:文件名字,标识符,类型,文件权限,标志,大小,位置,方向(节点),时间戳等等。字段的元素可以是C基本类型或者结构体、数组等等,由1个或多个字段所组成的集合称为记录,故文件头也可看作文件的属性记录。如果文件体元素数组的元素只是简单的C基本类型变量,则被看成是一个字符流文件、或者一个二进制文件,又称为流式文件。反之,如果是记录类型数组,则称为记录式文件或者称为表文件。记录式文件或说表文件的文件体可称为“表”,表中每行为一个记录,记录也可以拆分为字段、视为列;这样,每个表由行和列组成。其实,流式文件也可以看作为只有一个简单字段列的“孤立字段表文件”。而记录表文件可以视为多个“关联字段表文件”的合成体。是故,文件体可以视为表,而文件与表文件等价、看使用者的视角而已。数据库DB(data base)是存储数据表(data list/sheet)的集成仓库,可简单看作一个数据库目录,包含一堆数据表文件和相关文件。这样,文件目录系统可以看作数据库、反之也成立;设备驱动库、甚至系统内核(内存管理,进程和线程管理等等)也可以看作数据库;如此一来,总的编程代码量将大幅度减少。我是反对C++将方法封装到类里面,会使总代码量巨增;就好比将切空心菜的方法封装到空心菜类、切白菜的方法封装到白菜类、、、等等,简单的方法却变出无数的方法;除了使代码臃肿外,没多少用。一个人的精力是有限的,超过十万行代码量,我通常是放弃阅读(我英文基础差)。linux系统一切皆文件的思路是不错,但为何最终整出约1千万行的源代码、令我费解。对于任何事情,你可以简单化处理、也可复杂化处理。生活中,六星级、七星级甚至更高星级难度系数的事情比比皆是,对于通常是五星级难度系数以下的这些计算机技术或产品开发等等事情,一定要尽量简单化、细致啰嗦严密处理!
茶杯装有水,水装的是水分子、而水分子装有原子、原子装有、、、,一种容器套装另一种容器,这就是具有层次的树状结构的事物表现。事物都是具有层次的树状结构,所以对象描述文档也应是一种树结构,它从“根部”开始,然后扩展到“枝叶”,对象描述文档必须包含根节点。在节点树中,顶端的节点成为根节点,根节点之外的每个节点都有一个父节点。每个标签行是一个节点或叶子(属性),叶子是没有子节点的属性标签行,同级节点(兄弟节点)是拥有相同父节点的节点。根节点是树中唯一一个没有父节点的节点。对象是很复杂的,一个对象会从多方面去描述从而形成多种描述文档构成的文档包。文件目录系统、设备驱动库、系统内核库、数据库,等等都可看作是对象描述文档包,就连C语言代码、汇编代码、HTML文档、XML文档等等也都是一种对象描述文档。我们用BV(bit位, vessel容器,vector向量)来表示位容器向量,BVS(set集合)表述位容器向量集,数组只是位容器向量集BVS的一个特例。BVS V[N]; // 声明有N个分量的向量集变量V,向量是有方向的变量;V0、V1、V2、…也可各自表示为不同的BVS;从而构成2维,3维…多维的位容器向量集。一切变量、类、对象、对象描述文档、代码,等等都可视作向量BV或向量集BVS。BVS在空间位置或时间顺序上不一定是有序的。所以,尽管我们用有序的容器V来表示BVS,并不表示是BVS的时间或空间顺序。当然,可依一定的条件对向量容器集作排序。如X[X0,X1,…,XN ] = f(V[ V1,V2,…,VN ]),或简写为:X = f( V )。设V0表示一个根容器向量(根节点),而V1就生长在V0的某个分支上,V2又是生长在V1的某个分支上,…,就类同一棵树。对象描述文档等等就可以用V向量集来表示。 操作系统就是一个程序类的树容器向量集,内核向量集V0根、用户进程向量集V1生长在V0的某个分支上(时间管理员.用户进程调度),用户进程的线程向量集V2生长在相应的V1分支上。
我的初步开发规划:根就是内核任务,设想是包含有32个内核线程;将来会是独立CPU运行内核(用户进程运行在其它CPU),内核线程是根据内核线程32位图相应位为1时而运行到阻塞(进入时相应位图标志清0)。内核线程32个:前后台系统。实时中断响应事件,轮询(或说按消息)处理事件、执行到阻塞。主要有5个内核基本线程(包含中断下半部,用户实时线程也可以在这里):
1)、时间管理员(不同种类对象有它的时间定义或理解、称为时间片,不同类型的时间片可以不一样。核外用户进程调度和定时器管理等)。
2)、空间管理员(没有空间一切为0,只有时间而没有空间、只能说该对象曾经存在过,为虚对象。对象一生中的属性动态变化形成一些结果,对同类虚对象结果的分析形成经验。内存管理、FLASH管理,数据段和代码段管理、磁盘空间管理、加解密等等)。
3)、生发(生长发育)管理员:节点树结构,动态的生长和变化。文件目录管理、设备文件管理、 数据库管理、对象描述文档管理。
4)、通信管理员:负责对象间的通信。进程和设备相互之间的通信,网络通信,SNP通信(4字结构)协议。
5)、人机界面管理者:人机交流(自然语言语音等)。简单的输入(键盘鼠标等)输出(显示),内核SHELL线程。
用户软实时进程32个(0–31):前后台系统。实时中断响应事件,优先级抢先(内核返回时判断)轮询处理事件、执行到阻塞。也可其中部分设置为普通用户进程。普通用户进程最大4064个(32–4096):多任务系统。实时中断响应事件,动态优先级(1–230,0为不参与调度、或进程没创建或睡眠或阻塞挂起等等)抢先处理事件、时间片调度。(优先级数值越大越高级)
用双向链表进程控制块PCB结构来动态管理最大4096个进程,好处是自动伸缩、动态内存管理,进程数量少时可以节省些内存空间。一个进程或创建、或从挂起(睡眠或阻塞)变就绪等情况变化时,会与当前进程优先级比较、如大于则置需调度标志。软实时进程就是固定优先级为231–255,不会如普通进程那样因为每次调度得到时间片而减1,只是阻塞时自行设为0。如何更高效地做优先级比较、还得慢慢思考,不会一下就可以定好(如果当前进程挂起,如果没有就绪的进程了、等等,还有一堆情形需考虑,ARM一条并行指令可比较4对字节)。
个人观点,人机交流层或说应用层较为适合C语言(有现成的,如web服务、浏览器、等等,期望能找到相应的简洁C接口模块);其它全部用汇编写,linux或许用C编写是错了,哪有如此多的代码?我想象应该会小于3千行指令吧(不算应用层,我会想法简化MAC等外设驱动设备树的代码)。呵呵,很快我就会学习ARM的汇编了,到时也写一篇学习笔记。单片机应用只是使用内核线程就足够有余(没有限制、充分发挥,实时性最好),只要MPS、无需用户PSP,没必要复杂化。应用于我的产品开发“飞鸡3号:1600W纯数字功放电源PFC+LLC”的STM32H750VBT6单片机: 文件设备驱动?不用,那样复杂了;编写网卡驱动?是用别人的SPI口(或SDIO口)WIFI模块;内存管理?自行分配还更好一些。只用32个中的一部分内核线程,而5个内核基本线程留着位置、不写,有闲空时会规划一下。呵呵,实际单片机应用没那么复杂,往简单走。用stm32标准库或HAL库?那想多了,STM32H750的HAL库.h+.c文件、数量有160多个文件,估计有十多万行代码,不知会有多少函数和新名称;不适用于我这类英文小白,直接忽略掉、不看不用,标准库也一样;项目正好用来学习汇编,知道外设基地址就可以了,当然也得熟悉各种外设的基地址和寄存器及相应位的功用;STM32H750有中文参考手册,够用了,预估总代码量约500条指令。
每个内存地址对应有一个内存单元,单元内容表述的数据可以是不定的、可变的;我们用变量来标识内存单元,给变量起个易记的名字、就是变量名。某个变量A,如果是C语言的基本位容器类型,比如u8、单元内容就是一个字节,如是u32、单元内容就是一个字,如是f64、单元内容就是一个双字且表示为双精度浮点数;不同类型变量单元的对应内存地址,也只能是变量的首地址!变量A = 变量A的内容【A】= 变量A对应地址的单元内容【&A】,这三者的说法等效,【】只是我为了好理解而外加的符号。指针变量pi就是一个内存地址,或说指针变量的内容【pi】= 地址。32位系統、指針為32位地址值。我们使用“u8 *pi”声明指针变量,就是说pi所指向的单元内容为一个字节变量,或说內容的內容【【pi】】= *pi 为一个字节变量。pi = &i;賦值变量i的地址給指針變量pi,pi指向變量i的地址、或說pi的內容是變量i的地址(【pi】= &i),【【pi】】= *pi =【&i】= i。例子:
u32 *(*p(u16))[3]; // 先设X0 = (*p(u16)),有u32 *X0[3],X0是指针数组、数组的元素是指向32位无符号整数的指针,或说数组元素内容的内容是u32类型;再设X1 = p(u16),说明p是一个有类型u16参数的函数,X0 = (*X1)、说明函数p的返回值X1是一个指针,其指向的内容X0是一个指针数组。所以p 是一个参数为类型u16且返回一个指向u32类型的指针数组的指针变量的函数。(*X1)的()只是为了改变优先级。
int **p; // 同理,X0 = *p,int *X0、X0是一个指向整型数据的指针,p是一个指向X0的指针。所以p 是指向整型数据的指针的指针。
网上的说法、例如:
int *p[3]; // 首先从p 处开始,先与[]结合、因为其优先级比*高;所以p是一个数组,然后再与*结合,说明数组里的元素是指针;然后再与int 结合,说明指针所指向的内容的类型是整型,所以p 是一个指向整型数据的指针数组。
int (*p)[3]; // 首先从p处开始,先与*结合,说明p 是一个指针然后再与[]结合(与"()"这步可以忽略,只是为了改变优先级),说明指针所指向的内容是一个数组;然后再与int 结合,说明数组里的元素是整型的。所以p 是一个指向整型数组的指针。
int i = 50;
int *pi = &i; // 一个指向整数的指针变量 pi、且pi = &i。 *pi = 【【pi】】= 【&i】= i = 50。
int **ppi = π // 一个指向整数的二级指针变量ppi = &pi。*ppi =【ppi】=【&pi】= pi = &i,**ppi =【*ppi】=【&i】= i
对于指针概念,我是有点头晕,只好牢记“指针就是地址”。
数组名其实也就是指针,但数组名只是一个指针常量,不是变量!其值是不能修改的,因此不能类似这样操作:a++。
我们看下stm32单片机:
#define ADC1_BASE (AHB2PERIPH_BASE + 0x08000000UL) // ADC1_BASE是ADC1的基地址,挂在AHB2总线下。UL:unsigned long
typedef struct // #define宏的被替换字符串都加上()会好些。
{
__IO uint32_t ISR; /*!< ADC interrupt and status register, Address offset: 0x00 */
__IO uint32_t IER; /*!< ADC interrupt enable register, Address offset: 0x04 */
...
__IO uint32_t GCOMP; /*!< ADC calibration factors, Address offset: 0xC0 */
} ADC_TypeDef; // ADC的通用结构。我想、将所有外设看作一个大的字数组u32 IO[],也是一种方案吧。汇编干脆直接操作、地址+偏移。
#define ADC1 ((ADC_TypeDef *) ADC1_BASE) // (x *)强制转换ADC1_BASE(UL数)为基地址为指向ADC_TypeDef结构的指针,别名ADC1。
ADC1->CR=0; // CR寄存器清零,DEEPPWD清零,从深度睡眠唤醒。也可以写作:(*ADC1).CR=0;
个人观点:C语言的指针概念容易搞混,人发晕、易出错,不如汇编的直接地址内存操作清晰。C语言去掉指针,换为引用或别的简单替代品会更好。
我一个C和英文的双料小白,从没想过要精通C语言编程。学习C语言、只是为了能大体看懂别人写的C代码而已。这篇学习笔记,有错难免,仅供参考。今后,我的重点是学习汇编语言。