news 2026/10/12 10:49:58

C、Java、Python三种语言对比:类与对象的本质与用法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C、Java、Python三种语言对比:类与对象的本质与用法

面向对象编程的“类”和“对象”,说难也不难,但要说彻底弄明白,光靠一门语言真的容易绕进去。我在带新人做项目时,经常发现很多人Java里写类、Python里写类都还行,可一旦追问一句“C语言里没有类,它是怎么面向对象的”,当场就卡壳。反过来,只懂C语言的老手,第一次看Python的self又总觉得不对劲。所以这篇文章,我想用同一个例子,把C、Java、Python三种语言里的“类与对象”放在一起拆开揉碎讲。文章不纠结理论定义,重点放在“同一件事,三种语言各自怎么做、为什么这么做”,适合刚学完语法、开始接触面向对象编程,或者正在从一门语言跨到另一门语言的同学参考。看完之后,你会发现面向对象不是某门语言的专属语法,而是一种你能带得走的思维方式。

1. 为什么说“类与对象”是理解OOP的第一道门槛

1.1 类与对象到底在说人话什么意思

先忘掉教科书上的术语。现实世界里,一辆车、一只狗、一张订单,这些都是“对象”。对象有两样东西:它是什么样(属性),它能干什么(行为)。比如一只狗,属性是名字、品种、颜色,行为是叫、跑、摇尾巴。而“类”是什么呢?类是一张“设计图纸”,描述了所有狗都具备的属性和行为,但图纸本身不是狗。你照着图纸能造出无数条具体的狗,这条叫旺财,那条叫来福。每条狗都是图纸的一个实例,这个实例就是对象。

在编程里,类就是你的自定义数据类型,对象就是该类型的一个变量。过去我们用基本数据类型表示单个数、一个字符串,但要表示“一条狗”这种复杂的东西,就要用类把它内部的属性和方法打包。这个打包动作,就是“封装”。封装让狗的呼叫、饭量、年龄都跟着狗走,而不是散落在各个全局变量里,维护性和逻辑清晰度会高出好几个级别。

1.2 三种语言对“类与对象”的立场完全不同

Java是“强制面向对象”,几乎所有代码都写在类里,没有类你就没法写程序。类是一切代码的容器,哪怕是程序入口的main,也必须放到某个类里。这种设计的好处是规矩统一,坏处是刚学的人觉得两条路都走不了,只能走类这一条。

Python则是“既支持面向对象又不强迫你”。你既可以定义类、创建对象,也可以只用模块和函数就写完整个小脚本。Python里的类更像是一种可选的组织工具,写完的类还能动态加属性,灵活得很,代价是需要更强的自觉性。

C语言呢?它压根没有“类”这个语法。但C语言能实现面向对象吗?能,而且早期不少框架就这么干。经典做法是用struct结构体存属性,用函数指针存行为,再手动模拟“构造、继承、多态”。只要你愿意,C语言也能写出很像样的面向对象代码,只是整个过程几乎没有语法糖,每一步都得自己搭积木。这种对比特别有意思:你先在Java和Python里体会“有类”的方便,再回头用C写一遍“自制类”,原先那些抽象概念会一下子变得实在起来。

2. 三种语言实现一个类的核心差异

2.1 C语言:用结构体和函数指针模拟类

C语言没有class关键字,所以我们要“模拟”。定义一个“狗类”,第一步是定义结构体,把狗的所有属性放进去:

// dog.h typedef struct Dog { char name[32]; int age; void (*bark)(struct Dog* self); // 函数指针,指向“叫”的行为 } Dog;

属性有了,接下来是方法。在C语言里,方法就是一个普通函数,只是第一个参数必须接收“对象自己”。这一步很关键,我建议你务必记住这个思路,以后再看Java/Python里的this、self会发现完全是一回事。函数内访问对象属性时,用传来的指针去取,就是这样:

void bark_impl(Dog* self) { printf("%s: 汪汪!\n", self->name); }

