第2章 变量和基本类型

第2章 变量和基本类型

本章介绍了Cpp的几种典型数据类型,它们分别是:

  • 基本内置类型
  • 复合类型
  • 自定义数据结构

其中,char、 int、 long、 float、 double、 bool 是最常见的基本内置类型;

引用和指针是两种最重要的复合类型;struct 关键字和 class 关键字则常用于声明用户自定义的数据结构。

2.1 基本内置类型

基本数据类型主要有算术类型和空类型(void):

  • 算数类型:字符、整数型、布尔值、浮点数。
  • 空类型:不对应具体值,例如:函数不返回任何值时使用空类型作为返回类型。

2.1.1 算术类型

算术类型可分两类:

  • 整型(integral type ,包括字符和布尔类型);
  • 浮点型。

尺寸:该类型数据所占的比特数

表 2.1 列出了 Cpp 标准规定的尺寸的最小值,同时允许编译器赋予这些类型更大的尺寸。

1701228197022

Cpp语言规定一个 int 至少和一个 short 一样大,一个 long 至少和一个 int 一样大,一个 long long 至少和一个 long 一样大。(long long 是在 Cpp11 中新定义的)

可寻址的最小内存块称为“字节”。

存储的基本单元称为“字(word)”

字由32比特(4字节)或64比特(8字节)构成。即 4字节=字32比特;8字节=字64比特

关系:1个字 = 4字节 = 32比特

在一个字节为8比特,字为32比特的机器上,(其实就是4字节)一个字的内存区域如下:

1701228880409

其中,左侧是字节的地址,右侧是字节中8比特的具体内容。


浮点型可分为:

  • 单精度;
  • 双精度;
  • 扩展精度值。

通常:

  • float = 1个字(32比特)
  • double = 2个字(64比特)
  • long double = 3个字(96比特)或者4个字(128比特)

带符号类型和无符号类型

  • 带符号类型(signed):可以表示整数、负数或者0
    • int、short、long、long long 都是带符号的
  • 无符号类型(unsigned):仅能表示大于等于0的值
    • 在其他类型前面添加 unsigned 就可以的到无符号类型

建议:如何选择类型

  • 当明确知晓数值不可能为负时,选用无符号类型;
  • 使用 int 执行整数运算。在实际应用中,long 一般和 int 有一样的尺寸。如果你的数值超过了 int 的表示范围,选用 long long。
  • 在算术表达式中不要使用 char 和 bool,只有在存放字符或布尔值时才使用它们。如果使用一个不大的整数,请明确指定它的类型是 signed char 或者 unsigned char。
  • 执行浮点数运算选用 double,这是因为 float 通常精度不够而且双精度浮点数和单精度浮点数的计算代价相差无几。

2.1.1 练习

练习 2.1: 类型 int、long、long long、short的区别是什么?无符号类型和带符号类型的区别是什么?float 和 double 的区别是什么?

解答:

在 Cpp 语言中,int、long、long long、short 都属于整型,区别是 Cpp 标准规定的尺寸的最小值(即改类型在内存中所占的比特数)不同。其中, short 是短整型,占 16 位;int 是整型,占16位;long 和 long long 均为长整型,分别占 32 位和 64 位。

大多数整型都可以划分为无符号类型和带符号类型,在无符号类型中所有比特都用来存储数值,但是仅能表示大于等于 0 的值;带符号类型可以表示整数、负数或者 0。

float 和 double 分别是单精度浮点数和双精度浮点数,区别主要是内存中所占的比特数不同,以及默认规定的有效位数不同。

练习 2.2: 计算按揭贷款时,对于利率、本金、付款分别应选择何种数据类型?说明你的理由。

解答:

利率、本金、付款既有可能是整数,也有可能是普通的实数。因此应该选择一种浮点类型来表示。float、double、long double,其中 double 和float 的计算代价比较接近且表示范围更广,long double 的计算代价则相对较大,一般情况下没有选择的必要。因此,选择 double 是比较恰当的。

2.1.2 类型转换

将对象从一种给定的类型转换为另一种相关类型。

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

无符号数不会小于 0 这一事实同样关系到循环的写法。

for(int i = 10; i >= 0; --i)
    std::cout << i << std::endl;
// 可能你会觉得反正也不会输出负数,打算用无符号数来重写这个循环。
// 但是,这个改变却会导致死循环:
// 错误:变量 u 永远也不会小于 0,循环条件一直成立
for(unsigned u = 10; u >= 0; --u)
    std::cout << u << std::endl;

