一、面向过程和面向对象
C语言是面向过程的,而C++是面向对象的,那面向过程和面向对象到底是什么呢?
我们拿一个非常典型的外卖系统来进行举例:
面向过程:
我们知道外面系统中主要有商品上架、用户选餐、商家派单、骑手送单这4个步骤,而这4个步骤就是整个外卖系统的主要的4个过程。面向过程关心的就是整个过程中的过程步骤。
面向对象:
面向对象的话就不再具体关注整个外卖系统中的各个过程,而是关心整个过程中有谁参与到整个过程中来,我们细想一下整个过程主要就有商家、用户、骑手这三个对象参与进来,即面向对象。面向对象关心的是对象与对象之间的关系和交互,可以将现实世界类和对象映射到虚拟到计算机系统中。
二、类的引入
之前在C语言的学习过程中,我们知道在C语言结构体中只能定义变量;而在C++中,结构体内不仅可以定义变量,也可以定义函数。
其次我们要知道C++是兼容C语言的,C语言的struct用法在C++中依然是可以使用的,只不过在C++中把struct升级成了类。
struct Queue { //成员函数 void Init(int defaultCapacity = 4) { } }; struct Stack { //成员函数 void Init(int defaultCapacity = 4) { a = (int*)malloc(sizeof(int) * defaultCapacity); if (a == nullptr) { cout << "malloc申请空间失败" << endl; return; } capacity = defaultCapacity; top = 0; } void Push(int x) { //扩容...... a[top++] = x; } void Destroy() { free(a); a = nullptr; top = capacity; } //成员变量 int* a; int top; int capacity; }; int main() { struct Stack st1;//这里是C语言的语法,即利用结构体类型创建了一个结构体变量 st1.Init(20); Stack st2;//C++中把struct升级成了类,C语言是不可以这么使用的 st2.Init(); st2.Push(1); st2.Push(2); st2.Push(3); st2.Push(4); st2.Destroy(); return 0; }
虽然C++把struct升级成了类,即用struct也可以定义类,但是在C++中更喜欢使用class来定义类。
三、类的定义
类的定义的格式:
class calssName { //类体:有成员函数和成员变量组成 };//分号不能丢
class为定义类的关键字,ClassName为类的名字,{}中是类的主体(类体)。
类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。
类的定义有两种方式:
方式1:声明和定义全部放在类体中(成员函数如果在类中定义,编译器会根据需要把它当成内联函数处理)。请看:
方式2: 类也是可以声明和定义分离的。当函数中的内容较长时,我们就可以采用声明和定义分离的方式来定义类。(类里进行声明,类外进行定义)
加上Stack::就是在告诉编译器Init函数不是普通的全局函数,而是类里的一个成员函数的定义。
这里如果我们想让Init函数成为内联函数的话,我们不可以直接在声明前面加上inline,即:
这种写法是错误的,因为内联函数的声明和定义是不可以分离的,如果我们想让Init函数称为内联函数的话我们应该在类里面直接定义(不要分离,否则会链接不上)。同时在C++中,如果在类里面定义默认就是内联函数,比如上述代码中如果我们想让Push函数成为内联函数的话,没有必要加上inline(可以加也可以不加),因为在类里面进行定义的函数默认就是内联函数。所以在C++中长的函数就是声明和定义分离,短的函数就会直接在类里进行定义。但是如果有一个很长的函数也在类里直接定义的话,依然不会是内联函数,因为一个函数是否成为内联函数的话取决于编译器。
下面来看看成员变量的命名规则:
注意进行区分,所以采用前加或者后加_进行区分。
四、访问限定符
访问限定符说明:
1.public修饰的成员在类外可以直接访问。
2.protected和private修饰的成员在类外不能直接被访问(故这种情况protected和private功能是类似的。)
3.访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现为止。
4.如果后面没有访问限定符,作用域就到}即类结束。
5.class的默认访问权限private,struct为public(因为struct要兼容C)
注意:访问限定符只有在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。
现在有一个问题,我们用class和用struct来定义类,两者有什么区别呢?
我们直到C++是兼容C语言的,所以C++中的struct既可以当作结构体来进行使用,struct也可以定义类。和class定义类一样,区别就是struct定义的类默认访问权限是public,class定义的类默认访问权限是private。同时,我们一定要注意:在继承和模板参数列表位置那里,struct和class其实也是有区别的。当然无论是默认私有还是默认共有,建议是我们直接使用显式的指令。
五、类的作用域
类定义了一个新的作用域称为类域,类的所用成员都在类的作用域中。在类体外定义成员时,需要使用::作用域操作符指明成员属于哪个类域。
一般是按照局部域、其次类域、最后全局域,其中命名空间域与全局域基本上处于同一个等级,命名空间域不进行展开的话是没有办法访问的。
六、类的实例化
用类类型创建对象的过程,称为类的实例化。
对于变量而言,声明和定义的区别在于是否开辟内存空间,如果开了就是定义,如果没开就是声明。
那怎么样才算是定义呢?请看:
这一步叫做对象的实例化(或者叫做对象定义)。
类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。
一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量。
类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图。我们通过设计图可以设计很多建筑物,我们也可以通过类来实例化出多个对象。类和设计图并不会占据空间,只有通过设计图创造出来的房子和类实例化出来的对象才占据内存空间。
在来举个例子,请看:
class Date { public: void Init(int year) { _year = year; } private: int _year; };
上述代码中,Date类是不占用具体的空间的,只有经过对象的实例化之后,才会占有一定的内存空间。比如:
上图就是类的实例化。
但是我们不可以这样:
int main() { Date._year = 25; return 0; }
上述代码就是错误的,因为Date类是不占有内存空间的,只有Date类实例化出来的对象才占有一定的内存空间。
我们也不可以这样:
class Date { public: void Init(int year) { _year = year; } //private: int _year; }; int main() { Date::_year = 25; return 0; }
尽管上述代码中的_year变为了公有的,上述代码依旧是错误的。原因还是没有经过类的实例化。这种行为相当于往声明里放数据,当然是不可以的了,因为没有经过类的实例化之前是不占据任何内存空间的。
七、计算类对象的大小
计算类对象的大小依然是和计算结构体大小的方式一样:遵循内存对齐。
#include<iostream> using namespace std; class A { public: void Print() { cout << _a << endl; } private: char _a; }; int main() { cout << sizeof(A) << endl; return 0; }
我们在计算类大小的时候,是不需要考虑成员函数的(即不需要计算成员函数的大小)。sizeof(类)和sizeof(对象)计算出来的大小是一样的。
下面是结构体内存对齐规则:
1.第一个成员在结构体变量偏移量为0 的地址处。
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 = 编译器默认的一个对齐数与该成员大小中的较小值。vs中默认值是8 Linux默认值为4。
3.结构体总大小为最大对齐数的整数倍。(每个成员变量都有自己的对齐数)。
4.如果嵌套结构体的话,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(包含嵌套结构体的对齐数)的整数倍。
为什么要进行内存对齐:
1.不是所有的硬件平台都能访问任意地址上的数据。
2.某些硬件平台只能只在某些地址访问某些特定类型的数据,否则抛出硬件异常,及遇到未对齐的边界直接就不进行读取数据了。
3.提升CPU访问内存的效率。
内存对齐本质是空间换时间。
下面我们来计算比较特殊的计类对象的大小:
当类中仅有成员函数或者类中什么都没有的时候(即空类)。请看:
class A1 { public: void f1() { } }; class A2 { };
这里大家或许有些许疑惑,按照之前的说法,即我们在计算类大小的时候,是不需要考虑成员函数的(即不需要计算成员函数的大小),既然不需要考虑成员函数的大小的话,那为什么这里的结果输出不是0而是1呢?
我们首先来看一段代码:
当计算没有成员变量的类对象时候需要1byte进行占位,表示该对象存在,并不会存储有效数据。
八、this指针
好了,现在再来看一下这一端代码:
#include<iostream> using namespace std; 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 da1, da2; da1.Init(2023, 5, 20); da2.Init(2023, 5, 21); da1.Print(); da2.Print(); return 0; }
运行结果如下:
上述代码中我们调用了用一个Print函数来打印日期,但是打印结果是不一样的,为什么呢?
错误:有的友友说之所以打印结果不一样是因为da1和da2是两个对象,所以结果不同,这是错误的说法,调用Print函数跟这里的对象da1和da2是没有关系的。因为都是call Print(地址),即都是call的同一个地址,Print函数在公共的代码区域。
之所以打印结果不一样是因为这里存在着隐含的this指针。编译器在进行编译的时候会对Print函数进行“暗箱操作”,对调用的地方也会进行一些处理。
首先我们要知道编译器调用的的确是一个函数,但是调用时候的形参是不同的,一个this指针实指向da1的,另一个this指针是指向da2的。
虽然这里的确是this指针在起作用,但是编译器不允许我们在形参和实参的位置去显式的写this指针,而允许我们在成员函数内显式的写this指针。
即:
还有一点需要我们注意:虽然我们可以在函数内部去显式使用this,但是我们不可以对this指针进行更改,就比如说下面这种写法就是错误的,请看:
之所以不可以在函数内更改this指针,是因为this指针的类型其实是这样的:Data* const this,即const修饰的是this指针本身(即this指针本身不可以进行修改)。但是this指针指向的内容是可以更改的。
但是我们可以这样去写:Data const * const this,意思就是this指针本身不能修改的同时this指针指向的内容也不能进行修改。
这里插一点,我们在访问_year _month _day的时候是不可以这样去访问的:Data::_year或者da1::_year,这两种都是错误的访问方式,前一种是声明和定义的问题,后面那种是域访问限定符的问题。正确的访问方式应该是这样的:da1._year da1._month da1._day.
现在有一个问题,this指针是存放在哪里呢?
先来看第一个问题,this指针是一个形参,形参传给实参要进行压栈操作,所以this指针是栈的一个变量,所以this指针是和普通参数一样存在在栈里面,即this指针作为栈帧的一部分存在在函数调用的栈帧里面,随着函数调用的结束this指针会进行销毁。
这个x64环境下:
这是x86环境下:
vs下面对this指针传递进行优化,对象地址是放在ecx,ecx存储this指针的值。(x86)
请问下面程序运行结果是什么?
#include<iostream> using namespace std; class A { public: void Print() { cout << "Print()" << endl; } private: int _a; }; int main() { A* p = nullptr; p->Print(); return 0; }
这里其实是正常运行的,请看运行结果:
对比着上面的代码,请问下面代码运行结果是什么?
#include<iostream> using namespace std; class A { public: void Print() { cout << _a << endl; } private: int _a; }; int main() { A* p = nullptr; p->Print(); return 0; }
我们来看一下这段代码的运行结果:
发现啥都没有,其实这里是程序运行崩溃了。
第一段代码:指针p调用Print,不会发生解引用,因为Print的地址并不在对象中,Print函数的地址在公共的代码段中,这里的指针p(可以看到是空指针)会作为实参会传递给形参this指针,**传递一个空指针并不会报错,对空指针进行解引用会放生报错。**所以第一段代码正常运行的原因就是因为指针p调用Print函数的时候,并不会发生解引用。
第二段代码:函数内部访问_a本质上是this->_a,由于实参传给形参的时候传给this指针的是一个空指针,第二段代码中函数内部对空指针进行了解引用操作,所以第二段代码就直接程序运行崩溃了。
其实这里我们直接看汇编代码会更好的帮助我们理解,因为代码底层终究还是要转化为指令。
我们发现指令中并没有解引用的行为(即并不会通过指针p来寻找Print函数),而是编译阶段在公共代码区域去找到Print函数,这与访问成员变量有所区别,访问成员变量的话要到指针p所指向对象的空间中去寻找。
既然成员函数并不在对象中,那我们能不能这样去访问成员函数呢?A::Print()。
这中写法是坚决错误的,因为这里成员函数中的this指针什么也没有接收到,换句话说A::Print()什么也没有传给形参this,哪怕传一个空指针nullptr也可以,就怕什么都不传。我们要知道类创建的对象之所以可以调用成员函数,就是因为this指针的作用,我们通过指向这个对象的指针,this指针接收到指向这个对象的指针之后就可以访问对象中的成员变量,比如_year _month _day。
这里还要注意一个问题,我们是不可以这样去写的:A::Print(nullptr)或者 A::Print(p);,因为this指针不能在形参和实参进行显式传递。
好了,到这里就是C++中类和对象中第一部分的内容了。
就到这里了,再见啦各位!!!