您的位置:首页 > 编程语言 > C语言/C++

C++语言基础 —— 一个Java程序员的视角

2014-05-27 16:34 267 查看

前言

本文是针对Java程序员而言,且只包含了C++的语言特性讨论,尤其针对与Java语言不同之处,但并未深究其运行、实现机制,而且由于作者水平有限,不能做到精确全面。

本文目的在于让Java程序员更快的理解、学习C++语言,相当于一个C++的入门介绍,欢迎指正

基本

个人感想:C++由于要兼容C,所以实际上保留了很多面向过程的特性,由此C++的语法特性也显得非常繁杂。相比之下,Java几乎是纯面向对象(也有基本类型),而且由JVM提供跨平台特性,所以语法相对简单明了。

在使用上,Java是更安全的语言

它强制性的为程序员做了一些事情,防止因为程序员的疏漏导致程序崩溃。如:

数组的下标合法性检测(C++的迭代器也会进行范围检测)
没有指针(实际上C++程序设计时也通常建议管理指针成员,或是不使用指针)
对象的强制初始化(C++中默认不会初始化基本类型,使用类的默认构造函数对类对象做初始化)
自动的内存回收,防止内存溢出

简洁 vs 清晰

C++提供了许多可以使程序更加简洁的特性,而Java更加注重清晰。

有一个很大的区别是Java中只存在对象引用(引用即为句柄或直接指针);而C++中有对象、指向对象的指针以及引用的区别。这个不同点引出了非常多其他的不同。

C++中的同名变量声明遵循与作用域相关的名字屏蔽原则,而Java中不允许声明同名的局部变量(局部变量可以覆盖类的成员变量)。

C++力求使类像内置类型一样容易使用;而Java只是为了效率因素才保留几种有限的基本类型,其余所有都是对象,甚至main函数都包含在对象中,通过函数调用而不是操作符来编写程序。

C++中其他可以使程序更加简洁的特性包括:

操作符重载
用户自定义的类类型的转换
函数指针
map容器下标访问的自动插入行为

个人看法:过度追求简洁会让程序变得难以理解。

数据类型

数据类型种类

C++中有unsigned整数类型,以及用于存储扩展字符集的宽字符类型wchar_t(wchar_t的使用场景?)

基本类型长度

PS:word = 机器字长

基本数据类型C++Java
char0.25 word (8 bits, ASC II字符集)2 Byte (16 bits, Unicode字符集)
short0.5 word2 Byte
int1 word4 Byte
long1~2 words8 Byte
float1 word(6位有效数字)4 Byte
double2 words(10位有效数字)8 Byte
注意C++中的变量长度是平台相关的。

对象(包括所有类型)

C++中的定义(definition)与声明(declaration)

definition:为变量分配存储空间。

declaration:表明变量的类型、名字,定义也是声明。C++中可以使用extern关键字声明而不定义变量。

也可以声明而不定义类,如:
class MyClass;
,此时只知道MyClass是一个类型,而不知道其包含哪些成员。一般用来编写相互依赖的类。

只有当类定义完成之后才能定义类的对象,因此类不能具有自身类型的数据成员(无法确定存储空间)。然而只要类名一出现就可以认为该类已经声明,因此类的数据成员可以是指向自身类型的指针或引用。

变量可以声明多次,但只能定义一次。

Java中概念基本相似,但由于Java类的声明和定义必须同时给出,没有只声明类的语法,所以体现的不明显。从源文件的组织上也可以看出来,Java是每个类对应一个.java源文件,而C++的类通常由包含类声明的.h头文件以及包含类定义的.cpp源文件组成。Java中也没有extern关键字,无法只声明而不定义变量。

Java中接口的方法以及抽象方法的声明与定义是分开的。

不可变对象(包括基本类型)

C++: const

Java: final(也可用于方法,表示不可重载,参见本文的
函数重载.虚函数与动态绑定 部分)

引用

引用必须在定义时立即初始化,且之后不能更改指向的对象。

const引用:指向const对象的引用

使用const引用来避免保护性拷贝:C++中可以使用返回const引用的方式来保护类中的对象不被客户端无意或恶意修改,同时又避免保护性拷贝产生的复制对象的代价;而Java中由于没有引用的概念,只能从设计上避免直接返回整个对象,或者在返回时进行保护性拷贝。

原因可参考《Effective Java.2ed》中的第39条:保护性拷贝。

指针

指向const对象的指针(C++强制要求此类指针本身也必须具有const特性):

const double *cptr;

const指针:指针本身的值不能修改:

double *const cptr;


指向const对象的const指针,略。

对象的初始化

C++中动态创建对象的默认初始化: C++不会对基本类型做默认初始化,这一点很重要。对于类类型,会调用类的默认构造函数执行初始化。 可以通过在类型名后面使用一对内容为空的圆括号,对动态创建的对象做默认的值初始化。:

//将 a 中的元素初始化为默认值(0)
int *a = new int[100]();

注意:C++中初始化和赋值(=)是两种不同的操作,初始化类型对象时,如果采用复制初始化,则需要先生成一个临时对象存储初始化的值,然后将调用对象的复制初始化函数将临时对象复制给要初始化的对象。所以相比之下,直接初始化的效率更高。

Java中所有创建的对象一定会执行初始化(赋默认值),而且是先执行所有对象的默认初始化,然后再执行用户自定义的初始赋值操作。

定义

class Sales_item{
public:
double avg_price() const;
bool same_isbn(const Sales_item &rhs) const
{ return isbn == rhs.isbn; }
Sales_item():
units_sold(0), revenue(0.0) {}
private:
std::string isbn;
unsigned units_sold;
double revenue;
};

double Sales_item::avg_price() const{
if(units_sold)
return revenue / units_sold;
else
return 0;
}


定义类成员时,不能像Java中的成员变量一样直接定义变量的默认初始值,必须通过构造函数初始化。

C++中的class默认的访问级别位private(struct默认为public),而Java中默认是包访问级别(package-private)。

成员函数

成员函数含有隐含的形参this指针,它指向调用该函数的对象。

上例中的same_isbn函数可理解为:

bool Sales_item::same_isbn(const Sales_item *const this,
const Sales_item &rhs)
{ return this->isbn == rhs.isbn; }


用这种方式使用const的函数称为常量成员函数

Java中也有类似的this对象引用,但使用时必须显示指明this.xx。

构造函数

上例构造函数中冒号和花括号之间的代码成为构造函数初始化列表,可为类的一个或多个数据成员指定初值。

注意使用初始化列表,与直接在构造函数中赋初值的区别:在函数中赋初值时变量实际上经历了两个阶段,一是调用默认的构造函数为变量赋值,然后再执行函数体中的赋值语句。对于没有默认构造函数的类成员,以及const或引用类型的成员,都必须在构造函数初始化列表中进行初始化。故建议优先选择构造函数初始化列表。

注意2:默认生成的构造函数不会自动初始化内置类型的成员,故最好明确定义类的构造函数。

注意3:初始化执行的顺序是定义成员的次序。

Java中会强制为每一个对象赋默认初始值,即没有选择是否初始化变量的权利。所以Java中没有构造函数初始化列表的概念,如果需要设置成员变量的默认初始值,可在考虑变量定义时直接赋默认值。

static成员

static关键字只能在类定义体内部的声明,在类定义体外部定义,只在声明时标志static。

static成员变量只能定义一次,而且应该在定义时进行初始化(类定义体)。通常将static成员变量的定义放在包含类定义的源文件中。

复制控制

复制构造函数、赋值操作符以及析构函数总成为复制控制。

注意:当对象的引用或指针超出作用域时,不会运行析构函数,只有删除指向动态分配对象的指针,或实际对象超出作用域(而不是对象的引用)时,才会运行析构函数。

实现复制控制的关键是认识到它们对类的必要性。一种特别常见的情况是类具有指针成员时。

三法则:如果类需要析构函数,那么它通常也需要赋值操作符和复制构造函数。有一个例外是用作基类的类必须定义虚析构函数,以便在派生类对象销毁时应用动态绑定。

Java中没有这样的控制。实际上,由于Java的对象全部是引用的形式,在复制或赋值时会直接复制对象引用,不会有重新申请空间并复制对象内容的情况。

析构函数在Java中有类似的finalize方法,但它的工作方式与C++的析构函数大不相同,详见《Java编程思想》。

类类型的转换(conversion)

C++中可以定义到(To)类类型的转换,以及从(From)类类型的转换。即用户可自定义的类型转换。

转换操作符:在operator之后跟着转换的目标类型:

//转换必须是成员函数,不能注定返回类型,且形参表为空
operator int() const {...};


注意只能自动应用一次转换。

嵌套类(nested class)

C++中的嵌套类与其外部类基本没有关系,只是单纯的名字隐藏机制。

这与Java中的内部类(inner class)大不相同。Java中的非static内部类包含一个指向外部类对象的引用,拥有其外部类所有元素的访问权。Java中的static内部类由于没有相应的对象引用,所以在行为上与C++中类似,但机制使不同的。

C++中还有定义在函数体中的局部类(local class),局部类中可以继续定义嵌套类。。。

继承、多重继承与虚继承(未完成)

函数

默认实参

C++中的函数可以有默认实参,注意如果一个形参具有默认实参,那么它后面所有的形参也都必须具有默认实参:

string screenInit(string:size_type height = 24,
string:size_type width = 80,
char background = ' ');


