解码面向对象编程与内联函数

面向对象核心概念

面向对象(OOP)vs 面向过程(POP)

  • 面向对象:把问题拆成对象(比如 “学生”“课程”“老师”),通过对象的属性(特征)和行为(方法)交互解决问题。核心思想是抽象、封装、继承、多态
    • 抽象:提取事物的共性(比如所有 “学生” 都有姓名、学号);
    • 封装:把数据和操作打包,隐藏内部细节;
    • 继承:子类复用父类的特征(比如 “大学生” 继承 “学生” 的属性);
    • 多态:同一行为有不同表现(比如 “动物叫”,猫喵喵 / 狗汪汪)。
  • 面向过程:按步骤解决问题(比如 “选课系统” 拆成 “输入学号→查课程→选课程→保存”),关注 “怎么做” 而非 “谁来做”。

优缺点对比(通俗版)

对比项 面向过程 面向对象
性能 快(无额外内存开销) 稍慢(对象实例化占内存)
维护性 差(改一步可能影响全局) 好(低耦合,改局部即可)
适用场景 简单任务(比如单片机) 复杂系统(比如电商平台)
核心关注点 步骤流程 对象交互

类与对象:模板与实例

类(Class):自定义的 “数据类型模板”

类是对一类事物的抽象描述,包含属性(数据,描述事物特征)和方法(操作,描述事物行为)。比如 “Person 类”:

  • 属性:姓名(Name)、年龄(Age)、存款(Money);
  • 方法:说话(Speak)、走路(Walk)。
class Person {
public:
    char Name[20];  // 公有属性:外部可访问(比如设置姓名)
    void Speak() {  // 公有方法:描述行为
        cout << "我是" << Name << endl;
    }
protected:
    int Age;        // 保护属性:子类可访问,外部不可
private:
    float Money;    // 私有属性:仅类内部可访问(比如存款不对外暴露)
};

对象(Object):类的具体实例

对象是类的 “实际存在”,占用内存存储具体的属性值,且能调用类定义的方法。比如 “小明” 是 Person 类的一个对象(Name 存 “小明”、Age 存 18),“小红” 是另一个对象(Name 存 “小红”、Age 存 20)。

类与对象的关系

类是 “图纸”(比如 “汽车图纸”),对象是按图纸造出的具体汽车,而实例化就是 “造车” 的过程;类比基础数据类型:

  • int 是预定义类型(“螺丝图纸”),int a;a 是变量(具体螺丝)—— 类是自定义类型(“汽车图纸”),对象是类的变量(具体汽车);
  • 类只定义 “规则”(属性该存什么、方法该做什么),不占内存;对象是规则的 “落地”,占用内存存储具体数据。

简言之:类是抽象模板,对象是模板的具体实例,实例化是从模板创建实例的过程

封装:把细节藏起来

封装的本质

封装:把对象的属性和方法 “打包”,通过访问权限控制外部访问,只留 “接口”(public 方法)交互。比如手机:内部零件(private)藏在壳里,只留屏幕、按钮(public 接口)给用户用。

目的:隐藏内部实现细节,仅通过接口(API)与外界交互。

意义:

  • 信息隐蔽:通过 publicprotectedprivate 控制访问。
  • 代码复用:低耦合设计,便于维护和扩展。

访问权限规则(C++)

访问权限 类内访问 子类访问 类外访问 通俗例子
public 手机的屏幕、按钮
protected 手机留给维修员的接口
private 手机的内部电路板

如何设计一个 “好用” 的类

类的基本结构

class 类名 {
public:
    // 公有接口(对外暴露的方法,比如“设置年龄”“获取姓名”)
protected:
    // 子类可用的属性/方法
private:
    // 内部细节(仅类自己用,比如校验数据的逻辑)
};  // 注意:分号不能漏!

接口与实现分离(解耦)

把 “类的声明” 放头文件(.h),“方法的具体实现” 放源文件(.cpp),外部只关心接口,不关心内部怎么做。

示例:银行账户类

// BankAccount.h(接口声明:告诉外界能做什么)
class BankAccount {
private:
    double balance;  // 私有:账户余额(内部细节)
public:
    /**
     * 存款函数
     * @brief 向账户存入指定金额
     * @param amount 存入金额(需大于0)
     * @return void 无返回值
     */
    void Deposit(double amount);

