美文网首页
【C++温故知新】详解C++中的类和对象

【C++温故知新】详解C++中的类和对象

作者: 超级超级小天才 | 来源:发表于2019-08-27 17:10 被阅读0次

    这是C++类重新复习学习笔记的第 六 篇,同专题的其他文章可以移步:https://www.jianshu.com/nb/39156122

    类与接口

    类是一种将抽象转换为用户定义类型的C++工具,它将数据表示和操作数据的方法组合成一个整洁的包。接口提供给我们从外部访问类与类内的成员和方法的一个途径。

    一般对一个类的典型的实现策略是:将接口(类的定义)放在头文件中,将其实现(类方法的代码)放在源代码文件中

    类的声明框架

    class ClassName
    {
    private:
    // some private variables and functions
     
    public:
    // some public variables and functions
    };
    

    访问控制

    访问限定符有三个:privatepublicprotected,它们规定了修饰的变量和方法能够被访问的范围,在没有声明时,默认是private的。

    这里先对三个访问限定词做一个比较全面的介绍:

    • private :
      • 类(基类)自身的成员函数
      • 类(基类)友元的成员函数
    • public :
      • 基类自身的成员函数
      • 基类友元的成员函数
      • 基类所产生派生类的成员函数
      • 基类所产生的派生类的友元函数
      • 其他的全局函数
    • protected :
      • 基类的成员函数
      • 基类的友元函数
      • 基类派生类的成员函数

    例如一个类:

    一个类的结构

    类的成员函数的实现

    类的成员函数和一般的函数实现基本相同,还要增添如下两点:

    • 需要使用 :: 符号(作用域解析运算符)来标识这个函数是属于哪一个类的,因为不同的类可以有相同名称的函数
    • 类的方法可以访问类内的 private 的组件
    int ClassName::myFunction(double a);
    
    • 类的成员函数也可以是内联的,只要加上关键词 inline 即可
    • 类的成员函数可以在类内定义时同时完成逻辑,也可以在类的外部定义

    类的使用

    类的实例化和一般的数据类型相同,调用类实例下的某个成员函数或者变量使用 . 点。

    ClassName myClassInstance;
    myClassInstance.aFunction();
    

    类的构造函数和析构函数

    构造函数

    类的构造函数需要和类同名,是在类实例化的时候调用的,在实例化一个类的时候,虽然我们没有显示地声明,但是还是调用了构造函数,而C++对每一个类都有默认的构造函数,就是不接受任何参数,什么都不做,也无返回值。我们可以定义自己的构造函数并且调用它。

    例如一个类MyClass的定义如下:

    class MyClass
    {
    private:
        int myInt;
        double myDouble;
    public:
        MyClass(int mi, double md) { myInt = mi; myDouble = md;};
        MyClass() { myInt = 1; myDouble = 0.2;};
    }
    

    这里我们使用了一个包含两个参数的构造函数,它的作用是对两个private的成员变量赋值。

    构造函数不能像其他成员函数一样使用对象(类的实例)来用点调用,因为构造函数是在实例化类的时候就调用的,比如如下的调用方式:

    MyClass myClass = MyClass(1, 0.2);
    MyClass myClass(1, 0.2);
    MyClass * myClassPoint = new MyClass(1, 0.2);
    

    如果是使用的默认构造函数或者构造函数没有参数的话,可以直接声明对象而不显示地调用构造函数,比如我们的类中还有一个重载的没有参数的构造函数,它可以这样被调用:

    MyClass myClass; // 隐式调用
    MyClass myClass = MyClass(); // 显示调用
    MyClass * myClass = new MyClass(); // 隐式调用
    MyClass myClassFunction(); // 这是一个返回值是MyClass的函数
    

    析构函数

    用构造函数创建对象后,程序负责跟踪该对象,直到其过期为止,对象过期时,程序将自动调用一个特殊的成员函数,即析构函数。

    析构函数用于完成清理工作,所以非常有用,例如如果构造函数用new分配了内存,则可以在析构函数中用delete释放内存。

    默认的析构函数是什么都不做的。我们以可以显示地定义自己析构函数,析构函数是一个~符号加上类名来定义的,析构函数何时调用是取决于编译器的。

    class MyClass
    {
    private:
        int myInt;
        double myDouble;
    public:
        MyClass(int mi, double md) { myInt = mi; myDouble = md;};
        MyClass() { myInt = 1; myDouble = 0.2;};
        ~MyClass() { cout << "bye!"; };
    }
    

    const成员函数

    const成员函数是指,保证该成员函数不会改变调用的对象,声明和定义const成员函数需要将const限定符加在成员函数的后边:

    void show() const;
    void MyClass::show() const
    {
        // function body
    }
    

    以这种方式声明和定义的类函数即const成员函数,应该尽可能地将成员函数修饰为const,只要该类的方法不修改调用对象。

    this指针

    this指针在类的成员函数中,用来作为指向调用类对象自身的指针,即它指向自己的类的地址。我们上面的构造函数中的 myInt = mi; 这一语句,其实这里的 myInt 就是 this->myInt 的简写,因为在类中,可以直接用成员变量简单地替换 this-> 成员变量。

    this指针在只操作自身类内成员的时候不会有特别多的作用,因为都可以省略它,但是一旦我们的成员函数涉及到两个及以上的类的对象时,this就发挥了很大的作用。例如我们有一个compare函数,用于比较两个MyClass类的实例的哪一个的myInt值更大,那么我们必然需要另一个MyClass的实例作为参数,然后让它的myInt和自己的myInt比较,然后返回myInt较大的那个MyClass的引用,所以可以这样声明这个函数:

    const MyClass & MyClass::compare(const MyClass & myClass) const;
    

    函数定义中涉及到三个const:

    • 第一个const:表明返回值是一个MyClass,显然不能被改变,所以可以时const的
    • 第二个const:传入的MyClass实例只是用于比较的,不需要改变,所以使用const
    • 第三个const:由于成员函数不改变调用类对象,所以是const的成员函数

    比较myInt的函数可以使用this来这样实现:

    const MyClass & MyClass::compare(const MyClass & myClass) const
    {
        if(myClass.myInt > this->myInt)
            return myClass;
        else
            return *this;
    }
    

    很显然,上边的 this->myInt 可以使用 myInt 直接简写,而返回自己调用类对象的时候,就只能用 this 来称呼了,而且需要注意的是,返回的是一个MyClass的引用,从而需要使用*this而不是直接返回this,因为this指针

    对象数组

    类和其他数据结构一样,都可以创建数组,对象的数组即可以存储多个类对象,只需要像下边这样声明它们:

    MyClass myClasses[3];
    myClasses[0].show();
    myClasses[1].compare(myClasses[2]);
    

    运算符重载

    运算符重载即将C++中的运算符重载扩展到用户自定义的类型,例如,+这个运算符,只能用于整形、浮点型、字符串等基本的数据结构相加,但是我们可以通过用户的定义,将其用于两个类的对象相加,两个数组相加等等,编译器会根据操作数和目的数的类型决定使用哪种定义。

    运算符重载的写法

    运算符重载的格式为:

    operator op (arguments);
    

    比如:

    operator +( ); // 重载+运算符
    operator *( ); // 重载*运算符
    operator [ ]( ); // 重载[]运算符
    

    一个运算符重载的例子

    假设我们有一个时间类Time,由两个私有成员变量 hours、minutes 来代表小时和分钟,我们来实现Time类对象的相加逻辑。

    class Time
    {
    private:
        int hours;
        int minutes;
    public:
        Time;
        Time(int h, int m=0);
        Time operator + (const Time & t) const;
    };
     
    Time::Time()
    {
        hours = minutes = 0;
    }
     
    Time::Time(int h, int m)
    {
        hours = h;
        minutes = m;
    }
     
    Time Time::operator + (const Time & t) const
    {
        Time sum;
        sum.minutes = minutes + t.minutes;
        sum.hours = hours + t.hours + sum.minutes / 60;
        sum.minutes %= 60;
        return sum;
    }
    

    使用这个重载的+运算符可以将两个Time的对象像其他一般数据类型一样进行相加:

    Time time1;
    Time time2;
    Time total = time1 + time2; 
    

    运算符重载的限制

    多数C++运算符都可以用这样的方式重载。重载的运算符(有些例外情况)不必是成员函数,但必须至少有一个操作数是用户定义的类型。C++运算符重载的限制如下:

    • 重载后的运算符必须至少有一个操作数是用户定义的类型,这将防止用户为标准类型重载运算符因此,例如不能将减法运算符重载为计算两个 double 值的和,而不是它们的差。
    • 使用运算符时不能违反运算符原来的句法规则。例如,不能将求模运算符(%)重载成使用一个操作数的运算
    • 不能修改运算符的优先级。例如,如果将加号运算符重载成将两个类相加,则新的运算符与原来的加号具有相同的优先级
    • 不能创建新运算符。例如,不能定义 operator **() 函数来表示求幂
    • 不能重载下面的运算符
      • sizeof :sizeof 运算符
      • . :成员运算符
      • * :成员指针运算符
      • :: :作用域解析运算符
      • ? : :条件运算符
      • typeid:一个RTTI运算符
      • const_cast:强制类型转换运算符
      • dynamic_cast:强制类型转换运算符
      • reinterpret_cast:强制类型转换运算符
      • static_cast:强制类型转换运算符
    • 大多数运算符都可以通过成员函数或者非成员函数进行重载,但是如下的运算符只能通过成员函数进行重载:
      • =:赋值运算符
      • ( ):函数调用运算符
      • [ ]:下标运算符
      • ->:通过指针访问类成员运算符

    可以重载的运算符

    + - * / % ^
    & ` ` ~= ! = <
    > += -= *= /= %=
    ^= &= ` =` << >> >>=
    <<= == != <= >= &&
    ` ` ++ -- , ->* ->
    () [] new delete new[] delete[]

    友元函数

    类的友元函数是非成员函数,其访问权限与成员函数相同。

    一个友元函数的例子

    回到上面的Time类,我们重载运算符:将运算符重载成一个double值乘以一个Time类:

    Time Time::operator * (const double d) const
    {
        Time result;
        long totalMinutes = hours * d * 60 + minutes * d;
        result.hours = totalMinutes / 60;
        result.minutes = totalMinutes % 60;
        return result;
    }
    

    显然调用上述*的重载需要这样:

    Time A();
    Time B(1, 20);
    A = B * 2.5;
    

    相当于调用了这样的运算符重载的成员函数:

    A = B.operator*(2.5);
    

    但是,问题来了,如果使用 A = 2.5 * B 就无法成功,这似乎违背了乘法的分配律,这一点虽然并不有违于C++的语法,但是貌似并不用户友好,我们需要告诉使用的人只能用第一种方式而不能用第二种方式。解决这个问题有两个方法:

    • 使用一个非成员函数来定义反写的情况:
    Time operator * (double d, const Time & t)
    {
        return t * m;
    }
    

    这种方式不失为是一种非常好的方法,而且如果有所修改,只需要修改类内的运算符重载即可。

    • 使用友元函数
      和上述的思想类似,我们可以定义一个非成员函数,然后这样的重载运算符,从而定义一个double乘以一个Time类对象的操作:
    Time operator * (double d, const Time & t);
    

    但是问题在于类外的非成员函数无法访问类的私有变量。所以友元函数的作用在于可以访问类的私有成员,但是他是一个非成员函数。

    创建友元函数

    创建友元函数的第一步是将其原型放在类声明中,并在原型声明前加上关键字 friend:

    friend Time operator*(double m, const Time t);
    

    该原型意味着下面两点:

    • 虽然 operator*() 函数是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用
    • 虽然 operator*() 函数不是成员函数,但它与成员函数的访问权限相同

    第二步是编写函数定义。因为它不是成员函数,所以不要使用 Time:: 限定符。另外,不能在定义中使用关键字 friend

    Time operator * (double d, const Time & t)
    {
        Time result;
        long totalMinutes = hours * d * 60 + minutes * d;
        result.hours = totalMinutes / 60;
        result.minutes = totalMinutes % 60;
        return result;
    }
    

    上述定义后即可使用如下的语句来使用乘法:

    A = 2.5 * B;
    

    相当于调用友元函数:

    A = operator*(2.5, B);
    

    成员函数和非成员函数的选择

    对于一般的运算符重载,比如+和-这种不会出现乘法那种左右交换的问题的,有两种解决方式:

    Time operator + (const Time & t) const;
    friend Time operator + (const Time & t1, const Time & t2);
    

    第一种方式是通过this隐式地传递一个参数,另一个使用函数参数显示地传递;第二种方式是两个参数都显示地通过参数传递。在调用 T1 = T2 + T3 时,会分别编译成如下的形式:

    T1 = T2.operator+(T3);
    T1 = operator+(T2, T3);
    

    但是,两种方式不能同时定义,只能选择其中一个,否则会引发二义性的编译错误,基于乘法的例子,显然使用友元函数比较通用。

    类的自动转换和强制类型转换

    强制类型转换

    C++允许一些强制类型转换,比如强制将double值转换成int值,把double的2.5转换成int会成为2从而丢失0.5。但是如果用户希望进行强制转换只需要使用如下的方式:

    targetType valueName = (targetType) value;
    targetType valueName = targetType (value);
    

    使用构造函数进行类的自动转换

    假设我们有一个类

    class MyClass
    {
    private:
        int myInt;
        double myDouble;
    public:
        MyClass(double d);
        MyClass(int i, double d);
        MyClass();
        ~MyClass();
    }
     
    MyClass::MyClass(double d)
    {
        myDouble = d;
        myInt = 0;
    }
     
    MyClass::MyClass(int i, double d)
    {
        myDouble = d;
        myInt = i;
    }
     
    MyClass::MyClass()
    {
    }
    

    然后我们尝试将一个double值赋给一个MyClass类对象:

    MyClass myClass;
    myClass = 2.5;
    

    这是可以的,首先创建了一个MyClass的对象,然后使用2.5将其初始化,实际上是使用了第一个构造函数 MyClass(double),这是一个隐式转换的过程,不需要进行强制转换。

    只有接受一个参数的构造函数才能作为转换函数,如果像第二个构造函数那样有两个参数,不能用来转换类型,但是如果第二个参数有默认参数,就可以:

    MyClass(int i, double d = 1.5);
    

    这个可以将一个int值隐式地转换成MyClass类型。

    如果不希望编辑器进行这种隐式转换,可以使用explicit关键词修饰构造函数,这样就无法使用该构造函数进行类型转换:

    explicit MyClass(double d);
    

    这样会关闭隐式转换,但依然允许显示转换,即使用显式地强制转换:

    MyClass myClass;
    myClass = MyClass(2.5);
    myClass = (MyClass)2.5;
    

    转换函数

    上边提到了隐式或者显式地将基本数据类型的数据转换成类对象,接下来的问题是如何将一个类对象转换成其他的基本数据类型,这一点可以通过转换函数来实现。转换函数是用户定义的强制类型转换,需要这样定义:

    operator dateType();
    

    需要注意的是:

    • 转换函数必须是类方法
    • 转换函数不能指定返回类型
    • 转换函数不能有参数

    比如我们将MyClass转换为一个double类型的变量,需要这样一个成员函数:

    operator double();
     
    MyClass::operator double()
    {
        return myDoble;
    }
    

    然后就可以这样使用类型转换了:

    MyClass myClass(1, 2.5);
    double myDouble = (double) myClass;
    double myDouble = double (myClass);
    

    复制构造函数

    复制构造函数接受其所属类的对象作为参数。例如,MyClass类的复制构造函数的原型如下:

    MyClass(const MyClass &);
    

    在下述情况下,将使用复制构造函数:

    • 将新对象初始化为一个同类对象
    • 按值将对象传递给函数
    • 函数按值返回对象
    • 编译器生成临时对象

    如果程序没有使用(显式或隐式)复制构造函数,编译器将提供原型,但不提供函数定义;否则,程序将定义一个执行成员初始化的复制构造函数。也就是说,新对象的每个成员都被初始化为原始对象相应成员的值。如果成员为类对象,则初始化该成员时,将使用相应类的复制构造函数。

    复制构造函数

    复制构造函数接受其所属类的对象作为参数。例如,MyClass类的复制构造函数的原型如下:

    MyClass(const MyClass &);
    

    在下述情况下,将使用复制构造函数:

    • 将新对象初始化为一个同类对象
    • 按值将对象传递给函数
    • 函数按值返回对象
    • 编译器生成临时对象

    如果程序没有使用(显式或隐式)复制构造函数,编译器将提供原型,但不提供函数定义;否则,程序将定义一个执行成员初始化的复制构造函数。也就是说,新对象的每个成员都被初始化为原始对象相应成员的值。如果成员为类对象,则初始化该成员时,将使用相应类的复制构造函数。


    转载请注明出处,本文永久更新链接:https://blogs.littlegenius.xin/2019/08/27/【C-温故知新】六类和对象/

    相关文章

      网友评论

          本文标题:【C++温故知新】详解C++中的类和对象

          本文链接:https://www.haomeiwen.com/subject/cazeectx.html