c++从入门到精通(三)--面向对象部分

文章目录

    • 面向对象-初级
      • 构造函数
      • 成员函数和成员变量
      • 非成员函数
      • 有元
    • 面向对象-高级
      • 拷贝控制
        • 拷贝、赋值与销毁
        • 拷贝控制和资源管理
        • 拷贝控制示例
        • 右值引用和成员函数
      • 运算符重载
        • 基本概念
        • 输入输出运算符
        • 算术和关系运算符
        • 赋值运算符
        • 下标运算符
        • 递增递减运算符
        • 成员访问运算符
        • 函数调用运算符
        • 重载、类型转换与运算符
    • 面向对象-程序设计
      • 介绍
      • 基类和派生类
      • 虚函数
      • 抽象基类
      • 访问控制与继承
      • 继承中的类作用域
      • 构造函数与拷贝控制
      • 容器与继承
      • 实例文本查询

面向对象-初级

  • 成员函数的声明必须在类的内部,定义可以在外部,在外部的时候函数名需要带上类的命名空间标识类名::
  • 聚合类:所有成员都是public,没有定义任何构造函数,没有类内初始值,没有基类,也没有virtual函数。我们使用一个{}的初始值列表来初始化聚合类。
  • 字面值常量类:数据成员都是字面值类型的聚合类是字面值常量类。
  • 非聚合类,如果符合下面要求也是字面值常量类:
    1. 数据成员都必须是字面类型
    2. 必须有一个constexpr构造函数
    3. 数据成员含有类内初始值,初始值必须是一条常量表达式,如果成员属于某种类类型,初始值必须使用成员自己的constexpr构造函数。
    4. 类必须使用析构函数的默认定义。

构造函数

  • 构造函数没有返回值,如果自己定义了构造函数,默认构造函数需要显示定义类名()=default

  • 构造函数语法类名(int a,int b):成员变量名(a),成员变量名(b){}某个成员变量被构造函数初始值列表忽略时,他将以合成默认构造函数相同的方式隐式初始化。

  • 可以在类的外部定义构造函数,需要显示说明属于哪个类

  • 构造函数一开始执行,初始化就完成了,如果成员是const,引用或者属于某种未提供默认构造函数的类类型,必须通过构造函数初始值列表为这些成员函数提供初始值,不能在构造函数的构造体内为这些成员变量赋值!!

  • 成员变量初始化的顺序和定义的顺序一致,和构造函数初始值列表中这些值的顺序无关。

  • 尽量避免使用某些成员初始化其他成员

  • 委托构造函数,委托的构造函数的初始值列表仅有一个那就是被委托的构造函数,然后加括号,里面填写被委托的构造函数初始值列表中的值。

    class myclass{
      public:
      	myclass(int a,int b,int c): aa(a),bb(b),cc(c){}
      	//其余构造函数采用上面的构造函数进行委托
      	myclass(int a):myclass(a,0,0){}
      	myclass(int b):myclass(0,b,0){}
    }
    
  • 如果想定义一个使用默认构造函数的类类型,不要加括号Myclass obj;,如果加括号则声明了一个函数。

  • 编译器会进行隐式类类型转换,此时将会调用相应的构造函数,定义一个类的临时对象。编译器只会转换一次。我们可以在构造函数前面声明为explicit,则编译器无法通过该构造函数隐式的创建类的临时实例对象。对于explicit的构造函数,我们只能使用直接初始化,而不能使用拷贝初始化。但我们可以显式的强制转换explicit构造函数fun(Myclass(null_book)),此时myclass的构造函数时explicit类型,fun的输入时myclass类类型,此时构造函数创建了一个临时的myclass类类型传递给fun函数。

  • constexpr构造函数,函数体一般是空的,该构造函数必须初始化所有的数据成员。

