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 和 #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 |
|
程序运行的输出结果如下:
1 | 0 1 2 3 4 5 6 7 8 9 |
引用作函数参数
- 普通引用在声明时必须用其它的变量进行初始化,
int &a;
这样的写法是错误的(在结构体内声明除外) - 引用作为函数参数声明时,不会进行初始化
1 |
|
程序运行输出的结果如下:
1 | 35 |
引用的使用意义
- 引用作为其它变量的别名而存在,因此在一些场合可以代替指针
- 引用相对于指针来说,具有更好的可读性和实用性
使用引用和指针,分别实现交换两个数字的 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 |
函数返回值是引用
当函数返回值为引用时:
- 若函数返回的是栈变量(局部变量,即作用域只在函数体内的变量),不能成为其它引用的初始值,不能作为左值使用
- 若函数返回的是静态变量或全局变量,可以成为其他引用的初始值,即可作为右值使用,也可作为左值使用
函数返回值是基础类型当引用
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
引用的值