C++零基础到精通1:C++类型系统基础

第2章 C++类型系统基础

本文作者:黄邦勇帅(原名:黄勇),QQ:42444472 (读者意见可发至QQ)
本系列文章是对《C++语法详解》的增补版本,涵盖C++20的内容,本文参考ISO/IEC 14882 第6版(2020-12)。

本文是粗稿以后有更改的可能性且由于本人能力有限,文中难免有错漏之处,望广大读者指出更正,不胜感激
本文为原创文章,转载请注明出处,并注明转载自“黄邦勇帅(原名:黄勇)”,本文作者拥有完全版权


理解类型和对象的概念对学习好C++具有非常重要的作用,是学习并理解C++的重要基础,本章内容对学习C++有重要影响。

2.1 类型、内存、对象、左值和右值

2.1.1 基本概念

1、比特:一个二进制数,即0或1,被称为一位或一比特(bit)或一比特位,其单位为b,比如0110共有4个比特(或4位),即4b

2、字节:计算机通常以一个字节(Byte)的长度(通常为8位)为单位来存储数据,单位为B,即1B=8b

3、不同类型的数据需要使用不同的内存量来存储,使用的内存量越大,可表示的该类型的数值范围就越大。某类型数据所占的比特数(或内存量)被称为该类型的尺寸、长度、宽度或容量,很明显,尺寸越大,能表示的数值就越大,或者说取值范围就越大。

2.1.2 类型和对象

1、数据的类型

  • 1)、数据类型就是数据的类型,是对数据的一种分类。这在数学中见过很多,比如数学就有复数、整数、小数、分数、实数等分类,每一种其实就是不同的数据类型,当然C++划分数据类型的方法与数学不同,其中常见的几种类型是整型,如3、4、6等;浮点型,如3.4、4.6、7.8等;字符型,如’a’、‘b’、'e’等。注:C++的字符数据需使用单引号括起来。
  • 2)、数据自身的类型决定了数据以怎样的方式转换为二进制串(可将此步骤称为编码),比如,浮点型数据通常按照IEEE754的标准将其转换为相应的二进制串,整型数据通常直接转换为二进制串,英文字符通常按ASCII字符集的标准将其转换为二进制串等等。

2、内存空间的类型

  • 1)、内存中存储的是二进制数字(即0或者1),这些二进制数字对于计算机来讲,它并不知道代表什么意义,只有当我们决定如何解释这些二进制数字时才有意义,解释的规则是根据该二进制串的类型进行解释,比如,一串二进制数

    0100 0001

    若按整型类型解释该二进制串,则为整数65;若按字符型类型解释,则为字符’A’。除指定类型外,每种类型还需要一个指定的长度,比如,若以上二进制串以每4位为长度按整型进行解释,则会被解释为两个整数4和1,若按每8位按整型解释,则会被解释为65。

  • 2)、由此可见,若为内存空间指定一种类型并规定该类型的长度,则计算机便能读取并解释存储在内存空间中的二进制串了(可将此步骤理解为解码),所以,类型是相当重要的,它不但决定了怎样解释二进制串,还决定了内存空间的长度。

3、存储和读取数据的方法
由数据的类型和内存空间的类型可知,只需在二者之间制定一个相应的标准,就能保证正确的存储和读取数据,比如,将英文字符按ASCII字符集的标准将其编码为二进制串进行存储,在读取时再按照ASCII的标准进行相应的解码便能正确读取该字符;再如,将浮点数按照IEEE754的标准编码为二进制串进行存储,在读取时再按照IEEE754的标准解码该二进制串便能正确读取该浮点数。

4、类型的划分方法
C++将整数划分为整型,将浮点数划分为浮点型,将使用单引号括起来的字符划分为字符型。类似的,C++使用int、long、short等符号将内存空间划分为整型并规定了他们的长度,因此,这些整型的差别是内存空间的长度不相同;使用float、double将内存空间划分为浮点型并规定了他们的长度。此处仅列举一部分常见的类型,实际C++有更多的类型,详见后文。

5、数据的取值范围
从前面的讲解可知,分配的内存空间容量越大,可表示的该类型的数值范围就越大,或者说取值范围就越大。

示例2.1 数据类型与取值范围
假设int型使用16位内存来存储。若是有符号int型,则使用最高位表示符号位,当最高位为1时表示负数,为0时表示正数;若是无符号int型,则不使用符号位,即所有位都表示数值。试计算有符号int和无符号int的取值范围。
无符号int型的取值范围是0000 0000 0000 0000 ~ 1111 1111 1111 11112,即0 ~ 65535
有符号int型的取值范围是1000 0000 0000 0000 ~ 0111 1111 1111 11112,即−32678 ~ 32677
可以看到,无符号整型可以表示更大的整数,但不能表示负数。

