类的引入,是C++面向对象特点的基础。那么类是什么呢?看完本篇相信你就会知道。
C语言是面向过程的结构化和模块化的编程语言,关注的是过程,通过具体的步骤,一步一步解决问题。 C++语言是基于面向对象的,关注的是对象,通过将一件事情拆分成不同的对象,靠对象之间的交互解决问题。 在C语言中,有者和类相似的概念 - 结构体。 我们可以在C语言中创建不同的结构体类型,通常是把一些变量封装在结构体中,抽象为一个新类型。 比如C语言实现栈(部分):
在C语言中结构体中只封装了数据成员(变量),具体的功能实现(函数)在结构体外部。数据成员和函数实现之间是分开的、相互独立的。 这样就会产生一些问题: 实现相同的功能,代码一般较长,即实现比较麻烦; 往往涉及大量的指针操作,这非常容易出现意料之外的错误,使得我们必须非常小心。 结构体没有对使用者做出任何限制,太自由了。我们很多时候是不希望直接操作结构体里的数据的,使用者可能会选择不调用对应的功能函数而直接操作结构体里的数据,极有可能使用者并没有注意到实现的细节就直接使用结构体变量中的数据,非常容易导致出错。 C++语言则引入了类的概念,改进C语言存在的问题,并用类实现了面向对象的操作。
C++从C而来,可以兼容C语言代码,C语言所写的结构体在C++中也支持,体现了C++语言的向前兼容。
同时,C++对C中的结构体struct
进行了扩展和升级,struct
结构体具有了和C++中类class
基本相同的功能。
//升级的struct,与 类 class的功能相同
//C语言用法 - 结构体
struct ListNode {
int val;
struct ListNode* next;
};
//C++用法 - 类;C语言不能这么用(C的语法不支持)
struct ListNode {
int val;
ListNode* next;
};
于是,数据结构栈的写法可以变成下面的写法
C++中的结构体struct
为了和C语言中的结构体struct
兼容,在没有访问限定符时,默认是成员变量和成员函数公共的。
C++中的类class
则没有这个包袱,在没有访问限定符时类的成员变量和成员函数是私有的。
关键字class
,后接一个类名chassName
(标识符),接着是一对花括号{}
括起来的类体,最后以分号;
结束。
类体中的内容称为类的成员:
类体中的变量称为成员变量,也叫作做的属性;
类中的函数称为成员函数,也叫做类的方法。
class className{
//类体:成员函数 + 成员变量
};
在类中定义的成员函数编译器默认其为内联函数
class Stack {
public:
void Init(size_t capacity = 4) {
_array = (int*)malloc(sizeof(int) * capacity);
if (_array == nullptr) {
perror("Init file");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
void Destroy() {
free(_array);
_array = nullptr;
_top = _capacity = 0;
}
void Push(int val) {
if (_top == _capacity) {
size_t newcapacity = _capacity * 2;
int* tmp = (int*)realloc(_array, sizeof(int) * newcapacity);
if (tmp == nullptr) {
perror("Push flie");
exit(-1);
}
_array = tmp;
_capacity = newcapacity;
}
_array[_top] = val;
++_top;
}
void Pop() {
--_top;
}
int Top() {
return _array[_top - 1];
}
bool Empty() {
return _top == 0;
}
private:
int* _array;//指向数组
size_t _top;//栈顶的下一个位置
size_t _capacity;//栈容量
};
类的声明和定义都放在类中,这比较好理解,但是有一个问题:类中的成员函数比较少还可以这么整,但当类中成员函数较多时类就显得臃肿不堪了,也不方便去对类进行和调试,不能直观的分析类的功能。
类中的成员函数
statement.h
class Stack {
public:
//缺省值能声明或定义一处给出
void Init(size_t capacity = 4);
inline void Destroy();
bool Empty();
private:
int* _array;
size_t _top;
size_t _capacity;
};
class Queue {
public:
void Init();
void Destroy();
bool Empty();
private:
typedef struct ListNode {
int val;
struct ListNode* next;
}LTNode;
LTNode* _head;
LTNode* _tail;
size_t _size;
};
define.cpp
#include "statement.h"
//这里需要 加域作用限定符 指定属于哪个类
void Stack::Init(size_t capacity) {
_array = (int*)malloc(sizeof(int) * capacity);
if (_array == nullptr) {
perror("Init file");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
void Stack::Destroy() {
free(_array);
_array = nullptr;
_top = _capacity = 0;
}
bool Stack::Empty() {
return _top == 0;
}
void Queue::Init() {
_head = _tail = nullptr;
_size = 0;
}
void Queue::Destroy() {
while (!Empty()) {
LTNode* del = _head;
_head = _head->next;
free(del);
}
_head = _tail = nullptr;
_size = 0;
}
bool Queue::Empty() {
return _head == nullptr;
}
类中成员函数声明和定义分离的好处:
类体中的代码大量减少,只保留了成员函数的声明,需要时可以不看成员函数具体实现,通过快速在类中浏览成员函数的声明就可以迅速了解类的大致功能,方便他人也方便自己。
定义与声明分离,也可以保护代码,防止函数实现被修改,避免源码的泄露。
定义静态库或动态库,只提供接口给使用者,从而隐藏具体的实现细节。
** 类外成员函数实现的一个错误:**
原因是编译器不知道函数Init()
到底是属于哪个类的。
没有指定查找的地方时,编译器默认首先在函数内部局部域查找,找不到再去全局域查找,再找不到就报错了。
需要注意的是:
类外的成员函数在具体实现时,在函数开始需要使用对应的类名className
和域作用限定符::
对成员函数进行修饰限定,这是为了说明该成员函数是属于哪个类的,防止编译器发生混淆。
指定查找的地方时,编译器首先去函数内部局部域查找,再去指定的类作用域查找,找不到再去全局域查找,再找不到就报错。
C语言要求变量和函数需要先声明或定义再使用,这是因为C语言中各个部分是相对独立的,程序又是顺序执行的,只能向上寻找,C++中的类class
则与之不同。
类中的成员函数和成员变量定义和声明的先后位置是没有要求的,这是因为类是一个作用域,在类内的成员变量和成员函数是一个有机的整体,当需要使用类内的某个变量或函数时,会在类中所有地方寻找,而不是在使用的地方之前寻找。
先来看一个例子:
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;
};
形参与类内成员函数存在同名的情况,这可能会导致一些问题。
解决方法1:类内成员变量前加上域作用限定符修饰
方法2:类内成员变量定义时,对变量名进行手动修饰,如:加上前缀、后缀、大小写等。目的是区分变量和传入的形参。
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;
//_day 或 day_ 或 m_day 或 mDay等
};
int main() {
Date d;
d.Init(2122, 10, 10);
d.Print();
}
访问限定符分为
公有 :
public
私有:private
保护:protected
class A {
public:
int a;
private:
int b;
protected:
int c;
};
int main() {
A A1;
return 0;
}
public
修饰的成员在类外可以直接访问;
protected
和private
修饰的成员在类外不能被直接访问;
访问权限作用域从该访问限定符开始直到下一个访问限定符出现为止;如果后面没有访问限定符,作用域就到
}
结束;也就是说,域作用限定符把类作用域分隔开了,形成一个个属性不同的小作用域
class
的默认访问权限为private
,而struct
默认访问权限为public
。struct
可以当作结构体使用,也可以作为类使用。 这是为了兼容C语言中的struct
的使用,C语言中结构体的变量可以直接在结构体外使用,相当于是公共public
的。
访问限定符只有在编译时起作用(所以挑战访问限定符时在编译期间产生的是编译错误,由编译器控制),当数据映射到内存后,没有任何访问限定符上的区别。
首先我们直到面向对象的三大特征:封装、继承和多态。
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口用以和对象进行交互。 封装本质上是一种对数据和方法的管理,使用者因此可以方便的使用类。 C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来 隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
类定义了一个新的作用域,类的所有成员都在类的作用域中。
在类体外定义成员时,需要使用作用域操作符::
指明成员属于哪个类域。
这里有一个问题:
这里可以像命名空间域那样访问命名空间成员那样,使用域作用限定符::
访问某个类域中的某个成员吗?
答案是不能。
命名空间中变量或函数等是已经定义的,有着储存空间,是一个实际对象;而类只是一种类型 - 类类型,不占任何储存空间,不是一个实际的对象,只有在类实例化 - 定义了类对象后,才能访问到类对象内部成员。
类的实例化:用类类型创建对象的过程。
类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没 有分配实际的内存空间来存储它; 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量 ** 类实例化出对象就像现实中使用建筑设计图建造出房子**,类就像是设计图,类只是一个设计,实例化出的对象 才能实际存储数据,占用物理空间
来看一个简单的日期类:
class A {
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() {
A a;
a.Init(2122, 10, 3);
a.Print();
A b;
b.Init(3122, 10, 3);
b.Print();
}
我们思考一个问题:日期类Date
的成员函数Init()和Print()
都只有一个地址,不同的类对象调用Init()
函数时,成员函数Init()
怎么区分到底是哪一个对象调用的呢?它应该初始化那个对象呢?
C++中引入了this指针
解决了这个问题:C++编译器给每个“非静态的成员函数“增加了一个隐藏
的指针参数this
,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有对成员变量
的操作,都是通过该指针去访问操作的。只不过所有的操作对用户是透明的,即用户不需要来传递,编
译器自动完成。
类类型* const
因此,this指针
本身是不能被修改的,是指针常量,而可以修改this指针指向的对象,这也与成员变量的修改相呼应,即成员变量是通过this指针改变的。
this指针
只能在成员函数的内部使用,这是因为this是以成员函数形参的形式接受实参类对象的地址的,在成员函数栈帧创建时保存在成员函数的栈帧中。
this指针
本质上是成员函数的形参,当对象调用成员函数时,将类对象的地址作为实参传递给this形参,所以对象中不存储this指针 。
class A {
public:
void Init(int year, int month, int day) {
this->_year = year;
this->_month = month;
this->_day = day;
}
void Print() {
cout << "this: " << this << endl;
//cout << _year << "/" << _month << "/" << _day << endl;
cout << this->_year << "/"
<< this->_month << "/" << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
A a;
a.Init(2122, 10, 3);
a.Print();
cout << "&a: " << &a << endl;
A b;
b.Init(3122, 10, 3);
b.Print();
cout << "&b: " << &b << endl;
return 0;
}
this指针
是成员函数的第一个隐含的指针形参,一般不需要由用户传递,而是由编译器通过ecx寄存器
自动传递。
首先需要明确: 不同的类对象每次调用相同的成员函数时,该成员函数的地址都是相同的,也就是说,程序运行期间,函数代码转换成的二进制指令只有一份,储存在某处供不同类对象调用。 类对象如果保存成员函数,实际上保存的是成员函数的地址。 而不同的类对象的相同的成员变量则是完全不同的,不同对象成员变量最多值是相等的,地址一定是不相同,因为不同的类对象有着有系统分配的独属于自己的储存空间,而对象的成员变量则分别在自己的储存空间中,这与成员函数不同。
思路1::每一个类对象即保存成员变量,也保存成员函数的地址。 相同的函数地址被多次保存,由此产生的空间浪费不可忽视。 这种思路被舍弃。
思路2:每一个类对象除了都保存必要的成员变量之外,就只保存了类成员函数的函数表的地址,相比思路1,空间已经节约很多了。类函数表把类中的成员函数都放在且在内存中的某块空间而形成的。找到类函数表的地址,就可以找到对应的类成员函数了。
虽然思路2的空间已经节约很多了,但是还是存在着额外的空间占用,即类函数表地址的存放,所以还需要更完美的改进。 **思路3:**类对象中只存放成员变量的大小,类的总大小就是类对象所有成员变量的大小。而类对象的成员函数全部存放到了内存的公共代码区(常量区),这样当类对象调用类成员函数时,编译器直接去公共代码区去寻找待调用的成员函数即可。 在公共代码区存放的成员函数编译器直接就能够找到,不需要类对象自己保存类函数表地址然后自己寻找了。
所以结果显而易见,思路3被保留了下来:类对象中只存放成员变量的大小,类对象的成员函数全部存放到了内存的公共代码区(常量区)。
一个类对象中可能存放着不同类型变量和许多成员函数。那么类的实例(对象)的大小是多少呢? 其实,类对象的大小的计算和C语言中计算结构体变量的大小是相同的,都需要考虑内存对齐。 在计算类对象大小时,注意到类与C语言中结构体不同的是类域中有成员函数,那么类域中成员函数占不占类对象的大小呢?
答案是不占,类域中成员函数被统一放在了公共代码区(常量区),所以类中只考虑所以成员变量所占空间的大小即可。
class A {
void func2() {
;
}
};
//空类
class B{
};
//类储存方式的选择:
//类大小的计算:与C语言结构体相同
int main() {
A A1;
//不储存有效数据,占位,标识对象存在
cout << sizeof(A1) << endl;
B B1;
//不储存有效数据,占位,标识对象存在
cout << sizeof(B1) << endl;
return 0;
}
空类和没有成员函数的类的对象大小是
1byte
,而不是0byte
; 这一个字节大小不储存有效数据,而是占位,标识该对象存在,用以区分没有类的情况0byte
。
来看一个简单的日期类:
class A {
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() {
A a;
a.Init(2122, 10, 3);
a.Print();
A b;
b.Init(3122, 10, 3);
b.Print();
}
我们思考一个问题:日期类Date
的成员函数Init()和Print()
都只有一个地址,不同的类对象调用Init()
函数时,成员函数Init()
怎么区分到底是哪一个对象调用的呢?它应该初始化那个对象呢?
C++中引入了this指针
解决了这个问题:C++编译器给每个“非静态的成员函数“增加了一个隐藏
的指针参数this
,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有对成员
变量的操作,都是通过该指针去访问操作的。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
类类型* const
因此,this指针
本身是不能被修改的,而可以修改this指针指向的对象,这也与成员变量的修改相呼应,即成员变量是通过this指针改变的。
this指针
只能在成员函数的内部使用,这是因为this是以成员函数形参的形式接受实参类对象的地址的,在成员函数栈帧创建时保存在成员函数的栈帧中。
this指针
本质上是成员函数的形参,当对象调用成员函数时,将类对象的地址作为实参传递给this形参,所以对象中不存储this指针 。
class A {
public:
void Init(int year, int month, int day) {
this->_year = year;
this->_month = month;
this->_day = day;
}
void Print() {
cout << "this: " << this << endl;
//cout << _year << "/" << _month << "/" << _day << endl;
cout << this->_year << "/"
<< this->_month << "/" << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
A a;
a.Init(2122, 10, 3);
a.Print();
cout << "&a: " << &a << endl;
A b;
b.Init(3122, 10, 3);
b.Print();
cout << "&b: " << &b << endl;
return 0;
}
this指针
是成员函数的第一个隐含的指针形参,一般不需要由用户传递,而是由编译器通过ecx寄存器
自动传递。
这还是要从C语言说起: C语言实现数据结构时,比如实现一个栈,首先需要创建一个栈的结构体类型
typedef struct Stack{
//...
}Stack;
在定义栈的功能函数时往往需要传入栈实例的地址
void StackInit(Stack* pst) { };
void StackPush(Stack* pst) { };
既然每个栈的函数都需要入栈实例的地址,前人在设计类时考虑了这一点,C++中在实现类成员函数时就把数据结构实例的地址默认传入了,该地址就被隐藏起来了,对该地址的使用也隐藏起来了。也就是说变成了编译器帮助使用者完成对象(实例)地址的传入,即编译器做的事增多了,使用者要做的事变少了。
C++语言实现数据结构更有优势和方便。
C实现栈
typedef int STDataType;
struct Stack {
STDataType* val;
int top;
int capacity;
};
//初始化
void StackInit(struct Stack* pst) {
assert(pst);
pst->val = NULL;
pst->top = 0;
pst->capacity = 0;
}
//压栈
void StackPush(struct Stack* pst, int val) {
assert(pst);
if (pst->top == pst->capacity) {
int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
STDataType* tmp = (STDataType*)realloc(pst->val, sizeof(STDataType) * newcapacity);
if (tmp == nullptr) {
perror("realloc file");
exit(-1);
}
pst->val = tmp;
pst->capacity = newcapacity;
}
pst->val[pst->top] = val;
++(pst->top);
}
//出栈
void StackPop(struct Stack* pst) {
assert(pst);
--(pst->top);
}
//取栈顶元素
int StackTop(struct Stack* pst) {
assert(pst);
return pst->val[pst->top - 1];
}
//是否为空
bool StackEmpty(struct Stack* pst) {
assert(pst);
return pst->top == 0;
}
//销毁栈
void StackDestroy(struct Stack* pst) {
assert(pst);
free(pst->val);
pst->top = pst->capacity = 0;
}
对于数据结构栈,在用C语言实现时,Stack相关操作函数有以下共性:
Stack*
;NULL
;Stack*
参数操作栈的;C++实现栈
class Stack {
public:
//栈初始化
void Init(int capacity = 4) {
_val = (int*)malloc(sizeof(int) * capacity);
if (_val == nullptr) {
perror("Init file");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
//压栈
void Push(int val) {
if (_top == _capacity) {
int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
int* tmp = (int*)realloc(_val, sizeof(int) * newcapacity);
if (tmp == nullptr) {
perror("realloc file");
exit(-1);
}
_val = tmp;
_capacity = newcapacity;
}
_val[_top] = val;
++(_top);
}
//出栈
void Pop() {
--(_top);
}
//取栈顶元素
int Top() {
return _val[_top - 1];
}
//栈是否为空
bool Empty() {
return _top == 0;
}
//销毁栈
void Destroy() {
free(_val);
_val = nullptr;
_top = _capacity = 0;
}
private:
int* _val;
int _top;
int _capacity;
};
C++中通过类可以将数据 以及 操作数据的方法(函数)进行配合,通过访问权限可以控制那些方法在 类外可以被调用,即封装,在使用时就像使用自己的成员一样。 每个方法不需要传递Stack*的参数了,由编译器自动传递给隐式的
this指针
,编译器编译之后该参数会自动还原,即C++中Stack *
参数是编译器维护的,C语言中需用用户自己维护。``
本节最主要介绍类的基本概念,并与结构体进行了比较。 下次再见!