C++ 入门基础之二

大纲

const 关键字

提示

const 关键字是 C++ 对 C 语言增强的一部分,详细介绍请看 这里

const 简介

const 是 constant 的缩写,本意是不变的,不易改变的意思。在 C++ 中是用来修饰内置类型变量、自定义对象、成员函数、返回值、函数参数等。C++ 的 const 关键字允许指定一个语义约束,编译器会强制实施这个约束,允许程序员告诉编译器某个值是保持不变的。如果在编程中确实有某个值保持不变,就应该明确使用 const,这样可以获得编译器的帮助。

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>

using namespace std;

int main() {
const int a = 7;
int *p = (int *) &a;
*p = 8;
cout << a << " "<< *p;
return 0;
}

在上述代码中,对于 const 变量 a,我们取变量的地址并转换赋值给 指向 int 的指针,然后利用 *p = 8; 重新赋值,然后输出查看 a 的值,程序运行的输出结果如下:

1
7 8

从结果中可以看到,编译器认为 a 的值为一开始定义的 7,所以对 const a 的操作就会产生上面的情况。所以千万不要轻易对 const 变量赋值,这会产生意想不到的行为。C++ 编译器对 const 常量的处理机制是,当碰见常量声明时,往符号表中放入常量;在编译过程中若发现使用常量,则直接以符号表中的值替换。特别注意,C++ 编译器在编译过程中若发现对 const 常量使用了 extern 关键字(外部链接)或者 & 操作符(取地址),则会给对应的常量单独分配内存空间(兼容 C 语言),这也是上述代码中打印 *p 的值为 8 的原因,点击查看原理分析图。


如果不想让编译器察觉到上面对 const 变量的操作,我们可以在 const 前面加上 volatile 关键字。volatile 关键字跟 const 刚好相反,是易变的,容易改变的意思;所以不会被编译器优化,编译器也就不会改变对 a 变量的操作。

1
2
3
4
5
6
7
8
9
10
11
#include<iostream>

using namespace std;

int main() {
volatile const int a = 7;
int *p = (int *) &a;
*p = 8;
cout << a << " " << *p;
return 0;
}

程序运行的输出结果如下:

1
8 8

const 参数传递

对于 const 修饰函数参数可以分为以下三种情况:

第一种情况:值传递的 const 修饰传递,一般这种情况不需要 const 修饰,因为函数会自动产生临时变量复制实参值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

using namespace std;

void Cpf(const int a)
{
cout << a;
// ++a; 是错误写法,a 不能改变值(只读)
}

int main()
{
Cpf(8);
return 0;
}

第二种情况:当 const 参数为指针时,可以防止指针被意外篡改(指向其他内存地址)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

using namespace std;

void Cpf(int *const a) {
cout << *a << endl; // a 为 8
*a = 9;
}

int main() {
int a = 8;
Cpf(&a);
cout << a << endl; // a 为 9
return 0;
}

第三种情况:自定义类型的参数传递,需要使用临时对象复制参数;由于临时对象的构造需要调用拷贝构造函数,这个过程比较浪费资源,因此可以采取 const 外加引用传递的方式。并且对于一般的 intdouble 等内置类型,不需要采用引用的传递方式。

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
#include <iostream>

using namespace std;

class Test {
private:
int _cm;

public:
Test() {}

Test(int _m) : _cm(_m) {}

int get_cm() const {
return _cm;
}
};


void Cmf(const Test & _tt) {
cout << _tt.get_cm();
}

int main() {
Test t(8);
Cmf(t);
return 0;
}

程序运行的输出结果如下:

1
8

const 函数返回值

对于 const 修饰函数的返回值可以分为以下三种情况:

  • 第一种情况:当 const 修饰内置类型(如 intdouble)的返回值,修饰与不修饰返回值的作用都一样。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>

using namespace std;

const int Cmf() {
return 1;
}

int Cpf() {
return 0;
}

int main() {
int _m = Cmf();
int _n = Cpf();

cout << _m << " " << _n; // 输出结果为:1 0
return 0;
}
  • 第二种情况:const 修饰自定义类型作为返回值,此时返回的值不能作为左值使用,既不能被赋值,也不能被修改。

  • 第三种情况:const 修饰返回的指针或者引用,是否返回一个指向 const 的指针,取决于我们想让用户干什么。

