C++学习笔记-面向对象

类和对象

  • 结构体和类的区别 : (主要体现在访问控制) :
    1. Struct 中的成员默认是public, class默认是private
    2. 以struct为继承对象, 默认是public继承, 而以class为继承对象默认为私有继承
      注: struct 和class 都可以在定义的时候用{}赋初值, 只要被赋初值的class成员是public的)
  • 拷贝构造函数被调用的场景: 使用同一个类的其他对象进行初始化
      String func(){
          String b;
          String c(b);     // 显式调用了拷贝构造函数
          String a = b;    // 隐式调用了拷贝构造函数
          return b;        // 由于返回值是String对象, 隐式调用了拷贝构造函数
      }
    
  • 拷贝构造函数和赋值函数的区别
    拷贝构造函数和赋值函数非常容易混淆,常导致错写、错用。拷贝构造函数是在对象被创建时调用的,而赋值函数只能被已经存在了的对象调用。
      String a(“hello”);
      String b(“world”);
      String c = a; // 调用了拷贝构造函数,最好写成c(a);
      c = b; // 调用了赋值函数
    
  • C++初始化对象数组的过程: 首先用默认构造函数创建数组元素, 然后花括号中的构造函数将创建临时对象, 再将临时对象复制到相应的数组元素中.( 因此要创建对象数组必须有一个默认(无参)构造函数)
  • 声明为explicit的构造函数不能在隐式转换中使用
  • 使用new来申请内存, 必须用delete释放内存, 同样地用new []来申请的内存必须用delete [] 来释放
  • 如果将一个对象作为值传递而非作为引用传递( 如调用一个函数 func(Complex c1)而非func(Complex &c1) ),则传递时会产生一个临时对象, 这个临时对象在函数结束时也会调用一次析构函数(但不会调用默认构造函数, 因为该临时对象是使用复制构造函数创建的对象)
  • 一个new动态分配创建的指针如果被delete了, 那么其指向的内存不再是原来的内存, 而是一个未知的区域
    因此在一个类中若需要使用指针变量, 需要注意在复制对象时不能把指针指向的地址值赋值给新的对象, 而应为新的对象创建一块内存, 把原指针指向的内存整块地赋值给新的对象( 因此需要定义一个显示的复制构造函数, 而不能用系统默认的只是按值复制的复制构造函数)
  • 静态成员函数不与特定的对象相关联, 因此该函数只能使用静态成员(属性和函数)
  • 聚合类(Aggregate class)是一种具有某些性质的类, 结构或联合(union):(聚合也是一种类的关系)
    ①所有成员都是公有的
    ②没有定义任何构造函数
    ③没有类内初始值
    ④没有基类和虚函数
  • 自加(自减)运算符重载:
      Test& operator++(Test& val){                    //前递增
          val.a++;
          return val;
      }
      const Test operator++(Test& val,int){    //后递增,int在这里只起到区分作用,没有实际意义
          Test temp(val);//这里会调用拷贝构造函数进行对象的复制工作
          val.a++;
          return temp;
      }
    
  • 内联函数(inline)默认是static的, 因此可以在头文件中定义然后多个cpp文件中包含, 不会出现重复定义的错误
  • C++中的 Interface定义:
    当一个类满足以下要求时, 称之为纯接口:
    • 只有纯虚函数 (“=0”) 和静态函数 (除了下文提到的析构函数).
    • 没有非静态数据成员.
    • 没有定义任何构造函数. 如果有, 也不能带有参数, 并且必须为 protected.
    • 如果它是一个子类, 也只能从满足上述条件并以 Interface 为后缀的类继承.
      接口类不能被直接实例化, 因为它声明了纯虚函数. 为确保接口类的所有实现可被正确销毁, 必须为之声明虚析构函数 (作为上述第 1 条规则的特例, 析构函数不能是纯虚函数). 具体细节可参考 Stroustrup 的 The C++ Programming Language, 3rd edition 第 12.4 节.
  • 构造函数调用顺序:
      父类成员构造函数 -> 父类构造函数 -> 成员构造函数 -> 构造函数
    
  • static/friend函数不可加const修饰符, 因为const修饰符只是限定函数是否能够修改this->*
  • new和delete是运算符,不是函数,因此执行效率高
  • const常量的初始化必须在声明时或者在构造函数初始化列表中初始化,而不可以在构造函数函数体内初始化
  • 给成员变量用 mutable (易变的)修饰, 就可以在const成员函数中修改对象的该成员变量了
  • 一个类中const 限定的成员函数只能由const的该类的对象来调用, 无const限定的成员函数只能由非const的对象来调用

