深层探索C和C++总结

一、调用函数以及压栈:

(1)、每个int占4个字节。

(2)、通常栈是王内存低地址方向增长的,也就是说,先压栈的内容存放在高地址区域,后压的存放在低地址区域。

(3)、一般调用函数时,汇编中call(通常调用函数都是用call指令实现跳转)指令会在主函数体内进行自动压栈。将调用函数后面的语句的地址压栈存。

(4)、一般调用函数前会将传入参数都压栈保存。

二、变量的可见范围和生存期:

(1)、函数内部的非静态变量都会放到栈里面去。

(2)、函数间不能互相访问各自的内部变量(即使是静态的都不行,那是基于语义的要求),基于堆栈的声明和释放。

(3)、外部变量一般存放在数据段(data segment)中,而不是栈中。

(4)、外部变量的定义如果在某函数的函数声明之前,则可以直接使用,如果在某函数名之后,则要用extern进行声明(注意,不是定义,但是造型蛮像定义的)。然后还是要在外面再重新定义的。

(5)、不是所有的外部变量都是任何函数都能使用的,如果在某函数内声明的时候使用了static静态修饰,则不能了。

三、变量的声明和定义:

(1)、声明变量不会使编译器为其分配存贮空间。声明的关键字是extern,有extern的时候,编译器只是知道这个符号是什么意思,此外什么都不做。但是若同时又对其初始化,则会为其分配空间。

(2)、1、在C中,我们常见的类似int a;之类的都是对a的暂时定义(有初始化才叫正式定义,C中,外部变量只能被定义一次,注意是正式定义一次,暂时定义就没有限制了)。2、C++中没有暂时定义和正式定义之分,所有都是正式的。要注意。所以最好要么声明,要么定义,而且都只定义一次。3、C中的暂时变量可以定义多次,当在连接时系统全局空间没有相同名字的变量定义,暂时就升级为正式定义。系统就会为其分配存储空间。

四、编译(complie)和链接(link):

(1)、一般来说程序的编译和链接是整个工作的2个部分,首先是编译,然后链接。

(2)、编译有:预处理程序(包括包含入头文件等)à编译器将代码文件编译成汇编代码文件à汇编编译器将汇编代码文件编译成目标代码文件。

(3)、链接就是将编译得到的汇编代码中每个记录下的需要确定地址的符号(包括变量,子函数等)的地址计算并确定后传入。

五、外部变量的链接性质:

(1)、问题:如何使一个变量像外部变量那样存放在数据段,又不会被其他文件的代码影响到,因为不同文件的同名的外部变量会有冲突。

(2)、当对外部变量进行定义的时候(从这个时候开始,我应该注意区别定义、暂时定义和声明的不同),加static就会将该变量的链接性质从external变为internal,在汇编中就会相应不产生“.globl xx”此句,该变量就只能在本文件中被使用。

(3)、对于暂时定义同理也是加static,但是其会在汇编中加上“.local a”一句,效果是一样的。

(4)、同一个变量的定义和暂时定义间或者几个暂时定义之间,在使用static问题上前后不一致会导致报错。降低程序的可移植性。

(5)、暂时定义与声明不一致则以暂时定义为准,但编译器会发出警告。

六、静态内部变量:

(1)、有static修饰的内部变量可以使内部变量具有外部变量的存储性质,又具有内部变量的可见范围

(2)、静态内部变量汇编时会被编译器加后缀,用以区别不同函数内部定义的同名变量。而汇编时不加“.globl xx”使不同文件不会冲突。

(3)、静态内部变量只会被初始化一次。汇编时“.long xx”对其初始化存放到目标代码文件和后面的可执行文件中。不仅静态变量、所有存放在.data数据段的数据都是这样初始化的。

(4)、”int f(); void g(){int a=f();}”可行,但是”int f(); static void g(){int a=f();}”就不行。因为:初始化静态变量的值必须是编译期就能够求出的常数。

七、函数的声明和定义:

(1)、对函数的调用的步骤:参数的压栈à地址转移à通过压栈或者寄存器返回值。

(2)、函数在调用前若不声明则为K&R C风格,而声明的话可以是在主函数的内部(即内部函数声明,该声明只在此主函数内部有效,出去又无效了)也可以在主函数前面外部声明(即外部声明,即我们常见的)。

(3)、我们所接触的函数的声明和定义的风格一共有2种:K&R C和C90。

