C++ 类和对象全解析:从基础语法到高级特性

在 C++ 编程中,类和对象是面向对象编程(OOP)的核心基石,封装、继承、多态三大特性均围绕其展开。本文将从类的定义与实例化、默认成员函数、高级特性等维度,结合实战代码,系统梳理类和对象的关键知识点,帮助开发者夯实 OOP 基础。

一、类的基础认知:定义、访问控制与实例化

1.1 类的定义格式

C++ 使用class关键字定义类(struct也可定义类,兼容 C 语言用法且支持成员函数),类体包含成员变量(属性)和成员函数(方法),结束时必须加分号 。为区分成员变量与局部变量,惯例是给成员变量加前缀(如_)或后缀,例如_yearm_month

复制代码
class Date {
public:
    // 成员函数:初始化日期
    void Init(int year, int month, int day) {
        _year = year;
        _month = month;
        _day = day;
    }
private:
    // 成员变量:加前缀_区分
    int _year;
    int _month;
    int _day;
}; // 分号不可省略

1.2 访问限定符:封装的核心实现

访问限定符控制成员的访问权限,是封装特性的直接体现:

  • public:类外可直接访问(通常暴露接口函数);
  • private/protected:类外不可直接访问(通常隐藏成员变量,继承时二者有差异);
  • 访问权限作用域:从限定符出现位置到下一个限定符结束,class默认privatestruct默认public

1.3 类域与成员函数分离

类定义了独立的作用域,类外实现成员函数时需用::作用域操作符指明所属类,否则编译器会视为全局函数。

复制代码
// 类内声明,类外实现
void Date::Init(int year, int month, int day) {
    _year = year;
    _month = month;
    _day = day;
}

1.4 类的实例化

类是对象的 "设计图",仅声明成员变量(未分配空间),实例化是通过类创建对象并分配物理内存的过程。一个类可实例化多个对象,每个对象拥有独立的成员变量存储空间,成员函数则共享(存储在代码段)。

复制代码
int main() {
    Date d1; // 实例化对象d1,分配内存
    d1.Init(2024, 10, 1); // 调用成员函数初始化
    return 0;
}

1.5 对象大小计算

对象仅存储成员变量,大小遵循内存对齐规则(与结构体一致),目的是提高访问效率:

  1. 第一个成员偏移量为 0;

  2. 其他成员对齐到 "对齐数"(编译器默认值与成员大小的较小值,VS 默认 8)的整数倍;

  3. 总大小为最大对齐数的整数倍;

  4. 无成员变量的类对象大小为 1 字节(占位标识对象存在)。

    class A {
    private:
    char _ch; // 1字节
    int _i; // 4字节,对齐数4
    };
    // 内存对齐后大小:8字节(1+3填充+4)
    cout << sizeof(A) << endl; // 输出8

二、this 指针:对象的 "隐藏身份标识"

2.1 核心问题

多个对象共享成员函数,函数如何区分操作的是哪个对象?例如d1.Init()d2.Init(),函数需知道当前操作的是d1还是d2

2.2 本质与特性

C++ 编译器在成员函数形参第一个位置隐含添加this指针(类型为类名* const),指向当前调用函数的对象,函数体内访问成员变量本质是通过this指针访问(可显式使用,不可显式声明)。

复制代码
// 编译器优化后的Init函数原型
void Date::Init(Date* const this, int year, int month, int day) {
    this->_year = year; // 显式使用this
    _month = month;    // 隐式使用this
}

// 调用时编译器自动传递对象地址
d1.Init(2024, 10, 1); // 等价于d1.Init(&d1, 2024, 10, 1)

2.3 经典面试题解析

复制代码
// 题目1:编译运行结果?
class A {
public:
    void Print() { cout << "A::Print()" << endl; }
private:
    int _a;
};
int main() {
    A* p = nullptr;
    p->Print(); // 正常运行:Print未访问成员变量,无需解引用p
    return 0;
}

// 题目2:编译运行结果?
class A {
public:
    void Print() { cout << _a << endl; } // 访问成员变量,需解引用this
private:
    int _a;
};
int main() {
    A* p = nullptr;
    p->Print(); // 运行崩溃:this为nullptr,解引用出错
    return 0;
}

三、默认成员函数:编译器的 "自动实现"

