文章目录
可变数据成员
有时会发生这样一种情况,我们希望能修改类的某个数据成员,即使在一个const成员函数内,可以通过在变量的声明中加入mutable关键字做到这一点。
mutable int test;
void GetTest() const{
++test;
}
返回*this指针(返回调用函数对象的引用)的妙用
(1)*this指针可以将几个操作压缩在一个序列中
class Screen{
public:
Screen& set(char c);
Screen& move(int posX,int posY);
private:
int posX,posY;
char show;
};
inline Screen& Screen::set(char c){
this->show = c;
return *this;
}
inline Screen& Screen::move(int posX,int posY){
this->posX = posX;
this->posY = posY;
return *this;
}
Screen s;
s.move(4,0).set('#');
//上述语句等下于下面两句
s.move(4,0);
s.set('#');
(2)一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用。所以在压缩序列化后会出错,所以要进行重载
class Screen {
public:
Screen& set(char c);
Screen& move(int posX, int posY);
Screen& display(ostream& os) { do_display(os); return *this; }
const Screen& display(ostream& os) const { do_display(os); return *this; }
private:
void do_display(ostream& os) const { os << "OK"; }
int posX, posY;
char show;
};
inline Screen& Screen::set(char c) {
this->show = c;
return *this;
}
inline Screen& Screen::move(int posX, int posY) {
this->posX = posX;
this->posY = posY;
return *this;
}
//重载后就可以分情况调用了
Screen s;
s.set('#').display(cout); //调用非常量版本
s.display(cout); //调用常量版本
友元函数
一个类中将另一个类声明为友元函数,另一个类就能访问该类的私有的成员变量和成员函数
如:
class Window_mgr{
private void Set(Screen& s){ s.a = 1;}
}
//直接将类作为友元
class Screen{
friend class Window_mgr;
private int a;
}
友元关系不存在传递性。也就是说,如果Window_mgr有它自己的友元,则这些友元并不能理所当然地具有访问Screen的特权。
每个类负责控制自己的友元类或友元函数
构造函数
构造函数的初始值有时必不可少
class ConstRef{
public ConstRef(int ii):
private:
int i;
const int ci;
int& ri;
};
和其他常量对象或者引用一样,成员ci和ri都必须被初始化。因此,如果我们没有为它们提供构造函数初始值的话将引发错误。所以说如果成员是const、引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初值。
ConstRef(int ii):i(ii),ci(ii),ri(i){}
委托构造函数
C++新标准扩展了构造函数初始值的功能,使得我们可以定义所谓的委托构造函数。一个未通过构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些(或者全部职责委托给了其他构造函数)
class Sales_ data{
public:
//非委托构造函数使用对应的实参初始化成员
Sales_ data (stdstring s, unsigned cnt,double price) :
bookNo(s),units_ sold(cnt),revenue (cnt*price){}
//其余构造函数全都委托给另一个构造函数
Sales_ data(): Sales_ data("", 0,0) {}
Sales_ data(std: :string s): Sales_ _data(s,0,0) { }
Sales_ data(std: :istream &is) : Sales_ data()
{ read(is, *this); }
//其他成员与之前的版本一致
};
explicit关键字
使用explicit关键字可以抑制构造函数定义的隐式转换,推荐使用
定义基类与派生类
定义基类
class Quote {
public:
Quote() = default;
Quote(const string & book, double sales_price) :
bookNo(book), price(sales_price) { }
string isbn() const { return bookNo; } //返回给定数量的书籍的销售总额
//派生类负责改写并使用不同的折扣计算算法
virtual double net_price(size_t n) const
{
return n * price;
}
virtual ~Quote() = default; // 对析构函数进行动态绑定
private:
string bookNo; //书籍的ISBN编号
protected:
double price = 0.0; // 代表普通状态下不打折的价格
};
定义派生类
class Bulk_quote : public Quote {
// Bulk_ quote 继承自Quote
public:
Bulk_quote() = default;
Bulk_quote(const string&,double,size_t, double);
//覆盖基类的函数版本以实现基于大量购买的折扣政策
double net_price(size_t) const override;
private:
size_t min_qty = 0; //适用折扣政策的最低购买量
double discount = 0.0; //以小数表示的折扣额
};
Bulk_quote::Bulk_quote(const string& book, double p,size_t qty, double disc) :
Quote(book, p),min_qty(qty),discount(disc){ } //与之前一致
double Bulk_quote::net_price(size_t cnt) const
{
if (cnt >= min_qty)
return cnt * (1 - discount) * price;
else
return cnt * price;
}
防止继承的发生
class NoDerived final{}; //NoDerived不能作为基类
struct B{
void f1(int) final; //该函数禁止继承
}
类型转换与继承
Quote i tem;//基类对象
Bulk_ quote bulk;//派生类对象
Quote *p = &item;// p指向Quote对象
p = &bulk;//p指向bulk的Quote部分
Quote &r = bulk;// r绑定到bulk的Quote部分
虚函数与默认实参
和其他函数一样,虚函数也可以拥有默认实参。如果某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定。
换句话说,如果我们通过基类的引用或指针调用函数,则使用基类中定义的默认实参,
即使实际运行的是派生类中的函数版本也是如此。此时,传入派生类函数的将是基类函数
定义的默认实参。如果派生类函数依赖不同的实参,则程序结果将与我们的预期不符。
如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。
回避虚函数的机制
某些情况下,我们希望对虚函数的调用不要进行动态绑定,而是强迫其执行虚函数的某个特定版本,使用作用域运算符可以实现这一目的:
double undiscounted = baseP->Quote::net_price(42);
抽象基类和纯虚函数
含有纯虚函数的类是抽象基类。抽象基类是无法实例化对象的。
访问控制和继承
派生类的成员和友元只有通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权。
class Base {
protected :
int prot_ mem;// protected 成员
};
class Sneaky : public Base {
friend void clobber (Sneaky&) ; //能访问Sneaky: :prot_ _mem
friend void clobber (Base&) ; //不能访问Base: :prot_ mem
int j; // j默认是private
};
//正确: clobber能访问Sneaky对象的private和protected成员
void clobber (Sneaky &s) { s.j = s.prot_ mem = 0; }
//错误: clobber不能访问Base的protected成员
void clobber (Base &b) { b.prot_ _mem = 0; }
某个类对其继承而来的成员的访问权限受到两个因素影响:一是在基类中该成员的访问说明符,二是在派生类的派生列表中的访问说明符。
- 派生访问说明符对于派生类的成员(及友元)能否访问其直接基类的成员没什么影响。对基类成员的访问权限只与基类中的访问说明符有关。
继承中的类作用域
每个类定义自己的作用域,在这个作用域内我们定义类的成员。当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外层的基类作用域中寻找该名字的定义。
派生类的作用域位于基类作用域之内这一事实可能有点儿出人意料,毕竟在我们的程序文本中派生类和基类的定义是相互分离开来的。不过也恰恰因为类作用域有这种继承嵚套的关系,所以派生类才能像使用自己的成员一样使用基类的成员。
一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。即使静态类型与动态类型可能不一致,但是我们能使用哪些成员仍然是由静态类型决定的。
派生类的成员将会隐藏同名的基类成员,我们可以通过作用域运算符来使用隐藏的成员
但是除了覆盖继承而来的虚函数之外,派神隔离最好不要重用其他定义在基类中的名字。
构造函数与拷贝控制
虚析构函数
继承关系对基类拷贝控制最直接的影响是基类通常应该定义一个虚析构函数,这样我们就能动态分配继承体系中的对象了。
如前所述,当我们delete一个动态分配的对象的指针时将执行析构函数。如果该指针指向继承体系中的某个类型,则有可能出现指针的静态类型与被删除对象的动态类型不符的情况。例如,如果我们delete一个Quote*类型的指针,则该指针有可能实际指向了一个Bulk_quote类型的对象。如果这样的话,编译器就必须清楚它应该执行的是Bulk_ quote的析构函数。和其他函数一样,我们通过在基类中将析构函数定义成虚函数以确保执行正确的析构函数版本:
class Quote{
public:
virtual ~Quote() = default;
};
和其他虚函数一样,析构函数的虚属性也会被继承。因此,无论Quote的派生类使用合
成的析构函数还是定义自己的析构函数,都将是虚析构函数。只要基类的析构函数是虚函数,就能确保当我们delete基类指针时将运行正确的析构函数版本:
Quote *itemP = new Quote; //静态类型与动态类型一致
delete itemP; //调用Quote的析构函数
itemP = new Bulk_quote; //静态类型与动态类型不一致
delete itemP; //调用Bulk_quote的析构函数
虛析构函数将阻止合成移动操作
基类需要一个虚析构函数这一事实还会对基类和派生类的定义产生另外一个间接的影响:如果一个类定义了析构函数,即使它通过=default的形式使用了合成的版本,编译器也不会为这个类合成移动操作。
合成拷贝控制与继承
基类或派生类的合成拷贝控制成员的行为如下例子:
- 合成的Bulk_ quote默认构造函数运行Disc_quote的默认构造函数,后者又运行Quote的默认构造函数。
- Quote的默认构造函数将bookNo成员默认初始化为空字符串,同时使用类内初始值将price初始化为0。
- Quote 的构造函数完成后,继续执Disc_quote的构造函数,它使用类内初始值初始化qty和discount.
- Disc_quote 的构造函数完成后,继续执行Bulk_ quote 的构造函数,但是它什么具体工作也不做。
在我们的Quote继承体系中,所有类都使用合成的析构函数。其中,派生类隐式地使用而基类通过将其虚析构函数定义成=default而显式地使用。一如既往,合成的析构函数体是空的,其隐式的析构部分负责销毁类的成员。对于派生类的析构函数来说,它除了销毁派生类自己的成员外,还负责销毁派生类的直接基类;该直接基类又销毁它自己的直接基类,以此类推直至继承链的顶端。
派生类中删除的拷贝控制与基类的关系
就像其他任何类的情况-一样,基类或派生类也能出于同样的原因将其合成的默认构造函数或者任何一个拷贝控制成员定义成被删除的函数。此外,某些定义基类的方式也可能导致有的派生类成员成为被删除的函数:
- 如果基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符或析构函数是被删除的函数或者不可访问,则派生类中对应的成员将是被删除的,原因是编译器不能使用基类成员来执行派生类对象基类部分的构造、赋值或销毁操作。
- 如果在基类中有一个不可访问或删除掉的析构函数,则派生类中合成的默认和拷贝构造函数将是被删除的,因为编译器无法销毁派生类对象的基类部分。
- 和过去一样,编译器将不会合成一个删除掉的移动操作。当我们使用=default请一个移动操作时,如果基类中的对应操作是删除的或不可访问的,那么派生类中该函数将是被删除的,原因是派生类对象的基类部分不可移动。同样,如果基类的析构函数是删除的或不可访问的,则派生类的移动构造函数也将是被删除的。
移动操作与继承
如前所述,大多数基类都会定义一个虚析构函数。因此在默认情况下,基类通常不含有合成的移动操作,而且在它的派生类中也没有合成的移动操作。因为基类缺少移动操作会阻止派生类拥有自己的合成移动操作,所以当我们确实需要执行移动操作时应该首先在基类中进行定义。我们的Quote可以使用合成的版本,不过前提是Quote必须显式地定义这些成员。一旦Quote定义了自己的移动操作,那么它必须同时显式地定义拷贝操作:
class Quote {
public:
Quote() = default; //对成员依次进行默认初始化
Quote (const Quote&) = default; //对成员依次拷贝.
Quote (Quote&&) = default; //对成员依次拷贝
Quote& operator= (const Quote&) = default; //拷贝赋值
Quote& operator= (Quote&&) = default; //移动赋值
virtual ~Quote() = default; //其他成员与之前的版本一致
};
定义派生类的拷贝或移动构造函数
当为派生类定义拷贝或移动构造函数时,我们通常使用对应的基类构造函数初始化对象的基类部分:
class Base { /* ...*/ };
class D: public Base {
public:
D(const D& d): Base (d) {}
D(D&& d): Base (std: :move(d)) {}
};
派生类赋值运算符
与拷贝和移动构造函数一样,派生类的赋值运算符也必须显示地为其基类部分赋值:
D& D::operator=(const D& rhs){
Base::opeator=(rhs);
return *this;
}
派生类析构函数
在析构函数执行完成后,对象的成员会被隐式销毁,类似的,对象的基类部分也是隐式销毁的。因此,和构造函数及赋值运算符不同的是,派生类析构函数只负责销毁由派生类自己分配的资源:
class D:public Base{
~D() {/*清除派生类成员*/}
};
继承的构造函数
在C++11新标准中,派生类能够重用其直接基类定义的构造函数。
派生类继承基类构造函数的方式是提供一条注明了基类名的using声明语句。
class Bulk_ quote : public Disc_ quote {
public:
using Disc_ quote::Disc_ _quote; //继承Disc_ quote的构造函数
double net_ price(std: :size_ t) const;
};