您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Model Center 汽车系统工程   模型库  
会员   
   
基于UML和EA进行分析设计
2月3-4日 北京+线上
需求分析与管理
2月9-10日 北京+线上
AI大模型编写高质量代码
3月12-13日 北京+线上
     
   
 订阅
讲透C语言易混:堆栈&指针&关键字&枚举结构体联合体&可变参数
 
 
  115   次浏览      6 次
 2026-1-28
 
编辑推荐:
本文C语言中的核心编程概念和特性,包括5个方面:指针、关键字解析、内存分区、结构体、联合体、枚举、可变参数, 希望能为大家提供一些参考或帮助。
文章来自于二象性Libo,由火龙果Linda编辑推荐。

一、指针

1、概述

指针(Pointer)是C语言的一个重要知识点,其使用灵活、功能强大,是 C语言的灵魂 。指针与底层硬件联系紧密,使用指针可 操作数据的地址 ,实现数据的 间接访问 。

指针即 指针变量 ,用于存放其他数据单元(变量/数组/结构体/函数等)的 首地址 。若指针存放了某个数据单元的首地址,则这个指针 指向了这个数据单元 ,若指针存放的值是0,则这个指针为 空指针 ,对空指针取址会报错。

2、计算机存储机制

内存分为若干字节,每个字节都有对应的唯一地址!

  • int a = 0x12345678; 4个字节,把数据的低位存储在低地址,属于 小端 存储
  • short b = 0x5A6B; 2个字节
  • char c[ ] = {0x33,0x34,0x35}; 字符数组,3个元素占3个字节,数组元素地址按顺序

3、定义指针变量&操作

若已定义: int a; //定义一个int型的数据

int *p; //定义一个指向int型数据的指针

则对指针p有如下操作方式:

注:

1、char *p = &a;这里的*不是取地址的意思,是为了说明p是个指针变量(”地址变量”),也可写成char* p = &a 。

2、数据宽度是指指针指向的类型存储时占的字节数,如第一个表。

4、数组与指针

数组是一些相同数据类型的变量组成的集合,其 数组名 即为 指向该数据类型的指针 。 数组的定义等效于申请内存、定义指针和初始化。

例如:

char c[ ] = { 0x33 , 0x34 , 0x35 };

等效于:申请内存 ;定义 char *c = 0x4000;初始化数组数据

char * a = 0x4000 ;

a = ( char *)malloc( 3 * 1 );

//malloc申请内存,3个元素,每个元素占1个字节(函数返回值类型void*,用的时候根据情况类型转换)

*a = 0x33 ;

*(a+ 1 ) = 0x34 ;

*(a+ 2 ) = 0x35 ;

数组可看作是指针的另一种表现形式 ,用数组定义,可以用指针访问(与利用下标引用数组数据等效), 数组名就是首元素地址 ,即指向首元素的 指针变量 也即整个数组单元的首地址:

例如 : c[0]; 等效于: *c;

c[1]; 等效于: *(c+1);

c[2]; 等效于: *(c+2);

注意事项

在对指针取内容之前,一定要确保指针指在了 合法的位置 ,否则将会导致程序出现不可预知的错误。

同级指针之间才能相互赋值, 跨级赋值 将会导致编译器报错或警告。

5、指针的应用

①传递参数

使用 指针传递大容量的参数 ,主函数和子函数使用的是同一套数据,避免了参数传递过程中的数据复制(值传递,传递参数时需要复制一份值到另一个地址空间,不改变原变量的值,实现了安全保护,但是速度慢),指针传递提高了运行效率,减少了内存占用。

```c
#include <stdio.h>


/**
 * @brief  寻找数组中的最大值
 * @param  array: 输入数组指针(const修饰,确保数组内容只读不被修改,提升安全性)
 * @param  size:  数组元素个数
 * @retval 数组中的最大值
 */

int
 FindMax(const int *array, int size)
{
    int
 i;
    int
 max = array[0];  // 初始化最大值为数组第一个元素

    // 遍历数组,从第二个元素开始比较,更新最大值

    for
 (i = 1; i < size; i++)
    {
        if
 (array[i] > max)
        {
            max = array[i];
        }
    }

    return
 max;
}

int
 main(void)
{
    int
 a[] = {1, 2, 3, 4, 5, 6};
    int
 Max;

    // 调用FindMax函数获取数组最大值,参数:数组首地址 + 元素个数

    Max = FindMax(a, 6);
    // 可选优化:Max = FindMax(a, sizeof(a)/sizeof(int)); (跨平台兼容,无需硬编码4字节)


    // 打印最大值

    printf
("最大值:%d\n", Max);

    return
 0;
}

```

