问题

  1. 指针和引用有4点不同,分别是哪些?
  2. const 对象必须怎样
  3. const 对象的作用范围
  4. 什么是常量引用,如何声明,是顶层还是底层
  5. 常量引用与常量对象、非常量对象的关系。
  6. 什么是常量指针,如何声明,是顶层还是底层
  7. 常量指针与常量对象、非常量对象的关系。
  8. 顶层 const 和底层 const 都是什么,在什么位置
  9. 如何区分顶层 const 和底层 const
  10. constexpr 是什么,特点是什么

回答

  1. 指针是对象而引用不是;指针可以重定向引用不可以;有指向指针的指针无引用的引用;引用必须初始化指针不需要
  2. 必须初始化
  3. 默认范围是文件内
  4. 不能改变对象的引用是常量引用,const int& i = a,是底层 const
  5. 不能用非常量引用绑定常量对象,可以用常量引用绑定非常量对象。
  6. 常量指针表明指针是个常量,其内存储的地址不能改变,但是指针还能修改所指对象的值。int* const p = a,是顶层const。
  7. 可以用常量指针指向非常量对象。
  8. 顶层 const 表示指针本身是常量,底层 const 表示所指对象是常量。顶层 const 在右边,底层 const 在左边
  9. 只有指针同时有顶层和底层,const 在星号右边是顶层,左边是底层。引用的 const 是底层,其他类型 const 是顶层。
  10. 常量表达式。两个点:值不能改变、在编译阶段就可以计算出值

问题

  1. 浮点数赋给整型变量时如何舍入?
  2. decltype 是什么,如何使用
  3. 如何声明而非定义一个变量
  4. 如果指针不初始化会有什么影响
  5. 如何在多个文件间共享 const 对象
  6. 使用 auto 来定义引用时要注意什么
  7. 预处理变量的作用范围是什么
  8. C++属于静态类型语言,静态类型语言的含义是什么?
  9. C++有两种定义类型别名的方式,分别是什么

回答

  1. 只保留小数点前的部分,即向零舍入
  2. 用来获取变量类型,decltype(c) a;
  3. 使用 extern 修饰符: extern int i:
  4. 在块作用域中,未初始化的指针的值是未定义的。
  5. 如果想在多个文件间共享const对象,必须在变量的定义前添加extern关键字并在本文件中声明。声明和定义都要加extern
  6. 用 auto 定义引用时,必须要加 & 符号。尤其是在范围 for 循环中,当想要修改值时,一定要记得加上引用符。
  7. 文件内。
  8. 静态类型语言在编译时检查变量类型。
  9. typedef unsigned int size_type 和 using size_type = unsigned int;

第2章 变量和基本类型

​ C++定义了几种基本内置类型,如字符、整型、浮点数等。

2.1 基本内置类型

​ 基本内置类型包括算数类型和空类型。算数类型包括字符、整型数、浮点数和布尔值。

2.1.1 算术类型

c++算数类型
类型 含义 最小尺寸 说明
bool 布尔类型 未定义 取值为真(true)或假(false)
char 字符 8位 1个 char 的空间应确保可以存放机器基本字符集中任意字符对应的数字值
wchar_t 宽字符 16位 用于扩展字符集,确保可以存放机器最大扩展字符集中的任意一个字符
char16_t Unicode字符 16位 用于扩展字符集,为 Unicode 字符集服务
char32_t Unicode字符 32位 用于扩展字符集,为 Unicode 字符集服务
short 短整型 16位
int 整型 16位 一个 int 至少和 一个 short 一样大
long 长整形 32位 一个 long 至少和一个 int 一样大
long long 长长整形 64位 C++11 中新定义,一个 long long 至少和 一个 long 一样大
float 浮点型 6位有效数字
double 双精度浮点型 10位有效数字
long double 扩展精度浮点型 10位有效数字 常常用于有特殊浮点需求的硬件

​ 计算机以比特序列存储数据,每个比特非0即1,
​ 可寻址的最小内存块称为“字节(byte)”,内存的基本单元称为“字(word)”
​ 大多数机器的字节由8比特构成,字则由32或64比特构成

  • 无符号类型
    int、short、long和long long都是带符号的,前面加上unsigned就可以得到无符号类型,例如unsigned long
    unsigned int可以缩写成unsigned。
    char比较特殊,类型分为三种:char、signed char、unsigned char
    char是signed char或unsigned char的其中一种(编译器决定)