const 修饰指针变量

const 修饰指针变量有以下三种情况:

  • A: const 修饰指针指向的内容,则内容为不可变量。
  • B: const 修饰指针,则指针为不可变量。
  • C: const 修饰指针和指针指向的内容,则指针和指针指向的内容都为不可变量。

对于 A,指针指向的内容不可改变,简称左定值,因为 const 位于 * 号的左边。

1
2
3
4
5
int a = 10;
int b = 20;
const int *p = &a;
p = &b; // 正确写法
*p = 10; //错误写法

对于 B, const 指针其指向的内存地址不能够被改变,但其内容可以改变,简称右定向,因为 const 位于 * 号的右边。

1
2
3
4
5
int a = 8;
int * const p = &a;
*p = 9; // 正确写法
int b = 7;
p = &b; // 错误写法

对于 C,则是 A 和 B 合并的结果,即 const 指针指向的内容和指向的内存地址都已固定,不可改变。

1
2
int a = 8;
const int * const p = &a;

对于 A、B、C 三种情况,根据 const 相对于 * 号的位置不同,可以总结出三句便于记忆的话: 左定值,右定向,const 修饰不变量

const 修饰类成员函数

const 修饰类成员函数,其目的是防止成员函数修改被调用对象的值,如果我们不想修改一个调用对象的值,所有的成员函数都应当声明为 const 成员函数,此时 const 本质上修饰的是 this 指针。值得一提的是,const 关键字不能与 static 关键字同时使用,因为 static 关键字修饰静态成员函数,而静态成员函数不含有 this 指针,即不能实例化,但 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
29
#include <iostream>

using namespace std;

class Test {
private:
int _cm;

public:
Test() {}

Test(int _m) : _cm(_m) {}

int get_cm() const {
// _cm = 10; 是错误写法,对象的_cm属性值不能被改变
return _cm;
}
};


void Cmf(const Test & _tt) {
cout << _tt.get_cm();
}

int main() {
Test t(8);
Cmf(t);
return 0;
}

程序运行的输出结果如下:

1
8

上面的 int get_cm() const {} 函数用到了 const 成员函数,如果 int get_cm() {} 去掉 const 修饰,则 Cmf 函数传递的 const _tt 即使没有改变对象的值,编译器也认为函数 int get_cm() {} 会改变对象的值,所以我们尽量按照要求将所有的不需要改变对象内容的函数都作为 const 成员函数。下述两种的写法都是合法的,效果都一样,C++ 中一般将 const 写在函数的末尾处。

1
2
3
4
5
6
int get_cm() const {

}

int const get_cm() {
}

如果有个成员函数想修改对象中的某一个成员怎么办?这时我们可以使用 mutable 关键字修饰这个成员,mutable 的意思也是易变的,容易改变的意思,被 mutable 关键字修饰的成员可以处于不断变化中,如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

using namespace std;

class Test {
public:
int _cm;
mutable int _ct;

public:
Test(int _m, int _t) : _cm(_m), _ct(_t) {}

void Kf() const {
// ++_cm; 错误写法
++_ct; // 正确写法
}
};

int main() {
Test t(8, 7);
t.Kf();
cout << t._cm << " " << t._ct << endl;
return 0;
}

程序运行的输出结果如下:

1
8 8

这里在函数 void Kf() const {} 中可以通过 ++_ct; 修改 _ct 的值,但是通过 ++_cm 修改 _cm 则会报错,因为 _cm 没有用 mutable 修饰。

const 和 #define 的区别

C++ 中不但可以用 #define 定义常量(即宏常量),例如 #define c 5,还可以用 const 定义常量,例如 const int c = 5;,它们的区别如下:

  • #define MAX 255 定义的常量是没有类型的,所给出的是一个立即数,编译器只是把所定义的常量值与所定义的常量的名字联系起来,#define 所定义的宏常量在编译器执行预处理的时候进行替换,在程序中使用到该常量的地方都要进行拷贝替换
  • const float MAX = 255; 定义的常量是有类型的,存放在内存的静态区域中,在程序运行过程中 const 变量只有一个拷贝,而 #define 所定义的宏常量却有多个拷贝,所以宏定义在程序运行过程中所消耗的内存要比 const 变量的大得多
  • #define 定义的常量是不可以用指针变量去指向的,用 const 定义的常量是可以用指针去指向该常量的地址
  • #define 可以定义一些简单的函数,const 是不可以定义函数

