第二章 变量与基本类型 学习笔记

来源:互联网 发布:电算化考试软件下载 编辑:程序博客网 时间:2024/06/05 04:45

● C++的对象类型决定了能对该对象进行的操作, 一条表达式是否合法依赖于其中参与运算的对象的类型。

● C++是一种静态数据类型语言 , 它的类型检查发生在编译时, 因此, 编译器必须知道程序中每一个变量对应的数据类型。

● 数据类型是程序的基础: 它告诉我们数据的意义以及我们能在数据上执行的操作, 即数据类型决定了程序中数据和操作的意义。

这里写图片描述


● 1、位(bit)
来自英文bit,音译为“比特”,表示二进制位。位是计算机内部数据储存的最小单位

2、字节(byte)
字节来自英文Byte,音译为“拜特”,习惯上用大写的“B”表示。
字节是计算机中数据处理的基本单位。计算机中以字节为单位存储和解释信息,规定一个字节由八个二进制位构成,即1个字节等于8个比特(1Byte=8bit)
通常1个字节可以存入一个ASCII码,2个字节可以存放一个汉字国标码。

3、字
计算机进行数据处理时,一次存取、加工和传送的数据长度称为字(word)。一个字通常由一个或多个(一般是字节的整数位)字节构成。
1字=2字节
1字节=8位
1字=2*8=16位

除了布尔型和扩展的字符型之外,其他整型可以划分为:带符号的和无符号的。带符号的:正数,负数,0 无符号的:仅能表示大于等于0的值。

● 一个char的空间应确保可以存放机器基本字符集中任意字符对应的数字值, 也就说, 一个char的大小和一个机器字节一样大。

其他字符类型用于扩展字符集, 如 wchar_t, char16_t, char32_t, wchar_t 类型用于确保可以存放机器最大扩展字符集中的任意一个字符, 类型char16_t 和 char_t32 则为 Unicode 字符集服务(Unicode 是用于表示所有自然语言中字符的标准)。

● C++语言规定 一个int至少和一个short一样大, 一个long至少和一个int一样大,一个long long至少和一个long一样大,其中,数据类型long long 是在C++11 中新定义的。

● 大多数计算机以2的整数次幂个比特作为块来处理内存, 可寻址的最小内存块称为“字节(byte)”,存储的基本单元称为“字(Word)”, 它通常由几个字节组成。 在C++语言中, 一个字节要至少能容纳机器基本字符集中的字符。 大多数机器的字节由8比特构成, 字则由32位或64比特构成, 也就是4或8字节。

● 大多数计算机将内存中的每一个字节与一个数字(被称为“地址”),关联起来,

说明: 这里指的是”面向硬件的概念”
“面向硬件的概念”的字节: 被定义成可寻址的最小内存块; 它的大小”原则上”不确定,但是现实世界绝大多数计算机都为8 bit;

“面向硬件的概念”的字: 被定义成可存储的基本单元,其的大小为’字节’的整数倍; 它的大小”原则上”不确定,但是现实世界绝大多数计算机的’字’ 都是’字节’的4倍或8倍,又因为现实世界绝大多数计算机的’字节’都为8 bit,那么现实世界绝大多数计算机的’字’ 就为32bit或64bit;

“面向软件的概念”:
“面向软件的概念”的字节(Byte) = 8 bit;
“面向软件的概念”的字(Word) = 16 bit = 2 bytes;
“面向软件的概念”的双字(Double Word) = 32bit = 2 words = 4 bytes;
“面向软件的概念”的四字(Quad Word) = 64bit =4 words = 8 bytes;

我们通常说的32位操作系统。 说的就是: 这个操作系统是安装在’字’为32位的硬件上;
我们通常说的64位操作系统。 说的就是: 这个操作系统是安装在’字’为64位的硬件上;

所以当提及概念: 字(word)的时候,要根据上下文区分作者到底说的是”面向硬件的概念”还是”面向软件的概念”;


可以这样理解:

在写代码的时候,就按”面向软件的概念”理解字节/字/双字/四字/8字/16字……., 每一个都是前面的2倍.