    /**
     * 查询余额函数
     * @brief 获取当前账户余额
     * @return double 账户余额(非负)
     */
    double GetBalance() const;
};

// BankAccount.cpp(实现细节:告诉内部怎么做)
void BankAccount::Deposit(double amount) {
    if (amount > 0) {  // 内部校验:只存正数
        balance += amount;
    }
}

double BankAccount::GetBalance() const {
    return balance;
}

成员属性私有化(必做!)

把属性设为private,通过publicsetter/getter方法访问,好处:

  • 控制读写权限(比如 “余额” 只读,不能直接改);
  • 校验数据有效性(比如年龄不能是负数)。
class Person {
private:
    int age;  // 私有属性:年龄
public:
    /**
     * 设置年龄函数
     * @brief 给对象设置年龄,自动校验合法性
     * @param a 待设置的年龄(需≥0)
     * @return void 无返回值
     */
    void SetAge(int a) {
        if (a >= 0) {  // 校验:年龄不能为负
            age = a;
        } else {
            age = 0;  // 非法值设为默认
        }
    }

    /**
     * 获取年龄函数
     * @brief 获取对象的年龄
     * @return int 已校验的合法年龄
     */
    int GetAge() {
        return age;
    }
};

实例化对象:从模板到实体

用类创建具体对象的过程称为实例化,本质是为对象分配内存空间、初始化属性,最终得到可操作的 “实体”。实例化主要有两种方式:

栈上实例化(常用,自动管理内存)

直接通过类名定义对象,内存分配在栈区,程序结束或作用域退出时自动释放。

Person xm;  // 栈上实例化Person类的对象xm(小明)
Person xh;  // 再实例化一个对象xh(小红),与xm独立

堆上实例化(手动管理内存)

new关键字实例化,内存分配在堆区,需通过delete手动释放,否则会造成内存泄漏。

Person* p = new Person();  // 堆上实例化,返回对象指针
delete p;  // 必须手动释放堆内存

对象的内存分配细节

实例化后,对象的内存分配遵循 “属性占空间,方法共分享” 的原则:

成员属性:每个对象独立存储,遵循字节对齐

成员属性按定义顺序分配内存,为了提升访问效率,编译器会进行字节对齐(通常按当前平台最大基本类型的大小对齐,比如 32 位 / 64 位系统多按 4 或 8 字节对齐)。

  • 无填充的情况

    #include <iostream>
    using namespace std;
    
    class Person {
        char Name[20];  // 20字节(char[20])
        int Age;        // 4字节(int)
        float Money;    // 4字节(float)
    };
    
    int main() {
        Person xm;
        cout << sizeof(xm);  // 输出28(20+4+4,刚好对齐,无填充)
        return 0;
    }
    
  • 有填充的情况(字节对齐示例):

    class Student {
        char Gender;    // 1字节(char)
        int Score;      // 4字节(int)
        double Grade;   // 8字节(double)
    };
    
    int main() {
        Student s;
        cout << sizeof(s);  // 输出16(1+3填充+4+8,按8字节对齐)
        return 0;
    }
    

    这里Gender占 1 字节后,编译器会填充 3 字节,让Score从 4 字节边界开始存储,保证访问效率。

成员函数:所有对象共享,存储在代码区

成员函数(如Speak()Walk())不占用对象的内存空间,而是统一存储在代码区,所有对象调用时共享同一份逻辑 —— 因为方法的执行逻辑对每个对象是相同的,只是操作的属性数据不同。

class Person {
public:
    char Name[20];
    void Speak() {  // 方法存储在代码区,所有对象共享
        cout << "我是" << Name << endl;
    }
};

int main() {
    Person xm, xh;
    strcpy(xm.Name, "小明");
    strcpy(xh.Name, "小红");
    xm.Speak();  // 调用同一份Speak()逻辑,操作xm的Name
    xh.Speak();  // 调用同一份Speak()逻辑,操作xh的Name
    return 0;
}

空类的内存大小

