类和对象
- 面向过程和面向对象初步认识
- 类的引入
- 类的定义
- 命名规范
- 类的访问限定符及封装
- 访问限定符
- 封装
- 类的作用域
- 类的实例化
- 类的对象大小的计算
- 类成员函数的this指针
- this指针的引出
- this指针的特性
面向过程和面向对象初步认识
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
举个栗子:
比如说我们现在要洗衣服,那么就要大概进行下面的步骤:
我们可以对每个步骤都写一个函数,是不是很麻烦?
但如如果换成洗衣机呢?
没有理解面向对象和面向过程的话没关系,对于刚入门的同学来说,想要有一定的理解有点太过于强求了。
直接上知识:
类的引入
C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。
比如:在数据结构中,用C语言方式实现的栈,结构体中只能定义变量;现在以C++方式实现,会发现struct中也可以定义实现栈的函数。
下面把例子给出(实现方面的就不写了):
typedef int DataType;
struct Stack
{
void StackInit()
{
//栈的初始化操作
}
void StackPush(DataType x)
{
//入栈操作
}
void StackPop()
{
//出栈操作
}
DataType* _data;
int _sz;
int _top;
};
可以看到,我们现在可以在struct内部既可以写变量,又可以写函数。
而且我们在用结构体定义一个变量时不需要再写struct了,直接写跟在struct后面的Stack就行,编译器是不会报错的。
可以说C++中的struct就是C语言中的strcut的升级版本。不仅包含了C的用法,还新增了用法。
但是这个升级的结构体,C++一般不用,更多用的是类class。
类的定义
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。
C++中用{}括起来的都叫域,域要么影响生命周期,要么影响访问。
类体中内容称为类的成员:
类中的变量称为类的属性或成员变量;
类中的函数称为类的方法或者成员函数。
类的两种定义方式:
-
声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
-
类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::
例子:
那么类的定义的的时候推荐:
- 小函数(语句比较少),直接在类内定义,当作内联函数,提高程序运行效率
- 大函数(语句比较多),声明和定义分离,每次可以直接看到每个函数的声明,增加可读性
下面看几个例子:
class Date
{
public:
void Init(int year)
{
// 这里的year到底是成员变量,还是函数形参?
year = year;
}
private:
int year;
};
根据编译器的提示(背景颜色)可以知道形参year和下面的两个year是一样的。
所以说这种命名风格是非常挫的。
这样写的话就好多了:
class Date
{
public:
void Init(int year)
{
_year = year;
}
private:
int _year;
};
所以就要讲讲命名规范了:
命名规范
这里主要讲一下驼峰法。
每个单词之间的单词开头要用大写,比如说:MyName,不要写成myname,后者的可读性很差。
对于函数、类,首单词的首字母大写,后面单词首字母大写。
对于变量,首单词的首字母小写,后面单词首字母大写。
对于成员变量,首单词前面加上_。
类的访问限定符及封装
访问限定符
各位在上面已经见过public和private这两个关键字了,下面说这两和protected是干啥用的。
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
同过这三个限定符,可以控制类内的东西是否能在类外被用。
- public修饰的成员在类外可以直接被访问
- protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- 如果后面没有访问限定符,作用域就到 } 即类结束。
- class的默认访问权限为private,struct的默认访问权限为public(因为struct要兼容C)。
新手可能看不懂,马上给例子,但是注意第五点是class和struct的一个区别。
public修饰的成员在类外可以直接被访问, protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的):
目前阶段记住protected和private的是一样的(错误说法),只有到了之后继承那块才会有区别
访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止;如果后面没有访问限定符,作用域就到 } 即类结束。:
上面的public到private位置后权限由公有变为私有。
private到了}后就没了。
C++中struct和class的区别是什么?
C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类默认访问权限是private。
注意:在继承和模板参数列表位置,struct和class也有区别,后序给大家介绍。
封装
面向对象的三大特性:封装、继承、多态。
在类和对象阶段,主要是研究类的封装特性,那什么是封装呢?
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
上面的说法太过于官方了,给个例子就懂了。
比如说我现在用C语言写了一个栈,还用C++写了一个栈。
C语言实现的时候,标准情况下,我们会去实现一个得到栈顶元素的一个函数,
这时候想要得到栈顶元素只需要StackTop()就可以了,但是有的人在实现的时候不会,他们在获取栈顶元素的时候会直接访问数组的元素,就是data[top],这时候会引发歧义,因为栈顶初值有两种实现方式,一种是初值为0,一种是初值为-1,这样的话若是没有定义得栈顶元素得函数,就会导致不同用户使用不同的方法,就乱了。
C++得话,我们直接将成员变量设置成private的,就不会出现上面的情况,只能实现一个public的函数来获取栈顶元素。这就叫封装。用户在用的时候就只能通过函数来搞了。
所以:
C语言,没办法封装,可以使用函数访问数据,也可直接访问数据,使用者可能出错,不够规范。C++:封装必须规范使用函数访问数据,不能直接访问数据。可以使用户更方便的使用。
类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。
在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
这个在前面类的成员函数的声明和定义分离的时候讲过了。
类的实例化
用类类型创建对象的过程,称为类的实例化
- 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;比如:入学时填写的学生信息表,表格就可以看成是一个类,来描述具体学生信息。
- 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量Person类是没有空间的,只有Person类实例化出的对象才有具体的年龄。
- 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间。
类的对象大小的计算
其实就和结构体差不多,都有内存对齐。如果不懂内存对齐的话建议看下我这篇博客:只用看结构体的内存对齐就行
我们看一下上面的例子Person的大小:
可能有的同学就有疑惑了,但是如果你光算里面的成员变量的话,内存对齐后就是28,函数没有统计大小。
那么有三种存储的模型,来看看:
缺陷:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。那么如何解决呢?
代码只保存一份,在对象中保存存放代码的地址
只保存成员变量,成员函数存放在公共的代码段
对于上述三种存储方式,那计算机到底是按照那种方式来存储的?
我们再通过对下面的不同对象分别获取大小来分析看下:
空类:
第一种是不太可能了。
看第二个例子:如果是第二种存储方式的话,那么此处的大小就应该是4,因为有一个函数表的指针。但是这里的大小是1。所以第二种方式也排除了。
那就是第三种喽。也就是说,每个实例化的对象中的变量是相互独立的,但是类对象在使用成员函数的时候是在使用同一个函数。在编译链接时,就根据函数的名去公共的代码区找到函数的地址,call函数地址。
结论:一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐
注意空类的大小,空类比较特殊,编译器给了空类一个字节来占位,用来唯一标识这个类的对象存在,不存储实际数据。
类成员函数的this指针
this指针的引出
我们先来定义一个日期类 Date
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对象呢?
C++中通过引入this指针解决该问题,即:
C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
this指针的特性
- this指针的类型:类类型* const,即成员函数中,不能给this指针赋值。
- 只能在“成员函数”的内部使用。
- this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
- this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
给几个例子:
成员函数
但是上面编译器处理的我们是看不到的,而且也不能自己去显示的定义this指针这个参数。但是可以使用this指针去取出this指针指向的对象的成员变量。
函数调用
当然,我们也不能显示传递这个对象的地址,这些都是由编译器来完成的。
看道题:
这里竟然能够成功运行。
原因是,实例的对象中存放的是成员变量,并没有存放成员函数,成员函数是在公共代码区中的,调用这个函数并不是去这个对象中找,所以并不是去解引用d1,编译链接时就根据函数名去公共代码区找到函数的地址。
但是如果调用的函数中解引用了程序就崩掉了。
这是因为,这里的this指针实际上是空指针,也就是d1。
如果在函数内部解引用了this就是解引用了空指针,这样程序就崩掉了。
而上面的那个函数中并没有解引用this指针,所以就不会出现崩掉的情况。
this指针存放在哪里?
this指针是形参,存放在栈区。
但有的编译器还会对this指针做优化处理,直接将this指针放到寄存器中。
到此为止。。。。