计算机硬件中的字节和字,我们叫”机器字节”和”机器字”,简称为:字节和字. 注意: 不存在” 机器双字”,”机器四字”等…

“机器字节”和”机器字”,是有严格的物理上的概念定义的,即前者被定义成: 成可寻址的最小内存块;后者被定义成: 可存储的基本单元;

而”面向软件的概念”理解字节/字/双字/四字/8字/16字只是一个抽象的数量关系罢了,即: 每一个都是前面的2倍.

计算机硬件中的字节和字,只有这两个;
而软件中的: 字节/字/双字/四字/8字/16字…理论上有无数个,只是前面5个最常使用罢了。


● 为了赋予内存中某个地址明确的含义, 必须首先知道存储在该地址的数据的类型。 类型决定了数据所占的比特数以及如何解释这些比特的内容。

比如说: 指针指向的地址,指针的本身的大小通常是一个”机器字”,即指针本身的大小在32位机上为32bit,在64为机上为64位。

“那普通变量, 也是在内存中开辟内存空间, 也是机器字吗?”(问题)

比如你一个bool类型为”面向软件的”1byte,但是机器的最小存储单位是一个”机器字”即4字节,那么系统会分配4个字节来存储你的这1字节的bool数据;

当然如果你有2个bool类型数据,那么为2byte,那么系统还是会分配4byte来存储你这两个bool类型

如果你有5个bool类型的数据,那么为5byte,那么系统会分配8byte内存来存储你这5个bool类型的数据;
总之: 分配内存都是以一个机器字(4字节)来进行的。

通常我们不区分”软件字节”和”硬件字节”,因为虽然概念上不同,但是在现实世界中都为8bit,所以通常不做区分除非有非常之必要才做区分。

又因为没有: ” 机器双字”,”机器四字”等….
所以唯一要注意的就是: “软件字”和”机器字”这两个概念上又不一样,数值上也不一样。 他们只是恰好都使用了”字’这个词罢了。


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

● 类型unsigned int 可以缩写为 unsigned

● 与其他整型不同, 字符型被分为三种: char 、signed char 和 unsigned char

注意: 类型 char 和类型 signed char 并不一样, 尽管字符型有三种, 但是字符的表现形式却只有两种: 带符号的和无符号的, 类型char 实际上会表现为上述两种形式中的一种, 具体是哪种编译器决定。

● 无符号类型中所有比特都用来存储值, 例如: 8比特的 unsigned char 可以表示至255区间的值。

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


● 建议如何选择类型:
这里写图片描述


● 当一个算术表达式中既有无符号int值,又有有符号int的值时, 那么有符号int的值就会转换成无符号数, 把有符号int 转换成无符号数的过程和把int直接赋给无符号变量一样。

这里写图片描述


● 每个字面值都有相应的类型, 字面值常量的形式和值决定了它的数据类型。 称之为字面值,是因为只能用它的值称呼它;称之为常量, 是因为它的值不能修改。

● 注意: 只有内置类型存在字面值, 没有类类型的字面值,更没有任何 标准库类型的字面值

● 整型字面值具体的数据类型由它的值和符号决定。 默认情况下, 十进制字面值是带符号数, 八进制和十六进制字面值即可能是带符号的也可能是无符号的,

● 字面值整数常量的类型默认为int或long, 其精度类型决定于字面值——其值适合int,就是int类型, 比int大的值就是long类型。

● 注意: 如果一个字面值连与之关联的最大数据类型都放不下,将产生错误。 类型short 没有对应的字面值。

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

● 浮点型字面值表现为一个小数或以科学计数法表示的指数, 其中指数部分用E或e标识。 默认的浮点字面值常量为double类型,

● 字符串字面值的类型实际上是由常量字符构成的数组。 编译器在每个字符串的结尾处添加一个空字符(‘\0’), 字符串字面值的实际长度要比的内容多1。

● 在字符字面值前加个L就能够得到wchar_t 类型的宽字符字面值,如: L’a’

