C++学习——第二章《变量和基本类型》


前言

数据类型是程序的基础:它告诉我们数据的意义以及我们能在数据上执行的操作。
C++语言支持广泛的数据类型。它定义了几种基本内置类型(如宇符、整型、浮点数等 ), 同 时 也 为 程 序 员 提 供 了 自 定 义 数 据 类 型 的 机 制 。 基 于 此 , c + + 标 准 库 定 义 了 一 此 更 加 复杂的数据类型,比如可变长字符串和向量等。本章将主要讲述内置类型,并带领大家初 步了解C++语言是如何支持更复杂数据类型的。


一、基本内置类型

C++定义了一套包括算术类型 (arithmetic type )和空类型 (void )在内的基本数据类 型。其中算术类型包含了字符、整型数、布尔值和浮点数。 空类型不对应具体的值,仅用于一些特殊的场合,例如最常见的是, 当两数不返回任何值时使用空类型作为返回类型。

1、算术类型

算 术 类 型 分 为 两 类: 整 型 ( 包 括 字 符 和 布 尔 类 型 在 内 ) 和 浮 点 型 。

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

布 尔 类 型 (bool ) 的 取 值 是 真 (true ) 或 者 假 ( false ) 。

算术类型的尺寸(也就是该类型数据所占的比特数)在不同机器上有所差別,所以上表列出来的可能和大家的不一样,所以我就用我的设备计算出它的大小给大家参考:

#include <iostream>
using namespace std;

int main()
{
    cout << "bool类型的大小为: " << sizeof(bool) << "byte" << endl;
    cout << "char类型的大小为: " << sizeof(char) << "byte" << endl;
    cout << "wchar_t类型的大小为: " << sizeof(wchar_t) << "byte" << endl;
    cout << "char16_t类型的大小为: " << sizeof(char16_t) << "byte" << endl;
    cout << "char32_t类型的大小为:" << sizeof(char32_t) << "byte" << endl;
    cout << "short类型的大小为: " << sizeof(short) << "byte" << endl;
    cout << "int类型的大小为:  " << sizeof(int) << "byte" << endl;
    cout << "long类型的大小为:   " << sizeof(long) << "byte" << endl;
    cout << "long long类型的大小为: " << sizeof(long long) << "byte" << endl;
    cout << "float类型的大小为: " << sizeof(float) << "byte" << endl;
    cout << "double类型的大小为: " << sizeof(double) << "byte" << endl;
    cout << "long double类型的大小为: " << sizeof(long double) << "byte" << endl;
    return 0;
}

得到的结果:

bool类型的大小为: 1byte
char类型的大小为: 1byte
wchar_t类型的大小为: 4byte
char16_t类型的大小为: 2byte
char32_t类型的大小为: 4byte
short类型的大小为: 2byte
int类型的大小为: 4byte
long类型的大小为: 8byte
long long类型的大小为: 8byte
float类型的大小为: 4byte
double类型的大小为: 8byte
long double类型的大小为: 16byte

C++提供了几种字符类型,其中多数支持国际化。基本的字符类型是char,一个char 的空间应确保可以存放机器基本字符集中任意宇符对应的数字值。也就是说,一个char的大小和一个机器字节一样 。


带符号类型和无符号类型

  • 除去 布 尔 型 和 扩 展 的 宇 符 型 之 外 , 其 他 整 型 可 以 划 分 为 带 符 号 的 ( signed) 和 无 符 号 的 (unsigned )两种。带符号类型可以表示正数、负数或 0,无符号类型则仅能表示大于 等 于0的值。

  • 类型int、short、long 和long long都是带符号的,通过在这此类型名前添加 unsigned 就可以得到无符号类型,例如unsigned long。类型unsigned int 可以 缩写为unsigned。

  • 与 其 他 整 型 不 同 , 字符 型 被 分 为 了三 种 : char 、 signed char 和 unsigned char 。

  • 特别需要注意的是:类型char 和类型signed char 并不一样。尽管字符型有三种,但 是字符的表现形式却只有两种:带符号的和无符号的。类型char 实际上会表现为上述两种形式中的一种,具体是哪种由编译器决定。

  • 无符号类型中所有比特都用来存储值,例如,8比特的unsigned char 可以表示0 至255 区间内的值。

  • C++标淮并没有规定带符号类型应如何表示,但是约定了在表示范围内正值和负值的量应该平衡 。 因 此 ,8 比特的signed char 理 论 上应 该 可 以 表 示-127 至 127 区 间 内 的 值,大多数现代计算机将实际的表示范围定为- 128至127。

2、类型转换

对象的类型定义了对象能包含的数据和能参与的运算,其中一 种运算被大多数类型 支持,就是将对象从一种给定的类型转换 (convert )为另一种相关类型。 当在程序的某处我们使用了一种类型而其实对象应该取另一种类型时,程序会自动进行类型转换。

当我们像下面这样把一种算术类型的值赋给另外一种类型时:

bool b = 42;    	//b为真
int i = b;  		//i 的值为1
1 = 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。如果该整数所占的空间超 过了浮点类型的容量,精度可能有损失。
  • 当我们赋给无符号类型一 个超出它表示范国的值时,结果是初始值对无符号类型表示数值总数取模后的余数。例如,8比特大小的unsigned char可以表示0至 255 区间内的值,如果我们赋 了一 个区间以外的值,则实际的结果是该值对2 56 取模后所得的余数。因此,把-1赋给8比特大小的unsigned char所得的结果是255。
  • 当我 们 赋 给 带 符 号 类 型 一 个超 出 它 表 示 范 围 的 值 时 , 结 果 是 未 定 义 的 。 此时, 程序可能继续工作、 可能崩溃, 也可能生成垃圾数据。

