目录
C++面向对象的三大特性:封装、继承、多态
C++认为万事万物皆可为对象,对象有其属性和行为
具有相同性质的对象,可以抽象为类
封装
类在设计时,可以把属性和行为放在不同的权限下,加以控制
- public:公共权限,类内可访问、类外可访问,
- protected:保护权限,类内可访问、类外不可访问,儿子可以访问父亲的保护内容
- private:私有权限,类内可访问、类外不可访问,儿子不可以访问父亲的私有内容
struct和class的区别在于
- struct的默认权限是public
- class的默认权限是private
成员属性一般设置为私有,好处是
- 可以自己控制读写权限
- 对于写,可以检测数据的有效性
构造函数和析构函数
- 构造函数:主要用于创建对象时为成员属性赋值,由编译器自动调用
- 析构函数:主要用于对象销毁前的自动调用,执行一些清理工作
构造函数:类名(参数) {}
- 没有返回值,也不写void
- 函数名称与类名相同
- 构造函数可以有参数,可以重载
- 程序在创建对象的时候会自动调用构造函数,而且只会调用一次
析构函数:~类名() {}
- 没有返回值,也不写void
- 在构造函数名字前面加上“~”
- 不可以有参数,不会有重载的情况
- 在对象销毁前自动调用,而且只会调用一次
构造函数的分类与调用
- 按参数分为:有参构造、无参构造
- 按类型分为:普通构造、拷贝构造(传入对象)
- 三种调用方法:括号法、显示法、隐式转换法
括号法:
- Person p1; //默认构造函数调用
- Person p2(10); //有参构造函数
- Person p3(p2); //拷贝构造函数
- 调用默认构造函数时,不要加(),若添加会被编译器认为是一个函数的声明
显示法:
- Person p1; //默认构造函数调用
- Person p2 = Person(10); //有参构造函数
- Person p3 = Person(p2); //拷贝构造函数
- 形如Person(10)的对象,称为匿名对象,当前行执行后系统会自动回收
隐式转换法:
- Person p4 = 10; //有参构造函数
- Person p5 = p4; //拷贝构造函数
拷贝构造函数的调用时机通常有:
- 使用一个已经创建好的对象来初始化一个新对象
- 值传递的方式给函数参数传值(拿到传入的参数会进行拷贝)
- 以值方式返回局部对象(局部函数return出来的其实是拷贝数据)
深拷贝与浅拷贝
- 浅拷贝:简单的赋值拷贝操作
- 深拷贝:在堆区重新申请空间,进行拷贝操作
- 如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区的问题
类对象作为类成员
- C++类中的成员可以是另一个类的对象,我们称该成员为对象成员
- 当其他类对象作为本类成员,构造时先构造类对象,再构造自身
- 析构时先析构自身,再析构类对象
静态成员
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员
静态成员分为
静态成员变量
- 所有对象共享一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
- 可以直接通过类名访问
静态成员函数
- 所有对象共享一个函数
- 静态成员函数只能访问静态成员变量
- 可以直接通过类名访问
- 可以访问静态成员变量,不可以访问非静态成员变量
成员变量和成员函数是分开存储的
- 空对象占用空间为:1
- 只有非静态成员变量属于类的对象上
- 其他成员不属于类的对象上
非静态成员函数如何区分哪个对象调用自己?通过this指针解决
this指针指向被调用的成员函数所属的对象,常见用途:
- 当形参和成员变量同名,可用this指针来区分
- 在类的非静态成员函数中返回对象本身,可使用return *this
const修饰成员函数
常函数
- 成员函数前加上const后,我们称其为常函数
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable,在常函数中依然可以修改
常对象
- 声明对象前加上const,我们称其为常对象
- 常对象只能调用常函数
友元
- 友元的目的是让一个函数或者类访问另一个类中的私有成员
- 友元的关键字为friend
友元函数的三种实现
- 全局函数做友元
- 类做友元
- 成员函数做友元
运算符重载
- 定义:对已有运算符重新定义,赋予另一种功能,以适应不同的数据类型
- 成员函数重载,本质是p1.operator(p2)
- 全局函数重载,本质是operator(p1,p2)
- 左移运算符<< 只能利用全局运算符重载,配合友元可以实现输出自定义数据类型
ostream & operator<<(ostream& out,Person p){
out << "a:" << p.m_a << "b:" << p.m_b ;
return out;
}
- 自增运算符++ 分为前置和后置,重载实现如下
//重载前置++
MyInteger& operator++() {
m_num++;
return *this;
}
//重载后置++
//int是占位参数,用于区分前置后置
MyInteger& operator++(int) {
MyInteger temp = *this;
m_num++;
return temp;
}
- 赋值运算符= 重载实现
Person& operator=(Person &p){
//编译器提供的是浅拷贝,在析构时会出现问题
//应该先判断是否有属性在堆区,如果有先释放再深拷贝
if(m_Age != NULL){
delete m_Age;
m_Age = NULL;
}
//深拷贝
m_Age = new int(*p.m_Age);
//返回对象本身
return *this;
}
- 关系运算符重载
- 函数调用运算符重载(仿函数,可以实现匿名对象调用)
继承
- 好处:减少重复代码
- 语法:class 子类 : 继承方式 父类
- 子类也称派生类,父类也称基类
成员属性的继承
- 父类中所有非静态成员属性都会被子类继承下去
- 父类中私有成员属性是被编译器给隐藏了,因此访问不到,但确实是继承下去了的
构造与析构顺序
- 先构造父亲,后构造儿子;析构相反
同名成员处理
- 如果想访问父类中被隐藏的同名成员函数,需要加作用域 Base::
多继承语法
- 语法:class 子类: 继承方式 父类1 , 继承方式 父类2
- 当父类中出现同名成员,需要加作用域区分
菱形继承
- 两个派生类继承同一个基类,又有某个类同时继承两个派生类
- 二义性、数据冗余、使用不便
虚继承 virtual
- 采用虚继承可以解决菱形继承问题
- 虚继承后,继承类只会存储vbptr,意为虚基类指针,指向的是虚基类表
- 虚基类表中存储了被继承类的地址偏移量
- 通过这一偏移量,可以拿到被继承类中的数据
多态
- 静态多态:函数重载 和 运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
两种多态的区别
- 静态多态的函数地址早绑定——编译阶段确定函数地址
- 动态多态的函数地址晚绑定——运行阶段确定函数地址
动态多态满足条件
- 有继承关系
- 子类重写父类的虚函数
动态多态的使用
- 父类指针或引用指向子类对象
动态多态的原理
多态的好处
- 组织结构清晰
- 可读性强
- 利于扩展和维护
纯虚函数
- 在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
- 因此可以将虚函数改为纯虚函数:virtual 返回值类型 函数名 (参数列表) = 0;
- 当类中有了纯虚函数,该类也被称为抽象类
抽象类
- 抽象类无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也是抽象类
虚析构
- 可以解决父类指针释放子类对象时不干净的问题
- 如果子类中没有堆区数据(new),可以不写虚析构
纯虚析构
- virtual ~类名() = 0;
- 需要在类外实现
- 有纯虚析构的函数也是抽象类,无法实例化对象