一、从Java、C#到C++ (为什么C++比较难)

由于C++已经遗忘得差不多了,我翻起了最新初版的C++ Primer,打算深入了解一下C++这一门语言。C++ Primer第五版可谓是“重构”过的,融合了C++11的标准,并将它们全部在书中列举了出来。

在学习的过程中,我会把C++与Java、C#等纯面向对象的语言进行对比,中间的一些感悟,仅仅代表个人的意见,其中有对有错,也可能会存在一些争议。

差不多翻完了整本Primer C++,并了解完C++11标准后,我有了如下感慨:C++是一门灵活、强大、效率低下的语言。

所谓的“灵活”、“强大”,是指与Java、C#相比,而“效率低下”是指相对于C#、Java的开发效率,而不是程序的运行效率。同时,C++“约定俗成”了许多规则,这些规则难以全部记下来,因此在编程的时候手边最好有一本C++的手册。

我列举几个C++灵活的地方:

1、操作符重载,拷贝控制,隐式转换等

在C++中,几乎所有的操作都可以被重载,例如+、-、new、delete等,哪怕你使用=赋值,其实都是在运行一个函数。如果你没有定义这样的函数,则编译器会使用它的合成版本(也就是默认版本)。默认版本的拷贝其实就是把成员的值拷贝,如果成员是一个指针,则仅仅拷贝地址。举例说明,如果有一个A类的实例a,成员中含有一个指针ptr,当用默认版本进行拷贝后,如A b = a;,那么b和a中的ptr指向的是同一个地址,如果a、b其中之一,ptr所指向的对象被析构了,那么当析构另外一个对象的时候就会发生错误(C++
Primer 第五版,P447),所以,需要析构的对象也需要一个拷贝和赋值的操作。

上述的例子说明了,作为类的设计者,你必须要把所有的情况考虑清楚,要对它的内存分配了如指掌,否则你设计出来的类很可能会有问题。

另一个例子是隐式转换,诸如std::string str = "hello",它把C风格的const char*转换为了std::string,这种转换在我们的理解中是很直接的,但是有时候,这种转换不仅难以理解,还会造成二义性导致编译无法通过。

在我看来,操作符的重载对于一门语言不是必要的。在C++中我们可以轻易地想到两个std::string相加相当于连接两个字符串,而在Java、C#中,是禁止重载运算符的(C#中有个例外,就是它默认重载了String类的+),原因我猜想可能是防止程序结构太过于混乱。事实上,我是不太习惯于重载过多运算符,除非必须要重载(例如使用map类时,必须要重载<),因为它确实会增加阅读代码的难度。举例说明,我想在C++和C#(或Java)中分别构造一个类,它们拥有“加法”运算符,在C++中可能是这样:

class CanAdd{
public:
    int value;
    CanAdd& operator + (const CanAdd a){
        value += a.value; return *this;
    }
};

对于某些需要用到+运算的模板类,将这个类传入模板类中显然是没有问题的,但是模板类并不能保证传入的类一定有+运算符。在C#或Java中,我们更喜欢这样(Java代码):

abstract class ICanAdd{
	int value;
	abstract ICanAdd add(ICanAdd item);
}

class CanAdd extends ICanAdd{
	public ICanAdd add(ICanAdd item){
		value += item.value;
		return this;
	}
}

抽象类ICanAdd中明确包含了add方法,那么所有的ICanAdd类型的对象,都是可以调用add的,至于调用怎样的add由它们的基类决定。在C#、Java中,这种类型运用在泛型中是很安全的,因为我们可以约束这个泛型类一定要继承ICanAdd,从而保证它一定能够调用add,而模板类就不能这样保证了,编译器发现问题只有在模板实例化那一刻才知道,那么,如果有问题,面临着的可能是一大堆链接错误。

另外,过分使用重载符的意义是比较含糊的,例如std::cout << std::endl;,cout是std命名空间的一个成员,但是endl却是std中的一个函数,我个人认为如果一个程序中充斥着这样的“约定俗成”的运算符,会过于难以理解。

2、指针、值、引用及多态性