6、对象和变量的概念及内存空间的分配

  • 1)、通常情况下,应先分配内存空间再将数据存入内存中,所以,分配的内存容量需要足以容纳下需要存储的数据。但是C++在存储数据时,不但需要先指定期望分配的内存空间,还需要先指定期望分配的内存空间的类型,因此,对于C++而言,期望分配的内存容量不但需要足以容纳下需要存储的数据,还需要保证数据本身的类型与分配的内存空间的类型相兼容,否则有可能产生错误。基于此,二者之间存在一个映射关系,即数据期望(或指定)的内存空间,与实际上分配的内存空间之间的映射,前者被称为对象。如下图
    在这里插入图片描述

    因此,对象指的是具有某种类型的内存空间,说简单一点,对象就是指的内存空间,只是该内存空间具有某种类型而已。通常情况下,编译器会为对象实际分配内存空间,虽然有可能会因实际的内存不足而导致分配失败或者因其他原因而实际未分配内存空间,但我们仍认为为对象分配了内存空间,所以,本文所指的对象通常意味着已为其分配了内存空间,或者说对象就是指的内存空间(具有某种类型,且无论是否实际分配内存空间)。

  • 2)、为便于访问(或使用、引用)创建的对象,我们需要为对象取一个名称,这个名称被称为变量,有了名称之后,就可以使用该名称方便的操作该内存空间了,若使用内存的地址编号来访问内存单元是非常不方便和不易理解的。因此,变量是命名后的对象。

7、对象表示和值表示

  • 1)、对象表示:为对象分配的实际内存空间可以按字节连续分配,也可以不连续分配,这被称为对象表示。比如,对于int型(整型)需要至少16位(2字节)来存储,但怎样分配这2字节的内存空间呢?通常情况下,使用连续的2字节内存来存储int型,当然,也可以将int的2字节分别分配到彼此不相邻的位置。
  • 2)、值表示:内存中实际存储的二进制比特串也有多种表示形式,比如数字65,可以将该数转换为二进制数后直接存储为二进制串0100 00012 ,也可以存储为其他二进字串,比如,将该数经过某种加密算法后成为1011 11002 ,二进制在内存中的存储形式被称为对象的值表示。

示例2.2:理解类型和对象
存储数字65
①、将数据分类:这是一个整数,可以将其分类为整型。整型被直接转换为相应的二进制串进行存储,因此,需存储的二进制串为1011 1100 。
②、选择合适的内存空间类型:C++规定了多种情形的整型,其中int型使用最少16位来存储,long型最少使用32位来存储。由于数字65比较小,使用16位来存储已经足够了,所以,我们决定使用int型来存储该数字。
③、命名对象:为便于使用创建的对象,我们将该对象命名为a。
④、将数据存入内存空间:C++使用赋值运算符(最简单的赋值运算符是等号“=”)将数据存储到内存空间中。
⑤、最终,我们期望分配一个16位的内存空间,其类型为int,并将创建的对象命名为a,并使用等号将数据存储到为对象分配的内存空间中。
⑥、于是,我们可以使用以下语句来创建一个名称为a的对象,并将数字65存储到为该对象分配的内存空间中 int
a=65; //创建一个名为a的对象,并将数字65存入为该对象分配的内存空间中
⑦、下图为具体的存储过程,其中,假设为对象分配的实际内存空间为按字节连续分配(即对象表示),其值表示为0000 0000 0100
0001 。
在这里插入图片描述

8、总结
可见,类型是相当重要的,以下为类型的作用:

  • 1)、类型决定了怎样解释二进制串。
  • 2)、类型决定了可把什么数据赋给对象,比如整数3可以赋给int型(即整型)对象等。不能为对象赋予一个类型错误的值,编译器会记录每个对象的类型,并确认对它进行的操作是否与类型相一致。
  • 3)、类型决定了可以对该对象进行什么样的操作(或运算)以及操作的意义,也就是说,类型决定了对象的使用方式。比如可对整型变量进行加、减、乘、除等操作,不能对指针变量进行乘、除操作等,再如a + b,如果a和b的类型都是整型,则可执行普通的加法运算,但是,若a和b是一个类类型,则就不能把a + b理解为普通的加法运算,此时,需视该类类型对加法赋予的意义而定(详见操作符重载章节内容)。
  • 4)、类型还决定了对象的内存大小、布局和取值范围
2.1.3 左值和右值

1、左值原是指可以位于赋值语句左边的东西。左值和右值主要是为了区分是否可以对变量进行赋值(当然并不全是这样,但主要目的是这样)

2、右值:指的是为对象分配的内存单元中的数据值,也就是说右值表示的是一个值,因为右值是一个值,因此不能被改变,用户也不能对其进行寻址(即右值不存在内存地址,不可以使用&运算符提取右值的内存地址)。比如,数字3、4、5.6等是右值,是不可更改的一个值,也不存在内存地址。

3、左值可以出现在赋值语句的左边或者右边,但右值只能出现在赋值语句的右边,不能出现在赋值语句的左边。比如

6 = 7;

是没有意义的,因为6是右值,是不能更改的。

4、左值在C89标准文件中的定义
左值是指示对象的表达式,其类型是一种对象类型或除void外的不完整类型。从该定义可得出以下结论:

  • 1)、左值是除某些类型外的一个对象。这意味着左值实际上指的是存储数据的内存单元(除某些类型外),因此左值是一个可被寻址的对象(即左值拥有内存地址,可以使用&运算符提取左值的内存地址),也可以从左值读取一个值。因此在某种程度上左值与对象是相同的意思。

  • 2)、左值是一个表达式,不是一个值,也不是实体。也就是说,左值只适用于表达式,只有在存在表达式的地方才能被称为左值。比如

    int a=3;
    int b=a;
    第一个语句中的a不是左值,而是一个变量,3是一个右值。第二个语句中的a是左值,并且会进行从左值到右值的转换。

5、因为变量是命名的对象,因此变量指示了一个对象,再因为一个单独的变量也是一个表达式,因此一个单独的变量就是左值。

