C++面向对象程序设计(上)

C++面向对象程序设计(上)

 

C语言是一种基于过程的编程语言,C++在此基础上发展而成,保留了C的绝大部分的功能和运行机制。同时增加了面向对象的机制。C++面向对象的程序设计,除了主函数,其他的函数基本都在类中,只有通过类才能调用类中的函数。程序的基本单元是类,程序面对的是一个个类和对象。

☆面向过程的程序设计(Process oriented programming),也称为结构化程序设计(Structured programming),有时会被视为是指令式编程(Imperative programming)的同义语。。编写程序可以说是这样一个过程:从系统要实现的功能入手把复杂的任务分解成子任务,把子任务再分解成更简单的任务,层层分解来完成。可以采用函数(function)或调用(procedure)一步步细化调用实现。程序流程结构可分为循序(sequence)、选择(selection)及重复(repetition)或循环(loop)。

☆面向对象程序设计(Object Oriented Programming),是围绕着问题域中的对象(Object)来设计,对象包含属性、方法。对象则指的是类的实例。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性,对象里的程序可以访问及经常修改对象相关联的数据。在面向对象程序编程里,计算机程序会被设计成彼此相关的对象。

 

面向对象程序设计的基本概念概述

对象(Object)

对象是指客观存在的事物(可以是看得见摸得着的,也可以是看不见摸不着的),由一组属性和方法构成。

对象 = 属性 + 方法

在面向对象程序设计中,对象之间也需要联系,我们称作对象的交互。

属性(Property)是用来描述对象的外部特征。

属性的引用方法为:

对象名.属性名 = 属性值  或  变量名 = 对象名.属性名

方法(Method)是对某对象接收消息后所采取的操作的描述,它表明了一个对象所具有的行为能力。

调用对象的方法为:

对象名.方法名[参数列表]

类(Class)

(1)类是具有共同特征的对象的抽象。

(2)类是对具有共同属性和行为的一类事物的抽象描述。 共同的属性被描述为类的数据成员,共同行为被描述为类的成员函数。

类是对象之上的抽象,是对象的模板;对象是类的具体化,称为类的实例(instance)。类可以有子类,也可以有父类,从而形成层次关系。

 

面向对象程序设计的三大特征:

封装是基础,继承是关键,多态是补充,而多态又必须存在于继承的环境中,多态的实现受到继承性的支持。

1)封装(Encapsulation):

对对象的封装指的是把它一部分属性和功能对外界屏蔽,这样就做到了把对象的内部实现和外部行为分割开来。封装的好处:实现各个对象间的相对独立和信息隐蔽。封装(Encapsulation)是面向对象程序设计最基本的特性。把数据(属性)和函数(操作)合成一个整体,这在计算机世界中是用类(class)和对象(object)实现的。

2)继承(Inheritance)

继承是是面向对象的程序中两个类之间的一种关系,即一个类可以从另一个类(即它的父类)继承状态属性和行为方法。继承父类的类称为子类。

继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。被继承的类称为“基类”、“父类”或“超类”。通过继承创建的新类称为“子类”或“派生类”。

3)多态性(polymorphism)

不同继承关系的类对象去调用同一函数(方法)时,产生了不同的行为(功能)。

多态性又被称为“ 一个名字,多个方法”。

实现多态,有二种方式:

覆盖:是指子类重新定义父类的虚函数的做法。

重载:是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。

 

C++类
在C++中,每个对象都由数据和函数组成,数据体现了属性,函数体现了行为,也可以称之为方法。类是对象的抽象,而对象则是类的特例(类的实例化)。

C++中使用关键字 class 来定义类, 其基本形式如下:
class 类名
    {
        public:
            //公共的行为或属性

        private:
            //公共的行为或属性
};
说明:
1)类名 需要遵循一般的命名规则;
2)public 与 private 为属性/方法限制的关键字, private 表示该部分内容是私密的, 不能被外部所访问或调用, 只能被本类内部访问; 而 public 表示公开的属性和方法, 外界可以直接访问或者调用。
一般来说,类的属性成员都应设置为private, public只留给那些被外界用来调用的函数接口, 但这并非是强制规定, 可以根据需要进行调整;
3)结束部分的有分号。