C++的困难之处在于它的内存管理。在C#和Java中,有完善的垃圾回收机制,除了基本类型(以及C#中的struct),传递的方式都是引用(C#中也可以将一个值类型变量来引用传递)。不同的符号,如*、&在不同的位置有不同的含义,而且是很容易混淆的。例如:instance*t,它到底是表示instance乘以t,还是表示一个指向instance类的指针t呢。这一点在模板中尤为明显,对于包含域运算符的类型,一定要加上typename,例如typename CLASS<T>::member,因为编译器不知道member是一个类型还是一个成员变量。

左值引用、右值引用、值传递、引用传递是对编程人员提出的大挑战,当我们用decltype、auto、const等关键字时尤为明显。例如有语句int a,则decltype(a)返回的类型是int,而decltype((a))返回的类型是int&,这些规则,只能在实战中慢慢记忆。

C++在定义变量的时候,和C#、Java不同。例如已经定义好了一个类A,在C++中,A a;表示,a已经被定义(已经被分配好了空间),如果你只想声明它,要么是extern A a;,要么就是A* a;。在C#、Java中,A a;永远是声明,除非你用了A a = new A(),表示a已经被实例化。使用未被实例化的变量会引发异常。

C++虽然是一门“面向对象”的语言,但是我们却不能直接进行“面向对象”来操作它。举例如下:

#include <iostream>
using namespace std;

class Base {
public:
    virtual void Call(){
        std::cout << "Base func called" << std::endl;
    }
};

class Derived : public Base {
public:
    void Call(){
        std::cout << "Derived func called" << std::endl;
    }
};

void call(Base b){
    b.Call();
}

int main(int argc, const char * argv[])
{
    Base base;
    Derived derived;
    call(base);
    call(derived);
    return 0;
}

程序运行的结果:

Base func called

Base fund called

根据多态的原则,call函数调用了成员函数Call,为什么对于derived对象,仍然调用的是基类的Call呢?原因是,C++的多态性只体现在引用和指针上:当你传入一个derived给call时,其实编译器是按照Base的拷贝构造函数拷贝了一个参数b,则b的类型是Base,那么再调用b.Call(),调用的肯定是Base.Call了。为了防止Base调用拷贝构造函数,我们给call传的参数,要么是Base&,要么是Base*。&#8;如果我们要使用“多态”,那么对象必须是引用或指针,因为给它们赋值不会触发拷贝构造。

3、接口与多重继承

C++中没有接口的概念,但是有继承的概念。继承是面向对象编程中的一大特点。C++支持多重继承,也支持各种访问权限的继承;C#、Java不支持多重继承,它们只能继承于一个类,但是可以继承多个接口。C++支持多重继承的原因我想其中之一是因为C++中没有“接口”的概念。接口是一个完全抽象的类,它只声明了函数体,不能实现它们。C++中可以用抽象类来实现接口,因此,C++必须要支持多重继承。例如,一只小狗,它可以跑,可以吃东西,它是一只动物,那么在C++中,可以这样声明:class Dog : public
Animal, public CanEat, public CanRun,而在Java中,则是这样声明:class Dog extends Animal implements CanEat, CanRun,且CanEat,CanRun必须为interface——它们不能实现自己的成员函数,只能声明。

多重继承会带来一些麻烦,比方说父类们都继承了同一个类,那么此派生类会继承那个类两次,此时就要用虚基类来解决问题,另外,多重继承也会出现一些重名的问题。把“接口”从类中分离出来是有好处的,它清楚地说明了类中一定会存在的方法,如果用一个纯抽象类来代替接口,则可能会出现“Is a”这样的混淆,即——Dog是Animal,并且同时是CanEat, CanRun这样的混淆,而正确的看法应该是:Dog是animal,且存在CanEat、CanRun这样的能力。

因此到现在,我都有个习惯,一个类最多继承一个包含了成员或实现多个成员函数的类,并且可以继承多个纯抽象类(不包含成员字段,且仅包含成员函数的声明的类)。当然,这种习惯可能在某些情况下是有局限性的。

4、typedef和#define

先说说define吧。宏定义是提高程序效率的一种有效手段,因为它的时间消耗在了编译器。我们可以把很多常用的函数写成宏定义,这样运行它的时候参数就不会反复出栈入栈影响效率了,C++中也可以把函数定义成inline来提高效率。define在一定程度上可以提高代码的可读性,但是漫天的define会让人疑惑。因此,在C#中define的功能被做了限制,即define不能定义一个宏为一个值,它仅仅只能定义,以及判断它是否定义了,最常见的用途就是#define DEBUG,来进行某些调试。

typedef,以及C++11中常用的using,是给类型命别名的。它主要的作用,在我看来有两个:一是提高代码的可读性,例如,一个string类型的可读性肯定比const char*高,一个strVectorWithVector的可读性肯定比vector<vector<std::string> >高,同define一样,漫天的typedef同样会降低代码的可读性。第二个作用是增加软件的可移植性。在机器A上,int占8个自己,B机器上,int占16个字节,那么在A机器上定义的int a可能在B机器上都要改成long
a,这样的工作量巨大而且容易出错。因此,在A机器上,用typedef将int命名为MyInt,在B机器上,将MyInt对应的类型改成long就实现了所有类型的改变。

不过,这样的类型移植在C#和Java上应该出现得很少的,因为它们都有各自的“虚拟机”——.NET库有CLR,Java有Java虚拟机,它们在不同的终端上会处理这样的兼容性问题的。

5、内存管理

内存管理是C++程序员的噩梦,虽然标准库引入了boost中的shared_ptr,但是它仍然没有一个垃圾回收机制,shared_ptr使用起来也有一定难度,不像“垃圾回收”那样可以无脑使用,C++的设计者可能认为程序的所有控制权应该交给程序员,但是不是所有的程序员(或他们的老板)都是那么有耐心,一个完善的内存管理机制可能需要花费相当大的时间。ObjectiveC引入了autorelease垃圾回收机制,但我认为C++在下一次标准出来之前也不会有垃圾回收机制。

在C#和Java中,一个类的回收是不定时的,但是你可以定义它们在回收时应该要做些什么,而在C++中,析构神马的,都要自己写。

以上列举了几点C++十分灵活的地方,它灵活的地方还有许多,如迭代器——C++中,如果要使用range for,则这个类必须拥有begin(),end(),它们返回一个迭代器,这个迭代器可以是自己定义的一个类,它必须实现解引用*运算符、!=运算符和++运算符。标准库中提供了各种各样复杂的迭代器,而在range for出来之前,QT中已经实现了foreach“语句”。在C#、Java中,如果要使用foreach,类必须要实现迭代器接口。

C++没有“程序集”的概念,所以也就不存在反射机制,能够使用decltype已经算是一种“突破”了。C++中,一个对象即一块内存,在C#、Java中,我们可以用GetType或.class方法来获取类型的信息。

不得不说,C++中有很多“人为规定”,大部分操作、内存管理需要自己实现,对计算机的结构需要有很清晰地了解才能写出好的程序。C++如此复杂,征服它会不会很有成就感!

一、从Java、C#到C++ (为什么C++比较难)

时间: 2024-10-04 19:43:54

一、从Java、C#到C++ (为什么C++比较难)的相关文章

java udp 发送小数数字(较难)

代码全部来自:http://825635381.iteye.com/blog/2046882,在这里非常感谢了,我运行测试了下,非常正确,谢谢啊 服务端程序: package udpServer; import java.io.*; import java.net.*; /** * 服务端 * 1.创建服务+端口 * 2.准备接受容器 byte数组 * 3.把接收容器封装成包 DatagramPacket * 4.接受数据 * 5.分析数据 将字节数组转换成-------->各种类型数据 * 6

Java用个继承咋就这么难

