《C++Primer》第二章-变量和基本类型-学习笔记(3)

《C++Primer》第二章-变量和基本类型-学习笔记(3)

日志:
1,2020-02-27 笔者提交文章的初版V1.0

作者按:
最近在学习C++ primer,初步打算把所学的记录下来。

传送门/推广
上一篇
《C++Primer》第二章-变量和基本类型-学习笔记(2)

const 限定符

为了程序的可读性和可维护性,对于一些会变化的特殊的数字,应该用变量表示它,起个有意义的变量名。
但是定义一个变量代表某一常数的方法仍然有一个严重的问题。即 变量是可以被修改的。(可能被有意或无意地修改。)
const 限定符(const type qualifier)提供了一个解决办法,它把一个对象转换成一个常量。
因为常量在定义后就不能被修改,所以定义时必须初始化

const int bufSize = 512; // input buffer size

const 对象默认为文件的局部变量

非 const 变量默认为 extern。
在全局作用域里定义非 const 变量时,它在整个程序中都可以访问。可以把一个非 const 变更定义在一个文件中,假设已经做了合适的声明,就可在另外的文件中使用这个变量:

// file_1.cc
int counter; // definition
// file_2.cc
extern int counter; // uses counter from file_1
++counter; // increments counter defined in file_1

要使 const 变量能够在其他的文件中访问,必须显式地指定它为 extern。
与其他变量不同,除非特别说明,在全局作用域声明的 const 变量是定义该对象的文件的局部变量。此变量只存在于那个文件中,不能被其他文件访问。
通过指定 const 变更为 extern,就可以在整个程序中访问 const 对象,例如

// file_1.cc
// defines and initializes a const that is accessible to other files
extern const int bufSize = fcn();
// file_2.cc
extern const int bufSize; // uses bufSize from file_1
// uses bufSize defined in file_1
for (int index = 0; index != bufSize; ++index)
// ...

const关键字可以限定任何内置或用户定义的类型。另外,成员函数可能是const限定的,甚至是const重载的。

引用

引用(reference)就是对象的另一个名字。在实际程序中,引用主要用作函数的形式参数。引用是一种复合类型[2],通过在变量名前添加“&”符号来定义。复合类型是指用其他类型定义的类型。
在引用的情况下,每一种引用类型都“关联到”某一其他类型。不能定义引用型的引用,但可以定义任何其他类型的引用
引用必须用与该引用同类型的对象初始化!!!

//注意不能定义引用的引用!
int ival = 1024;
int &refVal = ival; // ok: refVal refers to ival
int &refVal2; // error: a reference must be initialized 没有初始化,错误!
int &refVal3 = 10; // error: initializer must be an object 字面值初始化,错误!

引用是别名

因为引用只是它绑定的对象的另一名字,作用在引用上的所有操作事实上都是作用在该引用绑定的对象上:

int ival = 1024;
int &refVal = ival; // ok: refVal refers to ival
refVal += 2;  //将 refVal 指向的对象 ival 加 2

当引用初始化后,只要该引用存在,它就保持绑定到初始化时指向的对象。不可能将引用绑定到另一个对象。引用只是对象的另一名字。这一规则的结果是必须在定义引用时进行初始化。初始化是指明引用指向哪个对象的唯一方法。

定义多个引用

可以在一个类型定义行中定义多个引用。必须在每个引用标识符前添加“&”符号:

int i = 1024, i2 = 2048;
int &r = i, r2 = i2; // r is a reference, r2 is an int
int i3 = 1024, &ri = i3; // defines one object, and one reference
int &r3 = i3, &r4 = i2; // defines two references

const 引用

const 引用(const reference)是指向 const 对象的引用。使用的时候也要注意,指向 const 对象的引用必须加const。

const int ival = 1024;
const int &refVal = ival; // ok: both reference and object are const
int &ref2 = ival; // error: non const reference to a const object  指向 const 对象的引用没有加const
/*用 ival 初始化 ref2 也是不合法的:ref2 是普通的非 const 引用,因此可以用来修
改 ref2 指向的对象的值。通过 ref2 对 ival 赋值会导致修改const 对象的值。为阻
止这样的修改,需要规定将普通的引用绑定到 const 对象是不合法的。*/

const 引用可以初始化为不同类型的对象,或者初始化为右值,如字面值常量

int i = 42;
// legal for const references only
const int &r = 42;      //初始化为右值,如字面值常量
const int &r2 = r + i;  //这里r是const引用,这里的初始化应该是值。

