C++ 类的继承封装和多态

C++ 特性

C++ 三大特性,封装继承多态。我们先实现一个Quote作为基类

class Quote
{
public:
    Quote() = default;
    Quote(const std::string &book, double sales_price)
    {
        price = sales_price;
        bookNo = book;
    }
    std::string isbn() const
    {
        return bookNo;
    }

    virtual double net_price(std::size_t n) const
    {
        cout << "this is Quote net_price" << endl;
        return n * price;
    }
    static void PrintHello()
    {
        cout << "hello world" << endl;
    }

    Quote(const Quote &quote) : bookNo(quote.bookNo), price(quote.price) {}

    void printMem()
    {
        cout << "price is " << price << " bookNo is " << bookNo << endl;
    }

    virtual ~Quote()
    {
        cout << "this is Quote destruct" << endl;
    }

    // final 阻止其他继承Quote的类重写f3函数
    virtual void f3() final {}

private:
    std::string bookNo;

protected:
    double price = 0.0;
};

net_price是一个虚函数,实现了基类的计算规则。同时我们实现了一个虚函数f3,但是f3末尾用final标识了,表示继承Quote的子类不能重写f3函数。
我们实现子类

class BulkQuote : public Quote
{
public:
    BulkQuote() = default;
    BulkQuote(const std::string &book, double p, std::size_t qty, double disc) : Quote(book, p), min_qty(qty), discount(disc)
    {
    }

    // override 是C11提供的继承关系检测工具,检测函数类型是否匹配,是否为虚函数等。
    double net_price(std::size_t) const override;
    // void f3() {}

private:
    //打折后最多买多少
    std::size_t min_qty = 0;
    //折扣额度
    double discount = 0.0;
};

子类无法重写f3所以注释了。子类BulkQuote重写了net_price,该函数后边用了overide关键字,C11规定写了override关键字的函数必须符合基类的规则,包括函数参数类型相同,返回值相同,函数名一致等。
基类Quote有一个静态函数PrintHello,子类继承Quote也将PrintHello继承过来。
可以通过如下方式调用

void use_base_static()
{
    Quote::PrintHello();
    BulkQuote::PrintHello();
}

可以将子类赋值给基类或者将子类对象传给基类的构造函数,这么做的结果是基类构造时只拷贝子类的基类部分

void use_derive_to_base()
{
    BulkQuote bulkquote(string("Live"), 1.2, 100, 0.8);
    //子类传给基类构造函数,或者子类赋值给基类
    //就会调用基类构造函数,只构造基类部分。
    Quote quote(bulkquote);
    quote.printMem();
    quote = bulkquote;
    quote.printMem();
}

当我们将一个子类对象传递给一个基类引用,或者将一个子类对象的指针传递给一个基类指针,通过基类的指针或引用调用虚函数,会动态调用子类对象的虚函数版本,这种特性叫做多态。我们先实现一个全局函数

void print_total(ostream &os, const Quote &quote, std::size_t n)
{
    os << quote.net_price(n) << endl;
}

多态特性会让编译器根据动态类型绑定虚函数调用的版本,所谓动态类型就是运行时才确定的类型。

void use_derive_param()
{
    BulkQuote bulkquote(string("Live"), 1.2, 100, 0.8);
    Quote quote(string("Quote"), 1.2);
    print_total(cout, quote, 100);
    print_total(cout, bulkquote, 100);
}

上面的程序会根据传给print_total具体的实参类型调用各自的虚函数net_price。

纯虚类

如果一个类只包含纯虚函数,不包含成员变量,则该类为纯虚类。所谓纯虚函数就是只有声明,函数体为=0的形式。

//纯虚类
class VirtualBase
{
public:
    VirtualBase() = default;
    virtual void mem() = 0;
    virtual void test() = 0;
};

纯虚类类似于Go语言的interface,当我们继承纯虚类后一定要重写其所有的纯虚函数。

class DeriveFromBase : public VirtualBase
{
    virtual void mem()
    {
    }
    virtual void test() {}
};

封装性

子类只可以访问基类的protected和public成员,不能访问private成员。子类的友元函数可以访问子类的私有变量,公有变量以及受保护的变量,当时不能访问基类的私有变量和protected变量。

// protected
class ProBase
{
public:
    ProBase() = default;
    ProBase(int n) : prot_mem(n) {}
    void mem_func()
    {
        cout << "this is ProBase mem_func" << endl;
    }

protected:
    int prot_mem;

private:
    int priv_mem;
};

ProBase包括一个私有变量priv_mem和一个受保护变量prot_mem。我们定义子类继承它

class Sneaky : public ProBase
{
public:
    Sneaky() = default;
    Sneaky(int n) : ProBase(1024), prot_mem(n) {}
    //子类可以使用基类的public和protected成员
    void UsePro()
    {
        cout << prot_mem << endl;
    }

    //子类无法使用基类的private成员。
    // void UsePriv()
    // {
    //     cout << priv_mem << endl;
    // }
    friend void clobber(Sneaky &);
    friend void clobber(ProBase &);

    void GetMem()
    {
        cout << "this is ProBase prot_mem: " << ProBase::prot_mem << endl;
        cout << "this is Sneaky prot_mem: " << prot_mem << endl;
    }

    void mem_func(int n)
    {
        cout << "this is Sneaky mem_func" << endl;
    }

private:
    int self_mem;
    int prot_mem;
};

通过继承Sneaky拥有了基类ProBase的私有变量priv_mem和受保护变量prot_mem。又定义了自己的私有变量self_mem和prot_mem。
可以看到即使在Sneaky的类声明中UsePriv这个函数里也无权访问基类的私有变量priv_mem。我们在如下函数测试

void use_probase()
{
    Sneaky sk(11);
    // sk.prot_mem;
    sk.GetMem();
    //调用子类的mem_func(int n)
    sk.mem_func(100);
    ProBase pb;
    //调用基类的mem_func()
    pb.mem_func();
    //基类的mem_func()被覆盖了
    //sk.mem_func();
    //想使用基类的mem_func()需要添加基类作用域
    sk.ProBase::mem_func();
}

在类的声明之外,通过对象的方式无法直接使用sk的私有变量prot_mem。因为子类实现了mem_func(int n)版本,所以把基类的mem_func(void)覆盖了,想调用基类版本的mem_func()需要通过sk.ProBase::mem_func()显示调用基类版本。
接下来我们实现Sneaky的两个友元函数

void clobber(Sneaky &s)
{
    s.prot_mem = 100;
    s.self_mem = 1000;
}
//子类友元无法访问基类受保护成员
void clobber(ProBase &b)
{
    // b.prot_mem = 10;
}

子类的友元函数无法访问基类的私有成员和保护成员。

重写和隐藏

子类继承基类,重新实现基类的虚函数就叫做重写,重写要求必须和基类的虚函数完全匹配,包括参数类型返回值等。
对于基类的非虚函数,子类实现了同名的函数,只要名字相同,即使参数不同,也可以覆盖基类函数,叫做隐藏。

class VBase
{
public:
    virtual int fcn()
    {
        cout << "this is VBase fcn()" << endl;
    }
};

class VD1 : public VBase
{
public:
    // VD1自己定义的fcn(int),因为和基类VBase的fcn参数不同
    //但是VD1也继承了VBase  fcn()这个版本
    //隐藏了基类的fcn()
    int fcn(int)
    {
        cout << "this is VD1 fcn(int)" << endl;
    }

    // VD1自己新定义的虚函数
    virtual void f2()
    {
        cout << "this is VD1 f2()" << endl;
    }
};

class VD2 : public VD1
{
public:
    //隐藏了VD1版本的fcn(int),因为VD1中fcn(int)不是虚函数
    int fcn(int)
    {
        cout << "this is VD2 fcn(int)" << endl;
    }

    //重写,因为VD1从VBase中继承了虚函数fcn()
    int fcn()
    {
        cout << "this is VD2 fcn()" << endl;
    }

    //重写了VD1的虚函数

    void f2()
    {
        cout << "this is VD2 f2()" << endl;
    }
};

如下函数展示了覆盖和重写等情况时调用规则

void use_hiddenbase()
{
    VD1 vd1;
    //调用基类VBase版本
    vd1.VBase::fcn();
    //调用VD1版本
    vd1.fcn(100);

    VD2 vd2;
    VBase *pvb = &vd1;
    //会调用基类的VBase::fcn()
    pvb->fcn();
    VBase *pvb2 = &vd2;
    //多态调用VD2::fcn()
    pvb2->fcn();

    VD1 *pvd1 = &vd2;
    //调用VD2版本的f2()
    pvd1->f2();
}

类的继承和多态总结

1 派生类向基类转换只在指针或引用时才生效
2 不存在默认的基类向子类转换,但是如果确认转换安全可以通过static_cast来转换。
3 类不想被继承,可以在类名后添加final关键字
4 如果子类无权访问基类构造函数,则无法实现子类对象向基类对象的转换。
5 子类对象可以向基类对象转换,默认只将子类对象中基类的成员赋值给基类对象。
6 多态就是将子类对象的指针赋值给基类对象的指针,通过调用基类的虚函数,实现动态绑定, 运行时调用了子类的虚函数。

7 final 声明的虚函数会阻止继承该类的类重写该函数
8 override 要求编译器检测重写的函数是否符合规则,是否为虚函数,是否为类型相同。
9 继承纯虚类,一定要实现它的所有纯虚方法,否则该类无法使用。
10 子类可以使用基类的public和protected成员,子类无法使用基类的private成员
11 proteced 和private成员不可被对象的方式访问。
12 子类的友元函数可以访问子类的私有变量和受保护变量,但是不能访问基类的受保护变量。
13 子类的友元函数可以访问子类自己定义的私有变量,但是不能访问从基类继承而来的私有变量。
14 子类和基类有相同名字的成员或者非虚函数非静态的成员函数,在使用的时候默认使用子类的,
如果想使用基类的需要加上基类名字的作用域。
15 如果子类实现的函数和基类的虚函数同名,但是参数类型不同,就不是重写而是隐藏,重写要求子类的函数和
基类的虚函数类型,名称完全一致。
16 针对一个普通的非虚函数的成员函数,子类实现了一个同名的函数,就是覆盖,会隐藏基类的同名函数
17 重载是对于一个类来讲,实现了多个同名函数,他们的参数不同。