继承和多态

  • 类之间的关系有继承(泛化), 关联, 组合和聚合, 其概念和区别如下:
    • 继承:指的是一个类(称为子类)继承另外的一个类(称为基类)的功能,并增加它自己的新功能的能力,继承是类与类之间最常见的关系。类图中继承的表示方法是从子类拉出一条闭合的、单键头(或三角形)的实线指向基类。
      类的继承在C++中呈现为:
        class B { }
        class A : public B{ }
      
    • 关联:指的是模型元素之间的一种语义联系,是类之间的一种很弱的联系。关联可以有方向,可以是单向关联,也可以是双向关联。可以给关联加上关联名来描述关联的作用。关联两端的类也可以以某种角色参与关联,角色可以具有多重性,表示可以有多少个对象参与关联。可以通过关联类进一步描述关联的属性、操作以及其他信息。关联类通过一条虚线与关联连接。对于关联可以加上一些约束,以加强关联的含义。
      类的关联在C++中呈现为:
        class A{...} 
        class B{ ...}
        A::Function1(B &b) 
        //或A::Function1(B b), A::Function1(B *b) 即一个类作为另一个类方法的参数。
      
    • 聚合:指的是整体与部分的关系。通常在定义一个整体类后,再去分析这个整体类的组成结构。从而找出一些组成类,该整体类和组成类之间就形成了聚合关系。例如一个航母编队包括海空母舰、驱护舰艇、舰载飞机及核动力攻击潜艇等。需求描述中“包含”、“组成”、“分为…部分”等词常意味着聚合关系。
      类的聚合在C++中呈现为:
        class A {...} 
        class B { A* a; .....}
        // 即类B包含类A的指针;
      
    • 组合:也表示类之间整体和部分的关系,但是组合关系中部分和整体具有统一的生存期。一旦整体对象不存在,部分对象也将不存在。部分对象与整体对象之间具有共生死的关系。
      类的组合在C++中呈现为:
        class A{...} 
        class B{ A a; ...}
        // 即类B包含类A的对象
      
      注: 聚合和组合的区别在于:聚合关系是“has-a”关系,组合关系是“contains-a”关系;聚合关系表示整体与部分的关系比较弱,而组合比较强;聚合关系中代表部分事物的对象与代表聚合事物的对象的生存期无关,一旦删除了聚合对象不一定就删除了代表部分事物的对象。组合中一旦删除了组合对象,同时也就删除了代表部分事物的对象。聚合的关键在于”聚”, 只是是一些各自独立的东西聚集在一起.
      关联和聚合的区别主要在语义上,关联的两个对象之间一般是平等的,例如你是我的朋友,聚合则一般不是平等的,例如一个公司包含了很多员工,其实现上是差不多的。聚合和组合的区别则在语义和实现上都有差别,组合的两个对象之间其生命期有很大的关联,被组合的对象是在组合对象创建的同时或者创建之后创建,在组合对象销毁之前销毁。一般来说被组合对象不能脱离组合对象独立存在,而且也只能属于一个组合对象,例如一个文档的版本,必须依赖于文档的存在,也只能属于一个文档。聚合则不一样,被聚合的对象可以属于多个聚合对象,例如一个员工可能可以属于多个公司。
  • 不要使用memset对类的对象进行初始化或放入构造函数, 因为假如类有虚函数或虚继承的话, memset会把虚表指针全部清零, 对类产生破坏
  • 派生类和基类的重要关系 : 基类指针可以在不进行显式转换的情况下指向派生类对象; 基类引用可以在不进行显式类型转换的情况下引用派生类对象.(向上强制转换)
  • 注意关键字virtual只用于类方法的声明时, 不用于类方法的定义时
  • 如果没有关键字virtual, 程序将根据引用类型或指针类型选择方法; 如果使用了virtual, 程序将根据引用或指针指向的对象的类型来选择方法. 假设Brass类和其派生类BrassPlus中都有View函数主程序如下:
      Brass dom("Dominic", 11224);         //Brass是基类
      BrassPlus dot("Dorothy", 12118);   //BrassPlus是派生类
      Brass & b1_ref = dom;
      Brass & b2_ref = dot;       // 基类可以随便引用派生类对象(这里 使用指针也毫无区别)
      b1_ref.View();              // 用Brass类的View()
      b2_ref.View();              // 这里, 假如View()函数不是虚的, 则用Brass类的View(), 即用引用类型(或指针类型)选择方法
                                // 假如View()函数是虚的, 则用BrassPlus类的View(), 即根据引用(或指针指向)的对象选择方法
    
  • 如果要在派生类中重新定义的类方法,通常一律声明为虚方法,这样程序将根据对象类型(符合用户需求)而非引用或指针的类型来选择方法。实际上, 仅将需要在派生类中重新定义的方法设为虚方法
  • 为基类声明一个虚析构函数也是一个惯例, 因为:如果其派生类需要一个析构函数, 在不加virtual声明的情况下, 可能会对派生类的对象调用到基类的析构函数造成错误( 即对派生类使用基类的引用或指针时)
  • 可以使用一个指针数组来存储指向不同类(基类及其各种派生类)的指针,这样就达到了数组中元素可以各不相同的目的(多态)
  • 编译器处理虚函数的方法 : 对于每个类, 编译器都创建一个虚函数地址表(数组) . 给类的每个对象添加一个隐藏成员, 隐藏成员中保存了一个指向函数地址数组的指针, 这种数组称为虚函数表, 虚函数表中存储了为类对象进行声明的虚函数的地址。
    注意:
    • 如果在派生类中未定义新的虚函数, 则该表将保存其基类的表的地址;
    • 若派生类中重新定义了基类的虚函数, 则用该新函数的地址覆盖掉基类的虚函数的地址;
    • 若派生类中声明了新的虚函数, 则该函数的地址也会被加进虚函数表中
  • 友元不能是虚函数, 因为友元不是类成员, 只有类成员才能作虚函数
  • 在派生类中如果重新定义(同名)了基类中的虚函数, 则派生类的对象无法再调用其基类的虚函数, 也就是说(对于派生类对象)基类的该函数被隐藏掉了(即使新的虚函数与基类的虚函数参数表不同也无妨)
  • 如果基类一个函数声明被重载了, 则应在派生类中重新定义所有的基类版本
  • 关键字protected和private的区别在派生类中才表现出来, 派生类无法访问基类的private成员但可以访问基类的protected成员
  • 最好对类的数据成员采用private, 只对某些成员函数才使用protected(如让派生类能访问公众不能使用的内部函数)
  • 使用作用域解析运算符可以访问基类的方法, 但如果要使用基类对象本身, 则需要使用强制类型转换, 如
      const string & Student::Name() const {
          return (cosnt string &) *this ; (这里this是Student类型指针)
      }
    
  • 通常应使用包含来建立has-a关系(非派生类( ; 如果新类需要访问原有的类保护成员, 或需要重新定义虚函数(派生类), 则应使用私有继承
  • 使用保护继承时, 基类的共有成员和保护成员都将成为派生类的保护成员
  • 私有继承和保护继承之间的主要区别主要体现在: 当从派生类再派生出另一个类时, 当前派生类(第二代)使用私有继承则则第一代的共有成员也变成第二代的私有成员了, 第三代类不能访问它们; 然而在第二代类使用保护继承时, 第一代类的公有成员(public)是第二代的保护成员, 第三代仍可以访问
  • 虚继承
    虚基类使得从多个基类相同的类中派生出的对象只继承一个基类对象. 例如, 通过在类声明中使用关键字virtual, 可以使得Worker被用作Singer和Waiter的虚基类(virtual和public次序无关紧要), 即
      Class Singer : virtual public Worker { }
      Class Waiter : public virtual Worker { }
    
    此后, 如果要定义一个以Singer和Waiter为基类的派生类SingingWaiter, 则只需要将其定义为
      Class SingingWaiter : public Singer, public Waiter {}
    
    这样SingingWaiter将只包含Worker的一个副本, 而不是各自引入Singer和Waiter中的Worker副本( Singer和Waiter的定义不加virtual则将会各自引入)
  • 如果一个派生类从两个基类那里分别继承了两个同名函数, 则需要使用域限定符来分别使用这两个函数, 否则无法编译, 会造成二义性
  • 一般需要在子类中重新定义的函数都应该声明为虚函数, 因为只有这样才能正确地根据调用的对象本身而非一个指针或引用的类型来调用相应的函数
  • 一个类只初始化它的直接基类, 也只会调用其直接基类的构造函数
  • 通常using声明只是令某个标识符在当前作用域内可见. 但当在派生类内使用using语句作用于构造函数时(即声明using Base::Base;), 对于基类的每一个构造函数, 编译器都会为派生类生成一个与之对应的(形参列表完全相同的)派生类构造函数. 这些编译器生成的派生类构造函数形如:
      Derived(parms) : base(args) { }
    

异常

  • 标准异常 - Logic errors
Name Comment
logic_error Logic error exception (class )
domain_error Domain error exception (class )
invalid_argument Invalid argument exception (class )
length_error Length error exception (class )
out_of_range Out-of-range exception (class )

其中, logic_error又可细分为:

Name Comment
domain_error Domain error exception (class )
future_error Future error exception (class )
invalid_argument Invalid argument exception (class )
length_error Length error exception (class )
out_of_range Out-of-range exception (class )
  • 标准异常 - Runtime errors
Name Comment
runtime_error Runtime error exception (class )
range_error Range error exception (class )
overflow_error Overflow error exception (class )
underflow_error Underflow error exception (class )
bad_alloc Exception thrown on failure allocating memory (class )
bad_cast Exception thrown on failure to dynamic cast (class )
bad_exception Exception thrown by unexpected handler (class )
bad_function_call Exception thrown on bad call (class )
bad_typeid Exception thrown on typeid of null pointer (class )
bad_weak_ptr Bad weak pointer (class )
ios_base::failure Base class for stream exceptions (public member class )
logic_error Logic error exception (class )
runtime_error Runtime error exception (class )

其中, runtime_error又可细分为:

Name Comment
overflow_error Overflow error exception (class )
range_error Range error exception (class )
system_error System error exception (class )
underflow_error Underflow error exception (class )
  • 继承标准异常exception并允许字符串参数构造函数的自定义异常类:

      class queue_exception : public std::exception {
      private:
          std::string msg;    
      public :
          queue_exception(const std::string & str) : msg(str){ } 
          const char * what() const noexcept{
              return ("queue_exception: " + msg).c_str();
          }
    
      };
    
  • 异常的捕获不会调用类型转换, 如throw 'a'只有
    catch(char c){}
    能够捕获到, 而
    catch(int i){}
    无法捕获
  • 捕获异常catch的参数列表中的参数类型最好是引用, 若非引用的话有可能出现对象切割(object slicing), 比如
      `throw derived_exception("asd");`
    
      `catch(base_exception e)`
    
    来捕获, 则derived_exception中的某些base中没有的函数, 或者是derived_exception中重写的函数无法被使用
    而用
      `catch(base_exception e)`
    
    则可以正常使用这些函数
  • 抛出异常时, 对于存在析构函数的对象, C++中通过堆栈反解将已经定义的对象进行析构,但是有一个例外就是构造函数中如果出现了异常,那么这会导致已经分配的资源无法回收