C 语言深度解析:变量特性与指针详解

        在 C 语言中,变量的作用域、生命周期以及指针的灵活运用是掌握这门语言的核心。本文将从变量的作用域与存储类型入手,逐步深入到指针的概念、定义与实战应用,通过丰富的案例帮助读者建立清晰的知识体系。

内容提要

  • 函数

    • 变量的作用域

    • 变量的生命周期

  • 指针

函数

变量的作用域

引入问题

        我们在函数设计的过程中,经常要考虑对于参数的设计,换句话说,我们需要考虑函数需要几个参数,需要什么类型的参数,但我们并没有考虑函数是否需要提供参数,如果说函数可以访问到已定义的数据,则就不需要提供函数形参。那么我们到底要不要提供函数形参,取决于什么?答案就是变量的作用域(如果函数在变量的作用域范围内,则函数可以直接访问数据,无需提供形参)

变量作用域

        概念:变量的作用范围,也就是说变量在什么范围有效。

变量的分类

 根据变量的作用域不同,变量可以分为:

  • 全局变量

    说明:定义在函数之外,也称之为外部变量或者全程变量。

    作用域:从全局变量定义到本源文件结束。

    初始值:整型和浮点型,默认值是0;字符型,默认值是\0;指针型,默认值NULL

    举例:

      int num1;  // 全局变量,num1能被fun1、fun2、main共同访问
      void fun1(){}
      int num2;  // 全局变量,num2能被fun2、main共同访问
      void fun2(){}
      void main(){}
      int num3;  // 全局变量,不能被任何函数访问  
  • 局部变量

    说明作用域初始值
    形式参数(形参)函数作用域随机值,需要手动赋初值
    函数内定义的变量函数作用域随机值,需要手动赋初值
    复合语句中定义的变量块作用域随机值,需要手动赋初值
    for循环表达式1定义的变量块作用域随机值,需要手动赋初值

    举例:

      // a,b就是形式参数(局部变量)
      int add(int a, int b)
      {
        
          return a + b;
      }
      
      int add2(int a, int b)
      {
          // z就是函数内定义的变量(局部变量)
          int z = a + b;
          return z;
      }
      
      int list(int arr[], int len)
      {
          // i就是for循环表达式1的变量(局部变量)
          for(int i = 0; i < len; i++)
          {
              // num就是复合语句中定义的变量(局部变量)
              int num = arr[i];
          }
      }

    使用全局变量的优缺点

    优点:

    1. 利用全局变量可以实现一个函数对外输出的多个结果数据。

    2. 利用全局变量可以减少函数形参的个数,从而降低内存消耗,以及因为形参传递带来的时间消耗。

    缺点:

    1. 全局变量在程序的整个运行期间,始终占据内存空间,会引起资源消耗。

    2. 过多的全局变量会引起程序的混乱,操作程序结果错误。

    3. 降低程序的通用性,特别是当我们进行函数移植时,不仅仅要移植函数,还要考虑全局变量。

    4. 违反了“高内聚,低耦合”的程序设计原则。

    总结:

    我们发现弊大于利,建议尽量减少对全局变量的使用,函数之间要产生联系,仅通过实参+形参的方式产生联系。

作用域举例

注意:

        如果全局变量和局部变量同名,程序执行的时候,就近原则(区分作用域)

  int a = 10;  // 全局变量  全局作用域
  // int a = 22;  // 错误的,全局作用域已经存在a变量
 ​
  int main()
  {
      int a = 20;  // 局部变量  函数作用域,作用域不同,可以出现同名变量
      
      printf("%d\n", a); // 20 就近原则
      
      for (int a = 0; a < 5; a++)  // 局部变量 块作用域
      {
          printf("%d", a); // 0 1 2 3 4  就近原则
      }
      
      for (int a = 0; a < 5; a++)  // 局部变量 块作用域
      {
          printf("%d", a); // 0 1 2 3 4  就近原则
      }
      
      printf("%d\n",a);  // 20 就近原则
  }

        注意:C语言中,同一个C文件中,是可以出现多个同名变量的,但是多个同名的变量的作用域不能相同。

变量的生命周期

定义

概念:变量在程序运行中的存在时间(内存申请到内存释放的时间)