当用户未显式定义时,编译器会自动生成 6 个默认成员函数,核心是前 4 个:构造、析构、拷贝构造、赋值重载。

3.1 构造函数:对象的 "初始化器"

  • 功能 :替代Init函数,对象实例化时自动调用,初始化成员变量;

  • 特性

    1. 函数名与类名相同,无返回值(无需写void);
    2. 可重载(无参、带参、全缺省);
    3. 无显式定义时,编译器生成默认构造(对内置类型不初始化,自定义类型调用其默认构造);
    4. 无参、全缺省、编译器默认生成的构造,统称 "默认构造"(不传参即可调用)。

    class Date {
    public:
    // 全缺省构造(推荐,兼顾多种初始化场景)
    Date(int year = 1, int month = 1, int day = 1) {
    _year = year;
    _month = month;
    _day = day;
    }
    private:
    int _year;
    int _month;
    int _day;
    };

    // 实例化方式
    Date d1; // 调用全缺省构造,默认1-1-1
    Date d2(2024, 10, 1); // 调用带参构造

3.2 析构函数:对象的 "清理工"

  • 功能 :替代Destroy函数,对象生命周期结束时自动调用,释放资源(如堆内存);

  • 特性

    1. 函数名~类名,无参数无返回值;
    2. 一个类仅一个析构函数(不可重载);
    3. 无显式定义时,编译器生成默认析构(对内置类型不处理,自定义类型调用其析构);
    4. 有资源申请(如mallocnew)时必须显式定义,否则内存泄漏。

    class Stack {
    public:
    Stack(int n = 4) {
    _a = (int*)malloc(sizeof(int) * n);
    _capacity = n;
    _top = 0;
    }
    // 显式定义析构,释放堆内存
    ~Stack() {
    free(_a);
    _a = nullptr;
    _top = _capacity = 0;
    }
    private:
    int* _a;
    size_t _capacity;
    size_t _top;
    };

3.3 拷贝构造函数:对象的 "复制器"

  • 功能 :用已有对象初始化新对象(如Date d2 = d1);

  • 特性

    1. 第一个参数必须是const 类名&(传值会引发无穷递归),后续参数可带默认值;
    2. 无显式定义时,编译器生成默认拷贝构造(内置类型值拷贝 / 浅拷贝,自定义类型调用其拷贝构造);
    3. 浅拷贝问题:若成员变量指向堆内存(如Stack_a),会导致多个对象共享同一块内存,析构时重复释放崩溃,需显式实现深拷贝。

    Stack::Stack(const Stack& st) {
    // 深拷贝:为新对象分配独立内存
    _a = (int*)malloc(sizeof(int) * st._capacity);
    if (_a == nullptr) perror("malloc fail");
    memcpy(_a, st._a, sizeof(int) * st._top);
    _top = st._top;
    _capacity = st._capacity;
    }

3.4 赋值运算符重载:对象的 "赋值器"

  • 功能 :两个已存在对象间的赋值(如d1 = d2),区别于拷贝构造(初始化新对象);

  • 特性

    1. 必须重载为成员函数,函数原型类名& operator=(const 类名&)
    2. 返回类名&支持连续赋值(如d1 = d2 = d3);
    3. 需检查自赋值(if (this != &d)),避免重复释放;
    4. 无显式定义时,编译器生成默认赋值重载(浅拷贝,需资源的类需显式实现深拷贝)。

    Date& Date::operator=(const Date& d) {
    if (this != &d) { // 避免自赋值
    _year = d._year;
    _month = d._month;
    _day = d._day;
    }
    return *this; // 支持连续赋值
    }

四、高级特性:提升代码灵活性与效率

4.1 初始化列表:成员变量的 "初始化源头"

  • 格式 :构造函数后加:,后跟成员变量初始化表达式(成员变量(值), ...);

  • 必要性 :引用成员、const成员、无默认构造的自定义类型成员,必须通过初始化列表初始化;

  • 注意:初始化顺序与类中成员声明顺序一致,与列表顺序无关。

    class Date {
    public:
    // 初始化列表初始化
    Date(int year, int month, int day, int& ref)
    : _year(year)
    , _month(month)
    , _day(day)
    , _ref(ref) // 引用必须初始化
    , _n(10) // const成员必须初始化
    {}
    private:
    int _year;
    int _month;
    int _day;
    int& _ref; // 引用成员
    const int _n; // const成员
    };