同样的初始化对于非 const 引用却是不合法的,而且会导致编译时错误。
其原因非常微妙,值得解释一下:
观察将引用绑定到不同的类型时所发生的事情,最容易理解上述行为。比如:

double dval = 3.14;
const int &ri = dval;  //注意看,ri与dval是不同的类型!,这里如果不用const会出错。

编译器会把这些代码转换成如以下形式的编码:

double dval = 3.14;
int temp = dval; // create temporary int from the double
const int &ri = temp; // bind ri to that temporary

如果 ri 不是 const,那么可以给 ri 赋一新值。这样做不会修改 dval,而是修改了 temp。期望对 ri 的赋值会修改 dval 的程序员会发现 dval 并没有被修改。仅允许 const 引用绑定到需要临时使用的值完全避免了这个问题,因为 const 引用是只读的。

非 const 引用只能绑定到与该引用同类型的对象(意思是数据类型一致的)。const 引用则可以绑定到不同但相关的类型的对象或绑定到右值。

需要说明一下,const 引用是不能指向const引用的对象的,因为引用是不能指向引用的!const 引用只能指向 const 对象。

typedef 名字

typedef 可以用来定义类型的同义词:

typedef float scores; // scores is a synonym for double

typedef 通常被用于以下三种目的:

  • 为了隐藏特定类型的实现,强调使用类型的目的。
  • 简化复杂的类型定义,使其更易理解。
  • 允许一种类型用于多个目的,同时使得每次使用该类型的目的明确。

枚举

枚举类型(enumeration)是C++中的一种派生数据类型,它是由用户定义的若干枚举常量的集合。枚举不但定义了常量集,而且还把它们聚集成组。

枚举的定义包括关键字 enum,其后是一个可选的枚举类型名,和一个用花括号括起来、用逗号分开的枚举成员列表。枚举类型的一般形式为:

enum 枚举名{ 
     标识符[=整型常数], 
     标识符[=整型常数], 
... 
    标识符[=整型常数]
} 枚举变量;

默认情况下,第一个名称的值为 0,第二个名称的值为 1,第三个名称的值为 2,以此类推。

enum color { red, green, blue } c;
c = blue;

但也可以给名称赋予一个特殊的值,只需要添加一个初始值即可。例如,在下面的枚举中,green 的值为 5。

enum color { red, green=5, blue };

在这里,blue 的值为 6,因为默认情况下,每个名称都会比它前面一个名称大 1,但 red 的值依然为 0。

枚举成员是常量
我们可以为一个或多个枚举成员提供初始值,用来初始化枚举成员的值必须是一个常量表达式。常量表达式是编译器在编译时就能够计算出结果的整型表达式。整型字面值常量是常量表达式,正如一个通过常量表达式自我初始化的 const对象也是常量表达式一样。不能改变枚举成员的值。枚举成员本身就是一个常量表达式,所以也可用于
需要常量表达式的任何地方。

类类型

C++ 中,通过定义来自定义数据类型。类定义了该类型的对象包含的数据和该类型的对象可以执行的操作。每个类都定义了一个接口和一个实现。接口由使用该类的代码需要执行的操作组成。实现一般包括该类所需要的数据。实现还包括定义该类需要的但又不供一般性使用的函数。

如何定义类

定义类时,通常先定义该类的接口,即该类所提供的操作。通过这些操作,可以决定该类完成其功能所需要的数据,以及是否需要定义一些函数来支持该类的实现。

class Sales_item {
public:
// operations on Sales_item objects will go here
private:
std::string isbn;
unsigned units_sold;
double revenue;
};    //注意这里的分号!

类定义以关键字 class 开始,其后是该类的名字标识符。类体位于花括号里面。花括号后面必须要跟一个分号。编程新手经常会忘记类定义后面的分号,这是个很普遍的错误!

类的成员

类体可以为空。类体定义了组成该类型的数据和操作。这些操作和数据是类的一部分,也称为类的成员。操作称为成员函数,而数据则称为数据成员。类也可以包含 0 个到多个 private 或 public 访问标号。访问标号控制类的成员在类外部是否可访问.
每一个类都定义了它自己的作用域。也就是说,数据和操作的名字在类的内部必须唯一,但可以重用定义在类外的名字。

