C++ 系统学习日记・第 14 天|面向对象核心:类与对象的封装全解

4 阅读10分钟

📖 学习信息

学习课程:黑马 C++ 零基础入门教程

本日学习:C++ 面向对象编程核心 —— 封装特性包含:面向对象 vs 面向过程、类与对象的基本概念、类的定义与对象创建、访问权限控制、成员属性与成员函数、struct 与 class 的区别、封装的意义与实践学习目标:理解面向对象编程思想,掌握类的定义和对象的使用方法,熟练运用封装特性设计安全、可复用的代码,为后续学习继承和多态打下坚实基础


一、前言

前面我们学习的都是面向过程编程:分析解决问题的步骤,用函数一步步实现这些步骤,调用函数解决问题。但当程序规模越来越大、逻辑越来越复杂时,面向过程的代码会变得臃肿、难以维护和扩展。面向对象编程(OOP) 是一种更符合人类思维的编程思想:将现实世界中的事物抽象为对象,每个对象包含属性(数据)和行为(函数),通过对象之间的交互解决问题。C++ 是一门面向对象的语言,封装、继承、多态 是面向对象的三大核心特性,而封装是三大特性之首,是学习面向对象的第一步。


二、面向过程 vs 面向对象

1. 面向过程

  • 核心思想:步骤化,关注 "怎么做"
  • 特点:以函数为单位,数据和函数分离
  • 缺点:代码复用性差、扩展性差、维护困难

2. 面向对象

  • 核心思想:对象化,关注 "谁来做"
  • 特点:以对象为单位,将数据(属性)和操作数据的函数(行为)封装在一起
  • 优点:代码复用性高、扩展性好、易于维护和理解

示例:设计一个 "圆" 的程序

  • 面向过程:写一个函数计算圆的周长,再写一个函数计算圆的面积,传入半径作为参数
  • 面向对象:设计一个 "圆" 类,包含 "半径" 属性和 "计算周长"、"计算面积" 两个行为,创建圆对象后直接调用对象的方法

三、类与对象的基本概念

1. 什么是类

类是对一类具有相同属性和行为的事物的抽象描述,是一个模板、一个蓝图。比如:所有的学生都有姓名、年龄、分数这些属性,都有学习、考试这些行为,我们就可以抽象出一个 "学生类"。

2. 什么是对象

对象是类的具体实例,是根据类这个模板创建出来的具体事物。比如:"张三" 是学生类的一个具体对象,"李四" 是学生类的另一个具体对象。

3. 类与对象的关系

  • 类是模板,对象是根据模板创建的实例
  • 一个类可以创建无数个对象
  • 所有对象都拥有类中定义的属性和行为

四、类的定义与对象创建

1. 类的定义语法

class 类名 {
    // 访问权限
    // 成员属性:描述对象的特征(数据)
    // 成员函数:描述对象的行为(函数)
};  // 注意:末尾必须有分号!

示例:定义一个圆类

// 圆类
class Circle {
public:  // 公共访问权限
    // 成员属性:半径
    int m_r;

    // 成员函数:计算周长
    double calculatePerimeter() {
        return 2 * 3.14 * m_r;
    }

    // 成员函数:计算面积
    double calculateArea() {
        return 3.14 * m_r * m_r;
    }
};

2. 对象的创建与使用

类定义完成后,就可以像使用 int、double 一样,创建类的对象并使用。创建对象有两种方式:栈区创建堆区创建

(1)栈区创建对象(最常用)

#include <iostream>
using namespace std;

class Circle {
public:
    int m_r;

    double calculatePerimeter() {
        return 2 * 3.14 * m_r;
    }

    double calculateArea() {
        return 3.14 * m_r * m_r;
    }
};

int main()
{
    // 栈区创建圆对象c1
    Circle c1;
    // 给对象的属性赋值
    c1.m_r = 10;

    // 调用对象的成员函数
    cout << "圆的半径:" << c1.m_r << endl;
    cout << "圆的周长:" << c1.calculatePerimeter() << endl;
    cout << "圆的面积:" << c1.calculateArea() << endl;

    return 0;
}

(2)堆区创建对象

int main()
{
    // 堆区创建圆对象,返回指针
    Circle *c2 = new Circle;
    // 通过指针访问成员
    c2->m_r = 20;
    cout << "圆的面积:" << c2->calculateArea() << endl;

    // 手动释放堆内存
    delete c2;
    c2 = nullptr;

    return 0;
}

