一文理解const关键字

const关键字在c/c++中的作用是限定修饰的变量为不可变的,初始化后将不可更改,任何试图更改const修饰的变量的行为都将导致编译错误。
const的修饰规则比较复杂,可以参照如下代码理解。

// NOTE: 以下代码仅为说明使用,在正常编码时,所有局部变量请在声明的同时进行初始化操作。
const int a;        //编译错误, 常量变量'a'需要初始值设定项
const int b = 15;   //正确
int c;

const int *p;       //正确, p为指向 const int 的指针, p可以被修改,指向不同的const int 变量。
p = &b;             //正确
int const *q;       //正确, q和q类型一样,没有任何区别
q = &c;             //正确, q为const int型指针,但是可以指向int型变量,代表不会通过指针q修改c的值。
*q = 12;            //编译错误, 不能修改p指针所指向的内容。

int * const x;      //编译错误, 常量变量'x'需要初始值设定项,x是常量指针,指向int型的变量,必须初始化且不可更改。
int * const y = &b; //编译错误, y指向的变量类型应该为int型,而非const int 型,因为代码后续可能通过y指针修改b的值。
int * const z = &c; //正确, z指针永远指向c当前的地址,不可更改z指针的值。

const int * const u = &b;   //正确, 常量指针u指向常量变量b,不可修改u的值,也不可通过u修改b的值。

当搞不清楚const到底修饰的是指针还是类型的时候,可以根据const*的位置判断:

  1. const*的左边时,修饰的是是类型, const int * pint const * qconst 都是用来修饰int的.
  2. const*的右边时,修饰的是指针, int * const yconst是修饰指针的。

通过const修饰指针理解了const关键字的含义,下面看看const关键字还可以修饰哪些内容:
const可以修饰变量,修饰引用变量,修饰函数的参数,修饰函数的返回值,修饰类的成员变量,修饰类的成员函数

const修饰变量

int a = 15;
const int b = a;    //正确,使用变量a的值初始化b,b今后不可被更改。
const int c = b;    //正确,使用常量变量b的值初始化c, c今后也不可被更改。
int d = c;          //正确,使用常量变量c的值初始化d.

b = c;              //编译错误,不能修改常量变量b
c = d;              //编译错误,不能修改常量变量c

const修饰引用

int a = 27;
const int &b = a;   //正确,b的类型为const int &, 这行代码翻译下就是:b是a的常量引用,即只可以通过b访问a,而不可以通过b修改a。
int &c = a;         //正确,c的类型为int &,这行代码翻译下就是:c是a的引用(别名),可以通过c访问a,也可以通过c修改a。
c = 12;             //正确,a的值变为12,访问b也会得到12
b = c;              //编译错误,b为常量类型,不可被修改。

const int d = c;    //正确,d为const int 类型,初始化为12,
const int &e = d;   //正确,e为d的别名。
int &f = d;         //编译错误,无法使用const int 给 int & 变量赋值。 
                    //因为f没有说自己不会修改变量的值,但是d的值是不可更改的,使用d给f赋值,可能导致d的值在后面被f修改,所以编译不通过。

int &g;             //编译错误,引用类型必须初始化
const int &h;       //编译错误,引用类型必须初始化

const修饰函数的参数

理解函数的参数是掌握函数调用中是非常重要的一个环节。
其中最重要得一个概念就是明白函数调用中使用的是按值传递还是按引用传递

按值传递(by value)/按引用传递(by reference)

  1. 按值传递的函数参数是原始对象的拷贝,对函数参数的操作和传入参数无关。
  2. 按引用传递的函数参数就是传递过来的对象的别名,对函数参数的操作就是对传入参数进行操作。
    以下是一个按值传递没有得到预期结果的示例:
#include <iostream>
void swap(int a, int b)  //a、b为形参
{
    int temp = a;
    a = b;
    b = temp;
}
int mian()
{
    int x = 1;
    int y = 2;
    swap(x,y);      //x、y为实参
    std::cout << "x = " << x << " y = " << y << std::endl;
}
//以上代码执行过程为,
//  x = 1,
//  y = 2,
//  a = x,(临时变量a使用x的值进行初始化,也可以说a是x的一份拷贝)
//  b = y,(临时变量b使用y的值进行初始化,也可以说b是y的一份拷贝)
//  temp = a,(temp = 1)
//  a = b,(a = 2)
//  b = temp,(b = 1)
//  函数运行结束,a和b生命周期结束,被析构。
//  输出 x = 1, y = 2,

以上代码没有得到预期结果的原因是使用了按值传递后,形参实参拷贝,对形参的修改和实参没有任何关系了。
所以以上代码只是交换了ab的值,xy的值没有发生任何变化。
修改为按引用传递即可,修改如下:

void swap(int &a, int &b)
{
    int temp = a;
    a = b;
    b = temp;
}

const修饰函数参数

了解了按值传递按引用传递的区别后,下面我们看看const关键字在修饰函数参数时对代码产生的影响。
下面是一个按引用传递的例子:

int abs_good(const int& x)
{
    if (x < 0) { return -x;}
    else { return x;}
}
int abs_bad(int& x)
{
    if (x < 0) { return -x;}
    else { return x;}
}
//上面是两个求绝对值的函数。
int main()
{
    int a = -15;
    const int b = -7;

    int c = abs_good(a);    //c = 15;
    int d = abs_bad(a);     //d = 15;

    int e = abs_good(b);    //e = 7;
    int f = abs_bad(b);     //编译错误,因为b为const int,不可被更改。
                           //但是abs_bad函数没有明说自己不会去修改b的值,
                          //编译器便认为abs_bad可能修改b的值(虽然它现在实际上没有修改,但难免以后不会去修改),所以编译不通过。
}

由上面按引用传递的例子可知,当不需要对函数的参数进行修改时,最好在声明参数时说明,这样可保证调用的最大兼容性,int型参数与const int型参数均可调用。同时代码的可读性与安全性更有保障。

下面是一个按值传递的示例:

int abs_good(const int x)
{
    if (x < 0) { return -x;}
    else { return x;}
}
int abs_normal(int x)
{
    if (x < 0) { return -x;}
    else { return x;}
}
//上面是两个求绝对值的函数。
int main()
{
    int a = -15;
    const int b = -7;

    int c = abs_good(a);    //c = 15;
    int d = abs_normal(a);     //d = 15;

    int e = abs_good(b);    //e = 7;
    int f = abs_normal(b);  //f = 7; 正确,abs_normal参数中的x为b的拷贝,对x的修改对b不产生任何影响
}

由上面按值传递的例子可知,在按值传递过程中,形参实参各自独立。
按值传递时无需增加const关键字,亦不会影响函数调用的兼容性。
但是在对形参确实不需要修改时,建议还是增加const关键字,提高代码的可读性,同时让编译器帮我们做检查,确保我们确实没有无意中修改。
函数参数为指针时,和按引用传递类似,下面简要示例下,自行体会:

#include <iostream>
void swap(int* pa, int* pb)
{
    int temp = *pa;
    *pa = *pb;
    *pb = temp;
}

int abs_good(const int * x)
{
    if (*x < 0) { return -*x; }
    else { return *x; }
}

int main()
{
    int a = 12;
    int b = -22;
    swap(&a, &b);
    const int c = abs_good(&a);
    std::cout << "a = " << a << " b = " << b << " c = " << c << std::endl;
}

const修饰函数返回值

当函数返回值是按值传递时,使用const关键字修饰没有任何效果,只有当函数的返回值是按引用传递时,才有必要使用const关键字。
在混合是用c/c++编程时,我们经常遇到的一个场景类型如下:

#include <stdio.h>
#include <string>
//一个古老的糟糕接口
void print_message_bad(char *msg)
{
    printf("Message[%s]\n", msg);
}

int main()
{
    std::string Info = "hello world";
    print_message_bad(Info.c_str());    //编译错误,"const char *" 类型的实参与 "char *" 类型的形参不兼容
    return 0;
}

以上代码编译不通过的原因是:std::string的成员函数c_str()的返回值是const char *类型的,不可以传递给char *类型的参数。
其原因是:std::string类型禁止直接修改其指针所指向的内存内容,只能读取其指针所指向的内存中的内容。
因为std::string类型的size()length()等函数在内容修改时需要相应的进行变更,所以可以使用std::string类型提供的接口,否则会导致std::string行为的异常。

#include <stdio.h>
const int kMessageLen = 64;
const char* get_message(int line)
{
    char *p = new char[kMessageLen];
    snprintf(p, kMessageLen, "Message line :[%d]", line);
    return p;
}
int main()
{
    int line = 15;
    const char *msg = get_message(line);    //
    printf("%s\n", msg);
    char *msg_error = get_message(line);    //编译错误,原因同上
    return 0;
}

上面说明了const修饰返回指针的情况,下面展示const修饰返回引用的情况。

const int& get_const_reference(const int & x)
{
    return x;
}

int main()
{
    int a = 15;
    int b = get_const_reference(a);
    const int c = get_const_reference(b);
    //??? 上面两行可以编译通过么?为什么?
    a = 17;
    b = 18;
    c = 19;
    //??? a b c 的值均为19么?

    int &d = get_const_reference(a);
    const int &e = get_const_reference(c);
    //???上面两行可以编译通过么?为什么?
    return 0;
}

好了,通过上面的例子,应该已经明白了const关键字修饰返回函数返回值的作用。
这种用法常见于类的成员函数,当希望你可以访问内部数据同时又希望保护内部数据时,可以通过const修饰的函数返回值来完成。

const修饰类成员变量

#include <string>
#include <iostream>
class Student{
public:
    explicit Student(std::string name, int age): age_(age), name_(name)
    {}

    ~Student() {};

    int get_age() { return age_; }
    const std::string& get_name() { return name_; }

private:
    int age_;
    const std::string name_;    //编译错误??? const变量未初始化?
};

int main()
{
    Student xiaoMing("xiaoMing", 8);
    Student xiaoHong("xiaoHong", 7);
    std::cout << "name: " << xiaoMing.get_name() << " age: " << xiaoMing.get_age() << std::endl;
    std::cout << "name: " << xiaoHong.get_name() << " age: " << xiaoHong.get_age() << std::endl;
    return 0;
}

const修饰类的成员变量时,不需要在声明时进行初始化,需要在构造函数中初始化。
这个规则其实比较容易理解,只有在类对象实例化(构造/存在)的时候,const修饰的类的成员变量才开始实例化(构造/存在)。
其实在c++11以后可以对类的成员变量在声明的时候赋值,此值可以作为默认值,在构造函数中未提供相应值时,会使用此值。详细信息:参考《深入理解 C++11》
可以执行以下代码查看:

// NOTE:此代码在c++98会编译不通过
#include <string>
#include <iostream>
class Student{
public:
    Student() {}
    Student(std::string name, int age): age_(age), name_(name)
    {}

    ~Student() {};

    int get_age() { return age_; }
    const std::string& get_name() { return name_; }

private:
    int age_ = 165;
    const std::string name_ = "gua";  
};

int main()
{
    Student xiaoMing("xiaoMing", 8);
    Student oldMan;
    std::cout << "name: " << xiaoMing.get_name() << " age: " << xiaoMing.get_age() << std::endl;
    std::cout << "name: " << oldMan.get_name() << " age: " << oldMan.get_age() << std::endl;
    return 0;
}

下面是muduo网络库中Timestamp中的一个代码片段,注意其中的static const int kMicroSecondsPerSecond = 1000 * 1000;

class Timestamp :
{
 public:
  Timestamp()
    : microSecondsSinceEpoch_(0)
  {
  }

  // default copy/assignment/dtor are Okay
  
  string toString() const;
  string toFormattedString(bool showMicroseconds = true) const;

  bool valid() const { return microSecondsSinceEpoch_ > 0; }

  static const int kMicroSecondsPerSecond = 1000 * 1000;    //在c++98和c++11下均可以编译通过。为什么?

 private:
  int64_t microSecondsSinceEpoch_;
};
class A {
    const static int num1; // 声明
    const static int num2 = 13; // 声明和初始化
};
const int A::num1 = 12; // 定义并初始化,必须类外初始化
const int num2;  // 定义

const修饰类成员函数

const修饰类的成员函数
在设计类的时候,一个重要原则就是对于不改变数据成员成员函数都要在后面加 const,而对于改变数据成员的成员函数不能加 const。所以 const 关键字对成员函数的行为作了更加明确的限定:

(1)有 const 修饰的成员函数(const 放在函数参数表的后面,而不是在函数前面或者参数表内),只能读取数据成员,不能改变数据成员;没有 const 修饰的成员函数,对数据成员则是可读可写的。
(2)除此之外,在类的成员函数后面加 const 还有什么好处呢?那就是常量对象(即使用 const 修饰的类对象)可以调用 const 成员函数,而不能调用非const修饰的函数。
详细内容可访问:
C++之const类成员变量,const成员函数

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容