C++学习笔记-继承
来源:互联网 发布:天基动能武器知乎 编辑:程序博客网 时间:2024/06/03 16:18
概述
正所谓龙生龙,凤生凤,老鼠的儿子会打洞,生物界建立在基因继承的基础上,同时又通过变异来发生改变,那C++既然面向对象,便也整了一套相似(有相同,又不完全相同,故名相似)的东西:继承。
派生类或者子类通过继承得到基类或者父类的所有属性与方法(私有除外),然后又通过重定义来产生类似于基因突变的奇妙现象,而且还可以通过添加新的东西来进行拓展,这与生物界的后天学习又有着异曲同工之妙,看来C++的设计者,不仅是伟大的黑客还是优秀的仿生学专家。
派生类的生成:
- 吸收基类的成员(继承)
- 改造基类的成员(重定义)
- 添加自己新的成员(扩展)
继承是为了代码复用,但代码复用不一定要用继承。一般来说,需要在已有类的接口基础上拓展接口时,使用继承是比较好的,如果仅仅是为了使用已有类的功能,使用组合更好。
从这个角度看,大自然母亲设计的这套生物遗传体系,也蕴含着资源复用,不重复造轮子的原则在里面,看来,作为人类,只要潜心向大自然母亲学习,就会发现,其实很多问题,大自然已经给了解决方案。
继承方式
补充:
友元类和友元函数可以访问类的所有成员,包括吗public、protected以及private的;
派生类可以访问基类的public和protected成员,但不能访问基类的private成员;
普通的非派生类只能访问public成员,protected和private都不能访问。
上表中,子类内部模块访问性看的是第二列,而子类对象访问性(子类外部访问特)看的是第三列。
公有继承(public):基类的public成员函数在派生类中仍然是public,基类的接口变为派生类的接口,所以public继承也叫接口继承;
私有继承(private):基类成员全变为private的,派生类不再支持基类的接口,只是为了重用基类的实现(代码),所以它是实现继承;
保护继承(protected):public变为protected,派生类不再支持基类的接口,属于实现继承。
对于派生类本身的成员来说,这三种继承没什么区别,除了基类的私有成员不能访问外,基类的public和protected成员可以认为已经变成自己的东西了,不管是什么属性,既然是自己的就都可以访问。
三种继承影方式影响的是,外部普通类以及它的派生类对它“体内”的基类成员的访问性。
重定义
有一点需要先说明,派生类成员对基类成员的重定义是无视权限的,也就是说,对于public继承来的基类的public成员,派生类里的private同名成员会隐藏它;同样的对于private继承来的基类成员,派生类里的public同名成员也会隐藏它,而需要必须是private成员。重定义与权限没有任何关系,所以下面的内容博主就不会再提权限这件事了。也就是说,重定义对基类成员的改造,不仅在功能上,还包括访问权限。
对基类数据成员的重定义:
当派生类中定义了与基类同名的数据成员时,会隐藏基类同名数据成员,静态数据成员同样适用。
这个隐藏是针对可见性而言的,不影响基类数据成员的作用域,也就是说基类的这个同名数据成员还安安静静地躺在内存里呢,并没消失,还在候着命。此时此刻(派生类内部)你把基类当成一个命名空间就行了,所以可以通过基类类名加作用域限定符来访问,对于没被隐藏的,加不加这个基类名作为限定都可以访问,因为不会产生二义性。
class Base{public: int i_; static int b_;};int Base::b_ = 10;class Derived : public Base{public: int i_; static int b_;};int Base::b_ = 20;int main(void){ Derived d; d.i_ = 0; // 访问派生类的数据成员 d:Base.i_ = 10; // 访问基类的被隐藏的数据成员 // 访问基类的被隐藏的静态数据成员 <=> Base::b_ cout << "Base::b_: " << Derived::Base::b_ << endl; // 访问派生类的静态数据成员 cout << "Derived::b_: " << Derived::b_ << endl; return 0;}
对基类成员函数的重定义:
1. overwrite(重写/隐藏)
是指派生类的函数屏蔽了与其同名的基类函数:
- 如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏。
- 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)
#include <iostream>using std::cout;using std::endl;class Base{public: Base() : x_(0) { } void Show() { cout << "Base show() ... " << x_ << endl; } int x_;};class Derived : public Base{public: Derived() : Base(), x_(0) { } // Derived::Show()与Derived::Show(int)构成重载overload void Show() // 与基类同名同参数,但不是虚函数,隐藏基类的同名函数 { cout << "Derived show() ... " << x_ << endl; } void Show(int n) // 与基类同名但参数不同,会隐藏基类的同名函数 { cout << "Derived show(int) ... " << x_ << ":" << n << endl; } int x_; // 重定义基类的数据成员,会对外隐藏基类的这个数据成员。};int main(void){ Derived d; d.x_ = 10; d.Base::x_ = 20; d.Show(); // 派生类 d.Show(20); // 派生类 d.Base::Show(); // 基类 cout << "sizeof(Base): " << sizeof(Base) << endl; cout << "sizeof(Derived): " << sizeof(Derived) << endl; return 0;}// OUT:// Derived show() ... 10// Derived show(int) ... 10:20// Base show() ... 10// 4// 8 //===========> Base:4 + Derived:4 = 8
但是需要注意的是,当派生类中的函数与基类中的某个函数具有相同函数名以及不同的参数列表时,它们构不成函数重载(overload),因为它们的作用域都不同,函数重载要求必须有相同的作用域。
2. override(覆盖)
它只针对虚函数:
- 不同的范围(分别位于派生类与基类);
- 函数名字相同;
- 参数相同;
- 基类对应的函数必须为虚函数。
#include <iostream>using std::cout;using std::endl;class Base{public: Base() : x_(0) { } virtual void Show(int n) { cout << "Base show() ..." << n << endl; } int x_;};class Derived : public Base{public: Derived() : Base(), x_(2) { } void Show(int n) // override, 虽然派生类没有virtual,它仍然是虚函数 { cout << "Derived show(int) ... " << n << endl; } int x_;};int main(void){ Derived d; d.x_ = 10; d.Base::x_ = 20; Base *p = &d; p->Show(20); // 调用的是Derived::Show(int) return 0;}
3. overload(重载)
重载不是对基类的重定义,为了完整,这里就加上这个,overwrite与override都是针对不同的作用域,即派生类和基类,而overload是针对相同的作用域(即必须位于同一个类中)。同一类中的的函数名相同参数不同(类型/顺序/数量)的成员函数构成重载。
overload、overwrite、override是三个很容易混淆的概念,但只要理解其含义,我觉得还是很好区分的。
继承的局限性:
不论何种继承方式,下面这些基类的特征是不能从基类继承下来的:
- 构造函数
派生类构造函数只需要初始化新增的成员,基类成员通过调用基类构造函数进行初始化。如果不在派生类初始化类表中显示调用基类构造函数,则编译器会默认调用基类的默认构造函数,如果基类没有默认构造函数,又没在派生类中显示调用基类构造函数,就会出错。 - 析构函数
派生类析构函数只需要处理新增的成员,基类成员调用基类的析构函数进行处理。如果不在派生类析构函数中调用基类析构函数,则编译器会默认调用基类的默认析构函数。 - 赋值运算符
- 用户重载的new 、delete运算符(operator new / operator delete)
- 友元关系
派生类的构造与析构
只能在派生类构造函数的初始化列表中初始化的成员:
- const成员;
- 引用成员;
- 没有默认构造函数的基类;
- 没有默认构造函数的类对象成员。
派生类构造函数的执行过程:
基类构造函数 -> 类对象数据成员的构造函数 -> 自己的构造函数体
|————构造函数的初始化列表————|
派生类的拷贝构造函数、转换构造函数也是构造函数,与上面过程一样。
派生类析构函数的执行过程:
自己的析构函数 -> 类对象数据成员的析构函数 -> 基类的析构函数
基类与派生类的转换
派生类对象向上转型成基类对象*
向上转型是安全的转型。
当派生类是public继承的时候,编译器可自动完成转换:
- 派生类对象指针自动转换为基类指针(实际上就是对象没变化);
- 派生类对象引用自动转换为基类对象引用(实际上就是对象没变化);
- 派生类对象自动转换为基类对象,对象本身的转换才是真正的转换,会发生对象切割(object slicing)。
#include <iostream>using std::cout;using std::endl;class Base1{public: int id_;};class Base{public: short name_; int id_;};class Derived : public Base1, public Base{public: int id_; int dep_;};int main(void){ Derived d; d.Base1::id_ = 1000; d.Base::name_ = 101; d.Base::id_ = 102; d.id_ = 103; d.dep_ = 104; Base *b = &d; // 派生类对象指针向基类指针转换 cout << b << endl; cout << sizeof(Base) << " " << sizeof(Derived) << endl; cout << "Base1::id_ : " << &d.Base1::id_ << endl; cout << "Base::name_: " << &d.Base::name_ << endl; // Base起始 cout << "Base::id_ : " << &d.Base::id_ << endl; // Base结束 cout << "id_ : " << &d.id_ << endl; cout << "dep_ : " << &d.dep_ << endl; getchar(); return 0;}// OUT:// 0x0032FE20// 8 20 // Base1::id_ : 0x0032FE1C// Base::name_: 0x0032FE20// Base::id_ : 0x0032FE24// id_ : 0x0032FE28// dep_ : 0x0032FE2C//// 内存模型: // |---------------|// | Base1::id_ | <----- low addr// |---------------|// | Base::name_ |// |---------------|// | Base::id_ |// |---------------|// | Derived::id_ |// |---------------|// | Derived::dep_ | <----- high addr// |---------------|
可以看出来, 对于派生类对象指针向基类指针转换,编译器实际上就是根据内存模型来对派生类的首地址作适当偏移,把偏移后的新地址作为基类指针的值,也就是指针在移动,而对象本身是不变的。这里用多继承是为了看出地址的偏移的变化,因为单继承时,派生类中所包含的基类的首地址恒等于派生类的首地址,偏移量为0。
当派生类是private或者protected继承的时候,编译器无法自动转型:
- 派生类对象指针不能自动转换为基类对象指针,但可以使用reinterpret_cast<>()或者C风格的强制类型转换,static_cast<>()不行;
- 派生类对象引用不能自动转换为基类对象引用,与指针一样,可以使用reinterpret_cast<>()或者C风格的强制类型转换;
- 对于派生类对象,即使使用强制类型转换也不行。
只与为什么向上转型会受继承方式的影响,暂时没搞明白。
基类对象向下转型成派生类对象
向下转型是不安全的,没有自动转换机制,同时向下转型与继承类型没关系。
基类指针可以通过强制类型转换转为派生类指针,但是基类对象无法通过强制类型转换完成这种转换。
上面说的是在类外部进行转换,如果非要向下转型,可以从类内部下手:
- 在派生类实现转换构造函数;
- 在基类重载类型转换运算符。
多重继承
单继承就是一个派生类只有一个基类,而多重继承就是一个派生类有多个基类,不同基类之间用逗号隔开,就像构造函数的初始化列表一样。
class Base1{ // CODE}class Base2{ // CODE}// 多重继承class Derived : public Base1, public:Base2{ // CODE}
多重继承的二义性:
所谓的二义性是指,当多个基类中有同名的函数时,编译器无法确定该采用哪个基类的实现版本,这就是所谓的二义性。
解决方法也很简单,就是直接用基类的类名作为作用域限定符,手动指定调用哪个基类的实现版本;再一种方式就是在派生类中定义一个同名函数,在这个函数中,根据不同的条件调用来自不同基类的同名函数,这样本类中其它函数直接调用本类的函数就行了,相当于将二义性处理代码封装起来:
class A {public: void print() // A中定义了print函数 { cout << "Hello,this is A" << endl; }};class B{public: void print() // B中同样定义了print函数 { cout << "Hello,this is B" << endl; }};class C : public A, public B // 类C由类A和类B共同派生而来{public: void disp() { A::print(); // 1. 指明采用A类中定义的版本 } // 2. 重载print函数, 根据条件调用不同基类的print函数 void print() { if (true) { A::print(); } else { B::print(); } }};
既然是通过类名来限定,那就有个问题了,如果有两个同名的基类怎么办?有的人可能会质疑,怎么可能有两个同名的基类呢?是的,C++的语法告诉我们,一个派生类不可能会有两个同名的基类,但这只针对直接继承的基类,而影响不到间接继承的基类。比如类B和类C继承自类A,而类D又继承了B和C(即所谓的钻石型继承),这样其实在D的“体内”就有了两份A,只不过这两份A分别被装在了B和C中,而B和C是不同,所以它们并不违反C++语法,但是当调用来自A的函数时,C++就不知道调用B中的这个函数还是C中的这个函数了,虽然实际上,在内存中这两条路径(A->C和A->B)来的这“两个函数”具有完全相同的入口地址,因为所有类的对象共享一份成员函数,这与数据成员不同。
class A {public: A(int x = 0) : x_(x) { } void SetX(xonst int& xp) { x_ = xp; }protected: int x_;};class B: public A{};class C: public A{};class D : public B, public C{};int main(){ D d; d.SetX(5); // SetX()具有二义性, 系统不知道是调用B类的还是C类的SetX()函数 return 0;}// 关系模型:// 整个D的内存中有两份A。// +------------------------------------------------+// | +---------------+ +---------------+ |// | | +-------+ | | +-------+ | |// | | | A | | | | A | | |// | | +-------+ | | +-------+ | |// | | | | | |// | | B | | C | |// | +---------------+ +---------------+ |// | |// | D |// +------------------------------------------------+
有问题不怕,解决就是了,C++的应对策略是:虚继承。所谓虚继承实际上就是如果出现上述那种情况,就把B和C中的两份A的拷贝合并为一份,让B和C共享这一份拷贝,这样就不会产生二义性了,同时又不影响功能,这也符合节省资源,不重复造轮子的原则。
这样钻石继承的内存模型就变成这样了:
// 整个D的内存中只有一份A,也就不会产生二义性了。// +---------------------------------------+// | +-------+ |// | | A | |// | +-------+ |// | +--------^-------+ |// | | | |// | +----+----+ +----+----+ |// | | B | | C | |// | +---------+ +---------+ |// | |// | D |// +---------------------------------------+
这个过程编译器不能自动完成,需要我们手动标记那些类要被这样处理,那如何告诉编译器这些信息呢,看下节。
虚基类
语法
我们还是以上一节的钻石继承来讲
A B继承A C继承A D继承B,C
我们只需要在继承时给基类来个virtual标志就行了,语法:
class A {public: A(int x = 0) : x_(x) { } void SetX(xonst int& xp) { x_ = xp; }protected: int x_;};class B: virtual public A // 虚继承,对于B,A就成为了虚基类{};class C: virtual public A // 虚继承,对于C,A也是虚基类{};// 这里不需要虚继承,这样当出现钻石继承这种形式时,编译器一看A是B和C的虚基类,// 那它就会让B和C共享一个A实例,但是如果没有上面的virtual让A成为B和C的虚基类,// 编译器就不会这样做。class D : public B, public C {};int main(){ D d; d.SetX(5); // SetX()没有二义性 return 0;}
有一点需要特别注意,只有A相对于B和C都是虚基类时,编译器才会做处理,如果A只是B的虚基类,但不是C的虚基类,那编译器就不会做处理,二义性仍然存在。
虚基类及其派生类的构造函数:
虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数来初始化的。
在整个继承结构中,直接或间接继承虚基类的所有派生类,都必须在构造函数的初始化列表中给出对虚基类的构造函数的调用,如果未列出表示调用虚基类的默认构造函数。
在建立对象时,只有最远派生类的构造函数调用虚基类的构造函数,该派生类的其它基类对虚基类构造函数的调用将被忽略。而且无论初始化列表中顺序如何,最远派生类都是先初始化最远的那个虚基类,然后是各个基类。
大热天的,再来个例子清爽一下吧:
#include <iostream>using std::cout;using std::endl;using std::ostream;// 虚基类是一个相对概念,不能单纯的说一个类是虚基类,因为虚基类的标志virtual// 是在继承的时候施加的,也就是说,只有相对于某个具体派生类,才有虚基类这个概念。// A是B的虚基类,并不一定是C的虚基类。class Furniture{public: Furniture(int weight) : weight_(weight) { cout << "Furniture one param ..." << weight << "..." << endl; } ~Furniture() { cout << "~Furniture ..." << endl; } int GetWeight() const { return weight_; } void SetWeight(int weight) { weight_ = weight; }private: int weight_;};// Furniture是Bed的虚基类class Bed : virtual public Furniture{public: // 直接或间接继承虚基类的所有派生类,都必须在构造函数的初始化列表中给出 // 对虚基类的构造函数的调用。 Bed(int weight) : Furniture(weight) { cout << "Bed one param " << weight << "..." << endl; } ~Bed() { cout << "~Bed ..." << endl; } void Sleep() const { cout << "Sleep ..." << endl; }};// Furniture是Sofa的虚基类class Sofa : virtual public Furniture{public: // 直接或间接继承虚基类的所有派生类,都必须在构造函数的初始化列表中给出 // 对虚基类的构造函数的调用。 Sofa(int weight) : Furniture(weight) { cout << "Sofa one param " << weight << "..." << endl; } ~Sofa() { cout << "~Sofa ..." << endl; } void WatchTv() const { cout << "WatchTv ..." << endl; }};class SofaBed : public Bed, public Sofa{public: // Furniture的最远派生类就是SofaBed,建立SofaBed对象时,只有这里对 // Furniture的初始化是有效的,Bed和Sofa中对Furniture的初始化都将被 // 忽略。 // 先调用Furniture的构造函数,然后是Bed和Sofa的构造函数,最后执行SofaBed // 构造函数的函数体。 SofaBed(int weight) : Bed(weight), Sofa(weight), Furniture(weight) { cout << "SofaBed one param " << weight << "..." << endl; } ~SofaBed() { cout << "~SofaBed ..." << endl; } void FoldOut() const { cout << "FoldOut ..." << endl; } void FoldIn() const { cout << "FoldIn ..." << endl; }};
虚基类对C++对象内存模型的影响
钻石型继承实例代码:
class A{public: int a_;};class B : public A{public: int b_;};class C : public A{public: int c_;};class D : public B, public C{public: int d_;};
上一节中说到,对于钻石型继承,为了避免D出现A的两份拷贝(B::a_和C::a_)造成的函数调用的二义性,C++中引入了虚基类,使得B和C共享一份A。那就有一个问题了,我们知道,如果不使用虚基类,内存模型为:
// +---------------+ -+- -+-// low addr -> | B::a_ | | |// +---------------+ | class B |// | b_ | | |// +---------------+ -+- |// | C::a_ | | | class D// +---------------+ | class C |// | c_ | | |// +---------------+ -+- |// high addr -> | d_ | | |// +---------------+ -+- -+-
具体的内存是这样的:对于B来说,最开始是A的数据成员,后面紧接着B特有的数据成员;对于C,最开始是A的数据成员,后面紧接着C特有的数据成员;对于D,最开始是B,后面紧接着的是C,最后是D新增的数据成员。
因为在B和C的开始处都一个完整的基类,所以B和C可以很方便的访问它们的基类A,但是一旦引入了虚基类,B和C此时就要共享一份A,那这份A究竟是放到A中还是B中呢?(g++的资料有点少,而且我觉得g++的相关内存结构有点复杂,VS2008的到时比较简单,网上资料也多,所以之后的内存模型,用32bit windows平台)
其实也很好想到,既然是共享,那我就把A单独拿出来,然后B和C都“链接”到这份A上去不就OK了吗,那怎么实现这个“链接”呢,最直观的一种就是在B和C的起始处放一个指向A的指针变量(或者存一个偏移量),这样内存模型基本上不会变化太大,然后A放哪都无所谓,整个D内存的最前面最后面甚至中间都行。但实际上编译器并不是这样做的,只于为什么我也不太清楚,上述代码的实际内存模型:
// // +--- -+-- +-------------+// | | | vbptr for B | ---------> +---------+// | B | +-------------+ | 0 |// | | | b_ | +---------+// | -+-- +-------------+ | 20 |// | | | vbptr for C | ---+ +---------+// D | C | +-------------+ |// | | | c_ | +-----> +---------+// | -+-- +-------------+ | 0 |// | | d_ | +---------+// | -+-- +-------------+ | 12 |// | A | | a_ | +---------+// +--- -+-- +-------------+
虚基类A位于D的最后端,B和C分别包含一个指向虚基类表的指针。
虚基类表(virtual base table)中保存的内容:
第一项:本类地址与虚基类表指针地址的差 [本类地址 - 虚基类表指针地址]
注意:本类地址是指vbptr的直接拥有者,如D类对象中分别有B和C,B和C又都有一个vbptr,则B的vbptr中本类地址指的是B对象的地址,而不是D。
第二项:虚基类地址与虚基类表指针地址的差 [虚基类地址 - 虚基类表指针地址]
通过这两项,我们可以算出虚基类相对于当前类(B或C)的偏移量,继而找到虚基类对象A。
那现在我们来算算类D的大小:
(4 + 4) + (4 + 4) + 4 + 4 = 24 bytes|---B---| |---C---| |-D'-| |-A-||---------------D----------------|
对于类B(或者C)原理也是一样的:
(4 + 4) + 4 = 12 bytes|---B'--| |-A-||-------B------|
派生类向虚基类转型(向上转型):
int main(void){ D d; cout << "&D : " << &d << endl; cout << "&D::a_: " << &d.a_ << endl; cout << "&D::b_: " << &d.b_ << endl; cout << "&D::c_: " << &d.c_ << endl; cout << "&D::d_: " << &d.d_ << endl; A* ptr_a = &d; // p所指向的并不是d的首地址,而是d中最后面A对象的首地址 cout << ptr_a << endl; C *ptr_c = &d; cout << ptr_c << endl;}// OUT:// &D : 002CF8F0// &D::a_: 002CF904// &D::b_: 002CF8F4// &D::c_: 002CF8FC // &D::d_: 002CF900// 002CF904// 002CF8F8
由输出结果可以看出来,ptr_a指向的是 D::a_内存单元,也就是说,当派生类D的指针向上转型成虚基类A的指针的时候,其规则与普通继承一样,也是直接指向派生类内部包含的那个基类/虚基类的首地址;ptr_c 指向的002CF8F8其实就是C对象的首地址,和普通继承的向上转型规则完全一样。
注意:虚基类表并不是多态,虚基类表的目的是为了解决共同基类引起的二义性,可以把它看成是一个间接寻址,寻得是虚基类的址。,下一章要讲的虚函数表才是多态。
-[其它]-:
virtual并不是虚拟的意思:
- 存在 [实质上的,事实上的]
- 共享:虚基类一般会被放在整个类对象内存模型的最后面。实际上放哪不所谓。
- 间接:派生类对象对虚基类对象的访问是间接,先找到派生类的vbptr,
然后用xx = vbptr[0] - vbptr[1]可以算出虚基类地址相对于本类地址的偏移量xx,
然后本类地址 + xx就是虚基类地址。
-[不知道对错]-:
无论是栈上的对象还是堆上的对象,它们的内存模型在编译期就可以确定
栈空间是由编译器管理的,栈上对象的地址是由编译器分配的,所以编译器可以明确知道栈上每一个对象的具体地址(虚拟内存中的地址),根据对象的内存模型,可以直接得到对象中每个成员的具体地址,所以凡是栈上的内存使用与访问,都可以在编译期确定,而且访问时是直接访问。
而堆空间是由系统进行管理的,它不归编译器管理,具体使用的是哪块地址,只有到运行时x向系统具体申请了堆内存,才知道使用的是哪块地址区域,所以编译器无法在编译时就确切知道这些地址,也就是堆上对象的地址在编译期是未知的,所以不能在编译期直接访问这些堆上的地址,而要通过间接手段。一般是通过相对于对象首地址的偏移来访问
- 学习笔记(objective-c)-继承
- Object-C学习笔记三-----继承
- Objective-C学习笔记第四章继承
- 【C++】【学习笔记】【028】多继承
- 【C++】【学习笔记】【029】虚继承
- Objective-C学习笔记九:继承一
- Objective-C学习笔记十:继承二
- 【学习笔记】C++primer plus 13.继承
- 【笔记-C++】 继承
- Objective-C学习笔记---类别(实现多重继承的方法)
- Objective-C学习笔记---类别(实现多重继承的方法)
- Object-C 学习笔记(八)---面相对象(继承)
- Object-c基础编程学习笔记-继承,复合
- 【C++】【学习笔记】【016】this指针和类的继承
- Objective-C 学习笔记 04 - 继承和对象的实例
- 黑马程序员 — Objective-C学习笔记(二):继承
- (C/C++学习笔记)多继承的二义性
- OBJ-C类的继承和派生学习笔记
- Android自动化测试|Espresso-简介
- Microsoft Office SharePoint Server 2016 备份与恢复解决方案(二,图文介绍服务器场备份)
- NDK入门实例,生成.so文件
- JavaScript对象概述 、 常用内置对象一 、 常用内置对象二 、 常用内置对象三
- 网络层(二)ARP协议
- C++学习笔记-继承
- SSH(Struts,Spring,Hibernate)和SSM(SpringMVC,Spring,MyBatis)的区别
- JavaWeb如何尽可能的获取到用户的真实IP
- php数组取不到值 报错还是不报错?
- python之range()函数的应用总结
- MarkDown_Language
- 外部对象概述 、 window 对象 、 document 对象
- java 解决母牛生牛问题 2种方法 2头母牛,3岁后每年生1头,9岁直接不生 15岁直接死亡
- 如何将eclipse设置为炫丽的背景!