编译器处理方式:

  • #define:在编译器的预处理阶段进行单纯的文本替换
  • const:在编译器的编译阶段确定其值

类型检查:

  • #define:无类型,不进行类型安全检查,可能会产生意想不到的错误
  • const:有类型,编译时会进行类型与作用域检查

内存空间:

  • #define:不分配内存,给出的是立即数,有多少次使用就进行多少次替换,在内存中会有多个拷贝,消耗内存大
  • const:在静态存储区中分配空间,在程序运行过程中内存中只有一个拷贝

其他方面:

  • 在编译时,编译器通常不为 const 常量分配内存空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。#define 宏替换只作替换,不做计算,不做表达式求解
  • 宏定义的作用范围仅限于当前文件,默认状态下,const 常量只在文件内有效,当多个文件中出现了同名的 const 常量时,等同于在不同文件中分别定义了独立的常量。如果想在多个文件之间共享 const 常量,必须在常量定义之前添加 extern 关键字(在声明和定义时都要添加)

C 语言与 C++ 的 const 对比

C 语言的 const 变量:

  • C 语言中的 const 变量属于伪常量
  • C 语言中的 const 变量是只读变量,有自己的内存空间
  • C 语言中,可以通过操作指针的方式来修改 const 变量的值

C++ 的 const 常量:

  • C++ 中的 const 常量属于真常量
  • 可能分配内存空间,也可能不分配内存空间
  • 当使用 & 操作符取 const 常量的地址时,编译器会临时开辟一块内存空间
  • const 常量为全局,并且需要在其它文件中使用(利用 extern 外部链接),编译器会分配内存空间
  • 当使用字面量常量初始化 const 引用(即常量引用),如 const int &a = 10;,编译器会分配内存空间

注意

C++ 编译器虽然可能为 const 常量分配内存空间,但不会使用其内存空间中的值,而且是在编译器的编译阶段分配内存空间

引用(普通引用)

变量名回顾

  • 变量名实质上是一段连续内存空间的别名,是一个标号(门牌号)
  • 程序中通过变量来申请并命名内存空间
  • 通过变量的名称可以使用内存空间

引用的概念

在 C++ 中新增加了引用的概念:

  • (a) 引用可以看作一个已定义变量的别名
  • (b) 引用的语法:Type & 别名 = 原名
  • (c) 引用作为函数参数声明时,不会进行初始化
  • (d) 普通引用在声明时必须用其它的变量进行初始化
  • (e) 当 & 写在左侧叫引用,如 int &b = a;,当 & 写在右侧叫取地址,如 int *p = (int *) &a;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

using namespace std;

void demo1() {
int a = 10; //编译器分配4个字节内存,a是内存空间的别名
int &b = a; // b就是a的别名,即b引用了a
a = 11; //直接赋值
{
int *p = &a;
*p = 12;
printf("a = %d \n", a);
}
b = 14;
printf("a = %d, b = %d", a, b);
}

int main() {
demo1();
}

程序运行的输出结果如下:

1
2
a = 12 
a = 14, b = 14

引用是 C++ 的概念,属于 C++ 编译器对 C 语言的扩展,下述代码在 C 语言中不能通过编译,这里不要用 C 语言的语法去思考 b = 11

1
2
3
4
5
6
int main() {
int a = 0;
int &b = a;
b = 11;
return 0;
}

对数组建立引用

在 C++ 中,对数组建立引用有两种常用的方式,代码如下:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>

using namespace std;

// 对数组建立引用(第一种方式)
void test03() {
int arr[10];
for (int i = 0; i < 10; i++) {
arr[i] = i;
}

// 给数组起别名
int (&pArr)[10] = arr;

for (int j = 0; j < 10; j++) {
cout << pArr[j] << " ";
}
cout << endl;
}

// 对数组建立引用(第二种方式)
void test04() {
int arr[10];
for (int i = 0; i < 10; i++) {
arr[i] = i;
}

// 定义一个具有 10 个元素的 int 类型的数组
typedef int(ARRAYREF)[10];

// 给数组起别名
ARRAYREF &pArr = arr;

for (int j = 0; j < 10; j++) {
cout << pArr[j] << " ";
}
cout << endl;
}

