解码面向对象编程与内联函数
面向对象核心概念
面向对象(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)与外界交互。
意义:
- 信息隐蔽:通过
public、protected、private控制访问。 - 代码复用:低耦合设计,便于维护和扩展。
访问权限规则(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,通过public的setter/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++ 里struct和class几乎一样,核心区别只有两点:
| 对比项 | 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))。
不适用场景:
- 递归函数(无限展开);
- 大函数(代码膨胀,反而变慢);
- 虚函数(动态绑定无法内联)。

浙公网安备 33010602011771号