当在程序的某处使用了一种算术类型的值而其实所需的是另一种类型的值时,编译器 同样会执行上述的类型转换。例如,如果我们使用了一个非布尔值作为条件,那么它会被自动地转换成布尔值,这一做法和把非布尔值赋给布尔变量时 的操作完全一样:

int i= 42;
if (i)  	//if条件的值将为true
i = 0;

如果i的值为0,则条件的值为false;i的所有其他取值 (非0 )都将使条件为true。

以此类推,如果我们把一个布尔值用在算术表达式里,则它的取值非0即1,所以一 般 不宜在算术表达式里使用布尔值。


含有无符号类型的表达式

  • 尽管我们不会故意给无符号对象赋一个负值,却可能(特别容易)写出这么做的代码。 例如,当一个算术表达式中既有无符号数又有int 值时,那个int值就会转换成无符号数。把int转换成无符号数的过程和把int 直接赋给无符号变量一样:
unsigned u = 10;
int i = -42;
std::cout<< i + i <<std::endl;  // 输出-84
std::cout<< u + i << std::endl; //如果int占32位,输出4294967264
  • 在第 一个输出表达式里,两个(负)整数相加并得到了期望的结果。在第二个输出表达式 里,相加前首先把整数-42 转换成无符号数。把负数转换成无符号数类似于直接给无符号 数赋一个负值,结果等于这个负数加上无符号数的模。

当从无符号数中减去一个值时,不管这个值是不是无符号数,我们都必须确保结果不能是一 个负值:

unsigned u1 = 42, u2 =10;
std::cout<<u1 - u2 << std::endl;	//正确:输出32
std::cout<< u2- u1 << std::endl; 	//正确:不过,結果是取模后的值

3、字面值常量

一个形如42的值被称作字面值常量(literal),这样的值一望而知。每个字面值常量都对应一种数据类型, 字面值常量的形式和值决定了它的数据类型。


整型和浮点数型字面值

· 我们可以将整型字面值写作十进制数、八进制数或十六进制数的形式。以0开头的整数代表八进制数,以0x或0X开头的代表十六进制数。例如,我们能用下面的任意一种 形 式来表示数值20:

20 /*十进制*/    024 /*八进制*/   0x14 /*十六进制*/

整型 字面值具体的数据 类型由它的值和符号决定。默认情况 下,十进 制字面值是带符号数, 八进制和十六进制字面值既可能是带符号的也可能是无符号的。 十进制字面值的类型是 int 、 long 和 long long 中 尺 寸 最 小 的 那 个 ( 例 如 , 三 者 当 中 最 小 是 i n t ), 当 然 前 提 是这种类型要能容纳 下当前的值。 八进制和 十六进制 字面值的 类型是能容 纳其数值的 int、unsigned int、long、unsigned long、long long和unsigned long long 中的尺寸最小者。如果一个字面值连与之关联的最大的数据类型都放不下,将产生错误。 类型short 没有对应的宇面值。

浮点型字面值表现为一个小数或以科学计数法表示的指数,其中指数部分用E或e 标识:
3.14159 | 3.14159E0 | 0. | | 0e0 | .001


字符和字符串字面值

  • 由单引号括起来的一个字符称为char 型字面值,双引号括起来的零个或多个字符则 构 成 字符 串 型 字面 值 。

’ a ’ //字符宇面值
“Hello World!” // 宇符串字面值

字符串字面值的类型实际上是由常量字符构成的数组(array)。编译器在每个字符串的结尾处添加一个空字符( ‘\0’ ),因此,字符串字 面 值 的 实 际 长 度 要 比 它 的 内 容 多 1 。 例 如 , 字 面 值 ‘A’ 表 示 的 就 是 单 独 的 字 符 A , 而 字 符 串 " A " 则 代 表 了 一个 字 符 的 数 组 , 该 数 组 包 含 两 个 字 符 :一 个 是 字 母 A 、 另 一 个 是 空 字 符 。


转义序列

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

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

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

std::cout<<'\n'; 	//转到新一行
std::cout<<"\tHi!\n";	//输出一个制表符,输出"Hi!",转到新一行