● 有两类字符程序员不能直接使用: 一类是不可打印的字符, 如退格和控制符, 因为它们没有可视的图符, 另一类是由特殊含义的字符(单引号 换行), 在这样的情况下可以用转义序列

我们也可以使用泛化的转义序列, 其形式是 \x 后紧跟1个或多个十六进制数, 或者 \ 后紧跟1、2或3个八进制数, 其中数字部分表示的是字符对应的数值。

这里写图片描述
这里写图片描述


● string 也是在命名空间std中定义的, std::string, 是一种表示可变长字符序列的数据类型, string类规定如果没有指定初值则生成一个空串

● 对象是指一块能存储数据并具有某种数据类型的内存空间, 在使用对象这个词时, 并不严格区分是类还是内置类型, 也不区分是否命名或是只读。

● 在C++语言中, 初始化和赋值是两个完全不同的操作。 初始化不是赋值, 初始化的含义是创建变量时赋予其一个初始值, 而赋值的含义是把变量的当前值擦除, 而已一个新值来替代。

 int s{0};  // 列表初始化, 还可以这样给s赋值 s={10};

注意: 当用于内置类型的变量时, 如果我们使用列表初始化且初始值存在丢失信息的风险, 则编译器将报错, 一般出现在类型转换的时候, 把大的数据类型转到小的数据类型。


默认初始化

如果定义变量时没有指定初值,则变量被默认初始化,此时变量被赋予了“默认值”, 默认值到底是什么由变量类型决定

如果是内置类型的变量未被显式初始化, 它的值由定义的位置决定。 定义于任何函数体之外的变量被初始化为0, 定义在函数体内部的内置类型变量将不被初始化, 一个未被初始化的内置类型变量的值是未定义的。

● 类的对象如果没有显式地初始化,则其值由类确定。

● 注意: 使用未初始化变量的值是一种错误的编程行为并且很难调试, 编译器不会检查此类错误, 但有时会发出警告。

建议初始化每一个内置类型的变量。

● 声明使得名字为程序所知, 一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义负责创建与名字关联的实体。

● 如果想声明一个变量而非定义它, 就在变量名前添加关键字extern, 而且不要显式地初始化变量,如果此时给这个变量赋初值, 那么extern作用消失, 那么此时不再是声明而是定义了。

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

● 如果要在多个文件中使用同一个变量, 就必须将声明和定义分离。此时, 变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明, 却绝不能重复定义。


这里写图片描述


● 注意 : 用户自定义的标识符中不能连续出现两个下画线,也不能以下画线紧连大写字母开头。

● main函数定义在函数体之外,所以是全局作用域


嵌套作用域


● 作用域能彼此嵌套, 被嵌套的作用域称为内层作用域, 包含着别的作用域的作用域称为外层作用域。

● 注意: 函数内部不宜定义与全局变量同名的新变量。


引用


● C++11 中新增了一种引用: 所谓的“右值引用”,这种引用主要用于内置类, 严格来说, 当我们使用术语“引用”时, 指的其实是“左值引用”。

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

● 注意: 引用并非对象, 它只是为一个已经存在的对象所起的另外一个名字。

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

● 注意: 因为引用本身不是一个对象, 所以不能定义引用的引用。

● 注意: 引用只能绑定在对象上, 而不能与字面值或某个表达式的计算结果绑定在一起。


指针


● 在函数体内定义的指针如果没有被初始化,也将拥有一个不确定的值。

● 因为引用不是对象, 没有实际地址, 所以不能定义指向引用的指针

● 指针的值(即地址)应属于下列4种状态之一:

(1) 指向一个对象

(2)指向紧邻对象所占空间的下一个位置。

(3) 空指针,意味着指针没有指向任何对象

(4) 无效指针, 也就是上述情况之外的其他值——野指针

● 注意: 试图拷贝或以其他方式访问无效指针的值都将引发错误。 编译器并不负责检查此类错误, 这一点和试图使用未经初始化的变量是一样的。 访问无效指针的后果无法预计, 因此程序员必须清楚任意给定的指针是否有效。

