专栏首页测试开发架构之路C++之面向对象的三个基本特征

C++之面向对象的三个基本特征

三大特性是:封装,继承,多态  

所谓封装 就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏.封装是面向对象的特征之一,是对象和类概念的主要特性. 简单的说,一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分.

所谓继承 是指可以让某个类型的对象获得另一个类型的对象的属性的方法。它支持按级分类的概念。继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展. 通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。继承的过程,就是从一般到特殊的过程。要实现继承,可以通过“继承”(Inheritance)和“组合”(Composition)来实现.继承概念的实现方式有二类:实现继承与接口继承.实现继承是指直接使用基类的属性和方法而无需额外编码的能力;接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力;

所谓多态 就是向不同对象发生同一个消息,不同的对象在接收时会产生不同的行为(即方法).

  运算符重载:重新赋予新的含义. 同一个函数名就可以用来代表不同功能的函数,(一名多用).

例如 +,—,*,/.  >>,<<. 运算符.

用户可以根据自己的需要对C++已提供的运算符进行重载吗?

//运算符重载
#include<iostream>
using namespace std;

class Complex{
    public:
        Complex(){real=0;imag=0;}
        Complex(double r,double i){real=r;imag=i;}
        Complex complex_add(Complex &c2);
        void display();
    private:
        double real;
        double imag;
}; 

Complex Complex::complex_add(Complex &c2)
{
    Complex c;
    c.real=real+c2.real;
    c.imag=imag+c2.imag;
    return c;
}

void Complex::display()
{
    cout<<"("<<real<<","<<imag<<"i)"<<endl;
}

int main()
{
    Complex c1(3,4),c2(5,-10),c3;
    c3=c1.complex_add(c2);
    cout<<"c1= ";c1.display();
    cout<<"c2= ";c2.display();
    cout<<"c1+c2= ";c3.display();
    return 0;
}
//运算符重载
#include<iostream>
using namespace std;

class Complex{
    public:
        Complex(){real=0;imag=0;}
        Complex(double r,double i){real=r;imag=i;}
    //    Complex complex_add(Complex &c2);
        Complex operator +(Complex &c2);
        void display();
    private:
        double real;
        double imag;
}; 

Complex Complex::operator +(Complex &c2)
{
    Complex c;
    c.real=real+c2.real;
    c.imag=imag+c2.imag;
    return c;
}

void Complex::display()
{
    cout<<"("<<real<<","<<imag<<"i)"<<endl;
}

int main()
{
    Complex c1(3,4),c2(5,-10),c3;
    c3=c1+c2;//运算符重载 
    cout<<"c1= ";c1.display();
    cout<<"c2= ";c2.display();
    cout<<"c1+c2= ";c3.display();
    return 0;
}

定义:

函数类型 operator 运算符( 形参表 )
    { 对运算符进行重载处理 }
int operator +(int a, int b)
{return (a+b);}

重载规则:

1)C++不允许用户定义新的运算符,只能对已有的运算符进行重载。

2)不能重载的运算符只有5个:. * :: sizeof(长度运算符)  ?: 9条件运算符

3)重载不可以改变操作数的个数;

4)重载不可改变运算符的优先级别

5)重载不能运算符的结合性;

6)重载运算符的函数不能用默认的参数;

7)重载的运算符必须和用户定义的自定义类型的对象一起使用,其参数至少应有一个是类对象(或类对象的引用)

8)用于类对象的运算符一般必须重载,但有两个例外,运算符“=”和“&”不必用户重载。

9)从理论上说,可以将一个运算符重载为执行任意的操作。

  运算符重载函数作为 类成员函数 与 友元函数(形参必须有要求)

一般将单目运算符和复合运算符重载为成员函数

一般将双目运算符重载为友元函数;

  重载双目运算符

C++构造函数的重载 在一个类中可以定义多个构造函数,以便提供不同的初始化的方法,供用户选用。这些构造函数具有相同的名字,而参数的个数或参数的类型不相同。这称为构造函数的重载。

关于构造函数的重载的几点说明:

