存储持续性
在C++中,数据的持续性指的是数据存储的生命周期。C++中有多种方式来实现数据的持续性,包括:
-
自动存储持续性:在函数内部定义的变量(包括函数参数)的存储持续性是自动的,其生命周期与其所在的作用域相同。当程序执行离开变量的作用域时,自动变量将被销毁。C++有两种存储持续性为自动的变量。
-
静态存储持续性:在函数定义外定义的变量和使用关键字static定义的变量的存储持续性都为静态,其生命周期从程序开始到程序结束。静态变量在内存中只有一份拷贝,并且在整个程序执行过程中可见。C++有3种存储持续性为静态的变量。
-
动态存储持续性(动态内存分配):使用new关键字可以在堆上动态地分配内存,使用delete关键字可以释放内存。动态分配的内存在使用后需要手动释放,否则会造成内存泄漏。
-
全局变量:全局变量定义在函数外部,其生命周期从程序开始到程序结束。全局变量在内存中只有一份拷贝,并且在整个程序执行过程中可见。
-
文件存储:数据可以存储在文件中,通过文件操作来读写数据。文件存储的数据持续性取决于文件的生命周期。
这些都是常见的实现数据持续性的方式,具体使用哪种方式取决于数据的需求和程序的设计。
作用域和链接性
作用域和链接是C++中用于控制变量和函数可见性和访问性的重要概念。
-
作用域(scope):作用域指的是一个标识符(变量、函数、类等)在程序中可见的范围。C++中有以下几种作用域:
- 块作用域(block scope):在代码块(花括号括起的一系列语句)中定义的变量和函数只在该块中可见,称为块作用域。
- 函数作用域(function scope):在函数内部定义的变量和函数只在该函数中可见,称为函数作用域。
- 文件作用域(file scope):在函数外部定义的变量和函数具有文件作用域,可以在整个文件中访问。
- 命名空间作用域(namespace scope):命名空间内定义的变量和函数只在该命名空间中可见。
-
链接(linkage):链接指的是在不同的编译单元(源文件)中如何访问变量和函数。C++中有以下几种链接类型:
- 外部链接(external linkage):具有外部链接的变量和函数可以在不同的文件中进行访问和共享。通常用extern关键字来定义具有外部链接的变量和函数。
- 内部链接(internal linkage):具有内部链接的变量和函数只在同一个文件中可见,不能在其他文件中访问。通常用static关键字来定义具有内部链接的变量和函数。
- 无链接(no linkage):具有无链接的变量和函数只在其定义的作用域内可见,不能在其他文件中访问。例如,局部变量和函数参数具有无链接。
作用域和链接的概念在编写C++程序时非常重要,可以帮助我们控制变量和函数的可见性,避免命名冲突和访问错误。
自动存储持续性
在函数中声明的函数参数和变量
在默认情况下,在函数中声明的函数参数和变量的存储持续性为自动,作用域为局部,没有链接性。
话不多说,上例子
void A(int a)
{
a=a-1;
int b=a;
}
int main()
{
int a=1;
int b=2;
A(a);
}
在A函数里面声明了变量b,在main函数里面也声明了变量b,但我们会发现,对A函数里面的b执行的任何操作都不会影响mian()里的b,同理函数参数a也是如此
代码块里定义的变量
代码块:由花括号括起的一系列语句
如果在代码块里定义了变量,则该变量的存在时间和作用域将被限制在该代码块。
话不多说,上例子
#include<iostream>
using namespace std;
int main()
{
int a = 9;
cout << a << endl;//结果是9
//代码块
{
int b = 8;
cout << a << endl;//结果是8
cout << b << endl;//结果是9
}
cout << b << endl;//这不可以
}
从上面这个例子得出,b的作用域被限制在它所在的代码块里,同时我们也发现在代码块外定义的变量a在内部代码块和外部代码块都是可见的
那如果将内部的变量命名为a,使得代码块内部和外部有两个同名的变量,这时情况会如何呢?
话不多说,上例子
#include<iostream>
using namespace std;
int main()
{
int a=9;
cout<<a<<endl;//结果是9
//代码块
{
int a=8;
cout<<a<<endl;//结果是8
}
cout<<a<<endl;//结果是9
}
结果是9 8 9,这表明程序在执行代码块里的语句时,将使用代码块内部定义的a,将a解释为局部变量,新的定义隐藏了旧的定义,新定义可见,旧定义暂时不可见,在程序离开该代码块时,旧定义又重新可见
自动变量的初始化
C++中,自动变量的初始化有两种方式:
- 声明的同时进行初始化:可以在变量声明的同时赋初值,例如:
int num = 10;
- 在声明后进行赋值:可以在声明变量后的任意位置进行赋值操作,例如:
int num;
num = 10;
需要注意的是,如果在函数内部声明的自动变量没有手动进行初始化操作,那么它的值将是不确定的。因此,为了确保变量的可预测性和正确性,建议在声明变量时进行初始化。
寄存器变量
在C++中,使用register
关键字可以声明寄存器变量。寄存器变量会被存储在寄存器中,而不是内存中,以提高访问速度。然而,现代编译器通常会自动管理寄存器变量的分配,因此register
关键字的使用已经不再常见,而且可能被忽略。
关键字register只是显式的指出变量是自动的
要声明寄存器变量,只需在变量声明前加上register
关键字,例如:
register int num;
需要注意的是,寄存器变量是有限的,编译器可以根据需要选择是否将变量存储在寄存器中。如果无法将变量分配到寄存器中,变量将自动转换为普通的局部变量。因此,register
关键字只是给编译器一个提示,但不能保证变量实际上会被存储在寄存器中。
此外,寄存器变量不能被取地址(即不能使用取地址运算符&
),因为寄存器变量没有内存地址。对寄存器变量使用&
运算符会导致编译错误。
静态持续变量
和c语言一样,c++也为静态存储持续性变量提供了3种链接性:外部链接性(可在其他文件中访问),内部链接性(只能在当前文件访问),无链接性(只能在当前函数或代码块中访问)。
这3种链接性都在整个程序执行期间存在,与自动变量相比,它们的寿命更长。它们不存放在栈里。编译器会分配固定的内存块来存储所有的静态变量,这些变量在整个程序执行期间一直存在。
另外,如果没有显式的初始化静态变量,编译器将把它设置为0。在默认情况下,静态数组和结构将每个元素或成员的所有位都设置为0。
3种静态持续变量的特点
所有静态持续变量在整个程序执行期间都存在
想创建链接性为外部的静态持续变量,必须在代码块的外面声明它;
想创建链接性为内部的静态持续变量,必须在代码块的外面声明它,并用static限定符;
想创建没有链接性的静态持续变量,必须在代码块的内部声明它,并用static限定符
话不多说,上例子
...
int a=100;//外部链接性
static int b=10;//内部链接性
...
int main()
{
...
}
void A(int u)
{
static int c=1;//无链接性
int f=9;
......
}
a,b,c在整个程序执行期间都存在
a,b的作用域都为整个文件,即在从声明位置到文件结尾的范围都可以被使用,更具体的说,可以在main(),A()里使用它们。它们的区别是b的链接属性是内部,因此只能在包含上述代码的文件里面使用,a的链接属性是外部,因此可以在程序的其他文件中使用它
在A()里声明的变量c的作用域为局部,没有链接性,这意味着只能在A()里调用它,就像变量f一样,与变量f不同的是,即使A()函数没有被执行时,变量c也留在内存里
5种变量存储方式
存储描述 | 持续性 | 作用域 | 链接性 | 如何声明 |
自动 | 自动 | 代码块 | 无 | 在代码块中 |
寄存器 | 自动 | 代码块 | 无 | 在代码块中,用关键字register |
静态,无链接性 | 静态 | 代码块 | 无 | 在代码块中,使用关键字static |
静态,外部链接性 | 静态 | 文件 | 外部 | 不在任何函数中 |
静态,内部链接性 | 静态 | 文件 | 内部 | 不在任何函数中,使用关键字static |
静态变量的初始化
除了默认的零初始化,还可以对静态变量进行常量表达式初始化和动态初始化。
零初始化意味着将变量设置为0,对于标量类型,0将被强制转换为合适的类型。
零初始化和常量表达式初始化被统称为静态初始化,这意味着编译器处理文件(翻译单元)时初始化变量。动态初始化意味着变量将在编译后初始化。
那么初始化形式由什么因素决定呢?
首先所有静态变量都将被零初始化,而不管程序员是否显式地初始化了它。接下来,如果使用了常量表达式初始化了变量,且编译器仅根据文件内容(被包含的头文件)就可计算表达式,编译器将执行常量表达式初始化。必要时,编译器将进行简单计算。如果没有足够的信息,变量将被动态初始化
话不多说,上例子
#include<cmath>
int x;
int y=5;
const double a=4.0*atan(1.0);
首先x,y,a,被零初始化,然后编译器将计算常量表达式,并将y初始化为5,但要初始化a,必须要调用函数atan(),这需要等到该函数被链接且程序执行时
外部链接性的静态持续变量
链接性为外部的变量通常简称为外部变量(也叫全局变量),它们的存储持续性为静态,作用域为整个文件。
外部变量是在函数外部定义的,因此对所有函数而言都是外部的(包括main()函数)。可以在main()前面或者头文件里定义它们,可以在文件中位于外部变量定义后的任何函数中使用它。
单定义原则
单定义原则,简而言之就是一个变量只能有一次定义声明
定义声明(简称定义):它为变量分配存储空间;
引用声明(简称声明):它不给变量分配存储空间,因为它引用已有的变量;引用声明使用关键字extern,且不进行初始化;
有的人就想问了啊,如果既用了extern关键字又对变量进行初始化了,那情况会怎么样?
答案是编译器会将这视作定义声明
话不多说,上例子
int a;//定义声明
extern int b;//这是引用声明
extern int c=3;//这被视作定义声明
如果要在多个文件使用外部变量,只需在一个文件中包含该变量的定义(单定义规则),但在使用该变量的其他所有文件中,都必须使用关键字extern声明它
话不多说,上例子(注:A.cpp,B.cpp是同一个程序的)
A.cpp文件(展示定义声明)
extern int a=2;
int b=1;
int c;
//以上都是定义声明
//c会被零初始化
...
int main()
{
...
}
B.cpp文件(展示引用声明)
extern int a;
extern int b;
extern int c;
//以上都是引用声明
...
int main()
{
...
}
注意:在多文件程序里面,可以在一个文件(且只能在一个文件)中定义一个外部变量。使用该变量的其他文件必须用extern声明它
全局变量和局部变量同名
请注意啊,单定义规则并不意味着不能有多个变量的名称相同。当出现多个同名的变量,会擦出什么样的火花呢?
话不多说,上例子
我们接着上面这个A,B文件啊,假设把B文件设置为下面这样子
extern int a;
extern int b;
extern int c;
//以上都是引用声明
...
void D()
{
cout<<a<<endl;//使用外部变量,结果是2
int a=99;
cout<<a<<endl;//使用局部变量,结果是99
}
int main()
{
...
D();
cout<<a<<endl;//全局变量,结果是2
}
结果很明显啊,新定义的会覆盖掉全局变量,但是出了局部变量的作用域后,全局变量将不会被覆盖
全局变量和局部变量
全局变量和局部变量都有各自的用途和优劣势,具体使用哪种要根据具体情况来决定。
全局变量的好处是可以在程序的任何地方都能访问和使用,可以方便地在多个函数之间共享数据。然而,过多地使用全局变量可能导致程序结构不清晰,难以维护和调试。
局部变量的好处是只在特定的函数或代码块中生效,能够提供更好的封装性和代码的可读性。此外,局部变量在使用完后会自动销毁,不会占用过多的内存空间。然而,局部变量的作用范围有限,只能在定义它的函数或代码块中使用。
因此,一般来说,尽量使用局部变量,只在必要的情况下使用全局变量。使用全局变量时要注意避免滥用,合理命名,避免命名冲突,并且要及时释放不再使用的全局变量。
内部链接性的静态持续变量
将static限定符用于作用域为整个文件的变量时,该变量的链接性将为内部的。
在多文件程序中,内部链接性和外部链接性的差别很有意义
我们看个例子
A.cpp
...
int a=10;
..
int main()
{
...
}
B.cpp
..
int a=10;
..
int main()
{
...
}
这种做法是不行的,因为它违反了单定义的规则
这时我们可以用static关键字
A.cpp
...
int a=10;
..
int main()
{
...
}
B.cpp
..
static int a=9;
..
int main()
{
...
cout<<a<<endl;//结果是9
}
这就没有任何问题了,这样子A,B文件里面的a就是不同的变量了
无链接性的静态持续变量
将static限定符用于在代码块里定义的变量,这将导致局部变量的存储持续性为静态的。
这意味着虽然该变量只在该代码块中可用,但它在该代码块不处于活动状态时仍然存在。
因此在两次函数调用中,静态局部变量的值将保持不变。
另外,如果初始化了静态局部变量,则程序只在启动时进行一次初始化。以后再调用函数时,将不会像自动变量那样再次被初始化。
下面看个例子
#include<iostream>
using namespace std;
void A()
{
static int a;
cout << a << endl;
a = a + 1;
cout << a << endl;
}
int main()
{
A();
A();
}
结果是0 1 1 2 ,第一次打印出0是因为零初始化,后面两次调用a的值都是从上一次调用完A函数的a的值的基础上去改动的
我们可以对比一下普通变量
#include<iostream>
using namespace std;
void B()
{
int b=0;
cout << b << endl;
b = b + 1;
cout << b << endl;
}
int main()
{
B();
B();
}
结果是0 1 0 1,每次B函数被调用时,自动变量都会被重置为0
const变量的链接性
在c++中,const限定符对默认存储类型稍有影响。
在默认情况下全局变量的链接性为外部的,但是const全局变量的链接性为内部的。
也就是说,在C++看来,全局const定义就像使用了static说明符一样。
const int a=0;
//相当于static int a=0;
...
int main()
{
...
}
这也意味着,可以在所有文件中使用相同的声明
我们看个例子
A.cpp
...
const int a=10;
..
int main()
{
...
}
B.cpp
..
const int a=10;
..
int main()
{
...
}
这样子做完全没有问题
为什么可以把const变量定义放在头文件?
因为const的内部链接性,每个文件都有自己的一组常量,而不是所有文件共享一组常量。每个定义都是其所属文件私有的。
把const的链接性改为外部的
这种情况下,必须在所有使用该常量的文件中使用extern关键字来声明它(定义声明和引用声明都要用extern)。不过定义声明只能有一次。
这与常规外部变量不同,定义常规外部变量时,不必使用extern关键字,但是在使用该变量的其他文件中必须使用extern。
我们看个例子
A.cpp
...
extern const int a=10;//必须要extern
..
int main()
{
...
}
B.cpp
..
extern const int a;
..
int main()
{
...
}
这完全ok
在函数和代码块声明const
这时其作用域为代码块,即仅当程序执行该代码块中的代码时,该常量才是可用的(和static一样)
函数链接性
和变量一样,函数也有链接性
和c语言一样,C++不允许在一个函数里定义另一个函数
所有函数的存储连续性都自动为静态的,即在整个程序执行期间一直存在。
在默认情况下,函数的链接性是外部的,即可以在文件间共享。
实际上,可以在函数原型使用关键字extern来指出函数是在另一个文件里定义的,不过这可写可不写
我们还可以用static关键字来将函数的链接性改为内部的,使之只能在同一个文件里使用。不过必须在函数原型和函数定义里同时使用static修饰
...
static int A();
...
static int A()
{
...
}
这意味这个函数只能在这个文件可见,还意味着在别的文件定义同名的函数。和变量一样,在定义静态函数的文件中,静态函数将覆盖外部定义,因此即使在外部定义了同名的函数,该文件仍使用静态函数
同时函数也遵循单定义规则,即只能有一个函数的定义只能放在一个文件里,每个使用该函数的文件都应该包含其函数原型
但是内联函数不遵循这项规则的约束,所以内联函数的定义可以放在头文件里,这样包含了头文件的每个文件都有内联函数的定义
C++在哪里查找函数?
如果该文件中的函数原型指出该函数是静态的,则编译器将只在该文件中查找函数定义;否则,编译器将在所有的程序文件中查找。如果找到两个定义,编译器将发出错误信息,因为每个外部函数只能有一个定义,如果在程序中没有找到,编译器将在库里找。
有的人就想了啊,如果定义了一个与库函数同名的函数,编译器将使用程序员定义的版本,而不是库函数吗?
答案是不可能,因为C++禁止使用标准库函数的名字来做标识符;
动态分配链接性
首先我想说的是编译器使用三块独立的内存,一块用于静态变量,一块用于自动变量,另一块用于动态存储。
动态存储由运算符new和delete控制,而不是由作用域和链接属性控制。因此可以在一个函数中分配内存,而在另外一个函数中将其释放。
在C++中,动态分配的内存具有默认的动态链接属性。这意味着通过关键字new
分配的内存是在堆上分配的,并且可以在整个程序的生命周期内可见。
具体来说,动态分配的内存链接属性如下:
-
全局可见性:通过
new
分配的内存可以在整个程序中使用。即使在函数内部分配,也可以在其他函数中访问。 -
生命周期:通过
new
分配的内存一直存在,直到使用delete
关键字显式释放它。它的生命周期不受函数或代码块的限制。 -
外部链接性:动态分配的内存默认具有外部链接性,这意味着可以在不同的编译单元(源文件)中访问和使用它。只需要在别的文件使用extern限定符作引用声明即可
可以看个例子
A.cpp
..
int main()
{
int*a=new int;
...
}
B.cpp
extern int*a;//引用声明
...
int main()
{
...
}
这样子就可以在B文件里使用a指针了,我们会发现这一链接属性实则使依靠指针来实现的
需要注意的是,在使用动态分配的内存时,应该遵循良好的内存管理实践,及时释放不再需要的内存,以避免内存泄漏。使用delete
关键字释放内存时,需要确保指向该内存的指针不再被使用,否则可能导致未定义的行为。
此外,通过使用智能指针(如std::shared_ptr
、std::unique_ptr
)来管理动态分配的内存,可以更好地管理内存,避免手动释放内存的疏忽和错误。智能指针会在其生命周期结束时自动释放相关的内存,从而简化了内存管理的工作。
在程序结束时,由new分配的内存都会被释放,不过情况也不总是这样。在不太健壮的系统里,不一定会释放,所以我们尽量使用delete来释放