例、定义一个点(Point)类, 具有以下属性和方法:
属性: x坐标, y坐标
方法: 1.设置x,y的坐标值; 2.输出坐标的信息。
class Point
    {
        public:
            void setPoint(int x, int y);
            void printPoint();

        private:
            int xPos;
            int yPos;
};

C++类成员函数的定义
一个类的数据和函数统称为类的成员。类成员函数的实现有两种方式:。
1、在类定义时定义成员函数
成员函数的实现可以在类定义时同时完成, 如代码:
#include <iostream>
using namespace std;
   class Point
   {
       public:
           void setPoint(int x, int y) //实现setPoint函数
           {
               xPos = x;
               yPos = y;
            }

           void printPoint()       //实现printPoint函数
           {
                cout<< "x = " << xPos << endl;
                cout<< "y = " << yPos << endl;
            }

        private:
            int xPos;
            int yPos;
    };

    int main()
    {
        Point M;        //用定义好的类创建一个对象 点M
        M.setPoint(10, 20); //设置 M点 的x,y值
        M.printPoint();     //输出 M点 的信息

        return 0;
     }

运行之,参见下图:

C++面向对象程序设计(上)

 

2、在类外定义成员函数
在类外定义成员函数通过在类内进行声明, 然后在类外通过作用域操作符 :: 进行实现, 形式如下:
返回类型 类名::成员函数名(参数列表)
    {
     //函数体
     }
       
将上例代码改用在类外定义成员函数的代码:
#include <iostream>
using namespace std;
class Point
{
    public:
        void setPoint(int x, int y); //在类内对成员函数进行声明
        void printPoint();

    private:
        int xPos;
        int yPos;
};

void Point::setPoint(int x, int y) //通过作用域操作符 '::' 实现setPoint函数
{
    xPos = x;
    yPos = y;
}

void Point::printPoint()       //实现printPoint函数
{
    cout<< "x = " << xPos << endl;
    cout<< "y = " << yPos << endl;
}

int main()
{
    Point M;//用定义好的类创建一个对象 点M
    M.setPoint(10, 20); //设置 M点 的x,y值
    M.printPoint();     //输出 M点 的信息

    return 0;
}

运行之,参见下图:

C++面向对象程序设计(上)

 

类对象的创建(类的实例化)
将一个类定义并实现后, 就可以用该类来创建对象了, 创建的过程如同 int、char 等基本数据类型声明一个变量一样简单,创建一个类的对象称为该类的实例化,格式:
类名 对象名;
如上面的例子中的:Point M;  

类对象成员的使用 
通过 对象名.公有函数名(参数列表); 的形式就可以调用该类对象所具有的方法——成员函数, 通过 对象名.公有数据成员; 的形式可以访问对象中的数据成员。
如上面的例子中的:M.setPoint(10, 20);

C++构造函数与析构函数
1、构造函数
构造函数的作用
C++中的构造函数类似于Python中的 __init__ 方法。构造函数主要用来在创建对象时完成对对象属性的一些初始化等操作, 当创建对象时, 对象会自动调用它的构造函数。一般来说, 构造函数有以下三个方面的作用:
①给创建的对象建立一个标识符;
②为对象数据成员开辟内存空间;
③完成对象数据成员的初始化。
        
默认构造函数
当用户没有显式的去定义构造函数时, 编译器会为类生成一个默认的构造函数, 称为 "默认构造函数", 默认构造函数不能完成对象数据成员的初始化, 只能给对象创建一标识符, 并为对象中的数据成员开辟一定的内存空间。
        
构造函数的特点
无论是用户自定义的构造函数还是默认构造函数都主要有以下特点:
①在对象被创建时自动执行;
②构造函数的函数名与类名相同;
③没有返回值类型、也没有返回值;
④构造函数不能被显式调用。

构造函数的显式定义
由于在大多数情况下我们希望在对象创建时就完成一些对成员属性的初始化等工作, 而默认构造函数无法满足我们的要求, 所以我们需要显式定义一个构造函数来覆盖掉默认构造函数以便来完成必要的初始化工作, 当用户自定义构造函数后编译器就不会再为对象生成默认构造函数。
    
在构造函数的特点中我们提到, 构造函数的名称必须与类名相同, 并且没有返回值类型和返回值, 看一个构造函数的定义:
#include <iostream>

using namespace std;

class Point
    {
        public:
            Point()     //声明并定义构造函数
            {
                cout<<"自定义的构造函数被调用...\n";
                xPos = 100;         //利用构造函数对数据成员 xPos, yPos进行初始化
                yPos = 100;
            }
            void printPoint()
            {
                cout<<"xPos = " << xPos <<endl;
                cout<<"yPos = " << yPos <<endl;
            }

        private:
            int xPos;
            int yPos;
    };

    int main()
    {
        Point M;    //创建对象M
        M.printPoint();

        return 0;
    }

运行之,参见下图:

C++面向对象程序设计(上)

说明:

在Point类的 public 成员中我们定义了一个构造函数 Point() , 可以看到这个Point构造函数并不像 printPoint 函数有个void类型的返回值, 这正是构造函数的一特点。在构造函数中, 我们输出了一句提示信息, "自定义的构造函数被调用...", 并且将对象中的数据成员xPos和yPos初始化为100。
在 main 函数中, 使用 Point 类创建了一个对象 M, 并调用M对象的方法 printPoint 输出M的属性信息, 根据输出结果看到, 自定义的构造函数被调用了, 所以 xPos和yPos 的值此时都是100, 而不是一个随机值。
构造函数的定义也可放在类外进行。

带有参数的构造函数
在上个示例中实在构造函数的函数体内直接对数据成员进行赋值以达到初始化的目的, 但是有时候在创建时每个对象的属性有可能是不同的, 这种直接赋值的方式显然不合适。不过构造函数是支持向函数中传入参数的, 所以可以使用带参数的构造函数来解决该问题。
#include <iostream>
using namespace std;

class Point
    {
        public:
            Point(int x = 0, int y = 0)     //带有默认参数的构造函数
            {
                cout<<"自定义的构造函数被调用...\n";
                xPos = x;         //利用传入的参数值对成员属性进行初始化
                yPos = y;
            }
            void printPoint()
            {
                cout<<"xPos = " << xPos <<endl;
                cout<<"yPos = " << yPos <<endl;
            }

        private:
            int xPos;
            int yPos;
    };

    int main()
    {
        Point M(10, 20);    //创建对象M并初始化xPos,yPos为10和20
        M.printPoint();

        Point N(200);       //创建对象N并初始化xPos为200, yPos使用参数y的默认值0
        N.printPoint();

        Point P;            //创建对象P使用构造函数的默认参数
        P.printPoint();

        return 0;
    }

运行之,参见下图:

C++面向对象程序设计(上)

说明:
在这个示例中的构造函数 Point(int x = 0, int y = 0) 使用了参数列表并且对参数进行了默认参数设置为0。在 main 函数*创建了三个对象 M, N, P。
M对象不使用默认参数将M的坐标属性初始化10和20;
N对象使用一个默认参数y, xPos属性初始化为200;
P对象完全使用默认参数将xPos和yPos初始化为0。

对象中的一些数据成员除了在构造函数体中进行初始化外还可以通过调用初始化表来进行完成, 要使用初始化表来对数据成员进行初始化时使用 : 号进行调出, 例如:
Point(int x = 0, int y = 0):xPos(x), yPos(y)  //使用初始化表
    {
        cout<<"调用初始化表对数据成员进行初始化!\n";
    }
在 Point 构造函数头的后面, 通过单个冒号 : 引出的就是初始化表, 初始化的内容为 Point 类中int型的 xPos 成员和 yPos成员, 其效果和 xPos = x; yPos = y; 是相同的。

与在构造函数体内进行初始化不同的是, 使用初始化表进行初始化是在构造函数被调用以前就完成的。每个成员在初始化表中只能出现一次, 并且初始化的顺序不是取决于数据成员在初始化表中出现的顺序, 而是取决于在类中声明的顺序。
 一些通过构造函数无法进行初始化的数据类型可以使用初始化表进行初始化, 如: 常量成员和引用成员, 这部分内容将在后面进行详细说明。
使用初始化表对对象成员进行初始化的例子:
#include <iostream>

using namespace std;

class Point
{
    public:
        Point(int x = 0, int y = 0):xPos(x), yPos(y)
        {
             cout<<"调用初始化表对数据成员进行初始化!\n";
        }

        void printPoint()
        {
             cout<<"xPos = " << xPos <<endl;
             cout<<"yPos = " << yPos <<endl;
        }

    private:
        int xPos;
        int yPos;
};

int main()
{
    Point M(10, 20);    //创建对象M并初始化xPos,yPos为10和20
    M.printPoint();
 
    return 0;
}
 

运行之,参见下图:

C++面向对象程序设计(上)

 

2、析构函数
与构造函数相反, 析构函数是在对象被撤销时被自动调用, 用于对成员撤销时的一些清理工作, 例如在前面提到的手动释放使用 new 或 malloc 进行申请的内存空间。析构函数具有以下特点:
析构函数函数名与类名相同, 紧贴在名称前面用波浪号 ~ 与构造函数进行区分, 如: ~Point();
构造函数没有返回类型, 也不能指定参数, 因此析构函数只能有一个, 不能被重载;
当对象被撤销时析构函数被自动调用, 与构造函数不同的是, 析构函数可以被显式的调用, 以释放对象中动态申请的内存。
C++中的析构函数类似于Python中的 __del__ 方法。
当用户没有显式定义析构函数时, 编译器同样会为对象生成一个默认的析构函数, 但默认生成的析构函数只能释放类的普通数据成员所占用的空间, 无法释放通过 new 或 malloc 进行申请的空间, 因此有时我们需要自己显式的定义析构函数对这些申请的空间进行释放, 避免造成内存泄露。
析构函数的例子
#include <iostream>
#include <cstring>

using namespace std;

class Book
{
    public:
        Book( const char *name )  //构造函数
        {
            bookName = new char[strlen(name)+1];
            strcpy(bookName, name);
        }
        ~Book()       //析构函数
        {
            cout<<"析构函数被调用...\n";
            delete []bookName;  //释放通过new申请的空间
        }
        void showName() { cout<<"Book name: "<< bookName <<endl; }

    private:
        char *bookName;
};

int main()
{
    Book CPP("C++ Primer");
    CPP.showName();

    return 0;

}

运行之,参见下图:

C++面向对象程序设计(上)

说明:
代码中创建了一个 Book 类, 类的数据成员只有一个字符指针型的 bookName, 在创建对象时系统会为该指针变量分配它所需内存, 但是此时该指针并没有被初始化所以不会再为其分配其他多余的内存单元。在构造函数中, 我们使用 new 申请了一块 strlen(name)+1 大小的空间, 也就是比传入进来的字符串长度多1的空间, 目的是让字符指针 bookName 指向它, 这样才能正常保存传入的字符串。
在 main 函数中使用 Book 类创建了一个对象 CPP, 初始化 bookName 属性为 "C++ Primer"。从运行结果可以看到, 析构函数被调用了, 这时使用 new 所申请的空间就会被正常释放。
        
自然状态下对象何时将被销毁取决于对象的生存周期, 例如全局对象是在程序运行结束时被销毁, 自动对象是在离开其作用域时被销毁。
        
如果需要显式调用析构函数来释放对象中动态申请的空间只需要使用 对象名.析构函数名(); 即可, 例如上例中要显式调用析构函数来释放 bookName 所指向的空间,只要: CPP.~Book();