内联函数

incline:对编译器来说也只是一个建议,是否展开由编译器判断。

注:内联函数的定义对编译器而言必须是可见的,以便编译器能够在调用点展开该函数,此时仅有函数的声明是不够的,所以内联函数应该在头文件中定义。这样可以确保在调用点其定义对编译器可见。

注2:编译器隐式地将在类内部定义的成员函数当作内联函数(一般成员函数是在类中声明,类外定义)。

Java中编译器可能会将final修饰的函数直接展开,但程序员无法控制,由编译器判断。实际上在Java中不应该为了获取内联特性而是用final关键字。

函数重载

重载函数的确定(overload resolution):

(1)找出所有候选函数;(2)从中选择可行函数;(3)寻找最佳匹配。

注意:在基类和派生类中使用统一名字的成员函数,即使函数原型不同,基类中的函数也将会被派生类覆盖(不是重载)。即不能通过派生类直接访问基类中被覆盖的函数,但可以通过基类指针或引用访问。如果派生类想要通过自身类型使用所有的重载版本,则要么重定义所有重载版本,或者一个都不定义(不会覆盖)。

解决方案是,可以通过using声明,将该函数的所有重载实例加到派生类的作用域。

函数调用的确定:

(1)确定进行函数调用的对象、引用或指针的静态类型;

(2)在该类中查找函数,若找不到则在其基类中继续往上查找;

(3)进行调用的合法性检查(类型检查);

(4)若函数是虚函数且通过指针或引用调用,则编译器生成代码以在运行时根据对象的动态类型确定函数版本(动态绑定),否则编译器生成直接调用函数的代码(静态绑定)。

虚函数与动态绑定

与Java的很大一个不同,C++中对函数的调用默认是在编译时确定,即默认执行静态绑定,需要在函数声明中显示使用virtual关键字来告诉编译器启用动态绑定来处理此函数;而Java中默认对所有函数调用执行动态绑定,只有final、static、private修饰的函数除外。相比之下,动态绑定当然会更耗时。

C++中引用和指针的静态类型(编译时可知的类型)与动态类型(实际指向的类型)可能是不同的,这一点是C++用以支持多态性的基础。即指针和引用是多态的,而对象不是多态的,对象类型已知且不能改变。这一点在使用C++的容器时会有影响,因为容器的类型是确定的,如果想要在容器中使用多态特性,则只能放指针或引用,而不能放对象。Java方面,由于全是对象引用,所有没有类似的问题。

virtual函数也是有函数体的,纯虚函数没有函数体。Java中的abstract方法更类似于C++中的纯虚函数。

//在函数形参后面加上=0以指明纯虚函数
//含有纯虚函数的类称为抽象基类(abstract base class)
double net_price(std::size_t) const = 0;


注意:派生类虚函数调用基类版本时,必须显示使用作用域操作符,否则函数调用会在运行确定并将是一个自身调用,导致无穷递归。

C++与Java中动态绑定的实现

C++中对于使用了虚函数机制的类,编译器都会为其保存一个虚函数表(基类和派生类都有),表中元素是指向虚函数实际版本的指针。在类的对象中也会加上一个成员变量,指向此类的虚函数表。运行时通过实际对象的虚函数表即可找到相应类实现的虚函数。

Java中的实现根据不同的JVM可能会不同,但原理是一样。常见的就是在类的方法去中建立一个虚方法表(Vitual Method Table),在表中存放各方法的实际入口地址。绑定时同样通过具体的对象类型找到对应类中的方法实现。

操作符重载

C++可以通过重载”<<”输出操作符来达到Java中类似默认调用toString()的效果(效果相似,但机制是完全不同的,Java中没有操作符重载)。即:

ostream& operator<<(ostream& os, const ClassType &object);


调用操作符”()”与函数对象

定义了调用操作符的类,其对象常称为函数对象(function object)。其行为类似函数,但因为可以在类中存储变量,所以比函数更加灵活。常用于标准库算法中。

/*
GT_cls类:判断字符串长度是否大于给定值 bound
通过在类中存储 bound 可以产生一个更加通用的判断算法
*/
class GT_cls{
public:
GT_cls(size_t val = 0): bound(val){}
bool operator()(const string& s)
{ return s.size() >= bound; }
}
private:
std::string::size_type bound;
};
//调用
count_if(words.begin(), words.end(), GT_cls(6));


容器与算法

C++中的容器只提供针对容器的基本操作,而许多常用的算法则另外实现,两者通过迭代器关联起来,算法通过操作迭代器而应用到多种容器上。为此C++提供了五种的迭代器类型,它们支持不同的操作,可以满足不同算法对参数的需求。

C++中由于没有强制规定基本类型的长度,所以在跨平台时可能会有问题。容器中定义的配套类型(companion type)的如size_type,就是为了提供跨平台的兼容性。

注意:C++容器中存储的元素都是副本,即将元素值复制到容器中。而Java中如果不做处理,当然是复制的对象引用。

注意:C++中的map容器使用红黑树实现的,另有基于哈希表的hasm_map容器。

Java中的容器分为三类:Collection、Set、Map,算法被封装在每一类容器的基类中,而不是单独定义(Java中的函数是不可能单独存在的)。

模板与泛型编程

C++与Java的泛型有很大区别。实际上,由于Java在JDK1.5之后才引入泛型,为了兼容旧的程序,所以其提供的泛型功能非常有限,不是真正意义上的泛型。

C++的泛型编程中,程序员使用模板来编写类型无关的代码。编译器以用户提供的实际特定类型(模板实参)来代替模板中的抽象类型(模板形参),并重新生成特定类型的代码,这个过程称之为模板的实例化

//类模板(在类中使用泛型)
template <typename T> class Queue{
public:
Queue ();
T &front ();
const T &front () const;
void push (const T &);
void pop ();
bool empty () const;
private:
//...
}
//函数模板(在函数中使用泛型)
template <typename T> int compare(const T &a, const T &b){
if (a < b) return -1;
if (b < a) return 1;
return 0;
}


而Java中的泛型,个人理解,只是为了提供编译时的类型检查以及类型的自动转换功能。实际上,Java为了兼容未使用泛型的代码,特定的泛型类如
List<String>
在运行时会被转型成为更一般的
List
类型,消除了使用泛型的痕迹。

详见《Java编程思想》。

另:由于笔者缺乏泛型编程经验,对其泛型的理解仅止于此,待日后补充。

异常

try{
//code
throw runtime_error("error!");
} catch (runtime_error err){
cout<<err.what()<<endl;
} catch (...){   //捕获所有异常
//...
}


相对于Java中的checked与unchecked异常来说,C++中所有异常都是unchecked的。这也意味着C++中即使在当前函数中没有捕获抛出的异常,也不需要在函数中添加类似Java中的异常声明语句。

在异常被捕获之前,将沿着函数调用链继续向上,直至查找失败,调用terminate库函数。这一过程在C++中称为栈展开(stack unwinding)。

terminate函数在exception头文件中定义,其行为依赖于系统,通常情况下将导致程序非正常退出。

C++中的异常以类似于实参传递给函数的方式抛出和捕获。异常是可以传给非引用形参的任意类型的对象(包括基本类型甚至指针),这意味着必须能够复制该类型的对象。throw表达式将初始化异常对象,传给对应的catch,并在完全处理异常后撤销。

注意:C++的异常处理中没有”finally”块,类似工作在对象的析构函数中处理。

Java中抛出异常的关键字是throws,而不是throw。

运行时类型识别(RTTI)

C++中可以使用基类的指针或引用来检索其指向的实际派生类型。

typeid操作符:返回指针或引用所致对象的实际类型。

dynamic_cast操作符:将基类类型的指针或引用安全地(执行运行时类型检查)转换为派生类型的指针或引用。

Java中通过instanceof获取类型信息,通过Class对象的cast()方法转换或直接执行类型转换。

实际上因为Java中Class对象的存在,其运行时能获得的类型信息比C++要丰富很多,这也是Java反射机制的基础。

其他语言特性

typedef(编译时处理)

作用域操作符

mutable(标示永远可修改的变量,不能为const)

friend(友元)

new, delete

union(联合)

volatile(与Java中的完全不同)

链接指示(作用类似Java中的JNI)

预处理、编译、链接

预处理

C++从C语言中继承了预处理器,预处理指令#ifndef、#define,、#include指令等。

例如在编写头文件时,为保证头文件不会被多次包含,应该定义头文件保护符(header guard),即通过定义预处理器变量来判断此文件是否已被同一程序包含过:

#ifndef HEADER_FILE_X
#define HEADER_FILE_X
//content of file x
#endif


预编译头文件

由于总有一堆头文件几乎是所有程序都必须包含的,所以对这部分文件预先编译生成预编译头文件(Precompiled Header Files,通常是一个.pch文件)。在编译用户程序时,会将预编译头中已经编译完的部分直接加载到内存。

linux下的编译操作

编译及链接:

g++ -o

编译、链接(便于分别编译,再行链接):

g++ -c “生成目标代码(*.o)文件

g++ -o

与Java的类加载机制的区别(未完成)

当然,C++与Java程序是两种完全不同的运行机制,此处主要讨论由这两种机制带来的程序设计、语言特性上的区别。

参考资料

《C++ Primer》

《Thinking In Java》

《Effective Java》

《深入理解Java虚拟机》
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