成员函数和成员变量

  • 默认情况,this指向类类型非常量版本的常量指针,因此我们不能把this绑定在一个常量对象上,这就使我们无法对一个常量类型引用普通成员函数。通过把成员函数设置成常量成员函数可以解决这个问题int fun() const {}

  • return *this;返回这个实例化对象。

  • 定义在类里面的成员函数是隐式内联的,我们可以在定义在类外面的成员函数前面加上inline来显示指示这是内联函数。(一般不要在类内成员函数声明时使用inline)

  • 我们把成员变量声明为mutable,无论实例对象是否是const都可以改变该变量mutable int a

  • 如果我们在常量成员函数中返回*this,则返回的是常量this,但我们调用该成员函数的实例对象可能是非常量。于函数重载类似,我们需要为该函数定义两个版本(const和非const版本)。如果实例对象是非常量会调用非常量版本,此时实际执行的函数还是常量函数,但是返回this指针的函数是非常量函数,可以返回非常量指针,

    class screen{
      public:
      	screen &display(std::ostream &os){ do_display(os); return *this;}
      	const screen &display(std::ostream &os) const {do_display(os); return *this;}
      private:
      	void do_display(std:ostream &os) const {os<<contents;}
    }
    
  • 类中可以包含指向它自身类型的指针或者引用。

  • 我们可以在类外部使用类内部定义的类型,需要加上定义域说明符

  • 当成员函数定义在类的外部时,返回类型中使用的名字都应该位于类的作用域之外,此时必须明确指明返回类型是哪个类的成员window_mgr::ScreenIndex window_mgr::fun(){}

  • 编译器处理所有有的声明后才会处理成员函数的定义。

  • 静态成员,通过在成员的声明前加上关键字static使得它和类关联在一起。我们使用作用域运算符leimign::静态成员名,或者对象访问静态成员,成员函数可以不通古作用域运算符直接使用静态成员。

  • 定义静态成员:在类的外部定义静态成员函数,不能重复使用static关键字。==我们不能再类的内部初始化静态成员,必须在类的外部定义和初始化每个静态成员函数和变量。==在类的外部定义静态成员的方式和在类的外部定义成员函数的语法类似。

  • 我们可以为静态数据成员提供const int类型的类内初始值,不过该静态成员必须是字面值常量类型的constexpr,static constexpr int period=30

  • 如果在类的内部为静态数据成员提供了一个初始值,则成员的类外定义不能再指定一个初始值了

  • 我们可以使用静态成员作为类内成员函数的默认实参,而无法使用普通成员变量作为类内成员函数的默认实参。

非成员函数

  • 如果函数在概念上属于类,但是不定义在类中,则它一般应该和类的声明在同一个头文件。

有元

  • 通过定义友元可以使其他类,或者函数访问本类的私有变量,友元的定义一般在最前面,不需要访问控制限定符friend int fun();
  • 使得其他类的成员函数成为友元friend void window_mgr::clear(ScreenIndes);此时我们需要先定义windows_mgr类,声明其中的clear函数,但是不能定义它,在使用screen中的screenindex时,还要先声明screen。接下来我们定义screen,包含对clear的友元声明,最后定义clear,此时它才可以使用screen的成员。–>需要定义为其他类友元的函数建议在类外定义而不是类内定义。
  • 重载函数的名字相同,但他们仍然是不同的函数,定义一组重载函数的友元需要定义所有重载函数各自的友元。
  • 友元的声明仅仅制定了访问的权限,并非一个通常意义上的函数声明。如果我们希望某个类的用户能够调用某个友元函数,那么我们就必须再友元声明之外在专门对函数进行声明。就算在类的内部调用该函数friend int fun(){},我们也必须在类外声明该函数才可以使用该友元函数。

面向对象-高级

拷贝控制

拷贝、赋值与销毁

拷贝构造函数:

  • 如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。
  • 虽然我们可以设置非常量引用,但是拷贝构造函数的第一个参数总是常量引用。拷贝构造函数会被隐式适用,通常不应该是explicit的。
  • 如果没定义拷贝构造函数,编译器也会默认定义一个。如果定义了其他构造函数编译器也会默认定义一个拷贝构造函数。
  • 虽然我们无法拷贝一个数组,但是合成拷贝构造函数会逐元素地拷贝一个数组类型的成员。
  • 直接初始化调用的是普通构造函数,而拷贝初始化调用的是拷贝构造函数。
class Sales_data{
public:
    Sales_data(const Sales_data &);
private:
    string bookNo;
    int units_sold=0;
    double revenue=0.0;
}
//拷贝构造函数
Sales_data::Sales_data(const Sales_data &ori):
bookNo(ori.bookNo),units_sold(ori.units_sold),revenue(ori.revenue){}

拷贝赋值运算符:

  • 类如果未定义,编译器会自动合成一个拷贝赋值运算符。为了与内置类型的赋值(参见4.4节,第129页)保持一致,赋值运算符通常返回一个指向其左侧运算对象的引用。另外值得注意的是,标准库通常要求保存在容器中的类型要具有赋值运算符,且其返回值是左侧运算对象的引用。
class Foo{
public:
    Foo& operator=(const F00&)
}

Sales_data& Sales_data::operator=(const Sales_data &rhs)
{
    bookNo=rhs.bookNo;
    //...
    return *this;//返回此对象的一个引用
}

析构函数

  • 析构函数是类的一个成员函数,名字由波浪号接类名构成。它没有返回值,也不接受参数。
  • 由于析构函数不接受参数,因此它不能被重载。对一个给定类,只会有唯一一个析构函数。
  • 在一个析构函数中,首先执行函数体,然后销毁成员。成员按初始化顺序的逆序销毁。
  • 析构函数体自身并不直接销毁成员!!成员是在析构函数体之后隐含的析构阶段中被销毁的。在整个对象销毁过程中,析构函数体是作为成员销毁步骤之外的另一部分而进行的
class Foo
{
public:
    ~Foo();
}

