Fork me on GitHub

引用&指针&const

  本文详细讲解了引用与指针的用法及具体区别,同时探讨了const限定符的基本用法。

引用

一条声明语句由一个基本数据类型和紧随其后的一个声明符列表组成。每个声明符命名了一个变量并指定该变量为与基本数据类型有关的某种类型。

1
2
3
4
5
int a;
基本数据类型 声明符(其实就是变量名)
//更复杂的声明符 ----接下来的指针和引用
int *b; //指针可以不初始化,没语法错误
int &b = a; //引用必须初始化

引用(reference)为对象起了另外一个名字,引用类型引用另外一种类型,通过将声明符号写成&d的形式来定义引用类型,其中d是声明的变量名。

1
2
3
int ival = 1024;
int &refVal = ival; //refVal指向ival(是ival的另一个名字)
int &refVal2; //报错,引用必须初始化

一般初始化变量时,初始值会被拷贝到新建的对象中。然而定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。一旦初始化完成。引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。

引用注意细节

  • 无法令引用重新绑定到另外一个对象,因此引用必须初始化。
  • 引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字,即引用即别名。
  • 引用本身不是对象,所以不能定义引用的引用。
  • 引用的类型要和绑定的对象严格匹配。而且,引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起。(两种例外,一种是对常量的引用 const int &
1
2
3
int &refVal4 = 10; //错误,引用类型的初始值必须是一个对象
double dval = 3.14;
int &refVal5 = dval; //错误:此处引用类型初始值必须是int型对象

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;
int main()
{
int a = 2;
int c = 4;
int &b = a; //b是a的引用,即b是a的别名
//&b = c; 错误,无法令引用重新绑定到另外一个对象上
cout<<a<<" "<<b<<" "<<&a<<" "<<&b<<endl;
a = 5;
cout<<a<<" "<<b<<" "<<&a<<" "<<&b<<endl;
b = 6;
cout<<a<<" "<<b<<" "<<&a<<" "<<&b<<endl;
a = c; //将a的值改变,b的值也相应改变,但&a和&b还是一样的
cout<<a<<" "<<b<<" "<<c<<" "<<&a<<" "<<&b<<" "<<&c<<endl;
b = c; //同上
cout<<a<<" "<<b<<" "<<c<<" "<<&a<<" "<<&b<<" "<<&c<<endl;
return 0;
}
2  2  0x28fef8  0x28fef8
5  5  0x28fef8  0x28fef8
6  6  0x28fef8  0x28fef8
4  4  0x28fef8  0x28fef8 0x28fef4
4  4  0x28fef8  0x28fef8 0x28fef4

指针

指针是“指向”另外一种类型的复合类型。与引用类似,指针也实现了对其他对象的间接访问。然而指针与引用相比又有很多不同点。

指针和引用的不同点

  • 指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。
  • 指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。

获取对象的地址

指针存放某个对象的地址,要想获取该地址,需要使用取地址符(&).

1
2
int ival = 42;
int *p = &ival; //指针变量p存放变量ival的地址,也即p是指向变量ival的指针

注意细节

  • 引用不是对象,没有实际地址,所有不能定义指向引用的指针。
  • 指针的类型要和它指向的对象严格匹配(两种例外,一种是指向常量的指针),因为在声明语句中指针的类型实际上被用于指定它所指向对象的类型,所以二者必须匹配。如果指针指向了一个其他类型的对象,对该对象的操作将发生错误。

指针值

指针的值(即地址)应属下列4种状态之一:

  1. 指向一个对象
  2. 指向紧邻对象所占空间的下一个位置
  3. 空指针,意味着指针没有指向任何对象
  4. 无效指针,也就是上述情况之外的其他值(比如指针未初始化,指针的值也就指向的地址为指针变量内存空间的当前内容,是一个随机值)

注意:指针没有指向任何具体对象,所以试图访问此类指针对象的行为不被允许,虽然编译可能通过,但如果这样做了,后果无法预计。

利用指针访问对象

使用解引用符(操作符*)来访问该对象。解引用操作仅适用于那些确实指向了某个对象的有效指针。

空指针

空指针不指向任何对象,在试图使用一个指针之前代码可以首先检查它是否为空。以下列出几个生成空指针的方法:

1
2
3
int *p1 = nullptr; //等价于int *p1 = 0;
int *p2 = 0; //直接将p2初始化为字面常量0
int *p3 = NULL; //等价于int *p1 = 0;

C++程序最好使用nullptr,同时尽量避免使用NULL。

  • 把int变量直接赋给指针是错误的操作,即使int变量的值恰好等于0也不行。
1
2
int zero = 0 ,*p;
pi = zero; // 错误:不能把int变量直接赋给指针

note:建立初始化所有指针

原因:在大多数编译器环境下,如果使用了未经初始化的指针,则该指针所占内存空间的当前内容将被看作一个地址值(即该地址值是一个随机数)。访问该指针,相当于去访问一个本不存在的位置上的本不存在的对象。糟糕的是,如果指针所占内存空间中恰好有内容,而这些内容又被当作了某个地址,我们就很难分清它到底是合法的还是非法的了。

因此建议初始化所有的指针,并且在可能的情况下,尽量等定义了对象之后再定义指向它的指针。如果实在不清楚指针应该指向何处,就把它初始化为nullptr或者0,这样程序就能检测并知道它没有指向任何具体的对象了。

void* 指针

void* 是一种特殊的指针类型,可用于存放任意对象的地址。一个void*指针存放着一个地址,这一点和其他指针类似。不同的是,我们对该地址中到底是个什么类型的对象并不了解:

1
2
3
double obj = 3.14, *pd = &obj; // 正确:void*能存放任意类型对象的地址
void *pv = &obj; // obj可以是任意类型的对象
pv = pd; // pv可以存放任意类型的指针

利用void指针能做的事儿比较有限:拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个void指针。不能直接操作void*指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。

概括说来,以void*的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象

代码示例

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
using namespace std;
int main()
{
double obj = 3.14,*pd = &obj;
void *pv = &obj;
cout<<&obj<<" "<<obj<<endl;
cout<<&pd<<" "<<pd<<" "<<*pd<<endl;
cout<<&pv<<" "<<pv<<" "<<endl; //不能直接操作void* 指针所指的对象,即*pv是不合法的
return 0;
}
0x28ff08   3.14
0x28ff04   0x28ff08   3.14
0x28ff00   0x28ff08 

理解复合类型的声明

修饰符(或&)和变量标识符写在一起,修饰符(或者&)都是修饰变量的。

1
int *p1,*p2; //p1和p2都是指向int的指针

指向指针的指针

指针是内存中的对象,像其他对象一样也有自己的地址,因此允许把指针的地址再存放到另一个指针当中。

通过的个数可以区分指针的级别。也就是说,表示指向指针的指针,表示指向指针的指针的指,依次类推:

1
2
3
4
5
int ival = 1024;
int *pi = &ival; //pi指向一个int型的数
int **ppi = &pi; //ppi指向一个int型的指针
//解引用
cout<<ival<<" "<<*pi<<" "<<**ppi<<endl;

指向指针的引用

引用本身不是对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用

1
2
3
4
5
int i = 42;
int *p; //p是一个int型指针
int *&r = p; //r是一个对指针p的引用, 此时r和p指针同名
r = &i; //r引用了一个指针,因此给r赋值&i就是令指向i
*r = 0; //解引用r得到i,也就是p指向的对象,将i的值改为0

要理解r的类型到底是什么,最简单的方法就是从右向左阅读r的定义离变量名最近的符号(此例中是&r的符号&)对变量的类型有最直接的影响,因此r是一个引用。声明符的其余部分用以确定r引用的类型是什么,此例中的符号*说明r引用的是一个指针。最后,声明的基本数据类型部分指出r引用的是一个int型指针。

const限定符

const定义

有时我们希望定义这样一种变量,它的值不能被改变。为了满足这一要求,可以用关键字const对变量的类型加以限制:

1
const int bufSize = 512; //输入缓冲区大小

这样就把bufSize定义成了一个常量。任何试图为bufSize赋值的行为都将引发错误。

因为const对象一旦创建后其值就不能再改变,所以const对象必须初始化。

1
2
3
const int i = get_size(); //正确:运行时初始化
const int j = 42; //正确:编译时初始化
const int k; //错误:k是一个未经初始化的常量

const类型能参与的操作:只能在const类型的对象上执行不改变其内容的操作。再不改变const对象的操作中还有一种是初始化。

默认状态下,const对象仅在文件内有效

当以编译时初始化的方式定义一个const对象时,就如对bufSize的定义一样:

1
const int bufSize = 512; //输入缓冲区大小

编译器将在编译过程中把用到该变量的地方都替换成对应的值也就是说,编译器会找到代码中所有用到bufSize的地方,然后用512替换。

默认情况下,const对象被设定为仅在文件内有效。当多个文件中出现同名的const变量时,不同文件中分别定义了独立的变量。

某些时候有这样一种const变量,它的初始值不是一个常量表达式,但又确实有必要在文件间共享。这种情况下,我们不希望编译器为每个文件分别生成独立的变量。相反,我们想让这类const对象像其他(非常量)对象一样工作,也就是说,只在一个文件中定义const,而在多个文件中声明并使用它。

解决办法: 对于const变量不管是声明还是定义都添加extern关键字,这样只需要定义一次就可以了:

1
2
3
4
//file1.cpp定义并初始化和一个常量,该常量能被其他文件访问
extern const int bufferSize = function();
//file1.h头文件
extern const int bufferSize; //与file1.cpp中定义的是同一个

note:如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字。

const引用

可以把引用绑定到const对象上,就像绑定其他对象上一样,我们称之为对常量的引用(reference to const)。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象。

1
2
3
4
const int ci = 1024;
const int &r1 = ci; //正确:引用及其对应的对象都是常量
r1 = 42; //错误:r1时对常量的引用
int &r2 = ci; //错误:试图让一个非常量引用指向一个常量对象

因为不允许直接为ci赋值,当然也就是不能通过引用去改变ci。因此,对r2的初始化也是错误的。假设该初始化合法,则可以通过r2来改变它引用对象的值,这显然是不正确。

初始化和对const的引用

引用的类型必须与其所引用对象的类型一致,但是有两个例外。第一种例外就是在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能抓换成引用的类型即可。 尤其,允许为一个常量引用绑定非常量的对象、字面值,甚至是个一般表达式

1
2
3
4
5
int i = 42;
const int &r1 = i; //允许将const int &绑定到一个普通int对象上
const int &r2 = 42; //正确:r1是一个常量引用
const int &r3 = r1*2; //正确:r3是一个常量引用
int &r4 = r1*2; //错误:r4是一个普通的非常量y

如果int &r4 = r1*2合法,那么就可以通过r4改变r1的值,而r1是常量引用。

常量引用被绑定到另外一种类型上时到底发生了什么:

1
2
3
4
5
double dval = 3.14;
const int &ri = dval;
//为了确保ri绑定一个整数,编译器把上述代码变成了如下形式:
const int temp = dval; //由双精度浮点数生成一个临时的整形常量
const int &ri = temp; //让ri绑定这个临时量

总结:常量引用(const &)可以绑定到const常量上,也可以绑定到int变量上,但是让一个非常量引用指向一个常量对象是不对的。

对const的引用可能引用一个并非const的对象

必须认识到,常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未做限定。因为对象也可能是个非常量,所以允许通过其他途径改变它的值:

1
2
3
4
5
int i = 42;
int &r1 = i; // 引用r1绑定对象i
const int &r2 = i; // r2也绑定对象i,但是不允许通过r2修改i的值
r1 = 0; // r1并非常量引用,i的值修改为0
r2 = 0; // 错误:r2是一个常量引用

r2绑定(非常量)整数i是合法的行为。然而,不允许通过r2修改i的值。尽管如此,i的值仍然允许通过其他途径修改,既可以直接给i赋值,也可以通过像r1一样绑定到i的其他引用来修改。

指针和const(指向常量的指针 const double *cptr)

指向常量的指针(pointer to const)不能用于改变其所指对象的值。要想存放常量对象的地址,只能使用指向常量的指针。

1
2
3
4
5
6
const double pi = 3.14; //pi是一个常量,它的值不能改变
double *ptr = &pi; //错误,ptr是一个普通指针
const double *cptr = &pi; //正确:cptr可以指向一个双精度常量
*cptr = 42; //错误:不能给*cptr赋值
double dval = 3.2;
cptr = &dval; //对的

注意:之前提到,指针的类型必须与其所指对象的类型一致,但是有两个例外。第一个就是允许令一个指向常量的指针指向一个非常量对象:

1
2
double dval = 3.14;
const double *ptr = &dval;

和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变

const指针(常量指针 int *const curErr)

指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定为常量。常量指针必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能改变了。把*放在const关键字之前用以说明指针是一个常量,这样的书写形式隐含着一层意味, 即不变的是指针本身的值而非指向的那个值:

1
2
3
4
int errNumb = 0;
int *const curErr = &errNumb; //curErr将一直指向errNumb
const double pi = 3.14159;
const double *const pip = &pi; //pip是一个指向常量对象的常量指针

从右向左阅读

此例中,离curErr最近的符号是const,意味着curErr本身是一个常量对象,对象的类型由声明符的其余部分确定。声明符中的下一个符号是*,意思是curErr是一个常量指针。最后,该声明语句的基本数据类型部分确定了常量指针指向的是一个int对象。与之相似,我们也能推断出,pip是一个常量指针,它所指向的对象是一个双精度浮点型常量。

指针本身是一个常量并不意味着不能通过指针修改其所值对象的值,能否这样做完全依赖于所指对象的类型。例如,pip是一个指向常量的常量指针,则不论是pip所指的对象值还是pip自己存储的那个地址都不能改变。相反的,curErr指向的是一个一般的非常量整数,那么就完全可以用curErr去修改errNumb的值:

1
2
3
4
5
6
7
*pip = 2.72; // 错误:pip是一个指向常量的指针
//如果curErr所指的对象(也就是errNumb)的值不为0
if(*curErr)
{
errorHandler();
*curErr = 0; //正确,把curErr所值得对象的值重置
}

总结

指向常量的指针(const doubel cptr)不能用于改变其所指向对象的值,但是可以改变指针本身的值,而常量指针(int const curErr)必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能在改变了,但是可以修改其所指向对象的值。

1
2
const double pi = 3.14
const double *const pip = &pi;

则此时pip是一个指向常量的常量指针,则不论是pip所指的对象值还是pip自己存储的那个地址都不能被改变。

-------------本文结束感谢您的阅读-------------

本文地址:http://www.wangxinri.cn/2017/09/01/引用&指针&const/
转载请注明出处,谢谢!

梦想夹带眼泪,咸咸的汗水!