小心C语言的定义与声明

C语言函数声明与定义:编译器视角的契约本质 函数声明是C语言分离编译模型中的核心契约机制,它在编译阶段提前约定函数接口(参数类型、数量、返回值),使编译器能静态校验调用合法性,避免运行时类型错配;函数定义则提供具体实现,二者共同支撑多文件项目的模块化构建。这种声明/定义分离的设计,直接决定了头文件(.h)与源文件(.c)的职责边界,并成为链接器解析符号依赖、解决undefined reference错误的前提。理解该机制,是掌握C语言工程化开发、VSCode多文件编译配置及Makefile构建演进的基础。 阅读详情

注:为便于说明问题,文中提及的变量和函数都被简化。

一、起源

DBProxy在测试过程中,发现对其执行某步管理操作后,程序有时会崩溃,但不是每次都出现。

二、GDB跟踪

反复多次测试,然后用GDB打开core dump文件,查看程序崩溃时的堆栈,发现可能的崩溃只有两处,这两处的共同点是前面都调用了一个函数get_pointer得到一个指针,如下图所示:

然后在使用该指针进行下步操作时程序崩溃。

查看该指针的值,发现其指向一个无效地址,所以操作该地址产生了段错误,如下图所示:

三、无效地址的产生原因

函数get_pointer的原型是char *get_pointer(void),函数体内只是通过简单的malloc操作获取一个char*类型的指针,然后返回给调用者。
malloc()结果只有两种:

  1. 成功,返回一个指向合法地址的指针
  2. 失败,返回NULL

为什么会返回一个无效地址呢?
在get_pointer函数返回前加入一个printf语句,打印即将返回给调用者的指针值:

再在调用处之后也加入一个printf语句,打印调用者接收到的指针值:

重新编译,反复进行多次测试,发现打印出的两个值有时相同有时不同,相同时程序正常运行,不同时程序必然崩溃。
更重要的是,这些值看上去很有规律:

p=1c34abd0
pointer=1c34abd0
正常运行

p=38ac7bda1690
pointer=7bda1690
接下来崩溃

p=a5ef6824
pointer=ffffffffa5ef6824
接下来崩溃

……

观察到一个现象:如果p值是4字节(即高4字节为0)且低4字节的最高位为0,则二者相同,否则二者必定不同。而pointer值的高4字节只有两种情况,一种是0×00000000,一种是0xFFFFFFFF,再结合低4字节的首位,可以看出pointer值的高4字节是由p值的低4字节补全了高4字节形成的。

四、遗漏的头文件

注意到编译时有如下信息输出“警告:初始化时将整数赋给指针,未作类型转换”。当

初察看该函数,发现返回的是指针,认为是编译器的误报,未作处理。重新审视该警告信息,结合调用处的源文件,发现该文件没有包含声明get_pointer函数的头文件。在源文件头部添加#include语句,包含该头文件后,重新编译,警告消失,反复测试,程序不再崩溃,非常稳定。看来问题产生的原因是因为漏包含了头文件,那么为什么不包含也可以编译通过呢?

五、一个简单的试验

/**********a.c**********/

#include <stdio.h>

int main()

{

void *p = func();

printf(“p=%lx\n”, p);

return 0;

}

/**********b.c**********/

void *func()

{

void *p = (void*)0x1234567890ABCDEF;

return p;

}

gcc –c a.c b.c,系统提示:

虽然有警告,但是编译却成功了。

我们再来链接一下,gcc –o all a.o b.o,链接也成功了。

运行程序,./all,输出结果是p=FFFFFFFF90ABCDEF,而我们期待的值是1234567890ABCDEF,问题重现了。

加上-Wall选项后重新编译,发现系统多了一行输出“警告:隐式声明函数 ‘func’”,

查阅了隐式声明的相关资料得知,原来,编译器会将所有隐式声明的函数的返回值类型都认定为int。