尽管第2种和第3种形式的指针是有效的, 但其使用同样受到限制。 显然这些指针没有指向任何具体对象, 所以试图访问此类指针(假定的)对象的行为不被允许。 如果这样做了, 后果也无法预计。


● 注意: 解引用操作仅适用于那些确实指向了某个对象的有效指针。

● 空指针: 不指向任何对象, 在试图使用一个指针之前的代码可以首先检查它是否为空, nullptr是一种特殊类型的字面值,它可以被转换成任意其他的指针类型,

● NULL——预处理变量,来给指针赋值, 这个变量在头文件cstdlib中定义的,它的值就是0。

● 预处理变量不属于命名空间std,它由预处理器负责管理, 因此我们可以直接使用预处理变量而无须在前面加std::

当用到一个预处理变量时, 预处理器会自动地将它替换为实际值, 因此用NULL初始化指针和用0初始化指针是一样的。

注意: 把int 变量直接赋给指针是错误的操作, 即使int变量的值恰好等于0也不行。

int *p1 = nullptr;    int zero = 0;    p1 = zero;  //错误, 不能把int变量直接赋给指针

这里写图片描述


● 注意: 只要指针拥有一个合法值,就能将它用在条件表达式中。 和采用算术值作为条件遵循的规则类似, 如果指针的值是0, 条件取false. 任何非0指针对应的条件值都是true.

● void* 用于存放任意类型对象的地址, 有用的操作有: 和别的指针比较、作为函数的输入或输出, 或者赋给另外一个void* 指针。 不能直接操作void*指针所指的对象, 因为我们不知道这个对象到底是什么类型, 也就无法确定能在这个对象上进行哪些操作。


指向指针的指针


● 指针也有自己的地址, 允许把指针的地址再存放到另一个指针当中, 通过* 的个数可以区分指针的级别

● 解引用int型指针会得到一个int型的数,同样, 解引用指向指针的指针会得到一个指针。 此时为了访问最原始的那个对象的, 需要对指针的指针做两次解引用


指向指针的引用


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

int i = 42;    int *p;    int *&r = p;    r = &i;    *r = 0;    cout << i << endl;    cout << *p << endl;    cout << *r << endl;

const 限定符


● const常量(对象)必须定义的同时初始化, 不可分开初始化, 它的初始值可以是任意复杂的表达式

● const跟非const类型的对象相比, 大部分的操作都可以完成,也不是所有的操作都适合, 主要的限制就是 只能在const 类型的对象上执行不改变其内容的操作, 可以参与算术运算, 也可以转换成一个布尔值。

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

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

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

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

如果想在多个文件之间共享const对象,解决的办法是: 对于const变量不管是声明还是定义都添加extern 关键字, 这样只需定义一次就可以了,

在别的文件中用extern 做限定, 作用是指明 该常量并非本文件所独有, 它的定义将在别处出现


const 引用


● 词组“对const的引用”简称为“常量引用”, 严格来说, 并不存在常量引用。 因为引用不是一个对象, 所以我们没法让引用本身恒定不变。

● 引用的对象是常量还是非常量可以决定其所能参与的操作, 却无论如何都不会影响到引用和对象的绑定关系本身。

● 注意 :如果需要将一个常量赋给一个引用,必须赋给一个常量引用, 非常量引用是无法这样做得。

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

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

int i = 42;const int &r1 = i;const int &r2 = 42;const int &r3 = r1 * 2

这里写图片描述

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

int i = 42;int &r1 = i;const int &r2 = i;r1 = 0;r2 = 0;  //错误: r2是一个常量引用

指针和const


● 与引用一样, 也可以令指针指向常量或非常量, 指向常量的指针不能用于改变其所指对象的值。 注意 : 要想存放常量对象的地址, 只能使用指向常量的指针

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

● 指针的类型必须与其所指对象的类型一致, 但是有两个例外:

(1) 允许令一个指向常量的指针指向一个非常量对象:

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

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


const指针


