《C++ Primer(第5版)》第二章笔记


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

2.1 基本内置类型

  • 算数类型
  • 空类型:不对应任何具体值
    在这里插入图片描述
    算数类型的尺寸在不同机器上有所差别。表2.1列出了C++标准规定的尺寸的最小值,同时允许编译器赋予这些类型更大的尺寸。

浮点型可表示单精度、双精度和扩展精度值。C++标准指定了一个浮点数有效位数的最小值,然而大多数编译器都实现了更高的精度。通常,float以1个字(32比特)来表示,double 以2个字(64 比特)来表示,long double以3或4个字(96或128比特)来表示。一般来说,类型float和double分别有7和16个有效位;类型long double则常常被用于有特殊浮点需求的硬件,它的具体实现不同,精度也各不相同。

带符号类型和无符号类型

类型int、short、long 和long long 都是带符号的,通过在这些类型名前添加unsigned就可以得到无符号类型,例如unsigned long。 类型unsigned int 可以缩写为unsigned.

与其他整型不同,字符型被分为了三种:char、signed char和unsigned char。
特别需要注意的是:类型char和类型signed char并不一样。尽管字符型有三种,但是字符的表现形式却只有两种:带符号的和无符号的。类型char实际上会表现为上述两种形式中的一种,具体是哪种由编译器决定。
在这里插入图片描述

2.1.2 类型转换

类型所能表示的值的范围决定了转换的过程:

  • 当我们把一个非布尔类型的算术值赋给布尔类型时,初始值为0则结果为false,否则结果为true.
  • 当我们把一个布尔值赋给非布尔类型时,初始值为false则结果为0,初始值为true则结果为1。
  • 当我们把一个浮点数赋给整数类型时,进行了近似处理。结果值将仅保留浮点数中小数点之前的部分。
  • 当我们把一个整数值赋给浮点类型时,小数部分记为0。如果该整数所占的空间超过了浮点类型的容量,精度可能有损失。
  • 当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。例如,8比特大小的unsigned char可以表示0至255区间内的值,如果我们赋了一个区间以外的值,则实际的结果是该值对256取模后所得的余数。因此,把-1赋给8比特大小的unsigned char 所得的结果是255。
  • 当我们赋给带符号类型一个超出它表示范围的值时,结果是未定义的(undefined)。此时,程序可能继续工作、可能崩溃,也可能生成垃圾数据。

含有无符号类型的表达式
尽管我们不会故意给无符号对象赋值,却可能特别容易写出这么做的代码。如有符号和无符号int相加

unsigned u = 10;
int i = -42;
cout << i + i << endl;	// 输出-84
cout << i + u << endl;	// 输出4294967264

当无符号减一个值时,不管这个值是不是无符号数,我们都必须确保结果不能是一个负值:

unsigned u1 = 42, u2 = 10;
cout << u1 - u2 << endl;	// 32
cout << u2 - u1 << endl;	// 取模后的值

无符号数不会小于零,这也关系到循环的写法:

for (unsigned u = 10; u >= 0; u--){
	cout << u << endl;
}

由于0 - 1 = 4294967295,所以会一直符合循环条件,变成死循环。

切勿混用带符号类型和无符号类型

2.1.3 字面值常量

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

整型和浮点型字面值
以0开头的整数代表八进制,以0x或0X开头的代表十六进制。如我们可以用下面的任意一个形式表示20:
十进制:20
八进制:024
十六进制:0x14

整型字面值具体的数据类型由它的值和符号决定。默认情况下,十进制字面值是带符号数,八进制和十六进制字面值既可能是带符号的也可能是无符号的。十进制字面值的类型是int、long和long long中尺寸最小的那个(例如,三者当中最小是int),当然前提是这种类型要能容纳下当前的值。八进制和十六进制字面值的类型是能容纳其数值的int、unsigned int、long、unsigned long、long long和unsigned long long中的尺寸最小者。如果一个字面值连与之关联的最大的数据类型都放不下,将产生错误。类型short没有对应的字面值。

默认的,浮点型字面值是一个double。