调用构造函数时不必给出实参的构造函数,称为默认构造函数(default constructor)。显然,无参的构造函数属于默认构造函数。一个类只能有一个默认构造函数。 如果在建立对象时选用的是无参构造函数,应注意正确书写定义对象的语句。 尽管在一个类中可以包含多个构造函数,但是对于每一个对象来说,建立对象时只执行其中一个构造函数,并非每个构造函数都被执行。

不同类型数据间的转换

int i=6;
i=i+7.5;//先将i转为double得到13.5,再向整型赋值转化为13给i,隐式类型转换

类型转换函数:将一个类的对象转换成另一个类型的数据。

继承与派生

软件的重用性 通过继承来实现;减少重复的工作量;

继承就是在一个已存在的类(基类)的基础上建立一个新类(派生类,子类)。

单继承:一个类只从一个基类派生;

多重继承:一个派生类有两个或多个基类。

派生类时基类的具体化;基类是派生类的抽象;

派生类的声明:

//一个例子
class Student1:public Student  //声明基类是Student
{  pubic:
      void display_1()
           {cout<<"age:"<<age<<endl;}
    private:
           int age;    
};
//第一行,关键字public,用来表示基类Student中的成员在派生类Student1中的继承方式;表示公共继承。
class 派生类名:[继承方式]基类名      ///继承方式:public,private(默认),protected
{
  派生类新增加的成员
};

 多态与虚函数

多态:向不同对象发送同一个消息,不同的对象在接收时会发生不同的行为(即方法)。每个对象可以用自己的方式去响应共同的消息。(消息即调用函数)

函数的重载属于多态的一种。

具有不同功能的函数可以用同一个函数名,这样就可以实现用一个函数名调用不同内容的函数。

多态性是一个接口,多种方法。

静态多态性与动态多态性

静态多态性 通过函数重载实现的。缺乏灵活。

动态多态性是通过虚函数实现的。

 C++虚函数:就是在基类声明函数是虚拟的,并不是实际存在的函数,然后在派生类中才正式定义此函数。

虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。

#include <iostream>
#include <string>
using namespace std;
//声明基类Student
class Student
{
public:
   Student(int, string,float);  //声明构造函数
  virtual void display( );//声明输出函数 为 虚函数
protected:  //受保护成员,派生类可以访问
   int num;
   string name;
   float score;
};
//Student类成员函数的实现
Student::Student(int n, string nam,float s)//定义构造函数
{
   num=n;
   name=nam;
   score=s;
}
void Student::display( )//定义输出函数
{
   cout<<"num:"<<num<<"\nname:"<<name<<"\nscore:"<<score<<"\n\n";
}
//声明公用派生类Graduate
class Graduate:public Student
{
public:
   Graduate(int, string, float, float);//声明构造函数
   void display( );//声明输出函数
private:float pay;
};
// Graduate类成员函数的实现
void Graduate::display( )//定义输出函数
{
   cout<<"num:"<<num<<"\nname:"<<name<<"\nscore:"<<score<<"\npay="<<pay<<endl;
}
Graduate::Graduate(int n, string nam,float s,float p):Student(n,nam,s),pay(p){}
//主函数
int main()
{
   Student stud1(1001,"Li",87.5);//定义Student类对象stud1
   Graduate grad1(2001,"Wang",98.5,563.5);//定义Graduate类对象grad1
   Student *pt=&stud1;//定义指向基类对象的指针变量pt
   pt->display( );
   pt=&grad1;
   pt->display( );
   return 0;
}

虚函数的使用方法是:

  1. 在基类用virtual声明成员函数为虚函数。 这样就可以在派生类中重新定义此函数,为它赋予新的功能,并能方便地被调用。在类外定义虚函数时,不必再加virtual。
  2. 在派生类中重新定义此函数,要求函数名、函数类型、函数参数个数和类型全部与基类的虚函数相同,并根据派生类的需要重新定义函数体。 C++规定,当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。因此在派生类重新声明该虚函数时,可以加virtual,也可以不加,但习惯上一般在每一层声明该函数时都加virtual,使程序更加清晰。如果在派生类中没有对基类的虚函数重新定义,则派生类简单地继承其直接基类的虚函数。
  3. 定义一个指向基类对象的指针变量,并使它指向同一类族中需要调用该函数的对象。
  4. 通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数。 通过虚函数与指向基类对象的指针变量的配合使用,就能方便地调用同一类族中不同类的同名函数,只要先用基类指针指向即可。如果指针不断地指向同一类族中不同类的对象,就能不断地调用这些对象中的同名函数。这就如同前面说的,不断地告诉出租车司机要去的目的地,然后司机把你送到你要去的地方。

