您的位置:首页 > 编程语言

《斯坦福:编程范式》

2015-07-04 22:12 155 查看
看了该课程的部分内容(1到13课),按照课程所做的笔记。如果有误请提出。课程内容可以概括为:操作系统在C语言的基础上使用的内存模型。

第5课

空指针类型是弱类型不能参与运算(但可以赋值,即传递地址)。可以使用(char*)强制转换后运算

首先说明一下C程序在运行时, 不同的内容或变量分别存储在什么地方:

分了几块区域分别是, code, constants, global, heap, stack; (内存地址从低到高)

其中constants存储常量(常量值不允许修改),

global存储在所有函数以外定义的全局变量(全局变量允许修改),

heap是一块动态内存区域(可存放持久化内容, 不会自动释放内存),

stack存放函数内的本地变量或者说局部变量(函数执行完后本地变量占用的内存将自动释放);

“aaa”:c类型字符串实际是一个char *类型,即指向char类型的指针。有相应的赋值语句char *pc=“aaa”;pc即为“aaa”的初始地址。

所以,下面关于char *类型的数组,或者说是char **类型的指针的内容就容易理解了。

char *notes[]={"Cb","F#","Gb","Eb","D"};//notes为一个存储char*类型的数组,即notes[3]代表”Eb“的地址(char *)的值,所以notes为char *地址的地址:char **

//这些常量字符串作为常量被分配在constants中,他们的类型实际为char**

这样解释:notes变量名是一个数组,他存储的是char *类型

char *favoriteNote="Eb";//指向“Eb”的指针favoriteNote

强制转换:

void *vp1;

char *s = *(char**)vp1;??????

函数和方法的区别在于,方法将与其相关的对象的地址,作为一个隐含参数(这个参数成为this指针)。

而函数不会有隐含参数。

栈的实现分配了一个stack的结构体,记录的栈的元素个数栈顶元素和栈的初始地址,可能包含类型等等。

assert(true/false);用来测试条件是否成立,不成立则报错。

stack栈的内部实现:

//stack.h

typedef struct

{

int *elems;//保存元素的地址

int logicalLen;//已存在的个数

int allocLen;//可用的内存空间所分配的个数

}stack;

void StackNew(stack *s);

void StackDispose(stack *s);

void StackPush(stack *s);

void StackPop(stack *s);

//main.c

stack s;//分配空间

StackNew(&s);//初始化栈的成员

for(int i=0;i<5;i++){

StackPush(&s);//进栈

}

StackDispose(&s);//删除栈

//stack.c

void StackNew(stack *s)

{

s->logicalLen=0;

s->allocLen=4;

s->elems=malloc(4*sizeof(int));

assert(s->element != NULL)//确保成功空间分配

}

void SatckDispose()

{

free(s->elems);

}

第六课

接上一课。

void StackPush(stack &s,int value)

{

if(s->lengthLen==s->allocLen){

allocLen*=2;

s->elems=realloc(s->elems,allocLen*sizeof(int));

assert(s->elems != NULL);

}

s->elems[s->logicalLen]=value;

s->logicalLen++;

}

int StackPop(int value)

{

assert(s->logicalLen>0);

logicalLen--;

return s->elems[logicalLen];

}

//注意地址中的logicalLen是从0开始,而记录是从1开始。比如有2个数据,保存在0,1地址中,logicalLen为2

在分配内存之后调用assert断言,在不允许free的地方free会把内存搞乱:

s->elems=malloc(4*sizeof(int)); assert(s->elems != NULL);//malloc返回值是void* 分配失败返回NULL。

c中大部分情况都不会初始化内存。

c中realloc(void*, unsigned int) 创建一个更大的空间并将原空间有效的内容拷贝到新空间中。失败返回NULL

第七课(40分--53分钟:malloc、free)

栈(stack):

栈的行为:在RAM中为运行的程序分配内存,程序的局部变量(包括main函数中的变量)被存储在栈中,在栈中存在一个阀值用来指定当前可访问的变量。

如果调用某一函数时,主函数的变量就会被屏蔽相当于函数进栈!我们可以访问的只是栈顶的变量(除非用特殊方式访问其他层次的变量),当函数返回时,相

当于出栈,函数中的变量被释放。相应的我们可以直接访问的栈顶就会改变。

命名为栈的原因:"最近调用的函数,会先于其他函数返回之前返回,除非该函数又调用了其他函数。因此它被称为栈"

汇编语言会在函数调用和函数返回之前,对栈的顶端进行加减。

堆(heap):

在RAM中会分配一些空间给程序,这些空间称为堆。实际上只是一块任意的字节。

栈不同的是,栈完全是通过硬件管理,一般是汇编代码管理。堆的空间是通过软件管理的,如malloc、realloc、free就属于这里的软件。

强调一点:堆内存是一个很大的线性的字节数组。

堆的空间有空间链表管理,在堆得每一块空闲链表的前端会划分出4字节或8字节的区域来记录该空闲断的信息:大小和下一个空闲段的地址。

汇编代码存在与另外的RAM内存部分中。

第八课

