C++学习笔记(三)
接下来是本书第二章的剩余内容
# 1. const 限定符
如果我们想定义一种值不要被随便改变的变量,就可以用 const 对变量类型加以限定,并且一旦创建后它的值就不能在改变,所以,我们必须初始化这个 const 对象。
默认状态下,const 对象仅在文件中有效。我们默认在多个文件中出现了同名的 const 变量时,其实等同于在不同的文件中分别定义了独立的变量。
如果我们想让 const 对象只在一个文件中定义它同时在其他文件中声明并使用它,只要在声明及定义时都添加 extern 关键字就可以了。
# 2. const 的引用
还记得上一篇文章提到的引用吗,(忘了回去看看),我们可以把引用绑定到 const 对象上,就和其他的对象一样,我们称之为对常量的引用(reference to const)。与普通引用的区别是,对常量的引用不能被用作修改它所绑定的对象。
常量引用是我们对于 const 引用的简称,虽然这简称挺形象,但务必记住这只是简称,毕竟常量引用严格而言是不存在的,因为引用不是一个对象,我们没法让引用本身恒定不变。事实上,由于 C++ 语言并不允许随意改变引用所绑定的对象,从这一定义上理解所有的引用又都算是常量。引用的对象是常量还是非常量可以决定其所能参与的操作,却无论如何都不会影响到引用和对象的绑定关系本身。
前面说过,引用的类型必须与其所引用对象的类型一致,但有两个例外。第一种例外情况就是在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。
我们讨论为何会发生这种例外。
double dval = 3.14; |
如上的代码,编译器为了确保让 ri 绑定一个整数,把它变成了下面这样。
double dval = 3.14; |
这种情况下,ri 绑定了一个临时量(temporary)对象。所谓临时量对象就是编译器需要一个空间来暂存表达式的求值结果时临时创建的一个未命名的对象,我们也将其称为临时量。
当然,如果 ri 不是常量,没有 const 限制,那么我们就允许通过 ri 对其所引用的对象进行赋值修改,但是我们实际绑定的只是一个临时量,没有人想要修改那个临时量,你无法通过 ri 修改原本的 dval 的值,因而这样做毫无意义,C++ 语言也自然把这种行为归为非法。
请记住,对 const 的引用可能引用一个并非 const 的对象,我们无法通过 const 引用改变所绑定的变量值,但是其他途径仍是能修改它的。
# 3. 指针和 const
与引用一样,也可以令指针指向常量或非常量,指向常量的指针(pointer to const)不能用于改变其所指对象的值。要想存放常量对象的地址,只能使用指向常量的指针。
同样,之前说过,指针的类型必须与其所指对象的类型一致,但是有两个例外。第一种例外情况就是允许令一个指向常量的指针指向一个非常量对象。
并且指向常量的指针也没有规定其所指的对象必须是一个常量。所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变。
也可以这么理解,常量引用和常量指针它们自认为指向了常量,所以不去改变所引用或所指对象的值。
我们还需要记得,指针本质上是对象,因此我们允许把指针本身定为常量。常量指针(const pointer)必须初始化,而且一旦初始化完成,它的值就不能再改变了。
const double pi = 3.14; // 常量 |
根据上面的代码,你应该区分清楚它们的区别,尤其是指向常量的指针和是常量的指针类型。百度上对常量指针和指针常量的解读非常模糊混乱,看到了如下解读方法。
从右往左读,首先是标识符,然后加上
is a
,之后遇到*
就替换为point to
,其余的关键字照抄,这样得到的一句英文的确可以较好地说明这个标识符亦或是变量的类型。
# 4. 顶层 const
就像上面所说的,由于指针本身既是对象,又可以指向对象,因此指针本身是否为常量以及指针所指向的对象是否为常量就是两个相互独立的问题。
用名词顶层 const(top-level const)表示指针本身是个常量。
用名词底层 const(low-level const)表示指针所指的对象是一个常量。
当然,顶层 const 可以表示任意对象是常量,更通俗地说就是,任意本身是常量的对象就是顶层 const。
而底层 const 只与指针和引用等复合类型的基本类型部分有关。
所以,指针类型可以既是顶层 const 又是底层 const,这是特殊于其他类型的。
当执行拷贝操作时,顶层 const 不受什么影响;而底层 const 要求拷入和拷出的对象必须具有相同的底层 const 资格,或者两个对象的数据类型能够转换。一般而言,非常量可以转换成常量,反之则不行。
# 5. constexpr 和常量表达式
常量表达式(const expression)是指值不会改变并且在编译过程就能得到计算结果的表达式。显然,字面值属于常量表达式,用常量表达式初始化的 const 对象也是常量表达式。一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定。
const int max_files = 20; |
尽管 staff_size 的初始值是个字面值常量,但由于它的数据类型只是一个普通 int 而非 const int,所以它不属于常量表达式。另一方面,尽管 sz 本身是一个常量,但它的具体值直到运行时才能获取到,所以也不是常量表达式。
为了更好地分辨一个初始值到底是不是常量表达式,C++11 规定,允许将变量声明为 constexpr 类型以便由编译器来验证变量的值是否是一个常量表达式。声明为 constexpr 的变量一定是一个常量,而且必须用常量表达式初始化。
并且,你可以用 constexpr 定义一个特殊的函数,它应该足够简单以使得编译时就可以计算其结果,这样就可以用 constexpr 函数去初始化 constexpr 变量。
常量表达式的值需要在编译时就得到计算,因此对声明 constexpr 时用到的类型必须有所限制。因为这些类型比较简单,值也显而易见、容易得到,就把它们称为字面值类型(literal type)。
目前为止,算术类型、引用和指针都属于字面值类型。
尽管指针和引用都能定义成 constexpr,但他们的初始值却受到严格限制。一个 constexpr 指针的初始值必须是 nullptr 或者 0,或者是存储于某个固定地址中的对象。
函数体内定义的变量一般来说并非存放在固定地址中,因此 constexpr 指针不能指向这样的变量;当然,函数还允许定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量一样也有固定地址,因此 constexpr 引用 / 指针可以绑定 / 指向这样的变量。
请注意,在 constexpr 声明中如果定义了一个指针,限定符 constexpr 仅对指针有效,与指针所指的对象无关。
# 6. 处理类型
由于程序复杂性的提升,程序中用到的类型也越来越复杂。
- 一些类型难于拼写,既难记又容易写错,还无法明确体现其真实目的和含义。
- 搞不清到底需要什么类型,程序员不得不通过程序上下文寻求帮助。
类型别名(type alias)因此而产生,它是一个名字,是某种类型的同义词。它让复杂的类型名字变得简单明了、易于理解和使用,还有助于程序员清楚地知道使用该类型的真实目的。可通过以下两种方法定义类型别名:
- 关键字 typedef
其中关键字 typedef 作为声明语句中的基本数据类型的一部分出现。含有 typedef 的声明语句定义的不再是变量而是类型别名。和以前的声明语句一样,这里的声明符也可以包含类型修饰,从而也能由基本数据类型构造出符合类型。 - 别名声明(alias declaration)
这种方法用关键字 using 作为别名声明的开始,其后紧跟别名和等号,其作用是把等号左侧的名字规定成等号右侧类型的别名。
类型别名和类型的名字等价,只要是类型的名字能出现的地方,就能使用类型别名。
# 7. auto 类型说明符
为了解决在声明变量时得知表达式类型的问题,C++11 引入了 auto 类型说明符,它能让编译器代替我们去分析表达式所属的类型。和之前那些只对应一种特定类型的说明符不同,auto 让编译器通过初始值来推算变量的类型。显然,auto 定义的变量必须有初始值。
可以使用 auto 在一条语句中声明多个变量,但是该语句中所有变量的初始基本数据类型都必须一样。
- 首先,使用引用实际是使用引用的对象,特别是当引用被用作初始值时,真正参与初始化的其实是引用对象的值。此时编译器以引用对象的类型作为 auto 的类型。
- 其次,auto 一般会忽略掉顶层 const,同时底层 const 则会保留下来。
- 如果希望推断出的 auto 类型是一个顶层 const,须明确指出。
- 还可以将引用的类型设为 auto,并适用于原初始化规则。
- 设置一个类型为 auto 的引用时,初始值中的顶层常量属性仍然保留。和往常一样,如果我们给初始值绑定一个引用,则此时的常量就不是顶层常量了。
- 要在一条语句中定义多个变量,切记符号 & 和 * 只从属于某个声明符,而非基本数据类型的一部分,因此初始值必须是同一种类型。
# 8. decltype 类型识别符
有时呢,我们希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。为了满足这一要求,C++11 引入了第二种类型说明符 decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。
如果 decltype 使用的表达式是一个变量,则 decltype 返回该变量的类型(包括顶层 const 和引用在内)。
需要指出的是,引用从来都作为其所指对象的同义词出现,只有用在 decltype 处是一个例外。
如下
int i =42, *p = &i, &r = i; |
因为 r 是一个引用,很明显 decltype® 结果是引用类型,如果想要结果类型是 r 所引用的类型,可以把 r 作为表达式的一部分,如 r+0,我们就可以得到一个具体值而非引用。
如果表达式是解引用(即 * )操作,则 decltype 将得到引用类型,就如上代码块最后一句代码结果类型就是 int&,而非 int。
对于变量 v,decltype ((v)) 的结果永远是引用,而 decltype (v) 结果只有当 v 本身是引用时才是引用。
# 9. 自定义数据结构
这里我们使用 struct 定义一个类。
以关键字 struct 开始,紧跟着类名和类体(类体部分可以为空),类体由花括号包围形成了一个新的作用域,类内部定义的名字必须唯一,但是可以与类外定义的名字重复。类体右侧表示结束的花括号后必须写一个分号,因为类体后可以紧跟变量名以示对该类型对象的定义,所以分号必不可少。
不建议直接在类体后定义类型对象,这样无异于把两种不同实体的定义混在了一条语句中。
类体定义类的成员,我们的类只有数据成员(data member),类的数据成员定义了类的对象的具体内容,每个对象有一份自己的数据成员拷贝,修改一个对象的数据成员,不会影响其他对象。
C++11 规定,可以为数据成员提供一个类内初始值(in-class initializer)用于初始化数据成员,没有初始值的成员将被默认初始化。初始化规则和之前类似,但不能使用圆括号。
当然,之后会介绍的 class,也会说明现在为何使用 struct。
# 10. 预处理器
为了让我们编写的类能被多个文件使用,需要编写自己的头文件,但我们写的头文件可能会导致使用该头文件的文件多次重复引入同样的头文件,因此我们要对自制头文件做适当处理,使其遇到多次包含的情况也能安全和正常地工作。
这就要提到预处理器(preprocessor)技术了,它由 C 语言继承而来,预处理器是在编译之前执行的一段程序,可以部分的改变我们所写的程序。就如之前用到的一项预处理器功能 #include,当预处理器看到 #include 标记时就会用指定的头文件的内容代替 #include。
C++ 程序还会用到的一项预处理功能是头文件保护符(header guard),头文件保护符依赖于预处理变量。预处理变量有两种状态:已定义和未定义。#define 指令把一个名字设定为预处理变量,另外两个指令则分别检查某个指定的预处理变量是否已经定义:#ifdef 当且仅当变量已定义时为真,#ifndef 当且仅当变量未定义时为真。一旦检查结果为真,则执行后续操作直到遇到 #endif 指令为止。
预处理变量无视 C++ 语言中关于作用域的规则。
整个程序中的预处理变量包括头文件保护符必须唯一,通常基于头文件中类的名字来构建保护符的名字,以确保其唯一性。为了避免与程序中其他实体发生名字冲突,一般把预处理变量的名字全部大写。
头文件即使还没有被包含在任何其他头文件中,也应该设置保护符。这很简单,只要养成习惯就好了,不必在意你的程序是否需要。
# 11. 总结
按照惯例,下面仍然是本章术语表总结。
- 地址(address)
- 别名声明(alias declaration)
- 算数类型(arithmetic type)
- 数组(array)
- auto
- 基本类型(base type)
- 绑定(bind)
- 字节(byte)
- 类成员(class member)
- 复合类型(compound type)
- const
- 常量指针(const pointer)
- 常量引用(const reference)
- 常量表达式(const expression)
- constexpr
- 转换(convertsion)
- 数据成员(data member)
- 声明(declaration)
- 声明符(declarator)
- decltype
- 默认初始化(default initialization)
- 定义(definition)
- 转义序列(escape sequence)
- 全局作用域(global scope)
- 头文件保护符(header guard)
- 标识符(identifier)
- 类内初始符(in-class initializer)
- 在作用域内(in scope)
- 被初始化(initialized)
- 内层作用域(inner scope)
- 整型(integral type)
- 列表初始化(list initialization)
- 字面值(literal)
- 局部作用域(local scope)
- 底层 const(low-level const)
- 成员(member)
- 不可打印字符(nonprintable character)
- 空指针(null pointer)
- nullptr
- 对象(object)
- 外层作用域(outer scope)
- 指针(pointer)
- 指向常量的指针(pointer to const)
- 预处理器(preprocessor)
- 预处理变量(preprocessor variable)
- 引用(reference)
- 对常量的引用(reference to const)
- 作用域(scope)
- 全局(global)
- 类(class)
- 命名空间(namespace)
- 块(block)
- 分离式编译(separate compilation)
- 带符号类型(signed)
- 字符串(string)
- struct
- 临时值(temporary)
- 顶层 const(top-level const)
- 类型别名(type alias)
- 类型检查(type checking)
- 类型说明符(type specifier)
- typedef
- 未定义(undefined)
- 未初始化(uninitialized)
- 无符号类型(unsigned)
- 变量(variable)
- void *
- void 类型
- 字(word)
- & 运算符(& operator)
- * 运算符( * operator)
- #define
- #endif
- #ifdef
- #ifndef
# 12. 读后感?
第二章的内容相对于第一章读起来已经感到有些拗口了,一些定义的概念也并不是特别清晰,尤其有些定义在百度上解释的五花八门,这种反而感觉英文上对于其定义的解释更为贴切。第二章变量和基本类型可以说是 C++ 编程的基础吧,更复杂的结构等等都是由基础演化而来,我的笔记也并不全面,书上会有更为详尽的示例,但由于时间和精力最优的选择就是只做这种纯文字且没什么排版的文章记录,我也更容易坚持下来,如果你想有更深的了解,还是建议去读一读这本书,当然也可以对照我的记录,我对其中部分难以理解的定义给出了自己的理解或是用自己的话语描述,如有错误,欢迎指正。
月缺不改光,剑折不改钢
共矜然诺心,各负纵横志