变量和基本类型
2.1 基本内置类型
和c基本一样
2.2 变量定义
列表初始化
现在要定义一个名为 “x” 的int变量,并初始化为0,下列4条语句都可以实现
int x = 0;
int x = {0};
int x{0};
int x(0);
作为C++11新标准的一部分,用花括号来初始化变量得到了全面应用,我们暂时先将这种初始化形式称为列表初始化。
当用于内置类型的变量时,这种初始化有一个重要的特点:如果我们使用在列表初始化且初始值存在求实信息的风险时,编译器会报错:
long double ld = 3.1415926536;
int a{ld}, b = {ld}; // 错误:转换为执行,因为存在丢失信息的危险
int c(ld), d = ld; // 正确:转换执行了,但也确实丢失了信息
默认初始化
如果变量定义时没有指定初值,则变量被默认初始化,此时变量被赋予了“默认值”。默认值是由变量的类型决定的,同时定义变量的位置也会对此有影响。
在函数体内部的内置类型变量将不被初始化,一个未初始化的内置类型变量的值是未定义的。
不管怎么样,定义变量的时候最好还是进行初始化,这是一个良好的编程习惯,比如定义指针时,未做初始化处理,将导致野指针。
声明和定义
变量能且只能被定义一次,但可以被多次声明
如果想声明一个变量而非定义它,就在变量名前加上关键字“extern”,而且不要进行显式初始化变量:
extern int i; // 声明而非定义i
int j; // 声明并定义j
extern double pi = 3.1416; // 定义 (在函数内部会报错
C++是一种静态类型(statically typed)语言,其含义是在编译阶段检查类型。其中,检查类型的过程称为类型检查(type checking)。
标识符
名字的作用域
不论是在程序的什么位置,使用到的每个名字都会指向一个特定的实体:变量、函数、类型等等。然而、同一个名字出现在程序不用位置,也可能指向的是不同的实体。
作用域(scope) 是程序的一部分,在其中名字有其特定的含义。C++语言中大多数作用域都以花括号({、})分隔。
一个典型的示例:
#include <iostream>
int mian()
{
int sum = 0;
//sum用于存放从1到10所有数的和
for (int val = 1; val <= 10; ++val)
{
sum += val;
}
std::cout << "Sum of 1 to 10 inclusive is " << sum << std::endl;
return 0;
}
这段程序定义了3个名字:mian
、sum
和val
,同时使用了命名控件名字std
,该空间提供了2个名字cout
和cin
供程序使用。
mian
定义于所有花括号之外,拥有全局作用域。名字sum
定义于mian
函数所限定的作用域之内,从什么sum
到mian
函数结束都可以访问,拥有块作用域。名字val
定义于for
语句内,在for
语句内可以访问val
,但在mian
函数其他地方就不能访问它了。
嵌套的作用域
作用域中一旦声明了某个名字,它所嵌套着的所有作用域中都能访问该名字。同时、允许在内层作用域内重新定义外层作用域已有的名字:
#include <iostream>
/* 该程序仅用于说明:函数内部不宜定义与全局变量同名的新变量 */
int reused = 42; //reused拥有全局作用域
int mian()
{
int unique = 0;
std::cout << reused << " " << unique << std::endl; //输出#1
int reused = 0;
std::cout << reused << " " << unique << std::endl; //输出#2 访问局部变量reused
std::cout << ::reused << " " << unique << std::endl; //输出#3 访问全局变量reused
return 0;
}
- 输出#1 出现在局部变量
reused
定义之前,因此这条语句使用全局作用域中定义的名字reused
,输出:42 0
- 输出#2 发生在局部变量
reused
定义之后,此时局部变量reused
正在作用域内(in scope
),因此第二条输出语句使用的是局部变量reused
而非全局变量,输出:0 0
- 输出#3 使用作用域操作符来覆盖默认的作用域规则,因为全局作用域本身并没有名字,所以当作用域操作符的左侧为空时,向全局作用域发出请求获取作用域操作符右侧名字对应的变量。结果是,第三条输出语句使用全局变量
reused
,输出:42 0
2.3 复合类型
复合类型(compound type) 是指基于其他类型定义的类型。C++语言有几种复合类型,其中有:引用和指针。
2.3.1 引用
分“右值引用”和“左值引用”,一般我们所“引用”多指“左值引用”
引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字。
般在初始化变量时,初始值会被拷贝到新建的对象中。然而定义引用时,程序把引用和它的初始值绑定(bind)在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。
定义了一个引用之后,对其进行的所有操作都是在与之绑定的对象上进行的。
为引用赋值,实际上是把值赋给了与引用绑定的对象。获取引用的值,实际上是获取了与引用绑定的对象的值。同理,以引用作为初始值,实际上是以与引用绑定的对象作为初始值:
//正确: refVa13绑定到了那个与refVa1绑定的对象上,这里就是绑定到ival上
int &refval3 = refVal;
//利用与refVal绑定的对象的值初始化变量i
int i = refval; //正确: i被初始化为ival的值
因为引用本身不是一个对象,所以不能定义引用的引用。
引用的定义
允许在一条语句中定义多个引用,其中每个引用标识符都必须以符号&开头
2.3.2 指针
指针也就是“指向”另外一种类型的复合类型。与引用类似,指针也实现了对其他对象的间接访问。然而指针与引用相比又有很多不同点。
- 其一,指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。
- 其二,指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
获取对象的地址
int ival = 42;
int *p = &ival; // p存放变量ival的地址,或者说p是指向变量ival的指针
double dval;
double *pd = &dval; //正确: 初始值是double型对象的地址
double *pd2 = pd; //正确: 初始值是指向double对象的指针
int *pi = pd; //错误: 指针pi的类型和pd的类型不匹配
pi = &dval; //错误: 试图把double型对象的地址赋给int型指针
指针值
指针的值(即地址)应属下列4种状态之一:
- 指向一个对象。
- 指向紧邻对象所占空间的下一个位置。
- 空指针,意味着指针没有指向任何对象。
- 无效指针,也就是上述情况之外的其他值。
空指针
空指针(null pointer)不指向任何对象,在试图使用一个指针之前代码可以首先检查它是否为空。以下列出几个生成空指针的方法:
int *pl = nullptr; //等价于int *pl = 0;
int *p2 = 0; //直接将p2初始化为字面常量0
//需要首先#include cstdlib
int *p3 = NULL; //等价于int *p3 =0;
当用到一个预处理变量时,预处理器会自动地将它替换为实际值,因此用NULL初始化指针和用0初始化指针是一样的。在新标准下,现在的C+程序最好使用nul1ptr,同时尽量避免使用NULL。
建议:初始化所有指针
使用未经初始化的指针是引发运行时错误的一大原因。和其他变量一样,访问未经初始化的指针所引发的后果也是无法预计的。通常这一行为将造成程序崩溃,而且一旦崩溃,要想定位到出错位置将是特别棘手的问题。
在大多数编译器环境下,如果使用了未经初始化的指针,则该指针所占内存空间的当前内容将被看作一个地址值。访问该指针,相当于去访问一个本不存在的位置上的本不存在的对象。糟糕的是,如果指针所占内存空间中恰好有内容,而这些内容又被当作了某个地址,我们就很难分清它到底是合法的还是非法的了。
因此建议初始化所有的指针,并且在可能的情况下,尽量等定义了对象之后再定义指向它的指针。如果实在不清楚指针应该指向何处,就把它初始化为nullptr或者0,这样程序就能检测并知道它没有指向任何具体的对象了。
void*
指针
void*
是一种特殊的指针类型,可用于存放任意对象的地址。一个void*
指针存放着一个地址,这一点和其他指针类似。不同的是,我们对该地址中到底是个什么类型的对象并不了解:
double obj = 3.14, *pd = &obj;
// 正确: void*能存放任意类型对象的地址
void *pv = &obj; // obj可以是任意类型的对象
pv = pd; // pv可以存放任意类型的指针
利用void*
指针能做的事儿比较有限:拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个void*
指针。不能直接操作void*
指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。
既括说来,以void*
的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象。
理解复合类型的声明
int *p1, p2; //P1是指向int的指针, p2是int
int *p1, *p2; //P1和p2都是指向int的指针
建议将*
(或是&
)与变量名连在一起。
2.4 const
限定符
const
定义的变量,在初始化之后不可被改变
const
和 指针
const int x = 0;
int *p1 = &x; //错误,p1只是一个普通指针
const int *p2 = &x; //正确
int y = 0;
const *p3 = &y; //正确
*p3 = 2; //错误,(const *) (但y可以改变,可以用于权限管理)
int *const p4 = &y; //正确,p4将一直指向y
const int *const p5 = x; //正确,p5是一个指向常量对象的常量指针
顶层 const
如前所述,指针本身是一个对象,它又可以指向另外一个对象。因此, 指针本身是不是常量 以及 指针所指的是不是一个常量 就是两个相互独立的问题。
- 用名词顶层const(top-level const)表示指针本身是个常量
- 而用名词底层const (low-level const)表示指针所指的对象是一个常量。
更一般的,顶层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
/* 使用示例 */
i = ci; //正确: 拷贝ci的值, ci是一个顶层const,对此操作无影响
p2 = p3; //正确: p2和p3指向的对象类型相同, p3顶层const的部分不影响
int *p = p3; //! 错误: p3包含底层const的定义, 而p没有
p2 = p3; //正确: p2和p3都是底层const
p2 = &i; //正确: int*能转换成const int*
int &r = ci; //! 错误: 普通的int&不能绑定到int常量上
const int &r2 = i; //正确: const int&可以绑定到一个普通int上
constexpr
变量
常量表达式(const expression
) ,声明为此类型表示此变量一定为常量,而且必须用常量初始化
2.5 处理类型
类型别名
有两种方法可用于定义类型别名。传统的方法是使用关键字typedef:
typedef double wages; //wages是double的同义词
typedef wages base, *p; //base是double的同义词, p是double*的同义词
关键字typedef作为声明语句中的基本数据类型的一部分出现,含有typedef的声明语句定义的不再是变量而是类型别名。
新标准规定了一种新的方法,使用 别名声明(alias declaration) 来定义类型的别名:
using sI = Sales item; // S1是Sales item的同义词
这种方法用关键字using作为别名声明的开始,其后紧跟别名和等号,其作用是把等号左侧的名字规定成等号右侧类型的别名。类型别名和类型的名字等价,只要是类型的名字能出现的地方,就能使用类型别名
auto类型说明符
让编译器通过初始值来推算变量的类型。
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)
decltype类型指示符
选择并返回操作符的数据类型。只得到类型,不实际计算表达式的值。
decltype (f()) sum = x; // sum的类型就是函数f的返回类型
2.6 自定义数据类型
(1) 类
数据结构是把一组相关的数据元素组织起来,然后使用它们的策略和方法。
类一般不定义在函数体内,为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应该与类的名字一样。
头文件通常包含那些被定义一次的实体。
(2) 预处理器
#ifndef __SALES_DATA_H__
#define __SALES_DATA_H__
#endif /* __SALES_DATA_H__ */
一般把预处理变量的名字全部大写。
小结
类型是C++编程的基础。
类型规定了其对象的存储要求和所能执行的操作。C++语言提供了一套基础内置类型,如int和char等,这些类型与实现它们的机器硬件密切相关。类型分为非常量和常量,一个常量对象必须初始化,而且一旦初始化其值就不能再改变。此外,还可以定义复合类型,如指针和引用等。复合类型的定义以其他类型为基础。
C++语言允许用户以类的形式自定义类型。C++库通过类提供了一套高级抽象类型,如输入输出和string等。