C++常见崩溃问题分析处理
使用Visual Studio 2013进行c++编码的时候经常会遇到一些编译不通过或者编译通过了链接出错,好不容易运行了又出现崩溃的问题,第一种问题编译器会自动检测出来,这种只需要仔细看错误描述就能知道问题出在哪,最麻烦的就是运行中崩溃了。
我整理了一些c++常见的错误,并进行了分析,给出了解决方案。(记录下来以便后面解决问题)
1、编译链接错误
(1)error C1083:很明显可以看出是找不到该头文件;
解决方案:去编译环境(debug/release)查看是否有该头文件,添加上头文件即可解决;
(2)error LNK2001:无法解析的外部符号,目前发现有三种可导致该错误
① 在.h文件中定义了静态变量num,却没有在.cpp文件中进行显式初始化;
解决方式:在.cpp文件中加上int CMainWnd::num=0;
② 在使用动态库时候,没有包含相应的lib;
解决方案:#pragma comment( lib,“xxxx.lib”);
③ 函数只在类中声明了而没有进行定义;
解决方案:为声明的函数添加函数实现;
(3)error MSB8020:移植项目的时候经常会出现这个问题,描述是平台工具v141找不到;
解决方案:右键项目属性-配置属性-常规-平台工具集下将工具改成自己的即可解决(通过下图可以很清楚的看到问题,v141未安装,需要选择自己常用的平台工具);
(4)error C1189:提示信息是使用/md[d](crt dll版本)生成MFC应用程序需要MFC共享dll版本;
解决方案一:右键项目属性-配置属性-常规-MFC的使用改为在共享dll中使用MFC;
解决方案二:右键项目属性-配置属性-C/C+±代码生成-运行库改为多线程调试(/MTd);
(5)error C2146,error C4430:提示信息是缺少“ ; ”,可是你会发现无论怎么看这个第9行的代码都没有问题,此刻就需要关注这个类型PCMainWnd,右键查找定义,会发现它其实被定义在.cpp文件中,却在.h中使用就会出现这种错误;
解决方案:如果必须在.h中使用这个类型,就先声明这个类,再定义这个类型PCMainWnd,即可编译通过(具体修改看下方代码);
class CMainWnd;
typedef CMainWnd *PCMainWnd;
class CMakePNG
{
private:
PCMainWnd m_pWnd;
};
2、运行崩溃
上面列举了几个编译链接出错的问题,接下来看一些运行崩溃的问题,今天先说一下访问内存出错。访问内存出错的几个常见问题如下表:
内存访问出错 | 原因 |
---|---|
数组越界 | 下标为负数或大于数组的长度 |
指针越界 | 超出指针分配的范围 |
字符串越界 | 字符串结束符不存在或目标字符串缓冲区小于源字符串 |
访问野指针 | 内存已被回收后又使用这个指针,会造成无法预估的错误 |
访问空指针 | 指针所指地址为NULL |
(1)数组越界,不一定报错,但是数据会被破坏,还有可能造成死循环
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, LPSTR /*lpCmdLine*/, int nCmdShow)
{
int i = 0;
int arr[10] = { 0 };
for (i = 0; i <= 15; i++)
{
arr[i] = 0;
}
return 0;
}
上面这段代码就会造成死循环,局部变量都在栈上存储,先定义的i在高地址,数组的地址是随着小标的增长而增长,后定义的arr数组,&arr[9]是数组的最后一个地址,当数组越界到合适的时候就到了i的位置,会存在&arr[x]=&i;将arr[x]改为0的同时,i值也变为0,会再次从i=0进行循环,就造成了死循环。
解决方案:将i定义为静态变量的时候,就会解除死循环,因为静态变量存在于静态区,数组再越界也不会改变i的值。但是平时编码的时候一定要注意越界这种问题,如果代码量大的话,你怎么都不会想到越界改变了哪里的数值,会导致bug出现。
(2)指针越界,和数组越界一样,不一定会报错,原因是如果越界的地址还在自己的进程里就不会报错,如果越界到其他进程,而且此时指针所指的地址正在被使用,就会出现read only之类的错误,没被使用的话还会篡改数据,也会造成无法预估的错误。
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, LPSTR /*lpCmdLine*/, int nCmdShow)
{
int a[10];
int *p = a + 9;
++p;
++p;
return 0;
}
(3)字符串越界,没有 ‘\0’ 结尾的只能称为字符数组,字符串的标志是 ‘\0’ 结尾,但是不防止误将字符数组当作字符串处理,以\0为循环结束条件,这样就会出现死循环,除非越界到一个值为0的地址上才能跳出循环。
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, LPSTR /*lpCmdLine*/, int nCmdShow)
{
char arr[3] = {'s','a','a'};
char c;
for (int i = 0; arr[i] != '\0'; i++)
{
c = arr[i];
}
return 0;
}
还有一种字符串越界是复制字符串fromStr到toStr时,toStr的大小不够存储需要存储的数据元素时,就会出现字符串越界。
bool copyStr(char *toStr,char *fromStr)
{
while (*fromStr != '\0')
{
*toStr = *fromStr;
toStr++;
fromStr++;
}
return true;
}
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, LPSTR /*lpCmdLine*/, int nCmdShow)
{
char str1[] = "jdgkhskj";
char str2[] = "hjh";
copyStr(str2, str1);
return 0;
}
上面的代码会造成str2复制到str1的前4个字符,且str2变成字符数组(str2的\0被重写了),如果继续把str2的数据当作str1使用就会造成数据不对的bug,如果再次把str2当作字符串去使用,还会造成上面字符串越界的第一种问题。
(4)访问野指针或空指针
new了一个Point对象,在fun函数中delete且置空指针之后再使用就会出现下面的崩溃(一般delete之后都会置空防止产生野指针)
解决方案:如果不是异步函数,可以在创建对象的函数里使用完之后再delete并置空,异步函数的话,在异步函数里delete并置空(但是调用之后就不能再使用这个对象了)
//直接调用
void fun(Point* p)
{
p->X = 2;
}
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, LPSTR /*lpCmdLine*/, int nCmdShow)
{
Point *pt = new Point;
pt->X = 1;
fun(pt);
pt->Y = 3;
delete pt;
pt = NULL;
return 0;
}
//异步函数调用
void CMainWnd::fun(LPARAM lParam)
{
Point *pt = (Point *)lParam;
int x = pt->X;
int y = pt->Y;
delete pt;
pt = NULL;
}
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, LPSTR /*lpCmdLine*/, int nCmdShow)
{
Point *pt = new Point;
pt->X = 1;
PostMessage(g_MainHwnd, WM_Main_MSG, FunCb, (LPARAM)pt);
//此处不能delete
return 0;
}
c++编译链接运行的崩溃点暂时整理到这里,后续遇到还会持续更新。
注释
[1]: https://blog.csdn.net/Code_beeps/article/details/83243342
[2]: https://www.cnblogs.com/zhoug2020/p/6025388.html