设为首页 加入收藏

TOP

一、从Java、C#到C++ (为什么C++比较难)(一)
2015-07-24 05:52:07 来源: 作者: 【 】 浏览:10
Tags:Java 为什么 比较

由于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 ::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 
  
   
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*。如果我们要使用“多态”,那么对象必须是引用或指针,因为给它们赋值不会触发拷贝构造。

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――它们不能实现
首页 上一页 1 2 下一页 尾页 1/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
分享到: 
上一篇POJ 3624 Charm Bracelet 背包题解 下一篇CodeForces 7D Palindrome Degree..

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容: