C++中常量引用指针常量指针指针常量常量引用顶层常量与底层常量

Posted 呆呆象呆呆

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++中常量引用指针常量指针指针常量常量引用顶层常量与底层常量相关的知识,希望对你有一定的参考价值。

主要问题

  • 哪个是指针不能变?
  • 哪个是指针指向的对象不能变?

常量

常量constconstant的缩写,本意是不变的,不易改变的意思。const在C++中是用来修饰内置类型变量,自定义对象,成员函数,返回值,函数参数。

加了const修饰的变量,一经定义值永远不能改变。通常我们用在一些定值的声明定义上,比如某缓冲区的大小、某个最大值最小值等等。

const float MaxLenth = 100.0f;

如果你试图在声明定义以外的范围内修改该变量的值,是不被允许的,通常编译器就会检查出错误。

举例如下

在const变量没有赋值的时候可以编译通过

有赋值的时候会报错

众所周知,变量在声明时可以不赋初值。而由于const的特殊性,被const修饰的变量必须要进行初始化。否则编译器也会报错。

const的初始化,可以根据所赋值形式,分为运行时初始化编译时初始化

const int i = get_size(); // 运行时初始化
const int j = 66;         // 编译时初始化

const修饰的变量,除了不能被赋值外,其他的使用操作与正常变量无异。

引用

先初步了解引用、指针的一些注意事项:

  • 引用并非对象
  • 引用必须初始化
  • 引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起
  • 不允许随意改变引用所绑定的对象
  • 类型要严格匹配
//例子1
int &a = 10;             //错误:引用类型的初始值必须是一个对象  
//例子2
double a = 3.14;  
int &b = a;              //错误:此处引用类型的初始值必须是int型对象

指针

指针本身就是对象

指针的类型要和它指向的对象严格匹配

double dval;  
double *pd = &dval;      //正确  
double *pd2 = pd;        //正确  
  
int *pi = pd;            //错误:指针pi的类型和pd的类型不匹配  
pi = &dval;              //错误:试图把double型对象的地址赋给int型指针  

有例外:引入const 限定符

空指针、野指针

既然讲到了指针,那顺便说一下空指针、野指针的问题。

空指针就是保存地址为空的指针,使用指针时必须先判断是否空指针,很多问题都是这一步导致的。

野指针是在delete掉指针之后,没有置0,导致指针随意指向了一个内存地址,如果继续使用,会造成不可预知的内存错误。

另外指针的误用很容易造成BUG或者内存泄漏。

看代码:

//-------空指针-------//
int *p4 = NULL;
//printf("%d",*p4); //运行Error,使用指针时必须先判断是否空指针
  
//-------野指针(悬浮、迷途指针)-------//
int *p5 = new int(5);
delete p5;
p5 = NULL; //一定要有这一步
printf("%d",*p5);  //隐藏bug,delete掉指针后一定要置0,不然指针指向位置不可控,运行中可导致系统挂掉
  
//-------指针的内存泄漏-------//
int *p6 = new int(6);
p6 = new int(7); //p6原本指向的那块内存尚未释放,结果p6又指向了别处,原来new的内存无法访问,也无法delete了,造成memory leak

常量与指针

常量指针(底层)

定义: 又叫常指针,可以理解为告知指针和编译器指针指向的是常量对象。const修饰的为所申明的类型不可变。

注意点:

  • 常量指针指向的对象不能通过这个指针来修改,可是仍然可以通过原来的声明修改
  • 常量指针可以被赋值为非常量变量对象的地址,之所以叫常量指针,是限制了通过这个指针修改变量的值;
  • 指针还可以指向别处,因为指针本身只是个变量,可以指向任意地址

代码形式:

int const* p;
const int* p;

指针常量(顶层)

**定义:**本质是一个常量,告知编译器说明这个常量的值是一个指针。常量表示这个指针的值(即它指向哪里)不能改变,也不可以再除了初始化之外的时候进行赋值,但是指针所指向的位置中的内容可以改变。const修饰的指针。

注意点:

  • 指针常量是个常量,只是类型是某一种数据类型的指针

  • 指针所保存的地址可以改变,然而指针所指向的值却不可以改变

  • 指针常量是常量,指向的地址不可以变化,但是指向的地址所对应的内容可以变化

  • 指针常量必须在声明的同时对其初始化,不允许先声明一个指针常量随后再对其赋值,这和声明一般的常量是一样的

代码形式:

int * const p = &a; 

指向常量的常量指针

**定义:**指向常量的常指针。其是一个常量,且它指向的对象也是一个常量。const同时修饰类型和指针

注意点:

  • 指针常量,指向的是一个常量对象;
  • 指针不可变,且指针指向的对象不能变化;

代码形式:

const int* const p = &a;

区别评析

那如何区分这几类呢? 带两个const的肯定是指向常量的常指针,很容易理解,主要是如何区分常量指针和指针常量:

第一种方式:

是看*const的排列顺序,比如

int const* p;    //const * 即常量指针
const int* p;    //const * 即常量指针
int* const p;    //* const 即指针常量

第二种方式:

是看const离谁近,即从右往左看,比如

int const* p;    //const修饰的是*p,即*p的内容不可通过p改变,但p不是const,p可以修改,*p不可修改;
const int* p;    //同上
int* const p;    //const修饰的是p,p是指针,p指向的地址不能修改,p不能修改,但*p可以修改;

实例

//-------常量指针-------
const int *p1 = &a; //可以不被赋初值
a = 300;     //OK,仍然可以通过原来的声明修改值,
//*p1 = 56;  //Error,*p1是const int的,不可修改,即常量指针不可修改其指向地址
p1 = &b;     //OK,指针还可以指向别处,因为指针只是个变量,可以随意指向;

//-------指针常量-------//
int*  const p2 = &a;
a = 500;     //OK,仍然可以通过原来的声明修改值,
*p2 = 400;   //OK,指针是常量,指向的地址不可以变化,但是指向的地址所对应的内容可以变化
//p2 = &b;     //Error,因为p2是const 指针,因此不能改变p2指向的内容

//-------指向常量的常量指针-------//
const int* const p3 = &a;
//*p3 = 1;    //Error
//p3 = &b;    //Error
a = 5000;    //OK,仍然可以通过原来的声明修改值

在实际应用中,常量指针要比指针常量用的多,比如常量指针经常用在函数传参中,以避免函数内部修改内容。

size_t strlen(const char* src); //常量指针,src的值不可通过传递进去的指针所改变;
char a[] = "hello";
char b[] = "world";
size_t a1 = strlen(a);
size_t b1 = strlen(b);

虽然a、b是可以修改的,但是可以保证在strlen函数内部不会修改a、b的内容。

常量与引用

常量引用

常量引用其实是对常量对象的引用的简称。顾名思义,它把它所指向的对象看作是常量(但是该对象本身不一定是常量),因此不可以通过该引用来修改它所指向的对象的值。

注意点:

  • 指向常量对象时,一定要使用常量引用,而不能是一般的引用
  • 常量引用可以指向一个非常量对象。与普通引用不同的是,针对常量引用(即对const的引用),不允许通过该引用修改这个非常量对象的值
  • 普通引用的类型必须和所引用的类型严格匹配,且不能与字面值或者某个表达式的计算结果绑定在一起,但是 “常量引用” 是例外(只要被引用的类型能够转换为常量引用的类型)
  • 在函数参数中,使用常量引用非常重要。因为函数有可能接受临时对象,而且同时需要禁止对所引用对象的一切修改

针对注意点1:

因为不允许直接为常量赋值,当然也就不能通过引用去改变常量。因此直接规定当引用一个常量时,必须使用常量引用

在没有做这样的规定的时候,编译器自己也不知道引用的对象是不是可以更改,所以需要告知这个对象不可被改动。

const int ci = 1024;
const int &r1 = ci;         // 正确:使用常量引用对常量对象进行引用
r1 = 42;                    // 错误:r1是对常量的引用,不能通过它修改所绑定的对象
int &r2 = ci;               // 错误:非常量引用不能绑定常量对象

针对注意点2:

const引用虽然不可以改变被引用变量的值,但如果被引用变量不是const常量,被引用变量的值是可以修改的,且引用拿到的值也将是被引用变量修改后的值。

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

int i = 42;
int &r1 = i;                // 普通引用可以指向非常量对象 i
const int &r2 = i;          // 常量引用可以绑定非常量对象 i
r1 = 0;                     // 正确,r1绑定的对象是普通变量
r2 = 0;                     // 错误:r2是一个常量引用,不可以通过这个引用来更改所引用对象的值

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

并且在改动后还是正常的可以被使用

int j = 6;
const int &rj = j;
cout<< "rj"<<rj<<endl;    // 输出 6
j = 10;
cout<<"rj"<<rj<<endl;    // 输出 10

针对注意点3:

和普通引用不同,允许为一个常量引用绑定非常量的对象、字面值,甚至是一个一般表达式:

int &ri = 6;   // 错误!
const &ri = 6;  // 正确!

int i = 42;
const int &r1 = i;          // 正确:常量引用可以绑定非常量对象
const int &r2 = 42;         // 正确:常量引用可以绑定字面值
const int &r3 = r1 * 2;     // 正确:常量引用可以绑定表达式
int &r4 = r1 * 2;           // 错误:普通引用不可以绑定表达式

引用初始值不可以是一个类型不同的变量,但 const 引用可以。下面的操作也是允许的:

double dval = 3.14;
const int &r1 = dval;

编译器实际上相当于执行了下列语句:引用和原 dval 已经不是同一个地址了:

const int temp = dval;      // 生成一个临时的整型常量
const int &r1 = temp;       // 让 r1 绑定这个临时量

在这些情况下,常量引用实际上是绑定了一个临时量(temporary)对象。也就是说,允许常量引用指向一个临时量对象。

r1不是常量引用时,如果执行了类似于上面的初始化操作会带来什么样的后果?如果r1不是常量引用,就允许对r1赋值,这样就会改变r1所引用的对象的值。注意,此时绑定的对象是一个临时量而非dval程序员既然想让r1引用dval,就肯定想通过r1改变dval的值,否则干什么要给r1赋值呢?如此看来,既然大家基本上不会想着把引用绑定到临时量上,C++语言也就把这种行为归为非法。

也就是说,不允许一个普通引用字面值或者某个表达式的计算结果,或类型不匹配的对象绑定在一起,其实就是不允许一个普通引用指向一个临时变量,只允许将常量引用指向临时对象。

针对注意点4:

下面程序执行发生错误,因为不可以将一个字面值常量赋值给普通引用函数的返回值如果是非引用类型时,实际上是作为一个临时变量返回的,经过上面的讨论,不允许一个普通引用指向临时对象

int test() 
	return 1;


void fun(int &x) 
    cout << x << endl;


int main()

	int m = 1;
	fun(m);         // ok
	fun(1);         // error  字面值在试图绑定非常量的引用,非法
    fun(test());    // error  临时对象在试图绑定非常亮的引用,非法
    return 0;

按下面修改后,fun()函数无论是接受字面值常量作为参数,还是将函数的返回值作为参数均可:

int test() 
	return 1;


void fun(const int &x) 
    cout << x << endl;


int main()

    fun(1);         // ok
	fun(test());    // ok
    return 1;

分析讨论

前几个比较容易理解,因为引用本身的定义,就是变量别名,也可以说是和被引用变量使用一块内存地址,只是在内存地址上起了一个新的名字。

而字面值常量并不被一个明确的变量拥有,可以认为是一个内存中的临时变量所拥有,而对该临时变量的操作显然不符合人们的使用习惯和思维。所以,对一个未知的临时变量操作是非法的。

常量引用可以绑定字面值的讨论:

float f = 1.0f;
const int temp = f;
const int &rf = temp;

同样的,因为存在变量的类型转换,编译器也同样生成了一个 temp 临时变量。

#include<iostream>
using namespace std;
int main()
    int a = 100; 
    const int &ri1 = a;
    const int &ri2 = 6;
    float f = 1.0f;
    const int &rf = f;
    cout << "a   address:"<<&a<<endl;
    cout << "ri1 address:"<<&ri1<<endl;
    cout << "ri2 address:"<<&ri2<<endl;
    cout << "f   address:"<<&f<<endl;
    cout << "rf  address:"<<&rf<<endl;
    return 0;

通过打印地址,也会发现,ri1a的地址是一样的,但是rff的地址是不一样的,也就是说并非引用了f,而是引用了临时变量

顶层与底层const

说明

顶层const可以表示修饰的任意对象是常量,对任何数据类型都适用

底层const则与指针和引用等复合类型有关,可以理解为const的是针对此种复合类型例如指针的修饰

比较特殊的是,指针类型既可以是顶层const,也可以是底层const或者二者兼备

拷贝中的顶层和底层const

