纹枰老妖的文章

  • C++利器:可以动态联编的虚拟函数

    一、虚函数的介绍与使用对于C++语言来说,程序在执行类成员函数时(跳转到相应的函数地址),有两种方式,一种是静态联编(static binding),意思就是程序在编译时就知道该函数的地址;另一种是动态联编(dynamic binding),意思是就算代码在编译后,程序还是不知道相应的函数地址,得到程序执行到这个地方,程序才能得到正确的函数地址。那么为什么需要动态联编呢?考虑这样的情况:比方说有一种类叫做宠物,狗、猫、兔子、金鱼、乌龟等都是它的子类,那么当我们定义宠物类的时候,应该定义一个吃饭的功能,因为宠物都应该吃饭。这样的话,我们把程序当成小明,小明要领养什么宠物,只有他才知道(程序执行时才能确定),宠物或许是狗,或许是兔子。可是我们不能给狗喂萝卜,兔子也不吃骨头。所以我们在定义宠物类的时候,就应该把吃饭这个类成员函数,声明称一个虚函数。
    先用代码来做一个简单的例子:
    #include <iostream>using namespace std;class Pet//这个是我们的基类:宠物类{public: char * name; int weight; int age; Pet() {}; //下面这个就是我们的虚函数 virtual void eat() { cout << "错误:我们还不能确定宠物的品种,无法喂食!" << endl; }; ~Pet() {};};class Dog : public Pet{public: Dog(char * s, int w, int a) { name = s; weight = w; age = a; } virtual void eat() //重新定义这个虚函数 { cout << "到点了,请准备好狗粮," << name << "该吃饭了。" << endl; } ~Dog() {};};class Rabbit : public Pet{public: Rabbit(char * s, int w, int a) { name = s; weight = w; age = a; } void eat() { cout << "到点了,请准备好萝卜," << name << "该吃饭了。" << endl; } ~Rabbit() {};};int main(){ int a, b, c; char s[20]; cout << "请输入1或2来创建一个生物(1是狗2是兔);然后分别输" << "入该生物的姓名、重量和年龄,用回车键分割" << endl; cin >> a; cin >> s; cin >> b; cin >> c; Pet * p; if (1 == a) { p = new Dog(s, b, c); cout << "您创建的生物是狗狗,它的名字叫做" << s << ",它今年" << c << "岁了,重大" << b << "公斤!" << "它现在被您收养了。" << endl; } else { p = new Rabbit(s, b, c); cout << "您创建的生物是兔子,它的名字叫做" << s << ",它今年" << c << "岁了,重大" << b << "公斤!" << "它现在被您收养了。" << endl; } cout << "接下来是您与宠物的日常:" << endl; for (int i = 0; i < 10; i++) { if (i < 9) cout << "您与宠物愉快的玩耍" << endl; else p->eat();//到了这里就会调用相应的虚函数,如果子类是狗类,那么调用狗类的吃饭函数,否则是兔类吃饭函数 } return 0;}
    二、虚函数的使用实境虚函数一般用在这种情况下:在菱形或更为复杂的继承体系下,由于同层子类不能互相引用或指针指向,所以一般都是使用基类的指针或引用指向派生类对象,而无需做任何强制类型转换。就像我们的上个梨子一样:由于我们不知道“小明”要养的是什么动物,所以无法确定应该调用怎样的“喂食”函数;然而我们可以创建一个基类的“宠物”指针,然后用它来指向实际中的实例对象,这样“喂食”时,就会调用对应的函数了。
    三、虚函数的工作原理虚函数的原理并不复杂,通常都是这样的:编译器在编译每一种类之后,会搜集出该类的所有函数地址,然后在某块内存(或许是代码区,或许是数据区)中,开辟出一个数组,数组中的每个元素,保存的都是该类的一个函数地址,而这个数组的头结点,就是该类的所谓虚函数表指针。然后在程序实例化该类的一个实际对象时,会偷偷的给这个对象增加一个数据成员,而这个数据成员,是一个指针,保存的就是前面说的数组地址的头结点。还是以我们上面的程序为例:编译器首先为基类宠物类创建一个地址表(虚函数地址数组),然后增添一个隐藏成员“虚函数表指针”,该成员指向这个地址表;然后对派生类比如“狗”类而言,将做同样的事:创建一个新的地址表,里面保存的是“狗类”的所有虚函数地址,然后用一个数据成员指向这个“狗”类的函数表地址;这样当程序使用这个类对象的虚函数时,会根据该虚函数在地址表中的偏移量来调用对应的函数。(但如果我们的派生类没有定义那个喂食函数,那么派生类数组地址中,对应的保存的是基类中的那个喂食函数,读者朋友如果有心,可以自己试一下)
    虚函数的使用注意事项:

    构造函数不能声明为虚函数。这是因为实例一个派生类对象时,先调用该类的构造函数,执行完毕后,再调用基类的某个构造函数。所以派生类不应该重新定义基类的构造函数
    友元函数不能声明为虚函数,这是友元函数不是类成员,而虚函数必须是类成员。如果这种规则会导致您编程时的困惑,可以试着在友元函数中调用虚函数来解决这个问题
    对于一个在继承体系下是基类或中间类的类而言,应该将它的析构函数定位为虚函数。考虑这样一种情况:A是基类,B是继承类,其中B比A多出了一个在堆中new出的字符串内容,假如有以下代码【A * p = b;】,其中那个b是B类的一个实例对象。当我们delete这个p指向的对象时,如果A不是虚析构函数,那么这将会导致内存泄漏,因为在B中的析构函数才会释放字符串占用的内存,所以说应该将A这种的类的析构函数定位为虚函数。【另外还有一种新手常见的错误,考虑这样一种继承体系,B继承自A,B内有虚函数,A内没有,新手犯得错误就是,先实例化一个B类对象,然后再做出一个A类指针指向这个B实例对象,最后delete这个指针时,程序崩溃了。。。其实原因很简单,因为B中有一个虚函数表指针,而A没有,所以当把B的对象“变成”A类型时,不应该包含这个虚函数表指针!如果我们的编译器是把虚函数表指针放在开头部分的话,那么当A指针指向B对象时,会漫过去这个虚表指针,最后当delete这个指针时,不崩溃才怪,因为malloc的返回值和free的传入值不一样嘛。所以如果子类中有虚函数时,我们应该在父类中也弄虚函数,如果确实没啥虚函数好弄的,我们可以把析构函数做成虚函数啊,总结归纳就是:把父类的析构函数声明为虚函数,是一种良好的编程习惯!】
    就像我们前面提过的一样:如果一个基类或中间类定义了某个虚函数,而在下一链中的派生类没有重新定义虚函数,那么对于这个派生类而言,它的实例对象中使用这个虚函数时,将会使用上一链的那个最新版本的虚函数
    所有虚函数的参数表应该是相同的,假如不这样,如下所示:
    class A{...virtual void fun(int i){...};}class B : public A{...virtual void fun(){...};
    那么下面的代码就会是:
    A * p = new B();p->fun(); //合法的代码p->fun(9);//非法的代码
    这是因为在B中,fun被定义为一个不接受任何参数的函数,而且隐藏了A中那个接受int参数的fun版本,而不是将fun生成具有两个函数特征标的重载版本。但是,如果返回值是基类的引用或指针,那么我们可以在重新定义下一链的派生类函数时,将返回值改为该派生类的引用或指针,这称为“返回类型协变”,英文叫做“covariance of return type”,她允许返回类型随类类型的变化而变化。
    如果某个基类或中间类的虚函数有很多重载版本,那么在下一链中的类虚函数定义中,应该要重新定义所有的重载版本。如果只定义一个或很少的虚函数,那么其它那些没有被定义的虚函数都会被隐藏掉,该派生类对象无法使用这些被隐藏掉的版本
    0  留言 2021-02-20 19:49:18
  • 编程学习笔记之vector学习心得

    一、vector介绍在c++中,vector是一个可以存储各种类型对象的集合容器,可以把它看成一种能够动态【吃胖】或【减肥】的数组。vector是一个类模板,使用【vector<T>】格式可以生成一个模板类,考虑到vector容器所应具备的一些特征,vector应该设计成不管传递给vector的模板参数是什么数据类型或类类型,一个vector生成的模板类,其内存大小应该是不变的。为验证我的想法,我在我的机器上测验了一下,使用【sizeof(A)】获得的内存大小为16,其中A为我的一个自定义类类型,不管我把A制造成多大,得到的结果总是 =16,然后我用高大上的开发工具vs13智能显示了一下vector模板类里面的数据成员,发现一共有4个,它们分别是:_Myfirst、_Myend、_Mylast和忘了怎么拼写的“myblabla。。。”,如果我没猜错的话,以上四种类型应该是三个指针和一个整型,它们所代表的意思分别是起始位置、终点位置、当前位置以及当前vector类所占据的内存大小。
    值得一提的是,vector是一种线性顺序式集合容器,虽然它可以动态分配存储空间,但它所占用的内存,是一块连续存储的内存。看到这里也许您有点迷惑,既然是动态分配了,为何还会像静态变量那样使用连续存储内存呢?让我先从设计初衷解释一下,首先前辈们在制造vector时,希望它具有数组一样的高效率,当我们通过下标访问vector中的某个元素时,可以直接用指针偏移量的方式寻址访问,而不要像链表那样弄个循环找到那个位置。其次再从内存分配机制这个角度阐述一下,为求容易理解,我使用例子吧:比如我们用vector生成一个int类容器A,那么机器首先为这个A在堆中开辟了一个大小为100的连续内存区域【其中100是我随便举得例子,A本身的内存是16,A相当于一个标签,贴在一个容器上面,这个容器现在的大小是100】,并把这块区域当成是A的对应容器,然后程序运行到某个位置时,需要往容器里面添加一个元素,但程序发现A容器已经饱和,一旦添加了这个元素,A现在对应的容器会容纳不下,怎么办呢?程序会重新向堆中申请一块更大的连续内存,比如200的大小。如果有,则把原先的100连续内存中的元素逐个拷贝到新内存区域中去,最后在拷贝完成之后,销毁原先的100内存。这个过程是比较麻烦点,但好歹达到了目的——访问方便。
    vector支持很多操作,向尾部添加元素可以用【push_back(T);】,其中T为模板参数的引用;在尾部销毁元素可以用【pop_back()】;在某个位置插入元素可以用【insert(n,T)】,其中n代表位置,T是模板参数的引用;判断容器是否为空可以用【empty()】,如果容器为空,此表达式会返回true,否则返回false;清空容器用clear()等。除此之外,vector还定义了很多操作符重载,比如【!=】、【==】等。
    二、vector的使用要在程序中使用vector,首先我们要包含它所在的库【#include <vector>】,并且还要做好相应的using说明【using std::vector;】,但为了写代码方便,我的说明一向都写成这样【using namespace std;】。做好以上工作后,我们就可以在自己的代码中使用vector生成模板类了。
    接下来说说vector生成模板类最常见的四种初始化情况,比如下面4个重载构造函数:
    // 创建一个int类型的vector容器,但这个容器是空的vector<int> vi; // 创建一个int类型的vector容器,该容器暂有10个int元素,每个元素被赋初始值0vector<int> vi2(10); // 创建一个int类型的vector容器,该容器暂有5个int元素,每个元素被赋值99vector<int> vi3(5,99); // 创建一个int类型vector容器,该容器拷贝了vi2,既该容器具有10个值为0的元素vector<int> vi4(vi2);
    说了这么多,下面让我们写一个简单小程序来巩固一下,该程序使用vector生成一个int型容器,然后由我们自己随便给容器添加若干个元素,接着程序负责求出这些元素的平均值以及找出最大值,最后程序把求出的平均值插入到容器中间,再对容器内所有的元素按照递增顺序排列,代码如下:
    #include <iostream>#include <vector>#include <algorithm> //调用sort排序函数必须包含的头文件using namespace std;int main(){ int value = 0, average = 0, max = 0, num = 0; vector<int>vi; while (cin >> value) vi.push_back(value); //利用循环向容器插入数值元素 cout << "现在容器内所有元素是:"; for (vector<int>::size_type vinum = 0; vinum != vi.size(); vinum++) //使用size_type声明一个变量,并用这个变量当作vi的下标 cout << vi[vinum]<<" "; value = 0; for (vector<int>::const_iterator i = vi.begin(); i != vi.end(); i++) //这次换个方式遍历,i是vector容器的指针 { value += *i; num++; *i > max ? (max = *i) : 0; } cout << value << endl; average = value / num; cout << "容器内平均值是" << average << "\n最大值是" << max << endl; for (num = 0; num != vi.size(); num++); num /= 2; vi.insert(vi.begin() + num, 1, average); //在这里我们把平均值插入到容器中间 sort(vi.begin(), vi.end()); //在这里我们对容器内所有元素进行排序 cout << "经过处理,容器内元素被处理成如下排列:"; for (vector<int>::const_iterator i = vi.begin(); i != vi.end(); i++) //最后我们输出经过处理的容器内所有元素 cout << " " << *i; cout << endl; return 0;}
    0  留言 2021-01-20 09:20:55
eject