使用指针传递输出参数,利用主函数和子函数使用同一套数据的特性,实现数据的返回,可实现 多返回值函数的设计。 弥补了C语言函数只可一个返回值的缺陷。

```c
#include <stdio.h>


/**
 * @brief  查找数组最大值并统计其出现次数(通过指针实现多返回值)
 * @param  max:  最大值存储地址(输出参数)
 * @param  count: 最大值出现次数存储地址(输出参数)
 * @param  array: 输入数组指针(const修饰,确保数组只读不被修改)
 * @param  size:  数组元素个数
 * @retval 无
 */

void
 FindMaxAndCount(int *max, int *count, const int *array, int size)
{
    // 初始化最大值为数组第一个元素(修正原代码错误:*max = array[0],非array = 0)

    *max = array[0];
    *count = 1;  // 初始化出现次数为1(最大值至少出现1次)
    
    int
 i;
    // 遍历数组,从第二个元素开始比较

    for
 (i = 1; i < size; i++)
    {
        // 找到更大的值,更新最大值并重置计数

        if
 (array[i] > *max)
        {
            *max = array[i];
            *count = 1;  // 新最大值,计数清1
        }
        // 找到相同最大值,增加计数

        else
 if (array[i] == *max)
        {
            (*count)++;  // 括号确保先解引用再自增
        }
    }
}

int
 main(void)
{
    int
 a[] = {2, 3, 4, 10, 6, 10, 7, 10, 8, 10};
    int
 Max;
    int
 Count;

    // 调用函数:传入变量地址,实现多返回值(修正函数名拼写错误FiindMaxAndCount→FindMaxAndCount)

    FindMaxAndCount(&Max, &Count, a, sizeof(a) / 4);
    // 优化建议:sizeof(a)/sizeof(int) 跨平台兼容,无需硬编码4字节


    // 打印最大值和出现次数

    printf
("最大值:%d\n", Max);
    printf
("出现次数:%d\n", Count);

    return
 0;
}

```

②传递返回值

将模块内的公有部分返回,让主函数持有模块的“句柄”,便于程序对指定对象的操作。

```c
#include <stdio.h>


// 定义全局整型数组Time,存储时间相关数据(时、分、秒)

int
 Time[] = {23, 59, 55};

/**
 * @brief  获取全局数组Time的首地址(返回指针类型)
 * @param  无
 * @retval 指向全局数组Time的整型指针(数组首元素地址)
 */

int
* GetTime(void)
{
    return
 Time;  // 数组名本质是首元素地址,直接返回即可(指针返回值)
}

int
 main(void)
{
    int
 *pt;  // 定义整型指针,用于接收GetTime函数返回的数组地址

    pt = GetTime();  // 接收数组首地址,pt指向Time数组的第一个元素

    // 打印数组元素:两种访问方式(指针解引用 / 数组下标)等价

    printf
("pt[0]=%d\n", *pt);          // 访问第1个元素(等价于pt[0]、Time[0])
    printf
("pt[1]=%d\n", *(pt + 1));    // 访问第2个元素(等价于pt[1]、Time[1])
    printf
("pt[2]=%d\n", *(pt + 2));    // 访问第3个元素(等价于pt[2]、Time[2])

    return
 0;
}
```

综合例子——C语言文件操作,参数和返回值都是指针。

```c
#include <stdio.h>


int
 main(void)
{
    // 修正:1. 路径转义字符\\ 2. 字符串引号格式 3. 补充文件打开失败判断

    FILE* f = fopen("D:\\fileTest.txt", "w");  // 创建/覆盖文件,"w"只写模式
    if
 (f == NULL)  // 增加健壮性判断:文件打开失败(如路径不存在)时直接退出
    {
        printf
("文件打开失败!\n");
        return
 -1;
    }

    fputc('A', f);                // 向文件写入单个字符'A'
    fputs
("Hello Conan!", f);      // 向文件写入字符串"Hello Conan!"
    fclose(f);                     // 关闭文件,释放文件资源

    return
 0;
}
```

