OOP的特性 |
- 抽象
- 封装 和 数据隐藏
- 多态
- 继承
- 代码可重用
类 |
- 将抽象转换为用户定义类型的C++工具
- 数据表示 + 操纵数据的方法
- 组成
- 类声明 – 蓝图
- 以数据成员的方式描述数据部分
- 以成员函数(方法)的方式描述公有接口
- 接口 – 供用户使用以操纵数据的共享框架
- 类定义 – 细节
- 描述如何实现类成员函数
- 类声明 – 蓝图
来看一个例子
// Object.h #pragma once #include <string> using std::string; struct vec3 { double x, y, z; }; class Object { public: void set_name(string name) { m_name = name; } // 内联方法 string get_name(); void update(float dt); void show(); protected: private: int m_id; // 编号 string m_name; // 名字 vec3 m_position; // 位置 vec3 m_size; // 大小 vec3 m_color; // 颜色 vec3 m_velocity; // 速度 }; inline string Object::get_name() { // 写在外面的内联方法 return m_name; }
// Object.cpp #include <iostream> #include "Object.h" using std::cout; using std::endl; void Object::update(float dt) { m_position.x += m_velocity.x; m_position.y += m_velocity.y; m_position.z += m_velocity.z; } void Object::show() { cout << "name: " << m_name << endl; }
- 访问控制
- 使用类对象的程序都可直接访问公有部分
- 但只能通过公有成员函数 或 友元函数(11章)来访问对象的私有成员
- 防止程序直接访问数据被称为数据隐藏
- proteced与继承有关,将在第13章讨论
- 不必在类中使用private,因为类对象的默认访问控制为private
- 这是与结构体struct的唯一区别 – struct默认访问控制为public
使用类对象
类对象的声明与内置类型的声明类似(目前来讲)
Object obj;
- 可以像结构体一样操作类成员和方法 – 受访问控制限制
- 每个新对象都有自己的存储空间,用于存储内部变量和类成员
- 但同一个类的所有对象共享同一组类方法 – 即每种方法只有一种副本
- 类声明和类方法构成了服务器, 供用户使用
- 而客户程序员将使用服务器提供的类,进行一些操作,此即客户端
- 修改类方法的实现时,只要接口没变,不影响客户端的运行,这也是OOP的核心思想之一
#include "Object.h" int main() { Object obj; obj.set_name("dua"); obj.show(); return 0; }
构造函数和析构函数 |
(标准)构造函数
- 目前来说,还不能基本类型和结构体那样使用初始化表初始类的成员变量(不可访问)
- 标准构造函数的目的便是想使类也可以像初始化表那样初始化其成员变量
- 对象无法调用构造函数
接受一个参数的构造函数允许使用赋值语法将对象初始化为一个值
Object obj = 1; // 如果有构造函数 Object(int)的话
- 但是这样做可能导致问题,后面会使用赋值构造函数替代
// 标准构造函数的声明 -- 无返回类型 -- 名称与类名相同 Object(int id, string name, vec3 pos, vec3 s, vec3 color, vec3 vel); // 标准构造函数的定义 -- 给成员变量赋初值 -- 或进行一些其它的初始化工作 Object::Object(int id, string name, vec3 pos, vec3 s, vec3 color, vec3 vel) { m_id = id; m_name = name; m_position = pos; m_size = s; m_color = color; m_velocity = vel; } // 标准构造函数的使用 // 显式调用标准构造 -- 编译器可能会自动创建一个临时变量,再赋值给obj, 也可能不会 Object obj = Object(1,"ddd",vec3{1,1,1},vec3{1,1,1},vec3{1,1,1},vec3{1,1,1}); // 隐式调用标准构造 Object obj(1,"ddd", vec3{1, 1, 1}, vec3{1, 1, 1}, vec3{1, 1, 1}, vec3{1, 1, 1}); obj.set_name("dua"); obj.update(1); obj.show(); // 也可结合new使用 Object* obj = new Object(1,"ddd",vec3{1,1,1},vec3{1,1,1},vec3{1,1,1},vec3{1,1,1}); obj->set_name("dua"); obj->update(1); obj->show();
默认构造函数
在未提供显式初始值时,用来创建对象的构造函数
Object obj; // 此时调用默认构造函数, 下同 Object obj = Object(); Object* obj = new Object; Object* obj = new Object();
- 注意
Object* obj();
是不对的, 这实际是一个函数声明
- 注意
- 如果没有提供任何构造函数,则C++将自动提供默认构造函数, 但此函数理论上不做任何工作
- 一旦为类提供了构造函数,就必须提供默认构造函数,否则
Object obj;
此句将报错 - 只能有一个默认构造函数 且此默认构造函数不接受任何参数
构造方法
给已有构造函数的所有参数提供默认值
Object(int id=1,const string& name="dd",vec3 pos={1,1,1}, vec3 s={1,1,1},vec3 color={1,1,1},vec3 vel={1,1,1});
通过函数重载定义另一个无参数的构造函数 – 一般用来提供隐式初始值
Object(); Object::Object() { m_id = 0; m_name = "dd"; m_position = {1,1,1}; m_size = {1,1,1}; m_color = {1,1,1}; m_velocity = {1,1,1}; }
析构函数
- 析构函数用来完成清理工作
- 静态变量在程序结束时自动调用析构函数
- 自动变量在其所在代码块运行结束时自动调用析构函数
- 使用new创建的变量将在使用delete释放内存时自动调用析构函数
- 有时程序会创建临时对象,此情况下程序会在结束对此临时对象的使用时自动析构
- 通常不会显式调用析构函数(12章有例外)
- 如果程序员没有定义析构函数,编译器将隐式地声明一个默认析构函数
// 析构函数 -- 无返回值 -- 名称为 ~ + 类名 -- 无参数 ~Object(); Object::~Object() {}
Object类的改进
- C++11初始化列表也可用于类,前提是此类的某个构造函数与列表匹配
只要类方法不修改调用此方法的对象,就应将其声明为const
void show() const;
改进后的Object类
// Object.h #pragma once #include <string> using std::string; struct vec3 { double x, y, z; }; class Object { public: Object(); Object(int id, const string& name, vec3 pos, vec3 s, vec3 color, vec3 vel); ~Object(); void set_name(string name) { m_name = name; } // 内联函数 string get_name() const; void update(float dt); void show() const; protected: private: int m_id; // 编号 string m_name; // 名字 vec3 m_position; // 位置 vec3 m_size; // 大小 vec3 m_color; // 颜色 vec3 m_velocity; // 速度 }; inline string Object::get_name() const{ // 写在外面的内联函数 return m_name; }
// Object.cpp #include <iostream> #include "Object.h" using std::cout; using std::endl; Object::Object() { m_id = 0; m_name = "dd"; m_position = {1,1,1}; m_size = {1,1,1}; m_color = {1,1,1}; m_velocity = {1,1,1}; } Object::Object(int id, const string& name, vec3 pos, vec3 s, vec3 color, vec3 vel) { m_id = id; m_name = name; m_position = pos; m_size = s; m_color = color; m_velocity = vel; } Object::~Object() { cout << "Object " << m_name << " is delete" << endl; } void Object::update(float dt) { m_position.x += m_velocity.x*dt; m_position.y += m_velocity.y*dt; m_position.z += m_velocity.z*dt; } void Object::show() const{ cout << "id: " << m_id << endl; cout << "name: " << m_name << endl; cout << "position: " << m_position.x << " - " << m_position.y << " - " << m_position.z << endl; }
this指针 |
- this指针指向用来调用成员函数的对象
- this指针被作为隐藏参传递给成员函数 – 且只能是第一个参数
- 在成员函数后面加const的实质是给第一个参数即this指针加const属性
const Object& Object::min_id(const Object& b) const {
if(m_id > b.m_id) return b;
return *this;
}
void Object::show() const;
// 其C风格定义为
void show(const Object* this);
对象数组 |
声明对象数组的方法同声明标准变量数组的方法相同
Object objs[maxn];
- 初始化方案
- 首先使用默认构造创建每个元素对象
- 花括号里的构造函数将创建临时对象
- 然后将临时对象复制到相应元素中
- 即要想创建类对象数组,这个类必须有默认构造函数
Object objs[4] = {
Object(1,"ddd",vec3{1,1,1},vec3{1,1,1},vec3{1,1,1},vec3{1,1,1}),
Object()
};
// 第一个和第二个分别用花括号里的两个构造函数构造,剩下的使用默认构造
类作用域 |
- 在类定义中的名称(成员变量和成员函数)的作用域是整个类
作用域为类的符号常量
直接使用const是行不通的 – 在创建对象之前没有用于存储值的空间
const int maxn = 100; Object* child[maxn]; // 会报错
使用枚举
enum { maxn = 100 }; Object* child[maxn]; // 正确
- 使用枚举并不会创建类成员, maxn只是一个符号名称
- 由于此枚举的作用只是创建符号常量,不用创建变量,故可省略名称
使用关键字static
static const int maxn = 100; Object* child[maxn]; // 正确
- 此时maxn为静态变量,与其他静态变量存储在一起,而不存储在对象中
作用域内枚举(C++11)
- 传统枚举的作用域为整个文件
这可能会导致两个枚举定义中的枚举量发生冲突
enum egg {small, medium, large}; enum shirt {small, medium, large}; // 报错,与egg冲突
C++11新的枚举量 – 作用域为类 – 使用class(或struct)关键字
enum class egg {small, medium, large};
需要使用枚举名来限定枚举量
egg x = egg::small;
作用域内枚举不会自动转换类型 – 需要手动强制转换
cout << int(x) << endl;
枚举的默认底层实现为int, 可使用如下方式改变底层实现 – 对于传统枚举也可
enum class egg:short { small, medium, large};