C++ Primer系列 第2章 变量和基本类型

C++ Primer系列 第2章 变量和基本类型

  • 2.1 基本内置类型
    • 2.1.1 算术类型
    • 2.1.2 类型转换
    • 2.1.3 字面值常量
  • 2.2 变量
    • 2.2.1 变量定义
    • 2.2.2 变量声明和定义的关系
    • 2.2.3 标识符
    • 2.2.4 名字的作用域
  • 2.3 复合类型
    • 2.3.1 引用
    • 2.3.2 指针
    • 2.3.3 理解复合类型的声明
  • 2.4 const限定符
    • 2.4.1 const的引用
    • 2.4.2 指针和const
    • 2.4.3 顶层const
    • 2.4.4 constexpr和常量表达式
  • 2.5 处理类型
    • 2.5.1 类型别名
    • 2.5.2 auto类型说明符
    • 2.5.3 decltype类型指示符
  • 2.6 自定义数据结构
    • 2.6.1 定义Sales_data类型
    • 2.6.2 使用Sales_data类
    • 2.6.3 编写自己的头文件
  • 小结

数据类型是程序的基础:它告诉我们数据的意义以及我们能在数据上执行的操作。

C++语言支持广泛的数据类型。它定义了几种基本内置类型(如字符,整形,浮点数等),同时也为程序员提供了自定义数据类型的机制。基于此,C++标准库定义了一些更加复杂的数据类型,比如可变长字符串和向量等。本章主要讲述内置类型,并带领大家初步了解C++语言是如何支持更复杂数据类型的。

2.1 基本内置类型

C++定义了一套包括算术类型(arithmetic)和空类型(void)在内的基本数据类型。空类型不对应具体的值,仅用于一些特殊的场合,例如最常见的是,当函数不返回任何值时使用空类型作为返回类型。

2.1.1 算术类型

算术类型分为两类:整形(integral type,包括字符和布尔类型在内)和浮点型。

算数类型的尺寸(也就是该类型数据所占的比特数)在不同机器上有所差别。下表列出了C++标准规定的尺寸的最小值,同时允许编译器赋予这些类型更大的尺寸。某一类型所占的比特数不同,它所表示的数据范围也不一样。

类型 含义 最小尺寸
bool 布尔类型 未定义
char 字符 8位
wchar_t 宽字符 16位
char16_t Unicode字符 16位
char32_t Unicode字符 32位
short 短整形 16位
int 整形 16位
long 长整型 32位
long long 长整型 32位
float 单精度浮点数 6位有效数字
double 双精度浮点数 10位有效数字
long double 扩展精度浮点数 10位有效数字

2.1.2 类型转换

含有无符号类型的表达式

unsigned u = 10;
int i = -42;
std::cout << i + i << std::endl; // 输出-84
std::cout << u + i << std::endl; // 如果int占32位,输出4294967264

在第一个输出表达式里,两个(负)整数相加并得到了期望的结果。在第二个输出表达式里,相加前首先把整数-42转换成无符号。把负数转换成无符号数类似于直接给无符号数赋一个负值,结果等于这个负数加上无符号数的模。

  • 提示:切勿混用带符号类型和无符号类型。如果表达式里既有带符号类型又有无符号类型,当带符号类型取值为负数时会出现异常结果,这是因为带符号数会自动转换成无符号数。

2.1.3 字面值常量

一个形如42的值被称作字面值常量(literal),这样的值一望而知。每个字面量常量都对应一种数据类型,字面值常量的形式和值决定了它的数据类型。

我们可以将整形字面值写作十进制数,八进制数或十六进制数的形式。以0开头的整数代表八进制数,以0x或0X开头的代表十六进制数。例如,我们能用下面的任意一种形式来表示数值20:
20 /* 十进制*/
024 /* 八进制*/
0X24 /* 十六进制*/

2.2 变量

变量提供一个具名的,可供程序操作的存储空间。C++中的每个变量都有其数据类型,数据类型决定着变量所占内存空间的大小和布局方式,该空间能存储的值的范围,以及变量能参与的运算。

2.2.1 变量定义

列表初始化
C++语言定义了初始化的好几种不同形式,这也是初始化问题复杂性的一个体现。例如,要想定义一个名为units_sold的int变量并初始化为0,以下的4条语句都可以做到这一点:

int units_sold = 0;
int units_sold = { 0 };
int units_sold{ 0 };
int units_sold(0);

作为C++11新标准的一部分,用花括号来初始化变量得到了全面应用,而在此之前,这种初始化的形式仅在某些受限的场合下才能使用。这种初始化的形式被称为列表初始化(list initialization)。现在,无论是初始化对象还是某些时候为新对象赋值,都可以使用这样一组花括号括起来的初始值了。

2.2.2 变量声明和定义的关系

为了允许把程序拆分成多个逻辑部分来编写,C++语言支持分离式编译(seperate compilation)机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。

为了支持分离式编译,C++语言将声明和定义区分开来。声明(declaration)使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义(definition)负责创建与名字关联的实体。

如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显示地初始化变量,任何包含了显示初始化地声明即称为定义。

extern int i; // 声明而非定义i
int j; // 声明并定义j
extern double pi = 3.1416; // 定义
  • 变量只能被定义一次,但是可以被多次声明。

2.2.3 标识符

C++地标识符(identifier)由字母,数字和下划线组成,其中必须以字母或下划线开头。标识符的长度没有限制,但是对大小写字母敏感。

2.2.4 名字的作用域

作用域(scope)是程序的一部分,在其中名字有其特定的含义。C++语言中大多数作用域都以花括号分割。

建议:当你第一次使用变量时再定义它
一般来说,在对象第一次使用的地方附近定义它是一种好的选择,因为这样做有助于更容易地找到变量地定义。更重要的是,当变量地定义与它第一次被使用的地方很近时,我们也会赋给它一个比较合理的初始值。

2.3 复合类型

复合类型(compound type)是指基于其他类型定义的类型。C++语言有几种复合类型,本章将介绍其中的两种:引用和指针。

2.3.1 引用

引用(reference)为对象起了另外一个名字,引用类型引用(refers to)另外一个类型。通过将声明符写成&d的形式来定义引用类型,其中d是声明的变量名:

int ival = 1024;
int& refVal = ival; // refVal指向ival(是ival的另一个名字)
int& refVal2; // 报错:引用必须被初始化

一般在初始化变量时,初始值会拷贝到新建的对象中。然而定义引用时,程序把引用和它的初始值绑定(bind)在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。

  • 引用即别名,引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字。

2.3.2 指针

指针是“指向(point to) ”另外一种类型的复合类型。

建议:初始化所有指针
使用未经初始化的指针是引发运行时错误的一大原因。

和其它变量一样,访问未经初始化的指针所引发的后果也是无法预计的。通常这一行为将造成程序崩溃,而且一旦崩溃,要想定位到出错位置将是特别棘手的问题。

因此建议初始化所有的指针,并且在可能的情况下,尽量等定义了对象之后再定义指向它的指针。如果实在不清楚指针应该指向何处,就把它初始化为nullptr或者0,这样程序就能检测并直到它没有指向任何具体的对象了。

void*指针
void* 是一种特殊的指针类型,可用于存放任意对象的地址。一个void* 指针存放着一个地址,这一点和其他指针类似。不同的是,我们对该地址中到底是个什么类型的对象并不了解:

double obj = 3.14, * pd = &obj;
void* pv = &obj; // 正确:void*能存放任意类型对象的地址
				// obj可以是任意类型的对象
pv = pd; // pv可以存放任意类型的指针

利用void* 指针能做的事儿比较有限:拿它和别的指针比较,作为函数的输入或输出,或者赋给另外一个void* 指针。不能直接操作void* 指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。

2.3.3 理解复合类型的声明

指向指针的引用
引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用:

int i = 42;
int* p; // p是一个int型指针
int*& r = p; // r是一个对指针p的引用

r = &i; // r引用了一个指针,因此给r赋值&i就是令p指向i
*r = 0; // 解引用r得到i,也就是p指向的对象,将i的值改为0

2.4 const限定符

默认状态下,const对象仅在文件内有效
当以编译时初始化的方式定义一个const对象时,就如对bufSize的定义一样:
const int bufSize = 512; // 输入缓冲区大小

编译器在编译过程中把用到该变量的地方都替换成对应的值。也就是说,编译器会找到代码中所有用到bufSize的地方,然后用512替换。

为了执行上述替换,编译器必须知道变量的初始值。如果程序包含多个文件,则每个用了const对象的文件都必须得访问它的初始值才行。要做到这一点,就必须在每一个用到变量的文件中都有对它的定义。为了支持这一用法,同时避免对同一变量的重复定义,默认情况下,const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量时,其实等于在不同文件中分别定义了独立的变量。

某些时候有这样一种const变量,它的初始值不是一个常量表达式,但是又确实有必要在文件间共享。这种情况下,我们不希望编译器为每个文件分别生成独立的变量。相反,我们想让这类const对象像其他(非常量)对象一样工作,也就是说,只在一个文件中定义const,而在其他多个文件中声明并使用它。

解决办法是,对于const变量不管是声明还是定义都添加extern关键字,这样只需定义一次就可以了:

// file_1.cc定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufSize = fun();
// file_1.h头文件
extern const int bufSize; // 与file_1.cc中定义的bufSize是同一个
  • 如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字。

2.4.1 const的引用

术语:常量引用是对const的引用
C++程序员经常把词组“对const的引用”简称为“常量引用”,这一简称还是挺靠谱的,不过前提是你得时刻记住这就是个简称而已。

严格来说,并不存在常量引用。因为引用不是一个对象,所以我们没法让引用本身恒定不变。事实上,由于C++语言并不允许随意改变引用绑定的对象,所以从这层意义上理解所有的引用又都算是常量。引用的对象是常量还是非常量可以决定其所能参与的操作,却无论如何都不会影响到引用和对象的绑定关系本身。

对const的引用可能引用一个并非const的对象
必须认识到,常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未作限定。因为对象也可能是个非常量,所以允许通过其他途径改变它的值:

int i = 42;
int &r1 = i; // 引用r1绑定对象i
const int &r2 = i; // r2也绑定对象i,但是 不允许通过r2修改i的值
r1 = 0; // r1并非常量,i的值修改为0
r2 = 0// 错误:r2是一个常量引用

r2绑定(非常量)整数i是合法的行为。然而,不允许通过r2修改i的值。尽管如此,i的值仍然允许通过其他途径修改,既可以直接给i赋值,也可以通过像r1一样绑定到i的其他引用来修改。

2.4.2 指针和const

指针是对象而引用不是 ,因此就像其他对象类型一样,允许把指针本身定位常量。常量指针(const pointer)必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了。把* 放在const关键字之前用以说明指针是一个常量,这样的书写形式隐含着一层意味,即不变的是指针本身的值而非指向的那个值:

int errNumb = 0;
int* const curErr = &errNumb; // curErr将一直指向errNumb
const double pi = 3.14159;
const double* const pip = &pi; // pip是一个指向常量对象的常量指针

要想弄清楚这些声明的含义最行之有效的办法是从右向左阅读。此例中,离curErr最近的符号是const,意味着curErr本身是一个常量对象,对象的类型由声明符的其余部分确定。声明符的下一个符号是 * ,意思时curErr是一个常量指针。

2.4.3 顶层const

如前所述,指针本身是一个对象,它又可以指向另外一个对象。因此,指针本身是不是常量以及指针所指的是不是一个常量就是两个相互独立的问题。用名词 顶层const(top-level const)表示指针本身是个常量,而用名词 底层const(low-level const)表示指针所指的对象是一个常量。

int i = 0;
int* const p1 = &i; // 不能改变p1的值,这是一个顶层const
const int ci = 42; // 不能改变ci的值,这是一个顶层const
const int* p2 = &ci; // 允许改变p2的值,这是一个底层const
const int* const p3 = p2; // 靠右的const是顶层const,靠左的是底层const
const int& r = ci; // 用于声明引用的const都是底层const

i = ci; //正确:拷贝ci的值,ci是一个顶层const,对此操作无影响
p2 = p3; // p2和p3指向的对象类型相同,p3顶层const的部分不影响

另一方面,底层const的限制却不能忽视。当执行对象的拷贝操作时,拷入和烤出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换成常量,反之则不行:

int* p = p3; //错误:p3包含底层const的定义,而p没有
p2 = p3; // 正确:p2和p3都是底层const
p2 = &i; // 正确:int*能转换成const int*
int& r = ci; // 错误:普通的int&不能绑定到int常量上
const int& r2 = i; // 正确:const int&可以绑定到一个普通int上

2.4.4 constexpr和常量表达式

常量表达式(const expression)是指值不会改变且在编译过程就能得到计算结果的表达式。

C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化:

constexpr int mf = 20; // 20是常量表达式
constexpr int limit = mf + 1; // mf + 1是常量表达式
constexpr int sz = size(); // 只有当size是一个constexpr函数时,才是一条正确的声明语句
  • 一般来说,如果你认定变量是一个常量表达式,那就把它声明成constexpr类型。

指针和constexpr
必须明确一点,在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关:

const int *p = nullptr; // p是一个指向整形常量的指针
constexpr int *q = nullptr; // q是一个指向整数的常量指针

p和q的类型相差甚远,p是一个指向整形常量的指针,而q是一个指向整数的常量指针,其中的关键在于constexpr把它所定义的对象置为了顶层const。

2.5 处理类型

随着程序越来越复杂,程序中用到的类型也越来越复杂。

2.5.1 类型别名

类型别名(type alias)是一个名字,它是某种类型的同义词。使用类型别名有很多好处,它让复杂的类型名字变得简单明了,易于理解和使用,还有助于程序员清楚的知道使用该类型的真实目的。

有两种方法可用于定义类型别名。传统的方法是使用关键字typedef:

typedef double wages; // wages是double的同义词
typedef wages base, *p; //base是double的同义词,p是double*的同义词

新标准规定了一种新的方法,使用别名声明(alias declaration)来定义类型的别名:

using SI = Sales_item; // SI是Sales_item的同义词

wages hourly, weekly; // 等价于double hourly, weekly
SI item; // 等价于Sales_item item

2.5.2 auto类型说明符

使用auto也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样:

auto i = 0, * p = &i; // 正确:i是整数,p是整形指针
auto sz = 0, pi = 3.14; // 错误:sz和pi的类型不一致

2.5.3 decltype类型指示符

有时会遇到这种情况:希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。为了满足这一要求,C++11新标准引入了第二种类型说明符decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值:
decltype(f()) sum = x; // sum的类型就是函数f的返回类型

decltype处理顶层const和引用的方式与auto有些许不同。如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内):

const int ci = 0, & cj = ci;
decltype(ci) x = 0; // x的类型是const int
decltype(cj) y = x; // y的类型是const int&,y绑定到变量x
decltype(cj) z; // 错误:z是一个引用,必须初始化

2.6 自定义数据结构

从最基本的层面理解,数据结构是把一组相关的数据元素组织起来然后使用它们的策略和方法。

2.6.1 定义Sales_data类型

  • 很多新手程序员经常忘记了在类定义的最后添加分号。

C++新标准规定,可以为数据成员提供一个类内初始值(in-class initializer)。创建对象时,类内初始值将用于初始化数据成员。没有初始值的成员将被默认初始化。

对类内初始值的限制:或者放在花括号里面,或者放在等号右边,记住不能使用圆括号

2.6.2 使用Sales_data类

so easy, skip;

2.6.3 编写自己的头文件

  • 头文件一旦改变,相关的资源文件必须重新编译以获取更新过的声明。

小结

类型是C++编程的基础

C++语言允许用户以类的形式自定义类型。C++库通过类提供了一套高级抽象类型,如输入输出和string等。

你可能感兴趣的:(c++,java,开发语言)