● 注意: 常量指针必须初始化, 而且一旦初始化,则它的值(也就是存放在指针中的那个地址)就不能在改变了,即指针不能指向别的对象了,

● 注意: int *const cp; //错误, 常量指针必须初始化,因为其值不能改变

● 注意: 指针本身是一个常量并不意味着不能通过指针修改其所指对象的值, 能否这样做完全依赖于所指对象的类型。


顶层const


● 用名词 顶层const表示指针本身是个常量, 而用名词 底层const 表示指针所指的对象是一个常量。

● 注意: 顶层const可以表示任意的对象是常量, 这一点对任何数据类型都适用, 如: 算术类型、类、 指针等。 底层const 则与指针和引用等复合类型的基本类型部分有关。

比较特殊的是, 指针类型既可以是顶层const也可以是底层const, 这一点和其他类型相比区别明显

注意: 用于声明引用的const都是底层const


constexpr和常量表达式


● 常量表达式 : 是指值不会改变并且在编译过程中就能得到计算结果的表达式,

字面值属于常量表达式, 用常量表达式初始化的const对象也是常量表达式,

● 注意: 一个(或表达式) 是不是常量表达式由它的数据类型和初始值 共同决定


constexpr 变量


● 关键字 constexpr 于 C++11 中引入并于 c++14 中得到改善。它表示常数表达式。与 const 相同,它可应用于变量,因此如果任何代码试图修改该值,均将引发编译器错误。与 const 不同,constexpr 也可应用于函数和类构造函数。 constexpr 指示值或返回值是常数,并且如果可能,将在编译时计算值或返回值。

● constexpr 变量:

const 和 constexpr 变量之间的主要区别在于:const 变量的初始化可以延迟到运行时,而 constexpr 变量必须在编译时进行初始化。所有 constexpr 变量均为常量,因此必须使用常量表达式初始化。

constexpr float x = 42.0;constexpr float y{108};constexpr float z = exp(5, 3);constexpr int i; // Error! Not initializedint j = 0;constexpr int k = j + 1; //Error! j not a constant expression

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

● constexpr 函数:

constexpr 函数是在使用需要它的代码时,可以在编译时计算其返回值的函数。当其参数为 constexpr 值并且在编译时使用代码需要返回值时(例如,初始化一个 constexpr 变量或提供一个非类型模板参数),它会生成编译时常量。使用非constexpr 参数调用时,或编译时不需要其值时,它将与正则函数一样,在运行时生成一个值。

#include <iostream>using namespace std;// Pass by value constexpr float exp(float x, int n){    return n == 0 ? 1 :        n % 2 == 0 ? exp(x * x, n / 2) :        exp(x * x, (n - 1) / 2) * x;};// Pass by referenceconstexpr float exp2(const float& x, const int& n){    return n == 0 ? 1 :        n % 2 == 0 ? exp2(x * x, n / 2) :        exp2(x * x, (n - 1) / 2) * x;};// Compile time computation of array lengthtemplate<typename T, int N>constexpr int length(const T(&ary)[N]){    return N;}// Recursive constexpr functionconstexpr int fac(int n){    return n == 1 ? 1 : n*fac(n - 1);}// User-defined typeclass Foo{public:    constexpr explicit Foo(int i) : _i(i) {}    constexpr int GetValue()    {        return _i;    }private:    int _i;};int main(){    // foo is const:    constexpr Foo foo(5);    // foo = Foo(6); // Error!    //Compile time:    constexpr float x = exp(5, 3);    // const float a = 2.0;    // const int  b  = 2;    // constexpr float xx = exp2(a, b); // Error!    constexpr float xx = exp2(2.0, 2);    constexpr float y{ exp(2, 5) };    constexpr int val = foo.GetValue();    constexpr int f5 = fac(5);    const int nums[]{ 1, 2, 3, 4 };    const int nums2[length(nums) * 2]{ 1, 2, 3, 4, 5, 6, 7, 8 };    cout << "The value of foo is " << foo.GetValue() << endl;}

● constexpr和指针:

还记得const与指针的规则吗?如果关键字const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针本身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。

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

const int *p     = 0; // non-const pointer, const dataconstexpr int *q = 0; // const pointer, non-const data

● 与其它常量指针类似,const指针既可以指向常量也可以指向一个非常量:

int j = 0;constexpr int i = 2;constexpr const int *p = &i; // const pointer, const dataconstexpr int *p1 = &j; // const pointer, non-const data

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

注意 : 声明为 constexpr 的变量一定是个常量, 而且必须用常量表达式初始化

constexpr int sz=size();

注意 :只有当size()这个函数也必须声明为constexpr , 那么这条声明语句才是正确的, 即不能使用普通函数作为constexpr 变量的初始值


注意 : 一般来说, 如果你认定变量是个常量表达式, 那么就把它声明为constexpr 类型

● 注意: 常量表达式的值需要在编译时就得到计算,因此对声明为constexpr 时用到的类型必须有所限制, 这些类型可以称为“字面值类型”

● 注意: 像自定义的类、IO库、 string类型则不属于字面值类型, 就不能被定义成constexpr , 算术类型、 指针和引用都属于字面值类型。

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

● 注意: 函数体内定义的变量一般来说并非存放在固定地址中, 因此constexpr 指针不能指向这样的变量。

定义于所有函数体之外的对象其地址固定不变, 能用来初始化constexpr 指针

● 允许函数定义一类有效范围超出函数本身的变量, 它们也有固定地址, 因此,constexpr 引用能绑定到这样的变量上, constexpr 指针也能指向这样的变量


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

● 注意 : constexpr把它所定义的对象置为了顶层const

● constexpr指针既可以指向常量也可以指向一个非常量


typedef和using别名声明 来声明类型别名


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

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

注意 : 含有typedef的声明语句定义的不再是变量而是类型别名, 这里的声明符也可以包含类型修饰, 从而也能由基本数据类型构造出复合类型来。

● C++11新标准还有一种新方法, 叫做 别名声明来定义类型的别名,语法格式为:

using 别名=已知的数据类型名;

注意: 已知地数据类型名可以是 基本数据类型和 用户自定义数据类型(比如: 类),

注意 : 类型别名和类型名(如:int) 等价, 只要是类型的名字能出现的地方, 就能使用类型别名。

注意 : 如果某个类型别名指代的是复合类型或常量, 那么把它用到声明语句中就会产生意想不到的结果

注意 : 遇到一条使用了类型别名的声明语句时, 会错误地尝试把类型别名替换成它本来的样子, 以理解该语句的含义: 这样的理解是错误的。


auto 类型说明符


● C++11 新标准 引入了 auto 类型说明符, 用它能让编译器替我们分析表达式所属的类型。和原来那些只对应一种特定类型的说明符(比如double)不同, auto 让编译器通过初始值来推算变量的类型 注意 : auto 定义的变量必须要有初始值

auto item=vall+va12;

说明: vall 和va12 相加的结果可以推断出 item的类型, 如果这两个变量的类型是double, 则item的类型就是double, 以此类推。

● 使用 auto 还能在一条语句中声明多个变量, 那么在该语句中所有变量的初始基本数据类型都必须一样。


复合类型、 常量和auto


● 编程时常常需要把表达式的值赋给变量,这就要求在声明变量的时候清楚知道表达式的类型。然而要做到这一点并非那么容易,有时候甚至根本做不到。为了解决这个问题,C++11标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。

● auto让编译器通过初值来推算变量类型。显然,auto定义的变量必须要有初始值。

● 使用auto具有以下几点好处:

●  可靠性:如果表达式的类型发生更改(包括函数返回值发生更改的情况),它也能工作。●  性能:确保将不会进行转换。●  可用性:不必担心类型名称拼写困难和拼写有误。●  效率:代码会变得更高效auto item = val1 + val2; // 由val1和val2相加的结果推断出item的类型auto i=0, *p = &i; // i是整数,p是整型指针

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

auto sz = 0, pi = 3.14; // Error!