热门评论

热门文章

  1. vscode搭建windows C++开发环境

    喜欢(596) 浏览(82708)
  2. 聊天项目(28) 分布式服务通知好友申请

    喜欢(507) 浏览(6061)
  3. 使用hexo搭建个人博客

    喜欢(533) 浏览(11805)
  4. Linux环境搭建和编码

    喜欢(594) 浏览(13445)
  5. Qt环境搭建

    喜欢(517) 浏览(24502)

最新评论

  1. 无锁并发队列 TenThousandOne:_head  和 _tail  替换为原子变量。那里pop的逻辑,val = _data[h] 可以移到循环外面吗
  2. 堆排序 secondtonone1:堆排序非常实用,定时器就是这个原理制作的。
  3. 网络编程学习方法和图书推荐 Corleone:啥程度可以找工作
  4. 类和对象 陈宇航:支持!!!!
  5. 构造函数 secondtonone1:构造函数是类的基础知识,要着重掌握
  6. 面试题汇总(一) secondtonone1:看到网络上经常提问的go的问题,做了一下汇总,结合自己的经验给出的答案,如有纰漏,望指正批评。
  7. C++ 并发三剑客future, promise和async Yunfei:大佬您好,如果这个线程池中加入的异步任务的形参如果有右值引用,这个commit中的返回类型推导和bind绑定就会出现问题,请问实际工程中,是不是不会用到这种任务,如果用到了,应该怎么解决?
  8. visual studio配置boost库 一giao里我离giaogiao:请问是修改成这样吗:.\b2.exe toolset=MinGW
  9. Qt MVC结构之QItemDelegate介绍 胡歌-此生不换:gpt, google
  10. 聊天项目(7) visualstudio配置grpc diablorrr:cmake文件得改一下 find_package(Boost REQUIRED COMPONENTS system filesystem),要加上filesystem。在target_link_libraries中也同样加上
  11. protobuf配置和使用 熊二:你可以把dll放到系统目录,也可以配置环境变量,还能把dll丢到lib里
  12. string类 WangQi888888:确实错了,应该是!isspace(sind[index]). 否则不进入循环,还是原来的字符串“some string”
  13. Qt 对话框 Spade2077:QDialog w(); //这里是不是不需要带括号
  14. 再谈单例模式 secondtonone1:是的,C++11以后返回局部static变量对象能保证线程安全了。
  15. 利用栅栏实现同步 Dzher:作者你好!我觉得 std::thread a(write_x); std::thread b(write_y); std::thread c(read_x_then_y); std::thread d(read_y_then_x); 这个例子中的assert fail并不会发生,原子变量设定了非relaxed内存序后一个线程的原子变量被写入,那么之后的读取一定会被同步的,c和d线程中只可能同时发生一个z++未执行的情况,最终z不是1就是2了,我测试了很多次都没有assert,请问我这个观点有什么错误,谢谢!
  16. 答疑汇总(thread,async源码分析) Yagus:如果引用计数为0,则会执行 future 的析构进而等待任务执行完成,那么看到的输出将是 这边应该不对吧,std::future析构只在这三种情况都满足的时候才回block: 1.共享状态是std::async 创造的(类型是_Task_async_state) 2.共享状态没有ready 3.这个future是共享状态的最后一个引用 这边共享状态类型是“_Package_state”,引用计数即使为0也不应该block啊
  17. 创建项目和编译 secondtonone1:谢谢支持
  18. 处理网络粘包问题 zyouth: //消息的长度小于头部规定的长度,说明数据未收全,则先将部分消息放到接收节点里 if (bytes_transferred < data_len) { memcpy(_recv_msg_node->_data + _recv_msg_node->_cur_len, _data + copy_len, bytes_transferred); _recv_msg_node->_cur_len += bytes_transferred; ::memset(_data, 0, MAX_LENGTH); _socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), std::bind(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_self)); //头部处理完成 _b_head_parse = true; return; } 把_b_head_parse = true;放在_socket.async_read_some前面是不是更好
  19. 聊天项目(9) redis服务搭建 pro_lin:redis线程池的析构函数,除了pop出队列,还要free掉redis连接把
  20. boost::asio之socket的创建和连接 项空月:发现一些错别字 :每隔vector存储  是不是是每个. asio::mutable_buffers_1 o或者    是不是多打了个o
  21. 聊天项目(15) 客户端实现TCP管理者 lkx:已经在&QTcpSocket::readyRead 回调函数中做了处理了的。
  22. 解决博客回复区被脚本注入的问题 secondtonone1:走到现在我忽然明白一个道理,无论工作也好生活也罢,最重要的是开心,即使一份安稳的工作不能给我带来事业上的积累也要合理的舍弃,所以我还是想去做喜欢的方向。
  23. interface应用 secondtonone1:interface是万能类型,但是使用时要转换为实际类型来使用。interface丰富了go的多态特性,也降低了传统面向对象语言的耦合性。

个人公众号

个人微信