2.1 基本内置类型
- C++定义了一套包括 算术类型(arithmetic type) 和 空类型(void) 在内的基本数据类型。
- 其中算术类型包括了字符、整型数、布尔值和浮点数。
- 空类型: 不对应任何具体的值,不能定义 void 类型变量。
2.1.1 算术类型
- 算术类型分为两类:整型(包括字符和布尔类型在内)和浮点型。
- C++中 算术类型的长度(也就是所占字节)在不同系统是有所差别的 ,所表示的数据范围也不同。
- 基本字符类型是char, 一个char的内存应确保可以存放机器基本字符集中任意字符对应的值 ,所以它的大小一般是一个字节,也就是8位。
- 其它字符类型(如char16_t、wchar_t、char32_t)则用于扩展字符集 。其中wchar_t类型确保可以存放机器最大扩展字符集中任意一个字符。而char16_t和char32_t则为了Unicode字符集服务。
- C++规定了一个int至少和一个short一样大,一个long至少和一个int一样大,一个long long至少和一个long一样大。其中long long是在C++11中新定义的。
- 可寻址的最小内存块称为 “字节”(byte) ,大多数机器的字节为8 位(bit) ; 计算机中进行数据存储和数据处理的运算的基本单元 被称为 “字”(word) ,它的长度(即字长)通常是几个字节,现代计算机多为16位、32位、64位。
- 大多数计算机将内存中的每一个字节与一个数字关联起来,这个数字就是 地址(address) 。
- 程序员可使用 某个地址 来表示 从这个地址开始的一定大小的内存块 。但是如果要知道该地址的具体含义,则要知道储存在该地址的数据的类型,因为 类型决定了数据所占的内存块大小和如何解释该内存块中的内容。
- 浮点型可以表示单精度,双精度和拓展精度值。
- C++标准指定了一个浮点数有效位数的最小值,然而大多数编译器都实现了更高的精度。通常float是4个字节、double是8个字节。 一般来说,类型float和double分别有7和16个有效位。
- 除了布尔型和拓展的正字符型外,其它整型还可以分为有符号和无符号两种。 其中 int 、 short 、 long 、 long long 都是带符号的。只需在前面添加 unsigned 即可得到无符号类型。
- unsigned int 的简写是 unsigned 。
- 与其它整型不同,字符类型分为三种 :char、signed char和unsigned char。 其中char与signed char是不一样的。
- 尽管字符型有三种,但表达型只有两种:有符号和无符号。而char实际上表示为这两种之一,具体是哪种由编译器决定。
- 执行浮点运算时使用double , 这是因为double精度比float更大,同时与float的计算速度相差不多,甚至在某些计算机上运算速度还要快于float。
2.1.2 类型转换
- 在类型定义的类型对象能参与的运算中,有一种运算能被大多数类型支持,就是类型的转换。
- 布尔类型与其它类型的相互转换: 0->false,非0值->true;true->1、false->0
- 浮点类型与整型的相互转换: 浮点型->整型,进行近似处理,截断小数部分;整型->浮点型,小数部分记为0,如果该整型内存大于浮点型,则可能会发送精度的丢失。
- 如果赋值给有符号类型一个超出其表示范围的值,它的结果是未知的,因为这个行为是未定义的。
- 未定义的(undefined): 无法预知的行为,源于编译器无需(有时是不能)检测的错误。即使代码编译通过了,仍有可能产生错误,生成与期望不符的垃圾数据。
- 切勿混用有符号类型和无符号类型: 如果表达式里有无符号类型,则整个表达式的有类型将会被自动转换成无符号类型,然后进行运算。
2.1.3 类字面值
- 每个 字面值(literal) 都对应了一种数据类型,而它的 形式和值 决定了它的数据类型。
- 整型字面值: 可以表现为十进制数、八进制数、十六进制数的形式。 默认情况下,十进制常量是有符号类型。八进制和十六进制可能是有符号型也可能是无符号型。 而十进制常量的类型是int、long和long long 中能容纳其值且长度最小的那个。八进制(以0开头)和十六进制(0x或0X开头)则是 int 、 unsigned int 、long 、 unsigned long 、 long long 和 unsigned long long 中长度最小的那个。
- short 没有对应的整型字面值。
- 浮点型字面值: 表示为一个小数或以科学计数法表示的指数,其中指数部分使用 E 或 e 标识。默认情况下,浮点型常量是一个 double 类型。
- 字符字面值: 使用单括号括起来的一个字符,比如 'a' 、 '\n' 。
- 字符串字面值: 使用双引号括起来的零个或多个字符,比如 " " 、 "ads da" 。 C++的字符串字面值其实是由字符常量构成的,最后一个元素为空字符('\0')的 char数组(array)。
- 如果两字符串相邻,且仅由 空白(比如制表符、换行符、空格) 分隔,那么它俩其实是一个相连的整体。
- 转义序列(escape sequence): 用于打印两类字符: 计算机中不可打印的字符 ,比如退格、换行等;以及 C++中具有特殊含义的字符 ,比如单双引号、问号、反斜线等。
- 泛式的转义序列: 其形式是\x后紧接1个或多个十六进制数字,或者\后紧接1~3个八进制数字,其中数字部分标识的是字符对应的数字。 如果\后紧接的八进制数字超过3个,则只有前三个数字与\构成转义序列。因为char类型一般为8位。
- 布尔字面值: true和false。
- nullptr 是一个指针字面值。 表示空地址。
- 通过添加前缀和后缀,可以改变整型、浮点型、和字符型常量的数据类型。
字符和字符串字面值
前缀 | 含义 | 类型 |
---|---|---|
前缀u | Unicode16字符 | char16_t |
前缀U | Unicode32字符 | char32_t |
前缀L | 宽字符 | wchar_t |
前缀u8 | UTF-8 | char |
整型常量
后缀 | 最小匹配类型 |
---|---|
U or u | unsigned int |
L or l | long |
LL or ll | long long |
浮点型字面值
后缀 | 类型 |
---|---|
F or f | float |
L or l | long double |
2.2 变量
- C++中每个变量都有其数据类型,数据类型决定了变量所占内存空间大小、所能储存的值的范围,以及其能参与的运算。
2.2.1 变量定义
- 变量定义的基本形式: 类型说明符(type specifier) 后接一个或多个变量名组成的列表,同时声明多个同类型变量,变量名之间使用逗号分隔,以分号结束。定义时还可以对指定的变量进行 初始化(initialized) 。
- 对象(object) 和变量其实是不同的叫法。都是指一块能存储数据并具有数据类型的内存空间。
- 在C++中,初始化和赋值是两种完全不同的操作。 初始化不是赋值,初始化的含义是指在声明新变量的同时赋予一个值。而赋值的含义是消除变量之前储存的值,然后用一个新值来替换。
- C++定义了多种不同的初始化形式。比如:
几种不同的初始化方式
int a = 0;
int b = {0};
int c{0};
int d = (0);
int e(0);
- 作为C++11新标准的一部分,列表初始化得到全面应用,在这之前只能在某些场合使用列表初始化。现在无论是 初始化对象还是对变量赋新值 ,都可以使用列表初始化。
- 列表初始化: 使用花括号来初始化变量。 当用于内置类型时的好处:使用列表初始化且初始值存在丢失信息的风险时,编译器会报错。 比如使用列表初始化将 double 类型的值初始化一个 int 变量。
- 默认初始化(default initialized): 如果定义变量时没有进行初始化,则变量被默认初始化。 至于默认值是什么,则由 变量的类型和定义变量的位置决定。
- 如果是 内置类型 ,则 它的初始值由定义的位置决定 。定义于任何函数之外的变量被初始化为0。然而一种例外是:定义在函数体内部的内置类型变量将不被初始化,其值是未定义的。
- 每个类的设计者各自决定其初始化对象的方式,而且,是否允许不经初始化就定义对象,也由类的设计者自己决定。如果类的设计者允许默认初始化,它将决定对象的初始值是什么。
2.2.2 声明和定义的关系
- C++支持 分离式编译(separate compilation) ,该机制允许将程序分割成若干文件,每个文件都可以被单独编译。这样有利于把程序拆成多个逻辑部分来编写。
- 声明(declaration):规定了数据的名字和类型,使得名字为程序所知。
- 定义(definition):在声明的基础上,定义还申请了存储空间并为其赋初值。
- 如果想声明一个变量而非定义它,就在变量名前加关键字 extern ,并且不要显式地初始化。如果使用了 extern ,但显式初始化了,则变成了定义。注意:如果在函数体内部试图初始化一个使用extern声明的变量,编译器会报错。
- 任何包含了显示初始化的声明即为定义。
- 变量能且只能被定义一次,但是可以被多次声明。
- 如果要在多个文件中使用同一个变量,则必须将声明和定义分开。 此时变量的定义能且只能出现在一个文件中,而用到该变量的文件必须对其进行声明,但不能进行重复定义。
- C++是一种 静态类型(statically typed) 的语言,其含义是:在编译阶段检查类型。其中,检查类型的过程被称为 类型检查(type checking) 。
- 任何包含有显式初始化的声明,都是定义。
extern int a; //声明
extern int b = 10; //声明并定义
//多次声明
extern int e;
extern int e;
extern int e;
2.2.3 标识符
- C++的 标识符(identifier) 由字母、数字和下划线组成,其中不能以数字开头。
- 标识符的长度没有限制,但是 C++区分大小写 。
- C++保留了一些名字供语言使用 ,这些名字不能被用作标识符,比如 关键字 。
- 变量命名规范:
- 标识符要能体现含义;
- 一般使用小写字母;
- 用户自定义的类名一般以大写字母开头;
- 如果标识符有多个单词组成,单词之间应该有明显区分。
- 最好别使用连续两个下划线开头,或以一个下划线后接大写字母开头的标识符。这些一般是C++的保留名。
2.2.4 名字的作用域
- 作用域(space) 是程序的一部分,在其中名字有特定的含义。 C++中的作用域都以花括号分隔。
- 同一个名字在不同的作用域中可能指向不同的实体。
- 名字的有效区域是从名字的声明语句开始,以声明语句所在作用域的末端为结束。
- 全局作用域(global scope) 中声明的名字在整个程序的范围内都可以使用。比如main函数。
- 不在全局作用域中声明的变量,都具有 块作用域(block scope) 。这里的块是指函数体和语句块。
- 作用域能实现嵌套,被包含(嵌套)的作用域被称为 内层作用域(inner scope) ,包含别的作用域的作用域被称为 外层作用域(outer scope) 。
- 某个作用域一旦声明了某个名字,它的所有内层作用域都可以访问该名字。同时允许在内层作用域中重新定义外层作用域已有的名字。
- 可以使用作用域操作符(::)来覆盖默认的作用域规则。当作用域操作符左侧为空时,则请求获取作用域操作符右侧名字对应的全局作用域中的变量。
#include <iostream>
int a = 2;
int main()
{
long a =3; //在内层作用域中重新定义外层作用域已有的名字
cout << ::a;
//此时使用的是全局作用域中的a
}
2.3 复合类型
- 复合类型(compound type): 基于其它类型定义的类型。
- 一条声明语句由一个 基本数据类型(base type) 和紧接着的一个 声明符(declaration)列表 组成。而简单的声明符就是变量名,复杂点的就类似于指针的 * 变量名。如果在一条声明语句中声明多个变量,则它们的基本数据类型都是相同的,因为一条声明语句只有一条基本数据类型。
2.3.1 引用
- 引用(reference): 为对象起了别名。通过将声明符写成 &d 的形式,其中的d是变量名。
- C++11新增了右值引用(rvalue reference),主要用于内置类。大部分情况下,使用的引用都是 左值引用(lvalue reference) 。
- 与一般初始化变量是通过拷贝=右侧运算对象的值,然后储存到左侧运算对象不同。定义引用时,程序将引用与初始值绑定,而不是拷贝后储存到新对象。一旦定义完成,引用与它的初始值对象将会 一直绑定 。
- 引用必须初始化。
- 引用并非对象,只是别名 。所以它没有实体,也就是没有内存和值。
- 又由于引用并非对象,所以 不能定义引用的引用 。
int main(){
int ival=1024;
int &refVal=ival;
int &(&refVal2)=refVal; //不允许定义对引用的引用
}
- 定义一个引用后,对引用的所有操作都是对于它绑定的对象上进行 。
- 允许在一条语句中定义多个引用,但是每个声明符都要是&d的形式。
- 除了两种特殊情况 (const 引用可绑定非 const 对象,基类引用可绑定派生类对象) , 引用的类型都要与与之绑定的对象严格匹配。而且不能与字面值或某个表达式的计算结果绑定。
2.3.2 指针
- 指针(pointer):是指向(refer to)另外一种类型的复合类型。
-
指针与引用的不同:
- 指针本身是一个对象,有自己的内存和值,允许对其进行拷贝和赋值。引用不是。
- 指针可以改变指向的对象。引用不行。
- 指针不要求初始化。引用必须初始化。
- 与其它内置类型相同的是, 在块作用域内未初始化的指针的值是未定义的。
- 允许在一条语句中定义多个指针,但是每个声明符都要是 *d 的形式,其中d是变量名。
- 因为引用不是对象,没有自己的地址,所以 不能定义指向引用的指针 。
int s= 2;
int& *a = ss; //这是不允许的,编译器会报错
- 除了两种特殊情况(const 指针可指向非 const 对象,基类指针可指向派生类对象),指针的类型都要与它所指向的对象严格匹配。
- 指针存放某个对象的地址,要想获取某个对象的地址,要使用 取地址运算符(&) 。
- 指针值应该属于下列4种状态之一:
- 指向一个对象;
- 指向紧邻对象所占空间的下一个位置;
- 空指针,意味着其没有指向任何对象;
- 无效指针,除上述三种情况以外的其它值。
- 试图拷贝或以其它方式访问无效指针的值都将引发错误 ,这种行为是未定义的,编译器不会检查此类错误。
- 尽管第2、3种形式的指针是有效的,但试图访问这类指针的值是不被允许的,后果是 未定义的 。
- 如果指针指向一个对象,则允许使用 解引用符( * ) 来访问该对象。
- 运算符可以通过重载来获得多种意义。
- 空指针(null pointer): 该指针不指向任何对象。
- 可以通过三个方法来初始化指针为空指针:
- 使用字面值 nullptr 来初始化指针。 nullptr 是C++11刚引入的一种特殊类型的字面值,它可以被转换成任意其他的指针类型;
- 直接使用字面值0对指针初始化来生成空指针;
- 以前还使用名为 NULL 的 预处理变量(preprocessor variable) 来给指针赋值,这个 宏(也就是预处理变量) 在头文件 cstdlib 中定义,它的值就是0;
- 预处理变量不属于命名空间 std ,它 由预处理器负责管理 ,因此我们可以直接使用预处理器变量而无需在前面加上 std:: 。 预处理器是运行于编译器之前的一段程序。
- 当使用到一个预处理变量时,在编译之前,预处理器会自动地将程序里全部的同名预处理变量替换成实际值。
- C++11之后,现在的C++程序最好使用 nullptr ,尽量避免使用NULL。
- 所以不论是作为条件出现还是参与比较运算,都应该使用合法指针, 使用非法指针作为条件或进行比较都是未定义的。
- void * 是一种特殊的指针类型,可用于存放任意对象的地址。
- 不能直接操作 void* 指针所指向的对象,因为不知道它指向什么类型的指针。只能用于拿它和别的指针比较、作为函数的输入或输出,或者赋值给另外一个 void* 指针。
2.3.3 理解复合类型的使用
- 类型修饰符只是声明符的一部分。
- 通过 * 的个数可以区分指针的级别。
- 如果要判断复合类型的类型:从右向左阅读该声明语句,离标识符最近的且优先级最高的类型修饰符,就是它的复合类型。至于是指向什么类型,可通过声明符剩下的部分来判断。
2.4 const 限定符
- 通过使用修饰符 const ,可以创建一个常量。
- 由于 const 对象创建之后就不能再修改,所以 const 对象必须初始化。
- 编译器将 在编译过程中 把所有用到 const 对象的地方 替换 成初始化的值。而预处理器是在 编译前替换 。
- 如果程序包含多个文件,则每个用了 const 常量的文件都必须得能访问到它的初始值才行,只有这样,编译器才能执行对 const 常量的替换。这要求在每一个用到 const 常量的文件中都有对它的定义。
- 默认情况下,const 常量被设定为仅在本文件内有效。当多个文件中出现同名的 const 常量时,其实相当于在不同文件中分别定义了独立的 const 常量。这样避免了对同一 const 常量的定义。
- 通过对 const 常量的声明和定义都使用 extern 关键字,可以做到只在一个文件中定义 const 常量,然后在其它多个文件中声明并使用它。
- 最好在源文件中定义 extern const 常量,在头文件中声明该 extern const 常量。这样可以避免多个头文件包含该头文件进而导致的重定义。
2.4.1 const 的引用
- 可以把引用绑定到 const 常量上,这被称为 对常量的引用(reference to const) 。 不能 通过对 const 的引用来改变其绑定的 const 常量的值。
- 并不存在“常量引用”,这只是个 简称 。实际上只有 对 const 的引用 。 因为引用不是一个对象,没法确保其永远不变,所以它也就没有顶层 const 。
- 作为引用的类型匹配的两个例外之一: 在初始化 const 常量引用时,允许任意表达式作为其初始值 。只要它能转换成引用的类型即可,也就是类型相匹配。尤其, 允许为一个对 const 的引用绑定非常量的对象、字面值,甚至是个一般表达式。
编译器是如何实现这种特殊情况的:double dval = 3.14; const int &val = dval;
编译器为了确保 val 绑定的是一个整型常量,它将上述代码变成下面这样。
double dval = 3.14; const int temp = dval; const int &val = temp;
这样做使得 val 绑定了一个 临时量(temporary)对象 。所谓临时量对象就是 当编译器需要一个空间来暂时存储表达式的求值结果时,临时创建的一个未命名的对象。
- 对 const 常量的引用只是对该引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未作限定。 也就意味着, 如果引用的对象本身不是一个常量,则可以通过其它途径来改变它的值。
2.4.2 指针和 const
-
指向常量的指针(pointer to const): 不能通过指向 const 的指针来改变其所指对象的值。比如
const int* a = 2;
。 - 指针常量只是对该指针可参与的操作做出了限定,对于它指向的对象本身是不是一个常量未作限定。 也就意味着, 如果指向的对象本身不是一个常量,则可以通过其它途径来改变它的值。
- 编译器是通过与上文一样的方法——临时量,通过让指针指向该临时量,从而实现这种特殊情况。
- 常量指针(const pointer): 将指针本身声明为常量。
- 由于常量指针是常量,所以 常量指针必须初始化 。
- 当指针为常量指针时,是不能修改其指向的对象,但是可以通过它修改指向对象的值。
2.4.3 顶层 const 与底层 const
- 顶层 const (top-level const): 表示变量或对象本身是一个常量。
- 底层 const (low-level const): 表示指针变量指向的对象本身或者被引用的对象本身是一个常量—— 该对象不一定是常量,只是说无法通过该指针对其值进行修改。
- 顶层 const 可以表示任意的对象是常量,这对于任意数据类型都适用 ,比如算术类型、类、指针等,至于引用,引用只是别名,没有实体,又怎么能有 top-level const 呢。 但底层 const 则与指针和引用等复合类型的基本数据类型部分相关。
- 当执行对象的拷贝操作时, 拷入和拷出的对象都必须具有相同的底层 const ,或是 两个对象的数据类型必须能转换 。
比如:
const int * const p3 = p2;
int *p = p3; // 错误 : p3 包含底层 const 的定义,而 p 没有
- 常量指针 (是 top-level-const )和 指针常量 (是 low-level-const )
2.4.4 constexpr 和常量表达式
- 常量表达式(const expression): 是指值不会改变并且在编译过程中就能得到计算结果的表达式。
- 一个对象或表达式是不是常量表达式由它的数据类型和初始值共同决定。
- C++11规定,允许将变量声明为 constexpr 类型 以便 由编译器来验证变量的值是否是一个常量表达式 。因为 声明为 constexpr 类型的对象必须要用常量表达式初始化 。
const 常量是可以用函数的返回值初始化的,而函数的返回值在编译过程中未知,在运行时才知道,所以这种不是常量表达式。 - 不能用普通函数作为 constexpr 变量的初始值,但是可以使用C++11中的一种新函数—— constexpr 函数。这种函数一定要足够简单以至于在编译时就计算出其结果,这样就可以用 constexpr 函数来初始化 constexpr 变量。
- 字面值类型(literal type): 值可以在编译时得到的类型。而字面值类型是 constexpr 变量的类型,可以从 constexpr 函数中生成、操作并返回它。
- 算术类型、引用、指针等一些特殊的类型都属于字面值类型。比如
constexpr int a = 1;//这里的 int 就是字面值类型
- 而程序员的自定义类、IO库的类、string 类型则不属于。比如
constexpr string b = "ss";//这是错误的
,因为 string 不是字面值类型,所以 "ss" 的值不能在编译时得到,不能用于初始化 constexpr 变量。- 全部的字面值类型
- 尽管指针和引用都可以定义成 constexpr ,但它们的初始值却有严格限制。 一个 constexpr 的指针和初始值必须是 nullptr 或者 0 ,或者是储存于某个固定地址的对象。
- 与 const 不同的是, constexpr 对于指针没有 low 和 top 之分,只对指针起修饰作用——将指针声明为常量指针 ,与指针所指对象无关。
constexpr int *q = nullptr; //这是一个指向 int 类型的常量指针。
- 与其它常量指针相似的是,constexpr 指针常量( constexpr 加 low-level-const )可以指向常量或非常量。
constexpr int *np = nullptr; // np 是一个指向整数的常量指针,其值为空
int j = 0, l = 0;
constexpr int i = 42; // i 的类型是整型常量
// i 和 j 都必须定义在函数体之外
constexpr const int *p = &i; // p 是常量指针,也是指针常量,指向整型常量 i
constexpr const int *p = &l; // p 是常量指针,也是指针常量,指向整型变量 l
constexpr int *p1 = &j; // p1 是常量指针,指向整型变量 j
2.5 处理类型
2.5.1 类型别名
- 类型别名(type alias): 是一个名字,它是某种类型的别名。使得复杂类型名字变得简单,使用更方便,提升代码的可读性。
- C++有两种方法用于定义类型别名:使用 关键字 typedef 和 使用C++11中新增的 别名声明(alias declaration) 来定义类型别名。
- 其中的 typedef 是作为声明语句中基本数据类型的一部分出现。使用了 typedef 定义的就不是变量了,而是类型别名。要注意的是,可以在声明符中使用类型修饰,从而定义出复合类型的类型别名。
typedef double wave; //wave 是 double 的别名
typedef wave base, *p ; //base 是 double 的别名,p 是 double* 的别名
- C++11新增的 别名声明 是通过 使用关键字 using 作为别名声明的开头,其后紧跟别名和等号,最后则是数据类型 。比如
using UI = int
,定义了一个 int 的类型别名 UI 。
using int_array4 = int[4];
等价于
typedef int int_array4 [4];
- 如果某个类型别名指代的是复合类型或常量 ,那么把它用到声明语句里会产生预料以外的结果。比如:
typedef char * pstring;
其中 pstring 是 char* 的类型别名。指代一个复合类型——指向 char 的指针。
const pstring cstr = nullptr; //这里的 cstr 是指向一个 char 变量的常量指针
//两个定义并不等价
const char *cstr; //这里的 cstr 是指向一个 char 常量的指针
遇到一条使用类型别名声明的语句,直接将类型别名替换成它指代的复合类型来理解,这种 理解是错误的 。
由于 const 在类型左侧,所以这里的 const 限定的是基本数据类型为常量。
因为第一条声明语句中的 pstring 是基本数据类型,所以其指代的 char * 在这里作为基本数据类型, 此处 * 不是声明符的一部分 ,所以 const 限定的是 char * 为常量 ——也就是指向一个 char 变量的 const 指针。
而 第二条声明语句的 * 是声明符的一部分, const 限定的是 char 为常量 ——也就是指向一个 char 常量的指针。
2.5.2 auto 类型说明符
- C++11新增 auto 类型说明符——用于让编译器通过初始值来推算变量的类型。
- 由于 auto 需要知道初始值,所以 auto 定义的变量必须初始化 。
- 使用 auto 也能在一条语句中声明多个变量,但是因为一条声明语句只能用一个基本数据类型, 所以 auto 在一条语句中定义的多个变量必须是同类型,所以它们的初始值必须是同类型 。
- 编译器推断出来的 auto 类型和初始值的类型不一定完全一样 ,编译器会适当改变结果类型使其更符合初始化规则。
- 使用引用其实是在使用它引用的对象,特别是使用它作为变量的初始值时, 真正参与初始化的其实是引用对象的值,此时编译器以引用对象的类型作为 auto 的类型 。
- auto 一般会忽略掉初始值的顶层 const ,同时底层 const 会保留。 ——这意味着使用 const 常量给 auto 变量初始化时,auto 并不会自动将变量声明为 const 常量。如果使用指向 const 常量的指针对 auto 变量初始化,则该 auto 变量会被声明为指向 const 常量的指针。
int i =10, &r = i;
const int ci = i, &cr = ci;
auto a = ci; //a 是一个 int 类型的变量,auto 忽略掉了 ci 的顶层 const
auto b = cr; //b 是一个 int 类型的变量,auto 忽略掉了 ci 的顶层 cosnt
auto c = &i; //c 是一个指向 int 类型的指针( i 的地址就是指向 int 的指针)
auto d = &ci; //d 是一个指向 const int 的指针(因为 ci 是 const int 类型的常量,它的地址相当于指向 const int 的指针),保留了底层 const
- 如果希望定义一个常量的 auto 对象,也就是顶层 const ,需要显式地指出:
const auto int app = 2;
- 但是如果将引用的类型声明为 auto ,此时的初始化规则仍然适用。
const int a = 233;
auto &a = ci; //正确,a是一个整型常量引用(因为对 const 的引用相当于底层 const )
auto &b = 233; //错误,233是一个 int 类型的字面值,不是一个 const int
const auto &c = 233; //正确:可以为对常量引用绑定字面值。
为什么第一条语句成立,而第二条就不成立呢?那是因为233的类型虽然是 int 类型,但是它是个字面值(常量),而 b 被 auto 定义为对 int 的引用,所以不匹配,应该改为第三条语句。
而第一条语句中 ci 是 const int 类型的常量,而且 对于 const int 类型的引用是 low-level const (底层 const) ,所以保留。
2.5.3 decltype 类型说明符
- C++11 新增第二种类型说明符 decltype ,它的作用是选择并返回操作数的数据类型。 一般用于希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。
-
decltype (表达式)
:编译器通过分析表达式并得到它的类型,却 不实际计算表达式的值。 - decltype 处理 top-level const (顶层 const )和引用的方式与 auto 有些不同。 如果 decltype 使用的表达式是一个变量,则 decltype 返回该变量的类型(包括顶层 const 和引用在内)。 这与 auto 是不一样的。
-
decltype 返回的数据类型的规则如下: 后面学到左值和右值会有新的认识
有 decltype(表达式)
①表达式是 标识符、类访问表达式 ,decltype 返回的类型 和表达式的类型一致
②表达式是 函数调用 ,decltype 返回的类型 和函数返回值的类型一致
③表达式是 解引用操作 ,则 decltype 返回的是 对表达式类型的引用 , 否则和表达式类型一致 。
④表达式是 算术运算表达式 ,则 decltype 返回的类型 和表达式结果的类型一致 。比如1 + 2
、1 + ass
等。
⑤表达式是 赋值表达式 ,则 decltype 返回的是 对表达式结果类型的引用。比如i += 1
、i = 2
等。 -
如果给 decltype(变量) 中的变量加上多个小括号(),decltype 都一定会返回关于对该对象类型的引用。因为变量是一种可以作为赋值语句左值的特殊表达式。 比如
decltype ((i)) d = i;
。
2.6 自定义数据结构
- 从最基本的层面理解,数据结构是把一组相关的数据元素组织起来然后使用它们的函数方法。
- 类的其中一种的定义 格式 ( struct + 类名 + 类体)如下:
struct name {
结构成员
};
其中类体 包括且由花括号围成 ,类体是 可以省略 的。
- 可以在类体后紧跟变量名以定义该结构类型的新变量。
struct name {
结构成员
} accum, trans, *salesptr;
- 类体里定义了类的成员,其中的数据成员(data member)定义了每一个类对象的具体内容。 不同的类对象,哪怕同类型,它们的数据成员是独立的。
-
C++11规定,可以为数据成员提供一个类内初始值(in-class initializer)。创建新对象时,类内初始值将被用于初始化数据成员。而没有类内初始值的数据成员,将执行默认初始化。
对于类内初始值的限制:①使用等号②等号+花括号③只使用花括号④不能使用圆括号()
struct Sales_date {
std::string bookNo;
int a = 0;
double dd {0.0};
float ff={0.0f};
};
- 除了 struct 以外,还可以使用 关键字 class 来定义类。
- 如果要在不同的文件中使用同一个类,类的定义就必须保持一致。而要保持类的定义一致,通常是将类定义在头文件中。而且类的名字应该与其所在头文件名一样。
-
预处理器(preprocessor): 在编译之前执行的一段程序 ,可以部分改变程序员所写的程序。比如
#include
和#define
。 同时还可以确保头文件多次包含仍能安全运行的常用技术。 - C++还会使用一项预处理功能—— 头文件保护符(header guard) , 头文件保护符依赖于预处理变量 。 预处理变量有两种形态:已定义和未定义。
-
#ifdef
和#ifndef
: 预处理指令,分别检查某个指定的预处理变量是否已经定义。 :#ifdef
当且仅当 变量已定义时为真 ,#ifndef
当且仅当 变量未定义时为真 。一旦检查结果为 真,则执行后续操作直到遇到#endif
为止。
#ifndef SALES_DATE_H
#define SALES_DATE_H
#include <string>
struct Sales_date {
std::string bookNo;
int a = 0;
float ff={0.0f};
};
#endif
第一次包含头文件 Sales_date.h 时,先插入头文件内的代码,然后检查 #ifndef
指令,为真则按顺序执行后续代码直到遇到 endif
。
- 预处理器无视C++中关于作用域的规则。
- 一般将预处理变量的名字全部大写。
- 这个程序的预处理变量包括头文件保护符应该是唯一的。