【笔记】C++的类和对象
前言
C++的类和对象学习笔记
定义类
1 | class 类名 |
创建对象
1 | 类名 对象名; |
获取对象的属性
1 | 对象名.属性名; |
为对象的属性赋值
1 | 对象名.属性名 = 值; |
调用对象的行为
1 | 对象名.方法名(参数列表); |
权限修饰符
- 权限修饰符可以加在属性前或方法前
- 权限修饰符定义一次后,权限修饰符之后的所有属性和函数都被修饰
关键字 | 权限 | 类内是否可以访问成员 | 类外是否可以访问成员变量 | 是否可以被继承 |
---|---|---|---|---|
public | 公共权限 | ✓ | ✓ | ✓ |
protected | 保护权限 | ✓ | × | ✓ |
private | 私有权限 | ✓ | × | × |
1 | class 类名 |
struct和class
- struct和class的默认访问权限不同
- struct的默认访问权限是public公共权限
- class的默认访问权限是private私有权限
封装
- 将成员属性私有化,成员函数公有化
1 | class 类名 |
对象的初始化和清理
- 在类中定义构造函数用来对象的初始化
- 在类中定义析构函数用来对象的清理
- 如果定义类的时候没有定义构造函数和析构函数,那么编译器会提供空的构造方法和析构方法
构造函数
定义构造函数
- 在创建对象时自动执行构造方法
- 构造函数没有返回值也不写void
- 构造函数的方法名与类名相同
- 构造函数可以有参数列表,所以可以有重载方法
定义无参构造
1 | class 类名 |
定义有参构造
1 | class 类名 |
定义拷贝构造
- 定义有惨构造,参数是本类的对象
1 | class 类名 |
调用构造函数
由于创建对象时,会自动调用构造函数,调用构造函数的操作实际上是创建对象的操作
注意事项
- 括号法不要加空括号,会被误认为是函数的声明
- 匿名对象在当前行执行结束后,系统会立即回收。不能用拷贝函数初始化匿名对象,因为编译器会把
类名(对象名)
转换成类名 对象名
1 | /* 调用无参构造(默认构造) */ |
析构函数
- 在对象销毁前自动执行析构方法
- 析构函数没有返回值也不写void
- 析构函数的方法名在类名前加
~
- 析构函数没有参数列表,所以没有重载方法
1 | class 类名 |
拷贝构造函数的调用时机
1 | // 定义一个有拷贝构造函数的类 |
默认构造函数
- 只要定义一个类,就默认存在的函数:
- 默认构造函数,无参且函数体为空
- 默认析构函数,函数体为空
- 默认拷贝构造函数,函数体中对属性值进行拷贝
1 | class 类名 |
- 如果自定义了有参构造函数,C++不会提供无参构造函数,但是会提供拷贝构造函数
- 如果自定义了拷贝构造函数,C++不会提供无参构造函数和其他构造函数
深拷贝与浅拷贝
- 如果拷贝的属性没有指针,那么使用浅拷贝即可
- 如果拷贝的属性有指针,那么需要手动定义深拷贝来使用
浅拷贝
- 编译器默认提供的拷贝构造函数是浅拷贝,浅拷贝的实质是把成员属性的值拷贝一份
- 当拷贝的成员属性是指针的时候,浅拷贝只会拷贝一份指针数据,不会在堆内存中创建新的空间,所带来的问题是,释放指针时会出现堆区内存重复释放的错误
1 | class Persion |
深拷贝
- 深拷贝需要自定义一个新的拷贝函数,深拷贝会在堆区开辟一块新的空间
- 当拷贝的成员属性是指针的时候,深拷贝会堆区开辟一块新的空间,所以存放的是一个新的指针
1 | class Persion |
初始化列表
- 初始化列表是另一种为成员属性赋初值的方式
1 | class Persion |
类对象作为类成员
- 将一个类的对象作为另一个类的成员属性
1 | class Son |
静态成员
- 在成员属性和成员函数前添加
static
关键字,就构成了静态成员 - 静态成员变量也受访问权限的制约
静态成员变量
- 所有对象共享一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
1 | class 类名 |
静态成员变量的访问
通过对象访问
1 | 类名 对象名; |
通过类名访问
1 | 类名.属性名; |
静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量,非静态成员变量无法访问
1 | class 类名 |
静态成员函数的访问
通过对象访问
1 | 类名 对象名; |
通过类名访问
1 | 类名::函数名(); |
this指针
- this指针是隐含每一个非静态成员函数内的一种指针,this指针不需要定义可以直接使用
- 当形参和成员变量同名时,可用this指针来区分
- 在类的非静态函数中返回对象本身,可用
*this
1 | class Persion |
常函数与常对象
常函数
- 声明函数时添加
const
关键字 - 常函数内不可修改成员属性(相当于this指针改为了常量)
- 常函数内只能调用常函数
1 | 返回值类型 函数名 const |
常对象
- 声明对象时添加
const
关键字 - 常对象只能调用常函数
1 | const 数据类型 对象名; |
常函数和常对象都能修改的成员属性
- 如果需要修改成员属性,可以在成员属性声明时添加
mutable
关键字,使得常含数内的成员属性可以被修改
1 | mutable 数据类型 成员属性; |
类外定义成员函数
定义普通函数
1 | class Persion |
定义构造函数
- 在类内声明成员函数,类外定义成员函数
1 | class Persion |
友元
- 友元的目的是让一个函数或者类访问另一个类的私有成员
成员函数做友元
- 通过
friend
关键字在类中定义其他函数作为友元
1 | class 类名 |
类做友元
- 通过
friend
关键字在类中定义其他类作为友元
1 | class 类名 |
成员函数做友元
- 通过
friend
关键字在类中定义其他类中的函数作为友元
1 | class 类名 |
运算符重载
算数运算符重载
- 此处以加法的运算符重载为例
- 为了能实现连续运算,返回值类型为类对象
+
:指定被重载的运算符,这里重载的运算符是加法
通过成员函数
1 | class Persion |
通过全局函数
- 直接在运算符重载函数的参数列表制定两个类型的参数,可以是不同类型的数据(例如自定义数据类型与int数据类型)
- 内置的数据类型是不可以改变的(例如两个int类型数据相加)
1 | class Persion |
左移运算符重载
- 可以实现输出任意自定义数据类型的数据
- 左移运算符重载的函数返回值为空,函数体内拼接需要输出的内容
通过全局函数
- 因为封装的时候通常将成员变量私有化,所以为了能让左移运算符重载函数可以访问私有成员变量,通常在类内定义友元
- 为了能实现连续左移,返回值类型为ostream类型
1 | class Persion |
递增运算符重载
- 前置递增
- 为了能实现递增的嵌套,返回值类型需要设置为引用类型
- 后置递增
- 返回值类型需要设置为值
- 为了能实现递增的嵌套,后置递增运算符重载函数的参数需要用int作为占位参数,且只能是int,用于区分前置运算符
1 | class Persion |
赋值运算符重载
- 默认的赋值运算符是浅拷贝数据,重载赋值运算符
- 为了能实现连等,返回值类型为类对象的引用
1 | class Persion |
关系运算符重载
1 | class Persion |
函数调用运算符重载
- 函数调用运算符(
()
)也可以重载,由于重载后使用的方式非常像函数的调用,因此称为仿函数,仿函数没有固定写法,非常灵活
1 | class Persion |
继承
- 将重复的代码定义为父类(基类),子类(派生类)继承父类,减少重复的代码
- 父类中所有非静态的成员属性都会被子类继承
- 父类中的私有成员属性无法被子类访问,但是确实被继承了,只是被编译器所隐藏了
权限修饰符
public
:公共继承pritected
:保护继承private
:私有继承
1 | class Father |
继承的分类
公共继承
- 公共继承的子类,无法继承父类的private成员属性
- 公共继承的子类,继承到的public成员属性还是public权限
- 公共继承的子类,继承到的protected成员属性还是protected权限
1 | class Father |
保护继承
- 保护继承的子类,无法继承父类的private成员属性
- 保护继承的子类,继承到的public成员属性和protected成员属性都变为protected权限
1 | class Father |
私有继承
- 保护继承的子类,无法继承父类的private成员属性
- 保护继承的子类,继承到的public成员属性和protected成员属性都变为private权限
1 | class Father |
报告单个类布局
利用Visual Studio开发人员命令提示符报告单个类布局
打开Visual Studio开发人员命令提示符
<class>
:类名<file>
:C++源码文件
1 | cl /d1 reportSimgleClassLayout<class> <file> |
访问父子类同名属性和函数
- 子类对象可以直接访问到之类中同名成员
- 子类对象加作用域可以访问到父类同名成员
- 子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类中所有同名成员函数(无论是否是重载的成员函数),如果想访问到父类中被隐藏的同名成员函数,需要强制加作用域
非静态成员属性和函数
1 | class Father |
静态成员属性和函数
1 | class Father |
多继承
1 | class Father |
菱形继承
- 菱形继承,又称钻石继承
- 两个子类继承同一个父类,一个孙子类又继承这两个子类
1 | class GrandFather |
虚继承
- 为了解决菱形继承遇到的问题,引出了虚继承
- 菱形继承遇到的问题:孙子类中包含了重复的同名成员属性,出自于两个父类,这两个父类由于继承自相同的爷爷类,所以同名成员属性传递给孙子类的时候,造成了资源的浪费
- 虚继承采用virtual关键字修饰基类,这种基类成为虚基类
- 虚继承能解决资源的浪费,采用了虚继承,继承时重复的同名成员属性会变成共享的成员属性
1 | class GrandFather |
多肽
多肽的分类
静态多肽:函数重载和运算符重载都属于静态多肽
动态多肽:派生类和虚函数实现运行时多肽
静态多肽和动态多肽的区别:静态多肽的函数地址在编译阶段确定;动态多台的函数地址在运行阶段确定
虚函数
父类引用(指针)指向子类对象
利用virtual关键字修饰函数,被称之为虚函数,虚函数可以实现多肽
动态多肽的条件
- 有继承关系
- 子类需要重写父类的虚函数
1 | class Father |
纯虚函数和抽象类
- 通常父类中虚函数的实现是无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数
- 包含纯虚函数的类,就是抽象类,抽象类无法实例化对象,继承抽象类的子类必须重写纯虚函数,如果不重写纯虚函数,那么子类也是抽象类
1 | class Father |
虚析构函数
- 如果子类中有属性开辟堆区,父类指针无法在释放时调用到子类的析构函数,如果想通过父类调用子类虚构函数,需要采用虚析构函数
- 虚析构函数与普通的析构函数不同,父类的虚析构函数在被子类调用时,父类的虚析构函数和子类的析构函数都会被调用
1 | class Father |
纯虚析构函数
- 如果只是用父类释放子类堆中的属性,那么父类的虚析构不需要函数体,此时可以使用纯虚析构函数
- 虽然父类析构函数有时只是释放子类堆中的属性,不需要函数体,但是父类的虚析构函数仍然有可能释放父类堆中的属性,所以仍然要定义父类析构函数的函数体
1 | class Father |