《c++ Primer中文版》第二章:变量和基本类型

目录

第2章  变量和基本类型

2.1基本内置类型

2.1.1算术类型arithmetic type

算术类型分为两类:

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

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.4const限定符

2.4.1const的引用

2.4.2 指针和const

2.4.3顶层 const

2.4.4 constexpr 和常量表达式

2.5 处理类型

2.5.1类型别名 (type alias )

2.5.2auto类型说明符    c++11​​

2.5.3 decltype 类型指示符​​​

2.6自定义数据结构​

2.6.3编写自己的头文件


 

第2章  变量和基本类型

2.1基本内置类型

2.1.1算术类型arithmetic type

算术类型分为两类:

整型(integral type,包括字符和布尔类型在内)和浮点型

 

1.基本的字符类型是char,一个char的空间应确保可以存放机器基本字符集中任意字符对应的数字值。(一个char的大小和一个机器字节一样)
2.扩展字符集: wchar_t类型用于确保可以存放机器最大扩展字符集中的任意一个字符。类型char16_t和char32_ t则为Unicode字符集服务( Unicode是用于表示所有自然语言中字符的标准)。

3.数据类型long long 是在C++11中新定义的。

4.浮点型可表示单精度、双精度和扩展精度值。

5.带符号类型signed和无符号类型unsigned

C++标准并没有规定带符号类型应如何表示,但是约定了在表示范围内正值和负值的量应该平衡。因此,8比特的signed char 理论上应该可以表示-127至127区间内的值,大多数现代计算机将实际的表示范围定为-128至127

6.可寻址的最小内存块称为“字节(byte)"存储的基本单元称为“字(word)"”,它通常由几个字节组成。

C中不同数据类型所占的字节大小,在不同的操作系统和编译器下,是不同的,一般主要说gcc下,32位或64位系统的,红色部分是32和64位系统不同的,做了个表如下:

2.1.2类型转换

1.转换从convert     先看几个例子:

#include <iostream>
int main()
{
   bool b = 42; //b为真
   std::cout << b << std::endl;//1
   int i = b; //i的值为1
   std::cout << b << std::endl;//1
   i = 3.14; //i的值为3
   std::cout << i << std::endl;//3
   double pi = i;//pi的值为3.0
    std::cout << pi << std::endl;//3
   unsigned char c = -1; //假设char占8比特 ,c的值为255、
    std::cout << c << std::endl;
   unsigned char c2 = 255; //假设char占8比特,c2的值是未定义的
    std::cout << c2 << std::endl;

    return 0;
}


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

2.当在程序的某处使用了一种算术类型的值而其实所需的是另一.种类型的值时,编译器同样会执行上述的类型转换。例如,如果我们使用了一个非布尔值作为条件,那么它会被自动地转换成布尔值,这--做法和把非布尔值赋给布尔变量时的操作完全一.样:

#include <iostream>
int main()
{
  int i = 29;
  if(i)
  {
    std::cout << i <<std::endl;
    i = 0;
    std::cout << i <<std::endl;
  }
return 0;

}

如果我们把一个布尔值用在算术表达式里,则它的取值非0即1,所以一般不宜在算术表达式里使用布尔值。


3.含有无符号类型的表达式

#include <iostream>
int main()
{
  unsigned u = 10;
  int i = -42;
  std::cout << i + i << std::endl;//-84
  std::cout << u + i << std::endl;//4294967264

}

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


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

#include <iostream>
int main()
{
  unsigned u1 = 42 , u2 = 10;

  std::cout << u1 - u2 << std::endl;//32
  std::cout << u2 - u1 << std::endl;//4294967264结果是取模后的值
  return 0;
}

5.来看一个例子:

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

解决办法:

练习2.3:

#include <iostream>
int main()
{
   unsigned  u = 10, u2 = 42;
   std::cout << u2 - u << std::endl;//32都是无符号整数
   std::cout << u - u2 << std::endl;//4264967264 得到-32,在表示无符号整数时自动加上了模

   int i = 10, i2 = 42;
   std::cout << i2 - i << std::endl;//32都是整数
   std::cout << i - i2 << std::endl;//-32都是整数
   std::cout << i - u << std::endl;//0 先把带符号数转换成无符号整数,所以结果还是0
   std::cout << u - i << std::endl;//0 同理
   return 0;
}

2.1.3字面量值

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

1.整型和浮点型字面值

20/*十进制*/       024/*八进制*/       0x14  /*十六进制*/

整型字面值具体的数据类型由它的值和符号决定。

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

尽管整型字面值可以存储在带符号数据类型中,但严格来说,十进制字面值不会是负数。如果我们使用了一个形如-42的负十进制字面值,那个负号并不在字面值之内,它的作用仅仅是对字面值取负值而已。

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

2.字符和字符串的字面值

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

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

#include <iostream>
int main()
{
    //分多行书写字符串字面值
    std::cout << "a really , really long string literral "
                   "that spans two lines "<< std::endl;
    return 0;

}

3.转义序列

有两类字符程序员不能直接使用:

一类是不可打印(nonprintable)的字符,如退格或其他控制字符,因为它们没有可视的图符;

另一类是在C+ +语言中有特殊含义的字符(单引号、双引号、问号、反斜线)。

转义序列(escape sequence), 转义序列均以反斜线作为开始,C++语 言规定的转义序列包括:
 

4.指定字面值的类型

L' a'//宽字符型字面值,类型是wchar_ t
u8"hi !"// utf-8字符串字面值(utf-8用8位编码一个Unicode字符)
42ULL//无符号整型字面值,类型是unsigned long long
1E-3F//单精度浮点型字面值,类型是float
3.14159L//扩展精度浮点型字面值,类型是long double


5.字面值和指针字面值c++11

练习2.5:

(a)'a表示字符a, L'a'表示宽字符型字面值a且类型是wchar_ t, "a"表示字符串a,L"a"表示宽字符型字符串a。
(b) 10是一个普通的整数类型字面值,10u 表示一个无符号数, 10L表示一个长整型数,10uL 表示一个无符号长整型数,012 是一个八进制数(对应的十进制数是10),0xC是一个十六进制数(对应的十进制数是12)。
(c)3.14是一个普通的浮点类型字面值,3.14f表示一个float类型的单精度浮点数,3.14L表示一个long double 类型的扩展精度浮点数。(d) 10是一个整数,10u是一个无符号整数,10.是- -个浮点数,10e-2 是

练习2.8:请利用转义序列编写一段程序, 要求先输出2M,然后转到新一行。 修改程序使其先输出2,然后输出制表符,再输出M,最后转到新一行。

#include <iostream>
int main()
{
   std::cout << " 2\x4d\012";  //输出2M。然后换行
   std::cout << "2\tM\n" ;   //输出2 制表符 M 然后换行

   return 0;
}

2.2变量

对C++程序员来说,“变量(variable)”和“对象(object)” 一般可以互换使用。

2.2.1变量定义

定义变量:首先是:类型说明符 type specifier  随后紧跟由一个或多个变量名组成的列表,其中变量名以逗号分隔,最后以分号结束。

列表中每个变量名的类型都由类型说明符指定,定义时还可以为-一个或多个变量赋初值:

#include <iostream>
int main()
{
   int sum = 0, value,  //sum value 和 units_sold 都是int
       units_sold  = 0; //sum 和 units_sold 初值都是0 
   Sales_item item; //itemde 的类型是 Sales_item
   //string 是一种库类型,表示一个可变长的字符序列
   std::string book ("0-201-78345-X"); //book通过一个string的字面值初始化 

   return 0;
}

1.初始值
当对象在创建时获得了一个特定的值,我们说这个对象被初始化( initialized )了.

//正确: price先被定义并赋值,随后被用于初始化discount
double price = 109. 99,discount = price * 0.16;
//正确:调用函数applyDiscount,然后用函数的返回值初始化salePrice
double salePrice = applyDiscount (price, discount) ;

2.初始化列表c++11  list initialization {}

c++初始化的几种形式

int units sold = 0;
int units sold = {0} ;
int units_ sold{0} ;
int units_ sold(0) ;

当用于内置类型的变量时,这种初始化形式有一个重要特点:如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器将报错:  也就是初始化列表,不建议窄化操作

#include <iostream>
int main()
{
	long double ld = 3.1415926536;
	// int a{ld} , b = {ld}; //错误:转换未执行,因为存在丢失的风险
	int c(ld), d = ld; //正确 :转换执行,且确实丢失了那部分

	std::cout << c << " " << d << std::endl;

	return 0;
}

3.默认初始化
如果定义变量时没有指定初值,则变量被默认初始化( default initialized)

如果是内置类型的变量未被显式初始化,它的值由定义的位置决定。

定义于任何函数体之外的变量被初始化为0一种例外情况是,定义在函数体内部的内置类型变量不被初始化(uninitialized)

