6.4 概念与约束 (Constraints and concepts)
C++是一门以C为基础发展而来的一门面向对象的高级程序设计语言,从1983年由Bjarne Stroustrup教授在贝尔实验室创立开始至今,已有30多个年头。C++从最初的C with class,经历了从C++98、C++ 03、C++ 11、C++ 14、C++17再到C++ 20多次标准化改造,功能得到了极大的丰富,已经演变为一门集面向过程、面向对象、函数式、泛型和元编程等多种编程范式的复杂编程语言。
年份 |
C++ 标准名称 |
非正式名称 |
---|---|---|
1998 |
ISO/IEC 14882:1998 |
C++98 |
2003 |
ISO/IEC 14882:2003 |
C++03 |
2011 |
ISO/IEC 14882:2011 |
C++11 |
2014 |
ISO/IEC 14882:2014 |
C++14 |
2017 |
ISO/IEC 14882:2017 |
C++17 |
2020 |
ISO/IEC 14882:2020 |
C++20 |
1. C++ 98
1954年,John Backus发明了世界上第一种计算机高级语言Fortran,为之后出现的高级编程语言奠定了基础。
1970年,AT&T的Bell实验室的 Ken Thompson,以BCPL语言为基础,设计出简单且接近硬件的B语言(取BCPL的首字母),并且他用B语言写了第一个Unix操作系统。
1972年,Bell实验室的Dennis Ritchie和Ken Thompson共同发明了C语言,并使用C重写Unix。
1979年,Bjame Stroustrup到了Bell实验室,开始从事将C改良为带类的C(C with Classes)的工作,1983年该语言被正式命名为C++,主要意图是表明C++是C的增强版。
1985年发布了第一个C++版本。第一个版本的C++,因其面向对象的思想使得编程变得简单,并且又保持了C语言的运行效率,在推出的一段时间内,得到了快速的发展,占据了编程语言界的半壁江山。
从1985年到1998年,C++从最初的C with Classes新增了很多其他的特性,比如异常处理、模板、标准模板库(STL)、运行时异常处理(RTTI)与名字空间(Namespace)等。
1998年,C++标准委员会统筹C++的所有特性,发布了第一个C++国际标准C++98。
2. C++ 03
从1998年到2003年,是C++标准从C++98到C++03的迭代期,期间C++扩增了很多额外的特性,比如以Boost MPL(Boost Metaprogramming Library)与Loki等为代表的模板元编程库的出现,让开发者更加便捷的使用C++在编译期的执行能力,即通过代码编译获得计算结果,学术性的称为模板元编程。到了2003年,C++标准委员会总结最新技术并发布了C++03标准。C++03 是给 C++98 打的补丁,所以现在的人提到 C++98, C++03 往往指的是同一个。
3. C++ 11
从2003年到2011年,也就是从C++03到C++11,期间C++引入了对象移动、右值引用、lamba表达式(函数式编程)、编译时类型识别(auto)、别名模板以及很多新型关键词(如nullptr、decltype、constexpr)等现代编程语言常具备的能力,让C++与时俱进,开发效率得到了很大的提升。这些新的特性随着C++11标准的发布而被正式确立下来。C++ 11版本也被称为现代C++,而C++ 98/03版本也被称为传统C++。
3.1 nullptr
实际开发中,避免产生“野指针”最有效的方法,就是在定义指针的同时完成初始化操作,即便该指针的指向尚未明确,也要将其初始化为空指针。所谓“野指针”,又称“悬挂指针”,指的是没有明确指向的指针。野指针往往指向的是那些不可用的内存区域,这就意味着像操作普通指针那样使用野指针(例如 &p),极可能导致程序发生异常。C++98/03 标准中,将一个指针初始化为空指针的方式有 2 种:
int *p = 0;
int *p = NULL; //推荐使用
可以看到,我们可以将指针明确指向 0(0x0000 0000)这个内存空间。一方面,明确指针的指向可以避免其成为野指针;另一方面,大多数操作系统都不允许用户对地址为 0 的内存空间执行写操作,若用户在程序中尝试修改其内容,则程序运行会直接报错。相比第一种方式,我们更习惯将指针初始化为 NULL。值得一提的是,NULL 并不是 C++ 的关键字,它是 C++ 为我们事先定义好的一个宏,并且它的值往往就是字面量 0(#define NULL 0)。 C++ 中将 NULL 定义为字面常量 0,虽然能满足大部分场景的需要,但个别情况下,它会导致程序的运行和我们的预期不符
由于 C++ 98 标准使用期间,NULL 已经得到了广泛的应用,出于兼容性的考虑,C++11 标准并没有对 NULL 的宏定义做任何修改。为了修正 C++ 存在的这一 BUG,C++ 标准委员会最终决定另其炉灶,在 C++11 标准中引入一个新关键字,即 nullptr。nullptr 是 nullptr_t 类型的右值常量,专用于初始化空类型指针。nullptr_t 是 C++11 新增加的数据类型,可称为“指针空值类型”。也就是说,nullpter 仅是该类型的一个实例对象(已经定义好,可以直接使用),如果需要我们完全定义出多个同 nullptr 完全一样的实例对象。nullptr 可以被隐式转换成任意的指针类型。举个例子:
int * a1 = nullptr;
char * a2 = nullptr;
double * a3 = nullptr;
3.2 auto
在之前的 C++ 版本中,auto 关键字用来指明变量的存储类型,它和 static 关键字是相对的。auto 表示变量是自动存储的,这也是编译器的默认规则,所以写不写都一样,一般我们也不写,这使得 auto 关键字的存在变得非常鸡肋。C++11 赋予 auto 关键字新的含义,使用它来做自动类型推导。也就是说,使用了 auto 关键字以后,编译器会在编译期间自动推导出变量的类型,这样我们就不用手动指明变量的数据类型了。auto 关键字基本的使用语法如下:
auto name = value;
int x = 0;
auto *p1 = &x; //p1 为 int *,auto 推导为 int
auto p2 = &x; //p2 为 int*,auto 推导为 int*
auto &r1 = x; //r1 为 int&,auto 推导为 int
auto r2 = r1; //r2 为 int,auto 推导为 int
name 是变量的名字,value 是变量的初始值。注意:auto 仅仅是一个占位符,在编译器期间它会被真正的类型所替代。或者说,C++ 中的变量必须是有明确类型的,只是这个类型是由编译器自己推导出来的。
auto 的限制:
- 使用 auto 的时候必须对变量进行初始化
- auto 不能在函数的参数中使用
- auto 不能作用于类的非静态成员变量
- auto 关键字不能定义数组,如比如下面的例子就是错误的:
char url[] = "http://c.biancheng.net/";
auto str[] = url; //arr 为数组,所以不能使用 auto
- auto 不能作用于模板参数
3.3 decltype
decltype 是 C++ 11新增的一个关键字,它和 auto 的功能一样,都用来在编译时期进行自动类型推导。既然已经有了 auto 关键字,为什么还需要 decltype 关键字呢?因为 auto 并不适用于所有的自动类型推导场景,在某些特殊情况下 auto 用起来非常不方便,甚至压根无法使用,所以 decltype 关键字也被引入到 C++11 中。auto 和 decltype 关键字都可以自动推导出变量的类型,但它们的用法是有区别的:
auto varname = value;
decltype(exp) varname = value;
其中,varname 表示变量名,value 表示赋给变量的值,exp 表示一个表达式。auto 根据=
右边的初始值 value 推导出变量的类型,而 decltype 根据 exp 表达式推导出变量的类型,跟=
右边的 value 没有关系。另外,auto 要求变量必须初始化,而 decltype 不要求。这很容易理解,auto 是根据变量的初始值来推导出变量类型的,如果不初始化,变量的类型也就无法推导了。decltype 可以写成下面的形式:
decltype(exp) varname;
原则上讲,exp 就是一个普通的表达式,它可以是任意复杂的形式,但是我们必须要保证 exp 的结果是有类型的,不能是 void;例如,当 exp 调用一个返回值类型为 void 的函数时,exp 的结果也是 void 类型,此时就会导致编译错误。C++ decltype 用法举例:
int a = 0;
decltype(a) b = 1; //b 被推导成了 int
decltype(10.8) x = 5.5; //x 被推导成了 double
decltype(x + 100) y; //y 被推导成了 double