c++字符类型选择:

​ 明确知晓数值不可能为负时,选用无符号类型。

​ 整数运算用 int,数值太大时用 long long,不用 short 和 long

​ 浮点数运算用 double。float 和 double 的计算代价相差无几

2.1.2 类型转换

几种类型转换的情况:

  1. 把浮点数赋给整型时,结果仅保留小数点前的部分。
  2. 赋给无符号类型超出范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。比如 -1 赋给 8 位 unsigned char 的结果是 255(-1=256*(-1)+255)
  3. 赋给带符号类型超出范围的值时,结果是未定义的。程序可能工作,可能崩溃。
1
2
3
4
5
6
7
bool b=42;		        //b为真
int i=b; //i的值为1
i=3.14; //i的值为3
double pi=i; //pi的值为3.0
unsigned char c=-1; //假设char占8比特,c的值为255
signed char c2=256; //假设char占8比特,c2的值未定义

  1. 程序尽量避免依赖于实现环境的行为。比如 int 的尺寸在不同环境可能不同。
  2. 含有无符号类型的表达式
  3. 个表达式中既有无符号数又有int值时,int会被转换成无符号数。
  4. 无符号减无符号数,结果还是无符号数,如果是负值就等于该符数加上无符号数的模,切勿混用带符号类型和无符号类型。
1
2
3
4
5
unsigned u=10;
int i=-42;
std::cout << i+i << std::endl;//输出-84
std::cout << u+i << std::endl;//如果int占32位,输出4294967264

2.1.3 字面值常量

​ 一个型如42的值就称为字面值常量(literal)

​ 整型和浮点型字面值

​ 整型字面值中 0 开头的整数是 8 进制,0x 开头的整数是十六进制。

​ 整型字面值的具体数据类型由它的值和符号决定。默认情况下十进制字面值是带符号数,类型是 int, long, long long 中能容纳当前值的尺寸最小的那个。

​ 浮点型字面值可以用小数或科学计数法表示,科学计数法中的指数部分用 E 或 e 标识。

1
3.14159 3.14169E0 0. 0e0 .001

字符和字符串字面值

​ 单引号括起来的一个字符是 char 型字面值,双引号括起来的 0 个或多个字符则构成字符串型字面值。

​ 字符串字面值的类型实际上是字符数组,编译器会向每个字符串结尾添加一个空字符(’\0’),因此字符串字面值的实际长度要比它的内容多 1。如 “A” 代表了一个长度为 2 的字符数组。

​ 如果两个字符串字面值紧邻且仅由空格、缩进和换行符分隔,则它们实际上是一个整体。因此字符串比较长时可以直接分行书写。

​ “A is B” “and B is A”; //两个字符串实际上是一个整体。

转义序列

C++ 定义的转义序列包括:

转义序列

在程序中,上述转义序列被当作一个字符使用。

也可以使用泛化的转移序列,形式是 \后跟 1~3 个八进制数字或 \x 后跟 1 个或多个十六进制数字。

泛化的转义序列

指定字面值的类型

可以通过给字面值增加前缀和后缀来改变字面值的默认类型。

常用字符和字符串字面值

注意 12f 是错的,不能给整型字面值加 f 后缀,可以使用 12.f。

2.2 变量

对于c++而言,”变量“和”对象“一般可以互换使用。

c++中,对象通常指一块能存储数据并具有某种类型的内存

2.2.1 变量定义

● 变量提供一个具有名称的、可供程序操作的存储空间。
● 变量都具有数据类型,通过数据类型可以决定变量所需要的内存空间、布局方式、以及能够表示值的范围

1
2
3
4
5
int sum=0,value,//sum、value和unit_sold都是int
units_sold=0;//sum和units_sold初值为0
Sales_item item;//item的类型是Sales_item
//string 是一种库类型,表示一个可变长的字符序列
std::string book("0-2-1-78345-X");//book通过一个string字面值初始化

注:对于我们自己定义的类型item,我们希望自己定义的类型和它内置的类型用法上是接近的。最好是一样,这样我们的学习成本比较低可以直接拿过来使用。

初始化

