专业网站推广引流国外交易平台
news/
2025/10/4 13:20:38/
文章来源:
专业网站推广引流,国外交易平台,吉林省建设招标网站,网站开发所需要的语言4.类和对象
C面向对象的三大特性为:封装,继承,多态C认为万事万物都皆为对象#xff0c;对象上有其属性和行为
例如#xff1a;
人可以作为对象#xff0c;属性有姓名、年龄、身高、体重...,行为有走、跑、跳、说话...车可以作为对象#xff0c;属性有轮胎、方向盘、车灯…4.类和对象
C面向对象的三大特性为:封装,继承,多态C认为万事万物都皆为对象对象上有其属性和行为
例如
人可以作为对象属性有姓名、年龄、身高、体重...,行为有走、跑、跳、说话...车可以作为对象属性有轮胎、方向盘、车灯...,行为有载人、放音乐、开空调...具有相同性质的对象我们可以对其进行抽象抽象为类人属于人类车属于车类
4.1 封装
4.1.1 封装的意义
封装是C面向对象三大特性之一封装的意义 将属性和行为作为一个整体表现生活中的事物将属性和行为加以权限控制
封装意义一在设计类的时候属性和行为写在一起表现事物
语法class 类名{访问权限: 属性 / 行为};
类中的属性和行为 我们统一称为成员
属性 - 成员属性/成员变量行为 - 成员函数/成员方法
示例1设计一个圆类求圆的周长类和对象-封装-属性和行为作为整体.cpp
#include iostream
using namespace std;
// 圆周率
const double PI 3.1415926;
// 设计一个圆类,求圆的周长
// 圆求周长的公式 : 2 * PI * 半径
class Circle {// 访问权限// 公共权限
public:// 行为// 获取圆的周长double calculatePerimeter(double radius) {return 2 * PI * radius;}// 属性:半径int m_radius;
};
int main() {// 通过圆类,创建具体的圆(对象)// 实例化(通过一个类 创建一个对象的过程)Circle c1;c1.m_radius 10;cout圆的周长为 : c1.calculatePerimeter(c1.m_radius)endl;return 0;
}
示例2设计学生类
#include iostream
using namespace std;
#include string
// 设计一个学生类,属性有姓名和学号
// 可以给姓名和学号赋值,可以显示学生的姓名和学号// 设计学生类
class Student{
public://公共权限// 类中的属性和行为 我们统一称为成员// 属性 - 成员属性/成员变量// 行为 - 成员函数/成员方法string m_name; // 姓名int m_Id; // 学号// 行为void setName(string name){ // 设置姓名m_name name;}void setId(int id){ // 设置学号m_Id id;}void display(){ // 显示姓名和学号cout 姓名 m_name endl;cout 学号 m_Id endl;}
};int main() {// 创建一个具体学生 实例化对象Student s1;// 给s1对象 进行属性赋值操作s1.m_name 张三;s1.m_Id 2019001;// 显示学生信息s1.display();s1.setName(李四);s1.setId(2019002);s1.display();
}
运行结果
PS D:\Work\c\build\bin .D:/Work/c/bin/app.exe
姓名张三
学号2019001
姓名李四
学号2019002
PS D:\Work\c\build\bin
封装意义二
类在设计时可以把属性和行为放在不同的权限下加以控制
访问权限有三种
public 公共权限protected 保护权限private 私有权限
#include iostream
using namespace std;/*三种访问权限:公共权限 public 成员 类内可以访问 类外可以访问保护权限 protected 成员 类内可以访问 类外不可以访问(儿子可以访问父亲中的保护内容)私有权限 private 成员 类内可以访问 类外不可以访问(儿子不可以访问父亲中的私有内容)
*/class Person{
public:void func() {m_Name 张三;//公共权限m_Car 拖拉机;//保护权限m_Password 123456;//私有权限}
public:// 公共权限string m_Name;//姓名
protected:// 保护权限string m_Car;//汽车
private:// 私有权限int m_Password;//密码
};int main() {// 实例化具体对象Person p1;p1.m_Name 呵呵哒;//类外可以访问(public)// p1.m_Car 保时捷;//保护权限内容,在类外访问不到 error:成员Person::m_Car不可访问// p1.m_Password 123456;//私有权限内容,在类外访问不到 error:成员Person::m_Password不可访问p1.func();//类外可以访问(public)return 0;
}
4.1.2 struct和class区别
在C中struct和class唯一的区别就是默认的访问权限不同
区别
struct默认权限是公共权限class默认权限是私有权限
#include iostream
using namespace std;class C1{int m_A;// 默认权限 是私有
};struct C2{int m_A;// 默认权限 是公共
};int main() {/*在C中struct和class唯一的区别就是默认的访问权限不同区别struct默认权限是公共权限class默认权限是私有权限*/C1 c1;// c1.m_A 10;// error:成员C1::m_A不可访问C2 c2;c2.m_A 100;// ok return 0;
}
4.1.3 成员属性设置为私有
优点1将所有成员属性设置为私有可以自己控制读写权限优点2对于写权限我们可以检测到数据的有效性
演示控制读写权限
#include iostream
using namespace std;
/*成员属性设置私有优点1将所有成员属性设置为私有可以自己控制读写权限优点2对于写权限我们可以检测到数据的有效性
*/
// 人类
class Person{
public:// 设置姓名void setName(string name) {m_Name name;}// 获取姓名string getName() const {// 返回类型为const表示返回的是常量不可修改返回值。return m_Name;// 返回m_Name的值。}// 获取年龄int getAge() const {// 返回类型为const表示返回的是常量不可修改返回值。return m_Age;// 返回m_Age的值。}// 设置偶像void setIdol(string idol) {m_Idol idol;}
private:string m_Name;// 姓名 可读可写int m_Age 18;// 年龄 只读string m_Idol;// 偶像 只写
};int main() {Person p;// 姓名设置p.setName(张三);cout姓名p.getName()endl;// 姓名张三// 获取年龄cout年龄p.getAge()endl;// 输出年龄18// 偶像设置p.setIdol(迪丽热巴);return 0; // 程序执行成功返回0
}
演示检测到数据的有效性例如年龄设置为0-150之间
#include iostream
using namespace std;
/*成员属性设置私有优点1将所有成员属性设置为私有可以自己控制读写权限优点2对于写权限我们可以检测到数据的有效性
*/
// 人类
class Person{
public:// 设置姓名void setName(string name) {m_Name name;}// 获取姓名string getName() const {// 返回类型为const表示返回的是常量不可修改返回值。return m_Name;// 返回m_Name的值。}// 设置年龄 0-150void setAge(int age) {if (age 0 || age 150) {cout 年龄: age ,输入错误 endl;// 输出错误信息。return;}m_Age age;}// 获取年龄int getAge() const {// 返回类型为const表示返回的是常量不可修改返回值。return m_Age;// 返回m_Age的值。}// 设置偶像void setIdol(string idol) {m_Idol idol;}
private:string m_Name;// 姓名 可读可写int m_Age 18;// 年龄 只读string m_Idol;// 偶像 只写
};int main() {Person p;// 姓名设置p.setName(张三);cout姓名p.getName()endl;// 姓名张三// 获取年龄p.setAge(250);cout年龄p.getAge()endl;// 输出年龄18// 偶像设置p.setIdol(迪丽热巴);return 0; // 程序执行成功返回0
}
练习案例1设计立方体类
设计立方体类Cube求出立方体的面积和体积分别用全局函数和成员函数判断两个立方体是否相等
1.类和对象-封装-设计案例1-立方体类
#include iostream
using namespace std;/*立方体类设计1.创建立方体类2.设计属性3.设计行为 获取立方体面积和体积4.分别利用全局函数和成员函数 判断两个立方体是否相等
*/
class Cube{
public:// 设置长void setL(int l){m_Ll;}// 获取长int getL(){return m_L;}// 设置宽void setW(int w){m_Ww;}// 获取宽int getW(){return m_W;}// 设置高void setH(int h){m_Hh;}// 获取高int getH(){return m_H;}// 获取立方体面积int getArea(){return 2*(m_L*m_Wm_L*m_Hm_W*m_H);}// 获取立方体体积int getVolume(){return m_L*m_W*m_H;}// 利用成员函数判断两个立方体是否相等bool isSameByClass(Cube c){if(getL() c.getL() getW() c.getW() getH() c.getH()) {return true;}else{return false;}}
private:int m_L;//长int m_W;//宽int m_H;//高
};// 利用全局函数判断 两个立方体是否相等
bool isSame(Cube c1,Cube c2) {if(c1.getL() c2.getL() c1.getW() c2.getW() c1.getH() c2.getH()) return true; // 判断长宽高是否相等如果相等则返回true否则返回falseelse return false;
}int main() {Cube c1,c2;c1.setL(5);c1.setW(4);c1.setH(3);c2.setL(10);c2.setW(10);c2.setH(10);coutc1的面积为c1.getArea()endl;coutc1的体积为c1.getVolume()endl;cout***************************endl;coutc2的面积为c2.getArea()endl;coutc2的体积为c2.getVolume()endl;// 判断c1和c2是否相等// 利用全局函数判断bool ret isSame(c1,c2);if(ret) cout利用全局函数判断:c1和c2相等endl;else cout利用全局函数判断:c1和c2不相等endl;// 利用成员函数判断ret c1.isSameByClass(c2);if(ret) cout利用成员函数判断:c1和c2相等endl;else cout利用成员函数判断:c1和c2不相等endl;return 0;
}
2.类和对象-封装-设计案例2-点和圆关系案例
#include iostream
using namespace std;// 点和圆关系案例// 点类
class Point{
public:// 设置xvoid setX(int x) {m_X x;}// 获取xint getX() {return m_X;}// 设置yvoid setY(int y) {m_Y y;}// 获取yint getY() {return m_Y;}
private:int m_X;int m_Y;
};// 圆类
class Circle {
public: // 设置半径void setR(int r) {m_R r;}// 获取半径int getR() {return m_R;}// 设置圆心void setCenter(Point center) {m_Center center;}// 获取圆心Point getCenter() {return m_Center;}
private:int m_R;// 半径// 在类中可以让另一个类 作为本类中的成员Point m_Center;// 圆心
};
// 判断点和圆关系
void isInCircle(Circle c, Point p) {// 计算两点之间距离 平方int distance (c.getCenter().getX() - p.getX()) * (c.getCenter().getX() - p.getX()) (c.getCenter().getY() - p.getY()) * (c.getCenter().getY() - p.getY());// 计算半径的平方int rDistance c.getR() * c.getR();// 判断关系if(distance rDistance) {cout点在圆上endl;}else if(distance rDistance) {cout点在圆内endl; }else {cout点在圆外endl;}
}
int main() {// 创建圆Circle c;c.setR(10);Point center;center.setX(10);center.setY(0);c.setCenter(center);// 创建点Point p;p.setX(10);p.setY(9);// 判断关系isInCircle(c,p);return 0;
} 进一步完善项目
point.h
#pragma once
// 点类
class Point{
public:// 设置xvoid setX(int x);// 获取xint getX();// 设置yvoid setY(int y);// 获取yint getY();
private:int m_X;int m_Y;
};
point.cpp
#include point.h// 设置x
void Point::setX(int x) {m_X x;}
// 获取x
int Point::getX() {return m_X;}
// 设置y
void Point::setY(int y) {m_Y y;}
// 获取y
int Point::getY() {return m_Y;} circle.h
#pragma once
#include point.h
// 圆类
class Circle {
public: // 设置半径void setR(int r);// 获取半径int getR();// 设置圆心void setCenter(Point center);// 获取圆心Point getCenter();
private:int m_R;// 半径// 在类中可以让另一个类 作为本类中的成员Point m_Center;// 圆心
};
circle.cpp
#include circle.h
// 设置半径
void Circle::setR(int r) {m_R r;}
// 获取半径
int Circle::getR() {return m_R;}
// 设置圆心
void Circle::setCenter(Point center) {m_Center center;}
// 获取圆心
Point Circle::getCenter() {return m_Center;}
4.2 对象的初始化和清理
生活中我们买的电子产品都基本会有出厂设置在某一天我们不用时候也会删除一些自己信息数据保证安全C中的面向对象来源于生活每个对象也都会有初始设置以及对象销毁前的清理数据的设置
4.2.1 构造函数和析构函数
对象的初始化和清理也是两个非常重要的安全问题
一个对象或者变量没有初始状态对其使用后果是未知同样的使用完一个对象或变量没有及时清理也会造成一定的安全问题
C利用了构造函数和析构函数解决上述问题这两个函数将会被编译器自动调用完成对象初始化和清理工作。对象的初始化和清理工作是编译器强制要我们做的事情因此如果我们不提供构造和析构编译器会提供编译器提供的构造函数和析构函数是空实现
构造函数主要作用在于创建对象时为对象的成员属性赋值构造函数由编译器自动调用无须手动调用析构函数主要作用在于对象销毁前系统自动调用执行一些清理工作
构造函数语法类名(){}
构造函数没有返回值也不写void函数名称与类名相同构造函数可以有参数因此可以发生重载程序在调用对象时候会自动调用构造无须手动调用而且只会调用一次
析构函数写法~类名(){}
析构函数没有返回值也不写void函数名称与类名相同在名称前加上符号~析构函数不可以有参数因此不可以发生重载程序在对象销毁时候会自动调用析构无须手动调用而且只会调用一次
#include iostream
using namespace std;// 对象的初始化和清理
// 1.构造函数 进行初始化操作
class Person {
public:// 1. 构造函数// 没有返回值 不用写void// 函数名 与类名相同// 构造函数可以有参数,可以发生重载// 创建对象的时候,构造函数会自动调用,而且只调用一次Person() {coutPerson 构造函数的调用endl;}// 2.析构函数 进行清理的操作// 没有返回值 不写void// 函数名和类名相同 在名称前加~// 析构函数不可以有参数的,不可以发生重载// 对象在销毁前,会自动调用析构函数,而且只会调用一次~Person () {coutPerson 析构函数的调用endl;}
};// 构造和析构都是必须有的实现,如果我们不提供,编译器会提供一个空实现的构造和析构
void test01() {Person p;// 在栈上的数据,test01执行完毕后,释放这个对象
}int main() {test01();return 0;
}
执行结果
PS D:\Work\c\bin .\app
Person 构造函数的调用
Person 析构函数的调用
PS D:\Work\c\bin
4.2.2 构造函数的分类及调用
两种分类方式
按参数分为有参构造和无参构造按类型分为普通构造和拷贝构造
三种调用方式
括号法显示法隐式转换法
#include iostream
using namespace std;// 1.构造函数的分类及调用
// 分类
// 按照参数分类 无参构造(默认构造)和有参构造
// 按照类型分类 普通构造 拷贝构造
class Person {
public:// 无参构造函数Person(){coutPerson的构造函数调用endl;}// 有参构造函数Person(int age) {m_ageage;coutPerson的构造函数调用endl;}// 拷贝构造函数Person(const Person p) {// 将传入的人身上的所有属性,拷贝到我身上m_age p.m_age;coutPerson的拷贝构造函数调用endl;}~Person(){coutPerson的析构函数调用endl;}int m_age;
};void test01() {// 1. (括号法)Person p1;// 默认构造调用Person p2(10);// 有参构造调用Person p3(p2);// 拷贝构造调用cout p2的年龄是: p2.m_ageendl;cout p3的年龄是: p3.m_ageendl;// 注意事项1// 调用默认构造函数时候,不要加()// 因为下面这行代码,编译器会认为是一个函数的声明,不会认为在创建对象// Person p1();// void func();
}void test02() {// 2.显示法Person p1;Person p2 Person(10);// 有参构造Person p3 Person(p2);// 拷贝构造Person(20);// 匿名对象 特点:当前行执行结束后,系统会立即回收掉匿名对象coutasasasaendl;// 注意事项2// 不要利用拷贝构造函数 初始化匿名对象// 编译器会认为Person(p3) Person p3;// 会认为这是一个对象的声明// Person(p3); // 此时重定义了
}void test03() {// 3,隐式转换法Person p4 10;// 相当于 写了 Person p4 Person(10); 有参构造Person p5 p4;// 拷贝构造
}// 调用
int main() {test02();return 0;
}
4.2.3 拷贝构造函数调用时机
C中拷贝构造函数调用时机通常有三种情况
使用一个已经创建完毕的对象来初始化一个新对象值传递的方式给函数参数传值以值方式返回局部对象
#includeiostream
using namespace std;// 拷贝构造函数调用时机
class Person {
public:Person() {coutPerson默认构造函数调用endl;}Person(int age) : m_Age(age) {coutPerson有参构造函数调用endl;}Person(const Person p) {m_Age p.m_Age;coutPerson拷贝构造函数调用endl;}~Person() {coutPerson析构函数调用endl;}int m_Age;
private:};// 1.使用一个已经创建完毕的对象来初始化一个新对象
void test01() {Person p1(20);Person p2(p1);coutp2: p2.m_Ageendl;/*Person有参构造函数调用Person拷贝构造函数调用p2: 20Person析构函数调用Person析构函数调用*/
}// 2.值传递的方式给函数参数传值
void doWork(Person p) {// 值传递会拷贝一个临时的副本出来,在调用它的拷贝构造函数,}void test02() {Person p;doWork(p);/*Person默认构造函数调用Person拷贝构造函数调用Person析构函数调用Person析构函数调用*/
}// 3.以值方式返回局部对象
Person doWork2() {Person p1;cout(int*)p1endl;return p1;
}
void test03() {Person p doWork2();cout(int*)pendl;
}int main() {test03();return 0;
}
4.2.4 构造函数调用规则
默认情况下C编译器至少给一个类添加3个函数
默认构造函数无参函数体为空默认析构函数无参函数体为空默认拷贝构造函数对属性进行值拷贝
构造函数调用规则如下
如果用户定义有参构造函数C不再提供默认无参构造但是会提供默认拷贝构造如果用户定义拷贝构造函数C不会再提供其他构造函数
#include iostream
using namespace std;// 构造函数的调用规则
// 1.创建一个类,C编译器会给每个类都添加至少3个函数
// 默认构造(空实现)
// 析构函数(空实现)
// 拷贝构造(值拷贝)// 2.如果我们写了有参构造函数,编译器就不再提供默认构造,依然提供拷贝构造
// 如果我们写了拷贝构造函数,编译器就不再提供其他普通构造函数了
class Person {
public:// 默认构造Person() { cout Person的默认构造函数调用 endl; }// 有参构造Person(int age) {m_Age age; cout Person的有参构造函数调用 endl; }// 拷贝构造// Person(const Person p) {// m_Age p.m_Age;// cout Person的拷贝构造函数调用 endl;// }// 析构函数~Person() { cout Person的析构函数调用 endl;}int m_Age;
};void test01() {Person p;p.m_Age 18;Person p2(p);coutp2的年龄为: p2.m_Ageendl;
}int main() {test01();return 0;
}
执行结果
PS D:\Work\c\bin .D:/Work/c/bin/app.exe
Person的默认构造函数调用
p2的年龄为: 18
Person的析构函数调用
Person的析构函数调用
如果我们写了拷贝构造函数,编译器就不再提供其他普通构造函数了
#include iostream
using namespace std;// 构造函数的调用规则
// 1.创建一个类,C编译器会给每个类都添加至少3个函数
// 默认构造(空实现)
// 析构函数(空实现)
// 拷贝构造(值拷贝)// 2.如果我们写了有参构造函数,编译器就不再提供默认构造,依然提供拷贝构造
// 如果我们写了拷贝构造函数,编译器就不再提供其他普通构造函数了
class Person {
public:// 默认构造Person() { cout Person的默认构造函数调用 endl; }// 有参构造Person(int age) {m_Age age; cout Person的有参构造函数调用 endl; }// 拷贝构造Person(const Person p) {m_Age p.m_Age;cout Person的拷贝构造函数调用 endl;}// 析构函数~Person() { cout Person的析构函数调用 endl;}int m_Age;
};void test01() {Person p;p.m_Age 18;Person p2(p);coutp2的年龄为: p2.m_Ageendl;
}int main() {test01();return 0;
}
执行结果
Person的默认构造函数调用
Person的拷贝构造函数调用
p2的年龄为: 18
Person的析构函数调用
Person的析构函数调用 如果我们写了有参构造函数,编译器就不再提供默认构造,依然提供拷贝构造
#include iostream
using namespace std;// 构造函数的调用规则
// 1.创建一个类,C编译器会给每个类都添加至少3个函数
// 默认构造(空实现)
// 析构函数(空实现)
// 拷贝构造(值拷贝)// 2.如果我们写了有参构造函数,编译器就不再提供默认构造,依然提供拷贝构造
// 如果我们写了拷贝构造函数,编译器就不再提供其他普通构造函数了
class Person {
public:// 默认构造Person() { cout Person的默认构造函数调用 endl; }// 有参构造Person(int age) {m_Age age; cout Person的有参构造函数调用 endl; }// 拷贝构造// Person(const Person p) {// m_Age p.m_Age;// cout Person的拷贝构造函数调用 endl;// }// 析构函数~Person() { cout Person的析构函数调用 endl;}int m_Age;
};void test02() {Person p(18);Person p2(p);
}int main() {test02();return 0;
}
执行结果
Person的有参构造函数调用
Person的析构函数调用
Person的析构函数调用
4.2.5 深拷贝与浅拷贝
深拷贝是面试经典问题也是常见的一个坑
浅拷贝简单的赋值拷贝操作深拷贝在堆区重新申请空间进行拷贝操作
#includeiostream
using namespace std;// 深拷贝与浅拷贝
class Person {
public:Person() { coutPerson的默认构造函数调用endl; }Person(int age,int height) { m_Age age;m_Height new int(height);coutPerson的有参构造函数调用endl; }// 自己实现拷贝构造函数,解决浅拷贝带来的问题Person(const Person p) {coutPerson的拷贝构造函数调用endl;m_Age p.m_Age;// m_Height p.m_Age; // 编译器默认实现就是这行代码// 深拷贝操作m_Height new int(*p.m_Height); // 在堆区创建一块内存}~Person(){ // 析构代码,将堆区开辟数据做释放操作if(m_Height ! NULL) {delete m_Height;m_Height NULL;}coutPerson的析构函数调用endl; }int m_Age;int* m_Height;// 身高
};void test01() {// 深拷贝,p1走p1的析构,p2走p2的析构.注意:堆栈是先进后出的// 浅拷贝,有交叉重复释放的问题Person p1(18,160);coutp1的年龄: p1.m_Age身高: *p1.m_Heightendl;Person p2(p1);// 默认的拷贝构造函数(浅拷贝)coutp2的年龄: p2.m_Age身高: *p2.m_Heightendl;
}int main() {test01();return 0;
}
执行结果
Person的有参构造函数调用
p1的年龄: 18身高: 160
Person的拷贝构造函数调用
p2的年龄: 18身高: 160
Person的析构函数调用
Person的析构函数调用
总结如果属性有在堆区开辟时一定要自己提供拷贝构造函数防止浅拷贝带来的问题
4.2.6 初始化列表
作用C提供了初始化列表语法用来初始化属性语法构造函数():属性1(值1),属性2(值2),…{}
#include iostream
using namespace std;// 初始化列表
class Person {
public:// 传统初始化操作// Person(int a,int b,int c) {// m_A a;// m_B b;// m_C c;// }// 初始化列表初始化属性Person(int a,int b,int c):m_A(a),m_B(b),m_C(c) { }int m_A;int m_B;int m_C;
};void test01() {Person p(1,2,3);cout p.m_A p.m_B p.m_C endl; // 输出1 2 3
}int main() {return 0;
}
4.2.7 类对象作为类成员
C类中的成员可以是另一个类的对象我们称该成员为对象成员
例如
class A{};class B{A a};
B类中有对象A作为成员A为对象成员。那么当创建B对象时A与B的构造和析构的顺序是谁先谁后呢
当其他类对象作为本类成员,构造时候先构造类对象,再构造自身,析构的顺序与构造相反
#include iostream
using namespace std;
#include string
// 类对象作为类成员// 手机类
class Phone {
public:Phone(string pName) {m_PName pName;coutPhone有参构造调用endl;}~Phone() { coutPhone的析构函数调用endl; }string m_PName;// 品牌名称
};// 人类
class Person {
public:// Phone m_Phone pName 隐式转换法Person(string name, string pName) : m_name(name), m_phone(pName) {coutPerson有参构造调用endl;}~Person() { coutPerson的析构函数调用endl; }// 姓名string m_name;// 手机Phone m_phone;
private:};// 当其他类对象作为本类成员,构造时候先构造类对象,再构造自身,析构的顺序与构造相反
void test01() {Person p(张三, 苹果);cout 姓名 p.m_name 手机品牌 p.m_phone.m_PName endl;
}int main() {test01();return 0;
}
执行结果
PS D:\Work\c\bin .D:/Work/c/bin/app.exe
Phone有参构造调用
Person有参构造调用
姓名张三 手机品牌苹果
Person的析构函数调用
Phone的析构函数调用
PS D:\Work\c\bin
4.2.8 静态成员
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员
静态成员分为
静态成员变量
所有对象共享同一份数据在编译阶段分配内存类内声明类外初始化
#include iostream
using namespace std;// 静态成员变量
class Person {
public:// 1.所有对象都共享同一份数据// 2.编译阶段就分配内存// 3.类内声明,类外初始化操作static int m_A; // 静态成员变量// 静态成员变量也是有访问权限的
private:static int m_B; // 静态成员变量
};int Person::m_A 100; // 类外初始化操作
int Person::m_B 200; // 类外初始化操作void test01() {Person p;cout m_A p.m_A endl; // 100Person p2;p2.m_A 400;cout m_A p.m_A endl; // 400
}void test02() {// 静态成员变量,不属于某个对象上,所有对象都共享同一份数据// 因此静态成员变量有两种访问方式// 1.通过对象进行访问// Person p;// coutp.m_A p.m_Aendl; // 100// 2.通过类名进行访问cout m_A Person::m_A endl; // 100// 类外访问不到私有静态成员变量// cout m_B Person::m_B endl; // error:成员Person::m_B不可访问
}int main() {test02();return 0;
}
静态成员函数
所有对象共享同一个函数静态成员函数只能访问静态成员变量
#include iostream
using namespace std;// 静态成员函数
// 所有对象共享同一个函数
// 静态成员函数只能访问静态成员变量
/*因为m_B必须通过创建对象才能访问,得创建一个对象,才能够去读/写这块内存.当你去调用静态成员函数func()这个函数体的内部不知道改变的是哪个对象的m_B非静态成员变量属于特定对象的成员变量
*/
class Person {
public:// 静态成员函数static void func() { m_A 100;//静态成员函数是可以访问静态成员变量// m_B 200;//静态成员函数是不可以访问非静态成员变量的,无法区分到底是哪个对象的m_B属性coutstatic void func调用endl;}static int m_A;// 静态成员变量int m_B;// 非静态成员变量// 静态成员函数也是有访问权限的
private:static void func2() { coutstatic void func2调用endl; }
};int Person::m_A 0;// 有两种访问方式
void test01() {// 1.通过对象访问Person p;p.func();// 2.通过类名访问(类外访问不到私有静态成员函数)Person::func();// Person::func2();// 错误,不可访问
}int main() {test01();return 0;
}
因为m_B必须通过创建对象才能访问,得创建一个对象,才能够去读/写这块内存.当你去调用静态成员函数func()。这个函数体的内部不知道改变的是哪个对象的m_B非静态成员变量属于特定对象的成员变量。静态成员函数是不可以访问非静态成员变量的,无法区分到底是哪个对象的m_B属性。
4.3 C对象模型和this指针
4.3.1 成员变量和成员函数分开存储
在C中类内的成员变量和成员函数分开存储只有非静态成员变量才属于类的对象上。
#include iostream
using namespace std;// 成员变量和成员函数是分开存储的
class Person {
public:int m_A;// 非静态成员变量 属于类的对象上static int m_B;// 静态成员变量 不属于类的对象上void func(){} // 非静态成员函数 不属于类的对象上static void func2(){} // 静态成员函数 不属于类的对象上
};int Person::m_B 100;// 初始化静态成员变量void test01() {Person p;// 空对象占用内存空间为:1// C编译器会给每个空对象特分配一个字节空间,是为了区分空对象// 占内存的位置// 每个空对象也应该有一个独一无二的内存地址coutsize of p sizeof(p)endl; // 1
}void test02() {Person p;coutsize of p sizeof(p)endl; // 4
}int main() {test02();return 0;
}
4.3.2 this指针概念
通过4.3.1 我们知道在C中成员变量和成员函数是分开存储的。每一个非静态成员函数只会诞生一份函数实例也就是说多个同类型的对象会共用一块代码。那么问题是这一块代码是如何区分那个对象调用自己的呢C通过提供的对象指针this指针解决上述问题this指针指向被调用的成员函数所属的对象
this指针是隐含每一个非静态成员函数内的一种指针this指针不需要定义直接使用即可
this指针的用途
当形参和成员变量同名时可用this指针来区分在类的非静态成员函数中返回对象本身可使用return *this
#include iostream
using namespace std;
class Person {
public:Person(int age) {// this指针指向的是被调用的成员函数 所属的对象this-m_Age age;}Person PersonAddPerson(Person p) {this-m_Age p.m_Age;// this指向p2的指针,而*this指向的就是p2这个对象本体return *this; // 返回对象本身}int m_Age;
};
// 1.解决名称冲突
void test01() {Person p1(18); // 创建一个Person对象年龄为18cout p1的年龄为: p1.m_Age endl; // 输出Person对象的年龄
}// 2.返回对象本身用*this
void test02() {Person p1(10); // 创建一个Person对象年龄为10Person p2(20); // 创建另一个Person对象年龄为20// 链式编程思想p2.PersonAddPerson(p1).PersonAddPerson(p1).PersonAddPerson(p1); coutp2的年龄为: p2.m_Ageendl;
}int main() { test02();return 0;
}
4.3.3 空指针访问成员函数
C中空指针也是可以调用成员函数的但是也要注意有没有用到this指针如果用到this指针需要加以判断保证代码的健壮性
#include iostream
using namespace std;// 空指针调用成员函数
class Person {
public:void showClassName() {coutthis is Person Classendl;}void showPersonAge() {// 常见报错原因:传入的指针是NULLif(this NULL) return;coutAge this-m_Ageendl;}int m_Age;
};void test01() {Person *p NULL;p-showClassName();p-showPersonAge();
}int main() {test01();return 0;
}
4.3.4 const修饰成员函数
常函数
成员函数后加const称此函数为常函数常函数内不可以修改成员属性成员属性声明时加关键字mutable则表示在常函数中依然可以修改
常对象
声明对象前加const称此对象为常对象常对象只能调用常函数
const 和 this
this指针的本质 是指针常量 指针的指向是不可以修改的,指针指向的值是可以修改的Person * const this;const Person * const this; 指针的指向的值也不可以修改了在成员函数后面加const,修饰的是this指向,让指针指向的值也不可以修改
注意
特殊变量,即使在常函数中,也可以修改这个值.需要加上关键字mutable 常对象不可以调用普通成员函数,因为普通成员函数可以修改属性
#include iostream
using namespace std;// 常函数
class Person {
public:Person();// this指针的本质 是指针常量 指针的指向是不可以修改的,指针指向的值是可以修改的// Person * const this;// const Person * const this; 指针的指向的值也不可以修改了// 在成员函数后面加const,修饰的是this指向,让指针指向的// 值也不可以修改void showPerson() const {// 常函数// this-m_A 10;// this 指针是不可以修改指针的指向的// this NULL;// error 分配到this(记时错误)this-m_B 100;}void func() {m_A 100;}int m_A;mutable int m_B;// 特殊变量,即使在常函数中,也可以修改这个值.需要加上关键字mutable
};void test01() {Person p;p.showPerson();
}// 常对象
void test02() {const Person p;// 在对象前加上const,变为常对象// p.m_A 100;//errorp.m_B 100;// m_B是特殊值,在常对象也可以修改// 常对象只能调用常函数p.showPerson();// p.func();// error:常对象不可以调用普通成员函数,因为普通成员函数可以修改属性
}int main() {return 0;
}
4.4 友元
生活中你的家有客厅Public,有你的卧室Private客厅所有来的客人都可以进去但是你的卧室是私有的也就是说只有你能进去。但是呢你也可以允许你的好闺蜜好基友进去。在程序里有些私有属性也想让类外特殊的一些函数或者类进行访问就需要用到友元的技术友元的目的就是让一个函数或者类访问另一个类中私有成员。友元的关键字为friend
友元的三种实现
全局函数做友元类做友元成员函数做友元
全局函数做友元
#include iostream
using namespace std;
#include string// 建筑物
class Building {// 告诉编译器 goodGay全局函数是 Building好朋友,可以访问Building中私有成员friend void goodGay(Building *building);
public:Building() {m_SittingRoom 客厅;m_BedRoom 卧室;}
public:string m_SittingRoom; // 客厅
private:string m_BedRoom; // 卧室
};// 全局函数
void goodGay(Building *building) {cout好基友全局函数 正在访问 : building-m_SittingRoomendl;cout好基友全局函数 正在访问 : building-m_BedRoomendl;
}void test01() {Building building;goodGay(building); // 调用全局函数
}int main() {test01();return 0; // 返回0表示正常退出
}
执行结果
PS D:\Work\c\bin .D:/Work/c/bin/app.exe
好基友全局函数 正在访问 : 客厅
好基友全局函数 正在访问 : 卧室
类做友元
#include iostream
using namespace std;
#include string
// 类做友元
class Building;
class GoodGay
{
public:GoodGay();void visit();// 参观函数 访问Building中的属性Building *building;
};class Building{// 告诉编译器 GoodGay 类是本来的好朋友,可以访问本类中私有成员friend class GoodGay;
public:Building();
public:string m_SittingRoom;// 客厅
private:string m_BedRoom;// 卧室
};// 类外写成员函数
Building::Building() {m_SittingRoom 客厅;m_BedRoom 卧室;
}GoodGay::GoodGay() {// 创建建筑物对象building new Building;
}void GoodGay::visit() {cout 好基友正在访问 building-m_SittingRoom endl;cout 好基友正在访问 building-m_BedRoom endl;
}void test01() {GoodGay gg;gg.visit();
}int main() {test01();return 0;
}
成员函数做友元
#include iostream
using namespace std;
#include string
class Building;
class GoodGay{
public:GoodGay();void visit();// 让visit函数可以访问Building中私有成员void visit2();// 让visit2函数不可以访问Building中私有成员Building* building;
};class Building{// 告诉编译器 GoodGay类下的visit成员函数作为本类的好朋友,可以访问私有成员friend void GoodGay::visit();
public:Building();string m_SittingRoom; // 客厅
private:string m_BedRoom; // 卧室
};// 类外实现成员函数
Building::Building(){m_SittingRoom 客厅;m_BedRoom 卧室;
}GoodGay::GoodGay(){building new Building;
}void GoodGay::visit() {coutvisit 函数正在访问: building-m_SittingRoomendl;coutvisit 函数正在访问: building-m_BedRoomendl;
}void GoodGay::visit2() {coutvisit 函数正在访问: building-m_SittingRoomendl;
}void test01() {GoodGay gg;gg.visit();gg.visit2();
}int main() {test01();return 0;
}
4.5 运算符重载
4.5.1 加号运算符重载
作用实现两个自定义数据类型相加的运算
总结
对于内置的数据类型的表达式的运算符是不可能改变的不要滥用运算符重载 加号运算符重载 #include iostream
using namespace std;// 加号运算符重载
class Person {
public:// 1. 成员函数重载 号Person operator(Person p) {Person temp;temp.m_A this-m_A p.m_A;temp.m_B this-m_B p.m_B;return temp;}int m_A;int m_B;
};// 2.全局函数重载号
// Person operator(Person p1,Person p2) {
// Person temp;
// temp.m_A p1.m_A p2.m_A;
// temp.m_B p1.m_B p2.m_B;
// return temp;
// }// 函数重载的版本
Person operator(Person p1,int num) {Person temp;temp.m_A p1.m_A num;temp.m_B p1.m_B num;return temp;
}void test01() {Person p1;p1.m_A 10;p1.m_B 10;Person p2;p2.m_A 10;p2.m_B 10;// 成员函数重载本质调用// Person p3 p1.operator(p2);// 全局函数重载本质调用// Person p3 operator(p1,p2);Person p3 p1 p2;// 运算符重载,也可以发生函数重载Person p4 p1 100;coutp3.m_A p3.m_Aendl;coutp3.m_B p3.m_Bendl;coutp4.m_A p4.m_Aendl;coutp4.m_B p4.m_Bendl;
}int main() {test01();return 0;
}
4.5.2 左移运算符重载 #include iostream
using namespace std;// 左移运算符重载
class Person {friend ostream operator(ostream cout,Person p);
public:Person(int a,int b):m_A(a),m_B(b){}// 利用成员函数重载 左移运算符 p.operator(cout) 简化版本 pcout// 不会利用成员函数重载运算符,因为实现cout在左侧// void operator(Person p) {// }private:int m_A;int m_B;
};// 只能利用全局函数重载左移运算符
ostream operator(ostream cout,Person p) // 本质 operator(cout,p) 简化 coutp
{coutm_A p.m_A , m_B p.m_B;return cout;
}void test01() {Person p(10,20);coutp, hello,world!endl;
}int main() {test01();return 0;
}
总结重载左移运算符配合友元可以实现输出自定义数据类型
4.5.3 递增运算符重载
作用通过重载递增运算符实现自己的整型数据
#include iostream
using namespace std;// 重载递增运算符// 自定义整型
class MyInterger {friend ostream operator(ostream cout,MyInterger myint);
public:MyInterger() {m_Num 0;}// 重载前置运算符 返回引用为了一直对一个数据进行递增MyInterger operator() {// 先进行运算m_Num;// 再将自身做返回return *this;}// 重载后置运算符 // void operator(int) int代表占位参数,可以用于区分前置和后置递增MyInterger operator(int) {// 先 记录当时结果MyInterger temp *this;// 后 递增m_Num;// 最后将记录结果做返回return temp;}
private:int m_Num;
};// 重载运算符
ostream operator(ostream cout,MyInterger myint) {coutmyint.m_Num;return cout;
}void test01() {MyInterger myint;cout(myint)endl;coutmyintendl;
}void test02() {MyInterger myint;coutmyintendl;coutmyint;
}int main() {test02();return 0;
}
总结前置递增返回引用后置递增返回值
4.5.4 赋值运算符重载
C编译器至少给一个类添加4个函数
默认构造函数无参函数体为空默认析构函数无参函数体为空默认拷贝构造函数对属性进行值拷贝赋值运算符operator,对属性进行值拷贝
如果类中有属性指向堆区做赋值操作时会出现深浅拷贝的问题 #include iostream
using namespace std;// 赋值运算符重载
class Person {
public:Person(int age) {m_Age new int(age);}~Person() {if(m_Age!NULL) {delete m_Age;m_Age NULL;}}// 重载 赋值运算符Person operator(Person p) {// 编译器是提供浅拷贝// m_Age p.m_Age;// 应该先判断是否有属性在堆区,如果有先释放干净,然后再深拷贝if(m_Age!NULL) {delete m_Age;m_Age NULL;}// 深拷贝m_Age new int(*p.m_Age);// 返回对象本身 而不是返回副本return *this;}int *m_Age;
};void test01() {Person p1(18);Person p2(20);Person p3(30);p3 p2 p1;// 赋值操作coutp1 的年龄为: *p1.m_Ageendl;coutp2 的年龄为: *p2.m_Ageendl;coutp3 的年龄为: *p3.m_Ageendl;
}int main() {test01();return 0;
}
执行结果
PS D:\Work\c\bin .D:/Work/c/bin/app.exe
p1 的年龄为: 18
p2 的年龄为: 18
p3 的年龄为: 18
4.5.5 关系运算符重载
作用重载关系运算符可以让两个自定义类型对象进行对比操作
#include iostream
using namespace std;
// 作用:重载关系运算符,可以让两个自定义类型对象进行对比操作
class Person{
public:Person(string name,int age) {m_Name name;m_Age age;}// 重载 号bool operator(Person p) {if(this-m_Name p.m_Name this-m_Age p.m_Age) {return true;}return false;}// 重载 ! 号bool operator!(Person p) {if(this-m_Name p.m_Name this-m_Age p.m_Age) {return false;}return true;}string m_Name;int m_Age;
};void test01() {Person p1(Tom,18);Person p2(Tom,18);Person p3(Jerry,18);if(p1 p2) {coutp1 和 p2 是相等的endl;}if(p1 ! p3) {coutp1 和 p3 是不相等的endl;}else{coutp1 和 p3 是相等的endl;}
}int main() {test01();return 0;
}
执行结果
PS D:\Work\c\bin .D:/Work/c/bin/app.exe
p1 和 p2 是相等的
p1 和 p3 是不相等的
PS D:\Work\c\bin
4.5.6 函数调用运算符重载
函数调用运算符() 也可以重载由于重载后使用的方式非常像函数的调用因此称为仿函数仿函数没有固定写法非常灵活
#include iostream
using namespace std;
#include string
// 函数调用运算符重载// 打印输出类
class MyPrint {
public:// 重载函数调用运算符void operator()(string test){couttestendl;}
};void MyPrint02(string test) {couttestendl;
}void test01() {MyPrint myPrint;myPrint(hello,world); // 由于使用起来非常类似于函数调用,因此称为仿函数MyPrint02(hello,heheda);
}// 仿函数非常灵活,没有固定的写法
class MyAdd{
public:int operator()(int num1,int num2){return num1num2;}
};void test02() {MyAdd myadd;int ret myadd(100,200);coutret retendl;// 匿名函数对象coutMyAdd()(100,10)endl;
}int main() {test02();return 0;
}
4.6 继承
继承是面向对象三大特性之一
有些类与类之间存在特殊的关系例如下图中 我们发现定义这些类时下级别的成员除了拥有上一级的共性还有自己的特性。
这个时候我们就可以考虑利用继承的技术减少重复代码 #include iostream
using namespace std;// 继承方式// 公共继承
class Base1{
public:int m_A;
protected:int m_B;
private:int m_C;
};class Son1:public Base1{
public:void func() {m_A 10;// 父类中的公共权限成员 到子类中依然是公共权限m_B 10;// 父类中的保护权限成员 到子类中依然是保护权限// m_C 10;// 父类中的私有权限成员 子类访问不到}
};
void test01(){Son1 s1;s1.m_A 100;// s1.m_B 20; // 到Son1中 m_B是保护权限 类外访问不到
}// 保护继承
class Base2{
public:int m_A;
protected:int m_B;
private:int m_C;
};
class Son2:protected Base2{
public:void func(){m_A 100; // 父类中公共成员,到子类中变为保护权限m_B 200; // 父类中保护成员,到子类中变为保护权限// m_C 400;// 父类中私有成员 子类访问不到};
};void test02() {Son2 s2;// s2.m_A 1000;// 在Son2中 m_A变为保护权限,因此类外访问不到// s2.m_B 120; // 在Son2中 m_B为保护权限,因此类外访问不到
}// 私有继承
class Base3{
public:int m_A;
protected:int m_B;
private:int m_C;
};
class Son3:private Base3{
public:void func(){m_A 100; // 父类中公共成员,到子类中变为私有权限m_B 200; // 父类中保护成员,到子类中变为私有权限// m_C 400;// 父类中私有成员 子类访问不到};
};void test03() {Son3 s3;// s2.m_A 1000;// 在Son3中 m_A变为私有权限,因此类外访问不到// s2.m_B 120; // 在Son3中 m_B变为私有权限,因此类外访问不到
}class GrandSon3:public Son3 {
public:void func() {// m_A 100; // 到了Son3中 m_A变为私有,即使是儿子,也是访问不到// m_B 20; // 到了Son3中 m_B变为私有,即使是儿子,也是访问不到}
};
4.6.3 继承中的对象模型
问题从父类继承过来的成员哪些属于子类对象中
#includeiostream
using namespace std;// 继承中的对象模型
class Base{
public:int m_A;
protected: int m_B;
private:int m_C;
};class Son:public Base{
public:int m_D;
};// 利用开发人员命令提示工具查看对象模型
// 跳转盘符 F:
// 跳转文件路径 cd 具体路径下
// 查看命名
// c1 /d1 reportSingleClassLayout类名 文件名void test01() {// 16// 父类中所有非静态成员属性都会被子类继承下去// 父类中私有成员属性 是被编译器给隐藏了,因此是访问不到,但是确实被继承了coutsize of Son sizeof(Son)endl;
}int main() {return 0;
} 打开工具窗口后定位到当前CPP文件的盘符
然后输入c1 /d1 reportSingleClassLayout查看的类名 所属文件名 4.6.4 继承中构造和析构顺序
子类继承父类后当创建子类对象也会调用父类的构造函数
问题父类和子类的构造和析构顺序是谁先谁后
继承中的构造和析构顺序如下:先构造父类,再构造子类,析构的顺序与构造的顺序相反
#include iostream
using namespace std;
// 继承中的构造和析构顺序
class Base{
public:Base() {cout Base constructor endl;}~Base() {cout Base destructor endl;}
private:};class Son:public Base{
public:Son() {cout Son constructor endl;}~Son() {cout Son destructor endl;}
};void test01() {// Base b;// 继承中的构造和析构顺序如下:// 先构造父类,再构造子类,析构的顺序与构造的顺序相反Son s;
}int main() {test01();return 0;
}
执行结果
PS D:\Work\c\bin .D:/Work/c/bin/app.exe
Base constructor
Son constructor
Son destructor
Base destructor
PS D:\Work\c\bin
4.6.5 继承同名成员处理方式
问题当子类与父类出现同名的成员如何通过子类对象访问到子类或父类中同名的数据呢
访问子类同名成员直接访问即可访问父类同名成员需要加作用域
总结
子类对象可以直接访问到子类中同名成员子类对象加作用域可以访问到父类同名成员当子类与父类拥有同名的成员函数子类会隐藏父类中同名成员函数加作用域可以访问到父类中同名函数
#include iostream
using namespace std;// 继承中同名成员处理
class Base{
public:Base() {m_A 100;}void func() {cout Base func() endl;}void func(int a) {cout Base func(int a) endl;}int m_A;
};class Son:public Base{
public:Son() {m_A 200;}void func() {cout Son func() endl;}int m_A;
};// 同名成员属性处理
void test01() {Son s;cout Son下的 m_A s.m_A endl;//200// 如果通过子类对象 访问到父类中同名成员,需要加作用域cout Base下的 m_A s.Base::m_A endl;//100cout Son::m_A s.Son::m_A endl;//200}// 同名成员函数处理
void test02() {Son s;s.func();//Son func() 直接调用 调用的是子类中的同名成员// 如何调用到父类中同名成员函数?s.Base::func();//Base func()// 如果子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类中所有的同名成员函数// s.func(100); // error// 如果想访问到父类中被隐藏的同名成员函数,需要加作用域s.Base::func(100);//Base func(int a)
}int main() {// test01();test02();return 0;
}
4.6.6 继承同名静态成员处理方式
问题继承中同名的静态成员在子类对象上如何进行访问
静态成员和非静态成员出现同名处理方式一致
访问子类同名成员直接访问即可访问父类同名成员需要加作用域
#include iostream
using namespace std;// 继承中的同名静态成员处理方式
class Base {
public:static int m_A;static void func() {cout Base func() endl;}static void func(int a) {coutBase func(int a)endl;}
};int Base::m_A 100;class Son:public Base{
public:static int m_A;static void func() {coutSon func()endl;}
};
int Son::m_A 200;// 同名的静态成员属性
void test01() {Son s;// 1.通过对象访问cout通过对象访问: endl;coutSon 下的 m_A s.m_Aendl;coutBase 下的 m_A s.Base::m_Aendl;// 2.通过类名访问cout通过类名访问: endl;coutSon 下的 m_A Son::m_Aendl;// 第一个::代表通过类名方式访问// 第二个::代表访问父类作用域下coutBase 下的 m_A Son::Base::m_Aendl;
}// 同名的静态成员函数
void test02() {Son s;// 1.通过对象访问cout通过对象访问: endl;s.func();//Son func()s.Base::func();//Base func()// 2.通过类名访问 cout通过类名访问: endl;Son::func();//Son func()Son::Base::func();//Base func()// 子类出现和父类同名静态成员函数,也会隐藏父类中所有同名成员函数// 如果想访问父类中被隐藏同名成员,需要加作用域Son::Base::func(1);//Base func(int a)
}int main() {test02();return 0;
}
总结同名静态成员处理方式和非静态处理方式一样只不过有两种访问方式通过对象 和 通过类名
4.6.7 多继承语法
C允许一个类继承多个类
语法class 子类:继承方式1 父类1,继承方式2 父类2...
多继承可能会引发父类中有同名成员出现需要加作用域区分
C实际开发中不建议多继承
#include iostream
using namespace std;// 多继承语法
class Base1 {
public:Base1() {m_A 100;}int m_A;
};class Base2 {
public:Base2() {m_A 200;}int m_A;
};// 子类 需要继承Base1和Base2
// 语法class 子类:继承方式1 父类1,继承方式2 父类2...
class Son:public Base1,public Base2 {
public:Son() {m_C 300;m_D 400;}int m_C;int m_D;
};void test01() {Son s;coutsize of Son: sizeof(s)endl;// 当父类中出现同名成员需要加作用域区分coutBase1::m_A s.Base1::m_Aendl;//100coutBase2::m_A s.Base2::m_Aendl;//200
}int main() {test01();return 0;
}
总结:多继承中如果父类中出现了同名情况子类使用时候要加作用域
4.6.8 菱形继承
菱形继承概念
两个派生类继承同一个基类又有某个类同时继承这两个派生类这种继承称为菱形继承或者钻石继承
典型的菱形继承案例 菱形继承问题
羊继承了动物的数据驼同样继承了动物的数据当草泥马使用数据时就会产生二义性草泥马继承自动物的数据继承了两份其实我们应清楚这份数据只需要一份即可
#include iostream
using namespace std;// 动物类
class Animal{
public:int m_Age;
};// 羊类
class Sheep:public Animal{};// 驼类
class Tuo:public Animal{};// 羊驼类
class SheepTuo:public Sheep,public Tuo{};void test01() {SheepTuo st;st.Sheep::m_Age 18;st.Tuo::m_Age 28;// 当菱形继承,两个父类拥有相同数据,需要加以作用域区分coutst.Sheep::m_Age st.Sheep::m_Ageendl;//18coutst.Tuo::m_Age st.Tuo::m_Ageendl;//28// 这份数据我们知道,只要有一份就可以,菱形继承导致数据有两份,资源浪费}int main() {test01();return 0;
} 利用虚继承 解决菱形继承的问题继承之前 加上关键字 virtual 变为虚继承
#include iostream
using namespace std;// 动物类
class Animal{
public:int m_Age;
};// 利用虚继承 解决菱形继承的问题
// 继承之前 加上关键字 virtual 变为虚继承
// Animal类称为 虚基类// 羊类
class Sheep:virtual public Animal{};// 驼类
class Tuo:virtual public Animal{};// 羊驼类
class SheepTuo:public Sheep,public Tuo{};void test01() {SheepTuo st;st.Sheep::m_Age 18;st.Tuo::m_Age 28;// 当菱形继承,两个父类拥有相同数据,需要加以作用域区分coutst.Sheep::m_Age st.Sheep::m_Ageendl;//28coutst.Tuo::m_Age st.Tuo::m_Ageendl;//28coutst.m_Age st.m_Ageendl;//28// 这份数据我们知道,只要有一份就可以,菱形继承导致数据有两份,资源浪费}int main() {test01();return 0;
}
总结
菱形继承带来的主要问题是子类继承两份相同的数据导致资源浪费以及毫无意义利用虚继承可以解决菱形继承问题
4.7 多态
4.7.1 多态的基本概念
多态是C面向对象三大特性之一
多态分为两类
静态多态函数重载和运算符重载属于静态多态复用函数名动态多态派生类和虚函数实现运行时多态
静态多态和动态多态区别
静态多态的函数地址早绑定 - 编译阶段确定函数地址动态多态的函数地址晚绑定 - 运行阶段确定函数地址
#include iostream
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;}
};// 执行说话的函数
// 地址早绑定 在编译阶段确定函数地址
// 如果想执行让猫说话,那么这个函数地址就不能提前绑定,需要在运行阶段进行绑定,
// 地址晚绑定// 动态多态满足条件
// 1.有继承关系
// 2.子类要重写父类的虚函数// 动态多态使用
// 父类的指针或者引用,指向子类对象void doSpeak(Animal animal) { // Animal animal cat;animal.speak();
}void test01() {Cat cat;doSpeak(cat);Dog dog;doSpeak(dog);
}int main() {test01(); return 0;
}
总结
多态满足条件
有继承关系子类重写父类的虚函数
多态使用条件
父类指针或引用指向子类对象
重写函数返回值类型 函数名 参数列表 完全一致称为重写
多态的原理剖析 当子类重写父类的虚函数子类中的虚函数表内部会替换成子类的虚函数地址。当父类的指针或者引用指向子类对象时候发生多态。
Animal animal cat;
animal.speak() 4.7.2 多态案例一.计算器类
案例描述
分别利用普通写法和多态技术设计实现两个操作数进行运算的计算器类
多态的优点
代码组织结构清晰可读性强利于前期和后期的扩展以及维护
#include iostream
using namespace std;// 分别利用普通写法和多态技术实现计算器
// 普通写法
class Calculator {
public:int getResult(string oper) {if(oper ) {return m_Num1 m_Num2;}else if(oper -) {return m_Num1 - m_Num2;}else if(oper *) {return m_Num1 * m_Num2;}// 如果想扩展新的功能,需要修改源码// 在真实开发中 提倡 开闭原则// 开闭原则: 对扩展进行开放,对修改进行关闭}int m_Num1;// 操作数1int m_Num2;// 操作数2
};void test01() {// 创建计算器对象Calculator cal;cal.m_Num1 10;// 操作数1为10cal.m_Num2 10;// 操作数2为10coutcal.getResult()endl;// 输出20coutcal.getResult(-)endl;// 输出0coutcal.getResult(*)endl;// 输出100
}// 利用多态实现计算器
// 多态好处:
// 1.组织结构清晰
// 2.可读性强
// 3.对于前期和后期扩展以及维护性高// 实现计算器抽象类
class AbstractCalculator {
public:virtual int getResult() {return 0;}int m_Num1;// 操作数1int m_Num2;// 操作数2
};// 加法计算器类
class AddCalculator:public AbstractCalculator {
public:int getResult() {return m_Num1 m_Num2;}
};// 减法计算器类
class SubCalculator:public AbstractCalculator {
public:int getResult() {return m_Num1 - m_Num2;}
};// 乘法计算器类
class MulCalculator:public AbstractCalculator {
public:int getResult() {return m_Num1 * m_Num2;}
};void test02() {// 多态使用条件// 父类指针或者引用指向子类对象// 加法运算AbstractCalculator* abc new AddCalculator;abc-m_Num1 10;// 操作数1为10abc-m_Num2 100;// 操作数2为100coutabc-m_Num1 abc-m_Num2abc-getResult()endl;// 输出20 // 用完后记得销毁delete abc;abc nullptr;// 减法运算abc new SubCalculator;abc-m_Num1 10;// 操作数1为10abc-m_Num2 100;// 操作数2为100coutabc-m_Num1 -abc-m_Num2abc-getResult()endl; delete abc;abc nullptr;// 乘法运算abc new MulCalculator;abc-m_Num1 10;// 操作数1为10abc-m_Num2 100;// 操作数2为100coutabc-m_Num1 *abc-m_Num2abc-getResult()endl;delete abc;abc nullptr;
}int main() {test02();return 0;
}
4.7.3 纯虚函数和抽象类
在多态中通常父类中虚函数的实现是毫无意义的主要都是调用子类重写的内容。因此可以将虚函数改为纯虚函数
纯虚函数语法virtual 返回值类型 函数名(参数列表) 0;
当类中有了纯虚函数这个类也称为抽象类
抽象类特点
无法实例化对象子类必须重写抽象类中的纯虚函数否则也属于抽象类
#include iostream
using namespace std;// 纯虚函数和抽象类
class Base{
public:// 纯虚函数// 只要有一个纯虚函数,这个类称为抽象类// 抽象类特点:// 1.无法实例化对象// 2.抽象类的子类 必须要重写父类中的纯虚函数,否则也属于抽象类virtual void func() 0; // 纯虚函数
};class Son:public Base{
public:void func() { cout Son::func() endl; }// 重写父类的纯虚函数
};void test01() {// Base b;// 抽象类无法实例化对象// new Base;//抽象类是无法实例化对象Son s;// 子类必须重写父类中的纯虚函数,否则无法实例化对象Base* base new Son;base-func();// Son::func()delete base;base NULL;
}int main() {test01();return 0;
}
4.7.4 多态案例二-制作饮品
案例描述制作饮品大致流程煮水-冲泡-倒入杯中-加入辅料
利用多态技术是实现本案例提供抽象制作饮品基类提供子类制作咖啡和茶叶
#include iostream
using namespace std;// 多态案例2: 制作饮品
class AbstractDrinking{
public:// 制作饮品virtual void boil() 0;// 冲泡virtual void brew() 0;// 倒入杯中virtual void pourInCup() 0;// 加入辅料virtual void putSomething() 0;// 制作饮品void makeDrink() {boil();brew();pourInCup();putSomething();}
};// 制作咖啡
class Coffee:public AbstractDrinking{
public:// 煮水void boil() {cout煮农夫山泉endl;}// 冲泡void brew() {cout冲泡咖啡endl;}// 倒入杯中void pourInCup() {cout倒入咖啡杯中endl;}// 加入辅料virtual void putSomething() {cout加入牛奶和糖endl;}
};// 制作红茶
class RedTee:public AbstractDrinking{
public:// 煮水void boil() {cout煮怡宝endl;}// 冲泡void brew() {cout冲泡茶叶endl;}// 倒入杯中void pourInCup() {cout倒入茶杯中endl;}// 加入辅料virtual void putSomething() {cout加入冰糖和柠檬endl;}
};// 制作函数
// AbstractDrinking* drinkings new Coffee
void doWork(AbstractDrinking *drinking) { drinking-makeDrink();delete drinking;// 释放drinking nullptr;
}void test01() {// 制作咖啡doWork(new Coffee);coutendl;// 制作红茶doWork(new RedTee);
}int main() {test01();return 0;
}
执行结果
PS D:\Work\c\bin .D:/Work/c/bin/app.exe
煮农夫山泉
冲泡咖啡
倒入咖啡杯中
加入牛奶和糖煮怡宝
冲泡茶叶
倒入茶杯中
加入冰糖和柠檬
PS D:\Work\c\bin
4.7.5 虚析构和纯虚析构
多态使用时如果子类中有属性开辟到堆区那么父类指针在释放时无法调用到子类的析构代码
解决方式将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性
可以解决父类指针释放子类对象都需要有具体的函数实现
虚析构和纯虚析构区别
如果是纯虚析构该类属于抽象类无法实例化对象
虚析构语法 virtual ~ 类名() {}
纯虚析构语法virtual ~ 类名() 0;
类名::类名(){}
#include iostream
using namespace std;
#include string// 虚析构和纯虚析构
class Animal{
public:// 纯虚函数Animal() { coutAnimal 构造函数调用 endl; }virtual void speak() 0;// 利用虚析构可以解决 父类指针释放子类对象的时不干净的问题// virtual ~Animal(){ coutAnimal 虚析构函数调用 endl; }// 纯虚析构 需要声明也需要实现// 有了纯虚析构之后,这个类也属于抽象类,无法实例化对象virtual ~Animal() 0;
};Animal::~Animal() {coutAnimal 纯虚析构函数调用 endl;
}class Cat:public Animal{
public:Cat(string name) {coutCat 构造函数调用 endl;m_Name new string(name); // 动态分配内存}void speak(){cout *m_Name 小猫在说话 endl;}~Cat(){cout Cat 析构函数调用 endl;if(m_Name ! NULL) {delete m_Name;m_Name NULL;cout释放m_Name内存endl;} }string* m_Name;
};void test01() {Animal* animal new Cat(Tom); animal-speak();// 父类指针在析构时候,不会调用子类中析构函数,导致子类如果有堆区属性,出现内存泄漏delete animal; // 父类指针指向子类对象调用子类的析构函数animal NULL; // 防止野指针
}int main() {test01();return 0;
}
执行结果
PS D:\Work\c\bin .D:/Work/c/bin/app.exe
Animal 构造函数调用
Cat 构造函数调用
Tom小猫在说话
Cat 析构函数调用
释放m_Name内存
Animal 纯虚析构函数调用
PS D:\Work\c\bin
总结
虚析构或纯虚析构就是用来解决通过父类指针释放子类对象如果子类中没有堆区数据可以不写为虚析构或纯虚析构拥有纯虚析构函数的类也属于抽象类
4.7.6 多态案例三-电脑组装
案例描述电脑主要组成部件为CPU用于计算显卡用于显示内存条用于存储。将每个零件封装出抽象基类并且提供不同的厂商生产不同的零件例如Intel厂商和Lenovo厂商。创建电脑类提供让电脑工作的函数并且调用每个零件工作的接口测试时组装三台不同的电脑进行工作。 #include iostream
#include string
using namespace std;// 目的:抽象不同的零件类// 抽象的CPU类
class CPU {
public:// 抽象的计算函数virtual void calculate() 0;
};// 抽象的显卡类
class VideoCard {
public:// 抽象的显示函数virtual void display() 0;
};// 抽象的内存条类
class Memory {
public:// 抽象的存储函数virtual void storage() 0;
};// 具体零件厂商 Intel厂商
class IntelCPU : public CPU {
public:void calculate() {cout IntelCPU 计算中... endl;}
};class IntelVideoCard : public VideoCard {
public:void display() {cout IntelVideoCard 显示中... endl;}
};class IntelMemory : public Memory {
public:void storage() {cout IntelMemory 存储中... endl;}
};// 具体零件厂商 Lenovo厂商
class LenovoCPU : public CPU {
public:void calculate() {coutLenovoCPU 计算中... endl;}
};class LenovoVideoCard : public VideoCard {
public:void display() {cout LenovoVideoCard 显示中... endl;}
};class LenovoMemory : public Memory {
public:void storage() {cout LenovoMemory 存储中...endl;}
};// 抽象的电脑类
class Computer {
public:// 电脑需要CPU、显卡、内存条// 构造函数中传入三个零件指针Computer(CPU* cpu, VideoCard* videoCard, Memory* memory){m_cpu cpu;m_videoCard videoCard;m_memory memory;}// 提供工作的函数,调用每个零件工作的接口void doWork() {m_cpu-calculate();m_videoCard-display();m_memory-storage();}// 提供析构函数 释放三个电脑零件~Computer() {cout Computer 析构 endl;if(m_cpu ! NULL) { // 释放CPU零件delete m_cpu;m_cpu NULL;cout释放m_cpuendl;}if(m_videoCard ! NULL) { // 释放显卡零件delete m_videoCard;m_videoCard NULL;cout释放m_videoCardendl;}if(m_memory ! NULL) { // 释放内存条零件delete m_memory;m_memory NULL;cout释放m_memoryendl;}}
private:CPU* m_cpu; // CPU的零件指针VideoCard* m_videoCard; // 显卡零件指针Memory* m_memory; // 内存条零件指针
};void test01() {// 第一台电脑组装和工作~cout第一台电脑组装和工作~~endl;CPU* cpu new LenovoCPU;VideoCard* videoCard new LenovoVideoCard;Memory* memory new LenovoMemory;// 创建一个Lenovo电脑Computer* computer1 new Computer(cpu, videoCard, memory);computer1-doWork();delete computer1;computer1 NULL;coutendl;// 第二台电脑组装和工作~cout第二台电脑组装和工作~endl;cpu new IntelCPU;videoCard new IntelVideoCard;memory new IntelMemory;// 创建一个Intel电脑Computer* computer2 new Computer(cpu, videoCard, memory); computer2-doWork();delete computer2;computer2 NULL;coutendl;// 第三台电脑组装和工作~cout第三台电脑组装和工作~endl;cpu new IntelCPU;videoCard new LenovoVideoCard;memory new IntelMemory;// 创建一个Intel电脑Computer* computer3 new Computer(cpu, videoCard, memory); computer3-doWork();delete computer3;computer3 NULL;
}int main() {test01();return 0;
}
执行结果
PS D:\Work\c\bin .D:/Work/c/bin/app.exe
第一台电脑组装和工作~~
LenovoCPU 计算中...
LenovoVideoCard 显示中...
LenovoMemory 存储中...
Computer 析构
释放m_cpu
释放m_videoCard
释放m_memory第二台电脑组装和工作~
IntelCPU 计算中...
IntelVideoCard 显示中...
IntelMemory 存储中...
Computer 析构
释放m_cpu
释放m_videoCard
释放m_memory第三台电脑组装和工作~
IntelCPU 计算中...
LenovoVideoCard 显示中...
IntelMemory 存储中...
Computer 析构
释放m_cpu
释放m_videoCard
释放m_memory
PS D:\Work\c\bin
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/927164.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!