目录
一、面向过程和面向对象初步认识
二、类的引入和定义
2.1类的引入
2.2类的定义
三、类的访问限定符及封装
3.1访问限定符
3.2封装
四、类的作用域
五、类的实例化
六、类的对象大小的计算
6.1如何计算对象的大小
6.2类对象的存储方式
七、类成员函数的this指针
7.1this指针的引出
7.2this指针的特性
一、面向过程和面向对象初步认识
面向过程:关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
例如:洗衣服
拿盆子——>放水——>放衣服——>放洗衣粉——>手搓——>换水——>手搓——>拧干——>晾衣服
我们知道C语言面向的是过程,例如我们要得到两数之和的结果,那么得造一个和函数,先进行传实参,形参相加,得到参数之和,返回之和的值,这就是得到两数之和的一个过程。
面向对象:关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
还是洗衣服,有四个对象:人、衣服、洗衣粉、洗衣机
那么洗衣服过程:人将衣服放入洗衣机、倒入洗衣粉,启动洗衣机,洗衣机就会完成洗衣过程,人不需要关注洗衣机是如何洗衣服的。
那么照样我们也要得到两数之和的结果,就不需要去实现函数了,而是直接用函数就行,这个函数是已经被实现的,这就是直接使用对象,不需要关注实现过程。
那么还有疑问的是,对象之间的操作怎么看起来也是一个过程,其实是不同角度产生的看法,那么对于同一件事情,洗衣服,从过程来看,这个衣服需要执行被洗的过程,在乎的是怎么洗的过程,而从对象来看,只需要用洗衣机完成洗衣服,怎么洗的过程不在乎。其实这就相当与面向对象是以更高的角度去看待面向过程。
我再从更高的角度去看待面向对象,那么面向对象又可以看成面向过程。所以对于任何实现从不同角度既可以看成面向过程,也可以看成面向对象
对于c++我们将其看成面向对象的语言,因为对标C语言,c++很多实现要比C语言简单的多,不用造轮子(自己实现没必要去浪费时间做的函数),当然你以更高的角度来看c++实现的函数也可以看成过程 ,但是你得在更高的角度弄出点东西来,不然也改变不了啥。
二、类的引入和定义
2.1类的引入
回顾C语言实现栈时,需要有一个结构体存放定义的变量,而具体的实现需要额外定义函数完成压栈、出栈、销毁栈等。那么在c++中,结构体不仅可以定义变量,也可以定义函数了,同样我们用栈来举个例子
#include <iostream>
using namespace std;typedef int DataType;
struct stack
{//栈的初始化void Init(size_t capacity){_array = (DataType*)malloc(sizeof(DataType*) * capacity);if (nullptr == _array){perror("malloc fail");exit(-1);}_capacity = capacity;_size = 0;}//压栈void Push(const DataType& data){_array[_size] = data;++_size;}//取队头元素DataType Top(){return _array[_size - 1];}//栈的销毁void Destroy(){if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}//变量的定义DataType* _array;size_t _capacity;size_t _size;
};int main()
{stack s;s.Init(5);s.Push(1);s.Push(2);s.Push(3);cout << s.Top() << endl;s.Destroy();return 0;
}
运行结果:
从上面代码可以看出,我们将变量的定义与函数的实现都放在结构体中,且这些变量可以在函数中使用,像这种将函数和变量都放在一个结构体中,在这种情况下,在c++中我们称之为类,也就是说,C语言中的结构体在c++中升级成了类,当然类依然支持C语言中的结构体用法,毕竟c++兼容C语言嘛。那么接下来讲解类定义的详细用法
注意:上述结构体的定义,在c++中更喜欢用class来代替,其实struct和class还是有区别的,在类的访问限定中会讲解。
2.2类的定义
形式规则:
class className
{//类体:由成员函数和成员变量组成};//一定要注意后面的分号
class为定义类的关键字,className为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。
类体中内容称为类的成员:类中的变量称为称为类的属性或成员变量;类中的函数称为类的方法或者成员函数
以上是类定义的形式规则,但对于类中的成员函数定义和成员变量命名也有一定的规则。
成员函数的定义有两种方式:
1.成员函数的声明和定义都放在类体中,需要注意的是,编译器可能会将其当成内联函数处理。
2.类声明放在.h文件中,成员函数放在.cpp中,注意:成员函数名前需要加类名::
//声明放在类的头文件fun.h中
#include <iostream>
using namespace std;class Person
{
public://显示基本信息void showinfo();
public:char* _name;char* _sex;int _age;
};//定义放在类的实现文件中
#include "fun.h"void Person::showinfo()
{cout << _name << "-" << _sex << "-" << _age << endl;
}int main()
{Person s;s._name = (char*)"张三";//由于张三是字面常量,类型为const char*而_name为char*类型s._sex = (char*)"男";s._age = 18;s.showinfo();return 0;
}
运行结果:
一般情况下,最好采用第二种方式,但在之后的讲解中,为了方便,可能会使用方式一。但在以后的工作中的话,那尽量使用方式二 。
成员变量命名规则的建议:
#include <iostream>
using namespace std;class Date
{
public:void Init(int year){//这里的year到底是成员变量还是函数形参,虽然知道左边的year是定义的变量,而右边的year是形参的year//但难免带来一种令人不友好的感觉year = year;//所以一般建议定义的变量加上前缀或者后缀,对于我而言习惯加上前缀int _year = year;int year_ = year;int myear = year;}
private:int year;int _year;int year_;int myear;
};int main()
{Date d;d.Init(3);return 0;
}
注意:默认直接在类里面定义的函数就是inline函数,不需要加inline关键字,当然在编译时展不展开函数还是取决编译器 。
三、类的访问限定符及封装
3.1访问限定符
在上述代码中,看到我们加了public、private这类符号,是什么?
像这类符号我们称之为访问限定符,其实,这是c++实现封装方式的必要手段,用类将对象的属性和方法结合在一块,让对象更加完善,再通过访问访问限定符选择性的将其接口提供给外部的用户使用。
那具体是啥意思,访问限定符有哪些?look
访问限定符,用来限定类外是否能访问类里面的成员,访问限定符分为三类,public(公有)protected(保护)和private(私有)
访问限定符说明:
1.public表示类外是可以访问类里面被public修饰的成员,
2.protected和private表示类外不能直接访问类里面被这两个修饰的成员,他们是类似的,但还是有区别的,在继承阶段会讲解。
3.访问权限作用域从该访问限定符出现的位置开始知道下一个访问限定符出现时为止,如果后面没有访问限定符,作用域就到 } 即类结束
4.上面我们提到了class和struct是有区别,对于class,其成员的默认访问权限为private,而struct为public(想想,虽然在c++中struct升级成了类,但依然支持C语言struct的用法,而C语言的struct成员都是可以直接被访问的,不需要限定符。所以在这里struct的默认访问权限是public)
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
3.2封装
对于对象而言,无非就三大特性:封装、继承、多态。继承和多态后面的章节再说,在类和对象阶段,主要说的就是封装,那么什么是封装呢?
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互,封装本质上是一种管理,让用户更方便实用类。说白了,不就是用类将他们包起来,再通过访问限定符是否对外提供接口来实现交互。
在现实中,这个道理也是一样的,像电脑,里面的零件都是被封装的,然后提供一个USB接口给用户使用。
四、类的作用域
在上面,我们提到并给出了代码,类中的函数长的声明和定义要分离,分离定义的函数怎么与类中声明的函数进行联系呢?跟命名空间一样,类也是一个作用域,类的所有成员在类的作用域中,类外定义成员时,需要使用::作用域操作 符指明成员属于那个类域
#include <iostream>
using namespace std;class Person
{
public://打印基本信息void printpersoninfo();
public:char _name[20];char _gender[3];int _age;
};//指明函数属于哪个类域
void Person::printpersoninfo()
{cout << _name << "-" << _gender << "-" << _age << endl;
}int main()
{Person s;strcpy(s._name , "李四");strcpy(s._gender , "女");s._age = 19;s.printpersoninfo();return 0;
}
运行结果:
五、类的实例化
仅仅有一个类不能对里面的内容进行访问,而是要创建一个对象,通过对象来对类中的成员进行访问。用类类型创建对象的过程,称为类的实例化。
类是对对象进行描述的,是一个模型一样的东西,限定类有那些成员,定义出一个类并没有分配实际的内存空间来存储它。必须实例化出对象才占有实际的物理空间,存储类成员变量。且一个类可以实例化出多个对象。
比如:房子设计图,就是一个类,设计图描述了房子的各种信息,但是并没有为这些信息建造出一个实际的空间。而只有建造出了真正的房子才是实例化,才分配了空间,且通过设计图,也可以建造出多个房子,即实例化多个对象。再通过代码来比较。
#include <iostream>
using namespace std;class Person
{
public://显示基本信息void printpersoninfo();
public:char _name[20];char _gender[3];int _age;
};//指明函数属于哪个类域
void Person::printpersoninfo()
{cout << _name << "-" << _gender << "-" << _age << endl;
}int main()
{Person s;//创建对象,且实例化多个对象Person t;Person u;strcpy(s._name , "李四");strcpy(t._name , "王五");strcpy(u._name , "李六");strcpy(s._gender, "女");strcpy(t._gender, "男");strcpy(u._gender, "男");s._age = 18;t._age = 19;u._age = 20;s.printpersoninfo();t.printpersoninfo();u.printpersoninfo();return 0;
}
运行结果:
六、类的对象大小的计算
6.1如何计算对象的大小
对于类中既有成员函数和成员变量,那么计算大小时是不是他们都要算呢?look
#include <iostream>
using namespace std;
class A
{
public:void PrintA(){cout << _a << endl;}
private:char _a;
};int main()
{A a1;cout << sizeof(a1);//猜猜多大return 0;
}
运行结果:
通过结果发现,大小只有1,只计算了成员变量_a 的大小。这是怎么回事呢?且看下面分析
6.2类对象的存储方式
以正常的理解,当一个类实例化多个对象时,对象与对象之间的成员变量、成员函数是互不干扰的,但每个对象都会保存一份该成员函数的代码,相同的代码保存了多份,实例化了多份,就会形成空间的浪费。
所以给了对象的新的存储方式,对象中只保存成员变量,而对于成员函数,只保存一份放在公共代码区,每个对象调用该函数时,其实就是调用公共代码区的该函数。所以在上述计算大小时,就不会算上成员函数。
再来看几个例子:
空类:
#include <iostream>
using namespace std;class A3
{};int main()
{cout << sizeof(A3);return 0;
}
运行结果:
为什么也是1呢,不应该是0吗?明明是一个空类。
OK,空类其实也可以实例化对象,这是前提。既然可以实例化对象,那么对象就会在内存分配空间,该对象有地址存在,只不过不存储任何数据。如果,空类的大小为0表示该类没有任何成员或对象。这意味着该类不占用内存空间。所以,为了标识该对象存在,编译器规定该空类的大小为1
类中仅有成员函数:
#include <iostream>
using namespace std;class A2 {
public:void f2() {}
};int main()
{cout << sizeof(A2);return 0;
}
运行结果:
大小也是1,根据上面的解释成员函数是放在公共代码区,它们在内存中独立存在,所以在计算大小时, 相当于计算空类的大小。
注意:计算类的大小依然遵守结构体内存对齐规则 !!!
七、类成员函数的this指针
7.1this指针的引出
#include <iostream>
using namespace std;class Date
{
public:void Init(int year = 2023, int month = 11, int day = 23)//给缺省值{_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(2023, 1, 11);d2.Init(2023, 1, 12);d1.print();d2.print();return 0;
}
运行结果:
对于上述类,实例化两个对象,在表面上两个对象都调用了Init函数时,但是在深层,函数怎么识别是谁调用?
c++引入了this指针来识别对象的调用:c++编译器给每个“非静态的成员函数”增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。但要注意的是,this指针的所有操作对于程序员而言是透明的,即this指针不需要程序员来传递,编译器会自动完成(可以认为是祖师爷设定好的)。
7.2this指针的特性
1.this指针的类型:类类型* const,例如Date* const this。由于this被const修饰,所以在成员函数中,不能给this指针赋值。
2.只能在“成员函数”的内容使用
3.this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象本身不存储this指针。
4.this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
其实上述代码的本质就是第二张图的形式,对应着上述规则。注意的是在调用函数传实参、以及函数的形参中不能显示的将this指针写出来,否则会报错,但在成员函数中,可以显示的写出。
【面试题】
1.this指针存在哪里?
将上述代码,转到反汇编后,可以发现,d1对象和d2对象存放到rcx寄存器。由此this指针在VS下存放在栈区的寄存器中。
2.this指针可以为空吗 ?
先看代码1:下面运行结果? A、编译报错 B、运行崩溃 C、正常运行
#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;
}
运行结果:
答案是正常运行,疑问的是p是空指针,为什么可以访问Print函数?
其实这是一个误区,想想前面讲解的,首先成员函数不是放在对象中,而是在公共代码区,其次在这里p并没有去访问Print函数,但是,如果没有访问该函数,那又是怎么寻得该函数。在C语言中,就知道了,是在编译、链接阶段通过符号表来寻找函数名字,所以在这里p的作用就是告诉编译器它属于A类,然后在编译阶段,A类中寻找是否存在Print函数。
代码2:下面运行结果? A、编译报错 B、运行崩溃 C、正常运行
#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;
}
运行结果:
运行结果是崩溃的,对比第一个代码,修改了成员函数的输出,且成员变量并没有初始化。本质上就是this->_a,这个就是空指针访问了成员变量,而空指针并不指向任何有效的对象,因此其中的成员变量也是未定义的。
综上所述,虽然成员函数的代码本身放在公共代码区,但成员函数通常涉及对对象中的成员变量进行操作,这可能导致问题。因此最好不要让this指针为空,以避免潜在的未定义行为。
end~