3. 成员访问语法

  • 栈区对象:对象名.成员属性 / 对象名.成员函数()
  • 堆区对象:对象指针->成员属性 / 对象指针->成员函数()

五、访问权限控制(封装核心)

C++ 提供了三种访问权限,用于控制类的成员(属性和函数)的访问范围,这是实现封装的核心机制。

1. 三种访问权限

表格

访问权限含义访问范围
public公共权限类内部可以访问,类外部也可以访问
private私有权限只有类内部可以访问,类外部不能访问
protected保护权限类内部可以访问,子类可以访问,类外部不能访问(继承时讲解)

2. 核心规则

  • 类的成员如果不指定访问权限,默认是 private
  • 封装的核心思想:将所有成员属性私有化,提供公共的成员函数来访问和修改属性

3. 示例:封装学生类

#include <iostream>
#include <string>
using namespace std;

class Student {
private:  // 私有成员:类外部不能直接访问
    // 成员属性私有化
    string m_name;
    int m_age;
    double m_score;

public:  // 公共成员:提供对外访问接口
    // 设置姓名
    void setName(string name) {
        m_name = name;
    }

    // 获取姓名
    string getName() {
        return m_name;
    }

    // 设置年龄(添加合法性校验)
    void setAge(int age) {
        if (age >= 0 && age <= 120) {  // 年龄必须在0-120之间
            m_age = age;
        } else {
            cout << "年龄输入不合法!" << endl;
            m_age = 0;
        }
    }

    // 获取年龄
    int getAge() {
        return m_age;
    }

    // 设置分数
    void setScore(double score) {
        if (score >= 0 && score <= 100) {
            m_score = score;
        } else {
            cout << "分数输入不合法!" << endl;
            m_score = 0;
        }
    }

    // 获取分数
    double getScore() {
        return m_score;
    }

    // 显示学生信息
    void showInfo() {
        cout << "姓名:" << m_name << ",年龄:" << m_age << ",分数:" << m_score << endl;
    }
};

int main()
{
    Student s1;
    // 不能直接访问私有属性:s1.m_name = "张三";  // 错误!
    // 只能通过公共接口访问和修改
    s1.setName("张三");
    s1.setAge(20);
    s1.setScore(95.5);
    s1.showInfo();

    // 测试非法输入
    s1.setAge(200);  // 输出"年龄输入不合法!"
    cout << "年龄:" << s1.getAge() << endl;  // 输出0

    return 0;
}

4. 封装的意义

  1. 提高代码安全性:通过公共接口访问私有属性,可以添加合法性校验,防止非法数据
  2. 提高代码复用性:将属性和行为封装在类中,多个对象可以复用同一个类的代码
  3. 降低代码耦合度:类外部不需要知道类内部的实现细节,只需要调用公共接口即可
  4. 便于维护和扩展:修改类内部的实现不会影响类外部的调用

六、struct 与 class 的区别

C++ 中 struct 和 class 都可以用来定义类,它们的唯一区别是默认访问权限不同

  • struct:默认访问权限是 public
  • class:默认访问权限是 private

示例

// struct定义的类,默认public
struct Person1 {
    string name;  // 默认public
    int age;      // 默认public
};

// class定义的类,默认private
class Person2 {
    string name;  // 默认private
    int age;      // 默认private
};

int main()
{
    Person1 p1;
    p1.name = "张三";  // 正确:struct默认public
    p1.age = 20;       // 正确

    Person2 p2;
    // p2.name = "李四";  // 错误:class默认private,类外部不能访问
    // p2.age = 20;       // 错误

    return 0;
}

使用建议

  • 当只需要封装简单的数据结构时,使用 struct
  • 当需要封装属性和行为,实现复杂逻辑时,使用 class

七、成员函数的分文件编写

和普通函数一样,类的成员函数也可以分文件编写,将类的声明放在头文件(.h)中,类的实现放在源文件(.cpp)中,这是工业级开发的标准规范。

1. 分文件编写步骤

  1. 创建头文件 Student.h:存放类的声明
  2. 创建源文件 Student.cpp:存放类的成员函数的实现
  3. 在主函数文件中包含头文件,使用类

2. 完整示例

第一步:头文件 Student.h

#pragma once  // 防止头文件重复包含
#include <iostream>
#include <string>
using namespace std;

// 类的声明
class Student {
private:
    string m_name;
    int m_age;
    double m_score;

public:
    void setName(string name);
    string getName();
    void setAge(int age);
    int getAge();
    void setScore(double score);
    double getScore();
    void showInfo();
};

