C++基础:变量和基本类型
基本内置类型
一、基本内置类型
C++定义了一套包括算术类型(arithmetic type) 和空类型(void) 在内的基本数据类型。其中算术类型包含了字符、整型数、布尔值和浮点数。空类型不对应具体的值,仅用于一些特殊的场合,例如最常见的是,当函数不返回任何值时使用空类型作为返回类型。
二、算术类型
算术类型分为两类:整型(integral type,包括字符和布尔类型在内)和浮点型。
带符号类型和无符号类型
除去布尔型和扩展的字符型之外,其他整型可以划分为带符号的(signed) 和无符号的(unsigned) 两种。带符号类型可以表示正数、负数或0,无符号类型则仅能表示大于等于0的值。
类型int、short、 long 和long long 都是带符号的,通过在这些类型名前添加unsigned就可以得到无符号类型,例如unsigned long。 类型unsigned int 可以缩写为unsigned。
与其他整型不同,字符型被分为了三种: char、signed char和unsigned char。特别需要注意的是:类型char和类型signed char并不一样。类型char实际上会表现为上述两种形式中的一种,具体是哪种由编译器决定。
建议:如何选择类型
和C语言一样,C++的设计准则之一也是尽可能地接近硬件。C++的算术类型必须满足各种硬件特质,所以它们常常显得繁杂而令人不知所措。事实上,大多数程序员能够(也应该)对数据类型的使用做出限定从而简化选择的过程。以下是选择类型的一些经验准则:
- 当明确知晓数值不可能为负时,选用无符号类型。
- 使用int执行整数运算。在实际应用中,short常常显得太小而long一般和int有一样的尺寸。如果你的数值超过了int的表示范围,选用long long。
- 在算术表达式中不要使用 char或bool,只有在存放字符或布尔值时才使用它们。因为类型char在一些机器上是有符号的,而在另一些机器上又是无符号的,所以如果使用char进行运算特别容易出问题。如果你需要使用一个不大的整数,那么明确指定它的类型是signed char 或者unsigned char。
- 执行浮点数运算选用double,这是因为float通常精度不够而且双精度浮点数和单精度浮点数的计算代价相差无几。事实上,对于某些机器来说,双精度运算甚至比单精度还快。long double 提供的精度在一般情况下是没有必要的,况且它带来的运行时消耗也不容忽视。
三、类型转换
对象的类型定义了对象能包含的数据和能参与的运算,其中- -种运算被大多数类型支持,就是将对象从一-种给定的类型转换(convert)为另一种相关类型。
- 当我们把一个非布尔类型的算术值赋给布尔类型时,初始值为0则结果为false,否则结果为true。
- 当我们把一个布尔值赋给非布尔类型时,初始值为false则结果为0,初始值为true则结果为1。
- 当我们把一个浮点数赋给整数类型时,进行了近似处理。结果值将仅保留浮点数中小数点之前的部分。
- 当我们把一个整数值赋给浮点类型时,小数部分记为0。如果该整数所占的空间超过了浮点类型的容量,精度可能有损失。
- 当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。例如,8比特大小的unsigned char 可以表示0至255区间内的值,如果我们赋了一个区间以外的值,则实际的结果是该值对256取模后所得的余数。因此,把-1赋给8比特大小的unsigned char 所得的结果是255。
- 当我们赋给带符号类型一个超出它表示范围的值时,结果是未定义的(undefined)。此时,程序可能继续工作、可能崩溃,也可能生成垃圾数据。
提示:切勿混用带符号类型和无符号类型
如果表达式里既有带符号类型又有无符号类型,当带符号类型取值为负时会出现异常结果,这是因为带符号数会自动地转换成无符号数。例如,在一个形如a*b的式子中,如果a=-1,b=1,而且a和b都是int,则表达式的值显然为-1。然而,如果a是int,而b是unsigned,则结果须视在当前机器上int所占位数而定。在我们的环境里,结果是4294967295。
四、字面值常量
一个形如42的值被称作字面值常量(literal),这样的值一望而知。每个字面值常量都对应一种数据类型, 字面值常量的形式和值决定了它的数据类型。
1、整型和浮点型字面值
我们可以将整型字面值写作十进制数、八进制数或十六进制数的形式。以0开头的整数代表八进制数,以0x或0X开头的代表十六进制数。例如,我们能用下面的任意一种形式来表示数值20:
20 /*十进制*/ 024/*八进制*/ 0x14 /* 十六进制*/
2、字符和字符串字面值
由单引号括起来的一个字符称为char型字面值,双引号括起来的零个或多个字符则构成字符串型字面值。
'a' //字符字面值 "Hello World!" // 字符串字面值
3、转义序列
4、指定字面值的类型
通过添加如下表中所列的前缀和后缀,可以改变整型、浮点型和字符型字面值的默认类型。
对于一个整型字面值来说,我们能分别指定它是否带符号以及占用多少空间。如果后缀中有U,则该字面值属于无符号类型,也就是说,以U为后缀的十进制数、八进制数或十六进制数都将从unsigned int、unsigned long和unsigned long long中选择能匹配的空间最小的一个作为其数据类型。如果后缀中有L,则字面值的类型至少是long;如果后缀中有LL,则字面值的类型将是long long和unsigned long long中的一种。显然我们可以将U与L或LL合在一起使用。例如,以UL为后缀的字面值的数据类型将根据具体数值情况或者取unsigned long,或者取unsigned long long。
- L' a' //宽字符型字面值,类型是wchar_ t
- u8"hi!" // utf-8字符串字面值(utf-8用8位编码一个Unicode字符)
- 42ULL //无符号整型字面值,类型是unsigned long long
- 1E- 3F //单精度浮点型字面值,类型是float
- 3.14159L //扩展精度浮点型字面值,类型是long double
5、布尔字面值和指针字面值
true和false是布尔类型的字面值:bool test = false;
nullptr是指针字面值。
变量
一、变量
变量提供一个具名的、可供程序操作的存储空间。C++中的每个变量都有其数据类型,数据类型决定着变量所占内存空间的大小和布局方式、该空间能存储的值的范围,以及变量能参与的运算。对C++程序员来说,“变量(variable)” 和“对象( object)”一般可以互换使用。
二、变量定义
☆ 注意:初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来替代。
1、列表初始化
C++语言定义了初始化的好几种不同形式,这也是初始化问题复杂性的一个体现。 例如,要想定义一个名为units_ sold 的int变量并初始化为0,以下的4条语句都可以做到这一点:
int units_ sold = 0;
int units_ sold = {0} ;
int units_ sold{0};
int units_ sold(0) ;
2、默认初始化
如果定义变量时没有指定初值,则变量被默认初始化(default initialized), 此时变量被赋予了“默认值”。默认值到底是什么由变量类型决定,同时定义变量的位置也会对此有影响。
建议初始化每一个内置类型的变量。虽然并非必须这么做,但如果我们不能确保初始化后程序安全,那么这么做不失为一种简单可靠的方法。
三、变量声明和定义的关系
为了允许把程序拆分成多个逻辑部分来编写,C++语言支持分离式编译( separate compilation)机制,该机制允许将程序分割为若千个文件,每个文件可被独立编译。
为了支持分离式编译,C++语言将声明和定义区分开来。声明(declaration) 使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义(definition)负责创建与名字关联的实体。
extern int i; // 声明i而非定义 i
int j; // 声明并定义 j
extern double pi =3.1416; // 定义
变量能且只能被定义一次,但是可以被多次声明。
声明和定义的区别看起来也许微不足道,但实际上却非常重要。如果要在多个文件中使用同-一个变量,就必须将声明和定义分离。此时,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义。
四、标识符
C++的标识符(identifier) 由字母、数字和下画线组成,其中必须以字母或下画线开头。标识符的长度没有限制,但是对大小写字母敏感。
变量命名有许多约定俗成的规范,下面的这些规范能有效提高程序的可读性:
- 标识符要能体现实际含义。
- 变量名一般用小写字母,如index,不要使用Index或INDEX。
- 用户自定义的类名一般以大写字母开头,如Sales_ item。
- 如果标识符由多个单词组成,则单词间应有明显区分,如student_ loan或studentLoan,不要使用studentloan。
五、名字的作用域
作用域(scope)是程序的一部分, 在其中名字有其特定的含义。C++语言中大多数作用域都以花括号分隔。
名字main定义于所有花括号之外,它和其他大多数定义在函数体之外的名字-样拥有全局作用域( global scope)。 一旦声明之后,全局作用域内的名字在整个程序的范围内都可使用。
名字sum定义于main函数所限定的作用域之内,从声明sum开始直到main函数结束为止都可以访问它,但是出了main函数所在的块就无法访问了,因此说变量sum拥有块作用域(block scope)。 名字val定义于for语句内,在for语句之内可以访问val,但是在main函数的其他部分就不能访问它了。
作用域能彼此包含,被包含(或者说被嵌套)的作用域称为内层作用域(innerscope),包含着别的作用域的作用域称为外层作用域(outer scope)。作用域中一旦声明了某个名字,它所嵌套着的所有作用域中都能访问该名字。同时,允许在内层作用域中重新定义外层作用域已有的名字。
!!!如果函数有可能用到某全局变量,则不宜再定义一个同名的局部变量。
复合类型
一、复合类型
复合类型(compound type)是指基于其他类型定义的类型。C++语言有几种复合类型,将介绍其中的两种:引用和指针。
二、引用
引用(reference) 为对象起了另外一个名字,引用类型引用(refers to)另外一种类型。通过将声明符写成&d的形式来定义引用类型,其中d是声明的变量名:
int ival = 1024;
int &refVal = ival; // refVal指向ival (是ival的另一个名字)
int &refVal2; //报错:引用必须被初始化
引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字。
1、引用的定义
允许在一条语句中定义多个引用,其中每个引用标识符都必须以符号&开头:
inti=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 &refVa14 = 10; 错误:引用类型的初始值必须是一个对象
double dval = 3.14;
int &refVal5 = dval; 错误:此处引用类型的初始值必须是int型对象
三、指针
指针(pointer) 是“指向(point to)” 另外一一种类型的复合类型。
与引用类似,指针也实现了对其他对象的间接访问。然而指针与引用相比又有很多不同点。
- 其一,指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。
- 其二,指针无须在定义时赋初值。和其他内置类型一样, 在块作用域内定义的指针如果没有被初始化,也将拥有-一个不确定的值。
定义指针类型的方法将声明符写成*d的形式,其中d是变量名。如果在一条语句中定义了几个指针变量,每个变量前面都必须有符号*:
int*ip1,*ip2; // ipl和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的类型不匹配
pi = &dval; 错误:试图把double型对象的地址赋给int型指针
2、指针值
指针的值(即地址)应属下列4种状态之一:
- 指向一个对象。
- 指向紧邻对象所占空间的下一个位置。
- 空指针,意味着指针没有指向任何对象。
- 无效指针,也就是上述情况之外的其他值。
3、利用指针访问对象
如果指针指向了一个对象,则允许使用解引用符(操作符*)来访问该对象:
int ival = 42;
int *p = &ival; // p存放着变量ival的地址,或者说p是指向变量ival的指针
cout << *p; // 由符号*得到指针p所指的对象,输出42
对指针解引用会得出所指的对象,因此如果给解引用的结果赋值,实际上也就是给指针所指的对象赋值:
*p=0; // 由符号*得到指针p所指的对象,即可经由p为变量ival赋值
cout << *p; // 输出0
如上述程序所示,为*p赋值实际上是为p所指的对象赋值。
int i= 42;
int &r=i; // &紧随类型名出现,因此是声明的一部分,r是一个引用
int *p; // *紧随类型名出现,因此是声明的一部分,p是一个指针
P= &i; // &出现在表达式中,是一个取地址符
*p=i; // *出现在表达式中,是一个解引用符
int &r2=p; // &是声明的一部分,*是一个解引用符
4、空指针
空指针(null pointer)不指向任何对象,在试图使用一个指针之前代码可以首先检查它是否为空。以下列出几个生成空指针的方法:
int *pl = nullptr; 等价于int*p1=0;
int *p2 = 0;
直接将p2初始化为字面常量0
需要首先#include cstdlib
int *p3 = NULL;
等价于int *p3 = 0;
得到空指针最直接的办法就是用字面值nullptr来初始化指针,这也是C++11新标准刚刚引入的一种方法。 nullptr是一种特殊类型的字面值,它可以被转换成任意其他的指针类型。
建议:初始化所有指针
使用未经初始化的指针是引发运行时错误的一大原因。和其他变量一样,访问未经初始化的指针所引发的后果也是无法预计的。通常这一行为将造成程序崩溃,而且一旦崩溃,要想定位到出错位置将是特别棘手的问题。在大多数编译器环境下,如果使用了未经初始化的指针,则该指针所占内存空间的当前内容将被看作一个地址值。访问该指针,相当于去访问一个本不存在的位置上的本不存在的对象。糟糕的是,如果指针所占内存空间中恰好有内容,而这些内容又被当作了某个地址,我们就很难分清它到底是合法的还是非法的了。因此建议初始化所有的指针,并且在可能的情况下,尽量等定义了对象之后再定义指向它的指针。如果实在不清楚指针应该指向何处,就把它初始化为nullptr或者0,这样程序就能检测并知道它没有指向任何具体的对象了。
5、赋值和指针
指针和引用都能提供对其他对象的间接访问,其中最重要的一点就是引用本身并非一个对象。 一旦定义了引用,就无法令其再绑定到另外的对象,之后每次使用这个引用都是访问它最初绑定的那个对象。
指针和它存放的地址之间就没有这种限制了。和其他任何变量(只要不是引用)一样,给指针赋值就是令它存放一个新的地址,从而指向一个新的对象:
inti=42;
int *pi = 0; //pi被初始化,但没有指向任何对象
int *pi2 = &i; //pi2被初始化,存有i的地址
int *pi3; //如果pi3定义于块内,则pi3的值是无法确定的
pi3 = pi2; //pi3和pi2指向同一个对象i
pi2 = 0; //现在pi2不指向任何对象了
6、其他指针操作
只要指针拥有一个合法值,就能将它用在条件表达式中。和采用算术值作为条件,遵循的规则类似,如果指针的值是0,条件取false:
int ival = 1024;
int *pi = 0; // pi合法,是一个空指针
int *pi2 = &ival ;// pi2是一个合法的指针,存放着ival的地址
if (pi) // pi的值是0,因此条件的值是false
// ...
if (pi2) // pi2指向ival, 因此它的值不是0,条件的值是true
任何非0指针对应的条件值都是true。
7、void*指针
void*是一种特殊的指针类型,可用于存放任意对象的地址。一个void*指针存放着一个地址,这一点和其他指针类似。不同的是,我们对该地址中到底是个什么类型的对象并不了解:
double obj = 3.14, *pd = &obj;//正确:void*能存放任意类型对象的地址
void *pv = &obj; //obj可以是任意类型的对象
PV = pd; //pv可以存放任意类型的指针
利用void*指针能做的事儿比较有限:拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个void* 指针。不能直接操作void* 指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。
四、理解复合类型的声明
定义语句中,虽然基本数据类型只有-一个,但是声明符的形式却可以不同。也就是说,一条定义语句可能定义出不同类型的变量:
// i是一个int型的数,p是一个int型指针,r是一个int型引用
inti=1024,*p=&i,&r=i;
1、指向指针的指针
通过*的个数可以区分指针的级别。也就是说,** 表示指向指针的指针,*** 表示指向指针的指针的指针,以此类推:
int ival = 1024;
int *pi = &ival; // pi指向一个int型的数
int **ppi = π // ppi 指向一个int型的指针
此处pi是指向int型数的指针,而ppi是指向int型指针的指针,下图描述了它们之间的关系。
cout << "The value of ival\n"
<< "direct value: " << ival << "\n"
<< "indirect value: " << *pi << "\n"
<< "doubly indirect value: " << **ppi
<< endl ;
该程序使用三种不同的方式输出了变量ival的值:
- 第一种直接输出;
- 第二种通过int型指针pi输出;
- 第三种两次解引用ppi,取得ival的值。
2、指向指针的引用
引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用:
inti=42;
int *p; p是一个int型指针
int *&r = p; r是一个对指针p的引用
r=&i; r引用了一个指针,因此给r赋值&i就是令p指向i
*r=0; 解引用r得到i,也就是p指向的对象,将i的值改为0
面对一条比较复杂的指针或引用的声明语句时,从右向左阅读有助于弄清楚它的真实含义。
const限定符
一、const限定符
1、初始化和const
inti=42;
const int ci = i;//正确: i的值被拷贝给了ci
intj=ci; //正确: ci的值被拷贝给了j
2、默认状态下,const对象仅在文件内有效
如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字。
二、const的引用
可以把引用绑定到const对象上,就像绑定到其他对象上一样,我们称之为对常量的引用(reference to const)。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象:
const int ci = 1024;
const int &r1 = ci; //正确:引用及其对应的对象都是常量
r1 = 42; //错误: r1是对常量的引用
int &r2 = ci; //错误:试图让一个非常量引用指向一个常量对象
1、初始化和对const的引用
inti=42;●
const int &r1 = i; 允许将const int&绑定到一个普通int对象上
const int &r2 = 42; 正确: r1是一个常量引用
constint&r3=r1*2; 正确: r3是一个常量引用
int&r4=r1★2; 错误: r4是一个普通的非常量引用,注意这里和普通引用的区别
2、对const的引用可能引用一个并非const的对象
必须认识到,常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未作限定。因为对象也可能是个非常量,所以允许通过其他途径改变它的值:
inti=42;
int &r1 = i; 引用ri绑定对象i
const int &r2 = i; r2也绑定对象i,但是不允许通过r2修改i的值
r1=0; rl并非常量,i的值修改为0
r2=0; 错误: r2是一个常量引用
上一篇: 继承
下一篇: 你真的会使用命令模式吗?