深入浅出const和static关键字
const
const是constant的缩写,意为不变的。在 C++ 中是用来修饰内置类型变量,自定义对象,成员/普通函数,返回值,函数参数。C++ const 允许指定一个语义约束,编译器会强制实施这个约束,允许程序员告诉编译器某值是保持不变的。如果在编程中确实有某个值保持不变,就应该明确使用const,这样可以获得编译器的帮助。
const与宏定义
宏定义发生的时机在预处理阶段,如果出现bug,需要在运行时才能得知;而const修饰发生在编译阶段,如果出现bug,编译时就可以得知。const修饰的变量称为常量,常量在定义的时候必须初始化。
const修饰普通类型的变量
const int a = 0;
a = 1; // error
// 强行修改const修饰的变量的值,会产生未定义行为
#include<iostream>
using namespace std;
int main(void)
{
const int a = 7;
int *p = (int*)&a;
*p = 8;
cout<<a;
system("pause");
return 0;
}
// 从调试窗口看到 a 的值被改变为 8,但是输出的结果仍然是 7
不要轻易对 const 变量设法赋值,这会产生未定义行为。
const修饰指针变量
const 修饰指针变量有以下三种情况:
- 指针常量:const 修饰指针,指针为不可变量,即指针的指向不可改变
- 常量指针:const 修饰指针指向的内容,内容为不可变量
- 双const:const 修饰指针和指针指向的内容,则指针和指针指向的内容都为不可变量
int a = 8;
int* const p = &a; // 指针常量
int const *b = 8; // 常量指针
const int * const q = &a; // 双const
根据 const 位于 * 号的位置不同,总结:"左定值,右定向,const修饰不变量"
const修饰参数和修饰函数返回值
对于 const 修饰函数参数可以分为三种情况:
1、值传递的const修饰,一般这种情况不需要 const 修饰,因为函数会自动产生临时变量复制实参值
#include<iostream>
using namespace std;
void Cpf(const int a)
{
cout<<a;
// ++a; 是错误的,a 不能被改变
}
int main(void)
{
Cpf(8);
system("pause");
return 0;
}
2、指针传递的const修饰,可以防止指针被意外篡改
3、自定义类型的参数传递,需要临时对象复制参数,对于临时对象的构造,需要调用构造函数,比较浪费时间,因此我们采取 const 外加引用传递的方法。并且对于一般的 int、double 等内置类型,我们不采用引用的传递方式。
对于 const 修饰函数返回值同样可以分三种情况:
- const 修饰内置类型的返回值,修饰与不修饰返回值作用一样
- const 修饰自定义类型的作为返回值,此时返回的值不能作为左值使用,既不能被赋值,也不能被修改
- const 修饰返回的指针或者引用,是否返回一个指向 const 的指针,取决于我们想让用户干什么
const修饰类成员函数
const 修饰类成员函数,其目的是防止成员函数修改被调用对象的值。注意:const 关键字与 static 关键字不能同时修饰类成员函数,因为 static 关键字修饰静态成员函数,静态成员函数不含有 this 指针,即不能实例化,const 成员函数必须具体到某一实例。
顶层const与底层const
指针本身是一个对象,它又可以指向另外一个对象。因此,指针本身是不是常量以及指针所指的是不是一个常量就是两个相互独立的问题。用名词顶层 const (top-level const)表示指针本身是个常量,而用名词底层 const(low-level const)表示指针所指的对象是一个常量。更一般的,顶层 const 可以表示任意的对象是常量,这一点对任何数据类型都适用,如算术类型、类、指针等。底层 const 则与指针和引用等复合类型的基本类型部分有关。比较特殊的是,指针类型既可以是顶层 const 也可以是底层const,这一点和其他类型相比区别明显。
static
static 是 C/C++ 中很常用的修饰符,它被用来控制变量的存储方式和可见性。
在 C++ 中,需要一个数据对象为整个类而非某个对象服务,同时又力求不破坏类的封装性,即要求此成员隐藏在类的内部,对外不可见时,可将其定义为静态数据。
static与全局变量
对于全局变量和被static
修饰的变量,它们的生命周期确实在程序的整个执行期间都是存在的,但它们的作用域和可见性是有所不同的,这也是它们的区别所在。
全局变量:
- 作用域:全局变量的作用域是整个文件,也可以通过
extern
关键字在其他文件中访问。 - 可见性:全局变量在定义它们的文件中可见,可以被文件中的任何函数访问。
- 生命周期:全局变量在程序启动时被创建,在程序结束时销毁,因此它们的生命周期与整个程序的生命周期相同。
被static
修饰的变量:
- 作用域:被
static
修饰的变量的作用域是限定在定义它们的函数内部。 - 可见性:这些变量只能在定义它们的函数内部访问,对于其他函数而言是不可见的。
- 生命周期:被
static
修饰的变量在程序的整个执行期间都存在,但是它们的值在函数调用之间保持不变,因此可以看作是一个全局变量,但作用域被限制在了函数内部。
静态数据的存储
全局(静态)存储区:分为 DATA 段和 BSS 段。DATA 段(全局初始化区)存放初始化的全局变量和静态变量;BSS 段(全局未初始化区)存放未初始化的全局变量和静态变量。程序运行结束时自动释放。其中BBS段在程序执行之前会被系统自动清0,所以未初始化的全局变量和静态变量在程序执行之前已经为0。存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。
在 C++ 中 static 的内部实现机制:静态数据成员要在程序一开始运行时就必须存在。因为函数在程序运行中被调用,所以静态数据成员不能在任何函数内分配空间和初始化。
这样,它的空间分配有三个可能的地方,一是作为类的外部接口的头文件,那里有类声明;二是类定义的内部实现,那里有类的成员函数定义;三是应用程序的 main() 函数前的全局数据声明和定义处。
静态数据成员要实际地分配空间,故不能在类的声明中定义(只能声明数据成员)。类声明只声明一个类的"尺寸和规格",并不进行实际的内存分配,所以在类声明中写成定义是错误的。它也不能在头文件中类声明的外部定义,因为那会造成在多个使用该类的源文件中,对其重复定义。
static 被引入以告知编译器,将变量存储在程序的静态存储区而非栈上空间,静态数据成员按定义出现的先后顺序依次初始化,注意静态成员嵌套时,要保证所嵌套的成员已经初始化了。消除时的顺序是初始化的反顺序。
优势:可以节省内存,因为它是所有对象所公有的,因此,对多个对象来说,静态数据成员只存储一处,供所有对象共用。静态数据成员的值对每个对象都是一样,但它的值是可以更新的。只要对静态数据成员的值更新一次,保证所有对象存取更新后的相同的值,这样可以提高时间效率。
static 修饰的变量存放在全局数据区的静态变量区,包括全局静态变量和局部静态变量,都在全局数据区分配内存。初始化的时候自动初始化为 0。
小结
-
static可以限制变量或者函数的可见域
-
- static 修饰全局变量的时候,这个全局变量只能在本文件中访问,不能在其它文件中访问,即便是 extern 外部声明也不可以
- static 修饰一个函数,则这个函数的只能在本文件中调用,不能被其他文件调用
-
static可以延长局部变量的生命周期
-
考虑到数据安全性时可以使用static(当程序想要使用全局变量的时候应该先考虑使用 static)
static在C++中的使用
被 static 修饰的变量、被 static 修饰的方法统一属于类的静态资源,是类实例之间共享的,换言之,一处变、处处变。在 C++ 中,静态成员是属于整个类的而不是某个对象,静态成员变量只存储一份供所有对象共用。所以在所有对象中都可以共享它。使用静态成员变量实现多个对象之间的数据共享不会破坏隐藏的原则,保证了安全性还可以节省内存。
static修饰类的成员函数
static修饰的成员函数称为静态成员函数,它有以下特点:
- 静态成员函数没有隐含的this指针参数
- 静态成员函数不能访问非静态数据成员和非静态成员函数
- 非静态成员函数可以访问静态的数据成员和成员函数(因为静态成员属于类本身,在类的对象产生之前就已经存在了,所以在非静态成员函数中是可以调用静态成员的)
- 如果静态成员函数需要访问非静态成员函数或者非静态数据成员,可以使用传参的方式或者在函数中创建非静态对象的方法使用
- 静态成员函数可以使用类名加作用域限定符的方式来进行调用,这是其独有的方法
static修饰类的数据成员
static修饰的成员函数称为静态数据成员,它有以下特点:
- 静态数据成员可以实现多个对象之间的数据共享,它是类的所有对象的共享成员,它在内存中只占一份空间,如果改变它的值,则各对象中这个数据成员的值都被改变
- 静态数据成员是在程序开始运行时被分配空间,到程序结束之后才释放,只要类中指定了静态数据成员,即使不定义对象,也会为静态数据成员分配空间
- 静态数据成员可以被初始化,但是只能在类体外进行初始化,若未对静态数据成员赋初值,则编译器会自动为其初始化为 0
- 静态数据成员既可以通过对象名引用,也可以通过类名引用