6、左值到右值的转换
在C++中一个变量既可以出现在赋值语句的左边,又可以出现在赋值语句的右边,在这种情况下就存在一个从左值到右值的转换问题,比如(假设a是一个变量)

a=a+1;

对于这种情况,变量a在等号的左边还是右边的作用就很不一样,变量a表示的是变量a所指的内存单元,是左值,但在右边,应使用的是变量a所指内存中的数据值,这时,就需要把右侧变量a的值从内存中提取出来,这一步骤就是从左值到右值的转换,这种转换通常是隐式进行的,因此语句a=a+1;的结果就是,将右边的变量a所指的内存单元中的数据值加1之后,再写入左边变量a所指的内存单元中,最终,将变量a所指的内存单元中存储的值增加1。

7、C89标准对左值到右值转换的规定
除作为sizeof、&、. 、–、++运算符的操作数或赋值运算符的左操作数外,不是数组类型的左值总是被转换为存储在所指示的对象中的值,于是不再成为左值。若该左值是限定类型的,则该值的类型是该左值类型的非限定形式;否则该值的类型就取左值的类型;若左值是不完整类型且不是数组类型,则行为是未定义的。

8、以上内容的前半部分说明了在哪些情况下左值会被转换为右值,即除作为某些少数运算符的左操作数时,左值(除个别情况外)总会被转换为右值。后半部分说明了转换之后的右值的类型,在此处有关其中提到的几种类型暂时不需要理解,重点理解前半部分。

示例2.3:理解从左值到右值的转换
int a=1;
int b[2]={};
cout<<a; cout<<b;
其中cout<<a;会输出变量a所指对象的值,因为按C89标准规定a会从左值转换为右值,但cout<<b不会输出b所指示对象的值,因为b是数组类型,所以不会被转换为右值(注:C89标准规定不是数组类型的左值才会被转换为右值)。

9、不可修改的左值
若对左值加以限定(比如,使用const进行限定),则还存在可修改和不可修改的左值,比如

cons int a=1;
a=2; //错误,a是不可修改的左值,不能修改a的值。

10、可修改的左值
指不是数组类型,也不是不完整类型,也不是const限定的类型,若类型是结构或联合时,其任何成员都不是const限定的类型。比如

int a; //变量a是可修改的左值;
const int b; //变量b是不可修改的左值;
int c[22]; //数组变量c是不可修改的左值。

11、C++11之后由于引入了右值引用以支持移动语义(move semantics),所以,对左值和右值进行了进一步的分类,并引入了值类别(value category)的概念,将其分为三种基本类别(详见后文)

  •  lvalue(left value,左值)
  •  prvalue(prue rvalue纯右值),这是传统意义上的右值。
  •  xvalue(exppiring lvalue,将亡值或即将过期的左值)
    以及两种混合类别(详见后文)
  •  rvalue(right value,右值)。注意,这不是传统意义上的右值。
  •  gvalue(generalized lvalue,广义左值或泛左值)

12、注意:类别(category)和类型(type)是两个不同的概念。

2.2 字符及其编码的基础知识

2.2.1 计算机怎样处理字符

计算机只能处理二进制数字,那么怎样处理字符呢?可以想到的最简单的办法就是在字符和数字之间进行映射(编码),即,只需把字符想办法转换为二进制数字就行了,比如将字符“A”映射为10进制整数65,然后再将65直接映射为二进制数0100 0001,同理,可将“B”映射为66等,这样计算机就能处理字符了,这里的“映射”在计算机中被称为“编码”,现在的计算机就是使用这种简单的思想处理字符的,只是其具体过程更复杂,至此,可能大家会产生出以下问题:

  •  为什么要进行两次编码,直接编码为二进制不是更省事吗?
    其实这很简单,第一次编码准确的说其实是为每个字符编了一个号,这个编号可方便人们使用,编号通常是10进制数或16进制数,这样在以后指定某个字符时,可以直接使用数字来指定这个字符,很明显,直接使用二进制更不方便,比如,对于ASCII字符集,可以方便的使用65表示字符“A”,显然,使用二进制数0100 0001 更不方便。
  •  为什么要使用编号来指定字符,而不是直接指定字符呢?
    比如,字符“A”,为什么要用编号65呢,直接用“A”不是更方便?对于英文字母这些常见的字符,直接使用字符当然更方便,但对于不是经常使用或者难输入、难显示、不显示的字符(比如积分号、求和符号、零宽度空格、回车换行符等),那么使用编号就更方便了。
  •  一个二进制比特串,比如0100 0001 ,为什么不会被处理为整数65,而被处理为字符“A”呢?
    这与该比特串的类型有关,如果类型为整型,则计算机就会解释为整数65,若为字符型,就会解释为字符,同理,若为浮点型,则会按浮点型的规则解释该比特串。
2.2.2 计算机怎样显示字符

1、字符集:说简单一点就是各种字符的集合,通常字符集还包括与字符相对应的编号,也就是说字符集中的每个字符都有一个编号。比如ASCII字符集、Unicode字符集等。

2、字符是什么
字符在计算机中其实是以图形的形式显示出来的,因此,可将字符理解为一个“图形”,或是一个具有某种图形的“符号”。

3、字形(glyph)
字形用于表示字符的外形,比如字母a可以以多种外形对其进行书写,再如中文字符中的每一笔画都是一个字形,如“才”,可认为是由“一”、“亅”、“ノ”三个字形组成,因此,可把字符进一步理解为由一个个的字形组成的图形(或符号)。注:glyph也翻译为图元,图像。

4、字体
字体是一个拥有相同设计风格(或样式)的字形及从字符到字形映射关系的集合,比如,宋体、楷体、华文新魏等都是字体。属于同一字体的字符,都具有相同的书写(或显示)风格或书写样式。

5、常常见到的点阵字体、矢量字体等,其实是对字形的一种设计方法,点阵字体是以一个一个点的形式来设计字形的轮廓的,矢量字体是以数学函数的形式来描述字形的轮廓的,具体从略。

6、计算机显示一个字符的简略步骤
有了以上知识之后,我们来看计算机怎样显示一个字符。当计算机接收到一串二进制之后,首先判断其类型,若是字符型,则检测使用的是什么字符集,再根据该字符集的相关规则,将这个二进制串转换为相应的编号,再根据这个编号在字符集中查找到对应的字符,再根据当前系统所使用的字体将其显示出来,其过程如图2.1所示。在以上过程中,若未在字符集中找到相应的字符或没有找到相应的字体,则可能会被显示为乱码或者直接不显示任何符号。
在这里插入图片描述

2.2.3 字符编码

1、字符的第一次编码
字符进行的第一次编码是将字符编码为与一个数值(比如一个整数)相对应,其实就是将每个字符都编了一个号,所以将这里的编码称为编号更准确。比如将字符A编码为十进制整数65,B编码为66等,通常把此步骤形成的“字符数字对”集合称为字符集,比如常用的ASCII字符集,就是将128个常用的字符编码为128个整数,另外,还有Unicode字符集,GB2312字符集等。

2、字符的第二次编码
字符的第二次编码就是把第一次编码好的数值再编码为相应的二进制串,这样计算机就可以直接存储该二进制串了,这次编码通常会使用相应的算法把数值转换(即编码)为二进制串,注意,并不一定是直接编码为相应的二进制数的。比如,ASCII码是直接编码为二进制数的,如字符A的第一次编码65,就直接编码为0100 0001;但是对于Unicode字符集有3种不同的二次编码方案,分别是UTF-8,UTF-16和UTF-32,目前使用较多的是UTF-8编码,该编码不是直接编码的,如“汉”字的第一次编码为U+6C49 (110 1100 0100 1001),其第二次编码为1110 0110 1011 0001 1000 1001 ,可见,第二次编码并不是把十六进制数6C49直接转换为二进制数的。
3、通常情况下,字符集中的字符与第一次编码后的数值(即编号)是一一对应的,但由于某些原因,Unicode并不总是如此,但是,人们还是习惯性的认为他们是一一对应的,于是通常使用第一次编码后的数值来确定一个字符。
4、易混、常混合使用的概念------编码
由于历史原因,“编码”一词经常被混用,通常把第一次编码、第二次编码、字符集都称为编码,比如ASCII字符集也常称为ASCII编码,再如Unicode的第二次编码算法UTF-8,也被称为UTF-8编码。

2.2.4 补充知识-----Unicode与ISO 10646

1、Unicode
Unicode又称为统一码、万国码、单一码,是一种试图容纳全球所有字符的编码方案。Unicode包括字符集、编码方案等,它为每种语言中的每个字符设定了统一且唯一的二进制编码,以满足跨语言、跨平台的要求。Unicode制定的内容非常多,有兴趣的读者可以参阅相关文章。

2、ISO 10646与通用字符集(Universal Character Set,UCS)
历史上,除了Unicode在试图制定全球统一的通用字符集外,国际标准化组织 ( ISO )也在制定相应的标准,其中通用多八位编码字符集(Universal Multiple-Octet Coded Character Set ),简称通用字符集(Universal Character Set,UCS)就是由ISO制定的ISO 10646标准所定义的字符集,是与Unicode并行的标准。最初,ISO与Unicode各自开发各自的项目,后来,双方意识到世界不需要两个不兼容的字符集,于是,双方进行了整合,并使彼此制定的标准相互兼容,以使两者保持一致,直到现在,两个组织都存在,并且各自独立公布各自的标准,但二者基本是一致的,不过Unicode的知名度比UCS更大,应用也更广泛。

3、代码点(Code Point,简称码点)
Unicode把第一次编码后的数值称为码点(或代码点),使用

U+十六进制数

的形式表示,如U+6C49。注:码点、码点值、代码点是有区别的,但三者经常混用。本文对码点的定义仅是一种简单粗略的理解,详细精确的定义请参阅有关Unicode编码的文献。

4、UTF-8、UTF-16、UTF-32简介

  •  Unicode在进行第二次编码时有3种不同的算法把码点转换为二进制,分别是UTF-8,UTF-16和UTF-32,这三种算法常被称为UTF-8编码、UTF-16编码、UTF-32编码,他们分别使用变长位、16或32位、32位编码。也就是说,同一个字符,使用不同的编码算法将得到不同的二进制数串,比如,“汉(U+6C49)”,若使用UTF-8编码,则第二次编码后的值为0xE6 B1 89,若使用UTF-16编码,则第二次编码后的值是0x6C49。
  •  需要注意的是,在Unicode中,任意一个码点都可以使用UTF-8、UTF-16、UTF-32三种算法将其分别编码为三个不同的二进制数串,具体使用哪一种编码算法,这需要视所使用的应用(或软件,现在也称为APP)而定,通常,应用可自行设置使用哪一种算法。

