今天在论坛上看到两个学神讨论C++的优劣性引申出来的各种问题,深深感觉自己差距很大,现就部分问题做个小的总结。
C和CPP的区别:
1. C没有bool类型。布尔类型是int。0是假,非0是真。
2. C里字符常量(如'a'、'\n'、'\0'等)是int型,而C++里是char型。但这不影响使用。
3. 没有引用类型的变量,一般使用指针。函数的参数也不能是引用类型,想要副作用请用指针。
4. 没有模版。可以用宏代替。但是C99开始支持inline,用法和C++一样。 (C99 is an informal name of ISO/IEC 9899:1999, a past version of the C programming language standard)
5. 结构的类型是struct xxx,而不是xxx。如果你定义struct foo {int bar}; 那么你应该用sruct foo foo1;来创建变量foo1,而不是用C++的语法foo foo1;。如果愿意,你可以typedef一下。typedef struct foo foo_t; foo_t foo1;。
6. 没有运行时类型机制,没有typeinfo,没有dynamic_cast。你自己知道对象是什么类型
你可以把类型编码到对象里面。
一般用tagged union思路:struct Value {int tag; union {int i; double d;} val;};,自己用tag标记是什么类型。
也可以将一个公共的struct作为头;struct Animal {int kind}; struct Cat { struct Animal header; int extra_field;};
7. 没有面向对象编程。你需要自己实现多态。一般用函数指针。struct Animal {void (*speak)();}; 或者让对象指向一个虚函数表,避免每个对象里存大量的指针,但每次调用虚函数都会多一次内存访问。struct AnimalFuncs { void (*speak)()} cat_funcs, dog_funcs; struct Animal { struct AnimalFuncs funcs;};
struct Cat { struct Animal header; } cat1; ((struct Animal*)(&cat1))->funcs = cat_funcs; 所以,调用一次虚函数要访问多少次内存,你永远知道。
8. 没有namespace,你要自己在函数前面加前缀,以避免冲突。
*想一想,如果C++使用了namespace,那么编译出来的可执行文件里,符号表(函数名对应函数位置的表)中某个函数的名字应该怎么写(这可是关系到链接的时候能不能找到这个函数哦)?可能不同的namespace里有重名的函数,同一个也有重载的同名函数。就是这个简单的“符号表怎么写”的问题就够c++程序员折腾的。[二进制兼容性问题,C++的硬伤]
*二进制兼容性 Application Binary Interface Transition(ABI Transition), 简单的说就是当程序已经编译完成产生了一个二进制文件,编译过程中依赖的某些头文件或者库文件,当这些文件发生更新升级时,二进制还能否正常运行?这里指的是动态库连接的二进制文件。
9. 没有string库,但是有string.h提供了字符串基本操作,你能想象到的几乎都有。C习惯用char型数组表示字符串,字符串以'\0'结尾。像其它函数传参数的时候,一般传指针,而不是拷贝字符串本身。如果你坚持要拷贝,请用strdup,并用free()回收内存。
10. 没有iostream库,但有stdio.h。输出可以用printf,输入用scanf。文件操作用fopen,fread,fwrite,fclose,fseek,ftell,feof等,也有fprintf和fscanf。cout对应全局变量stdout,cin对应全局变量stdin,cerr对应stderr。你也可以直接用操作系统提供的open, read, write, close等调用。
11. 没有new和delete。你可以使用malloc和free。它们在stdlib.h中。同样,小心内存碎片。
12. 没有stl。但是如果你用C,大概说明你需要自己设计适合自己的应用的简单、专用而高效的数据结构。如果你用到了极其复杂的数据结构,说明你应该使用别的编程语言。
p.s. Linux内核里的环形链表和红黑树实现不错。但是试试b-tree会不会更快。
13. 你可以使用大量的第三方库和系统调用来弥补C语言标准库小的缺憾。正则表达式、网络、图形界面、数据库、国际化什么的都有。但是留意:有可能你需要换一个语言。不是所有的事都适合C做。毕竟C的定位是系统编程语言。
C++,Java and Python
1. 都是编程语言
2. 都是图灵完备的编程语言
3. 都是命令式语言
4. 都是面向对象的语言
5. 都是静态类型语言
6. 都有大量的实际项目使用这两种语言
7. 都对学术界和工业界产生了巨大的影响
类型系统
8. 都是静态类型语言。每个变量都有一个编译时就知道的类型。
* 与之相反,Python是动态类型语言,变量都是引用,变量没有类型,但值有类型。
9. 基本数据类型都接近机器,如基本的整数、浮点数。C++的不必说,Java的int和long都是简单的32位和64位整数,float和double是IEEE754浮点数,boolean使用int存储,但boolean数组按每字节一个变量压缩存储。还有一个隐藏的“地址”类型,只有JVM能看到。
* 与之相反,Python的基本类型,如int,bool,str,都是对象。任何整数都是一个对象,有属性。
10. 与第9条想对应的,基本运算符很接近机器的指令。如加减乘除、逻辑、移位、赋值等。Java甚至区分算术右移“>>>”和逻辑右移“>>”两种不同的右移运算。C++对应的分别是无符号整数和有符号整数的右移。
* 与之相反,Python的基本运算符都按方法实现。如1+2,实际上是调用1.__add__(2)方法。__add__方法可以被对象重写,以实现特殊的“加法”,如字符串相加。
11. 都有基于类的复合类型系统。都支持面向对象编程的方法,包括继承、封装、多态。且都有一定的运行时类型信息(C++的typeinfo和dynamic_cast,Java的反射)
* 与之相反,C语言没有类,在运行时也不知道一个指针指向什么类型的对象。
语句
12. 都有基本的结构化编程。顺序结构、选择结构(if, switch)、循环结构(while, do-while,for)
* 早期的一些语言依赖GOTO。
13. 都有异常处理语法try-catch
* C没有异常处理
* Java有finally而C++依赖于栈上对象的析构函数。
14. 都有函数(Java称为方法),都支持函数的递归调用。
* 早期的Fortran不支持递归调用,有的语言没有函数。
15. 都支持按值传递。
* C++支持按引用传递。你可以将局部变量的“引用”传给函数,以改变变量的值。
* Java本质上只支持按值传递。引用类型可以认为引用本身是值,但是不能将局部变量“按引用传递”以改变局部变量。
* Python本质上只支持按值传递,原理和Java一样,可以理解为“引用”本身是值。而Python所有变量都是引用,甚至包括所有的整数,如42,也是将“42”这个对象的引用传入。但是整数、字符串等都是“不可变”(immutable)的类型,a=2; a=3实际上是创建一个新的对象“3”,把引用赋给a,而“2”这个对象仍然是2。你不能通过参数让另一个函数改变当前函数的局部变量。
* Haskell的所有函数调用都是按名传递。如果参数是一个表达式,那么这个参数不到需要求值的时候是不会求值的。而C++,Java,Python都必须先求参数值,后传入函数。
面向对象编程
16. 都有泛型系统。C++基于模板,为每种类型生成代码;而Java的泛型只在编译时用于检查,编译到JVM上则使用无类型的引用,运行时看不见类型参数。
* C没有。可以用void*指向任何东西。
内存管理
17. 都支持手动控制生存周期的内存管理(new),但C++要求手动回收(C++11有例外)。
实现
18. 编译:一般都是先编译,后执行。但C++是编译为本地代码,而Java一般先编译成bytecode。然后由JVM决定是解释执行还是编译成本地代码执行。
* Python, Ruby, JavaScript, PHP等脚本语言一般不预先编译,但运行时可以采用JIT Compiling策略。Haskell既可以解释又可以编译。LISP的执行过程就是源代码(S-Exp)的扩展、变换、展开的过程。
19. 发布:一般都以二进制发布,但C++的二进制是本地代码,而Java是bytecode。
* Python一般直接发布源代码。罕见的一些闭源软件会发布bytecode。
* PHP也是。源代码直接嵌在网页代码中(或者说网页代码嵌在PHP代码中,怎么说都行)
* JavaScript也是,直接用源代码让浏览器解释。
* Haskell既可以解释,也可以编译,如何发布都行,看需要。
20. 动态装载:依赖于实现,但C++、Java一般都支持。C++使用动态链接库,Java可以动态装载类。
21. 外语接口:都可以与C语言交互。C++可以用extern "C"。Java需要JNI。
都不具备的特性
22. Tagged Union数据类型:带标记的联合类型,在函数式语言中很常见。但C++和Java可以用类和继承来模拟,也可以直接用if/else。
* Scala支持Case Class
* Haskell、ML的基本数据类型
23. 模式匹配(Pattern Matching)结构,在函数式语言中很常见。但C++可以用dynamic_cast,Java可以用instanceof检测指针/引用的目标类型。
* Scala有match-case语句和unapply方法
* Ruby有case-when语句和模式匹配运算符“=~”
* Haskell、ML都有此类语句
24. 复杂查询语句
* Python有List Comprehension。类似[len(x) for x in names if x.startswith("S")]
* Scala有For Comprehension。类似for(x <- names; if x.startswith("S")) yield x.size;
* C#有LINQ。类似from x in names where x.StartsWith("S") select s.length
* Ruby没有此类语句,但是ruby的函数调用很容易用块串接。
25. lambda函数(C++11有例外)、闭包(C++11有例外)和高阶函数:将一个函数作为另一个函数的参数,即所谓“高阶函数”(C++是支持的,但在没有闭包的情况下很吃亏)。
* Python:filter(lambda x: x%2==0, [1,2,3,4,5,6,7,8,9,10])
* JavaScript:connection.on_data(function(d) { buffer.add(d);});
* Java可以用接口和对象来模拟函数作为参数:window.addEventListener(new EventListener(){@Override void onShow() { window.setTitle("Hello"); }});
26. coroutine(C++可以依赖库和系统调用实现)
* C:使用swapcontext或者longjmp模拟
* Python:几乎所有的for循环都依赖coroutine
* Ruby:Fiber
* JavaScript:Firefox17开始有实验性支持,但没有标准化。
27. 动态对象:将一个对象的所有方法调用抽象成“方法名+参数”而不是具体的某个函数和参数。允许你做一些匪夷所思的事,比如将一个对象做“代理”,对它的所有方法调用将被记录方法名和参数,通过网络发送到远端,再将传回的数据翻译成基本数据类型作为返回值。
* Ruby里所有的对象都是这样做的。
* Python可以用__getattr__实现这种行为。Python用这种方法实现了XML-RPC。
* Java可以用java.lang.reflect.Proxy实现这个,也可以借助CGLib。
28. 扩展方法:给一个类添加一个新的方法
* C#有extension method。
* Scala可以使用隐式类型转换,转换成一个封装类型,以造成“添加一个新方法”的假象。
* Haskell可以定义新的type class,将若干个已有的但不相干的类都定为成员。
* Ruby允许你随时重新打开一个类,随便加新方法。
29. 嵌入式语言:作为另一个程序的一部分,用于程序扩展语言。显然C++和Java都不是为这个而设计的。
* Lua的解释器很小,适合嵌入另一个程序(如魔兽世界)
* Python相对大一些,但也适合做嵌入式脚本语言。