五、重学C++—类(封装继承)
上一章节:
四、重学C++—CPP基础-CSDN博客https://blog.csdn.net/weixin_36323170/article/details/146298585?spm=1001.2014.3001.5501
本章源代码:
cpp · CuiQingCheng/cppstudy - 码云 - 开源中国https://gitee.com/cuiqingcheng/cppstudy/tree/master/cpp
cppClass.cpp
一、类:C++ 世界的 “事物模板”
类就像现实中 “汽车图纸”,把汽车的颜色、速度、尺寸等数据,以及加速度、制动,空调智能,娱乐方式等操作整合在一起。引入类的概念,是为了让编程模拟现实:将数据(属性)和操作(方法)封装,让复杂事物抽象化,便于管理与复用。
类定义关键字为“class”
class [类名]//类定义
{
...
}
实例代码:
// 定义“car”类
class Car {
std::string m_brand; // 数据成员:品牌
std::string m_color; // 颜色
int m_width; // 宽
int m_heigh; // 高
double m_speed; // 速度
public:
// 设置默认值
void init(std::string bStr, std::string sColor, int w, int h, double sp){
m_brand = bStr;
m_color = sColor;
m_width = w;
m_heigh = h;
m_speed = sp;
}
// 成员函数:设置品牌
void setBrand(std::string b) { m_brand = b; }
// 成员函数:显示Car信息
void showInfo() {
std::cout << "Brand:" << m_brand << ", color:" << m_color << " W:" << m_width << " H:" << m_heigh << " speed:" << m_speed << std::endl;
}
};
补充:类的大小,根据其内部定义的属性的大小决定,比如上面定义的这个类,大小为:80字节;
若是一个空类,如下:其大小为1字节
class emptyClass{};
std::cout << "class emptyClass size:" << sizeof(emptyClass) << std::endl; // 为1个字节
二、封装的核心角色:构造函数与析构函数
1、构造函数
对象的 “诞生礼”如同新生儿出生时登记信息,构造函数在对象创建时自动执行,完成成员变量初始化。
构造函数名与类名相同;
代码示例:
class Book {
std::string m_title;
int m_pages;
public:
// 构造函数:初始化书籍信息
Book(std::string t, int p) {
m_title = t;
m_pages = p;
std::cout << "构造函数调用:"<<" 新书《" << m_title << "》诞生,共" << m_pages << "页" << std::endl;
}
};
子类构造的顺序:父类构造——>子类构造
2、析构函数
对象的 “退场仪式”
对象生命周期结束时,析构函数释放资源,如同离开房间后收拾物品。
class ImageLoader {
std::string m_imageName;
public:
ImageLoader(std::string nM) {
m_imageName = nM;
std::cout << "构造函数调用,加载图像,分配内存" << std::endl;
}
~ImageLoader() { // 析构函数释放内存
std::cout << "析构函数调用图像卸载,内存释放" << std::endl;
}
};
2.1、虚析构函数
多态世界的 “安全锁”
在多态场景下,防止内存泄漏。如同不同包装的快递,拆开时确保清理所有包装。(主要用于基类的指针指向派生类的对象时,用于析构时,需要释放基类的内存)
例如:
class Figure {
public:
virtual ~Figure() {} // 虚析构函数
};
class Rectangle : public Figure {
int* width;
public:
Rectangle() { width = new int(10); }
~Rectangle() { delete width; }
};
// 多态使用与安全释放
Figure* fig = new Rectangle();
delete fig; // 调用虚析构,避免内存泄漏
注意:由以上代码中,可以发现,类的声明定义,构造函数/析构函数并不是必须的,类在没有构造函数的时候,会调用默认的构造,但是无法在构造阶段对成员变量进行初始化;析构函数同样的,也不是必须要定义的,其定义的目的是对变量的退出,进行一个资源释放。
子类对象析构的顺序:子类析构——>父类析构
3、访问控制:类的 “门禁系统”
public(公共区):像商场大门,类内外都能访问。
private(私密区):如同卧室,只有类内部成员能进入。
protected(家族区):类似家族内部场地,类和子类成员可访问。
默认控制:类中成员默认 private,结构体默认 public。
class Apartment {
private: // 私有权限,默认也是private
std::string m_bedroom; // 卧室(外人不可见)
protected: // 保护权限
std::string m_study; // 书房(子类可访问)
public: // 公共权限
std::string m_livingRoom; // 客厅(谁都能访问)
void cleanRooms() {
// 类内可访问所有成员
m_bedroom = "整理卧室";
m_study = "打扫书房";
m_livingRoom = "清扫客厅";
}
virtual ~Apartment()
{
std::cout << "Apartment 析构函数:" << m_bedroom << " " << m_study << " " << m_livingRoom << std::endl;
}
};
class SmallApartment : public Apartment {
public:
void useStudy() {
m_study = "在书房工作"; // 子类可访问protected成员
}
};
三、继承:代码的 “遗传密码”
继承如同孩子继承父母的特性,作用是复用代码 + 扩展功能。
1、按照基础权限分
三种继承权限
public 继承:父类的 public/protected 成员,在子类保持原样,像贵族血统传承。
protected 继承:父类 public 成员变 protected,如同家族手艺只传内部。
private 继承:父类成员全变 private,像秘密只藏在当前类。
代码示例:
// 基类:交通工具
class Vehicle {
public:
virtual ~Vehicle(){}
void move() {
std::cout << "Vehicle 移动中... " << m_model << std::endl;
}
protected:
std::string m_model;
};
// public继承:汽车类
class ChildCar : public Vehicle {
public:
void setModel(std::string m) {
m_model = m; // public继承可访问protected成员
move(); // 可访问public成员
}
};
// protected继承:公交车类
class Bus : protected Vehicle {
public:
void run() {
m_model = "Bus";
move(); // protected继承,类内可访问
}
};
// private继承:玩具车类
class ToyCar : private Vehicle {
public:
void play() {
m_model = "ToyCar";
move(); // private继承,仅类内可用
}
};
2、按照继承数量分
单继承:(略)上面列举均是单继承,即只继承了一个父类
多继承:一个子类继承多个基类,用于融合多种不同特性。
class Flyable {
public:
void func(){
fly();
}
private:
void fly() {
std::cout << "我可以飞" << std::endl;
}
};
class Swimable {
public:
void func(){
swim();
}
private:
void swim() {
std::cout << "我可以游泳" << std::endl;
}
};
class FlyingFish : public Flyable, public Swimable { // 多继承
public:
void func()
{
Flyable::func();
Swimable::func();
}
};
多继承可能引发 “菱形继承” 等问题,需谨慎使用。
菱形继承:
菱形继承存在问题
a、数据冗余:A 的成员在 D 中存储两次。
b、访问二义性:调用 A 的成员时,编译器不知道该选哪一份(如 D d; d.fun(); 若 A 有 fun,编译器无法确定路径)。
通过
virtual 关键字让基类在继承体系中只保留一份实例。语法上,子类声明继承时使用 virtual,确保最终派生类(如上述 D)只包含基类(A)的一个副本,消除冗余和二义性,同时,子类对象在释放时,避免了释放两次基类的现象。
// 基类 A
class A {
public:
~A(){
std::cout << "A 析构" << std::endl;
}
public:
int value;
};
// B 虚继承 A
class B : virtual public A {
public:
~B(){
std::cout << "B 析构" << std::endl;
}
};
// C 虚继承 A
class C : virtual public A {
public:
~C(){
std::cout << "C 析构" << std::endl;
}
};
// D 继承 B 和 C
class D : public B, public C {
public:
~D(){
std::cout << "D 析构" << std::endl;
}
};
// 菱形继承
D d;
d.value = 10; // 直接访问,无歧义,因虚继承后只有一份 A 的 value
std::cout << d.value << std::endl; // 输出 10