c++ primer 笔记:变量和基本类型及练习题解

基本内置类型

包括算数类型空类型。其中算数类型包含了字符、整型数、布尔值和浮点数。空类型不对应具体指,用于特殊场合。

算术类型

算数类型分为两类:**整型(包括字符和布尔类型)**和浮点型。算数类型的尺寸在不同机器上有差别,下表是 C++ 标准规定的最小尺寸值。

类型 含义 最小尺寸
bool 布尔类型 未定义
char 字符 8位
wchar_t 宽字符 16位
char16_t Unicode 字符 16位
char32_t Unicode 字符 32位
short 短整型 16位
int 整型 16位
long 长整型 32位
long long 长整型 64位
float 单精度浮点数 6位有效数字
double 双精度浮点数 10位有效数字
long double 扩展精度浮点数 10位有效数字

 C++ 提供了几种字符类型,基本的是 char,大小等于一个机器字节。其他字符类型用于扩展字符集,wchar_t 用于存放机器最大扩展字符集中任意一个字符,char16_t 和 char32_t 为 Unicode 字符集服务。
 C++ 规定一个 int 至少和一个 short 一样大,一个 long 至少和一个 int 一样大,一个long long 至少和 一个 long 一样大。其中 long long 是 C++11 中新定义的。
 通常,float 以 1 个字(32比特)表示,double 以 2 个字(64比特)表示,long double 以 3 或 4 个字表示。

带符号类型和无符号类型
 除了布尔型和拓展字符型,其他整型分为带符号的无符号的两种。带符号类型可以表示整数、负数或 0,无符号类型则仅能表示大于 0 的值。
 类型 int、short、long 和 long long 是带符号整数,在其前面加 unsigned 则可得到无符号类型。
 字符型被分为三种:char、signed char、unsigned。类型 char 经过编译器解释为 signed char 或 unsigned char。

如何选择类型

- 数值不可能为负时,选用无符号类型。
- 使用 int 执行算术运算。如果数值超过了 int 的表示范围,选用 long long。
- 在算数表达式中不要使用 char 或 bool。
- 执行浮点数运算选用 double。

练习2.1:类型 int、long、long long 和 short 的区别是什么?无符号类型和带符号类型的区别是什么?float 和 double 的区别是什么?
  • 三个整型的最小尺寸不同,int 是16位,long 是 16位,long long 是 32位。在不同的机器和编译器上三者尺寸可能会变大。不同类型占得位数不同,表示的范围也不同。
  • 无符号类型不能表示负数,有符号类型能表示正数、0、负数。
  • float 是单精度浮点数,double 是双精度浮点数。通常 float 在内存中占 32 位,double 占 64位,两者能表示数的范围和精度也不同。
    练习2.2:计算按揭贷款时,对于利率、本金和付款分别应选择何种数据类型?
    三者一般都是小数,用整型会造成小数点后的部分丢失,所以应选择浮点数。又因为 float 与 double 计算成本类似,而 double 范围与精度都比 float 大;long double 虽然表示范围与精度较大,但计算成本太高。所以选择 double。

类型转换

进行下面赋值:

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 的值是未定义的

对变量进行强制类型转换时,一般按照下面的规则:

  • 非布尔类型的算数值赋给布尔类型时,初始值为 0 则结果为 false,否则结果为 true。
  • 布尔值赋给非布尔类型时,初始值为 false 则结果为 0,初始值为 true 则结果为 1。
  • 浮点数赋给整数类型时,进行近似处理。只保留浮点数中小数点之前的部分。
  • 整数类型赋给浮点数类型时,小数部分记为 0 。如果整数所占空间超过了浮点类型的容量,精度有可能损失。
  • 赋给无符号类型一个超出他表示范围的值时,结果是初始值对无符号类型表示数值取模后的余数。
  • 赋给带符号类型一个超出它表示范围的值时,结果是未定义的。

当程序中使用了一种算数类型的值而所需的是另一种类型的值时,编译器同样会进行上述的类型转换。例如:

int i = 42;
if(i)
	i = 0;

如果 i 为 0, 则条件的值为 false;i 为非 0 则为true。

含有无符号类型的表达式

unsigned u = 10;
int i = -42;
std::cout << i + i << std::endl;  //输出-84
std::cout << i + u << std::endl;  //如果 int 占 32 位,输出4294967264

第一个表达式正确,但第二个表达式输出错误,因为 -42 在运算时会被强制转换成无符号数,其数值就为其取模后的值。

unsigned u1 = 42, u2 = 10;
std::cout << u1 - u2 << std::endl;  //输出32
std::cout << u2 - u1 << std::endl;  //输出4294967264

第一个表达式正确,第二个错误,原因和上一个代码段一样。

所以:切勿混用带符号类型和无符号类型

练习 2.3:读程序写结果。
unsigned u = 10, u2 = 42;
std::cout << u2 - u << std::endl;
std::cout << u - u2 << std::endl;

int i = 10, i2 = 42;
std::cout << i2 - i << std::endl;
std::cout << i - i2 << std::endl;
std::cout << i - u << std::endl;
std::cout << u - i << std::endl;
32 4294967264 32 -32 0 0 练习 2.4:编写程序检查你的估计是否正确,如果不正确,请仔细研读本节直到弄明白问题所在。

在这里插入图片描述

字面值常量

42 这样一望可知的值称为字面型常量,每个字面值常量都对应一种数据类型,字面值常量的形式和值决定了它的数据类型。

整型和浮点型字面值

整型可以写为十进制数、八进制数或十六进制数的形式。以 0 开头的整型数代表八进制数,以 0x开头的代表十六进制数。
十进制字面值的类型是能容纳其数值的 int、long 和 long long 中尺寸最小的那个。八进制与十六进制字面值的类型是能容纳其数值的 int、unsigned int、long、unsigned long、long long 和 unsigned long long 中的尺寸最小者。如果字面值关联类型中最大的类型都放不下,将产生错误。short 没用字面值。

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