int main() {
test03();
test04();
return 0;
}

程序运行的输出结果如下:

1
2
0 1 2 3 4 5 6 7 8 9 
0 1 2 3 4 5 6 7 8 9

引用作函数参数

  • 普通引用在声明时必须用其它的变量进行初始化,int &a; 这样的写法是错误的(在结构体内声明除外)
  • 引用作为函数参数声明时,不会进行初始化
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>

using namespace std;

struct Teacher {
char name[64];
int age;
};

// pT是指向t1的指针,这里相当于修改了t1
void printfT(Teacher *pT) {
cout << pT->age << endl;
pT->age = 23;
}

// pT是t1的别名,这里相当于修改了t1
void printfT2(Teacher & pT) {
cout << pT.age << endl;
pT.age = 33;
}

// pT和t1的是两个不同的变量,这里只会修改pT变量,不会修改t1变量
void printfT3(Teacher pT) {
cout << pT.age << endl;
pT.age = 43;
}

int main() {
Teacher t1;
t1.age = 35;

// pT是指向t1的指针
printfT(&t1);
printf("t1.age:%d \n", t1.age);

// pT是t1的别名
printfT2(t1);
printf("t1.age:%d \n", t1.age);

// pT是形参,相当于t1复制一份数据给pT ---> pT = t1
printfT3(t1);
printf("t1.age:%d \n", t1.age);
return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
6
35
t1.age:23
23
t1.age:33
33
t1.age:33

引用的使用意义

  • 引用作为其它变量的别名而存在,因此在一些场合可以代替指针
  • 引用相对于指针来说,具有更好的可读性和实用性

使用引用和指针,分别实现交换两个数字的 C++ 代码如下:

cplusplus-2

引用的本质分析

  • 1)引用在 C++ 中的内部实现是一个常指针,Type & name --> Type * const name
  • 2)C++ 编译器在编译过程中,使用常指针作为引用的内部实现,因此引用所占用的内存空间大小与指针相同
  • 3)从使用的角度看,引用会让人误会其只是一个别名,没有自己的内存空间,这是 C++ 为了实用性而做出的细节隐藏
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

using namespace std;

// C++ 编译器发现函数参数是引用时,会自动转换为 int * const ref = &a
void testFunc(int &ref) {
ref = 100;
}

int main() {
int a = 10;
int &aRef = a; // C++ 编译器会自动转换为 int * const aRef = &a,这就能说明引用为什么必须初始化
aRef = 20; // C++ 编译器发现 aRef 是引用时,会自动转换为 *aRef = 20
testFunc(a);
cout << "a = " << a << endl;
cout << "aRef = " << aRef << endl;
}

参考下述代码,函数参数间接赋值(指针方式)成立的三个条件如下:

  • (a) 定义两个变量(一个形参一个实参)
  • (b) 建立关联,实参取地址传给形参
  • (c) 使用 *a 形参去间接的修改实参的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

using namespace std;

void func(int &a) {
a = 10;
}

void func(int *const a) {
*a = 15;
}

int main() {
int x = 5;
func(x);
cout << x << endl; // 10
func(&x);
cout << x << endl; // 15
return 0;
}

引用在实现上,只不过是把间接赋值成立的三个条件的后两步和二为一;当实参传给形参引用的时候,是 C++ 编译器帮程序员自动取了一个实参地址传给了形参引用(常量指针)。当我们使用引用语法的时,不需要关心编译器引用是怎么做的;当我们分析奇怪的语法现象时,我们才去考虑 C++ 编译器是怎么做的。

引用的注意事项

在 C++ 中使用引用时,需要注意以下几点:

  • 可以对数组建立引用。
  • & 在引用中不是求地址运算符,而是起标识作用。
  • 当函数的返回值是引用时,这个函数调用可以作为左值。
  • 当函数的返回值是引用时,不要返回局部变量的引用,否则会出现意想不到的结果。
  • 普通引用在声明时必须用其它的变量进行初始化,int &a; 这样的写法是错误的(在结构体内声明除外)。
  • 不允许有 NULL 引用,且引用必须是和一块合法的内存空间关联,int &a = 10; 这样的写法是错误的。
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <iostream>

