CH15 面向对象程序设计
面向对象程序设计是基于三个基本概念的:数据抽象、继承和多态。
第7章介绍了数据抽象的知识,简单来说,C++通过定义自己的数据类型来实现数据抽象。
数据抽象是一种依赖于接口和实现分离的编程技术:类的设计者必须关心类是如何实现的,但使用该类的程序员不必了解这些细节。
封装是一项将低层次的元素组合起来形成新的、高层次实体的技术!函数和类都是封装的具体形式。其中类代表若干成员的聚集。大多数(设计良好的)类类型隐藏了实现该类型的成员。
一个简单的PersonInfo类,包含类成员、成员函数、构造函数
1 #include <string> 2 3 using namespace std; 4 class PersonInfo { 5 public: 6 PersonInfo(); 7 PersonInfo(string& name):strname(name),age(0){} 8 string getName() { return strname; } 9 int getAge() { return age; } 10 11 private: 12 string strname; 13 int age; 14 };
2.访问控制符
程序所有部分均可访问public成员
只有在类内部才能访问类的private成员,即类的private成员对外是不可见的。
3.成员函数可以被重载,上面的PersonInfo类就定义了两个版本的构造函数,被重载的成员函数之间的参数数量和类型不能完全相同,因为重载的成员函数被调用时会根据参数数量和类型进行匹配。重载构造函数的的原理和调用和普通成员函数的重载是一样的。
4.类的声明和定义
一个源文件中一个类只能定义一次。
可以只声明一个类,而先不定义它,一般,这种情况是有关联的类
class StrBlobPtr; class StrBlob { friend class StrBlobPtr; public: StrBlob(); StrBlob(initializer_list<string>il){} //其它成员函数 StrBlobPtr begin(); StrBlobPtr end(); private: //私有成员 };
但是只声明没有定义的类,不能使用,即:不能定义该类的对象,因为生命之后,定义之前,StrBlobPtr是不完全的类,并不知道这个类有哪些成员。不完全类型只能用于定义指向该类型的指针及引用,或者用于声明(而不是定义)使用该类型作为形参类型或返回类型的函数。,如上面的begin()和end()函数。
5.类对象
定义一个类后,就可以定义该类的对象,可以认为对象是类的实体,类通过对象实现一系列操作。定义对象时,会为其分配内存空间,定义类型时并没有进行存储分配。
//两种定义对象的方式都可以,一般是第一种 PersonInfo a; class PersonInfo b;
6.this指针
成员函数有一个隐形形参,指向该类对象的一个隐形形参,即this指针,this的形参是由编译器定义的,与成员的当前对象绑定在一起
1 class Screen { 2 public: 3 typedef string::size_type pos;//此处类为自己定义了一个局部类型,使用typedef为string::size_tyoe 定义了个别名 4 //为类做了一个更好的抽象, 5 Screen() = default; 6 Screen(pos ht, pos wd,char c):height(ht),width(wd),contents(ht*wd,c){}//define constructor and initizalize 7 char get()const { return contents[cursor]; } 8 inline char get(pos ht, pos wd)const;//declare operator member function 9 Screen &move(pos r, pos c);//Note:此处返回的是引用,该引用指向执行操作的那个对象 10 //add some function to set the char in the position pointed by the cursor 11 //和move()一样,set成员的返回值是调用set对象的引用,返回引用的函数是左值的 12 Screen &set(char); 13 Screen &set(pos, pos, char); 14 private: 15 pos cursor = 0; 16 pos height = 0, width = 0; 17 string contents; 18 }; 19 20 //define move() 21 //勒种一些规模较小是函数适合被声明为内联函数,类内部的默认自动是inlien 的。 22 inline Screen &Screen::move(pos r,pos c) 23 {//函数返回调用自己的对象,使用this 来访问该对象 24 pos row = r*width;//cacculate the position of the row 25 cursor = row + c;//move the cursou to the pointed column in the row 26 return *this;//return the object 27 28 } 29 30 char Screen::get(pos r, pos c)const 31 { 32 pos row = r*width; 33 return contents[row + c]; 34 } 35 inline Screen& Screen::set(char c) 36 { 37 contents[cursor] = c; 38 return *this;//返回的是this指向的对象,将this对象作为左值返回 39 } 40 //重载的set()函数 41 inline Screen& Screen::set(pos r, pos col, char ch) 42 { 43 contents[r*width + col] = ch; 44 return *this; 45 } 46 int main() 47 { 48 49 Screen myScreen; 50 51 myScreen.move(4, 0).set('*');//这句相当于下面两句, 52 myScreen.move(4, 0); 53 myScreen.set('*');//, 54 system("pause"); 55 return 0; 56 }
Note: set()成员不能定义为const 的,因为set()返回的是调用set()对象的引用,返回的是左值,而左值必须是可以修改值的,不能是const 的
下面我们定义一个函数从const 成员返回*this
在普通的非const成员函数中,this的类型是一个指向类类型的const指针。可以改变this所指向的值,但不能改变this所保存的地址。在const成员函数中,this的类型是一个指向const类类型对象的const指针。既不能改变this所指向的对象,也不能改变this所保存的地址。
不能从const成员函数返回指向类对象的普通引用。const成 员函数只能返回*this作为一个 const引用。
定义一个display 成员打印出Screen的内容,只是显示Screen的内容不需要改变,因此将const定义为一个const成员,此时this 是一个指向const的指针,*this 就是const对象 ,所以display的返回类型应该是const Screen&,但是,如 如果令display返回一个const引用,则我们不能把display嵌入到一组动作的序列中
此时若用display返回的对象调用set()成员,就会发生错误,,上面说了set成员为什么不能设为常量、
所以下面调用是错误的
myScreen.dipaly(cout).set('#');
Note:一个const成员函数如果以引用方式返回*this,那么它的返回类型将是常量引用。所以定义一个非const的display成员
此处注意,定义了一个小的私有成员,do_play(),看似没简化操作,实则,这样的小程序段是非常有用的,关于这样公共代码使用私有功能函数的建议,课本P248有详细说明
class Screen { public: //其它以定义的public成员 const Screen &dipaly(ostream& os)const { do_display(os); return *this; } Screen& dispaly(ostream&os) { do_display(os); return *this; } private: //其它以定义的prvate成员 void do_display (ostream &os)const { os << contents; } };
7.类作用域
名字查找与类的作用域
类的定义分两步
1.编译成员的声明
2.直到类全部可见后才编译函数体
编译器处理完类中的全部声明后才会处理成员函数的定义
OOP概述
1.OOP核心思想是数据抽象,继承和动态绑定,使用数据抽象可以将类的接口与实现分离,使用继承可以定义相似的类型并对其相似关系建模;使用动态绑定,可以在一定程度上忽略相似类型的区别,以统一方式使用它们的对象
2.定义基类和派生类
其中Quote类是基类,Bulk_quote类是派生类。派生类的构造函数必须重新写过,不能继承。(因为毕竟两个类的类名都不一样,不可能构造函数继承)只继承其他的成员函数和成员变量。
派生类可以覆盖基类的虚函数,但是也可以选择不覆盖(即直接使用父类的函数版本)
每个类控制它自己的成员的初始化过程,派生类初始化成员时,对于从父类继承来的成员必须使用父类的构造函数进行初始化,对于自己新增加的成员,调用自己的构造函数初始化
Note:基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此
1 #include <string> 2 //#include <cstddef> 3 #include <iostream> 4 using namespace std; 5 //基类 6 class Quote { 7 public: 8 //将默认构造函数声明为default,则要求编译器为嗯合成默认构造函数 9 Quote() = default; 10 //重载constructor 11 Quote(const string &book,double sales_price):bookNo(book),price(sales_price){} 12 string isbn()const { return bookNo; } 13 //返回某种书的销售总额 14 //派生类会根据不同情形重写该函数,所以定义为虚函数 15 virtual double net_price(size_t n)const { return n*price; } 16 virtual ~Quote() = default; 17 private: 18 string bookNo; 19 protected: 20 double price = 0.0; 21 }; 22 //派生类,从基类Quote那里继承了isbn()和bookNo和price,重新定义了net_price()函数 23 //新增加了min_qty 和discount 成员 24 class Bulk_quote:public Quote { 25 public: 26 Bulk_quote() = default; 27 Bulk_quote(const string&, double, size_t, double); 28 double net_price(size_t)const override; 29 private: 30 size_t min_qty = 0;//可以使用指定折扣的最低数量 31 double discount = 0.0;//折扣 32 }; 33 Bulk_quote::Bulk_quote(const string& book, double p, size_t qty,double disc): 34 Quote(book,p),min_qty(qty),discount(disc) 35 { 36 //注意初始化列表中对成员bookNo 和price的初始化,不是像之前那样bookNo(book),price(p) 37 //进行初始化,而是调用基类的构造函数进行初始化 38 } 39 double Bulk_quote::net_price(size_t cnt)const 40 { 41 if (cnt >= min_qty) 42 return cnt*price*(1 - discount); 43 else 44 return cnt*price; 45 } 46 //dynamic binding 47 double print_total(ostream &os, const Quote &item, size_t n) 48 { 49 //根据传入的item形参的对象类型调用net_price() 50 double ret = item.net_price(n); 51 os << "ISBN: " << item.isbn() <<'\t'<< "#sold: " << n << " total due: " 52 << ret << endl; 53 return ret; 54 } 55 int main() 56 { 57 Quote item("978-7-121-31092-8", 65);//调用基类构造函数 58 Bulk_quote bulk("978-7-121-31092-8", 65, 20, 0.2);//call the constructor of sub class 59 //bookA.net_price(20); 60 //Quote item;//基类对象 61 //Bulk_quote bulk;//派生类对象 62 63 Quote *p =&item; 64 p->net_price(40);//dynamic binding 65 // Quote base; 66 // Bulk_quote subcls; 67 print_total(cout, item, 20); 68 print_total(cout, bulk, 20); 69 system("pause"); 70 return 0; 71 }
继承与静态成员
如果基类有一个静态成员,那么基类和所有派生类(包括派生类的派生类)都共同拥有这仅有的一个静态成员。并且,对该成员的访问控制与非静态成员的访问控制方式一样,即,如果是private的,则派生类无权访问,,如果是可访问的,则既可以通过基类使用它,也可通过派生类使用它。
class Base { public: static void statmem();//static member }; class Derived :public Base { void f(const Derived&); }; void Derived::f(const Derived& derived_obj) { Base::statmem();//correct:difine in Base Derived::statmem();//correct: Derived derived it from Base derived_obj.statmem();//correct:visit it throgh the object of Derived statmem();//correct: visit it through object of pointed by this }
类型转换与继承
可以将基类的指针或引用绑定到派生类对象上(上面Quote例子中的63,64行):当使用基类引用(或指针)时,实际上我们并不清楚该引用(指针)所绑定的真实类型,该对象可能是基类的对象,也可能是派生类对象。
静态类型在编译时总是已知的,而动态类型的对象直到运行时才可知。
3.虚函数
我们必须为每一个虚函数都提供定义,不管它是否被用到了,因为编译器无法确定到底会使用哪个虚函数。
OOP的核心思想就是多态(polymorphism):具有继承关系的多个类型称为多态类型。
派生类中的虚函数,当在派生类中覆盖了基类的某个虚函数,可以使用virtual关键字指明,即使不这么做,C++也是默认virtual的。
一个派生类的函数如果覆盖了某个继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致。
同样,派生类中虚函数的返回类型也必须与基类函数匹配,当类的虚函数返回类型是类本身的指针或引用时例外。
只有虚函数才能被覆盖
基类的虚函数默认实参最好与派生类的虚函数默认实参一致。
因为如果通过基类的引用或指针调用函数,则将使用基类的默认实参版本,但是该函数的实现是动态绑定的,即可能是基类的函数也可能是派生类的函数。
struct B { virtual void f1(int)const; virtual void f2(); void f3(); }; struct D :B { void f1(int)const;//correct:f1matched with f1 in class B void f2(int)override;//incorrect:theris no f2(int) in class B void f3()override;//incorrect:f3 is not a virtual function void f4()override;//incorrect:there is no function named f4 in class B };
4.抽象基类
纯虚函数:纯虚函数不需要定义,在声明时写上=0即可,智能出现在虚函数声明处的语句。
含有(或未经覆盖而直接继承)纯虚函数的类为抽象类,抽象基类负责定义借口,而后续其它类可以覆盖接口。Note:不能创建抽象基类的对象
如将之前的net_price()定义为虚函数
1 class Dis_quote :public Quote{ 2 public: 3 Dis_quote() = default; 4 Dis_quote(const string& book, double p, size_t qty, double disc) : 5 Quote(book, p), quanty(qty), discount(disc) 6 { 7 8 } 9 10 double net_price(size_t)const = 0;//pure virtual 11 protected: 12 size_t quanty = 0; 13 double discount = 0.0; 14 };
Disc_quote discounted;//incorrect :can not define the object for abstract base class
Disc_quote 的派生类必须定义自己的net_price(),否则仍是抽象基类
现在可以重新定义Bulk_quote,让它继承Disc_quote而不是直接继承Quote
class Bulk_quote :public Disc_quote { Bulk_quote() = default; Bulk_quote(const string& book,double p,size_t qty,double disc): Disc_quote(book,p,qty,disc){} //overeide net_price() in class base double net_price(size_t)const override; };
访问控制与继承
对于访问控制,记住基类的私有成员,不管派生类的继承方式是什么,都是不可见的。派生类可以改变基类中的成员的访问权限,也只限于可访问的成员。
6. 继承中的类作用域
当存在继承关系时,派生类的作用域嵌套在其基类中的作用域内,如果一个名字在派生类的作用域内是无法解析的,则编译器将继续在外层的基类作用域寻找该名字,并且是在编译时进行名字查找;当派生类
的成员名字与基类成员名字冲时,此时定义在内层作用域(派生类)的名字将隐藏定义在外层作用域(基类)的名字
1 #include <iostream> 2 3 4 struct Base { 5 Base():mem(0){} 6 protected: 7 int mem; 8 9 }; 10 struct Derived :Base { 11 Derived(int i) :mem(i) {}//用i初始化mem, 12 //Base::mem 进行默认初始化 13 int get_mem() { return mem;}//返回Derived::mem 14 protected: 15 int mem;//与基类成员名形同,会隐藏基类中的mem 16 }; 17 int main() 18 { 19 Derived d(42); 20 std::cout << d.get_mem() << std::endl;//打印结果42,说明调用get_mem() 21 //对mem的解析结果是定义在Derived中的 22 system("pause"); 23 return 0; 24 25 }