字符和字符串字面值
由单引号括起来的一个字符称为 char 型字面值,双引号括起来的多个字符构成字符串型字面值。
字符串实际上是常量字符构成的数组。编译器在每个字符串结尾处添加一个空字符(‘\0’),因此,字符串字面值的实际长度比内容多 1。
如果两个字符串字面值位置紧邻且仅由空格、缩进和换行符分隔,则它们实际上是一个整体。

//多行书写的字符串字面值
std::cout << "a really, really long string literal"
		"that spans two lines" <

转义序列
有两类字符程序员不能直接使用:一类是不可打印的字符,如退格或其他控制字符,因为他们不可视;另一类是 C++ 中有特殊含义的字符(单引号、双引号、问号、反斜线)。在这些情况下要用到转义序列,C++ 规定的转义序列包括:

换行符:\n
横向制表符:\t
报警符:\a
纵向制表符:\v
退格符:\b
双引号:"
反斜线:\
问号: ?
单引号:’
回车符:\r
进纸符:\f

此外还有泛化的转义序列,其形式是 \x 后紧跟 1 个或多个十六进制数字,或者 \ 后紧跟 1 个、2个、3个八进制数字,假设使用 Latin-1 字符集,下面是一些实例:
\7(响铃) \12(换行符) \40(空格) \0 (空字符) \115(字符M) \x4d(字符M)

指定字面值的类型
通过添加下面几个表中所列的前缀和后缀,可以改变整型、浮点型和字符型字面值的默认类型。

字符和字符串字面值
前缀 含义 类型
u Unicode 16 字符 char16_t
U Unicode 32 字符 char32_t
L 宽字符 wchar_t
u8 UTF-8(仅用于字符串字面长量) char
整型字面值
后缀 最小匹配类型
u 或 U unsigned
l 或 L long
ll 或 LL long long
浮点型字面值
后缀 类型
f 或 F float
l 或 L long double
L'a' //宽字符型字面值,类型是 wchar_t
u8"hi!" //utf-8 字符串字面值(utf-8 用 8 位编码一个 Unicode 字符)
42ULL   //无符号长长整型字面值,类型是 unsigned long long 
1E-3F   //科学计数法表示的单精度浮点型字面值,类型是float
3.14159L//扩展精度浮点数字面值,类型是 long double

布尔字面值和指针字面值
true 和 false 是布尔类型的字面值,nullptr 是指针字面值。

练习 2.5:指出下述字面值的数据类型并说明每一组内几种字面值的区别:

1. ‘a’, L’a’, “a”, L"a"
2.10, 10u, 10L, 10uL, 012, 0xC
3. 3.14,3.14f,3.14L
4. 10,10u,10.,10e-2

答:
1. ‘a’:字符型字面值,L’a’:宽字符型字面值, “a”:字符串字面值, L"a":宽字符串字面值
2. 10:整型字面值, 10u:无符号整型字面值,类型是unsigned, 10L:长整型字面值, 10uL:无符号长整型字面值, 012:八进制表示的整数, 0xC:是一个十六进制数。
3. 3.14:浮点型字面值,3.14f:float类型的单精度浮点数,3.14L:long double类型的扩展精度浮点数。
4. 10:整数,10u:无符号整数,10.:浮点数,10e-2:科学计数法表示的浮点数。

练习 2.6:下面两组定义是否有区别,如果有,则叙述:

int month = 9, day = 7;
int month = 09, day = 07;

有区别,第一组定义是用十进制的字面值给整型变量赋值,month 和 day 的值分别为 9 和 7;第二组定义是用八进制整数给整型变量赋值,但是八进制表示中不会出现大于等于 8 的数,所以第二组赋值会报错。

练习 2.7:下述字面值表示何种含义?他们各自的数据类型是什么?
(a)“Who goes with F\145rgus?\012”
(b) 3.14elL
(c)1024f
(d) 3.14L
答:
(a)\145表示字符 e,\012 表示换行符,数据类型是字符串。
(b) 科学计数法表示的扩展精度浮点数。
(c)试图定义单精度浮点数,但是整数 1024 后直接跟了 f,编译器将报错;改写成 1024.f就好了。
(d) 扩展精度浮点数,类型是 long double。

练习 2.8:请利用转义序列编写一段程序,要求先输出 2M,然后转到新的一行。修改程序使其先输出 2,然后输出制表符,再输出 M,最后转到新一行。

cout << "2\115\12";
cout << "2\t\115\12";

变量

变量定义

变量定义的基本形式:首先是类型说明符,随后紧跟由一个或多个变量名组成的列表,其中变量名以逗号相隔,最后以分号结束:

int sum = 0, value, units_sold = 0;
std::string book("0-201-23049-X");

book 的定义用到了库类型 std::string,上例中把字面值拷贝给 string 对象。

术语:何为对象?
对象指一块能存储数据并具有某种类型的内存空间。

初始值
当对象在创建时获得了一个特定的值,这个对象就被初始化了。

初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而复制的含义是把对象的当前值擦除,而以一个新的值代替。

列表初始化
C++语言定义了初始化的几种形式。例如要定义一个名为 units_sold 的变量并初始化为 0,以下 4 条语句都可以:

int units_sold = 0;
int units_sold = {
    
    0};
int units_sold{
    
    0};
int units_sold(0);

第二、三种赋值方法称为列表初始化
当用于内置类型的变量时,这种初始化形式有一个重要特点:如果使用列表初始化且初始值存在丢失信息的风险,则编译器将报错:

long double ld = 3.1415926536int a{
    
    ld}, b = {
    
    ld};           //错误:转换未执行,存在丢失信息的风险。
int c(ld), d = ld;             //正确:转换执行,丢失部分信息。

默认初始化

定义变量时没有指定初值,则变量被默认初始化
如果是内置类型未被显示初始化,它的值由定义的位置决定。定义于函数之外的变量初始化为 0。定义在函数体内部的内置类型变量将不被初始化。一个未被初始化的变量值是未定义的。

练习 2.9:解释下列定义的含义。对于非法的定义,请说明错在何处并将其改正。

(a)std::cin >> int input_value;
(b)int i = {3.14};
(c)double salary = wage = 9999.99;
(d)int i = 3.14;
答:
(a)std::cin 后应该跟变量名称,不应该是变量的定义。改正后:int input_value; std::cin >> input_value;
(b)使用列表初始化将浮点数赋初始化整型对象,因存在信息丢失的风险,所以会警告。
(c)该语句试图将 9999.99 初始化 salary 和 wage,但是在声明多个变量时要用逗号隔开,不能直接用等号相连。改正后:double salary, wage; salary = wage = 9999.99;
(d)和 b 一样,会造成小数部分丢失。

练习 2.10:下列变量的初值分别是什么?

std::string global_str;
int global_int;
int main()
{
    
    
 	int local_int; 
 	std::string local_str;
}
答:string 不是内置类型,所以 global_str 与 local_str 都被初始化为空串。int 为内置类型,函数范围外的 global_int 被初始化为 0,函数内的 local_int 不会被初始化。

提示:未初始化变量引发运行时故障
使用未初始化的变量会带来无法预计的后果。有可能一访问此类对象就会报错,但也有可能产生错误的结果,无法把握。
所以要初始化每一个内置类型的变量。

变量声明和定义的关系

声明使得名字为程序所知,定义负责创建于名字关联的实体。
如果想声明变量而非定义它,就在变量名前添加关键字 extern,而且不要显示初始化变量,否则就称为了定义:

int j;                   //声明并定义 j
extern int i;            //声明 i 而非定义 i
extern double pi = 3.14  //定义 pi

在函数体内部,初始化一个由 extern 关键字标记的变量将引发错误。变量只能被定义一次,但是可以被多次声明。

练习 2.11:指出下面的语句是声明还是定义:

(a)extern int ix = 1024;
(b)int iy;
(c)extern int iz;

答:
(a)声明变量的同时进行了显式初始化,所以是定义。
(b)定义。
(c)使用 extern 关键字进行了声明。

关键概念:静态类型

C++是一种静态类型语言,在编译阶段检查类型,这个过程称为类型检查。编译器检查数据类型是否支持要执行的运算,如果试图执行类型不支持的运算,编译器将报错,且不会生成可执行文件。

标识符

C++的标识符由字母、数字和下划线组成,且必须以字母或下划线开头,对长度没有限制,但对大小写敏感。
C++ 关键字不能用作标识符。C++也为标准库保留了名字,用户自定义的标识符中不能连续出现两个下划线,也不能下划线紧连大写字母开头,在函数体外的标识符不能以下划线开头。

变量命名规范

  • 标识符体现实际含义。
  • 变量名一般用小写字母,用 index ,不要使用 Index 或 INDEX。
  • 用户自定义的类名一般以大写字母开头。
  • 如果标识符由多个单词组成,则单词间应有明显区分。

练习2.12:请指出下面的名字中哪些是非法的?
(a)int double = 3.14;
(b)int _;
(c)int catch-22;
(d)int 1_or_2 = 1;
(e)double Double = 3.14;

答:
(a)非法的,使用了关键字 double。
(b)合法。
(c)非法,标识符中不能出现 - 。
(d)非法,不能以数字开头。
(e)合法。

名字的作用域

**作用域(scope)**即名字的有效区域。同一名字在不同的作用域中可能指向不同的实体(变量、函数、类型等)。

有下面示例:

#include <iostream>
int main()
{
    
    
 	int sum = 0;
 	// sum values from 1 through 10 inclusive
 	for (int val = 1; val <= 10; ++val)
 		sum += val; // equivalent to sum = sum + val
 	std::cout << "Sum of 1 to 10 inclusive is " << sum << std::endl; 
 	return 0;
}

里面有 3 个名字: main、sum 和 val,main 在所有花括号外,拥有全局作用域,声明后在整个程序范围内都能使用,名字 sum 定义于 main 函数所限定的作用域之内,从声明 sum 开始到 main 函数结束都能访问,出了 main 后就不能访问了,val 定义于 for 语句内,在 for 语句之内可以访问 val,出了 for 循环就不能访问,val 与 sum 都具有块作用域。

建议:第一次使用变量时再定义它

嵌套的作用域
作用域能彼此包含,被嵌套的作用域称为内层作用域(inner scope),包含着别的作用域的作用域称为外层作用域(outer scope)

#include <iostream>
// Program for illustration purposes only: It is bad style for a function
// to use a global variable and also define a local variable with the same name
int reused = 42; // reused has global scope
int main()
{
    
    
 	int unique = 0; // unique has block scope
 	// output #1: uses global reused; prints 42 0
 	std::cout << reused << " " << unique << std::endl;
 	int reused = 0; // new, local object named reused hides global reused
 	// output #2: uses local reused; prints 0 0
 	std::cout << reused << " " << unique << std::endl;
 	// output #3: explicitly requests the global reused; prints 42 0
 	std::cout << ::reused << " " << unique << std::endl; return 0;
}

第一个输出,使用全局作用域中定义的名字 reused。
第二个输出,使用块作用域中的名字 reused。
第三个输出,使用作用域操作符覆盖默认的作用域,访问了全局作用域中的reused。

练习 2.13:下面程序中 j 的值是多少?

int i = 42;
int main()
{
    
    
	int i = 100; int j = i;
}

答:内层作用域的值对外层作用域的值进行了覆盖,j 所得的值是 内层作用域中 i 的值 100。

练习 2.14:下面的程序合法吗?如果合法,将输出什么?

int i = 100, sum = 0;
for (int i = 0; i != 10; ++i)
 sum += i;
std::cout << i << " " << sum << std::endl;
答:合法的。100 45。 循环内部定义的 i 是内层作用域中的,从 0 递增到 9,所以 sum 是 45。循环外部 for 中的 i 失效,输出的 i 是外层作用域中的,值为 100。

复合类型

复合类型(compound type) 是基于其他类型定义的类型。这里介绍引用和数组。

声明语句由一个基本数据类型和后面一个声明符列表组成。每个声明符命名了一个变量并指定该变量为与基本数据类型有关的某种类型。

引用

C++11 中新添了右值引用,这种引用主要用于内置类。当我们使用 “引用” 时,指的是左值引用
引用(reference) 为对象起了另外一个名字,引用类型 引用(refers to)另外一种类型。通常将声明符写成 &d 的形式来定义引用类型,其中 d 是声明的变量名:

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 i = 1024, i2 = 2048;  
int &r = i, r2 = i2;      //r是一个引用,与 i 绑定在一起
int i3 = 1024, &ri = i3;  //ri是一个引用,与 i3 绑定
int &r3 = i2, &r4 = i2;   //r3,r4 都是引用

大部分情况,所有引用的类型都要和与之绑定的对象严格匹配,而且引用只能绑定到对象上,不能与字面值或某个表达式绑定:

int &refVal4 = 10; // 错误:初始值必须是对象
double dval = 3.14;
int &refVal5 = dval; // 错误不能用字面值初始化对象

练习 2.15:下面的哪个定义是不合法的?为啥?
(a)int ival = 1.01;
(b)int &rval1 = 1.01;
(c)int &rval2 = ival;
(d)int &rval3;

答: (a)正确 (b)不合法,不能用字面值初始化对象 (c)正确 (d)不合法,引用必须被初始化。

练习 2.16:考察下面的所有赋值然后回答:哪些赋值是不合法的?为什么?哪些赋值是合法的?它们执行了什么样的操作?

int i = 0, &r1 = i;
double d = 0, &r2 = d;

(a)r2 = 3.14;
(b)r2 = r1;
(c)i = r2;
(d)r1 = d;

答:
(a)合法的,为引用赋值就是为引用绑定的对象赋值
(b)是合法的,获取引用的值就是获取引用绑定对象的值,为引用赋值就是为引用帮对的对象赋值,这个语句就是把 i 的值赋给 d。
(c)合法的,将 d 的值赋给 i。
(d)把 d 的值赋给 i。

练习 2.17:执行下面的代码将输出什么结果?

int i, &ri = i;
i = 5; ri = 10;
std::cout << i << " " << ri << std::endl;

答:输出10 10,对 ri 赋值就是对 i 赋值,输出 i 就是10,输出 ri 就是输出 i 的值 10。

指针

指针(pointer) 是指向另外一种类型的复合类型。指针也实现了对其他引用的间接访问。但是与引用有两点不用:指针是对象,允许指针赋值和拷贝,指针可以指向不同的对象;指针在定义时也无需赋值。在块作用域未初始化的指针会有一个不确定值。

将声明符写成 *d 的形式来定义指针,d 是变量名。如果在一条语句中定义了几个指针变量,每个变量前都要有 * 号:

int *ip1, *ip2; // p1 和 p2 都是整形指针。
double dp, *dp2; // dp2 是指向double的指针。

获取对象的地址
指针存放某个对象的地址,获取地址使用取地址符(&):

int ival = 42;
int *p = &ival; // p 存放变量 ival 的地址,p是指向ival的指针。

一般情况下,指针的类型都要和它所指的对象严格匹配:

double dval;
double *pd = &dval; // 正确:初始值是 double 型对象的地址。
double *pd2 = pd; // 正确:初始值是指向 double 对象的指针。
int *pi = pd; // 错误:指针 pi 的类型和 pd 的类型不匹配。
pi = &dval; // 错误:试图把 double 型对象的地址赋给 int 型指针。

指针值

指针的值有下列 4 中状态:

  1. 指向一个对象。
  2. 指向紧邻对象所占空间的下一个位置。
  3. 空指针,没有指向任何对象。
  4. 无效指针,是上述情况之外的其他值。

利用指针访问对象

使用**解引用符(*)**访问指针指向的对象。

int ival = 42;
int *p = &ival;
cout << *p;   //用符号 * 得到指针 p 所指的对象,输出 42

给解引用的结果复制,实际上就是给指针所指的对象赋值:

*p = 0; // 通过 * 获得 p 所指的对象,即可通过 p 为变量 ival 赋值。
cout << *p; 

空指针
空指针(null pointer): 不指向任何对象。通过以下方法生成空指针:

int *p1 = nullptr; 
int *p2 = 0; 
int *p3 = NULL;   //需要首先 #include cstdlib

把 int 变量直接赋给指针是错误的操作,即使 int 变量的值为 0:

int zero = 0;
pi = zero;

建议:初始化所有指针

赋值和指针

引用一旦定义,无法绑定其他对象。对指针赋新值就可以使其存放新的地址,指向新的变量:

int i = 42;
int *pi = 0; // pi is initialized but addresses no object
int *pi2 = &i; // pi2 initialized to hold the address of i
int *pi3; // if pi3 is defined inside a block, pi3 is uninitialized
pi3 = pi2; // pi3 and pi2 address the same object, e.g., i
pi2 = 0; 

这个语句改变了指针的值:

pi = &ival;

这个语句改变了指针所指的对象:

*pi = 0;

其他指针操作

只要指针拥有一个合法值,就能用在条件表达式中。如果指针的值是 0,条件取 false,值不是 0 条件取 true:

int ival = 1024;
int *pi = 0; // pi is a valid, null pointer
int *pi2 = &ival; // pi2 is a valid pointer that holds the address of ival
if (pi) // pi has value 0, so condition evaluates as false
 // ...
if (pi2) // pi2 points to ival, so it is not 0; the condition evaluates as true
 // ...

void* 指针

void* 指针可用于存放任意对象的地址。

double obj = 3.14, *pd = &obj;
// ok: void* can hold the address value of any data pointer type
void *pv = &obj; // obj can be an object of any type
pv = pd; // pv can hold a pointer to any type

练习 2.18:编写代码分别更改指针的值以及指针所指对象的值。
答:

int i = 0, i2 = 0;
int *p = &i;
p = i2;               //改变指针的值
*p = 1;               //改变指针所指的对象 i2 的值

练习 2.19:说明指针和引用的主要区别:

答:指针指向内存中的某个对象,引用绑定到内存中的某个对象,都实现了对其他变量的间接访问。
区别:
第一,指针本身就是对象,允许对指针进行赋值和拷贝,而且指针可以指向不同的对象;引用一旦初始化后就无法赋值,只能与一个对象绑定。
第二,指针无需在初始化时赋值,在全局作用域内未初始化会被赋予空指针,块作用域内会拥有不确定值;而引用必须在定义时赋值。

练习 2.20:请叙述下面这段代码的作用。

int i = 42;
int *p1 = &i;
*p1 = *p1 * *p1;

答:将 i * i 的值赋给 i。

练习 2.21:请解释下述定义。在这些定义中有非法的吗,如果有,为什么?

int i = 0;

(a)double* dp = &i;
(b)int *ip = i;
(c)int *p = &i;

答:
(a)非法,dp 是double指针,不能指向整型变量。
(b)非法,ip 是int指针,应指向 i 的地址。
(c)合法。

练习 2.22:假设 p 是一个 int 型指针,请说明下述代码的含义。

if(p) 
if(*p)

答:第一个语句中,若 p 的值是 0,即为空指针,则条件为 false。第二个语句中,若 p 所指对象的值是 0,则条件为 false。

练习 2.23:给定指针 p,你能知道它是否指向了一个合法的对象吗?如果能,叙述判断思路;不能则说明原因。

答:用 if 语句,指向不合法对象时条件则为 false。当 p 未初始化时,应将 if 语句放在 try 语句中防止出错。

练习 2.24:在下面这段代码中为什么 p 合法而 lp 非法?

int i = 42;
void *p = &i;
long *lp = &i;

答:p 是 void * 型指针,可以存放任意对象的地址;lp 是长整型指针,不能指向整型变量。

理解复合类型的声明

变量的定义包括一个基本数据类型和一组声明符。在一条定义语句中,虽然基本数据类型只有一个,但是声明符的形式可以不同。即一条定义语句可能定义出不同类型的变量:

int i = 1024, *p = &i, &r = i;

指向指针的指针

通过 * 的个数可以区分指针的级别。** 表示指向指针的指针,***表示指向指针的指针的指针。

int ival = 1024;
int *pi = &ival;
int *ppi = &pi;

解引用 int 型指针会得到一个 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; // p is a pointer to int
int *&r = p; // r is a reference to the pointer p
r = &i; // r refers to a pointer; assigning &i to r makes p point to i
*r = 0; // dereferencing r yields i, the object to which p points; changes i
to 0

从右往左阅读 r 的定义。离变量名最近的符号 & 说明 r 是一个引用。声明符的其余部分 * 说明 r 引用的是一个指针,所以 r 引用的是一个 int 指针。

练习 2.25:说明下列变量的类型和值。
(a)int *ip, i, &r = i;
(b)int i, *ip = 0;
(c)int *ip, ip2;

答:
(a)ip 整型指针,值为 nullptr,i 是整数,值为 0,r 是 i 的引用,值为 0。
(b)i 是整数,值为 0,ip 是整形指针,值为 0。
(c)ip 是整形指针,ip2是整数。值都为0。

const 限定符

使用关键字 const 限定的变量的值不能被改变。因为const 对象创建后它的值就不能被改变,所以 const 对象一定要初始化:

const int i = get_size();     //运行时初始化
const int j = 42;             //编译时初始化
const int k;                  //错误:k 是一个未经初始化的常量

i = 42;                       //错误:试图向 const 对象写值

初始化和 const

const 对象能完成大部分非 const 类型所能参与的操作,主要不能完成的就是改变 const 对象内容的操作。
不改变 const 对象的操作中有一种是初始化,如果利用一个对象去初始化另外一个对象,则它们是不是const都可以:

int i = 42;
const int ci = i;            //正确
int j = ci;                  //正确

因为拷贝一个对象的值不会改变它,所以上面的第三条是可以的。

默认状态下,const 对象仅在文件内有效

用编译时初始化定义一个 const 对象时,编译器会将在编译过程中用到该变量的地方都替换为对应的值。
为了执行替换,编译器必须知道初始值。如果程序有多个文件,则每个用了 const 对象的文件都必须能访问到初始值,即每个文件都必须要定义这个变量。为了避免重复定义,const 对象被设定为仅在当前文档生效。

初始值不是常量表达式的 const 变量如果要在文件间共享,又不想让每个文件分别生成独立的变量。那么就可以对于 const 变量的声明还是定义都添加 extern 关键字,这样定义一次就可以了:

//在 file1.cpp 中定义
extern const int bufSize = fun();
//在file2.cpp 中声明
extern const int bufSize;   //与 file1.cpp 中的 bufSize 是一个变量

如果想在多个文件之间共享 const 对象,必须在变量的定义之前添加 extern 关键字。