5、特别注意:在Unicode标准中,码点与字符并不一定总是一一对应的,在Unicode标准中,一个字符有可能有多个码点,也有可能由多个码点来表示一个字符(常见于组合字符)。比如U+51C9与U+F977都是同一个字符“凉”,这主要是为了兼容韩国字符集的标准。再如,以下字符
在这里插入图片描述
是由基本字符g(U+0067)和U+0308(这个字符称为组合字符)组合而成,虽然以上字符是由两个Unicode码点组成,但现实中,人们会认为这是一个字符。以上由两个码点组合而成的字符在Unicode中被称为“用户感知字符”。

2.3 C++程序文本的组成

2.3.1 C++使用的字符集

1、C++使用的是什么字符集是一个很重要的问题,这关系到能否正确显示、处理字符以及字符处理的方式,若两个C++系统使用了不同的字符集,有可能会出现乱码的情形。
2、C++实现(通常指编译器)有4种类型的字符集:基本源字符集、扩展源字符集、基本执行字符集、扩展执行字符集。其关系如图2.2所示
在这里插入图片描述

  •  基本源字符集用来编写程序的源代码,包括大小写字母、数字、常用符号等;
  •  基本执行字符集用于处理程序执行期间的字符,如显示到屏幕上的字符,基本执行字符集比基本源字符集增加了一些字符,如退格字符、振铃字符等。
  •  C++标准还允许实现提供扩展源字符集和扩展执行字符集。注,扩展是指的额外增加的,因此,扩展字符集是指的由实现额外提供的字符集。

3、C++标准对字符集的要求

  • 1)、C++标准规定,基本源字符集需由以下91个字符及空格、水平制表符、垂直制表符、换页符(form feed)、换行符共96个字符组成。
    在这里插入图片描述
  • 2)、基本执行字符集和基本执行宽字符集应分别包含基本源字符集中的所有成员,并加上表示警告、退格、回车的控制字符,以及一个值为0的空(null)字符(空宽字符)。基本执行字符集成员的值必须是非负的且彼此不同。
  • 3)、基本源字符集和基本执行字符集中十进制数字0之后的每个字符的值都应比前一个字符的值大1。

4、怎样使用字符集中的字符

  • 1)、基本源字符集中的字符可以用于源文件(通常后缀为.cpp)
  • 2)、任何不在基本源字符集中的源文件字符必须使用通用字符名的形式来引用。这意味着实现需要支持扩展字符集。

5、C++实现使用的字符集
C++具体使用的是什么字符集,要依实现而定。通常情况下,C++实现使用的字符集是主机系统的编码,如IBM大型机使用EBCDIC编码,若使用的是windows10系统中文版,则C++实现(通常指编译器)有可能会使用GB18030编码,不过,最常用的字符集是ASCII字符集,本文若未作特殊说明,均表示使用的ASCII字符集。

6、注意:很多常用的字符集,如Unicode、ISO10646等,都会把ASCII字符集作为其子集。

2.3.2 通用字符名(universal character name)

1、通用字符名的格式为

\uhhhh
\Uhhhhhhhh

其中,每一个h表示一个十六进制整数。指定的数字是ISO 10646的码点。需要注意的是\u之后必须是4位数,\U之后必须是8位数。比如\u00E2、\u00F6、\U00006C49等,若系统不支持ISO 10646编码,则无法显示这些字符。注:ISO 10646是与Unicode同步的国际标准

2、通用字符名的使用范围
通用字符名可用于标识符、字符串、字符字面值中。可在标识符中使用通用字符名意味着可以把通用字符名用于变量名、函数名等处,比如,变量名中可以有法文的重元音、汉字等。通用字符名不能指定以下范围的字符

  •  不能指定代码点范围外的字符。ISO/IEC 10646码点的范围为0x0~0x10ffff

  •  不能指定代理代码点范围内的字符。代理代码点范围为0xD800~0xDFFF

  •  在字符字面值或字符串字面值的c-char-sequence、s-char-sequence、r-char-sequence以外的地方不能指定基本源字符集中的字符或控制字符,注意:用户自定义的字面值内也不能指定这些字符。控制字符码点在00x1f或0x7F0x9F范围内

    示例2.4:使用通用字符名
    int \u6C49 = 2; //声明一个名称为\u6c49的变量。其中,6C49是“汉”的码点 //int
    \u0061=3; //错误,不能在变量名中指定基本源字符集中的字符。其中,0061是“a”的码点 char
    b=‘\u0061’; //正确,通用字符名指定的基本源字符集中的字符可用于字符字面值

3、VC++专用

  • 1)VC++可以使用 $ 字符。注:C++标准规定的基本源字符集没有 $ 字符。

  • 2)默认情况下VC++使用默认代码页(可将代码页理解为字符集)保存源文件,当使用特定于区域设置的代码页或Unicode代码页保存源文件时,VC++允许在源代码中使用该代码页中的任何字符,但基本源字符集中未明确允许的控制代码除外。因此,若使用中文代码页保存源文件,则可在注释、标识符或字符串中使用中文字符。VC++不允许使用不能转换为有效多字节字符或Unicode码点的字符序列。另外,并不是所有允许的字符都可在标识符中显示,具体取决于编译器的设置。

  • 3)VC++可把通用字符名形式的字符和文本形式的字符相互转换,这意味着可以声明一个通用字符名形式的变量,再以文本的形式使用它,比如

    int \u6C49 = 2; //声明一个名称为\u6c49的变量。其中,6C49是“汉”的码点
    cout<<汉<<endl; //输出2 int 中=3; //声明一个名称为“中”的变量。
    cout<<\u4E2D<<endl; //输出3。其中4E2D是“中”的代码点

2.3.3 C++程序文本的组成和标识符

1、C++程序文本由标记(token)和空白组成。标记是C++程序对编译器有意义的的最小元素,空白用于分隔标记。C++程序通常还会包含预处理标记,但预处理标记最终会被转换成标记。除此之外,C++程序还可以使用替代标记,替代标记将一些运算符和标点符号使用另一种形式进行了替代。

2、空白
空白是空格、水平制表符、垂直制表符、注释、换行符、换页符(form feed,也称为表单进纸、进纸符等,码点为u+000C)的统称。空白用于分隔标记,但最终会被编译器忽略。

3、标记(token)
标记有5种:标识符、关键字、字面值(literal)、运算符和标点符号。也就是说,标识符是标记,关键字也是标记,其余类似。

4、注释
注释中的内容会被编译器视为空白。C++支持两种注释。C++单行注释以“//”开头,在下一个换行符之前结束。多行注释使用 “ / * ” 开始,以“ */ ” 结束,多行注释不能嵌套。

5、关键字(keyword)
关键定是C++语言本身需要使用的名称,如int、float、main、void等。

6、运算符(operator)和标点符号(punctuator)
运算符或标点符号是指的以下字符之一。注意:不包含 ’ 、" 、\ 三个字符

在这里插入图片描述
以下运算符属于预处理运算符
在这里插入图片描述
7、标识符
标识符是一个名称,可以用于表示变量、函数、数组等名称。具体而言,标识符可用于表示以下内容:
在这里插入图片描述

8、标识符应遵守以下规则:

    1. 只能使用字母、数字、下划线( _ )以及使用通用字符名指定的某些字符,比如

    abc //正确
    a3bc_d //正确
    abc>de //错误,含有符号>
    ab\d //错误,含有符号\
    ab&d //错误,含有符号&

  • 2)不能将C++中的关键字作为标识符

  • 3)标识符的长度没有限制,但有一些系统会作限制,比如VC++将标识符长度限制为2048个字符。

  • 4)C++是大小写敏感的语言,因此标识符abc和Abc是两个不同的标识符。

  • 5)标识符使用空白分隔,因此,一个标识符中不应有空格、制表符、回车等字符,比如abc def将会是两个标识符,即abc和def。

  • 6)标识符中的通用字符名应是表2.1所示ISO/IEC 10646码点范围。从码点范围可见,标识符中可以含有组合字符、不可见字符、汉字、日文、缅甸文、希伯来字符、阿拉伯字符等多种字符。另外需要注意,从表2.1可见,字母、数字、下划线的码点并不在允许的码点范围之内,也就是说在标识符中的通用字符名不能指定字母、数字和下划线的码点。

  • 7)标识符的第一个字符不能是数字,并且不能是表2.2中ISO/IEC 10646码点范围的通用字符名所对应的字符(这些字符是组合字符)。

  • 8)因为系统所保留的名字或编译器生成的名字一般是以下划线开始的,所以不要使用以下划线开始的标识符,这样可以避免与编译器生成的或系统保留的名字相冲突。

  • 9)VC++专用
    VC++允许在标识符中使用 $ 符号。
    VC++还允许在标识符中使用通用字符名允许的范围所表示的实际字符,即,可以直接使用允许范围内的中文或日文等字符,但,必须将文件使用编写时的代码页保存。

在这里插入图片描述

示例2.5:标识符命名规则
int abc; //正确
int _abc; //正确。但不建议以下划线开头命名标识符 int
3abc; //错误,不能以数字开头
int a\u0061b; //错误,0061不在标识符允许的码点范围之内。注:0061是字母a的码点 int
\u0308ab; //错误,第一个字符不能以码点0308指定的字符开头
int a\0308b; //正确。注:0308是组合字符“̈”的码点

9、替代标记(alternative token)
替代标记将一些运算符和标点符号使用另一种形式进行了替代,如表2.3所示
在这里插入图片描述

10、字面值(literal)、预处理标记的讲解详见后文。

2.4 类型的分类和检测及sizeof运算符

2.4.1 基本概念

1、变量
前文已讲过,变量是指的命名的对象。除此之外,还可从另一角度来理解变量,即,将变量理解为其值可以改变的量,这样的理解方式与数学上的变量类似,其值是可以改变的。

2、常量
常量可以简单的理解为其值是不能改变的量,如整数2,他就是整数2,不能改变。

3、常变量
常变量是指使用C++关键字const把变量设置为值不可改变的量,称为常变量,有时也简称为常量。

4、字面值或字面值常量

  • 1)、概念
    只能使用它本身的值来表示其名称的量被称为字面值(常量),有时也称其为常量,也就是说,字面值可以直接表示其值。比如像22这样的整数值,只能使用22来称呼它,所以22是一个整数字面值或整数常量,再如浮点数2.2被称为浮点型字面值或浮点型常量。可见,字面值在很大程序上就是需要由计算机处理的数据。
  • 2)、分类
    C++的字面值分为整数字面值、字符字面值、浮点字面值、字符串字面值、布尔字面值、指针字面值、用户定义的字面值。可以将这些字面值的分类理解为字面值的类型,如,整数字面值相当于该字面值是整型的,浮点型字面值的类型是浮点型的。
  • 3)、由于字面值与相应的知识点有关,所以各字面值的详细内容分别分散在各相应章节中详细讲解。

