C/C++知识点总结(3)

时间:2022-09-05 10:07:04

操作符重载函数(Operator Overload Function)的基本概念:

  • 目的是以与对待内置数据类型相同的方式对待用户自定义类型(程序执行速度会受到影响),限制是不能随意选择函数名和参数个数(必须与重载的基本类型运算符保持一致);

  • 编译器首先检查的表达式中的左操作数是否为对象类型,若是则在其类定义中搜寻对应的operator开头的方法,最后进行函数名字替换,参数类型检查等等操作(作为重载操作符函数的参数,一般使用const的索引类型,除非某个参数需要改变);

  • 如果左操作数为数值类型,则检测右操作数是否为类类型,若是,则在友元函数中寻找对应的重载操作符函数,如果没有相关定义则为错误;如果左右操作数都不是程序员定义类型,则按照基本类型运算符进行操作;

  • 重载操作符函数解决的只是一个可读性问题,也就是让对象类型之间的操作看起来也像内置类型,仅此而已,编译器会将这种类型的操作替换回函数调用的形式

    z=x+y; ->>> z=operator+(x,y);或者z=x.operator+(x,y)

  • 赋值操作符重载函数需要注意三个地方:

    为了不发生内存共享问题,需要重新创建动态内存,并复制传入对象的动态内存;

    为了不发生自我赋值时的内存删除问题,需要检查是否传入对象为当前对象本身;

    为了实现链式表达式,需要将返回值设置为引用对象类型

    1 String& String::operator=(const String& s) {
    2 if(&s == this) return this;
    3 delete str;
    4 len=s.len;
    5 str=new char[len+1];
    6 if(str==NULL) exit(1);
    7 strcpy(str, s.str);
    8 return this;
    9 }

 

重载操作符函数定义为类成员函数:

  • 由于编译器会自动添加指向当前对象的this指针,所以定义他们的时候代表左操作数的参数可以缺省;

  • 如果我们在类成员函数和全局函数中都定义了针对同一个类型的重载操作符函数,当使用操作符表达式时会产生二义性,编译错误;但使用显示的函数调用则不会有错;较好的解决办法是将重载操作符函数定义为类型的友元函数:

    1 class Complex {……Complex& operator+(const Complex &b) const {……}};
    2 Complex& operator+(const Complex &a, const Complex &b) {……}
    3 Complex a,b,c,d;
    4 c=a+b;//二义性错误
    5 c=a.operator+(b); //正确
    6 c=operator+(a,b); //正确
    7 d=(a.operator+(b)).operator+(c); //正确

     

  • 如果作为类成员函数出现的重载操作符函数,如果不会修改当前对象的任何类成员变量,则可将函数声明为const(函数的声明和定义两个地方都需要添加);如果某个参数不会在函数内部改变,则良好的设计为加上const参数;
  • 为了支持混合类型运算的隐式转换,转换构造函数为一种解决办法,重载操作符函数为另一种解决办法;
    1 Rational r; r+=4;
    2 Rational(int x);//将4转换为临时匿名Rational对象,然后调用重载操作符函数
    3 void operator+=(Rational &r, Rational &x);
    4 void Rational::operator+=(int x);//r.operator+=(4);
    5 void operator+=(Rational &r, int x);//::operator+=(r,4);

     

  • 不要对实现了重载运算符函数的数值类的转换构造函数使用explicit;否则在函数调用中试图使用基本类型参数替代该类型的对象的操作就会出错(只能显示转换)

    为了使得某个类对象既可以与类对象进行操作,也可以与基本类型进行操作,可以进行如下设计:

    1 explicit Rational(int x);//转换构造函数
    2 void Rational::operator+=(Rational &r);
    3 Rational a,b; a+=b; a+= Rational(5);

    这样的设计就可以使得r既可以是类对象类型,也可以是内置数值类型;
    作为类成员的重载操作符函数只允许左边的操作数为对象类型,为了使得内置数值类型也可以为左边的操作数,可以进行如下设计:

    1 class Rational {
    2 friend Rational& operator+=(const Rational &x, const Rational &y);
    3 friend Rational& operator+=(const Rational &x, long y);
    4 friend Rational& operator+=(long x, const Rational &y);
    5 };
    6 //全局友元函数
    7 Rational& operator+=(const Rational &x, const Rational &y) {……}
    8 Rational& operator+=(const Rational &x, const long y) {……}
    9 Rational& operator+=(long x, const Rational &y) {……}

    友元函数除了可以访问目标类所有成员之外,与目标类没有任何关系;重载所有可能使用到的操作符函数就可以避免隐式调用类型转换构造函数,加快程序速度;

     C/C++知识点总结(3)

 