我 们 也 可 以 使 用 泛 化 的 转 义 序 列 , 其 形 式 是 \× 后 紧 跟 1 个 或 多 个 十 六 进 制 数 字, 或 者 \ 后 紧跟1个、2个或3个八进制数 字,其中数字部分表示的是字符对应的数值。假设使用的 是Latin- 1 字符集,以下是一些示例:
\7 (响铃〕| \12 (换行符) | \40 (空格)
\0 (空宇符) | \115 (宇符M) | \x4d (字符M)


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

L'a'	// 宽宇符型宇面值,类型是wchar_t
u8"hi!"	//utf-8宇符串宇面值(utf-8用8位编码一个Unicode宇符) 
42ULL	//无符号整型宇面值,类型是unsigned long long
1Е-3F		//单精度浮点型宇面值,类型是float
3.14159L	//扩展精度浮,点型宇面值,类型是long double

二、变量

斜体样式变量提供一个具名的、可供程序操作的存储空间。C++中的每个变量都有其数据类型, 数据类型决定着变量所占内存空间的大小和布局方式、该空间能存储的值的范围,以及变 量能参与的运算。对C++程序员来说,“变量(variable)” 和“对象(object)”一般可以互换使用。

1、变量定义

交量定义的基本形式是:首先是类型说明符( type specifier),随后紧跟由一个或多个变量名组成的列表,其中变量名以逗号分隔,最后以分号结束。列表中每个变量名的类型 都由类型说明符指定,定义时还可以为一个或多个变量赋初值:

int a = 0;
//int 就是那个类型说明符。a 就是变量名,

int sum = 0, value,units_sold = 0; 
//sum、value和units_sold都是int,sum和units_sold初值为0
 
Sales_item item; //item的炎型是sales_item 

//string是一种库类型,表示一个可交长的宇符序列
std::string name("hello wrold"); //name通过string进行初始化

初始化

  • 当一个变量或者对象被创建时给它一个特定的值时,我们就说对这个对象进行了初始化, 初始化的值可以是数值或者表达式,可以用新定义的变量去初始化其他变量。
int a;	//现在我只是定义了一个变量,并没有给它赋值,所以没有初始化这个变量
int b = 10; //我们定义了变量b并且给它赋了一个值,进行了初始化
int c = 5;

int sum = a + b;
//这也是定义一个变量并且进行了初始化,初始化的值可以是表达式

int d = b;//这也是定义一个变量并且进行了初始化,初始化的值可以是其他定义的变量

列表初始化

  • C++语言定义了初始化的好几种不同形式,这也是初始化间题复杂性的一个体现。例 如,要想定义一个名为units_sol d的int 变量并初始化为。以下的4条语句都可以 做到这一点:

int units _ sold = 0 :
int units_sold= (0);
int units_sold{0};
int units_so1d(0);

  • 作 为 C + + 1 1 新 标 准 的 一 部 分 , 用 花 括 号 来 初 始 化 变 量 得 到 了 全 面 应用 , 而 在 此 之 前 , 这种初始化的形式仅在某此受限的场合下才能使用 , 这 种 初 始 化 的 形 式 被 称 为 列 表 初 始 化 。 现 在 , 无 论 是 初 始 化 对 象 还 是 某 些 时 候 为 对 象 赋 新 值 , 都 可 以 使 用 这 样 一 组 由 花 括 号 括 起 来 的 初 始 值 了。 当用于内置类型的变量时,这种初始化形式有一 个重要特点:如果我们使用列表初始 化且初始值存在丟失信息的风险, 则编译器将报错:
long double ld= 3.1415926536;
int a{ld}, b= {ld};		//错误:转换未执行,因为存在丢失信息的危险
int c{ld}, d =ld;		//正确:转换执行,且确实丢失了部分值

使用long double的值初始化int 变量时可能丟失数据,所以编译器拒绝了a和b的初 始化请求。其中,至少ld的小数部分会丢失掉,而且int 也可能存不下ld的整数部分


默认值初始化
如果定义变量时没有指定初值,则变量被默认初始化(default initialized),此时变量 被赋予了“ 默认值”。如果是内置类型的变量未被最式初始化 ,它的值由定义的位置决定。定义于任何函数 体 之 外 的 变 量 被 初 始 化 为 0 ; 一 种 例 外 情 況 是 , 定 义 在 两 数 体 内 部 的 内 置 类 型 变 量 将 不 被 初 始 化,也就是全局变量默认值为0,局部变量不会默认初始化。

2、变量声明和定义的关系

  • 为了允许把程序拆分成多个逻辑部分来编写,C++语言支持分离式编译 (separate compilation )机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。

  • 为了支持分离式编译,C++语言将声明和定义区分开来。声明(declaration )使得名宇 为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名宇的声明。 而定义 (definition )负责创建与名字关联的实体。

  • 变量声明规定了变量的类型和名字,在这一点上定义与之相同。但是除此之外,定义还申请存储空间,也可能会为变量赋一个初始值

  • 如 果 想 声 明 一 个 变 量 而 非 定 义 它, 就 在 变 量 名 前 添 加 关 键 宇 extern , 而 且 不 要 显 式 地初始化变量:

extern int i;	//声明 i 而非定义 i
int j;			//声明并定义 j
  • 任何包含了显式初始化的声明即成为定义。我们能给由extern 关键字标记的变量赋 一 个初始值,但是这么做也就抵消了extern 的作用。extern 语句如果包含初始值就不再是声明, 而变成定义了:
extern double pi= 3.1416; //不再是声明,而是定义

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

3、标识符

C++的标识符 (identifier)由字母、数字和下画线组成,其中必须以字母或下划线开 头 。 标识符的长度没有限制 , 但 是 对 大 小 写 字 母 敏 感:

//定义4个不同的变量
int a,sum1, someName,_abc;

同时,C++也为标准库保留了一些名字。用户自定义的标识符中不能连续出现两个下划线,也不能以 下划线紧连 大写 字母开头。此外, 定义在函数体外的标识符 不能以 下画 线 开头。可以这样写,只是标准库里面的写法是这样,怕会冲突。

变量命名规范

变 量 命 名 有 许 至 约 定 俗 成 的 规 范, 下 面 的 这 些 规 范 能 有 效 提 高 程 序 的 可 读 性 :

  1. 标识符要能体现实际含义。
  2. 变量名一般用小写字母,如index,不要使用Index或INDEX。
  3. 用户自定义的类名一般以大写字母开头,如Sales_item。
  4. 如果标识符由多个单词组成,则单词间应有明显区分,如student_loan 或 studentLoan , 不要 使 用studentloan 。
int a;   //ok
int 12a;	//错误,不能以数字开头
int _abc;	//ok
int __AHL__, DESCB;//尽量不要这样定义,会于标准库发生冲突。
int double;	//错误,不能将关键字定义成变量

C++关键字

alignascontinuefriendregistertrue
alignofdecltypegotoreinterpret_casttry
asmdefaultifreturntypedef
autodeleteinlineshorttypeid
booldointsignedtypename
breakdoublelongsizeofunion
casedynamic_castmutablestaticunsigned
catchelsenamespacestatic_assertusing
charenumnewstatic_castvirtual
char16_texplicitnoexceptstructvoid
char32_texportnullptrswitch
classexternoperatortemplatewchar_t
constfalseprivatethiswhile
constexprfloatprotectedthread_local
const_castforpublicthrow

C++操作符替代名

andbitandcomplnot_eqor_eqxor_eq
and_eqbitornotorxor

4、名字的作用域

不论是在程序的什么位量,使用到的每个名 字都会指向一个特定的实体:变量、函数、 类型等。然而,同一个名 字如果出现在程序的不同位置,也可能指向的是不同实体。

  • 作用域是程序的一部分,在其中名字有其特定的含义。C++语言中大多数作用域都以花括号分隔。
  • 同一个名字在不同 的作用域中可能指向不同的实体。名字的有效区域始于名字的声明语句,以声明语句所在的作用域 末端为結束。
#include<iostream>
using namespace std;

int a = 10;
int main()
{
	int b = 0;

	for(int j = 0; j < 5; j++)
	{
		cout << j << endl;
	}
	return 0;
}

向上面代码,a变量定义在main函数和其他函数之外,这样就叫做全局变量,这个变量全局都有效。而像b变量定义在main函数或者其他函数内部,这样这个变量只在这个函数里面有效,一旦出了这个函数就不能用了,j 变量是定义在for循环里面的,出了for循环就用不了,所以b 和 j 两个变量就称之为局部变量

三、复合类型

复合类型(compoundiype)是指基于其他类型定义的类型。C++语言有几种复合类型 , 本章将介绍其中的两种:引用和指针。

1、引用

引 用为 对 象 起 了 另 外 一 个 名 字, 引 用 类 型 引 用 另 外 一 种 类 型 。 通 过 将 声 明 符 写 成 &d 的 形 式 来 定 义 引 用 类 型 , 其 中 d 是 声 明 的 变 量 名 :

int ival=1024;
int &refVal=ival;  //refVal指向ival(是ival的另一个名字) 
int &refVal2; 		//报错:引用必须被初始化

一般在初始化变量时,初始值会被拷贝到新建的对象中。然而定义引用时,程序把引用和它的初始值绑定 (bind)在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将 和 它 的 初 始 值 对 象 一 直 鄉 定 在一 起 。 因 为 无 法 令 引 用 重 新 绑 定 到 另 外 一 个 对 象, 因 此 引 用 必须初始化

引用即别名

引用并非对象,相反的,它只是为一个巴经存在的对象所超的另外 一个名宇

定 义 了一 个 列 用 之后 , 对 其 进 行 的 所 有 操 作 都 是 在 与 之绑定 的 对 象 上 进 行 的 :

int a = 10int &b = a;	//b指向a

b = 2//对b进行操作就相当于对a进行操作,所以a也等于2

为引用赋值,实际上是把值赋给了与引用鄉定的对象。获取引用的值,实际上是获取了与 引用绑定的对象的值。因 为 引 用 本 身 不 是 一 个 对 象 , 所 以 不 能 定 义引 用 的 引 用 。

引用的定义

  • 允许在一 条语句中定 义多个引用,其中每个引用标识符都必须以符号&开头:
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; //错误:此处引用类型的初始值必须是int型对象,而dval是double型

2、指针

指 针 、是 “ 指 向 ” 另 外 一 种 类 型 的 复 合 类 型 。 与 引 用 类 似 , 指 针 也实现 了对其他对象的间接访问。然而指针与引用相比 又有很多不同点。 其一,指针本身 就是一 个对象, 允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不 同的对象。其二,指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的 指针如果没有被初始化,也将拥有一个不确定的值。

定义指针类型的方法将声明符写成* d 的形式, 其中 d 是变量名。如果在一 条语 句中 定义了几个指针变量,每个变量前面都必须有符号*:

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

(1)获取对象的地址

指针存放某个对象的地址,要想获取该地址,需要使用取地址符(操作符&):

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的类型不匹配,一个是int型,一个是double型
pi= &dval;				//错误:试图把double型对象的地址赋给int型指针

(2)指针值
指 针 的 值 (即 地 址 ) 应 属 下 列 4 种 状 态 之一 :

  • 指向一 个对象。
  • 指向紧邻对象所占空间的下一个位置。
  • 空指针,意味着指针没有指向任何对象。
  • 无效指针,也就是上述情况之外的其他值。

试图拷贝或以其他方式访问无效指针的值都将引发错误。编译器并不负责检查此类错误, 这一点和试图使用末经初始化的变量是一样的。访问无效指针的后果无法预计,因此程序员必须清楚任意给定的指针是否有效。
尽管第2种和第3种形式的指针是有效的,但其使用同样受到限制。显然这些指针没 有指向任何具体对象,所以试图访问此类指针 (假定的 )对象的行为不被允许。如果这样 做 了, 后 果 也 无法 预 计。


(3)利用指针访问对象

如果指针指向了一个对象,则允许使用解引用符 (操作符 * ),来访问该对象:

int a = 42;
int *b= &a; //b存放着变量a的地址,或者说b是指向变量a的指针
cout < < *b; 1//由符号*得到指针b所指的对象,输出42

对指针解引用会得出所指的对象,因此如果给解引用的结果赋值, 实际 上也就是给指针所指的对象赋值:

*b = 0;		//由符号*得到指针b所指的对象,即可经由b为变量a赋值
cout << *p; //输出0 

如上述程序所示,为* b赋值实际上是为b所指的对象赋值。


(4)空指针
空 指 针 不 指 向 任 何 对 象 , 在 试 图 使 用 一 个 指 针 之前 代 码 可 以 首 先 检 查 它 是 否 为 空 。 以 下列 出 几 个 生 成 空 指 针 的 方 法 :

int * p1 = nullptr; 	//等价于int *p1= 0:
int *p2= 0;			//直接将p2 初始化为字面常量0
 // 需要首先#include <cstdlib>
int *p3= NUIL; 		//等价于int *p3=0

得到空指针最直接的办法就是用字面值nullptr来初始化指针,这也是C++11新标准刚刚引入的一种方法。nullptr是一种特殊类型的字面值,它可以被转换成任意其他的指针类型。另一种办法就如对p2 的定义一样,也可以通过将指针初 始化为字面值0来生成空指针。

  • 过去的程序还会用到一个名为NULL的预处理变量 (preprocessor variable )来给指针 赋 值, 这 个 变 量 在 头 文件 cstdlib 中 定 义, 它 的 值 就 是 0 。
  • 当用到一个预处理变量时,预处理器会自动地将它替换为实际值,因此用NULL 初始 化指针和用0初始化指针是一样的。在新标准下,现在的C++程序最好使用nullptr , 同时尽量避免使用NULL 。

把int 变量直接賦给指针是错误的操作,即使int 变量的值怡好等于0也不行。

int zero= 0;
int *p= zero; ///错误:不能把int变量直接赋给指针

(5)赋值和指针

  • 指针和引用都能提供对其他对象的间接访问,然而在具体实现细节上二者有很大不同,其中最重要的 一点就是引用本身并非一个对象。一旦定义了引用,就无法令其再绑定 到另外的对象,之后每次使用这个引用都是访问它最初绑定的那个对象。 指针和它存放的地址之间就没有这种限制了。和其他任何变量(只要不是引用)一样, 给指针赋值就是令它存放一 个新的地址, 从而指向一个新的对象:
int i =42:
int *pi = 0;	//pi被初始化,但没有指向任何对象
int *pi2 =&i;	//pi2被初始化,存有i的地址
int *pi3;		//如果pi3定义于块内,则pi3的值是无法确定的
pi3 = pi2; 		//pi3 和pi2指向同一个对象i
pi2= 0;			//现在pi2不指向任何对象了,而是空指针

(6)void*指针

void* 是一种特殊的指针类型,可用于存放任意对象的地址。一个void*指针存放着 一 个 地 址 , 这 一 点 和 其 他 指 针 类 似 。 不同 的 是, 我 们 对 该 地 址 中 到 底 是 个 什 么 类 型 的 对 象 并 不 了解 :

double obj= 3.14,*pd= &obj;		
void *pv= &obj; 
//正确:void*能存放任意类型对象的地址 obj可以是任意类型的对象
pv = pd; 	//pv可以存放任意类型的指针

利用 void* 指针能做的事儿比较有限:拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个void* 指针。不能直接操作void* 指针所指的对象,因为我们并不知道 这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。

3、理解复合类型的声明

如前所述,变量的定义包括一 个基本数据类型(base type )和一组声明符。在同一条 定义语句中 ,虽然基本数据类型只有一个,但是声明符的形式却可以不同。也就是说,一条 定 义语 句 可 能 定 义出 不 同 类 型 的 变 量 :

int i = 1024, *p = &i, &r = i;
//i是一个int型的数,p是一个int型指针,i是一个int型引用

(1)定义多个变量

经常有一种观点会误以为,在定义语句中,类型修饰符(*或&)作用于本次定义的全 部变量。造成这种错误看法的原因有很多,其中之一是我们可以把空格写在类型修饰符和 变 量名中间:

int * p;
//合法但 是容易产生误 导

我们说这种写法可能产生误导是因为int* 放在一起好像是这条语句中所有变量共同的类 型一样。其实恰恰相反,基本数据类型是int而非int* 。* 仅仅是修饰了p而己,对该 声明语句中的其他变量,它并不产生任何作用:

int* p1, p2;
//p1是指向int的指针,p2是int

涉及指针或引用的声明,一般有两种写法。 第一种把修饰符和变量标识符写在一起:

int *p1, *p2;
//p1和p2都是指向int的指针

这种形式着重强调变量具有的复合类型。第二种把修饰符和类型名写在 一起,并且每条语 句只定义一个变量:

int *pl ; 	//p1 是指向int 的指针
int *p2; 	//p2是指向int的指针

这种形式着重强调本次声明定义了一种复合类型。


(2)指向指针的指针

一般来说,声明符中修饰符的个数并没有限制。当有多个修饰符连写在一起时,按照其逻辑关系详加解释即可。以指针为例,指针是内存中的对象,像其他对象一样也有自己 的地址,因此允许把指针的地址再存放到另 一个指针当中。

通过* 的个数可以区分指针的级別。也就是说,**表示指向指针的指针,*** 表示指 向指针的指针的指针,以此类推:

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

此处pi 是指向int 型数的指针,而ppi 是指向int 型指针的指针,

ppi——>pi——>ival

  • 解引用int 型指针会得到一个int 型的数,同样,解引用指向指针的指针会得到一 个 指 针。 此 时 为 了访 问 最 原 始 的 那 个 对 象, 需 要 对 指 针 的 指 针 做 两 次 解 引 用 :
cout<<"ival = " << ival <<"\n"
	<<"pi->ival = " << *pi << "\n"
	<< "ppi->pi->ival = " << **ppi <<endl;

该程序使用三种不同的方式输出了变量ival 的值:第一种直接输出;第二种通过int 型 指 针 pi 输 出 : 第 三种 两 次 解 引 用 ppi , 取 得 ival 的 值 。


(3)指向指针的引用

引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对 指针的 引用 :

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

四、const限定符

有时我们希望定义这样一种变量,它的值不能被改变。例如,用一个变量来表示缓冲 区的大小。使用变量的好处是当我们觉得缓冲区大小不再合适时,很容易对其进行调整。 另一方面,也应随时警惕防止程序一 不小心改变了这个值。为了满足这一要求,可以用关 键宇const 对变量的类型加以限定:

const int bufsize= 512:	//输入缓冲区大小

这样就把bufsize定义成了一个常量。任何试图为bufsize赋值的行为都将引发错误:

bufsize= 512; 	//错误:试图向const对象写值

因为const 对象一旦创建后其值就不能再改变,所以const 对象必须初始化。一如既往, 初始值可以是任意复杂的表达式:

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

1、const的引用

  • 可 以 把 引 用 维 定 到 const对 象 上, 就 像 鄉绑定 到 其 他 对 象 上一 样, 我 们 称 之 为对 常 量 的引用 (reference to const )。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象:
const int ci= 1024;
const int &r1 = ci; //正确:引用及其对应的对象都是常量
r1 =  42; 	//错误:r1是对常量的引用
int &r2 = ci;	//错误:试图让一个非常量引用指向一个常量对象

因为不允许直接为ci 賦值,当然也就不能通过引用去改变c i。因此,对r2 的初始化是 错误的。假设该初始化合法,则可以通过r2 来改变它引用对象的值,这显然是不正确的。


初始化和对const 的引用

  • 引用的类型必须与其所引用对象的类型一致,但是有两个 例外。第一种例外情况就是在初始化常量引用时允许用任意表达式作为初始值,只要该表 达式的结果能转换成引用的类型即可。尤其,允许为一个常量 引用绑定非常量的对象、 字面值,甚至是个一般表达式:
int i = 42:
const int &rl = i; //允许将const int&鄉定到一个普通int对象上 
const int &r2 = 42;  //正确:r2是一个常量引用
const int r3 = r1 * 3// 正 确 : r3 是 一 个 常 量 引 用
int &r4= r1*2//错误:r4是一个普通的非常量引用,没有const

2、指针和const

与 引 用 一样 , 也 可 以 令 指 针 指 向 常 量 或 非 常 量 。 类 似 于 常 量 引 用 ,指向常量的指针不能用于改变其所指对象的值。要想存放常量对象的地址,只能使用指向常量的指针:

const double pi = 3.14;	//pi是个常量,它的值不能改变
double *ptr =&pi;		//错误:ptr是一个普通指针,没有const属性
const double *cptr= &pi; //正确:cptr可以指向一个双精度常量
*cptr = 42;		//错误:不能给*cptr賦值,是常量

之前提到,指针的类型必须与其所指对象的类型一致,但是有两个例 外。第一种例外情况是允许令 一个指向常量的指针指向一个非常量对象:

double dval = 3.14; //dval 是一个双精度浮点数,它的值可以改变 
cptr= &dval; //正确:但是不能通过cptr上改变dval的值

和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。所谓指 向常量的指针仅仅要 求不能通过该指针改变对象的值,而没 有规定那个对象的值不能通过 其他途径改变。

const指针

指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定为常量。常 量 指 针 必 须 初 始 化 , 而 且 一 旦 初 始 化 完 成 , 则 它 的 值 ( 也 就 是 存 放 在 指 针 中的那个地址)就不能再改变了。把 * 放在const 关键字之前用以说明指针是一个常量, 这 样 的 书写 形 式 隐 含 着 一 层 意 味 , 即 不 变 的 是 指 针 本 身 的 值 而 非 指 向 的 那 个 值:

int errNumb = 0;
int *const curErr = cerrNumb; //CurErr将一直指向errNumb
const double pi = 3.14159;
const double *const pip= &pi;  //pip是一个指向常量对象的常量指针

此 例 中 , 离 curErr最 近 的 符 号 是 const , 意 味 着 本curErr 身 是 一 个 常 景 对 象 , 对 象 的 类 型 由 声 明 符 的 其 余 部 分 确 定 。 声 明 符 中 的 下一 个 符 号 是 * , 意 思 是 curErr是 一 个 常 量 指 针 。 最 后, 该 声 明 语 句 的 基 本 数 据 类 型 部 分 确 定 了常 量 指 针 指 向 的 是 一 个 int 对象。与之相似,我们也能推断出,pip 是一个常量指针,它指向的对象是一个双精 度浮点型常量。

指针木身是一 个常量并不意味着不能通过指针修改其所指对象的值,能否这样做完全 依赖于所指对象的类型。例如,pip 是一个指向常量的常量指针,则不论是pi p 所指的 对象值还是pip 自己存储的那个地址都不能改变。相反的,curEr r 指向的是一个一般的 非常量整数,那么就完全可以用curErr 去修改errNumb的值:

*pip = 2.72;	//错误:pip是一个指向常量的指针
if (*curErr) (//如果curErr所指的対象(也就是errNumb)的値不为0
	errorHandler () ;
	*curErr = 0; //正确:把CurErr所指的对象的值重置
}

3、顶层const和底层const

我们都知道,const可以表示的对象如何数据类型,如:算术类型、类、指针等。

例如,指针本身是一个对象,它又可以指向另一个对象。因此,指针本身是不是一个常量以及指针所指向的对象是不是一个常量就是两个独立的问题。所以,所谓顶层const 所表示的就是指针本身是一个常量,而底层const 表示的是指针所指向的对象是一个常量。

简单来说就是,const 所修饰的变量是常量的话,也就是这个变量不可以改变,那么这个就是顶层const顶层const 对任意类型都适用,而底层const 则与指针和引用等复合类型的基本类型部分有关。特例就是指针可以同时具有顶层const底层const。看下面代码来理解一下:

const int ci = 22; //ci的值不能改变,所以就是顶层const
int i = 0;
int *const p1 = &i;//const所修饰的p1不能改变,所以这个是顶层const
const int *p2 = &ci;//这个是底层const,因为p2所指向的ci的值不可以改变
const int *const p3 = p2;
//靠左的是底层const,靠右的是顶层const,所以这个指针同时拥有底层和顶层const
const  int  &r = ci;//用于声明引用的const都是底层const

4、constexpr和常量表达式

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

例如:

const int a = 10;//a就是一个常量表达式
const int b = a +1;//b也是一个常量表达式
int c = 0;//c不是常量表达式,c的值可以改变
const int sz = get_size();//不是常量表达式,因为get_size()函数需要在运行阶段才能得到结果

(1)constexpr 变量

  • 在一个复杂系统中,我们很难分辨一个一个初始值到底是不是常量表达式。我们可能认为只要定义了const的变量就是常量表达式了,但并不是这样的,就像上面那个第四个表达式就不是常量表达式。

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

constexpr int mf = 10;//10是常量表达式,所以mf也是常量表达式
constexpr int lm = mf+1;//mf+1是常量表达式
constexpr int sz = size();//size()不是常量表达式,所以sz 就不是常量表达式,就会报错。

如果你认定变量是一个常量表达式,那就把它声明成constexpr类型。

(2)指针和constexpr

  • 必须明确一点,在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针本身有效,与指针所指向的对象无关。
const int *p = 10;//p是一个指向整型常量的指针,不可以修改p指向的值,但可以改变p的值来改变地址。
constexpr int *q = nullptr;//q是一个指向整型的常量指针,可以改变它所指向的值,但不可以改变其地址。
  • 和其他常量指针类似,constexpr指针也可以指向常量也可以指向非常量,但有一个前提,constexpr指针只能指向固定地址中,如果一个地址是在函数中那就不能指向它,变量地址必须定义函数外或者一类超出函数本身外还能有效的变量,这类变量超出函数体外还能拥有固定地址。

例如:

int i = 0;
constexpr int j = 10;
//i 和j 都必须声明在函数体之外
constexpr const int *p = &i;//正确
constexpr int * p1 = &j;//正确

//-------------------
//如果将变量声明在函数体内就会报错
int main()
{
	int i = 0;
	constexpr int *p = &i;//编译器会报错
	return 0;
}

五、处理类型

前言:随着程序的越来越复杂,程序中用到的类型也越来越复杂,这些主要体现在两方面:第一个是遇到一些类型时,它们的名字既难写又难记,还无法确定其含义;第二个是有时候根本搞不清楚需要什么类型,所以程序员不得不回头去上下文寻找,这时候就需要类型别名来方便我们使用。

1、 类型别名

类型别名 是一个名字,它是某种类型的同义词,通俗的说就是另一个名字。适用类型别名有很多好处,它让复杂的类型名字变得简单明了、便于理解和使用,还有助于程序员清楚地知道该类型的真实目的,反正就是让你更加好理解,把复杂的类型简单化,老铁们非常的好用,给一个大大的赞。

1.1 typedef

C++有两种方法定义别名,第一种就是关键字typedef,也是比较传统的定义
例如:

typedef long int l_i;
l_i num;//等价于long int num;
typedef const char * cc_;

看上面这两行代码:
第一行我们将 long int 这个数据类型重新定义成了 l_i, 看看这个用法,啧,哎呦呦,这也太妙了吧,以后只要需要用到long int 定义的变量就可以用l_i 这个名字来代替,这简直不要太爽好吧。

第二个也是一样的,如果你想定义多个 const char * 类型的指针变量,又闲它比较长,就可以重命名一下,使它变得简单好记,但这里建议你重命名和原来名字有一些联系,这样更容易记住。const char *cc_ 比较,用单词的第一个字符做重命名,* 用 _ 来代替。

typedef还有很多用途,这里只指出了常用的用法,其他更多的用法需要你们以后慢慢探寻。

1.2 using

别名声明using 是C++11新标准规定的新的方法,这个用法也比较好用,使用也方便,上面的和这个看大家喜好,喜欢什么用什么。让我们看一下用法:

using SI = Sales_item;//
SI item;//等价于Sales_item;

这种声明别名的方式偏向于等式类型的,可能会有一些小伙伴喜欢,不过哪种方式不影响你的程序。

1.3 指针、常量和类型别名

如果某个类型别名指代的是复合类型或者常量的话,那么将它放在程序里面声明语句的话可能会造成一些错误和意想不到的后果。例如下面的语句用到了类型 pstr , 它实际上是类型 char* 的别名:

typedef char * pstr;
const pstr ct = 0;
//ct是指向 char的常量指针
const pstr *ps;//ps是一个指针
//它指向的对象是一个指向char的常量指针

上述两条语句的基本类型都是const pstr类型,const 是对给定类型的修饰,也就是对pstr 的修饰。pstr实际上是指向char 的指针, 因此,const pstr 就是指向char 的常量指针,而非指向常量字符(const char)的指针。

遇到类型别名声明语句时,人们往往会错误的类型别名替代成它们原来的样子来理解语句,例如:

const pstr ct  = 0;
cosnt char * ct = 0;
//这是对const pstr ct 的错误理解

在强调一遍,这种理解是错误的。 声明语句用到pstr时,其基本数据类型是指针。可是用 char* 重写声明时,数据类型就变成了char, * 成为了声明符的一部分。这样改写的结果是, const char 成为基本数据类型。前后两种声明的含义截然不同,前者声明了一个指向char 的常量指针,后者则是声明了一个指向const char 的指针。

2、auto类型说明符

编程时常常需要将表达式的值赋值给一个变量,但有时候又不知道这个表达式返回的值是什么类型,所以这个变量就不知道是什么类型。为解决这个问题,C++11新标准引入了 auto 这个类型说明符,用了它就能让编译器代替我们去分析表达式所返回的值的类型。auto 是让编译器通过初始值来推断变量的类型,所以,auto 的定义必须有初始值,例如:

//由 val1 和 val2 相加的结果
//编译器就可以推断出item的类型
auto  item  =  val1  +  val2;
//item 初始化为两个相加的结果
auto  ite;//这是很严重的错误
//auto定义必须初始化

如果两个变量的类型是一个类,那item 的类型就是一个类;如果两个变量是double 类型,那么item的类型就是一个double

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

auto i = 1, *p = &i;
//正确,i是整型,p是整型指针
//两个的基本数据类型都是整形

auto  sz = 1, pi = 3.14;
//错误,sz 和 pi的基本数据类型不一样

复合类型、常量和auto

编译器推断出来的auto类型有时候和初始值类型并不完全一样,这时候编译器会适当地改变结果类型使其更符合初始化规则。
比如我们使用引用其实是使用引用的对象,当引用被用作初始值时,真正参与初始化的其实是引用所指向的对象的值,此时编译器已引用对象的类型作为auto 的类型。听起来可能会有一点抽象,下面有代码得大家解释一下:

int i = 1;
int &r = i;
auto a = r;
// r是i的引用,r是和i是连接的
//改变r相当于改变i的值。
//而r作为引用赋值给a,
//所以auto将直接分析i的类型作为a的类型
//所以a的类型是整形

其次,auto一般会忽略掉顶层const(至于什么是顶层const和底层const,后面会出),同时底层const会被保存下来,比如当初始指是一个指向常量的指针是:

const int ci = i, &cr = ci;
auto b = ci;
//b是一个整数(ci的顶层const特性被忽略掉了)
auto c = cr;
//c是一个整数(cr是ci的别名,ci的顶层const特性又被忽略掉了)
auto d = &i;
//d是一个整形指针(整形的地址就是指向整形的指针)
auto e = &ci;
//e是一种指向整形常量的指针(对常量对象取地址是一种底层const,会保留底层const)

如果希望auto推断出的是顶层const,可以这样做:

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

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

auto &g = ci;
//g是一个整形常量引用,绑定到ci
auto &h = 42;
//错误,非常量引用无法绑定字面值
const auto &j = 42;
//正确,可以为常量引用绑定字面值

设置一个类型为auto 引用时(auto &),值中的顶层常量的属性仍然保留,如果是类型auto的话,此时常量就不是顶层常量了(顶层const会被忽略)。

3、decltype类型指示符

-----有时候我们只想从表达式中获得它的类型,而不想给变量用表达式初始化,为满足这个要求,C++11新标准引入了第二种说明符decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,但并不实际计算它的值。例:

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

假如函数 f() 返回类型为int,那么sum的类型为int。

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;
// 错误,z的类型是const int&,所以z是一个引用,必须初始化

decltype和引用

—有些表达式向decltype返回一个引用类型:

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

因为r是一个引用,因此decltype® 的结果是引用类型。如果想让结果类型是r的类型,可以把r当做表达式的一部分,如:r+0,显然这个表达式是一个具体的值而不是引用。

----另一方面,如果表达式的内容是解引用操作,则decltype 将得到的是引用类型。正如我们熟悉的那样,解引用指针可以得到指针所指向的对象,并且还可以给这个对象赋值,所以,decltype(*p) 的结果类型就是int &,而不是int。

decltypeauto还有一个重要的区别是: decltype 的表达式加上一个括号就会有不同的意思,如果该变量不加括号,则返回表达式的结果类型;如果给变量加上一层或多层括号,编译器将会把它当做一个表达式。变量是一种可以作为赋值语句左值的特殊表达式,所以这样的decltype就会得到引用类型:

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

注: 切记:decltype((varibale)) (主义是双层括号)的结果永远是引用,而decltype(variable)结果只有当variable本身就是一个引用时才是引用。

第二章介绍到这里了,感兴趣的小伙伴不妨点个关注,请持续关注我的专栏C++系统学习,正在更新中…

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白学编程*

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值