目录
目录
2.1 基本内置类型
包括算术类型和空类型。
其中算术类型包含字符、整型数、布尔值和浮点数。
空类型不对应具体的值,仅用于一些特殊场合。
2.1.1 算术类型
算术类型分为两类:整型(integral type,包括字符和布尔类型在内)和浮点型。
C++算术类型:
带符号类型和无符号类型
除去布尔型和扩展的字符型外,其他整型可以划分为带符号的(signed)和无符号的(unsigned)两种。带符号类型可以表示正数、负数或0,无符号数仅能表示大于等于0的数。
在类型名前添加 unsigned就可以得到无符号类型数。如:unsigned long。
2.1.1节练习
1.类型int、long、long long和short的区别是什么?无符号类型和带符号类型的区别是什么?float和double的区别是什么?
在C++语言中,int 、long、long long和short都属于整型,区别是C++标准规定的尺寸的最小值(即该类型在内存中所占的比特数)不同。其中,short是短整型,占16位;int 是整型,占16位;long和long long均为长整型,分别占32位和64位。C++标准允许不同的编译器赋予这些类型更大的尺寸。某一类型占的比特数不同,所能表示的数据范围也不同。
无符号类型仅能表示大于等于0的数;带符号类型可以表示正数、负数和0;
float 和 double 分别表示单精度浮点数和双精度浮点数,区别主要在在内存中所占的比特数不同,以及默认的有效位数不同。
2.计算按揭贷款时,对于利率、本金和付款分别应该选择何种数据类型?说明你的理由。
都用double类型。
2.1.2 类型转换
类型所能表示的值的范围决定了转换的过程。
当我们给一个类型一个超出它表示范围的值时,结果是未定义的,可能损失精度,也可能崩溃、生成垃圾数据。
含有无符号类型的表达式
unsigned u = 10;
int i = -42;
cout << i + i <<endl;//输出-84
cout << u + i <<endl;//如果int占32位,则输出4294967264
在第二个输出表达式中,相加前首先把整数-42转换成无符号数。把负数转换成无符号数类似于直接给无符号数赋一个负值,结果等于这个负数加上无符号数的模。
当从无符号数中减去一个值时,不管这个值是不是无符号数,我们都必须确保结果不是一个负值:
unsigned u1 = 42, u2 = 10;
cout << u1 - u2 << endl;//正确:输出32
cout << u2 -u1 << endl;//正确:不过是取模后的值
注意事项:
不要混用带符号类型和无符号类型。
如果表达式中既有带符号类型又有无符号类型,当带符合类型取值为负时会出现异常结果,这是因为带符合类型整数会自动转换成无符号数。
2.1.2节练习
#include<iostream>
using namespace std;
//练习2.3 读程序,写结果
void test01()
{
unsigned u = 10, u2 = 42;
cout << u2 - u << endl;//结果是32
cout << u - u2 << endl;//结果是32
int i = 10, i2 = 42;
cout << i2 - i << endl;//结果是32
cout << i - i2 << endl;//结果是-32
cout << i - u << endl;//结果是0
cout << u - i << endl;//结果是0
}
int main()
{
test01();
system("pause");
return 0;
}
实际结果:
第二个表达式的值 为4294967264,计算出来是负值,所以得到的是取模后的值,也就是-32 + 2^32 = -32+4294967296=4294967264。
2.1.3 字面值常量
一个形如 42 的值被称做字面值常量。每个字面值常量都对应一种数据类型。
整型和浮点型字面值
八进制数:0开头的整数
十六进制数:0X或0x开头的整数
浮点型字面值,其指数部分用E或e标识:
0e0 3.14159E0
浮点型字面值默认是一个double。
字符和字符串字面值
由单引号括起来的一个字符称为char型字面值,双引号括起来的零个或多个字符则构成字符串型字面值。
'a' //字符字面值
"hello world" //字符串变量值
字符串字面值的类型实际上是由常量字符构成的数组(array)。编译器在每个字符串的结尾处添加一个空字符('\0'),因此,字符串字面值的实际长度要比它的内容多1。
转义序列
有两类字符程序员不能直接使用:一类是不可打印(nonprintable)的字符,如退格或其他控制字符,因为它们没有可视的图符;另一类是在C++语言中有特殊含义的字符(单引号、双引号、问号、反斜线)。这些情况需要用到转义序列(escape sequence),转义序列均以反斜线作为开始,包括:
指定字面值的类型
布尔字面值和指针字面值
true和false是布尔类型的字面值
nullptr是指针字面值。
2.1.3节练习
//练习2.8 利用转义序列编写一段程序,要求先输出2M,然后转到新一行。
//修改程序使其先输出2,然后输出制表符,再输出M,最后转到新一行
void test02()
{
//输出2M 然后换行
//cout << "2M" << "\n";
//另一种写法
cout << "2\x4d\012";
//输出2、制表符、M,然后换行
//cout << "2" << "\t" << "M" << "\n";
//另一种写法
cout << "2\tM\n";
}
2.2 变量
变量提供一个具名的、可供程序操作的存储空间。
2.2.1 变量定义
变量定义的基本形式是:首先是类型说明符(type specifier),随后紧跟由一个或多个变量名组成的列表,其中变量名以逗号分隔,最后以分号结束。定义是可为多个变量赋初值。
int sum = 0, value;//定义了两个变量,sum的初值是0
初始值
当对象在创建时获得了一个特定的值,我们就说这个对象被初始化了。
注意:初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来替代。
列表初始化
int units_sold = 0;
int units_sold = {0};
int units_sold {0};
int units_sold (0);
用花括号赋予初始值的形式被称为列表初始化。
默认初始化
如果定义变量时没有指定初值,则变量被默认初始化(default initialized),此时变量被赋予了“默认值”。
定义于任何函数体之外的变量被初始化为0。有一种例外情况,定义在函数体内部的内置类型变量将不被初始化(uninitialized)。
2.2.1节练习
//练习2.9 解释下列定义的含义,指出非法定义的错误
(1) cin>> int input_value;
//错误,输入运算符的右侧需要一个明确的变量名称,而非定义变量的语句
更正:
int input_value;
cin>>input_value;
(2) int i = { 3.14 };
//引发警告,小数部分将丢失
(3) double salary = wage = 9999.99;
//错误,声明多个变量时需要用逗号将变量名隔开,不能直接用赋值运算符连接。
更正:
double salary, wage;
salary = wage = 9999.99;
(4) int i = 3.14;
//引发警告,小数部分将丢失
//练习 2.10 下列变量的初值分别是什么?
#include<iostream>
#include<string>
using namespace std;
string global_str; //空串
int global_int; //0
int main()
{
int local_int; //不被初始化,未定义的奇异值
string local_str; //空串
system("pause");
return 0;
}
//因为string类型本身接受无参数的初始化方式,所以不论变量定义在函数内还是函数外都被默认初始化为空串
tips:建议初始化每一个内置类型的变量。
2.2.3 变量声明和定义的关系
为了允许把程序拆分成多个逻辑部分来编写,C++语言支持分离式编译(separate compilation)机制,该机制允许将程序分割成若干个文件,每个文件可被独立编译。
为了支持分离式编译,C++语言将声明和定义区分开来。声明(declaration)使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义(definition)负责创建与名字关联的实体。
变量声明规定了变量的类型和名字,在这一点上定义与之相同。但是除此之外,定义还申请存储空间,也可能会为变量赋一个初始值。
声明一个变量而非定义它:在变量前添加关键词 extern
extern int i; //声明 i ,而非定义 i
int j; //声明并定义j
extern double pi = 3.14; //定义
tips: 变量能且只能被定义一次,但是可以被多次声明。
2.2.2 节练习
//练习2.11 指出下面的语句是声明还是定义
(1) extern int ix = 1024;//定义
(2) int iy; //定义
(3) extern int iz; //声明
关键概念:
C++是一种静态类型(statically typed)语言,其含义是在编译阶段检查类型。这个过程叫做类型检查(type checking)。
2.2.3 标识符
C++的标识符(identifier)由字母、数字和下划线组成,必须以字母或下划线开头。标识符的长度没有限制,对大小写敏感。
C++语言保留了一些名字供语言本身使用,这些名字不能用作标识符。如下表:
同时,C++也为标准库保留了一些名字。用户自定义的标识符中不能连续出现两个下划线,也不能以下划线紧连大写字母开头。此外,定义在函数体外 的标识符不能以下划线开头。
(语法编译能过,不过不建议)
变量命名规范
- 标识符有可读性,能体现实际含义;
- 变量名一般用小写字母,如 index,不要使用Index 或INDEX;
- 用户自定义的类名一般以大写字母开头,如Sales_item;
- 多个单词之间应有明显区分,如 student_loan或studentLoan。
2.2.3 节练习
//练习2.12 指出下列哪些名字是非法的
(1) int double = 3.14
//非法
(2) int _;
//正确
(3) int catch-22;
//非法,只能由字母、数字、下划线组成
(4) int 1_or_2 = 1;
//非法,不能以数字开头
(5) double Double = 3.14;
//正确
2.2.4 名字的作用域
作用域(scope)是程序的一部分,在其中名字有其特定的含义。C++语言中大多数作用域都以花括号分隔。
嵌套的作用域
作用域能彼此包含,被包含(或者说被嵌套)的作用域称为内层作用域(inner scope),包含着别的作用域的作用域称为外层作用域(outer scope)。
WARNING: 如果函数有可能用到某全局变量,则不宜再定义一个同名的局部变量。
2.2.4 节练习
//练习2.13:下面程序中 j 的值是多少?
#include<iostream>
using namespace std;
int i = 42;
int mian()
{
int i = 100;
int j = i;
}
//值为100
//练习2.14:这段程序合法吗,如果合法将输出什么?
#include<iostream>
using namespace std;
int i = 100, sum = 0;
int main()
{
for (int i = 0; i != 10; ++i)
{
sum += i;
}
cout << i << " "<< sum << endl;
//合法 输出 100 45
system("pause");
return 0;
}
//sum的值为 0 加到 9
2.3 复合类型
复合类型(compound type)是指基于其他类型定义的类型。C++语言有几种复合类型,本章将介绍其中的两种:引用和指针。
一条声明语句由一个基本数据类型(base type)和紧随其后的一个声明符(declarator)列表组成。
2.3.1 引用
Note:C++11中新增了“右值引用”,主要用于内置型。严格来说,当我们使用术语“引用(reference)时,指的其实是“左值引用(lvalue reference)””。
引用(reference)为对象起了另一个名字,引用类型引用(refer to)另外一种类型。
int ival = 1024;
int &refVal = ival;//refVal指向ival(是 ival 的另一个名字)
int &refVal2;//报错,引用必须被初始化
在定义引用时,程序把引用和它的初始值绑定(bind)一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。
引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字。
refVal = 2;//把2赋给refVal指向的对象,此处即是赋给了ival
int ii = refVal;//与 ii = ival 执行结果一样
为引用赋值,实际上是把值赋给了与引用绑定的对象。获取引用的值,实际上是获取了与引用绑定的对象的值。
int &refVal3 = refVal;//正确:refVal3绑定到了那个与refVal绑定的对象上,即 ival 上
int i = refVal;//正确:i 被初始化为 ival 的值
因为引用本身不是一个对象,所以不能定义引用的引用。
引用的定义
引用标识符以“&”开头。引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起。除了例外情况,引用的类型都要和与之绑定的对象严格匹配。
int &refVal = 10; //错误:引用类型的初始值必须是一个对象
double dval = 3.14;
int &refVal5 = dval; //错误:此处引用类型的初始值必须是 int 型对象
2.3.1节练习
2.3.2 指针
指针(pointer)是“指向(point to)”另外一种类型的复合类型。与引用类似,指针也实现了对其他对象的间接访问。但是指针与引用有很多不同点。其一,指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。其二,指针无需在定义时赋初值。和其他内置类型一样,在块作用域内被定义的指针如果没有被初始化,也将拥有一个不确定的值。
定义指针类型的方法将声明符写成 *d 的形式,其中 d 是变量名。
int *ip1, *ip2; //ip1 和 ip2 都是指向int型对象的指针
double dp, *dp2; //dp2 是指向 double型对象的指针, dp是double型对象
获取对象的地址
指针存放某个对象的地址,要想获取该地址,需要用取地址符(操作符 & ):
int ival = 42;
int *p = &ival; //p存放变量ival的地址,或者说p是指向变量ival的指针
第二条语句把 p 定义为一个指向 int 的指针, 最后初始化 p 令其指向名为 ival 的 int 对象。
同样,指针的类型需要和它所指向的对象严格匹配。
double dval;
double *pd = &dval; //正确:初始值是double型对象的地址
double *pd2 = pd; //正确:初始值是指向 double 对象的指针
int *pi = pd; //错误:指针 pi 的类型和 pd 的类型不匹配
pi = &dval; //错误:试图把double型对象的地址赋给 int 型指针
指针值
指针的值(即地址)应属于下列四种状态之一:
- 指向一个对象。
- 指向紧邻对象所占空间的下一个位置。
- 空指针,意味着指针没有指向任何对象。
- 无效指针,也就是上述情况之外的其他值。
试图拷贝或以其他方式访问无效指针的值都将引发错误。
利用指针访问对象
如果指针指向了一个对象,则允许使用解引用符(操作符 * )来访问对象。
int ival = 42;
int *p = &ival; //p存放着变量 ival 的地址, 或者说 p 是指向变量 ival 的指针
cout<<*p; //由符号 * 得到指针p所指的对象,输出 42
tips: 解引用只适用于那些确实指向了某个对象的有效指针。
空指针
空指针(null pointer)不指向任何对象,以下是几个生成空指针的方法:
int *p1 = nullptr;
int *p2 = 0;
int *p3 = NULL;
建议:初始化所有指针
赋值和指针
指针和引用都能提供对其他对象的间接访问,但是在具体实现细节上二者有很大不同,最不同的一点是,引用本身并非一个对象。一旦定义了引用,就无法令其再绑定到另外的对象,之后每次使用这个引用都是访问它最初绑定的那个对象。
指针和它存放的地址之间就没有这种限制了。给指针赋值就是令它存放一个新的地址,从而指向一个新的对象。
int i = 42;
int *pi = 0; //pi被初始化,但是没有指向任何对象
int *pi2 = &i; //pi2被初始化,存有 i 的地址
int *pi3; //如果 pi3 定义与块内,则 pi3 的值是无法确定的
pi3 = pi2; //pi3 和 pi2 指向同一个对象i
pi2 = 0; //现在 pi2 不指向任何对象了
赋值永远改变的是等号左侧的对象。
pi = &ival; //pi的值被改变,现在pi指向了ival
*pi = 0; //ival的值被改变,指针pi并没有改变
其他指针操作
只要指针拥有一个合法值,就能将它用在条件表达式中。
void* 指针
void* 是一种特殊的指针类型,可用于存放任意对象的地址。一个 void* 指针存放着一个地址,这一点和其他指针类似。不同的是,我们对该地址中到底是个什么类型的对象并不了解:
double obj = 3.14, *pd = &obj;
void *pv = &obj; //obj可以是任意类型的对象
pv = pd; //pv 可以存放任意类型的指针
2.3.2节练习
//练习2.18 编写代码更改指针的值以及指针指向的值
void test01()
{
int a = 10, b=5;
cout << "a: " << a << " b: " << b << endl;
int *pa = &a;
int *pb = &b;
//更改指针的值
pa = pb;
//更改指针指向的值
*pa = 11;//这里应该改变的是b的值,b变成11
cout << "a: " << a << " b: " << b << endl;
}
//练习2.19
/*
指针和引用 的主要区别
指针“指向”内存中的某个对象,而引用“绑定到”内存中的某个对象,它们都实现了对其他对象的间接访问,二者的区别主要如下:
第一、指针本身就是一个对象,允许对指针拷贝和赋值,而且在指针的生命周期内它可以指向几个不同的对象;
引用不是一个对象,无法令引用重新绑定到另外一个对象
第二、指针无需在定义是赋初值,和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将
拥有一个不确定的值;引用则必须在定义时赋初值。
*/
//练习2.20:叙述这段代码的作用
void test02()
{
int i = 42;
int *p1 = &i;
*p1 = *p1 * *p1;//i=i*i
cout << "i: " << i << endl;
cout << "p1 :" << p1 << endl;
cout << "&i :" << &i << endl;
}
2.3.3 理解复合类型的声明
定义多个变量
int * p1, p2; //p1是指向 int 的指针, p2是int
指向指针的指针
int ival = 1024;
int *pi = &ival;//pi指向一个 int 型的数
int **ppi = π //ppi指向一个 int 型的指针
指向指针的引用
引用本身不是一个对象,因此不能定义指向引用的指针。但是指针是对象,所有存在对指针的引用。
int i = 42;
int *p; //p是一个int型指针
int *&r = p; //r是一个对指针p的引用
r = &i; //r引用了一个指针,因此给r赋值&i就是令p指向i
*r = 0; // i = 0
tip: 面对复杂的指针或引用的声明语句,从右到左阅读有利于弄清它的真实含义。
2.4 const限定符
有时候我们希望定义这样一种变量,它的值不能被改变。或者是防止程序不小心改变了这个值,这个时候可以使用 const 关键字对变量的类型加以限定。
const 对象必须初始化。因为 const 对象一旦创建后其值就不能改变。
const int k; //错误:k是一个未经初始化的常量
初始化和 const
利用一个对象去初始化另外一个对象,则它们是不是 const 都无关紧要:
int i = 42;
const int ci = i; //正确: i 的值被拷贝给了 ci
int j = ci; //正确: ci 的值被拷贝给了 j
默认状态下, const 对象仅早文件内有效
默认情况下, const 对象被设定为仅在文件内有效。当多个文件中出现了同名的 const 变量时,其实等同于在不同文件中分别定义了独立的变量。
如果一个 const 变量,它的初始值不是常量表达式,但又确实有必要在文件间共享。这种情况下,我们不希望编译器为每个文件分别生成独立的变量。相反,我们希望只在一个文件中定义 const ,而在其他多个文件中声明并使用它。
解决办法是:在 const 变量的声明和定义前都添加 extern 关键字。这样就只需定义一次了。
注意:如果想在多个文件之间共享 const 对象,必须在变量的定义之前添加extern关键字。
2.4节 练习
//练习2.26 下面哪些句子合法,如果不合法请说明为什么
const int buf;//不合法:未初始化
int cnt = 0;
const int sz = cnt;
++cnt;
++sz;//不合法,const 对象的值不能被改变
2.4.1 const 的引用
可以把引用绑定到对象上,我们称之为对常量的引用(reference to const)。对常量的引用不能被用作修改它所绑定的对象。
const int ci = 1024;
const int &r1 = ci; //正确:引用及对应的对象都是常量
r1 = 42; //错误: r1 是对常量的引用
int &r2 = ci; //错误:试图让一个非常量引用指向一个常量对象
术语:常量引用是对 const 的引用
C++程序员们经常把“对 const 的引用”简称为“常量引用”,你得时刻记得这只是一个简称。
初始化和对 const 的引用
一般情况下,引用的类型必须与其所引用对象的类型一致,但是有两个例外。
- 第一种情况:在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。允许为一个常量引用绑定非常量的对象、字面值,甚至是个一般表达式。
int i = 42;
const int &r1 = i;//允许将const int&绑定到一个普通int 对象上
const int &r2 = 42;//正确:r2是一个常量引用
const int &r3 = r1 * 2;//正确:r3是一个常量引用
int &r4 = r1 * 2;//错误:r4是一个普通的非常量引用
const int temp = i;
const int &r1 = temp;
此时,r1绑定了一个临时量(temporary)对象。编译器将前面前两行代码转换成了上面的两行代码。此时r1绑定的是临时量对象,而非i。C++语言把这种行为归为非法。
对 const 的引用可能一个并非 const 的对象
常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量为做限定。因为对象可能是非常量,所以允许通过其他途径改变它的值。
int i = 42;
int &r1 = i; //引用r1绑定对象i
const int &r2 = i;//r2也绑定对象i,但是不允许通过r2修改i的值
r1 = 0; //r1并非常量,i的值修改为0
r2 = 0; //错误:r2是一个常量引用
2.4.2 指针和const
指针也可指向常量或非常量。指向常量的指针(pointer to const)不能用于改变其所指对象的值。要想存放常量对象的地址,只能使用指向常量的指针。
const double pi = 3.14;
double *ptr = π//错误:ptr是一个普通指针
const double *cptr = π//正确:cptr可以指向一个双精度常量
*cptr = 42;//错误:不能给*cptr赋值
const 指针
允许把指针本身定为常量。常量指针(const pointer)必须初始化,且一旦初始化,则它的值(也就是存放在指针中的那个地址)就不能再改变了。
把 * 放在关键字之前用以说明指针是一个常量,即不变的是指针本身的值而非指向的那个值。
int errNumb = 0;
int *const curErr = &errNumb;//curErr将一直指向 errNumb
const double pi = 3.14;
const double *const pip = π//pip是一个指向常量对象的常量指针
2.4.2节练习
(a)不合法,非常量引用 r 不能引用字面值常量0;
(b) 合法,p2 是一个常量指针,p2的值永不改变,即p2永远指向变量i2;
(c) 合法,i 是一个常量,r 是一个常量引用,此时 r 可以绑定到字面值常量0;
(d) 合法, p3 是一个常量指针,p3 的值永不改变,即 p3 永远指向变量i2;同时,p3 指向的是常量,即我们不能通过p3改变所指对象的值;
(e) 合法,p1 指向一个常量,即我们不同通过 p1 改变所指对象的值;
(f) 非法,引用本身不是对象,因此不能让引用恒定不变;
(g) 合法,i2是一个常量,r 是一个常量引用。
(a) 非法,cp 是一个常量指针,因其值不能被改变,所以必须被初始化;
(b) 非法,cp2 是一个常量指针,因其值不能被改变,所以必须被初始化;
(c) 非法,ic 是一个常量,因其值不能被改变,所以必须初始化;
(d) 非法,p3 是一个常量指针,因其值不能被改变,所以必须初始化;同时p3 指向的是常量,即我们不能通过 p3 改变所指对象的值;
(e)合法,但是 p 没有指向任何实际的对象。
(a) 合法,常量 ic 的值赋给了非常量 i;
(b) 非法,普通指针 p1 指向了一个常量,从语法上说,p1 的值可以随意改变,这显然是不合理的;
(c) 非法,普通指针 p1 指向了一个常量,错误情况与上一条类似;
(d) 非法, p3 是一个常量指针,不能被赋值;
(e) 非法, p2 是一个常量指针,不能被赋值;
(f) 非法, ic 是一个常量,不能被赋值。
2.4.3 顶层 const
用名词顶层 const(top-level const)表示指针本身是个常量;用名词底层 const(low-level const) 表示指针所指的对象是一个常量。
更一般的,顶层 const 可以表示任意的对象是常量,这一点对任何数据类型都适用,如算术类型、类、指针等。底层 const 则与指针和引用等复合类型的基本类型部分有关。比较特殊的是,指针类型既可以是顶层 const 也可以是底层 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
当执行对象的拷贝操作时,顶层 const 不受什么影响。但是底层 const 的限制却不能忽视,拷入和拷出的对象必须具有相同的底层 const 资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换成常量,反之则不行。
int * p = p3;//错误:p3包含底层const的定义,而p没有
p2 = p3;//正确:都是底层 const
p2 = &i;//正确: int * 能转换成 const int*
int &r = ci;//错误:普通的int& 不能绑定到int 常量上
const int &r2 = i;//正确:const int& 可以绑定到一个普通int上
- r1 = v2;是合法的,r1 是一个非常量引用, v2 是一个常量(顶层 cinst),把 v2的值拷贝给 r1 不会对v2有任何影响;
- p1 = p2 ; 是非法的,p1 是普通指针,指向的对象可以是任意值, p2 是指向常量的指针(底层 const) ,令 p1 指向 p2 所指的内容,有可能错误地改变常量的值;
- p2 = p1 ; 是合法的,与上一条语句相反,p2 可以指向一个非常量,只不过我们不会通过 p2 更改它所指的值;
- p1 = p3 ; 是非法的,p3 包含底层 const 定义(p3 所指的对象是常量),不能把 p3 的值赋给普通指针。
- p2 = p3 ; 是合法的, p2 和 p3包含相同的底层 const,p3 的顶层 const 则可以忽略不计。
2.4.4 constexpr 和常量表达式
常量表达式(const expression)是指值不会改变并且在编译过程就能得到计算结果的表达式。
一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定。
//常量表达式
const int max_files = 20;//是
const int limit = max_files + 1;//是
int staff_size = 27;//不是
const int sz = get_size();//不是
constexpr 变量
C++11 新标准规定,允许将变量声明为 constexpr 类型以便由编译器来验证变量的值是否是一个常量表达式。声明为 constexpr 的变量一定是一个常量,而且必须用常量表达式初始化。
constexpr int mf = 20;
constexpr int limit = mf + 1;
constexpr int sz = size();//只有当size是一个constexpr函数时,才是正确的声明语句
一般来说,如果你认定变量是一个常量表达式,那就把它声明成 constexpr 类型。
字面值类型
算术类型、引用和指针都属于字面值类型,自定义类型、IO库、string类型则不属于字面值类型。
指针和 constexpr
在constexpr 声明中如果定义了一个指针,限定符 constexpr 仅对指针有效,与指针所指的对象无关。
const int *p = nullptr;//p是一个指向整型常量的指针
constexpr int *q = nullptr;//q是一个指向整数的常量指针
2.4.4 节练习
2.5 处理类型
2.5.1 类型别名
类型别名(type alias)是一个名字,它是某种类型的同义词。
有两种方法可用于定义类型别名。传统方法是使用关键字 typedef。
typedef double wages; //wages是double 的同义词
typedef wages base, *p; //base是double的同义词, p 是 double* 的同义词
新标准规定了一种新的方法,使用别名声明(alias declaration)来定义类型的别名。
using SI = Sales_item; //SI是Sales_item的同义词
类型别名和类型的名字等价,只要是类型的名字能出现的地方,就能使用类型别名。
指针、常量和类型别名
如果某个类型别名指代的是复合类型或常量,那么把它用到声明语句里就会产生意想不到的后果。
typedef char *pstring; //pstring 是 指向 char的常量指针
const pstring cstr = 0; //cstr 是指向 char 的常量指针
const pstring *ps; //ps 是一个指针,它的对象是指向char的常量指针
2.5.2 auto 类型说明符
C++11 新标准引入了 auto 类型说明符,用它就能让编译器替我们去分析表达式所属的类型。auto 定义的变量必须有初始值。
//由 val1 和 val2 相加的结果可以推断出 item 的类型
auto item = val1 + val2;//item 初始化为val1 和 val2相加的结果
auto i = 0, *p = &i; //正确:i是整数、p是整型指针
auto sz = 0, pi = 3.14; //错误:sz 和pi 的类型不一样
复合类型、常量和 auto
编译器推断出来的 auto 类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则。
首先,编译器以引用对象的类型作为 auto 的类型。
int i = 0, &r = i;
auto a = r; //a是一个整数
其次,auto 一般会忽略掉顶层 const,同时底层 const 则会保留下来。
如果希望推断出的 auto 类型是一个顶层 const ,需要明确指出。
const auto f = ci; //ci的推演类型是 int, f 是 const int
2.5.2 节练习
int i = 0, &r = i;
auto a = r; //a是一个整数
//练习2.35 根据定义推断类型,然后编写程序进行验证
const int i = 42;//整型常量
auto j = i;//整数
const auto &k = i;//整型常量
auto *p = &i;//指向整型常量的指针
const auto j2 = i, &k2 = i;//j2是整数,k2是整数 这个没看懂
cout << typeid(i).name() << endl;
cout << typeid(j).name() << endl;
cout << typeid(k).name() << endl;
cout << typeid(p).name() << endl;
cout << typeid(j2).name() << endl;
cout << typeid(k2).name() << endl;
2.5.3 decltype类型指示符
C++11 新标准引入了第二种类型说明符 decltype ,它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。
decltype(f()) sum = x; //sum 的类型就是函数 f 的返回类型
编译器并不实际调用函数 f ,而是使用当调用发生时 f 的返回值作为 sum 的类型。
如果 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 使用的表达式不是一个变量,则 decltype 返回表达式结果对应的类型。
decltype 和 auto 的重要区别:decltype 的结果类型与表达式形式密切相关。有一种情况需要特别注意:对于 decltype 所用的表达式来说,如果变量名加上了一对括号,则得到的类型与不加括号时会有所不同。
如果 decltype 使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果给变量加上了一层或多层括号,编译器就会把它当成一个表达式。变量是一种可以作为赋值语句左值的特殊表达式,所以这样的 decltype 就会得到引用类型。
int i = 2;
decltype((i)) d;//错误: d 是int&,必须初始化
decltype(i) e;//整数:e是一个(未经初始化的)int
2.5.3 节练习
//练习2.36 请指出每一个变量的类型及程序结束时它们各自的值
int a = 3, b = 4;
decltype(a) c = a;//整数
decltype((b)) d = a;//对整数a 的引用
++c;
++d;
cout << "a= " <<typeid(a).name()<<" "<< a << endl;
cout << "b= " << typeid(b).name() << " " << b << endl;
cout << "c= " << typeid(c).name() << " " << c << endl;
cout << "d= " << typeid(d).name() << " " << d << endl;
//练习 2.37 指出下列代码中每一个变量的类型和值
int a = 3, b = 4;
decltype(a) c = a;
decltype(a = b) d = a;
/*
a int型
b int型
c int型 值为3
d int& d是变量a的别名 值为3
a,b的值仍为3,4
*/
练习2.38:说明由 decltype 和 由 auto 指定类型有和区别。并分别举一个它们指定的类型相同的例子和指定的类型不同的例子。
它们的区别主要有三个方面:
- 第一、auto类型说明符用编译器计算变量的初始值类推断其类型,而 decltype 虽然也让编译器分析表达式并得到它的类型,但是不实际计算表达式的值。
- 第二、编译器推断出来的 auto 类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则。例如, auto 一般会忽略掉顶层 const ,而把底层 const 保留下来。与之相反, decltype 会保留变量的顶层 const。
- 第三、与 auto 不同, decltype 的结果类型与表达式形式密切相关,如果变量名加上了一对括号,则得到的类型与不加括号时会有不同。如果 decltype 使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果给变量加上了一层或多层括号,则编译器将推断得到引用类型。
2.6 自定义数据结构
从最基本的层面理解,数据结构是把一组相关的数据元素组织起来然后使用它们的策略和方法。
2.6.1 定义Sales_data类型
struct Sales_data
{
string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
以 struct 关键字开始,紧跟类名和类体。类体由花括号包围形成了一个新的作用域。类内部定义的名字必须唯一,但是可以与类外部定义的名字重复。注意,类定义的最后加上分号。
类数据成员
类体定义类的成员,我们的类只有数据成员(data member)。类的数据成员定义了类的对象的具体内容,每个对象有自己一份数据成员拷贝。修改一个对象的数据成员,不会影响其他Sales_data的对象。
C++11 新标准规定,可以为数据成员提供一个类内初始值(in-class initializer)。创建对象时,类内初始值将用于初始化数据成员。没有初始值的成员将被默认初始化。
对类内初始值的限制与之前介绍的类似:或者放在花括号里,或者放在等号右边,记住不能使用圆括号。
int a = 0;
int a = {0};
int a{0};
2.6.1 节练习
编译不通过,因为没有分号。
2.6.2 使用 Sales_data 类
前面定义的 Sales_data 类没有提供任何操作,如果想执行什么操作都必须自己实现。
添加两个 Sales_data对象
编码实现输入、输出和相加功能。
程序的总体结构如下:
#include<iostream>
#include<string>
#include"Sales_data.h"
using namespace std;
int main()
{
Sales_data data1,data2;
//读入data1 和 data2 的代码
//检查 data1 和 data2 的ISBN 是否相同的代码
//如果相同,求data1 和data2 的和
system("pause");
return 0;
}
Sales_data 对象读入数据
double price = 0;//书的单价,用于计算销售收入
//读入data1 和 data2 的代码
//读入第1笔交易:ISBN、销售数量、单价
cin >> data1.bookNo >> data1.units_sold >> price;
//计算销售输入
data1.revenue = data1.units_sold*price;
//读入第2笔交易
cin >> data2.bookNo >> data2.units_sold >> price;
//计算销售输入
data2.revenue = data2.units_sold*price;
输出两个 Sales_data对象的和
//检查 data1 和 data2 的ISBN 是否相同的代码
if (data1.bookNo == data2.bookNo)
{
unsigned totalCnt = data1.units_sold + data2.units_sold;
double totalRevenue = data1.revenue + data2.revenue;
//输出:ISBN 、总销售量、总销售额、平均价格
cout << data1.bookNo << " " << totalCnt
<< " " << totalRevenue << " ";
if (totalCnt != 0)
{
cout << totalRevenue / totalCnt << endl;
}
else
{
cout << "no sales" << endl;
}
return 0;
}
else
{
cerr << "data must refer to the same ISBN" << endl;
return -1;
}
2.6.3 编写自己的头文件
为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应与类的名字一样。
预处理器概述
确保头文件多次包含仍能安全工作的常用技术是预处理器(preprocessor),它由C++语言从C语言继承而来。预处理器是在编译之前执行的一段程序,可以部分地改变我们所写的程序。比如之前已经用到了一项预处理功能 #include,当预处理器看到 #include 标记时就会用指定的头文件的内容代替 #include。
C++ 程序还会用到一项预处理功能是头文件保护符(header guard)。
#ifndef SALES_DATA_H
#define SALES_DATA_H
...
#endif
也可使用 #pragma once。
#pragma once