using namespace std;

// 引用必须初始化
void test01() {
// int &a; 必须初始化,否则编译不通过
int b = 10;
int c = 20;
int &d = b;
// int &d = c; 引用初始化后不可修改,否则编译不通过

}

// 引用必须是和一块合法的内存空间关联
void test02() {
// int &a = 10; 引用必须是和一块合法的内存空间关联,否则编译不通过
}

// 函数的返回值是引用
int & hello() {
int a = 10;
// 返回局部变量的引用
return a;
}

// 当函数的返回值是引用时,不要返回局部变量的引用,否则会出现意想不到的结果
void test03() {
int &ref = hello();
cout << "ref = " << ref << endl; // 第一次正确打印值,是因为编译器做了优化
cout << "ref = " << ref << endl; // 第二次之后都无法正确打印值,会出现乱码现象
cout << "ref = " << ref << endl;
cout << "ref = " << ref << endl;
cout << "ref = " << ref << endl;
}

// 函数的返回值是引用
int & bye() {
static int a = 0;
// 返回全局变量的引用
return a;
}

// 当函数的返回值是引用时,这个函数调用可以作为左值
void test04() {
bye() = 100; // 相当于写了 a = 100
}

int main() {
test01();
test02();
test03();
test04();
return 0;
}

程序运行输出的结果如下:

1
2
3
4
5
ref = 10
ref = 32692
ref = 32692
ref = 32692
ref = 32692

函数返回值是引用

当函数返回值为引用时:

  • 若函数返回的是栈变量(局部变量,即作用域只在函数体内的变量),不能成为其它引用的初始值,不能作为左值使用
  • 若函数返回的是静态变量或全局变量,可以成为其他引用的初始值,即可作为右值使用,也可作为左值使用

函数返回值是基础类型当引用

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
29
30
31
32
33
34
#include <iostream>

using namespace std;

int getAA1() {
int a;
a = 10;
return a;
}

int & getAA2() {
int a;
a = 10;
return a;
}

int * getAA3() {
int a;
a = 10;
return &a;
}

int main() {
int a1 = getAA1();
int a2 = getAA2();
int &a3 = getAA2();
int *a4 = getAA3();

cout << "a1 = " << a1 << endl;
cout << "a2 = " << a2 << endl;
cout << "a3 = " << a3 << endl; // 这里用引用去接受函数的返回值,结果是不是乱码,关键是看返回的内存空间是不是被编译器回收了
cout << "a4 = " << *a4 << endl; // 这里用引用去接受函数的返回值,结果是不是乱码,关键是看返回的内存空间是不是被编译器回收了
return 0;
}

程序运行输出的结果如下:

1
2
3
4
a1 = 10
a2 = 10
a3 = 10 或者 a3 = 乱码
a4 = 10 或者 a4 = 乱码

函数返回值是 static 变量当引用

值得一提的是,static 关键字修饰变量的时候,变量是一个状态变量。

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
29
30
31
32
33
34
35
36
37
#include <iostream>

using namespace std;

int j() {
static int a = 10;
a++;
printf("a:%d \n", a);
return a;
}

int & j1() {
static int a = 10;
a++;
printf("a:%d \n", a);
return a;
}

int * j2() {
static int a = 15;
a++;
printf("a:%d \n", a);
return &a;
}

int main() {
// 错误写法,j()的运算结果是一个数值,没有内存地址,不能当左值,类似 11 = 100;
// j() = 3;

// 当被调用的函数当左值的时候,必须返回一个引用
j1() = 100;
j1();

*(j2()) = 200;
j2();
return 0;
}

程序运行输出的结果如下:

1
2
3
4
a:11
a:101
a:16
a:201

函数返回值是形参当引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

using namespace std;

int g1(int *p) {
*p = 100;
return *p;
}

int & g2(int *p) {
*p = 100;
return *p;
}

int main() {
int a1 = 10;
a1 = g2(&a1);
int &a2 = g2(&a1);
printf("a1:%d \n", a1);
printf("a2:%d \n", a2);
return 0;
}

程序运行输出的结果如下:

1
2
a1:100
a2:100

函数返回值是非基础类型