阻止拷贝:

  • 我们可以将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝。删除的函数是这样一种函数:我们虽然声明了它们,但不能以任何方式使用它们。在函数的参数列表后面加上=delete来指出我们希望将它定义为删除的
  • =delete必须出现在函数第一次声明的时候。我们可以对任何函数指定=delete(我们只能对编译器可以合成的默认构造函数或拷贝控制成员使用=default)。虽然删除函数的主要用途是禁止拷贝控制成员,但当我们希望引导函数匹配过程时,删除函数有时也是有用的。
NoCopy()=default;
NoCopy(const NoCopy&)=delete;
NoCopy &operator=(const NoCopy&)=delete;
~NoCopy()=default;

移动构造函数和移动赋值运算符:

  • 在某些情况下,我对象拷贝后就立即被销毁了,此时我们使用移动而非拷贝对象会大幅提升性能。
  • 在新标准中,我们可以使用容器保存不可拷贝的类型,只要他们能被移动即可。
  • 为了支持移动操作,新标准引入了一个新的引用–右值引用&&。右值引用必须绑定到一个将要销毁的对象。使用右值引用的代码可以自由的接管所引用对象的资源。
  • 我们可以调用std::move将一个左值引用转换为右值引用。
  • 移动构造函数的第一个参数是该类类型的右值引用
  • 如果希望在自定义类对象重新分配内存这类情况下对我们自定义类型的对象进行移动而不是拷贝,就必须显式地告诉标准库我们的移动构造函数可以安全使用。我们通过将移动构造函数(及移动赋值运算符)标记为noexcept来做到这一点。
  • 只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或移动赋值运算符。
  • 如果类定义了一个移动构造函数和/或一个移动赋值运算符,则该类的合成拷贝构造函数和拷贝赋值运算符会被定义为删除的。
  • 如果一个类既有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数,如果一个类没有移动构造函数,函数匹配规则保证该类型的对象会被拷贝,即使我们试图通过调用move来移动它们时也是如此。
  • ==如果实现了移动构造函数和交换运算,在赋值运算符中采用交换运算则可以根据实参的类型选择采用移动构造函数还是拷贝构造函数。==因此,单一的赋值运算符就实现了拷贝赋值和移动赋值两种功能。
StrVec::StrVec(StrVec &&s) noexcept//移动操作不应该抛出任何异常
    :elements(s.elements),first_free(s.first_free),cap(s.cap)
    {
        //令s进入这样一种状态,对其运行析构函数是安全的
        s.elements=s.first_free=s.cap=nullptr;
    }

StrVec &StrVec::operator=(StrVec &&rhs) noexpect
{
    if(this!=&rhs){
        free();
        elements=rhs.elements;
        first_free=rhs.first_free;
        cap=rhs.cap;
        rhs.elements=rhs.first_free=rhs.cap=nullptr;
    }
    return *this;
}

三五法则:

  • 如果一个类需要一个析构函数控制析构过程,那么我们几乎可以肯定他也需要一个拷贝构造函数和一个拷贝赋值运算符。
  • 所有五个拷贝控制成员应该看作一个整体:一般来说,如果一个类定义了任何一个拷贝操作,它就应该定义所有五个操作。
拷贝控制和资源管理

对象行为:

  • 对于管理资源的类,我们根据实际需要,把该类的行为定义为值行为或者指针行为。如果是指针行为最好用智能指针,也可以自己实现引用计数和普通指针。

  • 对于值行为的类,定义拷贝赋值运算符时,应该先拷贝值,再释放内存。否在无法规避自赋值的问题。(先释放再拷贝会导致自赋值拷贝时访问一个无效内存的指针)

  • 引用计数:自己实现的引用计数必须保存在共享内存中,普通构造函数创建的引用技术为1。拷贝构造函数的函数体中把引用计数+1,析构函数需要先判断引用计数–是否为0,如果为0释放指针指向的内存。拷贝赋值运算符先增加右侧的引用计数再递减左侧的引用计数(保持自赋值的时候引用计数不变),如果左侧引用计数为0,释放左侧对象指针指向的空间。最后把右侧对象拷贝给左侧对象。

交换操作:

  • swap函数应该调用swap而不是std::swap。如果显式调用std::swap则采用的是标准库内置版本的swap,如果使用swap会根据实参类型优先使用特定的swap。
  • 在赋值运算符中使用swap操作。注意此时赋值运算符的形参是拷贝而不是引用,在调用结束会释放rhs也就释放rhs所指向的内存,也就是原左侧运算对象的内存。并且该版本自动处理了自赋值。
class HasPtr{
    friend void swap(HasPtr&,HasPtr&);
};

inline void swap(HasPtr &l,HasPtr &r){
    using std::swap;
    swap(l.ps,r.ps);
    swap(l.i,r.i);
}

HasPtr& HasPtr::operator=(HasPtr rhs){
    swap(*this,rhs);
    return *this;
}
拷贝控制示例

消息和文件:

通常来说,分配资源的类更需要拷贝控制,但资源管理并不是一个类需要拷贝控制的唯一原因。下面我们将给出类需要拷贝控制来进行簿记操作的例子。

一个消息可以放在多个floder中,一个floder中存放多条消息。Message类提供save和remove操作,来向一个floader添加消息或者删除消息。创建的新的message不会指出floder。拷贝消息时候,副本和元对象是不同的message对象,但两个message都出现在相同的floder中。

c++从入门到精通(三)--面向对象部分_第1张图片

class Message{
  friend class Floder;
public:
	explicit Message(const string &str=""):contents(str){}
  Message(const Message&);
  Message& operator=(const Message&);
  ~Message();
  void save(Floder&);
  void remove(Floder&);
private:
  string contents;
  set<Floder*> floders;
  //将本Message添加到Floader中
  void add_to_Floders(const Message&);
  //从floder中删除message
  void remove_from_Floders();
}

void Message::save(FLoder &f){
  floders.insert(&f);
  f.addMsg(this);
}
void Message::remove(Floder &f){
  floder.erase(&f);
  f.remMsg(this);
}

void Message::add_to_Floders(const Message &m){
  for (auto f:m.floders)
    f->addMsg(this);
}
Message::Message(const Message &m):contents(m.contents),floder(m.floders)
{
  add_to_Floders(m);
}
void Message::remove_from_Folders()
{
  for(auto f:floders)
    f->remMsg(this);
}
Message::~Message(){
  remove_from_Floders();
}

Message& Message::operator=(const Message &rhs){
  //先删除指针再插入他们来处理自赋值情况
  remove_from_Floders();
  contents=rhs.contents;
  floders=rhs.floders;
  add_to_Floders(rhs);
  return *this;
}

//swap前后,消息和文件列表的逻辑关系没变,但是消息的地址变化了,所以文件中保存的消息的地址也要变化。
void swap(Message &lhs,Message &rhs){
  using std::swap;
  from (auto f:lhs.floders)
    f->remMsg(&lhs)
  from (auto f:rhs.floders)
    f->remMsg(&rhs)
  swap(lhs.floders,rhs.floders);
  swap(lhs.contents,rhs.contents);
  //从新把message插入到floder中
  from (auto f:lsh.floders)
    f->addMsg(&lhs); 
  from (auto f:rsh.floders)
    f->addMsg(&rhs); 
}

自定义Vector

在自定义的StrVec中,我们用alloctor来获得原始的动态内存,在添加新元素的时候,用aclloctor的construct成员创建对象。为了维护这一个序列,我们需要三个指针,elements指向分配内存的首地址,first_free指向最后一个实机未分配的地址,cap指向分配的内存末尾之后的地址。

class StrVec{
public:
  StrVec():
  
}
右值引用和成员函数
  • 我们也可以为成员函数定义类似于移动构造函数和拷贝构造函数的版本。
  • 在旧标准中,我们可以向右值对象赋值,在新标准中,我们可以定义自己的类,只允许向左值对象赋值。增加限定符&。我们也可以在任何成员方法后面组合使用const和&限定符。
  • 对于使用引用限定符的成员函数,如果我们定义了两个或两个以上具有相同名字和参数列表的成员函数,就必须对所有函数都加上引用限定符,或者所有都不加。
void StrVec::push_back(const string& s)
{
    chk_n_alloc();
    alloc.construct(first_free++,s);
}
void StrVec::push_back(string &&s){
    chk_n_alloc();
    alloc.construct(first_free++,std::move(s));
}

class Foo{
    Foo &operator=(const Foo&) &;// 最后一个&是引用限定符号。只能向左值赋值
}

运算符重载

基本概念
  • 调用重载运算符data1+2;operator+(data1,data2);data1.operator+=(data2);

  • 重载运算符会保留运算符的结合律,但是无法保留求职顺序和短路求值属性。不建议重载逻辑与,或,逗号,和取址运算符。也不建议重载与或运算符。

  • 具有对称性的运算符可能转换为任意一端的运算对象,例如算术、相等性、关系和位运算符等,因此他们通常应该定义为普通的非成员函数,其他情况通常定义为成员函数。

    **原因:**我们可能希望在混合类型中调用算术运算符,比如int和double的和,因此就不能把该运算符重载为int或者double对象的成员函数。

输入输出运算符

**重载输出与运算符:**输出运算符的返回值是ostream的引用类型,第一个参数是ostream的引用,因为我们无法复制一个ostream对象,并且输出一般会改变流的状态因此不能是const。第二个参数一般是要输出对象的常量引用,输出我们一般不改变被输出的对象因此是常量,为了避免复制,我们采用引用形式。

  • 重载输出运算符应尽量减少格式操作,一般不会打印换行。
  • 输入输出运算符必须是非成员函数,并且一般需要访问自定义类类型的成员,因此一般需要定义为右元
Ostream & operator<<(ostream &os,const Sales_data &item)
{
    os<<item.isbn()<<" "<<item.units_sold
    reutrn os;
}

**重载输入运算符:**同输出类似,不同之处在于第二个形参不能是const,因为要写入内容。

  • 一些输入运算符需要做更多的数据验证工作,例如,我们的输入运算符可能需要检查bookNo是否符规范的格式。例如如果不满足格式要求,我们可以设置failbit但是不能设置badbit
istream & operator>>(istream &is, Sales_data &item)
{
    double price;
    is>>item.bookNo>>item.units_sold>>price;
    if (is)
        item.revenue=item.units_sold*price;
    else
        item=Sales_data();//写入失败,对象被赋予默认的状态
    reutrn is;
}
算术和关系运算符
  • 容器内的对象一般要定义小于,如果定义了==一般也要定义一种关系运算符。
  • 某些情况下类只有符合逻辑的相等运算符,却无法定义符合逻辑的关系运算符。
Sale_data operator+(const Sales_data &r,const Sales_data &l){
    Sales_data sum=r;
    sum.item+=l.item;
    return sum;
}

bool operator==(const data &a,const data&b){
    return a.item1==b.item1 && a.item2==b.item2;
}
//基于等于定义不等于
bool operator!=(const data &a,const data&b){
    return !(a==b);
}
赋值运算符
StrVec &StrVec::operator=(initializer_list<string> il)
{
    auto data=alloc_n_copy(il.begin(),il.end());
    free();
    elements=data.first;
    first_free=cap=data.second;
    return *this;
}

data &data::operator+=(const data &item);
{
    unit+=item.unit;
    revenut+=item.revenut;
    return *this;
}
下标运算符

为了与下标的原始定义兼容,下标运算符通常以所访问元素的引用作为返回值,这样做的好处是下标可以出现在赋值运算符的任意一端。进一步,我们最好同时定义下标运算符的常量版本和非常量版本,当作用于一个常量对象时,下标运算符返回常量引用以确保我们不会给返回的对象赋值。

std::string & operator[](std::size_t n)
{return elements[n]}
const std::string & operator(std::size_t n) const
{return elements[n]}

递增递减运算符
  • 为了区分前置和后置,在后置版本中,存在一个int类型的形参,实际不使用。编译器会自动给这个形参赋值0。
  • 我们可以显式调用一个后置递增版本p.operator++(0)
class StrBlobPtr{
public:
    StrBlobPtr& operator++();//前置运算符
    StrBlobPtr& operator++(int);//后置运算符
}
StrBlobPtr& strBlobPtr::operator++()
{
    check(curr,"increament pass end of StrBlobPtr");
    ++curr;
    return *this;
}
StrBlobPtr& strBlobPtr::operator++(int)
{
    StrBlobPtr ret=*this;
    ++*this;//解引用并且递增,解引用得到的是StrBlobPtr类型的对象,递增调用的是该对象重载的前置递增运算符。
    return ret;
}
成员访问运算符
  • 解引用指向自定义类的指针得到的是一个对象,我们可以自定义,返回对象中的某一个成员变量的值。
  • 解引用运算符原则上可以返回任何类型,然箭头运算符返回的都是地址。我们重载箭头运算符改变的是从哪个对象获取成员,我们不会改变箭头获取对象这一事实。
  • point->mem的执行过程如下所示
    1. 如果point是指针,则我们应用内置的箭头运算符,表达式等价于(*point).mem。首先解引用该指针,然后从所得的对象中获取指定的成员。如果point所指的类型没有名为mem的成员,程序会发生错误。
    2. 如果point是定义了operator->的类的一个对象,则我们使用point.operator->()的结果来获取mem。其中,如果该结果是一个指针,则执行第1步;如果该结果本身含有重载的operator->(),则重复调用当前步骤。最终,当这一过程结束时程序或者返回了所需的内容,或者返回一些表示程序错误的信息。
class StrBlobPtr{
public:
    std::string& operator*() const{
        auto p=check(curr,"diasdf");
        return (*p)[curr];//*p返回的是指向某个对象的指针,该对象是一个vector或者列表,可以用下标运算符。
    }
    std::string* operator->() const{
        return & this->operator*();
    }
}
函数调用运算符
  • 如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象(python中的call魔法方法)。因为这样的类同时也能存储状态,所以与普通函数相比它们更加灵活。

  • 类重写了函数调用运算符我们叫做函数对象,函数对象常常作为泛型算法的实参==(函数对象可以相当于函数使用,任何需要函数的地方都可以传递函数对象)==。

  • labmda是函数对象。编译器会将lambda翻译成未命名的函数对象。

  • lambda通过引用捕获变量时,将由程序负责确保引用对象的存在,编译器创建的未命名函数对象不会存储捕获的成员。相反值捕获变量会被拷贝到lambda中。

  • 标准库定义的函数对象,这些类都被定义成了模板的形式。

