面向过程转变成面向对象的底层逻辑
本套课程有一定难度,讲得不好,请多多包涵!里面有很多我的个人见解(仅供参考!如有指导,请把邮件发送到该邮箱690141760@qq.com)
如果有人问面向对象四大特征是什么?我相信基本学过面向对象语言编程的人都基本回答了,*** 封装,继承,多态,抽象***
1 封装
1.1 封装的主要目的:
- 数据隐藏和保护(防止外部随意访问和修改内部数据)
- 提高代码的模块性和可维护性
- 控制访问权限(接口与实现分离)
1.2 C++ 实现封装的方式
封装主要通过 类(class) 和 访问控制符(access specifiers) 来实现:
类的访问控制符:
关键字 | 含义 |
---|---|
private |
私有成员:只能在类的内部访问 |
protected |
受保护成员:类内部和派生类可访问 |
public |
公有成员:任何地方都可以访问 |
#include <iostream>
#include <string>
using namespace std;class Person {
private:string name;int age;public:// 设置姓名和年龄void setInfo(string n, int a) {if (a > 0 && a < 150) {name = n;age = a;} else {cout << "无效年龄" << endl;}}// 获取姓名string getName() {return name;}// 获取年龄int getAge() {return age;}
};int main() {Person p;p.setInfo("小明", 25);cout << "姓名: " << p.getName() << endl;cout << "年龄: " << p.getAge() << endl;// p.age = -10; // ❌ 编译错误:age 是 privatereturn 0;
}
总结:封装实现类内部实现,类外部隐藏,增加了安全性和可控性
1.3 C 模拟C++的封装
1.3.1 思路
使用 struct
来封装数据;
使用函数操作结构体数据,不直接暴露结构体内部成员;
在 .c
文件中隐藏实现细节,只在 .h
文件中暴露接口;
可以用 static
或不暴露结构体定义来模拟“私有”。
1.3.2 示例
模拟C++封装的Person类
person.h(头文件)—— 只暴露接口:
#ifndef PERSON_H
#define PERSON_H// 声明不暴露内部细节
typedef struct Person Person;// 创建与销毁
Person* Person_create(const char* name, int age);
void Person_destroy(Person* p);// 设置与获取信息
void Person_setInfo(Person* p, const char* name, int age);
const char* Person_getName(Person* p);
int Person_getAge(Person* p);#endif
person.c(源文件)—— 封装内部实现:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "person.h"struct Person {char name[100];int age;
};Person* Person_create(const char* name, int age) {Person* p = (Person*)malloc(sizeof(Person));if (p) {strncpy(p->name, name, sizeof(p->name));p->age = (age > 0 && age < 150) ? age : 0;}return p;
}void Person_destroy(Person* p) {free(p);
}void Person_setInfo(Person* p, const char* name, int age) {if (!p) return;strncpy(p->name, name, sizeof(p->name));if (age > 0 && age < 150)p->age = age;
}const char* Person_getName(Person* p) {return p ? p->name : NULL;
}int Person_getAge(Person* p) {return p ? p->age : -1;
}
main.c(用户代码)—— 使用封装接口:
#include <stdio.h>
#include "person.h"int main() {Person* p = Person_create("小明", 25);printf("姓名: %s\n", Person_getName(p));printf("年龄: %d\n", Person_getAge(p));Person_setInfo(p, "小红", 30);printf("更新后姓名: %s\n", Person_getName(p));printf("更新后年龄: %d\n", Person_getAge(p));Person_destroy(p);return 0;
}
1.3.3 总结:
特性 | C++ | C(模拟) |
---|---|---|
类与封装 | 使用 class , private 等 |
使用 struct + 函数 ,隐藏实现 |
访问控制 | private , public |
通过头文件隐藏结构体定义 |
构造/析构函数 | 构造函数、析构函数 | 自己写 create 和 destroy 函数 |
成员函数 | 成员函数 | 用普通函数模拟,传入结构体指针作为参数 |
1.4 没有封装
1.4.1 不使用封装,代码会变得:
- 不安全(数据随意被修改)
- 不清晰(功能散乱)
- 不可维护(没有模块边界)
- 容易出 bug(调用者可以直接修改对象的内部状态)
1.4.2 举例
没有封装的写法(C语言)
#include <stdio.h>
#include <string.h>struct Person {char name[100];int age;
};int main() {struct Person p;// 外部直接访问和修改内部数据strcpy(p.name, "小明");p.age = -999; // 🚨 逻辑错误:年龄非法,但没人拦着你printf("姓名: %s\n", p.name);printf("年龄: %d\n", p.age);return 0;
}
问题出在哪里?
❌ 缺乏数据保护
- 年龄可以设置为负数、200、9999,没人检查。
- 没有“合法值”的验证逻辑。
❌ 外部耦合严重
- 所有模块都能任意改
Person
的成员,出了 bug 很难追踪是谁修改的。
❌ 没有统一接口
- 结构体的数据可能被不同模块用不同方式处理,改动一处会影响所有使用它的代码。
1.4.3 有封装 vs 无封装(对比)
对比项 | 有封装 | 无封装 |
---|---|---|
数据访问 | 只能通过接口访问(如 getAge() ) |
任何代码都能访问(如 p.age ) |
错误控制 | 可在函数中做校验 | 完全开放,容易出现非法数据 |
模块隔离 | 实现细节隐藏,接口清晰 | 实现和使用耦合严重,难以维护 |
维护性 | 只需维护接口,内部可随意重构 | 改动内部结构会影响所有使用的地方 |
2 继承
2.1 什么是继承?
派生类(子类)可以继承基类(父类)的属性和行为,并可以扩展或修改它们。
2.2 C++ 示例
基本语法
class Base {
public:void sayHello() {std::cout << "Hello from Base\n";}
};class Derived : public Base { // Derived 继承 Base
public:void sayHi() {std::cout << "Hi from Derived\n";}
};
继承方式(访问控制)
class Derived : public Base // 公有继承(最常用)
class Derived : protected Base // 受保护继承
class Derived : private Base // 私有继承
继承方式 | 基类的 public 成员在子类中 |
基类的 protected 成员在子类中 |
---|---|---|
public |
保持 public |
保持 protected |
protected |
降为 protected |
保持 protected |
private |
降为 private |
降为 private |
简单理解:子类会拥有父类的特征
整合:
#include <iostream>
using namespace std;class Animal {
public:void eat() {cout << "Animal is eating" << endl;}
};class Dog : public Animal {
public:void bark() {cout << "Dog is barking" << endl;}
};int main() {Dog d;d.eat(); // 从 Animal 继承来的d.bark(); // 自己的函数return 0;
}
2.3 C语言模拟实现C++的继承
目标 C++ 代码参考:
class Animal {
public:void speak() { cout << "Animal speaks" << endl; }
};class Dog : public Animal {
public:void speak() { cout << "Dog barks" << endl; }
};
C语言模拟实现:
animal.h
#ifndef ANIMAL_H
#define ANIMAL_Htypedef struct Animal Animal;struct Animal {void (*speak)(Animal* self); // 类似虚函数
};void Animal_init(Animal* a);
void Animal_speak(Animal* a);#endif
animal.c
#include <stdio.h>
#include "animal.h"void Animal_speak_impl(Animal* self) {printf("Animal speaks\n");
}void Animal_init(Animal* a) {a->speak = Animal_speak_impl;
}void Animal_speak(Animal* a) {a->speak(a);
}
dog.h
#ifndef DOG_H
#define DOG_H#include "animal.h"typedef struct Dog Dog;struct Dog {Animal base; // "继承" Animal
};void Dog_init(Dog* d);
void Dog_speak(Animal* a); // 注意是接收 Animal* 参数,实现多态#endif
dog.c
#include <stdio.h>
#include "dog.h"void Dog_speak(Animal* a) {// 从 Animal* 转换为 Dog*(向下转型)Dog* d = (Dog*)a;printf("Dog barks\n");
}void Dog_init(Dog* d) {Animal_init(&(d->base));d->base.speak = Dog_speak; // 重写虚函数
}
main.c
#include <stdio.h>
#include "dog.h"
#include "animal.h"int main() {Dog d;Dog_init(&d);Animal* a = (Animal*)&d; // 多态:用父类指针指向子类对象Animal_speak(a); // 输出:Dog barksreturn 0;
}
总结:C 模拟继承的关键点
C++ 特性 | C 实现方式 |
---|---|
类继承 | 子结构体中包含父结构体作为成员 |
虚函数 | 使用函数指针 |
方法重写 | 子类初始化时替换父类的函数指针 |
多态 | 父类指针指向子类对象 + 函数指针 |
向上/向下转型 | 强制转换结构体指针 |
2.4 C语言模拟实现C++的继承的构造/析构函数顺序
为啥要讲构造/析构函数顺序!!!!!
因为面向对象的父与子的继承构造函数和析构函数执行顺序是怎么样的,底层是怎么走的,很关键,如果不能好好理解,往往在获取资源和释放资源出现偏差,造成bug,甚至系统崩溃。
在 C++ 中,构造与析构的顺序遵循严格的规则,尤其在继承关系中:
C++ 中构造/析构顺序规则:
构造顺序(从父到子):
- 父类构造函数先执行;
- 然后才是子类构造函数。
析构顺序(从子到父):
- 子类析构函数先执行;
- 然后才是父类析构函数。
我们在 C 中可以模拟类似机制:
- 构造函数 = 初始化函数
- 析构函数 = 销毁函数
- 通过手动调用顺序实现构造/析构流程
示例:C 模拟 C++ 的构造与析构顺序
C++ 对应参考代码:
class Animal {
public:Animal() { cout << "Animal constructed\n"; }virtual ~Animal() { cout << "Animal destructed\n"; }
};class Dog : public Animal {
public:Dog() { cout << "Dog constructed\n"; }~Dog() { cout << "Dog destructed\n"; }
};int main() {Animal* a = new Dog();delete a;
}
C 语言模拟实现
animal.h
#ifndef ANIMAL_H
#define ANIMAL_Htypedef struct Animal Animal;struct Animal {void (*destroy)(Animal* self); // 虚析构
};void Animal_init(Animal* a);
void Animal_destroy(Animal* a);#endif
animal.c
#include <stdio.h>
#include "animal.h"void Animal_destroy_impl(Animal* self) {printf("Animal destructed\n");
}void Animal_init(Animal* a) {printf("Animal constructed\n");a->destroy = Animal_destroy_impl;
}void Animal_destroy(Animal* a) {if (a && a->destroy) {a->destroy(a);}
}
dog.h
#ifndef DOG_H
#define DOG_H#include "animal.h"typedef struct Dog Dog;struct Dog {Animal base; // 继承自 Animal
};void Dog_init(Dog* d);
void Dog_destroy(Animal* a); // 虚析构指针接收 Animal*#endif
dog.c
#include <stdio.h>
#include "dog.h"void Dog_destroy(Animal* a) {Dog* d = (Dog*)a;printf("Dog destructed\n");// 调用父类析构函数Animal_destroy(&(d->base));
}void Dog_init(Dog* d) {Animal_init(&(d->base));printf("Dog constructed\n");// 重写虚析构函数d->base.destroy = Dog_destroy;
}
main.c
#include <stdlib.h>
#include "dog.h"int main() {Dog* dog = (Dog*)malloc(sizeof(Dog));Dog_init(dog);Animal* a = (Animal*)dog;Animal_destroy(a); // 虚析构:调用 Dog 的析构,再调用 Animal 的析构free(dog);return 0;
}
模拟成功!
C++ 行为 | C 模拟方式 |
---|---|
构造函数链调用 | 父类 init() 在子类 init() 中手动调用 |
虚析构 | 使用函数指针 destroy 实现多态析构 |
析构顺序反向 | 子类 destroy() 手动调用父类析构 |
2.5 没有继承
使用继承了
class Animal {
public:void eat(){cout << "eat" << endl;}
};class Dog : public Animal {
public:void say(){cout << "speak" << endl;}
};int main() {Animal* a = new Dog();delete a;
}
如果没有继承
class Animal {
public:void eat(){cout << "eat" << endl;}
};class Dog {
public:void eat() {cout << "eat" << endl;}void speak(){cout << "speak" << endl;}
};int main()
{return 0;
}
没有继承,每次都得重新创建数据,创建方法,缺少重复利用。开发效率低!
3 多态
在学习多态前,我们要先学习C/C++的内存分布的底层逻辑,也是对上面封装和继承更高维度,更底层的学习和理解。
3.1 C/C++ 内存分布
3.1.1 代码区(Text Segment)
存放程序的机器指令(已编译的代码)。
由操作系统负责加载,通常是只读的,防止程序修改自己的代码。
可能是共享的(多进程共享同一份代码)。
3.1.2 全局/静态区(Data Segment)
存储全局变量和静态变量,根据是否初始化进一步划分:
已初始化数据区(Data Segment)
-
存放已初始化的全局变量和静态变量。
-
比如:
int a = 10; // 全局变量,已初始化 static int b = 5;
未初始化数据区(BSS Segment)
-
存放未初始化或初始化为 0 的全局和静态变量。
-
比如:
int c; // 全局变量,未初始化 static int d;
3.1.3 栈区(Stack)
- 用于存放函数的局部变量、参数、返回地址等。
- 由编译器自动分配和释放。
- 栈空间是有限的,溢出会导致“stack overflow”错误。
- 后进先出(LIFO)结构。
void func() {int x = 10; // 存在栈上
}
3.1.4 堆区(Heap)
-
程序运行时通过
new
/malloc
动态分配的内存。 -
需要手动释放(
delete
/free
)。 -
由程序员管理生命周期,不会自动回收。
-
比如:
int* p = new int(5); // 在堆上分配
3.1.5 常量区(Read-Only Data / Literal Pool)
- 存储常量字符串、常量数据。
- 通常是只读的,有时和代码区一起。
例如:
const char* str = "hello world"; // 字符串字面量在常量区
3.1.6 📊 图示(简化版)
低地址┌──────────────────────┐│ 代码区(text) │├──────────────────────┤│ 已初始化数据区(data) │├──────────────────────┤│ 未初始化数据区(bss) │├──────────────────────┤│ 堆(heap) ↑增长 ││ ││ ↓栈(stack) 收缩 │└──────────────────────┘
高地址
3.2 类内成员函数,是否在属于类对象内存内容!
#include<iostream>using namespace std;class A {
public:int i;int j;int z;
};class B {
public:int i;int j;int z;void test1() {this->i;this->j;cout << "hello world1" << endl;}void test2() {this->z;cout << "hello world2" << endl;}
};int main()
{cout << "A的大小:" << sizeof(A) << endl;cout << "B的大小:" << sizeof(B) << endl;return 0;
}
执行结果
A的大小:12
B的大小:12
可以看到类的成员函数并不属于类对象的内存内容
成员函数是代码,不是数据
类的成员函数是放在代码段(text segment)中的,它属于程序的指令区域,而不是对象实例的数据部分。无论你创建多少个类的对象,它们都共享同一份成员函数代码。
在内存分布图是怎么样的
3.3 什么是多态
反射
#include <iostream>
#include <string>
#include <map>// 模拟的类
class Person {
public:void sayHello() {std::cout << "Hello!" << std::endl;}void sayGoodbye() {std::cout << "Goodbye!" << std::endl;}
};// 注册表结构(方法名 -> 成员函数指针)
std::map<std::string, void (Person::*)()> methodTable;// 注册函数
void registerMethods() {methodTable["sayHello"] = &Person::sayHello;methodTable["sayGoodbye"] = &Person::sayGoodbye;
}int main() {Person p;registerMethods(); // 初始化注册表std::string methodName;std::cout << "Enter method name (sayHello / sayGoodbye): ";std::cin >> methodName;// 查表执行对应方法if (methodTable.find(methodName) != methodTable.end()) {// 获取函数指针void (Person::*method)() = methodTable[methodName];// 通过对象调用成员函数指针(p.*method)();} else {std::cout << "Method not found!" << std::endl;}return 0;
}