当 u 等于 0 时会发生什么,这次迭代输出 0 ,然后继续执行 for 语句里的表达时。表达式 --u 从 u 当中减去 1 ,得到的结果 -1 并不满足无符号数的要求,此时像所有表达式范围之外的其他数字一样, -1 被自动转换为一个合法的无符号数。假设 int 类型占 32 位,则当 u 等于 0 时, --u 的结果将会是 4294967295。

解决办法是:用 while 循环代替 for 语句

unsigned u = 11;	// 确定要输出的最大数,从比它大 1 的数开始
while(u > 0) {
    --u;			// 先减 1,这样最后一次迭代时就会输出 0
    std::cout << u << std::endl;
}

改写后的循环先执行对循环控制变量减 1 操作,这样最后一次迭代时,进入循环的 u 值为 1。此时将其减 1,则这次迭代输出的数就是 0:下一次在检验循环条件时,u 的值等于 0 而无法在进入循环。因为我们要先做减 1 的操作,所以初始化 u 的值应该比要输出的最大值大 1。这里,u 的初始化为 11,输出的最大数为10。

2.1.3 字面值常量

浮点型字面值表现为一个小数或以科学计数法表示的指数,其中指数部分用 E 和 e 标识:

3.14159 3.14159E0 0. 0e0 .001

字符和字符串字面值

单引号括起来的一个字符称为 char 型字面值,双引号括起来的零个或多个字符则构成字符串型字面值。

'a'				// 字符字面值
"Hello World!"	// 字符串字面值

字符串字面值的类型实际上是由常量字符构成的数组(array)。

编译器在每个字符串的结尾处添加一个空字符('\0'),因此,字符串字面值的实际长度要比它的内容多 1。

例如:字面值 'A'表示的就是单独的字符 A,而字符串 "A"则代表了一个字符的数组,包含字母 A、另一个是空字符。

如果两个字符串字面值位置紧邻且仅由空格、缩进和换行符分隔,则它们实际上是一个整体。当书写的字符串字面值比较长,写在一行不太合适时,就可以采取分开书写的方式:

// 分多行书写的字符串字面值
std::cout << "a really, really long string literal "
    		 "that spans two lines" << std::endl;

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

2.2 变量

2.2.1 变量定义

何为对象?

通常情况下,对象是指一块能存储数据并具有某种类型的内存空间。

初始化不等于赋值,注意区分!

初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来替代。

2.2.2 变量声明和定义的关系

如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显式地初始化变量:

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

但是如果给 extern 语句赋一个初值就变成定义了,这么做就抵消了 extern 的作用。

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

关键概念:静态类型

Cpp 是一种静态类型 (statically typed) 语言,其含义是在编译阶段检查类型。其中,检查类型的过程称为类型检查 (type checking)。

2.3 复合类型

2.3.1 引用

当我们使用术语“引用”时,指的其实是“左值引用”

  • 引用:就是给某个对象起别名,将声明符写成 &d 的形式来定义引用类型,d 是声明的变量名;
  • 一般初始化变量时,初始值会被拷贝到新建的对象中。然而定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用;
  • 一旦绑定便无法更改,因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。
  • 为引用赋值,实际上是把值赋给了与引用绑定的对象,获取引用的值,实际上是获取了与引用绑定的对象的值;
  • 引用本身不是一个对象,所以不能定义引用的引用。
int ival = 1024;
int &refVal = ival;		//refVal指向ival(是ival的另一个名字)
int &refVal2;			//报错:引用必须被初始化
refVal = 2;				//把2赋给refVal指向的对象,此处即是赋给了ival
int ii = refVal;		//与ii = ival执行结果一样
int &refVal3 = refVal;	//正确:refVal绑定到了那个与refVal绑定的对象上,这里就是绑定到ival上
int i = refVal;			//正确:i被初始化为ival的值
  • 引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起
  • 引用的类型要和与之绑定的对象严格匹配
int &refVal4 = 10;	 //错误:引用类型的初始值必须是一个对象
double dval = 3.14;
int &refVal5 = dval; //错误:此处引用类型的初始值必须是int型对象

引用的类型要和与之绑定的对象严格匹配,但有两种例外情况:

  1. 在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。
  2. 允许为一个常量引用绑定非常量的对象、字面值,甚至是个一般表达式。
