基本内置类型
C++定义了一套包括算术类型(arithmetic type)和空类型(void)在内的基本数据类型。其中算术类型包含了字符、整型数、布尔值和浮点数。空类型不对应具体的值。
算数类型
算数类型分为两类:整型(包括字符和 bool 型在内)和浮点型。字符和布尔值都属于整型。
bool 型的取值为 true 或 false。
基本的字符类型是 char ,一个 char 的空间应确保可以存放机器基本字符集中任意字符对应的数字值。也就是说,一个 char 的大小和一个机器字节一样。
带符号类型和无符号类型
除去布尔型和扩展的字符型之外,其他整型可以划分为带符号的(signed)和无符号的(unsigned)两种。带符号类型可以表示正数、负数或 0,无符号类型则仅能表示大于等于 0 的值。
类型转换
程序应该尽量避免依赖于实现环境的行为。如果把 int 的尺寸看成是一个确定不变的已知值,那么这样的程序就称作不可移植的(nonportable)。当程序移植到别的机器上后,依赖于实现环境的程序就可能发生错误。
unsigned u = 10;
int i = -42;
std::cout << u + i << std::endl;//如果 int 占32位,输出4294967264
相加前首先把整数 -42 转换成无符号数。10-42就是10的补码与-42的补码相加。
00000000 00000000 00000000 00001010 (10)原码
00000000 00000000 00000000 00001010 补码
00000000 00000000 00000000 00101010 (42)原码
11111111 11111111 11111111 11010101 反码
11111111 11111111 11111111 11010110 补码
把负数转化为无符号数类似于直接给无符号数赋一个负值,结果等于这个负数加上无符号数的模。
如果表达式里既有带符号类型又有无符号类型,当带符号类型取值为负时会出现异常结果,这是因为带符号数会自动地转换成无符号数。
字面值常量
每个字面值常量都对应一种数据类型,字面值常量的形式和值决定了它的数据类型
整型和浮点型字面值
整型字面值中 0 开头的整数是 8 进制,0x 开头的整数是十六进制。
浮点型字面值可以用小数或科学计数法表示,科学计数法中的指数部分用 E 或 e 标识。
3.14 0. 0e0 .001 3.14E2
默认的,浮点型字面值是一个double.
字符和字符串字面值
字符串字面值的类型实际上是由常量字符构成的数组。
编译器在每个字符串的结尾处添加一个空字符(‘\0’),因此,字符串字面值的实际长度要比它的内容多1。例如,字面值’A’表示的就是单独的字符A,而字符串"A"则代表了一个字符的数组,该数组包含两个字符:一个是字母A、另一个是空字符。
转义序列
指定字面值的类型
bool 字面值和指针字面值
true false // bool 类型的字面值
nullptr // 指针字面值
变量
变量提供一个具名的、可供程序操作的存储空间。每个变量都有其数据类型。
数据类型决定着变量所占内存空间的大小和布局方式、该空间能存储的值的范围,以及变量能参与的运算。
变量定义
类型说明符 变量名
初始值
当对象在创建时获得了一个特定的值,我们说这个对象被初始化(initialized)了。用于初始化变量的值可以是任意复杂的表达式。
初始化和赋值是两个完全不同的操作。初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来替代。
int units_sold=0;
int units_sold={0};
int units_sold{0};
int units_sold(0);
默认初始化
如果定义变量时没有指定初值,则变量被默认初始化(default initialized),此时变量被赋予了““默认值””。
如果是内置类型的变量未被显式初始化,它的值由定义的位置决定:
1.定义于函数体内的内置类型的对象如果没有初始化,则其值未定义。类的对象如果没有显式地初始化,则其值由类确定。
2.定义于任何函数之外的内置类型则被初始化为0;
变量声明和定义的关系
声明(declaration)使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义(definition)负责创建与名字关联的实体。
变量声明规定了变量的类型和名字,在这一点上定义与之相同。但是除此之外,定义还申请存储空间,也可能会为变量赋一个初始值。
如果想声明一个变量而非定义它,就在变量名前添加关键字 extern,而且不要显式地初始化变量。
extern int i; //声明 i 而非定义 i
int j; //声明并定义 j
extern double pi=3.1416; //定义pi
//任何包含了显式初始化的声明即成为定义。
在函数体内部,如果试图初始化一个由 extern 关键字标记的变量,将引发错误。
标识符
关键字:
操作符替代名
作用域
作用域中一旦声明了某个名字,它所嵌套着的所有作用域中都能访问该名字。同时,允许在内层作用域中重新定义外层作用域已有的名字。
如果函数有可能用到某全局变量,则不宜再定义一个同名的局部变量。
复合类型
复合类型(compound type)是指基于其他类型定义的类型。
一条声明语句由一个基本数据类型(base type)和紧随其后的一个声明符( declarator)列表组成。每个声明符命名了一个变量并指定该变量为与基本数据类型有关的某种类型。
引用
引用(reference)为对象起了另外一个名字,引用类型引用(refers to)另外一种类型。通过将声明符写成 &d
的形式来定义引用类型,其中 d
是声明的变量名:
int ival =1024;
int &refval = ival;//refval指向ival(是ival的另一个名字)
int &refVal2;//报错:引用必须被初始化(也就是赋值)
一般在初始化变量时,初始值会被拷贝到新建的对象中。然而定义引用时,程序把引用和它的初始值绑定(bind)在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。
引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字。
引用即别名
为引用赋值,实际上是把值赋给了与引用绑定的对象;获取引用的值,实际上是获取了与引用绑定的对象的值。同理,以引用作为初始值,实际上是以与引用绑定的对象作为初始值。
//正确: refVal3绑定到了那个与refval绑定的对象上,这里就是绑定到ival上
int &refVal3 = refval;
//利用与refval绑定的对象的值初始化变量i
int i = refval; //正确:主被初始化为ival的值
引用的定义
允许在一条语句中定义多个引用,其中每个引用标识符都必须以符号&开头:
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型对象
指针
指针(pointer)是“指向(point to)”另外一种类型的复合类型。指针也实现了对其他对象的间接访问。
指针和引用的不同:
1.指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。
2.指针无须在定义时赋初值。 和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
3.引用不是对象,没有实际地址,所以不能定义指向引用的指针。
int *ip1, *ip2;// ipl和ip2都是指向int型对象的指针
double dp,*dp2;// dp2是指向double型对象的指针,dp是double型对象
获取对象的地址
指针存放某个对象的地址,要想获取该地址,需要使用取地址符(操作符&):
int ival =42;
int *p = &ival; // p存放变量ival的地址,或者说p是指向变量ival的指针
指针值
指针的值(即地址)应属下列4种状态之一:
1.指向一个对象。
2.指向紧邻对象所占空间的下一个位置。
3.空指针,意味着指针没有指向任何对象。
4.无效指针,也就是上述情况之外的其他值。
利用指针访问对象
如果指针指向了一个对象,则允许使用解引用符(操作符*)来访问该对象:
int ival = 42;
int*p = &ival; //p存放着变量ival的地址,或者说p是指向变量ival的指针
cout<<*p;//由符号*得到指针p所指的对象,输出42
*p= 0;//由符号*得到指针p所指的对象,即可经由p为变量ival赋值
cout<<*p://输出0
空指针
空指针(null pointer)不指向任何对象,在试图使用一个指针之前代码可以首先检查它是否为空。
几个生成空指针的方法:
int *pl = nullptr;//等价于int *p1 = 0;
int *p2=0;//直接将p2初始化为字面常量0
//需要首先#include cstdlib
int *p3 = NULL;//等价于int *p3 = 0;
赋值和指针
赋值永远改变的是等号左侧的对象。
int i = 42;
int *pi = 0; // pi被初始化,但没有指向任何对象
int *pi2 = &i; //pi2被初始化,存有i的地址
int *pi3; //如果pi3定义于块内,则pi3的值是无法确定的
pi3 = pi2; //pi3和pi2指向同一个对象i
pi2 = 0;//现在pi2不指向任何对象了
pi = &ival;// pi的值被改变,现在pi指向了ival
*pi = 0;// lival的值被改变,指针pi并没有改变
两个指针存放的地址值相同(两个指针相等)有三种可能:它们都为空、都指向同一个对象,或者都指向了同一个对象的下一地址。
需要注意的是,一个指针指应某对象,同时另一个指针指向另外对象的下一地址,此时也有可能出现这两个指针值相同的情况,即指针相等。
void* 指针
void* 指针和空指针不是一回事。
void* 指针是一种特殊的指针类型,可以存放任意对象的地址。
利用void指针能做的事儿比较有限:拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个void指针。
复合类型的声明
变量的定义包括一个基本数据类型( base type)和一组声明符。一条定义语句可能定义出不同类型的变量。
//i是一个int型的数,p是一个int型指针,r是一个int型引用
int i = 1024,*p=&i,&r = i;
指向指针的指针
一般来说,声明符中修饰符的个数并没有限制。 当有多个修饰符连写在一起时,按照其逻辑关系详加解释即可。
指针是内存中的对象,像其他对象一样也有自己的地址,因此允许把指针的地址再存放到另一个指针当中。
通过*的个数可以区分指针的级别。也就是说,**表示指向指针的指针,***表示指向指针的指针的指针,以此类推。
int ival = 1024;
int *pi = &ival; // pi指向一个int型的数
int **ppi = π // ppi指向一个int型的指针
指向指针的引用
引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用。
int i =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 对象一旦创建后其值就不能再改变,所以const对象必须初始化。
const int i=get_size();//正确:运行时初始化
const int j=42;//正确:编译时初始化
const int k;//错误:k是一个未经初始化的常量
只能在 const 类型的对象上执行不改变其内容的操作。
**默认状态下,const对象仅在文件内有效。**编译器将在编译过程中把用到该变量的地方都替换成对应的值。
多个文件中出现了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。
如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字。
const 的引用
可以把引用绑定到 const 对象上,就像绑定到其他对象上一样,我们称之为对常量的引用。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象。
const int ci = 1024;
const int &r1 = ci;//正确:引用及其对应的对象都是常量
rl =42;//错误:r1是对常量的引用
int &r2 =ci;//错误:试图让一个非常量引用指向一个常量对象
对const的引用可能引用一个并非const的对象:
int i = 42;
int &r1 = i;//引用ri绑定对象i
const int &r2 = i;// r2也绑定对象i,但是不允许通过r2修改i的值
r1 = 0;// r1并非常量,i的值修改为0
r2 = 0;//错误:r2是一个常量引用
指针和 const
指向常量的指针(pointer to const)不能用于改变其所指对象的值。要想存放常量对象的地址,只能使用指向常量的指针。
允许令一个指向常量的指针指向一个非常量对象。
const double pi = 3.14;//pi是个常量,它的值不能改变
double *ptr = π//错误:ptr是一个普通指针
const double *cptr = π//正确:cptr可以指向一个双精度常量
*cptr = 42;//错误:不能给*cptr赋值
double dval = 3.14;//dval是一个双精度浮点数,它的值可以改变
cptr = &dval;//正确:但是不能通过cptr改变dval的值
const 指针
指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定为常量。常量指针(const pointer) 必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了。
把 * 放在const关键字之前用以说明指针是一个常量,这样的书写形式隐含着一层意味,即不变的是指针本身的值而非指向的那个值。
int errNumb = 0;
int *const curErr =&errNumb;// curErr将一直指向errNumb
const double pi =3.14159;
const double *const pip = π//pip是一个指向常量对象的常量指针
指针本身是一个常量并不意味着不能通过指针修改其所指对象的值,能否这样做完全依赖于所指对象的类型。
*pip = 2.72;//错误:pip是一个指向常量的指针
//如果curErr所指的对象(也就是errNumb)的值不为0
if (*curErr) {
errorHandler ();
*curErr = 0;//正确:把 curErr所指的对象的值重置
}
顶层 const
指针本身是一个对象,它又可以指向另外一个对象。
因此,指针本身是不是常量以及指针所指的是不是一个常量 就是两个相互独立的问题。指针类型既可以是顶层 const也可以是底层 const。
用名词 顶层 const 表示指针本身是个常量,而用名词 底层 const 表示指针所指的对象是一个常量。
int i = 0;
int *const pl = &i;//不能改变p1的值,这是一个顶层const
const int ci = 42;//不能改变ci的值,这是一个顶层 const
const int *p2 = &ci;//允许改变p2的值,这是一个底层const
constexpr 和常量表达式
**常量表达式(const expression)**是指值不会改变并且在编译过程就能得到计算结果的表达式。包括字面值、常量表达式初始化的const 对象。
constexpr 变量
允许将变量声明为 constexpr 类型以便由编译器来验证变量的值是否是一个常量表达式。声明为 constexpr 的变量一定是一个常量,而且必须用常量表达式初始化:
constexpr int mf =20;// 20是常量表达式
constexpr int limit =mf + 1;// mf +1是常量表达式
constexpr int sz=size();//只有当size是一个constexpr函数时才是一条正确的声明语句
字面值类型
字面值类型包括:算术类型、引用和指针等。自定义类不属于字面值类型,也就不能被定义为 constexpr 类型。
尽管指针和引用都能定义成 constexpr,但它们的初始值却受到严格限制。一个constexpr指针的初始值必须是 nullptr 或者 0,或者是存储于某个固定地址中的对象。
指针和 constexpr
在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关:
const int *p = nullptr;// p是一个指向整型常量的指针
constexpr int *q= nullptr;//q是一个指向整数的常量指针
constexpr 把他所定义的对象设置为了顶层 cosnt。
constexpr 变量所声明的是真正的常量,而 const 现在一般只用来声明某个对象只读,不可改变。
处理类型
类型别名
类型别名(type alias)是一个名字,它是某种类型的同义词。类型别名和类型的名字等价。
使用关键字 typedef 定义:
typedef double wages; //wages是double的同义词
typedef wages base,*p; //base是double的同义词,p是double*的同义词
使用 别名声明 定义:
using SI = Sales_item; // SI是sales_item的同义词
这种方法用关键字 using 作为别名声明的开始,其后紧跟别名和等号,其作用是把等号左侧的名字规定成等号右侧类型的别名。
指针、常量和类型别名
typedef char *pstring;//实际上类型pstring 为 char* 的别名
const pstring cstr = 0;//cstr 是指向char的常量指针
const pstring *ps;//ps是一个指针,它的对象是指向char的常量指针
const char *cstr = 0;//是对const pstring cstr的错误理解
/*
是用char*重写了声明语句后,数据类型就变成了char,*成为了声明符的一部分。这样改写的结果是,const char成了基本数据类型。前后两种声明含义截然不同,前者声明了一个指向char的常量指针,改写后的形式则声明了一个指向const char的指针。
*/
auto 类型说明符
auto 让编译器通过初始值来推算变量的类型。因此,auto定义的变量必须有初始值。
复合类型、常量和 auto
编译器推断出来的auto类型有时候和初始值的类型并不完全一样。
1.使用引用其实是使用引用的对象,特别是当引用被用作初始值时,真正参与初始化的其实是引用对象的值。
int i = 0,&r = i;
auto a = r;// a是一个整数(r是i的别名,而主是一个整数)
2.auto一般会忽略掉顶层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)
如果希望推断出的auto类型是一个顶层const,需要明确指出。
const auto f= ci;// ci的推演类型是int,f是const int
decltype 类型指示符
希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。
decltype 的作用是选择并返回操作数的数据类型。
decltype (f()) sum = x;//sum的类型就是函数f的返回类型
decltype 与 auto 的不同:decltype 不会忽略引用和顶层 const。此外,decltype的结果类型与表达式形式密切相关。
const int ci = 0, &cj=ci;
decltype(ci) ×=0;//x的类型是const int
decltype(cj) y=x;//y的类型是const int&, y绑定到变量×
decltype(cj) z;//错误:z是一个引用,必须初始化
decltype ((variable))(注意是双层括号)的结果永远是引用,而decltype (variable)结果只有当variable本身就是一个引用时才是引用。
自定义数据结构
头文件
预处理变量有两种状态:已定义和未定义。
//通常为如下形式
//检查某个指定的预处理变量是否已经定义
#ifndef//当且仅当变量无定义时为真,一旦检查结果为真,则执行后续操作直到遇到 #endif 为止
#define//把一个名字设定为预处理变量
#endif
预处理变量无视C++语言中关于作用域的规则。
重要术语
- 别名声明(alias declaration) 为另外一种类型定义一个同义询:使用“名字=类型”的格式将名字作为该类型的同义词。
- 算术类型(arithmetic type) 布尔值、字符、整数、浮点数等内置类型。
- 基本类型(base type) 是类型说明符,可用const修饰,在声明语句中位于声明符之前。基本类型提供了最常见的数据类型,以此为基础构建声明符。
- 转义序列(escape sequence) 字符特别是那些不可打印字符的替代形式。转义以反斜线开头,后面紧跟一个字符,或者不多于3个八进制数字,或者字母 x 加上 1 个十六进制数。
- 头文件保护符(header guard) 使用预处理变量以防止头文件被某个文件重复包含。
- 对常量的引用(reference to const) 是一个引用,不能用来改变它所绑定对象的值。对常量的引用可以绑定常量对象,或者非常量对象,或者表达式的结果。
- **分离式编译(separate compilation)**把程序分制为多个单独文件的能力。