文章目录
- 🌷专栏导读
- 🌷什么是面向对象?
- 🌷类的引入
- 🌷什么是类
- 🌷类的定义方式
- 🌷类的访问限定符与封装
- 🌺访问限定符
- 🌺封装
- 🌷类的作用域
- 🌷类的实例化
- 🌷类对象模型
- 🌷this指针
- 🌺this指针的特性
- 🌷C语言与C++实现栈的对比
- 🌺C语言实现
- 🌺C++实现
🌷专栏导读
🌸作者简介:花想云,在读本科生一枚,致力于 C/C++、Linux 学习。
🌸本文收录于 C++系列,本专栏主要内容为 C++ 初阶、C++ 进阶、STL 详解等,专为大学生打造全套 C++ 学习教程,持续更新!
🌸相关专栏推荐:C语言初阶系列 、C语言进阶系列 、数据结构与算法
本章内容为C++修炼之筑基期系列的第一篇文章,初步认识类与对象,本章我们会学习什么是面向对象?类的定义、类的作用域、类的实例化、类对象模型以及this指针。此外筑基期系列文章都是关于类与对象的讲解。
🌷什么是面向对象?
学习C语言时,我们就经常听说C语言是面向过程
的,那么什么是面向过程
呢?举个例子,我们现在要完成洗衣服的工作,此刻我们关注的是洗衣服的过程:
- 拿个盆子;
- 接水;
- 放衣服;
- 到洗衣粉
- …
C语言是
面向过程
的,关注的是过程
,分析出求解问题的步骤,通过函数调用逐步解决问题
。C++是
面向对象
的,关注的是对象
,在解决问题时,通过对象间的交互
来完成,每个对象负责不同的工作。
例如,我们将洗衣服的任务分配给几个对象来完成:
🌷类的引入
C语言中,结构体内只能定义变量,在C++
中,结构体内不仅可以定义变量
,还可以定义函数
。
以前用C语言
实现数据结构——栈
时,我们这样定义:
//C语言
typedef int dataOfStackType;
typedef struct stack
{
dataOfStackType* a;
int top;
int capacity;
}stack;
void StackInit(stack* ps);
void StackPush(stack* ps, dataOfStackType data);
void StackPop(stack* ps);
//...
而在C++
中,我们可以这样定义:
//C++
typedef int dataOfStackType;
typedef struct stack
{
void StackInit(stack* ps);
void StackPush(stack* ps, dataOfStackType data);
void StackPop(stack* ps);
//...
dataOfStackType* a;
int top;
int capacity;
}stack;
像上面的定义方式,C++
中更喜欢用一个新的名字——class
来代替struct
。
class stack
{
void StackInit(stack* ps);
void StackPush(stack* ps, dataOfStackType data);
void StackPop(stack* ps);
//...
dataOfStackType* a;
int top;
int capacity;
};
🌷什么是类
class className
{
//...类的主体:由成员函数和成员变量组成
};
如上所示,类的定义与结构体的定义非常相似,但我们还是得认识几个专有名词。
class
:定义类的关键字;class Name
:类的名字;类的主体
:{}中为类的主体,由成员变量
和成员方法
组成;成员变量
:又称类的属性
,指在类中定义的变量;成员函数
:又称类的方法
,指在类中定义的函数;
🌷类的定义方式
类的定义方式一般有两种:
声明和定义全部放在类体中
,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数
处理。
//日期类
class Date
{
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
int _year;
int _month;
int _day;
};
类的声明放在.h文件中
,成员函数定义放在.cpp文件中
,注意:成员函数名前需要加类名::
。
//Date.h文件中声明类
//日期类
class Date
{
public:
void Init(int year, int month, int day);
void Print();
int _year;
int _month;
int _day;
};
//Date.c文件中定义成员函数
#include"Date.h"
void Date::Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Date::Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
一般情况下,更期望采用第二种方式
。至于其中的public
是什么意思,这个等下再说。
🌷类的访问限定符与封装
🌺访问限定符
我们在类中定义了各种成员函数与成员变量,有时候,我们不想让别人随便访问类中的某些成员,比如成员变量,但其它的成员对外开放,比如成员函数,那么我们就需要用到访问限定符
来修饰这些成员。
访问限定符有三个:public
、protected
、private
。
访问限定符说明
class
的默认访问权限为private
,struct
为public
(因为struct要兼容C);public
修饰的成员在类外可以直接被访问
;protected
和private
修饰的成员在类外不能直接被访问
(此处 protected 和 private 是类似的);- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止;
- 如果后面没有访问限定符,作用域就到 } 即类结束。
注意:访问限定符只在编译时有用
,当数据映射到内存后,没有任何访问限定符上的区别。
一道面试题
问题:C++中 struct 和 class 的区别是什么?
解答: C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类默认访问权限是private
。
🌺封装
面向对象有三大特性:封装、继承、多态。在当前阶段,我们只学习封装的特性。
- 什么是封装呢?
用专业一点的话来回答就是:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互
。
封装本质上是一种管理,让用户更方便使用类
。比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。
对于计算机使用者而言,不用关心内部核心部件
,比如主板上线路是如何布局的,CPU内部是如何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可
。因此计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以及键盘插孔等
,让用户可以与计算机进行交互即可
。
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节
,控制哪些方法可以在类外部直接被使用
。
🌷类的作用域
类定义了一个新的作用域
,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 ::作用域操作符
指明成员属于哪个类域。
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
cout << _name << " "<< _gender << " " << _age << endl;
}
🌷类的实例化
用类创建对象的过程,称为类的实例化。
//类的声明
class Date
{
public:
void Init(int year, int month, int day);
void Print();
int _year;
int _month;
int _day;
};
有这样一个问题
- 上面实现的Date类占用多大的内存空间?
答案是,不占用空间
。因为定义类只是一个描述对象的过程
。
类实例化出对象就像现实中使用设计图建造出房子,类
就像是设计图
,只设计出需要什么东西,但是并没有实体的建筑存在,而实例化出的对象
才是真正盖好的房子
,能实际存储数据,占用物理空间。
像设计图一样,并不是一幅设计图只能盖一个房子,一个类也可以实例化出多个对象
。
class Date
{
public:
//...
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2;
Date d3;
cout << sizeof(d1) << endl;
cout << sizeof(d2) << endl;
cout << sizeof(d3) << endl;
return 0;
}
🌷类对象模型
我们刚刚用sizeo
f观察了实例化出来的对象确实占用了内存空间,但是我们并不知道结果为什么是 12
。
上文已经提到,C++中,类与结构体本质是相同的
。为了兼容C语言,C++肯定不能改变结构体大小的计算规则,而类与结构体相同(除默认访问限定符不同),那么类当然也是运用与结构体相同的计算规则
。
- 类中定义的成员函数不会占用空间吗?
答案是并不占用
,因为成员函数
在公共代码段
。
简单写个程序验证一下:
// 类中既有成员变量,又有成员函数
class A1 {
public:
void f1() {}
private:
int _a;
};
// 类中仅有成员函数
class A2 {
public:
void f2() {}
};
// 类中什么都没有---空类
class A3
{};
int main()
{
cout << sizeof(A1) << endl;
cout << sizeof(A2) << endl;
cout << sizeof(A3) << endl;
return 0;
}
结果显而易见,仅有成员函数
的类与空的
类大小是相同的,那么就验证了上面的说法。但是又出来一个新的问题:
- 为什么空类的大小为 1 呢?
答案是,空类比较特殊,编译器会给空类一个字节来唯一标识这个类的对象
。
总结
一个类的大小,实际上是该类中成员变量的大小之和
,当然与结构体相同,需注意内存对齐
。空类
须特殊处理,大小为1
字节。
在C语言阶段,博主已经对结构体的内存对齐做了超级详细的讲解,若有遗忘的小伙伴点击此处跳转。
以下是对内存对齐的简单回忆。
结构体内存对齐规则
- 第一个成员在与结构体
偏移量为0
的地址处。 - 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。注意:
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值
。VS中默认的对齐数
为8
- 结构体总大小为:
最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍
。 - 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
🌷this指针
在类的成员函数中,有这样一个存在,我们无时无刻不在用它,但是我们却看不见它,这个神奇的存在就是this指针
。
我们如何证明它存在呢?首先定义一个日期类。
//日期类
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
对它进行使用。
int main()
{
Date d1, d2;
d1.Init(2022, 1, 11);
d2.Init(2022, 1, 12);
d1.Print();
d2.Print();
return 0;
}
对于上述操作,有这样一个疑问:
- Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?
这就不得不提我们刚才谈到的this指针
了。C++编译器给每个非静态的成员函数
增加了一个隐藏
的指针参数,让该指针指向当前对象
(函数运行时调用该函数的对象),在函数体中所有成员变量
的操作,都是通过该指针去访问
。只不过所有的操作对用户是透明的
,即用户不需要来传递,编译器自动完成
。
🌺this指针的特性
this指针有以下特性:
- this指针的类型:
类的类型* const
,即成员函数中,不能给this指针赋值;
void Init(int year, int month, int day)
{
//错误示例
this=nullptr;
}
- 只能在
成员函数
的内部使用; - this指针本质上是
成员函数
的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针
; - this指针是
成员函数
第一个隐含的指针形参,一般情况由编译器通过ecx寄存器
自动传递,不需要用户传递;
有了this指针,我们上面实现的日期类还可以这样实现。
//日期类
class Date
{
public:
void Init(int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
void Print()
{
cout <<this-> _year << "-" <<this-> _month << "-" <<this-> _day << endl;
}
private:
int _year;
int _month;
int _day;
};
但是在实际写代码的过程中没有必要这样写,既然编译器已经帮我们做了,我们就不要干吃力不讨好的事情。
到目前为止,我们已经简单的认识了类和对象,更多关于类和对象的内容还没有讲到。认识了类与对象,我们就简单对比一下C语言实现栈和C++中运用类实现栈
的过程。
🌷C语言与C++实现栈的对比
🌺C语言实现
typedef int DataType;
typedef struct Stack
{
DataType* array;
int capacity;
int size;
}Stack;
void StackInit(Stack* ps)
{
assert(ps);
ps->array = (DataType*)malloc(sizeof(DataType) * 3);
if (NULL == ps->array)
{
assert(0);
return;
}
ps->capacity = 3;
ps->size = 0;
}
void StackDestroy(Stack* ps)
{
assert(ps);
if (ps->array)
{
free(ps->array);
ps->array = NULL;
ps->capacity = 0;
ps->size = 0;
}
}
void CheckCapacity(Stack* ps)
{
if (ps->size == ps->capacity)
{
int newcapacity = ps->capacity * 2;
DataType* temp = (DataType*)realloc(ps->array,
newcapacity * sizeof(DataType));
if (temp == NULL)
{
perror("realloc申请空间失败!!!");
return;
}
ps->array = temp;
ps->capacity = newcapacity;
}
}
void StackPush(Stack* ps, DataType data)
{
assert(ps);
CheckCapacity(ps);
ps->array[ps->size] = data;
ps->size++;
}
int StackEmpty(Stack* ps)
{
assert(ps);
return 0 == ps->size;
}
void StackPop(Stack* ps)
{
if (StackEmpty(ps))
return;
ps->size--;
}
DataType StackTop(Stack* ps)
{
assert(!StackEmpty(ps));
return ps->array[ps->size - 1];
}
int StackSize(Stack* ps)
{
assert(ps);
return ps->size;
}
int main()
{
Stack s;
StackInit(&s);
StackPush(&s, 1);
StackPush(&s, 2);
StackPush(&s, 3);
StackPush(&s, 4);
printf("%d\n", StackTop(&s));
printf("%d\n", StackSize(&s));
StackPop(&s);
StackPop(&s);
printf("%d\n", StackTop(&s));
printf("%d\n", StackSize(&s));
StackDestroy(&s);
return 0;
}
可以看到,在用C语言实现时,Stack相关操作函数有以下共性:
- 每个函数的第一个参数都是
Stack*
- 函数中必须要对第一个参数
检测
,因为该参数可能会为NULL
- 函数中都是通过
Stack*
参数操作栈的 - 调用时必须传递Stack结构体变量的地址
结构体中只能定义存放数据
的结构,操作数据的方法不能放在结构体中,即数据和操作数据的方式是分离开的,而且实现上相当复杂一点,涉及到大量指针操作,稍不注意可能就会出错。
🌺C++实现
typedef int DataType;
class Stack
{
public:
void Init()
{
_array = (DataType*)malloc(sizeof(DataType) * 3);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = 3;
_size = 0;
}
void Push(DataType data)
{
CheckCapacity();
_array[_size] = data;
_size++;
}
void Pop()
{
if (Empty())
return;
_size--;
}
DataType Top() { return _array[_size - 1]; }
int Empty() { return 0 == _size; }
int Size() { return _size; }
void Destroy()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
void CheckCapacity()
{
if (_size == _capacity)
{
int newcapacity = _capacity * 2;
DataType* temp = (DataType*)realloc(_array, newcapacity *
sizeof(DataType));
if (temp == NULL)
{
perror("realloc申请空间失败!!!");
return;
}
_array = temp;
_capacity = newcapacity;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
int main()
{
Stack s;
s.Init();
s.Push(1);
s.Push(2);
s.Push(3);
s.Push(4);
printf("%d\n", s.Top());
printf("%d\n", s.Size());
s.Pop();
s.Pop();
printf("%d\n", s.Top());
printf("%d\n", s.Size());
s.Destroy();
return 0;
}
C++中通过类可以将数据
以及 操作数据的方法
进行完美结合,通过访问权限
可以控制那些方法在类外可以被调用,即封装
,在使用时就像使用自己的成员一样,更符合人类对一件事物的认知。而且每个方法不需要传递Stack*
的参数了,编译器编译之后该参数会自动还原,即C++中 Stack *参数是编译器维护的,C语言中需用用户自己维护。