一、关键字
1.变量的声明和定义有什么区别?
1)为变量分配地址和存储空间的称为定义,不分配内存空间的称为声明。
2)一个变量可以在多个地方声明,但是只在一个地方定义。
3) 加入extem修饰的是变量的声明,说明此变量将在文件以外或在文件后面部分定义。
*说明:很多时候一个变量,只是声明不分配内存空间,直到具体使用时才初始化,分配内存空间,如外部变量。
2. sizeof 和 strlen 的区别
1)sizeof 是一个操作符(关键字),strlen是库函数。
2)sizeof 的参数可以是数据的类型,也可以是变量,而 strlen 只能以结尾为 ‘/0’的字符串做参数。
3) 编译器在编译时就计算出了sizeof的结果。而strlen函数必须在运行时才能计算出来。
4)sizeof 计算的是数据占内存的大小,strlen 计算的是字符串实际的长度(不包含结束符‘\0’)。
5)数组名用做 sizeof 的参数不退化,传递给 strlen就退化为指针了。
3. static关键字的作用,以及static在C语言中与在C++中的区别?
1)作用:
1、修饰全局变量
改变变量的作用域,让它只能在本文件中使用。所以多个源文件可以定义同名的static全局变量,不会产生重定义错误。
2、修饰局部变量
变量只初始化一次,生存期为整个源程序,程序结束,它的内存才释放。但是其作用域仍与自动变量相同,只能在定义该变量的函数内使用该变量。退出该函数后, 尽管该变量还继续存在,但不能使用它。
它放在.data 或者.bss段,默认初始化为0。初始化不为0放在.data段,没有初始化或初始化为0放在.bss段。程序一运行起来就给他分配内存,并进行初始化,也是唯一一次初始化。它修饰局部变量是改变它的生存期,变为和整个程序的生命周期一样。
3、修饰函数
和修饰全局变量一样,只在本文件中可见(即只能在本文件中被其他的函数调用)。
4、修饰类的成员变量
就变成静态成员变量,不属于对象,而属于类。不能在类的内部初始化,类中只能声明,定义需要在类外。类外定义时,不用加static关键字,只需要表明类的作用域。
5、修饰类的成员函数
变成静态成员函数,不属于对象,只属于类。形参不会生成 this 指针,仅能访问类的静态数据和静态成员函数。调用不依赖对象,所以不能作为虚函数。用类的作用域调用。
2)区别:
在C中 static 用来修饰局部静态变量、全局静态变量和函数。而C++中除了上述功能外,还用来定义类的成员变量和函数。即静态成员和静态成员函数。
注意:编程时 static 的记忆性,和全局性的特点可以让在不同时期调用的函数进行通信,传递信息,而C++的静态成员则可以在多个对象实例间进行通信,传递信息。
4. const 关键字的用途和作用
(1)可以定义 const 常量
(2)const 可以修饰函数的参数、返回值,甚至函数的定义体。被const 修饰的东
西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。
1)防止被修饰的成员的内容被改变。
2)修饰类的成员函数时,表示其为一个常函数,意味着成员函数将不能修改类成员变量的值。
3)在函数声明时修饰参数,表示在函数访问时参数(包括指针和实参)的值不会发生变化。
4)对于指针而言,可以指定指针本身为const,也可以指定指针所指的数据为const,const int *b = &a;或者int* const b = &a;修饰的都是后面的值,分别代表*b和b不能改变 。
5)const 可以替代c语言中的#define 宏定义,好处是在log中可以打印出BUFFER_SIZE 的值,而宏定义的则是不能。
#define BUFFER_SIZE 512
const int BUFFER_SIZE = 512;
*注意:const数据成员必须使用成员初始化列表进行初始化。
5. extern关键字的作用
extern 标识的变量或者函数声明其定义在别的文件中,提示编译器遇到此变量和函数时在其它模块中寻找其定义。(只声明不定义)
6. volatile 关键字的作用及使用场景
1)作用:
volatile修饰的变量在使用的时候不会直接在寄存器中取值,而是从内存中取值。
volatile的本意是“易变的” 因为访问寄存器要比访问内存单元快的多,所以编译器一般都会做减少存取内存的优化,但有可能会读脏数据。当要求使用volatile声明变量值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。精确地说就是,遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
2)使用场景:
a)中断服务程序中修改的供其它程序检测的变量需要加volatile【一个中断服务子程序中会访问到的非自动变量】
b)多任务环境下各任务间共享的标志应该加volatile【多线程应用中被几个任务共享的变量】
c)存储器映射的硬件寄存器通常也要加voliate,因为每次对它的读写都可能有不同意义。【并行设备的硬件寄存器】
1)一个参数既可以是const还可以是volatile吗?解释为什么。
2)一个指针可以是volatile吗?解释为什么。
3)下面的函数有什么错误:
int square(volatile int*ptr){
return*ptr**ptr;
}
下面是答案:
1)是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
2)是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。
3)这段代码有点变态。这段代码的目的是用来返指针*pr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:
int square(volatile int *ptr)
{ int a,b; a = *ptr; b = *ptr; return a * b; }
由于*ptr的值可能被意想不到地改变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:
long square(volatile int *ptr)
{ int a;
a = *ptr;
return a * a;
}
7. new 和 malloc 的区别?
1)大小:new分配内存按照数据类型进行分配,malloc分配内存按照指定的大小分配;
2)返回值:new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转换。
3)new不仅分配一段内存,而且会调用构造函数,malloc不会。
4)new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候会调用对象的析构函数,而free则不会。
5)重载:new是一个操作符可以重载,malloc是一个库函数,只能覆盖。
6)malloc分配的内存不够的时候,可以用realloc扩容。扩容的原理?new没用这样操作。
7)分配失败:new如果分配失败了会抛出bad_malloc的异常,而malloc失败了会返回NULL。
8) 申请数组时: new[]一次分配所有内存,多次调用构造函数,搭配使用delete[],delete[]多次调用析构函数,销毁数组中的每个对象。而malloc则只能sizeof(int) * n。
8. #define 与 typedef 的区别
1. 执行时间不同
关键字 typedef 在编译阶段有效,由于是在编译阶段,因此 typedef 有类型检查的功能。#define 则是宏定义,发生在预处理阶段,也就是编译之前,它只进行简单而机械的字符串替换,而不进行任何检查。
2、功能有差异
typedef 用来定义类型的别名,定义与平台无关的数据类型,与 struct 的结合使用等。#define 不只是可以为类型取别名,还可以定义常量、变量、编译开关等。
3、作用域不同
#define 没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用。而 typedef 有自己的作用域。
9. #define 和常量 const 的区别(const 相比于#define 的优点——const可以安全检查、调试)
1)类型和安全检查不同
宏定义是字符替换,没有数据类型的区别,同时这种替换没有类型安全检查,可能产生边际效应等错误;const常量是常量的声明,有类型区别,需要在编译阶段进行类型检查
2)编译器处理不同
宏定义是一个"编译时"概念,在预处理阶段展开,不能对宏定义进行调试,生命周期结束与编译时期;const常量是一个"运行时"概念,在程序运行使用,类似于一个只读行数据
3)存储方式不同
宏定义是直接替换,不会分配内存,存储与程序的代码段中;const常量需要进行内存分配,存储与程序的数据段中
4)定义域不同
5)定义后能否取消
宏定义可以通过#undef来使之前的宏定义失效;const常量定义后将在定义域内永久有效。
6)是否可以做函数参数
宏定义不能作为参数传递给函数;const常量可以在函数的参数列表中出现。
10. #define和inline的区别
1)define:定义预编译时处理的宏; 只进行简单的字符替换,无类型检测
inline: 内联函数对编译器提出建议,是否进行宏替换,编译器有权拒绝,既为提出申请,不一定会成功。
2)define:使用预编译器,没有堆栈,使用上比函数高效。但它只是预编译器上符号表的简单替换,不能进行参数有效性检测及使用C++类的成员访问控制。
inline:它消除了define的缺点,同时又很好地继承了它的优点。inline代码放入预编译器符号表中,高效;它是个真正的函数,调用时有严格的参数检测;它也可作为类的成员函数。
11. #ifndef #define #endif 的作用
防止头文件重复引用(主要是头文件中包含的全局变量的重复定义)
12.【类的sizeof大小】定义一个空的类型,里面没有任何成员变量和成员函数。对该类型求sizeof,得到的结果是多少?
答案是1。
为什么不是0?
空类型的实例中不包含任何信息,本来求sizeof应该是0,但是当我们声明该类型的实例的时候,它必须在内存中占有一定的空间,否则无法使用这些实例。至于占用多少内存,由编译器决定。Visual Studio中每个空类型的实例占用1字节的空间。
如果在该类型中添加一个构造函数和析构函数,再对该类型求sizeof,得到的结果又是多少?
和前面一样,还是1.调用构造函数和析构函数只需要知道函数的地址即可,而这些函数的地址只与类型相关,而与类型的实例无关,编译器也不会因为这两个函数而在实例内添加任何额外的信息。
那如果把析构函数标记为虚函数呢?
C++的编译器一旦发现一个类型中有虚拟函数,就会为该类型生成虚函数表,并在该类型的每一个实例中添加一个指向虚函数表的指针。
在32位的机器上,一个指针占4字节的空间,因此求sizeof得到4;如果是64位的机器,一个指针占8字节的空间,因此求sizeof则得到8
二、数组和字符串
1. 怎么将字符串赋值给字符数组
1)定义时直接用字符串赋值。 char a[10]="hello";但是不能先定义再赋值,即以下非法:char a[10]; a[10]="hello";
2)利用strcpy。 char a[10]; strcpy(a,"hello");
3)利用指针。 char *p; p="hello";这里字符串返回首字母地址赋值给指针p。另外以下非法:char a[10]; a="hello"; a已经指向在堆栈中分配的10个字符空间,不能再指向数据区中的"hello"常量。可以理解为a是一个地址常量,不可变,p是一个地址变量。
4)数组中的字符逐个赋值。
2. 以下四行代码的区别是什么?
指针:指针指向的是字符串常量,所以保存在常量区;
数组:自定义的,所以保存在栈区,加上const修饰,编译器做优化放到常量区
1)const char * arr = "123";
字符串123保存在常量区,const本来是修饰arr指向的值不能通过arr去修改,但是字符串“123”在常量区,本来就不能改变,所以加不加const效果都一样
2)char * brr = "123";
字符串123保存在常量区,这个arr指针指向的是同一个位置,同样不能通过brr去修改"123"的值。
3)const char crr[] = "123";
这里123本来是在栈上的,但是const修饰,编译器可能会做某些优化,将其放到常量区。
4)char drr[] = "123";
字符串123保存在栈区,可以通过drr去修改。
三、函数
1. strcpy和strlen
1)strcpy是字符串拷贝函数,原型:char *strcpy(char* dest, const char *src);
2)从 src 逐字节拷贝到dest ,直到遇到'\0'结束,因为没有指定长度,可能会导致拷贝越界,造成缓冲区溢出漏洞,安全版本是 strncpy 函数。
3)strlen 函数是计算字符串长度的函数(求元素个数),返回从开始到'\0'之间的字符个数(不包括 ‘\0’)【没有遇到‘\0’会一直找下去,容易发生越界访问】。【区别sizeof(求字节数的大小)】
2. 回调和递归
回调:把函数当做参数传递到另一个函数内部去调用。
递归; 在函数的内部调用自身。
3. 函数传参原理
C语言中参数的传递方式一般存在两种方式:一种是通过栈的形式传递(压栈),另一种是通过寄存器的方式传递的。
四、指针
1. 什么是野指针,怎么避免?
1)定义:野指针就是指向一个已删除的对象、未申请就访问受限内存区域的指针。
2)避免:
(1)指针变量声明时没有被初始化。解决办法:指针声明时初始化,可以是具体的地址值,也可让它指向NULL。
(2)指针p被free或者delete之后,没有置为NULL。解决办法:指针指向的内存空间被释放后指针应该指向NULL。
(3)指针操作超越了变量的作用范围。解决办法:在变量的作用域结束前释放掉变量的地址空间并且让指针指向NULL。
2. 设置地址为0x67a9的整形变量的值为0xaa66
int *ptr;
ptr = (int *)0x67a9;
*ptr = 0xaa66;
3. 指针常量和常量指针的区别
指针常量是指定义了一个指针,这个指针的值只能在定义时初始化,其他地方不能改变。常量指针是指定义了一个指针,这个指针指向一个只读的对象,不能通过常量指针来改变这个对象的值。指针常量强调的是指针的不可改变性,而常量指针强调的是指针对其所指对象的不可改变性。
注意:无论是指针常量还是常量指针,其最大的用途就是作为函数的形式参数,保证实参在被调用函数中的不可改变特性。
五、结构体和共用体
1. 结构体和共用体的区别?
1)共用体的成员共用一块内存区,结构体的成员有个自独立的内存区。
2)结构体可以同时存储多种变量类型,而共同体同一个时间只能存储和使用多个变量类型的一种。
3)结构体总空间大小,等于各成员总长度,共用体空间等于最大成员占据的空间。
4)共用体不能赋初值而结构体可以。
六、操作系统和内存管理
1. 内存泄漏和溢出的区别以及分类
1)内存溢出(out of memory),是指程序在申请内存时,没有足够的内存空间供其使用。比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。(你要求分配的内存超出了系统能给你的,系统不能满足需求【分配的内存不足以放下数据项序列】 )
2)内存泄露 (memory leak),是指程序在申请内存后,无法释放已申请的内存空间。一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。(内存泄漏是指你向系统申请分配内存进行使用(new/malloc),可是使用完了以后却不归还(delete/free)【原因】,结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。)
*补充:
a)memory leak 会最终会导致out of memory!——内存不够用
b)上溢:栈满时再做进栈必定产生的空间溢出
下溢:栈空时再做退栈产生的空间溢出
3)以发生的方式来分类,内存泄漏可以分为4类:
a)常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
b)偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
c)一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
d)隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。
4)内存泄漏的分类:
a)堆内存泄漏 (Heap leak)。对内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak。
b)系统资源泄露(Resource Leak)。主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。
c)没有将基类的析构函数定义为虚函数。当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。
2. 如何判断内存泄漏?
内存泄漏通常是由于调用了malloc/new等内存申请的操作,但是缺少了对应的free/delete。为了判断内存是否泄露,我们一方面可以使用linux环境下的内存泄漏检查工具Valgrind,mtrace检测;另一方面我们在写代码时可以添加内存申请和释放的统计功能,统计当前申请和释放的内存是否一致,以此来判断内存是否泄露。
3. 位段
1)定义:
在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域”
2)方法:
“:”后面的数字用来限定成员变量占用的位数。
3)限制:
a)位域的宽度不能超过它所依附的数据类型的长度。通俗地讲,成员变量都是有类型的,这个类型限制了成员变量的最大长度,“:”后面的数字不能超过这个长度。
b)只有有限的几种数据类型可以用于位域。在 ANSI C 中,这几种数据类型是 int、signed int 和 unsigned int(int 默认就是 signed int)【整形家族】;到了 C99,_Bool 也被支持了。
4. 浮点数和复数在计算机中的存储形式
1)浮点数
无论是单精度还是双精度在存储中都分为三个部分:
- 符号部分(Sign) : 0代表正,1代表为负
- 指数部分(Exponent):用于存储科学计数法中的指数数据,并且采用移位存储
- 尾数部分(Mantissa):尾数部分
2)复数
分别存储实部和虚部。
5. 什么是结构体对齐,字节对齐
1)原因:
a)平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
b)性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
2)规则
a)数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。
b)结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。
c)结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。
3)定义结构体对齐
可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是指定的“对齐系数”。
4、举例(C++面试题之结构体内存对齐计算问题总结大全_C 语言_脚本之家)
#pragma pack(2) //按2字节对齐
struct AA {
int a; //长度4 > 2 按2对齐;偏移量为0;存放位置区间[0,3]
char b; //长度1 < 2 按1对齐;偏移量为4;存放位置区间[4],占一个[5];
short c; //长度2 = 2 按2对齐;偏移量要提升到2的倍数6;存放位置区间[6,7]
char d; //长度1 < 2 按1对齐;偏移量为7;存放位置区间[8];共九个字节
};
#pragma pack() //恢复
6. malloc的原理,另外brk系统调用和mmap系统调用的作用分别是什么?
Malloc函数用于动态分配内存。为了减少内存碎片和系统调用的开销,malloc其采用内存池的方式,先申请大块内存作为堆区,然后将堆区分为多个内存块,以块作为内存管理的基本单位。当用户申请内存时,直接从堆区分配一块合适的空闲块。
Malloc采用隐式链表结构将堆区分成连续的、大小不一的块,包含已分配块和未分配块;同时malloc采用显示链表结构来管理所有的空闲块,即使用一个双向链表将空闲块连接起来,每一个空闲块记录了一个连续的、未分配的地址。
当进行内存分配时,Malloc会通过隐式链表遍历所有的空闲块,选择满足要求的块进行分配;当进行内存合并时,malloc采用边界标记法,根据每个块的前后块是否已经分配来决定是否进行块合并。
Malloc在申请内存时,一般会通过brk或者mmap系统调用进行申请。其中当申请内存小于128K时,会使用系统函数brk在堆区中分配;而当申请内存大于128K时,会使用系统函数mmap在映射区分配。
7. 什么时候会发生段错误
段错误通常发生在访问非法内存地址的时候,具体来说分为以下几种情况:
1)使用野指针;
2)试图修改字符串常量的内容。
8. C语言参数压栈顺序?
从右到左。
9. C语言是怎么进行函数调用的?
每一个函数调用都会分配函数栈,在栈内进行函数执行过程。
调用前,先把返回地址压栈,然后把当前函数的esp(栈指针,用于指向栈的栈顶(下一个压入栈的活动记录的顶部);EBP为帧指针,指向当前活动记录的底部)指针压栈。
10. C++/C的内存分配
32bit CPU可寻址4G线性空间,每个进程都有各自独立的4G逻辑地址,其中0~3G是用户态空间,3~4G是内核空间,不同进程相同的逻辑地址会映射到不同的物理地址中。其逻辑地址其划分如下:
各个段说明如下:
3G用户空间和1G内核空间
1)静态区域:
a)text segment(代码段):包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
b)data segment(数据段):存储程序中已初始化的全局变量和静态变量。
c)bss segment(bss段):存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。对于未初始化的全局变量和静态变量,程序运行main之前时会统一清零。即未初始化的全局变量编译器会初始化为0
2)动态区域:
a)heap(堆区): 调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。当进程未调用malloc时是没有堆段的,只有调用malloc时采用分配一个堆,并且在程序运行过程中可以动态增加堆大小(移动break指针),从低地址向高地址增长。分配小内存时使用该区域。 堆的起始地址由mm_struct 结构体中的start_brk标识,结束地址由brk标识。
b)memory mapping segment(映射区): 存储动态链接库以及调用mmap函数进行的文件映射
c)stack(栈区):使用栈空间存储函数的返回地址、参数、局部变量、返回值。从高地址向低地址增长。在创建进程时会有一个最大栈大小,Linux可以通过ulimit命令指定。
11. 请你说一说用户态和内核态区别
用户态和内核态是操作系统的两种运行级别,两者最大的区别就是特权级不同。
用户态拥有最低的特权级,内核态拥有较高的特权级。
运行在用户态的程序不能直接访问操作系统内核数据结构和程序。
内核态和用户态之间的转换方式主要包括:系统调用,异常和中断。
12. 请问什么是大端小端以及如何判断大端小端
大端是指低字节存储在高地址;小端存储是指低字节存储在低地址。我们可以根据联合体来判断该系统是大端还是小端,或者强制类型转换。因为联合体变量总是从低地址存储。
13. 你怎么理解操作系统里的内存碎片,有什么解决办法?
内存碎片通常分为内部碎片和外部碎片。
1. 内部碎片是由于采用固定大小的内存分区,当一个进程不能完全使用分给它的固定内存区域时就会产生内部碎片,通常内部碎片难以完全避免;(分配过大)
2. 外部碎片是由于某些未分配的连续内存区域太小,以至于不能满足任意进程的内存分配请求,从而不能被进程利用的内存区域。(分配过小)
现在普遍采取的内存分配方式是段页式内存分配。将内存分为不同的段,再将每一段分成固定大小的页。通过页表机制,使段内的页可以不必连续处于同一内存区域。
七、其它
1. include头文件的顺序以及双引号””和尖括号<>的区别
1)Include头文件的顺序:
对于include的头文件来说,如果在文件a.h中声明了一个在文件b.h中定义的变量,而不引用b.h。那么就要在a.c文件中引用b.h文件,并且要先引用b.h,后引用a.h,否则汇报变量类型未声明错误。
2)双引号和尖括号的区别:编译器预处理阶段查找头文件的路径不一样。
a)对于使用双引号包含的头文件,查找头文件路径的顺序为:
【编译器从当前工作路径开始搜索头文件】
当前头文件目录
编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)
系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径
b)对于使用尖括号包含的头文件,查找头文件的路径顺序为:
【编译器从标准库路径开始搜索 头文件】
编译器设置的头文件路径(编译器可使用-I显式指定搜索路径)
系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径
2. 源码到可执行文件的过程
1)预编译(预处理 )【宏定义、文件包含、条件编译】(-E —— .i)
主要处理源代码文件中的以“#”开头的预编译指令。处理规则见下
1、删除所有的#define,展开所有的宏定义。
2、处理所有的条件预编译指令,如“#if”、“#endif”、“#ifdef”、“#elif”和“#else”。
3、处理“#include”预编译指令,将文件内容替换到它的位置,这个过程是递归进行的,文件中包含其他文件。
4、删除所有的注释,“//”和“/**/”。
5、保留所有的#pragma 编译器指令,编译器需要用到他们,如:#pragma once 是为了防止有文件被重复引用。
6、添加行号和文件标识,便于编译时编译器产生调试用的行号信息,和编译时产生编译错误或警告是能够显示行号。
2)编译【生成汇编代码】(-S —— .s)
把预编译之后生成的xxx.i或xxx.ii文件,进行一系列词法分析、语法分析、语义分析及优化后,生成相应的汇编代码文件。
1、词法分析:利用类似于“有限状态机”的算法,将源代码程序输入到扫描机中,将其中的字符序列分割成一系列的记号。
2、语法分析:语法分析器对由扫描器产生的记号,进行语法分析,产生语法树。由语法分析器输出的语法树是一种以表达式为节点的树。
3、语义分析:语法分析器只是完成了对表达式语法层面的分析,语义分析器则对表达式是否有意义进行判断,其分析的语义是静态语义——在编译期能分期的语义,相对应的动态语义是在运行期才能确定的语义。
4、优化:源代码级别的一个优化过程。
5、目标代码生成:由代码生成器将中间代码转换成目标机器代码,生成一系列的代码序列——汇编语言表示。
6、目标代码优化:目标代码优化器对上述的目标机器代码进行优化:寻找合适的寻址方式、使用位移来替代乘法运算、删除多余的指令等。
3)汇编【生成机器代码】(-C —— .o)
将汇编代码转变成机器可以执行的指令(机器码文件)。 汇编器的汇编过程相对于编译器来说更简单,没有复杂的语法,也没有语义,更不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译过来,汇编过程有汇编器as完成。经汇编之后,产生目标文件(与可执行文件格式几乎一样)xxx.o(Windows下)、xxx.obj(Linux下)。
4)链接【生成可执行程序】(-O —— app)
将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。链接分为静态链接和动态链接:
1、静态链接:
函数和数据被编译进一个二进制文件。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件。
空间浪费:因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,会出现同一个目标文件都在内存存在多个副本;
更新困难:每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。
运行速度快:但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。
2、动态链接:
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
共享库:就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分,副本,而是这多个程序在执行时共享同一份副本;
更新方便:更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。
性能损耗:因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。