即使类中没有任何成员,实例化后的对象也会占用1 字节—— 这是编译器为了区分不同对象的内存地址,分配的 “占位符” 内存。

class Empty {};

int main() {
    Empty e;
    cout << sizeof(e);  // 输出1
    return 0;
}

实例化后的对象操作

实例化后可通过 “.”(栈对象)或 “->”(堆对象指针)访问公有成员(属性 / 方法),私有 / 保护成员无法直接访问:

class Person {
public:
    char Name[20];
    void SetAge(int a) {  // 公有方法,用于设置私有属性Age
        if (a >= 0) Age = a;
    }
private:
    int Age;  // 私有属性,外部无法直接访问
};

int main() {
    // 栈对象操作
    Person xm;
    strcpy(xm.Name, "小明");  // 访问公有属性
    xm.SetAge(18);            // 调用公有方法设置私有属性

    // 堆对象指针操作
    Person* xh = new Person();
    strcpy(xh->Name, "小红");  // 指针用->访问成员
    xh->SetAge(20);
    delete xh;

    return 0;
}

struct 与 class 的区别(C++)

很多人以为struct只能存数据,其实 C++ 里structclass几乎一样,核心区别只有两点:

对比项 struct class
默认访问权限 public private
默认继承方式 public继承 private继承
成员函数支持 ✅(C++ 里完全支持)
空类型大小 1 字节(C++) 1 字节(C++)
典型用途 简单数据聚合(比如坐标) 复杂对象设计(比如类)

示例:struct 也能有方法

struct Point {
    int x;
    int y;

    /**
     * 计算两点距离的辅助函数
     * @brief 计算当前点到目标点的曼哈顿距离
     * @param p 目标点对象
     * @return int 曼哈顿距离(|x1-x2|+|y1-y2|)
     */
    int ManhattanDistance(Point p) {
        return abs(x - p.x) + abs(y - p.y);
    }
};

内联函数:解决小函数的调用开销

什么是内联函数

编译器在调用处直接替换函数体的函数,避免 “压栈、跳转、返回” 的开销(比如一行的 getter 函数,调用开销比执行逻辑还大)。

定义方式

  • 类内隐式内联:类里直接写方法体,自动成为内联;
  • 类外显式内联:加inline关键字,且定义要和声明在一起(避免链接错误)。
// 类内隐式内联
class Calculator {
public:
    /**
     * 加法函数
     * @brief 计算两个整数的和
     * @param a 第一个加数
     * @param b 第二个加数
     * @return int 两数之和
     */
    int Add(int a, int b) {
        return a + b;  // 隐式内联:调用处直接替换成a+b
    }
};

// 类外显式内联(必须在头文件里定义,否则其他文件调用会链接错误)
inline int Max(int a, int b) {
    return (a > b) ? a : b;  // 显式内联:编译器建议替换
}

编译器的处理规则

inline建议不是强制!编译器会拒绝内联:

  • 函数体复杂(含循环、switch、递归);
  • 函数体超过 5 行左右;
  • 虚函数(需动态绑定,无法提前替换)。

内联函数 vs 宏定义(为什么不用宏?)

宏是文本替换(无类型检查),内联是编译器替换(类型安全):

特性 内联函数 宏定义
类型检查 ✅(比如参数是 int 就不能传字符串) ❌(比如 SQUARE ("abc") 也能过)
副作用 ✅(参数只算一次) ❌(参数可能算多次)

宏的副作用示例

#define SQUARE(x) ((x)*(x))  // 宏:文本替换int a = 3;
int b = SQUARE(a++);  // 展开成((a++)*(a++)) → a变成5,b=3*4=12(错误!)

inline int Square(int x) {  // 内联:参数先算一次
    return x*x;
}
int a = 3;
int b = Square(a++);  // x=3,a变成4,b=9(正确!)

适用场景:

  • 短小函数(1-5 行):比如 getter/setter、简单运算;
  • 高频调用:比如循环里的小判断(IsZero(value))。

不适用场景

  • 递归函数(无限展开);
  • 大函数(代码膨胀,反而变慢);
  • 虚函数(动态绑定无法内联)。
posted @ 2025-12-02 18:35  YouEmbedded  阅读(10)  评论(0)    收藏  举报