最后要“构造”。C语言没有构造函数,只能提供一个初始化函数,把属性填充好,并把函数指针绑定到具体实现上:

void Dog_init(Dog* d, const char* name, int age) { strcpy(d->name, name); d->age = age; d->bark = bark_impl; }

然后创建对象并调用:

Dog myDog; Dog_init(&myDog, "旺财", 3); myDog.bark(&myDog);

看到没?每个狗对象都得把自己传进去,操作自己。这种“手动传自己”的模式,是理解Python里self为何总要出现、Java里this为何指向当前的钥匙。C语言模拟出来会很繁琐,但每一步都没有神秘感,你会清楚看到“方法”不过是“绑定到对象身上的函数”而已。

2.2 Java: 强制面向对象的语法糖

Java里的类,不需要你手动传self,编译器隐式传了。定义一个狗类非常直观:

public class Dog { private String name; private int age; public Dog(String name, int age) { this.name = name; this.age = age; } public void bark() { System.out.println(name + ": 汪汪!"); } }

看起来比C简洁很多。三个点值得注意:private访问修饰符把属性藏起来,外部想改名必须通过方法,这就是封装在语法层面的强制力;构造函数方法名和类名一样,且没有返回值,创建对象靠new调用;this在方法里隐式存在,当参数名和属性名冲突时,不得不靠它区分。当你在Java里写this.name = name,其实就是C语言里self->name = name的另一种写法。

创建和使用如下:

Dog dog = new Dog("旺财", 3); dog.bark();

new背后做了三件事:在堆上分配内存、调用构造函数初始化、返回引用。Java的类还被要求必须定义在文件顶层(或者嵌套内部类),一个文件里也只能有一个public类。这种规定强迫你从类的视角思考问题,写多了就会习惯性的把任何功能都先考虑放在哪个类里。

2.3 Python: 动态灵活的类,却最容易用错

Python的类保持了一贯的动态风格,属性不用声明,直接往构造方法里塞:

class Dog: def __init__(self, name, age): self.name = name self.age = age def bark(self): print(f"{self.name}: 汪汪!")

Python里self是显式的,和C语言的手动传递一致,但每次调用不用自己传,解释器会替你传。很多从Java转Python的人受不了这个self,觉得重复;从C转过来的人反而觉得顺眼。Python的构造方法叫__init__,它并不是严格意义上的构造函数,而是“初始化方法”,真正的对象创建过程是__new__负责的,但通常我们只需要管__init__。

最需要注意的是:Python对象的属性是在运行时动态挂上去的。你甚至可以给一个实例单独增加属性,而同类别的其他实例不受影响。这是个福利也是个坑。福利是可以快速扩展功能;坑是稍不留神创建对象时拼错了属性名,程序不会报错,只会给被对象加上一个意料之外的属性,找bug时异常痛苦。所以我个人建议,在Python类里,__init__中的所有属性都要认真初始化,哪怕初始值是None,先把“这台狗该有哪些零件”定下来,以免后续调试时越找越乱。

3. 同一个“动物世界”例子,写出三种语言的味

3.1 场景设定与基类搭建

为了更直观地对比,我们动手写一个完整的组合例子。场景是这样的:有一个基类“动物”,所有动物都能发出叫声“say”,还有两个子类“狗”和“猫”,它们都会say但内容不同,另外各自有独特的“fetch”和“scratch”行为。在三个语言里,我们要实现:定义基类、定义子类、创建对象并调用相同接口。

这个例子故意把继承和多态都揉进去,因为它们是面向对象对比学习里最见功力的部分。先看Java版,代码规矩,每个关键字都干一件事:

class Animal { protected String name; public Animal(String name) { this.name = name; } public void say() { System.out.println(name + "发出声音"); } } class Dog extends Animal { public Dog(String name) { super(name); } @Override public void say() { System.out.println(name + ": 汪汪"); } public void fetch() { System.out.println(name + "捡球"); } } class Cat extends Animal { public Cat(String name) { super(name); } @Override public void say() { System.out.println(name + ": 喵喵"); } public void scratch() { System.out.println(name + "挠了沙发"); } }

