掘根宝典之C++存储持续性,作用域和链接性(自动存储连续性,静态持续变量,关键字static,const的链接性,函数链接性,动态分配链接性)

存储持续性

在C++中,数据的持续性指的是数据存储的生命周期。C++中有多种方式来实现数据的持续性,包括:

  1. 自动存储持续性:在函数内部定义的变量(包括函数参数)的存储持续性是自动的,其生命周期与其所在的作用域相同。当程序执行离开变量的作用域时,自动变量将被销毁C++有两种存储持续性为自动的变量。

  2. 静态存储持续性:在函数定义外定义的变量和使用关键字static定义的变量的存储持续性都为静态,其生命周期从程序开始到程序结束。静态变量在内存中只有一份拷贝,并且在整个程序执行过程中可见C++有3种存储持续性为静态的变量。

  3. 动态存储持续性(动态内存分配):使用new关键字可以在堆上动态地分配内存,使用delete关键字可以释放内存。动态分配的内存在使用后需要手动释放,否则会造成内存泄漏。

  4. 全局变量:全局变量定义在函数外部,其生命周期从程序开始到程序结束。全局变量在内存中只有一份拷贝,并且在整个程序执行过程中可见

  5. 文件存储:数据可以存储在文件中,通过文件操作来读写数据。文件存储的数据持续性取决于文件的生命周期。

这些都是常见的实现数据持续性的方式,具体使用哪种方式取决于数据的需求和程序的设计。

作用域和链接性

作用域和链接是C++中用于控制变量和函数可见性和访问性的重要概念。

  1. 作用域(scope):作用域指的是一个标识符(变量、函数、类等)在程序中可见的范围。C++中有以下几种作用域:

    • 块作用域(block scope):在代码块(花括号括起的一系列语句)中定义的变量和函数只在该块中可见,称为块作用域。
    • 函数作用域(function scope):在函数内部定义的变量和函数只在该函数中可见,称为函数作用域。
    • 文件作用域(file scope):在函数外部定义的变量和函数具有文件作用域,可以在整个文件中访问。
    • 命名空间作用域(namespace scope):命名空间内定义的变量和函数只在该命名空间中可见。
  2. 链接(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++中,自动变量的初始化有两种方式:

  1. 声明的同时进行初始化:可以在变量声明的同时赋初值,例如:
int num = 10;
  1. 在声明后进行赋值:可以在声明变量后的任意位置进行赋值操作,例如:
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种变量存储方式

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分配的内存是在堆上分配的,并且可以在整个程序的生命周期内可见。

具体来说,动态分配的内存链接属性如下:

  1. 全局可见性:通过new分配的内存可以在整个程序中使用。即使在函数内部分配,也可以在其他函数中访问。

  2. 生命周期:通过new分配的内存一直存在,直到使用delete关键字显式释放它。它的生命周期不受函数或代码块的限制。

  3. 外部链接性:动态分配的内存默认具有外部链接性,这意味着可以在不同的编译单元(源文件)中访问和使用它。只需要在别的文件使用extern限定符作引用声明即可

可以看个例子

A.cpp

..
int main()
{
int*a=new int;
...
}

B.cpp

extern int*a;//引用声明
...
int main()
{
...
}

 这样子就可以在B文件里使用a指针了,我们会发现这一链接属性实则使依靠指针来实现的

需要注意的是,在使用动态分配的内存时,应该遵循良好的内存管理实践,及时释放不再需要的内存,以避免内存泄漏。使用delete关键字释放内存时,需要确保指向该内存的指针不再被使用,否则可能导致未定义的行为。

此外,通过使用智能指针(如std::shared_ptrstd::unique_ptr)来管理动态分配的内存,可以更好地管理内存,避免手动释放内存的疏忽和错误。智能指针会在其生命周期结束时自动释放相关的内存,从而简化了内存管理的工作。

在程序结束时,由new分配的内存都会被释放,不过情况也不总是这样。在不太健壮的系统里,不一定会释放,所以我们尽量使用delete来释放

  • 41
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值