数据类型是程序的基础,它告诉我们数据的意义以及我们能在数据上执行的操作。
C++中定义了一套包含 算术类型(arithmetic type)和空类型(void) 在内的基本数据类型。其中算术类型包含了 字符(char)、整数形(int)、布尔值(bool)和浮点数(double、float)。空类型不对应具体的值,经常用于函数的返回值。
C++算术类型如下表
类型 | 含义 | 最小尺寸 |
---|---|---|
bool | 布尔类型 | 未定义 |
char | 字符 | 8位 |
wchar_t | 宽字符 | 16位 |
char16_t | Unicode字符 | 16位 |
char32_t | Unicode字符 | 32位 |
short | 短整形 | 16位 |
int | 整形 | 16位 |
long | 长整形 | 32位 |
long long(C++11) | 长整形 | 64位 |
float | 单精度浮点数 | 6位有效数字 |
double | 双精度浮点数 | 10位有效数字 |
long double | 扩展精度浮点数 | 10位有效数字 |
对于整形来说,即 short、int、long、long long
可以分为带符号(signed)--表示正数、负数、0和不带符号(unsigned)--仅仅表示大于0的值两种。整形都是带符号的。在整形前面使用关键字 unsigned
就变成了无符号类型。例如 unsigned int
。
对于字符型来说,字符型 char
被分为了三种: char
、 signed char
、 unsigned char
。注意:char
和 signed char
不是同一种类型。虽然字符有三种类型,但是表现形式只有两种:带符号和不带符号, 至于 char
会表现为那种形式,具体有编译器决定。
值在C++中是一个纯粹的数学抽象概念。在程序中作为一种只读数据存在。
字面值常量(literal)
顾名思义,字面值常量就是值一见便知。字面值常量的形式和值决定了它的数据类型。 字面值常量又分为整形和浮点型字面值、字符和字符串字面值、转义序列、布尔字面值和指针字面值。
整形和浮点型字面值
整形字面值的具体数据类型由它的值和符号决定。默认情况下,十进制字面值是带符号数。尽管整形字面值存储在带符号数据类型中,但严格来说,十进制字面值不会是负数,例如:当我们使用一个形如 -42
的负十进制字面值,那个负号并不在字面值之内,它的作用仅仅是对字面值取负而已。
浮点数字面值表现为一个小数或科学计数法表示的指数。例如:3.14159
或4e-10
。
字符和字符串字面值
由单引号括起来的一个字符称为char
型字面值;由双引号括起来的零个或多个字符则构成字符串型字面值。例如:
'a' // 字符字面值
"F U C++!" // 字符串字面值
字符串字面值的类型其实是由常量字符构成的数组(array),(见TODD)。编译器将会在每个字符串的结尾处添加一个空字符('\0'
),因此字符串字面值的实际长度要比内容多1。
转义序列
有两类字符程序员不能使用,一类是不可打印(nonprintable),例如退格(删除)和其他控制字符(上下左右)。另一类是在C++中有特殊含义的字符,这个时候就需要转义序列。转义序列均以反斜杠\
开始。C++规定的转义序列如下表:
转义序列 | 含义 |
---|---|
\n | 换行 |
\t | 横向制表符 |
\a | 宽字符 |
\v | 纵向制表符 |
\b | 退格符 |
\" | 双引号 |
\' | 单引号 |
\ | 反斜线 |
\r | 回车符 |
\f | 进纸符 |
布尔字面值和指针字面值
布尔字面值为 true
和 false
。 指针字面值(见TODO)
变量(variable),在C++中指的是一块内存空间,是与计算机内存这个物理设备关联在一起的。(故,需要区分数学上抽象的变量的概念)。在本笔记中,变量(以及后面的对象)均指具有某种数据类型的内存空间。
在定义(创建)一个变量(或对象)的时候,获得了一个特定的值,我们就说这个变量(或对象)被 初始化(initialized) 了。例如:
int x = 10; // 初始化整形变量x的值为10.
在C++中,初始化和赋值是两个完全不同的操作。二者在概念上严格区分。 初始化:在定义(创建)变量(对象)的时候赋予其一个初值 赋值:把变量(对象)的当前值擦除,用一个新的值来代替 例如:
int x = 10; // 初始化整形变量x的值为10.
x = 20; // 赋值,将x的当前值10擦除,用一个新值20代替.
以上概念需要严格区分!!
C++中定义初始化的好几种不同形式。
列表初始化
以下4中方式均能初始化整形变量x,而初始化列表是C++11引入的新标准,即用花括号去初始化变量。
int x = 0;
int x = {0}; // 列表初始化
int x{0}; // 列表初始化
int x(0);
关于列表初始化(list initialization),在类以及后面的vwctor
内容均会看到。这里只做一个说明。 需要说明的是,在使用列表初始化的时候,且存在 初始值丢失的风险(类型转换) 的时候,编译器将会报错,例如:
long double pi = 3.1415926536;
int a{pi}, b = {pi}; // 错误,转换未执行,存在丢失信息的危险.
int c(ld), d = ld; // 正确,转换执行,且确实丢失了部分值.
默认初始化
所谓默认初始化(default initialized),指的是定义(创建)变量(或对象)的时候,没有指定初始值,则变量就会被默认初始化。 对于内置类型的变量没有被显式初始化,它的值由定义(创建)它的位置决定。如果在任何函数之外的变量将会被初始化0。在函数体内部的内置类型将不被初始化。如果试图拷贝或者以其他形式访问,将会报错。 每个类各自决定其初始化对象的方式。而且,是否不经初始化就定义对象也由类自己决定。
总结:
- 定义于函数体内部的内置类型的对象(或变量)没有初始化,则其值未定义。
- 类的对象如果没有显式初始化,则其值由类确定。
使用未被初始化的变量将会带来无法预计的后果!!!!
为了允许把程序拆分成多个逻辑部分来编写,C++支持分离式编译(separate compilation),该机制允许将程序分割为若干个文件,每个文件可以被独立编译。 为了支持这种机制,C++将声明和定义区分开来。声明(declaration)使得名字为程序所知道,如果A文件想要使用B文件中的变量,则必须包含对那个名字的声明。而定义(declaration)则定义(创建)与名字相关联的实体,即内存空间。 定义和初始化的区别在于:初始化会为变量赋予一个初值。 要想声明一个变量而非定义它,在变量名前面添加关键字extern
,且不要显示初始化。 例如:
extern int x; // 声明整形变量x而并非定义x.
int y; // 定义整形变量y.
int z = 0; // 初始化整形变量z为0.
z = 10; // 赋值.
extern double pi = 3.14159; //定义,抵消 extern 的作用
如果在函数体内部试图初始化一个由 extern
关键字标记的变量,将会引发错误。
C++的标识符(identifier),说白了就是变量、函数、类等的名称。这里参考 谷歌开源代码风格之命名规范
标识符或者说命名,一定要规范!!!!。
在C++程序中,无论是在程序的什么位置,使用到的每一个名字(标识符)都会指向一个特定的实体:变量、函数、类型等。然而,同一个名字出现在不同的位置,也可能指向的是不同的实体。
名字(标识符)的作用域(scope)是程序的一部分,在作用域内的名字有其特定的含义,C++中大多数作用域均用花括号分割。
全局作用域
全局作用域(global scope),位于其他所有作用域之外的作用域。全局作用域天然存在于C++程序中,它不需要由程序员人为地定义。
块作用域
块作用域(block scope),又称局部作用域(local scope)。
关于全局变量和局部变量以及变量的生存周期,见(TODO)。
作用域能包含彼此,被包含(被嵌套)的作用域称为内层作用域(inner scope),包含别的作用域的作用域称为外层作用域(outer scope)。
在作用域中声明某个名字, 它所嵌套着的所有作用域中都能访问。同时也允许 在内层作用域中重新定义外层作用域已有的名字。例如:code/chapter1/demo1.cpp
/* 该程序仅仅做说明
* 同时需要注意:函数内部不宜定义与全局变量相同的新变量
*/
#include <iostream>
int reused = 42; // 全局变量
int main()
{
int unique = 0; // 变量 unique 具有块作用域
// 第一次输出:全局变量和局部变量
std::cout << reused << " " << unique << std::endl;
// 定义并初始化一个新的局部变量,该变量与全局变量同名,将会覆盖(隐藏)全局变量
int reused = 0; // 局部变量 reused 具有块作用域
// 第二次输出:两个局部变量
std::cout << reused << " " << unique << std::endl;
// 第三次输出:显示访问全局变量
std::cout << ::reused << " " << unique << std::endl;
return 0;
}
上述代码中,第24行的语句 int reused = 0
,隐藏了外层作用域中的 reused
变量,因此将会输出 0
。此时如果想要访问外层作用域中的 reused
变量,需要加上 域操作符 ::reused
。
需要说明的是:如果函数有可能用到全局变量,则不应该再定义一个同名的局部变量,因为 会覆盖(隐藏)外层作用域同名变量。因此在定义全局的变量的时候,根据谷歌命名规范,通常在变量名前面添加一个前缀 g
,即 g_reused
表示变量 g_reused
是一个全局变量。
所谓复合类型(compound type),指的是 基于其他类型定义 的类型。概念中的其他,可以指基本类型,也可以指自定义类型。(即:套娃)。可以这样去理解(数学角度):如果说基本类型是一个单一函数,那么复合类型就是一个复合函数。(可能不是很恰当)。
C++中有几种复合类型,这里介绍 引用 和 指针。
在C++11中引入了 “右值引用”(rvalue reference) 的概念,这个主要用于内置类。
严格来说,当我们说引用(reference)一词的时候,都是 "左值引用"(lvaue reference)
所谓 引用(reference),指的是给对象(变量)起了另外一种名字。(可以理解为引用就是家里人给你取的小名或者外号)。通过符号 &
来声明或者定义一个 引用类型。例如:
int value = 24;
int &refvalue = value; // 引用。变量 refvalue 指向 value.(换句话说:value的小名叫refvalue)
关于引用,需要注意以下几点:
-
引用 必须初始化
int x = 10; //初始化变量 int &refx = x; // 引用必须初始化 int &another_ref_y; // 错误,没有初始化
前面已经强调了:C++中 初始化 的概念。这里再次强调:所谓初始化,指的是在定义(创建)一个变量的时候,赋予一个初始值。
对于引用来说,在定义的时候会将引用和它的初始值 绑定(bind)在一起。而不是将初始值拷贝给引用。
一旦引用初始化完成,将和它的 初始值对象一直绑定在一起。所以无法再让引用重新绑定到另一个对象,因此引用必须初始化。(这个就叫专一)
-
引用即别名 引用 不是对象(变量),如果不对其初始化,是 没有地址 的。相反的,它只是为一个已经存在的对象(变量)所起的另一个名字。当我们定义了一个引用后,对其进行的所有操作,都是在 与之绑定的对象上 进行的。例如:
int x = 0; int &ref_x = x; ref_x = 2; // 把2赋值给ref_x指向的对象,此处是指x. 等价于 x = 2;
为引用赋值,实际上是把值赋给了与 引用绑定的对象;获取引用的值,实际上是获取 与之绑定的对象 的值。
引用一旦完成初始化,其 地址就和与之绑定的对象地址一致 。例如:
#include <iostream> int main() { int x = 0; int &ref_x = x; std::cout << &x << " " << &ref_x << std::endl; return 0; }
输出为:
0x7ffe9ac7a84c 0x7ffe9ac7a84c
这也就解释了,为什么引用是别名。因为从内存层面上来说,引用和与之绑定的对象都是在同一块内存下。
-
因为引用本身不是一个对象,故 不能定义引用的引用。例如:
int x = 0; int& &ref_x = x; //错误,不能定义引用的引用.
-
引用 只能绑定到对象(变量)上,不能绑定到字面值或者表达式的结果上 !(相关原因见TODO)。
int &x = 10; // 错误 int &x = 1 + 1; // 错误
-
引用类型必须要与之绑定的对象类型严格匹配。例如:
int x = 10; double &ref_x = x; // 错误,double类型的引用不能绑定到int类型上
-
在写代码的过程中,可能你会看到下面两种关于引用(指针)的定义。
int& x = value; int &x = value; int* ptr; int *ptr;
这两种方式表示的都是引用,只是编程习惯不一样罢了。我的理解是(可能不准确):
int& x
,强调的是本次声明定义了一种符合类型;int &x
,强调的是变量具有符合类型,我个人更加喜欢后者,不会让人产生误解。(仁者见仁智者见智)例如:
// 采用第一种方式 int* p1, p2;
请问上述语句中,
p1
是什么类型,p2
又是什么类型?很多人可能会说,
p1
和p2
都是int
类型的指针。其实不然,因为*
只修饰了p1
,并未修饰p2
,两个变量的基本类型都是int
,因此上述语句中,p1
是int
类型的指针变量,而p2
只是普通的int
类型变量,如果此时对p2
进行赋值操作,例如:p2 = &x;
就会报错。如果要想p2
也变成指针,那么就该做如下定义:int* p1; int* p2;
将二者分开单独声明。
如果采用第二种方式,就是这样
int *p1, *p2;
, 表明p1, p2
都是int
类型的指针。上述两种声明方式没有对错,只是侧重不一样, 但是一定要坚持一种方式,不要变来变去!
指针(poniter),是一种 “指向(point to)” 另外一种类型的复合类型。根据定义:我们可以说,指针是一种复合类型,一种什么样的复合类型呢?一种 指向类型 的复合类型。
定义指针,需要在对象(变量)名之前使用符号 *
,例如:
int *ptr; // 定义一个指针变量 ptr
关于指针,需要说明以下几点:
- 指针是一个对象,允许对其赋值和拷贝,而且在指针的生命周期内可以先后指向不同的对象。
- 指针无需在定义的时候赋予初值,同其他内置类型一样,在 块作用域 内定义指针没有被初始化,其值不确定。 以上两点是指针与引用不同的地方
- 指针的类型要和它所指向的对象(变量)严格匹配。
获取对象(变量)的地址
指针 存放的是某个对象(变量)的地址,要想获取该地址,则需要用 取地址符&
。例如:
int val = 42;
int *ptr = &val; //指针变量初始化,将val的地址当成初始值给p.故 p是指向变量val的指针.
由于引用不是对象,没有实际地址,故 不能定义指向引用 的指针,即 int& *ptr = x; //错误
。但是如果引用被初始化,就可以定义指向引用的指针。例如:
int x = 10;
int &ref_x = x; //引用初始化
int *ptr = &ref_x; //引用初始化后,可以定义指向引用的指针
int& *ptr = &x; // 错误,不能定义指向引用的指针。即:不能定义引用类型的指针。
指针值
在说指针的值之前,首先说,指针是什么,指针是一个存储对象(变量)地址的变量,那么也就是说指针的值,就是地址。总而言之:指针变量就是用来存储地址的, 指针变量的值就只能存储地址。
指针的值(即,地址) 有且只有 以下4种状态之一:
- 指向一个对象(某一个对象或变量的地址)
- 指向紧邻对象所占空间的下一个位置(TODO,尚未理解)
- 空指针,意味着该指针没有指向任何对象
- 无效指针,也就是上述情况之外的其他值
试图访问或者拷贝无效指针的值,都将引发错误,而编译器将不负责检查此类错误。
当指针没有指向任何对象(变量)的时候,任何对该指针的访问和操作都将引发不可预知的后果。
利用指针访问对象
如果一个指针指向了一个对象,那么可以用 解引用符*
来访问该对象。对指针解引用将会得出所指的对象(变量)。因此,如果给解引用的结果赋值,实际上也就是给指针所指向的对象(变量)赋值。例如:
#include <iostream>
int main() {
int x = 10;
int value = 0;
int *ptr = &x; // 指针变量ptr的初始化
value = *ptr; // 解引用,将指针ptr所指向的对象赋值给value
std::cout << value << std::endl; // 输出 10;
}
同时,对 *ptr
的操作,都会影响到其指向的对象(变量) x
。例如:*ptr = 100; std::cout << x << std::endl;
将会输出100
解引用操作只适用于那些确实指向了某个对象(变量)的有效指针
符号的多义性
从前面的内容来看,在C++语言中,某些符号在不同的语境下,有着不同的含义,编译器会自动根据上下文来确定符号的具体含义。例如:
int i =42;
int & r = i; // 引用,&跟随类型int出现
if(x&y) // &出现在语句中,故为“与”运算符
int *ptr = &i; // *跟着类型int出现,故为指针; &出现在表达式中,故为取地址
*ptr = 996; // *出现在表达式中,故为解引用
int &f = *ptr // &为引用,*为解引用
空指针
空指针(null pointer),一个 不指向任何对象的指针。在试图使用一个指针之前,可以用其检查该指针是否为空。
下面的几个方法都可以创建空指针:(重要)
int *ptr1 = nullptr; // 等价于 int* ptr1 = 0;
int *ptr2 = 0; // 直接将指针ptr2初始化为字面常量0
// 需要 #include <cstdlib>
int *ptr3 = NULL; // 等价于 int* ptr3 = 0;
使用 nullptr
来初始化指针变量是C++11引入的新标准,最直接简单。
建议将所有的指针都要初始化。如果不知道指向何处,就令其为 nullptr
或 0
赋值和指针
赋值永远改变的是等号左侧的对象(变量)。例如:
int x = 10;
int y = 100;
int *ptr = &x; // 初始化,ptr指向x
ptr = &y; // 赋值,prt现在指向y
指针的其他操作
只要指针是合法的,就可以将其运用在条件表达式中,以及对它进行算术运算。(关于指针的操作,见TODO)
void* 指针
void*
指针是一种特殊的指针,可以 用于存放任意对象的地址。和其他指针类似, void*
中也存储的是一个指针。不同的是:我们对该地址中到底是什么类型的对象并不了解。(该指针就很哲学:来者不拒,什么都能存,但是存的是什么,却不得而知)
不能直接操作该指针所指向的对象(变量),因为并不知道这个对象(变量)是什么类型,也就无法确定能在这个对象(变量)上进行怎样的操作。
double obj = 3.14;
void *pv = &obj; // 正确,void*能存放任何类型对象的地址
类型修饰符和声明语句
声明(或定义)语句的格式是: 基本数据类型 和 紧跟着其后的声明符列表 组成。所谓声明符,指的就是变量。例如: int x, y, z;;
,其中, int
就是基本数据类型,而 x,y,z
就是声明符列表。
在后面会看到更加复杂的数据类型和声明符,但是他们都是由基本数据类型得到的,因此基础很重要!
在C++中,类型修饰符用于修饰变量的类型, 例如:*
和 &
, 类型修饰符属于声明符(变量)的一部分,例如: int *x; int &y = z;
, 类型修饰符用于修饰变量。且声明符的修饰符的个数没有限制。
指向指针的指针
正如前面所说,类型修饰符没有限制,当有多个类型修饰符写在一起的时候,按照其逻辑关系解释即可。以指针为例,因为指针是对象,因此其本身也有地址,故可以把指针的地址再放到另一个指针中。
通过 *
的个数,可以区分指针的级别。例如: *p
表示 p
是一个指针, **P
是指向指针的指针, ***p
表示的是指向指针的指针的指针(禁止套娃!!!)
指向指针的引用
由于引用不是对象,故不能定义指向引用的指针(int& *p; // 错误
),但是指针是对象,所以存在对指针的引用。
int i = 42;
int *p = &i;
int *&r_p = p; // 定义指针的引用. 将变量 r_p 绑定到指针变量 p上
对于一条复杂的包含多个类型修饰符的语句,采用 从右向左 的方式,有助于理解。因为:离变量名最近的符号是对变量有最直接的影响
对于上面的代码来说, int *&r_p = p;
,从右向左阅读,最近的是 &
,故变量 r_p
是一个引用,然后 *
表明它引用的(要绑定的)的对象是一个指针。最后, int
说明变量 r_p
要绑定的是一个整形的指针。
类型修饰符用于修饰变量的类型,前面讲了两个类型修饰符引用和指针。现在将另一种变量修饰符 const
const
是一种类型修饰符,用于说明 永不改变的对象(变量)。任何对被 const
修饰的对象(变量)进行赋值操作都是错误的。因此: const
的对象(变量) 必须初始化。
const int x = 10; // 正确
const int y; // 错误,k是一个未被初始化的对象.
默认情况下,const
仅仅在当前文件中生效。因为编译器会把所有被 const
修饰的变量替换成一个常量。
假设,A文件有一个 const int x = 10;
, B文件中有一个 const int x = 10;
, 然后将两个文件放在一起编译,是允许的。
可以把引用绑定到 const
修饰的对象上,这种被称为 对常量的引用(reference to const)
, 对常量的引用不能用作修改它所绑定的对象。
const int x = 10; //被const修饰的变量必须初始化
const int &ref = x; //正确
ref = 42; // 错误,ref是对常量的引用,其值不能变化
int &ref_2 = x; // 错误:试图让一个非常量引用 ref_2 绑定到一个常量对象 x 上
正如前面所说的, 引用类型的对象必须和引用类型严格匹配。由于 x
是常量,因此对常量 x
的引用必须是常量类型,即 cosnt int &
同引用一样, 指向常量的指针(pointer to const),不能用于改变其所指的对象的值。要想存放常量对象的地址,就必须用指向常量的指针(指针类型必须和指向对象的类型严格匹配)。
const double pi = 3.14159;
const double *ptr = π // 正确
double *ptr_2 = π // 错误, ptr_2 是普通指针,不能指向常量对象.
非常量指针不能指向常量对象,但是常量指针可以指向非常量对象。例如:
double obj = 3.14;
const int *ptr = &obj; // 正确.
所谓指向常量的指针或引用,不过是指针“自以为是”罢了,他们自己以为自己指向(绑定)的对象是一个常量,所以会自觉的不去改变所指对象的值。
指针是对象而引用不是,因此指针同其他指针一样,允许把指针定义为常量,即 常量指针(const poniter)。常量指针必须初始化,而且一旦初始化,它的 值(存放在指针中那个地址)就不能发生改变。在书写的时候,把 *
放在 const
前,说明指针是一个常量,这样书写表明:不变的是指针本身的值而不是指针指向的那个值。
int x = 10;
int *const ptr = &x; // ptr将一直指向x
const double pi = 3.14159;
const double *const ptr_2 = π // ptr_2是一个指向常量对象的常量指针
正如前面所讲的,从右向左阅读。 ptr
右边的第一个是 const
,表名这是一个常量变量,声明符中的下一个是 *
,表名变量 ptr
是一个常量指针,最左边的 int
说明是整形。总结起来就是:ptr
一个指向整形的常量指针.
同理可以推断出:ptr_2
是一个指向双精度浮点类型常量的常量指针。
指针,它本身是一个对象,同时它又指向了一个对象。因此: 指针本身是不是常量以及指针所指向的对象是不是常量,是两个相互独立的问题。因此,为了区分。用 顶层const(top-level const) 表示指针本身是一个常量,用 底层const(low-level const) 表示指针所指向的对象是一个常量。
更一般的,顶层const
可以表示任意对象都是常量,这一点对任何数据类型都适用;底层const
则与指针和引用等复合类型有关。例如:
int i = 0;
int j = 1;
int *const p1; // 错误,顶层const修饰的对象,必须初始化
int *const p1 = &i; // 正确,顶层const,必须初始化,表示p1本身是一个常量
p1 = &j; // 错误,p1是常量,已经指向了i,不能再指向j
*p1 = 100; // 正确
i = 999; // 正确
int k = 2;
const int *p2 = &k; // 底层const,表示p2所指的对象是一个常量
p2 = &i; // 正确,指针本身是可以改变的
*p2 = 20; // 错误,不能通过 *p2 去修改对象的值
k = 10; // 正确
根据上面的代码,可以看到:所谓 底层const(指向常量的指针),是自己认为自己指向了常量,“自以为是”的不会通过自己去修改指向对象的值。
常量表达式(const expression) 是指值不会变,并且在 编译过程中就能得到计算结果的表达式。
一个对象(或者表达式)是不是常量表达式由它的 数据类型 和 初始值 共同决定。例如:
const int max_file = 20; // 是
const int limit = max_file + 1; // 是
int size = 27; // 不是
C++11引入,将变量声明为 constexpr
类型,以便于由编译器来验证变量的值是否是一个常量表达式。 声明为 constexpr
的变量一定是一个常量,且必须用常量表达式初始化。例如:
constexpr int mf = 20; // 20是常量表达式
constexpr int limit = mf + 1; // mf + 1 是常量表达式
在后面会讲 constexpr
函数(TODO)
类型别名(type alias),是一个名字,是 某种类型的同义词。(和引用类似但是又有不同)。
类型别名能让复杂的类型变得简单明了、已于理解和使用,还能帮助程序员清楚知道使用该了类型的目的。
- 传统方法,关键字
typedef
typedef double wages; // wages 是 double 的同义词
wages pi = 3.1415;
- 别名声明(alias declaration)
C++11新标准可以使用关键字
using
来定义类型别名。
using MyInt = int;
MyInt x = 3;
推荐使用 using
,因为更加直观,特别是在数组中。例如:
typedef char MyCharArr[4];
using MyCharArr = char[4];
如果某个类型别名指代的复合类型或常量(实际工程会经常看到这样),那么把这个类型别名用到声明语句中就会产生意想不到的后果。TODO