③直接访问物理地址下的数据(单片机常遇到)

访问硬件 指定内存 下的数据,如 设备ID号 等(stc单片机ID读取演示:)

```c
#include "LCD1602.h"


void
 main(void)
{
    unsigned
 char code *p;  // 定义指向代码区(unsigned char)的指针

    LCD_Init();  // 初始化LCD1602显示屏
    // 在LCD1602第1行第1列显示字符串"Hello Conan!"

    LCD_ShowString(1, 1, "Hello Conan!");

    // 强制转换地址0x1FF9为代码区unsigned char指针类型,赋值给p

    p = (unsigned char code *)0x1FF9;

    // 在LCD1602第2行第1列显示p指向地址的1字节数据(2位十六进制)

    LCD_ShowHexNum(2, 1, *p, 2);
    // 在LCD1602第2行第3列显示p+1地址的1字节数据(2位十六进制)

    LCD_ShowHexNum(2, 3, *(p+1), 2);
    // 省略后续类似显示代码...


    // 死循环,维持LCD显示状态

    while
 (1)
    {

    }
}
```

将复杂格式的数据 转换为字节 ,方便通信与存储。如float、double、struct等类型。

二、关键字解析

1、extern的作用

extern用来声明变量或者函数,extern声明不是定义,也不分配存储空间。

如果一个文件定义了函数或者变量,这个时候如果想在其他文件使用:(两种方法)

①用头文件声明,再引用头文件

②在其他文件中直接使用extern

2、static的用法

static可修饰局部变量、全局变量以及函数。

①修饰局部变量

静态局部变量使用static修饰符定义,static修饰局部变量可以把它初始化为0,且金泰局部变量存储与进程的全局数据区,即使函数返回,它的值也会保持不变(继承性)

②修饰全局变量

静态全局变量仅当前文件可见,其他文件不可访问,其他文件可以定义与其同名的变量,两者互不影响定义在函数体外部,在全局数据区分配存储空间,编译器会自动对其初始化。

③修饰函数

静态全局变量仅当前文件可见,其他文件不可访问;不同的文件可以使用相同名字的 静态函数 ,互不影响。

3、const与 #define区别

① 执行程序时: define是在编译的预处理阶段起作用;而const是在编译、运行的时候起作用。

② 对程序的作用: define只是简单的字符串替换,没有类型检查;而const有对应的数据类型,是要进行判断的,可以避免一些低级的错误。

const int n = 10;const修饰变量n后,保护了变量n,使其不能被赋值修改。const.c

const int *p = &n;n里面的值不能被改变,*p = 20会报错。const1.c

const放在*的左边 ,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来修改。

const修饰过的指针变量p不能修改a中的内容,而没有用const修饰过的指针变量q照样可以修改a中的内容,而且a自己也可以重新给自己赋值。

const放在*右边 ,修饰的是指针变量本身,保证了指针变量不能被修改。int* const p = &n指向n的行为不能被改变,如p=&m会报错。

拓展:

#define分为无参宏和有参宏 。定义有参宏时“形参”必须紧跟宏名,中间不可有空格。宏定义时的形参和调用时的实参不像函数那样采用“传值”的方式,而只是一种简单的字符替换。

4、typedef的理解

第一种理解:将变量重新定义新名字

系统默认的所有基本类型都可以利用typedef关键字来重新定义类型名,如typedef int data;之后用到int的地方都可用data替换。

第二种理解:将结构体重新定义新名字

```c
// 定义学生结构体(包含姓名、年龄、学号成员)

struct student {
    char
 name;    // 学生姓名(单个字符)
    int
 age;      // 学生年龄
    int
 number;   // 学生学号
};

// 为struct student结构体定义别名stu,简化后续使用

typedef
 struct student stu;
// 注释:此时struct student 和 stu 等价,均可作为结构体类型使用(类似int、char等基本类型)


// 定义并初始化stu类型的结构体变量conan

stu conan = {'A', 10, 110};
```

三、内存分区

①静态存储区: 操作系统分配使用。存储全局变量、static修饰的静态变量、exe程序代码等

②堆存储区: 程序员分配、使用,相信程序员但存在风险,用的时候分配不用的时候释放。堆区存储动态内存分配的(new/malloc)

③栈存储区: 编译器、程序分配使用,函数调用。栈区存储局部变量、函数的参数。函数调用数据都在栈上,会进行形参和返回值的压栈和出栈。

一个不太对的理解但有一定道理 ——栈中一般存的是指针,像一个目录;堆中存放实际内容,类似目录对应的具体内容。

堆和栈的空间都是在RAM上,堆是一个动态的概念,是在运行的时候确定的(空间大小在变化,决定于运行此刻的数据);栈是一个静态的概念,是在编译的时候确定的(空间大小已经定好)。访问效率上,堆因为是动态的,速度要慢,栈更快些;访问权限上,一个函数调用数据都是在栈上的,函数被调用后它栈上的数据无法被另一个函数访问,不同函数之间的栈数据不可共享,这个原则也适用于多线程,每个线程也有各自的栈。堆则不同,堆是进程上的堆,只要在一个进程上一个applicat内所有的线程都可访问堆上的数据。扩展方向,栈由高地址向低地址进行扩展,堆则是由低到高地址。

当 数据不确定时用堆,确定的话用栈(速度高) ;想使用庞大内存时一般用堆,用的时候分配不用的时候释放。

四、结构体&联合体(共用体)&枚举类型

1、结构体

定义结构体变量是需先对结构体类型进行声明:

```c
// 定义时间结构体,包含时、分、秒三个成员

struct Time
{
    unsigned
 char hour;    // 小时(0~23)
    unsigned
 char minute;  // 分钟(0~59)
    unsigned
 char second;  // 秒(0~59)
};
```

使用时的定义格式:

```c
struct Time t1;

t1.hour=1;

t1.minute=2;

t1.second=3;
```

Time叫作结构体类型名,t1叫作结构体变量名。 struct Time相当于int 这种一般类型。每个成员之间用 分号“;” 隔开。

2、联合体(共用体)

与结构体十分相似,最大不同是结构体变量各成员是按声明顺序存放在一块连续的存储空间,各成员分别分配相应大小的存储单元;共用体变量各成员则是共同使用一段存储空间,各成员的首地址都相同,存储空间按其中最大的成员分配,所以给共用体变量的某个成员赋值后将影响到其他成员。

也是先声明再使用:

```c
// 定义共用体MulType:所有成员共享同一块内存空间,同一时间仅能有效存储一种类型数据

union MulType
{
    char
 chardata;    // 字符型成员
    int
 intdata;      // 整型成员
    long
 longdata;    // 长整型成员
};

// 定义MulType类型的共用体变量m1

union MulType m1;

// 给共用体字符型成员赋值(赋值后,其他成员的值会因内存共享被覆盖/失效)

m1.chardata = 'A';

// 给共用体长整型成员赋值(此赋值会覆盖之前chardata的内存数据,chardata变为无效)

m1.longdata = -100;
```

3、枚举

特点是其变量只能在限定的范围内取值。大括号内的各成员是常量(①②是变量),叫作 枚举常量 列表,各常量用 逗号 分隔,它们 不用进行数据类型声明 ,仅仅是我们自己定义的一个个标识符,具体含义C语言并不关心,编译时用从0开始的整数依次代表这些枚举常量。

也是先声明类型,再使用,使用时与①②不同,wd是个枚举变量整体,它的 取值被限定在大括号里的枚举常量中,保障了变量取值安全 。

```c
// 定义枚举类型Weekday:表示一周的七天,枚举常量默认从0开始递增(Mon=0, Tue=1, ..., Sun=6)

enum Weekday
{
    Mon,    // 星期一
    Tue,    // 星期二
    Wed,    // 星期三
    Thu,    // 星期四
    Fri,    // 星期五
    Sat,    // 星期六
    Sun     // 星期日
};

// 定义Weekday类型的枚举变量wd,并将枚举常量Mon赋值给它

enum Weekday wd;
wd = Mon;
```

Mon,Tue这些枚举常量编译时只是整数,写成这样只是增加了可读写。所以枚举类型的数据可像 整数一样进行运算,也可用于switch、for等语句的表达式,或作为函数参数 等。

五、可变参数

如果观察过C语言中各函数的原型,可发现有些函数的参数是省略号“...”,如与输入输出有关的几个函数,例如printf和sprintf:

```c
// 标准库函数声明:格式化输出

// 1. printf - 格式化输出到控制台

int
 printf(const char *format, ...);

// 2. sprintf - 格式化输出到字符串缓冲区

int
 sprintf(char *str, const char *format, ...);
```

省略号叫作可变参数,因为printf这类函数支持多个参数,例如:

```c
printf
("%d%d%d",a,b,c);
```

每多写个%d,后面就可多写个参数。实现时主要借助了va_start()和va_arg()两个函数和一种新的类型va_list。

举例:

```c
#include <stdio.h>

#include <stdarg.h>  // 必须包含可变参数头文件,提供va_list/va_start等宏定义


/**
 * @brief  可变参数函数:接收固定参数+字符串类型可变参数,打印可变参数内容
 * @param  first: 第一个固定整型参数
 * @param  last:  最后一个固定整型参数(用于va_start初始化指针)
 * @param  ...:   可变参数列表(此处为字符串类型参数)
 * @retval 无
 */

void
 f(int first, int last, ...)
{
    va_list pArgs;  // 定义可变参数指针,用于访问可变参数列表
    // 初始化可变参数指针:指向第一个可变参数(last是最后一个固定参数)

    va_start(pArgs, last);

    int
 i;
    // 循环遍历可变参数(此处已知有2个字符串参数,循环2次;实际需自定义方式确定次数)

    for
 (i = 0; i < 2; i++)  // 修正:原i<=2会循环3次,超出实际可变参数个数(仅2个)
    {
        // 提取可变参数:第二个参数指定参数类型(char *,字符串类型)

        char
 *val = va_arg(pArgs, char *);
        printf
("%s\n", val);  // 修正:补充分号,解决编译语法错误
    }

    va_end(pArgs);  // 销毁可变参数指针,释放相关资源
}

int
 main(void)  // 优化:补充标准无参数格式void,更规范
{
    // 调用可变参数函数:固定参数(1,2) + 可变参数("hello","world")

    f(1, 2, "hello", "world");
    return
 0;
}
```

va_arg()函数第二个参数是可变参数的类型,这就是为什么printf要传入%d参数,就是简介告诉va_arg()函数可变参数的类型,va_arg()函数的返回值val就是参数的具体数值,上面直接就把val传入printf了,所以上面只是个简单demo。

实际实现printf还有许多要做,如遍历字符串、解析%等。 另外实现printf函数是一个典型笔试题!

总结

遇到挫折,要有勇往直前的信念,马上行动,坚持到底,决不放弃,成功者决不放弃,放弃者绝不会成功。成功的道路上,肯定会有失败;对于失败,我们要正确地看待和对待,不怕失败者,则必成功;怕失败者,则一无是处,会更失败。

   
115   次浏览       6 次
相关文章

深度解析:清理烂代码
如何编写出拥抱变化的代码
重构-使代码更简洁优美
团队项目开发"编码规范"系列文章
相关文档

重构-改善既有代码的设计
软件重构v2
代码整洁之道
高质量编程规范
相关课程

基于HTML5客户端、Web端的应用开发
HTML 5+CSS 开发
嵌入式C高质量编程
C++高级编程

最新活动计划
AI大模型编写高质量代码 2-9[在线]
基于UML+EA进行分析设计 2-3[北京]
需求分析与管理 2-9[北京]
基于模型的数据治理 3-10[北京]
UAF与企业架构 2-3[北京]
ASPICE4.0核心开发过程 3-21[上海]
嵌入式软件测试 3-27[上海]
 
 
最新文章
.NET Core 3.0 正式公布:新特性详细解读
.NET Core部署中你不了解的框架依赖与独立部署
C# event线程安全
简析 .NET Core 构成体系
C#技术漫谈之垃圾回收机制(GC)
最新课程
.Net应用开发
C#高级开发技术
.NET 架构设计与调试优化
ASP.NET Core Web 开发
ASP.Net MVC框架原理与应用开发
成功案例
航天科工集团子公司 DotNet企业级应用设计与开发
日照港集 .NET Framewor
神华信 .NET单元测试
台达电子 .NET程序设计与开发
神华信息 .NET单元测试