目录
一、 面向对象的基本理念
1.1 - 什么是对象?
1.2 - 类和对象
1.3 - 面向对象的五条原则
1.4 - 面向过程 vs 面向对象
二、C++ 中的结构体
三、类的定义
3.1 - 类的两种定义方式
3.2 - 成员变量的命名规范
四、类的访问限定符和封装
4.1 - 访问限定符
4.2 - 封装
五、类的实例化
六、类的存储方式
七、this 指针
7.1 - this 指针的引入
7.2 - this 指针的特性
7.3 - 练习
参考资料:
浙江大学 C++ 翁恺老师。
【C++】什么是对象?什么是类?_c++ 里的 对象定义。
面向过程 VS 面向对象 - 知乎 (zhihu.com)。
一文带你入门C++类和对象【十万字详解,一篇足够了】。
一、 面向对象的基本理念
1.1 - 什么是对象?
-
Object = Entity
-
Object may be visible or invisible.
-
Object is variable in programming languages.
在现实世界中,任何事物都是对象。
它可以是有形的具体存在的事物,例如一张凳子、一台电脑、一个学生、一辆汽车等,也可以是无形的抽象的事物,例如一次演出、一场球赛等。
在程序设计语言中,对象实际上就是变量。
-
Objects = Atrributes + Services
Data: the properties or status
Operations: the functions
对象是描述其属性的数据以及对这些数据施加的一组操作封装在一起构成的统一体。
例如,一个学生就是一个对象,学生的学号、姓名和成绩等数据就是他的属性,输入或输出学号、姓名和成绩等就是对数据施加的一组操作。
-
Object send and receive messages(objects do things!)
Messages are
—Composed by the sender
—Interpreted by the receiver
—implemented by the methods
Message
—May cause receiver to change state
—May return results
每个对象都有自己的属性和行为,对象和对象之间通过方法来交互。
1.2 - 类和对象
-
Objects(cat)
Represent things, events, or concepts
Respond to messages at run-time
-
Classes(cat class)
Define properties of instances
Act like types in C++
类和对象之间的关系是抽象和具体的关系。类是多个对象进行综合抽象的结果,对象又是类的个体实物,一个对象是类的一个实例。
1.3 - 面向对象的五条原则
-
Everything is an object.
-
A program is a bunch of objects telling each other what to do by sending messages.
-
Each objects has its own memory made up of other objects.
-
Every object has a type.
-
All objects of a particular type can receive the same messages.
第五条原则可以分别两个方面来理解。从正面理解,即一个特定类型的所有对象可以接受相同的消息;从反面理解,即所有可以接受相同消息的对象可以被认为是同一类型。
1.4 - 面向过程 vs 面向对象
面向过程(Procedure Oriented,简称 PO)是以 "事件" 为中心的编程思想,编程的时候首先把解决问题的步骤分析出来,然后用函数把这些步骤实现,在一步一步的具体步骤中再按顺序调用函数。
以五子棋为例,面向过程的设计思路是首先分析解决这个问题的步骤,即:
(1) 开始游戏 (2) 黑子先走 (3) 绘制画面 (4) 判断输赢 (5) 白子再走 (6) 绘制画面 (7) 判断输赢 (8) 返回步骤(2) (9) 输出最后结果
然后用函数实现上面一个一个的步骤,在一步一步的具体步骤中再按顺序调用函数。
面向对象(Object Oriented,简称 OO)是一种以 "对象" 为中心的编程思想,把要解决的问题分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描述某个对象在整个解决问题的步骤中的属性和行为。
在五子棋的例子中,用面向对象的方法来解决的话,首先将整个五子棋游戏分为三个对象:
(1) 黑白双方,这两方的行为是一样的
(2) 棋盘系统,负责绘制画面
(3) 规则系统,负责判定犯规、输赢等
然后赋予每个对象一些属性和行为:
第一类对象(黑白双方)负责接受用户输入,并告知第二类对象(棋盘系统)棋子布局的变化,棋盘系统接受到了棋子的变化,并负责在屏幕上显示出这种变化,同时利用第三类对象(规则系统)来对棋局进行判定。
二、C++ 中的结构体
C 结构体中只能包含成员变量,而 C++ 结构体的成员不仅可以是变量,还可以是函数。
C doesn't support the relationship between data and functions.
用 C 和 C++ 分别实现栈时,Stack.h
中的内容分别应该是:
<C 版本>:
#pragma once
#include <stdbool.h>
// 动态顺序栈
typedef int STDataType;
typedef struct Stack
{
STDataType* data;
int top;
int capacity;
}Stack;
// 基本操作
void StackInit(Stack* pst, int default_capacity); // 初始化
bool StackEmpty(Stack* pst); // 判断是否为空栈
void StackPush(Stack* pst, STDataType x); // 入栈
void StackPop(Stack* pst); // 出栈
STDataType StackTop(Stack* pst); // 返回栈顶元素
int StackSize(Stack* pst); // 返回栈的有效元素个数
void StackDestroy(Stack* pst); // 销毁
<C++ 版本>:
#pragma once
typedef int STDataType;
struct Stack
{
public:
void Init(int default_capacity = 5); // 初始化
bool Empty(); // 判断是否为空栈
void Push(const STDataType& x); // 入栈
void Pop(); // 出栈
STDataType Top(); // 返回栈顶元素
int Size(); //返回栈的有效元素个数
void Destroy(); // 销毁
private:
STDataType* _data;
int _top;
int _capacity;
};
问题:
public adj. 公开的;private adj. 私密的
这两个关键字的作用是什么(大概可以知道,该结构体仅公开对外的接口,而隐藏内部的数据)?
成员变量名前面为什么要加一个下划线(
_
)?在 C++ 中更喜欢用关键字 class 来代替 struct,那么 struct 和 class 的异同是什么?
成员函数又该如何定义?
三、类的定义
语法格式:
class className
{
// 类的成员,包括成员函数(类的方法)和成员变量(类的属性)
}; // 分号不能省略
3.1 - 类的两种定义方式
在 C++ 中,通常用一个 .h
和一个 .cpp
文件定义一个类(例如 Stack.h
和 Stack.cpp
)。① 声明放在 .h
文件中,成员函数定义则放在 .cpp
文件中。
由于类定义了一个新的作用域,即类作用域,所以在类体外定义成员函数,需要加作用域。例如:
Stack.cpp
:
#include "Stack.h"
#include <stdlib.h>
#include <assert.h>
// 初始化
void Stack::Init(int default_capacity)
{
_data = (STDataType*)malloc(sizeof(STDataType) * default_capacity);
if (nullptr == _data)
{
perror("initialization failed!");
exit(-1);
}
_top = 0;
_capacity = default_capacity;
}
// 判断是否为空栈
bool Stack::Empty()
{
return _top == 0;
}
// 入栈
void Stack::Push(const STDataType& x)
{
if (_top == _capacity)
{
STDataType* tmp = (STDataType*)realloc(_data, sizeof(STDataType) * 2 * _capacity);
if (nullptr == tmp)
{
perror("realloc failed!");
return;
}
_data = tmp;
_capacity *= 2;
}
_data[_top++] = x;
}
// 出栈
void Stack::Pop()
{
assert(!Empty()); // 前提是栈非空
--_top;
}
// 返回栈顶元素
STDataType Stack::Top()
{
assert(!Empty()); // 前提是栈非空
return _data[_top - 1];
}
//返回栈的有效元素个数
int Stack::Size()
{
return _top;
}
// 销毁
void Stack::Destroy()
{
free(_data);
_data = nullptr;
_top = _capacity = 0;
}
② 成员函数的定义也可以放在类体中,此时编译器可能会将其当成内联函数处理。
3.2 - 成员变量的命名规范
#include <iostream>
using namespace std;
class Date
{
public:
void Init(int year, int month, int day)
{
year = year;
month = month;
day = day;
}
void Display()
{
cout << year << "-" << month << "-" << day << endl;
}
private:
int year;
int month;
int day;
};
int main()
{
Date d;
d.Init(2023, 5, 1);
d.Display(); // 随机值(说明初始化失败)
return 0;
}
由此可以发现,当成员变量和形参同名时,是无法完成初始化的。类似于:
#include <iostream>
using namespace std;
int g_val = 10;
int main()
{
int g_val = 20;
g_val = g_val; // 该语句并不会将全局变量 g_val 的值赋值给局部变量 g_val
cout << g_val << endl; // 20
return 0;
}
解决方案如下:
class Date
{
public:
void Init(int year, int month, int day)
{
// 一、加作用域
/*
Date::year = year;
Date::month = month;
Date::day = day;
*/
// 二、使用 this 指针(后面会详解)
/*
this->year = year;
this->month = month;
this->day = day;
*/
// 三、对成员变量名进行修改
_year = year;
_month = month;
_day = day;
}
void Display()
{
cout << year << "-" << month << "-" << day << endl;
}
private:
int _year; // 或者 mYear, m 即 member
int _month; // 或者 mMonth
int _day; // 或者 mDay
};
四、类的访问限定符和封装
4.1 - 访问限定符
-
public 修饰的成员在类内和类外都可以直接被访问。
-
protected 和 private 修饰的成员在类外不能直接被访问(此处 protected 和 private 是类似的)。
-
访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,如果后面没有访问限定符,作用域就到
}
,即类结束。 -
class 的默认访问权限是 private,struct 为 public。
因为 C++ 要兼容 C,所以在 C++ 中 struct 可以当作结构体使用,也可以用来定义类,和用 class 定义类是一样的,区别在于 struct 定义的类的默认访问权限是 public,class 定义的类的默认访问权限是 private。
注意,在继承和模板参数列表位置,struct 和 class 也有区别,后续进行讲解。
4.2 - 封装
面向对象的三大特性:封装、继承和多态。
封装(encapsulation):将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口和对象进行交互。
capsule n. 胶囊
封装本质上是一种管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入、显示器、USB 插孔等,让用户和计算机进行交互,完成日常事物。但实际上工作的却是 CPU、显卡、内存等一些硬件原件。
对于计算机使用者而言,不用关心内部核心部件,比如主板上路线是如何布局的,CPU 内部是如何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关、鼠标及键盘插孔等,让用户可以与计算机进行交互即可。
在 C++ 中实现封装,可以通过类将数据及操作数据的方法进行有机结合,通过访问权限来隐藏对象的属性和实现细节,控制哪些方法可以在类外直接被使用。
五、类的实例化
用类创建对象的过程,称为类的实例化。
-
类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义一个类并没有分配实际的内存空间来存储它。
-
一个类可以实例化出多个对象,实例化出的对象,占用实际的物理空间,存储类成员变量。
做个比方,类实例化对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间。
六、类的存储方式
类对象的存储方式猜测:
-
对象中包含类的各个成员:
显然,这种存储方式的一大缺陷在于,当创建多个对象时,每个对象的成员变量不同,但成员函数是相同的,因此会存在多份相同的代码,造成空间浪费。
-
相同的代码只保存一份,在对象中存放代码的地址:
-
只保存成员变量,成员函数存放在公共的代码段:
实际上是采用第三种存储方式。
#include <iostream>
using namespace std;
class A
{
public:
void Init(char c1, int i, char c2);
void Print();
private:
char _c1;
int _i;
char _c2;
};
void A::Init(char c1, int i, char c2)
{
_c1 = c1;
_i = i;
_c2 = c2;
}
void A::Print()
{
cout << _c1 << " " << _i << " " << _c2 << endl;
}
int main()
{
cout << sizeof(A) << endl; // 12(注意:存在内存对齐)
return 0;
}
注意:对于只有成员函数的类和空类,
sizeof(classNmae)
为 1(编译器给一个字节来唯一标识这个类的对象)。
七、this 指针
7.1 - this 指针的引入
#include <iostream>
using namespace std;
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Display()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2;
d1.Init(2023, 5, 1);
d2.Init(2023, 5, 2);
d1.Display(); // 2023-5-1
d2.Display(); // 2023-5-2
return 0;
}
问题:Date 类中有 Init
和 Display
两个成员函数,函数体中没有关于不同对象的区分,那么当 d1 调用 Init
和 Display
函数时,这两个函数是如何知道初始化和显示的对象是 d1,而不是其他,例如 d2,对象呢?
解答:C++ 编译器给每个 "非静态的成员函数" 增加一个隐藏的指针参数 this,让该指针指向当前对象(函数运行时调用该函数的对象)。在函数体中所有 "成员变量" 的操作,都是通过该指针去访问,只不过所有的操作对用户是透明的,即用户不需要传递,编译器自动完成。
#include <iostream>
using namespace std;
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
// 可以在成员函数内部显示地使用 this 指针
cout << "this = " << this << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2;
cout << "&d1 = " << & d1 << endl;
d1.Init(2023, 5, 1);
// 输出的两个地址相同
cout << "&d2 = " << & d2 << endl;
d2.Init(2023, 5, 2);
// 输出的两个地址也相同
return 0;
}
7.2 - this 指针的特性
-
this 为指针常量,即在成员函数中,不能改变 this 的指向。
-
this 指针本质上是成员函数的形参,当对象调用成员函数时,将对象地址作为实参传递给 this 形参,所以对象中不存储 this 指针。
d1.Init(2023, 5, 1); // 可以视为 Date::Init(&d1, 2023, 5, 1); d1.Display(); // 可以视为 Date::Display(&d1); // 注意:不能显示地将对象的地址传递给 this 指针, // 所以 Date::Init(&d1, 2023, 5, 1); 和 Date::Display(&d1); 实际上是不被允许的
-
this 指针是成员函数第一个隐含的指针形参,一般情况由编译器通过 ecx 寄存器自动传递,不需要用户传递。
7.3 - 练习
#include <iostream>
using namespace std;
class A
{
public:
void f()
{
cout << "f()" << endl;
}
private:
int _i;
};
int main()
{
A* pa = nullptr;
pa->f();
return 0;
}
问:上面程序编译运行的结果是()?
编译报错
运行崩溃
正常运行
pa->f();
可以被视为A::f(pa);
,this 指针为空,但在函数内部没有对 this 指针进行解引用,所以并不会出现运行错误,而是正常输出 "f()"。