OOP

C++ OOP--const、引用、指针

May 30, 2017
C++, OOP

继续梳理C++中的问题, 今天梳理的是最基础的几个概念,也是比较麻烦的概念。 引用 # 引用是一个变量的别名,声明时必须初始化,而且之能在初始化的时候绑定一个变量对象。 引用不能绑定在引用上,因为引用本身不是对象。 普通引用不能绑定到立即数上,因为立即数不是对象,但是常量引用是可以的。 普通引用不能引用类型不同的对象。 以上的普通引用是相对于常量引用来说的,下面会介绍到。 指针 # 这个概念最简单,他不是是一种数据类型,他的数值是个地址,可以用解地址符*来直接操作指向的地址内的变量。 指针有下面四种状态: 指向一个对象; 指向紧邻对象的下一个位置; 空指针(nullptr, 避免使用NULL,nullptr可以被转为任意类型的指针); 其他指针装态(无意义的状态); const 常量 # const 修饰的常量,默认是文件内有效,如果想在多文件内可用,就加extern修饰。 这个和其他的东西一结合就比较烦。 首先const是声明常量的作用,用其修饰的变量值必须初始化且不可更改。这就是普通常量了,很好理解。 const 引用 # 如果你声明了一个引用,并且这个引用是const修饰的,那么,你要知道两个事情:第一,引用不能绑定到其他变量了;第二,用这个引用不能改变做引用的对象的值了,但是不影响那个变量通过其他途径改变,改变后的值一样会在引用中表现出来。 如果你引用的对象是个常量,那么这个引用也必须是常量。反过来没有这个限制。 const 指针 # 指针这块又有两种,因为指针是可以改变其指向的对象的,并且其指向的对象也是可以更改的。就出现了指针常量(pointer to const) 和 常量指针(const pointer)。汉字理解起来没有英文来的准确,所以下面不用常量指针和指针常量来表述。 pointer to const 很明显是指向的对象是个常量,而指针不能够修改这个对象的值。 const pointer 很明显就是这个指针是个常量,只能指向这个固定的内存,但是可以通过其修改对象的值。 在定义上如下: int a; // pointer to const const int *p1 = &a; // 等同于 int const *p = &a; // const pointer int *const p2 = &a; 所以可以看出来,const修饰的就是紧跟其后的内容,在pointer to const的定义中可以认为*p 指代指向的对象;而const pointer中const修饰的是p; ...

C++ OOP--函数解析过程

May 30, 2017
C++, OOP

类的作用域 # 名称查找优先,对于编译器来说,优先寻找名称一致的函数,再关心类型的问题。 编译器在进行名称查找的顺序: 直接类定义 -> 父类定义 -> 祖宗类定义。但是,搜索顺序是按照静态类型开始的,结合上面的动态绑定,如果你的指针类型是父类,那么即使实体是个子类也不会在子类域中搜索,直接从父类开始搜索,因为编译阶段无法做动态绑定。 隐藏&重载&覆盖 # 这个概念刚开始学C++的人可能会比较迷。 隐藏 # 隐藏是在继承中出现的概念。基类与派生类有一个同名函数,则无论是否参数类型一致,基类的函数都会被隐藏掉。这个隐藏的意思,与Java中的override是否一致,我觉得不一致,但是效果一致。因为编译器在搜索名字的时候首先搜索派生类的函数,然后看参数,如果参数类型对的上就调用,对不上就报错。所以根本调用不到基类的函数,实现了隐藏。但是,C++中基类的函数还是存在的,Java中是直接将父类的函数段重写,父类的函数完全从代码段消失了。 既然还存在,就意味着可以访问。使用using关键字调用函数可以直接调用基类被隐藏的函数,可以说using改变了编译器名字查找的顺序。 对于虚函数,我们要求参数类型必须一致,就是这个原因。要实现动态绑定就必须使得当查找到函数名时,此函数可以被调用,而不是报个错说参数不一致。 这个规则是可以类推出来的,从命名上我们就可以窥探出这个东西有什么特质。 比如作为成员函数,就是可以被隐藏,无论是虚函数还是普通函数都是可以的。虚函数可以被普通函数隐藏,普通函数也可以被虚函数隐藏,其他一些概念一样可以用命名来推导出来。 重载 # 重载是对于函数来说的,与类间继承没有关系。对于一个普通函数,即不存在任何的类中的独立函数,重载的概念很清晰,就是相同的函数名,不同的参数列表; 对于成员函数来说,在同一个类中,可以重载,重载的概念也是一样的。 覆盖 # 覆盖与上面的东西又不一样,但是这个不是C++中的重点概念。覆盖是对于基类的虚函数来说的,派生类写一个名称、参数类型完全相同的函数,可以实现覆盖,其实就是虚函数的那一套东西。 只要分清楚,隐藏和覆盖是在父子间作用的,重载是同级内作用的就可以了。 那么有一个问题,很恶心,哈。 就是基类的虚函数被重载了,有很多个版本,子类在覆盖的时候,可以选择覆盖哪个版本。一样是使用using修饰符来将基类的函数作用域同步到当前作用域上来,这样只有对需要覆盖的版本进行覆盖就行了,其他的就使用基类的实现。

C++ OOP基础--动态绑定

May 29, 2017
C++, OOP