根据变量存在的时间不同,变量可分为静态存储方式动态存储方式

变量的存储类型

语法:

  变量的完整定义格式: [存储类型] 数据类型 变量列表;   

存储类型:

  • auto

    auto存储类型只能修饰局部变量,被auto修饰的局部变量是存储在动态存储区(栈区和堆区)的。auto也是局部变量默认的存储类型。

     int main()
      {
          int a;
          int b;
          // 以下写法等价于上面写法
          auto int a;
          auto int b;
          
          int a,b;
          // 以下写法等价于上面写法
          auto int a,b;
      }

  • static

    修饰局部变量:局部变量会被存储在静态存储区。局部变量的生命周期被延长。但是作用域不发生改变,不推荐

    修饰全局变量:全局变量的生命周期不变,但是作用域衰减,一般限制全局变量只能在本源文件内访问,其他文件不可访问。

    修饰函数:被static修饰的函数,只能被当前文件访问,其他引用该文件的文件是无法访问的,有点类似于java中private

  • extern

    外部存储类型:只能修饰全局变量,此全局变量可以被其他文件访问,相当于扩展了全局变量的作用域。

    extern修饰外部变量,往往是外部变量进行声明,声明该变量是在外部文件中定义的。起到一个标识作用。函数同理。

    demo01.c
    
      #include "demo01.h"
      
      int fun_a = 10;
      int fun1(){..}
    demo02.c
    
      #include "demo01.h"
      
      // 声明访问的外部文件的变量
      extern int fun_a;
      // 声明访问的外部文件的函数
      extern int fun1();
          
      int fun2();
  • register

    寄存器存储类型:只能修饰局部变量,用register修饰的局部变量会直接存储到CPU的寄存器中,往往将循环变量设置为寄存器存储类型(提高读的效率)

      for (register int i = 0; i < 10; i++)
      {
          ...
      }
面试题
static关键字的作用
  1. static修饰局部变量,延长其生命周期,但不影响局部变量的作用域。

  2. static修饰全局变量,不影响全局变量的生命周期,会限制全局变量的作用域仅限本文件内使用(私有化);

  3. static修饰函数:此函数就称为内部函数,仅限本文件内调用(私有化)。static int funa(){..}

内部函数和外部函数
  • 内部函数:使用static修饰的函数,称作内部函数,内部函数只能在当前文件中调用。

  • 外部函数:使用extern修饰的函数,称作外部函数,extern是默认的,可以不写(区分编译环境),也就是说本质上我们所写的函数基本上都是外部函数,建议外部函数在被其他文件调用的时候,在其他文件中声明的时候,加上extern关键字,主要是提高代码的可读性。

指针

预备知识
  • 字节:字节是内存的容量单位,英文名Byte,1Byte = 8Bit = 1Char

  • 地址:系统为了便于区分每一个字节而对他们逐一进行编号(编号唯一),称之为内存地址,简称地址。

基地址(首地址)

        单字节数据:对于单字节数据,其地址就是其字节编号。举例:char a = 'A';

        多字节数据:对于多字节数据,其地址是所有字节中编号最小的那个,称为基地址,

或者首地址

指针 == 地址

        指针的目的是为了进行寻址操作,找到对应的内存。

内存

虚拟内存:实际上是硬盘上的一块区域

物理内存:计算机中安装的硬件内存(比如:内存条)

RAM:随机存储器,用于临时存储数据,读写速度快

虚拟内存

内存总大小:4G

内存单元:每个内存单元大小 节(byte)

内存单元总数:

内存块:地址空间连续的多个内存单元

地址:每个内存单元都有一个对应的地址,我们称之为内存地址,而地址我们通常使

用十六进制表示。

寻址:通过地址找到对应的内存

取地址符

        每个变量都是一块内存,都可以通过取地址符 & 获取其地址(基地址)。

例如:

 int a = 100;
 printf("整型变量a的地址:%p\n", &a); // 0x7ffd3f7d7894 64位系统下:地
 址是12位16进制整数,占8字节
 char c = 'x';
 printf("字符变量c的地址:%p\n", &c); // 0x7ffd3f7d7895

注意:

        虽然不同的变量的尺寸是不同的,但是它们的地址的尺寸是一致的。

        不同的地址虽然形式上看起来是一样的,但由于它们代表的内存尺寸和类型都不同,因此他们在逻辑上是需要严格区分。

为什么要引用指针

        为函数修改实参提供支持。

        为动态内存管理提供支持。

        为动态数据结构(链表、队列、二叉搜索树)提供支持。

        为内存访问提供了另一种途径。

变量指针与指针变量

指针的概念
内存单元与地址机制

内存单元划分

        系统将内存划分为连续的基本存储单元,每个单元的容量为1字节(8 Bits)

        每个内存单元 拥有唯一编号,称为内存单元(64位系统下,12位16进制表示:

        如: 0x7ffe382a8114 )

变量的存储特性

        变量根据数据类型占据不同数量的内存单元:

char 类型占1字节(1个单元)

int 类型占4字节(4个单元)

double 类型占8字节(8个单元)

        变量的基地址(首地址)是其首个内存单元的地址(首地址一般是这一组编号中

最小的那个)

指向

指针变量中存放谁的地址,就说明该指针变量指向谁。举例:

 int a = 10; // 变量a,存放常量10
 int* p = &a; // 指针变量p,存放变量a的地址,此时我们可以说 指针变量p指向对象
 a
 int b = 20;
 p = &b; // 指针变量p指向对象b
 double c = 12;
 double *p1 = &c;

指针的尺寸

系统类型指针尺寸地址位数十六进制显示长度
32位系统4字节(long)32Bit8位(如:0x0804A000)
64位系统8字节(long)48Bit12位(如: 0x7FFDEADBEEF)

小贴士:

Linux系统中打印地址时,最多显示12个十六进制数,为什么?

Linux64位操作系统中,一个内存地址占8个字节,一个字节8bit位,所以一个地址

8*8=64bit位, 每4个bit可以表示1个十六进制数; 64个bit位用十六进制表示最多有16

个数值位;

系统为了寻址方便,默认当前计算机系统没必要寻址64bit位,只寻址了48个bit位,

所以用12个十六进制数表示一个地址。

二进制: 0100 1010 十六进制: 0x4A 4*16+10 = 74

注意: 在Linux64位操作系统中,指针类型的变量占8个字节的内存空间 在Linux32

位操作系统中,指针类型的变量占4个字节的内存空间

指针的本质

        变量指针:数据的“门牌号”(&a)

        指针变量:存储门牌号的“笔记本”(int *p; 指针变量存储的地址所指向的对象的类型

        是int类型,不能说指针变量是int类型)

        指向操作:通过门牌号访问数据(*p)

 int a = 10;
 printf("%d\n", a); // 直接访问对象a的值
 int *p = &a;
 printf("%p\n", p); // 直接访问指针变量p的值,返回的是a的地址 p;访问指针
 变量,拿到的是地址值
 printf("%d\n", *p);// 间接访问对象a的值,*p,解引用,访问p指向对象的数据
 *p:访问指针变量指向的对象,拿到的是对象的值

内存数据的存取方式

        在C语言中堆内存数据(变量、数组元素)的存取方式有两种:

直接访问

        通过基本类型的变量,访问这个变量代表的内存空间的数据

        通过数组元素的引用(下标),访问这个引用代表的内存空间的数据

 // 基本类型的变量
 int a = 10 ; // 存
 printf("%d\n", a); // 取
 // 数组元素
 int arr[] = {11,22,33}; // 存
 arr[0] = 66; // 存
 printf("%d\n", arr[0]); // 取
间接访问

通过指针变量,间接访问内存中的数据

  * :读作指针声明符或者解引用符。如果*前面有数据类型,读取声明指针

        如果 * 前面没有数据类型,读作解引用

 /*************************************************************
 ************
 > File Name: demo03.c
 > Author: qianxi
 > Description:
 > Created Time: 2025年07月24日 星期四 14时24分54秒
 **************************************************************
 **********/
 #include <stdio.h>
 int main(int argc,char *argv[])
 {
 // 定义一个普通变量
 int a = 3;
 // 定义一个指针变量,并赋值(指针变量本身还是一个变量,只不过只
 能存放地址值)
 int *p = &a; // 这里的地址值一定要有其在内存中对应的空间
 // 访问变量a,直接访问
 printf("直接访问-%d\n", a); // 3
 // 访问变量a,通过指针变量p访问,间接访问
 printf("间接访问-%d\n", *p); // 3 *p 解引用 通过指针变量访问
 其指向的对象
 // 访问p,*p, &p
 printf("p-指针变量的值:%p\n*p-指针变量的值指向的对象:
 %d\n&p-获取指针变量的地址:%p\n", p, *p, &p);// a的地址,a的值,p
 的地址
 return 0;
 }

运行结果:

指针变量的定义

语法:

 // 写法1
 数据类型 *变量列表;
 // 写法2
 数据类型* 变量;

举例:

 //普通变量
 int a;
 ​
 //指针变量
 int *p1;    //p1是指针变量
 int *p2,*p3;//p2,p3是指针变量
 int* p5,p6; //p5是指针变量,p6是普通针变量
 ​
 int* p4;    //p4是指针变量
 int* p5,p6; //p5是指针变量,p6是普通针变量
 int *p7,p8; //p7是指针变量,p8是普通针变量

        注意:指针变量的值只能是8(32位系统)| 12(64位系统)位的十六进制整数。

注意:

① 虽然定义指针变量 *a ,是在变量名前加上 * ,但是实际变量名依然为 a ,而不是 *a

② 使用指针变量间接访问内存数据时,指针变量必须要明确指向。(指向:指针变量存放谁的地址,就指向谁)

③ 如果想要借助指针变量间接访问指针变量保存的内存地址上的数据,可以使用指针变量前加 * 来间接返回访问。

 /*********************************************************************
 ****
 > File Name: demo01.c
 > Author: qianxi
 > Description:
 > Created Time: 2025年05月19日 星期三 09时39分08秒
 **********************************************************************
 **/
 #include <stdio.h>
 int main(int argc,char *argv[])
 {
 int i = 5, *p;
 p = &i; // 将i的地址赋值给指针变量p
 printf("%lx,%p,%p\n",p,p,&p); // %p-p:访问的是p的值,也就是i的地
 址;%p-&p:访问的是p自身的地址
 printf("%d\n",*p);// 5
 *p = 10; // 间接的给p对应的地址上的数据空间赋值,也就是给变量i赋值
 printf("%d,%d\n",*p,i);// 10,10
 return 0;
 }

④ 指针变量只能指向同类型变量,借助指针变量访问内存,一次访问的内存大小是取决于指针变量的类型。

 int a = 10; // 普通变量
 double b = 10; // 普通变量
 int *pa = &a; // 指针的类型 要和指向的对象的 类型一致
 double *pb = &b; // 这里的double不是指针的类型,是指针指向的对象的类型

⑤ 指针变量在定义时可以初始化,这一点和普通变量一致。(指针变量本质上还是变量,只不过存放的是地址)

 int a = 5;
 int *p = &a; // 定义变量的同时初始化
 printf("%d\n", *p); // 5
 int b;
 int *p1 = &b;
 printf("%d\n", *p1);// 随机值
指针变量的使用
使用

指针变量的赋值

 // 方式1
 int a, *p;
 p = &a; // 先定义,后赋值
 // 方式2
 int a1, *p1, *q1 = &a1; // 定义并初始化
 p1 = q1; // 变量的赋值,将q1的值赋值给p1,此时q1和p1都指向了a1

操作指针变量的值

 int a = 6, *q = &a, b = 25;
 *q = 10; // 赋值操作,修改q指向的a的值,此时 a = 10
 printf("%d,%d\n", *q, a); // 10,10
 q = &b; // 赋值操作,给指针变量q重新赋值,此时q指向b
 printf("%d,%d\n", *q, a); // 25,10

指针运算符的使用

& :取地址运算符。&a是获取变量a的地址值。这个是变量指针。

* :指针运算符、间接访问运算符,*p是指针变量p指向的对象的值。这个是指针变量。

案例

案例1

        需求:通过指针变量访问整型变量

        分析

        代码:

 #include <stdio.h>
 int main()
 {
 int a = 3, b = 4, *p1 = &a, *p2 = &b;
 printf("a=%d,b=%d\n", *p1, *p2); // a=3,b=4
 return 0;
 }
案例2

        需求:声明a,b两个变量,使用间接存取的方式实现数据的交换。

        分析:

        实现:指针指向改变,指向对象的值不变。

代码:

 /*****************************************************************
 ********
 > File Name: demo05.c
 > Author: qianxi
 > Description:
 > Created Time: 2025年07月24日 星期四 15时52分09秒
 ******************************************************************
 ******/
 #include <stdio.h>
 int main(int argc,char *argv[])
 {
     int a = 3, b = 4, *p_a = &a, *p_b = &b;
     printf("交换前:a=%d-%d,b=%d-%d\n", a, *p_a, b, *p_b);// a=3-
     3,b=4-4
         // 指针的值交换,临时变量也是指针
         int *p_t = p_a;
     p_a = p_b; // p_a指向b
     p_b = p_t; // p_b指向a
     printf("交换后:a=%d-%d,b=%d-%d\n", a, *p_a, b, *p_b);// a=3-
     4,b=4-3
         return 0;
 }

        总结:此时改变的只是指针的指向,原始变量a,b中数据并没有发生改变。

        指针指向不变,指向对象的数据改变

代码:

 /*****************************************************************
 ********
 > File Name: demo06.c
 > Author: qianxi
 > Description:
 > Created Time: 2025年07月24日 星期四 16时00分28秒
 ******************************************************************
 ******/
 #include <stdio.h>
 int main(int argc,char *argv[])
 {
     int a = 3, b = 4, *p_a = &a, *p_b = &b;
     printf("交换前:a=%d-%d,b=%d-%d\n", a, *p_a, b, *p_b);// a=3-
     3,b=4-4
         // 交换:指向不变,指向对象的值改变
         int temp = *p_a;
     *p_a = *p_b; // 将p_b指向对象b的数据赋值给p_a指向的对象a
     *p_b = temp;
     printf("交换后:a=%d-%d,b=%d-%d\n", a, *p_a, b, *p_b);// a=4-
     3,b=3-4
         return 0;
 }  

        总结:此时改变的是原始变量,原始变量a,b中数据发生了改变。

案例3

        需求:输入a,b两个整数,按先大后小的顺序输出a和b。

        分析:

        实现:指针指向改变,指向对象的值不变

        代码:

 /*****************************************************************
 ********
 > File Name: demo07.c
 > Author: qianxi
 > Description:
 > Created Time: 2025年07月24日 星期四 16时14分17秒
 ******************************************************************
 ******/
 #include <stdio.h>
 int main(int argc,char *argv[])
 {
     int a, b, *p_a = &a, *p_b = &b, *p_t;
     printf("请输入两个整数:\n");
     scanf("%d%d", &a, &b);
     if (a < b)
     {
         p_t = p_a;
         p_a = p_b;
         p_b = p_t;
     }
     printf("按从大到小的顺序输出a,b的值:%d > %d\n", *p_a, *p_b);
     return 0;
 }

        实现:指针指向不变,指向对象的值改变

        案例:

 /*****************************************************************
 ********
 > File Name: demo07.c
 > Author: qianxi
 > Description:
 > Created Time: 2025年07月24日 星期四 16时14分17秒
 ******************************************************************
 ******/
 #include <stdio.h>
 int main(int argc,char *argv[])
 {
     int a, b, *p_a = &a, *p_b = &b, temp;
     printf("请输入两个整数:\n");
     scanf("%d%d", &a, &b);
     if (a < b)
     {
         temp = *p_a;
         *p_a = *p_b;
         *p_b = temp;
     }
     printf("按从大到小的顺序输出a,b的值:%d > %d\n", *p_a, *p_b);
     return 0;
 }

四、总结

        本文详细讲解了变量的作用域、生命周期、存储类型以及指针的核心概念与应用。变量的作用域决定了可见性,存储类型控制了生命周期与存储位置,而指针则提供了直接操作内存的能力,是 C 语言灵活性的根源。

        掌握这些知识点需要大量实践,建议结合调试工具观察变量的地址、指针的指向变化,逐步建立内存模型。后续将深入讲解指针与数组、函数的结合使用,敬请关注。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值