c++从入门到精通(三)--面向对象部分_第2张图片

  • 表示运算符的函数对象类常用来替换算法中的默认运算符。在默认情况下排序算法使用operator<将序列按照升序排列。如果要执行降序排列的话,我们可以传入一个greater类型的对象。

    sort(svec.begin(),svec.end(),greater());

  • 此外,如果我们想要使用指针的地址大小来排序,我们只能用标准库提供的less函数类来实现,因为指针之间的<是未定义的行为,所以我们无法自己提供一个函数类来实现这种排序。

重载、类型转换与运算符

**类型转换运算符:**类型转换定义了类类型如何转换为其他类型,type()为目标类型operator type() const;

  • 我们不允许转换成数组或者函数类型,但允许转换成指针(包括数组指针及函数指针)或者引用类型

  • 我们可以定义显示的类型转换explicit operator type() const:,在if ,while,do,for以及其他逻辑判断中,编译器会隐式执行显示类型转换。

  • 无论我们什么时候在条件中使用流对象,都会使用为IO类型定义的operator bool。while语句的条件执行输入运算符,它负责将数据读入到value并返回cin。为了对条件求值,cin被istream operator bool类型转换函数隐式地执行了转换。如果cin的条件状态是good,则该函数返回为真;否则该函数返回为假。

一些转换trick:

  • 对某个给定的类来说,最好只定义最多一个与算术类型有关的转换规则。

  • 不要对两个类定义相互的类型转换

  • 不要定义两个及以上的相同转换源。例如在B类中定义类构造函数从A中转换,又定义了转换为A的类型转换运算符。这种情况下就会产生二义性。此时我们必须显示的调用类型转换运算符或者转换构造函数。

  • 另外如果类定义了一组类型转换,它们的转换源(或者转换目标)类型本身可以通过其他类型转换联系在一起,则同样会产生二义性的问题。

  • 当调用重载函数时,如果两个(或多个)用户定义的类型转换都提供了可行匹配,则我们认为这些类型转换一样好。在这个过程中,我们不会考虑任何可能出现的标准类型转换的级别。下面会出现二义性

  struct A{
    A(int);
  }
  struct B{
    B(double);
  }
  void manip(const &A);//1
  void manip(const &B);//2
  manip(10);//二义性!!!可以调用1也可以调用2
  • 如果我们对同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符,则将会遇到重载运算符与内置运算符的二义性问题。

面向对象-程序设计

介绍

**虚函数:**基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明成虚函数(virtual function)。派生类中函数最后添加关键字override。派生类经常(但不总是)覆盖它继承的虚函数。如果派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似于其他的普通成员,派生类会直接继承其在基类中的版本。

**运行时绑定(动态绑定):**在C++语言中,当我们使用基类的引用(或指针)调用一个虚函数时将发生动态绑定。

