目录
先来说一下面向过程和对象的初步认识:
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成
举个例子来理解一下:
假设要设计一个外卖系统
面向过程的这样的:
关注:点餐,接餐,送餐过程,关注流程函数的实现
面向对象:
关注:用户,商家,骑手,关注对象之间的关系.
这个很抽象,在我们后面讲的时候慢慢体会理解面向对象的用法.
C语言中,结构体只可以定义变量,在C++中,结构体不仅可以定义变量.还可以定义函数.
在C++中定义结构体变量时不用写struct,直接写类名即可.但也同时兼容写struct.
- struct people
- {
- char* name;
- int age;
-
- //C++可以定义成员函数
- int Add(int x, int y)
- {
- return x + y;
- }
- int sub(int x, int y)
- {
- return x - y;
- }
- };
-
- int main()
- {
- //C语言定义结构体只能加struct
- struct people p1;
- //C++可以不用写struct
- people p2;
-
- return 0;
- }
C++将结构体升级为了类,C++更喜欢用class来替代struct.
先来看定义的格式
- class className
- {
- // 类体:由成员函数和成员变量组成
- }; // 一定要注意后面的分号
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号
类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数。
类的两种定义方式:
1. 声明和定义全部放在类体中.(需要注意:成员函数如果在类中定义,编译器可能(符合inline条件)会将其当成内联函数处理。)
2.声明放在.h文件,定义 放在.cpp文件里
注意在定义时加类域!!!,因为类是一个域,所以定义得指定域中的函数.
第二种方式一般是最为期望使用的.
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
先来看这三种访问限定符:
解释说明:
1.public修饰的成员可以在类外直接被访问
2.protected和private修饰的成员在类外不能直接被访问.(两个的区别在继承才体现出来,现在就当作暂时是一样的)
3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
4. class的默认访问权限为private,struct为public(因为struct要兼容C)
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
问题:C++中struct和class 的区别是什么?
C++需要兼容C语言,所以C++中struct可以当成结构体去使用。另外C++中struct还可以用来定义类,它和class是定义类是一样的.
区别是struct的成员默认访问方式是public,class是的成员默认访问方式是private.
面向对象三大特性:继承、多态、封装
在现阶段我们只说封装,继承和多态后面会说.
那么什么是封装呢?
将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口和对象进行交互.
看下面两个例子来理解一下:
1.封装本质上是一种管理:我们如何管理兵马俑呢?比如如果什么都不管,兵马俑就被随意破坏了。那么我们首先建了一座房子把兵马俑给封装起来。但是我们目的全封装起来,不让别人看。所以我们开放了售票通道,可以买票突破封装在合理的监管机制下进去参观。
类也是一样,我们使用类数据和方法都封装到一下。不想给别人看到的,我们使用protected/private把成员封装起来。开放一些共有的成员函数对成员合理的访问。所以封装本质是一种管理.
2.对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此在计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来。仅仅对外提供开关机、鼠标以及键盘插孔等,让用户可以与计算机进行交互即可。
刚才上面也提到了:
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员,需要使用::作用域解析符指明成员属于是哪个类域.
- class Person
- {
- public:
- void Print();
- private:
- char name[20];
- char gender[3];
- int age;
- };
- // 这里需要指定Print是属于Person这个类域
- void Person::Print()
- {
- cout<<_name<<" "_gender<<" "<<_age<<endl;
- }
这里顺便提一下:生命周期是从进作用域开始,出作用域结束.即和在内存中的存储位置有关.
用类类型创建对象的过程叫做类的实例化.
1. 类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它
2. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量
3. 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间
看下面这张图来理解:
类相当于设计图,对象相当于根据设计图造出的房子.
问题来了,既然只是一个类不实例化并不会开辟空间,那它的大小该怎么计算呢?
同样地,虽然我们并没有实例化出对象,但是我们也可以根据图纸计算出一共占地多少面积,可以住多少人等等...,所以照样可以计算出大小.
先看下面一段代码:
- class A
- {
- public:
- void PrintA()
- {
- cout << _a << endl;
- }
- private:
- char _a;
- };
- int main()
- {
- cout << sizeof(A) << endl;
- }
有一个char类型的成员变量,有一个成员函数.
那么大小应该是多少呢?
我们来运行以一下:
发现结果是1?!
我们心想不对啊,只一个成员变量_a的大小就一个字节了,那么成员函数的大小呢?去哪了呢?
也同时说明了成员函数不算入其中
那么是为什么呢?
这里有三种设计模式:
第一种:对象中包含类的各个成员
缺陷:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。那么如何解决呢?
第二种:将函数地址单独放到一张表(虚表)中,调用的时候再去找.
这种方法虽然也可以,但是并没有被采用,这种方式在后面多态虚表的时候会用到.
3.只保存成员变量,成员函数存放在公共代码区
既然这样存储,那这个成员函数不就和这个类没有关系了吗?存放在了公共代码区.而不是类所开辟的空间里.
假设有一个类A,A里面有一个 成员变量_a,一个成员函数 print();
用A创建了一个对象aa1,aa1._a,既然调用了_a,那么运行时就要去aa1类里面找到_a,然后调用.
但是调用print(),因为它是存在公共代码区,所以运行时编译器不会去这个对象里去找。
因为在编译链接的时候就根据函数名去公共代码区找到函数的地址 :call函数地址
这一条命令就已经被填充成了刚才的call地址.
那既然成员函数不在类里面调用,那为什么还要aa1.print()来调用呢?
这个其实是用来声明这个print()函数在aa1这个域里面.也和后面所说的this指针有关.
如果理解了这样的存储方式,那么看下面一段代码:
- class A
- {
- public:
- void PrintA()
- {
- ;
- }
- private:
- char _a;
- };
- int main()
- {
- A* ptr = nullptr;
- ptr->PrintA();
- }
按理来说,ptr是一个空指针,空指针指向任何内容都会报错,那么看看这个程序运行会报错吗?
正常运行了!
这就和刚才我们所说的有关系,因为成员函数存储在公共代码区了,类对象没有去调用它,所以直接调用这个函数当然没问题啦.
所以计算类的大小时,并不用计算上成员函数的大小,只需要计算成员变量的大小即可,同时注意要内存对齐!
结论:一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类
至于编译器是如何内存对齐的,大家可以去我之前的文章里:结构体的内存对齐,里面有详细的解释.
这里简单整理一下:
1. 第一个成员在与结构体偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的对齐数为8
3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
非常建议去看看之前写的结构体内存对齐.
这样最后算出来的总大小就是类的总大小了.
先来看下面一段代码:
- class Date
- {
- public:
- void Display()
- {
- cout << _year << "-" << _month << "-" << _day << endl;
- }
- void SetDate(int year, int month, int day)
- {
- _year = year;
- _month = month;
- _day = day;
- }
- private:
- int _year; // 年
- int _month; // 月
- int _day; // 日
- };
- int main()
- {
- Date d1, d2;
- d1.SetDate(2018, 5, 1);
- d2.SetDate(2018, 7, 1);
- d1.Display();
- d2.Display();
- return 0;
- }
会有这样一个问题:
Date类中有SetDate与Display两个成员函数,函数体中没有关于不同对象的区分,那当s1调用SetDate函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?
也就是说无论是s1还是s2调用成员函数,对于成员函数来说,都是调用,那函数怎么知道是s1还是s2呢?
C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
1. this指针的类型:类类型* const
2. 只能在“成员函数”的内部使用
3. this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
4. this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
也就是说,刚才的Display函数,表面看没有参数,实际上有一个隐藏的this指针.
即
void Display(Date* this);
注意,在写参数的时候不要显式的写this指针,这里只是为了说明所以写上了.
有了this的指针便知道了是哪个对象,所以输出成员变量的时候相当于前面有一个this指针指向它,这个this可写可不写。相当于:
- void Display()
- {
- cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
- }
关于this指针还有两个问题:
1.this指针存在哪里?
存在栈区。
因为它是一个形参,形参都是存在栈中。保存的是对象的地址.
但是vs编译器会对其进行一个优化:
vs中this指针是通过ecx寄存器传递的,这样this访问会提高效率.
优不优化取决于编译器.
2.this指针可以为空吗?
可以为空,当我们调用函数时,如果函数内部不需要使用到this,也就是不需要通过this指向当前对象并对其进行操作时才可以为空(当我们在其中什么都不放或者在里面随便打印一个字符串,其实主要是因为成员函数存在公共代码区,如上面讲的).
如果调用的函数需要指向当前对象,并进行操作,则会发生错误(空指针引用)就跟C中一样不能进行空指针的引用