1. 什么是类和对象?
C++面向对象的三大特性为:封装、继承、多态
C++认为万事万物都皆为对象,对象上有其属性和行为
例如:
人可以作为对象,属性有姓名、年龄、身高、体中...,行为有走、跑、跳、吃饭、唱歌...
车也可以作为对象,属性有轮胎、方向盘、车灯...,行为有载人、放音乐、开空调...
具有相同性质的对象,我们可以抽象称为类,人属于人类,车属于车类
在编程中,class(类) 是面向对象编程(OOP)的核心概念之一,用于描述具有相同属性和方法的对象的抽象模板。以下是关于 class
的详细解释:
2.1 基本定义
- 类 是一个蓝图或模板,定义了对象的数据结构(属性)和行为(方法)。
- 通过类可以创建多个实例(对象),每个实例拥有独立的属性值,但共享类定义的方法。
示例:
//定义类
//设计一个人类
class person
{
//私有权限
private:
//属性(成员变量)
string name;
int age;
//行为(成员函数)
//改名字
void Setname(string n)
{
name = n;
}
//获取名字
string Getname()
{
return name;
}
//获取年龄
int Getage()
{
return age;
}
//吃饭
void Eat()
{
cout << "吃饭" <<endl;
}
//睡觉
void Sleep()
{
cout << "睡觉" <<endl;
}
}
2.2 核心特性
-
封装(Encapsulation)
将数据(属性)和操作数据的方法绑定在一起,隐藏内部实现细节,仅暴露必要接口。 -
继承(Inheritance)
子类可以继承父类的属性和方法,实现代码复用和层次化设计。
-
多态(Polymorphism)
不同类的对象可以对同一方法调用做出不同响应(通过方法重写或接口实现)。
2.3 类的常见组成部分
组成部分 | 说明 |
---|---|
属性 | 类的变量(如 name , age ),存储对象的状态。 |
方法 | 类的函数(如 bark() ),定义对象的行为。 |
构造函数 | __init__ (Python)或类名(Java/C++),用于初始化对象。 |
类变量 | 所有实例共享的变量(如 Dog.species = "Canis" )。 |
静态方法 | 不依赖实例的方法,用 @staticmethod 修饰(Python)。 |
2.4 应用场景
- 建模现实实体(如用户、商品、订单)。
- 设计框架/库(如Django的
Model
类、TensorFlow的神经网络层)。 - 实现设计模式(如单例模式、工厂模式)。
2. 封装
封装是面向对象编程(OOP)的核心概念之一,它的核心思想是 将数据(属性)和操作数据的方法(行为)结合在一起封装成(类),并隐藏对象的内部实现细节,仅对外公开可控的访问接口来和对象进行交互。 简单来说就是套壳屏蔽细节啦~ 并且封装通过限制对对象内部状态的直接访问,提高了代码的安全性、可维护性和灵活性。
2.1 封装的意义
封装是C++面向对象三大特性之一
封装的意义:
- 将属性和行为作为一个整体,表现生活中的事物
- 将属性和行为加以权限控制
封装意义一:
在设计类的时候,属性和行为写在一起,表现事物
语法: class 类名{ 访问权限: 属性 / 行为 };
类和对象之间的关系:类是权限访问、属性和行为的集合,而对象就是类的实例化。
其实类还有一些术语,这里说一下以后看见别人提到知道什么意思:
类中的属性和行为,我们统一称为成员
属性可以被叫做:成员属性、成员变量
行为可以被叫做:成员函数、成员方法
类的属性也可以被称为的成员变量,而方法则是类的成员函数,用于操作类的数据(成员属性)。
在类中属性一般都是变量的集合,而行为一般都是函数的集合。
示例1:设计一个圆类,求圆的周长
圆周长公式:2 * PI(圆周率) * 半径
我们将圆的半径作为圆类的属性,则求圆的周长使用函数实现作为圆类的行为,然后将他们与访问权限封装在一起最后就是圆类。
类的设计:
//圆周率
const double PI = 3.14;
//设计一个圆类
//class代表设计一个类,类后面紧跟着就是类的名称
class Circle
{
//访问权限
public: //公共权限
//属性
int m_r;//半径
//行为
//获取圆的周长
double calculateZC()
{
return 2 * PI * m_r;
}
};
访问权限:
public是类的访问权限,是公共权限(类内和类外都能调用类的属性和行为)
通过类创建对象(类的实例化):
int main()
{
//通过圆类,创建具体的圆(对象)
//对象也被称为类的实例化
Circle c1;
//给圆对象的属性进行赋值
c1.m_r = 10;
cout << "圆的周长为: " << c1.calculateZC() << endl;
system("pause");
return 0;
}
运行结果:
示例2:设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号
通过以上的题目我们得知两点,需要我们创建一个学生类,成员属性有姓名和学号,成员方法(行为)就是创建一个函数来打印姓名和学号。
类的设计:
using namespace std;
#include<string>
//设计一个学生类
class Student
{
//访问权限
public: //公共权限
//属性
string name;//姓名
int ID;//学号
//成员方法(行为)
//显示姓名和学号
void showStudent()
{
cout << "名字:" << name << " 学号:" << ID << endl;
}
};
通过类创建对象(类的实例化):
int main()
{
//通过学生类,创建具体的学生(对象)
Student s1;
//给学生对象的属性进行赋值
cin >> s1.name >> s1.ID;
s1.showStudent();
//再创建一个学生对象
Student s2;
//给学生对象的属性进行赋值
cin >> s2.name >> s2.ID;
s2.showStudent();
//一个类有创建个对象,但是类只有一个
system("pause");
return 0;
}
那我们可以通过设计一个成员方法(行为)函数给成员属性赋值吗?答案是可以的,我们可以将当前的学生类更改一下:
//设计一个学生类
class Student
{
//访问权限
public: //公共权限
//属性
string name;//姓名
int ID;//学号
//成员方法(行为)
//显示姓名和学号
void showStudent()
{
cout << "名字:" << name << " 学号:" << ID << endl;
}
void SetName(string Name1)
{
name = Name1;//内部赋值操作
}
void SetID(int id)
{
ID = id;//内部赋值操作
}
};
然后我们就有两种给属性赋值操作:
1. 直接访问赋值
2. 调用成员方法(行为)进行赋值
int main()
{
//通过学生类,创建具体的学生(对象)
Student s1;
//给学生对象的属性进行赋值
//两种赋值方法
//第一种:
cin >> s1.name >> s1.ID;
//第二种:
s1.SetName("张三");
s1.SetID(10);
s1.showStudent();
system("pause");
return 0;
}
总结:
1. 类(class)和对象:类的内部组成分别是访问权限、成员属性(属性)和成员方法(行为)、构造函数和析构函数等...,对象是类的实体,而类就是对象的模板。类定义了一类事物的共同特征,而对象则是这些特征的具体表现。类和对象的关系可以用图纸和建筑的比喻来形容:类就像是建筑的设计图纸,而对象则是根据图纸建造出来的房子,所以对象就是类的实例化
2. 属性和行为:在类中属性一般都是变量的集合,而行为一般都是函数的集合,行为(方法)就是操作数据(属性)的函数
3. 类的访问权限:类中的成员可以被声明为私有权限(private)、保护(protected)[&或公有权限(public)],权限决定类的外部和内部能否调用类的成员。
4. 封装:是类的语法特性,隐藏部分细节(属性),仅保留必要的接口(成员函数)。可以通过类的接口对类的一些属性进行读和写。
2.2 访问权限
封装意义二:
类在设计时,可以把属性和行为放在不同的权限下,加以控制
访问权限有三种:
- public 公共权限 成员 类内可以访问 类外可以访问
- protected 保护权限 成员 类内可以访问 类外不可以访问
- private 私有权限 成员 类内可以访问 类外不可以访问
如果仔细观察我们就可以发现 保护权限 和 私有权限 的访问权限是一样的,保护权限其实是继承知识点的访问权限。
- 保护权限:儿子可以访问父亲的保护内容
- 私有权限:儿子不可以访问父亲的私有内容
#include<iostream>
using namespace std;
#include<string>
//三种权限
//public 公共权限 成员 类内可以访问 类外可以访问
//protected 保护权限 成员 类内可以访问 类外不可以访问
//private 私有权限 成员 类内可以访问 类外不可以访问
class Person
{
//姓名 公共权限
public:
string m_name;
//汽车 保护权限
protected:
string m_car;
//银行卡密码 私有权限
private:
int m_password;
public:
void func()
{
m_name = "张三";//公共权限,类内部可以访问
m_car = "拖拉机";//保护权限,类内部可以访问
m_password = 1234;//私有权限,类内部可以访问
}
};
int main()
{
Person p1;
p1.m_name = "李四";//公共权限,类外部可以访问
p1.m_car = "奔驰";//保护权限,类外部不可以访问
p1.m_password = 123;//私有权限,类外部不可以访问
p1.func();//公共权限,类外部可以访问
system("pause");
return 0;
}
2.3 struct和class 区别
在C++中 struct和class唯一的区别就在于 默认的访问权限不同
区别:
- struct 默认权限为公共
- class 默认权限为私有
class C1
{
int m_A;//默认访问权限为私有
};
struct C2
{
int m_A;//默认访问权限为公共
};
int main()
{
C1 c1;
c1.m_A = 100;//错误,访问权限是私有
C2 c2;
c2.m_A = 100;//正确,访问权限是公共
system("pause");
return 0;
}
2.4 成员属性设置为私有
优点1:将所有成员属性设置为私有,可以自己控制读写权限
优点2:对于写权限,我们可以检测数据的有效性
1、自己控制读写权限
示例:
class Person
{
//将成员方法设置为公有,通过成员方法实现对成员属性的读和写
public:
//写入姓名
void SetName(string name)
{
m_name = name;
}
//读取姓名
string GetName()
{
return m_name;
}
//读取年龄
int Getage()
{
return m_age;
}
//写入偶像
void SetIdol(string Idol)
{
m_Idol = Idol;
}
//将成员属性设置私有权限
private:
string m_name;//姓名 可读可写
int m_age = 18; //年龄 只读
string m_Idol;//偶像 只写
};
int main()
{
Person p1;
p1.SetName("张三");
cout << "姓名:" << p1.GetName() << " 年龄:" << p1.Getage() << endl;
p1.SetIdol("super idol");
return 0;
}
如果成员属性都是公共权限那造成的问题就是谁都能读和写
但是当我们把成员属性设置为私有权限,使用成员方法(行为)实现对某些属性成员的读写操作
那达成的效果是如果这个属性有可以进行读写的成员方法我就可以读写,没有我就访问不了。可以准确的自己控制读和写。
2、 对于写权限,我们可以检测数据的有效性
class Person
{
//写入年龄
void Setage(int age)
{
//检测数据有效性
if(age >= 0 && age <= 150){
m_age = age;
}
else{
cout << "不符合正常年龄段" << endl;
}
}
//读取年龄
int Getage()
{
return m_age;
}
//将成员属性设置私有权限
private:
int m_age; //年龄 只读 + 可写
};
int main()
{
Person p1;
int age = 0;
cin >> age;
p1.Setage(age);//写操作
cout << "年龄:" << p1.Getage() << endl;//读操作
return 0;
}
上面的语法,就是真正意义上的封装
封装:是类的语法特性,隐藏部分细节(属性),仅保留必要的接口(成员函数)。可以通过类的接口对类的一些属性进行读和写。
1. 练习案例1:设计立方体类
设计立方体类(Cube)
求出立方体的面积和体积
L:长 W:宽 H:高
立方体面积公式:(L * W + L * H + W * H) * 2
立方体体积公式:L * W * H
分别用全局函数和成员函数判断两个立方体是否相等
立方体类的设计思路:我们要设计立方体类,长 宽 高三个就必然是成员属性,求出立方体的面积和体积就是类的成员方法或成员函数(行为)。
立方体类的设计:
//创建立方体类
class Cube
{
//类的成员方法(行为)/
public: //设置公共权限
//给长宽高写入值
void SetLWH(int L, int W, int H)
{
m_L = L;
m_W = W;
m_H = H;
}
//读取长宽高
int* GetLWH()
{
int* ret = new int[3];
ret[0] = m_L;
ret[1] = m_W;
ret[2] = m_H;
return ret;
}
//计算立方体面积
int calculateS()
{
return m_L * m_W * 2 + m_L * m_H * 2 + m_W * m_H * 2;
}
//计算立方体体积
int calculateV()
{
return m_L * m_W * m_H;
}
//判断两个立方体是否想等
bool isSamebyClass(Cube& c)
{
int* ret = c.GetLWH();
if (m_L == ret[0] && m_W == ret[1] && m_H == ret[2]) {
return true;
delete[] ret;
}
return false;
delete[] ret;
}
//类的成员属性
private://设置私有权限
int m_L;//长
int m_W;//宽
int m_H;//高
};
通过类创建对象:
int main()
{
//创建第一个立方体对象
Cube c1;
c1.SetLWH(10, 10, 10);//写入长宽高
int* a = c1.GetLWH();//读取长宽高
cout << "长:" << a[0] << " 宽:" << a[1] << " 高:" << a[2] << endl;
cout << "面积:" << c1.calculateS() << " 体积:" << c1.calculateV() << endl;//求立方体的面积和体积
//创建第二个立方体对象
Cube c2;
c2.SetLWH(10, 10, 10);
//比较两个立方体是否相等
bool ret = c1.isSamebyClass(c2);
if (ret) {
cout << "立方体c1和立方体c2相等" << endl;
}
else {
cout << "不相等" << endl;
}
delete[] a;
return 0;
}
运行结果:
2. 练习案例2:点和圆的关系
设计一个圆形类(Circle),和一个点类(Point),计算点和圆的关系
假设:圆心的坐标为(x1, y1),点的坐标为(x2, y2),圆和点之间的距离公式为:
解决思路:我们需要定义两个类,一个圆类,一个点类,还需要定义一个判断点和圆之间的关系的接口
圆类:
- 成员属性:半径、圆心;
- 成员方法:设置半径、获取半径、设置圆心、获取圆心
点类:
- 成员属性:坐标x、坐标y
- 成员方法:设置坐标x、显示坐标x、设置坐标y、显示坐标y
判断点和圆关系的接口函数:
计算出圆心和点的距离,然后将计算出的距离和半径比较,最后判断并打印他们的关系
点类设计:
//设计一个点的类
class Point
{
public:
//设置x坐标
void SetX(int x)
{
p_x = x;
}
//获取x坐标
int GetX()
{
return p_x;
}
//设置y坐标
void SetY(int y)
{
p_y = y;
}
//获取y坐标
int GetY()
{
return p_y;
}
private:
int p_x;//x坐标
int p_y;//y坐标
};
圆类设计:
//设计一个圆的类
class Circle
{
public:
//设置半径
void Setc_r(int r)
{
c_r = r;
}
//获取半径
int Getc_r()
{
return c_r;
}
//设置圆心
void SetCenter(Point p)
{
c_Center = p;
}
//获取圆心
Point GetCenter()
{
return c_Center;
}
private:
int c_r;//半径
//一个类内可以嵌套其他类
Point c_Center;//圆心
};
判断点和圆的接口函数:
//判断点和圆的关系
void isInCircle(Circle& c, Point& p)
{
//先获取点和圆心之间的距离
// ______________________________
//公式: \/(x1 - x2) ^ 2 + (y1 - y2) ^ 2
//开根号((x1 - x2) ^ 2 + (y1 - y2) ^ 2)
int distance = (c.GetCenter().GetX() - p.GetX()) * (c.GetCenter().GetX() - p.GetX()) +
(c.GetCenter().GetY() - p.GetY()) * (c.GetCenter().GetY() - p.GetY());
//接下来可以分两个方式比较
// 第一种:
//我们可以使用数学函数sqrt()给distance开根号,然后与半径比较
int dis = sqrt(distance);
//第二种;
//不用给distance开根号,我们只需算出半径的二次方与distance比较即可
int rDistance = c.Getc_r() * c.Getc_r();
if (rDistance < distance) {
cout << "在圆外" << endl;
}
else if (rDistance == distance) {
cout << "在圆上" << endl;
}
else {
cout << "在圆内" << endl;
}
}
调用:
int main()
{
Circle c1;//创建一个圆类对象
c1.Setc_r(10);
Point center;//创建一个圆心
center.SetX(10);
center.SetY(0);
c1.SetCenter(center);
Point p1;//创建一个点类对象
p1.SetX(10);
p1.SetY(11);
//创建并写好数据后就可以判断圆和点的关系
isInCircle(c1, p1);
return 0;
}
这一个示例有个比较特殊的一点就是一个类中可以嵌套其他类的对象。
private:
int c_r;//半径
//一个类内可以嵌套其他类
Point c_Center;//圆心
我们可以通过方法来设置这个对象。
总结:类是可以嵌套其他类的对象的
2.5 类声明在头文件
在以后大型开发,类如果都声明在一个文件中是不可以的,类和函数一样,是需要声明写在头文件中,实现就写在其他文件,我们想要调用就可以包含这个头文件就形。
就拿上面的圆类举个例子吧,比如我创建了一个圆类,我要将它的实现放到其他文件,我就需要头文件放入圆类的声明,然后其他文件放入圆类的实现,创建头文件Circle.h和源文件Circle.cpp
在Circle.h头文件声明格式是这样的:
//设计一个圆的类
class Circle
{
public:
//设置半径
void Setc_r(int r);
//获取半径
int Getc_r();
//设置圆心
void SetCenter(Point p);
//获取圆心
Point GetCenter();
private:
int c_r;//半径
//一个类内可以嵌套其他类
Point c_Center;//圆心
};
成员函数和自定义函数一样,声明时去掉函数体,就和函数声明一样。
在Circle.cpp源文件实现格式是这样的:
#include "Circle.h"
//设置半径
void Circle::Setc_r(int r)
{
c_r = r;
}
//获取半径
int Circle::Getc_r()
{
return c_r;
}
//设置圆心
void Circle::SetCenter(Point p)
{
c_Center = p;
}
//获取圆心
Point Circle::GetCenter()
{
return c_Center;
}
每个函数名前面加个Circle::是告诉编译器这个函数是圆类Circle里的成员函数,它的作用域是圆类里的作用域。如果不加Circle::编译器就会认为这个函数就是全局函数,不是我们的成员函数。
总结:
1. 类的声明:当类在头文件声明时,我只需要声明有什么成员变量,有什么成员函数和访问权限。但是成员函数要想函数声明一样不写函数体。
2. 类的实现:类的实现指的是类成员函数的实现,当我们在其他文件需要实现类的成员函数,就要在这个函数名前面加上一个类名::,告诉编译器这个函数是这个类作用域下面的成员函数,如果不加,编译器就会将这个函数看作全局函数。至于函数的访问权限嘛,我们不需要再额外的在这个函数上面添加,因为类中已经声明了成员函数的权限。
类声明只声明成员函数和成员变量,不提供成员函数的实现。
类的实现也就是成员函数的实现只需要实现成员函数就行,但是要记得声明它的类作用域类名::。
不管是成员变量还是成员函数,类型后面和名称前面的就是它的类作用域。
类型 类名:: 变量名/函数名(参数列表);
2.6 封装详解
C++中的**封装(Encapsulation)是面向对象编程(OOP)的三大核心特性之一(另两个是继承和多态),其核心思想是将数据(属性)和操作数据的方法(行为)**捆绑为一个独立的单元(即类),并通过访问控制隐藏内部实现细节。以下是封装的详细解析:
1. 封装的核心目的
- 数据保护:通过限制外部对类内部数据(属性) 的直接访问,防止数据(属性) 被意外修改或误用。
- 接口与实现分离:用户只需知道类提供的公开接口(API),无需关心内部如何实现。
- 代码可维护性:内部逻辑修改时,只要接口不变,外部代码无需调整。
2. 实现封装的关键机制
- 访问修饰符:
private
:仅类内部可访问(默认隐藏)。protected
:类内部和派生类可访问。public
:完全开放的外部接口。
- 示例代码:
class BankAccount { private: double balance; // 隐藏数据 public: void deposit(double amount) { // 公开方法 if (amount > 0) balance += amount; } double getBalance() { return balance; } // 受控访问 };
3. 封装的实际应用场景
- 数据验证:通过
setter
方法检查输入合法性(如负数存款)。 - 隐藏复杂逻辑:例如,
std::vector
封装了动态数组的内存管理细节。 - 兼容性扩展:内部数据结构变更(如数组→链表)不影响用户代码。
4. 常见误区与注意事项
- 过度封装:并非所有数据都需要
private
,简单POD(Plain Old Data)结构可适当放宽。 - 性能权衡:频繁的
getter/setter
调用可能影响性能,需根据场景优化。 - 设计平衡:封装应与单一职责原则(SRP)结合,避免“上帝类”。
封装不仅是语法特性,更是设计哲学。合理使用封装能显著提升代码的健壮性和可扩展性,尤其在大型项目中体现其价值。
我的总结: 简单来说封装就是一种语法特性,这个语法特性就是隐藏部分成员变量(属性),公开成员方法(行为),通过成员方法来操作成员变量,有可控性,不是所有的成员变量(属性)都能随便被外部访问,只有内部成员方法可操作,最后生成一个属性部分隐藏,提供部分接口操作属性的类,这就是封装。
3. 对象特性(对象的初始化和清理)
- 生活中我们买的电子产品都基本会有出厂设置,在某一天我们不用的时候也会删除一些自己信息数据保证安全
- C++中的面向对象来源于生活,每个对象也都会有初始设置以及 对象销毁前的清理数据的设置
3.1 构造函数和析构函数
对象的初始化和清理也是两个非常重要的安全问题
- 一个对象或者变量没有初始状态,对其使用后果是未知
- 同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题
C++利用了构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。
对象的初始化和清理工作时编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供编译器提供的构造函数和析构函数是空实现。
- 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
- 析构函数:主要作用于对象销毁前系统自动调用,执行一些清理工作。
构造函数语法: 类名( ){ }
- 构造函数,没有返回值类型也不写void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时候会自动调用构造,无需手动调用,而且只会调用一次
析构函数语法: ~类名( ){ }
- 析构函数,没有返回值类型也不写void
- 函数名称与类名相同,在名称前加上波浪号~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
看下面构造函数和析构函数的定义:
class person
{
public:
//构造函数
//构造函数会在调用对象时调用
person()
{
cout << "person 构造函数的调用" << endl;
}
//析构函数
//析构函数会在对象销毁前调用
~person()
{
cout << "person 析构函数的调用" << endl;
}
};
构造函数会在调用对象时调用,析构函数会在对象销毁前调用,我们可以来测试一下:
void test01()
{
person p;
}
int main()
{
test01();
system("pause");//按任意键继续执行
return 0;
}
运行代码:
int main()
{
//test01();
person p;
system("pause");//按任意键继续执行
return 0;
}
运行代码:
函数调用对象:函数调用时调用对象,当函数销毁对象也会销毁所以构造函数和析构函数都调用了。
主函数调用对象:在主函数内部我们写了一个pause指令,按任意键才继续执行。所以我们不按任意键程序就会一直停在那里,不会向后指向return语句。所以我们在主函数内部调用对象时只执行了构造函数并打印。
总结:
- 构造函数可以有参数,因此可以发生重载。析构函数不可以有参数,因此不可以发生重载
- 程序在调用对象时候会自动调用构造,程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
- 就算我们没有创建构造函数和析构函数,编译器也会在我们调用对象时调用编译器的构造函数和析构函数,只不过里面都是空实现。
3.2 构造函数的分类及调用
两种分类方式:
- 按参数分为:有参构造和无参构造
- 按类型分为:普通构造和拷贝构造
三种调用方式:
- 括号法
- 显示法
- 隐式转换法
重点:当一个类中有多种不同参数的构造函数就被称为构造函数重载。
拷贝构造函数的存在:一个类可以创建多个对象,假设一个对象属性已经有一些特定的值,我们想再创建一个属性和那个对象一模一样的对象,我们就可以创建一个对象,让这个对象调用内部的拷贝构造函数,参数就是那个有属性的对象,然后将那个对象的属性拷贝一份到这个对象的属性上,所以,拷贝构造函数的存在是为了让同类对象可以拷贝属性。
3.2.1 拷贝构造函数的形参注意
在C++中,拷贝构造函数的参数必须使用引用(通常是const
引用),这是由语言的设计逻辑和实际需求共同决定的。以下是关键原因分析:
根本原因:避免无限递归
值传递的致命缺陷
- 如果拷贝构造函数的参数是值传递(
MyClass(MyClass obj)
),当调用拷贝构造时:- 需要先复制实参
obj
到形参,这又会触发拷贝构造函数 - 导致无限递归调用,直到栈溢出崩溃。
- 需要先复制实参
- 示例:
class MyClass { public: MyClass(MyClass obj) {} // 错误!值传递会引发递归 }; class MyClass { public: MyClass(const MyClass& obj) {} //正确,传的是地址,就不会引发无限递归 };
具体递归过程
- 第一次调用:外部代码尝试拷贝构造一个
MyClass
对象(例如通过函数传参或返回值)。- 值传递触发拷贝:编译器发现
MyClass
的拷贝构造函数参数是值传递,需要先创建一个临时副本。- 递归开始:为创建这个副本,又需要调用拷贝构造函数,而参数依然是值传递,循环重复。
- 栈溢出:递归无限进行,直到栈空间耗尽,程序崩溃。
通俗易懂的来讲如果拷贝构造函数使用值传递,因为这个参数是(对象),所以会先给该对象创建一个临时副本,然后再看这个对象里面还有一个拷贝构造函数是值传递,然后继续创建临时副本,所以会导致无限循环,直到栈区耗空为止。
引用传递的解决方案
- 使用引用(
MyClass(const MyClass& obj)
)直接绑定到原对象,避免复制形参,从而切断递归链条。
3.2.2 构造函数分类
重点:当一个类中有多种不同参数的构造函数就被称为构造函数重载。
class person
{
public:
//无参构造函数
person()
{
cout << "person 无参构造函数的调用" << endl;
}
//有参构造函数
person(int a)
{
age = a;
cout << "person 有参构造函数的调用" << endl;
}
//拷贝构造函数
person(const person& p)
{
age = p.age;
cout << "person 拷贝构造函数的调用" << endl;
}
//析构函数
//析构函数会在对象销毁前调用
~person()
{
cout << "person 析构函数的调用" << endl;
}
private:
int age;
};
3.2.3 构造函数重载的调用方式
在C++中,**构造函数的重载(重构)**允许通过不同方式初始化对象,以下是其调用方式与关键注意事项的总结:
1. 括号法(最常用)
//调用默认参数(无参)构造函数
person p1;
//调用有参构造函数
person p2(10);
//调用拷贝构造函数将p2的属性拷贝到p3
person p3(p2);
- 特点:直接明确,可读性强,适用于所有构造场景。
- 注意事项:调用无参重构函数时最好是只创建对象,后面不要添写括号。比如person p1();这样会导致编译器将它看作一个返回值类型为person的函数声明。
2. 显式转换法(C++11后推荐)
//调用默认参数(无参)构造函数
person p1;
//调用有参构造函数
person p2 = person(10);
//调用拷贝构造函数将p2的属性拷贝到p3
person p3 = person(p2);
- 用途:避免隐式转换歧义,增强代码安全性。
- 注意事项:匿名调用重载函数本身时匿名的,但是当person p2 = person(10);就不会被当做匿名,而是自动转换为person p2(10);使用对象p2覆盖
匿名调用重载函数:
//匿名调用重载函数 //匿名调用会导致刚创建好一个匿名对象并调用了有参重构就直接调用析构并销毁 person(10);
匿名调用会导致刚创建好一个匿名对象并调用了有参重构就直接调用析构并销毁
匿名调用拷贝重构函数:
//匿名调用拷贝重构函数 //person(p2)==person p2; //相当于又创建了一次对象p2,会导致对象重复定义 person(p2);
匿名调用拷贝重构函数会被转换:person(p2)==person p2;
相当于又创建了一次对象p2,会导致对象重复定义
3. 等号法(隐式转换法)
//调用默认参数(无参)构造函数
person p1;
//调用有参构造函数
person p2 = 10;
//调用拷贝构造函数将p2的属性拷贝到p3
person p3 = p2;
- 限制:仅适用于单参数构造函数(除非定义了转换运算符)。
后面直接填写参数会被隐式转换为显示法的调用方式,例如:
//调用有参构造函数
person p1 = 10;---隐式转换--->person p1 = person(10);
//调用拷贝构造函数将p2的属性拷贝到p3
person p2 = p1;---隐式转换--->person p2 = person(p1);
3.3 拷贝构造函数调用时机
C++中拷贝构造函数调用时机通常有三种情况
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数参数传值
- 以值方式返回局部对象
先设计一个类:
class person
{
public:
//无参构造函数
person()
{
cout << "person 无参构造函数的调用" << endl;
}
//有参构造函数
person(int a)
{
age = a;
cout << "person 有参构造函数的调用" << endl;
}
//拷贝构造函数
person(const person& p)
{
age = p.age;
cout << "person 拷贝构造函数的调用" << endl;
}
//析构函数
//析构函数会在对象销毁前调用
~person()
{
cout << "person 析构函数的调用" << endl;
}
private:
int age;
};
第一种:使用一个已经创建完毕的对象来初始化一个新对象(常用)
void test01()
{
person p1(10);//先初始化一个对象
person p2(p1);//将创建好的对象p1拷贝给新的对象p2
}
int main()
{
test01();
system("pause");//按任意键继续执行
return 0;
}
3.3.1 临时副本
在未优化的传统C++编译模式下有一个概念叫临时副本
临时副本:临时副本是对象的值传递和返回值形成的一种机制,临时副本就是作为值传递或返回值中间的一种中间交换器或者安检。就比如我将对象作为实参要值传递给形参,就会先创建一个临时副本。临时副本简单概述整体会做两件事:
- 拷贝实参:临时副本本质上是实参对象的完整拷贝,但具体拷贝方式由拷贝构造函数决定。
- 初始化形参:临时副本会调用拷贝构造函数将自己的数据初始化给形参。
看上面不管是临时副本拷贝实参还是初始化形参都是调用了拷贝构造函数实现的,这些不是我们自己调用的,所以这就是隐式调用拷贝构造函数。
举个例子:
临时副本就像一个传话的,比如上课时A需要对B传一句话,但是A和B距离太远了,C说你跟我说,我帮你传话给B,然后这个过程是一次隐式调用拷贝构造函数。然后C又去把这句话传给B,这又是一次隐式调用拷贝构造函数,所以一次值传递或返回值会进行两次隐式调用拷贝构造函数。
但是,在 标准优化模式(C++17起默认)后,值传递和返回值就没有创建临时副本这个步骤了,也就是实参直接传参给形参,中间只进行了一次隐式调用拷贝构造函数。
插曲:
为什么拷贝构造函数形参需要要引用而不能使用值传递?
在C++中,拷贝构造函数的参数必须使用引用(通常是
const
引用),这是由语言的设计逻辑和实际需求共同决定的。这是因为如果用值传递就会创建一个临时副本,而临时副本又会隐式调用这个参数为值传递的拷贝构造函数,然后又发现拷贝构造函数的参数是值参数,并且会再建立一个新的临时副本。然后就会无限递归建立临时副本,最后导致栈空间不足。示例:
class MyClass { public: MyClass(MyClass obj) {} // 错误!值传递会引发递归 }; class MyClass { public: MyClass(const MyClass& obj) {} //正确,传的是地址,不会建立临时副本 };
隐式调用拷贝构造函数:
第二种:值传递的方式给函数参数传值
void doWork(person p1)
{
}
void test02()
{
person p1(10);//初始化对象的属性
doWork(p1);//对象的值传递
}
第三种:以值方式返回局部对象
person test03()
{
person p1(10);//初始化对象的属性
return p1;//对象作为返回值
}
int main()
{
//test01();
//test02();
person p2 = test03();
system("pause");//按任意键继续执行
return 0;
}
总结:
拷贝构造的调用场景和两种调用:
- 手动调用拷贝构造:创建了一个对象,将这个对象的属性拷贝给新对象时需要手动调用拷贝构造
- 隐式调用拷贝构造(自动调用):当对象的值传递和返回值时,编译器会创建一个临时副本,这个临时副本会先隐式调用拷贝构造拷贝我们的实参/返回值,然后再将临时副本初始化(隐式调用拷贝构造) 给形参/接收返回值的对象。
3.4 构造函数调用规则
默认情况下,C++编译器至少给一个类添加3个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
C++提供的默认拷贝构造函数和我们的拷贝构造函数代码实现是一样的,都是将对象的属性拷贝到当前对象。
构造函数调用规则如下:
- 如果用户定义无参构造函数,C++会提供默认拷贝构造
- 如果用户定义有参构造函数,C++不再提供默认无参构造,但是会提供默认拷贝构造
- 如果用户定义拷贝构造函数,C++不会再提供其他构造函数
所以要在定义有参构造函数和拷贝构造函数时要注意再定义C++不再提供的构造函数。
重点:
- 普通有参构造有没有都不影响,但是无参构造和拷贝构造是一定要有的。
- 不管我们有没有定义拷贝构造构造函数,拷贝构造函数始终存在
3.5 深拷贝与浅拷贝
深浅拷贝是面试经典问题,也是常见的一个坑
- 浅拷贝:对象属性是普通变量,就可用在拷贝构造中进行简单的赋值拷贝操作
- 深拷贝:对象属性是在堆区申请空间,就需要在堆区重新申请空间,进行拷贝操作
对象属性是堆区申请空间的话,那需要在程序销毁之前的析构函数中使用delete销毁对象属性。
示例:
class person
{
public:
//无参构造函数
person()
{
cout << "person 默认构造函数的调用" << endl;
}
//有参构造函数
person(int a, int h)
{
//使用堆区开辟空间存储值作为属性
age = new int(a);
height = new int(h);
}
//拷贝构造函数
person(const person& p)
{
//使用浅拷贝拿到的是另一个对象属性存储的在堆区开辟空间的地址
//导致问题:属性指向同一块空间,一个对象进入析构销毁后另一个销毁时会报错
//原因就是对着已经被销毁过的属性继续销毁delete
//拷贝堆区的属性不要用浅拷贝,像这样:
age = p.age;//error
height = p.height;//error
//深拷贝:
//自己开辟一块堆区空间把另一个对象属性的空间里的值拷贝到这块空间
age = new int(*p.age);
height = new int(*p.height);
}
//析构函数
//析构函数会在对象销毁前调用
~person()
{
//当对象属性是堆区开辟空间后,析构负责释放堆区空间
if (age != NULL)
{
delete age;
}
if (height != NULL)
{
delete height;
}
cout << "person 析构函数的调用" << endl;
}
int* age;
int* height;
};
浅拷贝注意事项:
- 使用浅拷贝拿到的是另一个对象属性存储的在堆区开辟空间的地址
- 导致问题:属性指向同一块空间,一个对象进入析构销毁后另一个销毁时会报错
- 原因就是对着已经被销毁过的属性继续销毁delete
总结:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题
- 当属性是普通变量的话我们就可以使用浅拷贝。
- 当属性是在堆区开辟空间我们就要用深拷贝。
- 如果属性是堆区开辟空间我们就需要在构析函数中使用delete释放属性。
3.6 初始化列表
作用:
C++提供了初始化列表语法,用来初始化属性
语法: 构造函数( ):属性1(值1),属性2(值2) . . . { }
我们在给属性初始化的时候有三种方法。
第一种:传统方式初始化
class person
{
public:
//传统方式初始化
person(int a, int b, int c)
{
m_a = a;
m_b = b;
m_c = c;
}
private:
int m_a;
int m_b;
int m_c;
};
int main()
{
person p(10, 20, 30);//给属性初始化
system("pause");
return 0;
}
第二种:使用无参构造初始化列表初始化
class person
{
public:
//使用初始化列表初始化属性
person() :m_a(10),m_b(20),m_c(30)
{
}
private:
int m_a;
int m_b;
int m_c;
};
int main()
{
person p;//给属性初始化
system("pause");
return 0;
}
第三种:使用有参构造初始化列表初始化
class person
{
public:
//使用初始化列表初始化属性
person(int a, int b, int c) :m_a(a),m_b(b),m_c(c)
{
}
private:
int m_a;
int m_b;
int m_c;
};
int main()
{
person p(10,20,30);//给属性初始化
system("pause");
return 0;
}
当使用初始化列表初始化时,构造函数体可以为空实现。
3.7 其他类对象作为类成员
C++类中的成员可以是另一个类的对象,我们称该成员为 对象成员
例如:
class A{};
class B
{
A _a;
};
B类中有A的对象作为成员,A为对象成员
创建两个类,一个类的对象作为另一个类的成员:
//定义手机类
class phone
{
public:
phone(string n):p_name(n)
{
cout << "phone 构造函数调用" << endl;
}
~phone()
{
cout << "phone ~析构函数调用" << endl;
}
private:
string p_name;
};
//定义人类
class person
{
public:
//使用初始化列表初始化属性
person(string name, string p) :m_Name(name), m_phone(p)
{
cout << "person 构造函数调用" << endl;
}
~person()
{
cout << "person ~析构函数调用" << endl;
}
private:
string m_Name;//名字
phone m_phone;//手机
};
在初始化列表中,我们直接给成员对象m_phone初始化一个字符串相当于隐式调用构造函数将字符串初始化给成员对象m_phone. 所以:
m_phone(初始值) == person m_phone = 初始值
那么创建person人类对象时,是先走person的构造函数还是phone的构造函数,那析构函数呢?
创建person对象:
int main()
{
person p("张三","iPhone 15promax");//给属性初始化
system("pause");
return 0;
}
代码运行:
总结:
- 当另一个类的对象作为当前类的成员时,是先走对象成员的构造函数,然后再走当前对象的构造函数。销毁时是先走析构函数是先走当前对象的析构,再走对象成员的析构。
- 在组合类person实例化时它的成员对象phone才会自动实例化,但是成员对象会比组合类的对象先初始化完毕也就是先执行构造语句,这是因为成员对象作为一个对象的成员必须先初始化完毕,所以成员对象的初始化优先级是大于组合类对象的。
在初始化列表中,我们直接给成员对象m_phone初始化一个字符串相当于隐式调用构造函数将字符串初始化给成员对象m_phone. 所以:
m_phone(初始值) == person m_phone = 初始值
3.8 静态成员
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员
静态成员分为:
静态成员变量
- 所用对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
- 有两种访问方式,一种通过对象访问,一种通过类名表示作用域访问
静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
- 有两种访问方式,一种通过对象访问,一种通过类名表示作用域访问
3.8.1 静态成员变量
先定义一个静态成员变量:
class person
{
//公共权限
public:
static int m_a;//类内声明
};
//类外初始化就不用特意加个static
int person::m_a = 100;//类外初始化
将静态成员变量看作类声明时的成员函数:
可以把静态成员变量当做类声明时的成员函数一样,类内部时成员函数的声明,类外是成员函数的实现,并表明作用域是类的。静态成员变量也一样,声明在类内部,类外部一定要初始化并且表明是哪个作用域的,不然就会被当做全局变量。
注:记得,静态成员变量一定要完成:
- 在类的内部进行声明
- 在类的外部进行初始化
这两个步骤我们才能对其访问
静态成员变量类内声明类外初始化以后,我们才能对其进行访问:
int main()
{
person p1;
cout << p1.m_a << endl;//访问静态成员变量
person p2;//修改静态成员变量
p2.m_a = 200;
cout << p1.m_a << endl;//确认是否共享
system("pause");
return 0;
}
代码运行:
注意:因为成员变量是静态的,也就是不管创建多少个对象,这些对象的始终共享着一个静态成员变量,因此一个对象对其进行修改那其他对象的静态成员变量也会被修改。
因为静态成员不止是一个指定对象的成员,而是所有对象共享的,所以静态成员有两种访问方式
int main()
{
person p1;
cout << p1.m_a << endl;//通过对象访问共享成员
cout << person::m_a << endl;//直接访问指定类作用域下的共享成员
system("pause");
return 0;
}
静态成员有两种访问方式:
- 通过对象访问共享成员
- 直接访问指定类作用域的静态成员
注:静态成员也是可以设置访问权限的,如果是私有权限,类的外部就不能直接访问静态成员了
3.8.2 静态成员函数
静态成员函数的定义:
class person
{
//公共权限
public:
static void func()
{
m_a = 100;//静态成员函数可以访问静态成员变量
m_b = 200;//error,静态成员函数只能访问静态成员变量
}
static int m_a;
int m_b;
};
int person::m_a = 0;
注:静态成员函数只能访问静态成员变量,它之所以不能访问成员变量是因为它是多个对象共享的成员函数,它并不知道这个成员变量是哪个对象的成员变量,因此不能修改,会出现语法错误。
静态函数的两种访问权限:
int main()
{
person p1;
p1.func();//通过对象访问共享成员
person::func();//直接访问指定类作用域下的共享成员
system("pause");
return 0;
}
3.8.3 静态成员总结
总结:
- 静态成员是共享的,多个对象共享一个静态成员
- 静态成员变量一定要完成:1. 在类的内部进行声明 2. 在类的外部进行初始化,这两个步骤我们才能对其访问
- 静态成员函数只能访问静态成员变量,因为如果是普通成员变量静态成员函数并不知道是哪个对象调用它,所以不能将其修改。
- 静态成员有两种调用方式,1. 通过对象访问共享成员 2. 直接访问指定类作用域的静态成员
3.9 构造函数和析构函数总结
构造函数和析构函数的语法总结:
一、构造函数和析构函数:
1. 构造函数:是用来完成对象的初始化操作,程序在调用对象时会自动调用构造函数。
2. 析构函数:是用来完成对象的属性销毁操作,程序在对象销毁前会调用析构函数。
二、构造函数的分类
构造函数在一般情况下分为 默认构造(无参构造)、有参构造 和 拷贝构造 三大类,而构造函数之所以分这么多类是因为构造可以有参数从而形成构造函数重构,当我们调用构造函数时,只要传不同的参数就可以调用构造函数重构里对应的构造函数。
三、拷贝构造函数调用时机
拷贝构造的调用场景和两种调用:
- 手动调用拷贝构造:创建了一个对象,将这个对象的属性拷贝给新对象时需要手动调用拷贝构造
- 隐式调用拷贝构造(自动调用):当对象的值传递和返回值时,编译器会创建一个临时副本,这个临时副本会先隐式调用拷贝构造拷贝我们的实参/返回值,然后再将临时副本初始化(隐式调用拷贝构造) 给形参/接收返回值的对象。
四、构造函数调用规则
默认情况下,C++编译器至少给一个类添加3个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
C++提供的默认拷贝构造函数和我们的拷贝构造函数代码实现是一样的,都是将对象的属性拷贝到当前对象。
构造函数调用规则如下:
- 如果用户定义无参构造函数,C++会提供默认拷贝构造
- 如果用户定义有参构造函数,C++不再提供默认无参构造,但是会提供默认拷贝构造
- 如果用户定义拷贝构造函数,C++不会再提供其他构造函数
所以要在定义有参构造函数和拷贝构造函数时要注意再定义C++不再提供的构造函数。
五、深拷贝和浅拷贝
深拷贝和浅拷贝是拷贝构造函数的两种拷贝方式
深浅拷贝是面试经典问题,也是常见的一个坑
- 浅拷贝:对象属性是普通变量,就可用在拷贝构造中进行简单的赋值拷贝操作
- 深拷贝:对象属性是在堆区申请空间,就需要在堆区重新申请空间,进行拷贝操作
总结:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题
- 当属性是普通变量的话我们就可以使用浅拷贝。
- 当属性是在堆区开辟空间我们就要用深拷贝。
- 如果属性是堆区开辟空间我们就需要在构析函数中使用delete释放属性。
六、初始化列表
作用:
C++提供了初始化列表语法,用来初始化属性
语法: 构造函数( ):属性1(值1),属性2(值2) . . . { }
------------------------------------------------------分界线-------------------------------------------------------------------------
3.10 成员属性的总结
一、其他类对象作为类成员
- 当另一个类的对象作为当前类的成员时,是先走对象成员的构造函数,然后再走当前对象的构造函数。销毁时是先走析构函数是先走当前对象的析构,再走对象成员的析构。
- 在组合类person实例化时它的成员对象phone才会自动实例化,但是成员对象会比组合类的对象先初始化完毕也就是先执行构造语句,这是因为成员对象作为一个对象的成员必须先初始化完毕,所以成员对象的初始化优先级是大于组合类对象的。
二、静态成员变量
静态成员变量
- 所用对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
- 有两种访问方式,一种通过对象访问,一种通过类名表示作用域访问
先定义一个静态成员变量:
class person
{
//公共权限
public:
static int m_a;//类内声明
};
//类外初始化就不用特意加个static
int person::m_a = 100;//类外初始化
将静态成员变量看作类声明时的成员函数:
可以把静态成员变量当做类声明时的成员函数一样,类内部时成员函数的声明,类外是成员函数的实现,并表明作用域是类的。静态成员变量也一样,声明在类内部,类外部一定要初始化并且表明是哪个作用域的,不然就会被当做全局变量。
注:记得,静态成员变量一定要完成:
- 在类的内部进行声明
- 在类的外部进行初始化
这两个步骤我们才能对其访问
因为静态成员不止是一个指定对象的成员,而是所有对象共享的,所以静态成员有两种访问方式
int main()
{
person p1;
cout << p1.m_a << endl;//通过对象访问共享成员
cout << person::m_a << endl;//直接访问指定类作用域下的共享成员
system("pause");
return 0;
}
静态成员有两种访问方式:
- 通过对象访问共享成员
- 直接访问指定类作用域的静态成员
注:静态成员也是可以设置访问权限的,如果是私有权限,类的外部就不能直接访问静态成员了
------------------------------------------------------分界线-------------------------------------------------------------------------
4. C++对象模型和this指针
4.1 成员变量和成员函数分开存储
在C++中,类内的成员变量和成员函数是分开存储的
只有非静态成员变量才属于类的对象上。
根据上面的话来,我们可以得知:只有非静态成员变量属于类的对象上,而静态成员和非静态成员函数都是在其他地方存储。
虽然只有非静态成员变量是属于对象上的,但是这个对象可以通过访问成员找到对应的空间访问成员。
来猜猜下面代码的空对象占用大小是多少?:
class person
{
};
int main()
{
person p1;
cout << "size of is:" << sizeof(p1) << endl;
system("pause");
return 0;
}
代码运行:
答案是:1
答案解析:为什么呢?明明一个非静态成员变量都没有,为什么还会算出1个字节,这是因为当创建对象后,对象里没有任何非静态成员变量,所以对象需要占用一个字节空间来表示这块空间就是这个对象。因为一个类可以创建多个对象,但是对象没有非静态成员变量就需要占用一个字节来表示这是我当前对象的空间。
如果类中定义一个非静态成员变量:
class person
{
public:
int age;
};
int main()
{
person p1;
cout << "size of is:" << sizeof(p1) << endl;
system("pause");
return 0;
}
代码输出:
当对象是空时就需要占用一个字节空间表示这是当前对象,当对象有一个非静态成员变量就直接开辟非静态成员变量大小的空间来表示这是对象。
那如果只有非静态成员变量属于类的对象上,那一个类同时有非静态成员变量、静态成员变量、非静态成员函数 和 静态成员函数时这个类的对象会不会只有非静态成员变量的大小?:
class person
{
public:
int age;//非静态成员变量,属于类的对象上
static int m_a;//静态成员变量,不属于类的对象上
void func1(){}//非静态成员函数,不属于类的对象上
static void func2(){}//静态成员函数,不属于类的对象上
};
int person::m_a = 10;
int main()
{
person p1;
cout << "size of is:" << sizeof(p1) << endl;
system("pause");
return 0;
}
代码输出:
还是4,这就可以证明类的对象上实际只有非静态成员变量。
总结:
- 类的对象内存空间中只有非静态成员变量,其他的静态成员变量、非静态成员函数 和 静态成员函数在其他内存空间
- 非静态成员变量、静态成员变量、非静态成员函数 和 静态成员函数除了存储空间位置不同,分对象内和对象外。但是它们的作用域都是一个类中的,所以类的对象才可以直接调用
- 如果是空对象,始终都是占有一个字节空间来表示
4.2 this指针概念
通过4.1我们知道在C++中成员变量和成员函数是分开存储的
每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码
那么问题是:这一块代码是如何区分哪个对象调用自己呢?
C++通过提供特殊的对象指针,this指针,解决上述问题,this指针指向被调用的成员函数所属对象
简单理解:不管有多少对象,这些对象中的非静态函数也是独一份的,所以哪个对象调用非静态成员函数那非静态成员函数的this指针就指向谁, 因为有了这个this指针,所以非静态成员函数才知道是哪个对象对自己进行调用,可以随意访问这个对象的属性。
this指针是隐含每一个非静态成员函数内的一种指针
this指针不需要定义,直接使用即可
注:this是的本质指针常量,可以修改this指向的空间,但是不可以修改this的指向
this指针的用途:
- 当形参和成员变量同名时,可用this指针来区分
- 在类的非静态成员函数中返回对象本身,可使用return *this
第一种:当成员函数的形参和成员变量同名时
class person
{
publiv:
person(int age)
{
this->age = age;
}
int age;
};
使用this指针,因为当你使用对象调用成员函数时,成员函数的this指针就默认指向了你这个对象,直接通过this指针找到你对象的成员变量进行赋值,这样就不会报错了。
第二种:在非静态成员函数中返回对象本身
class person
{
public:
person(int age)
{
this->age = age;
}
person& PersonAdd(person& p)
{
this->age += p.age;
//如果要返回当前的对象,返回类型必须是引用
//如果是直接返回值,那调用拷贝构造函数只是将数据拷贝回去,并非原来的对象
return *this;
}
int age;
};
int main()
{
person p1(10);
person p2(10);
//链式语句
p2.PersonAdd(p1).PersonAdd(p1).PersonAdd(p1);
cout << p2.age << endl;
system("pause");
return 0;
}
this指针可以将对象作为返回值然后继续调用,从而形成链式访问。
总结:this指针就是指向当前调用成员函数的对象
一般我们调用非静态成员函数访问成员变量时,编译器会自动加个this指针,我们只需要知道this指针是干什么的和上面的两个使用场景就可以了。
4.3 空指针访问成员函数
C++中对象指针赋值空指针也是可以调用成员函数的,但是也要注意有没有用到this指针
如果用到this指针,需要加以判断保证代码的健壮性。
什么意思?
意思是给代码加个判断,判断this指向的对象是不是空,如果是就返回,不是就继续执行成员函数。
class person
{
public:
void PrintAge()
{
cout << "age = " << age << endl;
//等价于
cout << "age = " << this->age << endl;
}
int age;
};
当我们创建了一个对象指针并赋值NULL来调用该函数
int main()
{
person* p1 = NULL;
p1->PrintAge();
system("pause");
return 0;
}
this接收到的对象是NULL,从而导致对空解引用:
cout << "age = " << this->age << endl;
//this等于NULL就相当于
cout << "age = " << NULL->age << endl;
所以我们就可以通过给成员函数加一个判断来提高代码的健壮性:
class person
{
public:
void PrintAge()
{
if(this==NULL)
return;//如果this接收到NULL就返回
cout << "age = " << age << endl;
}
int age;
};
总结:对象指针类型的空指针是可以访问成员的,所以我们需要在成员内部加以判断来提高代码的健壮性。
4.4 const修饰成员函数
常函数:
- 成员函数后加const后我们称为这个函数为常函数
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改
常对象:
- 声明对象前加const称该对象为常对象
- 常对象只能调用常函数,为了防止调用普通函数会修改属性
注:当一个成员属性声明前面加上关键字mutable后,不管是常函数,还是常对象,都可以对齐进行修改。
常函数:
class person
{
public:
void PrintAge()const
{
m_a = 100;//error,常量函数不可以修改属性
m_b = 100;//可赋值,因为被mutable修饰过的属性可以无视const
}
int m_a;
mutable int m_b;
};
const是需要加到函数名( )和函数体之间才能修饰到this指针,这样this就无法更改它所指向的对象。但是当我们给对象的成员属性修饰mutabel后,mutable就可以无视const,依旧可以被修改。
常对象:
当类中再定义一个普通的成员函数
class person
{
public:
void PrintAge()const
{
m_b = 100;//可赋值,因为被mutable修饰过的属性可以无视const
}
void func()
{
}
int m_a;
mutable int m_b;
};
int main()
{
const person p1;
p1.PrintAge();
p1.func();//error,因为常对象不能调用普通成员函数,防止普通函数直接对属性进行修改
p1.m_a = 10;//error,因为常对象不能更改属性的
p1.m_b = 10;//可以赋值,m_b被mutable修饰过就可以不受const影响
system("pause");
return 0;
}
总结:
- const是需要加到函数名( )和函数体之间才能修饰到this指针
- 常对象是不可以调用普通成员函数的
- mutable修饰的属性可以不被const影响
5. 友元
生活中你家有客厅(public),有你的卧室(private)
客厅所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去
但是呢,你也可以允许你的好闺蜜好基友进去。
在程序里,有些私有属性,也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术
友元的目的就是让一个函数或者类 访问另一个类中私有成员
友元的关键字为 friend
友元的三种实现:
- 全局函数做友元
- 类做友元
- 成员函数做友元
5.1 全局函数做友元
定义一个全局函数,假设这个全局函数是我的朋友,但是不是那么要好的朋友。所以我会让这个朋友只能访问公共权限,不能访问私有权限
class Building
{
public://公共权限
Building()//创建构造初始化属性
{
SittingRoom = "客厅";
Bedroom = "卧室";
}
string SittingRoom;//客厅
private://私有权限
string Bedroom;//卧室
};
void GoodFriend(Building* building)
{
cout << "好朋友 全局变量正在访问你的:" << building->SittingRoom << endl;
cout << "好朋友 全局变量正在访问你的:" << building->Bedroom << endl;//error
}
当全局函数试图访问对象的私有权限就会报错。
但是当全局函数是对象从小到大穿着同一条裤子的铁哥们,它就在对象的友元列表中,友元就可以访问私有权限:
class Building
{
//告诉编译器,GoodFriend是对象的好朋友,可以访问私有权限
friend void GoodFriend(Building* building);
public://公共权限
Building()//创建构造初始化属性
{
SittingRoom = "客厅";
Bedroom = "卧室";
}
string SittingRoom;//客厅
private://私有权限
string Bedroom;//卧室
};
void GoodFriend(Building* building)
{
cout << "好朋友 全局变量正在访问你的:" << building->SittingRoom << endl;
cout << "好朋友 全局变量正在访问你的:" << building->Bedroom << endl;
}
代码运行:
语法:
//把函数声明放进类中并在前面加上一个friend修饰
//告诉类这个自定义函数是友元,可以访问私有权限
friend void GoodFriend(Building* building);
5.2 友元类
class Building
{
public://公共权限
Building();//构造函数内部声明
string SittingRoom;//客厅
private://私有权限
string Bedroom;//卧室
};
class Goodfriend
{
public:
Goodfriend();//构造函数内部声明
void visit();//成员函数内部声明
~Goodfriend() {
if (building != NULL)
delete building;//销毁动态开辟的属性
}
Building* building;
};
//Building构造函数的外部实现
Building::Building()
{
SittingRoom = "客厅";
Bedroom = "卧室";
}
//Goodfriend构造函数的外部实现
Goodfriend::Goodfriend()
{
//动态开辟一块Building对象大小的空间
building = new Building;//这个过程会自动进入Building对象的构造函数完成初始化
}
//Goodfriend成员函数外部实现
void Goodfriend::visit()//参观另一个类的房子函数
{
cout << "好朋友 类正在访问你的:" << building->SittingRoom << endl;
cout << "好朋友 类正在访问你的:" << building->Bedroom << endl;
}
因为我们还没有让Goodfriend这个类做Building的友元,所以还是不能访问私有权限。
让类作为友元:
class Building
{
//将那个类变成友元就可以访问自己的私有权限
friend class Goodfriend;
public://公共权限
Building();//构造函数声明
string SittingRoom;//客厅
private://私有权限
string Bedroom;//卧室
};
代码运行:
语法:
friend class Goodfriend;
5.3 成员函数作为友元
上面我们知道了一个类对象可以作为另一个类对象的友元并且可以访问它的私有权限。也就是说这个类对象不管是哪个成员函数都可以对另一个类对象的私有属性进行访问。但是我们想要限制一下,不想让他的每个地方都能随便访问私有权限。
解决思路:我们就不用这个类对象作为那个类的友元,而是这个类的成员函数作为那个类的友元,只有指定定的友元成员函数可以访问那个类对象的私有属性。
class Building
{
friend void Goodfriend::visit1();
public://公共权限
Building();//构造函数内部声明
string SittingRoom;//客厅
private://私有权限
string Bedroom;//卧室
};
class Goodfriend
{
public:
Goodfriend();//构造函数内部声明
void visit1();//可以访问成员对象的私有属性
void visit2();//不可以访问成员对象的私有属性
~Goodfriend() {
if (building != NULL)
delete building;//销毁动态开辟的属性
}
Building* building;
};
语法:
friend void Goodfriend::visit1();
注:记得成员函数作为另一个类对象的友元,必须表明它是哪个类作用域下的成员函数。
总结:
友元不管是全局函数、类还是成员函数,都是声明在想要访问私有权限的类前面,并且将声明前面加上一个friend告诉编译器它是这个类的 "朋友" ,可以让它访问这个类的私有权限。
注:记得成员函数作为另一个类对象的友元,必须表明它是哪个类作用域下的成员函数。
类名: :
6. 运算符重载
关键字:operator
运算符重载概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型
运算符重载的关键字后面加上不同的运算符就是不同的运算符重载:
1. 算术运算符
- 一元:
operator+
,operator-
,operator++
,operator--
- 二元:
operator+
,operator-
,operator*
,operator/
,operator%
2. 关系与比较
operator==
,operator!=
,operator<
,operator>
,operator<=
,operator>=
3. 逻辑与位运算
- 逻辑:
operator!
,operator&&
,operator||
- 位运算:
operator~
,operator&
,operator|
,operator^
,operator<<
,operator>>
4. 赋值与复合赋值
- 基础:
operator=
- 复合:
operator+=
,operator-=
,operator*=
,operator/=
,operator%=
,
operator&=
,operator|=
,operator^=
,operator<<=
,operator>>=
5. 特殊运算符
- 访问:
operator[]
,operator->
,operator->*
- 调用:
operator()
- 其他:
operator,
,operator new
,operator delete
关键规则:
固定格式
所有重载函数名必须以operator
开头,后接运算符符号(如+
、==
等)。操作数限制
至少一个参数为用户自定义类型(类或枚举),禁止重载内置类型的运算符。不可重载运算符
::
(作用域解析)、.
(成员访问)、?:
(三元条件)、sizeof
等不可重载。
6.1 加号运算符重载
作用:实现两个自定义数据类型相加的运算
为什么需要运算符重载呢?比如+运算符,平常我们可以让他两边相加内置类型的数据,比如:
int a = 10;
int b = 20;
int c = a + b;
但是我们创建了一个类,我们想让对象进行相加:
class person
{
public:
person()
{
m_a = 10;
m_b = 10;
}
int m_a;
int m_b;
};
int main()
{
person p1;
person p2;
//想让p1的m_a和p2的m_a相加,p1的m_b和p2的m_b相加,然后赋值给p3
person p3 = p1 + p2;//error,因为一般运算符是做不到直接对象进行操作的
return 0;
}
为了让运算符能够处理对象相加的操作,我们就需要定义一个运算符重载:
运算符重载有实现两种方式:成员函数实现、全局函数实现
1、成员函数实现
class person
{
public:
person()
{
m_a = 10;
m_b = 10;
}
//定义运算符重载
person operator+(person p)
{
person ret;//创建一个新的对象将两个对象进行相加
ret.m_a = this->m_a + p.m_a;//相加操作
ret.m_b = this->m_b + p.m_b;
return ret;//返回相加后的属性
}
int m_a;
int m_b;
};
然后我们就有两种方法使用运算符重载:
person p3 = p1 + p2;
person p3 = p1.operator+(p2);
如果使用第一种方式运算,p1+p2会被编译器自动转换为p1.operator+(p2)。
2、全局函数实现
person operator+(person p1, person p2)
{
person ret;
ret.m_a = p1.m_a + p2.m_a;
ret.m_b = p1.m_b + p2.m_b;
return ret;
}
全局两种使用运算符重载格式:
person p3 = p1 + p2;
person p3 = operator+(p1, p2);
编译器也是会将p1 + p2转换为operator+(p1,p2)
6.2 左移运算符重载
什么是左移运算符?左移元素符是<<,注意,这里的左移运算符非位运算符的左移二进制位操作,而是将一个值输出到终端的左移运算符。
那什么是左移运算符重载呢?左移运算符重载就是遇到不同的操作数可以调用不同的运算符重载实现来进行操作,比如我先定义一个类:
class person
{
public:
person();
int m_a;
int m_b;
};
person::person()
{
m_a = 10;
m_b = 20;
}
我想输出这个类中的两个成员变量,那该怎么做呢?
person p;
cout << p.m_a << p.m_b << endl;
但是我们想要直接输出p,不用每次都这么麻烦的用p访问成员再打印,而是调用运算符重载我们实现的操作,然后进行打印,就比如:
person p;
cout << p << endl;
那我们该如何实现这个左移运算符重载呢?是使用成员函数还是全局函数实现呢?
答案是:全局函数
为什么不能使用成员函数呢?那下面我给大家解释一下是为什么。
class person
{
public:
person();
ostream& operator<<(ostream& cout)
{
cout << m_a << m_b;
return cout;
}
int m_a;
int m_b;
};
因为cout << p本来是cout在<<运算符左边,p在右边,这是一种语法,就是将值<<向左边的地方传输,所以cout输出语句才可以多个<<将值向左传输到最后的终端。但是我们如果用成员函数实现左移运算符重载,由于编译器会对其进行转换,所以最后是这样的:
person p;
cout << p << endl;
p.operator<<(cout) << endl;
等于p << cout
这就倒反天罡了,本来我就是要把值通过左移运算符<<传输给cout,但是这次是让cout传输给值,那肯定会报错啊。
解决方法:使用全局函数
ostream& operator<<(ostream& cout,person& p)
{
cout << p.m_a << " " << p.m_b;
return cout;
}
这样顺序就不会调换了,就是这样:
person p;
cout << p << endl;
operator<<(cout, p) << endl;
等于cout << p
不会报错
如果我们继续用这种使用<<运算符打印p的数据是不会调用我们的左移运算符重载
person p;
cout << p.m_a << p.m_b << endl;
这是因为运算符重载和函数重载一样需要判断参数,如果两个参数是我们这个运算符的重载里有的就会调用重载里的函数,如果没有那就只是使用普通的左移运算符<<。
可能有人疑问这个ostream是什么啊?
ostream是一个类,cout是ostream的对象,是一个预定义对象,标准输出流对象。
总结:
左移运算符重载需注意两点:
- 我们调用的左移运算符重载的时候,需注意一定要用引用来接收cout,因为一个程序中只允许一个ostream的对象cout存在,如果我们值传递是需要再建立一个对象来接收cout的属性,所以会报错,我们就使用引用来接收。
- 左移运算符重载返回值:返回值一定也要用引用,因为后面还有个<<endl需要和cout进行链接,cout之所以可以连着多个<<像个火车一样是因为这是cout的特性<<链接,如果我们只是打印不给返回值,那后面就不可以使用<<继续链接了,所以这一点一定要注意。
6.3 递增运算符重载
递增运算符++可以对内置类型进行自加,但是递增运算符++有两种,一种前置++,一种后置++,我们需要实现这两种不同功能的递增运算符重载。
前置++:
//前置++的实现
MyInit& operator++()
{
//先++
m_a += 1;
//后返回
return *this;
}
返回值之所以是对象本身就是因为防止一下情况
++(++对象)
因为执行完一次需要再对原来的对象++就需要将对象的引用作为返回值继续操作。
后置++:
//后置++的实现
MyInit operator++(int)
{
//先记录一下当前的值
MyInit ret = *this;
//后++
m_a += 1;
//返回记录值
return ret;
}
- 为什么需要占位参数?
C++编译器通过参数列表区分重载的运算符。后置++
的int
参数仅作为语法标记,无实际用途,但能明确告知编译器这是后置版本。 - 为什么值返回?
因为必须需要记录原来值,所以需要创建一个局部的对象来接收,返回时不能使用引用,如果将局部的地址返回就会因函数的销毁而销毁,所以我们只需返回这个对象中的属性。
运算符重载前置++和后置++的区别:
特性 | 前置++ | 后置`++ |
---|---|---|
返回值类型 | T& (引用) | T (值) |
语义 | 修改后对象 | 修改前对象的副本 |
性能 | 高效(无拷贝) | 可能有拷贝开销 |
典型实现 | T& operator++() { ... } | T operator++(int) { ... } |
整体完整代码:
class MyInit
{
public:
MyInit()
{
m_a = 10;
}
//前置++的实现
MyInit& operator++()
{
//先++
m_a += 1;
//后返回
return *this;
}
//后置++的实现
MyInit operator++(int)
{
//先记录一下当前的值
MyInit ret = *this;
//后++
m_a += 1;
//返回记录值
return ret;
}
int m_a;
};
ostream& operator<<(ostream& cout, MyInit m)
{
cout << m.m_a;
return cout;
}
int main()
{
MyInit m;
cout << ++m << endl;
cout << m++ << endl;
cout << m << endl;
system("pause");
return 0;
}
可以发现我们还创建了左移运算符重载,但是有没有发现左移运算符的参数是对象的值,而不是对象的引用,然后下面调用前置++返回引用然后返回值引用作为左移运算符重载的参数并没有报错,这是为什么?
在C++中,值类型传参给引用类型会报错,而引用类型传参给值类型不会报错,这是由类型安全机制和参数传递的语义规则决定的。以下是详细分析:
核心机制对比
传递方向 | 是否报错 | 根本原因 |
---|---|---|
值类型 → 引用类型 | 报错 | 引用必须绑定到具名对象(左值),而值类型可能生成临时对象(右值) |
引用类型 → 值类型 | 不报错 | 值类型参数会隐式拷贝引用指向的对象(解引用+拷贝构造) |
总结:
1. 自增或自减运算符重载:语法特性
- 前置的重载实现返回值类型需要是引用
- 后置的重载实现返回值类型需要是值
2. 自增运算符重载的后置实现形参必须是占位参数,以区分前置和后置
3. 引用类型和值类型传参机制:
- 引用类型 → 值类型: 引用类型作为参数传递给值类型会隐式拷贝引用指向的对象给形参
- 值类型 → 引用类型:值类型作为参数传递给引用类型会报错
自增运算符重载的两种使用方式:
//自增运算符重载的两种使用方式
int main()
{
MyInt m;
//第一种:
cout << ++m << endl;
cout << m++ << endl;
cout << m << endl;
//第二种
operator<<(cout, m.operator++()) << endl;//调用前置++
operator<<(cout, m.operator++(1)) << endl;//调用后置++
operator<<(cout, m) << endl;
return 0;
}
从上面的代码我们可以总结两点:
1. 自增++运算符重载是有前置和后置的,用参数区分,前置没有形参,后置占位参数作为形参
2. 运算符重载是可以像函数那样链式访问的,因为运算符重载也是函数。
6.4 赋值运算符符重载
C++编译器至少给一个类添加4个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行拷贝
- 赋值运算符 operator= ,对属性进行值拷贝
如果类中有属性指向堆区,做赋值操作也会有深浅拷贝的问题
1. 为什么要给类添加赋值运算符函数operator=?那他的操作是什么?
当我们一个对象的属性初始化完毕,我想让另一个对象直接接收这个对象的属性有两种方法:
- 调用拷贝构造函数
- 调用赋值运算符函数
比如:
person p1(10,20);
person p2;
p2 = p1;//这个时候就是隐式调用赋值运算符函数了
p2.operator=(p1);
p2 = p1; 等价于 p2.operator=(p1);
2. 拷贝构造函数要注意深浅拷贝,那为什么赋值也要注意深浅拷贝呢?
如果你仔细观察,可以发现拷贝构造和赋值运算符函数的操作是一模一样的,因为他们默认都有编译器提供,而编译器提供的是浅拷贝,也就是直接赋值。因此我们的属性如果是堆区开辟的那导致的问题就是赋值和拷贝后,两个对象的属性指向同一块空间。这就是浅拷贝带来的麻烦。
解决方法:
- 属性不是堆区开辟:属性不是堆区开辟我们可以不用创建拷贝构造和赋值运算符函数,使用编译器默认提供的就可以了。
- 属性是堆区开辟:属性是堆区开辟我们就不要用编译器提供的拷贝构造和赋值运算符函数,自己创建并用深拷贝。
然后我们可以试着来实现一下:
//成员赋值运算符函数
void operator=(person& p)
{
//先判断之前是否有值
if (Age != NULL)
delete Age;
//赋值
Age = new int(*p.Age);
}
这个可以完成两个操作数之间的赋值了,但是还有一个缺陷,是什么呢?
int a = 10;
int b,c;
c = b = a;
可以发现普通赋值运算符是可以完成连续赋值操作的,那我们两个操作数赋值完什么也不返回,怎么给第三个操作数赋值。
修改升级:
//成员赋值运算符函数
person& operator=(person& p)
{
//先判断之前是否有值
if (Age != NULL)
delete Age;
//赋值
Age = new int(*p.Age);
//返回赋过值的对象
return *this;
}
这样我们也可以完成连续赋值的操作了。
6.5 关系运算符重载
作用:重载关系运算符,可以让两个自定义类型对象进行对比操作
- 关系运算符:>、<、>=、<=、==、!=
- 如果为真返回1,为假就返回0
知道了这些特性我们就可以实现关系运算符重载
bool operator==(person& p)
{
if (Age == p.Age) {
return true;
}
else {
return false;
}
}
自定义类型对比:
person p1(10);
person p2(20);
if (p1 == p2)
{
cout << "p1等于p2" << endl;
}
else
{
cout << "p1不等于p2" << endl;
}
6.6 函数调用运算符重载
- 函数调用运算符() 也可以重载
- 由于重载后使用的方式非常像函数的调用,因此称为仿函数
- 仿函数没有固定写法,非常灵活
注意:函数调用运算符重载必须是成员函数
class Add
{
public:
int operator()(int a,int b)
{
return a + b;
}
};
int main()
{
Add addfun;
int ret = addfun(10, 20);
cout << ret << endl;
return 0;
}
而对象就像函数名一样调用函数调用运算符函数,这就被称为仿函数。
6.7 运算符重载总结
总结:
1. 运算符重载的存在,就是为了应对自定义类型的数据运算。
- 内置类型:像int、char、double这些内置类型数据,可以用普通运算符:+、-、*、/、%来直接完成运算。
- 自定义类型:class、struct... 这些自定义类型我们如果想要直接使用运算符对他们进行运算就需要运算符重载,我们只需要使用operator关键字建立起对应的运算符的重载,实现其细节,再使用+、-、*、/、%对他们运算时,就会隐式转换为我们实现的接口从而将两个操作数当做参数传参,具体怎么运算就看我们是怎么实现的。
2. 运算符重载的使用格式:
比如我建立了对象:person p1,p2,p3;
- 运算符:p3 = p1 + p2;
- 调用接口:p3 = operator+(p1,p2);(全局函数实现运算符重载)
- 在编译过程编译器会将 p3 = p1 + p2;---- 隐式转换 ----> p3 = operator+(p1,p2);
前提是我们实现了这个运算符重载的函数
3. 就拿运算符+举例,运算符重载就是为了一个+能直接加自定义类型,面对多种情况才重载。比如对象+对象,对象+内置类型数据,对象+结构体变量... 这些情况,所以就是为了防止一个函数不够,才发生重载的。
4. 为什么运算符要用运算符重载?
- 函数重载也可以实现运算符重载,比如函数Add发生 + 的运算符重载,函数Sub发生 - 的 运算符重载。就是因为函数重载并不能完美复刻运算符的功能。
- 比如我通过运算符重载实现了一个运算符的多中不同操作数之间的运算,除了调用接口的方式来进行两个操作数的运算还有什么方式?那就是直接使用原来的运算符+、-、*、/、%来运算,因为编译器会帮我们隐式转换为调用接口模式的,只要我们自己方便就行。
- 术业有专攻嘛!关于同一类型的函数需要对不同类型的参数进行操作就可以函数重载。关于同一种运算符比如+需要对不同的自定义类型运算就可以运算符重载
5. 函数重载和运算符重载的类似点:
- 函数重载是一堆相同函数名的函数在遇到不同的参数调用不同的函数,
- 而运算符重载就是一个相同的运算符在面对不同的操作数组合可以通过找到对应的重载里的实现进行运算
6. 运算符重载的两种实现:
- 全局函数
- 成员函数
7. 继承
继承是面向对象三大特性之一
有些类与类之间存在特殊的关系,例如下图中:
我们发现,定义这些类是,下级别的成员除了拥有上一级的共性,还有自己的特性
这个时候我们就可以考虑利用继承的技术,减少重复代码
7.1 继承的基本语法
举个例子:我们看到很多网站中,都有公共的头部,公共的底部,甚至公共的左侧列表,只有中心不同
红框:公共页面
蓝框:不同页面
例如:
可以发现CSDN的网站也是:公共的头部、公共的底部、公共的左侧列表,但是点击不同的话题,中心内容就不同了。
接下来我们分别利用普通写法和继承的写法来实现网页中的内容,看一下继承存在的意义以及好处
普通实现:
//话题: 全部页面包含
class ALL
{
public:
//公共
void heater()
{
cout << "搜索、创作、话题:全部/资讯/deepseek/运维... (公共头部)" << endl;;
}
void footer()
{
cout << "帮助中心、交流合作、站内地图... (公共底部)" << endl;
}
void lefter()
{
cout << "订阅、关注、收藏、历史... (公共分类列表)" << endl;
}
//不同点
void all()
{
cout << "全部话题" << endl;
}
};
//话题: 咨询页面包含
class Info
{
public:
//公共
void heater()
{
cout << "搜索、创作、话题:全部/资讯/deepseek/运维... (公共头部)" << endl;;
}
void footer()
{
cout << "帮助中心、交流合作、站内地图... (公共底部)" << endl;
}
void lefter()
{
cout << "订阅、关注、收藏、历史... (公共分类列表)" << endl;
}
//不同点
void info()
{
cout << "资讯话题" << endl;
}
};
//话题:deepseek话题包含
class deepseek
{
public:
//公共
void heater()
{
cout << "搜索、创作、话题:全部/资讯/deepseek/运维... (公共头部)" << endl;;
}
void footer()
{
cout << "帮助中心、交流合作、站内地图... (公共底部)" << endl;
}
void lefter()
{
cout << "订阅、关注、收藏、历史... (公共分类列表)" << endl;
}
//不同点
void dep()
{
cout << "deepseek话题" << endl;
}
};
int main()
{
ALL a;
Info i;
deepseek d;
cout << "-----全部页面-----" << endl;
a.heater();
a.footer();
a.lefter();
a.all();
cout << endl;
cout << "-----资讯页面-----" << endl;
i.heater();
i.footer();
i.lefter();
i.info();
cout << endl;
cout << "-----deepseek页面-----" << endl;
d.heater();
d.footer();
d.lefter();
d.dep();
system("pause");
return 0;
}
程序运行:
过多的包含重复代码会让程序显得冗余,那有什么解决方法呢?答案就是:继承
解决思路:
将他们重复包含的代码统一处理成一个类,让他们继承这个类
继承实现(优化版本):
class BasePage
{
public:
//公共
void heater()
{
cout << "搜索、创作、话题:全部/资讯/deepseek/运维... (公共头部)" << endl;;
}
void footer()
{
cout << "帮助中心、交流合作、站内地图... (公共底部)" << endl;
}
void lefter()
{
cout << "订阅、关注、收藏、历史... (公共分类列表)" << endl;
}
};
//话题: 全部页面包含
class ALL:public BasePage//继承
{
public:
//不同点
void all()
{
cout << "全部话题" << endl;
}
};
//话题: 咨询页面包含
class Info :public BasePage//继承
{
public:
//不同点
void info()
{
cout << "资讯话题" << endl;
}
};
//话题:deepseek话题包含
class deepseek :public BasePage//继承
{
public:
//不同点
void dep()
{
cout << "deepseek话题" << endl;
}
};
int main()
{
ALL a;
Info i;
deepseek d;
cout << "-----全部页面-----" << endl;
a.heater();
a.footer();
a.lefter();
a.all();
cout << endl;
cout << "-----资讯页面-----" << endl;
i.heater();
i.footer();
i.lefter();
i.info();
cout << endl;
cout << "-----deepseek页面-----" << endl;
d.heater();
d.footer();
d.lefter();
d.dep();
system("pause");
return 0;
}
总结:
继承的好处:可以减少重复的代码
语法:class 类名 : 继承方式 需要继承类的名称
class A : public B
A 类称为子类 或 派生类
B 类称为父类 或 基类
就像儿子B继承了父亲A的衣钵(成员)
派生类中的成员,包含两大部分:
一类是从基类继承过来的,一类是自己增加的成员。
从基类继承过来的表现其共性,而新增的成员体现了其个性。
7.2 继承方式
继承的语法: class 子类 : 继承方式 父类
继承方式一共有三种:
- 公共继承 (类内可以访问,类外也可以访问)
- 保护继承 (类内可以访问,类外不可以访问,子类继承可以类内访问,子类外不可以访问)
- 私有继承 (类内可以访问,类外不可以访问)
从上图总结:
继承方式级别:公共继承 < 保护继承 < 私有继承
解析:
- 公共继承:不能将父类保护权限和私有权限的属性更改为公共权限
- 保护继承:不能将父类私有权限的属性更改为保护权限,但可以对公共权限改为保护权限并继承
- 私有继承:可以将父类保护权限和公共权限的属性改为私有权限并继承
注意:如果父类的属性是私有权限,子类继承过来是访问不到的。
但是如果子类通过私有继承的方式继承父类属性,就是将父类的非私有属性改为了我这个子类的私有属性,我当前的子类可以在内部访问私有属性。但是这个子类的下一个子类继承也就访问不到这个子类的私有属性。
7.3 继承中的对象模型
问题:从父类继承过来的成员,哪些属于子类对象中?
比如,一个子类继承父类,它会为了继承父类的属性而额外的开辟空间来存放父类的属性吗?
答案是:会的
例如:
//父类
class Base
{
public:
int m_a;
protected:
int m_b;
private:
int m_c;
};
//子类
class Son :public Base
{
public:
int m_d;
};
int main()
{
Son s;
cout << sizeof(s) << endl;
return 0;
}
程序运行:
可以发现父类那不管是什么权限的属性统统继承过来了
那有人就要问了:为什么子类都不能访问的私有权限也拷贝过来了
解析:
父类中所有非静态成员属性都会被子类继承下去
父类中私有成员属性 是被编译器给隐藏了,因此是访问不到,但是确实被继承下去了
接下来我们就可以查看继承中的对象模型了:
- 利用开发人员命令提示工具查看对象模型
- 跳转盘符 F:
- 跳转文件路径 cd 具体路径下
- 查看命名
- cl /d1 reportSingleClassLayout类名 文件名
总结:
打开工具窗口后,定位到当前CPP文件的盘符
然后输入: cl /d1 reportSingleClassLayout类名 文件名
最后展示出来的对象模型如下图:
结论:父类中私有成员也是被子类继承下去了,只是由编译器给隐藏后访问不到
7.4 继承中构造和析构的顺序
子类继承父类后,当创建子类对象,也会调用父类的构造函数
问题:父类和子类的构造和析构顺序是谁先谁后?
示例:
class Base
{
public:
Base()
{
printf("正在调用 Base的构造函数\n");
}
~Base()
{
printf("正在调用 Base的析构函数\n");
}
};
class Son :public Base
{
public:
Son()
{
printf("正在调用 Son的构造函数\n");
}
~Son()
{
printf("正在调用 Son的析构函数\n");
}
};
int main()
{
Son s;
return 0;
}
程序运行:
总结:继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反
7.5 继承同名成员处理方式
问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?
- 访问子类同名成员 直接访问即可
- 访问父类同名成员 需要加作用域
假设,子类的成员函数和父类的成员函数名字相同:
class Base
{
public:
void func()
{
printf("正在调用 Base的成员函数\n");
}
};
class Son :public Base
{
public:
void func()
{
printf("正在调用 Son的成员函数\n");
}
};
int main()
{
Son s;
s.func();//访问子类成员函数
s.Base::func();//访问父类成员函数
return 0;
}
如果成员函数同名,访问父类的成员函数格式:
s.Base::func();
注:成员变量如果同名也是像这样加个作用域才能调用
重点:
如果父类的和子类成员函数撞名的成员函数有重载的话,一定要像上面一样加个作用域才能发生父类成员函数重载,这是因为:
- 如果子类中出现父类同名的成员函数,子类的同名成员会隐藏掉父类中所有同名成员函数
- 如果想访问到父类中被隐藏的同名成员函数,需要加作用域
总结:
- 子类对象可以直接访问到子类同名成员
- 子类对象家作用域可以访问到父类同名成员
- 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数
7.6 继承同名静态成员处理方式
问题:继承中同名的静态成员在子类对象上如何进行访问?
静态成员和非静态成员出现同名,处理方式一致
- 访问子类同名成员 直接访问即可
- 访问父类同名成员 需要加作用域
示例:
class Base
{
public:
static int m_a;
};
int Base::m_a = 100;
class Son :public Base
{
public:
static int m_a;
};
int Son::m_a = 200;
int main()
{
Son s;
//对象访问
cout << "Son static int m_a = " << s.m_a << endl;
cout << "Son static int m_a = " << s.Base::m_a << endl;
//类名访问
cout << "Son static int m_a = " << Son::m_a << endl;
cout << "Son static int m_a = " << Son::Base::m_a << endl;
return 0;
}
有一点不同就是:
cout << "Son static int m_a = " << Son::Base::m_a << endl;
意思是Son::子类作用域下的Base::父类作用域下的m_a静态变量
总结:同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象 和 通过类名)
7.7 多继承语法
C++允许一个类继承多个类
语法: class 子类 : 继承方式 父类1,继承方式 父类2. . .
多继承可能会引发父类中有同名成员出现,需要加作用域区分
注:C++实际开发中是不建议用多继承的
总结:多继承中如果父类中出现了同名的情况,子类使用时要加作用域。这不利于实际开发
7.8 菱形继承
菱形继承概念:
- 两个派生类继承同一个基类
- 又有某个类同时继承着两个派生类
- 这种继承被称为菱形继承,或者钻石继承
典型的菱形继承示例图及案例:
菱形继承问题:
- 羊继承了动物的数据,驼同样继承了动物的数据,当羊驼使用数据时,就会产生二义性。
- 羊驼继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
那我们如何解决这个问题呢?那就是虚继承,子类虚继承的基类也被称为虚基类。
关键字:virtual
虚继承通过虚基类表指针(vbptr)实现基类共享,该指针指向存储偏移量的虚基类表(vbtable)
解析:
- virtual是专门为解决菱形继承的重复继承问题而存在的,比如两个类要继承同一个类,我们就不让他们直接继承,而是虚继承。
- 虚继承就是虚基类表的指针。通过该指针指向存储偏移量的虚基类表
- 如果一个类又同时继承了这两个类,就不会重复继承,而是继承一个,这两个类就是存储指向虚基类表存储在这个类内部的偏移量的指针,通过这个偏移量也能访问同一个继承成员。
虚基类表(vbtable)
编译器为每个虚继承类生成虚基类表,存储共享基类在对象中的偏移量。B和C的虚基类指针(vbptr)均指向该表。
使用虚继承(virtual)解决菱形继承问题:
class Animal
{
public:
int age;
};
//羊
class Sheep :virtual public Animal {};//虚继承
//驼
class Tuo :virtual public Animal {};//虚继承
//羊驼
class SheepTuo:public Sheep,public Tuo{};
int main()
{
SheepTuo s;
s.age = 10;
cout << s.age << endl;
//以下是相当于访问虚基类表通过偏移量找到SheepTuo的age空间,所以继承成员只有一个
cout << s.Sheep::age << endl;
cout << s.Tuo::age << endl;
//因为需要存储两个指向虚基类表的指针和一个继承过来的age所以是12个字节大小
cout << sizeof(s) << endl;
system("pause");
return 0;
}
总结:
- 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义
- 利用虚继承可以解决菱形继承问题
8. 多态
8.1 多态的基本概念
多态(Polymorphism)是面向对象编程(OOP)的三大支柱之一,其核心在于 “同一接口,多种实现”,允许程序在运行时或编译期根据对象类型或上下文自适应选择行为
多态分为两类:
- 静态多态:函数重载 和运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态的区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确认函数地址
下面是通过案例讲解多态:
先定义几个父类和子类:
class Animal
{
public:
void DoSpeack()
{
cout << "动物 在说话" << endl;
}
};
class Cat:public Animal
{
public:
void DoSpeack()
{
cout << "小猫 在说话" << endl;
}
};
class Dog :public Animal
{
public:
void DoSpeack()
{
cout << "小狗 在说话" << endl;
}
};
在C++中允许父类和子类之间的类型转换,什么意思?就比如:
void test01(Animal& animal)
{
animal.DoSpeack();
}
int main()
{
Cat c;
test01(c);
system("pause");
return 0;
}
Cat是Animal的子类,接收到Animal的所有公共、保护、私有这些成员,子类和基类的模版基本相同,然后用Animal基类的指针接收子类就可以隐式转换。
猜一下上面代码中的animal.DoSpeack()调用的是子类Cat的函数还是父类Animal的函数
觉得是调用子类的逻辑思想是接收子类就调用子类函数
觉得是父类可能逻辑思想是本来就是父类接收
运行程序:
因为普通成员函数是在编译阶段就已经绑定好地址的,所以基类的指针它早已绑定了基类自己的成员函数,就算接收到了子类那它的成员函数依旧是基类的成员函数
我们如果想要做到多态就需要把父类的这个成员函数修饰virtual
意思是将成员函数修饰成虚函数,让基类和子类的成员函数都从早绑定变成晚绑定,然后我们将子类对象作为参数传参给基类指针,因为是运行时才绑定的地址,所以它在运行时发现接收的是子类对象就绑定子类的地址。
class Animal
{
public:
virtual void DoSpeack()
{
cout << "动物 在说话" << endl;
}
};
接下来只要子类的成员变量返回类型、函数名和参数与父类的成员函数相同。我们传过来哪个子对象,就可以调用这个子对象的成员函数,这就是多态。
void test01(Animal& animal)
{
animal.DoSpeack();
}
int main()
{
Cat c;
test01(c);
Dog d;
test01(d);
system("pause");
return 0;
}
运行代码:
总结:
多态满足条件
- 有继承关系
- 子类重写父类中的虚函数
多态使用条件
- 父类指针或引用指向子类对象
重写:函数返回值类型 函数名 参数列表 完全一致称为重写
多态的深度剖析:
看下面代码,如果没有发生多态下面的animal调用的函数提早绑定父类函数了,就算接收了其他对象,调用的还是父类函数
void test01(Animal& animal)
{
animal.DoSpeack();
}
那为什么虚函数可以引发多态?
class Animal
{
public:
virtual void DoSpeack()
{
cout << "动物 在说话" << endl;
}
};
当我们先给父类的成员函数修饰成虚函数,那这个类就内部会创建一个成员指针指向虚函数表。
vfptr - 虚函数(表)指针
- v - virtual
- f - function(函数)
- ptr - pointer(指针)
这个指针指向虚函数表,而这个虚函数表内部会存储虚函数的地址
vftable - 虚函数(表)
- v - virtual
- f - function(函数)
- table - table(表)
然后我创建了一个子类Cat来继承父类Animal:
class Cat:public Animal
{
public:
};
如果子类内部没有重写(和父类的虚函数相同的成员函数),子类Cat将会继承父类的vfptr虚函数(表)指针,指向的虚函数表空间存放的还是Animal作用域下的DoSpeack函数
子类没有重写的情况:
但是,当子类Cat中有一个重写,那编译器会通过Cat存储虚函数表指针找到虚函数表并将里面父类作用域的虚函数给改为Cat作用域的重写地址
子类有重写的情况:
注:这里虚函数表Cat的重写地址将父类的虚函数地址覆盖不会影响到父类里的虚函数表
因为子类Cat在继承父类Animal的虚函数表指针时,会额外开辟一块属于自己的虚函数表
- 子类没有重写:就将继承的父类虚函数表里存储的虚函数地址初始化在自己的虚函数表
- 子类有重写:就将自己的重写地址放入虚函数表里,并存储指向自己的虚函数表的指针
总结:所以我们需要父类指针来接收子类对象,如果子类重写了父类的虚函数,父类接收子类对象会隐式转换也就是将子类的虚函数表里存储的函数地址覆盖在这个父类指针指向类的虚函数表里的函数地址
8.2 多态案例一 - 计算器类
案例描述:
分别利用普通写法和多态技术,设计实现两个操作数进行运算的计算器类
多态的优点:
- 代码组织结构清晰
- 可读性强
- 利于前期和后期的扩展以及维护
计算器的普通实现:
class Calculator
{
public:
int Cal(string f)
{
if (f == "+")
{
return num1 + num2;
}
else if (f == "-")
{
return num1 - num2;
}
else if (f == "*")
{
return num1 * num2;
}
else {
return num1 / num2;
}
}
int num1;
int num2;
};
int main()
{
Calculator c;
c.num1 = 10;
c.num2 = 20;
cout << c.num1 << " + " << c.num2 << " = " << c.Cal("+") << endl;
cout << c.num1 << " - " << c.num2 << " = " << c.Cal("-") << endl;
cout << c.num1 << " * " << c.num2 << " = " << c.Cal("*") << endl;
cout << c.num1 << " / " << c.num2 << " = " << c.Cal("/") << endl;
system("pause");
return 0;
}
计算器的多态实现:
//定义一个抽象类,也叫基类
class Abstract
{
public:
virtual int Cal()
{
return 0;
}
int num1;
int num2;
};
//加法模块
class Add:public Abstract
{
public:
int Cal()
{
return num1 + num2;
}
};
//加法模块
class Sub :public Abstract
{
public:
int Cal()
{
return num1 - num2;
}
};
//乘法模块
class Mul :public Abstract
{
public:
int Cal()
{
return num1 * num2;
}
};
//除法模块
class Div :public Abstract
{
public:
int Cal()
{
return num1 / num2;
}
};
int main()
{
//加法
Abstract* a = new Add;
a->num1 = 10;
a->num2 = 20;
cout << a->num1 << " + " << a->num2 << " = " << a->Cal() << endl;
delete a;
//减法
a = new Sub;
a->num1 = 10;
a->num2 = 20;
cout << a->num1 << " - " << a->num2 << " = " << a->Cal() << endl;
delete a;
//乘法
a = new Mul;
a->num1 = 10;
a->num2 = 20;
cout << a->num1 << " * " << a->num2 << " = " << a->Cal() << endl;
delete a;
//除法
a = new Div;
a->num1 = 10;
a->num2 = 20;
cout << a->num1 << " / " << a->num2 << " = " << a->Cal() << endl;
delete a;
system("pause");
return 0;
}
代码量虽然比较多,但是在开发过程中还是有很多人提倡使用多态,这就是因为多态开发性能好 比如:
如果想扩展新的功能,需求修改源码
在真实开发中,提倡 开闭原则
开闭原则:对扩展进行开发,对修改进行关闭
总结:C++开发提倡利用多态设计程序架构,因为多态优点很多
8.3 纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此可以将虚函数改为纯虚函数
纯虚函数语法: virtual 返回值类型 函数名 (参数列表) = 0;
当类中有了纯虚函数,这个类也称为抽象类
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
class Base
{
public:
//纯虚函数
virtual void func() = 0;
};
class Son :public Base
{
public:
};
int main()
{
Base a;//error,抽象类无法实例化
Son s;//error,如果抽象类是父类,Son子类必须重写父类,否则被视为抽象类无法实例化
return 0;
}
8.4 多态案例二 - 制作饮品
案例描述:
制作饮品的大致流程为:煮水 - 冲泡 - 倒入杯中 - 加入辅料
利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡喝茶叶
代码实现:
//饮品抽象类
class AbstractDrink
{
public:
virtual void Boil()
{
cout << "煮矿泉水" << endl;
}
virtual void drink() = 0;
virtual void cap()
{
cout << "倒入杯子" << endl;
}
virtual void Condiment() = 0;
//显示制作过程
void MakeDrink()
{
Boil();
drink();
cap();
Condiment();
}
};
//咖啡制作模块
class CoffeeDrink:public AbstractDrink
{
public:
virtual void drink()
{
cout << "冲泡咖啡" << endl;
}
virtual void Condiment()
{
cout << "放入辅料:牛奶、糖" << endl;
}
};
//茶制作模块
class TeaDrink :public AbstractDrink
{
public:
virtual void drink()
{
cout << "冲泡茶叶" << endl;
}
virtual void Condiment()
{
cout << "放入辅料:柠檬、枸杞" << endl;
}
};
void Drink(AbstractDrink* a)
{
a->MakeDrink();
delete a;
}
int main()
{
Drink(new CoffeeDrink);
cout << "------------------" << endl;
Drink(new TeaDrink);
return 0;
}
程序运行:
8.5 虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放是无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法:
virtual ~类名( ){ }
纯虚析构语法:
类内声明:virtual ~类名( ) = 0;
类外实现:作用域::~类名( ) = 0;
虚析构和纯虚析构就是为了防止子类中有堆区开辟的属性最后造成多态无法调用子类的析构函数导致内存泄漏。
虚析构、纯虚析构和虚函数、纯虚函数一样,是为了让基类指针,可以多态的调用子类的函数。如果子类的属性是堆区开辟我们就需要在基类中创建虚析构和纯虚析构。
纯虚析构必须在内部声明、外部实现,例如:
class person
{
person()
{
cout << "构造函数调用" << endl;
}
virtual ~person() = 0;//内部声明
};
person:: ~person()//外部实现
{
cout << "纯虚析构函数调用" << endl;
}
总结:
虚析构和纯虚析构就是用来解决通过父类指针释放堆区开辟的子类对象
如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
拥有纯虚析构函数的类也属于抽象类
8.6 多态案例三 - 电脑组装
案例描述:
电脑主要组成部件为CPU(用于计算),显卡(用于显示),内存条(用于存储)
将每个零件封装出抽象基类,并且提供不同的厂商生产不同的零件,例如Intel厂商和Lenovo厂商
创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口
测试时组装三台不同的电脑进行工作
代码实现:
//CPU抽象类
class Cpu
{
public:
virtual void cal() = 0;
};
//显卡抽象类
class Gpu
{
public:
virtual void display() = 0;
};
//内存条抽象类
class Ram
{
public:
virtual void Storage() = 0;
};
//Intel厂商硬件
class IntelCpu :public Cpu
{
public:
void cal()
{
cout << "Intel的CPU正在运算" << endl;
}
};
class IntelGpu :public Gpu
{
public:
void display()
{
cout << "Intel的显卡正在显示" << endl;
}
};
class IntelRam :public Ram
{
public:
void Storage()
{
cout << "Intel的内存条正在存储" << endl;
}
};
//Lenovo厂商硬件
class LenovoCpu :public Cpu
{
public:
void cal()
{
cout << "Lenovo的CPU正在运算" << endl;
}
};
class LenovoGpu :public Gpu
{
public:
void display()
{
cout << "Lenovo的显卡正在显示" << endl;
}
};
class LenovoRam :public Ram
{
public:
void Storage()
{
cout << "Lenovo的内存条正在存储" << endl;
}
};
//硬件组装电脑
class Computer
{
public:
Computer(Cpu* m_Cpu, Gpu* m_Gpu, Ram* m_Ram)
{
cpu = m_Cpu;
gpu = m_Gpu;
ram = m_Ram;
}
void CompyterRun()
{
cpu->cal();
gpu->display();
ram->Storage();
}
//对象销毁前调用
~Computer()
{
//销毁接收过来的堆空间
if (cpu != NULL)
{
delete cpu;
cpu = NULL;
}
if (gpu != NULL)
{
delete cpu;
cpu = NULL;
}
if (ram != NULL)
{
delete cpu;
cpu = NULL;
}
}
Cpu* cpu;
Gpu* gpu;
Ram* ram;
};
int main()
{
cout << "------第一台组装电脑------" << endl;
Cpu* intelcpu = new IntelCpu;
Gpu* intelgpu = new IntelGpu;
Ram* intelram = new IntelRam;
//组装过程
Computer* computer1 = new Computer(intelcpu, intelgpu, intelram);
computer1->CompyterRun();//运行电脑
delete computer1;
cout << "------第二台组装电脑------" << endl;
Computer* computer2 = new Computer(new LenovoCpu, new LenovoGpu, new LenovoRam);
computer2->CompyterRun();
delete computer2;
cout << "------第三台组装电脑------" << endl;
//混合组装
Computer* computer3 = new Computer(new LenovoCpu, new IntelGpu, new LenovoRam);
computer3->CompyterRun();
delete computer2;
system("pause");
return 0;
}
运行程序:
9. C++学习目前为止所接触到的关键字汇总
new(动态开辟) | delete(销毁) | class(类) | public(公共) |
private(私有) | protected(保护) | this(指向对象) | mutable(可变的) |
friend(友元) | operator(运算符重载) | virtual(虚拟) |
类与对象:完