● 注意 : 编译器推断出来的auto类型有时候和初始值的类型并不完全一样, 编译器会适当地改变结果类型使其更符号初始化规则,例如:

int count = 10;int& countRef = count;auto myAuto = countRef;countRef = 11;cout << count << " "; // print 11myAuto = 12;cout << count << endl; // print 11

● 你可能会认为 myAuto 是一个 int 引用,但它不是。它只是一个 int,因为输出为 11 11,而不是 11 12;如果 auto 尚未删除此引用,则会出现此情况。

● const限定符 :
(1)先引入一种表述:顶层const表示指针本身是个常量,底层const表示指针所指的对象是一个常量。一般auto会忽略掉顶层const,同时底层const则会保留下来,例如:

int i = 0;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是一个整型常量引用,绑定到ciauto &h = 42; // Error: 不能为非常量引用绑定字面值const auto &j = 42; // OK: 可以为常量引用绑定字面值

切记,符号*和&只从属于某个声明,而非基本数据类型的一部分,因此初始值必须是同一类型:

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

● 附上更多示例代码:

下面的声明等效。在第一个语句中,将变量j 声明为类型 int。在第二个语句中,将变量 k 推导为类型 int,因为初始化表达式 (0) 是整数

int j = 0;  // Variable j is explicitly type int.auto k = 0; // Variable k is implicitly type int because 0 is an integer.

以下声明等效,但第二个声明比第一个更简单。使用 auto 关键字的最令人信服的一个原因是简单

map<int,list<string>>::iterator i = m.begin(); auto i = m.begin(); 

使用 iter 和 elem 启动循环时:

#include <deque>using namespace std;int main(){    deque<double> dqDoubleData(10, 0.1);    for (auto iter = dqDoubleData.begin(); iter != dqDoubleData.end(); ++iter)    { /* ... */ }    // prefer range-for loops with the following information in mind    // (this applies to any range-for with auto, not just deque)    for (auto elem : dqDoubleData) // COPIES elements, not much better than the previous examples    { /* ... */ }    for (auto& elem : dqDoubleData) // observes and/or modifies elements IN-PLACE    { /* ... */ }    for (const auto& elem : dqDoubleData) // observes elements IN-PLACE    { /* ... */ }}

● 下面的代码片段使用 new 运算符和指针声明来声明指针

double x = 12.34;auto *y = new auto(x), **z = new auto(&x);

● 下一个代码片段在每个声明语句中声明多个符号。请注意,每个语句中的所有符号将解析为同一类型。

auto x = 1, *y = &x, **z = &y; // Resolves to int.auto a(2.01), *b (&a);         // Resolves to double.auto c = 'a', *d(&c);          // Resolves to char.auto m = 1, &n = m;            // Resolves to int.

此代码片段使用条件运算符 (?:) 将变量 x 声明为值为 200 的整数:

int v1 = 100, v2 = 200;auto x = v1 > v2 ? v1 : v2;

下面的代码片段将变量 x 初始化为类型 int,将变量 y初始化对类型 const int 的引用,将变量 fp 初始化为指向返回类型 int 的函数的指针。

