C++类和对象超详细
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
👦个人主页zoroxs
本文对C++中的类和对象知识,进行详细的解释,包括,如何定义类,默认成员函数,运算符重载…
没有涉及到继承和多态,之后会详细介绍
本文目录
类和对象初阶
类的定义
在C语言中,只有结构体的概念,没有类,而C++是兼容C语言的,所以在C++中定义类有两种方式
struct classname
{
//成员变量
//成员函数
};
class classname
{
//成员变量
//成员函数
};
这两种定义方式肯定是有区别的,后边再说
定义类一定要注意,{}结尾有分号,其实基本和C语言定义结构体是一样的,只不过里面可以定义函数
我们这样就可以简单写一个类了
万年不变的学生类, 不过使用class定义类这样是无法初始化对象的,往后看
类的访问限定符
- public成员在类外面可以访问
- protected 和 private 只能在类里面访问,类外都不能访问
- protected和private的区别:
- 继承的时候,protected成员可以继承到子类,但是private成员是不可见的,子类无法访问
- 访问权限的作用域从当前访问权限符出现,到下一个访问权限操作符出现为止
- class的默认访问权限符是private(私有), struct的默认访问权限符是public(公有),主要是为了兼容C语言
所以上边使用class写的那个学生类是不可以用的,因为默认是private,所以要添加访问权限控制符
一般都会把成员变量设置成私有的,成员函数设置成公有的,因为封装的特性,要隐藏内部实现细节,对外公开接口
类的作用域
类定义了一个新的作用域类的所有成员都在类的作用域中。在类体外定义成员时需要使用 ::作用域操作符指明成员属于哪个类域。
比如要声明和定义分离:
如果访问类中静态成员也是要这样访问
类的实例化
类的实例化就是创建对象
一个类可以实例化出多个对象,
类本身是没有空间的, 实例化出对象才会分配空间, 类就好像建造图纸一样, 而对象才是房子
我们可以在栈上定义局部对象,也可以在堆上申请,切记要释放哦
类对象大小的计算
我们知道,在C语言中可以使用sizeof来计算对象所占的空间,单位是字节,比如 sizeof(int)的返回值就是4
C++是兼容C语言的,那么如何计算一个对象的大小呢
规则和C语言计算结构体是一样的,那么怎么计算呢,我们来看看,涉及到的问题就是结构体内存对齐
规则有4条:
- 第一个成员与结构体变量偏移量为0的地址处
- 其他成员变量要对齐到 对齐数 的整数倍的地址处
对齐数 : 编译器默认对齐数与该成员大小的较小值 (vs中默认对齐数的值为8)
- 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
- 如果嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数的整数倍
所以sizeof(A)的结果是12
我们再来举一个例子,结构体嵌套结构体的
大家可以自己试着举例子练习一下,关键点在于结构体嵌套的时候需要注意的点
类对象大小的计算规则与这里类似,
虽然类里有成员函数, 但是成员函数是所有对象都调用的,如果每个对象里面都存一份函数,没有必要,非常浪费空间
所以成员函数被放在公共代码段,每个对象中都只存储了成员变量,当然继承后面涉及到虚函数,会有虚表指针
特殊情况
有一个特殊情况
空类的空间占用是多少呢??
只是编译器为了标识这个类的对象,所以赋予了一个字节,
其实空类也不是啥也没有,里面有默认成员函数,往后看就可以
this指针
我们来看一个问题
C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数让该指针指向当前对象(函数运行时调用该函数的对象)在函数体中所有“成员变量”的操作都是通过该指针去访问。只不过所有的操作对用户是透明的即用户不需要来传递编译器自动完成
所以其实我们调用print_date和Init函数本质是这样的
d1.print_date(&d1);
d1.Init(&d1, 2022, 1 ,1);
这个this指针在成员函数中是可以手动调用的
我们来玩玩this指针的特别地方
this指针相关经典题目
大家可以自己寻思寻思
如果这道题变形一下就会简单一点
这个比较简单,肯定会崩溃,因为_a 相当于是this->_a,会对空指针解引用
那这样呢??? 这样也不会奔溃, 因为编译器很智能,还是把p直接传入,并没有解引用
类和对象进阶
六大默认成员函数
类里面什么成员都没有,被称为空类,但是它真的什么都没有嘛?
并不是,编译器会自动生成6个默认成员函数(C++11又新增了2个 移动构造函数和移动赋值运算符重载 ),
编译器自动生成的就叫默认成员函数
下面就对每个默认成员函数进行详细的分析,我们使用这样两个类来分析
一个是栈,都比较熟悉的数据结构
一个是日期,存储年月日
构造函数
构造函数是用来完成初始化的,并不是开辟对象空间的, 这个名字就很容易让大家误解
那构造函数是编译器会自动生成的,那他都会干什么事情呢???
我们什么情况要自己写构造函数呢???
我们写又该怎么写呢???
编译器自动生成的会干什么事情
???这你也不干事啊, 哪有初始化啊,说明编译器帮我们生成的构造函数对int类型不做处理
事实上,编译器生成的构造函数不会对内置类型(int,char,…)处理 ,来试试自定义类型
说明编译器默认生成的构造函数:
- 对内置类型不做处理
- 对自定义类型会调用它的默认构造函数
构造函数特性
一定要切记:构造函数是特殊的成员函数,构造函数的任务是初始化对象,而不是给对象开辟空间
- 构造函数名称与类名相同
- 无返回值(不加void)
- 对象实例化时编译器自动调用对应的构造函数
- 构造函数可以重载
- 如果用户没有定义构造函数,就会默认生成一个无参的默认构造,如果用户定义了,就不再生成
构造函数的基本就这些东西,后边又引入了初始化列表
析构函数
析构函数的功能和构造函数基本是相反的,构造是初始化,析构是清理资源,所以它们的特性也很相似
与构造函数功能相反析构函数不是完成对对象本身的销毁局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数完成对象中资源的清理工作
定义
- 析构函数是在类名前加上~(和构造函数的差别只有一个C语言中取反的运算符)
- 无参数无返回(不写void)
- 一个类中只能有一个析构函数,如果没有显示定义,系统会生成默认的析构函数
- 对象生命周期结束时,编译器会自动调用析构函数
我们什么时候需要写析构函数
析构函数是完成资源清理的,如果此类中有从堆上申请的资源,使用new/malloc这些开辟出来的空间,就需要写析构函数,如果没有申请的资源,编译器自动生成的就可以,对自定义类型去调用它的析构函数,对内置类型不做处理
拷贝构造函数
函数调用的时候传值,是怎么传的呢??
所以这就可以引出拷贝构造,编译器无法进行深拷贝,只能字节序拷贝,所以如果涉及到深拷贝,我们要自己实现拷贝构造
拷贝构造写法
拷贝构造函数只有单个形参该形参是对本类类型对象的引用(一般常用const修饰)在用已存在的类类型对象创建新对象时由编译器自动调用
拷贝构造是构造函数的重载,拷贝构造函数的参数是当前类型的引用
我们不写编译器默认生成的拷贝构造会做什么事??
默认的拷贝构造函数对象按内存存储按字节序完成拷贝这种拷贝叫做浅拷贝或者值拷贝。
编译器默认生成的拷贝构造对自定义类型是调用它的拷贝构造 ,对于内置类型,进行浅拷贝
拷贝构造调用
赋值运算符重载
运算符重载
C++为了增强代码的可读性引入了运算符重载运算符重载是具有特殊函数名的函数也具有其返回值类型函数名字以及参数列表其返回值类型与参数列表与普通的函数类似
这么说吧
运算符重载需要注意的就是需要哪两个操作数,如果不写成成员函数,就要传入对应的参数, 和运算符调用顺序相同,哪个在前,函数参数列表就哪个位置
赋值运算符重载
赋值运算符重载就是 = ,就是两个相同类型的对象赋值
赋值运算符不能实现成全局函数,因为如果我们写了全局函数
编译器默认生成的赋值运算符重载对自定义类型成员会去调用它的赋值运算符重载,对内置类型的成员进行浅拷贝
前置++和后置++运算符重载
这里有一个特殊的规定,如果我们要实现++重载
返回值 operator++();
那如何区分前置后置呢??
C++规定了,无参的是前置++, 如果是后置++, 就要实现成
Date operator++(int);
编译器调用的时候会区分前置后置, 如果后置就会传入一个int型的值
Date类的++运算符重载就是这样
取地址以及const取地址操作符重载
我们知道,对象是无法直接使用运算符的,那么我们想对一个对象取地址,
但是我们发现可以直接用的,这是因为我们不写,类中就默认生成了一个,这个基本上不需要重载
const对象调用
我们知道,对象调用成员函数会传递this指针
比如日期类的对象: 传过去的this指针应该是 Date* const this ,因为this指针不可改,this是可以改的
但是const对象如何调用成员函数呢?? 类型应该是const Date const this ,this指针不可改变,对象本身也不可改变,
所以如果我们不修改对象,就要让const对象也能调用, 但是this指针我们又不能手动写, C++提供了一种新的方式
在函数定义或者声明的括号后面加上const
const对象取地址也是如此 ,但是编译器帮我们自动生成了
这两个一般不需要重载,默认六大成员函数重要的还是前4个,多加练习
初始化列表
我们之前说构造函数是进行初始化的,可以在构造函数给成员变量赋值,但是如果对象中有特殊的成员呢,
那如果对象中有const成员呢,我们试试
因为对象中有const成员, 那该成员在什么时候初始化呢???
其实对象定义的时候,调用构造函数,真正完成初始化的时候是在构造函数的其他地方,并不在函数体中,在初始化列表中
初始化列表以一个冒号开始接着是一个以逗号分隔的数据成员列表每个"成员变量"后面跟一个放在括号中的初始值或表达式。
在这里真正完成初始化,我们来看看初始化列表的特性
1.每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2.类中包含以下成员,必须在初始化列表位置进行初始化
大家可以想想,有哪些成员必须在定义时候初始化
- const成员
- 引用成员变量
- 自定义类型成员(且该类没有默认构造函数时)
const我们前面说了, 引用呢? 引用也要在定义时候初始化,具体引用的详细介绍可以看我的这篇文章C++引用
我们之前说,默认生成的构造函数会对内置类型不处理,对自定义类型要调用它的默认构造函数, 如果没有默认构造函数就会error, 那么我们写的如何调用非默认构造呢? 只能在初始化列表的时候进行
3.尽量使用初始化列表初始化因为不管你是否使用初始化列表对于自定义类型成员变量一定会先使用初始化列表初始化
就是无论你写不写初始化列表,每个构造函数都会走一遍,所有的成员变量都会走
只不过内置类型不处理罢了,所以我们要尽量使用初始化列表进行初始化
4.成员变量在类中声明次序就是其在初始化列表中的初始化顺序与其在初始化列表中的先后次序无关
explicit关键字
构造函数不仅可以初始化对象,_对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数还具有类型转换的作用。 _
static成员
类中的成员也可以是静态的,我们来瞅瞅
声明为static的类成员称为类的静态成员用static修饰的成员变量称之为静态成员变量;用static修饰的成员函数称之为静态成员函数。静态成员变量一定要在类外进行初始化
静态成员的特性
- 静态成员为所有类对象所共享不属于某个具体的对象存放在静态区
- 静态成员变量必须在类外定义定义时不添加static关键字类中只是声明
- 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
- 静态成员函数没有隐藏的this指针不能访问任何非静态成员
- 静态成员也是类的成员受public、protected、private 访问限定符的限制
静态成员可以说是不属于具体的对象,但是对象可以帮助突破类域
需要注意的点就是: 静态成员函数是无法调用非静态成员的, 因为静态成员函数没有this指针的传递
友元
友元是一种很有效的方式,但是并不推荐使用,因为它会破坏封装
比如说
Date d1;
cout << d1 << endl;
要实现这样的代码, 就只能重载 <<
如果实现成Date的成员函数, ostream& operator<<(ostream& _cout)
第一个参数是this, 所以调用的时候就成了 d1 << cout ;
这样特别奇怪,所以一般这个都会重载成全局函数,所以又引发新的问题, 类的成员变量一般是私有的
如果重载成全局函数,无法访问私有/保护成员,所以友元就派上用场了
**友元函数可以直接访问类的私有成员它是定义在类外部的普通函数不属于任何类但需要在
类的内部声明声明时需要加friend关键字
**
这样就可以访问类中私有成员
- 友元函数可访问类的私有和保护成员但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
友元类
两个类之间也可以是朋友关系
- 友元类的所有成员函数都可以是另一个类的友元函数都可以访问另一个类中的非公有成员。
- 友元关系是单向的不具有交换性。
- 友元关系不能传递,如果C是B的友元 B是A的友元则不能说明C时A的友元
友元类中就是所有成员函数都是另一个类的友元函数,
A说B是我的朋友, 可以随意访问, B说,你不是我的朋友 ,单向关系,
现在生活中往往也是如此, 你把人家当朋友,人家不知道把你当啥,人心叵测
内部类
概念如果一个类定义在另一个类的内部这个内部类就叫做内部类。内部类是一个独立的类
它不属于外部类更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越
的访问权限。
注意内部类就是外部类的友元类参见友元类的定义内部类可以通过外部类的对象参数来访
问外部类中的所有成员。但是外部类不是内部类的友元。
特性
- 内部类可以定义在外部类的public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static成员不需要外部类的对象/类名。
- sizeof(外部类)=外部类和内部类没有任何关系。
这个内部类感觉平时也不咋用,了解了解
匿名对象
有时候有些对象不需要有名字,比如
拷贝对象时优化
在传参和传返回值的过程中一般编译器会做一些优化减少对象的拷贝这个在一些场景下还是非常有用的 ,
我们来测试一下
先定义一个A类
我们换个方式接受一下
还有比如这种, 一个表达式,多次构造 +拷贝构造的 都会被优化为1次构造
test(A(2));
总结
好了,类和对象就到这里了,类和对象是C++学习的基础,所以一定要多加练习,感谢收看