学习C++有半个多月了,感触比较多。以前一直是和虚拟机类语言(C#/Java)打交道的,尽管早已对C/C++的恶劣环境有所准备,但当开始学习一段时间以后还是不禁吃了一惊。
本人阅读的是《C++ Primer》,这本书是C++标准委员会许多成员共同著作而成,权威性自然不需多说。书中频繁出现陷阱,注释的小Tip。注释一般是补充解释,提出某些建议或者方案。陷阱的Tip的频繁出现令人惊奇,这大概也是C++被人称作古怪语言的原因。
废话不多说,上点干货吧:
1.让我想起了Python:
a.字符串分为宽字符串型和普通字符串型。
b.允许在语句中加入\来换行分割显示,编译时会将\忽略掉。
2.让我想起了C:
a.#define #if #ifndef #endif这组预处理。 C用它来做DEBUG开关,以及保护.h头。
b.内置类型的长度是与机器相关的。为了保证兼容性,C/C++一般提倡用.h中用typedef声明了一堆的别名来操作。(例如size_t云云的)。
c.整个世界是全局的,源文件之间通过extern来交流。
d.坚决的将变量分为const和非const两种。const相当于将内存上锁,只许访问不许写入。
e.数组变量在通常情况下解释为int指针变量。然而,更恰当的说法(私以为)是数组变量是一个const指针,它不能改变指向。
int a[2];
a=new int[2]();
这个将产生编译错误。正确的写法应该是:
[cpp] view plaincopy
int a[2];
int *p=a;
p=new int[3]();
f.指针横行。与malloc,alloc,free相对应的。C++给出了delete,new,delete []。操作指针目标对象亦可使用->的便捷方式。
3.让我想起了Java:
a.#include<>:可不就是类似Java的BootstrapClassLoader,直接从jdk的指定lib文件/目录中查找么?
#include "":就是类似Java的AppClassLoader了,从App路径开始找。
b.vector可是Java中大名鼎鼎的Collections之一。C++标准库中的vector似乎与同步性是无关的。接触了C++模板后似乎开始懂得Java泛型中rawType的意思了。
遗憾的是,C++没有C#/Java中的foreach语法。为了操纵Iterator,不得不写一大堆的域操作符。糟糕的是,域操作符不可避免,因为C++模板并不是一个类。
c.对于比int小的整型来说,在计算前会自动提升为int来进行计算。C++中也是如此。
4.让我想起了C#:
a.用using来声明域。using namespace xxx可以在类内直接访问域成员,不需要域操作符::来直接访问。Java/C#/Python访问域均是直接'.'来获得,不知道C++为何如此另类。
b.使用引用。引用便是别名,C#中用ref关键字来指定,C++则用&来指定。
c.const,const <class>*与const *x:在C++中将与指针无关的const理解为#define,将与指针相关的const理解为C#的readonly。
d.操作符重载:也许这真的是个好东西,但是陷阱很多,而且对于新手也并不友好。
e.代理:C++版的delegate便是指针。
5.让我吓一跳的东西
a.C++模板不但可以传入类型,竟然也可以传入具体的值。bitset便是一个例子。
bitset<32> bitvec;
32可不是类型。可千万别把C++模板当做基类。bitset<32>是一个类型,bitset<24>又是一个类型。
b.C++中的对象既可以在栈区创建,也可以在堆区创建。
Person p;
这行语句便是调用默认构造器在栈区创建了一个Person对象。如果想要在堆区动态创建一个Person,则需要这么写:
Person *p=new Person();
c.C++中的数组和指针被称为复合类型。因此声名可是非常讲究的,《C++Primer》中建议对于指针变量从右向左进行阅读。
int *p[4];
int (*p2)[4];
第一个声名的是一个指针数组。(它是数组)
第二个声名的是一个指向数组的指针。(它是指针)
d.C++中的类分为声明和说明。声明一般放在.h文件中,只负责声明。实现体放在.cc文件中,负责对类定义进行实现。
C#/Java中的类定义是这样子的:
class A
{
public void method(){
//do something
}
}
而C++的类定义却是这样子的:
class A{
public:
void method();
};
void A::method()
{
//do something
}
e.C++的初始化是非常烦人的东西。它区分初始化情况为内置类型初始化,以及非内置类型初始化。内置变量只有全局变量才会自动初始化,其它情况一概是随机值。为了保证内置变量的正确初始化,必须手动进行初始化赋值或者在新创建时显示调用()来初始化。
C++中的构造器也非常的古怪。C++不像C#/Java一样,初始化的流程并不允许直接的字段初始化。C++只允许在构造器中做声明初始化。
f.C++中的抛异常很非主流。对于Java/C#来说,一般都是抛出一个继承自Exception基类的异常实例。而C++抛异常却是调用函数。
throw run_time_error("abcdefg");
g.C++中有一种称为默认实参的功能。这种功能很强大,但是可能会引起重载混乱。总体来说,这是一个好功能。
int test(int a=12);
h.C++中函数体中的静态变量。
void test()
{
static size_t called=0;
called++;
}
其中的静态变量called在首次调用时会初始化。之后的调用会绕开定义,直接访问变量。本人猜测这是预编译处理的一种语法特性。
6.让我一直警惕的C++特性:
a.预编译:
C/C++相比较虚拟机高级语言来说,多了一个预编译环节。这个环节非常重要,很容易漏掉预编译这部分的思考。区分清楚预编译,能够很大程度上掌握C/C++语法上一些古怪特性的背后原因。比如sizeof,它就是一个预编译阶段的东西。
b.cstring与string:
std内置的String类型与字面量不是一回事情,所有的字面量都是CString。按道理来说,
bool isEmpty(const String& s)
{
return s.size()==0;
}
isEmpty("abc")
类似这样的代码应该是compile不过去的。因为函数要求传入的是String类型的数据,而实参却是CString。令人奇怪的却是编译没有问题,运行也正常。C++一定是在编译期间背地里做了点手脚。
c.危险的数组:
C#/Java等高级语言,一般会将数组包装成为一个Array对象,其中会包含数组长度的信息。当数组越界时也会迅速报告。而C/C++中,数组就是数组。程序员需要自己维护数组,包括维护数组长度的变量(静态数组)。需要显示的delete [] xx来释放数组资源。
d.危险的类型转换:
C#/Java中类型转换都是安全的,对于C#来说,甚至可以指定implict和explict来添加语法。
C++的内置类型的自动转型是很危险的。数值类型运算前会根据两者类型来做“提升”。糟糕的是,若两者的size一样大,而一个有符号,一个无符号或者一个是浮点数,一个是整数时,转换的预期结果将很不确定。
而对于显示转换来说,C++提供了dynamic_cast,static_cast,const_cast,reinterpret_cast。这些转换很奇怪,reinterpret_cast重新解析了底层位模式结构。也就是说,它只更改类型,但不更改类型的真实内容,仅仅在指针cast中有用,平常是相当危险的东西。const_cast可以将只读的const变量cast成可读写的,非常奇怪的语法。
最后,从一个Java开发人员的角度来说。关于bool类型的自动转换让人感觉不可靠,也很难懂。
e.危险的enum
C++enum的声明与C#没有什么两样。但是事实上,每个enum都对应一个常量类型。并不是所有enum都是int或者long的。enum中的常量数值会根据编译器分析,最终决定一个刚好适配大小的实际类型。
enum Tokens {INLINE=128,VIRTUAL=129}
其中INLINE可能是unsigned char型,VIRTUAL是int型。
f.麻烦的函数重载:
函数重载会经过候选函数->可行函数->最佳匹配这几个步骤。当加入了默认实参时,函数重载变得晦涩难懂。
总结:
有人说《C++ Primer》过于复杂,过于细节,只适合做字典来使用。真正的C++入门教材应该是《Accelerate C++》这样子轻松简单的。
的确,《C++ Primer》不适合初学程序的人阅读。只有对程序设计有了足够深入了解后,才能有所体会。
C++这门语言语法庞杂,陷阱很多。Linus不屑一顾的称C++是门不合格的语言,CSDN的头版也曾有C++是2012年不宜进入的几门技术之一。《JavaScript语言精粹》的作者建议说:每一门语言都有其鸡肋、糟粕、精华的部分。了解全部知识,能够去除鸡肋、糟粕,发扬精华部分,才能成为这门语言的掌握者。
C++继承了C语言的预处理、指针、标准库。添加了模板、类等高层次的封装。重用方面和减少资源消耗方面做了很多努力,这就使得C++变得复杂怪异起来。