0

Const Pointer and Pointer to Const

 1 year ago
source link: https://yuxinli1.github.io/Const-Pointer-and-Pointer-to-Const/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文简单介绍了const对象的作用、初始化及其有效区,并展开说明了const与引用和指针结合之后的使用方法和注意点。

const用来定义一个常量,也就是一个不可修改的变量。在程序中,我们通常需要用到一种变量,在整个程序的生命周期都不会发生改变。为了防止程序一不小心更改了这个变量,我们使用const关键字定义一个常量。由于const对象一旦创建后便不能修改,所以要在定义常量的同时对其进行初始化。

const与初始化

const对象一经创建便不可修改,根据其初始化的时间,可以分为编译时初始化运行时初始化

编译时初始化即在定义const变量的同时便赋给其具体的值,如:

const int i = 10; // 编译时初始化

另一种是运行时初始化,例如某些时候const的值是一些函数的返回结果或表达式的值:

const int a = get_size(); // 运行时初始化

无论是何种方式,const对象一但经过初始化后便无法修改。

但是,也存在某些情况,如果利用一个对象去初始化另一个对象,则不管是否是const都可以:

int a = 10;
const int ca = a; // 正确,i的值被拷贝给了ca
int b = ca; // 正确,ca的值被拷贝给了b

这种不涉及指针的通常没有什么问题,如果涉及到指针,会有写不一样,我们下文再讲。

const对象的有效区

默认情况下,const对象仅在文件内有效,例如,有

const double timeout = 30.00;

编译时,编译器会找到文件中的每一个timeout,并用30.00替换它们。如果有多个文件想要使用该常量,则必须分别在每一个文件中定义它们,相当于在每个文件内定义了单独的变量。

为了避免变量的重复定义,并且某些时候const变量的值可能不是一个常量表达式,例如可能是get_size()等函数运算结果,我们不想或者无法定义多个重复的变量。也就是说,我们需要定义一个const变量,并且可以在多个文件中使用。这个时候,可以在const变量的定义和声明处都加上extern关键字,这样便只需定义一次。

// file1.h
extern const int bufsize;

// file1.c
extern const int bufsize = fcn();

const与引用

C语言无引用!

引用即别名,引用非对象!

所以不存在引用的引用。

对常量的引用

可以把引用绑定到const对象上,称之为对常量的引用(reference to const),简称常量引用。相比于对普通变量的引用,对常量的引用不能用作修改它所绑定的对象。

const int ci = 1024;
const int &r1 = ci; // 正确,引用及引用的对象都是const int
r1 = 42; // 错误,r1 是对常量的引用
int &r2 = ci; // 错误,非常量引用不能引用常量

由于引用一旦初始化完成,将会和它的初始值对象一直绑定在一起,而无法令引用重新绑定到另外一个对象。是不是可以说引用无法修改,这是否可以理解为某种方面上的“顶层const”。同时,对于对const的引用,例如r1,不能通过r1修改其引用对象的值,所以看作是底层const。

一般情况下,引用的类型必须与被引用对象的类型一致,但是有两种例外情况:

  • C++ Primer, 2.4.1节, P54 对象类型含有一个可接受的const类型转换规则。
  • C++ Primer, 15.2.3节, P534 对于存在继承关系的类,我们可以将基类的指针或引用绑定到派生类对象上。

这里仅讨论第一种情况。在初始化常量引用时,允许用任意表达式作为初始值,只要该表达式结果能转换成引用的类型即可。允许为一个 reference to const 绑定非常量的对象、字面值,甚至一个一般表达式。

int i = 42;
const int &r1 = i; // 正确,允许将const int&绑定到int上
const int &r2 = 42; // 正确,绑定了一个字面值
const int &r3 = r1 * 2; // 正确,绑定了一个一般表达式
int &r4 = r1 * 2; // 错误,r1为const int&,而r4为int&

为什么可以这样?中间发生了什么?

double dval = 3.14;
const int &ri = dval; // 正确

ri引用了一个int型的数,而dval是一个double。编译器为了确保让ri绑定一个整数,将上面的代码改成了:

const int temp = dval; // 由double生成一个临时int常量,temp=3
const int &ri = temp; // ri绑定临时量

所以说,ri并不是绑定到了dval,而是绑定到了一个临时量上。

当绑定到一个const常量上时当然是没问题的,因为不管是真的绑定到了dval上,还是绑定到了临时量上,我们都不能通过常量引用修改其绑定的对象。

但是,如果是非常量引用呢?这肯定与程序员的需求是不同的,程序员既然将一个引用绑定到了对象上,肯定是想通过引用修改其绑定的对象的。如果引用绑定到了一个临时量上,则无法通过引用修改原有对象。所以,C++规定引用的类型必须与被引用对象的类型一致,否则即为非法,如:

double pi = 3.14;
int &r = pi; // error: cannot bind non-const lvalue reference of type ‘int&’ to an rvalue of type ‘int’
// 之所以rvalue是type ‘int’,恰好说明编译器的确是做了const int temp = dval;的修改

const引用非const对象

