• 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() <<'	'<< "#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 }

                                                                                                                                                                                                                                                                                                                                                        

     

  • 相关阅读:
    C++类中的函数重载
    C++中的友元
    bzoj 2820
    莫比乌斯函数
    bzoj 2440: [中山市选2011]完全平方数
    莫比乌斯反演1
    [转]C++ 指针和引用
    P2756 飞行员配对方案问题
    P2055 [ZJOI2009]假期的宿舍
    P2654 原核生物培养
  • 原文地址:https://www.cnblogs.com/Holly-blog/p/7559733.html
Copyright © 2020-2023  润新知