一、类的引入
在C语言结构体中只能定义变量,但是在C++中,结构体内不仅可以定义变量,也可以定义函数。比如:
#include<iostream> #include<stdlib.h> using namespace std; typedef int DataType; struct Stack { void Init(int _capacity=4)//缺省参数 { DataType* tmp = (DataType*)malloc(sizeof(DataType) * _capacity); if (nullptr == tmp) { perror("malloc fail:"); exit(-1); } _a = tmp; _Top = 0; _capacity = _capacity; } DataType* _a; int _Top; int _capacity; }; int main() { Stack s1; //C++中struct定义的结构创建变量时不需要加struct s1.Init(10); s1._Top = 100; s1._capacity = 200; return 0; }
上面结构体的定义,在C++中更喜欢用class
来代替struct
。
二、类的定义
类的定义格式为:
class className { // 类体:由成员函数和成员变量组成 }; // 一定要注意后面的分号
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。
类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。
1、类的两种定义方式:
- 声明和定义全部放在类体中(如类的引入中的代码),需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
- 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 ::作用域操作符指明成员属于哪个类域,因此成员函数名前需要加类名 ::,一般情况下,更期望采用此种方式。
头文件:Test.h中
函数实现:Test.cpp中
主函数文件:2023.02.07.cpp中
2、成员变量命名规则的建议:
先看一个错误示范:
class Date { void Init(int year) { // 这里的year到底是成员变量,还是函数形参? year = year; } int year; };
实际上 = 号两边都是形参,因为局部变量优先,这里我们可以看出命名的重要性,为了避免与形参混淆,我们通常对成员变量前加一个"_"符号。
class Date { void Init(int year) { _year = year; } int _year; };
三、类的访问限定符及封装
1、访问限定符
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
访问限定符的使用:
class Stack { public: void Init(int _capacity = 4); private: DataType* _a; int _Top; int _capacity; };
【访问限定符说明】
- public修饰的成员在类外可以直接被访问
- protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- 如果后面没有访问限定符,作用域就到} 即类结束。
- class的默认访问权限为private,struct为public(因为struct要兼容C,C的struct是公有的)
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定
#include"Test.h" int main() { Stack s1; s1.Init(10);//编译成功 s1._Top = 100;//编译报错,无法访问 return 0; }
【面试题】
问题:C++中struct和class的区别是什么?
解答:C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类默认访问权限是private。
注意:在继承和模板参数列表位置,struct和class也有区别,后序给大家讲解。
2、封装
面向对象的三大特性:封装、继承、多态。
在类和对象阶段,主要是研究类的封装特性,那什么是封装呢?
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装本质上是一种管理,让用户更方便使用类。
例如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。
对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以及键盘插孔等,让用户可以与计算机进行交互即可。
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
再例如,我们使用C++实现栈与C语言实现栈进行对比:
- C语言实现栈:
#include<stdio.h> ... typedef int DataType; typedef struct Stack { DataType* a; int capacity; int Top; }Stack; void StackInit(Stack* ps){ assert(ps); ...; } void StackPush(Stack* ps, DataType data){ assert(ps); ...; } void StackPop(Stack* ps){ assert(ps); ...; } .... .... int main() { Stack s1; StackInit(&s); StackPush(&s, 3); StackDestroy(&s); return 0; }
- C++实现栈
#include<iostream> using namespeace std; ... typedef int DataType; class Stack { public: void Init(Stack* ps) { assert(ps); ...; } void Push(Stack* ps, DataType data) { assert(ps); ...; } void Pop(Stack* ps) { assert(ps); ...; } .... .... private: DataType* a; int capacity; int Top; }; int main() { Stack s1; Stack.Init(&s); Stack.Push(&s, 3); Stack.Destroy(&s); return 0; }
对比就会发现C语言可以随便访问数据结构本身,容易造成安全问题,而C++只允许使用成员函数来访问数据结构本身,避免了这样的安全性问题,而且使用起来更加简单方便。
C++把栈中用到的所有函数和变量放在一起,对外封装成一个类,这也是封装的一种体现。
四、类的实例化
1、类的实例化概念
我们在类中定义的变量其实是变量声明,与函数声明类似,在定义类时操作系统并不会给我们开辟空间,只有我们用类去定义对象时,操作系统才会给我们开辟空间,用类类型创建对象的过程,我们称为类的实例化。
我们可以将类比作建筑图纸,别墅就是建筑图纸的实例化,那么对象就是类的实例化。
class test { int a;//变量的声明 int b;//变量的声明 }; int main() { test t1;//用test类实例化了一个t1的对象 return 0; }
2、类对象的大小的计算
一个类可以实例化出多个对象,实例化出的对象 占用实际的内存空间,存储成员变量,那么我们如何计算类的大小呢?
废话少说,这里我们C++类的大小继承了C语言结构体大小的计算,也是按照结构体内存对齐规则
【结构体内存对齐规则】
- 第一个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的对齐数为8
- 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
请你计算一下32位平台下Date类的大小
class Date { public: void print() { cout<<_year<<"/"<<_month<<"/"<<_day<<"/"<<endl; } private: int _year; int _month; int _day; };
正确答案是:12,三个变量的对齐数都是4,类的最大对齐数为4,从前到后中间没有多余空间依次排列,故为12。
你可能会说,为什么不是16呢?函数print用一个函数指针存储起来而且32位平台下一个函数指针刚好占用4个字节。能想出这个问题说明你对函数指针学习的很好,但是呢我首先明确告诉你在C++的类中,类的大小只计算成员变量,不计算成员函数,再其次我们来讨论为什么C++的类中不计算成员函数的大小?
我们看下面一段代码,我们调用的两个函数Init()是不是同一个函数呢?
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 d1; Date d2; d1.Init(2023, 2, 9); d2.Init(2023, 2, 10); return 0; }
答案是是的,我们调用的是同一个函数,如果我们把成员函数也算进类的大小中,那么我们每次创建每个对象时都要为函数指针分配空间,当我们创建许多对象时,它们调用函数时都用同一个地址都要去调用同一个函数,但是呢,每个对象要有一个指针存储去存放相应函数地址,这就造成了一个函数的地址被许多个指针存储,内存空间造成了很大的浪费。
于是我C++就把成员函数全部放进一个叫公共区域,这个公共区域叫中叫做代码段,那个对象调用函数就去代码段中找相应的函数。
扩展知识:(可以忽略)
C/C++程序内存分配的几个区域:
- 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结
束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是
分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返
回地址等。 - 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分
配方式类似于链表。 - 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
- 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
有了上面的知识后我们再计算两个类的大小
// 类中仅有成员函数 class A2 { public: void f2() { } }; // 类中什么都没有---空类 class A3 { };
你可能认为它们是0,但是那也太奇怪了吧?定义一个对象但是对象不占用内存???
其实这种类中仅有成员函数或类中什么都没有的空类,都是比较特殊的,编译器给了这种类一个字节来唯一标识这个类的对象。
五、this指针
this
是C++ 63个关键字中的一个。
对于它的理解我们先看一段代码
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 d1, d2; d1.Init(2022, 1, 11); d2.Init(2022, 1, 12); d1.Print(); d2.Print(); return 0; }
对于上述类,有这样的一个问题:
Date类中有 Init()与 Print() 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?
首先我们知道如果我们是C语言实现这段代码的同样功能,在我们调用Init()与 Print() 函数时会采用传指针的方式,这个指针就可以帮我们区分不同的对象。
typedef struct Date { int _year; // 年 int _month; // 月 int _day; // 日 }Date; void Init(Data*pd,int year, int month, int day) { pd->_year = year; pd->_month = month; pd->_day = day; } void Print(Data*pd) { cout <<pd-> _year << "-" << pd->_month << "-" << pd->_day << endl; } int main() { Date d1, d2; Init(&d1,2022, 1, 11); Init(&d2,2022, 1, 12); Print(&d1); Print(&d2); return 0; }
与C语言不同的是C++中通过引入this
指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
上面的标准回答翻译成大白话就是:C++的实现原理与C语言一样,对象d1调用函数的话,this
指针就存储d1的地址,在传递参数时只不过我们不用自己传递指针了,是由编译器自动帮我们传递了指针。
C++实现的真实方式:
class Date { public: void Init(Date*this,int year, int month, int day)//此处只是为了演示,Date*this不用我们手动传递, //编译器会自动传递,我们手动传递会报错 { this->_year = year;//此处的this->可以省略 this->_month = month;//此处的this->可以省略 this->_day = day;//此处的this->可以省略 } void Print()//同理,不再演示 { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; // 年 int _month; // 月 int _day; // 日 }; int main() { Date d1, d2; d1.Init(2022, 1, 11); d2.Init(2022, 1, 12); d1.Print(); d2.Print(); return 0; }
this指针的特性
- this指针的类型:类型* const,即成员函数中,不能给this指针赋值。
- 只能在“成员函数”的内部使用
- this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参,所以对象中不存储this指针。
- this指针是“成员函数”第一个隐含的指针形参,一般存在与栈中(因为它的本质是函数形参),VS通过ecx寄存器存储。