第二步:源文件 Student.cpp

#include "Student.h"  // 包含对应的头文件

// 成员函数的实现:需要加上 类名:: 作用域限定符
void Student::setName(string name) {
    m_name = name;
}

string Student::getName() {
    return m_name;
}

void Student::setAge(int age) {
    if (age >= 0 && age <= 120) {
        m_age = age;
    } else {
        cout << "年龄输入不合法!" << endl;
        m_age = 0;
    }
}

int Student::getAge() {
    return m_age;
}

void Student::setScore(double score) {
    if (score >= 0 && score <= 100) {
        m_score = score;
    } else {
        cout << "分数输入不合法!" << endl;
        m_score = 0;
    }
}

double Student::getScore() {
    return m_score;
}

void Student::showInfo() {
    cout << "姓名:" << m_name << ",年龄:" << m_age << ",分数:" << m_score << endl;
}

第三步:主函数文件 main.cpp

#include "Student.h"

int main()
{
    Student s1;
    s1.setName("张三");
    s1.setAge(20);
    s1.setScore(95.5);
    s1.showInfo();

    return 0;
}

八、常见错误与避坑点

  1. 类定义末尾忘记写分号

    class Student {
        // 成员
    }  // 错误:缺少分号,编译器报错
    
  2. 类外部直接访问私有成员私有成员只能在类内部访问,类外部必须通过公共接口访问

  3. 成员函数实现时忘记加类名::

    // 错误:没有加Student::,编译器会认为是全局函数
    void setName(string name) {
        m_name = name;
    }
    
    // 正确
    void Student::setName(string name) {
        m_name = name;
    }
    
  4. 对象创建时加括号的误区

    // 错误:这是声明一个函数,不是创建对象
    Student s1();
    
    // 正确:创建对象
    Student s2;
    
  5. 堆区对象忘记释放用 new 创建的堆区对象,必须用 delete 释放,否则会导致内存泄漏


九、综合实战案例:设计一个立方体类

设计一个立方体类,包含长、宽、高三个属性,提供设置和获取属性的接口,以及计算体积和表面积的成员函数。

#include <iostream>
using namespace std;

class Cube {
private:
    int m_length;  // 长
    int m_width;   // 宽
    int m_height;  // 高

public:
    // 设置长
    void setLength(int length) {
        m_length = length;
    }

    // 获取长
    int getLength() {
        return m_length;
    }

    // 设置宽
    void setWidth(int width) {
        m_width = width;
    }

    // 获取宽
    int getWidth() {
        return m_width;
    }

    // 设置高
    void setHeight(int height) {
        m_height = height;
    }

    // 获取高
    int getHeight() {
        return m_height;
    }

    // 计算体积
    int calculateVolume() {
        return m_length * m_width * m_height;
    }

    // 计算表面积
    int calculateArea() {
        return 2 * (m_length * m_width + m_length * m_height + m_width * m_height);
    }
};

int main()
{
    Cube c1;
    c1.setLength(10);
    c1.setWidth(5);
    c1.setHeight(3);

    cout << "立方体的长:" << c1.getLength() << endl;
    cout << "立方体的宽:" << c1.getWidth() << endl;
    cout << "立方体的高:" << c1.getHeight() << endl;
    cout << "立方体的体积:" << c1.calculateVolume() << endl;
    cout << "立方体的表面积:" << c1.calculateArea() << endl;

    return 0;
}

十、今日学习总结

  1. 面向对象思想:以对象为单位,将属性和行为封装在一起,更符合人类思维

  2. 类与对象:类是模板,对象是类的具体实例,一个类可以创建多个对象

  3. 类的定义class 类名 { 成员列表 };,末尾必须有分号

  4. 访问权限控制

    • public:类内外都能访问
    • private:只有类内部能访问
    • 封装核心:属性私有化,提供公共接口访问
  5. struct 与 class 区别:默认访问权限不同,struct 默认 public,class 默认 private

  6. 成员函数分文件编写:声明放在.h 头文件,实现放在.cpp 源文件,加类名::作用域限定符

  7. 封装的意义:提高代码安全性、复用性、可维护性


✍️ 下节预告

下一篇将学习 C++ 类和对象的核心进阶:构造函数与析构函数,包含:构造函数的定义与分类、拷贝构造函数、析构函数的作用、深拷贝与浅拷贝问题、初始化列表,掌握对象的创建与销毁机制,彻底搞懂对象的生命周期。