(Boolan) C++面向对象高级编程(一)

字数 3467阅读 190

感谢侯捷老师的悉心讲授的课程,让我在对很多东西上有了更深层次的认识。

我呢,是一个非计算机专业毕业的本科生,毕业后带着对程序感兴趣的后知后觉开始学习编程,也不是抱着以工作的目的导向去的,学的比较杂,也缺乏系统性。算算日子,距离第一行java代码已经过去两年有余了,对于飞CS的我来说,坚持到今天也算不易。但是用了这么就的“面向对象编程”,但自己其实不能太说清其本质到底为何物。这也算是我正规划的第一步吧,再次感谢Boolan网和侯捷老师。那么我也就来简单的分享一下我所学到的知识吧,毕竟非专业出身,如果其中有错误的地方,希望大家能够指出来,谢谢大家。

Class是怎么来的

对于面向对象语言来说可能很重要的任务就是和class打交道吧(至少我接触的java、c++、python都是和class打交道的),但是在C语言中却没有见到过这个东西,最多也就有struct而已。

其实class对于认识世界来说是更加一致的,比如我们通常会把生物划分为动物、植物、微生物,把动物又划分为哺乳类、两栖类等等。我们在认识世界的时候往往会把具体的事物,比如猫、狗、人等抽象看待,找出其共性,再把不同点加入到他们自己的属性中去,就形成了“界门纲目科属种”这样的生物学划分规律。所以,也就是说,类和类之间应该有关系(比如人和猫都属于哺乳类动物),那么这些关系之间会有一些想通的属性(比如,人和猫都喝奶长大,都会运动等等)。但,不同种类也具有特殊的属性,比如猫有毛,人就没有等等。
而软件也应该对现实事物的一种抽象表现形式的描述,那么类就可以很好的把各种属性进行隔离并描述不同类之间的关系。比如,人和猫都具备喝奶的属性,但猫具备丰富的毛发,而人却不具备这个属性。因此,C++中使用class来隔离部分数据,将不同的数据分隔开来。同样,针对不同的属性,也就应该具有针对这个属性的响应操作方法(成员函数),比如,猫浓密毛发这个属性,那么他就具备“舔毛”的这个操作毛发的方法(函数),人没有浓密的毛发则就不需要这个函数了。

因此,C++相较于C增加重要的概念class,用这个概念来让程序能够使用更加抽象的方式(属性+操作属性的函数(方法))来描述这个世界。

什么是面向对象(Object Oriented)

其实无论Java还是C++的编程过程中,最重要的需要设计各种各样的class,对于Java来说,C++的class要复杂一些,C++ class可以分为“带有指针成员变量的的class”和“不带指针变量的class”。而,对于每次设计出来的单一class来说,这就属于一种“基于对象(Object Based)”的编程。

如果再拿刚才我说的人和猫来看,对于我们需要抽象一个class来描述这个类别的时候,我们这个过程,其实就是基于对象(Object Based)的过程,比如为了描述猫咪,而建立了一个class猫咪。如果为了实现某一个复杂的过程,我们往往设计一个class是不够的。比如我们养猫的过程来说吧,在构造这个人和猫的系统的时候,显然需要class人,也需要class猫咪,这两种class之间从抽象的角度来看是不是有一些共通之处呢?如果说吃的不一样我们在这里先不谈,那么喝的水总归是一样的吧,呼吸的空气总归也是一样的吧,如果为了描述人和猫分别独立设计两个class,也许并没有这个必要,所以,再进一步抽象的时候,我们也许会得到(这只是为了描述这个过程,不一定非常贴切,谁让我养猫呢,低头抬头看到的全是猫)一个class 动物,这时候把水和空气作为一种通用属性放在动物类中,而进一步设计class猫和class人的关系时,就只需要通过class动物这个类来描述他们俩之间的关系了比如对我来说一定class人里面少不了铲猫砂,撸猫等等这一类特有的函数(方法);class猫中存在捣乱、卖萌这类特有的函数了。而喝水、吃饭、呼吸、睡觉(猫一般一天能睡十八九个小时,真羡慕他们,有吃有喝有睡,而我必须挣钱养他们。。。。好像说着说着class人中又多了一个方法。。。)这类方法(函数)虽然动物类就有,但是人和猫毕竟都还是有区别,这时候又可以通过覆盖这些方法来表示共性中的不同点。