第9分钟【问题】:堆是线性数组,如何判断free中的参数是堆中合理的空间?

解决方式,某些时候,DeBug会记录下来分发出去的void *,每次free时检测是否是分发出去的void *,通着这样的匹配来确保不会free错误的内存。

堆的内存分配策略:最快、最佳、最差适配、连续分配等。

堆的占用空间释放后,空闲链表会重新连接成合适的形式。

堆的压缩(垃圾回收机制)

【26分】在堆中会将堆内存分成两部分,一部分用于malloc,realloc为用户分配内存,另一部分存放分配的句柄,系统通过句柄来管理内存。

当堆内存管理器压缩内存时,更改了相应的句柄的数据。所以内存压缩和双重解引用(获取句柄的地址)是不能同时进行的。

【句柄由系统分配的一个列表控制。所以第一部分分配的内存,实际由一个void**的指针控制。这些部分由堆内存管理器管理。】

当需要压缩的时候,通过列表操作句柄即可。

【为什么】堆内存的压缩和双重解引用是不能够同时执行的?

答:内存压缩的过程中,不确定句柄是否更新,这时用户通过双重解引用所获得的句柄的数据是不能被确定是否有效。毕竟真正的数据在内存中移动。

解决堆内存的压缩和双重解引用不能并发执行的方法:

当需要从使用一块堆内存时,应该对内存块加锁,即压缩进程不对加锁的区域进行处理,处理完后解锁,继续执行压缩进程。

压缩是由一个低优先级的进程控制,当空间不足是,该线程的优先级会提高。

栈会分配在较高的地址空间里。

栈的函数调用将会影响指针的地址,也就是类似的压栈操作,调用函数后该函数在栈中并不是被撤销,而是被屏蔽(即函数的数据不能被用户直接使用)。

在代码段中存储的是与c/c++对应的汇编代码。

第九课

汇编中不可以在一条指令中包含store(存储)和load(读取)操作。

如:不允许在两个内存之间交换数据。

因为需要将汇编代码以某种方式编码成4个字节来保存编码内存操作数的原地址、内存操作数的目的地址是很困难的。

汇编可以指定读取的字节数,从指定地址开始4个字节是默认方式。

汇编代码运行是由PC寄存器指定,每个时钟周期会运行一个汇编指令,之后PC计数器会指向后面4个字节。

int i;

int array[4];

for(i=0;i<4;i++)

{array[i]=0;}

翻译成汇编:

M[R1]=0;//对i初始化。R1指向i的地址

R2=M[R1];//将i的值传入R2中

BGE R2,4,PC+40//如果R2的值大于等于4,则从下面40个字节开始:1行代码4个字节,10行40字节

R3=M[R1];//R2用于循环,重新用R3记录i的值,来表示数组下标。

R4=R3*4;//4是int的字节数,R4实际记录目的地址相对于数组基地址的偏移量。【问题】每次循环R3应该+1才对啊???而下面操作没有对R3赋值加一的操作

R5=R1+4;//&array[0],R1为i的地址,而array[0]比i高4位

R6=R4+R5;//&array[i],当前循环的偏移量

M[R6]=0;//将array[i]赋值为0

R2=M[R1];//【问题】??R2本来就存着R1对应地址的值(见第2行),为什么还要赋值?

答:相当于每次循环都会用R1对应的值对R2初始化。

R2=R2+1;//用来判断BEG那一行的条件,相当于i++

M[R1]=R2;//这一句和上面两句表示i++,(i+=x的通用表达式)因为R2是判断循环的终止

JMP PC-40;

i--;

当我们书写一个很复杂的取地址,或者包含了强制类型转换的赋值语句时,编译器最终都会翻译成一个简单的M[R1]=number的汇编代码。

第十课

在用方格描述变量在内存中存放的顺序时,下方总是低地址,上方是高地址。

递归在汇编中的实现,以及内存的分配和回收顺序。

第十三课

编译成.o和链接成.amt,前者生成汇编代码,后者将所有.o文件合并成一个程序。

当.o文件中的函数没有定义时,gcc编译器会在标准库中查找。但标准库中的预定义不会找到,所以未声明assert会报错

编译时对所有未知函数的返回类型默认为int

大尾系统中对数据从低向高存储,小尾相反。

c语言生成的call指令只会标记函数名,c++的call会标记函数名和参数。“所以从安全角度来讲,c++安全一些”

段错误segment 多数因为指针的解引用没有找到正确的地址,即指向的地址不在任何段中。如:*(null)就是一个段错误

总线错误sub 指针指向的类型必须是该类型的整数倍。不然会报出总线错误。

总结:该科目主要讲内存模型

1.内存模型的高位在上边内存分配从上到下从高到低分配。

2.分配数组空间时,下标低的在内存模型下边,下标加一将sp指针指向上边。c不会检查下标溢出

3在51分钟讲到地址操作引起的更高级的死循环,原因是数组指针指向saved PC,使它的值减4造成不断调用代码call。

参数从右向左传入栈有利于确定非固定个数参数的位置。结构体参数的顺序也一样。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: