请试着编写一个程序,让用户传入一个数值N,判断N是否为3的倍数,如果是,就输出T?

第一章  开发中通用的方法和准则

建议1:不要在常量和变量中出现易混淆的字母;

(i、l、1;o、0等)。

建议2:莫让常量蜕变成变量;

(代码运行工程中不要改变常量值)。

建议3:三元操作符的类型务必一致;

建议4:避免带有变长参数的方法重载;

(变长参数的方法重载之后可能会包含原方法)。

建议5:别让null值和空值威胁到变长方法;

(两个都包含变长参数的重载方法,当变长参数部分空值,或者为null值时,重载方法不清楚会调用哪一个方法)。

建议6:覆写变长方法也循规蹈矩;

(变长参数与数组,覆写的方法参数与父类相同,不仅仅是类型、数量,还包括显示形式)。

建议7:警惕自增的陷阱;

建议8:不要让旧语法困扰你;

(Java中抛弃了中的goto语法,但是还保留了该关键字,只是不进行语义处理,const关键中同样类似)。

建议9:少用静态导入;

(Java5引入的静态导入语法import static,使用静态导入可以减少程序字符输入量,但是会带来很多代码歧义,省略的类约束太少,显得程序晦涩难懂)。

建议10:不要在本类中覆盖静态导入的变量和方法;

(例如静态导入Math包下的PI常量,类属性中又定义了一个同样名字PI的常量。编译器的“最短路径原则”将会选择使用本类中的PI常量。本类中的属性,方法优先。如果要变更一个被静态导入的方法,最好的办法是在原始类中重构,而不是在本类中覆盖)。

建议11:养成良好的习惯,显式声明UID;

(显式声明serialVersionUID可以避免序列化和反序列化中对象不一致,JVM根据serialVersionUID来判断类是否发生改变。隐式声明由编译器在编译的时候根据包名、类名、继承关系等诸多因子计算得出,极其复杂,算出的值基本唯一)。

建议12:避免用序列化类在构造函数中为不变量赋值;

(在序列化类中,不适用构造函数为final变量赋值)(序列化规则1:如果final属性是一个直接量,在反序列化时就会重新计算;序列化规则2:反序列化时构造函数不会执行;反序列化执行过程:JVM从数据流中获取一个Object对象,然后根据数据流中的类文件描述信息(在序列化时,保存到磁盘的对象文件中包含了类描述信息,不是类)查看,发现是final变量,需要重新计算,于是引用Person类中的name值,而此时JVM又发现name没有赋值(因为反序列化时构造函数不会执行),不能引用,于是它不再初始化,保持原始值状态。整个过程中需要保持serialVersionUID相同)。

建议13:避免为final变量复杂赋值;

(类序列化保存到磁盘上(或网络传输)的对象文件包括两部分:1、类描述信息:包括包路径、继承关系等。注意,它并不是class文件的翻版,不记录方法、构造函数、static变量等的具体实现。2、非瞬态(transient关键字)和非静态(static关键字)的实例变量值。总结:反序列化时final变量在以下情况下不会被重新赋值:1、通过构造函数为final变量赋值;2、通过方法返回值为final变量赋值;3、final修饰的属性不是基本类型)。

建议14:使用序列化类的私有方法巧妙解决“部分属性持久化问题”;

(部分属性持久化问题解决方案1:把不需要持久化的属性加上瞬态关键字(transient关键字)即可,但是会使该类失去了分布式部署的功能。方案2:新增业务对象。方案3:请求端过滤。方案4:变更传输契约,即覆写writeObject和readObject私有方法,在两个私有方法体内完成部分属性持久化)。

建议15:break万万不可忘;

(switch语句中,每一个case匹配完都需要使用break关键字跳出,否则会依次执行完所有的case内容。)。

建议16:易变业务使用脚本语言编写;

(脚本语言:都是在运行期解释执行。脚本语言三大特性:1、灵活:动态类型;2、便捷:解释型语言,不需要编译成二进制,不需要像Java一样生成字节码,依靠解释执行,做到不停止应用变更代码;3、简单:部分简单。Java使用ScriptEngine执行引擎来执行脚本代码)。

建议17:慎用动态编译;

好处:更加自如地控制编译过程。很少使用,原因:静态编译能够完成大部分工作甚至全部,即使需要使用,也有很好的替代方案,如JRuby、Groovy等无缝的脚本语言。动态编译注意以下4点:1、在框架中谨慎使用:debug困难,成本大;2、不要在要求高性能的项目中使用:需要一个编译过程,比静态编译多了一个执行环节;3、动态编译要考虑安全问题:防止恶意代码;4、记录动态编译过程)。

(instanceof用来判断一个对象是否是一个类的实例,只能用于对象的判断,不能用于基本类型的判断(编译不通过),instanceof操作符的左右操作数必须有继承或实现关系,否则编译会失败。例:null instanceof String返回值是false,instanceof特有规则,若左操作数是null,结果就直接返回false,不再运算右操作数是什么类)。

建议19:断言绝对不是鸡肋;

(防御式编程中经常使用断言(Assertion)对参数和环境做出判断。断言是为调试程序服务的。两个特性:1、默认assert不启用;2、assert抛出的异常AssertionError是继承自Error的)。

建议20:不要只替换一个类;

(发布应用系统时禁止使用类文件替换方式,整体WAR包发布才是完全之策)(Client类中调用了Constant类中的属性值,如果更改了Constant常量类属性的值,重新编译替换。而不改变或者替换Client类,则Client中调用的Constant常量类的属性值并不会改变。原因:对于final修饰的基本类型和String类型,编译器会认为它是稳定态(Immutable Status),所以在编译时就直接把值编译到字节码中了,避免了再运行期引用,以提高代码的执行效率。而对于final修饰的类(即非基本类型),编译器认为它是不稳定态(Mutable Status),在编译时建立的则是引用关系(该类型也叫作Soft Final),如果Client类引入的常量是一个类或实例,即使不重新编译也会输出最新值)。

建议21:用偶判断,不用奇判断;

(不要使用奇判断(i%2 == 1 ? "奇数" : "偶数"),使用偶判断(i%2 == 0 ? "偶数" : "奇数")。原因Java中的取余(%标识符):数据输入1 2 0 -1 -2,奇判断的时候,当输入-1时,也会返回偶数。

建议22:用整数类型处理货币;

(不要使用float或者double计算货币,因为在计算机中浮点数“有可能”是不准确的,它只能无限接近准确值,而不能完全精确。不能使用计算机中的二进制位来表示如mons.lang.builder包下的Hash码生成工具HashCodeBuilder)。

(原始toString方法显示不人性化)。

(package-info类是专门为本包服务的,是一个特殊性主要体现在3个方面:1、它不能随便被创建;2、它服务的对象很特殊;3、package-info类不能有实现代码;package-info类的作用:1、声明友好类和包内访问常量;2、为在包上标注注解提供便利;3、提供包的整体注释说明)。

建议51:不要主动进行垃圾回收

(主动进行垃圾回收是一个非常危险的动作,因为System.gc要停止所有的响应(Stop 天河world),才能检查内存中是否有可回收的对象,所有的请求都会暂停)。

建议52:推荐使用String直接量赋值;

(一般对象都是通过new关键字生成,String还有第二种生成方式,即直接声明方式,如String str = "a";String中极力推荐使用直接声明的方式,不建议使用new String("a")的方式赋值。原因:直接声明方式:创建一个字符串对象时,首先检查字符串常量池中是否有字面值相等的字符串,如果有,则不再创建,直接返回池中引用,若没有则创建,然后放到池中,并返回新建对象的引用。使用new String()方式:直接声明一个String对象是不检查字符串常量池的,也不会吧对象放到池中。String的intern方法会检查当前的对象在对象池中是否有字面值相同的引用对象,有则返回池中对象,没有则放置到对象池中,并返回当前对象)。

建议53:注意方法中传递的参数要求;

(replaceAll方法传递的第一个参数是正则表达式)。

(String使用“+”进行字符串连接,之前连接之后会产生一个新的对象,所以会不断的创建新对象,优化之后与StringBuilder和StringBuffer采用同样的append方法进行连接,但是每一次字符串拼接都会调用一次toString方法,所以会很耗时。StringBuffer与StringBuilder基本相同,只是一个字符数组的在扩容而已,都是可变字符序列,不同点是:StringBuffer是线程安全的,StringBuilder是线程不安全的)。

建议55:注意字符串的位置;

(在“+”表达式中,String字符串具有最高优先级)(Java对加号“+”的处理机制:在使用加号进行计算的表达式中,只要遇到String字符串,则所有的数据都会转换为String类型进行拼接,如果是原始数据,则直接拼接,如果是对象,则调用toString方法的返回值然后拼接)。

建议56:自由选择字符串拼接方式;

(字符串拼接有三种方法:加号、concat方法及StringBuilder(或StringBuffer)的append方法。字符串拼接性能中,StringBuilder的append方法最快,concat方法次之,加号最慢。原因:1、“+”方法拼接字符串:虽然编译器对字符串的加号做了优化,使用StringBuidler的append方法进行追加,但是与纯粹使用StringBuilder的append方法不同:一是每次循环都会创建一个StringBuilder对象,二是每次执行完都要调用toString方法将其准换为字符串--toString方法最耗时;2、concat方法拼接字符串:就是一个数组拷贝,但是每次的concat操作都会新创建一个String对象,这就是concat速度慢下来的真正原因;3、append方法拼接字符串:StringBuidler的append方法直接由父类AbstractStringBuilder实现,整个append方法都在做字符数组处理,没有新建任何对象,所以速度快)。

建议57:推荐在复杂字符串操作中使用正则表达式;

(正则表达式是恶魔,威力强大,但难以控制)。

建议58:强烈建议使用UTF编码;

(一个系统使用统一的编码)。

建议59:对字符串排序持一种宽容的心态;

(如果排序不是一个关键算法,使用Collator类即可。主要针对于中文)。

第五章  数组和集合

建议60:性能考虑,数组是首选;

(性能要求较高的场景中使用数组替代集合)(基本类型在栈内存中操作,对象在堆内存中操作。数组中使用基本类型是效率最高的,使用集合类会伴随着自动装箱与自动拆箱动作,所以性能相对差一些)。

建议61:若有必要,使用变长数组;

建议62:警惕数组的浅拷贝;

(通过Arrays.copyOf(box1,box1.length)方法产生的数组是一个浅拷贝,这与序列化的浅拷贝完全相同:基本类型是直接拷贝值,其他都是拷贝引用地址。数组中的元素没有实现Serializable接口)。

建议63:在明确的场景下,为集合指定初始容量;

(ArrayList集合底层使用数组存储,如果没有初始为ArrayList指定数组大小,默认存储数组大小长度为10,添加的元素达到数组临界值后,使用Arrays.copyOf方法进行1.5倍扩容处理。HashMap是按照倍数扩容的,Stack继承自Vector,所采用扩容规则的也是翻倍)。

建议64:多种最值算法,适时选择;

(最值计算时使用集合最简单,使用数组性能最优,利用Set集合去重,使用TreeSet集合自动排序)。

建议65:避开基本类型数组转换列表陷阱;

(原始类型数组不能作为asList的输入参数,否则会引起程序逻辑混乱)(基本类型是不能泛化的,在java中数组是一个对象,它是可以泛化的。使用Arrays.asList(data)方法传入一个基本类型数组时,会将整个基本类型数组作为一个数组对象存入,所以存入的只会是一个对象。JVM不可能输出Array类型,因为Array是属于java.lang.reflect包的,它是通过反射访问数组元素的工具类。在Java中任何一个数组的类都是“[I”,因为Java并没有定义数组这个类,它是编译器编译的时候生成的,是一个特殊的类)。

建议66:asList方法产生的List对象不可更改;

(使用add方法向asList方法生成的集合中添加元素时,会抛UnsupportedOperationException异常。原因:asList生成的ArrayList集合并不是java.util.ArrayList集合,而是Arrays工具类的一个内置类,我们经常使用的List.add和List.remove方法它都没有实现,也就是说asList返回的是一个长度不可变的列表。此处的列表只是数组的一个外壳,不再保持列表动态变长的特性)。

建议67:不同的列表选择不同的遍历方法;

(ArrayList数组实现了RandomAccess接口(随机存取接口),ArrayList是一个可以随机存取的列表。集合底层如果是基于数组实现的,实现了RandomAccess接口的集合,使用下标进行遍历访问性能会更高;底层使用双向链表实现的集合,使用foreach的迭代器遍历性能会更高)。

建议68:频繁插入和删除时使用LinkedList;

(ArrayList集合,每次插入或者删除一个元素,其后的所有元素就会向后或者向前移动一位,性能很低。LinkedList集合插入时不需要移动其他元素,性能高;修改元素,LinkedList集合比ArrayList集合要慢很多;添加元素,LinkedList与ArrayList集合性能差不多,LinkedList添加一个ListNode,而ArrayList则在数组后面添加一个Entry)。

建议69:列表相等只需关心元素数据;

(判断集合是否相等时只须关注元素是否相等即可)(ArrayList与Vector都是List,都实现了List接口,也都继承了AbstractList抽象类,其equals方法是在AbstractList中定义的。所以只要求两个集合类实现了List接口就成,不关心List的具体实现类,只要所有的元素相等,并且长度也相等就表明两个List是相等的,与具体的容量类型无关)。

建议70:子列表只是原列表的一个视图;

(使用==判断相等时,需要满足两个对象地址相等,而使用equals判断两个对象是否相等时,只需要关注表面值是否相等。subList方法是由AbstractList实现的,它会根据是不是可以随机存取来提供不同的SubList实现方式,RandomAccessSubList是SubList子类,SubList类中subList方法的实现原理:它返回的SubList类是AbstractList的子类,其所有的方法如get、set、add、remove等都是在原始列表上的操作,它自身并没有生成一个数组或是链表,也就是子列表只是原列表的一个视图,所有的修改动作都反映在了原列表上)。

建议71:推荐使用subList处理局部列表;

(需求:要删除一个ArrayList中的20-30范围内的元素;将原列表转换为一个可变列表,然后使用subList获取到原列表20到30范围内的一个视图(View),然后清空该视图内的元素,即可在原列表中删除20到30范围内的元素)。

建议72:生成子列表后不要再操作原列表;

(subList生成子列表后,使用Collections.unmodifiableList(list);保持原列表的只读状态)(利用subList生成子列表后,更改原列表,会造成子列表抛出java.util.ConcurrentModificationException异常。原因:subList取出的列表是原列表的一个视图,原数据集(代码中的list变量)修改了,但是subList取出的子列表不会重新生成一个新列表(这点与视图是不相同的),后面再对子列表操作时,就会检测到修改计数器与预期的不相同,于是就抛出了并发修改异常)。

(Comparable接口可以作为实现类的默认排序法,Comparator接口则是一个类的扩展排序工具)(两种数据排序实现方式:1、实现Comparable接口,必须要实现compareTo方法,一般由类直接实现,表明自身是可比较的,有了比较才能进行排序;2、实现Comparator接口,必须实现compare方法,Comparator接口是一个工具类接口:用作比较,它与原有类的逻辑没有关系,只是实现两个类的比较逻辑)。

建议74:不推荐使用binarySearch对列表进行检索;

(indexOf与binarySearch方法功能类似,只是使用了二分法搜索。使用二分查找的首要条件是必须要先排序,不然二分查找的值是不准确的。indexOf方法直接就是遍历搜寻。从性能方面考虑,binarySearch是最好的选择)。

(实现了compareTo方法,就应该覆写equals方法,确保两者同步)(在集合中indexOf方法是通过equals方法的返回值判断的,而binarySearch查找的依据是compareTo方法的返回值;equals是判断元素是否相等,compareTo是判断元素在排序中的位置是否相同)。

建议76:集合运算时使用更优雅的方式;

建议77:使用shuffle打乱列表;

建议78:减少HashMap中元素的数量;

(尽量让HashMap中的元素少量并简单)(现象:使用HashMap存储数据时,还有空闲内存,却抛出了内存溢出异常;原因:HashMap底层的数组变量名叫table,它是Entry类型的数组,保存的是一个一个的键值对。与ArrayList集合相比,HashMap比ArrayList多了一次封装,把String类型的键值对转换成Entry对象后再放入数组,这就多了40万个对象,这是问题产生的第一个原因;HashMap在插入键值对时,会做长度校验,如果大于或等于阈值(threshold变量),则数组长度增大一倍。默认阈值是当前长度与加载因子的乘积,默认的加载因子(loadFactor变量)是0.75,也就是说只要HashMap的size大于数组长度的0.75倍时,就开始扩容。导致到最后,空闲的内存空间不足以增加一次扩容时就会抛出OutOfMemoryError异常)。

建议79:集合中的哈希码不要重复;

(列表查找不管是遍历查找、链表查找或者是二分查找都不够快。最快的是Hash开头的集合(如HashMap、HashSet等类)查找,原理:根据hashCode定位元素在数组中的位置。HashMap的table数组存储元素特点:1、table数组的长度永远是2的N次幂;2、table数组中的元素是Entry类型;3、table数组中的元素位置是不连续的;每个Entry都有一个next变量,它会指向下一个键值对,用来链表的方式来处理Hash冲突的问题。如果Hash码相同,则添加的元素都使用链表处理,在查找的时候这部分的性能与ArrayList性能差不多)。

(Vector与ArrayList原理类似,只是是线程安全的,HashTable是HashMap的多线程版本。线程安全:基本所有的集合类都有一个叫快速失败(Fail-Fast)的校验机制,当一个集合在被多个线程修改并访问时,就可能出现ConcurrentModificationException异常,这是为了确保集合方法一致而设置的保护措施;实现原理是modCount修改计数器:如果在读列表时,modCount发生变化(也就是有其他线程修改)则会抛出ConcurrentModificationException异常。线程同步:是为了保护集合中的数据不被脏读、脏写而设置的)。

建议81:非稳定排序推荐使用List;

非稳定的意思是:经常需要改动;TreeSet集合中元素不可重复,且默认按照升序排序,是根据Comparable接口的compareTo方法的返回值确定排序位置的。SortedSet接口(TreeSet实现了该接口)只是定义了在该集合加入元素时将其进行排序,并不能保证元素修改后的排序结果。因此TreeSet适用于不变量的集合数据排序,但不适合可变量的排序。对于可变量的集合,需要自己手动进行再排序)(SortedSet中的元素被修改后可能会影响其排序位置)。

建议82:由点及面,一页知秋--集合大家族;

2、Set:Set是不包含重复元素的集合,其主要的实现类有:EnumSet、HashSet、TreeSet,其中EnumSet是枚举类型的专用Set,HashSet是以哈希码决定其元素位置的Set,原理与HashMap相似,提供快速插入与查找方法,TreeSet是一个自动排序的Set,它实现了SortedSet接口;

5、数组:数组能存储基本类型,而集合不行;所有的集合底层存储的都是数组;

第六章  枚举和注解

建议83:推荐使用枚举定义常量;

(在项目开发中,推荐使用枚举常量替代接口常量和类常量)(常量分为:类常量、接口常量、枚举常量;枚举常量优点:1、枚举常量更简单;2、枚举常量属于稳态性(不允许发生越界);3、枚举具有内置方法,values方法可以获取到所有枚举值;4、枚举可以自定义方法)。

建议84:使用构造函数协助描述枚举项;

(每个枚举项都是该枚举的一个实例。可以通过添加属性,然后通过构造函数给枚举项添加更多描述信息)。

建议85:小心switch带来的空值异常;

(使用枚举值作为switch(枚举类);语句的条件值时,需要对枚举类进行判断是否为null值。因为Java中的switch语句只能判断byte、short、char、int类型,JDK7可以判断String类型,使用switch语句判断枚举类型时,会根据枚举的排序值匹配。如果传入的只是null的话,获取排序值需要调用如season.ordinal()方法时会抛出NullPointerException异常)。

(switch语句在使用枚举类作为判断条件时,避免出现增加了一个枚举项,而switch语句没做任何修改,编译不会出现问题,但是在运行期会发生非预期的错误。为避免这种情况出现,建议在default后直接抛出一个AssertionError错误。含义是:不要跑到这里来,一跑到这里来马上就会报错)。

建议87:使用valueOf前必须进行校验;

(Enum.valueOf()方法会把一个String类型的名称转变为枚举项,也就是在枚举项中查找出字面值与该参数相等的枚举项。valueOf方法先通过反射从枚举类的常量声明中查找,若找到就直接返回,若找不到就抛出IllegalArgumentException异常)。

建议88:用枚举实现工厂方法模式更简洁;

(工厂方法模式是“创建对象的接口,让子类决定实例化哪一个类,并使一个类的实例化延迟到其子类”。枚举实现工厂方法模式有两种方法:1、枚举非静态方法实现工厂方法模式;2、通过抽象方法生成产品;优点:1、避免错误调用的发生;2、性能好,使用便捷;3、减低类间耦合性)。

建议89:枚举项的数量控制在64个以内;

(Java提供了两个枚举集合:EnumSet、EnumMap;EnumSet要求其元素必须是某一枚举的枚举项,EnumMap表示Key值必须是某一枚举的枚举项。由于枚举类型的实例数量固定并且有限,相对来说EnumSet和EnumMap的效率会比其他Set和Map要高。Java处理EnumSet过程:当枚举项小于等于64时,创建一个RegularEnumSet实例对象,大于64时创一个JumboEnumSet实例对象。RegularEnumSet是把每个枚举项编码映射到一个long类型数字得每一位上,而JumboEnumSet则会先按照64个一组进行拆分,然后每个组再映射到一个long类型的数字得每一位上)。

建议90:小心注解继承;

(不常用的元注解(Meta-Annotation):@Inherited,它表示一个注解是否可以自动被继承)。

建议91:枚举和注解结合使用威力更大;

(注解和接口写法类似,都采用了关键字interface,而且都不能有实现代码,常量定义默认都是public static final类型的等,他们的主要不同点:注解要在interface前加上@字符,而且不能继承,不能实现)。

建议92:注意@Override不同版本的区别;

(@Override注解用于方法的覆写上,它在编译期有效,也就是Java编译器在编译时会根据该注解检查方法是否真的是覆写,如果不是就报错,拒绝编译。Java1.5版本中@Override是严格遵守覆写的定义:子类方法与父类方法必须具有相同的方法名、输入参数、输出参数(允许子类缩小)、访问权限(允许子类扩大),父类必须是一个类,不是是接口,否则不能算是覆写。而在Java1.6就开放了很多,实现接口的方法也可以加上@Override注解了。如果是Java1.6版本移植到Java1.5版本中时,需要删除接口实现方法上的@Override注解)。

第七章  泛型和反射

建议93:Java的泛型是类型擦除的;

(加入泛型优点:加强了参数类型的安全性,减少了类型的转换。Java的泛型在编译期有效,在运行期被删除,也就是说所有的泛型参数类型在编译后都会被清除掉。所以:1、泛型的class对象时是相同的;2、泛型数组初始化时不能声明泛型类型;3、instanceof不允许存在泛型参数)。

建议94:不能初始化泛型参数和数组;

(泛型类型在编译期被擦除,在类初始化时将无法获得泛型的具体参数,所以泛型参数和数组无法初始化,但是ArrayList却可以,因为ArrayList初始化是向上转型变成了Object类型;需要泛型数组解决办法:只声明,不再初始化,由构造函数完成初始化操作)。

建议95:强制声明泛型的实际类型;

建议96:不同的场景使用不同的泛型通配符;

(Java泛型支持通配符(Wildcard),可以单独使用一个“?”表示任意类,也可以使用extends关键字表示某一个类(接口)的子类型,还可以使用super关键字表示某一个类(接口)的父类型。1、泛型结构只参与“读”操作则限定上界(extends关键字);2、泛型结构只参与“写”操作则限定下界(使用super关键字);3、如果一个泛型结构既用作“读”操作也用作“写”操作则使用确定的泛型类型即可,如List<E>)。

建议97:警惕泛型是不能协变和逆变的;

(Java的泛型是不支持协变和逆变的,只是能够实现协变和逆变)(协变和逆变是指宽类型和窄类型在某种情况下(如参数、泛型、返回值)替换或交换的特性。简单地说,协变是用一个窄类型替换宽类型,而逆变则是用宽类型覆盖窄类型。子类覆写父类返回值类型比父类型变窄,则是协变;子类覆写父类型的参数类型变宽,则是逆变。数组支持协变,泛型不支持协变)。

(1、List<T>是确定的某一个类型,编码者知道它是一个类型,只是在运行期才确定而已;2、List<T>可以进行读写操作,List<?>是只读类型,因为编译器不知道List中容纳的是什么类型的元素,无法增加、修改,但是能删除,List<Object>也可以读写操作,只是此时已经失去了泛型存在的意义了)。

建议99:严格限定泛型类型采用多重界限;

建议100:数组的真实类型必须是泛型类型的子类型;

(有可能会抛出ClassCastException异常,toArray方法返回后会进行一次类型转换,Object数组转换成了String数组。由于我们无法在运行期获得泛型类型的参数,因此就需要调用者主动传入T参数类型)。

建议101:注意Class类的特殊性;

Java处理的基本机制:先把Java源文件编译成后缀为class的字节码文件,然后再通过ClassLoader机制把这些类文件加载到内存中,最后生成实例执行。Java使用一个元类(MetaClass)来描述加载到内存中的类数据,这就是Class类,它是一个描述类的类对象。Class类是“类中类”,具有特殊性:1、无构造函数,不能实例化,Class对象是在加载类时由Java虚拟机通过调用类加载器中的defineClass方法自动构建的;2、可以描述基本类型,8个基本类型在JVM中并不是一个对象,一般存在于栈内存中,但是Class类仍然可以描述它们,例如可以使用int.class表示int类型的类对象;3、其对象都是单例模式,一个Class的实例对象描述一个类,并且只描述一个类,反过来也成立,一个类只有一个Class实例对象。Class类是Java的反射入口,只有在获得了一个类的描述对象后才能动态地加载、调用,一般获得一个Class对象有三种途径:1、类属性方式,如String.class;2、对象的getClass方法,如new

(getMethod方法获得的是所有public访问级别的方法,包括从父类继承的方法,而getDeclaredMethod获得的是自身类的所有方法,包括公用方法、私有方法等,而且不受限于访问权限。Java之所以这样处理,是因为反射本意只是正常代码逻辑的一种补充,而不是让正常代码逻辑产生翻天覆地的改动,所以public的属性和方法最容易获取,私有属性和方法也可以获取,但要限定本类。如果需要列出所有继承自父类的方法,需要先获得父类,然后调用getDeclaredMethods方法,之后持续递归)。

建议103:反射访问属性或方法是将Accessible设置为true;

(通过反射方式执行方法时,必须在invoke之前检查Accessible属性。而Accessible属性并不是我们语法层级理解的访问权限,而是指是否更容易获得,是否进行安全检查。Accessible属性只是用来判断是否需要进行安全检查的,如果不需要则直接执行,这就可以大幅度地提升系统性能。经过测试,在大量的反射情况下,设置Accessible为true可以提升性能20倍以上)。

建议104:使用forName动态加载类文件;

(forName只是加载类,并不执行任何代码)(动态加载(Dynamic Loading)是指在程序运行时加载需要的类库文件,一般情况下,一个类文件在启动时或首次初始化时会被加载到内存中,而反射则可以在运行时再决定是否要加载一个类,然后在JVM中加载并初始化。动态加载通常是通过Class.forName(String)实现。一个对象的生成必然会经过一下两个步骤:1、加载到内存中生成Class的实例对象;2、通过new关键字生成实例对象;动态加载的意义:加载一个类即表示要初始化该类的static变量,特别是static代码块,在这里我们可以做大量的工作,比如注册自己,初始化环境等,这才是我们重点关注的逻辑)。

建议105:动态加载不适合数组;

(通过反射操作数组使用Array类,不要采用通用的反射处理API)(如果forName要加载一个类,那它首先必须是一个类--8个基本类型排除在外,不是具体的类;其次,它必须具有可追索的类路径,否则会报ClassNotFoundException异常。在Java中,数组是一个非常特殊的类,虽然是一个类,但没有定义类路径。作为forName参数时会抛出ClassNotFoundException异常,原因是:数组虽然是一个类,在声明时可以定义为String[],但编译器编译后会为不同的数组类型生成不同的类,所以要想动态创建和访问数组,基本的反射是无法实现的)。

建议106:动态代理可以使代理模式更加灵活;

(Java的反射框架提供了动态代理(Dynamic Proxy)机制,允许在运行期对目标类生成代理,避免重复开发。静态代理是通过代理主题角色(Proxy)和具体主题角色(Real Subject)共同实现抽象主题角色(Subject)的逻辑的,只是代理主题角色把相关的执行逻辑委托给了具体主题角色而已。动态代理需要实现InvocationHandler接口,必须要实现invoke方法,该方法完成了对真实方法的调用)。

建议107:使用反射增加装饰模式的普适性;

装饰模式(Decorator Pattern)的定义是“动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式相比于生成子类更为灵活”。比较通用的装饰模式,只需要定义被装饰的类及装饰类即可,装饰行为由动态代理实现,实现了对装饰类和被装饰类的完全解耦,提供了系统的扩展性)。

建议108:反射让模板方法模式更强大;

(决定使用模板方法模式时,请尝试使用反射方式实现,它会让你的程序更灵活、更强大)(模板方法模式(Template Method Pattern)的定义是:定义一个操作中的算法骨架,将一些步骤延迟到子类中,使子类不改变一个算法的结构即可重定义该算法的某些特定步骤。简单说,就是父类定义抽象模板作为骨架,其中包括基本方法(是由子类实现的方法,并且在模板方法被调用)和模板方法(实现对基本方法的调度,完成固定的逻辑),它使用了简单的继承和覆写机制。使用反射后,不需要定义任何抽象方法,只需定义一个基本方法鉴别器即可加载复合规则的基本方法)。

建议109:不需要太多关注反射效率;

(反射效率低是个真命题,但因为这一点而不使用它就是个假命题)(反射效率相对于正常的代码执行确实低很多(经测试,相差15倍左右),但是它是一个非常有效的运行期工具类)。

建议110:提倡异常封装;(异常封装有三方面的优点:1、提高系统的友好性;2、提高系统的可维护性;3、解决Java异常机制本身的缺陷);

建议111:采用异常链传递异常;

责任链模式(Chain of Responsibility),目的是将多个对象连城一条链,并沿着这条链传递该请求,直到有对象处理它为止,异常的传递处理也应该采用责任链模式)。

建议112:受检异常尽可能转化为非受检异常;

(受检异常威胁到系统的安全性、稳定性、可靠性、正确性时、不能转为非受检异常)(受检异常(Checked Exception),非受检异常(Unchecked Exception),受检异常时正常逻辑的一种补偿处理手段,特别是对可靠性要求比较高的系统来说,在某些条件下必须抛出受检异常以便由程序进行补偿处理,也就是说受检异常有合理的存在理由。但是受检异常有不足的地方:1、受检异常使接口声明脆弱;2、受检异常是代码的可读性降低,一个方法增加了受检异常,则必须有一个调用者对异常进行处理。受检异常需要try..catch处理;3、受检异常增加了开发工作量。避免以上受检异常缺点办法:将受检异常转化为非受检异常)。

建议113:不要在finally块中处理返回值;

(在finally块中加入了return语句会导致以下两个问题:1、覆盖了try代码块中的return返回值;2、屏蔽异常,即使throw出去了异常,异常线程会登记异常,但是当执行器执行finally代码块时,则会重新为方法赋值,也就是告诉调用者“该方法执行正确”,没有发生异常,于是乎,异常神奇的消失了)。

建议114:不要在构造函数中抛异常;

(Java异常机制有三种:1、Error类及其子类表示的是错误,它是不需要程序员处理的也不能处理的异常,比如VirtualMachineError虚拟机错误,ThreadDeath线程僵死等;2、RuntimeException类及其子类表示的是非受检异常,是系统可能抛出的异常,程序员可以去处理,也可以不处理,最经典的是NullPointerException空指针异常和IndexOutOfBoundsException越界异常;3、Exception类及其子类(不包含非受检异常)表示的是受检异常,这是程序员必须要处理的异常,不处理则程序不能通过编译,比如IOException表示I/O异常,SQLException数据库访问异常。一个对象的创建过程要经过内存分配、静态代码初始化、构造函数执行等过程,构造函数中是否允许抛出异常呢?从Java语法上来说,完全可以,三类异常都可以,但是从系统设计和开发的角度分析,则尽量不要在构造函数中抛出异常)。

(在出现异常时(或主动声明一个Throwable对象时),JVM会通过fillInStackTrace方法记录下栈信息,然后生成一个Throwable对象,这样就能知道类间的调用顺序、方法名称以及当前行号等)。

建议116:异常只为异常服务;

(异常原本是正常逻辑的一个补充,但有时候会被当前主逻辑使用。异常作为主逻辑有问题:1、异常判断降低了系统性能;2、降低了代码的可读性,只有详细了解valueOf方法的人才能读懂这样的代码,因为valueOf抛出的是一个非受检异常;3、隐藏了运行期可能产生的错误,catch到异常,但没有做任何处理)。

建议117:多使用异常,把性能问题放一边;

(new一个IOException会被String慢5倍:因为它要执行fillInStackTrace方法,要记录当前栈的快照,而String类则是直接申请一个内存创建对象。而且,异常类是不能缓存的。但是异常是主逻辑的例外逻辑,会让方法更符合实际的处理逻辑,同时使主逻辑更加清晰,可让正常代码和异常代码分离、能快速查找问题(栈信息快照)等)。

建议118:不推荐覆写start方法;

(继承自Thread类的多线程类不必覆写start方法。原本的start方法中,调用了本地方法start0,它实现了启动线程、申请栈内存、运行run方法、修改线程状态等职责,线程管理和栈内存管理都是由JVM实现的,如果覆盖了start方法,也就是撤销了线程管理和栈内存管理的能力。所以除非必要,不然不要覆写start方法,即使需要覆写start方法,也需要在方法体内加上super.start调用父类中的start方法来启动默认的线程操作)。

建议119:启动线程前stop方法是不可靠的;

(现象:使用stop方法停止一个线程,而stop方法在此处的目的不是停止一个线程,而是设置线程为不可启用状态。但是运行结果出现奇怪现象:部分线程还是启动了,也就是在某些线程(没有规律)中的start方法正常执行了。在不符合判断规则的情况下,不可启用状态的线程还是启用了,这是线程启动(start方法)一个缺陷。Thread类的stop方法会根据线程状态来判断是终结线程还是设置线程为不可运行状态,对于未启动的线程(线程状态为NEW)来说,会设置其标志位为不可启动,而其他的状态则是直接停止。start方法源码中,start0方法在stop0方法之前,也就是说即使stopBeforeStart为true(不可启动),也会先启动一个线程,然后再stop0结束这个线程,而罪魁祸首就在这里!所以不要使用stop方法进行状态的设置)。

建议120:不适用stop方法停止线程;

(线程启动完毕后,需要停止,Java只提供了一个stop方法,但是不建议使用,有以下三个问题:1、stop方法是过时的;2、stop方法会导致代码逻辑不完整,stop方法是一种“恶意”的中断,一旦执行stop方法,即终止当前正在运行的线程,不管线程逻辑是否完整,这是非常危险的,以为stop方法会清除栈内信息,结束该线程,但是可能该线程的一段逻辑非常重,比如子线程的主逻辑、资源回收、情景初始化等,因为stop线程了,这些都不会再执行。子线程执行到何处会被关闭很难定位,这为以后的维护带来了很多麻烦;3、stop方法会破坏原子逻辑,多线程为了解决共享资源抢占的问题,使用了锁概念,避免资源不同步,但是stop方法会丢弃所有的锁,导致原子逻辑受损。Thread提供的interrupt中断线程方法,它不能终止一个正在执行着的线程,它只是修改中断标志唯一。总之,期望终止一个正在运行的线程,不能使用stop方法,需要自行编码实现。如果使用线程池(比如ThreadPoolExecutor类),那么可以通过shutdown方法逐步关闭池中的线程)。

建议121:线程优先级只使用三个等级;

线程优先级推荐使用MIN_PRIORITY、NORM_PRIORITY、MAX_PRIORITY三个级别,不建议使用其他7个数字)(线程的优先级(Priority)决定了线程获得CPU运行的机会,优先级越高,运行机会越大。事实:1、并不是严格尊重线程优先级别来执行的,分为10个级别;2、优先级差别越大,运行机会差别越大;对于Java来说,JVM调用的接口设置优先级,比如Windows是通过调用SetThreadPriority函数来设置的。不同操作系统线程优先级设置是不相同的,Windows有7个优先级,有140个优先级,Freebsd有255个优先级。Java缔造者也发现了该问题,于是在Thread类中设置了三个优先级,建议使用优先级常量,而不是1到10随机的数字)。

建议122:使用线程异常处理器提升系统可靠性;

(可以使用线程异常处理器来处理相关异常情况的发生,比如当机自动重启,大大提高系统的可靠性。在实际环境中应用注意以下三点:1、共享资源锁定;2、脏数据引起系统逻辑混乱;3、内存溢出,线程异常了,但由该线程创建的对象并不会马上回收,如果再重新启动新线程,再创建一批新对象,特别是加入了场景接管,就危险了,有可能发生OutOfMemory内存泄露问题)。

建议123:volatile不能保证数据同步;

(volatile不能保证数据是同步的,只能保证线程能够获得最新值)(volatile关键字比较少用的原因:1、Java1.5之前该关键字在不同的操作系统上有不同的表现,移植性差;2、比较难设计,而且误用较多。在变量钱加上一个volatile关键字,可以确保每个线程对本地变量的访问和修改都是直接与主内存交互的,而不是与本地线程的工作内存交互的,保证每个线程都能获得最“新鲜”的变量值。但是volatile关键字并不能保证线程安全,它只能保证当前线程需要该变量的值时能够获得最新的值,而不能保证多个线程修改的安全性)。

建议124:异步运算考虑使用Callable接口;

(多线程应用的两种实现方式:一种是实现Runnable接口,另一种是继承Thread类,这两个方式都有缺点:run方法没有返回值,不能抛出异常(归根到底是Runnable接口的缺陷,Thread也是实现了Runnable接口),如果需要知道一个线程的运行结果就需要用户自行设计,线程类本身也不能提供返回值和异常。Java1.5开始引入了新的接口Callable,类似于Runnable接口,实现它就可以实现多线程任务,实现Callable接口的类,只是表明它是一个可调用的任务,并不表示它具有多线程运算能力,还是需要执行器来执行的)。

建议125:优先选择线程池;

(Java1.5以前,实现多线程比较麻烦,需要自己启动线程,并关注同步资源,防止出现线程死锁等问题,Java1.5以后引入了并行计算框架,大大简化了多线程开发。线程有五个状态:新建状态(New)、可运行状态(Runnable,也叫作运行状态)、阻塞状态(Blocked)、等待状态(Waiting)、结束状态(Terminated),线程的状态只能由新建转变为运行态后才可能被阻塞或等待,最后终结,不可能产生本末倒置的情况,比如想把结束状态变为新建状态,则会出现异常。线程运行时间分为三个部分:T1为线程启动时间;T2为线程体运行时间;T3为线程销毁时间。每次创建线程都会经过这三个时间会大大增加系统的响应时间。T2是无法避免的,只能通过优化代码来降低运行时间。T1和T3都可以通过线程池(Thread Pool)来缩短时间。线程池的实现涉及一下三个名词:1、工作线程(Worker),线程池中的线程只有两个状态:可运行状态和等待状态;2、任务接口(Task),每个任务必须实现的接口,以供工作线程调度器调度,它主要规定了任务的入口、任务执行完的场景处理、任务的执行状态等。这里的两种类型的任务:具有返回值(或异常)的Callable接口任务和无返回值并兼容旧版本的Runnable接口任务;3、任务队列(Work Queue),也叫作工作队列,用于存放等待处理的任务,一般是BlockingQueue的实现类,用来实现任务的排队处理。线程池的创建过程:创建一个阻塞队列以容纳任务,在第一次执行任务时闯将足够多的线程(不超过许可线程数),并处理任务,之后每个工作线程自行从任务队列中获得任务,直到任务队列中任务数量为0为止,此时,线程将处于等待状态,一旦有任务加入到队列中,即唤醒工作线程进行处理,实现线程的可复用性)。

建议126:适时选择不同的线程池来实现;

(Java的线程池实现从根本上来说只有两个:ThreadPoolExecutor类和ScheduledThreadPoolExecutor类,还是父子关系。为了简化并行计算,Java还提供了一个Executors的静态类,它可以直接生成多种不同的线程池执行器,比如单线程执行器、带缓冲功能的执行器等,归根结底还是以上两个类的封装类)。

Lock类(显式锁)synchronized关键字(内部锁)用在代码块的并发性和内存上时的语义是一样的,都是保持代码块同时只有一个线程具有执行权。显式锁的锁定和释放必须在一个try...finally块中,这是为了确保即使出现运行期异常也能正常释放锁,保证其他线程能够顺利执行。Lock锁为什么不出现互斥情况,所有线程都是同时执行的?原因:这是因为对于同步资源来说,显式锁是对象级别的锁,而内部锁是类级别的锁,也就是说Lock锁是跟随对象的synchronized锁是跟随类的,更简单地说把Lock定义为多线程类的私有属性是起不到资源互斥作用的,除非是把Lock定义为所有线程共享变量。除了以上不同点之外,还有以下4点不同:1、Lock支持更细粒度的锁控制,假设读写锁分离,写操作时不允许有读写操作存在,而读操作时读写可以并发执行,这一点内部锁很难实现;2、Lock是无阻塞锁,synchronized是阻塞锁,线程A持有锁,线程B也期望获得锁时,如果为Lock,则B线程为等待状态,如果为synchronized,则为阻塞状态;3、Lock可实现公平锁,synchronized只能是非公平锁什么叫做非公平锁?当一个线程A持有锁,而线程B、C处于阻塞(或等待)状态时,若线程A释放锁。JVM将从线程B、C中随机选择一个线程持有锁并使其获得执行权,这叫做非公平锁(因为它抛弃了先来后到的顺序);若JVM选择了等待时间最长的一个线程持有锁,则为公平锁。需要注意的是,即使是公平锁,JVM也无法准确做到“公平”,在程序中不能以此作为精确计算。显式锁默认是非公平锁,但可以在构造函数中加入参数true来声明出公平锁;4、Lock是代码级的,synchronized是JVM级的,Lock是通过编码实现的,synchronized是在运行期由JVM解释的,相对来说synchronized的优化可能性更高,毕竟是在最核心不为支持的,Lock的优化需要用户自行考虑。相对来说,显式锁使用起来更加便利和强大,在实际开发中选择哪种类型的锁就需要根据实际情况考虑了:灵活、强大则选择Lock,快捷、安全则选择synchronized)。

建议128:预防线程死锁;

线程死锁(DeadLock)是多线程编码中最头疼问题,也是最难重现的问题,因为Java是单进程多线程语言。要达到线程死锁需要四个条件:1、互斥条件;2、资源独占条件;3、不剥夺条件;4、循环等待条件;按照以下两种方式来解决:1、避免或减少资源贡献;2、使用自旋锁,如果在获取自旋锁时锁已经有保持者,那么获取锁操作将“自旋”在那里,直到该自旋锁的保持者释放了锁为止)。

建议129:适当设置阻塞队列长度;

full队列已满异常;这是阻塞队列非阻塞队列一个重要区别:阻塞队列的容量是固定的,非阻塞队列则是变长的。阻塞队列可以在声明时指定队列的容量,若指定的容量,则元素的数量不可超过该容量,若不指定,队列的容量为Integer的最大值。有此区别的原因是:阻塞队列是为了容纳(或排序)多线程任务而存在的,其服务的对象是多线程应用,而非阻塞队列容纳的则是普通的数据元素。阻塞队列的这种机制对异步计算是非常有帮助的,如果阻塞队列已满,再加入任务则会拒绝加入,而且返回异常,由系统自行处理,避免了异步计算的不可知性。可以使用put方法,它会等队列空出元素,再让自己加入进去,无论等待多长时间都要把该元素插入到队列中,但是此种等待是一个循环,会不停地消耗系统资源,当等待加入的元素数量较多时势必会对系统性能产生影响。offer方法可以优化一下put方法)。

CountDownLatch协调子线程步骤:一个开始计数器,多个结束计数器:1、每一个子线程开始运行,执行代码到begin.await后线程阻塞,等待begin的计数变为0;2、主线程调用begin的countDown方法,使begin的计数器为0;3、每个线程继续运行;4、主线程继续运行下一条语句,end的计数器不为0,主线程等待;5、每个线程运行结束时把end的计数器减1,标志着本线程运行完毕;6、多个线程全部结束,end计数器为0;7、主线程继续执行,打印出结果。类似:领导安排了一个大任务给我,我一个人不可能完成,于是我把该任务分解给10个人做,在10个人全部完成后,我把这10个结果组合起来返回给领导--这就是CountDownLatch的作用)。

(CyclicBarrier关卡可以让所有线程全部处于等待状态(阻塞),然后在满足条件的情况下继续执行,这就好比是一条起跑线,不管是如何到达起跑线的,只要到达这条起跑线就必须等待其他人员,待人员到齐后再各奔东西,CyclicBarrier关注的是汇合点的信息,而不在乎之前或者之后做何处理。CyclicBarrier可以用在系统的性能测试中,测试并发性)。

建议132:提升Java性能的基本方法;

(如何让Java程序跑的更快、效率更高、吞吐量更大:1、不要在循环条件中计算,每循环一次就会计算一次,会降低系统效率;2、尽可能把变量、方法声明为final static类型,加上final static修饰后,在类加载后就会生成,每次方法调用则不再重新生成对象了;3、缩小变量的作用范围,目的是加快GC的回收;4、频繁字符串操作使用StringBuilder或StringBuffer5、使用非线性检索,使用binarySearch查找会比indexOf查找元素快很多,但是使用binarySearch查找时记得先排序;6、覆写Exception的fillInStackTrace方法,fillInStackTrace方法是用来记录异常时的栈信息的,这是非常耗时的动作,如果不需要关注栈信息,则可以覆盖,以提升性能;7、不建立冗余对象)。

建议133:若非必要,不要克隆对象;

(克隆对象并不比直接生成对象效率高)(通过clone方法生成一个对象时,就会不再执行构造函数了,只是在内存中进行数据块的拷贝,看上去似乎应该比new方法的性能好很多,但事实上,一般情况下new生成的对象比clone生成的性能方面要好很多。JVM对new做了大量的系能优化,而clone方式只是一个冷僻的生成对象的方式,并不是主流,它主要用于构造函数比较复杂,对象属性比较多,通过new关键字创建一个对象比较耗时间的时候)。

建议134:推荐使用“望闻问切”的方式诊断性能;

性能诊断遵循“望闻问切”,不可过度急躁)。

建议135:必须定义性能衡量标准;

(原因:1、性能衡量标准是技术与业务之间的契约;2、性能衡量标志是技术优化的目标)。

建议136:枪打出头鸟--解决首要系统性能问题;

(解决性能优化要“单线程”小步前进,避免关注点过多而导致精力分散)(解决性能问题时,不要把所有的问题都摆在眼前,这只会“扰乱”你的思维,集中精力,找到那个“出头鸟”,解决它,在大部分情况下,一批性能问题都会迎刃而解)。

建议137:调整JVM参数以提升性能;

四个常用的JVM优化手段:

1、调整堆内存大小,JVM两种内存:栈内存(Stack)堆内存(Heap)栈内存的特点是空间小,速度快,用来存放对象的引用及程序中的基本类型;而堆内存的特点是空间比较大,速度慢一般对象都会在这里生成、使用和消亡。栈空间由线程开辟,线程结束,栈空间由JVM回收,它的大小一般不会对性能有太大影响,但是它会影响系统的稳定性,超过栈内存的容量时,会抛StackOverflowError错误。可以通过“java -Xss <size>”设置栈内存大小来解决。堆内存的调整不能太随意,调整得太小,会导致Full GC频繁执行,轻则导致系统性能急速下降,重则导致系统根本无法使用;调整得太大,一则浪费资源(若设置了最小堆内存则可以避免此问题),二则是产生系统不稳定的情况,设置方法“java -Xmx1536 -Xms1024m”,可以通过将-Xmx和-Xms参数值设置为相同的来固定堆内存大小;

GC会再回收一次,然后再把剩余的对象移到养老区,如果养老区也满了,则会触发Full GC(非常危险的动作,JVM会停止所有的执行,所有系统资源都会让位给垃圾回收器),会对所有的对象过滤一遍,检查是否有可以回收的对象,如果还是没有的话,就抛出OutOfMemoryError错误。一般情况下新生区与养老区的比例为1:3左右,设置命令:“java

3、变更GC的垃圾回收策略,设置命令“java -XX:+UseParallelGC -XX:ParallelGCThreads=20”,这里启用了并行垃圾收集机制,并且定义了20个收集线程(默认的收集线程等于CPU的数量),这对多CPU的系统时非常有帮助的,可以大大减少垃圾回收对系统的影响,提高系统性能;

4、更换JVM,如果所有的JVM优化都不见效,那就只有更换JVM了,比较流行的三个JVM产品:HotSpot

建议138:性能是个大“咕咚”;

(1、没有慢的系统,只有不满足义务的系统;2、没有慢的系统,只有不良的系统;3、没有慢的系统,只有懒惰的技术人员;4、没有慢的系统,只有不愿意投入的系统)。

建议139:大胆采用开源工具;

(选择开源工具和框架时要遵循一定的规则:1、普适性原则;2、唯一性原则;3、“大树纳凉”原则;4、精而专原则;5、高热度原则)。

建议140:推荐使用Guava扩展工具包

(Apache Commons通用扩展包基本上是每个项目都会使用的,一般情况下lang包用作JDK的基础语言扩展。Apache Commons项目包含非常好用的工具,如DBCP、net、Math等)。

建议142:推荐使用Joda日期时间扩展包;

(Joda可以很好地与现有的日期类保持兼容,在需要复杂的日期计算时使用Joda。日期工具类也可以选择date4j)。

(三个比较有个性的Collections扩展工具包:1、FastUtil,主要提供两种功能:一种是限定键值类型的Map、List、Set等,另一种是大容量的集合;2、Trove,提供了一个快速、高效、低内存消耗的Collection集合,并且还提供了过滤和拦截功能,同时还提供了基本类型的集合;3、lambdaj,是一个纯净的集合操作工具,它不会提供任何的集合扩展,只会提供对集合的操作,比如查询、过滤、统一初始化等)。

建议144:提倡良好的代码风格;

(良好的编码风格包括:1、整洁;2、统一;3、流行;4、便捷,推荐使用Checkstyle检测代码是否遵循规范)。

建议145:不要完全依靠单元测试来发现问题;

(单元测试的目的是保证各个独立分隔的程序单元的正确性,虽然它能够发现程序中存在的问题(或缺陷、或错误),但是单元测试只是排查程序错误的一种方式,不能保证代码中的所有错误都能被单元测试挖掘出来,原因:1、单元测试不可能测试所有的场景(路径);2、代码整合错误是不可避免的;3、部分代码无法(或很难)测试;4、单元测试验证的是编码人员的假设)。

建议146:让注释正确、清晰、简洁;

(注释不是美化剂,而是催化剂,或为优秀加分,或为拙略减分)。

建议147:让接口的职责保持单一;

(接口职责一定要单一,实现类职责尽量单一)(单一职责原则(Single Responsibility Principle,简称SRP)有以下三个优点:1、类的复杂性降低;2、可读性和可维护性提高;3、降低变更风险)。

建议148:增强类的可替换性

(Java的三大特征:封装、继承、多态;说说多态,一个接口可以有多种实现方式,一个父类可以有多个子类,并且可以把不同的实现或子类赋给不同的接口或父类。多态的好处非常多,其中一点就是增强了类的可替换性,但是单单一个多态特性,很难保证我们的类是完全可以替换的,幸好还有一个里氏替换原则来约束。里氏替换原则:所有引用基类的地方必须能透明地使用其子类的对象。通俗点讲,只要父类型能出现的地方子类型就可以出现,而且将父类型替换为子类型还不会产生任何错误或异常,使用者可能根本就不需要知道是父类型还是子类型。为了增强类的可替换性,在设计类时需要考虑以下三点:1、子类型必须完全实现父类型的方法;2、前置条件可以被放大;3、后置条件可以被缩小)。

建议149:依赖抽象而不是实现;

(此处的抽象是指物体的抽象,比如出行,依赖的是抽象的运输能力,而不是具体的运输交通工具。依赖倒置原则(Dependence Inversion Principle,简称DIP)要求实现解耦,保持代码间的松耦合,提高代码的复用率。DIP的原始定义包含三层含义:1、高层模块不应该依赖底层模块,两者都应该依赖其抽象;2、抽象不应该依赖细节;3、细节应该依赖抽象;DIP在Java语言中的表现就是:1、模块间的依赖是通过抽象发生的,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的;2、接口或抽象类不依赖于实现类;3、实现类依赖接口或抽象类;更加精简的定义就是:面向接口编程。实现模块间的松耦合遵循规则:1、尽量抽象;2、表面类型必须是抽象的;3、任何类都不应该从具体类派生;4、尽量不要覆写基类的方法;5、抽象不关注细节)。

建议150:抛弃7条不良的编码习惯;

1、自由格式的代码;2、不使用抽象的代码;3、彰显个性的代码;4、死代码;5、冗余代码;6、拒绝变化的代码;7、自以为是的代码)。

建议151:以技术人员自律而不是工人;

(20条建议:1、熟悉工具;2、使用IDE;3、坚持编码;4、编码前思考;5、坚持重构;6、多写文档;7、保持程序版本的简单性;8、做好备份;9、做单元测试;10、不要重复发明轮子;11、不要拷贝;12、让代码充满灵性;13、测试自动化;14、做压力测试;15、“剽窃”不可耻;16、坚持向学习;17、重里更重面;18、分享;19、刨根问底;20、横向扩展)

在挖金矿小游戏中,玩家通过指令控制小人运动,小人按照一定的规则将地底的金矿全部收集完毕。现将地底的截面可用一个n*n的矩阵表示,其中第一行表示地面,不会出现金矿;第2行至第n行表示地底。每一行中黑色格子表示金矿,白色格子表示空地,其中小人在矩阵左上角第一行第一列的位置。

玩家通过4种指令控制小人运动:

· 左 X:小人向左移动X个格子

· 右 X:小人向右移动X个格子

· 下 X:小人向下移动X个格子

· 挖矿:收集小人所在格子的金矿

小人收集金矿需要遵循一定的规则:

①小人必须将当前行的金矿全部收集,才能去往下一行,即金矿需从上到下收集

②对于同一行的金矿,小人必须按照从左往右的顺序开始收集

现编写程序,以8*8的矩阵为例,在文本框Text1中输入金矿数量,点击按钮“开始”后,随机产生对应数量的金矿并借助图形控件输出(保证不会在同一个格子出现两个金矿),并且在列表框List1中按顺序输出指令,使小人按照规则得到所有金矿。如图a所示。请回答下列问题。

  1. (1) 现有4*4的地底截面如图b所示,小人在左上角位置,请写出任意一种可使小人按规则获得所有金矿的指令(指令之间用逗号或空格隔开)。

  2. (2) 请完成程序中的填空。

    '随机产生m个金矿并通过图形控件显示在界面上,代码略

        '将所有金矿按照从上到下、从左到右的顺序排序,以符合题目描述的规则

本文源自于个人github仓库:
立志收录计算机校招、社招面试最全面试八股文,无内鬼来点八股文~

如果各位发现有些问题的图片上传失败,这是由于不支持 github 图床自动上传,所以有时候图片上传成功、有时候上传失败...
这些问题的解答在github仓库上是好使的,不过还好有图片的八股文问题不多。对于这些图片上传失败的八股文问题,大家可以移步github看问题解答的答案。

估计全系列大概30W字+,持续更新ing,以下是该系列全部文章,不过会慢慢搬运到上来的。

先说两句闲话,前段时间加了一个粉丝,他告诉我是他们老师在班级群里推荐的我,我当时就懵了。。。

当时真的有点慌,原来真的有人会看我写的东西,还愿意推荐给身边的人看...

最重要的是,这还是一位计算机专业的 C++ 从业老师,在这里也感谢这位老师的推荐,以后一定不会辜负您的推荐。

emm,这两天在另一个文件夹里又发现了一些整理的 C++笔记,所以 C++ 部分可能要分成四期文章了,第三期是 C++ 提高篇,而最后一篇是 C++ 重头戏,也是面试必问的 STL 部分。

这是本期 C++ 八股文问题目录。

不逼逼了,《逆袭进大厂》系列第二弹 C++ 提高篇直接发车了。

这篇总字数是 26757 个字,建议收藏。

1.先来介绍它的第一条也是最重要的一条:隐藏。(static函数,static变量均可)

当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。

2.static的第二个作用是保持变量内容的持久。(static变量中的记忆功能和全局生存期)存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,说到底static还是用来隐藏的。

3.static的第三个作用是默认初始化为0(static变量)

其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是0x00,某些时候这一特点可以减少程序员的工作量。

1) 函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;

2) 在模块内的static全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问;

3) 在模块内的static函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内;

4) 在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;

5) 在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。

6) static类对象必须要在类外进行初始化,static修饰的变量先于对象存在,所以static修饰的变量要在类外初始化;

7) 由于static修饰的类成员属于类,不属于对象,因此static类成员函数是没有this指针的,this指针是指向本对象的指针。正因为没有this指针,所以static类成员函数不能访问非static的类成员,只能访问 static修饰的类成员;

51、静态变量什么时候初始化

1) 初始化只有一次,但是可以多次赋值,在主程序之前,编译器已经为其分配好了内存。

2) 静态局部变量和全局变量一样,数据都存放在全局区域,所以在主程序之前,编译器已经为其分配好了内存,但在C和C++中静态局部变量的初始化节点又有点不太一样。在C中,初始化发生在代码执行之前,编译阶段分配好内存之后,就会进行初始化,所以我们看到在C语言中无法使用变量对静态局部变量进行初始化,在程序运行结束,变量所处的全局内存会被全部回收。

而在C++中,初始化时在执行相关代码时才会进行初始化,主要是由于C++引入对象后,要进行初始化必须执行相应构造函数和析构函数,在构造函数或析构函数中经常会需要进行某些程序中需要进行的特定操作,并非简单地分配内存。所以C++标准定为全局或静态对象是有首次用到时才会进行构造,并通过atexit()来管理。在程序结束,按照构造顺序反方向进行逐个析构。所以在C++中是可以使用变量对静态局部变量进行初始化的。

1) 阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;

2) 对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;

3) 在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;

4) 对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量,类的常对象只能访问类的常成员函数;

5) 对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。

6) const成员函数可以访问非const对象的非const数据成员、const数据成员,也可以访问const对象内的所有数据成员;

7) 非const成员函数可以访问非const对象的非const数据成员、const数据成员,但不可以访问const对象的任意数据成员;

8) 一个没有明确声明为const的成员函数被看作是将要修改对象中数据成员的函数,而且编译器不允许它为一个const对象所调用。因此const对象只能调用const成员函数。

10) const类型变量必须定义的时候进行初始化,因此也导致如果类的成员变量有const类型的变量,那么该变量必须在类的初始化列表中进行初始化;

11) 对于函数值传递的情况,因为参数传递是通过复制实参创建一个临时变量传递进函数的,函数内只能改变临时变量,但无法改变实参。则这个时候无论加不加const对实参不会产生任何影响。但是在引用或指针传递函数调用中,因为传进去的是一个引用或指针,这样函数内部可以改变引用或指针所指向的变量,这时const 才是实实在在地保护了实参所指向的变量。因为在编译阶段编译器对调用函数的选择是根据实参进行的,所以,只有引用传递和指针传递可以用是否加const来重载。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。

1) 当const修饰指针时,由于const的位置不同,它的修饰对象会有所不同。

2) int *const p2中const修饰p2的值,所以理解为p2的值不可以改变,即p2只能指向固定的一个变量地址,但可以通过*p2读写这个变量的值。顶层指针表示指针本身是一个常量

3) int const *p1或者const int *p1两种情况中const修饰*p1,所以理解为*p1的值不可以改变,即不可以给*p1赋值改变p1指向变量的值,但可以通过给p赋值不同的地址改变这个指针指向。

底层指针表示指针所指向的变量是一个常量。

54、形参与实参的区别?

1) 形参变量只有在被调用时才分配内存单元,在调用结束时, 即刻释放所分配的内存单元。因此,形参只有在函数内部有效。 函数调用结束返回主调函数后则不能再使用该形参变量。

2) 实参可以是常量、变量、表达式、函数等, 无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值, 以便把这些值传送给形参。 因此应预先用赋值,输入等办法使实参获得确定值,会产生一个临时变量。

3) 实参和形参在数量上,类型上,顺序上应严格一致, 否则会发生“类型不匹配”的错误。

4) 函数调用中发生的数据传送是单向的。 即只能把实参的值传送给形参,而不能把形参的值反向地传送给实参。 因此在函数调用过程中,形参的值发生改变,而实参中的值不会变化。

5) 当形参和实参不是指针类型时,在该函数运行时,形参和实参是不同的变量,他们在内存中位于不同的位置,形参将实参的内容复制一份,在该函数运行结束的时候形参被释放,而实参内容不会改变。

55、值传递、指针传递、引用传递的区别和效率

1) 值传递:有一个形参向函数所属的栈拷贝数据的过程,如果值传递的对象是类对象 或是大的结构体对象,将耗费一定的时间和空间。(传值)

2) 指针传递:同样有一个形参向函数所属的栈拷贝数据的过程,但拷贝的数据是一个固定为4字节的地址。(传值,传递的是地址值)

3) 引用传递:同样有上述的数据拷贝过程,但其是针对地址的,相当于为该数据所在的地址起了一个别名。(传地址)

4) 效率上讲,指针传递和引用传递比值传递效率高。一般主张使用引用传递,代码逻辑上更加紧凑、清晰。

56、什么是类的继承?

1) 类与类之间的关系

has-A包含关系,用以描述一个类由多个部件类构成,实现has-A关系用类的成员属性表示,即一个类的成员属性是另一个已经定义好的类;

use-A,一个类使用另一个类,通过类之间的成员函数相互联系,定义友元或者通过传递参数的方式来实现;

is-A,继承关系,关系具有传递性;

所谓的继承就是一个类继承了另一个类的属性和方法,这个新的类包含了上一个类的属性和方法,被称为子类或者派生类,被继承的类称为父类或者基类;

子类拥有父类的所有属性和方法,子类可以拥有父类没有的属性和方法,子类对象可以当做父类对象使用;

4) 继承中的访问控制

5) 继承中的构造和析构函数

6) 继承中的兼容性原则

57、什么是内存池,如何实现

内存池(Memory Pool) 是一种内存分配方式。通常我们习惯直接使用new、malloc 等申请内存,这样做的缺点在于:由于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。内存池则是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块, 若内存块不够再继续申请新的内存。这样做的一个显著优点是尽量避免了内存碎片,使得内存分配效率得到提升。

这里简单描述一下《STL源码剖析》中的内存池实现机制:

一般是一次20*2个的申请,先用一半,留着一半,为什么也没个说法,侯捷在STL那边书里说好像是C++委员会成员认为20是个比较好的数字,既不大也不小

  1. 首先客户端会调用malloc()配置一定数量的区块(固定大小的内存块,通常为8的倍数),假设40个32bytes的区块,其中20个区块(一半)给程序实际使用,1个区块交出,另外19个处于维护状态。剩余20个(一半)留给内存池,此时一共有(20*32byte)
  2. 客户端之后有有内存需求,想申请(20*64bytes)的空间,这时内存池只有(2032bytes),就先将(10\64bytes)个区块返回,1个区块交出,另外9个处于维护状态,此时内存池空空如也
  3. 接下来如果客户端还有内存需求,就必须再调用malloc()配置空间,此时新申请的区块数量会增加一个随着配置次数越来越大的附加量,同样一半提供程序使用,另一半留给内存池。申请内存的时候用永远是先看内存池有无剩余,有的话就用上,然后挂在0-15号某一条上,要不然就重新申请。
  4. 如果整个堆的空间都不够了,就会在原先已经分配区块中寻找能满足当前需求的区块数量,能满足就返回,不能满足就向客户端报bad_alloc异常

《STL源码解析》侯捷 P68

allocator就是用来分配内存的,最重要的两个函数是allocate和deallocate,就是用来申请内存和回收内存的,外部(一般指容器)调用的时候只需要知道这些就够了。内部实现,目前的所有编译器都是直接调用的::operator new()和::operator delete(),说白了就是和直接使用new运算符的效果是一样的,所以老师说它们都没做任何特殊处理。

最开始GC2.9之前:

GC2.9的alloc的一个比较好的分配器的实现规则

128bytes,如果在申请时并不是8的倍数,那就找刚好能满足内存大小的那个位置。比如想申请 12,那就是找16了,想申请 20 ,那就找 24 了

58、从汇编层去解释一下引用

x的地址为ebp-4,b的地址为ebp-8,因为栈内的变量内存是从高往低进行分配的,所以b的地址比x的低。

ebp-8中上面两条汇编的作用即:将x的地址存入变量b中,这不和将某个变量的地址存入指针变量是一样的吗?所以从汇编层次来看,的确引用是通过指针来实现的。

59、深拷贝与浅拷贝是怎么回事?

1) 浅复制 :只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用,我们把这种拷贝叫做“(浅复制)浅拷贝”,换句话说,浅复制仅仅是指向被复制的内存地址,如果原地址中对象被改变了,那么浅复制出来的对象也会相应改变。

深复制 :在计算机中开辟了一块新的内存地址用于存放复制的对象。

2) 在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。

60、C++模板是什么,你知道底层怎么实现的?

1) 编译器并不是把函数模板处理成能够处理任意类的函数;编译器从函数模板通过具体类型产生不同的函数;编译器会对函数模板进行两次编译:在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。

2) 这是因为函数模板要被实例化后才能成为真正的函数,在使用函数模板的源文件中包含函数模板的头文件,如果该头文件中只有声明,没有定义,那编译器无法实例化该模板,最终导致链接错误。

1、 new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持;

2、 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。

3、 new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。

4、 new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。

5、 new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。

1、 动态数组管理new一个数组时,[]中必须是一个整数,但是不一定是常量整数,普通数组必须是一个常量整数;

2、 new动态数组返回的并不是数组类型,而是一个元素类型的指针;

3、 delete[]时,数组中的元素按逆序的顺序进行销毁;

4、 new在内存分配上面有一些局限性,new的机制是将内存分配和对象构造组合在一起,同样的,delete也是将对象析构和内存释放组合在一起的。allocator将这两部分分开进行,allocator申请一部分内存,不进行初始化对象,只有当需要的时候才进行初始化操作。

63、new和delete的实现原理, delete是如何知道释放内存的大小的额?

而对于复杂结构,先调用operator new分配内存,然后在分配的内存上调用构造函数;

对于复杂数据结构,new[]先调用operator new[]分配内存,然后在p的前四个字节写入数组大小n,然后调用n次构造函数,针对复杂类型,new[]会额外存储数组大小;

① new表达式调用一个名为operator new(operator new[])函数,分配一块足够大的、原始的、未命名的内存空间;

② 编译器运行相应的构造函数以构造这些对象,并为其传入初始值;

③ 对象被分配了空间并构造完成,返回一个指向该对象的指针。

2、 delete简单数据类型默认只是调用free函数;复杂数据类型先调用析构函数再调用operator delete;针对简单类型,delete和delete[]等同。假设指针p指向new[]分配的内存。因为要4字节存储数组大小,实际分配的内存地址为[p-4],系统记录的也是这个地址。delete[]实际释放的就是p-4指向的内存。而delete会直接释放p指向的内存,这个内存根本没有被系统记录,所以会崩溃。

3、 需要在 new [] 一个对象数组时,需要保存数组的维度,C++ 的做法是在分配数组空间时多分配了 4 个字节的大小,专门保存数组的大小,在 delete [] 时就可以取出这个保存的数,就知道了需要调用析构函数多少次了。

malloc /free的操作对象都是必须明确大小的,而且不能用在动态类上。

new 和delete会自动进行类型检查和大小,malloc/free不能执行与析构函数,所以动态对象它是不行的。

当然从理论上说使用malloc申请的内存是可以通过delete释放的。不过一般不这样写的。而且也不能保证每个C++的运行时都能正常。

1、 在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk、mmap、,munmap这些系统调用实现的;

2、 brk是将数据段(.data)的最高地址指针_edata往高地址推,mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系;

malloc小于128k的内存,使用brk分配内存,将_edata往高地址推;malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配;brk分配的内存需要等到高地址内存释放以后才能释放,而mmap分配的内存可以单独释放。当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。在上一个步骤free的时候,发现最高地址空闲内存超过128K,于是内存紧缩。

4、 malloc是从堆里面申请内存,也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的。当操作系统收到程序的申请时,就会遍历该,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点中删除,并将该结点的空间分配给程序。

省去了人为空间计算;malloc申请的空间的值是随机初始化的,calloc申请的空间的值是初始化为0的;

给动态分配的空间分配额外的空间,用于扩充容量。

67、类成员初始化方式?构造函数的执行顺序 ?为什么用成员初始化列表会快一些?

1) 赋值初始化,通过在函数体内进行赋值初始化;列表初始化,在冒号后使用初始化列表进行初始化。

这两种方式的主要区别在于:

对于在函数体中初始化,是在所有的数据成员被分配内存空间后才进行的。

列表初始化是给数据成员分配内存空间时就进行初始化,就是说分配一个数据成员只要冒号后有此数据成员的赋值表达式(此表达式必须是括号赋值表达式),那么分配了内存空间后在进入函数体之前给数据成员赋值,就是说初始化这个数据成员此时函数体还未执行。

2) 一个派生类构造函数的执行顺序如下:

① 虚拟基类的构造函数(多个虚拟基类则按照继承的顺序执行构造函数)。

② 基类的构造函数(多个普通基类也按照继承的顺序执行构造函数)。

③ 类类型的成员对象的构造函数(按照初始化顺序)

④ 派生类自己的构造函数。

3) 方法一是在构造函数当中做赋值的操作,而方法二是做纯粹的初始化操作。我们都知道,C++的赋值操作是会产生临时对象的。临时对象的出现会降低程序的效率。

68、成员列表初始化?

1) 必须使用成员初始化的四种情况

① 当初始化一个引用成员时;

② 当初始化一个常量成员时;

③ 当调用一个基类的构造函数,而它拥有一组参数时;

④ 当调用一个成员类的构造函数,而它拥有一组参数时;

2) 成员初始化列表做了什么

① 编译器会一一操作初始化列表,以适当的顺序在构造函数之内安插初始化操作,并且在任何显示用户代码之前;

② list中的项目顺序是由类中的成员声明顺序决定的,不是由初始化列表的顺序决定的;

69、什么是内存泄露,如何检测与避免

一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定)内存块,使用完后必须显式释放的内存。应用程序般使用malloc,、realloc、 new等函数从堆中分配到块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了

避免内存泄露的几种方式

  • 计数法:使用new或者malloc时,让该数+1,delete或free时,该数-1,程序执行完打印这个计数,如果不为0则表示存在内存泄露
  • 一定要将基类的析构函数声明为虚函数
  • 对象数组的释放一定要用delete []

70、对象复用的了解,零拷贝的了解

对象复用其本质是一种设计模式:Flyweight享元模式。

通过将对象存储到“对象池”中实现对象的重复利用,这样可以避免多次创建重复对象的开销,节约系统资源。

零拷贝就是一种避免 CPU 将数据从一块存储拷贝到另外一块存储的技术。

零拷贝技术可以减少数据拷贝和共享总线操作的次数。

在C++中,vector的一个成员函数emplace_back()很好地体现了零拷贝技术,它跟push_back()函数一样可以将一个元素插入容器尾部,区别在于:使用push_back()函数需要调用拷贝构造函数和转移构造函数,而使用emplace_back()插入的元素原地构造,不需要触发拷贝构造和转移构造,效率更高。举个例子:

“trivial destructor”一般是指用户没有自定义析构函数,而由系统生成的,这种析构函数在《STL源码解析》中成为“无关痛痒”的析构函数。

反之,用户自定义了析构函数,则称之为“non-trivial destructor”,这种析构函数如果申请了新的空间一定要显式的释放,否则会造成内存泄露

对于trivial destructor,如果每次都进行调用,显然对效率是一种伤害,如何进行判断呢?《STL源码解析》中给出的说明是:

construct等,如果能够对是否trivial进行区分,可以采用内存处理函数memcpy()、malloc()等更加高效的完成相关操作,提升效率。

72、介绍面向对象的三大特性,并且举例说明

三大特性:继承、封装和多态

让某种类型对象获得另一个类型对象的属性和方法。

它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展

常见的继承有三种方式:

  1. 实现继承:指使用基类的属性和方法而无需额外编码的能力
  2. 接口继承:指仅使用属性和方法的名称、但是子类必须提供实现的能力
  3. 可视继承:指子窗体(类)使用基窗体(类)的外观和实现代码的能力(C++里好像不怎么用)

例如,将人定义为一个抽象类,拥有姓名、性别、年龄等公共属性,吃饭、睡觉、走路等公共方法,在定义一个具体的人时,就可以继承这个抽象类,既保留了公共属性和方法,也可以在此基础上扩展跳舞、唱歌等特有方法

数据和代码捆绑在一起,避免外界干扰和不确定性访问。

封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏,例如:将公共的数据或方法使用public修饰,而不希望被访问的数据或方法采用private修饰。

同一事物表现出不同事物的能力,即向不同对象发送同一消息,不同的对象在接收时会产生不同的行为(重载实现编译时多态,虚函数实现运行时多态)

多态性是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单一句话:允许将子类类型的指针赋值给父类类型的指针

实现多态有二种方式:覆盖(override),重载(overload)。覆盖:是指子类重新定义父类的虚函数的做法。重载:是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。例如:基类是一个抽象对象——人,那教师、运动员也是人,而使用这个抽象对象既可以表示教师、也可以表示运动员。

《C++封装继承多态总结》:

73、C++中类的数据成员和成员函数内存分布情况

C++类是由结构体发展得来的,所以他们的成员变量(C语言的结构体只有成员变量)的内存分配机制是一样的。下面我们以类来说明问题,如果类的问题通了,结构体也也就没问题啦。 类分为成员变量和成员函数,我们先来讨论成员变量。

一个类对象的地址就是类所包含的这一片内存空间的首地址,这个首地址也就对应具体某一个成员变量的地址。(在定义类对象的同时这些成员变量也就被定义了),举个例子:

从代码运行结果来看,对象的大小和对象中数据成员的大小是一致的,也就是说,成员函数不占用对象的内存。这是因为所有的函数都是存放在代码区的,不管是全局函数,还是成员函数。要是成员函数占用类的对象空间,那么将是多么可怕的事情:定义一次类对象就有成员函数占用一段空间。 我们再来补充一下静态成员函数的存放问题:静态成员函数与一般成员函数的唯一区别就是没有this指针,因此不能访问非静态数据成员,就像我前面提到的,所有函数都存放在代码区,静态函数也不例外。所有有人一看到 static 这个单词就主观的认为是存放在全局数据区,那是不对的。

《C++类对象成员变量和函数内存分配的问题》:

74、成员初始化列表的概念,为什么用它会快一些?

在类的构造函数中,不在函数体内对成员变量赋值,而是在构造函数的花括号前面使用冒号和初始化列表赋值

用初始化列表会快一些的原因是,对于类型,它少了一次调用构造函数的过程,而在函数体中赋值则会多一次调用。而对于内置数据类型则没有差别。举个例子:

//默认构造函数A()

从代码运行结果可以看出,在构造函数体内部初始化的对象b多了一次构造函数的调用过程,而对象a则没有。由于对象成员变量的初始化动作发生在进入构造函数之前,对于内置类型没什么影响,但如果有些成员是类,那么在进入构造函数之前,会先调用一次默认构造函数,进入构造函数后所做的事其实是一次赋值操作(对象已存在),所以如果是在构造函数体内进行赋值的话,等于是一次默认构造加一次赋值,而初始化列表只做一次赋值操作。

《为什么用成员初始化列表会快一些?》:

75、(超重要)构造函数为什么不能为虚函数?析构函数为什么要虚函数?

1、 从存储空间角度,虚函数相应一个指向vtable虚函数表的指针,这大家都知道,但是这个指向vtable的指针事实上是存储在对象的内存空间的。

问题出来了,假设构造函数是虚的,就须要通过 vtable来调用,但是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。

2、 从使用角度,虚函数主要用于在信息不全的情况下,能使重载的函数得到相应的调用。

构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。

所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候可以变成调用子类的那个成员函数。而构造函数是在创建对象时自己主动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。

3、构造函数不须要是虚函数,也不同意是虚函数,由于创建一个对象时我们总是要明白指定对象的类型,虽然我们可能通过实验室的基类的指针或引用去訪问它但析构却不一定,我们往往通过基类的指针来销毁对象。这时候假设析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。

4、从实现上看,vbtl在构造函数调用后才建立,因而构造函数不可能成为虚函数从实际含义上看,在调用构造函数时还不能确定对象的真实类型(由于子类会调父类的构造函数);并且构造函数的作用是提供初始化,在对象生命期仅仅运行一次,不是对象的动态行为,也没有必要成为虚函数。

5、当一个构造函数被调用时,它做的首要的事情之中的一个是初始化它的VPTR。

因此,它仅仅能知道它是“当前”类的,而全然忽视这个对象后面是否还有继承者。当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码——既不是为基类,也不是为它的派生类(由于类不知道谁继承它)。所以它使用的VPTR必须是对于这个类的VTABLE。

并且,仅仅要它是最后的构造函数调用,那么在这个对象的生命期内,VPTR将保持被初始化为指向这个VTABLE, 但假设接着另一个更晚派生的构造函数被调用,这个构造函数又将设置VPTR指向它的 VTABLE,等.直到最后的构造函数结束。

VPTR的状态是由被最后调用的构造函数确定的。这就是为什么构造函数调用是从基类到更加派生类顺序的还有一个理由。可是,当这一系列构造函数调用正发生时,每一个构造函数都已经设置VPTR指向它自己的VTABLE。假设函数调用使用虚机制,它将仅仅产生通过它自己的VTABLE的调用,而不是最后的VTABLE(全部构造函数被调用后才会有最后的VTABLE)。

因为构造函数本来就是为了明确初始化对象成员才产生的,然而virtual function主要是为了再不完全了解细节的情况下也能正确处理对象。另外,virtual函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用virtual函数来完成你想完成的动作。

直接的讲,C++中基类采用virtual虚析构函数是为了防止内存泄漏。

具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。

所以,为了防止这种情况的发生,C++中基类的析构函数应采用virtual虚析构函数。

76、析构函数的作用,如何起作用?

1) 构造函数只是起初始化值的作用,但实例化一个对象的时候,可以通过实例去传递参数,从主函数传递到其他的函数里面,这样就使其他的函数里面有值了。

规则,只要你一实例化对象,系统自动回调用一个构造函数就是你不写,编译器也自动调用一次。

2) 析构函数与构造函数的作用相反,用于撤销对象的一些特殊任务处理,可以是释放对象分配的内存空间;特点:析构函数与构造函数同名,但该函数前面加~。

析构函数没有参数,也没有返回值,而且不能重载,在一个类中只能有一个析构函数。 当撤销对象时,编译器也会自动调用析构函数。

每一个类必须有一个析构函数,用户可以自定义析构函数,也可以是编译器自动生成默认的析构函数。一般析构函数定义为类的公有成员。

77、构造函数和析构函数可以调用虚函数吗,为什么

1) 在C++中,提倡不在构造函数和析构函数中调用虚函数;

2) 构造函数和析构函数调用虚函数时都不使用动态联编,如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本;

3) 因为父类对象会在子类之前进行构造,此时子类部分的数据成员还未初始化,因此调用子类的虚函数时不安全的,故而C++不会进行动态联编;

4) 析构函数是用来销毁一个对象的,在销毁一个对象时,先调用子类的析构函数,然后再调用基类的析构函数。所以在调用基类的析构函数时,派生类对象的数据成员已经销毁,这个时候再调用子类的虚函数没有任何意义。

78、构造函数、析构函数的执行顺序?构造函数和拷贝构造的内部都干了啥?

① 基类构造函数。如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序,而不是它们在成员初始化表中的顺序。

② 成员类对象构造函数。如果有多个成员类对象则构造函数的调用顺序是对象在类中被声明的顺序,而不是它们出现在成员初始化表中的顺序。

① 调用派生类的析构函数;

② 调用成员类对象的析构函数;

③ 调用基类的析构函数。

79、虚析构函数的作用,父类的析构函数是否要设置为虚函数?

1) C++中基类采用virtual虚析构函数是为了防止内存泄漏。

具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。

假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。

那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。

所以,为了防止这种情况的发生,C++中基类的析构函数应采用virtual虚析构函数。

2) 纯虚析构函数一定得定义,因为每一个派生类析构函数会被编译器加以扩张,以静态调用的方式调用其每一个虚基类以及上一层基类的析构函数。

因此,缺乏任何一个基类析构函数的定义,就会导致链接失败,最好不要把虚析构函数定义为纯虚析构函数。

80、构造函数析构函数可否抛出异常

1) C++只会析构已经完成的对象,对象只有在其构造函数执行完毕才算是完全构造妥当。在构造函数中发生异常,控制权转出构造函数之外。

因此,在对象b的构造函数中发生异常,对象b的析构函数不会被调用。因此会造成内存泄漏。

2) 用auto_ptr对象来取代指针类成员,便对构造函数做了强化,免除了抛出异常时发生资源泄漏的危机,不再需要在析构函数中手动释放资源;

3) 如果控制权基于异常的因素离开析构函数,而此时正有另一个异常处于作用状态,C++会调用terminate函数让程序结束;

4) 如果异常从析构函数抛出,而且没有在当地进行捕捉,那个析构函数便是执行不全的。如果析构函数执行不全,就是没有完成他应该执行的每一件事情。

81、构造函数一般不定义为虚函数的原因

(1)创建一个对象时需要确定对象的类型,而虚函数是在运行时动态确定其类型的。在构造一个对象时,由于对象还未创建成功,编译器无法知道对象的实际类型

(2)虚函数的调用需要虚函数表指针vptr,而该指针存放在对象的内存空间中,若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表vtable地址用来调用虚构造函数了

(3)虚函数的作用在于通过父类的指针或者引用调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类或者引用去调用,因此就规定构造函数不能是虚函数

(4)析构函数一般都要声明为虚函数,这个应该是老生常谈了,这里不再赘述

《为什么C++不能有虚构造函数,却可以有虚析构函数》:

82、类什么时候会析构?

1) 对象生命周期结束,被销毁时;

2) delete指向对象的指针时,或delete指向对象的基类类型指针,而其基类虚构函数是虚函数时;

3) 对象i是对象o的成员,o的析构函数被调用时,对象i的析构函数也被调用。

83、构造函数或者析构函数中可以调用虚函数吗

  • 从语法上讲,调用完全没有问题。
  • 但是从效果上看,往往不能达到需要的目的。

派生类对象构造期间进入基类的构造函数时,对象类型变成了基类类型,而不是派生类类型。 同样,进入基类析构函数时,对象也是基类类型。

语句1讲道理应该体现多态性,执行类A中的构造和析构函数,从实验结果来看,语句1并没有体现,执行流程是先构造基类,所以先调用基类的构造函数,构造完成再执行A自己的构造函数,析构时也是调用基类的析构函数,也就是说构造和析构中调用虚函数并不能达到目的,应该避免

《构造函数或者析构函数中调用虚函数会怎么样?》:

84、智能指针的原理、常用的智能指针及实现

智能指针是一个类,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏。动态分配的资源,交给一个类对象去管理,当类对象声明周期结束时,自动调用析构函数释放资源

实现原理:采用引用计数器的方法,允许多个智能指针指向同一个对象,每当多一个指针指向该对象时,指向该对象的所有智能指针内部的引用计数加1,每当减少一个智能指针指向对象时,引用计数会减1,当计数为0的时候会自动的释放动态分配的资源。

  • 智能指针将一个计数器与类指向的对象相关联,引用计数器跟踪共有多少个类对象共享同一指针
  • 每次创建类的新对象时,初始化指针并将引用计数置为1
  • 当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数
  • 对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数
  • 调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)

unique_ptr采用的是独享所有权语义,一个非空的unique_ptr总是拥有它所指向的资源。转移一个unique_ptr将会把所有权全部从源指针转移给目标指针,源指针被置空;所以unique_ptr不支持普通的拷贝和赋值操作,不能用在STL标准容器中;局部变量的返回值除外(因为编译器知道要返回的对象将要被销毁);如果你拷贝一个unique_ptr,那么拷贝结束后,这两个unique_ptr都会指向相同的资源,造成在结束时对同一内存指针多次释放而导致程序崩溃。

引用计数有一个问题就是互相引用形成环(环形引用),这样两个指针指向的内存都无法释放。需要使用weak_ptr打破环形引用。weak_ptr是一个弱引用,它是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是说,它只引用,不计数。如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前使用函数lock()检查weak_ptr是否为空指针。

主要是为了解决“有异常抛出时发生内存泄漏”的问题 。因为发生异常而无法正常释放内存。

auto_ptr有拷贝语义,拷贝后源对象变得无效,这可能引发很严重的问题;而unique_ptr则无拷贝语义,但提供了移动语义,这样的错误不再可能发生,因为很明显必须使用std::move()进行转移。

auto_ptr不支持拷贝和赋值操作,不能用在STL标准容器中。STL容器中的元素经常要支持拷贝、赋值操作,在这过程中auto_ptr会传递所有权,所以不能在STL中使用。

《智能指针的原理及实现》:

85、构造函数的几种关键字

default关键字可以显式要求编译器生成合成构造函数,防止在调用时相关构造函数类型没有定义而报错

如果没有加语句1,语句2会报错,表示找不到参数为空的构造函数,将其设置为default可以解决这个问题

delete关键字可以删除构造函数、赋值运算符函数等,这样在使用的时候会得到友善的提示

在执行语句1时,会提示new方法已经被删除,如果将new设置为私有方法,则会报惨不忍睹的错误,因此使用delete关键字可以更加人性化的删除一些默认方法

将虚函数定义为纯虚函数(纯虚函数无需定义,= 0只能出现在类内部虚函数的声明语句处;当然,也可以为纯虚函数提供定义,不过函数体必须定义在类的外部)

type-id 必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以用于类型之间进行强制转换。

  • 常量指针被转化成非常量的指针,并且仍然指向原来的对象

  • 常量引用被转换成非常量的引用,并且仍然指向原来的对象

该运算符把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法:

  • 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用引用的转换

    • 进行上行转换(把派生类的指针或引用转换成基类表示)是安全的

    • 进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的

  • 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。

  • 把空指针转换成目标类型的空指针

  • 把任何类型的表达式转换成void类型

有类型检查,基类向派生类转换比较安全,但是派生类向基类转换则不太安全

dynamic_cast运算符可以在执行期决定真正的类型,也就是说expression必须是多态类型。如果下行转换是安全的(也就说,如果基类指针或者引用确实指向一个派生类对象)这个运算符会传回适当转型过的指针。如果 如果下行转换不安全,这个运算符会传回空指针(也就是说,基类指针或者引用没有指向一个派生类对象)

dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换

从输出结果可以看出,在进行下行转换时,dynamic_cast安全的,如果下行转换不安全的话其会返回空指针,这样在进行操作的时候可以预先判断。而使用static_cast下行转换存在不安全的情况也可以转换成功,但是直接使用转换后的对象进行操作容易造成错误。

87、C++函数调用的压栈过程

从代码入手,解释这个过程:

当函数从入口函数main函数开始执行时,编译器会将我们操作系统的运行状态,main函数的返回地址、main的参数、mian函数中的变量、进行依次压栈;

当main函数开始调用func()函数时,编译器此时会将main函数的运行状态进行压栈,再将func()函数的返回地址、func()函数的参数从右到左、func()定义变量依次压栈;

当func()调用f()的时候,编译器此时会将func()函数的运行状态进行压栈,再将的返回地址、f()函数的参数从右到左、f()定义变量依次压栈

从代码的输出结果可以看出,函数f(var1)、f(var2)依次入栈,而后先执行f(var2),再执行f(var1),最后打印整个字符串,将栈中的变量依次弹出,最后主函数返回。

《C/C++函数调用过程分析》:

《C/C++函数调用的压栈模型》:

88、说说移动构造函数

1) 我们用对象a初始化对象b,后对象a我们就不在使用了,但是对象a的空间还在呀(在析构之前),既然拷贝构造函数,实际上就是把a对象的内容复制一份到b中,那么为什么我们不能直接使用a的空间呢?这样就避免了新的空间的分配,大大降低了构造的成本。这就是移动构造函数设计的初衷;

2) 拷贝构造函数中,对于指针,我们一定要采用深层复制,而移动构造函数中,对于指针,我们采用浅层复制。浅层复制之所以危险,是因为两个指针共同指向一片内存空间,若第一个指针将其释放,另一个指针的指向就不合法了。

所以我们只要避免第一个指针释放空间就可以了。避免的方法就是将第一个指针(比如a->value)置为NULL,这样在调用析构函数的时候,由于有判断是否为NULL的语句,所以析构a的时候并不会回收a->value指向的空间;

3) 移动构造函数的参数和拷贝构造函数不同,拷贝构造函数的参数是一个左值引用,但是移动构造函数的初值是一个右值引用。意味着,移动构造函数的参数是一个右值或者将亡值的引用。也就是说,只用用一个右值,或者将亡值初始化另一个对象的时候,才会调用移动构造函数。而那个move语句,就是将一个左值变成一个将亡值。

89、C++中将临时变量作为返回值时的处理过程

首先需要明白一件事情,临时变量,在函数调用过程中是被压到程序进程的栈中的,当函数退出时,临时变量出栈,即临时变量已经被销毁,临时变量占用的内存空间没有被清空,但是可以被分配给其他变量,所以有可能在函数退出时,该内存已经被修改了,对于临时变量来说已经是没有意义的值了

C语言里规定:16bit程序中,返回值保存在ax寄存器中,32bit程序中,返回值保持在eax寄存器中,如果是64bit返回值,edx寄存器保存高32bit,eax寄存器保存低32bit

由此可见,函数调用结束后,返回值被临时存储到寄存器中,并没有放到堆或栈中,也就是说与内存没有关系了。当退出函数的时候,临时变量可能被销毁,但是返回值却被放到寄存器中与临时变量的生命周期没有关系

如果我们需要返回值,一般使用赋值语句就可以了

《【C++】临时变量不能作为函数的返回值?》:

(栈上的内存分配、拷贝过程)

90、关于this指针你知道什么?全说出来

  • this指针是类的指针,指向对象的首地址。

  • this指针只能在成员函数中使用,在全局函数、静态成员函数中都不能用this。

  • this指针只有在成员函数中才有定义,且存储位置会因编译器不同有不同存储位置。

一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果。this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候(全局函数,静态函数中不能使用this指针),编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上this指针,编译器在编译的时候也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行

一种情况就是,在类的非静态成员函数中返回类对象本身的时候,直接使用 return *this;

另外一种情况是当形参数与成员变量名相同时用于区分,如this->n = n (不能写成n = n)

类的this指针有以下特点

(1)this只能在成员函数中使用,全局函数、静态函数都不能使用this。实际上,成员函数默认第一个参数T * const this

其中,func的原型在编译器看来应该是:

(2)由此可见,this在成员函数的开始前构造,在成员函数的结束后清除。这个生命周期同任何一个函数的参数是一样的,没有任何区别。当调用一个类的成员函数时,编译器将类的指针作为函数的this参数传递进去。如:

//此处,编译器将会编译成:

看起来和静态函数没差别,对吗?不过,区别还是有的。编译器通常会对this指针做一些优化,因此,this指针的传递效率比较高,例如VC通常是通过ecx(计数寄存器)传递this参数的。

91、几个this指针的易混问题

A. this指针是什么时候创建的?

this在成员函数的开始执行前构造,在成员的执行结束后清除。

但是如果class或者struct里面没有方法的话,它们是没有构造函数的,只能当做C的struct使用。采用TYPE xx的方式定义的话,在栈里分配内存,这时候this指针的值就是这块内存的地址。采用new的方式创建对象的话,在堆里分配内存,new操作符通过eax(累加寄存器)返回分配的地址,然后设置给指针变量。之后去调用构造函数(如果有构造函数的话),这时将这个内存块的地址传给ecx,之后构造函数里面怎么处理请看上面的回答

B. this指针存放在何处?堆、栈、全局变量,还是其他?

this指针会因编译器不同而有不同的放置位置。可能是栈,也可能是寄存器,甚至全局变量。在汇编级别里面,一个值只会以3种形式出现:立即数、寄存器值和内存变量值。不是存放在寄存器就是存放在内存中,它们并不是和高级语言变量对应的。

C. this指针是如何传递类中的函数的?绑定?还是在函数参数的首参数就是this指针?那么,this指针又是如何找到“类实例后函数的”?

大多数编译器通过ecx(寄数寄存器)寄存器传递this指针。事实上,这也是一个潜规则。一般来说,不同编译器都会遵从一致的传参规则,否则不同编译器产生的obj就无法匹配了。

在call之前,编译器会把对应的对象地址放到eax中。this是通过函数参数的首参来传递的。this指针在调用之前生成,至于“类实例后函数”,没有这个说法。类在实例化时,只分配类中的变量空间,并没有为函数分配空间。自从类的函数定义完成后,它就在那儿,不会跑的

D. this指针是如何访问类中的变量的?

如果不是类,而是结构体的话,那么,如何通过结构指针来访问结构中的变量呢?如果你明白这一点的话,就很容易理解这个问题了。

在C++中,类和结构是只有一个区别的:类的成员默认是private,而结构是public。

this是类的指针,如果换成结构体,那this就是结构的指针了。

E.我们只有获得一个对象后,才能通过对象使用this指针。如果我们知道一个对象this指针的位置,可以直接使用吗?

this指针只有在成员函数中才有定义。因此,你获得一个对象后,也不能通过对象使用this指针。所以,我们无法知道一个对象的this指针的位置(只有在成员函数里才有this指针的位置)。当然,在成员函数里,你是可以知道this指针的位置的(可以通过&this获得),也可以直接使用它。

F.每个类编译后,是否创建一个类中函数表保存函数指针,以便用来调用函数?

普通的类函数(不论是成员函数,还是静态函数)都不会创建一个函数表来保存函数指针。只有虚函数才会被放到函数表中。但是,即使是虚函数,如果编译期就能明确知道调用的是哪个函数,编译器就不会通过函数表中的指针来间接调用,而是会直接调用该函数。正是由于this指针的存在,用来指向不同的对象,从而确保不同对象之间调用相同的函数可以互不干扰

《C++中this指针的用法详解》

92、构造函数、拷贝构造函数和赋值操作符的区别

对象不存在,没用别的对象初始化,在创建一个新的对象时调用构造函数

对象不存在,但是使用别的已经存在的对象来进行初始化

对象存在,用别的对象给它赋值,这属于重载“=”号运算符的范畴,“=”号两侧的对象都是已存在的

93、拷贝构造函数和赋值运算符重载的区别?

  • 拷贝构造函数是函数,赋值运算符是运算符重载。

  • 拷贝构造函数会生成新的类对象,赋值运算符不能。

  • 拷贝构造函数是直接构造一个新的类对象,所以在初始化对象前不需要检查源对象和新建对象是否相同;赋值运算符需要上述操作并提供两套不同的复制策略,另外赋值运算符中如果原来的对象有内存分配则需要先把内存释放掉。

  • 形参传递是调用拷贝构造函数(调用的被赋值对象的拷贝构造函数),但并不是所有出现"="的地方都是使用赋值运算符,如下:

注:类中有指针变量时要重写析构函数、拷贝构造函数和赋值运算符

94、智能指针的作用;

1) C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。

智能指针在C++11版本之后提供,包含在头文件<memory>中,shared_ptr、unique_ptr、weak_ptr。shared_ptr多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。</memory>

3) 初始化。智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如std::shared_ptr<int> p4 = new int(1);的写法是错误的</int>

拷贝和赋值。拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象

unique_ptr“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。

智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。

6) weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少.

1) auto_ptr的出现,主要是为了解决“有异常抛出时发生内存泄漏”的问题;抛出异常,将导致指针p所指向的空间得不到释放而导致内存泄漏;

2) auto_ptr构造时取得某个对象的控制权,在析构时释放该对象。我们实际上是创建一个auto_ptr<type>类型的局部对象,该局部对象析构时,会将自身所拥有的指针空间释放,所以不会有内存泄漏;</type>

3) auto_ptr的构造函数是explicit,阻止了一般指针隐式转换为 auto_ptr的构造,所以不能直接将一般类型的指针赋值给auto_ptr类型的对象,必须用auto_ptr的构造函数创建对象;

4) 由于auto_ptr对象析构时会删除它所拥有的指针,所以使用时避免多个auto_ptr对象管理同一个指针;

6) auto_ptr支持所拥有的指针类型之间的隐式类型转换。

7) 可以通过*和->运算符对auto_ptr所有用的指针进行提领操作;

96、智能指针的循环引用

循环引用是指使用多个智能指针share_ptr时,出现了指针之间相互指向,从而形成环的情况,有点类似于死锁的情况,这种情况下,智能指针往往不能正常调用对象的析构函数,从而造成内存泄漏。举个例子:

从上面shared_ptr的实现中我们知道了只有当引用计数减减之后等于0,析构时才会释放对象,而上述情况造成了一个僵局,那就是析构对象时先析构sp2,可是由于sp2的空间sp1还在使用中,所以sp2.use_count减减之后为1,不释放,sp1也是相同的道理,由于sp1的空间sp2还在使用中,所以sp1.use_count减减之后为1,也不释放。sp1等着sp2先释放,sp2等着sp1先释放,二者互不相让,导致最终都没能释放,内存泄漏。

在实际编程过程中,应该尽量避免出现智能指针之前相互指向的情况,如果不可避免,可以使用使用弱指针——weak_ptr,它不增加引用计数,只要出了作用域就会自动析构。

《C++ 智能指针(及循环引用问题)》:

由于C++支持多继承,除了public、protected和private三种继承方式外,还支持虚拟(virtual)继承,举个例子:

上述代码所体现的关系是,B和C虚拟继承A,D又公有继承B和C,这种方式是一种菱形继承或者钻石继承,可以用如下图来表示

虚拟继承的情况下,无论基类被继承多少次,只会存在一个实体。虚拟继承基类的子类中,子类会增加某种形式的指针,或者指向虚基类子对象,或者指向一个相关的表格;表格中存放的不是虚基类子对象的地址,就是其偏移量,此类指针被称为bptr,如上图所示。如果既存在vptr又存在bptr,某些编译器会将其优化,合并为一个指针

98、如何获得结构成员相对于结构开头的字节偏移量

S结构体中各个数据成员的内存空间划分如下所示,需要注意内存对齐

99、静态类型和动态类型,静态绑定和动态绑定的介绍

  • 静态类型:对象在声明时采用的类型,在编译期既已确定;
  • 动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的;
  • 静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;
  • 动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;

从上面的定义也可以看出,非虚函数一般都是静态绑定,而虚函数都是动态绑定(如此才可实现多态性)。

A* pa = pc; //pa的静态类型是它声明的类型A*,动态类型是pa所指向的对象pc的类型C*; pa = pb; //pa的动态类型可以更改,现在它的动态类型是B*,但其静态类型仍是声明时候的A*; pnull->func(); //C::func() 不用奇怪为什么空指针也可以调用函数,因为这在编译期就确定了,和指针空不空没关系;

如果将A类中的virtual注释去掉,则运行结果是:

pnull->func(); //空指针异常,因为是func是virtual函数,因此对func的调用只能等到运行期才能确定,然后才发现pnull是空指针;
  • 如果基类A中的func不是virtual函数,那么不论pa、pb、pc指向哪个子类对象,对func的调用都是在定义pa、pb、pc时的静态类型决定,早已在编译期确定了。

  • 同样的空指针也能够直接调用no-virtual函数而不报错(这也说明一定要做空指针检查啊!),因此静态绑定不能实现多态;

  • 如果func是虚函数,那所有的调用都要等到运行时根据其指向对象的类型才能确定,比起静态绑定自然是要有性能损失的,但是却能实现多态特性;

    本文代码里都是针对指针的情况来分析的,但是对于引用的情况同样适用。

至此总结一下静态绑定和动态绑定的区别:

  • 静态绑定发生在编译期,动态绑定发生在运行期;

  • 对象的动态类型可以更改,但是静态类型无法更改;

  • 要想实现动态,必须使用动态绑定;

  • 在继承体系中只有虚函数使用的是动态绑定,其他的全部是静态绑定;

第三版》条款36),因为这样导致函数调用由对象声明时的静态类型确定了,而和对象本身脱离了关系,没有多态,也这将给程序留下不可预知的隐患和莫名其妙的BUG;另外,在动态绑定也即在virtual函数中,要注意默认参数的使用。当缺省参数和virtual函数一起使用的时候一定要谨慎,不然出了问题怕是很难排查。

pe->func(); //F::func() 0 哇哦,这是什么情况,调用了子类的函数,却使用了基类中参数的默认值!

《C++中的静态绑定和动态绑定》:

  • 引入了 auto 和 decltype 这两个关键字实现了类型推导

  • 类和结构体的中初始化列表

  • Lambda 表达式(匿名函数)

  • 右值引用和move语义

101、引用是否能实现动态绑定,为什么可以实现?

引用在创建的时候必须初始化,在访问虚函数时,编译器会根据其所绑定的对象类型决定要调用哪个函数。注意只能调用虚函数。

Base& b = s; // 基类类型引用绑定已经存在的Son对象,引用必须初始化

需要说明的是虚函数才具有动态绑定,上面代码中,Son类中还有一个非虚函数func(),这在b对象中是无法调用的,如果使用基类指针来指向子类也是一样的。

102、全局变量和局部变量有什么区别?

生命周期不同:全局变量随主程序创建和创建,随主程序销毁而销毁;局部变量在局部函数内部,甚至局部循环体等内部存在,退出就不存在;

使用方式不同:通过声明后全局变量在程序的各个部分都可以用到;局部变量分配在堆栈区,只能在局部使用。

操作系统和编译器通过内存分配的位置可以区分两者,全局变量分配在全局数据段并且在程序开始运行的时候被加载。局部变量则分配在堆栈里面 。

《C++经典面试题》:

103、指针加减计算要注意什么?

指针加减本质是对其所指地址的移动,移动的步长跟指针的类型是有关系的,因此在涉及到指针加减运算需要十分小心,加多或者减多都会导致指针指向一块未知的内存地址,如果再进行操作就会很危险。

首先变量a和b都是以16进制的形式初始化,将它们转成10进制分别是1280(5*16^2=1280)和1312(5*16^2+2*16=1312), 那么它们的差值为32,也就是说a和b所指向的地址之间间隔32个位,但是考虑到是int类型占4位,所以c的值为32/4=8

遇到指针的计算,需要明确的是指针每移动一位,它实际跨越的内存间隔是指针类型的长度,建议都转成10进制计算,计算结果除以类型长度取得结果

104、 怎样判断两个浮点数是否相等?

对两个浮点数判断大小和是否相等不能直接用==来判断,会出错!明明相等的两个数比较反而是不相等!对于两个浮点数比较只能通过相减并与预先设定的精度比较,记得要取绝对值!浮点数与0的比较也应该注意。与浮点数的表示方式有关。

105、方法调用的原理(栈,汇编)

1) 机器用栈来传递过程参数、存储返回信息、保存寄存器用于以后恢复,以及本地存储。而为单个过程分配的那部分栈称为帧栈;帧栈可以认为是程序栈的一段,它有两个端点,一个标识起始地址,一个标识着结束地址,两个指针结束地址指针esp,开始地址指针ebp;

2) 由一系列栈帧构成,这些栈帧对应一个过程,而且每一个栈指针+4的位置存储函数返回地址;每一个栈帧都建立在调用者的下方,当被调用者执行完毕时,这一段栈帧会被释放。由于栈帧是向地址递减的方向延伸,因此如果我们将栈指针减去一定的值,就相当于给栈帧分配了一定空间的内存。如果将栈指针加上一定的值,也就是向上移动,那么就相当于压缩了栈帧的长度,也就是说内存被释放了。

① 备份原来的帧指针,调整当前的栈帧指针到栈指针位置;

② 建立起来的栈帧就是为被调用者准备的,当被调用者使用栈帧时,需要给临时变量分配预留内存;

③ 使用建立好的栈帧,比如读取和写入,一般使用mov,push以及pop指令等等。

④ 恢复被调用者寄存器当中的值,这一过程其实是从栈帧中将备份的值再恢复到寄存器,不过此时这些值可能已经不在栈顶了

⑤ 恢复被调用者寄存器当中的值,这一过程其实是从栈帧中将备份的值再恢复到寄存器,不过此时这些值可能已经不在栈顶了。

⑥ 释放被调用者的栈帧,释放就意味着将栈指针加大,而具体的做法一般是直接将栈指针指向帧指针,因此会采用类似下面的汇编代码处理。

⑦ 恢复调用者的栈帧,恢复其实就是调整栈帧两端,使得当前栈帧的区域又回到了原始的位置。

⑧ 弹出返回地址,跳出当前过程,继续执行调用者的代码。

4) 过程调用和返回指令

106、C++中的指针参数传递和引用参数传递有什么区别?底层原理你知道吗?

1) 指针参数传递本质上是值传递,它所传递的是一个地址值。

值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,会在栈中开辟内存空间以存放由主调函数传递进来的实参值,从而形成了实参的一个副本(替身)。

值传递的特点是,被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值(形参指针变了,实参指针不会变)。

2) 引用参数传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。

被调函数对形参(本体)的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量(根据别名找到主调函数中的本体)。

因此,被调函数对形参的任何操作都会影响主调函数中的实参变量。

3) 引用传递和指针传递是不同的,虽然他们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。

而对于指针传递的参数,如果改变被调函数中的指针地址,它将应用不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量(地址),那就得使用指向指针的指针或者指针引用。

4) 从编译的角度来讲,程序在编译时分别将指针和引用添加到符号表上,符号表中记录的是变量名及变量所对应地址。

指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值(与实参名字不同,地址相同)。

符号表生成之后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。

107、类如何实现只能静态分配和只能动态分配

1) 前者是把new、delete运算符重载为private属性。后者是把构造、析构函数设为protected属性,再用子类来动态创建

2) 建立类的对象有两种方式:

① 静态建立,静态建立一个类对象,就是由编译器为对象在栈空间中分配内存;

② 动态建立,A *p = new A();动态建立一个类对象,就是使用new运算符为对象在堆空间中分配内存。这个过程分为两步,第一步执行operator new()函数,在堆中搜索一块内存并进行分配;第二步调用类构造函数构造对象;

3) 只有使用new运算符,对象才会被建立在堆上,因此只要限制new运算符就可以实现类对象只能建立在栈上,可以将new运算符设为私有。

108、如果想将某个类用作基类,为什么该类必须定义而非声明?

派生类中包含并且可以使用它从基类继承而来的成员,为了使用这些成员,派生类必须知道他们是什么。

好家伙,终于完了,要是能看到这里,那你是个狠人了。

我要回帖

更多关于 php输入3个数判断最大值 的文章

 

随机推荐