如此一来原因就比较清楚了,在b.c里定义的func函数返回的确实是指针类型,而在a.c里认为func返回的是int类型,程序运行时会将func返回的指针类型值强制转换为int,然后再强制转换为void*,赋给变量p。在64位机上,指针为8字节,int为4字节,在由指针转换为int时,高4字节被丢弃,值由0x1234567890ABCDEF变为0x90ABCDEF,然后在由int转换为指针的过程中,根据有符号数的补齐原则,按照int最高位是0还是1,将高4字节每一位全部补全为0或1。0x90ABCDEF的最高位是1,所以高4字节每一位都补全为1,最终形成了结果0xFFFFFFFF90ABCDEF。

那为什么实际运行时不是每次都崩溃呢?这是因为被调用的函数所返回的指针是动态分配的,其值事先不固定,如果被初始化的指针地址的高4字节和低4字节的最高位原本就是0,如0×0000000012345678,那么在将强制转换为int时丢弃高4字节对其就没有任何影响了,值还是0×12345678,然后再由int转为指针,高4字节补0,值为0×0000000012345678,所以程序可以正常运行下去。

六、编译与链接、定义与声明

在编译阶段,各个源文件独立编译,所以a.c和b.c是分开编译的,a.c里调用了func

函数而没有包含其声明(声明一般使用#include “b.h”,也可以使用extern函数原型的形式),编译器会认为func函数为隐式声明,将其返回值类型定为int。所以编译虽然有警告,但却成功了。

编译阶段是不需要函数的定义的。我们把b.c里的func函数注释掉,gcc –c a.c b.c一样可以执行成功。

在链接阶段,链接器将所有源文件编译得到的二进制文件以及调用的库链接到一个可执行文件中,此时链接器会去找func函数的具体定义,以供main函数调用。因为func函数确实有定义,所以链接也会成功。

链接阶段必须有函数的定义,否则链接器会报错。我们还是注释掉func,编译后再执行gcc –o all a.o b.o,系统输出如下:

在运行阶段,因为a.c在编译时认为func返回int类型,所以func的返回值(8字节指针)被截断为4字节的int,然后再进行高4字节的扩展,最后赋给了main函数里的变量p。在这两次类型转换中,p的值就有可能与func的返回值不相同了,p实际上已经成为一个野指针。

我们换用g++来编译看下效果:

看来g++对语法要求更严格,不允许隐式声明func函数。

返回值有可能因为隐式声明而不符合我们的期望,那么函数的参数呢?我们再来尝试一下。

/**********c.c**********/

extern void func(long);

int main()

{

func(0x1234567890ABCDEF);

return 0;

}

/**********d.c**********/

#include <stdio.h>

void func(int a)

{

printf(“a=%x\n”, a);

}

gcc -c c.c d.c -Wall,成功。

gcc -o all c.o d.o -Wall,成功。

运行程序./all,输出a=90abcdef,这显然不是我们想要的结果,但在编译和链接时却没有任何错误或警告报出。

使用g++编译,无法通过。

如果我们新建一个头文件d.h,将func函数的原型在d.h里声明,然后在c.c和d.c里都包含d.h,就可以避免参数或返回值可能的不一致了。

七、总结

  1. 在开发过程中,应该严格遵循先声明后定义、先声明后使用的原则,一方面保持良好的编码风格,另一方面也能避免很多潜在的错误;
  2. 从参数不一致造成的问题来看,最好不要使用extern声明函数,而应该使用包含头文件的形式;
  3. 编译时打开-Wall选项,对于编译过程中输出的每个WARNING都要仔细检查,防止出现各种匪夷所思的bug;
  4. 在某些场合,使用g++代替gcc可以获得更好的安全性。
C语言变量定义与声明详解:从内存分配到多文件编程实践 在编程语言中,变量作为数据存储的核心容器,其定义与声明机制是理解程序内存管理的基础。从原理上讲,变量定义会触发编译器分配实际内存空间,而声明仅向编译器提供类型信息以便进行类型检查。这一机制的技术价值在于支持模块化编程,允许代码分离与复用,是实现多文件项目组织的关键。在应用场景中,特别是在C语言开发中,正确处理定义与声明的关系能有效避免链接错误,确保程序正确编译和执行。本文聚焦于C语言中变量的定义与声明,深入解析其区别、内存分配机制及在头文件和多文件项目中的正确使用方法,帮助开发者掌握变量作用域、链接性和存储 阅读详情

相关推荐

干货收藏:68道C语言与C++常见面试题

1 变量的声明和定义有什么区别 变量的定义为变量分配地址和存储空间, 变量的声明不分配地址。一个变量可以在多个地方声明, 但是只在一个地方定义。 加入extern 修饰的是变量的声明,说明此变量将在文件以外或在文件后面部分定义。 说明:很多时候一个变量,只是声明不分配内存空间,直到具体使用时才初始化,分配内存空间, 如外部变量。 来源|公众号:C语言与CPP编程 int main() { extern int A; //这是个声明而不是定义,声明A是一个已经定义了的外部变量 /

编程与实战的博客 5245

ESP32-C3烧录后打印报错invalid header: 0xffffffff【flash_download_tool_3.9.3.exe工具烧录】

选择好地址后点击start,需要板子置boot,就是按住板子的boot按键1s,使其是低电压(0)。如果你是ESP32-C3系列,你需要选择你的bootloader.bin是0x0000开始。下载模式:需要GPIO2为(1),GPIO8为(1),GPIO9为(0)就是boot置0。如果你是ESP32其他系列,你需要选择你的bootloader.bin是0x1000开始。运行代码模式:GPIO2为(1),GPIO8无关,GPIO9为(1)。其次,你需要使用万用表来确定你的ESP32板子下载引脚是否置位正确。

weixin_52991272的博客 8659

宝塔面板磁盘扩容后不生效?5步搞定Linux服务器磁盘空间更新(附详细命令)

本文详细解析了Linux服务器磁盘扩容后宝塔面板显示不生效的常见问题。通过理解磁盘、分区、文件系统三层逻辑,提供5步解决方案:检查磁盘状态、安装growpart工具、扩展分区、使用resize2fs命令扩展文件系统,并最终验证结果。文中附有详细命令和针对XFS、LVM等进阶场景的指南,帮助用户彻底解决磁盘空间更新问题。

weixin_29271263的博客 127

esp32-c3-m1的micropython环境搭建过程报invalid header: 0xffffffff解决

esp32-c3-m1的micropython环境搭建过程报invalid header: 0xffffffff解决

qq_50322135的博客 3824

深入解析UVM寄存器模型:reg2bus与bus2reg的隐式调用机制

本文深入解析了UVM寄存器模型中reg2bus与bus2reg函数的隐式调用机制。通过追踪一次寄存器访问的完整旅程,揭示了这两个关键函数是如何在uvm_reg_map和uvm_reg_predictor中被框架自动调用的,从而在抽象的寄存器操作与具体总线事务之间完成转换,并实现镜像值的自动更新。掌握此机制是高效调试和灵活应用UVM寄存器模型的基础。

weixin_29211309的博客 439

【最全】ISTQB- FL模拟题(含答案)

汇总收集的所有ISTQB基础级考题和答案

maoxuexue的博客 1万+

关于ESP32出现invalid header:0xffffffff等的解决方法

当我们完成了ESP32的编译时,却出现了invalid header:0xffffffff或者rst:0x7 (TG0WDT_SYS_RST),boot:0x2a (SPI_FAST_FLASH_BOOT)的问题。该问题出现的本质就是固件地址的错误。找到我们的bootloader.bin,partition-table.bin,hello_world.bin(根据的情况修改)的起始地址,然后再填到我们的下载工具即可解决。

qq_67779631的博客 2217

沈阳计算机研究所复试,沈阳计算所复试试题附答案.doc

沈阳计算所复试试题附答案目录数据结构1200612007220085200914操作系统16200616200718200818200919组成原理20200620200720200821200921经典算法22数据结构2006数据结构部分(共25分,第1题10分,第2题15分)在(下图)所示的有向图中:v1 v2v4 ...

weixin_42511053的博客 859

ESP32-S3通过Thonny烧录Micropython固件后报错invalid header:0xffffffff

Thonny与ESP32-S3通信时报错invalid header:0xffffffff

YTCLEE的博客 2053

Juniper SRX PPPoE配置

set interfaces pp0 unit 1 ppp-options chap default-chap-secret 拨号用户名密码。set interfaces pp0 unit 0 ppp-options chap default-chap-secret 拨号密码。set interfaces pp0 unit 0 ppp-options chap local-name 拨号用户名。6号口接运营商进行拨号。

Jian Sun_的博客 750

跨境电商产品信息抓取:使用Python爬虫进行市场分析

跨境电商平台是指为不同国家和地区的买卖双方提供在线交易服务的电子商务平台。亚马逊(Amazon):全球最大的电商平台之一,提供丰富的商品种类和全球配送服务。eBay:全球知名的在线拍卖和购物网站,支持个人和企业卖家。速卖通(AliExpress):阿里巴巴旗下的跨境电商平台,主要面向全球消费者提供中国制造的商品。Wish:以移动端为主的跨境电商平台,主打低价商品和个性化推荐。这些平台上的商品数据包括商品名称、价格、销量、用户评价、库存等信息,对于市场分析具有重要价值。

2201_76125261的博客 1232

ESP32-S3 上电不断输出invalid header: 0xffffffff

注:如果BOOT脚接地,仍无法进入烧录模式。则对照手册看一下strapping管脚是否被占用。1、将BOOT引脚接地,上电进入烧录模式。原因:芯片内部固件问题。2、烧录正确的固件即可。

oHuanCheng的博客 8979

基于Cesium实现,动态水面+大坝排水动画

一、绘制面状水域,使用水波纹理,进行渲染,水体透明,可以看到底色。三、使用粒子系统,将喷泉改变方向,加上重力设置,实现排水动画。二、绘制线状三维体,用作模拟大坝拦截效果。四、最后,模拟水位上升,进行淹没分析,

一醉千秋的专栏 1418

c语言修改上述已声明变量,小心C语言的定义与声明

注:为便于说明问题,文中提及的变量和函数都被简化。一、起源DBProxy在测试过程中,发现对其执行某步管理操作后,程序有时会崩溃,但不是每次都出现。二、GDB跟踪反复多次测试,然后用GDB打开core dump文件,查看程序崩溃时的堆栈,发现可能的崩溃只有两处,这两处的共同点是前面都调用了一个函数get_pointer得到一个指针,如下图所示:然后在使用该指针进行下步操作时程序崩溃。查看该指针的值...

weixin_39781326的博客 161

反爬虫:POST 请求参数中 Sign 的加密机制详解,并设计带 Sign 校验的网站

本文介绍了API请求中sign签名的生成原理及实现方法。sign是通过对请求参数加密生成的一串字符串,用于验证请求合法性,防止参数篡改、重放攻击和伪造请求。其生成步骤包括参数排序、拼接字符串、加盐和哈希加密。文章还演示了如何在FastAPI后端实现带sign验证的新闻网站,包括参数校验、时间戳验证和签名比对等安全措施,有效提升API安全性,增加爬虫逆向难度。

数据知道的博客 4075

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

c语言可执行文件老是被360阻止,小心C语言的定义与声明

注:为便于说明问题,文中提及的变量和函数都被简化。一、起源DBProxy在测试过程中,发现对其执行某步管理操作后,程序有时会崩溃,但不是每次都出现。二、GDB跟踪反复多次测试,然后用GDB打开core dump文件,查看程序崩溃时的堆栈,发现可能的崩溃只有两处,这两处的共同点是前面都调用了一个函数get_pointer得到一个指针,如下图所示:然后在使用该指针进行下步操作时程序崩溃。查看该指针的值...

weixin_32384503的博客 740

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

C语言基础语法的总结_stm32中语句语法的学习

include “main.h”//这个需要添加,不然的话,就会主函数在查找相应的函数体或者是,什么之类的话,会遇到问题。//这个只是表示要应用外部的变量,在具体的变量的文件中,要引用定义时在头文件中。PS:这样做的话,就会使相应的变量能在整个工程上使用,但是,这个在使用上要十分小心;//这种边声明,边定义的方式是错的。如果在main.c中使用的时候,要注意,就是extern int a;#include “b.h”//要引用这个,然后再进行现有的调用;//这种方式是正确的。在b.c中写了 int a;

2401_87555661的博客 255

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

c language

第一次学习: 1、&ptrreturns the address of the pointer variable... pointer to a pointer if you will. 返回指针变量的地址,指向指针的指针。 2、round(number,digits) 四舍五入的值,digits小数点后面保留的位数 3、ceil (double x) 返回大于或者等于指定表...

探究苹果技术原理和学习新技术(锲而舍之,朽木不折;锲而不舍,金石可镂。) 884

【C语言】---- 指针数据类型(Pointer)

在C语言中,指针是一个用于存储变量地址的特殊数据类型。指针可以用于直接访问和修改内存中的数据,是实现动态内存分配和高效数据处理的重要工具。指针是C语言中强大而灵活的特性,但也容易导致内存访问错误。因此,在使用指针时要特别小心,确保正确地管理内存和指针的生命周期,以避免潜在的错误。

欢迎来到我的博客 696

tms320c2000 c语言伪指令,DSP C

15.2.6 asm语句C编译器可在编译器输出的汇编语言程序中直接输出汇编指令或语句。利用asm语句嵌入汇编语言程序,可实现一些C语言实现起来比较麻烦的硬件控制功能。asm语句在语法上就象是调用一个函数名为asm的函数,函数参数是一个字符串:asm (“assembler text”);编译器会直接将“参数字符串”复制到输出的汇编语言程序中,因此必须保证参数双引号之间的字符串是一个有效的汇编语言指...

weixin_33325305的博客 511

6.1 C语言函数的定义与声明

6.1 C语言函数的定义与声明在C语言中,函数是执行特定任务的代码块,可以提高代码的模块化和重用性。每个函数可以接收参数、执行一系列操作,并返回结果。函数的定义函数的定义包括函数的返回类型、函数名、参数列表以及函数体。返回类型:指定函数返回值的数据类型。函数名:函数的唯一标识符。参数列表:在括号内指定,包括形式参数的类型和名称,用于接收传递给函数的值。函数体:在花括号{}内定义,包含函数的执行语句。基本语法返回类型 函数名(参数类型 参数名1, 参数类型 参数名2, ...) {// 函数体。

Python老吕的博客 1138

函数的声明、定义、分类与调用(C语言)

函数的定义:指对函数功能的确立,包括指定函数名、函数值类型、形参及其类型、函数体等,它是一个完整的独立的函数单位。函数的声明:作用是把函数名、函数类型及形参的类型、个数和顺序通知编译系统,以便在调用该函数时编译系统能正确识别函数并检查调用是否合法。

多多想的博客 1268

声明为指针,定义为数组,声明为数组,定义为指针

之前写发的那篇指针和数组的文章,有网友评论觉得不是很舒服,我自己看了,觉得很不满意。所以想再写写,尽量把这个东西讲清楚。#定义为数组,声明为指针1.c中...

嵌入式Linux 1226

16 函数的声明与定义-嵌套定义

函数的声明与定义-嵌套定义

weixin_62213357的博客 357
上一篇: pache commons开源工具简介
下一篇: Makefile中的wildcard用法
程序员2
博客等级 码龄15年 6粉丝 27原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值