(4)、K&R C的特点:1、不对函数的参数进行类型检查(由于程序的多文件性);2、未声明函数仍可以直接调用。3、基本数据类型只有:char、int(2者为整数类)、float、double(2者为浮点数类)。4、对于传入参数,如果是整数类,一律扩展成int传入。5、对于传入参数,如果是浮点类,一律扩展为double传入。6、凡是没有指明类型(包括返回值和参数)的一律当做int。7、K&R C函数的定义风格:

fun(a)

float a;  //

{……}

(5)、C90继承了K&R C的规矩,也有一些新的规则,很像我们现在用的。在调用函数的之前必须对函数进行定义或者带参数、返回值类型的声明。注意K&R C和C90类型的混用会造成错误:1、压栈时按照K&R C规则扩展压入而C90多是按照数据长度对齐要求进行的。2、返回时按照K&R C规则返回的参数类型与C90不同从而取错寄存器值。

(5)、K&R C风格声明和C90风格声明(又叫原型声明)的几个注意点:1、同一个函数多个原型声明不能有任何的不一致。2、同一个函数多个K&R C风格声明的返回值必须一致。3、K&R C风格声明和原型声明可以共存,但返回值必须一致。4、是否对函数参数进行检查是以是否有原型声明为准。5、同一个函数的各种声明与函数定义的返回值类型必须一致。6、同一个函数的原型声明和定义必须一致。7、函数定义本身也是一种原型声明,会进行调用的参数检查。

八、函数的链接性质:

(1)、函数与变量相似,也有外部和内部之分。但是,函数的调用是不会压栈的,当然也无法压栈。如果没有特殊指定,函数都是外部的,即全局可见的。这样有时会引起冲突来。采用static可以使其变成内部的,仅在本文件可见。汇编中少了一个“.globl xx”。

(2)、如果声明和定义不一样:1、只有函数的声明和函数调用。编译器会给出警告。2、有函数声明和定义。如果在函数的定义前有至少一个函数声明为了static,或者函数定义为了static,则函数就是static。3、但是注意的是,在其他函数内部调用时进行的内部声明不会再全局其作用,作用范围只是在该主函数内部。出来后又要重新判断。

九、使用头文件:

(1)、一般来说,该放入有:函数原型声明、全局变量声明、自己定义的宏和类型(比如struct,可以在头文件中用typedef定义)。

(2)、相应的,不应该放入的有:全局变量和函数的定义(注意和声明区分)、static变量和static函数。

(3)、为了避免定义声明在头文件中的变量和函数出错,一般都要将头文件在定义变量和函数的文件中include一遍。

十、静态库:

(1)、静态库(即库文件的一种)的结构:把原来的目标代码放一起,链接的时候根据每一份目标代码的符号表查找相应的符号(函数和变量的名字),找到的话就把该函数里需要定位的符号进行定位,然后将整块函数代码放进可执行文件,若找不到就报错。

(2)、在库中定位未知的符号的地址是按照递归的顺序。

(3)、链接后产生的可执行文件包含了所有需要调用的函数的代码,占用空间大。

(4)、如果有多个(调用相同库函数)进程在内存中运行,内存中就有多份相同的库函数代码,占用空间就多了。

十一、动态库:

(1)、为了解决静态库占用空间比较多的为题,引入动态库,大致功能与静态库是相同的,不同点在于动态库是程序装载入内存时才真正把库函数代码链接进来确定其地址。

(2)、动态库载入程序时却确定动态库代码的逻辑地址的类型主要有:1、静态绑定,在一开始载入内存的时候,载入程序就会把所有要调用的动态代码的地址算出来,初始化较长,但运行较快。2、动态绑定,只有在运行时真正调用动态代码时才去计算,初始化较短,运行较慢。

十二、简单类型转换:

(1)、C90内置的简单类型有2种:整数类型和浮点数类型。整数类型包括char(1)、int(4)、short(2)、long(4)。浮点数类包括float(4)、double(8)、long double(8)括号内是所占字节数4bits=1word。

(2)、浮点数类型变量之间的相互赋值。当小的赋值给大的,那么就简单的最高位扩展补齐。当大的赋值给小的时候,整数型的会报错,而浮点数型的编译器会尝试去转换,一般不报错。

(3)、证书类性变量之间的相互赋值。要注意signed和unsigned之分。一般的定义都是默认signed,即带1个字节的符号整数。1、如果将小的赋值给大的,signed和unsigned变量间存在着一个符号位扩展问题,会导致值变化很大。2、如果将大的赋值给小的,就存在裁剪问题,将高位多出的无视即可。

(4)、将整数型à浮点数型:1、只有在该数的有效数字为6位的时候,才能够保证相同。2、其余的时候,都存在一个精度损失。所以,最好不要把整数转换给浮点数。

