虚函数与动态绑定的那点事

释放双眼,带上耳机,听听看~!

面向对象编程的概念

1)多态:简单理解就是多种形态,通过继承而相关联的类型,特别在运行的情况下,对象可能是基类也可能派生类类型

1)继承:能够对类型之间的关系建模,共享公共的东西,仅仅特化本质上不同的东西。

定义为virtual的函数是基类期待派生类重新定义的,基类不希望派生类继承的则定义为非虚函数,这样类就有虚函数与非虚函数之分

2)动态绑定:使程序使用继承层次中任意类型的对象,无需关心具体的类型。

在C++,通过引用或者指针调用虚函数,发生动态绑定,引用或指针既可以指向基类对象也可以指向派生类对象,这些都是在运行时确定的,被调用的函数是引用或指针所指对象的实际类型所定义的。

如下class item_base中定义的成员:


1
2
3
1   string book() const { return ISBN; }; //希望派生类继承的
2   virtual double net_prices(size_t n) const; //virtual希望派生类重定义的
3

定义基类和派生类:

基类:item_base

如下基类代码:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
1class item_base
2{
3public:
4   //virtual item_base(void); 构造函数不允许成为virtual
5   item_base(const string &book = "", double sale_price = 0);
6   virtual ~item_base(void);  //类成员可以是虚函数,非类成员不可以
7      
8   string book() const { return ISBN; }; //希望派生类继承的
9   virtual double net_prices(size_t n=1) const; //virtual希望派生类重定义的
10  virtual item_base* get_class(){ return this;};
11
12  virtual void print(ostream& os) { os << "base class:" << ISBN << endl; };
13  int test() const;
14
15  static void statmem() {};
16
17private:
18  string ISBN;
19
20protected:  //可以被派生类访问,但不能该类的普通用户访问
21  double price;
22};
23void print_total(ostream& os, const item_base&item, size_t n);
24//virtual int fun(); //error 只有类成员可以是虚函数
25

如下,构造函数和虚函数net_price的实现,其他暂略:


1
2
3
4
5
6
7
8
9
10
1item_base::item_base(const string &book, double sale_price)
2   : ISBN(book), price(sale_price)
3{
4}
5double item_base::net_prices(size_t n) const
6{
7   cout << __FUNCTION__ << endl;
8   return n * price;
9};
10

1
2
11)保留字virtual的目的是启用动态绑定,除了构造函数之外,任意非static成员函数都可以是虚函数。
2

2)保留字virtual只能出现在类成员函数声明处,不能用在类定义体外部的函数定义上。

3)访问控制,在普通类中经常看到public和private,这两个标号的作用如下:

public:类和用户都能访问类的public成员

private:只能由基类的成员和友元访问,不能被类的用户访问

这样一来派生类是不能访问基类的private成员的,但是protected成员是可以的:

protected:可以被派生类访问但不能被该类的普通用户访问。如上定义的price成员

4)派生类对象只能通过派生类对象访问其基类的protected成员,却没有基类的protected的访问权,如下:


1
2
3
4
5
6
7
8
1void bulk_item::memfcn(const bulk_item &d, const item_base &b)
2{
3   double ret = price;
4   ret = d.price;  //注意这里为类中访问
5   //ret = b.price; //error 不能访问基类的protect成员
6   //b.ISBN; //无论基类还是继承类对象都不能访问private memmber
7}
8

1
2
1注意:用户只能访问public成员,类成员和友元既可以访问public也可以访问private成员。
2

派生类:bulk_item

派生类的定义形式如下:

class derived_class: acess_label base_class{ …};,如下派生类代码:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
1class bulk_item: public item_base
2{
3public:
4   bulk_item();
5   ~bulk_item();
6   bulk_item(const string& str, double sales_price, size_t qty = 0, double disc_rate = 0);
7   string book() const { return ""; }; //
8  
9   /*virtual*/ double net_prices(size_t n=2) const; //虚函数不加virtual也是虚函数,永远是
10  //virtual item_base* get_class() { return this; };
11  virtual bulk_item* get_class() { return this; }; //虚函数的继承类成员可以返回派生类类型的指针或者引用
12 
13  void memfcn(const bulk_item &d, const item_base &b);
14
15  virtual void print(ostream& os) { item_base::print(os); os <<"derived print"<< endl; };
16  void print() { print(cout); };
17
18  int test() const;
19
20  void CallStatic(const bulk_item &);
21private:
22  size_t min_qty;
23  double discount;
24};
25

