C++ Primer 第2章 变量和基本类型
- 2.1 基本内置类型
- 2.1.1 算术类型
- 一、带符号类型和无符号类型
- 练习
- 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 指针
- 一、获取对象的地址
- 二、指针值
- 三、利用指针访问对象
- 四、空指针
- 五、赋值和指针
- 六、其他指针操作
- 七、void*指针
- 2.3.3 理解复合类型的声明
- 一、定义多个变量
- 二、指向指针的指针
- 三、指向指针的引用
- 2.4 const限定符
- 一、初始化和const
- 二、默认状态下,const对象仅在文件内有效
- 2.4.1 const的引用
- 一、初始化和对const的引用
- 二、对const的引用可能引用一个并非const的对象
- 2.4.2 指针和const
- 一、const指针
- 练习
- 2.4.3 顶层const
- 2.4.4 constexpr和常量表达式
- 一、constexpr变量
- 二、字面值类型
- 三、指针和constexpr
- 2.5 处理类型
- 2.5.1 类型别名
- 一、指针、常量和类型别名
- 2.5.2 auto类型说明符
- 一、复合类型、常量和auto
- 2.5.3 decltype类型指示符
- 一、decltype和引用
- 练习
- 2.6 自定义数据结构
- 2.6.1 定义Sales_data类型
- 一、类数据成员
- 2.6.2 使用Sales_data类
- 一、添加两个Sales_data对象
- 二、Sales_data对象读入数据
- 三、输出两个Sales_data对象的和
- 2.6.3 编写自己的头文件
- 一、预处理器概述
- 小结
2.1 基本内置类型
C++定义了一套包括算术类型和空类型在内的基本数据类型。其中算术类型包含了字符、整型数、布尔值和浮点数。空类型不对应具体的值,仅用于一些特殊的场合。
2.1.1 算术类型
算术类型分为两类:整型(包括字符和布尔类型在内)和浮点型。
布尔类型:取值是真或假。
字符类型:
(1)char:一个char的空间应确保可以存放机器基本字符集(ASCII码)中任意字符对应的数字值。一个char的大小和一个机器字节一样。
(2)其他字符类型用于扩展字符集:wchar_t、char16_t、char32_t。wchar_t类型用于确保可以存放机器最大扩展字符集中的任意一个字符。类型char16_t和char32_t为Unicode字符集服务(Unicode是用于表示所有自然语言中字符的标准)。
(3)其他整型:表示不同尺寸的整数(short <= int <= long <= long long)
计算机以比特序列存储数据,每个比特非0即1。大多数计算机以2的整数次幂个比特作为块来处理内存,可寻址的最小内存块称为“字节”,存储的基本单元称为“字”,它通常由几个字节组成。在C++语言中,一个字节要至少能容纳机器基本字符集中的字符。大多数机器的字节由8比特构成,字则由32或64比特构成,也就是4或8字节。大多数计算机将内存中的每个字节与一个数字(“地址”)关联起来。(字:在指定机器上进行整数运算的自然单位)
浮点型:
(1)单精度:float,以1个字(32比特)来表示,7个有效位
(2)双精度:double,以2个字(64比特)来表示,16个有效位
(3)扩展精度:long double,以3或4个字(96或128比特)来表示,常被用于有特殊浮点需求的硬件、
一、带符号类型和无符号类型
除去布尔型和扩展的字符型之外,其他整型可以划分为带符号的和无符号的两种。
带符号的可以表示正数、负数和0(8比特的signed char表示-128 ~ 127),无符号类型仅能表示大于等于0的值(8比特的unsigned char表示0 ~ 255)。
int、short、long、long long默认为带符号的
unsigned int(也可写成unsigned)、unsigned short、unsigned long、unsigned longlong为无符号的
signed char、unsigned char(char实际上会表现为上述两种形式的一种,具体是哪种由编译器决定)
选择类型注意事项:
(1)当明确知晓数值不可能为负时,选用无符号类型
(2)使用int执行整数运算。在实际应用中,short常常显得太小而long一般和int有一样的尺寸。如果数值超过了int的表示范围,选用long long。
(3)在算术表达式中不要使用char或bool。只有在存放字符或布尔值时才使用它们。因为类型char在一些机器上是有符号的,而在另一些机器上又是无符号的,所以如果使用char进行运算特别容易出问题。如果需要使用一个不大的整数,那么明确指定它的类型是signed char或unsigned char。
(4)执行浮点运算选用double。这是因为float通常精度不够且双精度浮点数和单精度浮点数的计算代价相差无几。long double提供的精度在一般情况下是没有必要的,且它带来的运行时小号也不容忽视。
练习
在C++语言中,int、long、long long和short都属于整型,区别是C++标准规定的尺寸的最小值(即该类型在内存中所占的比特数)不同。其中,short是短整型,占16位;int是整型,占16位;long和long long是长整型,分别占32位和64位。C++标准允许不同的编译器赋予这些类型更大的尺寸。某一类型占的比特数不同,它所能表示的数据范围也不一样。
2.1.2 类型转换
当把一个整数值赋给浮点类型时,小数部分记为0。如果该整数所占的空间超过了浮点类型的容量,精度可能有损失。
当给无符号类型赋值一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。
当给带符号类型赋值一个超出它表示范围的值时,结果是未定义的。此时,程序可能继续工作、可能崩溃,也可能产生垃圾数据。
数值在计算机中以补码形式存储
正数:原码=反码=补码(最高位0)
负数:(最高位1)反码:除符号位其余取反,补码:反码+1
unsigned char c = -1; // 假设char占8比特,c的值为255
-1 原码:1000 0001
反码:1111 1110
补码:1111 1111————>255
signed char c2 = 256; // 假设char占8比特,c2的值是未定义的
一、含有无符号类型的表达式
unsigned u = 10;
int i = -42;
// 当一个算术表达式中既有无符号数又有有符号数时,有符号数会转换成无符号数
std::cout << i + i << std::endl; // -84
std::cout << u + i << std::endl; // 如果int占32位,输出4294967264
/*
-42
原码:1000 0000 0000 0000 0000 0000 0010 1010
反码:1111 1111 1111 1111 1111 1111 1101 0101
补码:1111 1111 1111 1111 1111 1111 1101 0110
10
补码:0000 0000 0000 0000 0000 0000 0000 1010
相加:1111 1111 1111 1111 1111 1111 1110 0000————> 4294967264
*/
// 当从无符号数减去一个值时,不管这个值是不是无符号数,都必须确保结果不能是一个负值
unsigned u1 = 42, u2 = 10;
std::cout << u1 - u2 << std::endl; // 32
std::cout << u2 - u1 << std::endl; // 4294967264
/*
10
补码:0000 0000 0000 0000 0000 0000 0000 1010
42
补码:0000 0000 0000 0000 0000 0000 0010 1010
相减:1111 1111 1111 1111 1111 1111 1110 0000————> 4294967264
*/
// 注意!变量u永远也不会小于0,循环条件一直成立,死循环
for (unsigned u = 10; u >= 0; --u)
std::cout << u << std::endl;
2.1.3 字面值常量
一、整型和浮点型字面值
1、整型字面值具体的数据类型由它的值和符号决定。默认情况下,十进制字面值是带符号数(int、long、long long中能容纳的尺寸最小的那个),八进制和十六进制字面值既可能是带符号的也可能是无符号的(int、unsigned int、long、unsigned long、long long、unsigned long中能容纳的尺寸最小的那个)。
数值20:
(1)十进制:20
(2)八进制:024
(3)十六进制:0x14或0X14
2、浮点型字面值表现为一个小数或以科学计数法表示的指数,其中指数部分用E或e标识。默认情况为double类型。
3.14159 3.14159E0 0. 0e0 .001
二、字符和字符串字面值
由单引号括起来的一个字符称为char型字面值,双引号括起来的零个或多个字符则构成字符串型字面值。
'a' // 字符字面值
"Hello World!" // 字符串字面值
// 字符串字面值的类型实际上是由常量字符构成的数组,编译器在每个字符串的结尾处添加一个空字符('\0')
// 如果两个字符串字面值位置紧邻且仅由空格、缩进和换行符分隔,则它们实际上是一个整体
三、转义序列
使用转义序列的场景:
(1)不可打印的字符:退格或其他无可视图符的控制字符
(2)在C++语言中有特殊含义的字符:单引号、双引号、问号、反斜线
转义序列均以反斜线开始,转义序列被当做一个字符:
换行符\n 横向制表符\t 报警(响铃)符\a 纵向制表符\v 退格符\b 双引号\"
反斜线\\ 问号 \? 单引号 \' 回车符 \r 进纸符\f
std::cout << '\n'; // 转到新一行
泛化的转义序列:\x后紧跟1个或多个十六进制数字,或\后紧跟1个、2个或3个八进制数字,其中数字部分表示的是字符对应的数值
\7(响铃) \12(换行符) \40(空格)
\0(空字符) \115(字符M) \x4d(字符M)
std::cout << '\115' << '\n'; // 输出M,转到新一行
如果\后面跟着的八进制数字超过3个,只有前3个数字与\构成反义序列;\x要用到后面跟着的所有数字
"\1234"表示2个字符,即八进制数123对应的字符以及字符4
"\x1234"表示一个16位的字符,该字符由这4个十六进制数所对应的比特唯一确定,因为大多数机器的char型数据占8位,所以该例可能会报错
四、指定字面值的类型
UTF-8是编码格式
五、布尔字面值和指针字面值
1、true和false是布尔类型的字面值
2、nullptr是指针字面值
2.2 变量
2.2.1 变量定义
变量定义的基本形式:类型说明符+一个或多个变量名组成的列表(变量名以逗号分隔),最后以分号结束。
int sum = 0, value, units_sold = 0; // sum、value和units_sold都是int,sum和units_sold初值为0
对象是指一块能存储数据并具有某种类型的内存空间。
一、初始值
当对象在创建时获得了一个特定的值,我们说这个对象被初始化了。
double price = 109.99, discount = price * 0.16;
// 正确,price先被定义并赋值,随后被用于初始化discount
初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来替代。
二、列表初始化
int units_sold = 0;
int units_sold = {0};
int units_sold{0};
int units_sold(0);
如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器将报错。
long double ld = 3.1415926536;
int a{ld}, b = {ld}; // 错误:转换未执行,因为存在丢失信息的危险
int c(ld), d = ld; // 正确:转换执行,且确实丢失了部分值
三、默认初始化
如果定义变量时没有指定初值,则变量被默认初始化,此时变量被赋予了“默认值”。默认值到底是什么由变量类型决定,同时定义变量的位置也会对此有影响。
内置类型:
(1)定义于任何函数体之外的变量——初始化为0
(2)定义于函数体内部的变量——不被初始化(值未定义)
类:各自决定其初始化对象的方式
练习
double salary = wage = 9999.99; // 错误,在声明语句中声明多个变量时需要用逗号将变量名隔开,而不能直接用赋值运算符连接
2.2.2 变量声明和定义的关系
为了允许把程序拆分成多个逻辑部分来编写,C++语言支持分离式编译机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。
为了支持分离式编译,C++语言将声明和定义区分开来。声明使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义负责创建与名字关联的实体。
变量声明规定了变量的类型和名字,在这一点上定义与之相同。但是除此之外,定义还申请存储空间,也可能会为变量赋一个初值。
// 如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显示地初始化变量
extern int i; // 声明i
int i; // 声明并定义i
extern double pi = 3.1416' // 定义
在函数体内部,如果试图初始化一个由extern关键字标记的变量,将引发错误。
// a.cpp
# include <iostream>
//extern int n = 20; // 报错:重定义
int main()
{
extern int n;
// extern int n = 20; // 报错
// 矛盾:extern——既具有外部链接属性,初始化——定义为main函数的局部变量,只在内部可见
std::cout << "n = " << n << std::endl;
return 0;
}
// b.cpp
int n = 10;
变量能且只能被定义一次,但是可以被多次声明。
C++是一种静态类型语言,其含义是在编译阶段检查类型。
2.2.3 标识符
C++的标识符由字母、数字和下划线组成,其中必须以字母或下划线开头。C++为标准库保留了一些名字(用户自定义的标识符不能连续出现两个下划线,也不能以下划线紧连大写字母开头,定义在函数体外的标识符不能以下划线开头,编译无报错,只不过这些命名多出现于标准库)
一、变量命名规范
(1)标识符要能体现实际含义
(2)变量名一般用小写字母,如index
(3)用户自定义的类名一般以大写字母开头,如Sales_item
(4)如果标识符由多个单词组成,则单词间应有明显区分,如student_loan或studentLoan
2.2.4 名字的作用域
作用域是程序的一部分,在其中名字有其特定的含义。C++语言中大多数作用域都以花括号分隔。
同一个名字在不同的作用域中可能指向不同的实体。名字的有效区域始于名字的声明语句,以声明语句所在的作用域末端为结束。
#include <iostream>
// main定义于所有花括号之外,拥有全局作用域
// 一旦声明之后,全局作用域内的名字在整个程序的范围内都可使用
int main()
{
// sum定义于main函数所先定的作用域之内,拥有块作用域,出了main函数所在的块就无法访问
int sum = 0;
// val定义于for语句内,在for语句之内可以访问val,但是在main函数的其他部分就无法访问
for (int val = 1; val <= 10; ++val) {
sum += val;
}
std::cout << "Sum of 1 to 10 inclusive is " << sum << std::endl;
return 0;
}
一、嵌套的作用域
作用域能彼此包含,被包含的作用域称为内层作用域,包含着别的作用域的作用域称为外层作用域。
#include <iostream>
int reused = 42; // reused拥有全局作用域
int main()
{
int unique = 0; // unique拥有块作用域
std::cout << reused << " " << unique << std::endl; // 42 0
int reused = 0; // 新建局部变量reused,覆盖了全局变量reused
std::cout << reused << " " << unique << std::endl; // 0 0
// 显式地访问全局变量reused
std::cout << ::reused << " " << unique << std::endl; // 42 0
return 0;
}
2.3 复合类型
复合类型是指基于其他类型定义的类型。
一条声明语句由一个基本数据类型和紧随其后的一个声明符列表组成。每个声明符命名了一个变量并指定该变量为与基本数据类型有关的某种类型。
2.3.1 引用
引用包括左值引用和右值引用。
引用为对象起了另外一个名字,引用类型引用另外一种类型。
int ival = 1024;
int &refVal = ival; // refVal指向ival(是ival的另一个名字)
int &refVal2; // 报错:引用必须被初始化
// 一般在初始化变量时,初始值会被拷贝到新建的对象中
// 定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用
// 一旦初始化完成,引用将和它的初始值对象一直绑定在一起,因为无法令引用重新绑定到另外一个对象,因此引用必须初始化
一、引用即别名
引用并非对象,它只是为一个已经存在的对象所起的另外一个名字。为引用赋值,实际上是把值赋给了与引用绑定的对象,获取引用的值,实际上是获取了与引用绑定的对象的值。
因为引用本身不是对象,所以不能定义引用的引用。
int &refVal3 = refVal; // 正确,refVal3绑定到了那个与refVal绑定的对象上,即绑定到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; // 错误
2.3.2 指针
指针是指向另外一种类型的复合类型。
指针与引用的相同点:实现对其他对象的间接访问
指针与引用的不同点:
(1)指针本身就是一个对象,允许对指针赋值和拷贝,而且指针的生命周期你内它可以先后指向不同的对象;引用不是一个对象,无法令引用重新绑定到另外一个对象
(2)指针无需在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值;引用则必须在定义时赋初值
int *ip1, *ip2; // ip1和ip2都是指向int型对象的指针
double dp, *dp2; // dp2是指向double型对象的指针,dp是double型对象
一、获取对象的地址
int ival = 42;
int *p = &ival; // p存放变量ival的地址,或者说p是指向变量ival的指针
因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。
指针的类型都要和它所指向的对象严格匹配。
double dval;
double *pd = &dval; // 正确
double *pd2 = pd; // 正确
int *pi = pd; // 错误
pi = &dval; // 错误
二、指针值
指针的值(即地址)应属下列4种状态之一:
(1)指向一个对象
(2)指向紧邻对象所占空间的下一个位置
(3)空指针,意味着指针没有指向任何对象
(4)无效指针,也就是上述情况之外的其他值
三、利用指针访问对象
int ival = 42;
int *p = &ival;
cout << *p; // 由符号*得到指针p所指的对象,输出42
*p = 0;
cout << *p; // 0
四、空指针
空指针不指向任何对象,在试图使用一个指针之前代码可以首先检查它是否为空。
int *p1 = nullptr;
// nullptr是一种特殊类型的字面值,它可以被转换成任意其他的指针类型
int *p2 = 0;
int *p3 = NULL; // 需要#include <cstdlib>
// NULL为预处理变量,这个变量在头文件cstdlib中定义,其值就是0
// 预处理器是运行于编译过程之前的一段程序,预处理变量不属于命名空间std,它由预处理器负责管理
// 当用到一个预处理变量时,预处理器会自动地将它替换为实际值,因此用NULL初始化指针和用0初始化指针是一样的
// 把int变量直接赋给指针是错误的操作,即使int变量的值恰好等于0也不行
int zero = 0;
int *pi = zero; // 错误
五、赋值和指针
六、其他指针操作
int ival = 1024;
int *pi = 0;
int *pi2 = &ival;
if (pi) //pi的值是0,因此条件为false
//...
if (pi2) // pi2指向ival,因此它的值不是0,条件为true
//...
七、void*指针
void*是一种特殊的指针类型,可用于存放任意对象的地址。
double obj = 3.14, *pd = &obj;
void *pv = &obj; // 正确
pv = pd; // 正确
不能直接操作void*指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。
void类型:是一种有特殊用处的类型,既无操作也无值。不能定义一个void类型的变量。
2.3.3 理解复合类型的声明
int i = 1024, *p = &i, &r = i; // i是一个int型的数,p是一个int型指针,r是一个int型引用
一、定义多个变量
二、指向指针的指针
通过*的个数可以区分指针的级别。**表示指向指针的指针,***表示指向指针的指针的指针。
int ival = 1024;
int *pi = &ival; // pi指向一个int型数
int **ppi = π // ppi指向一个int型的指针
cout << "The value of ival\n"
<< "direct value: " << ival << "\n"
<< "indirect value: " << *pi << "\n"
<< "doubly indirect value: " << **ppi
<< endl;
三、指向指针的引用
int i = 42;
int *p;
int *&r = p; // r是一个对指针p的引用,从右向左阅读r的定义
r = &i; // r引用了一个指针,因此给r赋值&i就是令p指向i
*r = 0; // 解引用r得到i,也就是p指向的对象,将i的值改为0
2.4 const限定符
const int bufSize = 512;
bufSize = 512; // 报错,const对象一旦创建后其值就不能再改变,所以const对象必须初始化
const int i = get_size(); // 正确,运行时初始化
const int j = 42; // 正确,编译时初始化
const int k; // 错误
一、初始化和const
int i = 42;
const int ci = i; // 正确,i的值被拷贝给了ci
int j = ci; // 正确:ci的值被拷贝给了j
二、默认状态下,const对象仅在文件内有效
当以编译时初始化的方式定义一个const对象时,编译器将在编译过程中把用到该变量的地方都替换成对应的值。
默认情况下,const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。如想要const变量在文件间共享,则对于const变量不管是声明还是定义都要添加extern关键字。
// file_1.cpp定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufSize = fcn();
// file_1.h头文件
extern const int bufSize; // 与file_1.cpp中定义的bufSize是同一个
// a.cpp
#include <iostream>
// const int num = 10; // 若无const,则num全局可见,编译会报错重定义
// 若想要两文件共享num,则需要在声明和定义处均加extern
extern const int num = 10;
void func(void);
int main()
{
std::cout << "num = " << num << std::endl;
func();
return 0;
}
// b.cpp
#include <iostream>
// const int num = 20;
extern const int num;
void func(void)
{
std::cout << "num = " << num << std::endl;
}
2.4.1 const的引用
对常量的引用不能被用作修改它所绑定的对象。
const int ci = 1024;
const int &r1 = ci; // 正确,引用及其对应的对象都是常量,不允许通过引用修改其绑定的对象
r1 = 42; // 错误,r1是对常量的引用
int &r2 = ci; // 错误,试图让一个非常量引用指向一个常量对象
一、初始化和对const的引用
引用的类型必须与其多引用对象的类型一致,例外情况:
(1)初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。允许为一个常量引用绑定非常量的对象、字面值,甚至是一个一般表达式。
int i = 42;
const int &r1 = i; // 正确
const int &r2 = 42; // 正确
const int &r3 = r1 * 2; // 正确
int &r4 = r1 * 2; // 错误
double dval = 3.14;
const int &ri = dval; // 正确,为确保让ri绑定一个整数,编译器将代码变成下述形式
// const int temp = dval;
// const int &ri = temp; ri绑定了一个临时量对象temp上,而不是dval
二、对const的引用可能引用一个并非const的对象
int i = 42;
int &r1 = i;
const int &r2 = i; // 不允许通过r2修改i的值
r1 = 0;
r2 = 0; // 错误
2.4.2 指针和const
指向常量的指针:不能用于改变其所指对象的值,要想存放常量对象的地址,只能使用指向常量的指针。
const double pi = 3.14;
double *ptr = π // 错误
const double *cptr = π
*cptr = 42; // 错误
指针的类型必须与其所指向对象的类型一致,例外情况:
(1)允许另一个指向常量的指针指向一个非常量对象
double dval = 3.14;
cptr = &dval; // 正确,但不能通过cptr改变dval的值
一、const指针
允许把指针本身定为常量。常量指针必须初始化,而且一旦初始化完成,它的值就不能再改变了。
int errNumb = 0;
int *const curErr = &errNumb; // curErr将一直指向errNumb
const double pi = 3.14159;
const double *const = π // pip是一个指向常量对象的常量指针
练习
const int &const r2; // 错误,引用本身不是对象,因此不能让引用恒定不变
int i;
int *p1;
const int *const p3 = &i;
p1 = p3; // 错误,p3不能改变i的值,p1的值可以改变i的值,不合理
// 不能将const int*类型的值分配到int*类型的实体
2.4.3 顶层const
顶层const:指针本身是个常量。
底层const:指针所指的对象是一个常量。
int i = 0;
int *const p1 = &i; // 顶层
const int ci = 42; // 顶层
const int *p2 = &ci; // 底层
const int *const p3 = p2; // 底层+顶层
const int &r = ci; // 用于声明引用的const都是底层const
i = ci; // 正确
p2 = p3; // 正确
// 当执行对象的拷贝操作时,拷入和拷出到的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换
int *p = p3; // 错误,p3包含底层const,而p没有
p2 = p3; // 正确
p2 = &i; // 正确
int &r = ci; // 错误
const int &r2 = i; // 正确
2.4.4 constexpr和常量表达式
常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。
const int max_files = 20; // max_files是常量表达式
const int limit = max_file + 1; // limit是常量表达式
int staff_size = 27; // staff_size不是常量表达式,数据类型为int而不是const int
const int sz = get_size(); // sz不是常量表达式,值运行时才能获取到
一、constexpr变量
允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。
constexpr int mf = 20;
constexpr int limit = mf + 1;
constexpr int sz = size(); // 只有当size是一个constexpr函数时,才是一条正确的声明语句
二、字面值类型
常量表达式的值需要在编译时就得到计算,因此对声明constexpr时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见、容易得到,就把它们称为“字面值类型”。
算术类型、引用和指针都属于字面值类型。自定义类、IO库、string类型不属于字面值类型。
一个constexpr指针的初始值必须是nullptr或者0,或者是存储于某个固定地址中的对象。
函数体内的定义的变量一般来说并非存放在固定地址中,因此constexpr指针不能指向这样的变量。
定义于所有函数体之外的对象其地址固定不变,能用来初始化constexpr指针。
允许函数定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量一样也有固定地址。
三、指针和constexpr
在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关。
const int *p = nullptr; // p是一个指向整型常量的指针
constexpr int *q = nullptr; // q是一个指向整数的常量指针
// constexpr指针既可以指向常量也可以指向一个非常量
constexpr int *np = nullptr;
int j = 0;
constexpr int i = 42;
// i和j都必须定义在函数体之外
constexpr const int *p = &i; // p是常量指针,指向整型常量i
constexpr int *p1 = &j; // p1是常量指针,指向整数j
// 定义一个constexpr引用,只能绑定在全局变量和局部静态变量上
// constexpr引用与const引用不一样,仍然可以用引用来修改其绑定对象的值
2.5 处理类型
2.5.1 类型别名
类型别名是一个名字,它是某种类型的同义词。
typedef double wages; // wages是double的同义词
typedef wages base, *p; // base是double的同义词,p是double*的同义词
// 含有typedef的声明语句定义的不再是变量而是类型别名
using SI = Sales_item; //SI是Sales_item的同义词
一、指针、常量和类型别名
// 不能用简单替换来理解
// const是对指定类型的修饰,const作用在pstring上
// pstring是指向char的指针,const pstring就是指向char的常量指针
typedef char *pstring; // 是char*的别名
const pstring cstr = 0; // cstr是指向char的常量指针
const pstring *ps; // ps是一个指针,它的对象时指向char的常量指针
#include <iostream>
typedef char* pstring;
int main()
{
char ch = 'A';
const pstring cstr = &ch;
std::cout << "*cstr = " << *cstr << std::endl;
*cstr = 'B';
std::cout << "*cstr = " << *cstr << std::endl;
char st = 'X';
// cstr = &st; // 报错
return 0;
}
2.5.2 auto类型说明符
auto类型说明符:能让编译器替我们分析表达式所属的类型。auto让编译器通过初始值来推算变量的类型,auto定义的变量必须有初始值。
// 使用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是一个int
// auto一般会忽略掉顶层const,底层const会保留下来
const int ci = i, &cr = ci;
auto b = ci; //int
auto c = cr; // int
auto d = &i; // int*
auto e = &ci; // const int*
// 如果希望推断出的auto类型是一个顶层const,需要明确指出:
const auto f = ci;
auto &g = ci; // 整型常量引用 const int&
auto &h = 42; // 错误,不能为非常量引用绑定字面值
const auto &j = 42; // 正确
auto k = ci, &l = i; // k是int,l是int&
auto &m = ci, *p = &ci; // m是对整型常量的引用,p是指向整型常量的指针
auto &n = i, *p2 = &ci; // 错误,i的类型是int,而&ci的类型是const int
2.5.3 decltype类型指示符
类型说明符decltype作用是选择并返回操作数的数据类型,在此过程中,编译器分析表达式并得到它点的类型,却不实际计算表达式的值。
decltype(f()) sum = x; // sum的类型就是函数f的返回类型
// 如果decltype使用的表达式是一个变量,则decltype返回该变量的类型,包括顶层const和引用在内
const int ci = 0, &cj = ci;
decltype(ci) x = 0; // const int
decltype(cj) y = x; // const int&
decltype(cj) z; // 错误,z是一个引用,必须初始化
一、decltype和引用
// decltype的结果可以是引用类型
int i = 42, *p = &i, &r = i;
decltype(r + 0) b; // 正确,加法的结果是int,b是一个未初始化的int
// 如果表达式的内容是解引用操作,则decltype将得到引用类型
decltype(*p) c; // 错误,c是int&,必须初始化
// 如果decltype使用的是一个不加括号的变量,则得到的结果就是该变量的类型
// 如果给变量加上以一层或多层括号,编译器就会把它当成是一个表达式,将得到引用类型
decltype((i)) d; // 错误,int&类型必须初始化
decltype(i) e; // int
练习
// 赋值是会产生引用的一类典型表达式,引用的类型就是左值的类型
// 如果i是int,则表达式i=x的类型是int&
int a = 3, b =4;
decltype(a) c = a; // c为int,值为3
decltype(a = b) d =a; // 不计算表达式,a的值仍为3,d为int&,是a的别名,值为3
auto和decltype的区别:
(1)auto类型说明符用编译器计算变量的初始值来推断其类型,而decltype虽然也让编译器分析表达式并得到它的类型,但是不实际计算表达式的值
(2)编译器推断出来的auto类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则。例如,auto一般会忽略掉顶层const,而把底层const保留下来;decltype会保留变量的顶层const
(3)与auto不同,decltype的结果类型与表达式形式密切相关,如果变量名加上了一对括号,则得到的类型与不加括号时不同。
2.6 自定义数据结构
2.6.1 定义Sales_data类型
struct Sales_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
一、类数据成员
类的数据成员定义了类的对象的具体内容,每个对象有自己的一份数据成员拷贝。
2.6.2 使用Sales_data类
一、添加两个Sales_data对象
二、Sales_data对象读入数据
三、输出两个Sales_data对象的和
# include <iostream>
# include <string>
# include "Sales_data.h"
int main()
{
Sales_data data1, data2;
// 读入data1和data2
double price = 0;
std::cin >> data1.bookNo >> data1.units_sold >> price;
data1.revenue = data1.units_sold * price;
std::cin >> data2.bookNo >> data2.units_sold >> price;
data2.revenue = data2.units_sold * price;
// 检查data1和data2的ISBN是否相同
// 如果相同,求data1和data2的总和
if (data1.bookNo == data2.bookNo) {
unsigned totalCnt = data1.units_sold + data2.units_sold;
double totalRevenue = data1.revenue + data2.revenue;
std::cout << data1.bookNo << " " << totalCnt << " " << totalRevenue << " ";
if (totalCnt != 0)
std::cout << totalRevenue / totalCnt << std::endl;
else
std::cout << "(no sales)" << std::endl;
return 0;
} else {
std::cerr << "Data must refer to the same ISBN" << std::endl;
return -1;
}
}
2.6.3 编写自己的头文件
为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应与类的名字一样。
一、预处理器概述
确保头文件多次包含仍能安全工作的常用技术是预处理器。
预处理功能# include,当预处理器看到# include标记时就会用指定的头文件的内容代替# include。
预处理器功能:头文件保护符,其依赖于预处理变量。预处理变量有两种状态:已定义和未定义。# define指令把一个名字设定为预处理变量,另外两个指令则分别检查某个指定的预处理变量是否已经定义:# ifdef当且仅当变量已定义时为真,# ifndef当且仅当变量未定义时微针。一旦检查结果为真,则执行后续操作直至遇到# endif指令为止。
# ifndef SALES_DATA_H
# define SALES_DATA_H
struct Sales_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
# endif
小结
类型是C++编程的基础。
类型规定了其对象的存储要求和所能执行的操作。C++语言提供了一套基础内置类型,如int和char等,这些类型与实现它们的机器硬件密切相关。类型分为非常量和常量,一个常量对象必须初始化,而且一旦初始化其值就不能再改变。此外,还可以定义复合类型,如指针和引用等。复合类型的定义以其他类型为基础。
C++语言允许用户以类的形式自定义类型。C++库通过类提供了一套高级抽象类型,如输入输出和string等。