C++ 中的类(Class)与对象(Object)是面向对象编程(OOP)的核心。
1 面向对象的三大特性
C++ 面向对象的三大核心特性是:封装、继承、多态。
封装 (Encapsulation)是将属性(变量)和行为(函数)作为一个整体,并加以权限(public、protected、private)控制。类内的public、protected、private均可访问,类外仅public可访问。
继承 (Inheritance)是为了减少重复代码,子类(派生类)可以继承父类(基类)的成员,是实现"多态"的基石。通过class 子类名:继承方式 父类名 {},子类在父类的访问权限如下:
|-----------|-----------|-------------|-----------|
| 父类成员权限 | public继承 | protected继承 | private继承 |
| Public | public | protected | private |
| Protected | protected | protected | private |
| Private | - | - | - |
多态 (Polymorphism)分为 静态多态 和 动态多态**。** 静态多态包括 函数重载、运算符重载(编译阶段确定地址)。动态多态包括 派生类和虚函数 实现运行时多态(运行阶段确定地址)。动态多态满足 继承关系、子类重写(Overwrite)父类的虚函数(函数返回值、函数名、参数列表完全一致)、父类指针或引用指向子类对象。
2 类class
2.1初始化
对象的生命周期由构造和析构函数管理。如果程序员不提供,编译器 会自动提供默认的空实现。构造函数是在创建对象时 自动调用,可重载。析构函数是对象销毁前 自动调用,不可重载,无参数。构造函数按参数可分为无参构造和有参构造,有参构造分为 普通构造 和 拷贝构造,拷贝构造是将同类型对象的数据原封不动传递给新对象,通过常量引用 (const Student& s)作为参数的方式。
成员是普通类型:int,double,float
cpp
#include <iostream>
#include <string>
using namespace std;
class Student {
public: // 公共权限
Student() { // 【无参构造】(默认构造)
m_Name = " ";
m_Age = 0;
m_Score = 0;
cout << "【无参构造】被调用" << endl;
}
Student(string name, int age, double score) { //【有参构造】(同时也是普通构造)
m_Name = name;
m_Age = age;
m_Score = score;
cout << "【有参/普通构造】被调用" << endl;
}
Student(const Student& s) { // 【拷贝构造函数】
m_Name = s.m_Name;
m_Age = s.m_Age;
m_Score = s.m_Score; // 直接赋值 = 浅拷贝(会导致内存重复释放 崩溃)
cout << "【拷贝构造函数】被调用了" << endl;
}
~Student() { // 【析构函数】
cout << "【析构函数】被调用,对象已销毁" << endl;
}
void showInfo() { // 外部接口:用于打印信息
cout << "姓名: " << m_Name
<< ", 年龄: " << m_Age
<< ", 成绩: " << m_Score
<< ", 学号(私有): " << m_Id << endl;
}
protected: // 保护权限
string m_Name; // 保护成员:本类和子类可以访问,类外不可访问
int m_Age;
private: // 私有权限
double m_Score;
string m_Id = "20260001"; // 私有成员:仅本类内部可以访问
};
int main() {
Student s; // 实例化一个对象 s
s.showInfo(); // 对 public(公共权限)的访问
Student s4 = Student();
// 对 protected(保护权限)的访问 (编译报错)
// s.m_Name = "张三";
// s.m_Age = 18;
// 对 private(私有权限)的访问 (编译报错)
// s.m_Score = 90.0;
// s.m_Id = "20260002";
Student s2("张三", 18, 95.5); // 括号法, 调用有参构造/普通构造
Student s12 = Student("张三", 18, 90); //显示法
Student s3 = {"王五", 20, 85.0}; //隐式转换法
Student("王勾", 20, 85.0) //匿名对象,当前执行结束,系统会立即回收匿名对象
Student s6(s2); // 调用拷贝构造
Student s8 = s6; //隐式转换法
Student(s8) //不要利用拷贝构造函数初始化匿名对象,编译器默认Student(s8)=Student s8
return 0;
}
成员有指针
当类中存在指针成员且在构造函数中用 new 申请了堆区内存时,如果使用编译器默认的拷贝构造函数,会引发致命的浅拷贝崩溃问题 与内存泄漏(Memory Leak) 。因为默认拷贝只是简单地复制指针所存的内存地址(即浅拷贝),即两个 不同的对象同时 指向堆区的同一块内存 ,当对象生命周期结束时,先销毁的对象会通过析构函数顺利释放该堆内存,而随后销毁的对象在调用析构函数时,会对已经被释放的同一块内存进行二次释放(Double Free),从而直接引发程序异常崩溃。析构函数中没有手动释放这些堆内存,随后销毁的对象在栈区的指针变量虽被回收,但其指向的堆区内存会变成无法访问的"孤儿内存"(内存被锁死,系统不能回收利用),在对象不断创建和销毁的过程中将堆区内存无限吃掉,导致严重的内存泄漏。为了解决这一系列由于"共享堆内存"引发的灾难,解决办法是必须在类中同时手写析构函数和拷贝构造函数来实现深拷贝 :在析构函数中加入**delete语句以释放对象指针成员** 指向的堆区内存 ,并在拷贝构造函数中通过 new在堆区 为新对象重新申请一块完全独立的内存空间,再将原对象堆区里的值复制,使各个对象各自独立管理自己的堆区内存,在分别调用析构函数时互不干扰,实现内存的安全闭合。
内存碎片(Memory Fragmentation):系统能回收再利用。
cpp
#include <iostream>
using namespace std;
class Worker {
public:
Worker(int age, int id) {
m_Age = age;
m_ID = new int(id); // new在堆区(Heap)开辟一块内存存储工号,并用指针 m_ID 指向它
cout << "【有参构造】在堆区申请了内存,地址为: " << m_ID << endl;
}
// 手动实现【深拷贝】构造函数
Worker(const Worker& w) { //编译器会默认提供【浅拷贝】(即:m_ID = w.m_ID;仅仅复制了指针地址)
m_Age = w.m_Age; //浅拷贝
m_ID = new int(*w.m_ID); // 深拷贝的写法(安全):重新在堆区申请一块内存,然后把值复制过来
cout << "【深拷贝构造】在堆区开辟了独立内存,地址为: " << m_ID << endl;
}
~Worker() { // 析构函数(释放堆区内存)
if (m_ID != nullptr) {
cout << "【析构函数】正在释放堆区内存,地址为: " << m_ID << endl;
delete m_ID; // 释放指针指向的内存
m_ID = nullptr; // 将指针置空,防止野指针
}
}
void showInfo() const {
cout << "工人年龄: " << m_Age << ", 工人卡号: " << *m_ID << endl;
}
private:
int m_Age; // 普通变量(存储在栈区)
int* m_ID; // 指针变量(指向堆区)
};
void test01() {
cout << "--- 场景 A 开始 ---" << endl;
Worker w1(30, 1001);
w1.showInfo();
Worker w2(w1); // 2. 调用拷贝构造创建对象 w2(触发我们写的深拷贝)
w2.showInfo();
cout << "--- 场景 A 结束(系统自动调用析构函数) ---" << endl;
}
void test02() {
cout << "\n--- 场景 B 开始(堆区指针调用) ---" << endl;
// 1. 用指针方式调用有参构造
Worker* w_ptr1 = new Worker(25, 2002); // w_ptr 是一个指向 Worker 对象的指针,对象本身在堆区
w_ptr1->showInfo(); // 指针调用类内成员函数必须用 -> 指针运算符
// 2. 用指针方式调用拷贝构造
Worker* w_ptr2 = new Worker(*w_ptr1); // new Worker() 括号里要传入对象本身,需要对 w_ptr1 进行解引用 *w_ptr1
w_ptr2->showInfo();
cout << "--- 准备手动释放堆区对象 ---" << endl;
// 重点:堆区创建的对象如果不手动 delete,到程序结束都不会调用析构函数!
delete w_ptr1; // 释放 w_ptr1 对象的内存,同时触发 w_ptr1 的析构函数
delete w_ptr2; // 释放 w_ptr2 对象的内存,同时触发 w_ptr2 的析构函数
cout << "--- 场景 B 结束 ---" << endl;
}
this指针
包含类成员
静态成员
指针
const修饰成员函数
2.2友元
2.3运算符重载
2.4继承
子类继承父类后,父类中的成员在子类中权限会改变,取决于子类的继承方式。当创建一个子类对象时,父类和子类的构造、析构函数的执行顺序是 父类构造 -->子类构造 --> 子类析构 --> 父类析构。如果子类定义了一个和父类一模一样的变量名或函数,父类的同名成员会被隐藏(Hide),子类的变量名或函数可直接访问,访问父类的变量名或函数 需要加作用域(父类名::)。
2.4.1 同名成员
cpp
#include <iostream>
#include <string>
using namespace std;
class Base { //父类
public:
int m_A; // 父类同名变量
Base() {
m_A = 100;
}
void func() { // 父类同名函数
cout << "【Base】:: func() 被调用" << endl;
}
void func(int a) { // 父类同名函数的【重载版本】
cout << "【Base】:: func(int a) 重载版本被调用,参数 a = " << a << endl;
}
};
class Son : public Base { // 子类
public:
int m_A; // 子类同名变量,会隐藏父类的 m_A
Son() {
m_A = 200;
}
void func() { // 子类同名函数,会隐藏父类所有的 func 版本
cout << "【Son】 :: func() 被调用" << endl;
}
void testInternal() { // 子类内部访问测试函数
// 类内访问同名成员的规则与类外完全一致
cout << "类内直接访问 m_A: " << m_A << endl; // 默认子类
cout << "类内作用域访问 Base::m_A: " << Base::m_A << endl; // 指定父类
}
};
int main() {
Son s;
cout << "------ 同名变量测试 ------" << endl;
cout << "直接访问 s.m_A = " << s.m_A << endl; //直接访问:默认访问的是【子类】的同名变量, 输出 200
// 作用域访问:必须加 `Base::` 才能访问【父类】的同名变量
cout << "加作用域访问 s.Base::m_A = " << s.Base::m_A << endl; // 输出 100
cout << "\n------ 同名函数测试 ------" << endl;
s.func(); // 直接访问:默认调用【子类】的函数,输出 "【Son】 :: func() 被调用"
s.Base::func(); // 作用域访问:调用【父类】的函数,输出 "【Base】:: func() 被调用"
//同名隐藏重载
cout << "\n------ 隐藏重载测试 ------" << endl;
// s.func(10); 子类没有这个重载函数,父类才有,要调用父类重载函数
// 编译报错!提示:no matching function for call to 'Son::func(int)'
// 【核心原理】:只要子类出现了和父类同名的函数,子类会把父类中"所有"同名函数的重载版本通通隐藏!
s.Base::func(10); // 调用父类的重载版本
return 0;
}