5、符号常量
符号常量是指使用#define定义的常量,比如

#define PI 3.14

这样当程序中使用到PI这一名字时就表示常量3.14。有关#define的内容详见预处理器章节

6、常量、常变量、字面值、字面值常量、符号常量,这几个概念很多时候都统称为常量。

7、cv、cv限定符(cv-qualifier)、cv限定的(cv-qualified)

  • 1)、const、volatile(易变的)关键字被统称为cv限定符。
  • 2)、由cv限定符限定的类型被称为cv限定的(cv-qualified),即由const、volatile及其二者的任意组合限定的类型。
  • 3)、本文在描述类型时使用的字符cv表示cv限定符的任意集合,即{const}、{volatile}、{const与volatile}或空集之一。比如

    cv int;
    表示const int、volatile int、const volatile int、int等其中一种情形。

2.4.2类型的分类

1、C++对类型对类型进行了多种形式的划分。

2、最基本的划分方法是将类型划分为基本类型(fundamental type)和复合类型(compound type)。具体见表2.4

  • 1)、基本类型包括:整数类型、char型(字符型)、wchar_t、char8_t、char16_t、char32_t、bool类型(布尔类型)、浮点型、cv void、std::nullptr_t类型。其中:
     整数类型又分为short int(短整型)、int(整型)、long int(长整型)、long long int(长整型)。
     整数类型、char型、wchar_t有“有符号”和“无符号”之分,使用signed表示有符号的,使用unsigned表示无符号的。比如signed short int表示有符号短整型
     整数类型、char型、bool型、wchar_t、char8_t、char16_t、char32_t统称为整型。注意:整数类型也经常被称为整型。
     整型和浮点型统称为算术类型(arithmetic type)
  • 2)、复合类型包括:给定类型的对象的数组,函数,指向给定类型的cv void或对象或函数(包括类的静态成员)的指针,引用给定类型的函数或对象,类,共用体(union),枚举,指向非静态类成员的指针。可简单的将复合类型理解为:数组类型、函数类型、指针类型、引用类型、类类型、共用体类型、枚举类型。
    在这里插入图片描述
    3、C++还将类型划分为内置类型和用户自定义类型。内置类型是指C++语言本身提供的类型,如int、float、char等类型。用户自定义类型是指由用户自已定义的类型,自定义类型可理解为程序员自已创造的类型,比如,类类型、枚举等。

4、除以上划分方法外,C++还规定了如下的类型,以下类型牵涉到后续章节的内容,所以,暂时不需全部理解,可在学习完相应内容之后再来阅读。

5、不完整类型(incomplete type)
已声明但未定义的类、某些上下文中的枚举类型、或未知边界或不完整元素类型的数组和cv void类型被称为不完整类型。除cv void类型外的不完整类型被称为不完整定义的对象类型(incompletely defined object type)。类似的,还有不完整类类型、不完整数组类型等称呼。不完整类型具有以下特点:

  •  不完整类型的大小(即容量)不能确定,因此,无法为不完整类型分配内存空间,所以,不能使用不完整的类型。
  •  某些不完整类型在某个点可能是不完整的,但之后可以对其进行补全,以使其变为完整的。但cv void类型始终是不完整的,不能补全为完整类型,这种类型有一个空的值集。
  •  指向未知边界数组的指针的类型,或者由typedef 声明定义为未知边界数组的类型的指针的类型,不能是完整的。
  •  指针可以指向不完整类型,因为指针的大小是确定的。
	示例2.7:理解不完整类型
	#include <iostream>
	using namespace std;
	class A;				//A是不完整的类类型。因为A还未被定义
	typedef int T[];		//正确。T是int[]的别名
	extern int s[];		//正确。虽然s是不完整的数组类型,但可以声明
	//int s1[];			//错误。s1是不完整类型,不能对其进行定义
	A* p;					//正确。虽然p是指向不完整类型A的指针,但指针的大小是可以确定的
	A** p1;				//正确。原因同上。
	//A ma;				//错误。A是不完整的,不能使用A创建对象。
	T* p2;				//正确,p2是指向类型为T(即int [])的指针,由于T是
							//由typedef声明的,所以不能被补全为完整类型。
	extern int(*p3)[];	//正确。p3是指向不完整类型数组的指针,即,p3指向int []
	//int(*p3)[4];		//错误,p3不能被补全为完整的。
	void f(){
			//p++;			//错误。因为*p的类型是A,A是不完整类型其大小不能确定,所以,
							//不能确定应将指针偏移多少字节。
			p1++;			//正确。*p1的类型是A*,是一个指针,指针的大小能确定,所以,
							//可以确定p1++应偏移多少字节。
	}
	class A {};			//对类型A进行补充说明。现在A是完整类型了。
	int s[4];				//对类型s进行补充说明。现在s是完整类型了。
	int main(){
			p++;			//正确,*p的类型A现在是完整类型。
			p1++;			//正确。
			A ma;			//正确,A现在是完整的。
			p = &ma;
	}

6、对象类型(object type)
对象类型是除函数类型、引用类型、cv void外的可能含有cv限定的类型

7、标量类型(scalar type)
标量类型是算术类型、枚举类型、指针类型、指向成员的指针类型、std::nullptr_t和这些类型的cv限定版本的类型的统称。可见,标量类型可理解为算术类型、枚举类型、指针类型的统称。

8、平凡类型(trivially type,或普通类型)
平凡类型是标量类型、平凡类类型、此类类型的数组以及这些类型的cv限定版本的统称。