定义变量和定义数据成员存在非常重要的区别:一般不能把类成员的初始化作为其定义的一部分。当定义数据成员时,只能指定该数据成员的名字和类型。
类不是在类定义里定义数据成员时初始化数据成员,而是通过称为构造函数的特殊成员函数控制初始化。

访问标号

访问标号(access labels)负责控制使用该类的代码是否可以使用给定的成员。类的成员函数可以使用类的任何成员,而不管其访问级别。访问标号 public、private 可以多次出现在类定义中。给定的访问标号应用到下一个访问标号出现时为止。

使用struct 关键字

C++ 支持另一个关键字 struct,它也可以定义类类型struct 关键字是从C 语言中继承过来的。
如果使用 class 关键字来定义类,那么定义在第一个访问标号前的任何成员都隐式指定为 private;如果使用 struct 关键字,那么这些成员都是public。使用 class 还是 struct 关键字来定义类,仅仅影响默认的初始访问级别。

struct Sales_item {
// no need for public label, members are public by default
// operations on Sales_item objects
private:
std::string isbn;
unsigned units_sold;
double revenue;
};

用 class 和 struct 关键字定义类的唯一差别在于默认访问级别:默认情况下,struct 的成员为 public,而 class
的成员为 private

编写自己的头文件

头文件(head file):头文件为相关声明提供了一个集中存放的位置。头文件一般包含类的定义、
extern 变量的声明和函数的声明。

head file includes: class definition, declaration of extern variable and declaration of function.

名字在使用前必须先声明或定义。只要每个实体位于使用它的代码之前,这个策略就有效。但是,
很少有程序简单到可以放置在一个文件中。由多个文件组成的程序需要一种方法连接名字的使用和声明,在 C++ 中这是通过头文件实现的。为了允许把程序分成独立的逻辑块,C++ 支持所谓的分别编译。这样程序可以由多个文件组成。

头文件的正确使用能够带来两个好处:

  • 保证所有文件使用给定实体的同一声明;
  • 当声明需要修改时,只有头文件需要更新。

设计头文件还需要注意以下几点:

  • 头文件中的声明在逻辑上应该是统一的。
  • 编译头文件需要一定的时间。如果头文件太大,程序员可能不愿意承受包含该头 文件所带来的编译时代价。

头文件用于声明而不是用于定义

当设计头文件时,记住定义和声明的区别是很重要的[3][4]。定义只可以出现一次,而声明则可以出现多次。

extern int ival = 10; // initializer, so it's a definition
double fica_rate; // no extern, so it's a definition

一个程序中有两个以上文件含有上述任一个定义都会导致多重定义链接错误。因为头文件包含在多个源文件中,所以不应该含有变量或函数的定义。

对于头文件不应该含有定义这一规则,有三个例外。头文件可以定义类、值在编译时就已知道的 const 对象和 inline 函数(第 7.6 节介绍 inline 函数)。这些实体可在多个源文件中定义,只要每个源文件中的定义是相同的。
在头文件中定义这些实体,是因为编译器需要它们的定义(不只是声明)来产生代码。例如:为了产生能定义或使用类的对象的代码,编译器需要知道组成该类型的数据成员。同样还需要知道能够在这些对象上执行的操作。类定义提供所需要的信息。在头文件中定义 const 对象则需要更多的解释。

一些const 对象定义在头文件中

const 变量默认时是定义该变量的文件的局部变量。正如我们现在所看到的,这样设置默认情况的原因在于允许 const 变量定义在头文件中。
在 C++ 中,有些地方需要放置常量表达式。例如,枚举成员的初始化式必须是常量表达式。一般来说,常量表达式是编译器在编译时就能够计算出结果的表达式。当const 整型变量通过常量表达式自我初始化时,这个 const 整型变量就可能是常量表达式。而 const 变量要成为常量表达式,初始化式必须为编译器可见。为了能够让多个文件使用相同的常量值,const 变量和它的初始化式必须是每个文件都可见的。而要使初始化式可见,一般都把这样的 const 变量定义在头文件中。那样的话,无论该 const 变量何时使用,编译器都能够看见其初始化式。
但是,C++ 中的任何变量都只能定义一次。定义会分配存储空间,而所有对该变量的使用都关联到同一存储空间。因为 const 对象默认为定义它的文件的局部变量,所以把它们的定义放在头文件中是合法的。这种行为有一个很重要的含义:当我们在头文件中定义了 const 变量后,每个包含该头文件的源文件都有了自己的 const 变量,其名称和值都一样。
当该 const 变量是用常量表达式初始化时,可以保证所有的变量都有相同的值。但是在实践中,大部分的编译器在编译时都会用相应的常量表达式替换这些 const 变量的任何使用。所以,在实践中不会有任何存储空间用于存储用常量表达式初始化的 const 变量。
如果 const 变量不是用常量表达式初始化,那么它就不应该在头文件中定义。相反,和其他的变量一样,该 const 变量应该在一个源文件中定义并初始化。应在头文件中为它添加 extern 声明,以使其能被多个文件共享。

