类和对象
1. 类和对象的基本概念
在 C++ 中, 类是一种由用户定义的数据类型,用于封装数据和方法。
类定义了对象的属性和行为,对象是类的实例,它可以使用类定义的属性和方法。通过类和对象,可以实现面向对象编程的概念,包括如下三大特性。
- 封装
- 继承
- 多态
面向对象则是类的实例化,通过类和对象,可以实现数据的抽象和封装,提高代码的可维护性和可重用性。
在对象上有其属性和行为。
例如 人就可作为对象,其拥有的属性为姓名、年龄、身高等,其拥有的行为有:走、跑、跳等,具有相同性质的对象即可抽象成为一个类。
2. 封装
2.1 封装的意义
封装将属性和行为作为一个整体,表现一个事物,并将属性和行为加以权限控制。在设计类的时候,属性和行为写在一起,表现事物。
2.2 访问权限
类在设计时,可以把属性和行为放在不同的权限下,加以控制。
类的访问权限有三种
public
— [公共权限]protected
— [保护权限]private
— [私有权限]
其中不同类型的权限可访问的范围也不同:
权限类型 | 类内 | 类外 |
---|---|---|
public |
可以访问 | 可以访问 |
protected |
可以访问 | 不可访问 |
private |
可以访问 | 不可访问 |
protected
和 private
虽然类内类外访问权限相同,但是涉及到继承时存在不同;protected
作为父类被子类继承的时候子类可以访问父类中 protected
的属性、行为。而private不同的是子类无法访问父类中private的属性、行为。
示例 | |
---|---|
2.3 class
和struct
的区别
C++ 中 class
和 struct
都可以定义自定义的数据类型,但是他们之间有一定的区别。
2.3.1 默认访问权限
在 class
中,默认的成员访问权限是 private
,而在 struct
中默认的成员访问权限是 public
。
2.3.2 使用习惯
一般来说 class
更适合具有复杂行为和数据封装的情况,而 struct
更适合用于简单的数据集合。
2.3.3 继承
当使用 class
时,派生类默认继承的访问权限是 private
而当使用 struct
时,默认继承的访问权限是 public
。
示例 | |
---|---|
2.4 成员属性设置为私有
设置成员属性为私有,可以由自己控制读写权限,对于读写权限我们可以检测数据的有效性。
通过提供修改私有数据的接口,可以防止用户直接操作类内的成员,并且通过使用提供的数据修改方法,可以方便检测数据的合法性、有效性等。
2.5 构造函数与析构函数
生活中购买使用的电子产品通常具有出场设置, 并且在某一天我们不再使用时也会删除一些信息以确保安全。
在 C++ 中对象的初始化和清理也是两个非常重要的安全问题, 若一个对象或者变量没有初始状态, 则对其使用的后果未知; 同样如果使用完一个对象或变量后不及时进行清理, 也会造成一定的安全问题。
C++ 中使用构造函数和析构函数解决上述问题, 这两个函数会被编译器自动调用, 完成对象初始化和清理工作。
此外对象的初始化和清理工作时编译器强制要求的事情, 如果我们不提供构造函数和析构函数, 编译器会提供构造函数和析构函数是空实现。
- 构造函数: 在创建对象时为对象的成员进行赋值, 由编译器自动调用, 无需手动调用。
- 析构函数: 在对象销毁前自动调用, 执行一些清理工作。
2.5.1 构造函数
语法
- 构造函数无返回值, 也不写
void
- 函数名称与类名相同
- 构造函数可以有参数, 因此可以发生重载
- 程序在调用对象时会自动执行构造函数, 无需手动调用并且仅执行一次
2.5.2 析构函数
语法
- 析构函数, 没有返回值, 也不写
void
- 函数名称与类名相同, 在名称前加 ~
- 析构函数不可以有参数, 因此不可以发生重载
- 程序在对象销毁前会自动调用析构, 无需手动调用并且仅调用一次
示例
2.6 构造函数的分类及调用
构造函数根据分类有两种分类:
- 按参数分类:有参构造和无参构造
- 按类型分类:普通构造和拷贝构造
构造函数有三种调用方式:
- 括号法
- 显示法
- 隐式转换法
2.6.1 有参构造和无参构造
二者构造方式不同的是,有参构造函数在调用时需要传入参数而无参构造不需要。
如果用户在编写类的时候不写构造函数,则编译器会默认生成一个无参构造函数,因此无参构造函数也称默认构造函数。
2.6.2 拷贝构造
拷贝构造函数可以将传入的对象的属性拷贝至当前对象中。
其中对于传入的对象:
- 需要拷贝的对象需是
const
静态的 - 通过地址传入需要的对象
2.6.3 括号法调用构造函数
在实例化对象时,默认调用的就是无参构造函数。
在对象名后通过括号填写参数,即为括号法调用有参构造函数。
2.6.4 显示法调用构造函数
其中 Person(/*age*/ 10)
是一个匿名函数,其在执行完毕后便会马上被回收掉。
拷贝构造函数不能用于初始化匿名对象,编译器会认为
Person (p3)
==Person p3
2.6.5 隐式转换法调用构造函数
这个写法等价于下面的代码
2.7 拷贝构造函数调用时机
C++ 中拷贝构造函数的调用时机通常来说有三种情况:
- 使用一个已经创建完毕的对象来初始化一个新的对象。
person 类 p_1 使用了有参构造函数实例化了一个对象,此时将对象 p_1 作为实例化 p_2 对象的参数,此时就调用了 person 类中的拷贝构造函数。
- 值传递的方式给函数参数传值。
这里将对象 p 作为了函数 do_work 的形参传入,此时便调用了拷贝构造函数创建了一份副本传入函数 do_work。
- 以值的方式返回局部对象。
在函数 do_work 中创建了一个临时对象 p_temp 在函数执行完毕后返回局部对象的时候,调用了拷贝构造函数并将其返回作为函数的返回值,原对象执行完毕后便销毁。
2.8 构造函数的调用规则
默认情况下,C++ 编译器至少会为一个类添加三个函数:
- 默认构造函数 (无参构造,函数体为空)
- 默认析构函数 (无参构造,函数体为空)
- 默认拷贝构造函数,对属性值进行拷贝
构造函数的调用规则如下:
- 如果用户定义有参构造函数,C++ 不再默认提供无参构造,但是会默认提供拷贝构造。
- 如果用于定义拷贝构造函数,C++ 不再提供其它构造函数。
2.9 深拷贝与浅拷贝
- 深拷贝:在堆区重新申请内存空间,进行拷贝操作。
- 浅拷贝:简单的复制拷贝操作。
// 浅拷贝堆区内存重复释放
class person{
public:
person (int t){
this->t = new int(t);
}
~person (){
if (this->t != nullptr){
delete this->t;
t = nullptr;
}
}
int *t;
}
由于堆的特性,p_2 先执行析构函数释放空间,但是到 p_1 执行析构函数释放空间的时候空间已经被释放,此时便是非法操作。
如果类中的属性有在堆区开辟的,一定要自己提供深拷贝方法,避免浅拷贝带来的问题。