July 13, 2017
你没有看错,这篇文章的tags中有人生。又一次开始思考人生,总有Bug想害朕。
今天碰到了一个,算是一个BUG,跑出来是个ERROR,抛出了个Exception。讲道理,这些都是正常的,但是很不讲道理,这个错误报的我望而却步,这个异常更是一头雾水。Just A Fuck For You.
当我拿到这个集群巡检程序的时候,我认为应该是很简单的部署上服务器就可以了,因为这是一个已经在线上运行的包,我只要改改参数在日常环境跑起来就可以了。然而,真正的魔术开始了。日常环境与线上环境居然差异很大,包括巡检的目标集群、结果数据库机器等都要改。这个还算好,虽然我不知道,但是有师兄在搞搞还是比较简单的,然后就上线,运行。
第一个问题就是找不到主类。我懵逼,居然连启动都起不起来。好在还有进一步的报错信息,原来是找不到log的包。但是我在本地可以启动,我一直以为打jar包的时候会打依赖进去,结果是并不会打依赖包。这个是学艺不精,经验不足。于是把本地所有依赖包怼进服务器。这次顺利的启动起来了。
然后就开始报错,几乎所有集群都拒绝访问。这尼玛就很尴尬了,我对整个项目一无所知啊。那就去问一下呗: “师兄,这个巡检被拒绝访问了,所有集群都访问不到。” –“有点忙,你自己先看看。” “我看不懂啊,先给我讲讲这个整体逻辑啊” –“没空” …
然后就开始了一下午的生不如死。哼,回家睡觉!不干了! 第二天来到,我决定再深入看一下代码,应该能从代码中找到问题。作为ACM选手,从来就没有机器DEBUG的习惯,眼爆bug在C++100多行的代码里还是很快的,但是在Java里就有点懵逼了。Java的代码你说长,其实也不长。但就是会让你抓狂,自己看自己写的Java项目那是怎么看怎么喜欢。去看看其他人的Java项目,呵呵。写一个大工程当然就是要功能块分拆,功能隔离,减少代码内耦合,利于修改调整,兼具灵活性与健壮性。所以Java代码的特点就是不断的继承,类间调用,流程拆分……有的时候一个很简单的功能可能要涉及7、8个类和接口,所以看代码,就要有个够大的脑内栈空间。但是,就算难看,也不机器DEBUG,手动DEBUG的基本功还是有的。
但凡报错靠谱点,都没有那么难,痛苦就在于,他报的错误与真正引起错误的地方相差太远,异常却又报的模模糊糊。首先是拒绝连接错误,现在代码里找到所有要访问的集群IP,打出来。然后找师兄去确认,集群是否在用,就这一个工作就前前后后搞了两个多小时,都在忙的不亦乐乎。集群有问题就好办多了,因为这锅就可以甩出去啦,偏偏机器没有问题。既然机器本身没有问题,那就只有代码的问题了。
坑就坑在这次抛出的异常居然没有抛出一场的地点信息。或者说异常信息说明的地点距离异常真正发生的位置也有一段距离。主异常是有连接器客户端抛出的,抛出信息就是链接失败啦。然而依靠其抛出的异常栈只能找到发出链接请求的这个类,再往下就没有栈内信息了。然后就去看调用的函数,结果一进去发现这个类没有抛出异常,而是自己处理掉了。这本应该是非常值得表扬的,因为捕获了异常意味着这个异常是可以被预料并处理的,所以优秀的程序员都会在能处理的情况下都处理掉,如果是一些不好处理的,或需要根据使用者的需求来处理就继续向上抛,抛给函数使用者去处理。很多初级的Java程序员最常见的做法就是捕获这个异常,然后打印出来,交给运维去发现异常再回来改代码。我手里的这个类并没有抛出异常,而是把一场catch下来自己处理掉了。但是他只处理了其中的IO异常,还有一个可抛出异常也被他catch下来了,并且仅仅是打印到了log里,所以我就只能从日志中追踪到这里了。然而由于没有抛出,所以也就没有信息表明这个一场出现在上面的哪个位置,只能手动二分位置查错了。这个的原理很简单,就是发生异常位置以后的代码都不会被执行,所以只要在其中夹杂一些输出语句,就可以定位到出错的那句了。接下来定位到一个新的类中,至于这个类是干什么的我也不清楚,但是可以找bug。而这个函数就完全没有抛出异常也没有捕获异常,所以这个就要对函数里所有代码都做二分定位,而不是像刚才一样只定位try代码块中的代码就可以了。几个过大概5轮定位,又定位到了一个新的类里,通过对新的这个类的定位,可以基本确定异常就来源于这个函数中的某一句。异常信息中有一个是数字格式错误,通过上面的分析,我也基本上确定在这里发生的就是这个格式错误的异常,由于函数参数中只有一个double的参数,就重点找他的问题。先输出了他的值,发现没有问题。然后发现代码中他与另外一个变量做了比较,而定位异常位置也就发生在这个比较代码块中,参与比较的另外一个是从配置文件中提取的数据。经过往回找这个配置文件的参数选项,发现问题竟然是因为拼写错误导致找不到这个配置选项,所以这条语句的值变成了if( double > null ) 然后就有了这个数字格式错误的异常了。
草泥马我的心好累。代码中写的是max_master_clients,然而配置文件中是master_max_clients,真不知道线上的那个代码是怎么跑的,害怕。
异常 Exception # 通过上面的故事,你应该已经了解了Exception的一些特性了。下面具体的介绍一下,以及正确用法!
July 12, 2017
日志 # Tomcat的日志系统还算很完善的。来看一下,当你的应用出现问题的时候要去哪里找原因吧。
apache-tomcat/logs/ |_ catalina.2017-07-12.log |_ catalina.out |_ host-manager.2017-07-12.log |_ localhost.2017-07-12.log |_ localhost_access_log.2017-07-12.txt |_ manager.2017-07-10.log |_ 对应的App应用的log文件夹,取决与你项目用的日志工具。 catalina.out 这里的数据都是代码输出到标准输出流的重定向,所以你的SOUT都在这里。 localhost.DATE.log 是你的应用抛出的错误,如页面运行错误,servlet错误等,常用查错工具。 其实目前我也就用过以上两种日志,可能用法还是不太对,入门的时候查日志感觉日志乱的要死。其实这个也不影响使用就是很影响心情,找错误一般可以直接搜索字符串定位就好了。但是我自己感觉还是要在项目开发之前就做好日志的规划工作,什么时候要打log什么时候不打,log的格式是怎样的。心情必须照顾!
tomcat 安装到发布应用 # 这一部分非常简单。
安装 # 下载tomcat-core代码包,并解压。 到其bin/目录下,linux执行./startup.sh, windows执行startup.bat脚本。 前提是需要配置JDK到环境变量。 打开http://IP:8080 查看效果。 发布应用 # 将你的应用编译打包成一个war包,然后copy到其webapps文件夹下,通过http://IP:8080/NameOfWar访问。 如何开发Java Web?你需要了解:servlet、SpringMVC、maven、javaBean等。
tomcat 源码梳理 # 打算看一下源码是怎么玩的,也有可能看不下去,坑先挖上…
July 9, 2017
JDBC(Java DataBase Connectivity) 这是JDK中集成的一个数据库API,可以访问任何类型表列数据,特别是存储在关系数据库中的数据。 本文以mysql为例。
所谓数据库交互,无非就是那么几个部分:建立连接,执行SQL,取回数据。JDBC的设计思路被Php借鉴过去后就产生了PDO,跟JDBC一曲同工,加之轻量级,然后就感觉很好的样子… 与数据库交互体现在程序上,也无非就是那么几个部分:引入包,实例化对象,调用函数。
那么学习JDBC学他的什么?学会了怎么用然后呢?可以学习其设计思路,有的时候,从设计思路来学习怎么用会比直接学怎么用要容易,Let me show you.
架构 # 现在把自己当成设计者,从设计的角度出发来看JDBC。 你要做的事情是将市面上诸多数据库整合到一起,给Java程序提供一个格式统一的工具。用更学术的语言来表达就是将底层差异屏蔽掉,抽象出一个数据库抽象层,使得数据库对于Java程序透明(学术其实也是将事物统一整理成一个较高的层次,忽略底层实现差异,用思想指导实践。例如当你碰到一个新的问题,整体十分复杂,你不可能直接就想到一个实践方法将其实现,这个时候就需要知识来抽象这个问题,当问题抽象到了某个知识领域内,这个问题就可以再从抽象顺着这个知识往下拆解,最后落实到具体实践跟抽象知识已经没什么关系了)。
要实现统一接口,我们借鉴操作系统与硬件之间的做法,就是每个数据库按照我给的一个标准来写你自己的驱动程序,这个标准就是初步将数据库之间的差异屏蔽,例如我规定所有的select语句由什么函数执行,返回什么格式的数据等。这就是底层的Driver。
再往上,我们需要一个统一的入口。在Java中,不同数据库实现的Driver肯定是不同的类,这样的话难道我们要用什么数据库就用什么类吗?当然能透明就透明,最好是我在参数中体现我要用什么数据库,灵活性更大。所以这个时候我们在几个类上面在做一个抽象,抽出来一个Manager,这个Manage负责数据库的选择,动态实例化,连接等等乱七八糟的事情,然后这个manage在整理封装成JDBC API提供给Java程序使用。
Over.
但是你知道了怎么设计,还是没有卵用,哈哈哈呵呵!上面已经说过了,当你具体实践的时候已经跟上层知识没有什么关系了,但是你会很快理解具体该怎么做。
实例 # jdbc简单的使用差不多在下面的代码中都有体现,另外我对于这个资源关闭的问题还有点迷惑,因为在jdbc的代码中实现了一个Autocloseable的接口,对于当前版本的JDBC要不要显示关闭资源的问题,有可能是不需要的。有待深入了解这个AutoCloseAble接口。
//STEP 1. Import required packages import java.sql.*; public class FirstExample { // JDBC driver name and database URL static final String JDBC_DRIVER = "com.mysql.jdbc.Driver"; static final String DB_URL = "jdbc:mysql://localhost/emp"; // URL 格式(mysql): jdbc:mysql://host:port/databaseName // Database credentials static final String USER = "root"; static final String PASS = "123456"; public static void main(String[] args) { Connection conn = null; PreparedStatement stmt = null; try{ //STEP 2: Register JDBC driver // JDBC要求要显式注册你要用的驱动类 // 这个代码就是让JVM加载对应的类,并执行其中的静态代码。 Class.
...
July 4, 2017
Java某些特性决定了他今天的地位,反射就是其中之一。
反射是一种在运行过程中使用的,对于任意一个对象都可以获得他所属的类,进而都能够获取他的任意的方法和属性。我对反射的理解就是可以实现动态调用。 最常见的应用场景就是在Java的Web框架中,就是根据一些路由信息来定位到某个类的某个函数,定位到这个函数后就要启动这个函数。不可能写N多个if-else来匹配是否是当前类,是否是这个函数,我们希望的就是我传个参数,然后就能动态调用这个函数。至于类的实例化,函数的调用,交给JVM来做,这就是反射的意义。也是一种语言特性,很多解释型语言都具备这一特性,例如php等,其实java究其本质也算是一个解释型语言,只是有一个预编译的过程,编译出来的字节码也还是要用java虚拟机来解释执行。
实例化对象并调用对应函数 # 使用反射可以根据类名称来实例化对象,调用函数,传递参数。 这一部分涉及到的内容还是很多的,比如可变长参数,反射的机制,函数的重载问题等。
首先要获取我们要调用的函数,所以第一步先Class.forName()。这个函数是让JVM去加载这个类,然后获取其对应的函数getMethod()。在获取函数的时候会涉及重载的问题,多个函数名称一样的函数会被随机获取,我们可以通过制定参数类型的方式来确定我们要获取的函数具体是哪一个,下面的例子中就有这个问题的体现。后面还会有一个函数的重载的问题,有待求证。
获得了函数,接下来就可以做调用了, invoke函数参数列表也是可变的,第一个参数是执行函数的实例,如果是静态函数第一个参数就是null。这里我是先new了一个实例,然后传进去的,一般也可以在第一个参数的位置上写class1.newInstance(),然后就是参数列表了,按照与原函数参数顺序一致的顺序填进去就可以了。我在例子中用的是另外一种方式,也是由于目标函数的参数列表并不是普通的参数导致的,这也是一个坑点。
如果你要调用的函数有一个可变长的参数,你就得绕个弯。一般我们调用可变长参数的函数的时候就是传一个数组进去就可以了,但是这里并不可以,如果参数位置是一个数组的话就会报IllegalArgumentException: wrong number of arguments。然后就发现这个invoke本身就是一个参数可变长的函数,所以当我们把参数传进去之后被JVM拆成了单项,这样再往我们要调用的函数里传的时候就会参数数量不一致。这个是由于JVM会做拆分导致的,为了避免这个问题就把参数搞成JVM不会拆分的数据结构,这样做个整体传进去就可以了。下面的代码里就是使用Object来解决这个问题,可能也有其他的方式。
package com.paladnix /** * Created by paladnix on 17-7-17. */ public class TestReflect { public static void main(String[] args) throws Exception { String className = "com.paladnix.TestReflect"; String methodName = "exec"; Class<?> class1 = Class.forName(className); TestReflect testInstance = new TestReflect(); Method method = class1.getMethod(methodName, String.class); // 调用exec(String args) // Method method = class1.getMethod(methodName, String[].class); // 调用exec(String... args) String[] params = new String[3]; params[0] = "123"; params[1] = "234"; params[2] = "345"; Object[] p = new Object[]{params}; System.
...
July 3, 2017
最近在高频率的开新坑,所以博客也就高频率的开新东西,但是由于精力有限而且需求不是很大,所以在很多问题上暂时是不求深解的。浅尝辄止,还要去快攻下一个堡垒。但是以后都是要补回来的。。
但是这个应该不是浅尝,因为会经常用到。
String # 其构造方法比较特殊,可以直接等于赋值,也可以用严格的Java面向对象的写法。
构造 # String s = "abc"; s = "abc" String s = new String("abc"); char[] data = {'a','b','c'}; String s = new String(data); String str = s.subString(1,2); //start from 1, length=2 方法 # charAt(int ); length(); replace(char old, char new); // 常见的对象转换成字符串与转换回去的问题。 // 待补充 String对象是不可改变的。每次使用 System.String类中的方法之一时,都要在内存中创建一个新的字符串对象,这就需要为该新对象分配新的空间。在需要对字符串执行重复修改的情况下,与创建新的 String对象相关的系统开销可能会非常昂贵。如果要修改字符串而不创建新的对象,则可以使用System.Text.StringBuilder类。例如,当在一个循环中将许多字符串连接在一起时,使用 StringBuilder类可以提升性能。
StringBuilder # 其一般使用方式很简单。
StringBuilder sb = new StringBuilder(); sb.append("abc"+"bbc"); sb.append(1.0); System.out.println(sb.toString()); 至于StringBuffer,可以理解为线程安全的StringBuilder。Builder的是非线程安全的,Buffer是安全的。所以在单线程的时候就可以使用StringBuilder。在速度上是 Builder > Buffer > String 。
...
July 2, 2017
使用maven已经是不可避免的事情了,如果你是个java工程师的话。并且,使用maven基本上是你最好的选择。但是我并不喜欢用IDE来集成maven,这算是强迫症,命令行强迫症。
Maven 是什么? # 传说他的功能十分强大,目前我感受到的是如下几个功能:
自动构建项目结构,根据不同的框架需求 自动包依赖管理 自动编译工程 自动启动测试 总而言之就是恰到好处的做了你觉得很麻烦又没有必要自己做的事情。 安装 # 跟JDK是一个思路的,在环境变量中添加M2_HOME即可。
使用 # # 创建Java Web 项目 mvn archetype:generate -DgroupId=com.hello -DartifactId=hello -DarchetypeArtifactId=maven-archetype-webapp # 生成项目 mvn install # 发布到tomcat # 复制生成的./target/xxx.war到tomcat目录下的webapps中去,如果不能访问就重启一下tomcat。 上述是将项目自带的helloworld页面显示出来,我们自己使用会用到一个更核心的方法–配置文件。
pom.xml # 这个配置文件是maven的核心。
在创建好的文件夹中有一个pom.xml 这里的内容分成两部分,一部分是你的项目基本信心,叫啥,啥版本的等等;还有一部分是我们要配置的部分。这一部分又分成好几部分,有依赖关系(dependencies)、生成(build)等等。
下面是一个Maven的基本结构
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <!-- The Basics --> <groupId>...</groupId> <artifactId>...</artifactId> <version>...</version> <packaging>...</packaging> <dependencies>...</dependencies> <parent>...</parent> <dependencyManagement>...</dependencyManagement> <modules>...</modules> <properties>...</properties> <!-- Build Settings --> <build>...</build> <reporting>...</reporting> <!-- More Project Information --> <name>...</name> <description>.
...
July 1, 2017
jersy是一个Web Service的框架,据说是符合RESTful架构的一种框架,这是一种新的思路,而不是新的技术,是对于前端的各种技术而言,所设计的后台实现方式,力图以一种统一的方便的方式组织为前端提供数据。 jersey的官网.
此次接触这个框架是业务需要了,但是,觉得这个框架比较简单,正好也有时间,所以就做一个学习实验,探索一下在资源短缺的情况下如何学习一个新的东西。事实上资源也确实不是很多,这一次主要依靠官方文档学习使用。
原本打算直接上代码的,但是在官网上看了一会后就发现有好多名词解释的问题。那就先来看几个名词。
首先第一个就是JAX-RS,这是JAVA EE6引进的新技术,全称Java api for RESTful Web Service. 主要是使用了注解的形式来简化Web开发和部署。然后跟Jersey的关系是Jersey实现了JAX-RS的框架接口,并且扩展了更多的东西,提供了自己的API。
然后学习Jersey的第一步就是搞懂他的路由方式,在这里就是注解了。
在讲注解之前还有一个不是很重要的名词:POJO(Plain Old Java Object), 称之为简单一般Java对象,这个概念是与JavaBean做区分的。其实没有什么必要,引用Martin Fowler的一句话:
“我们疑惑为什么人们不喜欢在他们的系统中使用普通的对象,我们得到的结论是——普通的对象缺少一个响亮的名字,因此我们给它们起了一个,并且取得了很好的效果。” ——Martin Fowler
所以事实上也就是个名词,所谓简单Java对象就是不包含业务逻辑的对象,一般用于描述数据实体。具体的区别等到写JavaBean的时候就看出来了,这里不讲了。
在我手中的项目使用了其中的两个包,maven代码如下:
<!-- jersey --> <dependency> <groupId>org.glassfish.jersey.containers</groupId> <artifactId>jersey-container-servlet</artifactId> <version>2.25</version> </dependency> <dependency> <groupId>org.glassfish.jersey.core</groupId> <artifactId>jersey-client</artifactId> <version>2.25</version> </dependency> 可以看到一个是servlet的Container,另外一个是client客户端。具体的用法,等到用到再说。
注解路由 # 这里用的路由就是JAX-RS的规范。(一次写不完,慢慢补充)。
@Path # 这个注解的原文解释非常好,所以有时间的还是去看一下原文是怎么写的吧,下面我就结合自己理解写一下。
这个@Path注解的内容是一个相对的URI路径,由其注解的类会被相应的路径访问到。
在下面这个例子中可以看到,这个类被一个@Path注解为"printers",当URL路径为/printers的时候就会路由到这个类中,那么这个类有那么多函数,调用那个函数呢?下面还有子路径以及路径的通信方式。
首先HTTP-Methods都是被支持的,常用的有:
@POST @GET @PUT @DELETE 并且对于每个函数都可以进行@Path的进一步注解,有过Web开发经验的人都知道是怎么玩的。如果你的URL路径是/printers/list, 就会定位到getListOfPrinters()函数。同理可知其他的用法。 需要说的可能是如果一个函数没有注解而其他的函数有注解,在路径为printers的时候就会定位那个没有注解的,因为其他的都是精确匹配的,按照精确匹配无法匹配到其他的函数,这个是不精确匹配的,所以就过来了。 对于/,开始和结尾的位置可以加可以不加,都能够被解析。除此以外,这里还有些内容你可能看不太懂,下面会讲。
@Path("/printers") public class PrintersResource { @GET @Produces({"application/json", "application/xml"}) public WebResourceList getMyResources() { ... } @GET @Path("/list") @Produces({"application/json", "application/xml"}) public WebResourceList getListOfPrinters() { .
...
May 29, 2017
在使用Java的过程中最主要的就是接口、继承这些东西。其实概念十分的简单,只是名字特殊了点,设计的技巧性较强。
之前听到有人在思考抽象类和接口的区别,以及为什么要两个都存在。但是,可能是我的见识太短,并没有这样的疑问。
抽象 # 面向对象编程抽象是避不开的内容,最经典的动物类,没有任何一种生物你可以说他就是动物,但是就是有动物这个概念,所以动物就是思维的抽象。良好的设计就要这样从抽象一步一步实例化最后具象为实体。 那么对于抽象的类来说,有一些东西就没有办法确定,比如动物的行走是用腿还是用腹?这个在动物类中无法确定,所以就需要先抽象着。
抽象类用修饰符abstract 修饰,并且抽象类不可以实例化,所以就不能用final来叠加修饰。抽象类可以没有抽象函数,但是有抽象函数的类必须定义为抽象类。
抽象函数 # 就是只有声明没有定义的函数,声明如下:
public abstract boolean Update(); 抽象类 # public abstract class A{ ... } 接口 # 与抽象类最大的不同就是,这并不是个类。接口不是类。所以这两个东西在本质上就不一样,所以我不认为二者有什么好冲突的。
而且最重要的就是,在Java中类是单一继承的,也就是说一个类只有一个父亲。那么对于从动物类派生出来的陆地动物、水生动物来说是没有什么问题,但是对于一个两栖动物就很尴尬,他没办法从上面的两个类中做继承,你要单独分出一个两栖动物也不是不可以,但是如果在一个系统中这样的复杂类型非常多就非常的麻烦了,然而在C++中是可以多继承的,于是就出现了接口这个东西。
接口是一种特殊的、完全没有实现的类。其中所有的方法都是没有实现的,且其中的域全都是常量。
接口的定义 # public interface interfaceName [extends superInterface1, superInterface2, ...]{ // 常量定义(类型已经默认,可以省略不写) [public] [static] [final] type Name = constValue; // 方法定义 [public] [abstract] returnType functionName(params)[throws exceptionList]{ ... } } 在子接口中对父接口的函数可以进行覆盖。
接口的实现 # public class A implements Interface1, Interface2, ...{ // 必须实现所有的接口函数 } public abstract class A implements Interface1, Interface2, .
...
May 27, 2017
面向对象编程(Object-Oriented Programming)中,最初开始接触到的就是访问控制修饰符。访问控制修饰是几乎所有的OOP语言都会涉及到的,下面就整理三个我熟悉的语言。
C/C++ # 更详细的内容可以参考《C++ primer》-类 这一章。
概述 # 有三种修饰符,分别:private、public、protected。其中最后一个protected是在继承出现以后才有效的。对于无论是成员变量还是成员函数,这些修饰符都具有相同的作用,即访问控制。其设计的主要作用是为了防止封装的类内成员被类的使用者无意更改或误操作,同时,对访问权限的限制可以实现代码间的松耦合。在类内算法做调整的过程中保持使用者可调用的代码接口,只更改私有函数等。 在详述各个控制符之前要讲一下struct 和class的区别。二者都可以用来定义类,唯一的不同就是默认的访问权限不同。struct的第一个修饰符之前定义的成员默认是public的,但是class是默认private的。
private 与 public # 这两个要一起来说,因为二相相生。private就是将控制权限制在类内,只能是类内的函数访问。public完全没有限制。这两个是最容易理解的,也没有什么好说的。
protected # 比上面两个略复杂一点点。在没有继承的情况下,protected与private是一样的,所以protect之所以与private不同在于派生类的访问。其修饰的成员可以被派生类直接访问,但是不被派生类对象访问。这个有些博客写的不太严谨,protected无论什么时候都不能与public一样,也就是类的使用者不能直接访问。 换句话说,派生类可以将其修饰的成员当成自己的亲成员来访问,派生类内部可以用对象实例来访问,但是类外不行。
举个例子:
class Base{ protected: int m; ... } class A : public Base{ public: // Note-1 void Add(A & tmp){ tmp.m += 1; } ... } int main(){ A tmp; // Note-2 int x = tmp.m; return 0; } /* * 其中,Note-1的写法没有问题,但是Note-2的写法就是非法的。 */ friend # 有的时候我们的类成员不能被使用者直接访问,但是我需要让其他的一些非类内成员函数来访问,如重载运算符的时候需要被模板函数访问,所以又有了一个新的控制访问修饰符,这个修饰符只用来修饰一些函数或类的声明,也就是说可以让一些类或函数成为该类的友元,使得其可以访问类内的成员变量和成员函数,不受其他修饰符的限制。
只能修饰非成员函数的声明,或其他类。 只能在类内修饰。 其声明不受其他几个修饰符影响。 友元关系不能传递, 也不可继承。 友元声明,只是声明了访问权限,并没有实际声明,如要使用该函数,仍然需要显示用普通声明再声明一次。 用作继承权限修饰符 # 除了用于成员权限声明,此三个修饰符海可以用来修饰类的继承方式,不同的继承方式使得基类成员在派生类中的权限不同。
...