需要说明;有时在基类中定义的非虚函数会在派生类中被重新定义(如例12.1中的area函数),如果用基类指针调用该成员函数,则系统会调用对象中基类部分的成员函数;如果用派生类指针调用该成员函数,则系统会调用派生类对象中的成员函数,这并不是多态性行为(使用的是不同类型的指针),没有用到虚函数的功能。 以前介绍的函数重载处理的是同一层次上的同名函数问题,而虚函数处理的是不同派生层次上的同名函数问题,前者是横向重载,后者可以理解为纵向重载。但与重载不同的是:同一类族的虚函数的首部是相同的,而函数重载时函数的首部是不同的(参数个数或类型不同)。

关联—binding

确定调用的具体对象的过程称为 关联

在这里指把一个函数名与一个类对象捆绑在一起,建立关联;

一般来说,关联指把一个标识符和一个存储地址联系起来;

静态关联—在编译时既可确定其调用的虚函数属于哪一类,其过程称为静态关联;由于是在运行前进行关联的,又称为早期关联;

动态关联—在编译后的运行阶段把虚函数和类对象“绑定”在一起,这种多态性是运行阶段的多态性(动态多态性)。

在什么情况下应当声明虚函数?

Tips:

1)只能用virtual声明类的成员函数,把它作为虚函数,而不能将类外的普通函数声明为虚函数;

2)一个类的成员函数被声明为虚函数后i,在同一类族中的类就不能再定义一个非virtual的但与该函数具有相同的参数和函数返回值类型的同名函数;

根据什么考虑是否把一个成员函数声明为虚函数呢?主要考虑一下几点

1)首先看成员函数所在的类是否是基类;然后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数;

2)如果成员函数在类被继承后功能无需修改,或派生类用不到该函数,则不要把它声明为虚函数;不要仅仅考虑到要作为基类而把类中的所有成员函数都声明为虚函数。

3)应考虑对成员函数的调用是通过对象名还是通过基类指针或引用去访问,如果通过基类指针或引用去访问的,则应当声明为虚函数;

4)有时,在定义虚函数时,并不定义其函数,即函数体是空的。它的作用只是定义了一个虚函数名,具体功能留给派生类去添加。

使用虚函数,系统要有一定的空间开销。

虚析构函数

析构函数的作用是在对象撤销之前做必要的“清理现场”工作;当派生类的对象从内存中撤销时,一般先调用派生类的析构函数,再调用基类的析构函数。

在C++中,构造函数用于在创建对象时进行初始化工作,不能声明为虚函数。因为在执行构造函数前对象尚未创建完成,虚函数表尚不存在,也没有指向虚函数表的指针,所以此时无法查询虚函数表,也就不知道要调用哪一个构造函数。下节会讲解虚函数表的概念。 析构函数则用于在销毁对象时完成相应的资源释放工作,可以被声明为虚函数。 为了说明虚析构函数的必要性,请大家先看下面一个例子:

#include <iostream>
using namespace std;

//基类
class Base{
private:
    int *a;
public:
    Base();
    ~Base(){ cout<<"Base destructor"<<endl; }
};
Base::Base(){
    a = new int[100];
    cout<<"Base constructor"<<endl;
}

//派生类
class Derived: public Base{
private:
    int *b;
public:
    Derived();
    ~Derived( ){ cout<<"Derived destructor"<<endl; }
};
Derived::Derived(){
    b = new int[100];
    cout<<"Derived constructor"<<endl;
}

int main( ){
   Base *p = new Derived;
   delete p;
   return 0;
}
运行结果:
Base constructor
Derived constructor
Base destructor

本例中定义了两个类,基类 Base 和派生类 Derived,它们都有自己的构造函数和析构函数。在构造函数中,会分配100个 int 型的内存空间;在析构函数中,会把这些内存释放掉。

