计算机系统应用教程网站

网站首页 > 技术文章 正文

C\C++语言2|变量、常量、指针的声明、定义和左值、右值

btikc 2024-09-12 12:07:08 技术文章 21 ℃ 0 评论

计算机的内存(RAM)由数百万个顺序存储位置组成,每个位置都有唯一的地址。计算机的内存地址范围从0开始至最大值(取决于内存容量,可访问的最大值取决于计算机的数据总线的数量,如果是32位,则是到2^32,4G)。

运行计算机时,操作系统要使用一些内存。运行程序时,程序的代码(执行程序中不同任务的机器语言指令)和数据(该程序使用的信息)也要使用一些内存。

C\C++语言中的数据和代码是需要存放才可以使用的,C\C++语言用变量来存储数据,用函数来定义一段可以重复使用的代码,它们最终都要放到内存中才能供 CPU 使用。

对于程序中使用到的常量、变量的类型要事先进行定义才能使用,这是保证程序可靠性的手段之一。早期的一些计算机程序设计语言不要求对变量的类型进行定义,因此,一个变量的类型在程序运行期间是不确定的,这将会降低程序的可靠性。

数据和代码都以二进制的形式存储在内存中,计算机无法从格式上区分某块内存到底存储的是数据还是代码。当程序被加载到内存后,操作系统会给不同的内存块指定不同的权限,拥有读取和执行权限的内存块就是代码,而拥有读取和写入权限(也可能只有读取权限)的内存块就是数据。

CPU 访问内存时需要的是地址,而不是变量名和函数名!变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序后,它们都会被替换成地址。编译和链接过程的一项重要任务就是找到这些名称所对应的地址。

然而指针也是一种变量,他里面装的就是所指向数据或者代码的地址。所以它可以指向变量,也可以指向函数。

1 变量

在C程序中声明一个变量时,编译器会预留一个内存位置来储存变量。此位置有唯一的地址。编译器把该地址与变量名相关联。当程序使用该变量名时,将自动访问正确的内存位置。所以,编程语言中的变量可以理解为:一段可寻址的内存空间,其值未初始化之前是一个随机值(或垃圾值),初始化后可以通过赋值符号“=”去更新。

double pi = 3.14; //定义double型变量pi 
pi = 3.1415; 
pi = 3.1415926; //可以通过赋值不断改变pi的值 scanf( "%lf", &pi ); 

要理解变量作用域,首先要理解结构化编程的思路。结构化编程把程序分成若干独立的函数,每个函数都执行特殊的任务。这里的关键是函数独立。为了真正让函数独立,每个函数的变量都不能受其他函数代码的影响。只有隔离每个函数数据,才能确保函数在完成自身任务时不会被其他函数破坏。在函数中定义变量,便可“隐藏”这些变量,让程序的其他部分无法访问它们

然而,并非所有情况都要在函数间完全隔离所有的数据。程序员通过指定变量的作用域能很好地控制数据隔离的程序。

任何变量都有一个指定的存储类别,用于决定变量的作用域(在程序中何处可见)和生命期(变量在内存中的存活时间)。

对于结构化编程,正确使用存储类别非常重要。在函数中使用局部变量,提高了函数间的独立性。尽量使用自动存储类别的变量,除非有特殊原因需要使用外部或静态变量。

既然外部变量在程序中的任何地方都可用,为何不将所有的变量都声明为外部变量?

随着程序越来越大,包含的变量也越来越多。外部变量在程序运行期间会一直占用内存,而自动变量只在执行它所在的函数时占用内存。因此,使用局部变量节约内存空间。然而,更重要地是,使用局部变量能减少程序不同部分不必要的交互,从而减少了程序的bug,同时也遵循了结构化编程的原则。

2 常量

常量命名的一段只读的内存空间,常量按照数据类型主要分为整型常量、浮点型常量、字符型常量、字符串常量、转义字符常量、地址常量等6种。

常量按声明的方式,可分为:

2.1 const声明的常量;
2.2 constxpr声明的常量表达式;
2.3 enum声明的枚举常量;
2.4 #define定义的常量(不推荐);
2.5 字面常量

在C中,字面量(右式)除了字符串字面量以外,都只作代码处理,并不单独分配内存,而字符串字面量要分配内存,并保存到内存的数据段、栈区或堆区。

