http://v.163.com/special/opencourse/paradigms.html
Lecture 1
C
Assembly
C++
Concurrent Programming
Scheme
Python
The real intellectual value in learning all of these language is to really study the paradigms that they represent.
1. C/C++中以下七种基本数据类型, 在内存中的表示.
bool 1 byte
char 1 byte
short 2 byte
int 4 byte
long 4 byte
float 4 byte
double 8 byte
2. 正负整数的表示
方法一: 符号-幅值表示法
即: 00000010 与 10000010 分别表示 2与-2.
缺点: 计算时麻烦,比如: 2+(-2)
0在计算机中就会有两种形式,即:00000000 和 10000000.
方法二: 2的补码表示法( Two's Complement Representation)
可以克服方法一的两个缺点.
《代码揭密-从C/C++的角度探秘计算机系统》 P50 补码公式
直观的求补码方法
正数的补码: 就是它自身
负数的补码: 先得到相应正数的二进制表示,再按位取反(取反时包括符号位,所得结果为补码)加一.
补码与相应10进制数间的快速转换(以一个字节为例), 借助"值盒子", 可以避免上面负数求补码时,通过反码求补码的麻烦:
-128 64 32 16 8 4 2 1
计算机中的整数都是,用方法二,以补码形式保存在内存中的(无论正负).
如果一个单字节整数在内存中的值为: 10000000 它就表示: 整数-128.
3. C/C++中不同类型间整数的赋值运算
要先把赋值号右边的值的类型转换成与赋值号左边相同的类型.(无论是它是长整数还是短整数,有符号还是无符号.)
短整数赋值给长整数,低字节按位copy;高字节,将原短整数符号位逐位copy至高字节所有位( 即正数补0,负数补1 ).
长整数赋值短整数,丢弃高字节,低字节逐位copy. (符号位丢失)
参考:http://www.ncre8.net/html/64-2/2546.htm
4. C/C++中的浮点数
float 类型 [IEEE85]标准,内存表示:四字节,其中:
最高位为符号位
接下来的8个二进制位指数参数,参于指数的计算( 减去偏移量127得到实际指数 ).
最后23位表有效数的尾数.(最后计算时会自动在此尾数前加1,以获得相应有效数.)
参见:http://blog.csdn.net/yzfuture2010/article/details/7411948
指数参数部分为0,表示:有效数是介于0-1之间的数,而非1-2之前的数。
指数参数部分为11111111且有效数尾数部分为0,表示:算术运算中的溢出,该值无法表示成一个数字。
需要注意的是:
浮点数并不是实数,而只是实数的一个近似表达( 有些数本身用基于二进制的浮点数表示时,只能近似,比如:1.2, 有些数由于浮点数的位数有限也无法表达,比如:圆周率 )。
不要试图比较两个浮点数是否相等( 可以用一个宏或inline函数来取代 )。
浮点数存在与整型数相同的“上溢”问题,同时还有一个“下溢”问题。
浮点数本身是一个近似值,且有限的位数使它会在计算中经常会“丢弃”一些数,从而产生精度误差。
在一些运算中,比如迭代,小误差不断积累会产生大误差。
5. 整数与浮点数间的转换
1) 正确的转换
int i = 5;
float f = i; //1.25*2^2 将5不断地除以2.注意:其中1.25在转换成浮点值前,要先去掉1.
f 的值为5.0
2) 内存解释的变换
int i=5;
float f=*(float*)&i;
f 的值不是 5.0
6. C/C++中字符的表示
整数、浮点数等实际上是依靠一定的编码规则,得以在计算机中表示。( 用二进制的0、1来表示数值 )
字符实际上也是依靠另一套编码规则,才能够通过0/1等二进制数,在计算机内得以表达的。
最著名的ASCII码,就是用一个字节表示各种字符的编码方法。
C/C++是采用ASCII编码来表示内存中的字符的。也就是说,只要内存中的数据被解释成字符类型,那么就一定会以ASCII编码来加以解读。
Lecture3
冯.诺依曼机: 二进制 程序存储 顺序执行
计算机的内存以字节来组织, 一个内存单元就是一个字节. 计算机所有内存地址按字节编号, 它的所有内存是连续的, 也就是说计算机的内存地址值是连续的.
1.
对机器而言,变量类型仅意味着一次性读取内存的长度(最小长度为一个字节). 一个变量的地址称为此变量类型的指针.不同类型的变量所占内存空间大小不同,故其相应的变量指针,在做同一种算术运算(比如:p++)时,所移动的内存单元个数(字节数)是不同的.
2.
数组就是一块连续的内存区域,此内存区域以某种数据类型为元素.
数组名就是这块内存区域的首地址,但它是一个常量,所以不能对它进行算术运算.
通过将数组相应元素地址赋值给同类型指针变量,可以用此指针变量来操作对应的内存中的值.
C/C++对数组不作边界检查.因此在用指针变量对数组中数据进行操作时,要特别当心越界问题.
另外,使用下标号[]对数组元素进行检索,采用的是变址寻址方式.变址寻址是以基址寄存器的值+变址寄存器的值来得到实际数据地址的
( 参见:http://www.cnblogs.com/lilongjiang/archive/2011/06/14/2080581.html ), 而指针变量加偏移值的方法,则实际上是寄存器相对寻址方式 ( 参见:http://blog.ixpub.net/html/13/13151113-235109.html ), 效率要高一些.
3.
struct也定义了一块连续的内存区域,但此内存区域中可以包括多个数据域,每个数据域可以是不同的数据类型.
C/C++中,sizeof(x)是一种编译时运算符,不是一种函数,所以它在编译的时候就能够返回对象x的字节数.
如果x是数组名,返回的是此数组所占内存空间的总的字节数.
如果x是struct变量,返回的字节数有一个"对齐"问题.( #progam pack(1)可以得到实际的字节数)
由于基本数据类型所占的内存空间大小与编译器有关,所以程序中应尽量使用sizeof(),以方便的程序的移植.
4.
C语言允许程序员将任何指针类型转换成其他的指针类型.所谓的类型转换就是编译器直接强制地以另外一种数据类型来解释原内存中的数据.
类似以下的操作在C/C++中是合法的,可以运行.但如果不注意,会产生"超量写内存"等错误:
1)
int arr[5];
arr[3] = 128;
((short*)arr)[6] = 2;
((short*)(((char*)(&arr[1]))+8))[3] = 100;
//对复杂的转换的理解方法:从核心区开始.(从内向外逐层扩展)
2)
struct student
{
char *name;
char SUID[8];
int nunUnits;
};
student pupils[4];
pupils[0].numUnits = 21;
pupils[2].name = strdup("Adam");
pupils[3].name = pupils[0].SUID + 6;
strcpy(pupils[1].SUID, "40415xx");
5.常见的内存操作错误
1) 内存泄漏
new后未delete.
常见原因1: 异常,错误及其他各种throw...catch.
常见原因2: struct St{ char* pString;}; St* p = new St; p->pString = new char[3]; delete p; //但delete p;之前
没有delete p->pString.
2) 重复delete同一内存空间
常见原因:将new出来的内存空间地址赋值给多个指针变量.
3) 野指针
指针指向不正确的内存空间.
参见:http://baike.baidu.com/view/1291320.htm
常见原因:指针变量未被初始化.或所指内存被delete后,指针变量未被置为NULL.
4) 超量写内存
常见原因:a. 数组越界
b. 分配内存不足:
例一:
#define array_size 100
int *a = (int*)malloc(array_size);
a[99] = 0;
例二:
char* new_string(char* s)
{
int len = strlen(s);
char* new_s = (char*)malloc(len);
strcpy(mew_s, s);
return new_s;
}
c.字符串操作未做检查
char s1[] = "Hello";
char s2[] = "World";
char s[5];
strcpy(s, s1);
strcat(s, s2);
d.返回stack frame中的指针.
Lecture4
本课主要讲C中的模板函数实现方法.
通过这个例子,生动而具体地示范了指针在实际编程中的使用方法.
几点说明:
void 不能用作变量类型,而只能作为函数返回类型.
不能对void类型指针进行解引用.
编译器不允许对void*指针变量作算术运算操作.
用C实现的模板函数,不同类型都用同一段代码. 用C++实现的模板函数, 根据传入数据类型的不同, 在编译过程中会生成不同的代码段.
用C实现的模板函数实例:
void swap( void* vp1, void* vp2, int size )
{
char buffer[size]; //有些版本的GCC及G++编译器允许定义依赖于参数的数组变量.
memcpy(...);
memcpy(...);
memcpy(...);
}
void* search( void* key, void* base, int n, int elemSize)
{
for(int i=0; i<n; i++)
{
void* elemAddr = (char*)base + i*elemSize; //base必须要做类型转换,因为编译器不允许对void*类型变量作算术运算操作.
if(memcmp(key, elemAddr, elemSize) == 0)
return elemAddr;
}
return 0;
}
Lecture5
在VC中可以在Disassembly窗口中,得到与源代码相应的汇编码.(在快捷菜单选择[Code Bytes]选项,可得到相应机器码.)Disassembly窗口最左边能显示出相应指令的指令地址.(此指令地址,在编译完成后就已确定 --- 确切地说是指令的逻辑地址被确定.故可以通过此地址,得到相应的编译后的机器码.)
C/C++中无法象取数据的内存地址那样取指令地址.但可以通过函数指针获得被调函数中的首指令地址.
注意区别: void (*f)() 与 void* f();
由于现代CPU一般的指令集都是变长的,所以对取得的指令地址f作类似数据指针的操作,如:f++,是无意义的,无法得到下一条指令地址.所以C/C++对指令指针的操作是有限的.
函数指针的最大作用就如本课程所介绍的:编写通用的模板函数(C语言中).
void* search(void* key, void* base, int n, int elemSize, int (*cmpfn)(void*, void*))
{
for(int i=0; i<n; i++)
{
void* elemAddr = (char*)base + i * elemSize;
if(cmpfn(key, elemAddr) == 0)
return elemAddr;
}
return NULL;
}
int IntCmp(void* elem1, void* elem2)
{
int* ip1 = elem1;
int* ip2 = elem2;
return (*ip1 - *ip2);
}
int StrCmp(void* vp1, void* vp2)
{
char* s1 = *(char**)vp1;
char* s2 = *(char**)vp2;
return strcmp(s1, s2);
}
模拟C++中的封装,在C中实现stack.
typedef struct
{
int* elems;
int logicalLen;
int allocLength;
}Stack;
void StackNew(Stack* s);
void StackDispose(Stack* s);
void StackPush(Stack* s, int value);
int StackPop(Stack* s);
void StackNew(Stack* s)
{
s->logicalLen = 0;
s->allocLength = 4;
s->elems = malloc(4*sizeof(int));
assert(s->elems != NULL);
}
Lecture6
由于C 的struct中没有私有成员变量( 但可以模仿C++的私有成员函数). 所以需要通过文档强制要求用户, 通过调用相关函数而非直接访问的方式,对struct对象中的数据进行修改.
void StackDispose(Stack* s)
{
free(s->elems);
}
void StackPush(Stack* s, int value)
{
if(s->logicalLen == s->allocLength)
{
s->allocLength *= 2;
s->elems = realloc(s->elems, s->allocLength*sizeof(int)); //realloc: 重新分配内存, 拷贝原内存中数据, 释放原内存.
assert(s->elems = NULL);
}
s->elems[s->logicalLen] = value;
s->logicalLen ++;
}
void StackPop(Stack* s)
{
assert(s->logicalLen > 0);
s->logicalLen --;
return s->elems[s->logicalLen];
}
更一般化的Stack模板 //
typedef struct
{
void* elems;
int elemSize;
int logLength;
int allocLength;
}Stack;
void StackNew( Stack* s, int elemSize );
void StackDispose( Stack* s );
void StackPush( Stack* s, void* elemAddr );
void StackPop(Stack* s, void* elemAddr);
void StackNew(Stack* s, int elemSize)
{
s->logLength = 0;
s->allocLength = 4;
s->elemSize = elemSize;
s->elems = malloc[4*elemSize];
assert( s->elems != NULL);
}
void StackPush(Stack* s, void* elemAddr)
{
if(s->logLength == s->allocLength)
StackGrow(s);
void* target = (char*)s->elems + s->logLength * s->elemSize;
memcpy(target, elemAddr, s->elemSize);
s->logLength ++;
}
Static void StackGrow( Stack* s ) //Static 说明这是一个私有函数, 本目标文件以外的目标文件无法调用此函数.
{
s->allocLength += 2;
s->elems = realloc(s->elems, s->allocLength*s->elemSize);
}
void StackPop( Stack* s, void* elemAddr)
{
void* source = (char*)s->elems + (s->logLength-1)*(s->elemSize);
memcpy(elemAddr, source, s->elemSize);
s->logLength --;
}
Lecture7
1. Stack对象的应用及Stack类的改进.
2.memcpy使用的前提: source区与target区不重叠.
memmove使用时 source区与targe区可以重叠.
3.
void Rotate( void* front, void* middle, void* end)
{
int frontSize = (char*)middle - (char*)front;
int backSize = (char*)end - (char*)middle;
char buffer[frontSize];
memcpy( buffer, front, frontSize);
memmove( front, middle, backSize );
memcpy( (char*)end - frontSize, buffer, frontSize );
}
4.
子函数的一种别称: helper
堆管理器描述内存的数据结构实际上是一个链表.
malloc的分配内存策略: malloc(n) 从低地址开始扫描, 找到第一块大小与n最匹配( 最小 )的空闲区.
例子: (堆内存的变化)
int main()
{
void* a = malloc(40);
void* b = malloc(60);
free(a);
void* c = malloc(44);
void* d = malloc(20);
return 1;}