int i = 42;
const int &r1 = i;//允许将const int& 绑定到一个普通int对象上
const int &r2 = 42;  //正确:r1是一个常量引用
const int &r3 = r1*2;//正确:r3是一个常量引用
int &r4 = r1*2;      //正确:r4是一个普通的非常量引用

2.3.2 指针

与引用类似,指针也实现了对其他对象的间接访问。

  • 定义指针类型的方法将声明符写成 *d 的形式,d是变量名。如果一条语句中定义了几个指针变量,每个变量前面都必须有符号 *
  • 指针本身就是一个对象,允许对指针赋值和拷贝,在指针的声明周期内它可以先后指向几个不用的对象;
  • 指针无须再定义时赋初值,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
int *ip1, *ip2;		//ip1和ip2都是直指向int型对象的指针
double dp, *dp2;	//dp2是指向double型对象的指针,dp是double型对象

除了有两种情况可以例外,其他所有指针的类型都要和它所指向的对象严格匹配:

double dval;
double *pd = &dval;//正确:初始值是double型对象的地址
double *pd2 = pd;  //正确:初始值是指向double对象的指针

int *pi = pd;	   //错误:指针pi的类型和pd的类型不匹配
pi = &dval;		   //错误:试图把double型对象的地址赋给int型指针

因为在声明语句中指针的类型实际上被用于指定它所指向对象的类型,所以二者必须匹配。如果指针指向了一个其他类型的对象,对该对象的操作将发生错误。

第一种例外情况:允许令一个指向常量的指针指向一个非常量对象:

const double pi = 3.14;
const double *cptr = &pi;
double dval = 3.14;//dval是一个双精度浮点数,它的值可以改变
cptr = &dval;		//正确:但是不能通过cptr改变dval的值

指针值应属下列4种状态之一:

  1. 指向一个对象;
  2. 指向紧邻对象所占空间的下一个位置;
  3. 空指针,意味着指针没有指向任何对象;
  4. 无效指针,也就是上述情况之外的其他值。

对指针解引用会得出所指的对象,因此如果给解引用的结果赋值,实际上也就是给指针所指的对象赋值:

*p = 0;	  //由符号*得到指针p所指的对象,即可经由p为变量ival赋值
cout << *p; //输出0

要搞清楚一条赋值语句到底改变了指针的值还是改变了指针所指对象的值不太容易,最好的办法就是记住赋值永远改变的是等号左侧的对象

pi = &ival;  //pi的值被改变,现在pi指向了ival
//意思是为pi赋一个新的值,也就是改变了那个存放在pi内的地址值。

*pi = 0;	 //ival的值被改变,指针pi并没有改变
//则*pi(也就是指针pi指向的那个对象)发生改变。

指针相等有以下三种情况:

  1. 都为空;
  2. 都指向同一个对象;
  3. 都指向了同一个对象的下一地址。

void*指针是一种特殊的指针类型,可用于存放任意对象的地址。

指向指针的引用

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

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

面对一条比较复杂的指针或引用的声明语句时,从右向左阅读有助于弄清楚它的真实含义。

int *&r = p;

离变量名最近的符号(此例中式&r的符号&)对变量的类型有最直接的影响,因此r是一个引用。声明符的其余部分用以确定r引用的类型是什么,此例中的符号*说明r引用的是一个指针。

2.4 const限定符

const 对象一旦创建后其值就不能在改变,所以 const 对象必须初始化。

const int i = get_size();//正确:运行时初始化
const int j = 42;		 //正确:编译时初始化
const int k;			 //错误:k是一个未经初始化的常量

初始化和 const

如果利用一个对象去初始化另外一个对象,则它们是不是const都无关紧要:

int i = 42;
const int ci = i;	//正确:i的值被拷贝给了ci
int j = ci;			//正确:ci的值被拷贝给了j

尽管 ci 是整形常量,但无论如何 ci 中的值还是一个整型数。ci 的常量特征仅仅在执行改变 ci 的操作时才会发挥作用。当用 ci 去初始化j时,根本无须在意 ci 是不是一个常量。拷贝一个对象的值并不会改变它,一旦拷贝完成,新的对象就和原来的对象没什么关系了。

默认const对象仅在文件内有效

默认情况下,const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。

如果我们不希望编译器为每个文件分别生成独立的变量,想要只在一个文件中定义const,而在其他多个文件中声明并使用它。

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

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

Note:如果想在多个文件之间共享 const 对象,必须在变量的定义之前添加 extern 关键字。