按值传递对象时容易发生的错误(共享同一份动态内存,多次调用析构函数):

  • 隐式调用拷贝构造函数,其将使用实际参数对象初始化形式参数对象;或者赋值操作符重载函数,使用实际参数对象赋值形式参数对象;(所以如果将拷贝构造函数,或者赋值操作符重载函数显式定义为private函数则可禁止按值传递对象)

  • 当对象内部有成员指针变量指向动态堆内存,如果该对象按值传递,则函数内部的形式参数对象会与实际参数对象共享同一份动态堆内存数据;

  • 形式参数对象被销毁时会调用其析构函数将内部成员指针变量指向的动态堆内存销毁,而这正是与实际参数对象共享的那一份的动态堆内存;

  • 当实际参数对象被销毁的时候会再次调用析构函数试图再次销毁已经被销毁的动态堆内存,此时会发生重复删除内存的错误;

 

动态管理内存的类设计时,需要考虑如下函数:

  • 缺省构造函数:当提供其他构造函数后,系统不会主动提供缺省构造函数

    Rational(int a=0, int b=1);

  • 拷贝构造函数:系统提供的拷贝构造函数仅为浅赋值,遇到动态内存问题可能出问题

    Rational(const Rational &r);

  • 多个转换构造函数:可以使用多种其他类型初始化本类类型的对象

    Rational(const char *r);

    Rational(int c);

    ……

  • 多个赋值运算符重载函数:使用多种其他类类型对本类类型对象进行赋值,并可以避免在调用赋值运算符重载函数时调用转换构造函数生成适合类型

    Rational& operator=(const Rational& r);

    Rational& operator=(int c);

    ……

  • 析构函数:释放资源

    ~Rational();

 

数据通信方式(对于C++中不同程序段之间的通信而言,总是使用最低程度的耦合度:首先考虑使用函数内部的局部变量,然后考虑使用类成员变量,其次考虑使用函数形式参数,最后才考虑使用全局变量)

  • 全局变量或者静态变量

  • 方法参数

  • 类成员变量

  • 方法的局部变量

 

复合对象的创建和销毁:

C/C++知识点总结(3)

  • 创建对象时,其构造函数在所有成员变量创建(分配内存,初始化)后才进行调用;并且成员变量的创建顺序是按照其在类定义中声明的先后顺序进行;

    如果有继承关系,则基类优先于所有派生类进行创建(也就是在调用派生类的构造函数之前进行调用),如果没有在成员初始化列表显示指定调用基类的哪一个构造函数,则缺省调用无参构造函数(所以无参构造函数还有这一功能),但是最好使用成员初始化列表显示调用,从而避免不必要的函数调用;

  • C++类定义中不能对基本和类变量进行显示初始化;数值变量保持未初始化,类变量使用无参缺省构造函数进行;而类构造函数是在执行完所有数据成员的构造函 数之后执行(所以作为成员变量的类对象需要有无参构造函数,否则会发生错误);唯一的例外是static const变量可以在类定义的时候初始化;

  • C++类成员对象有两种设置值的方式:一种是利用无参构造函数初始化,再通过其他方式赋值;另一种是利用成员初始化列表调用指定参数的构造函数初始化;前者效率低下,成员对象被多次设置,后者仅设置一次;

    1 class Rational {
    2 Point p1;Point p2;
    3 Rational(const Point& pp1, const Point& pp2) {p1=pp1;p2=pp2}
    4 };

    上述类定义中,p1和p2会首先调用其无参缺省构造函数进行初始化,然后调用赋值操作符重载函数进行赋值:

    1 class Rational {
    2 Point p1; Point p2;
    3 Point p3&;
    4 const int WEIGHT;
    5 Rational(const Point& pp1, const Point& pp2, Point &pp3, int w):
    6 p1(pp1),P2(pp2),p3(pp3),WEIGHT(w) {}
    7 };

    上述类定义中,p1和p2直接调用其拷贝构造函数进行初始化(从而避免不必要的构造函数调用);成员初始化列表不会改变类成员变量的初始化顺序(由类定义顺序决定),一般不在构造函数声明的地方定义,而是在实现的地方定义;

    如果某个类成员变量为const类型,则必须使用成员初始化列表,否则其不能再赋其他值;如果某个类成员变量为引用类型,则也必须使用成员初始化列表,否则其不能其赋其他值;

    一个类型使用它自身作为类成员变量的时候,不能直接使用Rational r;因为类成员变量在类创建之前创建,但此时Rational并没有创建,所以语法错误;但可以使用Rational *ptrr;或者Rational &refr;实现对自身类型的引用;

    如果将某个类成员变量声明为static,则即使他们声明为private成员,也可以在类定义之外进行初始化:(可使用类作用域运算符访问,也可使用目标对象访问)
    int Rational::count=0; p1.count=9;

    有继承关系的派生类的实例在内存中不仅保存自身的所有非static成员变量,还保存所有基类的所有非static成员变量(包括private,protected和public)

  • 销毁对象时,首先调用对象的析构函数,执行资源释放操作,然后以成员变量初始化创建相反的顺序进行销毁(也就是从类定义中最后一个成员变量开始进行销毁);如果有继承关系,在派生类销毁完毕之后再进行派生类的销毁;

  • 成员初始化列表的目的是避免在复合类构造函数被调用之前,编译器调用成员对象类的缺省无参构造函数(如果没有的话会报错),而在构造函数中又会重新进行赋 值,所以在成员初始化列表中强制要求调用的构造函数类型。成员初始化列表防止语法错误并改善性能;函数声明中为参数提供的缺省值只能自右向左依次进行,调 用的时候实际参数赋值的顺序为自左向右依次进行;成员初始化列表使得具有继承关系的类创建更有效率;

 