9、平凡可复制类型(trivially copyable type)
平凡可复制类型是标量类型、平凡可复制类类型、此类类型的数组以及这些类型的cv限定版本的统称。

10、标准布局类型(standard layout type)
标准布局类型是标量类型、标准布局类类型、此类类型的数组以及这些类型的cv限定版本的统称。

11、隐式生命期类型(implicit lifetime type)
隐式生命期类型是标量类型、隐式生命期类类型、数组类型以及这些类型的cv限定版本的统称。

12、字面值类型(literal type)
字面值类型是一个表面上可以在常量表达式(constant expression)中创建对象的类型,它不保证可以创建这样的对象,也不保证该类型的任何对象都可以在常量表达式中使用。以下类型是字面值类型:

  •  cv void
  •  标量类型
  •  引用类型(reference type)
  •  字面值类型的数组
  •  具有以下所有属性的可能含有cv限定的类类型
     该类有一个constexpr析构函数
     该类要么是闭包类型(closure type),要么是聚合类型(aggregate type),要么至少有一个constexpr构造函数或构造函数模板(可能从基类继承),而不是复制或移动构造函数。
     若是共用体(union),则它的非静态数据成员中至少有一个是non-volatile literal type(非易变的字面值类型)
     若不是共用体,则它的所有非静态数据成员和基类都是非易变的字面值类型。

示例2.8:区分变量、常量、标识符、关键字
①、int a = 2.3; //声明一个整型(int)变量a,并将2.3赋值给变量a
其中:
int是关键字,表示整型 a是标识符,在此处表示变量名,其类型被声明为int型(整型)
=是赋值运算符
2.3是字面值,其类型默认是浮点型,所以被称为浮点型字面值。 ②、char cc = d; //错误
其中:
cc是标识符,表示变量名
d在这里是标识符,并不是字符型字面值,标识符应先声明才能使用,所以,以上示例会出现错误。
注:C++的字符需要使用单引号括起来。

2.4.3 检测类型的方法

以下语句可在VC++中输出待测对象的类型

cout<<typeid(待测对象).name();

以上语句使用MinGW (即,g++)输出过于简短(只有一个字母),要在MinGW中输出完整类型,应在MinGW中包含以下头文件(注:以下头文件在VC++中是不存在的)

#include <cxxabi.h>

然后使用以下语句才能输出完整的类型名

cout << abi::__cxa_demangle (typeid (待测对象).name(),0,0,0 );

示例2.9:使用VC++检测整型字面值的类型
#include <iostream>
using namespace std;
int main()
{	cout << typeid(int).name() << endl;		//输出int。检测int的类型
    cout << typeid(44).name() << endl;		//输出int。检测44的类型
    cout << typeid('a').name() << endl;}		//输出char。检测'a'的类型
2.4.4 sizeof运算符

本小节目前只需知道sizeof的作用和使用方法即可,可在学习完后续章节相应内容之后再来阅读。

1、sizeof运算符的作用
sizeof运算符用于返回一个对象或类型名的长度,返回值的类型为size_t (一般被定义为整型),长度的单位是字节,sizeof表达式的结果是一个编译时常量,也就是说sizeof返回的结果可以用于任何常量可以使用的地方,比如用作函数的参数等。

2、sizeof有3种语法形式:

sizeof(typename)
sizeof(expr)
sizeof expr

其中,typename表示类型名,expr表示表达式,因此

  •  sizeof不但可以用于求出类型的长度,还可以求出表达式的长度,比如

    sizeof 3+3.0; //返回3+3.0的结果的长度
    sizeof a+b+c; //返回a+b+c计算之后的结果的长度
    sizeof(int) //返回int类型的长度

  •  当sizeof操作符用于内置类型名时必须要加括号,而表达式则可省略括号。比如

    sizeof(int) //正确
    sizeof int //错误

3、注意:sizeof被称为“运算符”,这意味着应把sizeof看作与“+、-、*、/”等运算符一样是用于运算的,运算符是C++语言内置的元素。

4、sizeof运算符的求值规则(见表2.5):

  • 1)、对char类型或char类型的值执行sizeof操作将恒为1,因为char占据1个字节。

  • 2)、对引用类型执行sizeof操作将返回存放该引用类型对象所需内存空间的大小。

  • 3)、对指针作sizeof操作将返回存放指针所需内存的大小。要反回指针所指对象的大小,则必须对指针作解引用操作。

  • 4)、对数组名作sizeof操作将返回其元素类型的大小乘以数组元素的个数,即返回整个数组在内存中的字节长度,它不是数组中第一个元素的长度,也不是数组包含的元素个数。所以要求出数组的实际大小,需对数组名作sizeof操作,再除以数组类型的sizeof操作。比如

    int s[11];
    cout << sizeof(s) ; //输出44,即整个数组的大小(假设int为4字节)
    cout<< sizeof(s)/sizeof(int); //输出11,即数组的大小。

  • 5)、对于string类型的对象使用sizeof运算符时,长度是固定的,不因字符串的多少而不同。

  • 6)、sizeof运算符不能用于函数类型或不完整类型的表达式,比如

    sizeof(void); //错误,因为void是不完整类型;
    void f(){}; //声明一个函数
    sizeof f; //错误,标识符f一个函数类型
    sizeof(f) //错误,同上

在这里插入图片描述


作者:黄邦勇帅(原名:黄勇)

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值