关于C/C++语言的部分BUG

scanf格式匹配引起的错误

  运行以下程序时,出现这类错误:*** stack smashing detected ***: ./test_global terminated。错误缘由多是由于scanf("%d%d", &row, &col)接收的是int型,可是我使用的是short int,长度是Int的一半。修改为int后错误消失。linux

#include<stdio.h>

int main(){ 
    int row, col;
    
    scanf("%d%d", &row, &col);
    printf("%d %d", row, col);
    return 0;
}

  使用gcc编译时出现的警告以下:
    
  出现的错误以下:
    ios

局部变量被释放引起的bug

  运行以下程序时,会无终止地打印-1。缘由是变量p所指向的变量k在addr()函数执行后自行销毁,k所使用的内存被分配给loop()中的变量i,从而致使p指向i。而此时对p的操做是减1,对i的操做是加1,致使i的值始终为-1,没法跳出循环。shell

#include<stdio.h>

void addr();
void loop();

long *p;
int main(){
    addr();
    loop();
}

void addr(){
    long k;
    k = 0;
    p = &k;
}

void loop(){
    long i, j;
    j = 0;
    for (i = 0; i<10;i++){
        (*p)--;
        j++;
        printf("%d\n", i);
    }
}

  程序运行输出结果以下:
    
  程序调试结果以下:
    数组

数组写入超出索引维度

  虽然运行下面代码不会出错,可是对数组a[10]的写操做超出了维度,致使在地址为a+10的地方也写入了数据,可是容易引起潜在bug。函数

#include<stdio.h>

int main()
{                                                        
    int i;
    int a[10];
    for (i = 0; i <= 10; ++i)
    {    
        a[i] = 0;
        printf("%d\n", i);
    }
    exit(0);    
}

指针的指针引起的思考

  对于将指针做为参数进行传递时,若是是将在子函数内赋值给一个新申请的空间,那么就要注意在传递指针时,须要传递指针的地址,即指针的指针。错误程序以下:oop

#include<stdio.h>

void allocateInt(int * i, int m);
void main()
{
    int m = 5;
    int * i = &m;
    printf("i address: %x\n", &i);
    allocateInt(i, m);
    printf("*i = %d\n", *i);
}

void allocateInt(int * i, int m)
{
    printf("i address: %x\n", &i);
    i = (int *) malloc(sizeof(int));
    *i = 3;
}

指针的指针引起的思考——思考

  虽然对该问题的解释通常是:在传递参数时,系统为子函数的变量新申请一部分空间,所以在void allocateInt(int * i)中,i的地址和在void main()中的地址是不一样的,而void allocateInt(int * i)中的i是局部变量,在子函数运行结束会被释放掉,所以void main()中的i是没法获得malloc的地址的,更不可能获得新的赋值。
  下面经过gdb调试以及反汇编来进行说明:性能

  1. 程序在运行至main函数中的allocateInt(i, m);语句时,变量i和m的内存地址以下图所示,&i=0x7fffffffdaf0,&m=0x7fffffffdaec:
  2. 以后使用命令si对汇编语言进行单步调试,连续运行5次si命令后(主要是保留变量i和m的值),程序进入allocateInt函数。进入时,i=0x7ffff7ffe168, m=0,也就是说i和m还并无被传递赋值,结果以下所示:

    但此时,变量i和m的地址是不一样的,&i=0x7fffffffdac8,&m=0x7fffffffdac4,以下图所示:
  3. 再运行5次汇编指令后,才将参数的完成传递赋值,程序的指针才开始指向void allocateInt(int * i, int m)中的printf("i address: %x\n", &i);,以下图所示:

    此时的i和m已经被赋值,i=(int *) 0x7fffffffdaec, m=5。
  4. 针对在第3点提到的4次汇编指令,这里进一步说明。
    • 第1条指令是push %rbp,也就是把rbp寄存器入栈;
    • 第2条指令是mov %rsp,%rbp,其中rsp是堆栈指针。也就是把堆栈指针的值赋值给rbp寄存器;
    • 第3条指令是sub $0x10,%rsp,也就是把堆栈指针所指向的地址减小16个字节。这是由于变量i和m一共占用了16个字节;
    • 第4条指令是mov %rdi,-0x8(%rbp),也就是把寄存器rdi的值(rdi=0x7fffffffdaec,以下图所示)赋值给i。由于i的地址就是rbp-0x8;
    • 第5条指令是mov %esi,-0xc(%rbp),做用相似于第4条,将寄存器esi的值(esi=0x5,以下图所示)赋值给m。
  5. 关于寄存器的相关知识、gdb的调试命令能够参考下面的参考资料;
  6. 关于汇编指令中出现的lea命令能够网上查找,主要就是一种更加有效的mov方法;
  7. 关于汇编指令中出现的callq 0x4004a0 <printf@plt>,意思是调用print函数。可是这里并非直接调用print函数,而是调用相似于print函数在进程中的别名。由于这是公用库中的函数,所以不一样进程中都会调用,因此只在进程中存留一个函数地址或者别名就好。具体参见stackoverflow上的一篇文章What does @plt mean here?

未定义赋值的变量引起的bug

  运行以下代码时,本意是用g_logger.WriteLog()将"in A()"写入文本文件中,可是结果倒是将"in A()"打印在了shell里。ui

// file: main.cc
#include <iostream>
#include "CLLogger.h"

using namespace std;

extern CLLogger g_logger;

class A
{
public:
    A()
    {
    CLStatus s = g_logger.WriteLog("in A()", 0);
    if(!s.IsSuccess())
        cout << "g_logger.WriteLog error" << endl;
    }
};

A g_a;

CLLogger g_logger;

int main()
{
    return 0;
}

// file: CLLogger.h
#include "CLStatus.h"

class CLLogger
{
public:
    CLLogger();
    virtual ~CLLogger();

    CLStatus WriteLog(const char *pstrMsg, long lErrorCode);

private:
    CLLogger(const CLLogger&);
    CLLogger& operator=(const CLLogger&);

private:
    int m_Fd;
};

// file: CLStatus.h
class CLStatus
{
public:
    CLStatus(long lReturnCode, long lErrorCode);
    CLStatus(const CLStatus& s);
    virtual ~CLStatus();

public:
    bool IsSuccess();

public:
    const long& m_clReturnCode;
    const long& m_clErrorCode;

private:
    long m_lReturnCode;
    long m_lErrorCode;
};

  缘由是g_a是定义在g_logger以前,所以在运行到语句CLStatus s = g_logger.WriteLog("in A()", 0);时,g_logger仍未定义。但因为在文件开头声明了extern CLLogger g_logger;,所以编译器不会报错,而此时默认将声明为外部变量的g_logger中的文件操做符m_Fd赋值为0,以下:
    spa

题外话

  • 在编写时注意局部性原理,提升性能。通常cache会把某次访问的内存地址附近区域的内容都加载进去。若是在编写程序时相邻语句访问的数据是在内存中连续的,那么就会调高cache的命中率。
  • 在编写时注意分支预测致使的性能问题。在向下跳转的状况下,优先将最有可能执行的语句放在if分支下,减小分支预测时的开销(向下跳转在静态分支预测中通常默认不跳转;向上跳转在静态分支预测中通常默认跳转),例如:
int a = -5;
int b = 0; 
................................................
if(a > 0){                 if(a <= 0){
    b = 1;                        b = 2;
}                             }
else{                      else{
    b = 2;                    b=1;
}                            }

  关于分支预测的一些预测方式能够参考一篇博客C++性能榨汁机之分支预测器

参考资料
Visual Studio文档:寄存器使用
探究Linux下参数传递及查看和修改方法
gdb 调试入门,大牛写的高质量指南
GDB的调试命令
What does @plt mean here?
C++性能榨汁机之分支预测器

相关文章
相关标签/搜索