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 构造函数的初始化列表
初始化列表的目的与意义
- 在构造对象时,同步构造内部对象
- 部分成员(常量与引用)只能初始化,不能赋值
- 部分成员(类的对象)如果赋值,将导致两次构造
- 在分配内存时,调用缺省构造函数构造,然后执行构造函数体内的赋值语句再次构造,效率不佳
- 若类没有缺省构造函数,则会导致问题
注意事项
- 成员初始化按照成员定义顺序,而不是初始化列表顺序
- 必须保持初始化列表和成员定义的顺序一致性,但允许跳过部分成员,否则后续成员可能不会正确初始化
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 派生类的构造函数与析构函数
构造函数的执行顺序
- 调用基类的构造函数,调用顺序与基类在派生类中的继承顺序相同
- 调用派生类新增对象成员的构造函数,调用顺序与其在派生类中的定义顺序相同
- 调用派生类的构造函数
析构函数的执行顺序
- 调用派生类的析构函数
- 调用派生类新增对象成员的析构函数,调用顺序与其在派生类中的定义顺序相反
- 调用基类的析构函数,调用顺序与基类在派生类中的继承顺序相反
5.6 类的赋值兼容性
共有派生时,任何基类对象可以出现的位置都可以使用派生类对象代替
- 将派生类对象赋值给基类对象,仅赋值基类部分
- 用派生类对象初始化基类对象引用,仅操作基类部分
- 使指向基类的指针指向派生类对象,仅引领基类部分
保护派生与私有派生不可以直接赋值
- 尽量不要使用保护派生与私有派生(因为无法赋值初始化对象)
6. 多态
多态性
- 目的:不同对象在接收到相同消息时做不同响应
- 现象:对应同样成员函数名称,执行不同函数体
多态性的实现
- 虚函数:使用virtual关键字声明成员函数
- 声明格式:
virtual 函数返回值 函数名称(参数列表);
- 基类的析构函数应写为虚函数