C++ 入门基础之二

const 关键字

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 常量的处理机制是,当碰见常量声明时,往符号表中放入常量;在编译过程中若发现使用常量,则直接以符号表中的值替换,例如在编译过程中若发现对 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 修饰函数参数可以分为三种情况:

A:值传递的 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;
}

B:当 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;
}

C:自定义类型的参数传递,需要使用临时对象复制参数,对于临时对象的构造,需要调用拷贝构造函数,比较浪费资源,因此可以采取 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 修饰函数的返回值可以分三种情况:

  • A: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;
}
  • B:const 修饰自定义类型的作为返回值,此时返回的值不能作为左值使用,既不能被赋值,也不能被修改。

  • C: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 指针 p 其指向的内存地址不能够被改变,但其内容可以改变。简称右定向,因为 const 位于 * 号的右边。

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

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

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 定义常量还可以用 const 定义常量,例如 const int c = 5; ≈ #define 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 常量:

  • 可能分配内存空间,也可能不分配内存空间
  • 当使用 & 操作符取 const 常量的地址时,会分配内存空间
  • const 常量为全局,并且需要在其它文件中使用,会分配内存空间
  • const int &a = 10;,即 const 修饰引用时,也会分配内存空间

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

引用(普通引用)

变量名回顾

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

引用的概念

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

  • a) 引用可以看作一个已定义变量的别名
  • b) 引用的语法:Type & name = var;
  • c) 引用作为函数参数声明时,不会进行初始化
  • d) 普通引用在声明时必须用其它的变量进行初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

using namespace std;

int main() {
{
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);
return 0;
}

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

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;
}

引用作函数参数

  • 普通引用在声明时必须用其它的变量进行初始化,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
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;
}

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

  • a) 定义两个变量(一个实参一个形参)
  • b) 建立关联,实参取地址传给形参
  • c) 使用 *a 形参去间接的修改实参的值

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

函数返回值是非基础类型

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

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
38
39
40
41
42
43
#include <iostream>
#include <string.h>

using namespace std;

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

// 二级指针作函数参数
int getTe(Teacher **myp) {
Teacher *p = (Teacher *) malloc(sizeof(Teacher));
if (p == NULL) {
return -1;
}
memset(p, 0, sizeof(Teacher));
p->age = 33;
*myp = p;
return 0;
}

// 指针引用作函数参数
int getTe2(Teacher *&myp) {
myp = (Teacher *) malloc(sizeof(Teacher));
if (myp == NULL) {
return -1;
}
myp->age = 34;
return 0;
}

int main() {
Teacher *p = NULL;
getTe(&p);
printf("age:%d \n", p->age);

Teacher *pp = NULL;
getTe2(pp);
printf("age:%d \n", pp->age);
return 0;
}

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

1
2
age:33
age:34

常引用

使用变量初始化 const 引用

在 C++ 中可以声明 const 引用,例如 const Type & name = var;,其中的 const 引用让变量拥有只读属性。

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

using namespace std;

int main() {
int a = 10;
const int &b = a;
// b = 11; 是错误写法,这里不能通过引用改变a的值,无法通过编译

// 只能用指针来改变引用的值
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 引用

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

using namespace std;

int main() {
const int b = 10;
printf("b:%d\n", &b);

// int &a = 19; 若不加const关键字,则编译失败

const int &a = 19;
printf("&a:%d \n", &a);
return 0;
}

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

using namespace std;

int main() {
// 普通引用
int a = 10;
int &b = a;

// 常量引用,让变量拥有只读属性
const int &c = a;

// 常量引用的初始化分为以下两种

// 1.用变量初始化常量引用
{
int x = 20;
const int &y = x;
printf("y:%d \n", y);
}

// 2.用字面量常量初始化常量引用
{
// int &m = 10; // 错误写法,引用是内存空间的别名,字面量10没有内存空间,没有方法做引用
const int &m = 10;
}
return 0;
}

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;),将生成一个只读变量,但可以使用指针的方式更改变量的值,示例代码如下:
1
2
3
4
5
6
7
8
9
10
11
#include <iostream>

using namespace std;

int main() {
const int &a = 100;
int *p = (int *) &a;
*p = 30;
cout << *p << endl;
cout << a << endl;
}

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

1
2
30
30