C++ 入门基础之二
大纲
const 关键字
const 简介
const
是 constant 的缩写,本意是不变的,不易改变的意思。在 C++ 中是用来修饰内置类型变量、自定义对象、成员函数、返回值、函数参数。C++ 的 const
关键字允许指定一个语义约束,编译器会强制实施这个约束,允许程序员告诉编译器某值是保持不变的。如果在编程中确实有某个值保持不变,就应该明确使用 const
,这样可以获得编译器的帮助。
1 |
|
在上述代码中,对于 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 |
|
程序运行的输出结果如下:
1 | 8 8 |
const 参数传递
对于 const
修饰函数参数可以分为三种情况:
A:值传递的 const
修饰传递,一般这种情况不需要 const
修饰,因为函数会自动产生临时变量复制实参值。
1 |
|
B:当 const
参数为指针时,可以防止指针被意外篡改。
1 |
|
C:自定义类型的参数传递,需要使用临时对象复制参数,对于临时对象的构造,需要调用拷贝构造函数,比较浪费资源,因此可以采取 const
外加引用传递的方式。并且对于一般的 int
、double
等内置类型,不需要采用引用的传递方式。
1 |
|
程序运行的输出结果如下:
1 | 8 |
const 函数返回值
对于 const
修饰函数的返回值可以分三种情况:
- A:
const
修饰内置类型(如int
、double
)的返回值,修饰与不修饰返回值的作用都一样。
1 |
|
B:
const
修饰自定义类型的作为返回值,此时返回的值不能作为左值使用,既不能被赋值,也不能被修改。C:
const
修饰返回的指针或者引用,是否返回一个指向const
的指针,取决于我们想让用户干什么。
const 修饰指针变量
const
修饰指针变量有以下三种情况:
- A:
const
修饰指针指向的内容,则内容为不可变量。 - B:
const
修饰指针,则指针为不可变量。 - C:
const
修饰指针和指针指向的内容,则指针和指针指向的内容都为不可变量。
对于 A,则指针指向的内容不可改变,简称左定值,因为 const
位于 *
号的左边。
1 | int a = 10; |
对于 B, const
指针 p 其指向的内存地址不能够被改变,但其内容可以改变。简称右定向,因为 const
位于 *
号的右边。
1 | int a = 8; |
对于 C,则是 A 和 B 合并的结果,即 const p
指向的内容和指向的内存地址都已固定,不可改变。
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 和 #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 |
|
程序运行的输出结果如下:
1 | a 12 |
引用是 C++ 的概念
引用属于 C++ 编译器对 C 语言的扩展,下述代码在 C 语言中不能通过编译,这里不要用 C 语言的语法去思考 b = 11
。
1 | int main() { |
引用作函数参数
- 普通引用在声明时必须用其它的变量进行初始化,
int &a;
这样的写法是错误的(在结构体内声明除外) - 引用作为函数参数声明时,不会进行初始化
1 |
|
程序运行输出的结果如下:
1 | 35 |
引用的使用意义
- 引用作为其它变量的别名而存在,因此在一些场合可以代替指针
- 引用相对于指针来说,具有更好的可读性和实用性
使用引用和指针,分别实现交换两个数字的 C++ 代码如下:
引用的本质分析
- 1)引用在 C++ 中的内部实现是一个常指针,
Type & name --> Type * const name
- 2)C++ 编译器在编译过程中,使用常指针作为引用的内部实现,因此引用所占用的内存空间大小与指针相同
- 3)从使用的角度看,引用会让人误会其只是一个别名,没有自己的内存空间,这是 C++ 为了实用性而做出的细节隐藏
1 |
|
参考上述代码,函数参数间接赋值(指针方式)成立的三个条件如下:
- a) 定义两个变量(一个实参一个形参)
- b) 建立关联,实参取地址传给形参
- c) 使用
*a
形参去间接的修改实参的值
引用在实现上,只不过是把间接赋值成立的三个条件的后两步和二为一;当实参传给形参引用的时候,是 C++ 编译器帮程序员自动取了一个实参地址传给了形参引用(常量指针)。当我们使用引用语法的时,不需要关心编译器引用是怎么做的;当我们分析奇怪的语法现象时,我们才去考虑 C++ 编译器是怎么做的。
函数返回值是引用
当函数返回值为引用时:
- 若函数返回的是栈变量(如作用域只在函数体内的变量),不能成为其它引用的初始值,不能作为左值使用
- 若函数返回的是静态变量或全局变量,可以成为其他引用的初始值,即可作为右值使用,也可作为左值使用
函数返回值是基础类型当引用
1 |
|
程序运行输出的结果如下:
1 | a1 = 10 |
函数返回值是 static 变量当引用
值得一提的是,static
关键字修饰变量的时候,变量是一个状态变量。
1 |
|
程序运行输出的结果如下:
1 | a:11 |
函数返回值是形参当引用
1 |
|
程序运行输出的结果如下:
1 | a1:100 |
函数返回值是非基础类型
如果函数返回的引用不是基础类型,而是一个类,那么此时的情况非常复杂,涉及到 copy
构造函数和 =
操作重载的知识内容,这里暂时不展开讨论。
1 |
|
指针引用
1 |
|
程序运行输出的结果如下:
1 | age:33 |
常引用
使用变量初始化 const 引用
在 C++ 中可以声明 const
引用,例如 const Type & name = var;
,其中的 const
引用让变量拥有只读属性。
1 |
|
程序运行的输出结果如下:
1 | a:11 |
1 |
|
使用字面量常量初始化 const 引用
1 |
|
const 引用综合使用示例
1 |
|
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 |
|
程序运行的输出结果如下:
1 | 30 |