因此,面向对象相较于面向过程来说,就是进一步对数据和操作方法的抽象,通过进一步的抽象来描述多个class之间的关系的抽象方法。

C++程序的基本形式

说了那么多关于基于对象和面向对象的故事,那么C++程序到底是由什么东西构成呢

  • C++程序可以分类两种:.cpp(C++文件)和.h(头文件)

    而头文件往往也分为两部分,一部分为class的声明(Classes Declaration),另外一部分则为标准库(Standard Library),其中标准库部分里面包含了大量的算法,可以让我们在设计类的时候不需要重复造轮子。

  • 那么.cpp和.h如何关联在一起呢?

    是通过#include来引入标准库或这头文件。其中如果是引入标准库的部分,使用的是<>来引入,系统中的标准库文件,比如#include <iostream.h>。如果引入的是C语言的标准库,可以使用cname或者name.h的方式引入,比如#include <stdio.h>或这#include <cstdio>;

如果是自己所编写的头文件需要使用""来引入进来,比如#include "complex.h",一般情况下,这个表示的是和cpp文件在同一个目录下的.h文件,如果.h文件在cpp文件目录的某个文件夹中,需要使用#include "/dir/xxx.h"来引入了。

  • 对于C和C++的输出有什么区别呢?

  • C++

#include <iostream>

