C++基础的不能再基础的学习笔记——面向对象程序设计(二)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/fancynece/article/details/79424192

在前面的学习中,我们了解到了面向对象程序设计的一些基础知识,接下来我们继续学习相关内容。http://blog.csdn.net/fancynece/article/details/79398653

一、访问控制与继承

protected成员

如前所述,一个类使用protected关键字来声明那些希望与派生类共享但是不能被用户访问的成员。

  • protected成员对于类的用户是不可访问的
  • protected成员对于派生类的成员和友元来说是可以访问的
  • 派生类的成员和友元 只能通过派生类对象的基类部分来访问基类的protected成员。对于普通的基类对象中的成员不具有特殊的访问权限。
class Base{
protected:
    int prot_mem;
};

class Sneaky : public Base{
    friend void clobber(Sneaky&);        //可以访问Sneaky::prot_mem
    friend void clobber(Base&);          //不可访问Base::prot_mem
    int j;
};

void clobber(Sneaky& s)
{
    s.j = s.prot_mem = 0;
}
void clobber(Base& b)
{
    b.prot_mem = 0;            //错误,不可访问Base::prot_mem
}
公有、私有和受保护继承

每个类对其继承而来的成员的访问受到两个方面的影响:

  • 基类中该成员的访问说明符
  • 派生列表中的访问说明符
class Base {
public:
    void pub_mem();
protected:
    int prot_mem;
private:
    char priv_mem;
};

class Pub_Derv : public Base {
    int f() { return prot_mem; }   //正确,可访问protected成员
    int g() { return priv_mem; }   //错误,不可访问private成员
};

class Priv_Derv : private Base {
    //private不影响派生类的访问权限
    int f() { return prot_mem; }
};

由此可见,对于派生类的成员和友元的访问权限而言,派生列表的访问说明符没有任何影响,起作用的只是基类的访问说明符,派生类的成员和友元都可以访问它们。

但是,对于派生类的对象、派生类的派生类而言,则是不同的。

派生列表访问说明符为public:则由基类继承而来的成员保持原有访问说明

派生列表访问说明符为protected:则由基类继承而来的public成员为protected,其他成员保持原有访问说明

派生列表访问说明符为private:则由基类继承而来的成员为private

派生类向基类转换的可访问性

派生类向基类的类型转换,派生类访问说明符也会对其有影响。

  • 只有当D公有的继承B时,用户代码才能使用派生类向基类的转换。
  • 不论D以什么方式继承B,D的成员和友元都能使用派生类向它的直接基类转换。
  • 如果D继承B是public或protected的,则D的派生类成员和友元可以使用D向B的类型转换。
友元与继承

友元关系不可以传递不可以继承。基类的友元在访问派生类时没有特殊性,派生类的友元在访问基类时也没有特殊性。

class Base{
    friend class Pal;
    ……
};

class Pal {
    int f1(Base b) { return b.prot_mem; }
    //int f2(Sneaky s) { return s.j; }     //错误,j为Sneaky类定义的成员
    int f3(Sneaky s) { return s.prot_mem; }  //正确
};

对于函数f3而言,是正确的。Pal类为Base类的友元,可以通过Base类的对象访问其成员,即使是嵌套在派生类对象中的也可以

改变成员的可访问性

有时我们需要改变派生类继承的某个成员的访问级别,通过using声明可以达到。

class Base{
public:
    size_t size()const { return n;}
protected:
    size_t n;
};

class Derived : private Base{
public:
    using Base::size;
protected:
    using Base::n;
};

Derived类使用了私有继承,所以size()和n都是私有成员。

声明语句声明在public下,则派生类的用户、派生类、成员和友元都可以访问;
声明语句声明在protected下,则派生类的派生类、成员和友元都可以访问;
声明语句声明在private下,则派生类的成员和友元可以访问。

我们在这里还需要知道,由struct和class定义的类只有两点不同,即默认成员访问说明符(struct为public,class为private)、默认派生访问说明符(struct为public,class为private)。

二、继承中的类作用域

每个类定义自己的作用域,在这个作用域内我们定义类的成员。

当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外层的基类作用域中寻找该名字的定义。

恰恰因为类作用域有这种继承嵌套的关系,所以派生类才能像使用自己的成员一样使用基类的成员。

Bulk_quote bulk;
bulk.isbn();

上述代码的解析过程如下所示:

  • 首先在bulk_quote中查找isbn,没有找到
  • 因为Bulk_quote是Disc_quote的派生类,所以在Disc_quote中查找isbn,没有找到
  • 因为Disc_quote是Quote的派生类,所以接着查找Quote,找到了isbn,所以我们使用的isbn最终被解析为Quote中的isbn
在编译时进行名字查找

一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的,即使静态类型与动态类型不一致。

class Disc_quote : public Quote {

public:
    std::pair<size_t, double> discount_policy() const {
        return{ quantity,discount };
    }
    ……
};

Bulk_quote bulk;
Bulk_quote *b = &bulk;
Quote *q = &bulk;

