ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

面向对象编程入门:类与对象的本质及Java、Python、C++对比解析

面向对象编程入门:类与对象的本质及Java、Python、C++对比解析 聊到面向对象不少初学者第一个反应就是类、对象、方法这三个词背得滚瓜烂熟真到写代码时又不知道从哪下手。加上网上资料东一块西一块今天讲Java明天讲Python后天又跳到C语言一换术语长得差不多行为却不完全一样很容易把人绕晕。我这些年带过不少人写代码也自己踩过不少坑越来越觉得面向对象不是一套必须死记硬背的语法而是一种组织代码的思维方式。类是对现实事物的抽象模板对象是这个模板下的具体实例方法则是让对象产生行为的入口。搞清楚这三者的关系不管切换到哪种语言写出来的代码都会清晰很多。这篇文章就打算把类与对象这件事从头到尾捋一遍。不偏向某一种特定语言但会重点用Java、Python、C这三种常见语言做对比因为它们的面向对象语法各有特点对照着看反而更容易理解本质。无论你是刚接触编程的入门者还是已经写过一段时间、想回头补一补理论基础的同学这篇都能给你一些可以直接落地的经验。1. 类和对象到底在解决什么问题1.1 从面向过程到面向对象在面向对象出现之前主流的编程范式是面向过程。面向过程的思路是程序等于数据结构加算法代码按步骤执行函数操作全局或局部变量一步一步完成业务。比如要写一个学生成绩管理系统面向过程的写法大概是这样先定义几个数组一个存姓名一个存分数再定义几个函数一个负责录入一个负责排序一个负责输出。主函数按顺序调用它们。这种方式在小规模程序里很直观可一旦业务复杂起来问题就出现了数据散落在各个函数之间修改一处可能牵连很多地方。学生除了姓名和分数还有学号、班级、选课列表甚至还有“请假”“选课”“查成绩”这些行为。用数组加函数的组合去组织这些信息代码会越来越难维护。面向对象的思路是反过来的先把问题域里的实体找出来比如“学生”“课程”“成绩单”把每个实体的数据和它相关的行为打包在一起形成一个类。程序不再是一连串函数调用而是多个对象之间的协作。这个转变听起来简单却是思维方式上的大变化。面向过程关注的是“怎么做”面向对象关注的是“谁来做什么”。同样是完成一个功能面向对象的代码一开始可能觉得绕但扩展和维护时的优势非常明显。1.2 类就是模板对象就是实例类和对象的关系我用一个最直白的类比类就是饼干模具对象就是用模具压出来的饼干。模具本身不能吃但它定义了饼干应该长什么样——大小、形状、花纹。每块压出来的饼干都是独立的一块碎了不影响其他饼干。对应到代码里类定义了属性比如学生的姓名、学号和方法比如选课、查成绩。对象是根据类创建出来的具体个体。同一个类可以创建出无数个对象每个对象的属性值可以不同但它们拥有的属性和方法结构是一样的。这里有个新手经常混淆的点类的名字和对象的名字。类名在Java、C里习惯首字母大写比如Student对象名一般是变量名首字母小写比如student1。Python里虽然不强制但大家也遵循这个习惯。这个约定不是语法要求是为了让人一眼分清自己写的到底是“模具”还是“饼干”。1.3 用生活场景理解对象和类的边界再举一个贴近生活的例子。“汽车”是一个类“我家楼下那辆白色的SUV”是一个对象。“汽车”这个类会定义什么属性有品牌、型号、颜色、发动机排量、当前速度方法有启动、加速、刹车、转弯。但汽车类本身不会真有速度只有具体的某辆车才有当前速度。你不能说“汽车这个类现在跑到了120码”你只能说“那辆白色SUV跑到了120码”。这个边界在写代码时非常关键。类里只能写结构和逻辑不能有具体的业务数据。数据必须通过创建对象来承载。如果违背了这个原则比如在类里用静态变量存客户数据那所有对象共享同一份数据一个对象改了其他对象全受影响程序就乱套了。2. 类的设计从需求到代码2.1 识别业务里的名词和动词拿到一个需求怎么设计类我的经验是先读需求把里面的名词圈出来这些大概率是候选类再把动词圈出来这些大概率是方法形容词和数量词则是属性候选。比如需求里有这么一句“学生可以选修多门课程每门课程有学分和任课教师学期结束时学生参加考试并获得成绩。”名词主要有学生、课程、学分、任课教师、考试、成绩。这其中“学分”和“成绩”更像是值字段而不是独立类可以分别挂在课程和学生下面而“学生”“课程”“教师”适合做成三个类。动词“选修”“参加考试”“获得成绩”这些是方法。这个分析过程不一定一次到位也不需要追求一步设计出完美类图。先列出来写着写着发现不合适再调整这很正常。刚开始最怕的是不敢动手反复纠结哪个该做类哪个不该做。2.2 成员变量、成员方法和构造方法确定好类之后类内部的成员就分成了三大类成员变量也叫属性、字段、成员方法也叫方法、函数、构造方法也叫构造函数、初始化方法。成员变量描述对象的状态比如Student类里的name、studentId、selectedCourses。成员方法描述对象的行为比如selectCourse()、dropCourse()、getAverageScore()。构造方法比较特殊它没有返回类型名字和类名完全一致在创建对象时自动执行用来初始化成员变量。看一段Java代码public class Student { private String name; private String studentId; public Student(String name, String studentId) { this.name name; this.studentId studentId; } public void showInfo() { System.out.println(学生 name 学号 studentId); } }创建对象时执行构造方法Student s new Student(张三, 2025001); s.showInfo();Python的写法更简洁class Student: def __init__(self, name, student_id): self.name name self.student_id student_id def show_info(self): print(f学生{self.name}学号{self.student_id}) s Student(张三, 2025001) s.show_info()C则和Java比较像只是多了指针、引用的选项以及析构函数这种手动释放资源的机制。回头细说。2.3 封装为什么不能把所有属性都设为公有的很多零基础的同学写类时第一直觉是把所有属性都设为public方法也都public然后直接通过属性赋值。这写起来确实顺手但后面会出事。举个例子学生的年龄属性你直接公开代码里执行student.age -5程序不会报错但数据明显不合理。如果年龄是私有的赋值只能通过setAge(int age)方法你就可以在方法里做校验——年龄必须在合理范围内否则拒绝赋值。这个“控制外部访问”的思想就是封装。封装不是要把所有东西都藏起来而是把对外暴露的接口收窄只暴露必要的操作同时把内部实现细节隐藏起来。这样外部不需要知道类内部怎么工作只需要调方法就行类内部改逻辑时只要方法签名不变外部代码就不会坏。2.4 访问修饰符该怎么选Java和C里有四个访问修饰符public、protected、默认Java里叫包级私有、private。Python里没有这些关键字习惯上用下划线约定_name表示“内部使用”虽然外部还能访问但不建议。选修饰符的原则很简单成员变量一律private方法看用途决定。如果这个方法是给外部调用的接口就public如果是内部辅助用的工具方法就private如果是给子类重写的钩子方法就protected。我见过不少代码把成员变量全设为public图省事。短期看节省了几个setter/getter的代码量长期看就是把类的内部数据完全暴露给了外部所有依赖这个类的代码都能随便改它的数据一旦你需要加校验逻辑就得满项目找调用点改到怀疑人生。3. 对象的创建与方法调用3.1 new关键字背后发生了什么在Java和C里创建对象用new关键字Python则直接写类名加括号。new这个动作不是简单的“定义了一个变量”它背后做了三件事在内存中为对象分配一块空间调用构造方法把这块空间里的成员变量初始化成指定值返回这块内存的引用地址赋给变量。所以执行Student s new Student(张三, 2025001)之后s其实不是学生对象本身而是指向学生对象的一个引用。这个区分在传参和赋值时特别重要。把s赋给另一个变量t并不会复制一个新学生而是让两个变量指向同一个学生对象改t等于改s。C这里有个差异如果写成Student s(张三, 2025001)系统会在栈上直接创建对象而不是用new在堆上创建。两种方式的生存周期和资源管理方式不一样用栈对象更省心作用域结束自动析构用堆对象就得记得delete否则内存泄漏。Python则没有栈和堆这种手动区分对象的创建和垃圾回收都由解释器管理。对初学而言Python最友好但理解“引用”这个概念在哪门语言里都躲不掉。3.2 引用类型和值类型的区别Java里除了8种基本类型int、double、boolean等其余都是引用类型。C里有值类型和指针、引用之分Python则万物皆对象变量全是引用。这个区别带来的最大影响是赋值和函数传参时的行为。看Java这个例子int a 10; int b a; b 20; System.out.println(a); // 输出10 Student s1 new Student(张三, 1); Student s2 s1; s2.setName(李四); System.out.println(s1.getName()); // 输出李四基本类型赋值是拷贝值两个变量互不影响引用类型赋值是拷贝引用两个变量指向同一个对象一个改了另一个跟着变。很多初级bug就是这么来的——以为s2 s1创建了一个新对象其实只是多了一个“绰号”。C如果也想让两个对象完全独立可以定义拷贝构造函数用深拷贝实现。Java没有“运算符重载”这种东西想复制对象就得手动创建新对象或实现Cloneable接口或用拷贝工厂。Python里可以用copy模块的深拷贝方法。3.3 方法调用时参数传递的真相方法调用的参数传递是另一个高频踩坑点。Java官方说得很清楚Java只有值传递。基本类型传的是值的副本引用类型传的是引用的副本——关键就是这个“引用的副本”它还是指向同一个对象。看这段代码public void changeName(Student s) { s.setName(王五); } public void reassign(Student s) { s new Student(赵六, 3); }调用changeName后原对象的name被改成了“王五”因为方法里的s和外面的s指向同一个对象通过引用的副本调对象的方法修改动作作用在同一个对象上。但调用reassign后外面的s没变还是指向原来的对象因为方法里只是把那个“引用的副本”指向了新对象外面不受影响。这个机制理解透了就能解释很多“为什么我在方法里改了对象外面却没变”的问题。C的逻辑又不一样默认是值传递整个对象会被拷贝一份效率不高还可能切掉派生类部分所以C里常用引用传参写Student student这样既不用拷贝对象又能修改原对象。Python则把值传递和引用传递的概念简化成了“对象传参”可变对象列表、字典、自定义对象传进函数后修改会生效不可变对象数字、字符串、元组传进去怎么改都不影响原变量。3.4 构造方法的重载与对象初始化一个类可以有多个构造方法参数列表不同而已这叫构造方法重载。比如Student类可以有一个无参构造、一个只有name的构造、一个name和studentId都有的构造。这样调用者可以在不同场景下用不同方式创建对象。Java里如果只定义了带参构造没写无参构造那么new Student()就会编译报错因为无参构造不会自动生成了。这是个特别容易坑到新手的点很多框架比如Jackson、MyBatis反序列化时都需要无参构造如果你没写运行时就给你抛异常。Python里对应的是__init__方法想要多种初始化方式可以用默认参数或类方法。C除了构造函数还有初始化列表这种成员变量的初始化方式Student::Student(string name, string id) : name_(name), student_id_(id) {}初始化列表比在函数体里赋值更高效尤其成员是const或用引用类型时必须用初始化列表。这一块是C进阶的内容初学者可以先知道有这个东西。4. 方法的设计与进阶玩法4.1 方法的命名、职责和返回值方法设计得好不好直接影响类好不好用。我的标准是方法名要读起来像一个动作职责要单一返回值要明确。命名方面查数据的方法用get开头修改数据的方法用set开头判断条件返回布尔的方法习惯用is或has开头比如isOverdue()、hasPermission()。这在Java生态里已经是铁律C和Python虽然没有强约束但按这个风格写代码的可读性会高很多。职责单一的意思是一个方法只做一件事。比如getStudentInfo()就只返回学生基本信息不要顺带把成绩统计也做了。职责混在一起不仅测试难写后面修改时也会像拆弹一样小心翼翼生怕动一处影响另一处。返回值方面方法要么返回数据要么返回void/None不要两边摇摆。一个方法既能在某些路径返回对象又能在另一些路径返回null调用者就必须记住判空一不留神就空指针。如果实在没有可返回的内容我更建议用OptionalJava、NonePython配合类型注解或者用异常把异常路径讲清楚。4.2 方法重载和可变参数方法重载不限于构造方法普通方法也能重载。同一个方法名下可以有多个版本参数个数、类型或顺序不同编译器/JVM会自行匹配调用哪个版本。Python没有严格意义的方法重载同名方法写在同一个类里只有最后一个生效。C、Java则支持得很彻底。Python里如果确实需要重载效果常用默认参数值替代def greet(self, name同学, greeting你好): return f{greeting}{name}可变参数也值得掌握。Java写String... namesC可以按数组处理或用initializer_listPython用*args和**kwargs。可变参数适合那些参数个数不确定的场景但要注意签名变得太灵活调用方反而看不懂。我一般只在真正需要时才用比如日志工具、聚合函数。可变参数的核心价值在于简化调用方代码。比如你要实现一个求和的函数Java不写可变参数就得传数组调用方还得先new一个数组写了可变参数直接sum(1, 2, 3, 4)就行。4.3 静态方法 vs 实例方法静态方法属于类本身不依赖具体对象实例方法属于对象必须通过对象调用。用static修饰的Java方法、staticmethod修饰的Python方法以及C中类内static函数都是同一类东西。什么时候用静态方法当这个方法和实例数据无关时。比如Math.max(a, b)它只需要入参不需要依赖某个“Math对象”的状态。当一个方法不访问成员变量时就该考虑把它定义成静态方法因为静态方法不持有对象引用语义更清晰调用也更方便。实例方法则天然和对象绑定可以访问对象的所有成员。判断标准就是一个问题这个方法的执行结果会不会因为对象状态不同而不同如果会就是实例方法如果不会就可以考虑静态。Python里还有classmethod接收cls参数可以访问类属性、调用类构造方法。它比staticmethod强一些适合需要“以类为上下文”的工厂方法场景比如根据配置字符串创建对象。4.4 递归方法的使用场景和注意点递归是方法的一个重要用法方法自己调用自己。典型场景是遍历树形结构、目录扫描、分治算法、深度优先搜索。递归代码往往很简洁但代价是需要理解调用栈。每次递归调用都会在系统栈里压一层新栈帧如果递归深度太大Java会抛StackOverflowErrorC同样会栈溢出Python有递归深度限制默认约1000层可以手动调但危险。所以递归方法必须有两个要素明确的递归终止条件每次递归都向着终止条件推进。比如文件目录遍历终止条件是“当前目录没有子目录”推进方向是“一层层往下进”。如果漏了终止条件或者子目录里有循环引用程序就会无限递归直到栈爆炸。递归适合“自相似结构”的问题但不是所有场景都要用递归。像求斐波那契数列普通递归会重复计算大量子问题效率极差。这时候用迭代或者带记忆化的递归反而更合适。写递归时脑子里要有这么一根弦递归能简化代码但可能牺牲性能和栈空间。5. 常见误区与排查技巧5.1 对象判空别拿null当常态Java里最常见的运行时异常就是NullPointerExceptionPython里是AttributeError: NoneType object has no attribute xxx。根子是同一个对null/None对象调用了方法或访问了属性。判空很简单但如果到处都写if (obj ! null)代码会变得很难看。更合理的做法是从源头减少空值出现。构造方法里不能为空的参数就做requireNonNull校验方法的返回值如果可能为空就返回Optional或者空集合而不是null。这样调用方就不用处处判空。有些人习惯用try-catch把空指针异常包住我不推荐。异常应该表示“意外情况”空值如果是方法正常路径之一应该在返回值上明确表达用异常兜底会让代码流程变得不可预测debug时摸不着头脑。5.2 属性封装与setter里的校验逻辑刚才说过属性要私有化这里再补一个实战细节setter不是摆设它是校验逻辑最佳的安放位置。比如用户的邮箱字段如果直接对public属性赋值别人给你传个“abc”你没法拦。如果走setEmail(String email)里面可以做格式校验不合法直接抛异常或返回错误。这是封装最简单、最直接的应用。但也要注意不是每个属性都非得写getter和setter。如果一个类的内部状态不需要对外暴露就没必要写一堆空转的getter/setter。只暴露必须暴露的类才能保持“高内聚、低耦合”。5.3 对象复制时牵连旧对象有一个坑我踩过不止一次想复制对象做备份结果改备份时把原对象也改了。原因就是上文说的引用复制。解决办法按语言分几种Java手动new一个对象把原对象的字段逐个拷过去或用构造器拷贝模式C定义拷贝构造和拷贝赋值运算符注意深拷贝与浅拷贝的差别Python用copy.deepcopy(obj)做深拷贝或者自己写clone方法。判断要不要深拷贝的标准是这个对象内部还有没有引用类型的成员只要有浅拷贝大概率会出联带问题。比如一个Order对象里有List items浅拷贝后两个Order的items指向同一个List往一个里加项另一个也变。5.4 跨语言对比速查很多同学学会一门语言后学下一门时会对不上号。我整理了核心概念在不同语言里的对应关系方便查阅。概念JavaPythonC类的声明class Student {}class Student:class Student {};构造方法方法名与类同名init方法名与类同名创建对象new Student()Student()new Student() 或 Student()访问私有属性private getter/setter_name 约定private 友元或getter静态方法staticstaticmethodstatic删除/回收GC自动GC自动delete或RAII接口/抽象类interface / abstractABC纯虚函数C的析构函数值得一提它是三兄弟里唯一有析构机制的语言配合类的构造和析构可以自动管理资源也就是RAII思想。这个概念理解透了写C会非常舒服理解不透内存泄漏和悬垂指针就轮流过来找你。5.5 三个实用的Debug方法最后分享几个排错技巧是我实际写代码时反复用到的第一打印对象时不要只打印引用地址。Java里重写toString方法Python里写__repr__或__str__让对象打印出来能看到关键字段。不然调试时输出一堆Student1b6d3586等于啥也没说。第二对象状态变化定位不到时检查是不是多个引用共享同一对象。通过在setter里临时加日志打印当前对象hashCode或id就能确认是不是同一个实例被多处修改。第三构造方法报错时仔细看异常栈的Caused by部分。很多框架报错不会直接指向你的代码而是经过好几层反射调用后才暴露真实原因。从Caused by往内追往往比看外层异常快得多。写在最后的个人体会写了这些年代码带了这么多年人我越来越觉得面向对象最重要的不是背熟“封装、继承、多态”六个字而是在写代码时习惯性地想清楚这段数据属于谁这个行为应该由谁来执行谁可以改我的状态谁不应该看到我的内部。类是一个边界对象是一个演员方法是一个动作。把这三样理顺了代码自然清晰。如果这篇文章里的内容让你对类和对象有了更明确的感觉我建议你立刻动手写一个小项目练手比如一个图书管理系统或课程选课系统。不要用任何框架纯手动创建几个类、几个对象相互之间通过方法协作。写完这个小的再回过头看刚才讲的所有概念你会觉得每一个都似曾相识这才算真正开始入门了。
返回列表