面向对象编程的概念
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 = &base;
5 item_base &br1 = base;
6 item_base *bp2 = &derived; //动态绑定 与形参使用基类类类型传递是一致的
7 item_base &br2 = derived;
8
这里print的定义在类中已经声明,主要说一下,在继承类中有实参的虚函数继承中,如果不指定基类就会导致无穷递归,也就说使用了如下的方式:
1
2
3 1virtual void print(ostream& os) { print(os); os <<"derived print"<< endl; };
2
3
1
2 1无实参的非虚函数调用的是继承类中的虚函数版本,因为当前this指针指向的是bulk_item\*。
2
调用方式如下:
1
2
3
4
5 1 bp1->print(cout); //调用基类
2 br1.print(cout); //调用基类
3 bp2->print(cout); //调用继承类
4 br2.print(cout); //调用继承类
5
1
2 1这里可以看出引用于指针并无任何区别,关键的是对象本身是什么,这就需要动态运行期间才能确定,由于bp2 br2默认是指向或者绑定于继承类对象,所以在运行期间使用的是继承类版本。
2