一个未被初始化的内置类型变量的值是未定义的,如果试图拷贝或以其他形式访问此类值将引发错误。

练习2.10:

对于string类型的变量来说,因为string类型本身接受无参数的初始化方式,所以不论变量定义在函数内还是函数外都被默认初始化为空串。
对于内置类型int来说,变量global_ int 定义在所有函数体之外,根据C++的规定,globaI_ int 默认初始化为0;而变量local_ int定义在main函数的内部,将不被初始化,如果程序试图拷贝或输出未初始化的变量,将遇到一个未定义的奇异值。

#include <iostream>

std::string global_str;
int global_int;

int main()
{
  int local_int;
  std::string local_str;

  std::cout << global_str << std::endl;
  std::cout << global_int << std::endl;
  std::cout << local_int << std::endl;
  std::cout << local_str  << std::endl;

   return 0;
}

 

2.2.2变量声明和定义的关系

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

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

变量声明规定了变量的类型和名字,在这一点上定义与之相同。但是除此之外,定义还申请存储空间,也可能会为变量赋一个初始值。

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

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


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

2.2.3标识符

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

2.2.4名字的作用域

作用域(scope)是程序的一部分, 在其中名字有其特定的含义。C++语言中大多数作用域都以花括号分隔。
同一个名字在不同的作用域中可能指向不同的实体。名字的有效区域始于名字的声明语句,以声明语句所在的作用域末端为结束

#include <iostream>

int main()
{
  int sum = 0;
  //sum用于存放从1到10所有数的和
  for(int val = 1; val <=10; ++val)
    sum += val;  //等价于sum = sum + val;
  std::cout << "Sum of 1 to 10 inclusive is "
                << sum << std::endl;
   return 0;
}

全局作用域 global scope    块作用域block scope

1.嵌套的作用域

内层作用域(inner scope),
外层作用域(outer  scope)

#include <iostream>
//该程序仅用于说明:函数内部不宜定义与全局变量同名的新变量
int reused = 42; // reused 拥有全局作用域
int main()
{
	int unique = 0; // unique 拥有块作用域
	//输出#1:使用全局变量reused;输出 42 0
	std::cout << reused << " " << unique << std::endl;
	int reused = 0; //新建局部变量reused,覆盖了全局变量reused
	//输出#2:使用局部变量reused; 输出0 0
	std::cout << reused << " " << unique << std::endl;
	//输出#3:显式地访问全局变量reused;输出42 0
	std::cout << ::reused << " " << unique << std::endl;
	return 0;
}

输出#2发生在局部变量reused定义之后,此时局部变量reused正在作用域内(inscope),因此第二条输出语句使用的是局部变量reused而非全局变量。


2.3复合类型

复合类型(compound type)是指基于其他类型定义的类型。其中的两种:引用和指针

一条简单的声明语句由一个数据类型和紧随其后的一个变量名列表组成。一条声明语句由 一个 基本数据类型(base type) 和紧随其后的一个声明符(declarator)列表组成。每个声明符命名了一个变量并指定该变量为与基本数据类型有关的某种类型。

2.3.1引用

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


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

refVal = 2; //把2赋给refVal指向的对象,此处即是赋给了ival
int ii = refVal; //与ii = ival执行结果一样

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


定义了一个引用之后,对其进行的所有操作都是在与之绑定的对象上进行的。

为引用赋值,实际上是把值赋给了与引用绑定的对象。获取引用的值,实际上是获取了与引用绑定的对象的值。同理,以引用作为初始值,实际上是以与引用绑定的对象作为初始值:

//正确: refVal3绑定到了那个与refVal绑定的对象上,这里就是绑定到ival上
int &refVal3 = refVal ;
//利用与refVal绑定的对象的值初始化变量i
int i = refVal; //正确: i被初始化为ival的值

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

引用的定义

int i=1024,i2=2048;//i和i2都是int
int &r=i, r2= i2;  //r是一个引用,与i绑定在一起,r2是int
int i3 = 1024,&ri = i3; // i3是int, ri是一个引用,与i3绑定在一起
int &r3 = i3,&r4 = i2;  // r3和r4都是引用
 
int &refVal4 = 10; //错误:引用类型的初始值必须是一个对象
double dval = 3.14;
int &refVal5 = dval ;//错误:此处引用类型的初始值必须是int型对象

练习2.17:

#include <iostream>

int main()
{
    int i , &ri = i;
    i = 0;
    ri = 10;
    std::cout << "i:"<< i <<"  ri:" << ri << std::endl;

   return 0;
}