b->discount_policy();    //正确,b的静态类型是Bulk_quote*,访问的到该函数
q->discount_policy();    //错误,q的静态类型是Quote*,访问不到该函数

bulk中确实存在着discount_policy()成员,但对于q而言,该成员是不可见的,因为在解析时,会从Quote开始解析,找不到discount_policy()。

名字冲突与继承

派生类可以重用定义在其直接基类或间接基类中的名字,此时定义在内层作用域(即派生类)的名字将隐藏定义在外层作用域(即基类)的名字。

class Base{
publicBase():mem(0){}
protected:
    int mem;
};

class Derived : public Base{
public:
    Derived(int i):mem(i){}        //初始化Derived的,Base的mem默认初始化
    int get_mem() { return mem;}   //返回Derived的
protected:
    int mem;                       //隐藏基类的mem
};


Derived d(42);
cout << d.get_mem() << endl;

//输出42
通过作用域运算符来使用隐藏的成员

我们可以通过作用域运算符来使用一个被隐藏(即被重写或覆盖)的基类成员。

class Derived:public base{
    int get_base_mem() {return Base::mem;}
};

为了理解函数调用的解析过程,我们以调用p->mem()为例。

  • 首先确定p的静态类型
  • 在p的静态类型对应的类中查找mem(),如果找不到则在基类中找。如果找遍了该类及其基类仍然找不到,则编译器报错
  • 一旦找到了mem(),则进行常规的类型检查以确认对于当前找到的mem(),本次调用是否合法。
  • 假设调用合法,则编译器将根据调用的是否是虚函数而产生不同的代码。
同名成员

如前所述,声明在内层作用域中的成员 会覆盖不回重载 声明在外层作用域中的成员。

因此,定义派生类中的函数也不会重载其基类中的成员。如果派生类的成员与基类的某个成员同名,则派生类将在其作用域内隐藏该基类成员,即使派生类成员和基类成员的形参列表不一致。

class Base{
public:
    int memfun();
};

class Derived : public Base{
public:
    int memfun(int);
};

Derived d;
Base b;

b.memfun();
d.memfun(10);
d.memfun();     //错误
d.Base::memfun();  //正确
class Base{
public:
    virtual int fcn();
};

class D1 : public Base{
public:
    int fcn(int);          //隐藏Base的fcn
    virtual void f2();
};

class D2 : public D1{
public:
    int fcn(int);         //隐藏D1的fcn
    int fcn();            //重定义Base的fcn
    void f2();            //重定义D1的f2
};

三、构造函数与拷贝控制

和其他类一样,位于继承体系中的类也需要控制当其对象执行一系列操作室发生什么样的行为,这些操作包括创建、拷贝、移动、赋值和销毁。如果一个类没有定义拷贝控制操作,那么编译器将为它合成一个版本。

1. 虚析构函数

继承关系 对基类拷贝控制 最直接的影响就是 基类通常应该定义一个 虚析构函数,这样我们就可以动态分配继承体系中的对象了。

当我们delete一个动态分配对象的指针时将执行析构函数。如果该指针指向继承体系中的某个类型,则有可能出现该指针的静态类型和动态类型不一致的情况。因此我们需要在基类中将析构函数定义成虚函数以确保执行正确的析构函数版本。

之前我们曾经介绍过一条准则,如果一个类需要析构函数,那么它同样也需要拷贝和赋值操作。但是,基类的析构函数是一个例外。一个基类总是需要虚析构函数,但是我们无法判断它是否需要拷贝和赋值操作。

2. 合成拷贝控制与继承

基类或派生类的合成拷贝控制成员的行为,与其他合成的构造函数、赋值运算符、析构函数类似。

此外,这些合成的成员负责使用 直接基类中相应的操作 对一个对象的直接基类部分进行初始化、赋值或销毁。

派生类中删除的拷贝控制与基类的关系
  • 如果基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符、析构函数是被删除的或不可访问,那么派生类中对应的成员将是被删除的。
  • 如果基类中的析构函数是删除的或不可访问的,那么派生类中合成的默认构造函数、拷贝构造函数、拷贝赋值运算符是被删除的。
3. 派生类的拷贝控制成员

我们已经知道,派生类的拷贝、移动构造函数和拷贝赋值运算符在拷贝、移动、赋值派生类自有成员的同时,也要拷贝、移动、赋值基类部分的成员。

然而,析构函数只负责销毁派生类自己分配的资源。我们已知对象的成员是被隐式销毁的,类似的,派生类的基类部分也是自动销毁的。

定义派生类的拷贝或移动构造函数
class Base{
    ……
};

class D : public Base{
pulic:
    D(const D &d) : Base(d) { }   //拷贝基类成员
    D(const D &d) { }             //初始化基类成员
};

在上述代码中,

第一个函数,调用了基类的拷贝构造函数Base(const Base&),并将派生类对象传递过去,完成基类成员的拷贝。

第二个函数,默认调用了基类的默认构造函数,初始化派生类对象中的基类部分。

因此,如果我们想拷贝或移动基类部分,则必须在派生类构造函数初始值列表中,显示地使用基类的拷贝或移动构造函数。

