文章目录
前言
在讲解了C++的部分基础后,我们来到了C++的精华部分,类与对象.在本章节中,博主将会带领大家了解
类的定义和类的使用
.
面向过程与面向对象
我们熟悉的C语言是面向过程,关注的是过程,通过分析问题而写出对应的函数并调用来解决问题.
而C++是基于面向对象的,关注的是对象,通过把复杂的事情拆解成对象之间的互动完成.
那他们到底什么区别呢?博主这里不解释,请继续玩下面看,大家就自然会明白.
类与对象的引入
在c语言中,我们经常会用到结构体,但是在使用结构体上,C和C++ 却有一定差异,比如下图两张图:
你会发现,在C++中,如果想定义链表的时候,在内部可以直接写结构体名,而不需要
struct
,原因:在c语言中,struct
是结构体,但是在c++中,struct
却被升级为类.
何为类
类,即是一种类型,和
int,char
等性质一样,只是类的功能与作用比它们大很多.具体大多少呢?我们往下看.
pp
类的定义
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
class
为关键字,用于定义类.classname
为类名,即我们定义的类的名字.其中,类里面的函数称为类的方法或者成员函数
类里面的变量称为类的属性或者成员变量
其中,这里博主要先介绍下类访问限定符:public(公有),protected(保护),private(私有)
,其中public
的作用是无论类内还是类外,都可以访问类的属性和方法,后两者的作用是类外不能访问,类内可以访问,至于后两者区别是什么?博主在后面的章节会详细介绍.
现在,我们来定义一个日期类
class Date
{
private:
int _year;
int _month;
int _day;
};
注意:类的方法定义有两种方式,一是声明与定义一起,二是声明与定义分离.
第一种:
class Date { public: void print() { cout<<"我们创建的第一个类"<<endl; } };
第二种:
class Date { public: void print(); }; void Date:: print() //需要使用 类名 加上 类作用域符号::在函数名前面. { cout<<"我们创建的第一个类"<<endl; }
那这两种定义方式有什么区别呢?
如果所定义的函数比较短,且没有循环和递归,便可以函数声明与定义放在一起.
如果所定义的函数比较长,且含有循环或者递归,那么函数必须声明与定义分离.
原因是,如果成员函数在类的内部定义,编译器会把函数当成内联函数进行展开.如果含有递归或者循环,可能会被忽略.
现在我们知道了完整的类定义方法,博主便抛出一个需求:
定义一个计算类
, 其有属性n,代表一个数字n.该类具有两个方法,一是给_n
赋值,一是计算1累加到n的值,然后输出.
class caculate
{
public:
void outn(int n) //声明与定义一起
{
_n = n;
}
void sumval(); //声明与定义分离
private:
int _n;
};
void caculate:: sumval()
{
int ans = 0;
for(int i = 1;i<=_n;i++)
{
ans+=i;
}
cout<<"累加结果为:"<<ans<<endl;
}
访问限定符说明
-
public修饰的成员在类外可以直接被访问(已讲解)
-
protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)(已讲解)
-
访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
-
class的默认访问权限为private,struct为public(因为struct要兼容C)
其中第四点的意思是,如果我们在定义类时不写访问限定符,那么class类中成员是默认为
private
属性,而struct类中的成员默认public
属性.
问题:C++中struct和class的区别是什么?
解答:C++需要兼容C语言,所以C++中struct可以当成结构体去使用。另外C++中struct还可以用来定义类。
和class是定义类是一样的,区别是struct的成员默认访问方式是public,class是的成员默认访问方式是
private.
封装
面向对象有三个特点,分别是:封装,继承与多态.
博主在这篇文章主要介绍封装,那么什么是封装呢?
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互
其实简而言之就是 保证数据的安全性. 比如我们上面介绍类的定义时候其实就是封装特性,利用访问限定符,限制性的让某些数据只在某些地方可以访问.
而封装的特性其实就好比去看 兵马俑,学习c语言时候是没有封装特性的,也就相当于兵马俑没有围栏,游客可能会去坑里涂画,造成兵马俑不安全,但是C++有了该特性,就相当于给兵马俑修了围栏,游客只能按照特定的路线去观看兵马俑,而不会破坏兵马俑.
类的实例化
概念: 用类创建对象的过程称为类的实例化
其中我们创建的类就好像一张图纸,而我们通过该图纸建造出来的房子就是对象,而这个建造的过程就称为 类的实例化
这个图纸在我们生活中是不占据空间的,但是建造出来的防止确占据很大空间.
这在我们的程序中同样如此,我们设计的类并不会占据空间,但是通过类实例化出来的空间确占据空间.
类的实例化例子:
通过我们创建的人
类—这个可以知道身高体重名字,以及可以说话的抽象类,而在右边实例出来无数个具有这些功能的对象,就是实例化.
类模型
在看下一知识点前,大家猜猜下面的结果是啥:
class person
{
public:
void say()
{
cout << "我可以说话" << endl;
}
double height;
string name;
double weight;
};
int main()
{
person man1;
man1.height = 122.3;
man1.name = "dddd";
man1.weight = 123.4;
person man2;
man2.height = 150.3;
man2.name = "uuuu";
man2.weight = 326.1;
if(sizeof(man1) == sizeof(man2)) cout<<"111111111111111111111111";
else cout<<"222222222222222222";
return 0;
}
答案:
其实这样很好理解,man1和man2都是创建的对象,相当于房子,而他们的图纸是同一份,也就是说创建出来的防止空间占据一样大,那无论房子里面装了多少东西,房子占据的空间仍一样大.
1.如何计算类对象大小
那么,如何计算类对象的大小呢?其实类的大小计算和在c语言中结构体计算方法一模一样,并且只计算成员属性,成员方法不需要计算.
比如:
class person
{
public:
void say()
{
cout << "我可以说话" << endl;
}
private:
double height;
char name[20];
double weight;
};
按照结构体对齐,我们知道,
height
占据8字节,此时偏移量已经到了8,刚好是8的整数倍,所以name从8开始往后又占据20字节,此时一共占据了28字节,偏移量也已经是28了,而28并不是8的整数倍,所以需要往后空出4字节,也就是现在消耗了32字节,偏移量也达到了32%8==0,所以weight开始往后占据8字节,到此,一共占据40字节,而40也是8的倍数,所以person类对象的大小是40字节
![image-20211011203125496](https://gitee.com/du-weiwei/typora/raw/master/20211011203131.png)
2.类对象的存储方式
我们知道,一个类一般是包括成员属性和成员方法的,那么其实例出的对象,会是怎样存两者的呢?
答:
每个对象都会存储其成员属性,但不会存储成员方法,而类的成员方法是放在一个公共区域(代码段)中.
原因:
每个对象都有不同的属性(属性值不一样),但是他们的方法确是一样的(方法定义上),而不同的对象,我们只需要知道其属性,然后需要哪个方法就去公共区域调用哪个方法,这将会极大的节约空间.反之,如果每个对象都存储一个相同的方法,就会造成极大的空间浪费
this指针
什么是this指针呢?这个问题先放一放,现在我们会想一下学习数据结构时候,以栈为例.
我们给某一个创建的栈放数据时候,函数声明是这样的
void StackPush(Stack* stack,DataType x);
我们要判断栈是否满时,函数声明是这样的
bool StackFull(Stack* stack);
我们要弹出栈的数据时候,函数声明是这样的
void stackPop(Stack* stack);
我们会发现,无论如何我么都必须给函数传一个
栈指针
过去,否则函数的操作对象将不知道.但是在类中,是否存在这种情况呢?我们看下面:
class caculate
{
public:
void init(int m,int n)
{
_m = m,_n = n;
}
void sum()
{
int ans = _m + _n;
cout <<"相加结果为"<<ans<<endl;
}
void sub()
{
int ans = _m - _n;
cout <<"相乘结果为"<<ans<<endl;
}
private:
int _m;
int _n;
};
int main()
{
caculate p1;
p1.init(1,2);
p1.sum();
p1.sub();
return 0;
}
在上面的例子中,我们可以清晰的看到,我们通过对象调用类方法时候,并没有传p1地址,而类函数又是放在公共区域的,它是怎么知道要计算哪一个类呢?而这就是我们要讲解的this指针
其实,我么在调用函数时候,系统便已经悄悄帮我们转换了:
比如我们调用init
时候,编译器会悄悄传给init
一个caculate*
指针,即实际传参为p1.init(&p1,1,3)
而定义init
时候,编译器也悄悄的给init
放了caculate*
形参,即实际定义为void init(caculate* this,int m,int n)
而那个悄悄放的指针就称为
this
指针,而init
内部的_m编译器会变成this->_m
,同理还有this->_n
面试题
1.this
指针存在哪里?
通过上面的讲解我们可以看到,this指针是函数形参,而形参是放在内存四区的栈区,所以this指针存在栈区
2.this指针可以为空吗?
下面分别调用了
PrintA
和Show
函数,请问下面的两个函数是否会崩溃?
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
void Show()
{
cout<<"Show()"<<endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
p->Show();
}
答:
PrintA调用时会崩溃,Show调用时顺利进行
原因:调用PrintA时候,它会接收this指针,然后通过this指针访问其成员_a(之前已经说过成员值本质是this->成员
),但是,但是,但是,我们的p是nullptr,那么this也就是nullptr,而空指针是没有办法访问其成员的,所以报错.
那么this指针可以为空指针吗? 答案是可以的,但是要注意分情况,正如上面介绍的面试题一样