2.3.2指针

指针(pointer) 是“指向(point to)” 另外一种类型的复合类型。 与引用类似,指针也实现了对其他对象的间接访问。然而指针与引用相比又有很多不同点其一,指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。

其二,指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化也将拥有一个不确定的值。

int *ipl, *ip2; //ipl和ip2都是指向int型对象的指针
double dp,*dp2; // dp2是指向double型对象的指针,dp是double型对象

1.获取对象的地址
取地址符( 操作符&),因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。

double dval;
double *pd = &dval;//正确:初始值是double型对象的地址
double *pd2 = pd;//正确:初始值是指向double对象的指针
int *pi = pd;//错误:指针pi的类型和pd的类型不匹配
pi = &dval;//错误:试图把double型对象的地址赋给int型指针

2.指针值

指针的值(即地址)应属下列4种状态之一:
1.指向一个对象。
2.指向紧邻对象所占空间的下一个位置。
3.空指针,意味着指针没有指向任何对象。
4.无效指针,也就是上述情况之外的其他值。


3.利用指针访问对象
解引用符(操作符*)来访问该对象

4.空指针

空指针(null pointer)不指向任何对象,在试图使用一个指针之前代码可以首先检查它是否为空。以下列出几个生成空指针的方法:

int *pl = nullptr;      //等价于int*p1=0;
int *p2 = 0;   //直接将p2初始化为字面常量0
//需要首先#include cstdlib
int *p3 = NULL;   //等价于int*p3=0;

c++11  nullptr是一种特殊类型的字面值,它可以被转换成第任意其他的指针类型。(c++11最好使用这个)

名为NULL的预处理变量( preprocessor variable) 来给指针赋值,这个变量在头文件cstdlib中定义,它的值就是0


5.赋值和指针

	int i = 42;
	int *pi = 0;  // pi被初始化,但没有指向任何对象
	int *pi2 = &i;  // pi2被初始化,存有i的地址
	int *pi3;  //如果pi3定义于块内,则pi3的值是无法确定的
	pi3 = pi2; // pi3 和pi2指向同一个对象主
	pi2 = 0;  //现在pi2不指向任何对象了

6.其它的指针操作

可以判断

int ival = 1024;
int *pi = 0;   // pi合法,是一个空指针
int *pi2 = &ival;  // pi2是一个合法的指针,存放着ival的地址
if (pi)   // pi的值是0,因此条件的值是false
.....
if (pi2)  // pi2指向ival,因此它的值不是0,条件的值是true
...

== 。!=。这里两个指针存放的地址值相同(两个指针相等)有三种可能:

它们都为空指向同一个对象,或者都指向了同一个对象的下一地址

需要注意的是,一个指针指向某对象,同时另一个指针指向另外对象的下一地址, 此时也有可能出现这两个指针值相同
的情况,即指针相等。

6.void *指针

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

以void*的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象.

练习2.18:编写代码分别更改指针的值以及指针所指对象的值。

#include <iostream>
int main ()
{
   int i=5,j=10;
   int *p = &i;
   std::cout << p <<" "<< *p << std::endl;
   p=&j;
   std::cout << p <<" "<< *p << std::endl;
   *p=20;
   std::cout << p <<" "<< *p << std::endl;
   j=30;
   std::cout << p <<" "<< *p << std::endl;
   return 0;
}

练习2.22:

#include <iostream>
int main ()
{
    int i = 0;
    int *p1 = nullptr;
    int *p = &i;
    if (p1) //检验指针的值(即指针所指对象的地址)
    std::cout << "p1 pass" << std::endl;
    if (p) //检验指针的值(即指针所指对象的地址)
    std::cout << "p pass" << std::endl;
    if (*p) // 检验指针所指对象的值
    std::cout << "i pass" << std::endl;

    return 0;

}

2.3.3理解复合类型的声明

变量的定义包括一个基本数据类型(base type)和一组声明符。

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

1.定义多个变量

int *pl, *p2; //pl和p2都是指向int的指针
int* p1, p2; //pl是指向int的指针,p2是int

坚持一种写法,不要变来变去。

2.指向指针的指针

int ival = 1024;
int *pi = &ival; // pi指向一个int型的数
int **ppi = &pi; // ppi指向一个int型的指针

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

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.4const限定符

const int bufSize = 512;//输入缓冲区大小

使用变量是为了:方便修改。使用const是为了:防止被随意篡改,试图修改就会报错。

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

1.初始化和const

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

const int bufSize = 512; //输入缓冲区大小

编译器将在编译过程中把用到该变量的地方都替换成对应的值。也就是说,编译器会找到代码中所有用到bufSize的地方,然后用512替换。
为了执行上述替换,编译器必须知道变量的初始值。如果程序包含多个文件,则每个用了const对象的文件都必须得能访问到它的初始值才行。就必须在每一个用到变量的文件中都有对它的定义。

为了支持这一用法,同时避免对同一变量的重复定义,默认情况下,const对象被设定为仅在文件内有效。

当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。

解决办法:

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

2.4.1const的引用

对常量的引用reference to const

const int ci = 1024;
const int &r1 = ci;  //正确:引用及其对应的对象都是常量

r1 = 42;  //错误: r1是对常量的引用
int &r2 = ci;  //错误:试图让一个非常量引用指向一个常量对象

 

1.初始化和对const的引用

引用的类型必须与其所引用对象的类型一致, 但是有两个例外。

第一种例外情况就是在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。尤其,允许为一个常量引用绑定非常量的对象、字面值,甚至是个一般表达式:

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;
编译器所做的操作:
const int temp = dval; //由双精度浮点数生成一个临时的整型常量
const int &ri = temp;  //让ri绑定这个临时量

这里引入了temp临时量(temporary):所谓临时量对象就是当编译器需要一个空间来暂存表达式的求值结果时临时创建的一个未命名的对象。

当ri不是常量时,如果执行了类似于上面的初始化过程将带来什么样的后果。如果ri不是常量,就允许对ri赋值,这样就会改变ri所引用对象的值。注意,此时绑定的对象是一个临时量而非dval。程序员既然让ri引用dval,就肯定想通过ri改变dval的值,否则干什么要给ri赋值呢?如此看来,既然大家基本上不会想着把引用绑定到临时量上,C++语言也就把这种行为归为非法


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

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

不允许通过r2修改i的值。i的值仍然允许通过其他途径修改,既可以直接给i赋值,也可以通过像r1一样绑定到!i的其他引用来修改。
 

2.4.2 指针和const

指向常量的指针( pointer to const) 

const double pi = 3.14;// pi是个常量,它的值不能改变
double *ptr = &pi;//错误: ptr是一个普通指针
const double *cptr = &pi;//正确: cptr可以指向一个双精度常量
*cptr = 42;  //错误:不能给*cptr赋值

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


const 指针

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

常量指针(const pointer)必须初始化

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

*pip = 2.72; //错误:pip是一个指向常量的指针
//如果curErr所指的对象(也就是errNumb)的值不为0
if (*curErr) {
errorHandler () ;
*curErr = 0; //正确:把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的部分不影响

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)是指值不会改变并且在编译过程就能得到计算结果的表达式。

字面值属于常量表达式,用常量表达式初始化的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不是常量表达式
            //具体值直到运行时才能获取到,所以也不是常量表达式。

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

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

字面值类型
“字面值类型”(literal type)

到目前为止接触过的数据类型中,算术类型、引用和指针都属于字面值类型。自定义类Sales_ item、I0库、string类型则不属于字面值类型,也就不能被定义成constexpr。
尽管指针和引用都能定义成constexpr, 但它们的初始值却受到严格限制。一个constexpr指针的初始值必须是nullptr或者0,或者是存储于某个固定地址中的对象

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

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

与其他常量指针类似,constexpr 指针既可以指向常量也可以指向一个非常量:

constexpr int *np = nullptr; // np是一个指向整数的常量指针,其值为空
int j= 0;
constexpr int i = 42; // i的类型是整型常量
// i和j都必须定义在函数体之外
constexpr const int *p = &i; // p是常量指针,指向整型常量i
constexpr int *p1 = &j; // pl是常量指针,指向整数j

2.5 处理类型

2.5.1类型别名 (type alias )

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

别名声明(alias declaration) c++11

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

指针、常量和类型别名

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

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

错误:

const char *cstr  = 0;
//是对const pstring cstr的错误理解

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

2.5.2auto类型说明符    c++11

//由vall和val2相加的结果可以推断出item的类型
auto item= va11 + val2:   // item初始化为vall和val2相加的结果
使用auto也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据
类型,所以该语句中所有变量的初始基本数据类型都必须一样:

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

复合类型、常量和auto
编译器推断出来的auto类型有时候和初始值的类型并不完全.样,编译器会适当地改变结果类型使其更符合初始化规则。

int i = 0, &r = i;
auto a = r: //a是一个整数(r是i的别名,而i是一个整数)

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 &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;
#include <iostream>
#include<typeinfo>
int main ()
{
   const int i = 42;
   auto j=i;
   const auto &k =i;
   auto *p = &i;
   const auto j2 = i, &k2= i;
   std::cout << typeid(i).name() << std::endl;
   std::cout << typeid(j).name() << std::endl;
   std::cout << typeid(k).name() << std::endl;
   std::cout << typeid(p).name() << std::endl;
   std::cout << typeid(j2).name() << std::endl;
   std::cout << typeid(k2).name() << std::endl;
   return 0;
}

2.5.3 decltype 类型指示符

有时会遇到这种情况:希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。为了满足这一要求, C++11新标准引入了第二种类型说明符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是一个引用,必须初始化

decltype和引用

// decltype 的结果可以是引用类型
int i= 42,*p=&i,&r=i;
decltype(r + 0) b; // 正确:加法的结果是int,因此b是一个(未初始化的) int
decltype(*p) C;//错误:c是int&,必须初始化

因为r是一个引用,因此decltype (r)的结果是引用类型。如果想让结果类型是r所指的类型,可以把r作为表达式的一部分, 如r+0,显然这个表达式的结果将是一个具体值而非一个引用。

另一方面,如果表达式的内容是解引用操作,则decltype 将得到引用类型。解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。因此,decltype (*p)的结果类型就是int&,而非int。

// decltype 的表达式如果是加上了括号的变量,结果将是引用
decltype( (i) ) d;//错误:d是int&,必须初始化
decltype(i) e;//正确: e是一个(未初始化的) int

如果给变量加上了一层或多层括号,编译器就会把它当成是一个 表达式。变量是一种可以作为赋值语句左值的特殊表达式,所以这样的decltype就会得到引用类型。

int a = 3, b = 4;
decltype(a) c = a;
decltype (a = b) d = a;

根据decltype的上述性质可知,c的类型是int,值为3;表达式a=b作为decltype的参数,编译器分析表达式并得到它的类型作为d的推断类型,但是不实际计算该表达式,所以a的值不发生改变,仍然是3; d的类型是int&,d是a的别名,值是3; b的值一直没有发生改变,为4。
 

auto和decltype的区别主要有三个方面:
第一,auto类型说明符用编译器计算变量的初始值来推断其类型,而decltype虽然也让编译器分析表达式并得到它的类型,但是不实际计算表达式的值。
第二,编译器推断出来的auto类型有时候和初始值的类型并不完全一"样, 编译器会适当地改变结果类型使其更符合初始化规则。例如,auto一般会忽略掉顶层const,而把底层const保留下来。与之相反,decltype会保留变量的顶层const。
第三,与auto不同,decltype 的结果类型与表达式形式密切相关,如果变量名加上了一对括号,则得到的类型与不加括号时会有不同。如果decltype使用的是一个不加括号的变量,则得到的结果就是该变量的类型:如果给变量加上了一层或多层括号,则编译器将推断得到引用类型。

#include <iostream>
#include<typeinfo>
int main ()
{
	int a = 3;
	auto c1 = a;
	decltype(a) c2 = a;
	decltype((a)) c3 = a;

	const int d = 5;
	auto f1 = d;
	decltype(d) f2 = d;

   std::cout << typeid(c1).name() << std::endl;
   std::cout << typeid(c2).name() << std::endl;
   std::cout << typeid(c3).name() << std::endl;
   std::cout << typeid(f1).name() << std::endl;
   std::cout << typeid(f2).name() << std::endl;

   c1++;
   c2++;
   c3++;
   f1++;
   // f2++;//错误
   std::cout << a << " " << c1 << " " << c2 << " " << c3 << " "
	   << f1 << " " << f2 << std::endl;
   return 0;
}

2.6自定义数据结构

C++11新标准规定,可以为数据成员提供一个类内初始值( in-class initializer)。 创建对象时,类内初始值将用于初始化数据成员。没有初始值的成员将被默认初始化。因此当定义Sales_ data的对象时,units_ sold 和revenue都将初始化为0,bookNo 将初始化为空字符串。

2.6.3编写自己的头文件

头文件保护符(header guard )

确保头文件多次包含仍能安全工作的常用技术是预处理器(preprocessor)

#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
struct Sales_data {
  std::string bookNo;
  unsigned units_ sold = 0;
  double revenue = 0.0;
};
#endif

练习2.41 2.42:答案上册p37 p40

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值