(5)、浮点数型à整数型:1、将小数部分舍掉。2、将浮点数型进行转换时,首先必须要转换成int、long、long long以及其unsigned类中的一种,不能直接转成short或者char。

十三、复合类型:

(1)、复合类型包括struct、union、bit field。即包含多种简单类型的类型。

(2)、其实就是告诉我们一个问题:对齐(alignment)。

(3)、由于CPU对内存的访问都是4个字节(即1个block)一次的,所以内存的地址一般都是4的倍数。而数据最好也是按4个字节来存取。那么在复合类型中,定义的简单变量的占内存空间最好都是4的倍数,若不是,则会在存储空间进行对其操作。相关的汇编为”.align 4”。使例如char之类的不足4的也按4存取,方便寻址。

(4)、我们应该小心处理复合类型的变量布局:

例如: struct s { char c; int I; }; à8bits        struct s { char x; int I; char y; int j; };à16bits             struct s { int i; int j; char x; char y; };à12bits而前面的要用16bits。同理struct s { int I; char x; };à8bits 而不是5bits。

十四、指针:

(1)、像函数名也是一种函数指针,代表函数入口地址,比如:void fun(void);

ptr=fun; (*ptr)();也是可以调用函数的。

(2)、如果知道了函数的入口地址的值(假设0x12345678)也可以调用函数:(假设是一个返回值void的函数)(*(void(*)())0x12345678();à看一个数据的类型,方法是在声明中去掉其名字,我们知道函数(1)中的例子用ptr进行声明时为void (*ptr)();那么去掉名字ptr后,其类型应该为void (*)();故强制类型转换时加一个括号成为(void (*)())即可强制装换。

(3)、指针和数组名都代表某种变量的地址。但是指针是变量,编译器会为其分配空间,那么其值也可以改变。而数组名编译器不会赋给空间,其被当做一个常量,不能改变。

(4)、注意:1、数组名和指针虽然都是地址,但是不好混用的,因为初始化数组的时候赋的值被指针当做地址去访问的时候有时会出错,比如像0之类的,会被当做空指针。2、当用作函数参数时,2者就相等了。因为在入栈的时候,都会被赋予存储空间,那么数组名其实也成了指针。//有问题

十五、typedef:

(1)、typedef跟变量一样,是有可视范围的,同时也有内层覆盖外层的属性。

(2)、同一范围内不能有相同的名字定义不同的类型。

(3)、与#define想比,typedef有先天的优势,编译器会对其进行语义分析,而不是前者的单纯替换,前者可能会因为优先级的问题产生问题,而后者一般很友好。。。

(4)、但是typedef不能和其他的标识符组合使用,比如typedef int INT;unsigned INT a;就不行的。不能组合,而#define可以

(5)typedef和const组合起来的时候也会有问题,要知道在使用typedef后,const修饰的对象就是typedef定义的性的类型了,那么,就是定义了一个const的类型的对象:typedef char * str;const str s;定义的是一个指向char的const指针。àchar * const s;

十六、C-V限定词:

(1)、C++能将已用常数赋值的const变量看做编译期常数,C不行(const int BUF 1024;char buf[BUF]在C++里可以,C不行,而C必须是#define BUF 1024  char buf[BUF];使用预编译)。用常量初始化的const变量可以当常数。

(2)、C++默认const变量的链接性质是内部的,而C则是外部的(const int a=0;int main(void){}在C中无疑是外部的,而在C++中除非加上extern,否则一律内部,其他的文件看不到const的a)。

(3)、const只能允许用常理初始化const外部变量,C++没有这种限制(int f(void); const int a=f(); int main(void){}在C++中可以而C不行)。

(4)、volatile告诉编译器这个变量可能会被外部事件修改,让其在优化代码的时候不要擅自作出假定而导致错误。通常用在一些硬件的时钟端口上。

十七、字符串:

(1)、字符串是常理的一种,其会被编译器安排到只读区。

(2)、用字符串初始化指针和初始化数组是不同的,前者用指针指向该字符串的地址,对其修改时会报错。而后者在栈中给数组分配了存储空间,那么仅仅用只读区的字符串值初始化了栈内的区域,接下来的操作都是对栈的,故不会报错。

(3)、但是,并不是字符串永远被放在只读区的。当我们将字符串在一些特定的地方声明而编译器有特殊处理这些位置的设定,则该字符串就不一定是在只读区了。比如将字符串定义在全局(即放函数外部),那么就会被编译器默认放到数据段去(汇编是:.data .type xx,@object .size xx,yy)。那么在函数里对其修改就没有问题了。同理将字符串定义为静态内部变量或者静态外部变量都可以实现目的。