-
#1.基本内置类型
- 1.1 算术类型
- 1.2 类型转换
- 1.3 字面值常量
-
#2.变量
- 2.1 变量定义
- 2.2 变量定义和声明的关系
- 2.3 标识符
- 2.4 名字的作用域
-
#3.复合类型
- 3.1 引用
- 3.2 指针
- 3.3 理解复合类型的声明
-
#4.const限定符
- 4.1 const引用
- 4.2 指针和const
- 4.3 顶层const
- 4.4 constexpr和常量表达式
-
#5.处理类型
- 5.1 类型别名
- 5.2 auto类型说明符
- 5.3 decltype类型指示符
#1. 基本内置类型
C++定义了一套包括算术类型和空类型在内的基本数据类型。其中算术类型包含了字符、整型数、布尔值和浮点数。空类型不对应具体的值,仅用于一些特殊的场合。
1.1 算术类型
算术类型:整型(包括字符和布尔类型在内)和浮点型
布尔类型(bool)的取值为true或者false。
带符号类型和无符号类型
除去布尔型和扩展字符型之外,其他整型可以划分为带符号的(signed)和无符号的(unsigned)两种。带符号类型可以表示正数、负数或0。无符号类型则仅能表示大于等于0的值。
==类型选择经验准则:==
- ==明确知道数值不可能为负数的时候,选择无符号类型。==
- ==使用int执行整数运算。如果数值超过了int的取值范围,选用long long。==
- ==在算术表达式中不要使用char或bool,只有在存放字符或布尔值的时候才使用它们。==
- ==执行浮点运算选用double。这是因为float通常精度不够而且双精度浮点数和单精度浮点数的计算代价相差无几。==
1.2 类型转换
对象的类型定义了对象包含的数据和能参与的运算,其中一种运算被大多数类型支持,就是将对象从一种给定的类型转换为另一种相关的类型。
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的值是未定义的
含有无符号类型的表达式
尽管我们不会故意给无符号对象赋值一个负值,却可能写出这样的代码。例如:当一个算术表达式中既有无符号数又有int值时,int值就会转换为无符号数。
unsigned u = 10;
int i = -42;
std::cout << i + i << std::endl; //输出-84
std::cout << u + i << std::endl; //如果int占32位,输出4294967264
当从无符号数中减去一个值,不管这个值是不是负数,必须保证结果不能为一个负值。
unsigned u1 = 42, u2 = 10;
std::cout << u1 - u2 << endl; //正确:输出32
std::cout << u2 - u1 << endl; //正确:结果是取模后的值
无符号数不会小于0这一事实同样关系到循环的写法。
//死循环
for (unsigned i = 10; i >= 0; i--) {
std::cout << i << endl;
}
==切忽混用带符号类型和无符号类型==
如果表达式中既有带符号类型又有无符号类型,当带符号类型取值为负时会出现异常结果,这是因为带符号数会自动地转换成无符号数。
1.3 字面值常量
每个字面值常量都对应一种数据类型,字面值常量的形式和值决定了它的数据类型。
整形和浮点型字面值
我们可以将整形字面值写作十进制数、八进制数或十六进制数的形式。以0开头的整数代表八进制,以0x或0X开头的代表十六进制。整形字面值具体的数据类型由它的值和符号决定。
20/*十进制*/ 024/*八进制*/ 0x14/*十六进制*/
浮点型字面值表现为一个小数或以科学计数法表示的指数,其中指数部分用E或e标识:
3.14159 3.14159E0 0. 0e0 .001
默认的,浮点型字面值是一个double。
字符和字符串字面值
由单引号括起来的一个字符称为char型字面值,双引号括起来的零个或多个字符则构成字符串型字面值。
'a' //字符字面值
"helloworld" //字符串字面值
转义序列
有两类字符程序员不可直接使用:一类是不可打印的字符,如退格或其他控制字符,因为它们没有可视的图符;另一类是在C++语言中有特殊含义的字符(单引号、双引号、问号、反斜线)。在这些情况下需要用到转义序列,转义序列均以反斜线作为开始,C++语言规定的转义序列包括:
换行符 \n 横向制表符 \t
回车符 \r ......
指定字面值的类型
L'a' //宽字符字面值,类型是wchar_t
u8"hi!" //utf-8字符串字面值
42ULL //无符号整形字面值,类型是unsigned long long
1E-3F //单精度浮点型字面值,类型是float
3.14159L//扩展精度浮点类型字面值,类型是long double
前缀 | 含义 | 类型 |
---|---|---|
u | Unicode16字符 | char16_t |
U | Unicode32字符 | 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来标记,因为小写字母l和数字1太容易混淆了。==
布尔字面值和指针字面值
true和false是布尔类型的字面值:
bool test = false;
nullptr是指针字面值。
#2. 变量
变量提供了一个具名的、可供程序操作的存储空间。C++中每个变量都有其数据类型,数据类型决定着变量所占内存空间的大小和布局方式、该空间能存储值的范围,以及变量能参与的运算。
2.1 变量定义
变量定义的基本形式是:首先是类型说明符,随后紧跟由一个或多个变量名组成的列表,其中变量名以分号隔开,最后以句号结束。
//sum,value和units_sold都是int类型
int sum = 0,value,units_sold = 0;
Sales_item item;
std::string book("0-123-456");
初始值
当对象在创建时获得了一个特定的值,即对象初始化。用于初始化变量的值可以是任意复杂的表达式。
//正确:price先被定义并赋值,随后被用于初始化discount。
double price = 109.99,discount = price * 0.6;
//正确:调用函数applyDiscount,然后用函数的返回值初始化salePrice。
double salePrice = applyDiscount(price,discount);
==初始化不是赋值,初始化的含义是创建一个变量时赋予一个初始值,而赋值是把对象当前的值擦除,而以一个新值来代替。==
列表初始化
C++语言定义了初始化的好几种不同的形式,这也是初始化问题复杂性的一个体现。
int units_sold = 0;
int units_sold = {0};
int units_sold{0};
int units_sold(0);
用花括号来初始化变量的这种形式,称为列表初始化。当用于内置类型的变量时,这种初始化形式有一个重要特点:如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器会报错:
long double ld = 3.1415926;
int a{ld},b = {ld}; //错误:转换未执行,因为存在丢失信息的风险
int c(ld),d = ld; //正确:转换执行,且确实丢失了部分值
默认初始化
如果定义变量时没有指定初始值,则变量被默认初始化。默认值到底是什么由变量的类型决定,同时定义变量的位置也会对此有影响。
std::string empty; //empty非显示地初始化为一个空串
Sales_item item; //被默认初始化的Sales_item对象
==定义于函数体内的内置类型的对象如果没有初始化,则其值未定义。类的对象如果没有显示地初始化,则其值由类决定。==
2.2 变量声明和定义的关系
为了允许把程序拆分成多个逻辑部分来编写,C++语言支持分离式编译机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。
为了支持分离式编译,C++语言将声明和定义区分开来。声明使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义负责创建与名字相关的实体。
如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显示地初始化变量:
extern int i; //声明i而非定义i
int j; //声明并定义j
任何包含了显示初始化的声明即成为定义。
extern double pi = 3.1416; //定义
==变量可以被声明多次,但是只能被定义一次。==
2.3 标识符
C++的标识符由字母、数字和下划线组成,其中必须以字母或下划线开头。
//定义4个不同的int类型变量
int somename,someName,SomeName,SOMENAME;
变量命名规范
- 标识符能体现实际含义。
- 变量名一般用小写字母,如index,不要使用Index或INDEX。
- 用户自定义的类名一般以大写字母开头,如Sales_item。
- 如果标识符由多个单词组成,单词间应有明显的区分,如student_loan或studentLoan,不要使用studentloan。
2.4 名字的作用域
作用域是程序的一部分,在其中名字有其特定的含义。C++语言中大多数作用域都以花括号分隔。
#include <iostream>
int main() {
int sum = 0;
for(int val = 1;val <= 10 ;i++) {
sum += val;
}
std::cout << "Sum of 1 to 10 inclusive is " << sum << std::endl;
return 0;
}
名字main定义于所有花括号之外,它和大多数定义在函数体之外的名字一样拥有全局作用域。名字sum定义在main函数所限定的作用域之内,sum拥有块作用域。
嵌套的作用域
作用域能彼此包含,被包含的作用域称为内层作用域,包含着别的作用域的作用域称为外层作用域。
#include <iostream>
int reused = 42; //reused拥有全局作用域
int main() {
int unique = 0;//unique拥有块作用域
//输出#1:使用全局变量reused;输出42 0
std::cout << reused << " " << unique <<std::endl;
int reused = 0;//新建局部变量reused,覆盖了全局变量reused。
//输出#2:使用局部变量reused;输出0,0
std::cout << reused << " " << unique <<std::endl;
//输出#3:显示地访问全局变量reused;输出42 0
std::cout << ::reused << " "<< unique <<std::endl;
return 0;
}
==如果函数有可能用到某全局变量,则不宜再定义一个同名的局部变量。==
#3. 复合类型
复合类型是指基于其他类型定义的类型。C++语言有几种复合类型,其中包括引用和指针。
3.1 引用
引用为对象起了另外一个名字,引用类型引用另外一种类型。通过将声明符写成&d形式来定义引用类型。
int ival = 1024;
int &refval = ival; //refval指向ival(是ival的另外一个名字)
int &refval2; //报错:引用必须被初始化
refval = 2; //把2赋值给refval所指向的对象
引用即别名
定义了一个引用后,对其进行的所有操作都是在与之绑定的对象上进行的:
int ival = 1024;
int &refval = ival; //refval指向ival(是ival的另外一个名字)
refVal = 2; //把2赋给refVal所指向的对象,此时即是赋给了ival
int ii = refVal; //与ii = ival执行结果一样
==引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字。==
引用的定义
允许一条语句中定义多个引用,其中每个引用标识符都必须以&开头。
int i1 = 1024,i2 = 2048; //i1和i2都是int类型
int &r1 = i1,r2 = i2; //r1是一个引用与i1绑定在一起,r2是int
int i3 = 1024,&r3 = i3; //i3是int类型,r3是一个引用与i3绑定在一起
int &r4 = i3,&r5 = i2; //r4和r5都是引用
所有的引用类型都必须和与之绑定的对象严格匹配。而且,引用只能绑定到对象上,而不能与字面值或某个表达式的计算结果绑定在一起。
int &refVal4 = 10; //错误:引用类型的初始值必须是一个对象
double dval = 3.14;
int &refVal5 = dval; //错误:此引用类型的初始值必须是int型对象
3.2 指针
指针是“指向”另外一种类型的复合类型。与引用类似,指针也实现了对其他对象的间接访问。定义指针类型的方式是将声明符写成*d的形式,其中d是变量名。
获取对象的地址
指针存放某个对象的地址,要想获取该地址,需要使用取地址符(操作符&)。
int ival = 42;
int *pi = &ival; //pi存放变量ival的地址
指针值
指针的值应属下列4种状态之一:
- 指向一个对象。
- 指向紧邻对象所占空间的下一位置。
- 空指针,意味着指针没有指向任何对象。
- 无效指针,上述情况之外的其他值。
利用指针访问对象
如果指针指向了一个对象,则允许使用解引用符(操作符*)来访问该对象。
int ival = 42;
int *pi = &ival; //pi存放了int型变量ival的地址。
std::cout << *pi << std::endl; //由操作符*得到指针所指向的对象,输出42
*pi = 0; //由操作符*得到指针pi所指向的对象,即可经由pi为变量ival赋值。
std::cout << *pi << std::endl; //输出0
==解引用操作仅适用于那些确实指向了某个对象的有效指针。==
空指针
空指针不指向任何对象,在试图使用一个指针前代码可以首先检查它是否为空。
int *p1 = nullptr; //等价于int *p1 = 0;
int *p2 = 0;
int *p3 = NULL;
得到空指针最直接的办法就是用字面值nullptr来初始化指针,这也是C++11新标准刚引入的一种方法。
==建议初始化所有的指针。==
赋值和指针
指针和引用都能提供对其他对象的间接访问,然后具体实现细节上二者有很大的不同,其中最重要的一点就是引用本身不是对象。
int i = 42;
int *pi = 0; //pi被初始化,没有指向任何对象
int *pi2 = &i; //pi2被初始化,存有i的地址
int *pi3; //如果pi3定义在块内,则pi3的值是无法确定的
pi3 = pi2; //pi3和pi2指向同一个对象i
pi2 = 0; //现在pi2不指向任何对象
其他指针操作
只要指针拥有合法值,就能将它应用于表达式中。
int ival = 42;
int *p1 = 0; //p1合法,是一个空指针
int *p2 = &ival; //p2存储的是变量ival的地址值
if(p1) {} //p1的值为0,因此条件为false
if(p2) {} //p2指向ival,条件为true
void*指针
void*是一种特殊类型的指针,可以存放任意对象的地址。
3.3 理解复合类型的声明
变量的定义包括了一个基本数据类型和一组声明符。在同一条定义语句中,虽然基本数据类型只有一个,但是声明符的形式却可以不同。
//i是一个整型变量,pi是一个int型指针,r是一个int型引用
int i = 1024, *pi = &i, &r = i;
定义多个变量
经常会有一种观点误认为,在定义语句中,类型修饰符(*或&)作用于本次定义的全部变量。
int *p1,p2; //p1是指针,p2是整型变量
指向指针的指针
一般来说,声明符中修饰符的个数并没有限制。以指针为例:指针本身就是个变量,具有其地址值,因此,允许将指针的地址值存在另一个指针当中。
以*操作符的个数来区分指针的级别。也就是说**表示指向指针的指针,***表示指向指针的指针的指针,以此类推。
int ival = 42;
int *pi = &ival; //pi指向一个int型数
int **ppi = π //ppi指向一个int型指针
指向指针的引用
引用本身不是一个对象,因此不能定义一个指向引用的指针。但是指针是对象,所以存在对指针的引用:
int i = 42;
int *p; //p是一个int型指针
int &*pr = p; //pr是一个对指针p的引用
pr = &i; //pr引用了指针,因此给r赋值&i,就是令p指向i
*pr = 0; //解引用pr得到i,也就是p所指向的对象,将i的值改为0
#4. const限定符
有时我们希望定义一种变量,它的值不能被改变。为了满足这一需求,可以用关键字const对变量的类型加以限定。
初始化和const
只能在const类型的对象上执行不改变其内容的操作。
int i = 42; //定义一个整型变量i,并赋值为42
const int ci = i; //正确:i的值被拷贝给了ci
int j = ci; //正确:ci的值被拷贝给了j
默认情况下,const对象仅在文件内有效
当以编译时初始化的方式定义一个const对象时,例如:
const int bufsize = 512;
编译器将在编译的过程中把用到该变量的地方都替换为对应的值。也就是说,编译器会找到代码所有用到bufsize的地方,然后用512替换。
如果想在一个文件中定义const,而在其他多个文件中声明并使用它。解决的办法是,对于const变量不管是声明还是定义都添加extern关键字,这样只需定义一次就可以了:
//file_1.c 定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufsize = fun();
//file_1.h 头文件
extern const int bufsize;
==如果想在多个文件之间共享const对象,必须在变量定义之前添加extern关键字。==
4.1 const引用
可以把引用绑定到const对象上,称之为对常量的引用。
const int ci = 1024;
const int *ri = ci; //引用及其对应的对象都是常量
ri = 42; //错误:ri是对常量的引用
int &r2 = ci; //错误:试图让一个非常量引用指向一个常量对象
初始化和对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的引用可能引用一个并非const的对象
常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未作限定。
int i = 42;
int &r1 = i; //引用r1绑定对象i
const int &r2 = i; //引用r2也绑定对象i,但是不允许修改r2的值
r1 = 0; //i的值被修改为0
r2 = 0; //错误:r2是一个常量引用
4.2 指针和const
与引用一样,也可以令指针指向常量或非常量。指向常量的指针不能用于改变其所指对象的值。要想存放常量的地址,只能使用指向常量的指针:
const double pi = 3.14;
double *ptr = pi; //错误:ptr是一个普通指针
const double *cptr = pi; //正确:cptr是一个常量指针
*cptr = 3.15; //错误:不能给cptr赋值
const指针
指针是一个变量,因此和其他类型一样,允许把指针本身定为常量。常量指针必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变。
int errNum = 0;
int *const curErr = &errNum; //curErr将一直指向errNum
const double pi = 3.15;
const double *const ptr = π //ptr是一个指向常量对象的常量指针
4.3 顶层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
4.4 constexpr和常量表达式
常量表达式是指值不会改变并且在编译过程中就能得到计算结果的表达式。
const int max_files = 20; //max_files是常量表达式
const int limit = max_files + 1; //limit是常量表达式
int staff_size = 27; //staff_size不是常量表达式
const int sz = get_size(); //sz不是常量表达式
constexpr变量
C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化:
constexpr int mf = 20; //20是常量表达式
constexpr int limit = mf + 1; //mf+1是常量表达式
constexpr int sz = size(); //只有当size是一个constexpr函数时,才是一条正确的声明语句
==一般来说,如果你认定变量是一个常量表达式,那就把它声明成constexpr类型。==
字面值类型
常量表达式的值需要在编译时就得到计算,因此对声明为constexpr时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见、容易得到。就把它们称为“字面值类型”。
指针和constexpr
在constexpr声明中如果定义一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关:
const int *p = nullptr; //p是一个指向整形常量的指针
constexpr int *q = nullptr; //q是一个指向整数的常量指针
#5. 处理类型
5.1 类型别名
类型别名是一个名字,它是某种类型的同义词。有两种方法可用于定义类型别名:
- 使用typedef关键字
- 使用别名声明来定义类型的别名
[1].使用typedef
typedef double wages; //wages是double的同义词
typedef wages base,*p; //base是double的同义词,p是double*的同义词
含有typedef的声明语句定义的不再是变量而是类型别名。
[2].使用别名声明
新标准规定了一种新的方法,使用别名声明来定义类型的别名:
using SI = Sales_item; //SI是Sales_item的同义词
指针、常量和类型别名
如果某个类型别名指代的是复合类型或常量,那么把它用到声明语句里就会产生意想不到的后果。
typedef char *pstring;
const pstring cstr = 0; //cstr是指向char的常量指针
const pstring *ps; //ps是一个指针,它的对象是指向char的常量指针
上述两条声明语句的基本数据类型都是const pstring,const是对给定类型的修饰。pstring实际上是指向char的指针,因此, const pstring就是指向char的常量指针,而非指向常量字符的指针。
const char *cstr = 0; //是对const pstring cstr的错误理解
5.2 auto类型说明符
C++新标准引入了auto类型说明符,用它能让编译器替我们去分析表达式所属的类型。和原来那些只对应一种特定类型说明符不同,auto让编译器通过初始值来推算变量的类型。显然,auto定义的变量必须有初始值:
//由val1和val2相加的结果可以判断出item的类型
auto item = val1 + val2; //item初始化为val1和val2相加的结果
复合类型、常量和auto
编译器推断出来的auto类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化的规则。
int i = 0, &r = i;
auto a = r; //a是一个整数(r是i的别名,而i是一个整数)
其次,auto一般会忽略掉顶层const,同时底层const会保留下来:
int i = 0;
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)
5.3 decltype类型指示符
有时会遇到这种情况:希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。为了满足这一要求,C++11新标准引入了第二种类型说明符decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值:
decltype(f()) sum = x; //sum的类型就是函数f的返回类型
decltype处理顶层const和引用方式与auto有些许不同。
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返回表达式结果对应的类型。
int i = 42, *p = &i,&r = i;
decltype(r + 0) b; //正确:加法的结果是int,因此b是一个未初始化的int
decltype(*P) c; //错误:c是int&,必须初始化
因为r是一个引用,因此decltype(r)的结果是引用类型。如果想让结构类型是r所指的类型,可以把r作为表达式的一部分,如r+0。另一方面,如果表达式的值是解引用操作,则decltype将得到引用类型。
decltype和auto的另一重要区别是,decltype的结果类型与表达式形式密切相关。
//decltype的表达式如果是加上了括号的变量,结果将是引用
decltype((i)) d; //错误:d是int &必须初始化
decltype(i) e; //正确:e是一个(未初始化的)int
==decltype((variable))(注意是双层括号)的结果永远是引用,而decltype(variable)结果只有当variable本身就是一个引用时才是引用。==
网友评论