2.4.1 const 的引用

对常量的引用不能被用作修改它所绑定的对象:

const int ci = 1024;
const int &r1 = ci;  //正确:引用及其对应的对象都是常量
r1 = 42;		   	 //错误:r1是对常量的引用
int &r2 = ci;	   	 //错误:试图让一个非常量引用指向一个常量对象

因为不允许直接为 ci 赋值,当然也就不能通过引用去改变ci。因此,对 r2 的初始化是错误的。假设该初始化合法,则可以通过 r2 来改变它引用对象的值,这显然是不正确的。

术语:常量引用是对 const 的引用

初始化和对const的引用

引用的类型要和与之绑定的对象严格匹配,但有两种例外情况:

  1. 在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。
  2. 允许为一个常量引用绑定非常量的对象、字面值,甚至是个一般表达式。
int i = 42;
const int &r1 = i;	 //允许将const int& 绑定到一个普通int对象上
const int &r2 = 42;  //正确:r1是一个常量引用
const int &r3 = r1*2;//正确:r3是一个常量引用
int &r4 = r1*2;      //正确:r4是一个普通的非常量引用

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

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

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

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

在这种情况下,ri 绑定了一个临时量对象。对临时变量操作是被归为非法的,因为对临时变量的操作并不会影响 dval ,这并没有什么意义。

对const的引用可能引用一个并非const的对象

int i = 42;
int &r1 = i;		//引用ri绑定对象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 double pi = 3.14;		//pi是个常量,它的值不能改变
double *ptr = &pi;			//错误:ptr是一个普通指针
const double *cptr = &pi;	//正确:cptr可以指向一个双精度常量
*cptr = 42;					//错误:不能给*cptr赋值

前面提到,指针的类型必须与其所指对象的类型一致,但是有两个例外情况:

第一种例外情况:允许令一个指向常量的指针指向一个非常量对象:

const double pi = 3.14;
const double *cptr = &pi;
double dval = 3.14;			//dval是一个双精度浮点数,它的值可以改变
cptr = &dval;				//正确:但是不能通过cptr改变dval的值

和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变。

Tip:可以这样想,所谓指向常量的指针或引用,不过是指针或引用 “自以为是” 罢了,它们觉得自己指向了常量,所以自觉地不去改变所指对象地值。

const指针

指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定位常量。

常量指针必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了。

*放在 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 是一个常量指针。指针存储的那个地址不能改变,但是指向的一个一般的非常量整数,那么就完全可以用curErr去修改 errNumb 的值。

*pip = 2.72;	//错误:pip是一个指向常量的指针
//如果curErr所指的对象(也就是errNumb)的值不为0
if(*curErr) {
    errorHandler();
    *curErr = 0;//正确:把curErr所指的对象的值重置
}

2.4.3 顶层 const

指针本身是不是常量以及指针所指的是不是一个常量就是两个相互独立的问题。

  • 顶层const(top-level const)表示指针本身是个常量;

    • 顶层const可以表示任意的对象是常量,这一点对任何数据类型都适用,如算术类型、类、指针等。
  • 底层const(low-level const)表示指针所指的对象是一个常量。

    • 底层const则与指针和引用等符合类型的基本类型部分有关。

特殊的是:指针类型既可以是顶层const也可以是底层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

当执行对象的拷贝操作时,常量是顶层const还是底层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)是指值不会改变并且在编译过程就能得到计算结果的表达式。

属于常量表达式的:

  1. 字面值;
  2. 用常量表达式初始化的 const 对象也是常量表达式。
const int max_files = 20;		//max_files是常量表达式
const int limit = max_files + 1;//limit是常量表达式
int staff_size = 27;			//staff_size不是常量表达式
const int sz = get_size();		//sz不是常量表达式

尽管 sz 本身是一个常量,但它地具体值直到运行时才能获取到,所以也不是常量表达式。

constexpr 常量

Cpp11 新标准规定,允许将声明变量声明为 constexpr 类型以便由编译器来验证变量地值是否是一个常量表达式。

constexpr int mf = 20;		 // 20 是常量表达式
constexpr int limit = mf + 1;// mf+1 是常量表达式
constexpr int sz = size();	 //只有size是一个constexpr函数时才是一条正确的声明语句

一般来说,如果你认定变量是一个常量表达式,那就把它声明成 constexpr 类型。

字面值类型

字面值类型指的是:类型简单、值也显而易见、容易得到,就把它们称为“字面值类型”。

比如:

  • 属于字面值类型的:算术类型、引用、指针
  • 不属于字面值类型的:自定义类、IO 库、string 类型

指针和引用都能定义成 constexpr ,但它们的初始值都受到严格限制。一个 constexpr 指针的初始值必须是 nullptr 或者 0,或者是存储与某个固定地址中的对象。

指针和 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* 的同义词

新规定,使用别名声明:

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

类型别名和类型的名字等价,只要是类型的名字能出现的地方,就能使用类型别名:

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

指针、常量和类型别名

书中主要讲了在C++中使用类型别名时可能出现的误解,特别是涉及到指针、常量和类型别名组合的情况。 不可以简单地将类型别名替换为其原始类型

让我逐步解释一下:

  1. 类型别名的定义:

    typedef char *pstring;
    

    这里定义了一个类型别名 pstring,它是 char* 的别名,即指向字符的指针。

  2. 使用类型别名和常量的声明:

    const pstring cstr = 0;
    

    这一行声明了一个常量指针 cstr,它指向字符 (char),并被声明为常量。但是,由于 pstringchar* 的别名,这实际上是一个指向字符的常量指针。

  3. 使用类型别名和指针的声明:

    const pstring *ps;
    

    这一行声明了一个指针 ps,它指向的对象是指向字符的常量指针。

  4. 错误的理解:
    作者指出,在阅读这样的声明时,人们可能错误地尝试将类型别名替换为其原始类型,以理解声明的含义。例如,尝试将 const pstring cstr 理解为 const char *cstr。这是错误的,因为 const pstring 表示指向字符的常量指针,而不是指向常量字符的指针。

  5. 改写示例:

    const char *cstr = 0;
    

    这是错误理解的改写,它将 const pstring cstr 错误地理解为 const char *cstr。这样改写的结果是声明了一个指向常量字符的指针,而不是指向字符的常量指针。这两种声明的含义是截然不同的。

  6. 正确的改写应该是:

    const char *const cstr = 0;
    

    这表示 cstr 是一个指向字符的常量指针,并且它本身也是一个常量。这样的改写保持了原始声明的含义,即 cstr 是一个指向字符的常量指针。

总的来说,这段话强调了在使用类型别名时,特别是涉及到指针和常量的情况下,需要正确理解类型别名的含义,而不能简单地将其替换为原始类型。这有助于避免在代码理解上的混淆和错误。

2.5.2 auto 类型说明符

auto 可在一条语句中声明多个变量,但是初始基本数据类型都必须一样:

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

复合类型、常量、auto

auto 一般会忽略掉顶层 const,同时底层 const 则会保留下来,比如当初始值是一个指向常量的指针时:

const int ci = i, &cr = ci;
auto b = ci;		// b 是一个整数(ci 的顶层const 特性被忽略掉了)
auto c = cr;		// c 是一个整数(cr 是 ci 的别名,ci 本身是一个顶层 const)
auto d = &i;		// d 是一个整型指针(整数的地址就是指向整数的指针)
auto e = &ci;		// e 是一个指向整数常量的指针(对常量对象取地址是一种底层 const)

如果希望推断出的 auto 类型是一个顶层 const,需要明确指出:

const auto f = ci;	// ci 的推演类型是 int,f 是 const int

还可以将引用的类型设为 auto,此时原来的初始化规则仍然适用:

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

设置一个类型为auto的引用时,初始值中的顶层常量属性仍然保留。和往常一样,如果我们给初始值绑定一个引用,则此时的常量就不是顶层常量了。
要在一条语句中定义多个变量,切记,符号 &* 只从属于某个声明符,而非基本数据类型的一部分,因此初始值必须是同一种类型:

auto k = ci, &l = i;	// k 是整数,1 是整型引用
auto &m = ci,*p = &ci; 	// m 是对整型常量的引用,p 是指向整型常量的指针
						// 错误: i 的类型是 int 而 &ci 的类型是 const int
auto &n = i, *p2 = &ci;

2.5.3 decltype 类型指示符

Cpp11 新标准引入的第二种类型说明符 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;			// x 的类型是 const int
decltype(cj) y = xi;		// y 的类型是 const int&,y 绑定到变量 x
decltype(cj) z;				// 错误: z 是一个引用,必须初始化

小结

类型是 Cpp 编程的基础。

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

  • 19
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

霜晨月c

谢谢老板地打赏~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值