如果函数返回的引用不是基础类型,而是一个结构体或类,那么此时的情况非常复杂,涉及到拷贝构造函数和 = 操作符重载的知识内容,这里暂时不展开讨论。

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

using namespace std;

struct Teachar {
char name[64];
int age;
};

struct Teachar & OpTeacher(struct Teachar &t1) {

}

指针引用

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
29
30
31
32
33
34
35
36
37
#include <iostream>

using namespace std;

struct Person {
int age;
};

// 二级指针作函数参数
// 函数参数说明: **p 是 Person 对象,*p 是 Person 对象的指针,p 是指针的指针
void allocatMemory1(Person **p) {
*p = (Person *) malloc(sizeof(Person));
(*p)->age = 18;
}

void test01() {
Person *p = NULL;
allocatMemory1(&p);
cout << "age = " << p->age << endl;
}

// 指针引用作函数参数
void allocatMemory2(Person *&p) {
p = (Person *) malloc(sizeof(Person));
p->age = 20;
}

void test02() {
Person *p = NULL;
allocatMemory2(p);
cout << "age = " << p->age << endl;
}

int main() {
test01();
test02();
}

程序运行输出的结果如下:

1
2
age = 18
age = 20

常量引用

使用普通变量初始化 const 引用

在 C++ 中可以使用普通变量声明 const 引用,例如 const Type & name = var;,其中的 const 引用让普通变量拥有了只读属性,但可以使用指针的方式更改 const 引用的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

using namespace std;

int main() {
int a = 10;

// 使用普通变量初始化 const 引用
const int &b = a;

// b = 11; 错误写法,这里不能通过引用改变 a 的值,无法通过编译

// 可以使用指针的方式更改 const 引用的值
int * p = (int*) &b;
*p = 11;

printf("a:%d\n", a);
printf("b:%d\n", b);
printf("&a:%d\n", &a);
printf("&b:%d\n", &b);
return 0;
}

程序运行的输出结果如下:

1
2
3
4
a:11
b:11
&a:1323872140
&b:1323872140

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
#include <iostream>

using namespace std;

struct Teacher {
char name[64];
int age;
};

// const引用让变量(所指内存空间)拥有只读属性
void printTe(const Teacher &t) {
// t.age = 11; 是错误写法,无法通过编译
}

// const 修饰指针和指针指向的内容,那么指针指向的内容都不能更改
void printTe2(const Teacher *const pt) {
// pt->age = 11; 是错误写法,无法通过编译
}

int main() {
Teacher t1;
t1.age = 33;
printTe(t1);
printTe2(&t1);
return 0;
}

使用字面量常量初始化 const 引用

当使用字面量常量对 const 引用进行初始化时(如 const int &m = 10;),C++ 编译器会为常量值单独分配内存空间,并将引用名作为这段内存空间的别名;也就是会生成一个只读变量,但可以使用指针的方式更改 const 引用的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>

using namespace std;

int main() {

// 引用必须是和一块合法的内存空间关联,若不加 const 关键字,则编译失败
// int &a = 19;

// 使用字面量常量初始化 const 引用
const int &a = 10;

// 可以使用指针的方式更改 const 引用的值
int *p = (int *) &a;
*p = 20;

cout << "a = " << a << endl;
cout << "*p = " << *p << endl;

return 0;
}

程序运行的输出结果如下:

1
2
a = 20
*p = 20

使用 const 引用作为函数参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

using namespace std;

// 函数参数是 const 引用
void showValue(const int &val) {
// 无法直接更改形参的值,编译会不通过
// val = 10;

// 但可以使用指针的方式更改形参的值
int *p = (int *) &val;
*p = 30;
}

int main() {
int a = 10;
showValue(a);
cout << "a = " << a << endl;
return 0;
}

程序运行的输出结果如下:

1
a = 30

const 引用的使用总结

  • 普通引用语法 int &e = a; 相当于 int * const e = &a;普通引用的本质是常指针
  • 常量引用语法 const int &e; 相当于 const int * const e;
  • 当使用字面量常量对 const 引用进行初始化时(如 const int &m = 10;),C++ 编译器会为常量值单独分配内存空间,并将引用名作为这段内存空间的别名
  • 当使用字面量常量对 const 引用初始化后(如 const int &m = 10;),将生成一个只读变量,但可以使用指针的方式更改 const 引用的值