一个潜在的问题:使用两个已封装好的产品,而它们都含一个名字wanda()函数,当使用wanda()函数时,编译器就不知道指的是哪个版本。命令空间让能够将产品封装叫做名称空间的单元中。这样就可以用命令空间的名称来指出想使用哪个厂商的产品。也就说,命令空间是用来把变量名封装起来。
下面一行代码表明,可以使用std名称空间中定义的名称,而不必使用std::前缀。
using namespace std;
但是在大型项目中,这样做会有一些潜在的危险。
C++编译器通常进行以下4个步骤将源代码编译为可执行程序:
(1)预处理器的预处理阶段,替换#开头的内容;
(2)编译器编译阶段,将预处理后的源程序处理成汇编源程序;
(3)汇编器的汇编阶段,将汇编源程序汇编为可重定位的目标二进制程序;
(4)连接器将可重定位的目标程序连接成为可执行的二进制目标程序。
预编译阶段,该阶段预处理器会处理以#开头的命令(如#include,#define,#if等),处理之后的输出文件就完全是个纯C++源文件,不再包含预处理命令,当该处理过程将根据#if,#ifdef,#ifndef,#endif来确定是否需要时行相应的处理。.ii为后缀的文件,是已经预处理过的C++源代码文件,.i为后缀的文件,是已经预处理过的C源代码文件。
编译阶段,编译器使用预处理器的输出文件生成汇编源文件,注意只是汇编源代码,也就是此时的文件还只是文本文件。默认情况下生成的文件后缀为.s。
汇编阶段,使用汇编器将汇编源代码汇编为可重定位的目标程序(relocatable object program),该阶段生成的文件为二进制文件,它的字节码是机器语言指令而不是字符。默认输出文件为源程序名加后缀.o。
链接阶段,将各种目标代码以及库文件(*.lib文件),资源文件(*.rec)进行链接处理最终生成可以执行的*.exe文件。重定位发生于目标代码链接阶段,在链接阶段链接器就会查找符号表。当所有的符号变量都能够找到合法的内存地址时,链接阶段重定位完成。在一个目标文件中,其text区从地址0开始,随后是data区,再后面是bss区。而要运行程序,必须装载到内存中,所以这些区的地址需要在内存中重新安排,也就是重定位。
symbols
通常情况下可重定位的目标文件中包含三种类型的symbols(其实也是ELF格式的三种类型):
relocation
重定位是指一个过程,该过程将为程序中不同部分指定载入地址并调整程序中的代码和数据以将其映射到指定的地址。与重定位过程紧密相关的有重定位表(relocation tables)和一些包含额外信息的特定的节(sections),例如其中一个section就是.rela.text,该节与重定位可执行目标文件中的.text节有关。深入了解相关问题可以参见ELF文件格式。每一个.o文件都有一个重定位表(relocation table),该表给出了每一个需要被链接器更新的符号symbol,以及该如何更新的信息。
参考网站:http://notes.maxwi.com/2016/06/05/source-to-program/
类是用户定义的一种数据类型,要定义类,需要描述它能够表示什么信息和可对数据执行哪些操作。类之于对象就像类型之于变量,也就是说,类定义描述的是数据格式及其用法,而对象则是根据数据格式规范创建的实体。
类描述了一种数据类型的全部属性(包括可使用它执行的操作),对象是根据这些描述创建的实体。
让程序能够访问名称空间std的方法有多种,下面是其中的4种:
在不同操作系统,不同编译环境下,C++中变量的类型所占的字节数也不相同。下面是在windows 7 64操作系统环境,编译器使用的是DEV-C++5.11。因为64位操作系统,故指针所占4个字节。
首先类是C++中面向对象独有的,但是C和C++中都有结构体,下面我们来看一下C和C++中结构体的区别,这里主要从封装、多态、继承、封装和访问权限几个方面来说。
首先,先介绍C和C++中结构体区别。
(1)多态,C的结构体内不允许有函数存在,但是有默认的构造函数,就是把所有的成员属性设置为0,不能自定义。但是C的结构体是没有构造函数、析构函数 、this指针的,所有没有多态而言;C++允许有内部成员函数,且允许该函数是虚函数,可以多态。
(2)继承,C语言的结构体是不可以继承的,C++的结构体是可以从其他的结构体或者类继承过来的,和类一样,实现了代码的复用。
(3)封装,C的结构体只是把数据变量给封装起来了,并不涉及算法,是一种“复合类型”,其功能基本与int、double的用法相同,它主要解决多类型问题。而C++中把数据变量及对这些数据变量的相关算法给封装起来,并且给对这些数据和类不同的访问权限。
(4)访问权限,C的结构体对内部成员变量的访问权限只能是public,而C++允许pubilc、protected、private三种。
以上是四点都是表面的区别,实际区别就是面向过程和面向对象编程思路的区别。
C++的结构体和C++类的区别,主要是访问权限的区别:
(1) C++结构体内部成员变量及成员函数默认的访问级别是public,而C++类的内部成员变量及成员函数的默认访问级别是private。
(2)C++结构体的继承默认是public,而C++类的继承默认是private。
共用体(union)是一种数据格式,它能够表示不同数据类型,但只能同时存储其中的一种类型。
共用体的用途之一是,当数据项使用两种或更多种格式(但不会同时使用)时,可以节省空间。
C++中enum工具提供了另一种创建符号常量的方式,这种方式可以替代const,
枚举指定的值必须是整数,也可以只显示地定义其中一些枚举量的值,默认从0开始,后面依次加一
这里,first在默认情况下为0,后面没有被初始化的枚举量的值将比起前面的枚举量大1,因此,third的值为101,最后,可以创建多个值相同的枚举量。
1、指针和引用
指针和引用都是间接引用其他对象。但是引用不能指向空值的引用,也就说,引用必须要初始化,但是指针可以指向空的,但是这样做很危险,很容易出现野指针。引用指向初始化时的对象后,就不能改变了,但是指针可以再指向其他的对象。
不存在指向空值的引用这个事实意味着使用引用的代码效率比使用指针的要高,毕竟不要验证是否是空指针。
如果总是指向一个对象并且一旦指向一个对象后就不会改变指向,那么就应该使用引用。
2、数组和指针
数组一般在栈区开辟空间,也可以在静态区开辟空间(全局数组、静态数组),空间的开辟与回收都由编译器或操作系统来完成,不需要程序员手动执行,不会产生内存泄露。
指针首先是一个变量,是变量就要有存储空间,所以指针变量一般在栈区存储,如果说指针变量指向一个已存在的变量,则不需开辟空间;如果说指针指向一块动态开辟的空间,需要使用malloc或new开辟,指针变量的值保存的是在堆上开辟空间的首地址,不再使用指针时,需使用free或delete手动释放内存,否则会造成内存泄露。
注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。
计算机程序在存储数据时必须跟踪的3中基本属性。
面向对象编程与传统的过程性编程区别在于,OOP强调的是运行阶段(而不是编译阶段)进行决策,运行阶段指的是程序正在运行时,编译阶段指的是编译器将程序组合起来时。C++采用的方法是,使用关键字new请求正确数量的内存以及使用指针来跟踪新分配的内存的位置。
指针的危险,极其重要的一点是:在C++中创建指针时,计算机将分配用来存储数据地址的内存,但不会分配用来存储指针所指向的数据的内存。
fellow确实是一个指针,但它指向哪里呢?上述代码没有将地址地址赋给fellow,那么223323将被放在哪里呢?我们不知道,由于fellow没有被初始化,它可能有任何值,不管值是什么,程序都将它解释为存储223323地址,如果fellow的值碰巧为1200,计算机将把数据放在地址1200上,即使这恰巧是程序代码的地址,fellow指向的地方很可能不是所要存储223323的地方,这种错误可能会导致一些最隐匿、最难以跟踪的bug。
一定要在对指针应用解除引用运算符(*)之前,将指针初始化为一个确定的、适当的地址。这是关于使用指针的金科玉律。
要将数字值作为地址来使用,应通过强制类型转换将数字转换为适当的地址类型:
指针数组:首先它是一个数组,数组的元素都是指针,数组占多少个字节由数组本身的大小决定,每一个元素都是一个指针,在32位操作系统的指针永远是占4个字节,它是“存储指针的数组”的简称。Int *ptr[10]。
数组指针:首先它是一个指针,它指向一个数组,在32位系统下任何类型的指针永远是占4个字节,至于它指向的数组占多少字节,不知道,具体要看数组大小,它是“指向数组的指针”的简称。Int (*ptr)[10]。
讨论a和&a之间的区别,char a[5]={'A','B','C','D'};&a是整个数组的首地址,a是数组首元素的首地址,其值相同但意义不同。
会报错,需要进行修改
表达式“a+1”与“&a+1”,指针变量与一个整数相加减并不是用指针变量里的地址直接加减这个整数,这个整数的单位是元素的个数,不是byte。所以p+0x1的值为0x100000+sizeof(Test)*0x01,至于此结构体的大小为20byte,所以p+0x1的值为0x100014。
还有一个差别,要得到第一个元素,只需对a解除一次引用,但需要对&a解除两次引用。
(unsigned long)p + 0x1 的值呢?这里涉及到强制转换,将指针变量p保存的值强制转换成无符号的长整型数,任何数值一旦被强制转换,其类型就改变了,所以这个表达式其实就是一个无符号的长整型数加上另一个整数,所以其值为:0x100001。
(unsigned int*)p +0x1的值呢?这里的p被强制转换成一个指向无符号整型的指针,所以其值为:0x100000+sizeof(unsigned int)*0x1,等于0x100004。
变量是在编译时分配的有名称的内存,而指针只是为可以通过名称直接访问的内存提供了一个别名。在运行阶段为一个int值分配未命名的内存,并使用指针来访问这个值。在C++中使用new运算符,
需要指出一点的是,new分配的内存块通常与常规变量声明分配的内存块不同,变量的值都存储在栈的内存区域中,而new从被称为为堆或自由存储区的内存区域分配内存。
计算机有可能会由于没有足够的内存而无法满足new的请求,在这种情况下,new通常会引发异常。
在使用完内存后,要使用delete运算符来释放已使用的内存,使用delete时,后面要加上指向内存块的指针(这些内存块最初是用new分配的)。
这将释放ps指向的内存,但不会删除指针ps本身内存。
一定要配对地使用new和delete;否则将发生内存泄露(memory leak),也就是说,被分配的内存再也无法使用了,如果内存严重泄露,则程序将由于不断寻找更多的内存而终止。
一般来说,不要创建两个指向同一个内存块的指针,因为这将增加错误地删除同一个内存块两次的可能性。
使用静态联编时,必须在编写程序时指定数组的长度;使用动态联编时,程序将在运行时确定数组的长度。下面使用new来创建动态数组。
delete中方括号告诉程序,应释放整个数组,而不仅仅是指针指向的元素。
总之,使用new和delete时,应遵守以下规则:
对空指针应用delete是安全的。
栈,就是那些编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区,栈里面的变量通常是局部变量,函数参数,在一个进程中,位于用户虚拟地址空间顶部的用户栈,编译器用它来实现函数的调用。
堆,就是那些有new分配的内存块,它们的内存释放,编译器不管,有我们应用程序去控制,一般一个new就要对应一个delete,如果程序员没有释放掉,那么在程序结束后,操作系统自动收回,堆可以动态地扩展和收缩。
全局/静态存储区,全局变量和静态变量被分配到同一块内存中。
常量存储区,这是一块比较特殊的存储区,它们里面存放的是常量,不允许修改。
代码区(code):用来存放代码的。
BSS(Block Started by Symbol)通常是指用来存放程序中未初始化的全局变量和静态变量的一块内存区域。特点是:可读写的,在程序执行之前BSS段会自动清0。所以,未初始的全局变量在程序执行之前已经成0了。
数据段:数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。
代码段:代码段(code segment/text segment)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)
栈(stack):栈又称堆栈, 是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。
堆和栈的区别:
管理方式:对于栈而言,是由编译器自动管理,无需手工控制;对于堆而言,释放工作由程序员控制,容易产生memory leak。
空间大小:一般来讲在32位操作系统,堆内存可以到达4G的空间,从这个角度来看堆内存几乎是没有限制的。但是对于栈来讲,一般都是有一定的空间大小的。
碎片问题:对于堆而言,频繁的使用new和delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的,它们是一一对应的,永远都不可能从栈的中间弹出。
生长方向:对于堆而言,生长方向是向上的,也就是向着内存地址增加的方向增长;对于栈来讲,它的生长方向是向下的,是向着内存地址减少的方向增长。
分配方式:堆都是动态分配的,没有静态分配的堆,栈有2种分配方式,静态分配方式和动态分配方式。静态分配是由编译器完成的,比如说局部变量的分配;动态分配由alloca函数进行分配的,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放的,无需手工实现。
分配效率:栈是机器系统提供的数据结构,计算机会在底层栈提供支持,分配专门的寄存器存放的栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如,为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够空间(可能由于碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大的内存,然后进行返回。显然,堆的效率比栈要低得多。
根据用于分配内存的方法,C++有3种管理数据内存的方式:自动存储、静态存储和动态存储(有时也叫自由存储空间或堆),在存在时间的长短方面,以这3种方式分配的数据对象各不相同。
自动存储,在函数内部定义的常规变量使用自动存储空间,被称为自动变量,自动变量是一个局部变量,其作用域为包含它的代码块。自动变量通常存储在栈中,栈是后进先出,因此,在程序执行过程中,栈将不断地增大和缩小。
静态存储,是整个程序执行期间都存在的存储方式,使变量成为静态变量的方式有两种,一种是在函数外面定义,另一种是在声明变量时使用关键字static。
编译器将分配固定的内存块来存储所有的静态变量,这些变量在整个程序执行期间一直存在如果没有显示地初始化静态变量,编译器将把它设置为0。
动态存储,它们管理一个内存池,这在C++中被称为自由存储空间或堆中,该内存池同于静态变量和自动变量的内存时分开的,在栈中,自动添加和删除机制使得占用的内存总是连续的,但new和delete的相互影响可能导致占用的自由存储区不连续,这使得跟踪新分配内存的位置更困难。
如果使用new运算符在自由存储空间(或堆)上创建变量后,没有调用delete,将发生什么情况呢?如果没有调用delete,则包含指针的内存由于作用域规则和对象生命周期的原因而被释放,在自由存储空间上动态分配的变量或结构也将继续存在。实际上,将会无法访问自由存储空间中的结构,因为指向这些内存的指针是无效的,这将导致内存泄露,被泄露的内存将在整个程序的生命周期都不可使用,这些内存被分配出去,但无法收回。极端情况下,内存泄露可能会导致非常严重的情况,以致于应用程序可用的内存被耗尽,出现内存耗尽错误,导致程序崩溃。
要避免内存泄露,最好养成这样一种习惯,即同时使用new和delete运算符,在自由存储空间上动态分配内存,随后便释放它。C++智能指针有助于自动完成这种任务。
C++中一种处理符号常量的方法,这种方法就是使用const关键字来修改变量声明和初始化。关键字const叫做限定符,因为它限定了声明的含义。使用const关键字在一定程度上可以提高程序的安全性和可靠性。
const主要作用:
const只修饰其后的变量,至于const放在类型前还是类型后并没有区别,如,const int a和int const a都是修饰a为const。注意*不是一种类型,如果*pType之前是某类型,那么pType是指向该类型的指针。
一个简单的判断方法:指针运算符*,从右向左,那么如:char const * pContent,可以理解为char const (* pContent),即* pContent为const,而pContent则是可变的。
(1)int const * p1,p2;
p2是const;(*p1)是一整体,因此(*p1)是const,但p1是可变的。
(2)int const * const p1,p2;
p2是const,是前一个const修饰的,*p1也被前一个const修饰,而p1被后一个const修饰。
(3)int * const p1,p2;
p1是const,(* const p1)是整体,所以const不修饰p2。
const在*的左边,则指针指向的变量的值不可直接通过指针改变;在*的右边,则指针的指向不可变。简记为“左定值,右定向”。
cin该操作符是根据后面的变量的类型读取数据,输入结束条件:遇到Enter、Space、Tab键。对结束符的处理:丢弃缓冲区中的结束符。
cin.get()函数有三种格式:无参数,一参数,二参数即cin.get(),cin.get(char ch), cin.get(array_name, Arsize)读取字符的情况。输入结束条件:遇见Enter键。对结束符的处理:不丢弃缓冲区的Enter键。但是对于函数cin.get(array_name,Arsize)函数而言,会丢弃紧挨着Enter键前面的一个space键,同时也会丢弃Enter键。
cin.getline(array_name,Arsize)与 cin.get(array_name,Arsize)用法差不多,以Enter结束,可以接收空格,按照长度Arsize读取字符,会丢弃最后的Enter字符,但是这两个函数是有区别的:cin.get(array_name,Arsize)当输入的字符串超长时,不会引起cin函数的错误,后面的cin操作会继续执行,只是直接从缓冲区中读取数据。但是cin.getline(array_name,Arsize)当输入超长时,会引起cin函数的错误,后面的cin函数的错误,后面的cin操作将不再执行。
我们经常会看到程序中会出现cin.clear(),cin.ignore(), cin.fail()等函数,这些函数都是与cin的错误处理有关,我们将进一步分析一下cin的错误处理机制,并且学习几个重要的函数:cin.fail(), cin.bad(), cin.good(), cin.clear(), cin.ignore()等。程序执行时有一个标志变量来标志输入的异常状态,其中有三位标志位分别用来标志三种异常信息,它们分别是:failbit、eofbit、badbit。以上四个常量对应的取值为:
ios::badbit=001输入(输出)流出现致命错误,不可挽回
ios::eofbit=010已经到达文件尾
ios::failbit=100输入(输出)流出现非致命错误,可挽回
ios::goodbit=000流状态完全正常, 各异常标志位都为0。
当cin出现异常时,没有利用函数cin.clear()函数来讲标志清零。
通常,要声明指向特定类型的函数指针,可以首先编写这种函数的原型,然后用函数指针替换函数名。
为提供正确的运算符优先级,必须在声明中使用括号将*pf括起,括号的优先级比*运算符高,因此*pf(int)意味着pf()是一个返回指针的函数,而(*pf)(int)意味着pf是一个指向函数的指针。
常规函数和内联函数之间的主要区别不在于编写方式,而在于C++编译器如何将它们组合到程序中。
编译过程的最终产品是可执行程序----由一组机器语言指令组成。运行程序时,操作系统将这些指令载入到计算机内存中,因此每一条指令都有特定的内存地址。计算机随后将逐步执行这些指令。常规函数调用使程序跳到另一个地址,并在函数结束时返回。执行到函数调用指令时,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈中,跳到标记函数起点的内存单元,执行函数代码(也还需将返回值放入到寄存器中),然后跳回到地址被保存的指令出处。来回跳跃并记录跳跃位置意味着需要一定的开销。
C++提供另一种选择,内联函数。内联函数的编译代码与其他程序代码“内联”起来。也就是说,编译器将使用相应的函数代码替换函数调用。对于内联代码,程序无需跳到另一个位置处执行代码,再跳回来。因此,内联函数的运行速度比常规函数稍快,但代价是需要占用更多的内存。如果程序在10个不同的地方调用同一个内联函数,则该程序将包含该函数代码的10个副本。
内联函数应该选择地使用,如果执行函数代码的时间比处理函数调用机制的时间长,则节省的时间将只占整个过程的很小一部分。如果代码执行时间很短,则内联调用就可以节省非内联调用使用的大部分时间。
要使用内联特性,必须采用下述措施之一:
使用内联函数的时候要注意:
1.递归函数不能定义为内联函数
2.内联函数一般适合于不存在while和switch等复杂的结构且只有1~5条语句的小函数上,否则编译系统将该函数视为普通函数。
3.内联函数只能先定义后使用,否则编译系统也会把它认为是普通函数。
4.对内联函数不能进行异常的接口声明。
返回引用时最重要的一点是,应避免返回函数终止时不再存在的内存单元引用。为了避免这种问题,最简单的方法,返回一个作为参数传递给函数的引用,作为参数的引用将指向调用函数使用的数据,因此返回的引用也将指向这些数据。
函数重载是指在同一个作用域内,可以有一组具有相同函数名,不同参数列表的函数。
何时使用函数重载,虽然函数重载很吸引人,但不能乱用,仅当函数基本执行相同的任务,但是用不同形式的数据时,才应采用函数重载,重载函数通常用来命名一组功能相似的函数,这样做减少了函数名的数量,对于程序的可读性有很大的好处。
函数重载时,并不区分const和非const变量,主要是由于将非const值赋给const变量是合法的,但反之则是非法的。
编译器如何解决命令冲突的,可以利用反汇编看出,重载的函数名变了,不再是以前的名字,这样不存在命令冲突的问题了,但是变名机制是怎样的?根据返回类型+函数名+参数列表来定义不同的函数名。
变量定义,用于变量分配存储空间,还可为变量指定初始值,在程序中,变量有且仅有一个定义。
变量声明,用于向程序表明变量的类型和名字。
定义也是声明,当定义变量时我们声明了它的类型和名字。
extern关键字,通过使用extern关键字声明变量名而不是定义它。
声明仅仅是将一个符号引入到一个作用域,而定义提供了一个实体在程序中的唯一描述,在一个给定的定义域中重复声明一个符号是可以的,但是却不能重复定义,否则将会引起编译器错误。但是在类中的成员函数和静态数据成员却是例外,虽然在类内它们都是声明,但是也不能有多个。
在编译时,编译器只检测程序语法和函数、变量是否被声明。如果函数未被声明,编译器会给出一个警告,但可以生成目标文件,而在链接程序时,链接器会在所有的目标文件中找寻函数的实现。如果找不到,那到就会报链接错误。
链接把不同编译单元产生的符号联系起来,有两种链接方式,内部链接和外部链接。
如果一个符号名对于它的编译单元来说是局部的,并且在链接时不可能与其他编译单元中的同样的名称相冲突,那个这个符号就是内部链接。
带有static、const关键字和枚举类型的连接时内部的。
在一个多文件的程序中,如果一个符号在链接时可以和其他编译单元交互,那么这个名称就有外部链接。外部链接意味着该定义不仅仅局限在单元中,它可以在.o文件中产生外部符号,可以被其他编译单元访问用来解析它们未定义的符号。
非内联成员函数、非内联函数、非静态函数都具有外部函数。
判断一个符号是内部链接还是外部链接的一个很好的方法就是看该符号是否被写入.o文件。
注意头文件中不可以放变量的定义!一般情况下头文件中只放变量的声明,因为头文件要被其他文件包含,如果把定义放到头文件的话,就不能避免多次定义变量,C++不允许多次定义变量,一个程序中对指定变量的定义只有一次,声明可以无数次。
不过有三个列外,第一个,值在编译时就已知const变量的定义可以放到头文件中;第二个,类的定义可以放到头文件中;第三个,inline函数。
在同一个文件中只能将同一个头文件包含一次。但是很有可能在不知情的情况下将头文件包含多次。例如,可能使用包含了另外一个头文件的头文件。有一种标准的C/C++技术可以避免多次包含同一个头文件。它是基于预处理器编译指令#ifndef(即if not defined)的。下面的代码片段意味着仅当以前没有使用预处理器编译指令#define定义名称COORDIN_H时,才处理#ifndef和#endif之间的语句:
通常,使用#define语句来创建符号常量,如下所示:
C++为静态存储持续性变量提供了三种链接性。 外部链接性:可在其它文件中访问;声明不在任何函数内。 内部链接性:只能在当前文件中访问;声明不在任何函数内,使用关键字static。 无链接性:只能在当前函数或代码块中访问;声明在代码块中,使用关键字static。 这三种链接性在整个程序执行期间存在,与自动变量相比(栈中),寿命更长。编译器将分配固定的内存块来存储错有的静态变量。
使用#ifndef只是防止了头文件被重复包含,但是无法防止变量的被重复定义。由于工程中每个.c文件都是独立的解释的,即使头文件有
在其他文件中只要包含了global.h就会独立的解释,然后每个.c文件生成独立的标识符。在编译器链接时,就会将工程中所有的符号整合在一起,由于文件中有重名变量,于是就出现了重复定义的错误。
解决方法:
在.c文件中定义变量,然后建一个头文件(.h文件)在所有变量的声明前加上extern,注意这里不要对变量进行初始化,然后在其他需要的使用全局变量的.c文件中包含.h文件,编译器会为.c文件生成目标文件,然后链接时,如果该.c文件使用了全局变量,链接器就会链接到定义的全局变量的.c文件。
假设在程序的某个文件中调用一个函数,C++将到哪里去寻找该函数的定义呢?如果该文件中的函数原型指出该函数是静态的,则编译器只在文件中查找函数定义;否则,编译器(包括链接程序)将在所有的程序文件中查找。如果找到两个定义,编译器将发出错误消息,因为每一个外部函数只能有一个定义。如果在程序文件中没有找到,编译器将在库中搜索。这意味着如果定义了一个与库函数同名的函数,编译器将使用程序员定义的版本,而不是库函数。
(1) 可能由于循环的递归引起的.
(2) 由于分配了过大的局部变量引起的.
内存泄露是指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。
C++中内存泄露一般指的是堆中内存泄露,堆内存使我们手动malloc/realloc/new申请的程序不会自动收回,需要调用free或delete手动释放,否则就会造成内存泄露。内存泄露其实还应包括系统资料的泄露,比如socket连接等,使用完后也要释放。
内存泄露的原因:
1.使用malloc、new申请的内存,没有使用free或delete手动释放。
2.“无主”内存,申请内存后,指针指向内存的起始地址,若丢失或修改这个指针,那么申请的内存将丢失且没有释放。
3.隐式内存泄露:程序运行中不断申请内存,但是直到程序结束才释放。有些服务器会申请大量内存作为缓存,或申请大量的socket资源作为连接池,这些资源一直占用程序直到程序退出。服务器运行起来一般持续几个月,不及时释放可能会导致内存耗尽。
抽象就是将一些事物的共性和相似点抽离出来,并将这些属性归为一个类。封装,隐藏内部实现细节,可以使得代码模块化。继承可以扩展已存在的代码,目的是代码重用。多态则是为了接口重用,也就是说,不论传递过来的究竟是那个类的对象,函数都能够通过同一个接口调用到适应各自对象的方法。
内存碎片一般是由于空闲的连续空间比要申请的空间小,导致这些小内存块不能被利用。
一、C++中宏的作用
1、使用宏定义常量
并且可能在以后对该值做出修改时,使用宏定义,我们仅需要改变宏定义所表示的值即可。
二、const与宏区别
(1) 编译器处理方式不同:define宏是在预编译阶段展开;const常量是编译运行阶段使用。
(2) 类型和安全检查不同:define宏没有类型,不做任何类型检查,仅仅是展开。const常量有具体的类型,在编译阶段会执行类型检查。
(3) 可以节省空间,避免不必要的内存分配, const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝。
(4) 对const常量取地址时,编译器会进行内存分配,并将常量转换为立即数存入内存,而不是存入记录在常量表中的地址,在使用常量时,编译器回到常量表中查询对应的常量,并将其替换,没有了存储与读内存的操作,使得它的效率也很高。
一般放在函数体后,形如:void fun() const;
如果一个成员函数的不会修改数据成员,那么最好将其声明为const,因为const成员函数中不允许对数据成员进行修改,如果修改,编译器将报错,这大大提高了程序的健壮性。
const成员函数不可以更改对象内任何non-static成员变量。
mutable关键字释放掉non-static成员变量的const的约束。
当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。
定义一个空的类型,里面没有任何成员变量和成员函数,对该类型求sizeof,得到结果是1。因为,空类型的实例中不包含任何信息,本来求sizeof应该是0,但是当声明该类型的实例的时候,它必须在内存中占有一定的空间,否则无法使用这些实例,至于占用多少内存,由编译器决定,VS2010中每个空类型的实例占用1字节的空间。
如果在该类型中添加一个构造函数和析构函数,再对该类型求sizeof,得到的结果又是多少?
还是1,调用构造函数和析构函数只需要知道函数的地址即可,而这些函数的地址只与类型相关,而与类型的实例无关,编译器也不会因为有这两个函数而在实例内添加任何额外的信息。
那如果把析构函数标记为虚函数呢?
C++的编译器一旦发现一个类型中有虚函数,就会为该类型生成虚函数表,并在该类型的每一个实例中添加一个指向虚函数表的指针。在32位的机器上,一个指针占4字节的空间,因此求sizeof得到4,如果是64位机器,一个指针占8字节,因此求sizeof则得到8。
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
回调函数就是允许用户把需要调用的方法的指针作为参数传递给一个函数,以便该函数在处理相似事件的时候可以灵活的使用不同的方法。
我们要编写一个库,它提供了某些排序算法的实现(如冒泡排序、快速排序、shell排序、shake排序等等),为了能让库更加通用,不想在函数中嵌入排序逻辑,而让使用者来实现相应的逻辑;或者,能让库可用于多种数据类型(int、float、string),此时,该怎么办呢?可以使用函数指针,并进行回调。
核心就是类成员函数需要this指针访问函数,而全局或者静态函数不需要this指针。简言之,类的成员函数需要隐含的this指针而回调函数没有办法提供。
virtual函数是动态绑定,而缺省参数值是静态绑定的,意思是可能会在调用一个定义与派生类在内的虚函数的同时,却使用基类为它所指向的缺省参数值。因此绝不重新定义继承而来的缺省参数值!
内存对齐会浪费一些空间,但是这种空间上浪费却可以减少时间取数时间。需要遵守的原则:
(1)结构体内成员按自身按自身长度自对齐,自身长度,如char =1,short = 2,int = 4,double = 8,所谓自对齐,指的是该成员的起始位置的内存地址必须是它自身长度的整数倍。如int只能以0,4,8地址开始。
(2)结构体的总大小为结构体的有效对齐值的整数倍,结构体的有效值对齐值的确定:
当未明确指定时,以结构体中最长的成员的长度为其有效值。
当用#pragma pack(n)指定时,以n和结构体中最长的成员的长度中较小者为其值。
当__attribute__ 指定时,以它为准。
联合体
用途:使几个不同类型的变量共占一段内存(相互覆盖)
结构体是一种构造数据类型
用途:把不同类型的数据组合成一个整体-------自定义数据类型
结构体的内存分配:
结构体在内存中分配一块连续的内存,但结构体内的变量并不一定是连续存放的,这涉及到内存对齐。
原则1 数据成员对齐规则:结构(struct或联合union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开始存储)。
原则2 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。(struct a里存有struct b,b里有char,int,double等元素,那b应该从8的整数倍开始存储。)
原则3 收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐。
assert含义是断言,它是标准C++的cassert头文件中定义的一个宏,用来判断一个条件表达式的值是否为ture,如果不为true, 程序会终止,并且报告出错误,这样就很容易将错误定位。
通常我们开发的程序有2种模式:Debug模式和Release模式
1. 在Debug模式下,编译器会记录很多调试信息,也可以加入很多测试代码,比如加入断言assert,方便我们程序员测试,以及出现bug时的分析解决。
2. Release模式下,就没有上述那些调试信息,而且编译器也会自动优化一些代码,这样生成的程序性能是最优的,但是如果出现问题,就不方便分析测试了。
在继承中派生类的对象调用构造函数的顺序,应该是先调用基类的构造函数,然后是成员中的对象对应类的构造函数,最后是派生类自己的构造函数。
野指针:指向垃圾内存的指针,而非空指针。野指针产生原因:
1.声明的指针未被初始化,指针默认值随机产生。创建指针应该将其初始化为NULL或者指向某一内存。
2.free和delete掉的指针未重置为NULL,free后的指针仍指向该内存,但该内存已变为垃圾内存。指针变量的值没变,只是指向的空间不合法了。
有些信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态, 用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”是把一个字节中的二进位划分为几个不同的区域, 并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。
如果结构体中含有位域(bit-field),那么VC中准则是:
1) 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;
2) 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
3) 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式(不同位域字段存放在不同的位域类型字节中),Dev-C++和GCC都采取压缩方式;
系统会先为结构体成员按照对齐方式分配空间和填塞(padding),然后对变量进行位域操作。
"typename"是一个C++程序设计语言中的关键字。相当用于泛型编程时是另一术语"class"的同义词。这个关键字用于指出模板声明(或定义)中的非独立名称(dependent names)是类型名,而非变量名。
这段代码看起来能通过编译,但是事实上这段代码并不正确。因为编译器并不知道T::bar究竟是一个类型的名字还是一个某个变量的名字。究其根本,造成这种歧义的原因在于,编译器不明白T::bar到底是不是“模板参数的非独立名字”,简称“非独立名字”。注意,任何含有名为“bar”的项的类T,都可以被当作模板参数传入foo()函数,包括typedef类型、枚举类型或者变量等。
解决问题的最终办法,就是显式地告诉编译器,T::bar是一个类型名。这就必须用typename关键字。
template内出现的名称如果相依于某个template参数,称之为从属名称,如果从属名称在class内呈嵌套状,称它为嵌套从属名称。
声明template参数时,前缀关键字class和typename可互换。请使用关键字typename标识嵌套从属类型名称;但不得在base class lists(基类列)或member initialization list(成员初值列)内以它作为base class修饰符。
面向过程
优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源,比如嵌入式开发,linux/unix一般采用面向过程的开发,性能是最重要的因素。
缺点:没有面向对象易维护、易复用、易扩展。
面向对象
优点:易维护、易扩展、易复用,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统。
缺点:性能比面向过程低。
相同点:都可用于申请动态内存和释放内存。
不同点:
(1)操作对象有所不同
malloc与free是C++/C 语言的标准库函数,new/delete 是C++的运算符。对于非内部数据类的对象而言,光用maloc/free 无法满足动态对象的要求。对象在创建的同时要自动执行构造函数, 对象消亡之前要自动执行析构函数。由于malloc/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加malloc/free。
(2)用法也有所不同
运算符new 使用起来要比函数malloc 简单得多,例如:
int *p1 = (int *)malloc(sizeof(int) * length);
int *p2 = new int[length];
这是因为new 内置了sizeof、类型转换和类型安全检查功能。对于非内部数据类型的对象而言,new 在创建动态对象的同时完成了初始化工作。如果对象有多个构造函数,那么new 的语句也可以有多种形式。
如果用new 创建对象数组,那么只能使用对象的无参数构造函数。例如Obj *objects = new Obj[100];// 创建100 个动态对象
不能写成
Obj *objects = new Obj[100](1); // 创建100 个动态对象的同时赋初值1
在用delete 释放对象数组时,留意不要丢了符号‘[]’。例如
delete []objects; // 正确的用法
delete objects; // 错误的用法
后者相当于delete objects[0],漏掉了另外99 个对象。
//
1、new自动计算需要分配的空间,而malloc需要手工计算字节数。
2、new是类型安全的,而malloc不是,比如:
int* p = new float[2]; // 编译时指出错误。
int* p = malloc(2*sizeof(float)); // 编译时无法指出错误。
new operator 由两步构成,分别是 operator new 和 construct。
3、operator new对应于malloc,但operator new可以重载,可以自定义内存分配策略,甚至不做内存分配,甚至分配到非内存设备上。而malloc无能为力。
4、new将调用constructor,而malloc不能;delete将调用destructor,而free不能。
5、malloc/free要库文件支持,new/delete则不要。
1、本质区别
malloc/free是C/C++语言的标准库函数,new/delete是C++的运算符。
对于用户自定义的对象而言,用maloc/free无法满足动态管理对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。因此C++需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。
2、联系
既然new/delete的功能完全覆盖了malloc/free,为什么C++还保留malloc/free呢?因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。如果用free释放“new创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序出错。如果用delete释放“malloc申请的动态内存”,理论上讲程序不会出错,但是该程序的可读性很差。所以new/delete、malloc/free必须配对使用。
new 操作符的执行过程
(1). 调用operator new分配内存 ;
(2). 调用构造函数生成类对象;
(3). 返回相应指针。
Java与C++都是面向对象的语言,都使用了面向对象的思想(封装、继承、多态),由于面向对象由许多非常好的特性(继承、组合等),因此二者有很好的可重用性。
1、Java为解释性语言,其运行过程为:程序源代码经过Java编译器编译成字节码,然后由JVM解释执行。而C/C++为编译型语言,源代码经过编译和链接后生成可执行的二进制代码,可直接执行。因此Java的执行速度比C/C++慢,但Java能够跨平台执行,C/C++不能。
2、Java是纯面向对象语言,所有代码(包括函数、变量)必须在类中实现,除基本数据类型(包括int、float等)外,所有类型都是类。此外,Java语言中不存在全局变量或者全局函数,而C++兼具面向过程和面向对象编程的特点,可以定义全局变量和全局函数。
3、与C/C++语言相比,Java语言中没有指针的概念,这有效防止了C/C++语言中操作指针可能引起的系统问题,从而使程序变得更加安全。
4、与C++语言相比,Java语言不支持多重继承,但是Java语言引入了接口的概念,可以同时实现多个接口。由于接口也有多态特性,因此Java语言中可以通过实现多个接口来实现与C++语言中多重继承类似的目的。
5、在C++语言中,需要开发人员去管理内存的分配(包括申请和释放),而Java语言提供了垃圾回收器来实现垃圾的自动回收,不需要程序显示地管理内存的分配。在C++语言中,通常会把释放资源的代码放到析构函数中,Java语言中虽然没有析构函数,但却引入了一个finalize()方法,当垃圾回收器要释放无用对象的内存时,会首先调用该对象的finalize()方法,因此,开发人员不需要关心也不需要知道对象所占的内存空间何时被释放。
耦合性:也称块间联系。指软件系统结构中各模块间相互联系紧密程度的一种度量。模块之间联系越紧密,其耦合性就越强,模块的独立性则越差。模块间耦合高低取决于模块间接口的复杂性、调用的方式及传递的信息。
内聚性:又称块内联系。指模块的功能强度的度量,即一个模块内部各个元素彼此结合的紧密程度的度量。若一个模块内各元素(语名之间、程序段之间)联系的越紧密,则它的内聚性就越高。
1、什么是虚继承?
虚拟继承(Virtual Inheritance),当在多条继承路径上有一个公共的基类,在这些路径中的某几条汇合处,这个公共的基类就会产生多个实例(或多个副本),若只想保存这个基类的一个实例,可以将这个公共基类说明为虚基类。
在继承中产生歧义的原因有可能是继承类继承了基类多次,从而产生了多个拷贝,即不止一次的通过多个路径继承类在内存中创建了基类成员的多份拷贝。虚基类的基本原则是在内存中只有基类成员的一份拷贝。这样,通过把基类继承声明为虚拟的,就只能继承基类的一份拷贝,从而消除歧义。用virtual限定符把基类继承说明为虚拟的。
2、虚继承解决了什么问题?
解决了二义性问题,也节省了内存,避免了数据不一致的问题。
virtual base class的原始模型是在class object中为每一个有关联的virtual base class加上一个指针vptr,该指针指向virtual基类表。有的编译器是在继承类已存在的virtual table直接扩充导入一个virtual base class table。不管怎么样由于虚继承已完全破坏了继承体系,不能按照平常的继承体系来进行类型转换。
继承关系说明:A是B和C的基类,B和C是D的基类
因为A是D的间接基类,通过D访问A的数据成员有两条路可走,一是D->B->A,另外一条是:D->C->A.所以导致了二义性。
C++类型转换分为显式类型转换和隐式类型转换 ,隐式类型转换由编译器自动完成,这里只讨论显式类型转换。
static_cast,dynamic_cast,const_cast和reinterpret_cast四种,表示转换的方式
(1)static_cast
任何编写程序时能够明确的类型转换都可以使用static_cast(static_cast不能转换掉底层const,volatile和__unaligned属性)。由于不提供运行时的检查,所以叫static_cast,因此,需要在编写程序时确认转换的安全性。
(2)dynamic_cast
dynamic_cast转换仅适用于指针或引用。
(3)const_cast
const_cast用于移除类型的const、volatile和__unaligned属性。
(4)reinterpret_cast
非常激进的指针类型转换,在编译期完成,可以转换任何类型的指针,所以极不安全。非极端情况不要使用。
volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。声明时语法:int volatile vInt; 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。例如:
volatile int i=10;
int a = i;
...
// 其他代码,并未明确告诉编译器,对 i 进行过操作
int b = i;
volatile 指出 i 是随时可能发生变化的,每次使用它的时候必须从 i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在 b 中。而优化做法是,由于编译器发现两次从 i读数据的代码之间的代码没有对 i 进行过操作,它会自动把上次读的数据放在 b 中。而不是重新从 i 里面读。这样以来,如果 i是一个寄存器变量或者表示一个端口数据就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问。
1) 中断服务程序中修改的供其它程序检测的变量需要加 volatile;
2) 多任务环境下各任务间共享的标志应该加 volatile;
3) 存储器映射的硬件寄存器通常也要加 volatile 说明,因为每次对它的读写都可能由不同意义;
多线程下的volatile
有些变量是用 volatile 关键字声明的。当两个线程都要用到某一个变量且该变量的值会被改变时,应该用 volatile 声明,该关键字的作用是防止优化编译器把变量从内存装入 CPU 寄存器中。如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行。volatile 的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值,如下: