C++之const关键字详解

47 篇文章 4 订阅

const变量

它的值不能被改变,只读的变量。const对象一旦创建后其值就不能再改变,所以const对象必须初始化。初始值可以是任意复杂的表达式:

const int bufSize = get_size();   //正确:运行时初始化

const int size = 42;           //正确:运行时初始化

const int size;                //错误:size是一个未经初始化的常量

在不改变const对象的操作中还有一种初始化,如果利用一个对象去初始化另外一个对象,则它们是不是const都无关紧要:

int i = 42;        

const int ci = i;  //正确:i的值被拷贝给了ci

int j = ci;        //正确:ci的值被拷贝给了j

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

const int bufSize = 512; 

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

const变量可以用来定义数组,如:

const int size = 10;
int array[size] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

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

在一个文件中定义而在多个文件中使用的解决方式是:对于const变量不管是声明还是定义都添加extern关键字,这样只需定义一次就可以了:

//xxx.cpp定义并初始化了一个常量,该常量能被其他文件访问

extern const int bufSize = fcn();

//xxx.hpp 头文件

extern const int bufSize; //与xxx.cpp中定义的bufSize是同一个.

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

const的引用

可以把引用绑定到const对象上,就像绑定到其他对象上一样,称之为对常量的引用。对常量的引用不能被用作修改它所绑定的对象。

允许为一个常量引用绑定非常量的对象,字面值,甚至是一个表达式:

int i = 42;

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

在这里为什么r3是合法的,而r4是不合法的呢?理解这种情况最简单的办法是弄清楚当一个常量引用被绑定到另外一种类型上是到底发生了什么:

double dValue = 3.14;
const int& ri = dval;

此处ri引用了一个int类型的数。对ri的操作应该是整数运算,但dValue确实一个double类型。因此为了确保让ri绑定一个整数,编译器把上述代码变成了如下形式:

const int temp = dValue; //由double生成一个临时的整型变量
const int& ri = temp;    //让ri绑定这个临时变量

在这种情况下,ri绑定到了一个临时变量对象。那么当ri不是常量引用时,就允许对ri赋值,这样就会改变ri所引用对象的值。注意,此时绑定的对象是一个临时变量而非dValue。程序员既然让ri引用dValue,就肯定想通过ri改变dValue的值。否则干么给ri赋值呢?如此看来,程序员既然不会想着把引用绑定到临时变量上修改其值,C++语言也就把这种行为归为非法。

指针和const

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

int i = 0;

int *const cptr = &i;   //cptr将一直也只能指向i,不能指向别的int变量
const int* c_ptr = &i; //不能通过c_ptr修改i的值,c_ptr可以指向别的int变量
const int* const ccptr = &i; //cptr将一直也只能指向i,不能通过c_ptr修改i的值

通过指针修改const局部变量内存中的值

const在C和C++中的编译器处理时区别的,

C编译器会给该常量分配内存空间,此后我们可以取它的地址然后间接修改它的值。
C++不给他分配内存空间,只是把它保存在符号表中,此后用到它时都只从符号表中读取然后替换值而不会涉及内存的存取,因此效率高!如果对该局部const变量使用了取地址符&,会给它分配内存,但是之后仍然从符号表中读取,而不管这个内存地址中的东西怎么变了。全局const变量是不能被修改的,因为它被分配到了全局只读数据区。

#include <iostream>

int main()
{
    const int ci = 10;   //ci是局部const变量存储在stack中
    int* ptr = (int*)(&ci);

    std::cout << &ci << std::endl;
    std::cout << ptr << std::endl;
    std::cout << ci << std::endl;
    std::cout << *ptr << std::endl;

    *ptr = 11;
    std::cout << &ci << std::endl;
    std::cout << ptr << std::endl;
    std::cout << ci << std::endl;
    std::cout << *ptr << std::endl;

    return 0;
}

运行结果如下:

 在这里局部变量ci地址被分配到了栈上,虽然ptr也是的是ci的内存地址,但是通过ptr修改了值后ci的值为什么仍然为10呢?因为ci是const变量,编译器将在编译过程中把用到ci变量的地方都已经替换成对应的值了。也就是说,编译器会找到代码中所有用到ci的地方,然后10替换。程序运行后虽然修改了ci变量所在内存中的值,但是不影响在编译时已经替换掉的值。

不能通过指针修改全局const变量内存中的值

#include <iostream>

const int ci = 10;

int main()
{

    int* ptr = (int*)(&ci);

    std::cout << &ci << std::endl;
    std::cout << ptr << std::endl;
    std::cout << ci << std::endl;
    std::cout << *ptr << std::endl;

    *ptr = 11;
    std::cout << &ci << std::endl;
    std::cout << ptr << std::endl;
    std::cout << ci << std::endl;
    std::cout << *ptr << std::endl;

    return 0;
}

程序运行到*ptr = 11是发生了段错误。

全局cosnt变量ci被存储在只读的静态存储区。其变量所在地址只有只读属性,不可修改。

const修饰类对象时:

  const修饰类对象时,其对象中的任何成员都不能被修改。const修饰的对象,该对象的任何非const成员函数都不能调用该对象,因为任何非const成员函数都会有修改成员变量的可能。

#include <iostream>
#include <string>

class Test
{
public:
    int getScore()    //非const函数
    {
        return score;
    }
    const std::string& getName() const   //const函数
    {
        return name;
    }

private:
    int score{100};
    std::string name{"c++"};
};

int main()
{
    Test t;          //非cosnt对象
    const Test ct;   //const对象

    std::cout << "score = " << t.getScore() << std::endl; //非cosnt对象可以调用非const函数
    std::cout << "name = " << t.getName() << std::endl;   //非cosnt对象可以调用const函数

    //std::cout << "score = " << ct.getScore() << std::endl;//const对象不能调用非cosnt函数
    std::cout << "name = " << ct.getName() << std::endl;   //cosnt对象可以调用const函数

    return 0;
}

const修饰成员变量:

  const修饰的成员变量不能被修改,同时只能在初始化列表中被初始化,因为常量只能被初始化,不能被赋值;

  赋值是使用新值覆盖旧值构造函数是先为其开辟空间然后为其赋值,不是初始化;而初始化列表开辟空间和初始化是同时完成的,直接给与一个值,所以const成员变量一定要在初始化列表中完成。

#include <iostream>

class Test
{
public:
    Test():id{0}, age{0}
    {
    }

    Test(int id, int age):id{id}
    {
        //this-id = id;  //error
        this->age = age;
    }

    void setAge(int age)
    {
        this->age = age;
    }

    int getAge() const
    {
        return age;
    }

    int getId() const
    {
        return id;
    }
    /*
    void setId(int id)
    {
        this->id = id;
    }*/

private:
    const int id;
    int age;
};

int main()
{
    Test t;
    t.setAge(28);
    std::cout << "age = " << t.getAge() << std::endl;
    std::cout << "id = " << t.getId() << std::endl;

    return 0;
}

const修饰类的成员函数

  const成员函数表示该成员函数不能修改类对象的成员变量。但是const函数可以修改mutable修饰的成员变量。

#include <iostream>

class Test
{
public:
    Test():i{1}, ci{2}, mi{3}
    {
    }

    void print() const
    {
        std::cout << "i = " << i << ", ci = " << ci << ", mi = " << mi << std::endl;
    }

    void process() const
    {
        //i = 100;   //const函数不能修改普通成员变量
        //ci = 10;   //const函数不能修改const成员变量
        mi = 200;    //const函数可以修改mutable修饰的成员变量
    }

private:
    int i;
    const int ci;
    mutable int mi;
};

int main()
{
    Test t;
    t.process();

    return 0;
}
  • c++顶层const和底层const:

使得被修饰的变量本身无法改变的const是顶层const,其他的通过指针或引用等间接途径来限制目标内容不可变的const是底层const
顶层const表示修饰的对象本身是一个常量,如const int a=10(a是一个常量);int *const ptr(指针ptr本身是一个常量)。如果对象本身不是常量,则就是底层const。顶层=对象本身被定义为常量。

  • const函数对int *p变量是顶层const:

对于类中的指针类型的成员变量,在const函数中可以通过这个指针来修改所指向成员变量的值,但是不能在const函数中让该指针指向其他成员变量。也就是说,在类中,指针类型成员变量在const函数中是顶层const(顶层const表示修饰的对象本身是一个常量)。引用成员边浪也可以在const函数中修改值。

我们使用一个例子来测试:

#include <iostream>
using namespace std;
 
class Test
{
public:
    Test(int& y) :x{ 10 }, y{ y }, z{ 30 }
    {
        p = &x; 
        std::cout << "x=" << x << ", y=" << y << ", z=" << z << ", *p=" << *p << std::endl;
    }
 
    void someSet(int value) const//const函数不能修改没有被mutable修饰的成员变量
    {
        *p = value;
        //x = value;//x不能在const函数中修改,在visual studio 2019中error信息如下:
                  //Error (active)	E0137	expression must be a modifiable lvalue
                  //Error	C3490	 'x' cannot be modified because it is being accessed through a const object
        z = value;
        //p = &y;//const函数中,p只能指向成员变量x,不能指向成员变量y,否则编译错误如下:
                //Error (active)	E0137	expression must be a modifiable lvalue
                //Error	C2440	 '=': cannot convert from 'const int *' to 'int *const '
        y = y + value; //通过引用初始化的引用成员y也可以在const函数中修改
    }
    void display() const
    { 
        std::cout << "x=" << x << ", y = " << y << ", z=" << z << std::endl; 
    }
 
private:
    int x;
    int &y;//引用成员变量:需要在构造函数中通过引用初始化
    mutable int z;
    int* p;
};
 
int main() {
    int y = 20;
    Test t(y);
    int max = 12;
    for (int i = 11; i <= max; ++i)
    {
        t.someSet(i);
        t.display();
    }
 
    return 0;
}

 结果如下:

const关键字对C++成员函数的修饰

const对C++成员函数的修饰分为三种:1. 修饰参数;2. 修饰返回值;3. 修饰this指针。

1. 对函数参数的修饰。

  1)const只能用来修饰输入参数。输出型参数不能用const来修饰。

  2)如果输入参数采用“指针传递”,那么加const修饰可以防止意外地改动该指针,起到保护作用。

  3)如果输入参数采用“值传递”,函数将产生临时变量(局部变量),复制该参数的值并且压入函数栈。函数中使用该参数时,访问的是函数栈中临时变量的值,原变量无需保护,所以不要加const修饰。

  4)基本变量类型的参数作为“值传递”的输入参数,无需采用引用。自定义变量类型(class类型,struct类型)参数作为“值传递”的输入参数,最好采用"const+引用"格式,即 void func(const A& a)。原因是自定义变量类型作为值传递时,设计创建临时变量,构造,复制,析构,这些过程很消耗时间。

  从函数栈的基本原理考虑原因。我们知道,函数在被调用时,会为创建各个实参创建临时变量并将其压入函数栈。如果是基本变量类型,则压入函数栈的临时变量中存储的是实参的副本;如果是自定义变量类型,则会在堆上创建该类型实例,复制该实参到堆上,然后将堆上该实例的地址压入函数栈;如果是指针,则会将指针地址的副本(其实也可以认为这个保存这个指针的变量是基本变量类型)压入函数栈。

  也就是说,函数栈上要么保存的是一个基本类型参数的副本,要么是个顶层指针。对于函数栈上保存的参数,实参的副本可以作为一个普通的局部变量,是可以修改值的,而对于指针变量,其可以视为顶层指针,本身的值不可以修改,但其指向的值可以修改。

  故而可知,对于基本变量类型,函数内部操作的是函数栈上的副本,不会对原值产生影响,对于类类型(非指针输入性参数),操作的也是函数栈上的地址指向的实例副本,同样不会对原值产生影响;而对于指针,函数内部虽然改变不了指针变量保存的指针值(该指针为顶层指针),但该指针却指向的是原值的地址,故而能修改原值。

  对于引用,a)引用只是变量的一个别名,引用指向元变量内存地址,不会进行新的内存分配和变量的拷贝;b)引用声明后必须马上初始化;c)引用一经定义,不能改变其值,也就是不能再作为其它变量的引用; d)通过引用可以完全操作原变量。

  可以看出,当占空间很大的变量作为输入型实参时,很适合用引用传递。因为用引用传递时,只是传递变量本身的一个别名,不会进行新变量的内存分配,构造,赋值,析构等操作。

  如果函数中不允许改变该实参,那么就应该在引用参数上加const修饰。

  基于上述考虑。const修饰输入型参数时,只需要修饰指针类型和引用类型即可(虽然不是强制,但对于输入型指针或者引用参数用指针修饰应该成为一种习惯)。

  同时,这也说明一个编程时应该养成的习惯,对于输入型参数,应该在函数起始位置定义一个局部变量接收该输入型参数,而不是直接使用。

 2.  对返回值的修饰。

  这个应用比较少。大部分返回值采用的时“值传递”。如果将返回值修饰为const,那么接收返回值的变量也必须定义为const。

 3. 对this指针的修饰。

  我们知道,c++成员函数在编译时候,会传入一个this指针,指向实例本身。这个this指针默认实际上是个顶层指针。即如果有classA,那么这个指针其实类似如下的定义:

  classA * const this;

  即this指针指向实例本身并且不可以修改,但可以通过this指针修改其指向的成员变量。在成员函数内访问成员变量m_var,实际上时如下形式方位的:

  this.m_var;

  如果我们设计一个成员函数时,不想让其修改成员变量,那么就应该将this指针定义为底层指针。c++定义的方式就是在函数签名后面加上const,即

  void func(const A& a, int b, const int* c, int* d)const;

  显然,上述成员函数中,a为const引用传递,不可以改变原值;b为值传递;c为const指针传递,不可改变原值;d为输出参数,可以改变原值。而该函数为const成员函数,不可以修改成员变量值。

  以下是const成员函数注意的几点

  1)const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数.即对于class A,有

  const A a;

  那么a只能访问A的const成员函数。而对于:

  A b;

  b可以访问任何成员函数。

  2)const对象的成员变量不可以修改。

  3)mutable修饰的成员变量,在任何情况下都可以修改。也就是说,const成员函数也可以修改mutable修饰的成员变量。c++很shit的地方就是mutable和friendly这样的特性,很乱。

  4)const成员函数可以访问const成员变量和非const成员变量,但不能修改任何变量。检查发生在编译时。

  5)非const成员函数可以访问非const对象的非const数据成员、const数据成员,但不可以访问const对象的任意数据成员。

  6)const成员函数只是用于非静态成员函数,不能用于静态成员函数。

  7)const成员函数的const修饰不仅在函数声明中要加(包括内联函数),在类外定义出也要加。

  8)作为一种良好的编程风格,在声明一个成员函数时,若该成员函数并不对数据成员进行修改操作,应尽可能将该成员函数声明为const 成员函数。

const常量与define宏定义的区别:

  1)处理阶段不同:

  define是在预处理阶段,define常量从未被编译器看见,因为在预处理阶段就已经替换了;

  const常量在编译的阶段通过其值来替换const变量在文件中出现的地方。

  2)类型和安全检查不同

  define没有类型,不做任何检查,仅仅是字符替换,没有类型安全检查,并且在字符替换时可能会产生意料不到的错误

       define N 2 + 3

        int a = N / 2; // 预想结果应该是2, 但结果却是3,因为被替换为了int a = 2 + 3/2;

  const常量有明确的类型,在编译阶段会进行类型检查;

        const int bufSize = 512;

  3)存储方式不同

  define是字符替换,有多少地方使用,就会替换多少次,不会分配内存;

  编译器通常不会为const常量分配空间,只是将它们保存在符号表内,使他们成为一个编译期间的一个常量,没有读取内存的操作,效率也很高;如果有指针指向const变量,也就是使用了&,会给它分配个内存,但是之后仍然从符号表中读取!!!而不管这个内存地址中的东西怎么变了。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值