这里有三处需要仔细体会。第一,protected让子类可以访问name,但外部仍不能直接访问,这是一个比private宽松一点的访问级别。第二,子类构造第一行必须调用super(name),因为基类没有无参构造,你没得选。第三,@Override是告诉编译器“我是重写基类方法的”,写错了拼写能提前报错,这在C语言完全不可能。

3.2 Python版本:看起来和Java很像,但深水区不同

Python版本代码,初看几乎和Java一一对应:

class Animal: def __init__(self, name): self.name = name def say(self): print(f"{self.name}发出声音") class Dog(Animal): def __init__(self, name): super().__init__(name) def say(self): print(f"{self.name}: 汪汪") def fetch(self): print(f"{self.name}捡球") class Cat(Animal): def __init__(self, name): super().__init__(name) def say(self): print(f"{self.name}: 喵喵") def scratch(self): print(f"{self.name}挠了沙发")

super().__init__(name)对应Java的super(name),Python里super()不需要传参数给父类构造,__init__可以省略子类版本不写,那样就会直接继承父类的__init__。很多人问为什么子类还是要写一遍__init__,其实如果子类不需要额外属性,完全可以不写。写了也白写的情况不在少数。真正区别较大的还在后头——Python里“接口”是靠动态分派实现的,不需要任何关键字,只要一个类有say方法,就能传给需要“能叫”的函数,这就是“鸭子类型”。在Java里你则需要一个接口或者把类型声明为Animal。

3.3 C语言版本:手写“继承”和“多态”的方法

到了C语言,继承和多态要完全手动。一种经典方案是把基类结构体放在子类结构体的第一个字段,这样你就能把子类指针强制转换成基类指针访问公有字段。以“动物”为基类:

typedef struct Animal { char name[32]; void (*say)(struct Animal*); } Animal; typedef struct Dog { Animal animal; // 基类对象作为第一个字段,类似继承 void (*fetch)(struct Dog*); } Dog;

这样创建的Dog对象,内存布局的前半部分就是Animal结构体。把(Dog*)强转为(Animal*),就可以调用say。子类需要重写方法时,构造函数里把animal.say指向自己的函数实现:

void dog_say(Animal* animal) { Dog* dog = (Dog*)animal; // 通过基类指针回溯子类 printf("%s: 汪汪\n", dog->animal.name); } void dog_init(Dog* d, const char* name) { animal_init(&d->animal, name); d->animal.say = dog_say; d->fetch = dog_fetch; }

这样当你在外部持有Animal*并调用a->say时,实际执行的是狗叫声还是猫叫声,取决于构造时绑定到了哪个函数。这就是“函数指针驱动的多态”。C语言里这属于高级技巧了,复杂的框架会进一步用虚表(vtable)把同样签名的函数指针打包,原理其实和Java/Python中的虚拟方法表是同一套东西。

当你亲自用C语言走一次这个流程,再回去看Java的new Dog()、Python的Dog(),就会明白它们内部都隐藏着类似的内存布局、指针查找、函数绑定的过程。面向对象语法不是魔法,只是编译器帮你把这些繁琐的C代码给隐藏了。

4. 三种语言的对象模型和内存管理对比

4.1 对象存储位置:栈上、堆上、还是就呆在字典里

C语言里,前面代码中Dog myDog是分配在栈上的,生命周期由作用域管理;也可以Dog* myDog = malloc(sizeof(Dog))放在堆上,用完后free。这是C程序员必须关注的细节:对象可能在哪,谁负责释放,一旦悬空指针就是灾难。Java里,除了基本类型(int、char等)外,所有对象都分配在堆上,由JVM自动垃圾回收(更准确说,对象在堆上,引用可能在栈上)。你只管new,不用管释放,JVM会在对象不再可达时回收内存。Python几乎同样如此,所有实例对象都在堆上,由引用计数和垃圾回收器定期清除。

表面看,Java/Python省了一大笔心,但代价是内存分配与释放时机更加不可控,大量对象的创建高频时性能不如C手动管理稳定。C语言如果对小型、生命周期明确的对象放在栈上,能极大减少动态内存申请,在嵌入式系统里这几乎是必选策略。面向对象并不等于“开堆就万事大吉”,这点在C语言学习时最能练出来。

4.2 访问控制和可见性:谁都能摸还是只能走门

C语言结构体默认全部公开。你声明一个Dog,外部代码直接myDog.age = 99;也没人拦你。所谓“私有”全靠约定,比如字段名叫_age暗示别碰,但编译器才不管。想真正强制私有,得用不透明指针(在头文件只放结构体名字,实现放在.c文件里),访问一个对象全部得通过暴露出的函数。这种手段复杂,所以很多小项目直接放弃封装。

Java让我们见识了严格的访问控制:private、protected、public、default四个级别,编译期就能拦截非法访问。这种做法让类边界清晰,但也会导致一个常见问题:频繁地写getter和setter,四行代码里三行是模板代码,类变得很臃肿,久了反而不爱加约束。

Python则走了第三条路,类成员没有强制私有,“私有的默认约定是用变量名双下划线前缀,例如__name,这会让解释器做名字重整,能在一定程序上防止误访问,但真要访问,用_ClassName__name也还是能捅进去。”之所以有这个特点,和Python“大家自觉,我们相信程序员”的哲学有关。实际工作中,我建议尊重约定:单下划线开头的属性对外部当私有看待,除非你清楚改了会有什么后果,否则不要动它。

4.3 对象比较:地址相同还是内容相同

这地方坑过很多人。C语言里比较两个结构体不能直接用==,除非是基本成员,否则需要手动逐个比较字段。Java里用==比较引用,也就是看两个变量是不是指向同一个对象;要比较内容得重写equals。Python里==默认调用__eq__,而默认实现又是比较两个变量是否同一个对象,所以自定义类时经常需要自己实现__eq__。这正好是个极好的学习线索:对象之间的“相等”,到底是“同一个东西”还是“所有属性都相同”,必须由我们自己定义。面向对象语言把这种语义交给了类本身,而C语言要自己写函数。

5. 常见错误与排查技巧,这些坑我们换着语言踩过

5.1 忘记初始化:C的野指针和Java/Python的空指针

C语言最爽也最狠的,是定义结构体后不初始化就用。比如只声明了Dog myDog;没调用Dog_init,然后直接printf("%s", myDog.name),会读出内存里的随机垃圾,严重时直接段错误。Java里引用类型默认值是null,我遇到过不少次“忘记new直接调用方法”的报错,提示信息里写着NullPointerException,看到它第一步复查对象到底创建了没有。Python也不省心会有AttributeError: 'NoneType' object has no attribute 'name',常见于某函数应该返回对象但条件分支漏了返回值。

这种问题统一的排查思路是:先确认对象真的创建了,再确认创建之后属性真的初始化了。写C代码时多用= {0}初始化整个结构体,给所有指针先置空;Java里构造方法里给每个字段一个明确的默认值;Python的__init__里同样给默认值。养成习惯,能免掉一半以上的空指针问题。

5.2 重载和重写搞混,尤其从C++/Java转Python时

Java里很容易区分:重载是在同一个类里多个同名方法、参数不同;重写是子类覆盖父类的方法,方法签名要求一致。C语言里没有重载,因为函数名在链接时就是唯一符号;也正因如此,C语言里模拟多态必须完全依赖函数指针,没有别的路。Python既支持重载也支持重写——不过Python的重载和Java不太一样,不要指望同名方法可以靠参数个数区分“自动重载”,Python里的同名方法就只剩下一个,后定义覆盖先定义。如果你从Java过来,想用不同参数个数实现重载,Python做法是设置默认参数或内部判断,或者用*args, **kwargs。

Python里重写父类方法时,如果新方法没有调用super()的同名方法,父类逻辑就完全没了。我见过有人想扩展某个子类的say,却忘了写super().say(),排错半天发现父类压根没执行。Java可以用super.say(),C语言则是在子类的函数里手动调用绑定的父类函数指针,同样要主动做。

5.3 可变默认参数在Python类里是个常年陷阱

给Python类写方法时,默认参数经常只用None,原因之一就是可变默认参数会被所有实例共享。比如你写:

def __init__(self, items=[]): self.items = items

第一次创建对象往里添加东西后,第二次再创建一个不带items的新对象,你会发现它居然继承了上一次的内容。这是因为默认的[]是在函数定义时创建的同一个列表对象。正确写法是:

def __init__(self, items=None): self.items = items if items is not None else []

这个问题在Java、C的对比里并不对应——它们根本没有可变默认参数这种机制。每门语言都有自己特有的坑,在对比学习时就得一个个揪出来。

5.4 对象传参修改,传的是引用还是副本

按值传递还是按引用传递,是三门语言区别极大的地方。C语言里函数形参默认是值拷贝,想改“呼应的那个结构体”就要传递指针,不然改完外边没反应。Java里对象引用是按值传递的,这个“引用值”被复制,所以方法里能改对象内容,但不能把形参重新指向新对象让外界生效。Python和Java相似,形参持有的是对象的引用,可以修改对象内部状态,但不能把参数重新赋值成别的对象来影响调用方。

碰到“为什么我调用方法后对象没有变化”这类问题,先画一张内存图,标注哪个变量在栈上、哪个对象在堆上、哪个引用被复制了,画完基本就清楚了。这也是用C语言学指针的最大收获——有了指针概念,Java和Python的传参行为都迎刃而解。

6. 从对比学习中找到面向对象的内功心法

6.1 语法可以不同,但“消息传递”的内核不变

把三种语言的代码放一起看,会发现所有面向对象最终都指向同一件事:给对象发送消息,让对象自己处理。C语言的myDog.bark(&myDog)、Java的dog.bark()、Python的dog.bark(),都是在说“喂,dog,你来执行一下bark”。差异只在传“自己”的方式不同:C显式传,Java和Python由语法隐式传。

因此,学面向对象别死记“三大特征”,而是抓住“数据与操作绑定”这个核心。当你看到一个对象,就知道它既有属性又有操作它的行为,这就是封装;当你看到一个类型继承另一个类型,就知道它是在复用父类的能力的同时自己部分定义行为,这就是多态的前提。这层理解出来以后,你再看C语言的函数指针、Java的接口、Python的鸭子类型,都会觉得都是同一株树上结的果子。

6.2 为什么要先学C再看Java和Python

我的个人建议是:如果你还在本科学的课本是第一门C语言,就别急着无视它。C虽然不能原生写OOP,但它能让你看到“类”底层的内存结构、函数绑定的实现、指针和结构体如何构成对象。当你之后学Java,看到this和private,就不会只是背语法,而是能联想到“其实这就是那个隐藏的结构体指针和约定俗成的访问规则”。当你学Python,看到self和__init__,会自然明白这就是C语言手动Dog_init(&dog, ...)的升华版。

万一你先学Python也无所谓,反过来补C也能大有收获。我曾带过一个Python很熟的新人,他会写class和继承,但从没想过类对象怎么存内存。后来学C语言的struct和函数指针,几天时间,以前看不懂的那些关于对象引用的报错信息豁然开朗,他说“原来对象不过是一块内存,引用不过是内存地址的别称罢了”。

6.3 一条对比学习路线建议

我给准备系统整理OOP基础的同学一条可执行路线:先选择一个最简单的业务例子,比如银行账户;然后用三种语言各实现一遍,要求包含属性、构造、存钱取钱方法、一个账户继承类(比如学生账户、积分账户等)。每写完一种语言,画一张对象内存草图。接着把三个版本的代码并排放在编辑器里,逐行对照,写出三种语言的差异清单(包括构造语法、属性访问级别、方法调用、继承关键字、多态的写法)。最后自己设计一套像Cat、Dog、Animal的多态练习题,用纯C手写一遍,给自己造一遍“OOP轮子”。

这条路走下来,你对类与对象的理解会比刷十遍概念都深。别怕C语言的代码啰嗦,正是那种啰嗦能逼着你看到本质;也别烦Java的规矩重重,那些规矩正是为了让大项目不乱;更别嫌Python的自我自由,自由之下,恰恰考验你对“边界”的自觉。三种语言各有各的一片森林,走进去对照着找路,很快你就发现,面向对象编程说的不只是“怎么用class”,而是“如何组织代码,才能让一块问题域的世界观,清晰而稳健地映射为程序世界里的模型”。

最后分享一个我在实际教学中屡试不爽的小技巧:当你给跨语言的同学讲清某个概念后,立刻让他们各自写出三份代码来。写不出来,就是仍有盲区;写出来了,把三份代码放一块儿看,那些曾经死记硬背的规定,比如Java构造必须第一行调兄弟、Python重写要不要super、C语言函数指针签名要一致,全都会变成自然而然的选择。等到下一次再看到别人发愁一个问题在某个语言不好解决,你很容易就会冒出来三种思维的答案。这就是对比学习最大的礼物,也是我写了这么多年代码仍然觉得它特别值的原因。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 10:49:39

永磁同步电机参数辨识CCS工程源码详解:FOC高精度控制的关键

做电机驱动开发的工程师,几乎都遇到过这种情况:FOC工程在空载和仿真环境里跑得挺顺,一旦接上实际负载,或者电机温度升高,电流环路性能肉眼可见地变差,严重的还会出现低频振荡。多数人第一反应是PID参数没调…

作者头像 李华
网站建设 2026/10/12 10:45:55

抛开沉重的 ORM:用 Python 3.12+ 原生 sqlite3 与 Dataclass 打造轻快映射

秋雨过后的清晨,书桌上的水仙球茎悄悄冒出了一抹嫩白的小芽。我坐在电脑前,准备为一个新构思的秋季食谱收集工具编写本地数据存储模块。 在 Python 社区的惯性认知里,只要一谈到数据库交互,绝大多数教程和脚手架都会毫不迟疑地推荐…

作者头像 李华
网站建设 2026/10/12 10:45:23

RPA智能路由:基于内容指纹的多模型自动切换机制

1. 为什么“同一个脚本自动切换模型”不是炫技,而是RPA落地的生死线我第一次在某跨平台数据处理项目里看到“蓝耘智能路由RPA处理30条数据”这个需求时,下意识以为是又一个包装精美的PPT方案。直到客户把三份原始数据甩过来:一份是带OCR识别错…

作者头像 李华
网站建设 2026/10/12 10:44:18

不加班也能持续成长:AI工程师的高效学习与时间管理实战指南

先给自己泼一盆冷水:作为一名在职AI工程师,你大概率已经发现一个扎心的事实——技术栈更新快到你还没把上一套框架吃透,新一代大模型又已经刷屏了。GitHub上每天上百个新仓库冒出来,ArXiv上论文刷得比朋友圈还勤快。如果不持续学习…

作者头像 李华
网站建设 2026/10/12 10:37:50

Go控制语法深度解析:if、for、switch、select与defer实战指南

1. 从一段真实的代码维护经历说起接手过一个日均请求量七位数的日志聚合服务,核心逻辑是用Go写的。前任开发者离职后,代码交到我手上,第一次做需求变更就让我头皮发麻——一个看似简单的“根据日志级别和来源过滤”的功能,代码里嵌…

作者头像 李华
网站建设 2026/10/12 10:37:33

告别AI失忆:claude-mem打造Claude长期记忆层与实战

前阵子和一个做技术方案的朋友聊起AI提效,他吐槽最多的就是:每次开新会话,AI助手就像失忆了一样,之前交代过的偏好、约束、进度统统不认账。我自己也是被这个问题折磨了挺久,最后折腾到一个叫 claude-mem 的工具上。…

作者头像 李华