练习 2.26:下面哪些句子是合法的?如果有不合法的句子,请说明为什么?
(a)const int buf;
(b)int cnt = 0;
(c)const int sz = cnt;
(d)++cnt; ++sz;

答:
(a)不合法,没有初始化
(b)合法
(c)合法
(d)不合法,sz 是常量,不能进行自加操作

const 的引用

一个引用绑定到 const 对象上,这个引用就是常量引用(reference to const)。对常量的引用不能被用来修改它所绑定的对象。:

const int ci = 1024;
const int &r1 = ci;    //正确:引用及被绑定的对象都是常量
r1 = 42;               //错误:r1 是对常量的引用,不能用过常量引用改变常量的值。
int &r2 = ci;          //错误:试图让一个非常量引用来引用一个常量对象。

常量初始化后不能再赋值,所以不能通过引用 r1 去改变 ci。所以对常量的引用也一定要是常量,因此,对 r2 的初始化是错误的。

初始化和对 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 是一个普通的非常量引用,需要绑定一个整型变量

为什么会有上面情况呢?

先让我们来看一下一个常量引用被绑定到另一种类型上时发生了什么:

double dval = 3.14;
const int &ri = dval;

此处虽然 dval 是一个双精度浮点数,但是 ri 却引用了一个 int 型的数。为了确保让 ri 绑定一个整数,编译器做了如下操作:

const int temp = dval;   //由双精度浮点数生成一个临时的整型常量
const int &ri = temp;    // 让 ri 绑定这个临时常量

这时 ri 绑定的就是一个**临时量(temporary)**对象。临时量对象简称临时量,就是编译器需要一个空间来暂存表达式求值结果时临时创建的一个对象。

上面如果 ri 不是常量时,上面的赋值会有什么问题呢?

如果 ri 不是常量,ri 会引用一个临时量,我们可以通过 ri 对其修改,那么我们修改的是临时量,没有任何意义,所以这种行为是非法的。

对 const 的引用可能引用一个非 const 对象

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

int i = 42;
int &r1 = i; // r1 绑定到对象 i
const int &r2 = i; // r2 是 i 的常量引用,不允许通过 r2 修改 i 的值
r1 = 0; // i 为 0
r2 = 0; //错误:r2 是常量引用,不允许通过 r2 修改 i 的值

指针和 const

与引用一样,指针也可以指向常量或非常量。指向常量的指针(pointer to const) 不能用于改变其所指对象的值。存放常量对象的地址,只能使用指向常量的指针:

const double pi = 3.14; // pi is const; its value may not be changed
double *ptr = &pi; // error: ptr is a plain pointer
const double *cptr = &pi; // ok: cptr may point to a double that is const
*cptr = 42; // error: cannot assign to *cptr

上面提到,指针的类型必须与其所指的对象类型一致,但是有两个例外,其中一个就是允许令一个指向常量的指针指向一个非常量对象:

double dval = 3.14;
cptr = &dval;

所谓指向常量的指针常量引用,不过是指针或引用“自以为是”,他们觉得自己指向了常量,所以自觉地不去改变所指对象的值。

const 指针
指针是对象而引用不是,可以像对其他对象类型一样,把指针本身定为常量。常量指针(const pointer) 必须初始化,而且一旦初始化完成,它的值就不能改变了。把 * 放在 const 前面来说明指针是一个常量,说明指针变量本身是不能变的:

int errNumb = 0;
int *const curErr = &errNumb; // curErr will always point to errNumb
const double pi = 3.14159;
const double *const pip = &pi; // pip is a const pointer to a const object

这个例子中 pip 是什么类型呢?

像在指向指针的引用中分析 r 的定义时一样,我们从右往左剖析。
离 pip 最近的是 const,说明 const 是常量对象就,声明符中的下一个符号是 *,意思是 pip 是常量指针,基本数据类型为 double,最左侧 const 说明 pip 是指向常量的指针。所以 pip 就是一个指向常量的常量指针

练习 2.27:下面哪些初始化是合法的?请说明原因。
(a)int i = -1, &r = 0;
(b)int *const p2 = &i2;
(c)const int i = -1, &r = 0;
(d)const int *const p3 = &i2;
(e)const int *p1 = &i2;
(f)const int &const r2;
(g)cosnt int i2 = i, &r = i;

答:
(a)非法的,非常量引用不能引用常量 0。
(b)合法的,p2 是常量指针,指向 i2 后不能指向其他对象。
(c)合法的,i 是常量 -1,r 是常量引用,值为0.
(d)合法的,p3是一个常量指针,p3永远指向 i2,且p3是指向常量的指针,不能通过 p3 修改 i2 的值。
(e)合法的,p1 指向一个常量,不能通过 p1 修改 i2 的值。
(f)非法的,指向常量的指针定义时必须初始化。
(g)合法的,i2是一个常量,r是一个常量引用。

练习 2.28:说明下面这些定义时什么意思,挑出其中不合法的。
(a) int i, *const cp;
(b) int *p1, *const p2;
(c) const int ic, &r = ic;
(d) const int *const p3;
(e) const int *p;

答:
(a) 不合法,常量指针必须初始化。
(b)同上
(c)不合法,常量必须初始化。
(d)不合法,常量指针必须初始化。
(e) 合法。

练习 2.29:假设已有上一个练习中定义的那些变量,则下面的哪些语句是合法的?
(a) i = ic;
(b) p1 = p3;
(c) p1 = &ic;
(d) p3 = &ic;
(e) p2 = p1;
(f) ic = *p3;

答:
(a) 合法,常量 ic 的值付给了 非常量 i。
(b) 非法,p1 是普通指针,能改变所指对象的值,p3 是指向常量的常量指针,其所指的值不能改变。
(c)非法,p1 是普通指针,ic 是常量。
(d)非法,p3 是常量指针,不能被赋值。
(e) p2 是常量指针不能被赋值。
(f) ic是常量,不能被赋值。

