目录
1.面向过程和面向对象
在学习类和对象之前,我们首先要理解一下面向过程和面向对象。
我们以洗衣服为例理解面向过程和面向对象:
在用面向过程思想解决问题的时候,我们关注的是求解问题的过程,分析出求解问题的步骤,通过函数调用一步一步解决问题。在上面洗衣服的问题中,就将洗衣服拆分成了多个步骤,完成所有的步骤就能完成整个问题。
在用面向对象思想解决问题的时候,我们关注的是构成问题的对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。在上面洗衣服的问题中,总共有四个对象,分别是人、衣服、洗衣液、洗衣机;整个过程依靠对象之间的交互完成,并不关系具体的步骤细节。
2.类
我们使用C语言编写程序的时候,就是利用了面向过程的编程思想,解决问题的步骤通过函数调用来实现。而C++是一门具有面向对象特点的编程语言,它是如何体现自己的面向对象的呢?类就是C++面向对象的体现。
什么是类
面向对象的语言关注的是一个个的对象,所以,面向对象的编程语言必须要提供描述对象的能力,用来描述对象的就叫做类。
那么如何描述对象呢?对于这个问题,我们只需要考虑对象有什么?对象拥有的东西可以划分为两类,一类是对象的属性,另一类是对象的动作。用标准的语言来说就是成员属性、成员方法。
类的定义
类有属性,肯定有不同的属性,这些属性可能是不同的数据类型,在C语言中,能够表示不同数据类型的集合的只有结构体,在C++中,对结构体做了升级,不仅仅可以在结构体中定义变量,还可以定义函数,也就是说,在C++中结构体被升级成了类。
stu类的定义:
struct stu
{
int age;
string name;
string tel;
void eat()
{
//…
}
void run()
{
//…
}
};
但是C++中更喜欢用class替换struct来定义类:
class stu
{
int age;
string name;
string tel;
void eat()
{
//…
}
void run()
{
//…
}
};
总结:在C++中定义类和定义结构体其实很类似,在结构体中定义函数,并且将struct改成class就是定义一个类。
上面定义类的方式只是C++中定义类的一种方式,也就是将成员函数的声明和定义全部放在类体中,需要注意的是,成员函数在类体中定义,默认是内联函数。
还有一种定义类的方式就是将成员函数的声明和定义分离:
- 定义成员函数的时候,需要在成员函数的前面加 类名::,相当于指定类域,也就是告诉编译器这个函数在哪个类里面声明了。
3.封装
面向对象具有三大特性:封装、继承、多态,我们现在主要讨论封装。
什么是封装?
- 封装就是将数据和操作数据的方法进行结合,隐藏对象的属性和实现细节,仅对外公开接口来实现交互。
为什么要封装?
- 封装本质上是一种管理,让用户更加方便的使用类。以汽车为例:汽车内部有发动机、各种齿轮、制动系统等等一系列的东西,可以说汽车内部结构是比较复杂的,但是使用汽车的时候,我们只是简单的踩油门、刹车,转动方向盘。这其实就是汽车制造厂商隐藏了汽车内部的实现细节,对外提供统一的接口给用户使用,减小了用户的使用成本。
C++如何实现封装?
- 在C++中,通过类将数据以及操作数据的方法进行结合,通过访问权限来隐藏类内部的实现细节,提供公有的方法给外部使用。
4.访问限定符
在C++的类当中,类内成员的访问权限主要依靠访问限定符来控制,C++中的访问限定符主要有public、protected、private。
访问限定符的说明:
- public表示公共的,被public修饰的成员在类里类外都能直接被访问。
- protected表示保护的,被protected修饰的成员只能在本类和子类中访问。
- private表示私有的,被private修饰的成员只能在本类中访问。
访问限定符的一些注意事项:
- 访问权限的作用域从该访问限定符出现的位置开始,直到下一个访问限定符出现为止。
- 如果后面没有访问限定符,作用域就到类结束的位置。
- class定义的类,默认访问权限时private(更好的体现了封装);struct定义的类,默认的访问权限是public(因为C++要兼容C语言)。
需要注意的是:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问权限上的区别。
5.类的实例化
前面我们学习过类的定义,定义出来的类相当于一个自定义的类型,类型是用来干嘛的?类型不就是用来定义变量的吗?不过在C++中对于自定义类型的变量更喜欢用对象来称呼。用类定义对象就叫做类的实例化。
我们定义出来的类并没有实际的内存空间,他只是一张图纸一样的东西;类实例化出的对象才会占用实际的物理空间,存储成员变量。
class stu
{
private:
int age;
string name;
string tel;
public:
void eat()
{
//…
}
};
int main()
{
stu s1; // 类的实例化
return 0;
}
总结:类和对象之间的关系就相当于类是对象的模板,对象是类的实例。
6.类对象模型
前面我们讨论的都是类的相关知识,现在我们来学习对象的相关知识。
类中既有成员变量又有成员函数,那么一个类对象中包含了什么?如何计算一个类类型的大小呢?
我们可以通过下面这份代码求证:
#include <iostream>
using namespace std;
// 类中既有成员变量,又有成员函数
class A
{
public:
void f(){}
private:
int _a;
};
// 类中仅有成员函数
class B
{
public:
void f() {}
};
// 类中仅有成员变量
class C
{
private:
int _c;
};
// 类中什么都没有---空类
class D
{};
int main()
{
cout << "sizeof(A): " << sizeof(A) << endl;
cout << "sizeof(B): " << sizeof(B) << endl;
cout << "sizeof(C): " << sizeof(C) << endl;
cout << "sizeof(D): " << sizeof(D) << endl;
return 0;
}
运行结果如下:
通过观察可知,当类中出现一个成员变量的时候,类的大小就是这个成员变量的大小;当类中啥也没有的时候,类的大小是1字节;成员函数似乎并不影响类的大小。
于是,我们可以得出结论:一个类的大小就是类中的成员变量之和(要注意内存对齐),空类的大小比较特殊,编译器给空类的对象开一个字节的空间,这个字节并不用来存贮数据,而是用来标识定义的对象存在。
那么问题又来了,类对象中只有成员变量,那成员函数到底存放在哪呢?
我们可以对比生活中的例子来理解,我们每个人都有独属于自己的物品,只有自己能用,但是,每个人在社会上都能使用社会提供的公共的设施、服务…… 这部分公共的,大家都要用的东西就没必要各自都存有一份了,否则就会造成空间的浪费。
类对象模型的设计者也是这么想的。每个类对象的属性肯定会有所不同,所以,类对象中必须要保存属性,但是对象调用的成员方法都是相同的,如果每个对象中都存一份成员方法,势必会造成空间的浪费,所以,这部分公共的东西可以存放在公共的区域。也就是说,类对象中只保存成员变量,成员函数放在了公共的代码段。
7.隐藏的this指针
认识this指针
问题:当我们用一个类定义出多个对象之后,并用这些对象调用同一个成员方法,这个成员方法是如何区分不同的对象,并使用该对象的资源的呢?
C++通过引入this指针解决该问题:
具体过程如下:
- C++编译器给每个非静态的成员函数增加了一个隐藏的指针参数(this指针),在函数运行时,让该指针指向调用该函数的对象。
- 在函数体中,所有访问成员的操作都是通过该指针去访问的。
- 传递参数给this指针,通过this指针访问成员的操作都是由编译器自动完成的。
this指针的特性
1.this指针的类型是:类类型 * const。比如上图中this指针的类型为 stu * const。
- const在 * 的右边,修饰的是指针本身不能改变,说明非静态成员函数内部的this指针不能改变,这也符合我们的认知;因为this指针指向当前调用函数的对象,如果this指针改变了,就找不到当前对象中的成员了,这是不合理的,所以this指针不能改变。
2.this指针本质上是一个形参:
- 既然是形参的话,就只能在该函数内部使用。
- 作为函数的形参,在调用该函数的时候,该函数的参数需要入栈(栈区),所以this指针是存储在栈区的,对象中不存储this指针。
- this指针是一个隐藏的形参,由编译器自动传递,不能显示的传递,也不能在参数列表显示的写this形参,但是可以显示的调用。
3.this指针不能为空:
- 当this指针为空的时候,在函数内部访问成员需要使用this指针,也就会访问空指针,会造成程序崩溃!
- 如果this指针为空,但是不访问成员,也就不会访问空的this指针,程序可以正常运行,但这是没有意义的。
综上,this指针最好不要为空。