int f(int x) { return x; }int main(){    auto x = f(0);    const auto & y = f(1);    int (*p)(int x);    p = f;    auto fp = p;    //...}

● 注意 : 当引用被用作初始值时, 真正参与初始化的其实是引用对象的值。 那么此时编译器以引用对象的类型作为 auto 的类型:

int i=0,&r=i;auto a =r;

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

注意 : 对常量对象取地址是一种底层const

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

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

注意 : 设置一个类型为 auto 的引用时, 初始值中的顶层常量属性仍然保留。 如果我们给初始值绑定一个引用, 则此时的常量就不是顶层常量了。

注意 : 要在一条语句中定义多个变量, 切记, 符号 & 和 * 只从属某个声明符, 而非基本数据类型的一部分, 因此初始值必须是同一种类型。


decltype 类型指示符


● decltype 类型说明符生成指定表达式的类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。

语法为:

decltype(expression ) 要求返回类型的变量 = 初始值; int i = 4; decltype(i) a; //推导结果为int。a的类型为int。

编译器使用下列规则来确定expression 参数的类型。

● 如果 expression 参数是标识符表达式或类成员访问表达式,则 decltype(expression) 是 expression 命名的实体的类型。如果不存在此类实体或 expression 参数命名一组重载函数,则编译器将生成错误消息。

● 如果 expression 参数是对一个函数或一个重载运算符函数的调用,则 decltype(expression) 是函数的返回类型。将忽略重载运算符两边的括号。

如果 expression 参数是右值,则 decltype(expression) 是 expression类型。如果 expression参数是左值,则 decltype(expression) 是对 左值引用 类型的expression。


下面的代码示例演示 decltype 类型标识符的一些用途。 首先,假定已编码下列语句。

int var;  const int&& fx();   struct A { double x; }  const A* a = new A();  

这里写图片描述


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

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

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

注意 : decltype和 auto的一处区别是 : decltype的结果类型与表达式形式密切相关

注意 :对于decltype所用的引用来说,如果变量名加上了一对括号,则得到的类型与不加括号时会有所不同。如果decltype使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果给变量加上了一层或多层括号,编译器就会把它当成是一个表达式。 变量是一种可以作为赋值语句左值得特殊表达式, 所以这样的decltype 就会得到引用类型

decltype((i)) d; // Error, d是int&, 必须初始化decltype(i) e;   // OK, e是一个未初始化的int

● 模板函数的返回类型

在 C++11 中,可以结合使用尾随返回类型上的 decltype 类型说明符和 auto 关键字来声明其返回类型依赖于其模板参数类型的模板函数。

在 C++14 中,可以使用不带尾随返回类型的 decltype(auto) 来声明其返回类型取决于其模板参数类型的模板函数。

例如,定义一个求和模板函数:

//C++11 template<typename T, typename U>auto myFunc(T&& t, U&& u) -> decltype (forward<T>(t) + forward<U>(u))    {      return forward<T>(t) + forward<U>(u);    };//C++14template<typename T, typename U>decltype(auto) myFunc(T&& t, U&& u) {  return forward<T>(t) + forward<U>(u); };

注意 : (forward:如果参数是右值或右值引用,则有条件地将其参数强制转换为右值引用。)

附上一段源码:

#include <iostream>#include <string>#include <utility>#include <iomanip>using namespace std;template<typename T1, typename T2>auto Plus(T1&& t1, T2&& t2) ->    decltype(forward<T1>(t1) + forward<T2>(t2)){   return forward<T1>(t1) + forward<T2>(t2);}class X{   friend X operator+(const X& x1, const X& x2)   {      return X(x1.m_data + x2.m_data);   }public:   X(int data) : m_data(data) {}   int Dump() const { return m_data;}private:   int m_data;};int main(){   // Integer    int i = 4;   cout <<       "Plus(i, 9) = " <<       Plus(i, 9) << endl;   // Floating point   float dx = 4.0;   float dy = 9.5;   cout <<         setprecision(3) <<       "Plus(dx, dy) = " <<      Plus(dx, dy) << endl;   // String         string hello = "Hello, ";   string world = "world!";   cout << Plus(hello, world) << endl;   // Custom type   X x1(20);   X x2(22);   X x3 = Plus(x1, x2);   cout <<       "x3.Dump() = " <<       x3.Dump() << endl;}

运行结果为:

Plus(i, 9) = 13Plus(dx, dy) = 13.5Hello, world!x3.Dump() = 42

decltype和引用


● 注意 : 如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型。但是有些时候,一些表达式向decltype返回一个引用类型。一般来说,当这种情形发生时,意味着该表达式的结果对象能作为一条赋值语句的左值:

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

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

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

注意 : decltype 类型说明符与 auto 关键字一起主要对编写模板库的开发人员有用。 使用 auto 和 decltype 声明其返回类型取决于其模板参数类型的模板函数。 或者,使用 auto 和 decltype 声明包装对其他函数的调用,然后返回包装函数的返回类型的模板函数。

阅读全文
0 0