收集整理了一份《2024年最新物联网嵌入式全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升的朋友。
需要这些体系化资料的朋友,可以加我V获取:vip1024c (备注嵌入式)
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人
都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
指针传递、引用传递都是传递对象的地址,传给函数时都是把这个地址压入栈,32位平台为四个字节,64位平台为八个字节,除结构实例、类实例外的标量类型,其数据长度均固定,可以精确的计算参数所需空间;我们做个简单的计算:取平台位宽为参数空间平均长度,以平均每个函数三个参数、1000级函数调用来计算,他们的占用空间如下:
32位平台(固定长度参数):1000 * 3 * 4Byte/ 1024Byte = 11.71875KB 64位平台(固定长度参数):1000 * 3 * 8Byte/ 1024Byte = 23.4375KB |
由此可以看出,标量类型、指针、引用等数据类型长度都小于等于机器字长,占用的空间小,入栈、出栈速度都是非常块的,一般情况下默认栈空间足够使用,不会出现堆栈溢出的问题;
哪些数据类型会潜在的降低程序效率呢?答案是结构类型、类类型,他们是程序低效率的潜在幕后黑手;由于标量类型、指针、引用占用的空间等都是机器字长,标量类型无论使用哪种方式传递,和指针、引用都是同样的速度;结构类型、类类型的值传递方式呢?
咱们需要了解一下结构类型、类类型的值传递过程:调用参数类的复制构造函数生成新的类型实例并入栈,复制构造函数编译器自动生成,用户亦可以自己编写一个;我们可先看一个案例:
struct TestClass { public: ~TestClass() {AfxMessageBox(“~TestClass()”);} TestClass() {AfxMessageBox(“TestClass()”);} TestClass(INT32 publicData, INT32 privateData, const CString & strName, constCString & strValue) : m_PublicData(publicData), m_PrivateData(privateData), m_DataName(strName), m_DataValue(strValue) { m_PublicWindow = new CWnd(); m_PrivateWindow = new CWnd(); AfxMessageBox(“TestClass(INT32, INT32, CString, CString)”); } explicit TestClass(const TestClass & obj) {AfxMessageBox(“TestClass(const TestClass & obj)”);} void operator=(const TestClass & obj) {AfxMessageBox(“void operator=(const TestClass & obj)”);} void Click() { CString strText(“”); strText.AppendFormat(“Click(%d, %d, %s, %s, %p, %p)”, m_PublicData, m_PrivateData, m_DataName, m_DataValue, m_PublicWindow, m_PrivateWindow); AfxMessageBox(strText); } public: INT32 m_PublicData; CString m_DataName; CWnd * m_PublicWindow; private: INT32 m_PrivateData; CString m_DataValue; CWnd * m_PrivateWindow; }; void DoValueArgs( TestClass obj ) { obj.Click(); } TestClass object(10000, 99999, “10001”, “88888”); DoValueArgs(object); object.Click(); |
案例代码运行图谱,从左到右从上到下顺序摆放 |
这段代码在 Visual C++ 编译器下运行的结果如上所示,其中多次执行最后三行代码,第三个窗口表现一致,由此我们可以判断出结构、类的复制构造函数不会深度复制对象,用值传递时会丢失数据。结构、类由于包含多个成员,逐个复制会倍数于标量和指针操作,带来了速度的降低;
前面的试验探讨了值传递、指针传递、引用传递,标量类型、指针、引用传递参数长度固定,安全高效,但结构、类的值传递方式带来诸多问题,例如堆栈溢出、数据丢失、效率低下等,建议结构、类完全使用指针或者引用传递;
安全隐患 | 简要说明 | 备注 |
堆栈溢出 | 如果结构和类都是很大,创建其副本会消耗大量的空间和时间,最终产生溢出错误 | |
数据丢失 | 类对象创建副本时,会受到类实现的影响而无法完全复制,参见文档《Effective C++》第二章 | 效率降低 |
3.5、变量生命周期
变量声明了,是不是直接使用就万事大吉了呢?我们当然希望就是这么简单,动态语言和托管类型语言确实实施了严格初始化机制:变量只要声明就初始化为用户设置的初始值或者零值;然而 C/C++ 不是这种实施了保姆级初始化机制的语言,透彻了解 C/C++ 的初始化规则对帮助我们写出健壮的程序大有裨益;
3.5.1、变量内存分配
C / C++ 支持声明静态变量(对象)、全局变量(对象)、局部变量(对象)、静态常量等,这些变量在分配时机、内存分配位置、初始化等方面上有些细微上的差别,熟悉并掌握他们对于写出正确的程序非常有帮助,请看下表:
生命周期 | 变量类型 | 分配时机 | 初始化 |
全局生命周期 (Global lifetime) (C: Static) | 函数 | 编译时, 虚拟方发表 | |
全局变量 | 编译时, | 首次执行,默认置零或赋值 | |
全局对象 | 编译时, | 首次执行,构造函数 | |
全局静态变量 | 编译时, | 首次执行,默认置零或赋值 | |
全局静态对象 | 编译时, | 首次执行,构造函数 | |
局部静态变量 | 编译时, | 首次执行,默认置零或赋值 | |
局部静态对象 | 编译时, | 首次执行,构造函数 | |
局部生命周期 (Local lifetime) (C: Automatic) | 局部变量 | 执行时,栈(Stack) | 可选:赋值操作 |
局部对象 | 执行时,堆(Heap) | 构造函数 |
对象创建后的成员数据取决于构造函数及其参数,系统自动生成的构造函数是不会初始化成员变量的;
对于函数、结构实例、类实例中的变量,编译器不会自动初始化,其值是不确定的,故直接使用会导致不确定的行为,这就是实践中经常碰到的程序行为表现莫名其妙的根源所在;
对于动态分配的内存(new/delete、new[]/delete[]、malloc/free),默认是不会置初值的,需要显式的初始化;对于结构和类型实例,new/new[]操作会自动调用构造函数初始化内存,详情请参见【对象初始化】;
【注】使用 VirtualAlloc/VirtualAllocEx 分配的虚拟内存会自动化初始化为零值;
【注】使用 HeapAlloc 分配的堆内存可以通过参数设置初始化为零值
3.5.2、变量初始化
从前面的变量初始化中得知结构实例、类实例、函数中声明的变量是不会自动初始化的,需要用户显式的初始化;值类型相对比较安全,可以声明时即初始化,这是最安全的作法;
数据类型 | 声明即初始化 | 备注 |
标量类型 | int data = 10; double cost = 999.22; | 所有算数类型和指针类型 |
聚合类型 | int x[ ] = { 0, 1, 2 }; char s[] = {‘a’, ‘b’, ‘c’, ‘\0’}; POINT stPoint = {0, 0}; | 数组、结构、联合类型 |
字符串类型 | char code[ ] = “abc”; char code[3] = “abcd”; | Microsoft C/C++ 支持最长2048字节的字符串 |
C/C++ 提供了两种初始化的机制可以完成结构实例和类实例的初始化,他们是:
初始化机制 | 简要说明 | 备注 |
构造函数 | 1、用户使用 new/new[] 操作时自动调用 2、构造函数顺序:从基类到子类逐层调用 3、成员变量可在构造函数主体执行前初始化 | 编译器会自动安插基类构造函数调用代码 |
用户函数 | 用户自定义并显式调用完成实例对象初始化, 例如:Initialize(); | 容易忘记调用 |
子类的构造函数被 new/new[] 操作时自动触发,它首先调用最底层基类的构造函数对其成员进行初始化,以此类推直到子类构造函数完成整个初始化过程;编译器会自动在子类构造函数的最前面中插装基类的默认构造函数以完成基类数据的初始化,如需要传递特别参数,则需要显示的调用基类构造函数。
由于类存在继承关系,基类和子类的构造函数调用存在着先后顺序关系,这意味着新对象的内存空间初始化会因为构造函数的调用顺序而呈现不同的状态:即这个对象内存块是一部分一部分的初始化; 由于这个特点,缺陷的幽灵就有了可乘之机,我们先看一个案例:
0001 class Base { 0002 public: 0003 Base():m_IntData(0){Initialize();} 0004 ~Base(){} 0005 virtual Initialize() {m_IntData = 10;} 0006 private: 0007 int m_IntData; 0008 } 0009 0010 class Derived : public Base { 0011 public: 0012 Derived() {m_pBuffer = malloc(4096);} 0013 ~Derived() {free(m_pBuffer);} 0014 virtual Initialize() {strncpy(m_pBuffer, “Testing…”, _TRUNCATE);} 0015 0016 private: 0017 void* m_pBuffer; 0018 } 0019 0020 Derived * pDerived = new Derived(); 0021 Base * pBase = dynamic_cast<Base *>(pDerived); 0022 delete pBase; 0023 |
上述代码由于继承关系和内存初始化的特点而产生了两处缺陷:
代码位置 | 缺陷说明 | 备注 |
Line 20 | 由于 Initialize 函数是虚拟的并且在子类中覆盖了子类的定义,当基类构造函数调用 Initialize 时,它使用了子类未分配的内存; | 产生崩溃 |
Line 22 | delete 操作调用Base类的析构函数,然后释放对象所占用的内存,导致未释放分配的内存; | 局部释放 |
【经验总结】
构造函数中要避免调用虚函数;
析构函数中要避免抛出异常;
3.5.3、变量多态与切片
在我们深入探讨这个问题前我们先看一个代码案例,然后我们基于这个案例讲解本节:
class Shape { public: virtual ~Shape(); virtual void Draw() const {} protected: uint32 m_lineWidth; uint32 m_lineColor; }; class Rectangle : public Shape { public: virtual ~Rectangle(); virtual void Draw(); protected: uint32 m_width; uint32 m_height; }; class Trapezium : public Rectangle { public: virtual ~Trapezium(); virtual void Draw(); private: uint32 m_widthUp; }; |
图(三)类(Trapezium)实例内存空间分布图
类继承关系带来了两个全新的概念:多态(类透视)和对象切片;这两类应用在面向对象编程(OOP)语言中都很常见两个技术;
多态常见的应用情况是对象泛化,即已基类视图操作对象。它的典型构成是基类数据结构视图 + 基类成员方法视图,从字面意思我们可以解读透视图只是视野范围的改变,即用户只能看到并调用基类定义视图中的数据和方法,而非数据和方法的改变,所以函数调用的依然是当前对象的方法。如图(四)所示展示的Shape透视图所示;
图(四)类(Shape)多态透视图
下面我们来举例为您演示一下多态类透视效果,通过基类指针指向同一个对象实例,只是透过基类的结构视图来调用相关方法,由于虚拟方法表指针指向同一个虚拟方法表,所以调用的还是同一个类的函数。
// 创建对象 Trapezium objTrapezium; objTrapezium.Draw(); // 演示多态(类透视) Shape * pShape = dynamic_cast<Shape *>(&objTrapezium); if (pShape) { pShape->Draw(); } // 演示切片 Shape objShape = (Shape)objTrapezium; objShape.Draw(); |
对象切片很好理解,相当于32位整数转换为16位整数时会根据目标类型裁减丢弃一部分数据,对象切片亦会裁减对象数据,它的变换过程是:分配目标类对象空间 è 复制源对象等长内存 è 设置虚拟方法表指针【如果有】,类对象切片与普通数据类型唯一的不同是它会切换对应的函数视图,如果有虚方法则还会切换虚拟方法表指针以确保调用正确的虚函数;
3.5.4、变量对象释放
自动分配的对象在离开其生命周期时会自动释放,这是由编译器自动保证的,一般情况下无需我们担忧;
我们需要关注的是对象指针所指的对象释放情况,尤其是跨越函数的对象值得关注,由于它的 new/delete、new[]/delete[]、malloc/free 等匹配性不明确,很容易被遗落而导致内存泄漏;比如模块A创建一个结构对象通过消息传递给模块B,模块B需要复制对象后即刻释放或者使用完毕后释放;
多态类型是我们需要着重关注的设计案例,它的析构函数在没有标记为虚函数和标记为虚函数的表现截然不同:
未标记为虚函数时它只会析构当前类实例,从对象指针类型开始向基类逐层析构,子类析构函数不会调用,会导致子类分配并持有的资源未释放,造成内存泄漏;
标记为虚函数时会按照对象指针所指对象类型往基类逐层调用其析构函数;
在图(三)所示案例中,如果基类 Shape 的析构函数未标记为虚函数,下面的代码会导致啥结果:
Shape * pNewShape = new Trapezium(); pNewShape->Draw(); delete pNewShape; |
是的,会发生内存泄漏!!!
释放对象导致内存泄漏的另一个典型案例是对象数组释放不匹配导致的,为了解释清楚这个问题,我们先看一看 delete 操作是如何实现的:
Complex * pc = new Complex(1,2); … delete pc; // 编译器将 delete pc 编译为如下代码: pc->~Complex(); // 先析构 ::operator delete(pc); // 释放对象内存 |
编译器释放对象的过程分两步:调用其析构函数释放持有的资源,然后释放对象占用的内存;由于对象数组用普通对象释放操作来释放,其结果是只有第一个对象的析构函数被调用,其它对象都未调用析构函数,导致其它对象持有的内存资源未释放;我们先看一个具体的案例:
string * pNameArray = new string[3]; // 此处省略 N 行代码 delete pNameArray; // 内存泄漏 |
您或许会问:字符串对象数组本身是否完全释放?根据技术分析来看,Visual C++ 编译器会完全释放,其它编译器不确定。由于它使用普通对象释放操作,第二个、第三个字符串对象未调用其析构函数,字符串对象持有的资源未释放,导致内存泄漏。
四、C++ 错误根源分析
前面我们回顾了各个方面的技术点,分析和解决实践中遇到的案例就比较容易了,下面请跟我一起来看看一些常见案例;
4.1、变量未声明
由于 C/C++ 是静态类型编译语言,这类型错误一般都在编译阶段就会发现,不会带入到运行时阶段,但是这种类型的错误客观存在,并且会增大我们的排错时间;
出现这种类型的错误一般源自两种情况,一种是从动态语言转为使用 C/C++ ,由于习惯问题而直接使用未定义的对象;另一种是由于粗心而写错了变量名字,导致编译器理解为一个新的未声明的变量。
4.2、变量初始化
变量初始化看似平淡无奇,但它却是我们程序运行过程中不确定行为的幕后推手,并且常常在我们意料之外;重视变量的初始化对于我们写出正确的程序非常重要;为了帮助各位认识到其重要性,我们先看几个案例:
CEdit * pNameEditCtrl; // 此处省略N行代码 CString strUserName pNameEditCtrl->GetWindowText(strUserName) |
上面的代码会导致程序运行崩溃:访问无效的指针;
LOGFONT stLogFont; stLogFont.lfHeight = 0 - MulDiv(10, this->GetDC()->GetDeviceCaps(LOGPIXELSY), 72); m_ListView.GetPaintManager()->SetItemsFontIndirect(&stLogFont, TRUE); |
上面的代码运行会导致不可预知的行为,实践中表现为字体异常粗大,界面错乱;
void CTestDialogDlg::OnOK() { INT32 nTestData; UINT32 uTestData; CString strTestData; strTestData.AppendFormat(_T(“%d, %d”), nTestData, uTestData); AfxMessageBox(strTestData); } |
上面的代码在不同的编译版本下表现出不同的行为,具体请看下面的输出:
在 Debug 状态下输出为:-858993460, -858993460,
在 Release 状态下输出为:4381392, 4381392
void CTestDialogDlg::OnOK() { CString strData; std::string stlText; CString strDisplay; strDisplay.AppendFormat(_T(“%s, %s”), strData, stlText.c_str()); AfxMessageBox(strDisplay); } |
上面的代码在不同的编译版本下表现出不同的行为,具体请看下面的输出:
在 Debug 状态下输出为:
在 Release 状态下输出为:
前面我们回顾知识点时介绍了只有全局变量(全局名字空间变量和子名字空间内变量)、静态变量会在首次执行时初始化,其它例如函数内局部变量、类成员变量、结构成员变量都不会自动初始化,每次执行时会为每一个变量分配内存,局部变量、成员变量指向未初始化的内存,于是就出现了上述案例所出现的情况;
局部变量、成员变量不会自动初始化,所以我们要养成声明即初始化的良好习惯;
4.3、内存访问
内存访问错误是所有C/C++开发人员都曾亲密接触的一类错误,这类错误最常见,它常常在我们无意识状态下蹦出来了,下面我们分析一下这类错误的根源;
#define MAX_ARRAY_COUNT 16 LOGFONT arrFonts[MAX_ARRAY_COUNT]; for (int index = 0; index <= MAX_ARRAY_COUNT; index++) { // 此处省略初始化代码N行 arrFonts[index].lfHeight = 0 - MulDiv(10, this->GetDC()->GetDeviceCaps(LOGPIXELSY), 72); // 此处省略初始化代码N行 } |
内存访问触发的错误时常发生,但总结起来可以归纳为几类,他们分别是:数组访问越界、指针访问越界、字符串访问越界、迭代器访问越界、访问游移指针对象、访问空指针,他们有共同特征,也存在着一些细微的差别,让我们一起来看看:
内存访问出错类别 | 出错关键点 |
数组访问越界 | 索引序号大于等于最大个数 |
指针访问越界 | 指针超出最大分配范围 |
字符串访问越界 | 1、字符串结束符不存在 2、目标字符串缓冲区小于源字符串 |
迭代器访问越界 | 1、迭代器越过右边界 2、用其它容器迭代位置赋值 |
访问游移指针 | 指针所指内存被释放并回收再分配使用 |
访问野指针 | 变量声明时未初始化,链接器分配地址对应的随机值 例如:0xCDCDCDCD |
访问空指针 | 指针所指地址为零(NULL) |
为节省篇幅,这里不准备一一列举案例,有兴趣的同学可收集和罗列一下案例。
对指针加强检测自始至终都是一个良好的习惯,这是防御性编程的核心;
4.4、分配和释放
内存分配和释放在我们的程序中分分秒秒的进行着,它分为隐式分配回收和显式分配回收两种,我们详细说明一下这两种情况:
分配回收类型 | 表现特征 | 案例、说明 |
隐式分配回收 | 1、直接声明并使用 2、编译器生成分配、回收代码 | 适用于自动变量 CListCtrl m_listProject; |
显示分配回收 | 1、间接声明并使用 2、用户编写分配、回收代码 | new/delete new[]/delete[] malloc/realloc/free OS提供的分配回收API |
按照摩尔定律,内存器件的成本迅速下降,但内存紧缺的问题却没有随之解决,内存分配失败的问题依然存在,保持检测内存指针或捕获内存异常的习惯依然有必要;由于内存分配失败的原因是内存不足,故我们把探讨的重点放到内存不足的原因上来。
内存分配释放语义简单、明确,只需要配对使用正确即可,如果不配对使用则会导致内存泄漏,进而导致内存分配失败。我们着重讨论内存泄漏的正常和不正的原因,详细如下:
内存泄漏类型 | 原因分析 | 案例、备注 |
对象内存未释放 | 分配、释放操作未配对使用导致: new/delete new[]/delete[] malloc/free 其它 API | |
对象内存局部释放 | 基类指针指向子类对象,释放该指针对象 | 基类析构函数未定义为虚函数 |
对象数组释放错误 | 未逐个调用对象的构造函数 | new[]/delete[] 配对使用 |
内存碎片 | 由于数据对齐、内存分块分配后出现无法使用的小内存块 | 这个难以避免,可以忽略它 |
4.5、参数传递
函数参数传递的不像内存分配、释放那么自由,受到诸多的限制,例如类型限制、常量修饰符限制、传递类型限制等,并且编译器能检测出大部分参数传递方面的错误,然而仍然无法阻止我们犯错误,到底是由于疏忽还是认识不足导致这样的情况呢?
在详细阐述前我们一起来看一个实践中碰到的因为参数传递错误引发的崩溃案例,请看代码:
3 LPMBuffer pBuffer = m_LexerState.buff; 16 this->ResizeBuffer(&pBuffer, newsize); 19 pBuffer->buffer[pBuffer->length++] = cast(char, ch); ------------> 程序崩溃 |
代码的真实意图是要扩充缓冲区(m_LexerState.buff),但由于通过中间变量的方式传递,并未真正的把扩充后的缓冲区地址传给&m_LexerState.buff,所以对象缓冲区实际没有变化,当访问扩充后的地址空间时,访问越界,程序崩溃;
在堆栈溢出章节我们还将看到类、结构类型的参数以值传递方式带来的危害:堆栈溢出、无法深度复制导致数据丢失,因此这两类参数应该尽量以指针、引用方式传递,对于不需要修改的参数尽量使用常量修饰符修饰(const)。
4.6、堆栈溢出
实践中碰到的另一类典型的崩溃是堆栈溢出,代码能编译通过,运行过程中会出现堆栈溢出而崩溃,为了加深对堆栈溢出的印象我们先看一个案例:[直接摘取自项目代码]
void CPerfJobRunResultModel::Load(LPCTSTR a_pszJobRunResultFilePath) { int iRet = 0; // 获取头结构元数据 LPTDRMETA pstDrMetaData = tdr_get_meta_by_name(m_pstDrMetaLibrary, “PERF_JOBRUN_RESULT”); // 读取结构头获取整个结构空间大小 TDRDATA tdrHost; PERF_JOBRUN_RESULT stJobRunResult; memset(&stJobRunResult, 0, sizeof(PERF_JOBRUN_RESULT)); tdrHost.iBuff = sizeof(PERF_JOBRUN_RESULT); tdrHost.pszBuff = (char *)&stJobRunResult; iRet = tdr_input_file(pstDrMetaData, &tdrHost, a_pszJobRunResultFilePath, tdr_get_meta_current_version(pstDrMetaData), TDR_IO_NEW_XML_VERSION); if (TDR_ERR_IS_ERROR(iRet)) { // 错误处理代码,省略之 } // 重新分配内存并加载文件 UINT memSize = sizeof(PERF_JOBRUN_RESULT) + (stJobRunResult.dwSuiteNum - 1) * sizeof(PERF_JOBRUN_SUITE_RESULT); m_pstJobRunResultModel = (LPPERF_JOBRUN_RESULT)malloc(memSize); if (NULL == m_pstJobRunResultModel) { UserThrowATPException(01005, “分配内存失败!”); } memset(m_pstJobRunResultModel, 0, memSize); tdrHost.iBuff = memSize; tdrHost.pszBuff = (char *)m_pstJobRunResultModel; iRet = tdr_input_file(pstDrMetaData, &tdrHost, a_pszJobRunResultFilePath, tdr_get_meta_current_version(pstDrMetaData), TDR_IO_NEW_XML_VERSION); if (TDR_ERR_IS_ERROR(iRet)) { UserThrowATPException(01005, “加载 TDR 实例文件失败: %s\n%s”, a_pszJobRunResultFilePath, tdr_error_string(iRet)); } m_HasUpdated = FALSE; } |
这个函数初期运行平稳,没出现啥问题;后来为了支持扩容,修改了性能测试相关数据结构,随后被发现出现了堆栈溢出崩溃;扩大程序栈空间(4M è 8M è 16M),仍然出现堆栈溢出;反复调试验证,堆栈溢出都集中出现在同一个函数:即进入函数的瞬间
void CPerfJobRunResultModel::Load(LPCTSTR a_pszJobRunResultFilePath)
随着一个个疑点的排除,问题集中在函数代码内;进一步测试发现性能测试数据结构占用16M空间:【PERF_JOBRUN_RESULT stJobRunResult;】,但还是没办法证实问题根源,于是在网络上搜寻触发函数(_chkstk)原因,终于找到一个说法是:函数内局部变量是在堆栈分配空间,当局部变量空间大于4K时(x86为4K, x64为8K,Itanium为16K)会触发函数(_chkstk)检查;结构变量属于值变量,在栈(Stack)空间分配,结构变量占用16M空间远远大于默认的1M空间,所以引发了堆栈溢出;
堆栈溢出并不可怕,只要我们认识它、掌握它的规律就知道如何防范;这里把常见的堆栈溢出类型一一列举,工作中稍加注意就可以预防;
实现类型 | 核心表现 | 备注 |
递归调用 | 结束条件不能满足而无法返回 | |
循环调用 | 间接的函数调用循环 | |
消息循环 | 消息处理不当导致消息构成循环 | |
大对象参数 | 结构、对象以值传递方式使用 | 应以指针、引用传递 |
大对象局部变量 | 函数中结构、类变量直接定义 | 使用 new 操作创建 |
4.7、转换错误
标量类型强制转换出错是比较隐秘,因为 C/C++ 中本身就隐藏着大量的类型转换,不易为人察觉;但它经常来得莫名欺骗,排查起来亦痛苦万分。
我们看一个实践中发生的案例:http://km.oa.com/group/728/articles/show/14051
该段取时间的代码一直运行正常,突然有一天出现了错误,此前运行非常完好的代码怎么会突然出错呢?你百思不得其解。从代码本身来看,主要涉及从 uint64_t 到 int 类型的转换,即从无符号类型向有符号类型转换;据当事人事后分析得出的结论是**由于转换操作是直接截断,而有符号类型的正负是根据最高位来解读的,0 表示该数据为正数,1 表示该数据为负数;**基于此,转换的正确与否基于此那只能求菩萨保佑了。
我们再来看一个类型宽度一样的数据类型转换的案例,由于类型宽度相同,无需做截断处理,有符号类型同样基于最高位来确定数据的数值,于是就看到如下的结果。
int main() { // 有符号到无符号 short i = -3; unsigned short u = 0; cout << (u = i) << “\n”; // 输出: 65533 // 无符号到有符号 i = 0; u = 65533; cout << (i = u) << “\n”; // 输出: -3 } |
我们再看一个实践中的案例:http://km.oa.com/group/533/topics/show/14900
//导航提示逻辑,提示显示3秒 if (oper->typeNavigate >=0) { oper->typeNavigateCount++; if (oper->typeNavigateCount > 2) { oper->typeNavigate = -1; oper->typeNavigateCount = 0; TCtrlBase_Invalidate((TCtrlBase*)object, NULL); } } |
现象:这段代码在模拟器运行正常,在MTK真机有问题;
原因:typeNavigate是个char型变量,受ARM编译器编译参数影响,这里的char等同于unsigned char,导致 if 语句永远为真,引起逻辑错误;
【问题】如果要逐字节操作大块内存时应该使用什么类型?char or signed char or unsigned char ?
五、C++ 编程最佳实践
收集整理了一份《2024年最新物联网嵌入式全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升的朋友。
需要这些体系化资料的朋友,可以加我V获取:vip1024c (备注嵌入式)
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人
都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
peNavigateCount > 2) { oper->typeNavigate = -1; oper->typeNavigateCount = 0; TCtrlBase_Invalidate((TCtrlBase*)object, NULL); } } |
现象:这段代码在模拟器运行正常,在MTK真机有问题;
原因:typeNavigate是个char型变量,受ARM编译器编译参数影响,这里的char等同于unsigned char,导致 if 语句永远为真,引起逻辑错误;
【问题】如果要逐字节操作大块内存时应该使用什么类型?char or signed char or unsigned char ?
五、C++ 编程最佳实践
收集整理了一份《2024年最新物联网嵌入式全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升的朋友。
[外链图片转存中…(img-NH56j0sp-1715846705536)]
[外链图片转存中…(img-gBcpDVIT-1715846705536)]
需要这些体系化资料的朋友,可以加我V获取:vip1024c (备注嵌入式)
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人
都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!