1
2
1派生类有增加了两个成员( size_t min_qty; double discount;),加上之前的两个( string ISBN;  double price;),现在派生类拥有四个成员变量;当然函数凡是非虚函数的都被继承下来了,虚函数可能被重新定义,也可能没有,没有的话就调用基类的。  
2

如下派生类的有些函数的实现:


1
2
3
4
5
6
7
8
9
10
11
1bulk_item::bulk_item()
2   :min_qty(0),discount(0)
3{
4}
5double bulk_item::net_prices(size_t nNum) const
6{
7   cout << __FUNCTION__ << endl;
8
9   return (nNum >= min_qty)? nNum*(1-discount)*price: nNum * price;
10}
11

注意:

1)派生类中的虚函数声明必须与基类的定义方式完全匹配。但有一个例外,返回对基类型的引用或者指针,如下:


1
2
3
4
5
1//base class
2virtual item_base* get_class(){ return this;};
3//derived class
4virtual bulk_item* get_class() { return this; }; //虚函数的继承类成员可以返回派生类类型的指针或者引用
5

1
2
1如上第一个虚函数,其实返回的是item_base \* ,派生类中的虚函数返回的是bulk_item\*  
2

2)一旦函数在基类中声明为虚函数,它就一辈子为虚函数,派生类是无法改变它的出身的

3)派生类重定义虚函数时,可以使用虚函数virtual保留字也可以不使用,如下:


1
2
1/*virtual*/ double net_prices(size_t n=2) const; //虚函数不加virtual也是虚函数,永远是
2

1
2
14)派生中可以使用基类的成员,要不然共享的东西也没有意义
2

5)基类必须是已定义的,如果只是前向声明,那么就无法使用他作为基类类定义继承类

6)派生类也可以作为基类,无穷继承下去都是可以的,子子孙孙无穷尽也。

7)如果需要一个派生类,则声明包含类名但不含派生列表


1
2
3
1//class bulk_test1: public item_base; //error
2class bulk_test2; //ok
3

虚函数的动态绑定:

1. 满足条件

1)只有指定为虚函数的成员函数才能进行动态绑定

2)必须通过基类的引用或者指针进行函数调用

即: 动态绑定 = 虚函数+指针/引用传递

2. 虚函数需要注意的地方:

1)从派生类到基类的转换,如下调用


1
2
3
4
5
6
7
8
9
10
1//net_price使用默认实参,将由调用该函数的类型定义,与对象的动态类型无关
2void print_total(ostream& os, const item_base& item, size_t n)
3{
4   os << "ISBN:" << item.book() << "\tNum sold:" << n  //总是调用基类的book函数,即使继承类也定义了
5       << "\tTotal price:" << item.net_prices() << endl;  //先调用的虚函数,然后才是普通的成员函数,与顺序无关
6
7// os << item.book() << item.test() << endl;
8}   
9
10

1
2
1如下调用:
2

1
2
3
4
5
6
1   item_base base("hello");
2   bulk_item derived;
3
4   print_total(cout, base, 3);
5   print_total(cout, derived, 4);
6

1
2
1这里可以使用基类类型的指针或者引用来引用派生类对象,所以使用基类类型的引用或者指针时,不知道指针或者引用所绑定的对象的类型,将派生类对象当做基类对象时安全的,因为每个派生类对象都拥有基类子对象,而且派生类也继承基类的操作,任何可以在基类对象上执行的操作也可以通过派生类对象使用。  
2

2)运行时确定virtual虚函数的调用

将基类类型的引用或者指针绑定到派生类对象对及基对象没有影响,基对象本身也没有改变,仍为派生类对象。对象的实际类型可能不同于该对象的引用或者指针的静态类型。

如上,第一个item形参绑定到item_base对象上,因此调用基类的net_price,第二个形参绑定到bulk_item,所以调用派生类的net_price函数。

注意:引用和指针的动态类型与静态类型可以不同。如果调用非虚函数,无论实际对象时什么类型,都会调用基类类型所定义的函数。只有通过引用或者指针调用的虚函数,才会在运行时确定对象的实际类型。

