面向对象的核心思想与基本概念
在程序设计中,主要有两种范式:面向过程(POP)和面向对象(OOP)。它们处理问题的方式截然不同。
面向对象 vs 面向过程
面向对象(OOP) 将现实世界中的事物抽象为“对象”,每个对象拥有自己的属性和行为,并通过与其他对象的交互来完成任务。其四大核心特性为:抽象、封装、继承、多态:
- 抽象:提取共性特征,忽略无关细节。例如,“学生”这一类都具备姓名、学号等共同属性;
- 封装:将数据和操作绑定在一起,隐藏内部实现机制,仅暴露必要的接口;
- 继承:子类可以复用父类的属性和方法,提升代码复用性。如“大学生”继承自“学生”;
- 多态:同一操作作用于不同对象时可产生不同的行为表现。比如“动物叫”这个动作,猫发出“喵喵”,狗则“汪汪”。
面向过程(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; // 私有属性:仅本类内部可访问
};
int
对象(Object) 是类的具体实例化产物,是真实存在于内存中的实体。每一个对象都有属于自己的属性值,并能调用类中定义的方法。
例如,“小明”是 Person 类的一个实例,其 Name 为“小明”,Age 为 18;“小红”则是另一个对象,Name 为“小红”,Age 为 20。
int a;
a
可以把类理解为“图纸”,而对象就是根据这张图纸制造出来的具体产品。比如“汽车图纸”对应“类”,按照图纸生产出的一辆辆汽车就是“对象”;“实例化”就是从图纸到实物的建造过程。
类本身不占用运行时内存空间,它只定义了结构和规则;只有当对象被创建后,才会在内存中分配空间存储具体的数据。
简而言之:
类是抽象的模板,对象是具体的实例,实例化是从类生成对象的过程。
封装:控制访问权限,隐藏实现细节
封装 的本质是将对象的属性和方法打包在一起,通过设定访问权限来限制外界对内部成员的直接访问,仅提供公开的接口进行交互。
就像一部手机,用户只能通过屏幕和按钮(public 接口)进行操作,而内部电路板(private 成员)被外壳保护起来,无法随意触碰。
封装的目的 在于:
- 实现信息隐蔽,防止外部误操作破坏内部状态;
- 提高代码的复用性和可维护性,降低模块间的耦合度。
public
protected
private
访问权限详解(以 C++ 为例)
| 访问权限 | 类内访问 | 子类访问 | 类外访问 | 通俗类比 |
|---|---|---|---|---|
| public | 手机的屏幕与按钮 —— 所有人可用 | |||
| protected | 维修专用接口 —— 内部人员可用 | |||
| private | 内部电路板 —— 外人无法接触 |
public
protected
private
如何设计一个结构良好的类
类的基本构成结构
class 类名 {
public:
// 提供给外部使用的接口,如获取/设置属性的方法
protected:
// 供派生类使用的受保护成员
private:
// 类内部使用的私有成员,包括属性和辅助方法
}; // 注意末尾必须加分号
接口与实现分离(解耦设计)
为了增强模块化和可维护性,应将类的声明与实现分开:
- 类的声明放在头文件中(.h 文件),作为对外接口说明;
- 方法的具体实现写在源文件中(.cpp 文件),隐藏内部逻辑。
.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 访问修饰符,通过
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;
}
};
对象实例化:从类到具体对象
使用类创建具体对象的过程称为实例化,其本质是为对象分配内存空间并初始化成员属性,从而生成一个可操作的实体。常见的实例化方式有两种:
1. 栈上实例化(推荐,内存自动管理)
通过类名直接声明对象,内存位于栈区,当程序退出作用域时自动释放,无需手动干预。
Person xm; // 在栈中创建Person对象xm(代表小明) Person xh; // 创建另一个独立对象xh(代表小红)
2. 堆上实例化(需手动管理内存)
使用
new 关键字在堆区创建对象,返回指向该对象的指针。此类内存必须通过 delete 显式释放,否则会导致内存泄漏。
Person* p = new Person(); // 堆中实例化,获取指针 delete p; // 必须手动调用 delete 释放内存
对象内存布局详解
对象在实例化后的内存分布遵循一个基本原则:成员属性各自独立,成员函数共享一份。
成员属性:每个对象独占空间
每个对象都拥有自己独立的成员变量存储区域。属性按声明顺序依次排列,同时编译器会根据平台规则进行字节对齐(通常以4或8字节对齐),以提升访问效率。
无填充示例:
#include <iostream>
using namespace std;
class Person {
char Name[20]; // 占20字节
int Age; // 占4字节
float Money; // 占4字节
};
int main() {
Person xm;
cout << sizeof(xm); // 输出28(20+4+4,无需额外填充)
return 0;
}
有填充示例(体现字节对齐机制):
class Student {
char Gender; // 1字节
int Score; // 4字节
double Grade; // 8字节
};
int main() {
Student s;
cout << sizeof(s); // 输出16(1 + 3填充 + 4 + 8,按8字节边界对齐)
return 0;
}
在此例中,
Gender 占用1字节后,编译器自动填充3字节空白,使接下来的 Score 从8字节对齐的位置开始存储,确保高效访问。
成员函数:所有对象共享代码区逻辑
成员函数如
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(),输出“我是小明”
xh.Speak(); // 调用同一份Speak(),输出“我是小红”
return 0;
}
空类的内存占用
即使一个类没有定义任何成员变量,其实例化后的对象仍会占用1字节内存。这是编译器为了保证每个对象都有唯一的内存地址而添加的占位字节,以便区分不同的对象实例。
class Empty {};
int main() {
Empty e;
cout << sizeof(e); // 输出1
return 0;
}
对象的实例化与成员访问方式
在C++中,类实例化后的对象可以通过栈或堆的方式创建。对于栈上创建的对象使用直接访问符“.”,而堆中通过指针分配的对象则需使用“->”操作符进行成员调用。
公有成员(包括属性和方法)可以被外部代码直接操作,但私有或保护成员无法从类外部直接访问,必须借助公有接口间接操作。
示例如下:
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;
}
.
->
C++ 中 struct 与 class 的差异分析
许多开发者误认为 struct 仅能用于封装数据,但在 C++ 环境下,struct 和 class 功能几乎完全一致,主要区别仅体现在两个方面:默认访问权限与默认继承方式。
| 对比项 | struct |
class |
|---|---|---|
| 默认访问权限 | |
|
| 默认继承方式 | 继承 |
继承 |
| 成员函数支持 | ?(C++ 里完全支持) | ? |
| 空类型大小 | 1 字节(C++) | 1 字节(C++) |
| 典型用途 | 简单数据聚合(比如坐标) | 复杂对象设计(比如类) |
struct
class
struct
class
实际上,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);
}
};
内联函数:优化小型函数调用性能
为减少频繁调用短小函数所带来的压栈、跳转和返回等开销,C++ 提供了内联机制。编译器会在调用点将函数体直接展开,从而避免函数调用本身的开销。例如简单的 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行;
- 虚函数(因涉及动态绑定,无法在编译期确定目标函数)。
inline
inline
内联函数 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)
不推荐使用的场景
- 递归函数:可能导致无限展开;
- 函数体较大:造成代码膨胀,反而降低性能;
- 虚函数:运行时绑定机制使其无法内联。


雷达卡


京公网安备 11010802022788号