int i = 0;
int *const p1 = &i;         //  顶层const 属于指针常量 p1这个指针不能改变,但是可以通过
const int ci = 42;          //  顶层const 属于常量    ci这个常量不能改变
const int *p2 = &ci;        //  底层const 属于常量指针 p2这个指针可以改变,但是不能通过指针改变所绑定的地址中的内容
const int *const p3 = p2;   //  靠右的 const 是顶层 const,靠左的是底层 const
const int &r = ci;          //  所有的引用本身都已经是顶层 const,因为引用一旦初始化就不能再改为其他对象的引用,这里用于声明引用的 const 都是底层 const

当执行对象的拷贝操作时,常量是顶层const还是底层const的区别明显。

顶层const不受什么影响。

i = ci;     //  正确:拷贝 ci 的值给 i,ci 是一个顶层 const,对此操作无影响。常量可以用于给非常量赋值
p2 = p3;    //  正确:p2 和 p3 指向的对象相同,p3 顶层 const 的部分不影响。

底层const的限制却不能被忽视。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换,一般来说,非常量可以转化为常量,反之不行。

int *p = p3;        //  错误:p3 包含底层 const 的定义,而p没有。假设成功,p 就可以改变 p3 指向的对象的值。
p2 = p3;            //  正确:p2 和 p3 都是底层 const
p2 = &i;            //  正确:int* 能够转化为 const int*,这也是形参是底层const的函数形参传递外部非 const 指针的基础。
int &r = ci;        //  错误:普通 int& 不能绑定到 int 常量中。
const int &r2 = i;  //  正确:const int& 可以绑定到一个普通 int 上。

函数重载、形参中的顶层和底层const

顶层const不影响传入函数的对象,一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开:

Record lookup(Phone);
Record lookup(const Phone);         //重复声明了Record lookup(Phone)

Record lookup(Phone*);
Record lookup(Phone* const);        //该const是顶层,重复声明了Record lookup(Phone* const)

另一方面,如果形参是某种类型的指针或引用,则通过区分其是否指向的是常量对象还是非常量对象可以实现函数重载。此时的const是底层的。

Record lookup(Phone&);
Record lookup(const Phone&);        //正确,底层const实现了函数的重载

Record lookup(Phone*);
Record lookup(const Phone*);        //正确,底层const实现了函数的重载

函数重载与强制类型转换

以下节选自《Effective C++》,假设要重载一个函数,而重载函数的区别仅在于函数形参一个是底层const一个是非底层的,那么最佳的做法是采用代码的复用,减少这两个重载函数实现过程的相同代码段,可以通过const_cast来实现这一做法,在非const函数中调用const。

class Textbook

public:
    ...
    const char& operator[](std::size_t position) const   
    
        ...
        return text[position];
    
    char& operator[](std::size_t position)
    
        //这里首先将op[]的返回值的const去除,因为const char&不能自动转换为char&,除此之外,由于调用以上const函数的必须是const对象,因此将调用此函数的对象(即*this)强制转化为一个(底层)const对象,再调用const op[]。
        return
            const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
    
;

相关问题:

为什么不能在一个常量对象中调用非常成员函数?

因为在默认情况下,this的类型是指向类的非常量版本的常量指针(意思是this的值不能改变,永远指向那个对象,即“常量指针”,但是被this指向的对象本身是可以改变的,因为是非常量版本,这里this相当于是顶层const),而this尽管是隐式的,它仍然需要遵循初始化规则,普通成员函数的隐式参数之一是一个底层非const指针,在默认情况下我们无法把一个底层const的this指针转化为非const的this指针,因此我们不能在常量对象上调用普通的成员函数。因此在上例中,形参列表后的const就意味着默认this指针应该是一个底层const, 类型是 const ClassName&。而非常对象却可以调用常成员函数,因为底层非const可以默认转化为底层const。

| | |

参考文献

C语言——常量指针、指针常量以及指向常量的指针常量三者区别详解_望崖的博客-CSDN博客_常量指针和指针常量的区别

【C++基础之二】常量指针和指针常量_偶尔e网事的博客-CSDN博客_指针常量

C++ 引用 | 菜鸟教程

以上是关于C++中常量引用指针常量指针指针常量常量引用顶层常量与底层常量的主要内容,如果未能解决你的问题,请参考以下文章

C++中常量引用指针常量指针指针常量常量引用顶层常量与底层常量

C++ - 返回指针或常量引用

指针和引用指针常量与常量指针

五万字读懂c++

五万字读懂c++

指针和引用与及指针常量和常量指针