​ 可以在同一条定义语句中使用先定义的变量去初始化后定义的其他变量。

​ 初始化不是赋值,初始化是创建变量时赋予一个初始值,赋值是把对象的当前值擦除并用一个新值来替代。

​ 下面四种初始化方式都是可行的,其中使用花括号的方式叫做列表初始化。

1
2
3
4
int units_sold = 0;
int units_sold = {0};//列表初始化
int units_sold{0};//列表初始化
int units_sold(0);

​ 当用于内置类型的变量时,使用列表初始化且初始值存在信息丢失的风险,编译器会报错。

1
2
3
long double ld = 3.1415926536;
int a{ld},b={ld};//错误:转换未执行,因为存在丢失信息的风险
int c(ld),d=ld;//正确:转换执行,且确实丢失了部分值

默认初始化

● 如果定义变量没有定义初始值,则变量被赋予默认值。
● 默认值是由变量类型决定的,同时定义变量的位置也会有影响。
● 内置类型:由定义的位置决定,函数体之外初始化为0,函数体内不初始化。
● 每个类各种决定其初始化对象的方式

类的对象如果没有显式地初始化,则其由类确定。string 默认初始化为一个空串。

1
2
3
std:::string empty;//empty非现实地初始化一个空串
Sales_item item;//被默认初始化的Sales_item对象

未初始化的变量含有一个不确定的值,将带来无法预计的后果,应该避免。建议初始化每一个内置类型的变量。

2.2.2 变量声明和定义的关系

Q:变量声明和定义的区别

A:为变量分配地址和存储空间的称为定义,不分配地址的称为声明。一个变量可以在多个地方声明,但是只在一个地方定义。加入 extern 修饰的是变量的声明,说明此变量将在文件以外或在文件后面部分定义。

​ 说明:很多时候一个变量,只是声明不分配内存空间,直到具体使用时才初始化,分配内存空间,如外部变量。

​ c++是一种静态类型语言,其含义是在编译阶段检查类型。这就要求我们在使用某个变量之前必须先声明。
​ 如果想声明一个变量而非定义它,就在变量名前添加extern关键字,而且不要显示的初始化,任何包含了显式初始化的声明即成为定义。声明变量不能赋值。例:extern int i = 1; // 定义 i,初始化抵消了 extern 的作用。

​ extern不是定义,是引入(声明)在其它源文件中定义的非static全局变量

1
2
3
4
5
extern double pi = 3.14;//定义,不能放在函数体内部
int main(){
extern int i;//声明i而非定义i
int j;//声明并定义j
}

变量只能被定义一次,但是可以多次声明。

2.2.3 标识符

标识符组成:字母、数字、下划线。不能以数字开头,对大小写敏感。标识符的长度没有限制。

名字的作用域

​ 同一个名字如果出现在程序的不同位置,也可能指向不同的实体。
​ C++中大多数作用域都以花括号分隔。
​ 名字的有效区域始于名字的声明语句,以声明语句所在的作用域末端为结束。

image-20220422150135299

如果函数有可能用到某全局变量,则不宜再定义一个同名的局部变量。

2.3 复合类型

复合类型就是基于其他类型定义的类型,引用和指针是其中两种。

2.3.1 引用

引用是为对象起的另一个名字定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用引用本身并不是对象,所以不能定义引用的引用

1
2
3
4
5
6
7
8
9
10
int ival = 1024;
int &refVal = ival;//refVal指向ival(是ival的另一个名字)
int &refVal2;//报错:引用必须初始化
refVal = 2;//把2给refVal指向的对象,此处即是赋给了ival
int li = refVal;//等同于li=ival
//正确:refVal13绑定到了那个与refVal绑定的对象上,即绑定了ival
int &refVal3 = refVal;
//利用与refVal绑定的对象的值初始化变量i
int i = refVal;//正确:i被初始化为ival的值

引用不是对象,不存在地址,所以不能定义指向引用的指针。

引用必须初始化。引用的初始值必须是一个对象,不能是字面值。

对引用的所有操作都是对与之绑定的对象的操作。

引用只能绑定在对象上,不能与字面值或表达式绑定。

引用只能绑定同类型对象。

2.3.2 指针