调用一个派生类(Drived class)实例的某个函数时,实际调用可能为:

  • 调用的函数来自基类,并在派生类中无定义;

  • 调用的函数来自基类,并在派生类中重新定义(如果函数名和标签完全相同,则为over-write,如果仅函数名相同,标签不同,则为隐藏);

  • 调用的函数来自派生类,基类无定义;

  • 虚函数(涉及多态,指针),基类定义的函数由virtual关键字,并且派生类中有函数标示符完全相同的实现;

 

C++的继承方式:(不同的继承模式实质上是对基类中成员访问权限的一种修改,只能向可见范围变小的方向修改,也就是说即使使用private继承,也可 以通过public: Base::publB将基类的成员调整为原始访问权限,但是不能放大权限;这些修改影响子类,子类的客户类和子类的子类。虚拟函数仅在public继承 模式下才有效)

  • public:公共继承:客户在子类中对基类中成员的访问权限与基类保持一致,子类中可以访问父类中的public和protected成员;子类的客户类可以访问子类和基类中的public成员;完全的类扩展;

  • protected:受保护继承:从父类中继承的public和protected成员在子类中变成protected,也就是说子类的客户仅能访问子类 的public成员,子类对象可访问父类中的public和protected成员,常用于基类的一些方法仅提供派生类访问;

  • private:私有继承:从父类中集成的public和protected成员在子类中变成private,也就是说即使是子类,子类的子类也不能再访问这些父类的成员变量,常用于派生类完全重新定义基类的方法;

    C/C++知识点总结(3)

 

派生类中非虚拟函数的查找过程以及函数名隐藏问题:

  • 从当前类定义的类成员函数表开始向基类方向进行搜索,一旦找到对应函数头的函数定义则结束搜索过程;

  • 这个过程中一旦编译器找到函数名相同的函数声明就不再搜索,如果参数列表不匹配则报错而不会继续搜索(即使基类中有其他相同函数名的函数),这就是函数名 覆盖;(函数名相同的设计方法,对于同一作用域是函数名重载,对于不同作用域是隐藏,也就是同一个类定义与父子类定义中);

  • 如果直到基类都没有找到对应的函数名,则搜索全局函数,以及来自其他文件的extern函数的范围;否则调用失败:

    函数重载仅仅适用于同一类定义中,相同函数名有不同的参数列表,这时候编译器会根据参数列表选择正确的函数。但是在继承关系中,基类和派生类中也可能出现 这种函数名相同但是参数列表不同的情况,但是这个时候编译器并不会使用重载函数的策略,而是自底向上地搜索函数名相同的函数,一旦找到之后就不再寻找,此 时如果参数列表不匹配则会报错。这是一种函数名覆盖的机制。这应该和继承关系中的函数重写区别(函数名,参数列表,返回值完全相同),也应该和虚拟函数区 别(使用virtual修饰的重写函数);解决这个问题的设计如下:

    1 class Account {void deposit(double amount) {……}}
    2 class CheckingAccount: public Account {
    3 void deposit(double amount) {Account::deposit(amount) {……}
    4 void deposit(double amount, double free) {……}
    5 };
    6
    7 CheckingAccount ca;

    当ca实例试图调用Account的deposit(double)方法时,如果CheckingAccount没有定义 deposit(double),则客户只能调用deposit(double, double),父类的deposit(double)被隐藏;因此上述设计在CheckingAccount中定义一个 deposit(double),然后再调用基类的方法,注意一定需要使用作用域运算符Account::deposit(double),否则调用自 身;