4.2 static 成员:类的 "共享资源"

  • 静态成员变量
    1. static修饰,所有对象共享,存储在静态区;
    2. 类内声明,类外初始化(类型 类名::变量名 = 值);
    3. 受访问限定符控制,可通过类名::变量对象.变量访问。
  • 静态成员函数
    1. static修饰,无this指针;
    2. 仅可访问静态成员变量 / 函数,不可访问非静态成员。

应用场景:统计对象创建个数:

复制代码
class A {
public:
    A() { ++_scount; }
    A(const A&) { ++_scount; }
    ~A() { --_scount; }
    static int GetCount() { return _scount; } // 静态成员函数
private:
    static int _scount; // 静态成员变量
};
int A::_scount = 0; // 类外初始化

int main() {
    A a1, a2;
    cout << A::GetCount() << endl; // 输出2
    return 0;
}

4.3 友元:突破封装的 "特殊权限"

友元允许外部函数 / 类访问类的私有成员,分为友元函数和友元类,慎用(破坏封装)

  • 友元函数 :非成员函数,类内声明时加friend

  • 友元类:类 A 是类 B 的友元,则 A 的所有成员函数可访问 B 的私有成员(单向关系,不可传递)。

    class Date {
    // 友元声明:operator<<可访问私有成员
    friend ostream& operator<<(ostream& out, const Date& d);
    private:
    int _year;
    int _month;
    int _day;
    };

    // 全局函数实现
    ostream& operator<<(ostream& out, const Date& d) {
    out << d._year << "-" << d._month << "-" << d._day;
    return out;
    }

    // 使用
    Date d(2024, 10, 1);
    cout << d << endl; // 输出2024-10-1

4.4 匿名对象:临时使用的 "无名称对象"

  • 格式类名(实参),生命周期仅当前行;

  • 场景:临时调用成员函数,无需定义命名对象。

    class Solution {
    public:
    int Sum(int n) { return n*(n+1)/2; }
    };

    // 匿名对象调用函数
    int ret = Solution().Sum(100); // 输出5050

五、C++ vs C 语言:封装的优势

Stack为例,对比 C 语言与 C++ 的实现差异:

特性 C 语言实现 C++ 实现
数据与函数关系 分离(函数需显式传结构体指针) 封装(数据 + 函数在类内,this 指针隐式传递)
访问控制 无,可直接修改结构体成员 访问限定符控制,私有成员不可直接修改
初始化与清理 需手动调用Init/Destroy 构造 / 析构函数自动调用,避免遗漏
代码简洁性 需 typedef,函数参数繁琐 类名直接作为类型,语法更简洁

六、总结

类和对象是 C++ 面向对象编程的核心,核心要点可概括为:

  1. 封装:通过类整合数据与函数,访问限定符控制权限;
  2. 默认成员函数:构造(初始化)、析构(清理)、拷贝构造(复制新对象)、赋值重载(对象赋值)是基础,需区分使用场景;
  3. 高级特性:初始化列表解决特殊成员初始化,static 成员实现共享资源,友元灵活访问私有成员(慎用),匿名对象简化临时操作;
  4. 内存与效率:理解 this 指针、内存对齐、编译器拷贝优化,避免内存泄漏和性能问题。
相关推荐
fpcc18 小时前
模板编程—模板和元编程中的错误输出
c++·模板编程
一勺菠萝丶18 小时前
Java 后端想学 Vue,又想写浏览器插件?
java·前端·vue.js
Tao____18 小时前
企业级物联网平台
java·网络·物联网·mqtt·网络协议
山峰哥18 小时前
数据库工程与SQL调优实战:从原理到案例的深度解析
java·数据库·sql·oracle·性能优化·编辑器
小温冲冲18 小时前
C++与QML信号绑定完全指南:实现跨语言无缝通信
c++
kaico201818 小时前
远程调用组件openfeign
java·spring cloud
SunnyDays101118 小时前
如何使用 JAVA 将 PDF 转换为 PPT:完整指南
java·开发语言·pdf转ppt
qq_124987075318 小时前
基于springboot归家租房小程序的设计与实现(源码+论文+部署+安装)
java·大数据·spring boot·后端·小程序·毕业设计·计算机毕业设计
是一个Bug18 小时前
Java后端开发面试题清单(50道)
java·开发语言·jvm