3)虚函数的覆盖机制

在默认情况下,继承类调用虚函数,一般是使用继承类的版本,如何使用基类版本呢??如下调用就可以使用基类版本:


1
2
3
4
5
1   item_base base("hello");
2   bulk_item derived;
3   derived.item_base::test();  //调用基类版本
4   derived.test();   //调用继承类
5

如果派生类需要调用基类中定义的虚函数,就需要使用覆盖虚函数机制。派生类虚函数调用基类版本时,必须显式使用作用域操作符。

4) 虚函数的形参使用默认实参

如果一个调用省略了具体有默认默认值的实参,则所用的值由调用该函数的类型定义,与对象的动态类型无关。如下调用


1
2
3
4
1   base.net_prices(); //调用基类版本
2   derived.net_prices(); //调用继承类版本
3   derived.item_base::net_prices(); //调用基类版本
4

这里还不出端倪,再看下面的调用


1
2
3
1   print_total(cout, base, 3);
2   print_total(cout, derived, 4);
3

上面调用中我传入了实参,但是为了测试默认实参的作用,我修改了下面函数的调用方式,这里面传过来的实参就没有用了,默认使用函数声明的实参,使用如下:


1
2
3
4
5
1//base class
2   virtual double net_prices(size_t n=1) const; //virtual希望派生类重定义的
3//derived class
4/*virtual*/ double net_prices(size_t n=2) const; //虚函数不加virtual也是虚函数
5

1
2
3
4
5
6
7
1//net_price使用默认实参,将由调用该函数的类型定义,与对象的动态类型无关
2void print_total(ostream& os, const item_base& item, size_t n)
3{
4   os << "ISBN:" << item.book() << "\tNum sold:" << n  //总是调用基类的book函数,即使继承类也定义了
5       << "\tTotal price:" << item.net_prices() << endl;  //先调用的虚函数,然后才是普通的成员函数
6}
7

另外一个需要注意的就是我在调试这个非类成员函数的时候,发现默认先是进入虚函数net_price,然后才是非虚函数book,这个是<<操作符先调用右边的表达式或者函数!这里两次调用虽然进入的函数不同,一个是基类版本,一个是继承类版本,但是传入的参数都是1,也就是基类的默认实参,看出端倪没?!
通俗点讲就是谁离他最近他就使用谁,他不需要关注间接人后面的真家伙,
但是动态绑定正好跟这不一样或者说是相反的,动态绑定要知道真正的家伙是谁!!。

5)函数调用的延伸

看如下的定义


1
2
3
4
5
6
7
8
1   base.print(cout);
2   derived.print(cout);  //会导致无穷递归自己 ,这里进行了修改
3   derived.print(); //会调用继承类的print然后无穷递归
4   item_base *bp1 = &amp;base;
5   item_base &amp;br1 = base;
6   item_base *bp2 = &amp;derived;  //动态绑定 与形参使用基类类类型传递是一致的
7   item_base &amp;br2 = derived;
8

这里print的定义在类中已经声明,主要说一下,在继承类中有实参的虚函数继承中,如果不指定基类就会导致无穷递归,也就说使用了如下的方式:


1
2
3
1virtual void print(ostream&amp; os) { print(os); os &lt;&lt;&quot;derived print&quot;&lt;&lt; endl; };
2
3

1
2
1无实参的非虚函数调用的是继承类中的虚函数版本,因为当前this指针指向的是bulk_item\*。  
2

调用方式如下:


1
2
3
4
5
1   bp1-&gt;print(cout); //调用基类
2   br1.print(cout); //调用基类
3   bp2-&gt;print(cout); //调用继承类
4   br2.print(cout); //调用继承类
5

1
2
1这里可以看出引用于指针并无任何区别,关键的是对象本身是什么,这就需要动态运行期间才能确定,由于bp2 br2默认是指向或者绑定于继承类对象,所以在运行期间使用的是继承类版本。  
2

给TA打赏
共{{data.count}}人
人已打赏
安全运维

MySQL到MongoDB的数据同步方法!

2021-12-11 11:36:11

安全运维

Ubuntu上NFS的安装配置

2021-12-19 17:36:11

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索