前面说了C++的基本数据类型,下面来看看在C++中如何定义变量和常量。
C++定义变量的方式和C语言一样,也可以在定义的同时初始化。值得一提的是列表初始化,在原来的C++版本中可以用于初始化数组等。C++ 11标准增加了更广泛的列表初始化,所以可以用列表初始化来初始化单个变量。
//定义变量
int a;
//定义并初始化
int b = 1;
//同时定义多个变量
int c = 5, d = 6;
//C++ 11 新特性:列表初始化
int e{3};
列表初始化有一个限制,如果要初始化的值超过了可容纳的范围,就会引发编译错误,而直接赋值就可以。
//不能编译
//short s{3.1415};
//可以编译
short s = 3.1415;
对于函数内部的局部变量来说,如果不初始化的话,值是未定义的。对于未初始化的局部变量进行操作可能导致无法预料的后果。对于全局变量,如果没有给定初始值,默认值是0。
变量定义告诉编译器,我要创建一个变量,以后再用它。而变量声明告诉编译器,我要引用一个变量,所以你先按照这个变量的类型和名字去找它。声明变量需要使用extern
关键字,而且声明的时候不能赋值。如果使用extern
关键字并赋值,那么变量声明就变成了变量定义,而且这只能用于全局变量的声明和定义。如果对一个函数内部的本地变量声明添加初始化式,就会引发编译错误。
//定义了一个全局变量
int global_count;
//声明在另一个文件中定义的全局变量
extern int global_count;
标识符也就是变量、函数、类的名字,用于标识不同的对象。和大多数编程语言一样,C++的标识符需要以字母或下划线开头,有数组、字母和下划线组成,而且对大小写敏感。
如果一个标识符定义在花括号外面,那么这个标识符的作用域就是全局作用域。全局作用域的变量可以在本文件的任何地方访问,如果在其他文件中声明这个标识符,那么还可以在其他文件中访问。
如果一个标识符在某对花括号中定义,那么这个标识符的作用域就在这对花括号中,这就是局部作用域。局部作用域的标识符在超出这个块后,就无法被访问了。如果有一个全局变量,然后在某个作用域中又定义了一个同名变量,那么这个局部变量就会屏蔽对全局变量的访问。如果希望访问全局变量,需要使用域操作符::
来指定。
//声明在另一个文件中定义的全局变量
extern int global_count;
void declare_and_define()
{
cout << global_count << endl;
if (true)
{
int global_count = 5;
cout << "同名局部变量覆盖全局变量:" << global_count << endl;
cout << "使用全局变量:" << ::global_count << endl;
}
}
复合类型指的是基于其他类型定义的更复杂的类型,这些复合类型也是C++语言的重点和难点。
指针是C++语言从C语言中继承的类型。每个变量在内存中都有一个地址来存储,指针就是这个地址。利用指针我们可以直接对变量进行修改。定义指针需要在指针名前添加星号*
。如果要在一行定义多个指针,那么每一个指针前都需要星号。
//指针
int *p1, *p2;
有了指针,还需要将变量的地址赋给它,这需要使用取地址符&
。注意指针和变量的类型必须匹配,将int
型变量的地址赋给double *
类型的指针是错误的。
int d1 = 5, d2 = 6;
//指针
int *p1, *p2;
p1 = &d1;
p2 = &d2;
cout << "d1=" << d1 << ",p1=" << *p1 << endl;
如果想由指针访问其指向的对象,使用解引用符*
。由于指针指向的是对象本身,所以使用解引用符修改对象会修改实际对象的值。
*p1 = 100;
cout << "d1=" << d1 << ",p1=" << *p1 << endl;
指针有两种状态,一种是指向某个内存空间,另一种是无效状态。对于无效状态的指针进行解引用会引发不可预料的后果,所以这种情况应该尽量避免。对于无效状态的指针,最好将其清空。在老版本的C++语言中,我们需要引用cstdlib
头文件,并且使用其中预定义的NULL
来清空指针,这个预定义的值实际上是0。在C++ 11标准中引入了一个新的字面量nullptr
来代替NULL
,所以在以后的程序中,我们最好使用nullptr
。
引用是C++语言新增的一种类型,它和指针既有相似之处,也有不同之处。
先来看看如何定义引用。
int d1 = 5;
//d2是d1的引用
int& d2 = d1;
如果要在一行同时定义多个引用,需要在每个引用名前添加&
。
int &r1 = d1, &r2 = d2;
引用实际上是一个别名,一旦定义好,对引用的所有操作都相当于直接对原变量进行操作。这一点和指针很类似。
//修改引用也会修改原变量
d2 = 100;
cout << "d1=" << d1 << ",d2=" << d2 << endl;
但是需要注意一点,指针也是一个变量,所以一个指针可以多次赋值,指向不同变量的地址。而引用只能和一个变量绑定,所以引用在定义的时候必须初始化,而且一旦初始化,无法再绑定到其他变量。
前面介绍了引用和指针两种复合类型,这些复合类型还可以互相组合,生成更加复杂的类型声明。对于指针和引用声明,它们是和变量组合在一起的。所以下面的定义中,p是一个指针,而d是一个变量。如果希望声明多个指针, 需要在每一个变量名前添加*
号。
int *p, d;
//即使星号和类型放在一起,p仍然是指针,d仍然是变量
//int* p, d;
符合类型还可以互相组合。比如说,我们可以定义指针的指针。
int **pp = &p;
引用是一个别名,所以无法定义引用的指针。但是反过来可以定义指针的引用。
int*& r = p;
常量和变量一样,唯一的不同点是常量一经定义,它的值就不能够在改变。常量定义和变量差不多,只不过需要使用const
限定符修饰。由于常量一经赋值就无法再改变,所以常量在定义的时候必须初始化。
//定义常量
const int const_count = 5;
编译器在处理常量的时候,会直接将常量替换为其对应的值,所以编译器需要知道常量的值。默认情况下,常量定义只在本文件中有效。如果在多个文件中定义了同名的常量,那么这些常量是各不相同的常量。如果需要在文件之间共享常量,就需要在常量定义和声明上都添加extern
关键字。
我们可以把引用标记为const
的,这种情况下这个引用变为只读的,我们可以修改原变量,可以通过引用读取原变量,但是无法通过引用修改原变量。
//引用常量
int i = 5;
const int& r = i;
i = 10;
//r = 10;
指针存储的就是对象的地址,如果我们把指针本身定义为const
的,那么我们将无法将这个指针指向其他对象的地址,但是我们可以通过这个指针修改指向对象的值。需要注意,这种定义必须将const
关键字置于紧挨的变量名的位置。
int j = 100;
//const指针
int *const cp = &i;
//可以修改指针指向的对象的值
*cp = 10;
//无法修改指针指向的对象
//cp = &j;
这种情况和上面那种情况正好相反,这次是将指针指向的对象声明为const
的,这样一来,我们无法修改指针指向的对象的值,但是我们可以修改指针指向其他对象的地址。这种定义需要将const
放到符合声明的最前面。虽然这种情况叫做指向常量的指针,但是这是对指针类型的声明,实际上这个指针完全可以指向一个变量,只不过我们无法通过指针修改这个变量的值。
const int *p = &i;
//可以修改指针指向其他对象
p = &j;
//无法通过指针修改值
//*p = 200;
前面我们看到了,指针是一个非常复杂的话题。指针本身是一个对象,而它又指向另一个对象。这些情况和常量常量声明组合在一起,将会变得非常复杂。所以我们需要对其做出分类。我们把本身是const
的对象叫做顶层const,而指向的对象是const
的就叫做底层const。这样一来就比较清楚了,指向常量的指针就是底层const,而指针常量就是顶层const。
下面这种情况,变量ccp
即使顶层const又是底层const。
//既是顶层const又是底层const
const int*const ccp = &i;
有时候编译器要求程序中的某些值不能改变,而且必须在编译期就能计算出来,这样的值叫做常量表达式。显然,字面量和用常量表达式初始化的const对象都是常量表达式。
当然,一个变量并不是常量表达式,哪怕我们在程序中没有修改过变量的值也不行。一个用变量初始化的const
对象也不是常量表达式。
C++ 11标准新规定了一个关键字constexpr
,它可以让编译器检查声明的常量。如果这个常量不是合法的常量表达式,那么就无法编译。
//常量表达式
constexpr int MAX_COUNT = 100;
constexpr int MIN_COUNT = -MAX_COUNT;
//i不是常量,所以下面的代码不能编译
//constexpr int VARIABLE_COUNT = i;