C++预处理器

要使用头文件,必须在源文件中#include 该头文件。为了编写头文件,我们需要进一步理解 #include 指示是怎样工作的。#include 设施是C++ 预处理器的一部分。
预处理器处理程序的源代码,在编译器之前运行。C++ 继承了 C 的非常精细的预处理器。现在的 C++ 程序以高度受限的方式使用预处理器。
#include 指示只接受一个参数:头文件名。预处理器用指定的头文件的内容替代每个 #include。系统的头文件可能用特定于编译器的更高效的格式保存。无论头文件以何种格式保存,一般都含有支持分别编译所需的类定义及变量和函数的声明。

头文件经常需要其他头文件

包含其他头文件是如此司空见惯,甚至一个头文件被多次包含进同一源文件也不稀奇。例如,使用 Sales_item 头文件的程序也可能使用 string 库。该程序不会(也不应该)知道 Sales_item 头文件使用了 string 库。在这种情况下,string 头文件被包含了两次:一次是通过程序本身直接包含,另一次是通过包含 Sales_item 头文件而间接包含。
因此,设计头文件时,应使其可以多次包含在同一源文件中,这一点很重要。我们必须保证多次包含同一头文件不会引起该头文件定义的类和对象被多次定义。使得头文件安全的通用做法,是使用预处理器定义头文件保护符头文件保护符用于避免在已经见到头文件的情况下重新处理该头文件的内容。

避免多重包含

预处理器允许我们自定义变量。预处理器变量 的名字在程序中必须是唯一的。任何与预处理器变量相匹配的名字的使用都关联到该预处理器变量。为了避免名字冲突,预处理器变量经常用全大写字母表示。
预处理器变量有两种状态:已定义或未定义。
#define指示接受一个名字并定义该名字为预处理器变量。
#ifndef指示检测指定的预处理器变量是否未定义。如果预处理器变量未定义,那么跟在其后的所有指示都被处理,直到出现#endif

头文件应该含有保护符,即使这些头文件不会被其他头文件包含。编写头文件保护符并不困难,而且如果头文件被包含多次,它可以避免难以理解的编译错误。

当没有两个头文件定义和使用同名的预处理器常量时,这个策略相当有效。我们可以为定义在头文件里的实体(如类)命名预处理器变量来避免预处理器变量重名的问题。通过使用类名来组成头文件和预处理器变量的名字,可以使得很可能只有一个文件将会使用该预处理器变量。

使用自定义的头文件

#include 指示接受以下两种形式:

  • #include <standard_header>
  • #include “my_file.h”

如果头文件名括在尖括号(< >)里,那么认为该头文件是标准头文件。编译器将会在预定义的位置集查找该头文件,这些预定义的位置可以通过设置查找路径环境变量或者通过命令行选项来修改。使用的查找方法因编译器的不同而差别迥异。
如果头文件名括在一对引号里,那么认为它是非系统头文件,非系统头文件的查找通常开始于源文件所在的路径。

参考资料

【1】C++ Primer 中文版(第四版·特别版)
【2】《C++Primer》第二章-变量和基本类型-学习笔记(1)
【3】《C++Primer》第二章-变量和基本类型-学习笔记(2)

注解

【4】变量的定义用于为变量分配存储空间,还可以为变量指定初始值。在一个程序中,变量有且仅有一个定义。变量的声明用于向程序表明变量的类型和名字。定义也是声明:当定义变量时我们声明了它的类型和名字。可以通过使用extern 关键字声明变量名而不定义变量不定义变量的声明包括对象名、对象类型和对象类型前的关键字extern:

本文许可证

本文遵循 CC BY-NC-SA 4.0(署名 - 非商业性使用 - 相同方式共享) 协议,转载请注明出处,不得用于商业目的。
CC BY-NC-SA 4.0

发布了52 篇原创文章 · 获赞 72 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/engineerxin/article/details/104534715