C++面向对象的三大特性:封装、继承、多态。
任何物体都有对象,对象有属性和行为功能
1.1 封装 1.1.1 封装的意义封装是C++面向对象的三大特性之一
封装的意义:
将属性和行为作为一个整体,表现为生活中事物
将属性和行为加以权限控制
public 类内可访问,类外也可访问
protected 类内可访问,类外不可访问(子类可访问)
private 类内可访问,类外不可访问(子类不可访问)
一般将成员属性设置为私有权限
优点:可以自己控制读写权限;对于写可以检测数据的有效性
1.1.2 struct和class的区别
struct的默认权限是public
class的默认权限是private
1.1.3 封装案例1、设计立方体类(Cube),求出立方体的面积和体积,分别用全局函数和成员函数判断两个立方体是否相等。
#includeusing namespace std; class Cube { private: int c; int k; int g; public: Cube(int c, int k, int g) { this->c = c; this->k = k; this->g = g; } int minaji() { return ((this->c * this->k) + (this->c * this->g) + (this->g * this->k))* 2; } int tiji() { return this->c * this->g * this->k; } void is_deng(Cube c1 , Cube c2) { if (c1.c = c2.c && c1.g == c2.g && c1.k == c2.k) cout << "两个立方体完全相同" << endl; else cout << "两个立方体不完全相同" << endl; } }; void Is_deng(Cube c1, Cube c2); int main() { Cube c1(10, 10, 10); Cube c2(10, 10, 10); cout << "c1的体积和面积:"< 1.2 对象的初始化和清理 C++中的面向对象来源于生活,每个对象也都有初始化设置及其在对象销毁前的清理数据设置。
1.2.1 构造函数和析构函数对象的初始化和清理也是两个非常重要的安全问题
一个对象或者变量没有初始状态,对其使用后果是未知
同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题
C++利用了构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供编译器提供的构造函数和析构函数是空实现。
**构造函数:**主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
**析构函数:**主要作用在于对象销毁前系统自动调用,执行一 些清理工作。
构造函数语法:类名(){}
1、构造函数,没有返回值
2、函数名称与类名相同
3、构造函数可以有参数,因此可发生重载
4、程序在调用对象时会自动调用构造函数,无须手动调用,且只调用一次
析构函数语法:~类名(){}
1、析构函数,没有返回值
2、函数名称与类名相同,在名称之前加~
3、析构函数不可以有参数,因此不可发生重载
4、程序在对象调用前会自动调用析构函数,无须手动调用,且只调用一次
1.2.2 构造函数的分类与调用两种分类方式:
1、按参数分为:有参构造和无参构造
2、按类型分为:普通构造和拷贝构造
补充:拷贝构造函数语法示例:Person(const Person &p){}
作用:将传入的人身上的所有属性,拷贝到当前对象身上。
(const和引用的作用是为了保护被拷贝对象本身不会被更改)
三种调用方式:
括号法:类名 对象(参数)
显示法:类名 对象 = 类名(参数) 等号后相当于创建了一个匿名对象
隐式转换法:类名 对象 = 参数 相当于类名 对象 = 类名(参数)
1.2.3 拷贝构造函数的调用时机C++中拷贝构造调用时机通常有三种情况:
使用一个已经创建完毕的对象初始化一个新的对象以值传递的方式给函数参数传值以值方式返回局部对象 1.2.4 拷贝构造函数的调用规则
默认情况下,C++编译器至少给一个类添加三个函数
默认构造函数(无参,函数体为空)默认析构函数(无参,函数体为空)默认拷贝构造函数,用于拷贝对象属性值
构造函数调用规则:
如果自定义有有参构造函数,C++不再提供默认无参构造,但会提供默认拷贝构造函数如果自定义拷贝构造函数,C++不再提供其他构造函数 1.2.5 深拷贝与浅拷贝
浅拷贝:简单的赋值拷贝操作(编译器默认提供) 注:会造成堆区的内存重复释放深拷贝:在堆区重新申请空间,进行拷贝操作 可解决浅拷贝带来的问题,重新开辟堆区空间 1.2.6 静态成员
静态成员函数特点:
程序共享一个函数
静态成员函数只能访问静态成员遍历
静态成员函数访问方式:1、对象.函数名() 2、类名::函数名()
1.3 C++对象模型和this指针 1.3.1 成员变量和成员函数分开存储补充:空对象占用的内存空间为:1,为了区分空对象占用内存的位置
非静态成员变量,属于类的对象上
非静态成员函数,不属于类的对象上
静态成员变量或函数,不属于类的对象上
1.3.2 this指针通过上面的内容我们知道在C+ +中成员变量和成员函数是分开存储的
每一个非静态成员函数只会诞生一份函数实例, 也就是说多个同类型的对象会共用一块代码
那么问题是:这- -块代码是如何区分那个对象调用自己的呢?
C++通过提供特殊的对象指针,this指针, 解决主述问题。this指针指向被调用的成员函数所属的对象
this指针是隐含每一个非静态成员函数内的一种指针
this指针不需要定义,直接使用即可
this指针的用途:
当形参和成员变量同名时,可用this指针来区分
在类的非静态成员函数中返回对象本身,可使用return *this
**this指针的本质:**是一个指针常量,指针的指向不可修改
示例:Person * const this
1.3.3 const修饰成员函数常函数:
成员函数后加const后,此函数为常函数常函数内不可以修改成员属性成员属性声明时添加mutable关键字后,在常函数中可修改
常对象:
在声明对象前添加const关键字后,则对象为常对象常对象只能调用常函数 1.4 友元
友元是指在类中虽然有private权限的内容,既不可被别的类访问也不可在类外被访问,但是通过友元的技术,可以进行特权访问。
友元的关键字:friend
友元的三种实现:
全局函数做友元类做友元成员函数做友元 1.4.1 全局函数做友元
在被作为友元类中提前声明,声明语法:friend 返回类型 函数名(参数);
1.4.2 类做友元在被作为友元类中提前声明,声明语法:friend class 类名;
1.4.3 成员函数做友元在被作为友元类中提前声明,声明语法:friend 返回类型 类名::函数名(参数);
1.5 运算符重载 1.5.1 加号运算符重载成员函数重载
#include#include using namespace std; class Person { public: int age; int id; Person operator+(Person& p) { Person temp; temp.age = this->age + p.age; temp.id = this->id + p.id; return temp; } }; int main() { Person p1; p1.age = 10; p1.id = 1; Person p2; p2.age = 10; p2.id = 2; Person p3; p3 = p1 + p2; //等价于p1.operator+(p2) cout << p3.age << endl; cout << p3.id << endl; return 0; } 全局函数重载
#include1.5.2 左移运算符重载#include using namespace std; class Person { public: int age; int id; }; Person operator+(Person& p1, Person&p2) { Person temp; temp.age = p1.age + p2.age; temp.id = p1.id + p2.id; return temp; } int main() { Person p1; p1.age = 10; p1.id = 1; Person p2; p2.age = 10; p2.id = 2; Person p3; p3 = p1 + p2; //等价于p3 = operator+( p1, p2) cout << p3.age << endl; cout << p3.id << endl; return 0; } #include#include using namespace std; class Person { public: int age; int id; ostream& operator<<(ostream& cout) //因为目标所需的格式为cout< age << endl; cout << "id:" << this->id << endl; return cout; } }; ostream& operator<<(ostream &cout, Person&p) //cout类型为ostream,因为只能有一个cout关键字,所以使用引用 { cout << "age:" << p.age << endl; cout << "id:" << p.id << endl; return cout; //这里返回cout是为了可以继续追加<<运算符, 连续执行 } int main() { Person p; p.age = 10; p.id = 1; cout << p << endl; //等价于 operator<<(operator<<(cout, p), endl) return 0; }
其他运算符重载与上述类似在此不再赘述
1.6 继承继承是面向对象的三大特性之一
继承是指大类分出的小类,不仅有自己的特点还具有大类的特点。
1.6.1 继承的基本语法class 子类 : 访问方式 基类
例:class A : public B;
A类称为子类或者派生类
B类称为父类或者基类
继承的作用:可以减少重复的代码
派生类中的成员,包含两大部分:
1、一类是从基类继承过来的,一类是自己增加的成员。
2、从基类继承过过来的表现其共性,而新增的成员体现了其个性。
1.6.2 继承方式一共有三种继承方式:
公共继承 public保护继承 protected私有继承 private
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sx6Aszob-1648379839018)(D:Typora图像image-20210810155855511.png)]
1.6.3 继承中的对象模型子类在继承基类的属性时,会继承所有的属性,而对于private下的属性同样继承只是隐藏起来不显示
利用开发人员命令提示工具查看对象模型 跳转盘符 F: 跳转文件路径 cd 具体路径下 查看命名 cl /d1 reportSingleClassLayout类名 文件名[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mszQXbAc-1648379839019)(D:Typora图像image-20210810162026349.png)]
1.6.4 继承中构造和析构顺序**顺序:**父类构造函数---->子类构造函数---->子类析构函数---->父类析构函数
1.6.5 继承中同名成员的处理方式问题:当子类与父类中出现同名的成员,如何通过子类对象,访问到子类或父类中的同名成员呢?
访问子类同名成员 直接访问即可 (语法:子类.同名成员)
访问父类同名成员 需要添加作用域(语法:子类.父类::同名成员)
注意:如果子类中出现父类同名的成员函数,子类的同名函数会隐藏掉父类中所有同名函数,所有需要添加作用域
1.6.6 继承中同名静态成员的处理方式**问题:**继承中同名的静态成员在子类对象上如何进行访问?
1、通过对象访问
访问子类同名成员 直接访问即可 (语法:子类.同名成员)访问父类同名成员 需要添加作用域(语法:子类.父类::同名成员)
2、通过类名访问
访问子类同名成员 直接访问即可 (语法:子类::同名成员)访问父类同名成员 需要添加作用域(语法:子类::父类::同名成员) 1.6.7 多继承
C++中允许一个类继承多个类
语法:class 子类 : 继承方式 父类1, 继承方式 父类2...
因为使用多继承时,常会导致出现多个同名成员,所以不建议使用多继承
1.6.8 菱形继承菱形继承:指的是两个子类同继承自一个基类,然后又出现了一个子类同时继承于前两个子类
例:马和驴继承于动物类,骡子又继承于马和驴
出现的问题:
当两个父类有相同的成员,子类在访问同名成员时需要加作用域
当出现1的情况时,子类继承了两份相同的成员,造成了资源浪费
解决方法:利用虚继承
语法:class 子类 : virtual 访问方式 父类
未采用虚继承:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FycKUsLE-1648379839019)(D:Typora图像image-20210810174441742.png)]
采用虚继承后:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O9LoMf8a-1648379839020)(D:Typora图像image-20210810174524492.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PRzutNfh-1648379839021)(D:Typora图像image-20210810174740092.png)]
使用了虚继承后SheepTuo没有继承Sheep和Tuo的全部属性,仅继承了两者的vptr(虚指针),而vptr指向vbtable(vbtable中记录了获取SheepTuo继承自Sheep和Tuo类所需偏移的地址值:Sheep需要偏移8、Tuo需要偏移4)
1.7 多态 1.7.1 多态的基本概念多态是C++面向对象三大特性之一
多态分为两类
静态多态:函数重载 和 运算符重载,复用函数名动态多态:派生类和虚函数实现运行时多态
示例:
#include#include using namespace std; class Animal { public: virtual void speak() { cout << "动物在叫" << endl; } }; class Cat : public Animal { public: void speak() { cout << "小猫在叫" << endl; } }; class Dog : public Animal { public: void speak() { cout << "小狗在叫" << endl; } }; class DogSon : public Dog { public: void speak() { cout << "狗儿子在叫" << endl; } }; void test(Animal& animal) //相当于 Animal &animal = cat 注:父类到子类不需要类型转化 { animal.speak(); } int main() { DogSon DS; test(DS); return 0; } 输出结果: 狗儿子在叫 静态多态和动态多态区别:
静态多态的函数地址早绑定 — 编译阶段确定函数地址动态多态的函数地址晚绑定 — 运行阶段确定函数地址
多态的满足条件:
有继承关系
子类重写父类中的虚函数
注:重写和重载的区别:重载是函数的名称相同参数不同;重写是指函数名、返回值、参数都相 同
多态的使用条件:
父类指针或引用指向子类对象 例:Animal * animal = new Cat 或 Animal & animal = Cat cat
多态的优点:
组织结构清晰可读性强对于前期和后期的扩展和维护性高 1.7.2 多态的原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-22bddBTb-1648379839022)(D:Typora图像image-20210811143420506.png)]
当子类重写父类的虚函数时,vftable中的原地址替换为子类中重写后的函数地址
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YQbmituk-1648379839022)(D:Typora图像image-20210811143545296.png)]
在开发人员命令行下显示:
1、若父类同名函数前未声明virtual关键字,内部结构如下图所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oODmUPfw-1648379839023)(D:Typora图像image-20210811143750579.png)]
因为此时类的内部什么都没有,为空类,所以大小为1
2、若父类同名函数前声明virtual关键字,内部结构如下图所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yNNwYVyT-1648379839024)(D:Typora图像image-20210811143918176.png)]
生成了一个vfptr指针,所以大小为4,vfptr指向vftable,存放函数地址
3、当子类没有重写父类中的speak函数时,子类的内部结构如下图所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SwFQgApu-1648379839024)(D:Typora图像image-20210811144058093.png)]
因为Cat类继承了Animal类,所以内部结构完全复刻了Animal中的结构
4、当子类重写父类中的speak函数时,子类的内部结构如下图所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ryq8MaKT-1648379839026)(D:Typora图像image-20210811144302926.png)]
当Cat类重写了speak函数后vftable中存放的函数地址替换为了重写后的函数地址
1.7.3 纯虚函数和抽象类在多态中,通常父类中的虚函数的实现是毫无意义的,主要都是调用子类重写的内容
所以可以将虚函数改为纯虚函数
纯虚函数的语法:virtual 返回类型 函数名(参数) = 0;
另外当一个类中有了纯虚函数,这个类也称为抽象类
抽象类的特点:
1、无法实现实例化对象2、子类必须重写抽象类的纯虚函数,否则也属于抽象类
1.7.4 虚析构和纯虚析构多态使用时,如果是使用父类指针指向子类对象使用多态时,例:Animal * animal = new Cat,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构语法:
virtual ~类名(){}纯虚析构语法:
类内:virtual ~类名() = 0;
类外:类名::~类名(){}
虚析构和纯虚析构共性:
都可以解决父类指针释放子类对象的问题都需要有具体的函数实现
虚析构和纯虚析构的区别:
如果是纯虚析构,该类属于抽象类,无法实例化对象
总结:
虚析构或纯虚析构就是用来解决通过父类指针释放子类对象如果子类中没有堆区数据,可以不写为虚析构或纯虚析构拥有纯虚析构函数的类也属于抽象类
无意义的,主要都是调用子类重写的内容 所以可以将虚函数改为**纯虚函数** 纯虚函数的语法:`virtual 返回类型 函数名(参数) = 0;` 另外当一个类中有了纯虚函数,这个类也称为**抽象类** **抽象类的特点:** 1、无法实现实例化对象 2、子类必须重写抽象类的纯虚函数,否则也属于抽象类 #### 1.7.4 虚析构和纯虚析构 多态使用时,如果是使用**父类指针指向子类对象使用多态**时,例:`Animal * animal = new Cat`,如果**子类中有属性开辟到堆区**,那么**父类指针在释放时无法调用到子类的析构代码** 解决方式:将父类中的析构函数改为虚析构或者纯虚析构 **虚析构语法:** `virtual ~类名(){}` **纯虚析构语法:** 类内:`virtual ~类名() = 0;` 类外:`类名::~类名(){}` **虚析构和纯虚析构共性:** - 都可以解决父类指针释放子类对象的问题 - 都需要有具体的函数实现 **虚析构和纯虚析构的区别:** - 如果是纯虚析构,该类属于抽象类,无法实例化对象 **总结:** - 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象 - 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构 - 拥有纯虚析构函数的类也属于抽象类



