前言

上一篇博客C++对象模型的那些事儿之一为大家讲解了C++对象模型的一些基本知识,可是C++的继承,多态这些特性如何体现在对象模型上呢?单继承、多重继承和虚继承后内存布局上又有哪些变化呢?多态真正的底层又是如何实现呢?本篇博客就带大家全面理解一下C++对象模型,从而理解上述疑惑。

引例

还是以上篇博客的Animal类说起,假设我们有一个Dog类,它继承了Animal类。程序如下:

class Animal{
public:
    char name[10];//动物名字
    int weight;//体重
    virtual void eat(){
        cout<<"Animal eat"<<endl;
    }

    virtual void sleep(){
        cout<<"Animal sleep"<<endl;
    }
};

class Dog : public Animal{
public:
    int breed;//引入一个breed变量,表示狗的品种
    virtual void eat(){
        cout<<"Dog eat"<<endl;
    }
    virtual void yelp(){//狗吠
        cout<<"Dog yelp"<<endl;
    }
};

还是同样的问题,这些类占有多少内存空间呢?不急,我们在主函数中仍然添加如下代码:

Dog dog;
cout<<sizeof(dog)<<endl;//输出32

还是在这里卖个关子,为什么输出32?

C++的多态性

多态性是指相同对象收到不同消息或不同对象收到相同消息时产生的不同的实现动作,简单点说就是:“一种接口,多种方法”。C++支持两种多态性:

  • 编译时多态:通过函数重载实现
  • 运行时多态:通过虚函数实现

由于函数重载在编译时就确定了,不会影响到对象的内存布局,所以本篇博客不讨论。

通过虚函数实现的多态,由于虚表的存在就影响到了对象的内存布局,所以本篇博客着重讨论此种多态性。

C++以以下三种方式支持多态:

  • 经由一组隐式的转换操作
  • 经由virtual function机制
  • 经由dynamic_cast和typeid运算符

结合引例中定义的两个类,观察下列代码的输出:

int main(){
    //1.经由一组隐式转换
    Animal *animal = new Dog();//将一个anmial指针指向一个dog类对象virtual
    //2.经由virtual function机制
    animal->eat();
    //3.经由dynamic_cast运算符
    if(Dog *dog = dynamic_cast<Dog*>(animal){
        dog->eat()
    }
}

以上测试代码输出结果如下:

Dog eat //由animal->eat()输出
Dog eat //由dog->eat()输出

如果不考虑多态性的话,animal指针调用eat()应该输出Animal eat,下面的dog->eat()还是输出dog eat。

引入多态后,在运行时,编译器会检查animal的真实类型,它指向一个Dog类,于是通过虚函数多态特性输出dog eat,那么编译器在运行时是如何判断该指针指向对象的真实类型呢?又是如何定位到dog::eat函数呢?下面大家带着这些问题继续跟着我一起探索C++对象模型的底层机制。

引入继承后的C++内存布局

众所周知,C++的继承可以有以下几类:

  • 单一继承
  • 多重继承
  • 虚拟继承

下面就从这三个方面来继续探索:

单一继承

以引例中的继承关系来说明,单一继承即子类只从一个父类继承下来。如下图所示:

内存布局

上一篇博文中讲到,Animal类的内存布局由虚表指针和非静态成员变量name[10]和weight组成。

我们知道,在继承关系中,子类将获得父类对象所有的数据成员和成员函数。按照引例中提到的继承关系,dog类通过继承Animal类将拥有Animal类的两个非静态数据成员name[10]和weight,以及虚表指针。dog类自身有一个非静态数据成员breed和一个虚表指针,dog类里面是否直接存放两个虚表指针,还是编译器会将两个虚表合成一个呢?

答案明显是后者,dog类里面有且仅有一个虚表指针,指向自身的虚函数表,为了支持多态,编译器会根据继承关系来更新虚表。下面通过一张图来说明Dog类的内存布局。

如图所示,Dog类的虚表中依次存放这Dog::eat()、Animal::sleep()和Dog::yelp(),这个时候我们再来回顾一下为什么在C++多态性一节中父类指针调用eat()函数会输出Dog eat,很明显吧,虚表中虚函数Animal::eat()被替换成Dog::eat()了。

而且引例中sizeof(dog)=32也很容易理解了,Dog类的内存布局较Animal类多了一个int breed消耗,考虑到八字节对齐,dog的大小就等于32。

测试小结

在好奇心的驱使下,还是忍不住去编译器探个究竟。于是,狂敲了如下代码:

#include <stdio.h>
#include <iostream>
#include <string.h>

using namespace std;

typedef void(*Fun)(void);

class Animal{
public:
    char name[10];//动物名字
    int weight;//体重
    virtual void eat(){
        cout<<"Animal eat"<<endl;
    }

    virtual void sleep(){
        cout<<"Animal sleep"<<endl;
    }
};

class Dog : public Animal{
public:
    int breed;//引入一个breed变量,表示狗的品种
    virtual void eat(){
        cout<<"Dog eat"<<endl;
    }
    virtual void yelp(){
        cout<<"Dog yelp"<<endl;
    }
};

int main(){
    Dog dog;
    cout<<"表虚指针vptr的地址:"<<&dog<<endl;
    cout<<"虚函数表的地址:"<<(long long *)(*((long long*)&dog))<<endl;
    cout<<"测试虚表里的函数输出:"<<endl;
    cout<<"----第一个函数:";
    Fun pfun1 = NULL;
    pfun1 = (Fun)*((long long*)*(long long*)(&dog));
    pfun1();
    cout<<"----第二个函数:";
    Fun pfun2 = NULL;
    pfun2 = (Fun)*((long long*)*(long long*)(&dog)+1);
    pfun2();
    cout<<"----第三个函数:";
    Fun pfun3 = NULL;
    pfun3 = (Fun)*((long long*)*(long long*)(&dog)+2);
    pfun3();
    for (int i = 0; i < 10; ++i)
    {
        cout<<"name["<<i<<"]的地址为:"<<(long long *)&(dog.name[i])<<endl;//name每个参数的地址
    }
    cout<<"weight的地址为:"<<(long long *)&(dog.weight)<<endl;//weight的地址
    cout<<"breed的地址为:"<<(long long *)&(dog.breed)<<endl;
}

上述测试代码输出:

虚表指针vptr的地址:0x7ffd528d2100
虚函数表的地址:0x400f30
测试虚表里的函数输出:
----第一个函数:Dog eat
----第二个函数:Animal sleep
----第三个函数:Dog yelp
name[0]的地址为:0x7ffd528d2108
name[1]的地址为:0x7ffd528d2109
name[2]的地址为:0x7ffd528d210a
name[3]的地址为:0x7ffd528d210b
name[4]的地址为:0x7ffd528d210c
name[5]的地址为:0x7ffd528d210d
name[6]的地址为:0x7ffd528d210e
name[7]的地址为:0x7ffd528d210f
name[8]的地址为:0x7ffd528d2110
name[9]的地址为:0x7ffd528d2111
weight的地址为:0x7ffd528d2114
breed的地址为:0x7ffd528d2118

对比一下,与对象模型中的内存布局一致,Dog类重写了Animal类的eat()函数,所以为了支持多态性,在虚表中将Animal::eat()

替换成Dog::eat()。

注意:除了虚表指针必须在object内存最开始外,其他数据成员都是按照声明顺序依次摆放。

多重继承

多重继承,即一个类直接继承了多个基类,所以我们在引例的基础上新增一个犬科基类

class Canidae{
public:
    int age;
    virtual void eat(){
        cout<<"Canidae eat"<<endl;
    }
    virtual void jump(){
        cout<<"Canidae jump"<<endl;
    }
}

然后再修改一下说Dog类的继承关系

class Dog : public Animal,public Canidae{
public:
    int breed;//引入一个breed变量,表示狗的品种
    virtual void eat(){
        cout<<"Dog eat"<<endl;
    }
    virtual void yelp(){
        cout<<"Dog yelp"<<endl;
    }
};

这样Dog类就有两个直接基类了,如下表所示:

同样的,我们来看看继承两个基类后,Dog类所占的内存大小,运行cout<

Canidae * canidae = new Dog();
canidae->eat();

以上代码输出Dog eat,一样可以体现出多态性。在多重继承下,C++对象模型的内存布局又是以何种方式来支持这些特性呢?

内存布局

从上图的关系可以看出,Dog类将拥有两个父类的属性,其中从Animal类继承了name[10],weight以及一个虚表,从Canidae类继承了age和一张虚表。

如果继续按照单一继承下Dog类只拥有一张虚表的话,就无法区分eat()函数是重写了Animal::eat(),还是Canidae::eat(),可能在Dog类重写了eat()函数的情况下不好理解。假设此时Dog类没有重写eat()函数,那么如果只有一张虚表的话,怎么定位到Animal::eat()?怎么定位到Canidae::eat()?

按照上面的推论,Dog类应该是拥有两张虚表,也就是两个虚指针,分别指向从Animal类继承来的虚表和从Canidae类继承来的虚表。于是可以得出如下的布局图。

测试小结

下面通过一段测试代码来验证上述推论,代码如下:

#include <stdio.h>
#include <iostream>
#include <string.h>

using namespace std;

typedef void(*Fun)(void);

class Animal{
public:
    char name[10];//动物名字
    int weight;//体重
    virtual void eat(){
        cout<<"Animal eat"<<endl;
    }

    virtual void sleep(){
        cout<<"Animal sleep"<<endl;
    }
};

class Canidae{
public:
    int age;
    virtual void eat(){
        cout<<"Canidae eat"<<endl;
    }
    virtual void jump(){
        cout<<"Canidae jump"<<endl;
    }
};

class Dog : public Animal,public Canidae{
public:
    int breed;//引入一个breed变量,表示狗的品种
    virtual void eat(){
        cout<<"Dog eat"<<endl;
    }
    virtual void yelp(){
        cout<<"Dog yelp"<<endl;
    }
};

int main(){
    Dog dog;
    cout<<"从Animal继承来的虚表指针vptr的地址:"<<&dog<<endl;
    cout<<"从Animal继承来的虚表的地址:"<<(long long *)(*((long long*)&dog))<<endl;
    cout<<"测试Animal虚表里的函数输出:"<<endl;
    cout<<"----第一个函数:";
    Fun pfun1 = NULL;
    pfun1 = (Fun)*((long long*)*(long long*)(&dog));
    pfun1();
    cout<<"----第二个函数:";
    Fun pfun2 = NULL;
    pfun2 = (Fun)*((long long*)*(long long*)(&dog)+1);
    pfun2();
    cout<<"----第三个函数:";
    Fun pfun3 = NULL;
    pfun3 = (Fun)*((long long*)*(long long*)(&dog)+2);
    pfun3();
    for (int i = 0; i < 10; ++i)
    {
        cout<<"name["<<i<<"]的地址为:"<<(long long *)&(dog.name[i])<<endl;//name每个参数的地址
    }
    cout<<"weight的地址为:"<<(long long *)&(dog.weight)<<endl;//weight的地址
    cout<<"从Canidae继承来的虚表指针vptr的地址:"<<(long long*)(&dog)+3<<endl;
    cout<<"从Canidae继承来的虚表的地址:"<<(long long *)(*((long long*)&dog+3))<<endl;
    cout<<"测试Canidae虚表里的函数输出:"<<endl;
    cout<<"----第一个函数:";
    Fun pfun4 = (Fun)*((long long*)*((long long*)(&dog)+3));
    pfun4();
    cout<<"----第二个函数:";
    Fun pfun5 = (Fun)*((long long*)*((long long*)(&dog)+3)+1);
    pfun5();
    cout<<"age的地址为:"<<(long long *)&(dog.age)<<endl;
    cout<<"breed的地址为:"<<(long long *)&(dog.breed)<<endl;
}

测试结果:

从Animal继承来的虚表指针vptr的地址:0x7ffd5450e510
从Animal继承来的虚表的地址:0x4011c8
测试Animal虚表里的函数输出:
----第一个函数:Dog eat
----第二个函数:Animal sleep
----第三个函数:Dog yelp
name[0]的地址为:0x7ffd5450e518
name[1]的地址为:0x7ffd5450e519
name[2]的地址为:0x7ffd5450e51a
name[3]的地址为:0x7ffd5450e51b
name[4]的地址为:0x7ffd5450e51c
name[5]的地址为:0x7ffd5450e51d
name[6]的地址为:0x7ffd5450e51e
name[7]的地址为:0x7ffd5450e51f
name[8]的地址为:0x7ffd5450e520
name[9]的地址为:0x7ffd5450e521//因为内存对其填补了两个字节
weight的地址为:0x7ffd5450e524//weight占4个字节
从Canidae继承来的虚表指针vptr的地址:0x7ffd5450e528
从Canidae继承来的虚表的地址:0x4011f0
测试Canidae虚表里的函数输出:
----第一个函数:Dog eat
----第二个函数:Canidae jump
age的地址为:0x7ffd5450e530
breed的地址为:0x7ffd5450e534

从地址上可以看出,在内存布局上与上一小节的内存布局图相同,虚表内存放的函数指针通过测试输出也符合上图。

注意:在多重继承的内存布局中是按照继承顺序依次摆放从基类继承下来的数据成员和虚表指针。

看到这里,应该就能理解为什么sizeof(dog)会输出40了,大家可以按照内存布局推一推。

虚拟继承

再讲解虚继承之前,我先引入一个家畜(Livestock)类,这个类由Animal类派生而来

class Livestock : public Animal{
public:
    int color;//表示家畜的颜色
    virtual void eat(){
        cout<<"Livestock eat"<<endl;
    }
    virtual void watch(){//看门
        cout<<"Livestock watch"<<endl;
    }
}

然后在调整一下上面的犬科(Canidae)类,并由Canidae类和Livestock类派生出WatchDog类,说白了就是一条看门狗。

class Canidae : public Animal{
public:
    int age;
    virtual void eat(){
        cout<<"Canidae eat"<<endl;
    }
    virtual void jump(){
        cout<<"Canidae jump"<<endl;
    }
};
class WatchDog : public Canidae,public Livestock{
public:
    int breed;//引入一个breed变量,表示狗的品种
    virtual void eat(){
        cout<<"Dog eat"<<endl;
    }
    virtual void yelp(){
        cout<<"Dog yelp"<<endl;
    }
};

上述继承关系可以如下图所示:

按照多重继承的内存布局方式的话,WatchDog类的内存布局应该如下图所示:

如果这时候你运行如下程序:

WatchDog watchdog;
watchdog.name[1];

就会出现二义性错误,编译器不知道调用Canidae::name[1]还是Livestock::name[1],虽然你可以通过显示指定watchdog.Canidae::name[1]来调用Canidae::name[1],但是两个name[10]和两个weight完全不符合常理,而且完全没有必要的增大了内存消耗。

内存布局

针对上述问题,C++引入了虚继承的概念,虚继承是指一个指定的基类,在继承体系结构中,将其成员数据实例共享给也从这个基类型直接或间接派生的其它类。虚继承下WatchDog类中只有一个Animal实例。其继承关系如下:

按照虚继承的特点,WatchDog类中只有一份Animal实例,则其内存布局推测为如下图:

测试小结

多重虚拟继承比较复杂,其测试代码如下:

#include <stdio.h>
#include <iostream>
#include <string.h>

using namespace std;

typedef void(*Fun)(void);

class Animal{
public:
    char name[10];//动物名字
    int weight;//体重
    virtual void eat(){
        cout<<"Animal eat"<<endl;
    }

    virtual void sleep(){
        cout<<"Animal sleep"<<endl;
    }
};

class Livestock : public virtual Animal{
public:
    int color;//表示家畜的颜色
    virtual void eat(){
        cout<<"Livestock eat"<<endl;
    }
    virtual void watch(){//看门
        cout<<"Livestock watch"<<endl;
    }
};

class Canidae : public virtual Animal{
public:
    int age;
    virtual void eat(){
        cout<<"Canidae eat"<<endl;
    }
    virtual void jump(){
        cout<<"Canidae jump"<<endl;
    }
};
class WatchDog :  public  Canidae , public  Livestock{
public:
    int breed;//引入一个breed变量,表示狗的品种
    virtual void eat(){
        cout<<"WatchDog eat"<<endl;
    }
    virtual void yelp(){
        cout<<"WatchDog yelp"<<endl;
    }
};

int main(){
    WatchDog watchdog;
    cout<<"从Canidae继承来的虚表指针vptr的地址:"<<&watchdog<<endl;
    cout<<"从Canidae继承来的虚表的地址:"<<(long long *)(*((long long*)&watchdog))<<endl;
    cout<<"测试Canidae虚表里的函数输出:"<<endl;
    cout<<"----第一个函数:";
    Fun pfun1 = NULL;
    pfun1 = (Fun)*((long long*)*(long long*)(&watchdog));
    pfun1();
    cout<<"----第二个函数:";
    Fun pfun2 = NULL;
    pfun2 = (Fun)*((long long*)*(long long*)(&watchdog)+1);
    pfun2();
    cout<<"----第三个函数:";
    Fun pfun3 = NULL;
    pfun3 = (Fun)*((long long*)*(long long*)(&watchdog)+2);
    pfun3();
    cout<<"Canidae::age的地址为:"<<(long long *)&(watchdog.age)<<endl;

    cout<<"从Livestock继承来的虚表指针vptr的地址:"<<(long long*)(&watchdog)+2<<endl;
    cout<<"从Livestock继承来的虚表的地址:"<<(long long *)(*((long long*)&watchdog+2))<<endl;
    cout<<"测试Livestock虚表里的函数输出:"<<endl;
    cout<<"----第一个函数:";
    Fun pfun5 = (Fun)*((long long*)*((long long*)(&watchdog)+2));
    pfun5();
    cout<<"----第二个函数:";
    Fun pfun6 = (Fun)*((long long*)*((long long*)(&watchdog)+2)+1);
    pfun6();
    cout<<"Livestock::color的地址为:"<<(long long *)&(watchdog.color)<<endl;
    cout<<"WatchDog::breed的地址为:"<<(long long *)&(watchdog.breed)<<endl;

    cout<<"从Animal虚继承来的虚表指针vptr的地址:"<<(long long*)(&watchdog)+4<<endl;
    cout<<"从Animal虚继承来的虚表的地址:"<<(long long *)(*((long long*)&watchdog+4))<<endl;
    cout<<"测试Animal虚表里的函数输出:"<<endl;
    cout<<"----第一个函数:";
    Fun pfun8 = (Fun)*((long long*)*((long long*)(&watchdog)+4));
    pfun8();
    cout<<"----第二个函数:";

    Fun pfun7 = (Fun)*((long long*)*((long long*)(&watchdog)+4)+1);
    pfun7();

    for (int i = 0; i < 10; ++i)
    {
        cout<<"name["<<i<<"]的地址为:"<<(long long *)&(watchdog.name[i])<<endl;//name每个参数的地址
    }
    cout<<"weight的地址为:"<<(long long *)&(watchdog.weight)<<endl;//weight的地址
}

以上测试代码输出结果为:

从Canidae继承来的虚表指针vptr的地址:0x7fffebf824e0
从Canidae继承来的虚表的地址:0x4014f8
测试Canidae虚表里的函数输出:
----第一个函数:WatchDog eat
----第二个函数:Canidae jump
----第三个函数:WatchDog yelp
Canidae::age的地址为:0x7fffebf824e8
从Livestock继承来的虚表指针vptr的地址:0x7fffebf824f0
从Livestock继承来的虚表的地址:0x401528
测试Livestock虚表里的函数输出:
----第一个函数:WatchDog eat
----第二个函数:Livestock watch
Livestock::color的地址为:0x7fffebf824f8
WatchDog::breed的地址为:0x7fffebf824fc
从Animal虚继承来的虚表指针vptr的地址:0x7fffebf82500
从Animal虚继承来的虚表的地址:0x401558
测试Animal虚表里的函数输出:
----第一个函数:watchdog eat
----第二个函数:Animal sleep
name[0]的地址为:0x7fffebf82508
name[1]的地址为:0x7fffebf82509
name[2]的地址为:0x7fffebf8250a
name[3]的地址为:0x7fffebf8250b
name[4]的地址为:0x7fffebf8250c
name[5]的地址为:0x7fffebf8250d
name[6]的地址为:0x7fffebf8250e
name[7]的地址为:0x7fffebf8250f
name[8]的地址为:0x7fffebf82510
name[9]的地址为:0x7fffebf82511
weight的地址为:0x7fffebf82514

从上述测试结果中可以看出,watchdog的内存布局中依次摆放的是:

  • 从Canidae类继承来的虚表和age
  • 从Livestock类继承来的虚表和color
  • watchdog自身的bread
  • 从Animal超类继承来的虚表、name[10]和weight

最后,在运行一下sizeof(watchdog) = 56,与我们的内存布局一样。

结束语

本篇博客对引入继承关系后类的内存布局做了相对全面的分析和测试,从内存地址上一步一步推算和验证了常见继承关系下的类内存布局。不过,还有些许不完善的地方,比如单一虚拟继承这种情况就没有分析到,不过,一般虚拟继承都是将超类放在类的最后面,有且仅有一份;再比如,由于编译器的不同可能会导致内存布局上微小的差异性,这方面可以参考陈浩专栏的C++ 对象的内存布局系列文章。

About Me

由于本人也是初学,在写作过程中,难免有错误的地方,读者如果发现,请在下面留言指出。

最后,如有疑惑或需要讨论的地方,可以联系我,联系方式见我的个人博客about页面,地址:About Me

另外,本人的第一本gitbook书已整理完,关于leetcode刷题题解的,点此进入One day One Leetcode

欢迎持续关注!Thx!

C++对象模型的那些事儿之二:对象模型(下)的更多相关文章

  1. C++对象模型的那些事儿之一:对象模型(上)

    前言 很早以前就听人推荐了<深入理解C++对象模型>这本书,从年初买来到现在也只是偶尔翻了翻,总觉得晦涩难懂,放在实验室上吃灰吃了好久.近期由于找工作对C++的知识做了一个全面系统的学习, ...

  2. C++对象模型的那些事儿之五:NRV优化和初始化列表

    前言 在C++对象模型的那些事儿之四:拷贝构造函数中提到如果将一个对象作为函数参数或者返回值的时候,会调用拷贝构造函数,编译器是如何处理这些步骤,又会对其做哪些优化呢?本篇博客就为他家介绍一个编译器的 ...

  3. C++对象模型的那些事儿之四:拷贝构造函数

    前言 对于一个没有实例化的空类,编译器不会给它默认生成任何函数,当实例化一个空类后,编译器会根据需要生成相应的函数.这类函数包括一下几个: 构造函数 拷贝构造函数 析构函数 赋值运算符 在上一篇博文C ...

  4. C++对象模型的那些事儿之三:默认构造函数

    前言 继前两篇总结了C++对象模型及其内存布局后,我们继续来探索一下C++对象的默认构造函数.对于C++的初学者来说,有如下两个误解: 任何class如果没有定义default constructor ...

  5. Android实训案例(二)——Android下的CMD命令之关机重启以及重启recovery

    Android实训案例(二)--Android下的CMD命令之关机重启以及重启recovery Android刚兴起的时候,着实让一些小众软件火了一把,切水果,Tom猫,吹裙子就是其中的代表,当然还有 ...

  6. IDEA环境下GIT操作浅析之二-idea下分支操作相关命令

    上次写到<idea下仓库初始化与文件提交涉及到的基本命令>,今天我们继续写IDEA环境下GIT操作之二--idea下分支操作相关命令以及分支创建与合并. 1.idea 下分支操作相关命令 ...

  7. 8-13 canvas专题-阶段练习二(下)

    8-13 canvas专题-阶段练习二(下) <!DOCTYPE html> <html lang="zh-cn"> <head> <me ...

  8. 联盛德 HLK-W806 (二): Win10下的开发环境配置, 编译和烧录说明

    目录 联盛德 HLK-W806 (一): Ubuntu20.04下的开发环境配置, 编译和烧录说明 联盛德 HLK-W806 (二): Win10下的开发环境配置, 编译和烧录说明 联盛德 HLK-W ...

  9. 【C++对象模型】构造函数语意学之二 拷贝构造函数

    关于默认拷贝构造函数,有一点和默认构造函数类似,就是编译器只有在[需要的时候]才去合成默认的拷贝构造函数. 在什么时候才是[需要的时候]呢? 也就是类不展现[bitwise copy semantic ...

随机推荐

  1. NOIP2014-6-14模拟赛

    Problem 1 抓牛(catchcow.cpp/c/pas) [题目描述] 农夫约翰被通知,他的一只奶牛逃逸了!所以他决定,马上出发,尽快把那只奶牛抓回来. 他们都站在数轴上.约翰在N(O≤N≤1 ...

  2. linux x86内核中的分页机制

    Linux采用了通用的四级分页机制,所谓通用就是指Linux使用这种分页机制管理所有架构的分页模型,即便某些架构并不支持四级分页.对于常见的x86架构,如果系统是32位,二级分页模型就可满足系统需求: ...

  3. Python中模块之collections系列

    collection系列功能介绍 1. 常用的集中类 1. Counter(计数器) 计数器的常用方法如下: 创建一个字典计数器 格式:collections.Counter(obj) 例如:prin ...

  4. TensorFlow + Keras 实战 YOLO v3 目标检测图文并茂教程

    运行步骤 1.从 YOLO 官网下载 YOLOv3 权重 wget https://pjreddie.com/media/files/yolov3.weights 下载过程如图: 2.转换 Darkn ...

  5. Spring AOP @Around @Before @After 区别

    此段小代码演示了spring aop中@Around @Before @After三个注解的区别@Before是在所拦截方法执行之前执行一段逻辑.@After 是在所拦截方法执行之后执行一段逻辑.@A ...

  6. Linux的发行版,不同发行版之间的联系和区别

    Linux 主要作为Linux发行版(通常被称为"distro")的一部分而使用.这些发行版由个人,松散组织的团队,以及商业机构和志愿者组织编写.它们通常包括了其他的系统软件和应用 ...

  7. CF | Alyona and Mex

    Someone gave Alyona an array containing n positive integers a1, a2, ..., an. In one operation, Alyon ...

  8. ACM Misha and Changing Handles

    Misha hacked the Codeforces site. Then he decided to let all the users change their handles. A user ...

  9. Latex:TexStudio的使用

    http://blog.csdn.net/pipisorry/article/details/54565608 Texsdudio 快捷键 The keyboard shortcuts can be ...

  10. 【java集合系列】--- LinkedList

    开篇前言--LinkedList中的基本用法 在前面的博文中,小编介绍List接口中的ArrayList集合,List这个接口,有两个实现类,一个就是ArrayList另一个是LinkedList(链 ...