指针:对地址的封装,本身就是一个对象
●定义指针类型的方法是将声明符写成d的形式
●如果一条语句中定义了几个指针变量,每个变量前面都必须加上符号
●和其他内置类型一样,在块作用域内定义指针如果没有初始化,将拥有一个不确定的值

指针必须指向指定的类型,不能指向其他类型。

1
2
int *ip1, *ip2;//ip1和ip2都是指向int型对象的指针
double dp, *dp2;

指针与引用的不同:

  1. 指针是一个对象而引用不是;
  2. 指针可以重定向引用不可以;
  3. 有指向指针的指针无引用的引用;
  4. 指针不需要在定义时赋初值而引用需要。
  5. 不能定义指向引用的指针。可以定义指向指针的引用。

​ int p; int &r = p; // r是对指针p的引用

面对如上 *&r 这样比较复杂的指针或引用的声明语句时,从右向左读比较易于弄清。

可以使用取地址符(操作符&)获取指针所封装的地址:

1
2
3
int ival = 42;
int *p = &ival;//p是指向ival的指针
double *dp = &ival;//错误:类型不匹配

可以使用解引用符(操作符*)利用指针访问对象

1
2
3
4
5
int ival = 42;
int *p = &ival;//p是指向ival的指针
std::cout<<*p ;//输出42
*p=0;
std::cout<<*p; //输出0

空指针(null pointer)不指向任何对象

在使用一个指针之前,可以首先检查它是否为空。

1
2
3
int *p1 = nullptr; //C++11(最好使用这种方式), NULL 是在头文件 cstdlib 中定义的预处理变量,值为 0
int *p2 = 0;
int *p3 = NULL; //需要#include cstdlib
1
2
int zero = 0;
p1 = zero; //错误:类型不匹配

建议初始化所有指针。

非零指针对应的条件值是 ture,零指针对应的条件值是 false。

void*指针

纯粹的地址封装,与类型无关。可以用于存放任意对象的地址。

void* 指针和空指针不是一回事。

1
2
3
double obj = 3.14, *pd = &obj;
void *pv = &obj;
pv = pd;

指向指针的指针

通过*的个数可以区分指针的级别

1
2
3
int ival = 1024;
int *pi = &ival;
int **ppi = &pi;//ppi指向一个int型的指针

指向指针的引用

指针是对象,可以定义引用。

1
2
3
4
5
6
int i = 1024;
int *p;
int *&r = p; //r是一个对指针p的引用

r = &i;//r引用了一个指针,就是令p指向i
*r = 0;//解引用r得到i,也就是p指向的对象,将i的值改为0

2.3.3 理解复合类型的声明

定义复合类型的变量要比定义基本类型的变量复杂很多。

一条声明语句是由一个基本数据类型和紧随其后的声明符列表组成的。

引用符 & 和指针符 * 都是类型说明符,类型说明符是声明符的一部分。

​ int &a=b, &c=b; int *a=nullptr, b=1;

2.4 const限定符

把变量定义成一个常量

使用const对变量的类型加以限定,变量的值不能被改变

1
2
const int bufSize = 512; //输入缓冲区大小
bufSize = 512; //错误:试图向const对象写值

​ const 对象必须初始化,因为一旦创建就不能再改变值。(其他时候不能出现在等号左边)

1
2
3
const int i = get_size(); //正确:运行时初始化
const int j = 42; //正确:编译时初始化
const int k; //错误:k是一个未经初始化的常量
1
2
const int bb=0;
void * a= bb;//在编译的时候,会把bb编程常量

​ 默认情况下,const 对象仅在文件内有效。如果想在多个文件间共享 const 对象,必须在变量的定义前添加 extern 关键字并在本文件中声明。声明和定义都要加 extern

1
2
3
4
//file_1.cc定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufSize = fcn();
//file_1.h头文件
extern const int bufSize;

2.4.1 const的引用

​ const 的引用:常量引用。

1
2
3
4
5
6
const int ci = 1024;
const int &r1 = ci; //正确:引用及其绑定的对象都是常量

r1 = 42;//错误,相当于c1=42,试图修改常量
int &r2 = ci;//错误:ci是常量,存在通过r2改变ci(const)的风险

1
2
3
4
5
int i = 42;
const int &r1 = i; //正确:i依然可以通过其他途径修改
const int &r2 = 42;
const int &r3 = r1*2;
int &r4 = r1*2; //错误:不能通过一个非常量的引用指向一个常量可以用常量引用指向一个非常量对象