int main()
{
    int i = 7;
    std::cout << "i= " << i << endl;
    return 0;
}
  • C语言
  #include <stdio.h>
  int main()
  {
      int i = 7;
      printf("i=%d \n", i);
      return 0;
  }
  • 头文件编写的防御式声明

    #ifndef __COMPLEX__
    #define __COMPLEX__  //防御式的声明
    .........
    #endif
    
    • 目的:

      1. 可以让使用者更加自由的include这个头文件
      2. 防止同一个程序中重复的导入这个头文件
    • 头文件的布局

       #ifndef __COMPLEX__
       #define __COMPLEX__
       //前置声明(forward declarations)
       class ostream;
       class complex;
      
       complex& __doapl(complex* ths, const complex& r);
       
       //类-声明(class declarations)
       class complex
       {
          ....
       };
      
       //类-定义
       complex::function ....
       #endif
      
  • 以complex类(复数类)举例说明类-声明的定义

      class complex      //class head
      {                           //class body
        public:      //访问级别access level为public的部分可以被外部直接访问的部分
            complex (double r = 0, double i = 0)
                : re(r), im(i)
             {   }
             //complex () : re(0), im(0) {  }    //构造函数的重载,但是由于有参数的函数有默认值,所以不能共同存在
             complex& operator += (const complex&);  //有些函数在body之外定义
             double real() const { return re; }  //有些函数再次直接定义
             double imag() const { return im; }
             void  imag(double i){ im = i; }      //函数的重载
        private:    //访问级别access level为private的部分只能被class内部和friend的函数直接访问
            double re, im;    //数据应该放在private里面,以达到封装的效果
      
            friend complex& __doapl(complex* ths, const complex& r);    
      }
    
  • inline(内联)函数

    • 在类本体内所定义的函数
      例如(之前定义的class):
      double imag() const { return im; }
    • 不在本体内定义的函数,使用inline关键字修饰的函数(是否能成为inline函数具体情况需要由编译器来决定,属于对编译器的建议)
      例如:
      inline double imag(const complex& x)
      {
          return x.imag();
      }
      
  • 构造函数(Constructor)

    complex (double r = 0, double i = 0)    //默认实参
       : re(r), im(i)      //初始列,构造函数专有的设置参数初值的方法,效率比直接赋值要高
       {   }
    complex () : re(0), im(0) {  }    //构造函数的重载
    
    • 构造函数会在创建对象的时候被自动调用

    • 函数名和类型相同,并且无返回值

    • 如果创建对象时没有传递参数,会调用默认参数的构造函数

    • 使用初始列设置初值的效率高于在函数体中赋值的过程

    • 构造函数可以重载(overloading)

    • 构造函数可以放在private中,作为私有的,可以作为singleton的设计模式(内存中只有一个对象)

      //singleton
      class A{
        public:
        static A& getInstance();
        setup(){.........}
        private:
        A();
        A(const A& rhs);
        ....
      };
      
      A& A::getInstance()
      {
       static A a;
       return a;
      }
      
  • 常量成员函数 double real() const {return re;}

    • 对于不改变数据的函数,应该添加const关键字
    • 如果不添加const关键字,则使用者创建一个常量c1const complex c1(2, 1)会报错
  • 参数传递:pass by value vs. pass by reference (to const)

    • pass by value:将数据打包整体传传递过去(如果对象比较大,会是效率降低)
    • pass by reference:引用在底部为指针,传递效率相当于传递指针
      • 尽量传递引用,而不要传递值
      • 传递引用被修改,则原始值也被修改
      • 如果为了提高效率,但不希望原始内容被修改,则应该使用const修饰
        complex& operator += (const complex& x)
  • 返回值传递:return by value vs. return by reference (to const)

    • 返回值尽量使用return by reference

    • 如果返回的结果实在函数中创建的,则不能以reference返回,因为随着函数结束而结束,不能以reference返回,否则会获取已被销毁的对象

    • 传递着无需知道接收者是以reference的形式接收

       inline complex& __doapl(complex* ths, const complex& r)
        {
            ....
            return *ths;//返回值要求为reference,则此处返回实际内容即可
            //传递着无需知道接收者是以reference的形式接收
        }
      
  • 友元(friend)

    • 对于被friend修饰的函数,可以直接取得private中的成员

    • 相同class的各个object互为友元(friend)

        //定义
        class complex
        {
          public:
            complex(double r = 0, double i = 0):re(r), im(i){}
            int func (const complex& param){return param.re + param.im}
          private:
            double re, im;
        }
      
        {
          //调用
          complex c1(2, 1);
          complex c2;
          c2.func(c1);  //可以直接访问c1中的成员(相同class的不同对象互为友元)
        }
      
  • 操作符重载(c2 += c1;)

    • 成员函数类型
      • 二元操作符,具有两个操作数,系统会将操作符作用在左边身上,如果左边操作数有定义,系统能够找到对应的操作符重载
      • 对于成员函数类型的操作符重载,编译器在编译时会为该函数自动添加this指针,该this指针就相当于操作符左侧的操作数,所以在定义函数时并不需要传入两个参数,只需要传入右侧的操作数即可
        • 例如

            c2 += c1;//操作符作用在c2上
          
        • 定义方法

            inline complex& complex::operator += (const complex& r)
            {
                return __doapl(this, r);
                //返回值的设计要求主要是为了满足链式调用的情况
                //返回值可以满足这样的要求:c3 += c2 += c1;
                //过程是c2 += c1先运算,然后产生返回值,再和c3运算
            }
            //实际上函数会被编译器添加this指针
            //complex::operator+= (this, const complex& r)
            //其中c2 += c1;调用时,相当于将c2以this的身份传入,c1以另外的参数传入
          
    • 非成员函数类型(无this)
      • 由于是非成员函数,则编译器并不能帮助函数添加this指针,则则参数列表中,需要两个形式参数,分别来表示操作符左右两侧的操作数
        inline complex
        operator + (const complex& x, const complex& y)
        {
            return complex(real(x) + real(y), imag(x) + imag(y));
        }
        inline complex
        operator + (const complex& x, double y)
        {
            return complex(real(x) + y, imag(x);
        }      
        inline complex
        operator + (double x, const complex& y)
        {
            return complex(x + real(y),  imag(y));
            //此处生成了新的复数对象,输入内部变量,所以返回值不能是reference
        }
        //为了方便例如7+c1的情况,所以不讲该函数设计为成员函数,而是使用全局函数来定义
      

推荐阅读更多精彩内容