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-150
void 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_L=l;}
// 获取长
int getL(){return m_L;}
// 设置宽
void setW(int w){m_W=w;}
// 获取宽
int getW(){return m_W;}
// 设置高
void setH(int h){m_H=h;}
// 获取高
int getH(){return m_H;}
// 获取立方体面积
int getArea(){return 2*(m_L*m_W+m_L*m_H+m_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,否则返回false
else 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);
cout<<"c1的面积为:"<<c1.getArea()<<endl;
cout<<"c1的体积为:"<<c1.getVolume()<<endl;
cout<<"***************************"<<endl;
cout<<"c2的面积为:"<<c2.getArea()<<endl;
cout<<"c2的体积为:"<<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:
// 设置x
void setX(int x) {m_X = x;}
// 获取x
int getX() {return m_X;}
// 设置y
void setY(int y) {m_Y = y;}
// 获取y
int 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:
// 设置x
void setX(int x);
// 获取x
int getX();
// 设置y
void setY(int y);
// 获取y
int 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() {cout<<"Person 构造函数的调用"<<endl;}
// 2.析构函数 进行清理的操作
// 没有返回值 不写void
// 函数名和类名相同 在名称前加~
// 析构函数不可以有参数的,不可以发生重载
// 对象在销毁前,会自动调用析构函数,而且只会调用一次
~Person () {cout<<"Person 析构函数的调用"<<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(){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() {
// 1. (括号法)
Person p1;// 默认构造调用
Person p2(10);// 有参构造调用
Person p3(p2);// 拷贝构造调用
cout<< "p2的年龄是: "<<p2.m_age<<endl;
cout<< "p3的年龄是: "<<p3.m_age<<endl;
// 注意事项1
// 调用默认构造函数时候,不要加()
// 因为下面这行代码,编译器会认为是一个函数的声明,不会认为在创建对象
// Person p1();
// void func();
}
void test02() {
// 2.显示法
Person p1;
Person p2 = Person(10);// 有参构造
Person p3 = Person(p2);// 拷贝构造
Person(20);// 匿名对象 特点:当前行执行结束后,系统会立即回收掉匿名对象
cout<<"asasasa"<<endl;
// 注意事项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++中拷贝构造函数调用时机通常有三种情况
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数参数传值
- 以值方式返回局部对象
#include<iostream>
using namespace std;
// 拷贝构造函数调用时机
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;
private:
};
// 1.使用一个已经创建完毕的对象来初始化一个新对象
void test01() {
Person p1(20);
Person p2(p1);
cout<<"p2: "<<p2.m_Age<<endl;
/*
Person有参构造函数调用
Person拷贝构造函数调用
p2: 20
Person析构函数调用
Person析构函数调用
*/
}
// 2.值传递的方式给函数参数传值
void doWork(Person p) {// 值传递会拷贝一个临时的副本出来,在调用它的拷贝构造函数,
}
void test02() {
Person p;
doWork(p);
/*
Person默认构造函数调用
Person拷贝构造函数调用
Person析构函数调用
Person析构函数调用
*/
}
// 3.以值方式返回局部对象
Person doWork2() {
Person p1;
cout<<(int*)&p1<<endl;
return p1;
}
void test03() {
Person p = doWork2();
cout<<(int*)&p<<endl;
}
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);
cout<<"p2的年龄为: "<<p2.m_Age<<endl;
}
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);
cout<<"p2的年龄为: "<<p2.m_Age<<endl;
}
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 深拷贝与浅拷贝
深拷贝是面试经典问题,也是常见的一个坑
- 浅拷贝:简单的赋值拷贝操作
- 深拷贝:在堆区重新申请空间,进行拷贝操作
#include<iostream>
using namespace std;
// 深拷贝与浅拷贝
class Person {
public:
Person() { cout<<"Person的默认构造函数调用"<<endl; }
Person(int age,int height) {
m_Age = age;
m_Height = new int(height);
cout<<"Person的有参构造函数调用"<<endl;
}
// 自己实现拷贝构造函数,解决浅拷贝带来的问题
Person(const Person& p) {
cout<<"Person的拷贝构造函数调用"<<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;
}
cout<<"Person的析构函数调用"<<endl;
}
int m_Age;
int* m_Height;// 身高
};
void test01() {
// 深拷贝,p1走p1的析构,p2走p2的析构.注意:堆栈是先进后出的
// 浅拷贝,有交叉重复释放的问题
Person p1(18,160);
cout<<"p1的年龄: "<<p1.m_Age<<"身高: "<<*p1.m_Height<<endl;
Person p2(p1);// 默认的拷贝构造函数(浅拷贝)
cout<<"p2的年龄: "<<p2.m_Age<<"身高: "<<*p2.m_Height<<endl;
}
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;
cout<<"Phone有参构造调用"<<endl;
}
~Phone() { cout<<"Phone的析构函数调用"<<endl; }
string m_PName;// 品牌名称
};
// 人类
class Person {
public:
// Phone m_Phone = pName 隐式转换法
Person(string name, string pName) : m_name(name), m_phone(pName) {
cout<<"Person有参构造调用"<<endl;
}
~Person() { cout<<"Person的析构函数调用"<<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; // 100
Person p2;
p2.m_A = 400;
cout << "m_A = " << p.m_A << endl; // 400
}
void test02() {
// 静态成员变量,不属于某个对象上,所有对象都共享同一份数据
// 因此静态成员变量有两种访问方式
// 1.通过对象进行访问
// Person p;
// cout<<"p.m_A = "<<p.m_A<<endl; // 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属性
cout<<"static void func调用"<<endl;
}
static int m_A;// 静态成员变量
int m_B;// 非静态成员变量
// 静态成员函数也是有访问权限的
private:
static void func2() { cout<<"static 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++编译器会给每个空对象特分配一个字节空间,是为了区分空对象
// 占内存的位置
// 每个空对象也应该有一个独一无二的内存地址
cout<<"size of p = " <<sizeof(p)<<endl; // 1
}
void test02() {
Person p;
cout<<"size 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对象,年龄为18
cout <<"p1的年龄为: "<<p1.m_Age << endl; // 输出Person对象的年龄
}
// 2.返回对象本身用*this
void test02() {
Person p1(10); // 创建一个Person对象,年龄为10
Person p2(20); // 创建另一个Person对象,年龄为20
// 链式编程思想
p2.PersonAddPerson(p1).PersonAddPerson(p1).PersonAddPerson(p1);
cout<<"p2的年龄为: "<<p2.m_Age<<endl;
}
int main() {
test02();
return 0;
}
4.3.3 空指针访问成员函数
- C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针
- 如果用到this指针,需要加以判断保证代码的健壮性
#include <iostream>
using namespace std;
// 空指针调用成员函数
class Person {
public:
void showClassName() {
cout<<"this is Person Class"<<endl;
}
void showPersonAge() {
// 常见报错原因:传入的指针是NULL
if(this == NULL) return;
cout<<"Age = "<<this->m_Age<<endl;
}
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;//error
p.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_SittingRoom<<endl;
cout<<"好基友全局函数 正在访问 : "<<building->m_BedRoom<<endl;
}
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() {
cout<<"visit 函数正在访问: "<<building->m_SittingRoom<<endl;
cout<<"visit 函数正在访问: "<<building->m_BedRoom<<endl;
}
void GoodGay::visit2() {
cout<<"visit 函数正在访问: "<<building->m_SittingRoom<<endl;
}
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;
cout<<"p3.m_A = " << p3.m_A<<endl;
cout<<"p3.m_B = " << p3.m_B<<endl;
cout<<"p4.m_A = " << p4.m_A<<endl;
cout<<"p4.m_B = " << p4.m_B<<endl;
}
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) 简化版本 p<<cout
// 不会利用成员函数重载<<运算符,因为实现cout在左侧
// void operator<<(Person &p) {
// }
private:
int m_A;
int m_B;
};
// 只能利用全局函数重载左移运算符
ostream & operator<<(ostream &cout,Person &p) // 本质 operator<<(cout,p) 简化 cout<<p
{
cout<<"m_A = "<<p.m_A<<" , m_B = "<<p.m_B;
return cout;
}
void test01() {
Person p(10,20);
cout<<p<<", 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) {
cout<<myint.m_Num;
return cout;
}
void test01() {
MyInterger myint;
cout<<++(++myint)<<endl;
cout<<myint<<endl;
}
void test02() {
MyInterger myint;
cout<<myint++<<endl;
cout<<myint;
}
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;// 赋值操作
cout<<"p1 的年龄为: "<<*p1.m_Age<<endl;
cout<<"p2 的年龄为: "<<*p2.m_Age<<endl;
cout<<"p3 的年龄为: "<<*p3.m_Age<<endl;
}
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) {
cout<<"p1 和 p2 是相等的"<<endl;
}
if(p1 != p3) {
cout<<"p1 和 p3 是不相等的"<<endl;
}else{
cout<<"p1 和 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){
cout<<test<<endl;
}
};
void MyPrint02(string test) {
cout<<test<<endl;
}
void test01() {
MyPrint myPrint;
myPrint("hello,world"); // 由于使用起来非常类似于函数调用,因此称为仿函数
MyPrint02("hello,heheda");
}
// 仿函数非常灵活,没有固定的写法
class MyAdd{
public:
int operator()(int num1,int num2){
return num1+num2;
}
};
void test02() {
MyAdd myadd;
int ret = myadd(100,200);
cout<<"ret = "<<ret<<endl;
// 匿名函数对象
cout<<MyAdd()(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 继承中的对象模型
- 问题:从父类继承过来的成员,哪些属于子类对象中?
#include<iostream>
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
// 父类中所有非静态成员属性都会被子类继承下去
// 父类中私有成员属性 是被编译器给隐藏了,因此是访问不到,但是确实被继承了
cout<<"size 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;//100
cout << "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) {
cout<<"Base func(int a)"<<endl;
}
};
int Base::m_A = 100;
class Son:public Base{
public:
static int m_A;
static void func() {
cout<<"Son func()"<<endl;
}
};
int Son::m_A = 200;
// 同名的静态成员属性
void test01() {
Son s;
// 1.通过对象访问
cout<<"通过对象访问: "<<endl;
cout<<"Son 下的 m_A = "<<s.m_A<<endl;
cout<<"Base 下的 m_A = "<<s.Base::m_A<<endl;
// 2.通过类名访问
cout<<"通过类名访问: "<<endl;
cout<<"Son 下的 m_A = "<<Son::m_A<<endl;
// 第一个::代表通过类名方式访问
// 第二个::代表访问父类作用域下
cout<<"Base 下的 m_A = "<<Son::Base::m_A<<endl;
}
// 同名的静态成员函数
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;
cout<<"size of Son: "<<sizeof(s)<<endl;
// 当父类中出现同名成员需要加作用域区分
cout<<"Base1::m_A = "<<s.Base1::m_A<<endl;//100
cout<<"Base2::m_A = "<<s.Base2::m_A<<endl;//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;
// 当菱形继承,两个父类拥有相同数据,需要加以作用域区分
cout<<"st.Sheep::m_Age = "<<st.Sheep::m_Age<<endl;//18
cout<<"st.Tuo::m_Age = "<<st.Tuo::m_Age<<endl;//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;
// 当菱形继承,两个父类拥有相同数据,需要加以作用域区分
cout<<"st.Sheep::m_Age = "<<st.Sheep::m_Age<<endl;//28
cout<<"st.Tuo::m_Age = "<<st.Tuo::m_Age<<endl;//28
cout<<"st.m_Age = "<<st.m_Age<<endl;//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;// 操作数1
int m_Num2;// 操作数2
};
void test01() {
// 创建计算器对象
Calculator cal;
cal.m_Num1 = 10;// 操作数1为10
cal.m_Num2 = 10;// 操作数2为10
cout<<cal.getResult("+")<<endl;// 输出20
cout<<cal.getResult("-")<<endl;// 输出0
cout<<cal.getResult("*")<<endl;// 输出100
}
// 利用多态实现计算器
// 多态好处:
// 1.组织结构清晰
// 2.可读性强
// 3.对于前期和后期扩展以及维护性高
// 实现计算器抽象类
class AbstractCalculator {
public:
virtual int getResult() {
return 0;
}
int m_Num1;// 操作数1
int 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为10
abc->m_Num2 = 100;// 操作数2为100
cout<<abc->m_Num1 <<"+"<<abc->m_Num2<<"="<<abc->getResult()<<endl;// 输出20
// 用完后记得销毁
delete abc;
abc = nullptr;
// 减法运算
abc = new SubCalculator;
abc->m_Num1 = 10;// 操作数1为10
abc->m_Num2 = 100;// 操作数2为100
cout<<abc->m_Num1 <<"-"<<abc->m_Num2<<"="<<abc->getResult()<<endl;
delete abc;
abc = nullptr;
// 乘法运算
abc = new MulCalculator;
abc->m_Num1 = 10;// 操作数1为10
abc->m_Num2 = 100;// 操作数2为100
cout<<abc->m_Num1 <<"*"<<abc->m_Num2<<"="<<abc->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);
cout<<"================"<<endl;
// 制作红茶
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() { cout<<"Animal 构造函数调用" <<endl; }
virtual void speak() = 0;
// 利用虚析构可以解决 父类指针释放子类对象的时不干净的问题
// virtual ~Animal(){ cout<<"Animal 虚析构函数调用" <<endl; }
// 纯虚析构 需要声明也需要实现
// 有了纯虚析构之后,这个类也属于抽象类,无法实例化对象
virtual ~Animal() = 0;
};
Animal::~Animal() {
cout<<"Animal 纯虚析构函数调用" <<endl;
}
class Cat:public Animal{
public:
Cat(string name) {
cout<<"Cat 构造函数调用" << 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() {
cout<<"LenovoCPU 计算中..." << 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_cpu"<<endl;
}
if(m_videoCard != NULL) { // 释放显卡零件
delete m_videoCard;
m_videoCard = NULL;
cout<<"释放m_videoCard"<<endl;
}
if(m_memory != NULL) { // 释放内存条零件
delete m_memory;
m_memory = NULL;
cout<<"释放m_memory"<<endl;
}
}
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;
cout<<"===================================="<<endl;
// 第二台电脑组装和工作~
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;
cout<<"===================================="<<endl;
// 第三台电脑组装和工作~
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>