您现在的位置是:首页 >技术杂谈 >类和对象 -上(C++)网站首页技术杂谈
类和对象 -上(C++)
目录
2、如何让结构体按照指定的对齐参数进行对齐?能否按照3、4、5即任意字节对齐?
认识面向过程和面向对象
面向过程:
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题
洗衣服例子:
C语言关注的是过程:
面向对象:
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象。靠对象之间的相互交互完成!
洗衣服例子:
C++关注的是参与的对象:
类的引入
C++中把结构体升级成了类:
在C语言中结构体中只能定义变量。而C++中,结构体内不仅可以定义变量,还可以定义函数。
例如:
数据结构栈的实现,C语言的方式实现栈,结构体中只能定义变量。现在以C++的方式,结构体中既可以定义变量,也可以定义函数!
代码:
#include <iostream> using namespace std; //C++实现栈 // 在C++中将struct升级成了类,内部既可以定义变量,也可以定义函数 //用C++实现栈 typedef int DataType; struct Stack { //初始化函数 void Init(int capacity = 4) { _a = (DataType*)malloc(sizeof(DataType) * capacity); //判断是否开辟成功 if (_a == nullptr) { perror("malloc tail: "); return; } _capacity = capacity; _size = 0; } //扩容函数 void Expansion() { //扩容 if (_size == _capacity) { DataType* tmp = (DataType*)realloc(_a, sizeof(DataType) * 2 * _capacity); if (nullptr == tmp) { perror("realloc tail: "); return; } _a = tmp; _capacity *= 2; } } //入栈函数 void Push(const DataType x) { Expansion(); _a[_size] = x; ++_size; } //出栈函数 void Pop() { //判断栈是否为空 if (Empty()) { printf("Stack is Empty "); } --_size; } //返回栈顶元素 DataType Top() { return _a[_size - 1]; } //判断栈是否为空 bool Empty() { return _size == 0; } //栈里面的元素个数 int Count() { return _size; } //销毁 void Destory() { if (_a != nullptr) { free(_a); _a = nullptr; _size = 0; _capacity = 0; } } DataType* _a; int _size; int _capacity; }; int main() { //对象实例化 Stack st1; //初始化 st1.Init(); //入栈 st1.Push(1); st1.Push(2); st1.Push(3); st1.Push(4); //栈顶元素 cout << st1.Top() << endl; //出栈 st1.Pop(); //栈里面的元素个数 cout << st1.Count() << endl; //销毁栈 st1.Destory(); return 0; }
总:
以上结构体的定义,C++中更喜欢用Class关键字来定义类
类的定义
语法:
在C++中常用Class关键字定义类
语法:
class className { // 类体:由成员函数和成员变量组成 }; // 一定要注意后面的分号
Class是定义类的关键字。className是类名(我们自己起的)。{}内是类的主体,可以有变量,也可以有函数。注意类定义结束时一定不能省略分号;
类中的成员:
类中的内容称为类的成员。类中的变量称为类的属性或成员变量。类中的函数称为类的方法或者成员函数。
类的两种定义方式:
1、声明和定义全在类体内
声明和定义全部放在类体中。需注意在类中定义的成员函数默认都是用inline修饰的。至于编译器是否让它成为inline函数,取决于编译器本身!
代码:
//类的方法(成员函数)的声明和定义全都放在类体内 //在类中定义的方法默认时inline修饰的 // 至于是否成为inline函数是由编译器决定的! //定义一个时间日期类 class Data { //打印方法,成员函数的方法和定义都放到类体内 //默认是inline修饰的! void Print() { cout << _year <<" - "; cout << _month << " - "; cout << _day << endl; } int _year; //年 int _month; //月 int _day; //日 };
2、声明和定义分离
类声明放在.h文件中,成员函数定义在.cpp文件中。注意:成员函数名前需要加类名和作用域限定符(类名::函数名)。否则编译器无法确认该函数时那个类中的!
代码:
注意:
声明和定义分离的成员函数默认不是inline修饰的!
总结:
一般情况下,建议采用第二种方式来定义类!
成员变量命名规则建议
成员变量的命名规则建议:
一般情况下,为了防止成员变量名和成员函数中的形参或者定义的变量名同名。为了提高代码可读性,更好的区分两者。我们在对类的成员变量命名时,加一些区分标记用于区分。一般情况下在类的成员变量名前加一个下划线(_year) 、或者在类的成员变量的后面加一个下划线(year_)。又或者在成员变量的前面加一个特定字母和下划线(m_year) ……等
代码:
//一般情况下,为了区分类里面的成员变量 // 给其加一个特定的修饰(这个是自己来定) class Date { public: //区分成员变量和形参,将成员变量名进行特定修饰 void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } void Pint() { cout << _year <<" - "; cout << _month << " - "; cout << _day << endl; } int _year; //年 int _month; //月 int _day; //日 };
建议:
一般采取前置下划线_ 或后置_来修饰变量名!
类的访问限定符及封装
访问限定符
C++实现封装的方式:
用类将对象的属性与方法结合在一起,让对象更加完善,通过访问权限选择性的将其接口提供给外部用户使用!
访问限定符:
public (公有):
用public 修饰,类中从public开始直到遇到下一个访问限定符为止之间的内容,类外面和类里面都可以访问!若没有下一个访问限定符。类中从public位置开始直到类域结束之间类体中的内容即可以通过外部进行访问,也可以通过内部进行访问!因为是公有的!
protected(保护)和private(私有):
此处protected和pirvate是类似的,protected和private修饰的成员在类外不能直接被访问。从protected 或 pribate 位置开始,直到遇到下一个访问限定符为止,两者之间的内容是不能在类外面直接访问。若没有下一个访问限定符, 从protected 或 pribate 位置开始直到类结束,之间的内容也是不能在类外面直接访问的!目前私有和保护可以看做是类似的!后续再做区别!
说明:
1、public 修饰的成员在类外可以直接被访问
2、protected 和 private 修饰的成员在类外不能直接被访问(此处protected和private是类似的)
3、访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现为止
4、如果后面没有访问限定符,作用域就到 } 即类结束。
5、class的默认访问权限private,struct默认为public(因为struct要兼容C的结构体用法)
注意:
访问限定符只在编译时有用,当数据映射到内存后,没有访问限定符上的区别!
C++ 中 class 和 struct 的区别?
解答:
因为C++要兼容C语言,所以C++中的struct 可以当作结构体来使用,另外C++中的struct还可以定义类。和class定义类是一样的!区别:C++ 中的class 定义类的成员默认访问是用private修饰的!而struct定义的类默认访问是public修饰的!
注意:
在继承和模板参数列表中,class和struct 也有区别!(后续文章讲解)
封装
封装:
将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互!
本质:
封装本质上是一种管理,为了更好的管理类内的数据,进行封装操作,让用户方便使用类,实现交互
例如:
对于电脑这样一个复杂的设备,提供给用 户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日 常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。 对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如 何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计 算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以 及键盘插孔等,让用户可以与计算机进行交互即可。
C++中的封装:
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限(访问限定符)来隐藏对象内部实现细节,控制那些方法(函数)可以在类外部直接被使用!
类的作用域
类域:
类定义了一个新的作用域,叫类域。类中的所有成员都在类作用域中。在类体外定义成员时,要加上作用域限定符,通过::作用域限定符指明成员属于那个类域!
代码:
#include <iostream> using namespace std; //定义时间日期类 class Date { public: void Init(int year = 2023, int month = 4, int day = 29) { _year = year; _month = month; _day = day; } //打印 //类里面给声明,类外面定义 void Print(); int _year; int _month; int _day; }; //在类外面定义的时候,需要通过::域访问限定符 //指定该成员是那个类的 void Date::Print() { cout << _year <<" - "; cout << _month << " - "; cout << _day << endl; } int main() { //实例化对象 Date st1; st1.Init(); st1.Print(); return 0; }
类的实例化
实例化:
用类的类型创建对象的过程叫做类的实例化!
类是对 对象进行描述的:
如同是一个模型一样的东西,实际类里面的成员变量都是声明并没有开空间,只有在类进行实例化之后才开辟了空间,而且用类定义的每个对象都有一个独立的空间!
一个类可以实例化出多个对象:
一个类是可以实例化出多个对象的。类本身是没有空间的。而类实例化出的对象是具有空间的,且每个对象都有一个独立的空间!
打个比方:
类实例化对象,就像现实中使用建筑设计图建造出房子,类就是设计图,只是设计出需要做什么东西,是一张图纸,并没有实体建筑存在。而类实例化出的对象就是真正的房子建筑!即类只是声明,而实例化出的对象才能开辟空间,实际存储数据!总的来说:类实例化出对象就是通过图纸建造出了房子!
代码:
#include <iostream> using namespace std; //定义时间日期类 //类中的成员变量只是声明,并未开辟空间 class Date { public: void Init(int year = 2023, int month = 4, int day = 29) { _year = year; _month = month; _day = day; } //打印 //类里面给声明,类外面定义 void Print() { cout << _year <<" - "; cout << _month << " - "; cout << _day << endl; } int _year; int _month; int _day; }; int main() { //类实例化对象 //开辟了空间 Date st; //通过实例化的对象调用类的成员函数 st.Init(); st.Print(); return 0; }
类对象模型
如何计算类对象的大小
类中既然有成员函数(方法),也有成员变量(类的属性),那么一个类的大小该怎么计算呢?
代码:
#include <iostream> using namespace std; //用class 定义时间日期类 class Date { public: //打印函数 void Print(); }; //定义一个Std类 class Std { }; int main() { cout << sizeof(Date) << endl; cout << sizeof(Std) << endl; return 0; }
注意:
类中大小只计算的是成员变量的大小,对于成员函数是不计入空间的,因为他是一个单独的栈空间,在类中若是没有成员变量,类的总大小是1byte。在实例化对象的时,若类中没有成员变量,对象饿大小也是1byte,因为该对象需要占一个位置。如同我们以上所述的一样,定义类是一张图纸,而实例化对象是建造房子,类中的对象如同是卧室客厅等!假设我们没有建造房子,那么我们就得有一个地基,这个地基就是用来占位的,类中的大小也是如此,没有成员变量的类的大小 1 个字节只是用来占位的,实际不存储有效数据!类的存储依旧是遵从结构体的内存对齐的!因为C++要兼容C语言的结构体,所以C++ 中类的存储依旧是执行内存对齐的!
总节:
一个类的大小实际是各个成员变量的总和,当然要注意内存对齐问题,注意空类的大小,因为空类比较特殊,编译器给了空类一个字节来标识类的对象!
结构体的内存对齐规则
对齐规则:
1、第一个成员在与结构体偏移量为0的地址处
2、其它成员变量要对齐到某个数字(对齐数)的整数倍的地址处!
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的的较小值。Vs中默认对齐数为8
3、结构体的总大小为:最大对齐数(所有成员变量类型的最大者,与默认对齐数的最小值)的整数倍!
4、如果嵌套了结构体的情况,嵌套的结构体对齐到自己的默认对齐数的整数倍处,结构体整体的大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍!
面试题
1、结构体怎么对齐?如何对齐?
解答:
结构体是按照结构体的对齐规则来进行对齐:
1、第一个成员变量从偏移量为0的地址处开始存储
2、后续的成员变量首先取到该类型的的大小,与默认对齐数两者之间的最小值,作为该变量类型的对齐数,按照这个对齐数,该变量在存储的时候,对应到取到的对齐数的整数倍处。进行存储,存完前一个成员,若下一个位置的偏移量不是该对齐数的整数倍,则对齐到该对齐数的整数倍进行存储!
3、最后将所有成员都对齐存储完毕之后,取到所有成员的最大值和默认对齐数进行比较,取到两者之间的最大值。最后结构体的大小对齐到该最大值的整数倍!
4、结构体的大小就是对齐到最大值的整数倍的大小!
2、如何让结构体按照指定的对齐参数进行对齐?能否按照3、4、5即任意字节对齐?
解答:
结构体的对齐是有规定的,不可任意对齐,但有一条指令可以修改默认对齐数(#pragma pack)
结构体进行规定对齐是跟底层的硬件有关系的,每种硬件对于数据的读取是不同的,为了提高硬件读取数据的效率,所以在代码层进行了结构体内存对齐。
比如:假设一台机器有32根地址线,即该机器每次能够读取32位的数据,也就是4byte的数据。例如将以下代码在该机器下进行读取:
若内存对齐的方式进行读取
char _a变量总共读取一次(先读取四个字节,只取到第一个字节的数据即可),int _b总共读取一次。当读取完char _a 之后接着读取int _b 的时候恰好读取四个字节的内容,而这四个字节的内容恰好是int _b 的内容,刚好读取完成!
若不用内存对齐的方式进行存储:
char _a 读取一次,而 int _b 却要读取两次,读取一两个效率没啥影响,但是每种程序中读取的数据可能成千上万,效率就会有所降低!
总结:
不能进行任意对齐,也不可不进行对齐,因为可能会降低效率!
3、什么是大小端?如何测试一台机器是大端还是小端?
解答:
大小端是数据的两种存储方式,因为市面上电脑硬件的制造不同,不同的硬件对于数据的处理方式不同,常用的市面上的硬件机器存储方式大致有两种:大端存储和小端存储!
概念:
大端存储:数据的低权值位放到高地址处,高权值位放到低地址处,是大端存储
小端存储:数据的低权值位放到内存的低地址处,高权值位放到高地址处,是小端存储
测试一台机器是大端还是小端:
利用联合体的特性,可以得出机器是大端还是小端。因为联合体共用同一块空间,所以我们给两个成员 一个char 和一个int 给int成员数字1,随后通过char成员去读取数据,访问成员的时候是从低地址到高地址开始访问的!若取到的char结果是1,是小端存储。若是0则是大端存储!
代码:
#include <iostream> using namespace std; union S { char a; int b; }; int main() { S s1; //给整型变量赋值1 s1.b = 1; //通过char进行访问 cout << (int)s1.a << endl; return 0; }
结论:
通过联合体共用同一块空间的特性来判断大小端!
博主当前的机器是小端机器!
this指针
this指针的引出
我们首先来定义一个日期类:
#include <iostream> using namespace std; //定义一个日期类 class Date { public: //初始化 void Init(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //打印函数 void Print() { cout << _year << " - "; cout << _month << " - "; cout << _day << endl; } private: int _year; int _month; int _day; }; int main() { //类的实例化对象 Date d1; Date d2; //调用Init方法 d1.Init(2023, 4, 30); d2.Init(2023, 5, 1); //打印 d1.Print(); d2.Print(); return 0; }
问题:
Date类中有Init 和 Print 两种方法(成员函数)。而函数体中没有关于不同对象的区分。那当d1调用Init函数时,该函数是如何知道应该设置d1对象的值,而不是设置d2对象的呢?
解答:
在C++中引入this指针来解决该问题。
C++编译器给每个非静态的成员函数,增加了一个隐藏的指针参数,让该指针指向当前对象(调用该函数的对象)在函数体中所有对成员变量的操作都是通过该指针去访问!且参数是编译器自动传递的,不需要用户来传递!而该参数指针起名为this
this指针的特性
1、this指针类型:
this指针的类型就是类的类型加*号并用const修饰指针,比如Date类 它的this指针就是:Date * const this
2、使用范围:
this指针只能在成员函数的内部使用,因为出了函数作用域形参就被销毁了!在成员函数内部可以通过显式的书写this指针进行访问成员变量。也可以默认用隐式的访问。而this指针本身是不能在函数内部修改的因为是用const修饰的!
3、存储本质:
this指针本质上是函数的形参,只不过是隐式传递的参数(编译器自动传递)当对象调用成员函数时,将对象的地址隐式的传给this形参。所以this指针不在对象中,而是存储在栈区中!
4、传递方式:
this指针是成员函数,第一个隐含的指针形参,一般情况下都是由编译器自动传递的(编译器通过ecx寄存器自动传递),不需要用户自己传(用户自己显式传递会报错)!
代码:
#include <iostream> using namespace std; //定义一个日期类 class Date { public: //初始化 void Init(int year = 1, int month = 1, int day = 1) { //编译器会自动将对象的地址传进来 用this指针形参接收 //可在函数内部显式的写this指针进行访问 this->_year = year; this->_month = month; this->_day = day; } //打印函数 void Print() { //切记this指针是不能被修改的因为是用const修饰的参数 // this = nullptr; 会报错error:不可修改的左值 //编译器会自动将对象的地址传进来 用this指针形参接收 //可在函数内部显式的写this指针进行访问 cout << this->_year << " - "; cout << this->_month << " - "; cout << this->_day << endl; } private: int _year; int _month; int _day; }; int main() { //类的实例化对象 Date d1; //调用Init方法, //调用的时候编译器会自动的将对象的地址传进去 // 且用this指针接收地址 d1.Init(2023, 4, 30); //打印 //调用的时候编译器会自动的将对象的地址传进去 // 且用this指针接收地址 d1.Print(); //但我们不能显式的去传递对象的地址 //会报错 error:函数调用中的参数太多 //因为编译器已经默认传了对象的地址了 //我们在传递就会出现错误 //d1.Print(&d1); return 0; }
图片:
总结:
对象在调用成员函数的时候,编译器会自动将对象的地址传给this指针,用户不可在进行显式传对象地址。在成员函数内部可以显式的通过this指针去访问成员变量,this指针默认是用const修饰的不可在成员函数内部修改this指针的值。this指针本质是一个形参,是存储在栈区上的!
面试题
1、this指针是存在哪里?
解答:
this指针本质是成员函数的形参,只是被编译器隐式的传递操作了。因为是函数的形参,是存在栈区上的,不存在对象中!
2、this指针可以为空嘛?
解答:
this指针本质是用const修饰的,所以我们不能在成员函数内部将this指针置为空。而对象实例化之后必然是有地址的,空对象也是占用1个字节的空间。但是外部给this指针传一个空指针进来 this指针是可能为空的,但为空之后this指针是没有啥意义的,空指针是不能进行访问成员的,很危险!即:this指针可能为nullptr
3、下面程序运行的结果是?
1、第一个:
#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; }
A、编译报错
B、运行崩溃
C、正常运行
解答:C
上面定义了一个A类 的指针p并给其赋予空值,通过指针去调用成员函数Print,此时在传的时候给this指针形参传过去的是p指针的值 也就是nullptr,而在成员函数内部并没有通过this指针去访问成员(进行解引用操作),this指针啥都没做,也没有用到this指针。即程序是可以正常运行的!
2、第二个
#include <iostream> using namespace std; class A { public: void PrintA() { cout << _a << endl; } private: int _a; }; int main() { A* p = nullptr; p->PrintA(); return 0; }
A、编译报错
B、运行崩溃
C、正常运行
解答:B
上面定义了一个A类 的指针p并给其赋予空值,通过指针去调用成员函数Print,此时在传的时候给this指针形参传过去的是p指针的值 也就是nullptr,而在成员函数内部通过this指针去访问成员_a(进行解引用操作),而此时的this指针是一个nullptr,对空指针解引用是不可行的。上述语法是没有问题的,编译时候是不会报错的,但访问是有问题的。即程序会出现运行崩溃!
C语言和C++实现Stack(栈)的对比
C语言实现
//C语言实现栈 #include <stdio.h> #include <stdlib.h> #include <assert.h> #include <stdbool.h> //定义结构体 typedef int DateType; typedef struct Stack { DateType* a; int size; int capacity; }Stack; //初始化 void StackInit(Stack* ps) { ps->a = (DateType*)malloc(sizeof(DateType) * 4); if (NULL == ps->a) { perror("malloc fail"); return; } ps->capacity = 4; ps->size = 0; } //增容 void CheckCapacity(Stack* ps) { if (ps->size == ps->capacity) { DateType* tmp = (DateType*)realloc(ps->a, sizeof(DateType) * ps->capacity * 2); if (NULL == tmp) { perror("realloc tail "); return; } ps->a = tmp; ps->capacity *= 2; } } //入栈 void StackPush(Stack* ps, DateType x) { assert(ps); //是否要增容 CheckCapacity(ps); ps->a[ps->size] = x; ps->size++; } //判断栈是否为空 bool StackEmpty(Stack* ps) { assert(ps); return ps->size == 0; } //出栈 void StackPop(Stack* ps) { //栈为空就不能在出栈了 if (StackEmpty(ps)) { printf("Stack is NULL "); return; } ps->size--; } //取到栈顶元素 DateType StackTop(Stack* ps) { assert(ps); assert(!StackEmpty(ps)); return ps->a[ps->size - 1]; } //栈元素个数 int StackSize(Stack* ps) { return ps->size; } //销毁 void StackDestroy(Stack* ps) { free(ps->a); ps->a = NULL; ps->capacity = 0; ps->size = 0; } int main() { Stack s; StackInit(&s); StackPush(&s, 1); StackPush(&s, 2); StackPush(&s, 3); StackPush(&s, 4); printf("%d ", StackTop(&s)); printf("%d ", StackSize(&s)); StackPop(&s); StackPop(&s); printf("%d ", StackTop(&s)); printf("%d ", StackSize(&s)); StackDestroy(&s); return 0; }
用C语言实现时,Stack相关操作函数有以下共性:
1、每个函数的第一个参数都是Stack*
2、函数中必须要对第一个参数检测,因为该参数可能会为NULL
3、函数中都是通过Stack*参数操作栈的 调用时必须传递Stack结构体变量的地址
结构体中只能定义存放数据的结构,操作数据的方法不能放在结构体中,即数据和操作数据 的方式是分离开的,而且实现上相当复杂一点,涉及到大量指针操作,稍不注意可能就会出 错。
C++实现
//C++实现栈 #include <iostream> using namespace std; typedef int DateType; //定义类(栈) class Stack { public: //初始化 void Init() { _a = (DateType*)malloc(sizeof(DateType) * 4); if (_a == nullptr) { perror("malloc fail "); return; } _capacity = 4; _size = 0; } //入栈 void Push(DateType x) { //是否增容 CheckCapacity(); _a[_size] = x; _size++; } //出栈 void Pop() { if (Empty()) return; _size--; } //判空 bool Empty() { return 0 == _size; } //元素个数 int Size() { return _size; } //栈顶元素 DateType Top() { return _a[_size - 1]; } //销毁 void Destroy() { if (_a != nullptr) { free(_a); _a = nullptr; _size = 0; _capacity = 0; } } //增容 void CheckCapacity() { if (_size == _capacity) { DateType* tmp = (DateType*)realloc(_a, sizeof(DateType) * _capacity * 2); if (nullptr == tmp) { perror("realloc fail"); return; } _a = tmp; _capacity *= 2; } } private: DateType* _a; int _size; int _capacity; }; int main() { Stack s; s.Init(); s.Push(1); s.Push(2); s.Push(3); s.Push(4); printf("%d ", s.Top()); printf("%d ", s.Size()); s.Pop(); s.Pop(); printf("%d ", s.Top()); printf("%d ", s.Size()); s.Destroy(); return 0; }
C++实现栈较为C语言好:
C++中通过类可以将数据 以及 操作数据的方法进行完美结合,通过访问权限可以控制那些方法在 类外可以被调用,即封装,在使用时就像使用自己的成员一样,更符合人类对一件事物的认知。 而且每个方法不需要传递Stack*的参数了,编译器编译之后该参数会自动还原,即C++中 Stack * 参数是编译器维护的,C语言中需要用户自己维护。