派生类赋值运算符
Base::operator=(const Base&);

D& D::operator=(const D& rhs)
{
    Base::operator=(rhs);         //为基类部分赋值
    //为派生类赋值

    return *this;
}

上面的运算符首先显示地调用了基类赋值运算符,令其为派生类对象的基类部分赋值。然后我们为派生类其他成员赋值。

派生类析构函数

如前所述,在析构函数体执行完成后,对象的成员会被隐式销毁。类似的,对象的基类部分也是隐式销毁的。因此,和构造函数及赋值运算符不同的是,派生类析构函数只负责销毁由派生类自己分配的资源

class D : public Base{
public:
    ~D() {销毁派生类成员}   Base::~Base()会被自动调用
};
在构造函数和析构函数中调用虚函数

如果构造函数或析构函数中调用了某个虚函数,则我们应该执行与构造函数或析构函数所属类型相对应的虚函数版本

因为当我们初始化某个对象时,先从基类部分开始,在初始化基类部分时,其派生类部分是未定义的,自然不能执行派生类的虚函数。
而当我们销毁某个对象时,先从派生类部分开始,在销毁基类部分时,其派生类部分已经被销毁,也不能执行派生类的虚函数。

4. 继承的构造函数

在C++11新标准中,派生类能够重用其 直接基类 定义的构造函数。一个类只初始化它的直接基类,出于同样的原因,一个类只继承其直接基类的构造函数,并且类不会继承默认、拷贝和移动构造函数。

我们将用using声明语句表示继承了直接基类的构造函数。

class Bulk_quote : public Disc_quote{
public:
    using Disc_quote:Disc_quote;    //继承Disc_quote的构造函数
    ……
}

在上述声明语句的作用下,对于基类的每个构造函数,编译器都在派生类中生成一个形参列表完全相同的构造函数。派生类名字(形参列表): 基类名字(形参列表);

如果派生类含有自己的成员,那么该成员将被默认初始化

继承的构造函数的特点
  • 无论using语句在哪里,派生类对构造函数的访问权限不变
  • 当一个基类含有默认实参时,这些实参并不会被继承。相反,派生类在获得该构造函数的同时,还会获得去掉这个实参的构造函数
  • 默认、拷贝、移动构造函数不会被继承
  • 当派生类定义了与直接基类形参列表一致的构造函数时,会替换从基类继承而来的构造函数

四、容器与继承

当我们使用容器存放继承体系中的对象时,必须采用间接存储的方式,因为容器中元素的类型必须是统一的。此时,我们实际上存放的通常是基类的指针(或者智能指针)

vector< shared_ptr<Quote> > basket;

basket.push_back(make_shared<Quote>("003", 50));
cout << basket.back()->net_price(15) << endl;

basket.push_back(make_shared<Bulk_Quote>("004", 50, 10, .25));
cout << basket.back()->net_price(15) << endl;

编写basket类

对于C++面向对象的编程来说,一个悖论是我们无法直接使用对象进行面向对象编程,而必须使用指针和引用。因为指针会增加程序的复杂性,所以我们经常定义一些辅助的类来处理这种复杂情况。首先,我们定义一个表示购物篮的类。

class Basket{

public:
    void add_item(const shared_ptr<Quote> &sale)
    {
        items.insert(sale);
    }

    double total_receipt(ostream&)const;

private:

    static bool compare(const shared_ptr<Quote> &lhs,const shared_ptr<Quote> &rhs)
    {
        return lhs->isbn() < rhs->isbn();
    }

    multiset<shared_ptr<Quote>,decltype(compare)*> items{compare};
    //采用compare函数排序的set
};

double Basket::total_receipt(ostream &os) const
{
    double sum = 0.0;
    for (auto iter = items.cbegin(); 
        iter != items.cend(); 
        iter = items.upper_bound(*iter)) //返回第一个大于关键字*iter的元素的迭代器
    {
        sum += print_total(os, **iter, items.count(*iter)); //统计*iter关键字的元素的个数
    }

    os << "total sale: " << sum << endl;
    return 0.0;
}

然而,当我们使用Basket类时,依然需要用户分配内存。

Basket bsk;
bsk.add_item(make_shared<Quote>("001",45));

我们希望将分配内存交给类的管理者来做,因此我们要定义另一个版本的函数 add_item(const Quote&);将内存分配在此函数内完成。

可是由于静态类型和动态类型可能不一致,我们无法确定该为Quote类申请内存还是该为Bulk_quote类申请内存。

模拟虚拷贝

为了解决这个问题,我们为Quote类添加一个虚函数clone,该函数将申请一份当前对象的拷贝。

virtual Quote* clone() const& { return new Quote(*this); } //Quote类

virtual Bulk_Quote* clone() const& { return new Bulk_Quote(*this); } //Bulk_Quote类

void add_item(const Quote& sale)
{
    items.insert(shared_ptr<Quote>(sale.clone()));
}

猜你喜欢

转载自blog.csdn.net/fancynece/article/details/79424192