基类和派生类

  • 住作为继承关系中根节点的类通常都会定义一个虚析构函数。
  • 关键字virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。
  • 如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数。
  • 成员函数如果没被声明为虚函数,则其解析过程发生在编译时而非运行时
  • 继承时的访问说明符是控制派生类从基类继承而来的成员是否对派生类的用户可见。
  • 我们在派生列表中使用了public,所以Bulk_quote的接口隐式地包含isbn函数(则基类的公有成也是派生类接口的组成部分。,同时在任何需要Quote的引用或指针的地方我们都能使用Bulk_quote的对象。
  • 派生类对象的基类部分与派生类对象自己的数据成员都是在构造函数的初始化阶段(参见7.5.1节,第258页)执行初始化操作的。
  • 派生类的作用域嵌套在基类的作用域之内。可以直接使用变量名(函数名)访问基类中的public和protected成员(成员函数)。
  • 尽管从语法上来说我们可以在派生类构造函数体内给它的公有或受保护的基类成员赋值,但是最好不要这么做。和使用基类的其他场合一样,派生类应该遵循基类的接口,并且通过调用基类的构造函数来初始化那些从基类中继承而来的成员。
  • 如果我们想将某个类用作基类,则该类必须已经定义而非仅仅声明。此该规定还有一层隐含的意思,即一个类不能派生它本身。
  • 在类型名后面添加final阻止其他类继承它class NoDSerived final {}
  • 我们还能把某个函数指定为final,如果我们已经把函数定义成final了,则之后任何尝试覆盖该函数的操作都将引发错误
class Quote{
public:
  Quote()=default;
  Quote(const std::string &book. double sales_price):bookNo(book),price(sales_price){}
  std::string isbn() const {return bookNo;}
  virtual double net_price(std::size_t n) const{ return n*price;}
  virtual ~Quote()=default;
private:
  std::string bookNo;
protected:
  double price=0.0;
}

class Bulk_quote::public Quote{
public:
  Bulk_quote()=default;
  Bulk_quote(const std::string&,double,std::size_t,double);
  Bulk_quote(const std::string &book,double p,std::size_t qty,double disc):Quote(book,p),min_qty(qty),discount(disc){}//调用基类的构造函数
  double net_price(std::size_t)const override;
  
private:
  std::size_t min_qty=0;
  double discount=0.0;
}


  • 如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致。
  • 不能将基类转换为派生类,可以将派生类转换为基类。即使一个基类指针或引用绑定在派生类上(该指针或引用的静态类型是基类),我们也不能执行从基类向派生类的转换
  • 如果我们已知某个基类向派生类的转换是安全的,则我们可以使用static_cast(参见4.11.3节,第144页)来强制覆盖掉编译器的检查工作。
  • 当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分将被忽略掉。

虚函数

  • 普通类型调用虚函数,编译时就会将调用版本确定下来。

  • 引用或指针的静态类型与动态类型不同这一事实正是C++语言支持多态性的根本所在。

  • 基类中的虚函数在派生类中隐含地也是一个虚函数。当派生类覆盖了某个虚函数时,该函数在基类中的形参必须与派生类中的形参严格匹配。

  • 在派生类中编写和基类虚函数有不同形参列表或者返回值的函数是符合语法的,编译器会认为这是不同的函数,但这么做可能和实际冲突。如果我们使用override标记了某个函数,但该函数并没有覆盖已存在的虚函数,此时编译器将报错

  • 注意:如果我们通过基类的引用或指针调用函数,则使用基类中定义的默认实参,即使实际运行的是派生类中的函数版本也是如此。此时,传入派生类函数的将是基类函数定义的默认实参。如果派生类函数依赖不同的实参,则程序结果将与我们的预期不符。

  • 使用作用域运算符可以强制使用虚函数的某个特定版本double un=baseP->Quote::net_price(43)。改代码片段无论baseP实际指向基类还是派生类,都执行基类(Quote)的net_price()函数。通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数机制。

  • **纯虚函数:**和普通的虚函数不一样,一个纯虚函数无须定义(没有实际意义)。我们通过在函数体的位置(即在声明语句的分号之前)书写=0就可以将一个虚函数说明为纯虚函数。其中,=0只能出现在类内部的虚函数声明语句处:值得注意的是,我们也可以为纯虚函数提供定义,不过函数体必须定义在类的外部。

    double net_price(std::size_t) const = 0;
    

抽象基类

  • 含有纯虚函数的类是抽象基类,抽象基类负责定义接口,而后续的其他类可以覆盖该接口。我们不能(直接)创建一个抽象基类的对象。我们可以定义覆盖了抽象基类中纯虚函数的派生类。
  • **重构:**重构负责重新设计类的体系以便将操作和/或数据从一个类移动到另一个类中。用户代码无需改变,不过一旦类被重构了,就意味着我们重新重新编译含有这些类的代码。

访问控制与继承

  • 派生类的成员或者友元只能通过派生类对象访问基类的受保护成。。派生类的成员和友元只能访问派生类对象中的基类部分的受保护成员
  • 派生访问说明符的目的是控制派生类用户对于基类成员的访问权限。
void clobber(S &s){s.j=s.promt=0;}//clobber是S类型的成员函数,可以访问S对象基类的受保护成员
void clobber(B &b){b.promt=0;}//错误, S的成员无法访问基类对象的受保护成员。

class S: private B{//继承自B的成员是private
}
  • ==只有当D公有地继承B时,用户代码才能使用派生类向基类的转换;==如果D继承B的方式是受保护的或者私有的,则用户代码不能使用该转换。不论D以什么方式继承B,D的成员函数和友元都能使用派生类向基类的转换
  • 总结:对于代码中的某个给定节点来说,如果基类的公有成员是可访问的,则派生类向基类的类型转换也是可访问的;反之则不行。
  • **友元与继承:**友元关系无法传递,也不能继承。但是友元(是A的友元)可以访问派生类(B是A的派生类)中友类的成员(可以访问B中A类的成员)。例如P是Base的友元,S继承Base,Base中的prompt成员是protected。则在P类中,可以访问S类的prompt成员。
  • **使用using声明改变成员的可访问性:**using声明语句中名字的访问权限由该using声明语句之前的访问说明符来决定
class Base{
public:
  size_t size() const{return n;}
protected:
  size_t n;
}
class Derived :private Base{
public:
  using Base::size;//私有继承,所以继承来的size是私有的,使用using声明语句改变了这些成员的可访问性。Derived的用户将可以使用size成员,而Derived的派生类将能使用n。
}
  • 默认情况下,使用class关键字定义的派生类是私有继承的
  • 人们常常有一种错觉,认为在使用struct关键字和class关键字定义的类之间还有更深层次的差别。事实上,唯一的差别就是默认成员访问说明符及默认派生访问说明符;除此之外,再无其他不同之处。

继承中的类作用域

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

  • 派生类中定义的名字会屏蔽外层作用域中的名字(基类中的名字)。我们可以使用作用域运算符来使用隐藏的成员。

  • 调用p->mem()的过程

    1. 首先确定p(或obj)的静态类型。因为我们调用的是一个成员,所以该类型必然是类类型。

    2. 在p(或obj)的静态类型对应的类中查找mem。如果找不到,则依次在直接基类中不断查找直至到达继承链的顶端。如果找遍了该类及其基类仍然找不到,则编译器将报错。

    3. 一旦找到了mem,就进行常规的类型检查(参见6.1节,第183页)以确认对于当前找到的mem,本次调用是否合法。

    4. 假设调用合法,则编译器将根据调用的是否是虚函数而产生不同的代码:

      ----如果mem是虚函数且我们是通过引用或指针进行的调用,则编译器产生的代码将在运行时确定到底运行该虚函数的哪个版本,依据是对象的动态类型。

      ----反之,如果mem不是虚函数或者我们是通过对象(而非引用或指针)进行的调用,则编译器将产生一个常规函数调用

  • 有时一个类仅需覆盖重载集合中的一些而非全部函数,此时,如果我们不得不覆盖基类中的每一个版本的话,显然操作将极其烦琐。一种好的解决方案是为重载的成员提供一条using声明语句。==using声明语句指定一个名字而不指定形参列表,所以一条基类成员函数的using声明语句就可以把该函数的所有重载实例添加到派生类作用域中。==此时,派生类只需要定义其特有的函数就可以了,而无须为继承而来的其他函数重新定义

构造函数与拷贝控制

  • 继承关系对基类拷贝控制最直接的影响是基类通常应该定义一个虚析构函数(参见15.2.1节,第528页),这样我们就能动态分配继承体系中的对象了。例如,如果我们delete一个Quote*类型的指针,则该指针有可能实际指向了一个Bulk_quote类型的对象。如果这样的话,编译器就必须清楚它应该执行的是Bulk_quote的析构函数。和其他函数一样,我们通过在基类中将析构函数定义成虚函数以确保执行正确的析构函数版本。
  • 和其他虚函数一样,析构函数的虚属性也会被继承。
  • 之前我们曾介绍过一条经验准则,即如果一个类需要析构函数,那么它也同样需要拷贝和赋值操作。基类的析构函数并不遵循上述准则,它是一个重要的例外。
  • 基类需要一个虚析构函数这一事实还会对基类和派生类的定义产生另外一个间接的影响:如果一个类定义了析构函数,即使它通过=default的形式使用了合成的版本,编译器也不会为这个类合成移动操作
  • 因为基类缺少移动操作会阻止派生类拥有自己的合成移动操作,所以当我们确实需要执行移动操作时应该首先在基类中进行定义。
  • 派生类的拷贝或移动操作必须显式为其基类部分赋值或移动。和构造函数及赋值运算符不同的是,派生类析构函数只负责销毁由派生类自己分配的资源。对象的基类部分也是隐式销毁的。
  • 如果构造函数或析构函数调用了某个虚函数,则我们应该执行与构造函数或析构函数所属类型相对应的虚函数版本。例如在基类的构造函数中使用派生类的虚函数显然是不正确的。

**“继承”构造函数:**派生类能够重用其直接基类定义的构造函数。尽管如我们所知,这些构造函数并非以常规的方式继承而来,但是为了方便,我们不妨姑且称其为“继承”的。

  • 我们可以使用using让派生类继承基类定的构造函数。
  • 和普通成员的using声明不一样,一个构造函数的using声明不会改变该构造函数的访问级别
  • 当一个基类构造函数含有默认实参时,这些实参并不会被继承。相反,派生类将获得多个继承的构造函数,其中每个构造函数分别省略掉一个含有默认实参的形参。例如,如果基类有一个接受两个形参的构造函数,其中第二个形参含有默认实参,则派生类将获得两个构造函数:一个构造函数接受两个形参(没有默认实参),另一个构造函数只接受一个形参,它对应于基类中最左侧的没有默认值的那个形参。
  • 基类有几个构造函数,派生类就会继承几个构造函数,除非基类写了自己的构造函数,并且形参和基类中某个构造函数的形参已完全一致。默认,拷贝和移动构造函数不会被继承。
  • 继承的构造函数不会被作为用户定义的构造函数来使用,因此,如果一个类只含有继承的构造函数,则它也将拥有一个合成的默认构造函数。

容器与继承

  • 当派生类对象被赋值给基类对象时,其中的派生类部分将被“切掉”,因此容器和存在继承关系的类型无法兼容。当我们希望在容器中存放具有继承关系的对象时,我们实际上存放的通常是基类的指针,更好的选择是智能指针
  • c++的特性使得我们无法直接使用对象来进行面向对象编程,而必须使用指针或引用。因此我们需要定义辅助类,把指针和引用相关的操作封装起来,提供给用户可以直接使用对象的接口。

实例文本查询

见本人csdn主页

你可能感兴趣的:(c++,开发语言)