1. 对面向对象与面向过程的初步认识
在C++入门1中我们已经知道:C语言是面向过程的,C++是面向对象的。
那哗啦啦说了一大堆,到底什么是面向过程编程、什么是面向对象编程呢?面向对象编程难道就是面对面,对着自己的恋爱对象写代码吗?——哈哈!开个玩笑,当然不是这样的。下面请让我用吃饺子的例子初步解释一下什么是面向过程编程、什么是面向对象编程吧!
C语言吃饺子
C语言是面向过程的,关注的是过程,注重实现这个功能的步骤,第一步干什么、第二步干什么......分析出求解问题的步骤,通过函数调用逐步解决问题。
那么用C语言得到一碗饺子的方法就可以表达为以下图解:
C++吃饺子
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。在面向对象编程中,抛弃了函数,想要实现一个功能不再是通过函数的叠加调用实现的了,而是通过对象。
那么C++用吃饺子就可以直接打开外卖APP找到饺子,下单就可以了。在这个场景中,外卖APP就是对象。人不需要关注饺子是怎么擀皮,怎么剁馅,怎么包的......
2. 类
2.1 类的引入
在讲解类之前,我们先回忆一下用C语言实现顺序表、以及初始化和实现头插时是怎样定义的:
struct SeqList { int a[1000]; int size; }; void SLInit(struct SeqList* SL); void SLPushFront(struct SeqList* SL);
在C语言中,结构体与函数(数据和方法)是分离的
但在C++中,C++在兼容所有C语言struct用法的同时,又把struct升级成了类
同时,在C语言申请结构体变量时需要struct SeqList SL;如果不想写struct又必须用到typedef(如果忘记了C语言结构体的基础知识,详看初识结构体,相信你一定会有更深的印象),如此繁琐,C++就摒弃了这种写法,在申请结构体变量时,类名就是类型,不需要再加struct,直接就可以SeqList SL;
在C++中,函数也可以直接定义在类(结构体)里面:
struct SeqList { int a[1000]; int size; void SLInit(SeqList* SL) { int a[1000] = { 0 }; int size = 0; } void SLPushFront(int x) { //...... } };
同样的,C语言在调用函数时需要这样写:
struct SeqList { int a[1000]; int size; }; void SLInit(struct SeqList* SL); void SLPushFront(struct SeqList* SL, int x); int main() { struct SeqList SL; SLInit(&SL); SLPushFront(&SL, 1); SLPushFront(&SL, 2); return 0; }
C++就可以这样写:
struct SeqList { int a[1000]; int size; void SLInit(SeqList* SL) { int a[1000] = { 0 }; int size = 0; } void SLPushFront(int x) { //...... } }; int main() { SeqList SL; SL.SLInit(&SL); SL.SLPushFront(1); SL.SLPushFront(2); return 0; }
综上所述,谁更方便和简洁显而易见。
可是在C++中,结构体的定义更喜欢用class替代struct,但是把class换成struct又会使程序发生一些变化。
2.2 类的定义
上面说到,把class换成struct会使程序发生一些变化,具体发生哪些变化呢?下面就让我们详细探究一下。
与结构体相似:
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。
类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。
2.3 类的访问
知道了类的定义,其实就与结构体有很大相似性,那么新的问题又来了,类又如何来进行访问呢?
类的访问限定符:
1. public修饰的成员在类外可以直接被访问
2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
4. 如果后面没有访问限定符,作用域就到 } 即类结束。
5. class的默认访问权限为private,struct为public(因为struct要兼容C)
(现阶段认为protected与private相同,不同点会在后面详细探究)
具体使用请看如下代码:
①数据为私有,函数为公有
class SeqList { private: int a[1000]; int size; public: void SLInit(SeqList* SL) { int a[1000] = { 0 }; int size = 0; } void SLPushFront(int x) { //...... } }; int main() { SeqList SL; SL.SLInit(&SL); SL.SLPushFront(1); SL.SLPushFront(2); return 0; }
验证:
②数据和函数都为私有:
class SeqList { private: int a[1000]; int size; void SLInit(SeqList* SL) { int a[1000] = { 0 }; int size = 0; } void SLPushFront(int x) { //...... } };
这时的函数被定义为了私有,所以不能被正常访问:
可是上面已经说过了,struct与class的用法是相同的呀!我也知道了C++经常用的是class,那他们二者是不是一点区别也没有呢?
答案在上面已经提到了:
class的默认访问权限(即不加访问限定符的时候)为private,struct为public(因为struct要兼容C),其他地方二者都是相同的。
验证如下:
class:
class SeqList { int a[1000]; int size; void SLInit(SeqList* SL) { int a[1000] = { 0 }; int size = 0; } void SLPushFront(int x) { //...... } }; int main() { SeqList SL; SL.SLInit(&SL); SL.SLPushFront(1); SL.SLPushFront(2); return 0; }
换成struct:
struct SeqList { int a[1000]; int size; void SLInit(SeqList* SL) { int a[1000] = { 0 }; int size = 0; } void SLPushFront(int x) { //...... } }; int main() { SeqList SL; SL.SLInit(&SL); SL.SLPushFront(1); SL.SLPushFront(2); return 0; }
所以,这个问题的正解为:
C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类 默认访问权限是private。(在继承和模板参数列表位置,struct和class也有区别,后续会详细介绍。)
3. this指针
3.1 this指针的引出
这里我们写一个交换类:
#include <iostream> using namespace std; class Swap { public: void Init(int a, int b) { _a = b; _b = a; } void Print() { cout << _a << " " << _b << endl; } private: int _a; int _b; }; int main() { Swap s1; Swap s2; s1.Init(8, 5); s2.Init(5, 8); s1.Print(); s2.Print(); return 0; }
运行结果:
结果显示s1交换后为5 8,s2交换后为8 5,转到反汇编看一下调用的Print函数是否为同一个函数:
函数地址确实是相同的呀!
那么问题来了,s1、s2都调用的Print函数,我也没有传递任何参数,输出的结果应该是一样的才对呀!编译器是怎么知道s1应该输出5 8,s2应该输出8 5呢?
说到这里,我们的this指针就自然而然地引出了:
C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
“该指针”说的就是this指针,也就是说,C++在这里增加了一个隐藏的this指针,在函数调用的时候也是通过指针传参的方式进行的。
如图,相当于:
需要注意的是:这里只是说明了存在this指针,编译器已经帮我们进行了传参,所以不用我们再把this相关的实参和形参再写出来,如果再写出来,编译器就会报错。就好比我们耳熟能详的一句话:“哪有什么岁月静好,不过是有人为我们负重前行!”编译器已经为我们“负重前行”了,我们就不要再添乱把this相关的实参和形参加上了。
3.2 this指针的特性
上面已经说过,我们不能显示地写this的实参和形参:
void Print(Swap* this)//错误 { cout << this->_a << " " << this->_b << endl; }
但是却可以在类里面显示地使用:
class Swap { public: void Init(int a, int b) { _a = b; _b = a; } void Print() { cout << this->_a << " " << this->_b << endl; cout << _a << " " << _b << endl; } private: int _a; int _b; };
另外也需要注意:
事实上this指针使用const修饰的,this指针本身是不能被修改的,即:
//void Print(Swap* const this)
验证如下:
但是它所指向的类型是可以修改的。
综上所述,我们写一段代码验证以上说法,并且打印一下s1、s2的地址:
#include <iostream> using namespace std; class Swap { public: void Init(int a, int b) { _a = b; _b = a; } void Print() { //在类里显示地使用this cout << this << endl; cout << _a << " " << _b << endl; } private: int _a; int _b; }; int main() { Swap s1; Swap s2; cout << "&s1=" << &s1 << endl; cout << "&s2=" << &s2 << endl; s1.Init(8, 5); s2.Init(5, 8); s1.Print(); s2.Print(); return 0; }
this指针的特性总结:
1. this指针的类型:类型为* const,即成员函数中,不能给this指针赋值。
2. 只能在“成员函数”的内部使用。
3. this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给 this形参,所以对象中不存储this指针。
4. this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。