• const对象一旦创建其值不可改变,所以const对象必须初始化。

  • 默认情况,const对象仅在本文件内有效。不同文件中出现同名const变量,等同于不同文件分别定义的变量。使用extern关键字可以在不同文件共享const变量。

    1
    2
    3
    4
    5
    // file_1.cc定义并初始化一个常量,该常量能被其他文件访问
    extern const int bufSize = fcn();

    // file_1.h头文件
    extern const int bufSize; // 与file_1.cc中定义的bufSize是同一个

const的引用(常量引用、指向常量的引用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const int ci = 1024;
const int &r1 = ci; // 正确,引用及其对应的对象都是常量
r1 = 42; // 错误,r1是const的引用,其对应对象也是const
int &r2 = ci; // 错误,不允许一个非常量引用指向一个常量对象


int i = 42;
const int &r1 = i; // 正确,允许常量引用绑定到普通int对象上
const int &r2 = 42; // 正确
const int &r3 = r1 * 2; // 表达式也可以
int &r4 = r1 * 2; // 错误,不允许改变r4进而影响到r1


// const的引用可以指向非常量对象
int i = 42;
int &r1 = i; // 正确
const int &r2 = i; // 正确
r1 = 0; // 正确,r1变为0,导致i变为0,进而导致r2变为0,允许这种情况
r2 = 0; // 错误,r2为常量引用


// 另一个例子
int i = 42;
const int &r1 = i;
int &r2 = i;
int other_val = 50;
// r1 = other; // 报错,企图通过r1改变i,使i变为50
r2 = other; // 正确,普通引用就可以,此时i的值会变为50 !!

指向常量的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const double pi = 3.14;
double *ptr = π // 错误,通过ptr可能导致常量pi的改变
const double *cptr = π // 正确
*cptr = 42; // 错误,不能改变const double *cptr的值

// 指向常量的指针,可以指向非常量(这话说得就很别扭)
double dval = 3.14;
const double *cptr_2 = &dval; // 正确
dval = 233.24; // 正确,指向的对象随便修改,而且*cptr_2也跟着被修改了


// 另一个例子
double dval = 3.14;
const double *cptr_2 = &dval;
double abc = 66.66;
cptr_2 = &abc; // 正确,这种指针变换是不会影响到dval的值的。现在*cptr_2为66.66,dval仍为3.14。(这也是指向常量的指针与const的引用的小区别)
  • 可以这样理解,“指向常量的指针”与“const的引用”,不过是指针和引用“自以为是”罢了,它们觉得自己指向了常量(实际未必)。不能通过它们两个去改变所指向对象的值,但可以通过改变所指对象的值进而改变它们两个。

常量指针(const指针)

  • *后加const表常量指针
  • 常量指针必须初始化
  • 常量指针指向的地址不可变,但指向的内容随意改变
1
2
3
4
5
6
7
int test = 233;
int *const ptr = &test;
*ptr = 0; // 常量指针,指针指向的地址不可变,但指针指向的内容可通过常量指针随便修改
int other = 110;
// ptr = &other; // 报错,常量指针指向的地址不可变

const int *const ccptr = &test; // 存在这种指针情况
  • 顶层const表示指针本身是一个常量;底层const表示指针所指向的对象是一个常量。

constexpr变量

请拜读C++ Primer原著…

const形参和实参

1
2
3
4
5
6
7
8
// 调用fcn函数时,实参可以传const int,也可传int
// 换句话说,形参的顶层const被忽略掉了
void fcn(const int i){...}

// C++允许定义同名函数,前提是形参列表有所区别
// 但是下面这个fcn函数会报错,因与上面fcn函数冲突,重复定义了fcn。因为fcn(233)无法判断到底是调用上面还是下面的fcn函数
// 也就是说,尽管形式上有所差异,但实际上这两个fcn函数的形参列表没什么不同
void fcn(int i){...}
  • 调用函数时,形参的初始化规则与变量的初始化规则是一样的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // 变量的初始化规则
    int i = 42;
    const int *cp = &i; // 正确,但是不能通过cp去改变i
    const int &r = i; // 正确,但是不能通过r去改变i
    const int &r2 = 42; // 正确
    int *p = cp; // 错误,可通过改变 *p 进而导致改变 *cp,最终改变 i,这是不允许的
    int &r3 = r; // 错误,同上
    int &r4 = 42; // 错误,不能用字面值初始化一个非常量引用


    // 上述变量的初始化规则同样适用于形参的初始化
    void reset(int *ip) { *ip = 0 }
    void reset(int &i) { i = 0 };

    int i = 0;
    const int ci = i;
    reset(&i); // 正确,调用了reset(int *ip)
    reset(&ci); // 错误,reset(int *ip)可改变指针所指向变量的值,这里ci不允许
    reset(i); // 正确,调用了reset(int &i)
    reset(ci); // 错误,reset(int &i)可改变引用变量的值,这里ci不允许
    reset(42); // 错误,reset(int &i)不能把字面值初始化一个非常量引用
  • 把函数不会改变的形参定义成普通引用是一种比较常见的错误,这么做会给函数调用者一种误导,使其认为函数可以修改它的实参值。

函数重载

  • 如果同一作用域内,几个函数名字相同但形参列表不同,称之为重载函数

    • 形参列表不同指形参数量不同或形参类型有所不同
    • 不允许两个函数除了返回类型外其他所有的要素都相同,第二个函数的声明是错误的。
  • main函数不能重载。

  • 有时候两个形参列表看起来不一样,但实际是相同的东西:

    1
    2
    3
    4
    5
    6
    7
    // 以下两组内容不构成重载
    void func(const Account &acct);
    void func(const Account&); // 与上形参列表相同,只是省略了形参的名字

    typedef Phone Telno;
    void func(const Phone&);
    void func(const Telno&); // 与上形参列表相同,Telno与Phone类型相同
  • 重载与const形参

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

      1
      2
      3
      4
      5
      void func(Phone);
      void func(const Phone); // 重复声明

      void func(Phone*);
      void func(Phone* const); // 重复声明
    • 另一方面,如果形参是指针或引用,可通过区分其指向的是常量对象还是非常量对象来实现函数重载。此时const是底层的。(这一概念与**“基于const的重载”**非常类似,详见博客“类与结构体”一篇的总结。)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      /*
      编译器可以通过实参是否是常量来推断应该调用哪个重载版本。
      因const不能转换成其他类型,我们只能把const对象(或指向const的指针)传递给const形参。
      相反,因非常量可以转换成const,所以下面的4个函数都能作用于非常量对象或非常量对象的指针,但编译器会优先选用非常量的重载版本。
      */
      void func(Account&);
      void func(const Account&);

      void func(Account*);
      void func(const ACCOUNT*);
  • 重载与作用域

    • 重载对作用域的一般性质并无改变:如果我们在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体。在不同作用域中无法重载函数名

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      string read();
      void print(const string &);
      void print(double); // 外层作用域重载print函数

      void func() {
      bool read = false; // 内层作用域,隐藏了外层的read函数
      string s = read(); // 错误,read此时只是一个布尔值,而非函数

      void print(int); // 内部作用域,隐藏了外层的print函数(因在不同作用域无法重载同一函数名,不涉及形参列表的比较问题,因此只要名字相同就会进行覆盖)
      print("tempstr"); // 错误,外层的print(const string &)函数被隐藏掉了
      print(233); // 正确,调用内部作用域的print(int)
      print(3.14); // 正确,注意,double类型的实参会转换成int类型,调用内部作用域的print(int)。外层的print(double)被隐藏掉了。
      }

      ​ 当我们调用print函数时,编译器首先寻找对该函数名的声明,找到的是接收int值的那个内部作用域声明。一旦在当前作用域中找到了所需名字(名字相同即可),编译器就会忽略掉外层作用域中的同名实体(被隐藏)。剩下的工作就是检查函数调用是否有效了(即形参实参是否匹配,函数能否调用成功)。