C++学习 十二、内存模型,作用域,名称空间 (2)内存模型

前言

本篇继续C++学习记录,变量与函数的内存模型。

存储持续性,作用域,链接性

存储持续性

存储持续性指的是数据保留在内存中的时间。

C++中,数据的存储方案有三种,分别是自动存储,静态存储和动态存储。

在函数或代码块中声明的变量就是自动变量,对应自动存储,在执行函数或者代码块时被创建,执行段结束后释放内存。

在函数外定义和使用static关键字声明的变量就是静态变量,对应的是静态存储,在程序运行时被创建,程序结束时释放内存。

通过new分配的内存是动态存储,new的时候创建,delete的时候释放内存。

作用域

作用域指的是符号在文件中的可见范围。

函数中定义的变量可在该函数内定义变量的语句之后使用。

函数外定义的变量可在定义变量语句之后的函数中使用。

链接性

链接性指的是符号在不同文件中的共享性质。

外部链接性可在一同编译的文件中共享。

内部链接性只在该文件中的函数共享。

无链接性则不能被共享。

自动存储

一般而言,在函数中声明的变量是自动变量,自动存储持续性,局部作用域,无链接性。

自动变量在程序执行到函数或代码块时被分配内存,但作用域的起点在变量声明的位置,在代码块执行完后释放内存,变量被销毁。

代码块是在函数内部使用大括号括起来的部分:

void main(){
	int a = 0;
	{ // a的内存被分配 
		double a = 1.1; // 内部a的作用域起点
		cout << a << endl;
	} // 代码块对应的内存释放,内部的a被销毁
	cout << a << endl;
}
// 1.1
// 0

同一个代码块中不能声明同名变量,但不同代码块内是可以的。上面这个示例中,当程序执行到内部代码块时,double a = 1.1;隐藏了外部int a = 0;的变量定义。当执行回到外部代码块时,整型a又可见了。

自动变量,栈

自动变量的内存由栈管理。

栈是先进后出的数据结构,由栈底与栈底指针,栈顶与栈顶指针组成。栈底是栈开始的位置,栈顶是数据插入的位置。

函数被调用时,自动变量从栈顶压入,栈顶指针指向栈的下一个可用内存单元。函数结束时,栈顶指针被重置为函数调用前的值。先前自动变量占用的内存单元不再被标记,这些内存单元将接收其它函数和代码块声明的自动变量。

关键字auto, register

关键字autoregister在早期C++与C++11之后有了很大的改变。

C++11之前,关键字auto用于显式指出变量是自动变量。

C++11中,关键字auto用于自动类型推断,例如:

auto a = 0; // 自动推断a类型为int
auto b = 1.; // 自动推断b类型为double

等同于:

int a = 0;
double b = 1.;

C++11之前,关键字register用于建议编译器使用CPU寄存器来存储自动变量,即寄存器变量。

C++11中,关键字register用于显式指出变量是自动变量。

静态存储

C++中,静态变量的链接性可以为外部,内部,以及无链接性。

静态变量在程序运行期间一直存在,因此编译器将分配固定的内存块来存储所有静态变量。

如果静态变量没有被显式初始化,则编译器自动设置为0;静态数组和结构将每个元素或成员的所有位都设为0。

全局变量,静态全局变量,静态局部变量

声明在函数外的变量是全局变量,全局变量是静态变量,具有外部链接性。(注意:不是静态局部变量!)

通过static关键字声明在函数外的变量是静态全局变量,具有内部链接性。

通过static关键字声明在函数内的变量是静态局部变量,无链接性。

三种静态变量示例如下:

int globalvar = 1;
static int infilevar = 2;

void func(){
	static int infuncvar = 3;
	int temp = 4;
	infuncvar++;
	temp++;
}

全局变量globalvar,静态全局变量infilevar,静态局部变量infuncvar在程序运行时一直存在。

只有在func()函数中才能使用infuncvar,但在程序开始时,它就已经在内存中了。并且,即使func()运行结束,temp被销毁,infuncvar仍然存在,直到程序结束。

globalvar, infilevar在声明位置以后都可以使用,但infilevar只能在该文件中使用,而globalvar可以在其它文件中使用。

如果通过for循环调用func()

void main(){
	for(int i=0;i<2;i++) 
	{
		func();
	}
}

第一次循环时,infuncvar值为4,并保留下来,而temp值为5,然后被销毁。
第二次循环时,infuncvar的值为4,调用func()后变为5,而temp值仍然为5,然后被销毁。

静态初始化,动态初始化

无论静态变量是否被显式初始化,编译器都会先对其进行零初始化,即把变量设置为零(指针设置为空指针)。

如果代码中静态变量使用常量表达式做初始化,则编译器再计算常量表达式,然后给静态变量做初始化,称为常量表达式初始化

由于零初始化和常量初始化都是在编译阶段进行,因此称为静态初始化

如果静态变量使用函数做初始化,则初始化要在程序运行时进行,因此称为动态初始化

示例如下:

int a;
int b = 1;
int c = 2 * 1;
int d = std::abs(3);

编译时,首先将a,b,c,d零初始化,然后编译器计算常量表达式并给b,c赋值。d的初始化调用了std::abs()函数,因此要在程序运行时再动态赋值。