面向对象其实就是那样,都差不多,不一样就是关键字还有一些奇怪的机制问题,总的来说实现的东西都是一样的。 虚函数 # 对应Java的抽象函数。但是略有区别。 虚函数在定义的时候使用关键字virtual来修饰声明,但是不一定是没有实现的,可以指定一个已经是实现的函数为虚函数。虚函数最主要的定义是使得每个继承他的子类都实现自己的版本。并且虚函数是实现C++动态绑定的关键。 虚函数只能声明在类的内部。 动态绑定 # 所谓动态绑定就是,有些函数并不是在编译的时候就知道要去调用那个函数的,而是在真正运行的时候才能根据变量的类型去调用对应的函数。这个实现就是对于基类的虚函数,子类也实现一个版本(子类在实现的时候不需要指定函数为虚函数,因为继承过来的函数就是虚函数,是可以传递的),这样对于同一个函数就有两个版本,当我在调用的时候使用指针调用或引用调用(注意必须是这两种调用之一的方式才会有动态绑定的效果,其他都是在编译阶段就能够确定下来。)就可以实现动态绑定。 由于虚函数是在执行的时候才调用,所以在编译阶段就有些错误报不出来,比如如果我们没有实现某个子类的虚函数,在调用的时候就没有函数去执行,所以所有的虚函数都必须被定义,而不仅仅是声明,普通的函数只要我们永不到就可以不进行定义。 引用和指针调用 # 所谓引用和指针调用,是指函数中关于虚函数的调用对象是作为引用参数或者是指针参数传进去的,这样就需要根据参数的类型来调用不同版本的虚函数。这里就要说一下基类与派生类之间的类型转化问题,正是因为其二者可以进行类型转化,所以在参数列表中的类型并不能唯一限定参数的类型,才有了虚函数调用时的动态绑定。 类间类型转换 # 子类中有一部分是来自与基类的,这一部分可以被单独利用。也就是说,一个子类,我们也可以把他当作他的某个基类来使用,使用的时候只用到基类的部分。也就是说,我有一个基类的指针,我就可以指向这个子类,此时,指针就操作子类的基类部分。这就是从派生类到基类的类型转换。 反过来就是不行的,可想而知,多的东西可以不要,少了东西就会有问题,所以不存在从基类到派生类的转化。同时我们也就明白了,如果你要对于一个虚函数动态绑定,在调用函数的对象的类型声明的时候就要声明为基类的类型,这样当你传入一个子类的引用的时候,该引用就会转换为子类,进而去调用子类的虚函数实现;如果你写声明的时候声明为了子类的类型,就没有办法动态绑定了,因为反向没有办法转换。 虚函数覆盖 # 子类要实现自己的虚函数版本就要对基类的虚函数进行覆盖,override,又是这个熟悉的东西。覆盖当然就要参数类型完全一样,参数不一样的那是重载,是不同的函数。书上说,一般我们都是要进行覆盖而不是重载,但是有的时候我们会搞错参数列表的顺序等细节,所以,你可以在函数声明的参数列表后加override说明符来告诉编译器,你是要覆盖虚函数的,如果有细节错误就告诉我! 如果你在后面加上了final修饰符,就意味着,这个虚函数不希望再被覆盖了。 关于虚函数的默认参数,不同版本可能会有不同的默认参数值,但是在默认参数的选取上,是依据静态类型决定的,也就是说不能做到动态绑定,调用函数的对象定义成什么类型就用什么默认值。如你调用函数的对象是基类的引用,那么即使你最后执行的是子类的函数,默认值也是基类的默认值,所以这个就会出现隐蔽的问题。所以安全起见,默认参数一致最好。 猜想 # 同时从这里我们也可以窥探出编译器的原理。对于一个函数覆盖来说,函数的默认值必须是在编译阶段就写进机器码中的,尽管你有两个版本的函数,编译器会对两个函数分别编译,但是默认值存储的实现是与函数编译分开的。这里只是猜测,对于函数的默认值,其实编译器可以将其存储在对应的栈位置上,也可以将其转成一个赋值命令存储在函数代码段中,这两种理论上都可以实现默认参数的功能,但是从编译器的定义角度,参数是单独存放在数据段的,而不在代码段,那么在编译的时候就会根据你参数的字面类型进行设定,而无法进行动态绑定。这个应该是C++的一个设计缺陷,最初设计函数编译的时候没有动态一说,就直接写进数据段了,等到出现动态绑定的时候沿用之前的函数处理的方式,就无法实现绑定。(不知道这个思路对不对,以后有时间考证下)。 回避动态绑定 # 除了动态绑定,也可以用作用域操作符来强制执行某一类的虚函数版本。 double ans = C->Base::Update(); 为什么要回避动态绑定,这与类间设计有关。如果基类与派生类关于虚函数功能设定是:基类虚函数处理通用处理,派生类处理剩余子类相关内容,那么对于真的要完整实现功能的时候就要在派生类的虚函数中先调用一下基类的虚函数,再实现派生类的函数剩余部分,所以这时候就要回避动态绑定。 纯虚函数 # 你的直觉会告诉你这个纯虚函数应该是个只有声明没有定义的函数。没错,这就是纯虚函数,在格式上有一个特点,就是函数声明最后有一个=0标志。这个声明必须在类的内部,并且类内不能对这个函数进行定义,在类外部定义是可以的。 纯虚函数是在一些设计上必要的。比如我们有一个操作,有4个版本,每个版本根据不同的参数值有不同的动作,那么我们会设计一个基类,并在基类中声明这个函数,但是这个函数明显什么都不能做,而是要等到不同的子类根据自己的方案实现这个函数。所以这时候就声明一个纯虚函数就好了。 子类只要正常的覆盖这个函数就可以,如果不覆盖就继续往下传递。 抽象类 # 很熟悉,Java中含有抽象函数的类是抽象类,在这里有纯虚函数的类就是抽象类。并没有什么修饰符,就是个类。 继承中的友元 # 友元不可继承、不可传递。 派生类的友元不可访问基类中的非公有成员。

Java 抽象与接口

May 29, 2017
Java, OOP

在使用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, . ...