顶层 const

指针本身是个对象,也可以指向另外一个对象。指针本身是不是常量以及指针所指的是不是一个常量是两个问题。用名词顶层const(top-level const) 表示指针本身是个常量,而用名词底层const(low-level const) 表示指针指向常量。

更一般的,顶层 const 可以表示任意的对象是常量。底层 const 则与指针和引用等复合类型的基本类型部分有关。指针类型既可以是顶层 const 也可以是 底层 const,这一点和其他类型有明显区别:

int i = 0;
int *const p1 = &i; // we can't change the value of p1; const is top-level
const int ci = 42; // we cannot change ci; const is top-level
const int *p2 = &ci; // we can change p2; const is low-level
const int *const p3 = p2; // right-most const is top-level, left-most is not
const int &r = ci; // const in reference types is always low-level

执行对象的 copy 操作时,常量是顶层 const 还是底层 const 区别明显。其中顶层const不受影响:

i = ci; // ok: copying the value of ci; top-level const in ci is ignored
p2 = p3; // ok: pointed-to type matches; top-level const in p3 is ignored

底层const执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层 const 资格,或者拷出对象的数据类型能转换为拷入对象的数据类型:

int *p = p3; // error: p3 has a low-level const but p doesn't
p2 = p3; // ok: p2 has the same low-level const qualification as p3
p2 = &i; // ok: we can convert int* to const int*
int &r = ci; // error: can't bind an ordinary int& to a const int object
const int &r2 = i; // ok: can bind const int& to plain int

练习 2.30:对于下面的这些语句,请说明对象被声明成了顶层 const 还是底层 const?

const int v2 = 0; int v1 = v2;
int *p1 = &v1, &r1 = v1;
const int *p2 = &v2, *const p3 = &i, &r2 = v2;
答: 顶层 const:v2,p3,r2。 底层 const:p2,r2。

练习 2.31:假设有上一个练习的声明,下列哪些语句是合法的?请说明顶层const 和底层 const 在每个例子中有何体现。

r1 = v2;
p1 = p2; p2 = p1;
p1 = p3; p2 = p3;

答:
第一个合法
第二个不合法,p1 是普通指针,p2 是底层指针,底层指针不能转化为普通指针。
第三个合法,int *能转化成const int*。
第四个不合法,const int* 不能转化成int*。
第五个合法,都是底层指针。

constexpr 和常量表达式

**常量表达式(const expression)**是指值不会改变并且在编译过程就能得到结果的表达式。字面值、用常量表达式初始化(编译初始化)的const对象也是常量表达式。

一个对象是不是常量表达式由它的数据类型和初始值共同决定:

const int max_files = 20; // max_files is a constant expression
const int limit = max_files + 1; // limit is a constant expression
int staff_size = 27; // staff_size is not a constant expression
const int sz = get_size(); // sz is not a constant expression

constexpr 变量

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

constexpr int mf = 20; // 20 is a constant expression
constexpr int limit = mf + 1; // mf + 1 is a constant expression
constexpr int sz = size(); // ok only if size is a constexpr function

constexpr 函数在编译时就可以确定计算结果,这样就能用 constexpr 函数去初始化 constexpr 变量了。

字面值类型

指针和引用都能定义成 constexpr,但是初始值受到严格限制。constexpr 指针的初始值必须是0、nullptr 或者是存储在某个固定地址中的对象。
函数体内定义的普通变量一般并非存放在固定地址中,因此 constexpr 指针不能指向这样的变量。相反,函数体外定义的变量地址固定不变,可以用来初始化 constexpr 指针。函数体内的静态变量和函数体外的变量一样,也可以用来初始化 constexpr 指针。

指针和 constexpr

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

const int *p = nullptr;   //p 是一个指向整型常量的指针,底层指针。
constexpr int *q = nullptr;// q 是一个指向整数的常量指针,顶层指针。

constexpr 指针可以指向常量也可以指向非常量。

练习 2.32:下面的代码是否合法?如果非法,请将其修改正确。

int null = 0, *p = null;

答:非法的,null 是 int 变量,p 是一个指针,指针要指向 null 的地址,正确的写法是:

int null = 0, *p = &null;

处理类型

类型别名

类型别名(type alias) 是一个名字,它是某种类型的同义词。

有两种方法可用于定义类型别名。传统的方法是使用 typedef:

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

新方法是使用别名声明(alias declaration) 来定义类型的别名:

using SI = Sales_item;  

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

typedef char *pstring;
const pstring cstr = 0; // cstr is a constant pointer to char
const pstring *ps; // ps is a pointer to a constant pointer to char

pstring 是指向 char 的指针,因此,const pstring 就是指向 char 的常量指针。
如果将 pstring 换成 char * 对 cstr 解读会产生错误的结果:

const char *cstr = 0;

cstr 成了指向 const char 的指针。与原来的声明截然不同。

auto类型说明符

C++11新标准引入了 auto 类型说明符,能让编译器自动分析表达式所属的类型。auto 定义的变量必须有初始值。

// 由val1和val2相加的结果可以推断出item的类型
auto item = val1 + val2;    // item初始化为val1和val2相加的结果

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

auto i = 0, *p = &i; // ok: i is int and p is a pointer to int
auto sz = 0, pi = 3.14; // error: inconsistent types for sz and pi

复合类型、常量和auto

编译器推断出来的 auto 类型有时和初始值的类型并不完全一样,编译器会适当地改变结果类型,使其更符合初始化规则。

  1. 当引用被用作初始值时,真正参与初始化的其实是引用对象的值。编译器以引用对象的类型作为 auto 的类型。
int i = 0, &r = i;
auto a = r;     // a是一个整数(r是i的别名,而i是一个整数)

  1. auto 一般会忽略顶层 const,同时底层 const 则会保留下来。
const int ci = i, &cr = ci;
auto b = ci; // b is an int (top-level const in ci is dropped)
auto c = cr; // c is an int (cr is an alias for ci whose const is top-level)
auto d = &i; // d is an int*(& of an int object is int*)
auto e = &ci; // e is const int*(& of a const object is low-level const)

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

const auto f = ci;    

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

auto &g = ci;        //g 是一个整型常量引用。
auto &h = 42;		 //不能为非常量引用绑定字面值
const auto &j = 42;  //正确:可以为常量引用绑定字面值。

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

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

练习 2.33:利用本节定义的变量,判断下列语句的运行结果。
a = 42; b = 42; c = 42;
d = 42; e = 42; g = 42;

答:
abc都是整数,正确;de是指针,错误;g是整型常量引用,初始化后不能对其赋值,错误。

练习 2.34:基于上一个练习中的变量和语句编写一段程序,输出赋值前后变量的内容,你刚才的推断正确吗?

答:正确

练习 2.35:判断下列定义推断出的类型是什么,然后编写程序进行验证。

const int i = 42;
auto j = i; const auto &k = i; auto *p = &i;
const auto j2 = i, &k2 = i;

答:
j是整型 int,拷贝时候忽略 i 的顶层const;
k是整型常量引用;
p是整型指针;
j2是整型常量;
k2是整型常量引用。

decltype 类型指示符

C++11新增 decltype 类型指示符,作用是选择并返回操作数的数据类型,此过程中编译器不实际计算表达式的值。

decltype(f()) sum = x;  // sum的类型就是函数f的返回类型

decltype 处理顶层 const 和引用的方式与 auto 有些不同,如果 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;     	// error: z是一个引用,必须初始化

decltype 和引用

如果 decltype 使用的表达式不是一个变量,则 decltype 返回表达式结果对应的类型;如果表达式的内容是解引用操作,则 decltype 将得到引用类型:

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

decltype 和 auto 的另一处重要区别是,decltype 的结果类型与表达式形式密切相关。注意:如果 decltype 使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果给变量加上了一层或多层括号,则 decltype 会得到引用类型,因为变量是一种可以作为赋值语句左值的特殊表达式。

// decltype的表达式如果是加上了括号的变量,结果将是引用
decltype ((i)) d;  // 错误:d是int&,必须初始化
decltype (i) e;    // 正确:e是一个(未初始化的)int

练习 2.36:关于下面的代码,请指出每一个变量的类型以及程序结束时他们各自的值。
int a = 3, b = 4;
decltype(a) c = a;
decltype((b)) d = a;
++c;
++d;

答:a是整数,值为4;b是整数,值为4;c是整数,值为4;d是整型引用,值为4.

练习 2.37:赋值是会产生引用的一类经典表达式,引用的类型就是左值的类型。也就是说,如果 i 是 int,则表达式 i=x 的类型是 int&。根据这一特点,请指出下面代码中每一个变量的类型和值。

int a = 3, b = 4;
decltype(a) c = a;
decltype(a = b) d = a;

答:c 是整型,值为 3;a=b 作为decltype 的参数,只给 decltype 提供类型,并不进行赋值,所以 a 的值不变,还是 3;d 是 a 的引用,值为 3;b为整数3。

练习 2.38:说明由decltype 指定类型和由auto指定类型有何区别。请举一个例子,decltype指定的类型与auto指定的类型一样;再举一个例子,decltype指定的类型与auto指定的类型不一样。

答:auto 和 decltype 的区别主要有三个方面:

第一,auto 类型说明符用编译器计算变量的初始值来推断其类型,而 decltype 虽然也让编译器分析表达式并得到它的类型,但是不实际计算表达式的值。

第二,编译器推断出来的 auto 类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则。例如,auto 一般会忽略掉顶层 const,而把底层 const 保留下来。与之相反,decltype 会保留变量的顶层 const。

第三,与 auto 不同,decltype 的结果类型与表达式形式密切相关,如果变量名加上了一对括号,则得到的类型与不加括号时会有不同。如果 decltype 使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果给变量加上了一层或多层括号,则编译器将推断得到引用类型。

示例如下:

#include <iostream>
using namespace std;
#include<typeinfo>
int main()
{
    
    
	int a = 3;
	auto c1 = a;
	decltype(a)c2 = a;
	decltype((a))c3 = a;

	const int d = 5;
	auto f1 = d;
	decltype(d)f2 = d;

	cout << typeid(c1).name() << endl;
	cout << typeid(c2).name() << endl;
	cout << typeid(c3).name() << endl;
	cout << typeid(f1).name() << endl;
	cout << typeid(f2).name() << endl;

	c1++;
	c2++;
	c3++;
	f1++;
	f2++; // 错误:f2是整型常量,不能执行自增操作
	cout << a << " " << c1 << " " << c2 << " " << c3 << " " << f1 << " " << f2 << endl;
	return 0;
}

对于第一组类型推断来说,a 是一个非常量整数,c1 的推断结果是整数,c2 的推断结果也是整数,c3 的推断结果由于变量 a 额外加了一对括号,所以是整数引用。c1、c2、c3 依次执行自增操作,因为 c3 是变量 a 的别名,所以 c3 自增等同于 a 自增,最终 a、c1、c2、c3 的值都变为4。

对于第二组类型推断来说,d 是一个常量整数,含有顶层 const 使用 auto 推断类型自动忽略掉顶层 const,因此 f1 的推断结果是整数;decltype 则保留顶层 const ,所以 f2 的推断结果是整数常量。f1 可以正常执行自增操作,而常量 f2 的值不能被改变,所以无法自增。

自定义数据结构

后面关于类的内容中会仔细介绍。



关注博仔不迷路

猜你喜欢

转载自blog.csdn.net/weixin_45773137/article/details/125689394
今日推荐