​ 对象不必是常量。对 const 对象的引用也必须是常量。

​ 引用必须初始化,因此常量引用也必须初始化。

​ 注意引用不是对象,因此常量引用不是说引用是常量,引用本来就只能绑定一个对象,而是引用不能改变引用的对象了。

​ 引用的类型必须与其所引用对象的类型一致,但是有两个例外。其中一个例外就是初始化常量引用时允许用任意表达式作为初始值(包括常量表达式),只要该表达式结果可以转换为引用的类型。

​ const int &r = 42; // 常量引用可以绑定字面值

当用常量引用绑定一个非常量对象时,不能通过引用改变引用对象的值,但是可以通过其他方式改变值。常量指针也一样。

2.4.2 指针和const

​ 指向常量的指针的用法和常量引用相似,但是是不一样的。它既可以指向常量也可以指向非常量,不能改变对象的值。但是非常量对象可以通过其他途径改变值

●指向常量的指针

1
2
3
4
5
6
7
const double pi = 3.14;
double *ptr = &pi; //错误:存在通过ptr指针修改pi的风险
const double * cptr = &pi;
*cptr = 42; //错误

double dval = 3.14;
cptr = &dval; //正确:但不能通过cptr修改dval的值

const指针:指针是对象,也可以限定为常量(必须初始化)
把*放在const之前,说明指针是一个常量
不变的是指针本身的值,而非指向的那个值

1
2
3
4
5
6
7
8
9
10
11
int errNumb = 0;
int *const curErr = &errNumb;
const double pi = 3.14159;
const double *const pip = &pi;//指向常量的常量指针

*pip = 2.71;//错误: 试图修改常量pi

if(*curErr){
errorHandler();
*curErr = 0; //正确:试图修改变量errNumb
}

2.4.3 顶层const

​ 顶层 const 表示指针本身是个常量,底层 const 表示指针所指的对象是一个常量。顶层 const 对任何数据类型通用,底层 const 只用于引用和指针。顶层 const 的指针表示该指针是 const 对象,因此必须初始化。底层 const 的指针则不用。

​ 实际上只有指针类型既可以是顶层 const 也可以是底层 const,因为引用实际上只能是底层 const,常量引用即为底层 const,不存在顶层 const 的引用。

​ 从右向左读来判断是顶层 const 还是底层 const。

​ 对于指针和引用而言,顶层 const 在右边,底层 const 在左边。对于其他类型,全都是顶层 const

​ 执行对象的拷贝操作时,不能将底层 const 拷贝给非常量,反之可以,非常量将会转化为常量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int i = 0;
int *const p1 = &i; //顶层
const int ci = 42; //顶层
const int *p2 = &ci;//底层
const int *const p3 = p2;//(左:底层 ), (右:顶层)

i = ci;//正确
p2 = p3;//正确

int *p = p3;//错误:存在通过*p修改*p3(const)的风险
p2 = &i; //正确:只是不能通过p2修改i而已
int &r = ci; //错误:存在通过r修改ci(const)的风险
const int &r2 = i; //正确:只是不能通过r2修改i而已

2.4.4 constexpr和常量表达式(const expression)

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

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

1
2
3
4
const int max_files = 20; //是
const int limit = max_files +1; //是
int staff_size = 27; //不是
const int sz = get_size(); //不是

cosntexpr变量

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

需要在编译时就得到计算,声明constexpr时用到的类型必须显而易见,容易得到(称为:字面值类型)。自定义类型(例如:Sales_item)、IO库、string等类型不能被定义为constexpr

​ cosntexpr 指针的初始值必须是 nullptr 或 0 或存储于固定地址的对象。函数体之外的对象和静态变量的地址都是固定不变的。

1
2
3
constexpr int mf = 20;
constexpr int limit = mf +1;
constexpr int sz = size(); //只有当size是一个constexpr函数时才正确

指针和constexpr
限定符仅对指针有效,对指针所指的对象无关

1
2
3
4
5
6
7
constexpr int *np = nullptr; //常量指针
int j = 0;
constexpr int i = 42;

constexpr const int *p = &i; //p是常量指针,指向常量
constexpr int *p1 = &j; //p1是常量指针,指向变量j