在 main 函数中,定义了基类类型的指针 p,并指向派生类对象,然后希望用 delete 释放 p 所指向的空间。 从运行结果可以看出,执行delete p;语句时只调用了基类的析构函数,却没有调用派生类的析构函数。这会导致 b 所指向的 100 个 int 型内存空间得不到释放,除非程序运行结束被操作系统回收,否则就再也没有机会释放这些内存。这是典型的内存泄露。 内存泄露问题是程序员需要极力避免的。本例中出现的内存泄露是由于派生类的析构函数未被调用引起的,为了解决这个问题,需要将基类的析构函数声明为虚函数。修正后的代码如下所示:

class Base{
private:
    int *a;
public:
    Base();
    virtual ~Base(){ cout<<"Base destructor"<<endl; }
};
Base::Base(){
    a = new int[100];
    cout<<"Base constructor"<<endl;
}
运行结果:
Base constructor
Derived constructor
Derived destructor
Base destructor

如此,派生类的析构函数也会自动成为虚析构函数。当执行delete p;语句时,会先执行派生类的析构函数,再执行基类的析构函数,这样就不存在内存泄露问题了。 这个例子足以说明虚析构函数的必要性,但是如果不管三七二十一的将所有的基类的析构函数都声明为虚函数,也是不合适的。通常来说,如果基类中存在一个指向动态分配内存的成员变量,并且基类的析构函数中定义了释放该动态分配内存的代码,那么就应该将基类的析构函数声明为虚函数。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • C++之虚函数的作用和使用方法

    在同一类中是不能定义两个名字相同、参数个数和类型都相同的函数的,否则就是“重复定义”。但是在类的继承层次结构中,在不同的层次中可以出现名字相同、参数个数和类型都...

    互联网金融打杂
  • C语言之函数

      结构化程序设计主张按功能来分析需求,主要原则自顶向下,逐步求精,模块化等。 主张按功能把软件系统逐步细分,每个功能都负责对数据进行一次处理,每个功能接收一些...

    互联网金融打杂
  • Java基础知识学习笔记(一)

      理解面向对象: Java纯粹的面向对象的程序设计语言,主要表现为Java完全支持面向对象的三个基本特征:继承、封装、多态。 Java程序的最小单位是类,类代...

    互联网金融打杂
  • 虚函数与纯虚函数的区别

    虚函数(impure virtual)   C++的虚函数主要作用是“运行时多态”,父类中提供虚函数的实现,为子类提供默认的函数实现。   子类可以重写父类...

    柴银磊
  • 万物皆可 Serverless 之关于云函数冷热启动那些事儿

    本文带大家来了解一下云函数的冷热启动过程,以及面对云函数这种冷热启动模式,开发者需要注意哪些问题。 效果展示 云函数被第一次调用(冷启动) ? 云函数被第一次...

    腾讯云serverless团队
  • 在下函数式编程,有何贵干?

    本文简单介绍了一下函数式编程的各种基本特性,希望能够对于准备使用函数式编程的人起到一定入门作用。 ? 函数式编程,一个一直以来都酷,很酷,非常酷的名词。虽然诞生...

    CSDN技术头条
  • [一] java8 函数式编程入门 什么是函数式编程 函数接口概念 流和收集器基本概念

    说起来好像很啰嗦,但是如果有人告诉你 通过sin(x) 计算后, x的值被改变了,你不会觉得异常奇怪么

    noteless
  • c++虚函数注意事项

    》在基类方法声明中使用关键字virtual,可以使该方法在基类及所有的派生类中是虚的 》如果使用指向对象的引用或指针来调用虚方法,程序将使用对象类型定义的方法,...

    lpxxn
  • Scala 学习笔记之高阶函数

    Scala混合了面向对象和函数式的特性.在函数式编程语言中,函数可以像任何其他数据类型一样被传递和操作.如果想要给算法传入明细动作时,只需要将明细动作包在函数当...

    smartsi
  • 马里兰大学论文:可视化神经网络的损失函数

    选自arXiv 机器之心编译 参与:黄小天、刘晓坤 在这篇论文中,研究人员提出了「过滤器归一化」方法,用于可视化损失函数的曲率,并对损失函数进行了实验性的并行对...

    企鹅号小编

扫码关注云+社区

领取腾讯云代金券