试想一个问题: 如果我们需要给一个超类的方法实现一种更强的功能,也就是加强版的超类,一般会怎么做? 继承? Too young too simple! 看看下面的例子: 当我们需要一个类,需要HashSet类的所有方法,但是随时需要知道在其创建到目前,已经加入过多少元素,该如何实现? 一般使用继承,覆盖add()和addAll()方法,会显得很合理: 1 public class InstrumentedHashSet<E> extends HashSet<E>{ 2 privat

转:最近5年133个Java面试问题列表

最近5年133个Java面试问题列表 Java 面试随着时间的改变而改变.在过去的日子里,当你知道 String 和 StringBuilder 的区别就能让你直接进入第二轮面试,但是现在问题变得越来越高级,面试官问的问题也更深入. 在我初入职场的时候,类似于 Vector 与 Array 的区别.HashMap 与 Hashtable 的区别是最流行的问题,只需要记住它们,就能在面试中获得更好的机会,但这种情形已经不复存在.如今,你将会被问到许多 Java 程序员都没有看过的领域,如 NIO,

关于写的Java书籍进展

大家好,去年说要写本Java书,近期就快出版了.眼下已经開始打印样书了,最快于本月中旬左右就能够在互动网www.china-pub.com上看到消息,其它各个站点何时会发售要看详细进货情况. 去年我预期是半年写好这本书,6个月左右确实将手稿写好,但因为是第一次写书,所以没有意料到很多review的成本也是非常高的.另外须要在每次review过后与出版社沟通,一直拖到如今才准备出版(并且还仅仅出了上冊),非常多小伙伴已经等得花儿都谢了.哈哈!我也有类似的感觉,去年写的有些内容如今已经快过时了,呵呵

java书系列之——前言

第1章Java的起源 对于计算机语言的发展史,业界一般认为:B语言导致了C语言的诞生,C语言演变出了C++语言,而C++语言将让位于Java语言.要想更好地了解Java语言,就必须了解它产生的原因.推动它发展的动力,以及它对其他语言的继承.像以前其他成功的计算机语言一样,Java继承了其他语言的先进原理,同时又因其独特的环境要求而提出了一些创新性的概念.在这本书的其他各章中,将从实用的角度,对Java语言.库及应用程序进行包括语法在内的详细介绍.在本章里,我们将介绍Java语言产生的背景.发展过

Java程序基本结构(一)

先来吐槽下,算是空闲时间来继续写博客之旅,记录点点滴滴.说是开始着手写Java系列的文章,可是坑货儿的公司第二天就派我出差了,一搞就一个星期,而且更坑的是出差住的地方竟然是老板在出差地方买的房子,房子装修滴垃圾,而且连热水都没有(因为常年没人在那住),洗澡都是一个问题,最让人受不了的是晚上还要义务的做事,把我自己的时间都给占用(搞到晚上十一点才睡觉),因为老板比较的抠,他和我们(还有一个和我一起出差滴)一起过来滴,是盯着我们啊.fuck,我心里一肚子的火,可是没办法啊,如今常流行说遇到这样的老板

Java技术综述

自己打算好好学习下Java,所以想先明晰Java开发中到底有哪些技术,以便以后学习的过程中,可以循序渐进,随着学习的深入,本文将不断更新. Java基础教程将Java的入门基础知识贯穿在一个实例中,逐步深入,可以帮助你快速进入Java编程的世界.万事开头难,逐步跟着这个教程走一遍,对Java应该就会有一种入门的感觉了.然后再去学习更高级的主题,或者更深入地学习其中的某些知识点. 首先学习Java的一些基础知识,包括: Java开发环境安装与配置 Eclipse的安装与使用 面向对象基础 基本数据

我和Java的故事-------第1弹

前言:兴趣是学习任何新知识最好的老师 --------------------------------------------------------------------------------------------------- 今天给自己点任务,把Java所有的基础知识总结下,Java,没你想的那么难,只要你想,啥都不是事,没有什么你办不到的,我的所有的博客均适合零基础的小白. 学习目标: 计算机的基础知识 Java语言概述 jdk的下载以及安装 HelloWorld小案例 环境变量的

java浅谈

java作为一种热门的程序语言,受到了很多程序员们的青睐.在这里我简单的介绍一下java.Java是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由Sun Microsystems公司于1995年5月推出的Java程序设计语言和Java平台.我一开始学习的时候觉得java是很难得,对Java的兴趣并不是很大,但随着java的学习,我发现Java其实并没有我们想象中的那么难,属于一种非常好上手的程序语言,一些比较简单的程序开发,可以在很短的时间内通过网上学习得到提升,同时,随着学习的深入,

java字符集编码乱码问题

博客分类: web javajspservlet 最近做网页这块时碰到了正文字符乱码问题.别看这小小的一个问题,对我来说却花费了好长一段时间.现在让我慢慢分析它吧(说实话.这些有部分是从网上找的,但都是自己亲自打出来的.这样对自己来说不仅理解了而且还加深了印象).  在java内部运算中.涉及到所有字符串都会被转化UTF-8编码来运算,然而.在被java转化之前,字符串是怎么样的字符集呢?其实java总是根据操作系统的默认编码字符集来决定字符串的初始编码的:而且java系统的输入输出都是采取操作