由于常量引用的对象本身并不也需要是一个const对象,所以允许通过其他途径改变它的值。

int i = 42;
int &r1 = i; // 普通引用绑定i
const int &r2 = i; // 常量引用绑定i,不能通过r2修改i
i = 99; // 正确,可以直接赋值
r1 = 0; // 正确,r1非常量,i被修改为0
r2 = 1; // 错误,r2为常量,不能通过r2修改其所引用的对象

const与指针

指针与普通变量常量不同,它的内容是变量在内存中的地址。因此,当指针与const结合起来后,会有一些不一样的地方。我们需要分成两类来讨论:一类是 const pointer,即常量指针;另一类是 pointer to const,即指向常量的指针。

const pointer 常量指针

不像引用,指针是一个对象,因此应该能够把指针定义为一个常量,也就是常量指针。一旦将指针定义为一个常量,那么它的值,也就是存放在指针中的地址,便不能改变了。

常量指针是一个常量,根据C语言的规则,从右向左,所以const关键字位于*的右面,常量指针通常形式如下:

int num = 10;
int *const cp = #

cp最近的符号是const,意味着cp是一个const对象;然后是*,表明cp是一个 const pointer,最后是int,表明该 const pointer 指向的是一个int对象。

尽管我们不能修改常量指针的内容,即存放在其中的地址,但这并不意味着不能通过常量指针修改其所指向的内存单元中的数据。只要它指向的不是常量,便可以修改。

pointer to const 指向常量的指针

除了 const pointer,也就是常量指针外,还有 pointer to const。与 const pointer 不同,pointer to const 是一个pointer,只不过它指向一个const。也就是说指向常量的指针首先是一个指针,它指向一个常量,所以不允许通过该指针修改其所指向的常量。通常使用如下形式定义:

const double pi = 3.14156;
double *ppi = π // 错误,ppi是一个普通指针
const double *cpi = π // 正确,cpi是一个指向double常量的指针
// double const *cpi = π // 同样正确
*cpi = 2.17; // 错误,表达式必须是可修改的左值,不能给*cpi赋值

BUT......

double pi = 3.14;
const double *cpi = π
pi = 2.17; // 正确,pi是一个普通变量
// *cpi = 2.17 // error: assignment of read-only location ‘*cpi’ 编译不通过
printf("%f\n", *cpi); // output:2.170000

也就是说,const *可以指向常量,也可以不指向常量。如果const *指向的是一个普通变量,那么仍然可以通过原有变量名修改变量值,但是不能通过const *修饰的指针变量修改原值。

01.png

指向常量的常量指针

除了 const pointer 和 pointer to const,我们还可以将两者结合起来,即为指向常量的常量指针,既无法修改存放在指针中的地址,也无法通过该指针修改其所指向的内存单元数据。

const double pi = 3.14;
const double *const ccpi = π
ccpi = &pi+1; // error: assignment of read-only variable ‘ccpi’
*ccpi = 2.17; // error: assignment of read-only location ‘*ccpi’

顶层const与底层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
const int &r = ci; // 用于声明引用的const都是底层const(底层const ci绑定到底层const r上,正确)(参考“const与引用”小节)

当执行对象的拷贝时,顶层const不受影响,但是底层const却不能忽视。只有具有相同的底层const或者数据类型可以转换(一般是非常量转换到常量)时,才可以执行对象的拷贝。

i = ci;            // 正确,ci是顶层const,可以直接拷贝
p2 = p3; // 正确,p2和p3都具有相同的底层const,顶层const可以忽略
int *p = p3; // 错误,p3具有底层const而p没有
p2 = &i; // 正确,int *可以转换为const int *
int &r1 = ci; // 错误,普通的int&不能绑定到int常量上,即“引用的类型必须与被引用对象的类型一致”
const int &r2 = i; // 正确,const int&可以绑定到普通int上,引用中的一种例外情况

说起来,想要了解const还是写C语言中的“字符串”时看到strcpy、memcpy中的第二个参数是const修饰的指针,所以才想要看一下这块。当时觉得这不是都被const修饰了吗,怎么还能修改其所指向的对象?现在大概可以回答一下这个问题了。首先,__src参数都是指向常量的指针,所以说无法通过这个指针来修改其所指向的对象。但是,这并不意味着__src指针所指向的对象无法修改,其仍然能够被普通指针或者普通对象修改,这是没有问题的。其次,参数中的const都是底层const,在string.c的源码实现中,均有对原参数__src的一个拷贝操作,底层const不影响对指针变量值的修改,因此源码中的src++都是正确的,没有疑问的。

/* Copy SRC to DEST.  */
char *strcpy (char *__restrict __dest, const char *__restrict __src);
/* Copy N bytes of SRC to DEST. */
void *memcpy (void *__restrict __dest, const void *__restrict __src, size_t __n);
/* Copy N bytes of SRC to DEST, guaranteeing
correct behavior for overlapping strings. */
void *memmove (void *__dest, const void *__src, size_t __n);

References:

Pointers and const

C++中的const:指针、引用、函数、参数、类

《C++ Primer 2.4 const》


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK