C/C++中的const

9 篇文章 0 订阅

索引


前言

  在C/C++中,常量是固定值或字面量,且在程序执行期间不会改变。常量可以是任何的基本数据类型,比如整数常量、浮点常量、字符常量,或字符串字面值,也有枚举常量;也可以是自定义类型。
  

const关键字

  const关键字,也叫const限定符。用来声明或定义一个常变量。
  语法:

extern const type name;      // 声明
const type name = value;     // 定义
// 说明 若type中不含指针类型,const与type的位置可以互换,即const int与int const是一样的
// 即定义了一个名为name的变量,它的类型为const int,值为value,且不可被修改
// 我们判断一个变量类型的时候,把变量名拿掉,剩下的就是它的类型

  常量与普通变量的区别是,常量的值在定义后不能进行修改,且常量在定义时必须初始化。
  接下来我们探讨一下const在C/C++中的用途,主要分为两部分,其一为const修饰限定变量或对象,其二为const修饰函数。
  

用途一:const关键字限定该变量的内容在程序运行期间不会改变。

代码片段:

const int a = 10;
a = 20;              // 错误 a由const限定,不能给a赋值

  我们知道,通过指针可以修改其指向变量的值,那么通过指针,可以修改由const限定变量的值吗?

const int a = 10;
int * p = (int *)&a;
*p = 20;              // a现在的值是多少???

  执行完*p = 20;之后,a的值是不是被修改为20了呢?
  VC6、VS2013以及gcc 4.4.7上,a的值均保持原值不变,为10。
  const对象a不能修改,尝试直接这么做是编译时错误,且尝试间接这么做(例如通过到非 const 类型的引用或指针修改 const 对象)导致未定义行为。
  再看下面的例子(gcc环境下)

struct {int a; const int b; } s1 = {.b=1}, s2 = {.b=2};
s1 = s2; // 错误: s1 的类型无限定,但它有 const 成员

  综上,我们可以看到,指代 const 限定类型对象的左值表达式,和指代拥有至少一个 const 限定类型成员(包含为聚合体或联合体所递归含有的成员)的结构体或联合体的左值表达式,不是可修改左值。具体而言,它们不可赋值。

A member of a const-qualified structure or union type acquires the qualification of the type it belongs to (both when accessed using the . operator or the -> operator).
cosnt 限定的结构体或联合体类型的成员,同样具有成员所属类型的限定类型。

struct s { int i; const int ci; } s;
// the type of s.i is int, the type of s.ci is const int
const struct s cs;
// the types of cs.i and cs.ci are both const int

  再来看看const修饰数组会怎样:

const int arr[2] = {2, 3}; // 数组名本身即是常量,此处const限定符修饰的是数组成员
arr[0] = 1;                // error arr[0]具有const属性

用途二:const修饰指针或引用

  const与指针结合,是使用最为频繁的,也是使初学者望而却步的知识点之一。
  下面以char * 类型的指针来举例说明:
  我们先来看看const可以放在哪些位置,如下图:
  char * p
  const必须放置在变量名之前,所以,const只能放在编号为1、2或3的位置。在确定p的类型时,先把p拿掉,然后从右向左读,就可以确定其类型。
  

序号组合说明
1const char * p = str定义了一个const char * 类型的指针p,
不能通过p修改str的内容,
但p可以指向其他的char * 或 const char *类型的变量或常量
2char const * p = str由定义常变量的语法可知,同1
3char * const p = str定义了一个char * const类型的变量,
const限定的是变量p,
p不能再指向其他的char * 或const char *的变量或常量
但可以通过p修改str的内容
4const char * const p = str定义了一个const char * const类型变量p,
左起第一个const表示p所指向的是一个常量类型的变量或常量,
第二个const限定了变量p的指向,
表示p只能够指向str,不能再指向别的变量或常量
5char const * const p = str同4

  从这张表不难看出,只要正确分析出了变量p是什么类型的,就可以了解到它可以做哪些操作了。
  
  下面来看一下C++中的引用:

int main(void)
{
    int a = 10;
    int & ref_a = a;          // ok 定义了一个到int类型的引用,并与a关联
    const int & ref_b = a;    // ok 定义了一个到const int类型的引用,并与a关联

    const int b = 20;
    int & ref_c = b;          // error 试图定义一个到int类型的引用,
                              // 并与const int类型的变量b关联
    const int & ref_d = b;    // ok 定义了一个到const int类型的引用,并与b关联

    ref_a = 20;               // ok 通过引用ref_a修改a的值
    ref_b = 20;               // error 虽然ref_b也是a的别名,但ref_b被const限定了
    ref_d = 10;               // error 无论b是否具有const属性,但ref_d被const限定了

    // 那么怎么通过具有const的引用,来修改没有const限定变量的值呢?
    // 我们可以通过const_cast来给相关变量去掉或添加上const属性
    const_cast<int &>(ref_b) = 20;  // ok 去掉ref_b的const约束,并通过ref_b来修改a的值
    const_cast<int &>(ref_d) = 200; // ok 去掉ref_d的const约束,并试图通过ref_d来修改b的值
                                    // 很遗憾,虽然语法正确,但其行为是未定义的。

    return 0;
}

  C++中,被const限定的引用可以指向该类型的普通变量,但普通引用不能指向被const限定的该类型的变量。即可以由更多限定的引用指向更少限定的变量,但不可以由更少限定的引用指向更多限定的变量。

  type a;
  const type b;
  const type & ref_a = a;        // ok
  type & ref_b = b;              // error

用途三:const修饰函数参数

  语法:

return_type function_name(const type * var1, const type * var2);  // C/C++
return_type function_name(const type & var1, const type & var2);  // C++

  const修饰形参列表时,表示不希望该参数在函数内部被修改。C中参数的传递方式有两种,一种是传值,另一种是传地址[其时也是传值,只不过这个值是地址],C++中又引入了第三种方式,即引用方式。在使用const修饰普通类型的形参变量时,由于编译器采取的是传值方式,编译器会复制一个临时变量传递到函数内部,这个临时变量除了值与实参一样外,再没有其他关联,因此,使用const修饰这种类型的形参,虽然语法上没有什么问题,但是没有实际意义。

void foo(int a)
{
    a = 200;
}
// 使用const,表明p为输入参数,且不希望在函数内部修改它的值
void goo(const int * p)
{
    // *p = 200;         // 编译器可以保证p所指内容不被修改,这样写编译器会报错
    int * q = (int *)p;
    *q = 200;            // 如果p所指的实参未被const限定,其值会被修改,否则不会被修改
}

int main(int argc, char ** argv)
{
    const int a = 100;
    int * p = (int *)&a;
    foo(a);
    goo(p);

    return 0;
}

这里写图片描述
  结合这张图,在调用foo(a)时,编译器会生成一个临时变量,我们设其为b,b与a的值相同,地址不同,是两个不同的变量,在foo()内部修改b的值,对实参a没有任何影响。调用goo(p)时,编译器依然会生成一个临时变量,假设为q[与goo()中的局部变量q不同],p与q的值都是实参a的地址,如果实参a没有被const限定,那么goo()中执行*q = 200;后,实参a将被修改为200。但是由于a被const限定了,即使调用了goo(),a的值仍然没有被修改。
  下面来关注一下C++中对自定义类型的传值是怎么处理的。

class A
{
public:
    A() { cout << "A ctor" << endl; }
    A(const A &) { cout << "A copy ctor" << endl; }
    A operator=(const A &) { cout << "A operator = " << endl; }
    ~A() { cout << "A dtor" << endl; }
    // ...
};

void foo(A a)
{
}

void foo(A * a)
{
    cout << "foo &a: " << static_cast<void *>(&a) << endl;
}

void goo(A & a)
{
    cout << "goo &a: " << static_cast<void *>(&a) << endl;
}

int main(int argc, char ** argv)
{
    A a;        // 调用A的构造函数来创建对象a,main结束时调用A的析构函数
    A * p = &a; // 定义指针变量不会调用构造函数
    cout << "&a: " << static_cast<void *>(&a) << endl;
    cout << "&p: " << static_cast<void *>(&p) << endl;
    foo(a);     // 调用A的拷贝构造函数,调用foo()结束时,会调用A的析构函数释放临时对象
    foo(&a);    // 不会调用A的拷贝构造函数
    goo(a);     // 不会调用A的拷贝构造函数

    return 0;
}

运行结果

A ctor
&a: 0x7ffe14a01f16
&p: 0x7ffe14a01f08
A copy ctor
A dtor
foo &a: 0x7ffe14a01ee8
goo &a: 0x7ffe14a01f16
A dtor

  可见,C++中对自定义类型的处理,与内建类型是一致的,即自定义类型的普通参数,也会产生临时对象,如果对象包含的成员信息比较多,那么复制操作将会是一笔可观的开销。而使用引用可以节省这笔额外的开销,但是又面临一个新的问题,引用的对象可能会在函数内部被修改。因此,如果希望尽可能的提高效率,且对象不被修改,需要使用const来修饰该自定义类型的引用。
  示例中,对于指针类型及引用类型的参数列表没有使用const限定符,因此在函数内部,修改参数的内容,会影响到实参,在我们希望参数是输出参数,或既是输入又是输出参数时,不需要加const限定符;如果我们希望实参不在被调函数中修改,或用来说明参数仅仅是输入参数,则需要使用const限定符。
  
  在C/C++中,如果函数参数[指针类型或引用]被const修饰了,则说明这是一个输入参数,且不希望该参数在函数内部被修改。
  
  C99标准以后,函数声明中,const 可出现在用于声明数组类型函数参数的方括号内。它限定数组类型变换到的指针类型。下列二个声明声明同一函数:

void f(double x[const], const double y[const]);      // C89环境下编译错误
void f(double * const x, const double * const y);

用途四:修饰类的成员变量

  由于使用const限定的非外部变量必须初始化,因此,类的成员变量使用const修饰时,必须初始化该变量。可以使用两种方式来初始化const修饰的变量,其一是使用初始化列表,其二是同时将其定义成静态的,可转化成整型类型的变量,并初始化。

class A
{
public:
    A() : a(20) {}     // ok
    // A() { a = 20; } // error
    // ...
private:
    const int a;
};

class B
{
public:
    B() {}                       // ok
    // B() : pdbl(&db) {}        // ok
    // B() {pdbl = &db; }        // ok
    // ...
private:
    const static int b = 100;    // ok
    static const char c = 'a';   // ok
    static const double = 200;   // gcc ok, vs2013 failed,要求必须是整型
    // const static char * p = "hello world"; // failed
    // const string str = "hello";             // gcc failed, vs2013 ok
    const char * p;              // ok
    const string str;            // ok
    const string * pstr;         // ok
    const double * pdbl;         // ok
    double db;
};

  如果类的成员变量需要设置为某类型的常量,可以使用const限定符来修饰,并在初始化列表中将其初始化。
  如果成员变量是整型的,且对类的所有对象均有效,则可将其定义为const static type类型的变量,type为整型或可转换为整型的类型,如char。

用途五:限定函数返回值

  在什么情况下,函数的返回值需要使用const来限定呢?
  当然是不希望返回值被修改的情况下使用const了。问题是,是不是所有的返回类型都可以使用const来限定呐?先来看下面一个例子:

int foo()
{
    return 0;
}

int goo()
{
    int ret = 10;
    return ret;
}

  这类函数返回的并非左值,而const是用来限定一个左值的,表示该变量的值在运行期间不会被修改。因此,此类函数的返回值没有必要使用const来限定。

  接下来看一下指针类型的返回值[C/C++]:

int * foo()
{
    int a[2] = {10};
    return a;    // 这种返回一个局部变量地址的函数,是存在问题的,
                 // 调用结束后,函数的栈空间已被销毁,加上const也是没有意义的
}

char * goo()
{
    char * p = "HELLO";// 字符串文字量"HELLO"是静态分配的,其类型为const char[6]
    return p;          // p是其地址,将其返回是可以的
}

int main(void)
{
    goo()[0] = 'h';    // 试图将"HELLO"首字符修改为'h'
    return 0;
}

  这段代码编译是没问题的,但运行期间会出异常或段错误,原因是 “HELLO”是const char[6]类型的。为了能够及早发现这类错误,我们可以让编译器在编译阶段就诊断出这类问题,只需要对goo()的返回类型使用const进行限定即可。
  修改如下:

const char * goo()
{
    char * p = "HELLO";
    return p;
}

// 在函数内部修改str指向的变量,并返回str
const char * goo(const char * str)
{
    char * p = (char *)str;
    while(*p) ++(*p), ++p;
    return str;
}

char * foo(const char * str)
{
    char * p = (char *)str;
    while(*p) ++(*p), ++p;
    return (char *)str;
}

int main(void)
{
    char buf[] = "HELLO";  // 仅希望buf在goo()/foo()内部被修改
    goo()[0] = 'h';        // error  编译期间就可以发现问题
    goo(buf)[0] = 'h';     // error
    foo(buf)[0] = 'h';     // ok buf[0]被修改为'h'
    return 0;
}

  为了避免写出形似foo()[index] = value或(*foo())++等类似组合时,可以给函数返回类型使用const加以限定。

  接着来看返回引用的情况[C++]:

int & foo(int a, int & b)
{
    return b += a;
}

int main(void)
{
    int x = 10;
    foo(10, x);        // ok x = 20
    foo(10, x) = 100;  // ok x = 100
    foo(10, x)++;      // ok x = 111

    return 0;
}

  显然,上面有两行代码虽然能完美的运行,但其写法并不是我们希望的。这时,有必要使用const来限定一个函数的返回值。

const int & foo(int a, int & b)
{
    return b += a;
}

int main(void)
{
    int x = 10;
    foo(10, x);        // ok x = 20
    foo(10, x) = 100;  // error
    foo(10, x)++;      // error
    x += 1;            // ok const限定的是函数返回的引用,并没有限定x

    return 0;
}

  由于C++中提供了对运算符的重载,因此不合理的重载可能会发生意想不到的结果。下面我们来看一段代码:

class ABC
{
public:
    ABC() : x(0) {}
    friend ABC operator+(const ABC & op1, const ABC & op2)
    {
        ABC a;
        a.x = op1.x + op2.x;
        return a;
    }
    friend bool operator==(const ABC & op1, const ABC & op2)
    {
        return op1.x == op2.x;
    }
    operator bool()
    {
        return 0 != x;
    }
    // ... other
private:
    int x;
};

int main()
{
    ABC a, b, c;
    a = b + c;             // ok 这是我们想要的
    (a + b) = c;           // ok 合法,但这是不正常的
    if ((a + b) == c)      // ok 我们的本意就是比较
        0;
    if ((a + b) = c)       // ok 本意是比较操作,但此处变成了赋值操作
        0;

    return 0;
}

  为了去除这种畸形的代码,让编译帮我们发现更多的错误,以提高代码健壮性,需要给相关的重载函数添加const限定。

friend const ABC operator+(const ABC & a, const ABC & b) {...}

综上,我们可以总结到,需要使用const来限定其返回值类型的函数,其声明形式一般如下:

const return_type * function(parameters-list);
const return_type & function(parameters-list);

用途六:限定类的成员函数

  我们先来看一下一般函数的声明形式:

[qualifier] return_type [*/&] function([parameters-list]);
// [] 表示这部分是可选的
// qualifier 即CV限定符const volatile

  那么C++中用来限定成员函数的const被安放在了什么位置呢?
  我们拿int来说明:

const int * const foo(const int * const p);

  猛一看好像没有什么位置可以再放一个const了。仔细一看,似乎只有函数调用符()与;之间还有一个位置可用。得喽,就把const放这了吧。[这只是我自己臆想的啦]
示例代码:

class A
{
public:
    void foo() { x = 1; }
    void goo() { }
    void hoo() const { }            // ok    const成员函数没有修改成员变量
    void koo() const { x = 1; }     // error const成员函数不能修改成员变量
    void loo() const { foo(); }     // error const成员函数不能调用非const成员函数
    void moo() const { goo(); }     // error 同上,尽管goo()什么也没有做
    void yoo() const { hoo(); }     // ok    const成员函数只能调用const成员函数
private:
    int x;
};

int main(void)
{
    A a;
    const A b = a;
    a.foo();
    a.hoo();
    b.foo();     // error b是A的const对象,只能调用const成员函数
    b.hoo();     // ok

    return 0;
}

  通过示例,我们可能得到一些const成员函数的使用方法及特征。为什么会有这些特征呢?
  我们知道,对象在调用成员时,隐式的传入了this指针,默认情况下,this为class_name * const类型,而使用const限定成员函数时,const实际上是修改了this的类型,此时,this为const class_name * const类型,由于this指向的是当前对象,即当前对象为const对象,其成员变量也就具有了const约束,因此,在const成员函数内部修改成员变量会被视为非法操作。

总结一下const成员函数的作用及注意点:
const成员函数内部不能修改成员变量;
const成员函数只能调用const成员函数,非const成员函数可以调用任意成员函数;
const对象的成员变量具有const约束,参考用途一,const对象只能调用const成员函数;

与const相关的常见面试题

待续。。。。。。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值