{
char *s1, *s2, *s3 = “abcde”; //字面量保存在数据段,返回了一个地址,此外赋值给了指针s3
char ch[] = “fff”; //保存到栈区
s1 = ch;
s2 = new char[10]; //堆区
strcpy(s2, “fgh”);
delete [] s2;
}

这样保存的好处是当有多个地方使用同一字符串常量时,可以直接引用。

3 常量和变量

常量和变量都是在程序中需要经常使用到的,特别是变量,它们有着不同的特征,常量和变量的主要区别如下。

3.1 是否可更新?常量的值不可以修改,任何尝试修改常量的操作都会导致编译错误。而变量可以通过赋值来改变值。

3.2 初始化要求:常量定义以后就不可以修改,所以常量在定义时必须初始化。变量可以在定义时暂不进行初始化。常量初始化的时候必须直接复制常量初始化的示例代码如下:

const char a = "test" //正确 
char p; 
p="test"; 
const test = p; //错误,常量必须直接赋值 

3.3 常量值的地址不允许赋给非常量指针。

3.4 常量在编译的时候,可以以立即数形式编译进指令,比起使用内存的变量执行效率更高。

3.5 常量本身没有地址属性(除字符串常量外),而变量有地址属性。所以常量只能用做右值,而变量用作左值、右值都可以。

4 指针变量

变量有两个值:一个是内存块的首地址,一个是内存块的一串二进制码按数据类型的编码解码规则解码出来的数据值。

如果数据值也是一个地址值,那这个变量就是指针变量。

int i=0;
int* p=&i;

变量名只是隐式地声明了变量值存储的地址,而它的指针则是显式地声明了变量值存储的地址。

声明指针为什么需要声明类型,原因之一就是当指针运算产生偏转或读取数据时,它知道按不同的类型需要读取多少内存或偏转多少位置?

4.1 空指针

空指针与同类型的其它所有指针都不相同, 也与任何对象或函数的指针都不相等。也就是说, 用取地址操作符&永远也不能得到空指针, 同样对 malloc() 的成功调用也不会返回空指针, 除非调用失败, malloc() 才返回空指针。

实际上,空指针表示“未分配”或者“尚未指向任何地方”的指针。它在概念上不同于未初始化的指针。因为,空指针可以确保不指向任何对象或函数,而未初始化指针则可能指向任何不确定的地方。

NULL是C语言中定义的预处理宏,它一般代表0 或者 ((void *)0),用于表示一个空指针。在C程序中,NULL实际上和0完全等价的。需要注意的是:NULL只能用作指针常量来使用。

4.2 野指针

野指针主要是因为以下疏忽而出现的删除或申请访问受限内存区域的指针:

指针变量未初始化(初始化时置 NULL)

任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。如果没有初始化,编译器会报错“ ‘point’ may be uninitializedin the function ”。

指针释放后之后未置空(释放时置 NULL)

有时指针在free或delete后未赋值 NULL,便会使人以为是合法的。别看free和delete的名字(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。此时指针指向的就是“垃圾”内存。释放后的指针应立即将指针置为NULL,防止产生“野指针”。

指针操作超越变量作用域(不要返回指向栈内存的指针或引用或使用超出作用域的指针)

不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。当然也不能使用超出作用域的指针。

野指针与空指针不同,野指针无法通过简单地判断是否为NULL避免,而只能通过养成良好的编程习惯来尽力减少。

4.3 多级指针

指针指向的内容还是一个指针,称为多级指针

如有定义:

char *string[10];

string是一个数组,数组元素可以通过指针来访问。如果p是指向数组string的某一个元素,那么p指向的内容是一个指向字符的指针,因此p就是一个多级指针。

等价于a[i][j]的表达式
*(a[i] + j)
(*(a + i))[j]
*((*(a + i)) + j)
*(&a[0][0] + 5 * i + j)

可以用指向指针的指针访问指针数组的元素。如int** p; 解引用*p的内容还是一个地址,**p才是具体值。

4.4 指针的作用

4.4.1 间接访问;

4.4.2 允许在函数间共享内存空间;可以让参数作为返回值(参数一般是作为函数输入的);

4.4.3 动态变量,如动态数组:

int* arr, n=11;
arr = (int*)malloc(n*sizeof(int));

4.5 智能指针

因为没有明确的所属关系,一些在多个模块中共亨的资源指针很容易成为“野指针”,从而导致多种内存汸问的问题。首先,因为共享某个内存块指针的多个模块并不拥有这个指针,它们只是使用这些指针,并不负责释放这些指针所指向的内存资源,这可能会导致程序的内存泄露。其次,因为这些指针被多个模块所共享,这可能会导致某个指针所指向的内存资源巳经被释放,但是另外的模块又试图访问这个指针所指向的内存资源,最终导致内存访问错误。

为此,C++使用了智能指针的办法,详细介绍可见:

C++|智能指针为何智能?

5 左值和右值

在编程中,左值位于赋值运算符的左侧,右值位于赋值运算符的右侧。

变量和常量都是按其相关的类型存储在内存空间内的,不过变量是可以寻址的而常量不可以寻址。对于每一个变量都有两个相关值:地址值和数据值:

数据值:存储在其内存地址中的数据,也称为变量的右值,通常用于取值;

地址值:存储数据值的那块内存的地址,也称为变量的左值,通常用于更新值;

如以下代码:

int a = 3;
a = a + 5;

在a=a+5中,变量a同时出现在赋值操作符的左边和右边。右边的实例被读取,与其相关联的内存中的数据值被读出,左边的a用作写入。表达式a+5的值将要存储在a的位置值所指向的内存区中,原来的数据值会被覆盖。即在赋值操作符右边的a+5为右值(数据值,供读入)。达边的a为右值(地址值,供写入)。

变量是左值,因此可以出现在赋值语句的左边。数字字面值是右值,因此不能被赋值。实际上,左值是一个存储地址,也就是一块内存存储数据所要操作的地址。而右值是一个具体的数据或者数值,也就是该内存存储的数据内容。只有左值和右值都是单一变量的时候二者才可以相互交换位置,因为变量具有固定的内存地址。

左值可以出现在赋值语句的左边或右边,也就是说左值可以当右值使用。右值只能出现在赋值的右边,不能出现在赋值语句的左边。左值表示程序中必须有一个特定的名字引用到这个值。右值表示程序中没有一个特定的名字引用到这个值。

左值和右值的示例代码如下:

int a = 1; // 变量a是一个左值 
char str[] = "hello, world"; // 数组成员str[i]是左值 
//"hello, world"; 这个表达式是一个数据内容,它是一个右值 
string("hello, world"); // "hello, world"这也是一个右值 

注意:有些操作符,例如赋值,要求其中的一个操作数必须是左值。结果,可以使用左值的上下文比右值更广。左值出现的上下文决定了左值是如何使用的。

变量是左值,可以出现在赋值语句的左边。数字字面值是右值,不能被赋值。

6 变量的声明和定义

C语言做为强类型语言,要先声明后才可以使用。

在C语言中,每个变量有两个属性:

类型:变量所存储的数据类型
存储类型:变量所存储的区域

标准的变量定义:

存储类型 数据类型 变量名;

存储类型:

自动变量:auto
寄存器变量:register
外部变量:extern
静态变量:static

声明用于向程序表明变量的类型和名称。定义也是一种声明,当定义变量时编程者声明了它的类型和名称。也可以通过使用extern关键字声明变量名但是不定义它。不定义变量的声明包括对象名、对象类型和对象类型前的关键字extern,示例代码如下。

extern int a; //声明但是未定义a,在其它文件有全局声明和定义:int a;
int b; //定义b,声明和定义同时进行

注意:extern声明不是定义,也不会分配存储空间。它只是说明变量定义在程序的其他地方(只是一个名字的引入)。含有初始化的extern声明被当做是定义,程序中变量可以声明多次,但只能定义一次。

定义性声明:需要建立存储空间的声明, 如:int a; 
引用性声明:不需建立存储空间的声明,如extern int a;

变量的定义(同时声明和定义)用于为变量分配存储空间,还可以给变量初始化。在一个程序中,变量有且只有一个定义,定义变量的示例代码如下。

int a; 
int b; 
int c; //定义3个整型变量a,b,c 
int d,e,f; //同时定义3个整型变量 
int g = 10; //定义变量g并且初始化 

声明一个不在本模块作用范围内的全局变量。如:

extern int num;

num为某一个文件中声明和定义的全局变量。

在某函数中引用了一个声明在本函数后的全局变量时,需要在函数内用extern声明此全局变量。

当一个程序有多个源文件组成时,用extern可引用另一文件中的全局变量。

7 复合声明

当一个声明中同时存在指针声明符号*、数组符号[]或函数符号()时,称为复合声明。

下面到底哪个是数组指针,哪个是指针数组呢?

int *p1[10]; //指针数组,一个元素是指针的数组
int (*p2)[10]; //数组指针,一个指向一个数组的指针

“[]”的优先级比“*”要高。p1 先与“[]”结合,构成一个数组的定义,数组名为p1,int *修饰的是数组的内容,即数组的每个元素。那现在我们清楚,这是一个数组,其包含10 个指向int 类型数据的指针,即指针数组。至于p2 就更好理解了,在这里“()”的优先级比“[]”高,“*”号和p2 构成一个指针的定义,指针变量名为p2,int 修饰的是数组的内容,即数组的每个元素。数组在这里并没有名字,是个匿名数组。那现在我们清楚p2 是一个指针,它指向一个包含10 个int 类型数据的数组,即数组指针。

同样的,

int* func(); // 指针函数,一个返回指针的函数
int (*func)(); // 函数指针,一个指向一个函数的指针

函数指针数组:

	double (*p[3])()={sin,cos,tan}; //sin,cos,tan是函数

8 变量的初始化

如果变量在定义的时候没有被初始化,它的值将是不确定的,这也是程序员在使用变量时需要考虑到的一点,不确定的值有可能导致程序出现错误。一般来说,编程者为了保证程序的稳定性,尽量做到将所有的变量都进行初始化,如:

int i;
int j = ++i;//此时i未初始化,也未有赋值操作,此时为一垃圾值,会出现错误

所以最佳的选择就是在声明和定义时同时就行初始化:

int i=0;
int j = ++i;//此时i有初始化

9 变量的存储类别

变量的声明和定义的位置以及使用的存储类别的关键字不同而有不同的存储类别,存储在进程空间中不同的位置。

在C++中,变量的存储类型有自动类型、寄存器类型、静态类型、外部类型等4种。

(1)自动类型变量(auto)

自动类型只能是局部类型的变量,属于动态存储类型。

(2)静态类型变量(static)

static,即在程序运行的过程中静态变量始终是占用一个存储空间。静态变量只能在他的作用范围内使用,使用局部静态变量是为了在下次调用该函数时,能使用上次调用后得到的该变量的值。

(3)寄存器类型变量(register)

属于动态存储类型,编译器不为寄存器类型的变量分配内存空间,而是直接使用CPU的寄存器。以便提高对这类变量的存取速度。主要用于控制循环次数等不需要长期保存值得变量。

(4)外部类型变量(extern)

外部类型变量必须是全局变量,在C++中,有两种情况需要使用外部类型变量。一种是在同一源程序文件中,当在全局的定义之前使用该变量时,在使用前要对该变量进行外部类型变量声明。另一种是当程序有多个文件组成时,若在一个源文件中要引用在另一个源文件中定义的全局变量,则在引用前必须对所引用的变量进行外部声明。

如果在某文件中定义的全局变量不想被其他文件所调用,则必须将该变量声明为静态全局变量,也就是说,静态全局变量只能供所在的文件使用。

10 名字空间

名字空间是C++中一种避免全局程序实体的冲突机制。

C++规定了四种作用域

10.1 局部作用域,由声明和定义的位置区分;

在局部域中定义或者声明的变量或函数,其可见性从声明点开始,一直延伸到匹配的花括号的结束外。(VC6在for循环中初始化循环变量时,其作用域会超出函数块)

C++约定的局部作用域有三种:复合语句的块域、函数域和函数声明域。

函数参数只是一个占位符,最终总是需被赋值,用常量赋实值,用变量赋实值或地址值或别名,其本身并不存在什么作用域。

10.2 文件作用域,使用关键字extern;

10.3 名字空间作用域,使用域操作符::或using关键字:

unsing namespace 名字空间名
using 名字空间名::成员名
using namespace std;
using std::cout;

10.4 类作用域,使用域操作符"::",如:

class account
{
static float rate;
……
};
account::rate = 1.3;

-End-

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表