【c++】【基础】【primer_plus】【第十章】面向对象与类

前言

本章开始说明面向对象编程的核心概念,即的概念,但是并不打算详细介绍面向对象的编程思想。

从面向过程编程到面向对象编程是一种思想上的转变,需要经过时间和实践的积累,言语上不必多言,只总结一下其大体特点如下。

抽象

封装 -- 数据隐藏

多态

继承

代码可重用

代码易扩展

其具体指导思想会在设计模式模块进行详细论述。

是一种将__抽象__转换为__用户定义类型__的c++工具。它包括属性(数据表示)方法(操纵数据的方法)两部分组成。

实现一类,需要像其他数据结构一样,对其进行声明和定义。

其中类声明是类的蓝图,它以数据成员的方式描述数据部分,以成员函数(方法)的方式描述公有接口。这里的接口是指供用户使用以操纵数据的共享框架。

而类的定义则描述其细节,描述如何实现类成员函数。

示例如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 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;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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;
}

访问控制

防止程序直接访问数据被称为数据隐藏。c++类通过访问控制关键字publicprivateprotected来控制外部访问其数据的权利。

public

使用类对象的程序都可直接访问其公有部分。

private

只能通过公有成员函数友元函数(11章)来访问对象的私有成员。

类对象的默认访问控制为private,而结构体struct默认访问控制为public。这大概是类和结构体的唯一区别了。

protected

protecedprivate类似。

其区别与继承有关,将在第13章讨论。

构造函数

标准构造函数

目前来说,还不能像基本类型和结构体那样使用初始化表初始类的成员变量,因为private的成员变量不可访问。

而标准构造函数产生的原因的便是想使类也可以像初始化表那样初始化其成员变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 标准构造函数的声明 -- 无返回类型 -- 名称与类名相同
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();

注意,类对象是无法调用构造函数的。

接受一个参数的构造函数允许使用赋值语法将对象初始化为一个值,如下所示。

1
Object obj = 1;                 // 如果有构造函数 Object(int)的话

但是这样做可能导致问题,后面会使用赋值构造函数替代。

默认构造函数

默认构造函数是在未提供显式初始值时,用来创建对象的构造函数。

1
2
3
4
Object obj;                 // 此时调用默认构造函数, 下同
Object obj = Object();
Object* obj = new Object;
Object* obj = new Object();

注意Object* obj();是不对的,这实际是一个函数声明。

如果没有提供任何构造函数,则c++将自动提供默认构造函数,但此函数理论上不做任何工作。

一旦为类提供了构造函数,就必须提供默认构造函数,否则Object obj;此句将报错。

只能有一个默认构造函数,且此默认构造函数不接受任何参数。

我们可能通过给已有的构造函数的所有参数提供默认值,来隐式提供默认构造函数。

1
2
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});

也可以通过函数重载定义一个无参数的构造函数作为默认构造函数。

1
2
3
4
5
6
7
8
9
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章有例外)。

如果程序员没有定义析构函数,编译器将隐式地声明一个默认析构函数。

1
2
3
// 析构函数 -- 无返回值 -- 名称为 ~ + 类名 -- 无参数
~Object();
Object::~Object() {}

this指针

this指针指向用来调用成员函数的对象。

this指针被作为隐藏参传递给成员函数,且只能是第一个参数。

在成员函数后面加const的实质是给第一个参数即this指针加`const属性。

1
2
3
4
const Object& Object::min_id(const Object& b) const {
if(m_id > b.m_id) return b;
return *this;
}
1
2
3
void Object::show() const;
// 其C风格定义为
void show(const Object* this);

使用类对象

类对象的声明与内置类型的声明类似。

1
Object obj;

可以像结构体一样操作类成员和方法,但其受访问控制限制。

每个新对象都有自己的存储空间,用于存储内部变量和类成员。

但同一个类的所有对象共享同一组类方法,即每种方法只有一种副本。

类声明和类方法构成了__服务器__,供用户使用。而客户程序员将使用服务器提供的类,进行一些操作,此即__客户端__。

修改类方法的实现时,只要接口没变,便不影响客户端的运行,这也是OOP的核心思想之一。

1
2
3
4
5
6
7
8
9
10
#include "Object.h"

int main() {

Object obj;
obj.set_name("dua");
obj.show();

return 0;
}

对象数组

声明对象数组的方法同声明标准变量数组的方法相同。

1
Object objs[maxn];

首先使用默认构造创建每个元素对象。花括号里的构造函数将创建临时对象。然后将临时对象复制到相应元素中。即要想创建类对象数组,这个类必须有默认构造函数。

1
2
3
4
5
Object objs[4] = {
Object(1,"ddd",vec3{1,1,1},vec3{1,1,1},vec3{1,1,1},vec3{1,1,1}),
Object()
};
// 第一个和第二个分别用花括号里的两个构造函数构造,剩下的使用默认构造

类作用域

在类定义中的名称(成员变量和成员函数)的作用域是整个类。这是有别于第九章所介绍的作用域之外的一种新型的作用域。

在类作用域内直接使用const是行不通的,在创建对象之前没有用于存储其值的空间。

1
2
const int maxn = 100;
Object* child[maxn]; // 会报错

可以使用枚举来满足这种需求。

1
2
enum { maxn = 100 };
Object* child[maxn]; // 正确

使用枚举并不会创建类成员,maxn只是一个符号名称。

由于此枚举的作用只是创建符号常量,不用创建变量,故可省略名称。

除此之外,我们也可以使用关键字static来创建静态变量。

1
2
static const int maxn = 100;
Object* child[maxn]; // 正确

此时maxn为静态变量,与其他静态变量存储在一起,而不存储在对象中。

作用域内枚举(c++11)

传统枚举的作用域为整个文件,这可能会导致两个枚举定义中的枚举量发生冲突。

1
2
enum egg {small, medium, large};
enum shirt {small, medium, large}; // 报错,与egg冲突

c++11提供了一种新的枚举量,其作用域为类,使用class(或struct)关键字声明。

1
enum class egg {small, medium, large};

使用该种枚举量的时候,我闪需要使用枚举名来限定枚举量。

1
egg x = egg::small;

类作用域内枚举不会自动转换类型,所以需要手动强制转换。

1
cout << int(x) << endl;

枚举的默认底层实现为int,可使用如下方式改变底层实现,此法也适用于传统枚举。

1
enum class egg:short { small, medium, large};