全局变量,外部变量,关键字extern

全局变量的作用域为整个文件(声明位置以后),因此称为全局变量;而全局变量拥有外部链接性,因此也称为外部变量。

C++提供了两种声明方法:定义声明(简称定义),引用声明(简称声明)。定义将给变量分配存储空间,而声明只使用已定义的变量。对于变量而言,TypeName VarName就是定义变量。

每个使用全局变量的文件都需要声明该变量,但每个变量只能定义一次。因此,在某个文件中定义了全局变量后,其它使用该变量的文件必须通过extern关键字声明该变量:

// file1.cpp
int global = 1;

// file2.cpp
extern int global;

file1.cpp中的定义也可以写成extern int global = 1;extern在文件中定义时可有可无,但在其它使用文件中声明时必须存在。

同一作用域中,变量只能定义一次;但不同作用域是可以存在同名变量的。局部代码块内的变量将隐藏外部同名变量:

// file3.cpp
extern int global;

void func(){
	int global = 2;
	cout << global << endl;
}

void main(){
	func();
	cout << global << endl;
}
// 2
// 1

如果需要在局部代码块中,使用同名变量的全局版本,可以使用作用域解析运算符::,比如上面的func()函数中,::global表示的是全局变量而不是函数内部定义的局部变量。

注意:虽然全局变量具有外部链接性,更容易传递参数,但也更容易发生数据篡改的问题。应当尽量避免使用全局变量。

全局变量更适用于表示常量数据。

静态全局变量

如果静态变量仅在本文件中使用,则可以定义静态全局变量。某个文件中,如果静态全局变量与外部变量同名,则静态全局变量隐藏外部变量名:

// file4.cpp
static int global = 3;

静态局部变量

静态局部变量只在程序运行时进行初始化,当函数调用时,不会被再次初始化。

const全局变量

全局变量具有外部链接性,但const全局变量则具有内部链接性。

因为const全局变量具有内部链接性,所以在头文件中使用const全局变量不会出现重定义的问题。这还意味着,每个包含该头文件的源文件都有一份自己的常量数据。

如果需要const全局变量具有外部链接性,需要使用extern关键字声明:

int const x = 1; // 内部链接性
extern int const y = 2; // 外部链接性

此时,需要在每个使用外部常量的文件中,通过extern进行声明。

局部const变量不必担心这种名称冲突问题。

函数的存储持续性与链接性

C和C++中,都不能在函数中定义另一个函数,因此函数都是静态存储的。

一般而言,函数具有外部链接性,因此多文件编译容易出现函数符号重定义的原因。(函数符号由函数名,参数列表共同组成。)

同一程序的不同文件中,外部函数只能在某个文件中定义一次,不过可以声明很多次。每个使用该函数的文件都应该先声明后调用。

如果需要使用内部链接的函数,可以通过static关键字声明,示例如下:

static int infileFunc();

static int infileFunc(){
	cout << " This is an in file function. " << endl;
}

内部函数的声明和定义都必须使用static关键字,由于链接性是内部的,因此其它文件中也可以存在同名函数。在本文件中,static声明和定义的函数将隐藏同名的外部函数。有时也会把内部函数称为静态函数。

静态函数也只能定义一次。

内联函数

C++内联函数具有内部链接性,因此内联函数可以写在头文件中。

内联函数虽然具有内部链接性,理论上可以在不同文件中出现同名而定义不同的内联函数。但这样的做法可能导致程序运行结果不确定。具体原因下面这个博客写的比较清楚。

https://blog.csdn.net/friendbkf/article/details/45621177

因此,同名内联函数应当具有相同的定义。

动态存储

C++中,通过new分配的内存是动态存储,生命周期从new开始,到delete结束。从而能够在一个函数中创建内存,在另一个函数中释放内存。

动态存储是通过堆管理的。

因此,编译器使用三种内存,一块用于静态变量,一块用于自动变量,一块用于动态存储。

动态存储的数据不受作用域和链接性规则控制,不过用于接收动态存储数据地址的指针是有作用域和链接性的,如上面各种变量所述。

一般来说,程序结束时,new分配的动态内存会被系统自动回收,但比较老旧的操作系统可能不会自动回收动态内存,因此在程序设计中就应当使用delete来释放动态内存。

new运算符初始化

之前在指针一章记录了使用new分配动态内存的方法:

int* p = new int;
*p = 1;
cout << *p;
// 1

这样分配了一个int类型的内存空间,并把地址赋给了指针p,再把值放到内存空间中。

实际上,可以直接采用new来初始化动态内存中存放的值:

int *p = new int (1);
cout << *p;
// 1;

这里使用的是圆括号,要注意和方括号区分:圆括号用于初始化,方括号用于为动态数组分配内存。

在C++11中,new可以使用列表初始化。除了简单的int, float等类型,数组、结构也能被初始化了:

float *pa = new float {1.1};
int *pb = new int [5] {0, 1, 2, 3, 4};

注意:如果new失败了,比如内存不够了,会报std::bad_alloc异常。

后记

本篇主要记录了C++中,变量与函数的内存模型,即存储持续性,作用域和链接性。下篇将继续记录名称空间。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值