字符和字符串字面值
由单引号括起来的一一个字符称为char型字面值,双引号括起来的零个或多个字符则构成字符串型字面值。
字符串字面值的类型实际上是由常量字符构成的数组(array)。编译器在每个字符串的结尾处添加一个空字符( ‘\0’),因此,字符串字面值的实际长度要比它的内容多1。例如,字面值’ A’表示的就是单独的字符A,而字符串"A"则代表了一个字符的数组,该数组包含两个字符::一个是字母A、另一个是空字符。

转义序列
在这里插入图片描述

指定字面值的类型
在这里插入图片描述

通过添加表2.2中所列的前缀和后缀,可以改变整型、浮点型和字符型字面值的默认类型。
在这里插入图片描述

布尔字面值和指针字面值

true和false是布尔字面值
nullptr是指针字面值

2.2 变量

变量提供一个具名的、可供程序操作的存储空间。C++中的每个变量都有其数据类型,数据类型决定着变量所占内存空间的大小和布局方式、该空间能存储的值的范围,以及变量能参与的运算。对C++程序员来说,“变量(variable)” 和“对象(object)” 一般可以互换使用。

2.2.1 变量定义

何为变量
遵循大多数人的习惯用法,即认为对象是具有某种数据类型的内存空间。我们在使用对象这个词时,并不严格区分是类还是内置类型,也不区分是否命名或是否只读

初始化
在C++语言中,初始化是一个异常复杂的问题,我们也将反复讨论这个问题。很多程序员对于用等号=来初始化变量的方式倍感困惑,这种方式容易让人认为初始化是赋值的一种。事实上在C++语言中,初始化和赋值是两个完全不同的操作。然而在很多编程语言中二者的区别几乎可以忽略不计,即使在C++语言中有时这种区别也无关紧要,所以人们特别容易把二者混为一谈。 需要强调的是,这个概念至关重要,我们也将在后面不止一次提及这一点。
初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来替代。

列表初始化

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

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

默认初始化
如果定义变量是没有指定初值,则变量被默认初始化,此时变量被赋予默认值。默认值到底是什么由变量类型决定,同时定义变量的位置也会对此有影响。
如果是内置类型的变量未被显式初始化,它的值由定义的位置决定。定义于任何函数体之外的变量被初始化为0。然而一种例外情况是,定义在函数体内部的内置类型变量将不被初始化(uninitialized)。一个未被初始化的内置类型变量的值是未定义的,如果试图拷贝或以其他形式访问此类值将引发错误。

int a;
void main(){
	int b;
	cout << a << endl;	// 正确:默认初始化为0
	cout << b << endl;	// 错误:未定义的
}

2.2.2 变量声明和定义的关系

为了允许把程序拆分成多个逻辑部分来编写,C++语言支持分离式编译(separate compilation)机制,该机制允许将程序分割为若千个文件,每个文件可被独立编译。
为了支持分离式编译,C++语言将声明和定义区分开来。声明(declaration) 使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义(definition)负责创建与名字关联的实体。
变量声明规定了变量的类型和名字,在这一点上定义与之相同。但是除此之外,定义还申请存储空间,也可能会为变量赋一个初始值。如果想声明一个变量而非定义它,就在变量名前添加关键字extern而且不要显式地初始化变量

extern int i;		// 声明i而非定义i
int j;				// 声明并定义j

任何包含了显式初始化的声明即成为定义。我们能给由extern关键字标记的变量赋一个初始值,但是这么做也就抵消了extern的作用。extern语句如果包含初始值就不再是声明,而变成定义了:

extern double pi = 3.1415926;	// 定义

在函数体内部,如果试图初始化一个由extern关键字标记的变量,将引发错误。

一个变量可以被多次声明,但只能被定义一次。

声明和定义的区别看起来也许微不足道,但实际上却非常重要。如果要在多个文件中使用同一个变量,就必须将声明和定义分离。此时,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义。

【注】静态类型
C++是一种静态类型(statically typed)语言,其含义是在编译阶段检查类型。其中,检查类型的过程称为类型检查( type checking )。
我们已经知道,对象的类型决定了对象所能参与的运算。在C++语言中,编译器负责检查数据类型是否支持要执行的运算,如果试图执行类型不支持的运算,编译器将报错并且不会生成可执行文件。
程序越复杂,静态类型检查越有助于发现问题。然而,前提是编译器必须知道每一个实体对象的类型,这就要求我们在使用某个变量之前必须声明其类型

2.2.3 标识符

C++的标识符(identifier)由字母、数字和下画线组成,其中必须以字母或下画线开头。标识符的长度没有限制,但是对大小写字母敏感。
C++语言保留了一些名字供语言本身使用,这些名字不能被用作标识符。
同时,C++也为标准库保留了-些名字。用户自定义的标识符中不能连续出现两个下画线,也不能以下画线紧连大写字母开头。此外,定义在函数体外的标识符不能以下画线开头。

变量命名规范

变量命名有许多约定俗成的规范,下面的这些规范能有效提高程序的可读性:

  • 标识符要能体现实际含义。
  • 变量名一般用小写字母,如index, 不要使用Index或INDEX.
  • 用户自定义的类名一般以大写字母开头,如sales_item。
  • 如果标识符由多个单词组成,则单词间应有明显区分,如student_loan或,studentLoan,不要使用studentloan.

在这里插入图片描述

2.2.4 名字的作用域

同一个名字在不同的作用域中可能指向不同的实体。名字的有效区域始于名字的声明语句,以声明语句所在的作用域末端为结束。

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

嵌套的作用域

作用域能彼此包含,被包含(或者说被嵌套)的作用域称为内层作用域(innerscope),包含着别的作用域的作用域称为外层作用域(outerscope)。
作用域中一旦声明了某个名字,它所嵌套着的所有作用域中都能访问该名字。同时,允许在内层作用域中重新定义外层作用域已有的名字

#include <iostream>
using namespace std;

int reused = 42;
int main() {
	cout << reused << endl;		// 42
	int reused = 20;
	cout << reused << endl;		// 20
	cout << ::reused << endl;	// 显式的访问全局变量,输出42
}

2.3 复合类型

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

与我们已经掌握的变量声明相比,定义复合类型的变量要复杂很多。一条简单的声明语句由一个数据类型和紧随其后的一个变量名列表组成。其实更通用的描述是,一条声明语句由一个基本数据类型(base type)和紧随其后的一个声明符(declarator)列表组成。每个声明符命名了一个变量并指定该变量为与基本数据类型有关的某种类型。
目前为止,我们所接触的声明语句中,声明符其实就是变量名,此时变量的类型也就是声明的基本数据类型。其实还可能有更复杂的声明符,它基于基本数据类型得到更复杂的类型,并把它指定给变量。

2.3.1 引用

C++11增加了“右值引用”,将在以后做介绍;这里的引用指的是左值引用。

引用(reference) 为对象起了另外一个名字,引用类型引用(refers to)另外一种类型:

int ival = 1024;
int& refVal = ival;
int& refVal2;		// 报错

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

  • 引用即别名
  • 引用本身不是一个对象,所以不能定义引用的引用

2.3.2 指针

  • 指针本身就是一个对象
  • 允许对指针进行赋值、拷贝
  • 在生命周期内可以先后指向不同的对象
  • 指针的类型都要和他所指向的对象类型严格匹配
  • 可以不赋初值
  • 和其他内置类型一样,如果没有初始化,将拥有一个不确定的值(建议初始化所有指针,如果不知道指向何处就置为空指针

获取对象的地址

&取地址符

int ival = 42;
int *p = &ival;		// 存放ival的地址

指针值

  1. 指向一个对象
  2. 执行紧邻对象所占空间的下一个位置
  3. 空指针(没有指向任何对象)
  4. 无效指针

访问无效指针的后果无法预计,因此程序员必须清楚任意给定的指针是否有效

利用指针访问对象

*解引用符

int ival = 42;
int *p = &ival;	
cout << *p;		// 42

空指针

以下都是空指针的生成方法:

int *p1 = nnullptr;
int *p2 = 0;
int *p3 = NULL;

其他操作

int val = 42;
int *p = &val;
if (p){}		//true
p = nullptr;
if	(p){}		// false

只要不是空指针任何指针的条件值都为true。

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

double obj = 3.14, *pd = &obj;
void *pv = &obj;
pv = pd;	// pv可以存放任意类型的指针

利用void指针能做的事儿比较有限:拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个void指针。不能直接操作void指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。
概括说来,以void
的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象。以后将讲述获取void*指针所存地址的方法。

2.3.3 理解复合类型的声明

如前所述,变量的定义包括-一个基本数据类型(base type)和一-组声明符。 在同一条定义语句中,虽然基本数据类型只有一个,但是声明符的形式却可以不同。也就是说,一条定义语句可能定义出不同类型的变量:

int i = 1024, *p = &i, &r = i;
// i是一个int型的数,p是一个int型指针,r是一个int型引用

定义多个变量

经常有一种观点会误以为,在定义语句中,类型修饰符(*或&)作用于本次定义的全部变量:

int *p1, p2;	
int* p, p2;

这两种写法中,p1都是指向int的指针,p2都是int

指向指针的引用

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

int i = 42;
int *p;
int &r = p;

r = &i;
*r = 0;

2.4 const限定符

有时我们希望定义这样一种变量, 它的值不能被改变。例如,用一个变量来表示缓冲区的大小。使用变量的好处是当我们觉得缓冲区大小不再合适时,很容易对其进行调整。另一方面,也应随时警惕防止程序一不小心改变了这个值。 为了满足这一要求,可以用关键字const对变量的类型加以限定。因为const对象一旦创建就不能更改,所以const对象必须初始化

int i = 42;
const int ci = i;
int j = ci;

默认状态下,const对象仅在文件内有效

如果只在一个文件中定义const,而在其他多个文件中声明并使用它,解决办法是,对于const变量不管事声明还是定义都添加extern关键字,这样只需定义一次就可以了:

// file_1.cpp 定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufSize = fcn();
// file_1.h 头文件
extern const int bufSize;

如上述程序所示,file_1.cpp 定义并初始化了bufSize。因为这条语句包含了初始值,所以它(显然)是一次定义。然而,因为bufSize是一个常量,必须用extern加以限定使其被其他文件使用。
file_1.h 头文件中的声明也由extern做了限定,其作用是指明bufSize并非本文件所独有,它的定义将在别处出现。

2.4.1 const引用

对const的引用也可称为常量引用。
可以把引用绑定到const对象上,就像绑定到其他对象上一样,我们称之为对常量的引用(reference to const)。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象。

初始化和对const的引用

int i = 42;
const int &r1 = i;			// 正确
const int &r2 = 42;			// 正确
const int &r3 = r1 * 2;		// 正确
int &r4 = r1 * 2;			// 错误

要想理解这种例外情况的原因,最简单的办法是弄清楚当一个常量引用被绑定到另外一种类型上时到底发生了什么:

double dval = 3.14;
const int &ri = dval;

此处ri引用了一个int型的数。对ri的操作应该是整数运算,但dval却是一个双精度浮点数而非整数。因此为了确保让ri绑定一个整数,编译器把上述代码变成了如下形式:

const int temp = dval;		// 由双精度浮点生成一个临时的整型变量
const int &ri = temp;		// 让ri绑定这个临时变量

在这种情况下,ri绑定了一个临时量(temporary) 对象。所谓临时量对象就是当编译器需要一个空间来暂存表达式的求值结果时临时创建的一个 未命名的对象。C++程序员们常常把临时量对象简称为临时量。

当ri不是临时量时:

  1. 如果ri不是常量,就允许对ri赋值,这样就会改变ri所引用对象的值
  2. 此时绑定的对象是一个临时量而非dval

程序员既然让ri引用dval, 就肯定想通过ri改变dval的值,否则干什么要给ri赋值呢?如此看来,既然大家基本上不会想着把引用绑定到临时量上,C++语言也就把这种行为归为非法。

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

int i = 42;
int &r1 = i;
const int &r2 = i;		// r2绑定对象i,但是不允许通过r2修改i的值
r1 = 0;					// r1并非常量,可以修改i的值
r2 = 0;					// 错误

2.4.2 指针和const

指针常量
与引用一样,也可以令指针指向常量或非常量。类似于常量引用,指向常量的指针(pointer to const)不能用于改变其所指对象的值。要想存放常量对象的地址,只能使用指向常量的指针:

const double pi = 3.14;
double *ptr = &pi;			// 错误
const double *cptr = &pi;	// 指针常量
*cptr = 42;					// 错误

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

int errNumb = 0;
int *const curErr = &errNumb;
const double pi = 3.14;
const double *const pip = &pi;

要想弄清楚这些声明的含义最行之有效的办法是从右向左阅读。此例中,离curErr最近的符号是const,意味着curErr本身是一个常量对象,对象的类型由声明符的其余部分确定。声明符中的下一个符号是*,意思是CurErr是一个常量指针。最后,该声明语句的基本数据类型部分确定了常量指针指向的是一个int对象。与之相似,我们也能推断出,pip 是一个常量指针,它指向的对象是一个双精度浮点型常量。

2.4.3 顶层const

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

2.4.4 constexpr和常量表达式

常量表达式(const expression)是指值不会改变并且在编译过程就能得到计算结果的表达式。显然,字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。后面将会提到,C++语言中有几种情况下是要用到常量表达式的。

一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定,例如:

const int max_files = 20;			// 是
const int limit = max_files + 1;	// 是
int staff_size = 27;				// 不是
const int sz = get_size();			// 不是

尽管staff_size的初始值27是个字面值常量,但由于它的数据类型只是一个普通int而非const int, 所以它不属于常量表达式。另一方面,尽管sz本身是一个常量,但它的具体值直到运行时才能获取到,所以也不是常量表达式。

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

constexpr int mf = 20;		// 是
constexpr int limit mf + 1;	// 是
constexpr int sz = size();	// 只有当size()是一个constexpr函数时蚕食一条正确的声明语句

尽管不能使用普通函数作为constexpr变量的初始值,但c++11新标准允许定义一种特殊的constexpr函数。这种函数应该足够简单以使得编译时就可以计算其结果,这样就能用constexpr函数去初始化constexpr变量了。
一般,如果你认定变量是一个常量表达式,那就把它声明成constexpr类型。

字面值类型
算是类型、引用和指针都属于字面值类型。但自定义类、IO库、string类型不属于字面值类型,也就不能定义成constexpr。

constexpr int age = 20;							// 正确
constexpr string name = "liu";					// 错误
constexpr Person person = Person(20, "liu");	// 错误

尽管指针和引用都能定义成constexpr, 但它们的初始值却受到严格限制。一个constexpr指针的初始值必须是nullptr或者0,或者是存储于某个固定地址中的对象。
函数体内定义的变量一般来说并非存放在固定地址中,因此constexpr指针不能指向这样的变量。相反的,定义于所有函数体之外的对象其地址固定不变,能用来初始化constexpr指针。
允许函数定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量一样也有固定地址。因此,constexpr引用能绑定到这样的变量上,constexpr指针也能指向这样的变量。

int i = 0;
const int j = 0;
int main(){
	constexpr int* p = 0;
	constexpr int* pi = &i;
	constexpr int* pj = &j;		// 错误
}

指针和constexpr

在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关。

2.5 处理类型

随着程序越来越复杂,程序中用到的类型也越来越复杂,这种复杂性体现在两个方面。一是一些类型难于“拼写”,它们的名字既难记又容易写错,还无法明确体现其真实目的和含义。二是有时候根本搞不清到底需要的类型是什么,程序员不得不回过头去从程序的上下文中寻求帮助。

2.5.1 类型别名

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

有两种方法可以定义类型别名。
一是typedef

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

二是别名声明

using SI = Sales_item;	// 等号左侧的名字规定成等号右侧类型的别名
wages hourly, weekly;	// 等价于double hourly, weekly;
SI item;				// 等价于Sales_item item;

指针、常量和类型别名
如果某个类型别名指代的是复合类型或常量,那么把它用到声明语句里就会产生意想不到的后果。例如下面的声明语句用到了类型pstring,它实际上是类型char*的别名:

typedef char *pstring;
const pstring cstr = 0; 	// cstr 是指向char的常量指针
const pstring *ps;			// ps是一个指针,它的对象是指向char的常量指针

上述两条声明语句的基本数据类型都是const pstring,和过去一样,const 是对给定类型的修饰。pstring实际上是指向char的指针,因此,const pstring就是指向char的常量指针,而非指向常量字符的指针。
遇到一条使用了类型别名的声明语句时,人们往往会错误地尝试把类型别名替换成它本来的样子,以理解该语句的含义:

const pstring cstr = 0;	// 常量指针(因为pstring是指针) -->	int *const cstr = 0;
const char *cstr = 0;	// 指针常量		这是对const pstring cstr的错误理解

再强调一遍:这种理解是错误的。声明语句中用到pstring时,其基本数据类型是指针。可是用char* 重写了声明语句后,数据类型就变成了char, *成为了声明符的一部分。这样改写的结果是,const char成了基本数据类型。前后两种声明含义截然不同,前者声明了一个指向char的常量指针,改写后的形式则声明了一个指向const char的指针。

2.5.2 auto类型说明符

用它就能让编译器替我们去分析表达式所属的类型。显然,auto定义的变量必须有初始值:

auto item = val1 + val2;

此处编译器将根据val1和val2相加的结果来推断item的类型。如果val1和val2是类Sales_item的对象,则item的类型就是Sales_item。

复合类型、常量、auto

  • 当引用被作为初始值时,编译器以引用对象的类型作为auto的类型
  • auto一般会忽略顶层const,同时底层const会保留下来
  • 如果希望推断出auto类型是一个顶层const需要明确指出
  • 引用的类型也可以是auto
const int ci = i, &cr = ci;
auto b = ci;		// 整数,ci的顶层const特性被忽略掉了
auto c = cr;		// 整数,cr是ci的别名,ci本身就是一个顶层const
auto d = &i;		// 整型指针
auto e = &ci;		// 底层const,常量指针

const auto f = ci;	// ci的推演类型是int,f是const int
auto &g = ci;		// g是一个整型常量引用
auto &h = 42;		// 错误:不能为非常量引用绑定字面值
const auto &j = 42;	// 正确:可以为常量引用绑定字面值

2.5.3 decltype类型指示符

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

decltype(f()) sum = x;	// sum的类型就是函数f的返回类型

编译器并不实际调用f,而是将f的返回值类型作为sum的类型。

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

const int ci = 0, &cj = ci;
decltype(ci) x = 0;				// const int
decltype(cj) y = x;				// const int&,引用
decltype(cj) z;					// 错误:z是一个引用,必须被初始化

需要注意的是,引用从来都是作为其所指对象的同义词出现,只有在decltype处是一个例外。

decltype和引用
如果decltype使用的表达式不是一一个变量,则decltype返回表达式结果对应的
类型。如4.1.1 节(第120页)将要介绍的,有些表达式将向decltype返回一个引用类
型。一般来说当这种情况发生时,意味着该表达式的结果对象能作为一条赋值语句的左值:

// decltype 的结果可以是引用类型
inti=42*p=&i,&r=i;
decltype(r + 0) b;	// 正确:加法的结果是int,因此b是一个(未初始化的) int
decltype(*p) c;		// 错误:p本身是int*,而*是解引用符,所以经过解引用候p为引用而不是int,所以c是int&,必须初始化

因为r是一个引用,因此decltype (I)的结果是引用类型。如果想让结果类型是r所指的类型,可以把r作为表达式的一部分,如r+0,显然这个表达式的结果将是-一个具体值而非一个引用。
另一方面,如果表达式的内容是解引用操作,则decltype将得到引用类型。正如我们所熟悉的那样,解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。因此,decltype (*p)的结果类型就是int&,而非int。

如果在decltype()的括号中加一个(),则返回的必是一个引用

decltype((i)) d;	// 错误:d是int&
decltype(i) e;		// 正确:e是int

2.6 自定义数据结构

从最基本的层面理解,数据结构是把一组相关的数据元素组织起来然后使用它们的策略和方法。举一个例子,我们的Sales_item 类把书本的ISBN编号、售出量及销售收入等数据组织在了一起,并且提供诸如isbn函数、>>、<<、+、+=等运算在内的一系列操作,Sales_ item类就是一个数据结构。
C++语言允许用户以类的形式自定义数据类型,而库类型string、 istream、ostream等也都是以类的形式定义的。

2.6.1 定义Sales_data类型

目前这个数据结构还没有任何运算功能,不妨坝顶命名为Sales_data,以示与Sales_item的区别:

struct Sales_data{
	string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0;
};

类体右侧的表示结束的花括号后必须写一个分号。

struct Sales_data{/*...*/} accum, trans, *salesptr;
// 与上一条语句等价,但可能更好一些
struct Sales_data;
Sales_data accum, trans, *salespr;

一般来说,最好不要把对象的定义和类的定义放在一起。这么做无异于把两种不同实体的定义混在了一条语句里,一会儿定义类,一会儿又定义变量,显然这是一种不被建议的行为。

C++新标准规定,可以为数据成员提供一个类内初始值。创建对象时,类内初始值将用于初始化数据成员。

2.6.2 使用Sales_data类

和sales_item 类不同的是,我们自定义的sales_data类没有提供任何操作,sales_data类的使用者如果想执行什么操作就必须自己动手实现。

添加两个Sales_data对象

#include <iostream>
#include <string>
#include "Sales_data.h"

using namespace std;

int main() {
	Sales_data data1, data2;
	// 读入data1和data2的代码
	// 检查data1和data2是否相同的代码
	// 如果相同求data1和data2的总和
}

Sales_data对象读入数据

double price = 0;
cin >> data1.bookNo >> data1.units_sold >> price;
data1.revenue = data1.units_sold * price;
cin >> data2.bookNo >> data2.units_sold >> price;
data2.revenue = data2.units_sold * price;

输出两个Sales_data对象的和

	if (data1.bookNo == data2.bookNo) {
		unsigned totalCnt = data1.units_sold + data2.units_sold;
		double totalRevenue = data1.revenue + data2.revenue;
		cout << data1.bookNo << " " << totalCnt << " " << totalRevenue << " ";
		if (totalCnt != 0) cout << totalRevenue / totalCnt << endl;
		else cout << "(no sales)" << endl;
		return 0;
	}
	else {
		cerr << "Data must refer to the same ISBN" << endl;
		return -1;
	}

2.6.3 编写自己的头文件

为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应与类的名字一样。例如,库类型string在名为string的头文件中定义。又如,我们应该把Sales_data类定义在名为Sales_data.h的头文件中。

头文件通常包含那些只能被定义一次的实体,如类、const和constexpr变量等。头文件也经常用到其他头文件的功能。例如,我们的sales_data类包含有一个string成员,所以Sales_data.h必须包含string.h头文件。同时,使用sales_data类的程序为了能操作bookNo成员需要再一次包含string.h头文件。这样,事实上使用sales_data类的程序就先后两次包含了string.h头文件:一次是直接包含的,另有一次是随着包含sales_data.h被隐式地包含进来的。有必要在书写头文件时做适当处理,使其遇到多次包含的情况也能安全和正常地工作

预处理器
确保头文件多次包含仍能安全工作的常用技术是预处理器( preprocessor),它由C++
语言从C语言继承而来。预处理器是在编译之前执行的一-段程序,可以部分地改变我们所
写的程序。之前已经用到了一项预处理功能# include,当预处理器看到#include标记
时就会用指定的头文件的内容代替#include.

C++程序还会用到的一项预处理功能是头文件保护符(header guard),头文件保护符依赖于预处理变量。预处理变量有两种状态:已定义和未定义。#define指令把一个名字设定为预处理变量,另外两个指令则分别检查某个指定的预处理变量是否已经定义: #ifdef当且仅当变量已定义时为真,#ifndef当且仅当变量未定义时为真。一旦检查结果为真,则执行后续操作直至遇到#endif指令为止。

#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>

using namespace std;
struct Sales_data {
	string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0;
};
#endif

整个程序中的预处理变量包括头文件保护符必须唯一,通常的做法是基于头文件中类的名字来构建保护符的名字,以确保其唯一性。为了避免与程序中的其他实体发生名字冲突,一般把预处理变量的名字全部大写。

小结

类型是C++编程的基础。

类型规定了其对象的存储要求和所能执行的操作。C++语言提供了一套基础内置类型,如int和char等,这些类型与实现它们的机器硬件密切相关。类型分为非常量和常量,一个常量对象必须初始化,而且一旦初始化其值就不能再改变。此外,还可以定义复合类型,如指针和引用等。复合类型的定义以其他类型为基础。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值