C++ OOP--const、引用、指针

继续梳理C++中的问题, 今天梳理的是最基础的几个概念,也是比较麻烦的概念。

引用

引用是一个变量的别名,声明时必须初始化,而且之能在初始化的时候绑定一个变量对象。

  1. 引用不能绑定在引用上,因为引用本身不是对象。
  2. 普通引用不能绑定到立即数上,因为立即数不是对象,但是常量引用是可以的。
  3. 普通引用不能引用类型不同的对象。

以上的普通引用是相对于常量引用来说的,下面会介绍到。

指针

这个概念最简单,他不是是一种数据类型,他的数值是个地址,可以用解地址符*来直接操作指向的地址内的变量。

指针有下面四种状态:

  1. 指向一个对象;
  2. 指向紧邻对象的下一个位置;
  3. 空指针(nullptr, 避免使用NULL,nullptr可以被转为任意类型的指针);
  4. 其他指针装态(无意义的状态);

const 常量

const 修饰的常量,默认是文件内有效,如果想在多文件内可用,就加extern修饰。

这个和其他的东西一结合就比较烦。

首先const是声明常量的作用,用其修饰的变量值必须初始化且不可更改。这就是普通常量了,很好理解。

const 引用

如果你声明了一个引用,并且这个引用是const修饰的,那么,你要知道两个事情:第一,引用不能绑定到其他变量了;第二,用这个引用不能改变做引用的对象的值了,但是不影响那个变量通过其他途径改变,改变后的值一样会在引用中表现出来。

如果你引用的对象是个常量,那么这个引用也必须是常量。反过来没有这个限制。

const 指针

指针这块又有两种,因为指针是可以改变其指向的对象的,并且其指向的对象也是可以更改的。就出现了指针常量(pointer to const)常量指针(const pointer)。汉字理解起来没有英文来的准确,所以下面不用常量指针和指针常量来表述。

pointer to const 很明显是指向的对象是个常量,而指针不能够修改这个对象的值。

const pointer 很明显就是这个指针是个常量,只能指向这个固定的内存,但是可以通过其修改对象的值。

在定义上如下:

1
2
3
4
5
6
7
8
int a;

// pointer to const
const int *p1 = &a;
// 等同于 int const *p = &a;

// const pointer
int *const p2 = &a;

所以可以看出来,const修饰的就是紧跟其后的内容,在pointer to const的定义中可以认为*p 指代指向的对象;而const pointerconst修饰的是p;

const 函数

以上是const的基础知识,在下面的讨论中依然有效。

const参数

const的参数无非就是希望函数不要改变这个参数的值,在传值的时候没有什么意义,但是如果传过来的是个引用或是指针就很有必要考虑是否允许函数修改值的问题。

函数在使用实参初始化形参列表的时候,会忽略顶层const(顶层const指等号左边对象不可更改的const修饰符,底层const指等号右边的对象不更改的const修饰符即pointer to const)。也就是说在传递参数的时候我们可以使用非常量传递给常量参数。

在传常量引用的时候自然就是不希望通过这个引用修改值。尤其是当一个参数是引用的时候,如果要传立即数就必须是const修饰的,否则编译不会通过。
一般建议尽量使用常量引用,只要能确定函数不会改变该参数的值就可以声明为常量引用。

顺带提一下数组传参的注意事项。首先就是肯定是传指针或引用,数组不允许拷贝传参。指针就不说了,如果不能改变值就用const修饰,否则就声明成正常的类型。要说一下的就是引用。
对于数组的引用传参来说,维度也是参数的一部分,正确的声明方式如下:

1
void func(int (&a)[100]){...}

注意括号,一定是这样声明,因为你引用的是a这一个数组,而不是一个引用数组a[100]。

传递二维数组,数组本质上没有维度之分,二维数组就是第一维存放指针,第二维是正常的数组。所以在声明参数的时候第一维不必管,但是第二维及以后的维度要注明长度。

1
void func(int (*a)[][10]){...}

const 修饰成员函数

在成员函数的参数列表的后面使用const修饰符说明该函数不会更改成员变量。

const 修饰函数返回值

这个今天不想讨论了,之后写一个函数的整理,在其中讨论这个问题比较好。

最后,要多用const。

关于C++修饰符的规则

整理了几天,整理来整理去,发现学习的过程中反反复复的就在纠结几个修饰符。然后有一些自己的思考。

修饰符并不都是一样的,有那么几种类型,比如有修饰类型的、有修饰权限的等。但是无论是哪种修饰符,其要修饰的主体是一样的,就是我们定义的变量、函数、类。
说白了,C++的关键字,其自己没有异议,是什么就是什么。但是我们自己定义的变量就不一样了,我们要赋予他们不同的身份、功能、作用、权限,而这一切都要靠修饰符来明确。

举个例子: const int *p = &a;,这个中有两个或者说有三个修饰符,两个运算符。三个修饰符分别是:constint*,并且其作用对象都是p
但是不能单纯的拆开来分别修饰,因为修饰符的顺序会影响其修饰的结果,总的来说,每个修饰符都是在修饰其以后的代码。
上面这句代码我们其实倒着来读就是他的正确意义:这是一个变量,名字叫p,而且这是一个指针变量,这个指针是指向int类型变量的,且通过其指向的int类型变量是不可变的。

换个例子也是一样的: int * const p = &a;: 这是一个变量,名字叫p,而且这个p的值是不可变的,这个p是一个指针,指向int型变量。

为什么会这样呢?因为编译器需要按照固定的规则来解析这个3个修饰,甚至来说,绝大多数的修饰符都是按照一个规则来解释的,所以在设计修饰符的时候就按这样的规则设计,这个规则就是每个修饰符修饰其后的代码,且修饰的最终对象是我们设的变量名。

上面还有几点其实有意思的。就是这个指针是不是个类型的问题,当然不是了。
*本身就是一个修饰符,也可以是个操作符,但是绝对没有int*这个类型,所以写声明代码的时候就将其当成修饰符来写就好了。同样的还有一个问题,就是引用是不是个类型呢? 当然也不是啦。一样都是修饰符。

Talk is not cheap.