09.类与对象

1. 程序抽象与面向对象

1.1 类与对象的概念与意义

1.1.1 类的概念与意义

属性与行为的辩证统一

1.1.2 程序抽象

数据封装、信息隐藏

如果没有类的概念,无法定义非指针量,且控制性不佳

1.1.3 对象的概念与意义

对象行为的主动性

2. 类类型

2.1 类的声明与定义

类声明:仅声明类的存在,而不提供 细节

使用关键字class 示例:class A;

类定义:一般定义格式

类成员:数据与函数

三个保留字顺序任意

示例:

class A
{
    public:       //其后成员公开
    	成员类型 成员名称;
    protected:    //其后成员有限公开
    	成员类型 成员名称;
    private:      //其后成员私有,仅本对象可直接访问
        成员类型 成员名称;
};

注意:使用class定义类类型时,数据成员与成员函数不写访问控制关键字默认缺省为private;使用struct定义类类型时,数据成员与成员函数默认缺省为public

2.2 示例

//点类库接口
class Point
{
    public:
        Point(int x, int y);
        ~Point();
        void GetValue(int *x, int *y);
        void SetValue(int x, int y);
        bool Compare(const Point &point);
        char* TransformIntoString();
        void Print();
    private:
    	int x,y;
}

2.3 关于类声明与定义的说明

仅限于函数原型使用类类型的声明

不能用于定义类的数据成员

示例:

class B;
class A
{
    public:
    	void func(B b);   //正确
    private:
    	B b;              //错误
};
class B{ ... };

3. 对象

3.1 对象的定义与使用

对象的定义:

像结构体一样定义和使用对象及其公开的成员

私有成员不可在对象外部直接访问

3.2 对象的构造

3.2.1 对象构造的意义

构造就是初始化,在定义对象时初始化其数据成员

3.2.2 对象构造的技术手段:使用构造函数

与类类型同名,没有返回值类型(包括void类型)

构造函数允许重载

构造函数可以带缺省参数,但不建议这么做,会影响判断构造函数真实的参数有几个

至少公开一个构造函数,因为构造对象是在类的外部做的

只能由系统在创建对象时自动调用,程序其他部分不能直接调用

3.2.3 类没有明确的构造函数

系统会自动产生一个缺省构造函数,自动调用

缺省构造函数无参数,且函数体中没有任何语句

如果定义了任意一个构造函数,则不再生成缺省构造函数

调用缺省构造函数示例

正确示例:Circle circle;

错误示例:Circle circle();

在构造函数无参数时,不能使用函数形式构造对象

3.2.4 拷贝构造函数

拷贝构造函数用于构造已有对象的副本

拷贝构造函数单参数,为本来的常对象的引用

如未定义,系统自动产生一个缺省拷贝构造函数

缺省拷贝构造函数为位拷贝(浅拷贝),如需深拷贝(例如成员为指针),需自行定义

3.2.5 构造函数的初始化列表

初始化列表的目的与意义

  1. 在构造对象时,同步构造内部对象
  2. 部分成员(常量与引用)只能初始化,不能赋值
  3. 部分成员(类的对象)如果赋值,将导致两次构造
    • 在分配内存时,调用缺省构造函数构造,然后执行构造函数体内的赋值语句再次构造,效率不佳
    • 若类没有缺省构造函数,则会导致问题

注意事项

  1. 成员初始化按照成员定义顺序,而不是初始化列表顺序
  2. 必须保持初始化列表和成员定义的顺序一致性,但允许跳过部分成员,否则后续成员可能不会正确初始化

3.3 对象的析构

意义

析构就是终止化,在对象生命期结束时清除它

对象析构的技术手段:使用析构函数

  • 与类类型同名,前有”~“记号,无返回值类型(包括void类型),无参数
  • 析构函数必须是公开的
  • 可以由系统在销毁对象时自动调用,也可以由程序其他部分直接调用,但两者工作原理不同
  • 每个类只能有一个析构函数
  • 若未定义,系统会产生一个缺省析构函数,该函数无代码

定义析构函数的目的

  • 用于构造对象中动态分配内存的目标数据对象

示例

class A{
	public:
		A(int x);
		~A();
	private:
		int *P;
};
A::A(int x){
	p = new int;
    *p = x;
}
A::~A(){
    delete p;
    p == NULL;
}

3.4 对象数组

像普通数组一样定义和使用

3.4.1 对象数组的初始化

  • 当构造函数单参数时,像普通数组一样构造所有元素
  • 当构造函数多参数时,使用下述方法构造

Circle circle[2] = {Circle(1.0,0.0,0.0),Circle(2.0,1.0,1.0)};

4. 类与对象的成员

4.1 内联函数

目的:程序优化,展开函数代码而不是调用(当函数体较为简单时,多次函数调用的开销远大于直接执行函数本身的,这时适合使用内联函数)

内联函数使用的注意事项

  • 在函数定义前添加inline关键字,仅在函数原型前使用此关键字无效
  • 编译器必须能看见内联函数的代码才能在编译期展开,因而内联函数必须实现在头文件中
  • 在类定义中给出函数体的成员函数自动成为内联函数
  • 函数体代码量较大,或包含循环,不要使用内联
  • 构造函数和析构函数有可能隐含附加操作,慎用内联
  • 内联函数仅是建议,编译器会自主选择是否内联

4.2 常数据成员

常数据成员:值在程序运行期间不可变

  • 定义格式:const 类型 数据成员名称;
  • 初始化:只能通过构造函数中的初始化列表进行

示例:

class A
{
    public:
	    A(int a);
    private:
    	const int num;
};
A::A(int a):num(a){......};

4.3 常成员函数

常成员函数:不能修改对象成员值的函数

  • 定义格式:类型 成员函数名称(参数列表) const;
  • 常成员函数不能调用类中非常成员函数,因为可能修改对象成员值
  • 静态成员函数不能定义为常成员函数
  • 如果对象为常量,则只能调用其常成员函数

示例:

class Circle{
    public:
    	double GetArea() const;
	    ......;
};
double Circle::GetArea() const{......}

4.4 静态数据成员

静态数据成员只有一份,由该类所有对象共享

  • 声明格式:static 类型 静态数据成员名称;
  • 仅声明,不在对象上分配空间
  • 定义格式:类型 类名称::静态数据成员名称 = 初始值;
  • 必须在外部初始化,初始化动作与访问控制无关,初始化动作放在源文件,而非头文件

示例:

class A
{
    private:
    	static int count;
};
int A::count = 0;

4.5 静态成员函数

  • 在类而不是对象上调用
  • 目的:访问类的静态数据成员,若要访问类的非静态数据成员,必须指定对象或者使用指向对象的指针

示例:

class A
{
    public:
    	static int f();
	    static int g(const A &a);
    private:
    	static int count;
     	int num;
};
int A::count = 0;
int A::f(){
    return count;
}
int A::g(const A &a){
    return a.num;
}

4.6 静态常数据成员

静态常数据成员:值在程序运行期间不可变且只有唯一副本

  • 定义格式:static const 类型 数据成员名称;
  • 初始化:只能在类的外部初始化

示例:

class A{
    private:
    	static const int count;
};
const int A::count = 10;

4.7 友元函数与友元类

友元:破坏类数据封装与信息隐藏

  • 类的友元可以访问该类对象的私有与保护成员
  • 友元可以是函数、其他类成员函数,也可以是类
  • 定义格式:friend 函数或类声明;
  • 两个类的友元关系不可逆,除非互为友元(如下例,Globe可以访问Circle中的私有成员,但Circle未必可以访问Globe的私有成员,除非Globe也将Circle设为友元)

示例:

class Circle{
	friend double Get_Radius();
    friend class Globe; //将Globe类所有成员函数声明为友元
    private:
    	double radius;
};

5. 继承

5.1 继承与派生

继承的基本概念

  • 类类型:描述分类的概念
  • 继承:描述类之间的血缘(继承)关系
  • 基类、派生类
  • 父类、子类(不恰当的概念)

继承的意义

  • 派生类拥有基类的全部属性与行为
  • 派生类可以增加新的属性与行为,不能删除原有的属性的行为

5.2 单继承

单继承的基本语法格式

class 派生类名称:派生类型保留字 基类名称{......};

派生类型保留字

  • public:基类的public、protected成员在派生类中保持原有访问控制,private成员在派生类中不可见(属于基类隐私)
  • protected:基类的private成员在派生类中不可见,public、protected成员在派生类中变为protected成员
  • private:基类的private成员在派生类中不可见,public、protected成员在派生类中变为private成员
  • 设计类时若需要使用继承机制,建议将派生类需要频繁使用的基类数据成员设为protected的

5.3 多继承

多继承的基本语法格式

class 派生类名称:派生类型保留字 基类名称1,派生类型保留字 基类名称2,...{...};

多继承示例

class A {...};
class B {...};
class C:public A, protected B{..};

多继承可能导致的问题:派生类中可能包含多个基类副本,要慎用

5.4 虚继承

虚拟继承的目的

  • 取消多继承时派生类中公共基类的多个副本,只保留一份
  • 格式:派生时使用关键字virtual

使用示例:D中只有A的一份副本

class A {public: void f();};
class B:virtual public A {public: void f();};
class C:virtual public A {public: void f();};
class D:public B,public C {public: void f();};

5.5 派生类的构造函数与析构函数

构造函数的执行顺序

  1. 调用基类的构造函数,调用顺序与基类在派生类中的继承顺序相同
  2. 调用派生类新增对象成员的构造函数,调用顺序与其在派生类中的定义顺序相同
  3. 调用派生类的构造函数

析构函数的执行顺序

  1. 调用派生类的析构函数
  2. 调用派生类新增对象成员的析构函数,调用顺序与其在派生类中的定义顺序相反
  3. 调用基类的析构函数,调用顺序与基类在派生类中的继承顺序相反

5.6 类的赋值兼容性

共有派生时,任何基类对象可以出现的位置都可以使用派生类对象代替

  • 将派生类对象赋值给基类对象,仅赋值基类部分
  • 用派生类对象初始化基类对象引用,仅操作基类部分
  • 使指向基类的指针指向派生类对象,仅引领基类部分

保护派生与私有派生不可以直接赋值

  • 尽量不要使用保护派生与私有派生(因为无法赋值初始化对象)

6. 多态

多态性

  • 目的:不同对象在接收到相同消息时做不同响应
  • 现象:对应同样成员函数名称,执行不同函数体

多态性的实现

  • 虚函数:使用virtual关键字声明成员函数
  • 声明格式:virtual 函数返回值 函数名称(参数列表);
  • 基类的析构函数应写为虚函数
posted @ 2020-10-14 12:27  bear-Zhao  阅读(127)  评论(0编辑  收藏  举报