欢迎来到博主的专栏——c++杂谈
博主ID:代码小豪
关于const的权限问题
如果还将c++里的const和C语言里的const画上一个等号,那么你是一定会吃上大亏的。
在C语言中,const修饰的变量是不可改变的。但是我们可以通过指针,绕开这层束缚。
const int a = 0;
a = 10;//error ,const变量不能被修改
int* pa = &a;
(*pa) = 10;//ok,因为这个指针并没有const修饰,因此修改它的数据是合法的
//但是a也因此被修改了
也就是说,C语言的const更像是在声明一个const变量,而不是给这个变量加上一个权限。(也不是说没有权限,而是这个权限太薄弱了)
于是c++改变了这个问题,在c++中,const变量的修改变得严格了。在c++中,如果一个变量被声明成了const类型,那么我们无法使用非const的指针或者引用指向这个变量。
const int a = 0;
a = 10;//error ,const变量不能被修改
int* pa = &a;//error,非const指针不能指向const变量
int& ra = a;//error,非const引用不能引用const变量
这在c++当中称为权限的放大。一个const权限的对象,如果被非const的指针/引用指向/引用了。那么其const的权限就被放大成非const,这在c++中是不允许的
与权限放大相反的概念是权限的缩小。即一个非const对象,可以被const修饰的指针/引用指向。权限的缩小时编译器允许的行为。因为这不会对对象的权限造成影响,你只是想要在使用这个指针/引用时,得到编译器的权限保护
在函数当中const的使用
const被应用最广泛的场景就是函数的声明了。在一个函数声明式内,const可以修饰函数返回值、各参数、成员函数。当const作用在不同的位置(即返回值、参数、成员函数)时,造成不同的函数性质。
这里我们抛出这么一个概念
- 尽可能使用const(use const whenever possible)
想要知道这是为何。我们就得先了解不这么做会导致什么?
我们先从const修饰的返回值开始说吧。
令函数返回一个常量值,目的在于降低使用者错误使用而造成意外。提高程序的安全性。
比如我们设定了一个const对象。那么根据const的定义,这个对象的内容是无论如何都不能被改变的吧。如果我们忘记了对返回值添加const,很可能就会发生这种乌龙。
class array
{
public:
array() : arr(new int[10])
{
}
int& operator[](size_t pos) const {
return arr[pos];
}
private:
int* arr;
};
int main()
{
const array arr1;
arr1[5] = 10;//对const对象修改程序竟然被通过了!!!这当然不行
}
我们仔细分析一下原因,原来是operator []的返回值是int&,非const引用是可以被修改的,所以造成了这种乌龙。如果程序当中不能被修改的对象都被修改了,会造成什么结果是不可预计的。而且还难以发现(因为你看到const修饰了这个对象,就对其放心了,将注意力转到非const对象身上)。
于是解决方案就是在返回值前面加上const,以保证这个函数的返回值不被修改,也就保护了const对象的常性(constness)。
const修饰函数参数
const修饰函数参数的作用是什么,首先我们先来了解一个概念。c++的函数的返回值和参数都有两种传递方式,一是值传递,二是引用传递(指针传递)
- 值传递(pass by value),值传递的方式是函数形参拷贝调用实参。也就是说明形参在函数内部发生的任何变化都不会影响实参,因此函数形参无论具不具备常性(constness),都可以接收实参。所以值传递的函数不在我们的考虑范围。(因为形参、返回值是不是const都不会对对象造成任何影响,也就无所谓的权限放大问题了)
- 引用传递(pass by reference),一个具有常性的参数或返回值会导致调用实参的权限发生变化,因此、use const whenever possible这个概念在此处是应用最广泛的,也是本博客的重点。
- 指针传递(pass by pointer),指针传递和引用传递的性质是一样的,因为c++当中的引用实际上是指针的语法糖,因此无论是引用传递还是指针传递,其底层原理都是一致。
如果你了解了拷贝构造函数,就会明白在类类型的成员函数中,引用传递比值传递的效率要高了不少(由于这不是本篇的重点,所以博主就不加以概述了),我们只需要了解一件事,在有关对象的函数中,引用传递的使用率比值传递的使用率高了好几十倍。
那么当函数形参是引用类型时,const对象和非const对象作为调用实参会有不同的效果。我们回顾一下开篇提到的关于c++权限放大的问题。如果函数形参是非const类型的引用,那么根据权限不能放大这个概念,const对象是不能作为调用参数进行传递的。而当函数形参是const的引用类型时,根据权限可以缩小这个概念,无论是const对象还是非const对象,都能调用这个函数。但是请你先确定这个函数不会修改参数的数据。
class array
{
public:
array() : arr(new int[10]), _size(10)
{
}
array& operator=(array& copyarr) const {
memcpy(arr, copyarr.arr, _size * sizeof(int));
}
private:
int* arr;
int _size;
};
int main()
{
const array arr1;
array arr2;
arr2 = arr1;
//error,原因是operator =函数声明的形参是非const类型的引用,
//因此不能将const对象作为参数上传。
//但是该函数的原理是拷贝赋值,const对象也能被拷贝(这不对const对象的数据进行修改),不符合我们的设计逻辑,
//因此更好的方案是将copyarr加上const修饰。
}
因此我们在设计函数时,首先要明白这个函数能不能将const对象作为调用参数,如果可以,那么请将函数形参加上const修饰。前提是你确定这个函数设计的初心就是不对const对象造成修改。
关于const修饰成员对象,可以在本专栏的上一篇博客中看到,因此博主不再赘述。