C++ 入门基础之二
大纲
const
提示
const 关键字是 C++ 对 C 语言增强的一部分,详细介绍请看 这里。
const 简介
const 是 constant 的缩写,本意是不变的,不易改变的意思。在 C++ 中是用来修饰内置类型变量、自定义对象、成员函数、返回值、函数参数等。C++ 的 const 关键字允许指定一个语义约束,编译器会强制实施这个约束,允许程序员告诉编译器某个值是保持不变的。如果在编程中确实有某个值保持不变,就应该明确使用 const,这样可以获得编译器的帮助。
1 |
|
在上述代码中,对于 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 |
|
程序运行的输出结果如下:
1 | 8 8 |
const 参数传递
对于
const修饰函数参数可以分为以下三种情况:
第一种情况:值传递的 const 修饰传递,一般这种情况不需要 const 修饰,因为函数会自动产生临时变量复制实参值。
1 |
|
第二种情况:当 const 参数为指针时,可以防止指针被意外篡改(指向其他内存地址)。
1 |
|
第三种情况:自定义类型的参数传递,需要使用临时对象复制参数;由于临时对象的构造需要调用拷贝构造函数,这个过程比较浪费资源,因此可以采取 const 外加引用传递的方式。并且对于一般的 int、double 等内置类型,不需要采用引用的传递方式。
1 |
|
程序运行的输出结果如下:
1 | 8 |
const 函数返回值
对于
const修饰函数的返回值可以分为以下三种情况:
- 第一种情况:当
const修饰内置类型(如int、double)的返回值,修饰与不修饰返回值的作用都一样。
1 |
|
第二种情况:
const修饰自定义类型作为返回值,此时返回的值不能作为左值使用,既不能被赋值,也不能被修改。第三种情况:
const修饰返回的指针或者引用,是否返回一个指向const的指针,取决于我们想让用户干什么。
const 修饰指针变量
const 修饰指针变量有以下三种情况:
- A:
const修饰指针指向的内容,则内容为不可变量。 - B:
const修饰指针,则指针为不可变量。 - C:
const修饰指针和指针指向的内容,则指针和指针指向的内容都为不可变量。
对于 A,指针指向的内容不可改变,简称左定值,因为 const 位于 * 号的左边。
1 | int a = 10; |
对于 B, const 指针其指向的内存地址不能够被改变,但其内容可以改变,简称右定向,因为 const 位于 * 号的右边。
1 | int a = 8; |
对于 C,则是 A 和 B 合并的结果,即 const 指针指向的内容和指向的内存地址都已固定,不可改变。
1 | int a = 8; |
对于 A、B、C 三种情况,根据 const 相对于 * 号的位置不同,可以总结出三句便于记忆的话: 左定值,右定向,const 修饰不变量。
const 修饰类成员函数
const 修饰类成员函数,其目的是防止成员函数修改被调用对象的值,如果我们不想修改一个调用对象的值,所有的成员函数都应当声明为 const 成员函数,此时 const 本质上修饰的是 this 指针。值得一提的是,const 关键字不能与 static 关键字同时使用,因为 static 关键字修饰静态成员函数,而静态成员函数不含有 this 指针,即不能实例化,但 const 成员函数必须关联某一对象实例。
1 |
|
程序运行的输出结果如下:
1 | 8 |
上面的 int get_cm() const {} 函数用到了 const 成员函数,如果 int get_cm() {} 去掉 const 修饰,则 Cmf 函数传递的 const _tt 即使没有改变对象的值,编译器也认为函数 int get_cm() {} 会改变对象的值,所以我们尽量按照要求将所有的不需要改变对象内容的函数都作为 const 成员函数。下述两种的写法都是合法的,效果都一样,C++ 中一般将 const 写在函数的末尾处。
1 | int get_cm() const { |
如果有个成员函数想修改对象中的某一个成员怎么办?这时我们可以使用 mutable 关键字修饰这个成员,mutable 的意思也是易变的,容易改变的意思,被 mutable 关键字修饰的成员可以处于不断变化中,如下面的例子:
1 |
|
程序运行的输出结果如下:
1 | 8 8 |
这里在函数 void Kf() const {} 中可以通过 ++_ct; 修改 _ct 的值,但是通过 ++_cm 修改 _cm 则会报错,因为 _cm 没有用 mutable 修饰。
const 针对指针的不同写法
| 写法 | 含义(读法) | 什么是常量 | 类型分类 |
|---|---|---|---|
const char * p | 指向 const char 的指针 | 指针所指向的对象是常量 | 常量指针(pointer to const) |
char const * p | 指向 const char 的指针 | 指针所指向的对象是常量 | 常量指针(pointer to const) |
char * const p | 指针 p 本身是 const,但可以改指向的内容 | 指针本身是常量 | 指针常量(const pointer) |
const char * const p | 指向 const char 的指针常量 | 指针本身、指向的数据都是常量 | 指向常量的指针常量(const pointer to const) |
char const * const p | 指向 const char 的指针常量 | 指针本身、指向的数据都是常量 | 指向常量的指针常量(const pointer to const) |
| 写法 | 口诀判断(左定值,右定向) | 指针是否可变 | 指向的数据是否可变 | 结果 |
|---|---|---|---|---|
const char * p | const 在 * 左边(定值) | 是 | 否 | 常量指针(指针可变,指向的数据不可改) |
char const * p | const 在 * 左边(定值) | 是 | 否 | 常量指针(指针可变,指向的数据不可改) |
char * const p | const 在 * 右边(定向) | 否 | 是 | 指针常量(指针不可变,指向的数据可改) |
const char * const p | 左定值 + 右定向 → 两者都受限 | 否 | 否 | 指向常量的指针常量(指针和指向的数据都不可变) |
char const * const p | 左定值 + 右定向 → 两者都受限 | 否 | 否 | 指向常量的指针常量(指针和指向的数据都不可变) |
(1)
const char *- 含义:
- 指向
const char的指针(常量指针)
- 指向
- 特性:
- 指针:可以改
- 指向的数据:不能改
- 示例:
1
2
3
4
5
6const char str[] = "Hello";
const char other[] = "World";
const char *p = str;
p = other; // 正确写法:指针可变
*p = 'A'; // 错误写法:不能修改指针所指向的内容 - 注意:
- 跟
char const *表示的意义完全一致(const放在类型前或后含义都相同),只是写法不同
- 跟
- 含义:
(2)
char const *- 含义:
- 指向
const char的指针(常量指针)
- 指向
- 特性:
- 指针:可以改
- 指向的数据:不能改
- 示例:
1
2
3
4
5
6const char str[] = "Hello";
const char other[] = "World";
char const *p = str;
p = other; // 正确写法:指针可变
*p = 'A'; // 错误写法:不能修改指针所指向的内容 - 注意:
- 跟
const char *表示的意义完全一致(const放在类型前或后含义都相同),只是写法不同
- 跟
- 含义:
(3)
char * const- 含义:
const指针(指针常量)指向char
- 特性:
- 指针:不能改
- 指向的数据:可以改
- 示例:
1
2
3
4
5
6const char str[] = "Hello";
const char other[] = "World";
char * const p = str;
p = other; // 错误写法:指针本身不能变
*p = 'A'; // 正确写法:可以修改指针所指向的内容
- 含义:
(4)
const char * const p- 含义:
- 指向
const char的const指针(指向常量的指针常量) - 也就是:指针本身不能变,指向的数据也不能变
- 指向
- 特性:
- 指针:不能改
- 指向的数据:不能改
- 示例:
1
2
3
4
5
6
7const char str[] = "Hello";
const char other[] = "World";
const char * const p = str;
p = other; // 错误写法:指针 p 是 const,不可改变
*p = 'A'; // 错误写法:指向的数据是 const,不可修改
char c = p[0]; // 正确写法:可以读取数据 - 注意:
- 跟
char const * const p表示的意义完全一致(const放在类型前或后含义都相同),只是写法不同
- 跟
- 含义:
(5)
char const * const p- 含义:
- 指向
const char的const指针(指向常量的指针常量) - 也就是:指针本身不能变,指向的数据也不能变
- 指向
- 特性:
- 指针:不能改
- 指向的数据:不能改
- 示例:
1
2
3
4
5
6
7const char str[] = "Hello";
const char other[] = "World";
char const * const p = str;
p = other; // 错误写法:指针 p 是 const,不可改变
*p = 'A'; // 错误写法:指向的数据是 const,不可修改
char c = p[1]; // 正确写法:可以读取数据 - 注意:
- 跟
const char * const p表示的意义完全一致(const放在类型前或后含义都相同),只是写法不同
- 跟
- 含义:
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变量不是编译器在编译时可以确定的常量(即不属于编译期常量),而是运行时的常量,所以const变量不可以作为数组的大小
C++ 的 const 常量:
- C++ 中的
const常量属于真常量 - C++ 中,不可以通过操作指针的方式来修改
const常量的值 - C++ 中,
const常量可能分配内存空间,也可能不分配内存空间 - 当使用
&操作符取const常量的地址时,编译器会临时开辟一块内存空间 - 当
const常量为全局,并且需要在其它文件中使用(利用extern外部链接),编译器会分配内存空间 - 当使用字面量常量初始化
const引用(即常量引用),如const int &a = 10;,编译器会分配内存空间 - C++ 中,
const常量是编译器在编译时可以确定的常量(即属于编译期常量),而不是运行时的常量。在编译期间所有出现const常量名称的地方,都会被const常量的初始化值替换掉,所以const常量可以作为数组的大小 - C++ 中,
const常量使用字面量来初始化(如const int a = 10;)和使用变量来初始化(如int b = 10; const int a = b;)是有区别的,前者不可以通过操作指针的方式来修改常量的值,而后者却是可以(通常也叫做常变量,其特性跟 C 语言的const变量一样)
注意
C++ 编译器虽然可能为 const 常量分配内存空间,但不会使用其内存空间中的值,而且是在编译器的编译阶段分配内存空间
普通引用
变量名回顾
- 变量名实质上是一段连续内存空间的别名,是一个标号(门牌号)
- 程序中通过变量来申请并命名内存空间
- 通过变量的名称可以使用内存空间
引用的概念
在 C++ 中新增加了引用的概念:
- (a) 引用可以看作一个已定义变量的别名
- (b) 引用的语法:
Type & 别名 = 原名 - (c) 引用作为函数参数声明时,不会进行初始化
- (d) 普通引用在声明时必须用其它的变量进行初始化
- (e) 当
&写在左侧叫引用,如int &b = a;,当&写在右侧叫取地址,如int *p = (int *) &a;
1 |
|
程序运行的输出结果如下:
1 | a = 12 |
引用是 C++ 的概念,属于 C++ 编译器对 C 语言的扩展,下述代码在 C 语言中不能通过编译,这里不要用 C 语言的语法去思考
b = 11。
1 | int main() { |
引用的使用意义
- 引用作为其它变量的别名而存在,因此在一些场合可以代替指针
- 引用相对于指针来说,具有更好的可读性和实用性
使用引用和指针,分别实现交换两个数字的 C++ 代码如下:

引用的本质分析
- 1)引用在 C++ 中的内部实现是一个常指针,
Type & name --> Type * const name - 2)C++ 编译器在编译过程中,使用常指针作为引用的内部实现,因此引用所占用的内存空间大小与指针相同
- 3)从使用的角度看,引用会让人误会其只是一个别名,没有自己的内存空间,这是 C++ 为了实用性而做出的细节隐藏
1 |
|
参考下述代码,函数参数间接赋值(指针方式)成立的三个条件如下:
- (a) 定义两个变量(一个形参一个实参)
- (b) 建立关联,实参取地址传给形参
- (c) 使用
*a形参去间接的修改实参的值
1 |
|
引用在实现上,只不过是把间接赋值成立的三个条件的后两步和二为一;当实参传给形参引用的时候,是 C++ 编译器帮程序员自动取了一个实参地址传给了形参引用(常量指针)。当我们使用引用语法的时,不需要关心编译器引用是怎么做的;当我们分析奇怪的语法现象时,我们才去考虑 C++ 编译器是怎么做的。
引用的注意事项
在 C++ 中使用引用时,需要注意以下几点:
- 可以对数组建立引用。
&在引用中不是求地址运算符,而是起标识作用。- 当函数的返回值是引用时,这个函数调用可以作为左值。
- 当函数的返回值是引用时,不要返回局部变量的引用,否则会出现意想不到的结果。
- 普通引用在声明时必须用其它的变量进行初始化,
int &a;这样的写法是错误的(在结构体内声明除外)。 - 不允许有 NULL 引用,且引用必须是和一块合法的内存空间关联,
int &a = 10;这样的写法是错误的。
1 |
|
程序运行输出的结果如下:
1 | ref = 10 |
对数组建立引用
在 C++ 中,对数组建立引用有两种常用的方式,代码如下:
1 |
|
程序运行的输出结果如下:
1 | 0 1 2 3 4 5 6 7 8 9 |
引用作函数参数
- 普通引用在声明时必须用其它的变量进行初始化,
int &a;这样的写法是错误的(在结构体内声明除外) - 引用作为函数参数声明时,不会进行初始化
1 |
|
程序运行输出的结果如下:
1 | 35 |
引用作函数返回值
当函数返回值为引用时:
- 若函数返回的是栈变量(局部变量,即作用域只在函数体内的变量),不能成为其它引用的初始值,不能作为左值使用
- 若函数返回的是静态变量或全局变量,可以成为其他引用的初始值,即可作为右值使用,也可作为左值使用
函数返回值是基础类型当引用
1 |
|
程序运行输出的结果如下:
1 | a1 = 10 |
函数返回值是 static 变量当引用
值得一提的是,static 关键字修饰变量的时候,变量是一个状态变量。
1 |
|
程序运行输出的结果如下:
1 | a:11 |
函数返回值是形参当引用
1 |
|
程序运行输出的结果如下:
1 | a1:100 |
函数返回值是非基础类型
如果函数返回的引用不是基础类型,而是一个结构体或类,那么此时的情况非常复杂,涉及到拷贝构造函数和 = 操作符重载的知识内容,这里暂时不展开讨论。
1 |
|
指针引用
1 |
|
程序运行输出的结果如下:
1 | age = 18 |
常量引用
使用普通变量初始化 const 引用
在 C++ 中可以使用普通变量声明 const 引用,例如 const Type & name = var;,其中的 const 引用让普通变量拥有了只读属性,但可以使用指针的方式更改 const 引用的值。
1 |
|
程序运行的输出结果如下:
1 | a:11 |
1 |
|
使用字面量常量初始化 const 引用
当使用字面量常量对 const 引用进行初始化时(如 const int &m = 10;),C++ 编译器会为常量值单独分配内存空间,并将引用名作为这段内存空间的别名;也就是会生成一个只读变量,但可以使用指针的方式更改 const 引用的值。
1 |
|
程序运行的输出结果如下:
1 | a = 20 |
使用 const 引用作为函数参数
1 |
|
程序运行的输出结果如下:
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引用的值