注意区分 constexpr 和 const 。constexpr 都是顶层 const,仅对指针本身有效。

区分const和constexpr

constexpr 限定了变量是编译器常量,即变量的值在编译器就可以得到。

const 则并未区分是编译器常量还是运行期常量。即 const 变量可以在运行期间初始化,只是初始化后就不能再改变了。

constexpr 变量是真正的“常量”,而 const 现在一般只用来表示 “只读”。

2.5 处理类型

随着程序越来越复杂,程序中的变量也越来越复杂。
拼写变得越来越困难。
搞不清楚变量到底需要什么类型。

2.5.1 类型别名

作用:提高程序可读性

1
2
3
4
5
6
typedef double wages; 
typedef wages base, *p; //base是double的同义词,p是double *的同义词

using SI = Sales_item; //C++11,别名声明
wages hourly, weekly;
SI item; //等价于Sales_item item

对于指针这样的复合类型,类型别名的使用可能会产生意想不到的结果:

1
2
3
4
5
typedef char *pstring;
const pstring cstr = 0; //指向char的常量指针
const pstring *ps; //ps是指针变量,它的对象是指向char的常量指针

const char *cstr = 0; //是对const pstring cstr =0 的错误理解

有两种方法定义类型别名。

​ typedef double wages; // 使用 typedef 关键字

using wages = double; // 使用 using 关键字进行别名声明

typedef 作为声明语句中的基本数据类型的一部分出现。含有 typedef 的声明语句定义的不再是变量而是类型别名。和其他声明语句一样,typedef 的声明语句中也可以包含类型修饰符,从而构造符合类型。

​ typedef wages base, p; // base 是 double 的别名,p 是 double 的别名。

指针、常量和类型别名

​ typedef char* pstring; const pstring cstr = 0; // 注意:const 是一个指向 char 的常量指针。不能采用直接替换的方式将其理解为 const char* cstr = 0,这是错误的。

2.5.2 auto类型说明符

auto 说明符让编译器根据初始值来分析表达式所属的类型。理解:使用 auto 会增加编译时间,但不会增加运行时间。

1
2
3
4
auto item = val1 + val2; 

auto i =0, *p = &i; //正确
auto sz = 0, pi = 3.14; //错误:auto已经被推断为int型,却需要被推断为double型

auto 可以在一条语句中声明多个变量,但是多个变量必须是同一个基本数据类型(整型与整型指针和整型引用算一个类型)。

复合类型、常量和auto

编译器推断出的 auto 类型有时和初始值并不一样,编译器会进行适当的调整:

  1. auto 根据引用来推断类型时会以引用对象的类型作为 auto 的类型。
  2. auto 一般会忽略掉顶层 const,因此对于非指针类型的常量对象,auto 推断出的结果是不含 const 的。如果希望 auto 是一个顶层 const,需要明确指出。
  3. auto 会保留底层 const。

概括一下就是 auto 会忽略引用与顶层 const。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int i = 0, &r = i;
auto a = r; //a是int型

const int ci = i, &cr = ci;
auto b = ci; //b是int型,ci的顶层const被忽略
auto c = cr; //c是int型,ci的顶层const被忽略
auto d = &i; //d是整形指针,整数的地址就是指向整形的指针
auto e = &ci; //e是指向整数常量的指针(底层const没有被忽略)

const auto f = ci; //auto的推演类型为 int,f是const int

auto &g = ci; //g是一个整形常量引用,绑定到ci,(底层const没有被忽略)

auto &h = 42; //错误:不能为非常量引用绑定字面值
const auto &j =42; //正确:可以为常量引用绑定字面值

auto k = ci, &l = i;
auto &m = ci, *p = &ci;
auto &n = i, *p2 = &ci; //错误:类型不一致

int 与 int *、int & 是一个基本数据类型,而 const int 与 int 不是一种类型。

用 auto 定义引用时,必须用 & 指明要定义的是引用。

2.5.3 decltype类型指示符

当希望获得表达式的类型但是不要值的时候,可以使用类型说明符 decltype。

如果 decltype 使用的表达式是一个变量,则它返回该变量的类型(包括顶层 const 和引用在内)。

decltype 与 auto 的不同:decltype 不会忽略引用和顶层 const。

注意当获得的类型是引用时,必须初始化。

1
2
3
4
5
6
decltype(f()) sum = x;// sum的类型就是函数f返回的类型

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

引用从来都是作为对象的别名出现,只有在 decltype 处是例外。

decltype 和引用

如果 decltype 使用的表达式不是一个变量,则 decltype 返回表达式结果对应的类型。可以使用这种方式来保证不获取引用类型。

注意:如果表达式的内容是解引用操作,则decltype将得到引用类型。给变量加括号的结果也是引用类型。赋值操作的结果也是引用类型。

1
2
3
4
5
6
7
8
9
10
int i = 42, *p = &i, &r = i;
decltype(r+0) b; //正确:b为int型

//注意:下面的*不是出现在声明中,而是表达式中的解引用运算符
decltype(*p) c; //错误:解引用表达式,c的类型为引用,需要初始化

//变量如果是加上括号, decltype的结果将是引用
decltype( (i) ) d; //错误:d是int&类型,必须初始化
decltype( ( (i) ) ) d1 = i; //正确:d1是int&类型,绑定为了i
decltype( i ) e; //正确:e是一个(未初始化的)int

decltype((var)) 的结果永远是引用,而 decltype(var) 的结果只有当 var 本身就是引用时才是引用。

2.6 自定义数据结构

一组数据以及相关操作的集合

2.6.1 定义sales_data类型

类定义:类定义可以使用关键字class或struct
二者默认的继承访问权限不同
struct是public的,class是private的

1
2
3
4
5
struct Sales_data{
std::string bookNo;
unsigned units_sold = 0; //C++ 11
double revenue = 0.0;
};//类定义的最后需要加上分号

数据成员定义了类的对象的具体内容,每个对象有自己的一份拷贝。

struct+类名+类体+分号。类体可以为空。

​ struct Sales_data{}; // 注意:结尾加分号

定义类时可以给数据成员提供类内初始值以进行初始化。没有类内初始值的成员则被默认初始化。

类内初始值可以放在花括号中或等号的右边,不能使用圆括号。

类使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
#include <string>
#include "Sales_data.h"
int main(){
Sales_data data1, data2;
double price = 0; // 书的单价, 用于计算总收入

// 读取第1条交易记录: ISBN, number of books sold, price per book
std::cin >> data1.bookNo >> data1.units_sold >> price;
// calculate total revenue from price and units_sold
data1.revenue = data1.units_sold * price;

// 读取第2条交易记录:
std::cin >> data2.bookNo >> data2.units_sold >> price;
data2.revenue = data2.units_sold * price;

if (data1.bookNo == data2.bookNo) {
unsigned totalCnt = data1.units_sold + data2.units_sold;
double totalRevenue = data1.revenue + data2.revenue;

// print: ISBN, total sold, total revenue, average price per book
std::cout << data1.bookNo << " " << totalCnt << " " << totalRevenue << " ";
if (totalCnt != 0) std::cout << totalRevenue/totalCnt << std::endl;
else std::cout << "(no sales)" << std::endl;

return 0; // indicate success
} else { // transactions weren't for the same ISBN
std::cerr << "Data must refer to the same ISBN" << std::endl;
return -1; // indicate failure
}
}

输出结果

1
2
3
1-1 5 5
1-1 10 4.5
1-1 15 70 4.66667

2.6.3 编写自己的头文件

类通常定义在头文件中,类所在头文件的名字应与类的名字一样。

头文件通常定义那些只能被定义一次的实体,比如类、const、constexpr 等。

头文件一旦改变,相关的源文件必须重新编译以获取更新过的声明。

预处理器概述

确保头文件多次包含仍能安全工作的常用技术是预处理器。

预处理变量有两种状态:已定义和未定义。一般把预处理变量的名字全部大写。

整个程序中的预处理变量包括头文件保护符必须唯一,通常基于头文件中类的名字来构建保护符的名字,以确保其唯一性。

c++ 中包含三个头文件保护符

  1. #define:把一个名字设定为预处理变量
  2. #ifndef:当且仅当变量已定义时为真,一旦检查结果为真,则执行后续操作直到遇到 #endif 为止
  3. #endif

预处理变量无视作用域的规则,作用范围是文件内

P4BHJQ(31HASZWPEZL)0YDK