debug实例与分析(一)
使用软件:VS2022
调试环境:Debug环境
调试快捷键:f11
监视窗口:点击上方的调试,然后点击监视,从4个窗口中选一个
实例1:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> vec = { 1, 2, 3, 4, 5 };
for (auto x : vec)
{
vec.push_back(10);
cout << x << " ";
}
cout << endl;
return 0;
}
输出结果:
附图:
解释:范围for 底层是用迭代器遍历,从这张附图可以看出,size与capacity是相等的,而vec.push_back(10) 会让 vector 重新分配内存,从vector底层来看,开辟了新的空间,旧的空间被释放了,然而迭代器仍然指向旧的空间,因此迭代器已失效了,继续遍历就是错误行为。
建议:遍历迭代器时,千万不要改变容器的大小。
实例2:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> v = { 1, 2, 3, 4, 5 };
for (auto it = v.begin(); it != v.end(); ++it)
{
if (*it % 2 == 0)
{
v.erase(it);
}
}
for (int x : v)
{
cout << x << " ";
}
cout << endl;
return 0;
}
输出结果:编译器直接报错
解释:当it指向2时,满足条件,就会删除2,但是it仍然指向那删除的内存空间,++it后就会导致程序错误。
建议:erase后千万别用旧it,必须返回新的迭代器。
修改后的:
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v = { 1, 2, 3, 4, 5 };
for (auto it = v.begin(); it != v.end(); ) {
if (*it % 2 == 0) {
it=v.erase(it);
}
else{
it++;
}
}
for (int x : v) {
cout << x << " ";
}
cout << endl;
return 0;
}
实例3.
#include <iostream>
using namespace std;
void modify_array(int arr[]) {
// 目标:把数组所有元素改成0
int n = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < n; ++i) {
arr[i] = 0;
}
}
int main() {
int arr[5] = { 1, 2, 3, 4, 5 };
modify_array(arr);
// 输出数组
for (int i = 0; i < 5; ++i) {
cout << arr[i] << " ";
}
return 0;
}
输出结果:
解释:其实在编译器的视角里,void modify_array(int arr[])会被替换为void modify_array(int* arr),这就是数组退化,C++不准许直接将整个数组安置传参,他会自动退化为指向数组首元素的指针,所以在计算arr的n时,计算的就是指针本身的大小,64位系统下是8字节,所以n=8/4,所以循环仅会执行两次。
实例4.
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <cstring>
using namespace std;
class Student {
public:
Student(const char* name, int age) {
m_name = new char[strlen(name) + 1];
strcpy(m_name, name);
m_age = age;
}
~Student() {
delete[] m_name;
}
void show() const {
cout << "姓名:" << m_name << ",年龄:" << m_age << endl;
}
private:
char* m_name;
int m_age;
};
int main() {
Student s1("张三", 20);
Student s2 = s1;
Student s3(s1);
s1.show();
s2.show();
s3.show();
return 0;
}
输出结果:运行时出错
解释:没有手动定义拷贝构造函数,C++编译器就会自动生成一个默认拷贝构造函数,这个函数的行为是浅拷贝(按字节逐位拷贝), s1构造时,在堆上new了一块内存,m_name指针指向这块内存(比如地址0x100),存放字符串"张三", 用s1拷贝构造s2、s3时,默认拷贝构造函数只会把s1的m_name指针的值(0x100)复制给s2、s3的m_name,不会重新申请新的堆内存,所以最终结果:s1、s2、s3三个对象的m_name,指向了同一块堆内存,main函数结束时,局部对象会按构造逆序析构,先析构s3,delete[]释放0x100的内存;接着析构s2,再次对已经释放的0x100内存执行delete[],这是重复释放堆内存,属于C++标准明确的未定义行为,直接触发程序崩溃;最后析构s1时,还会第三次释放同一块内存,进一步加剧问题。并且还有额外的隐藏bug,哪怕程序没立刻崩溃,但是只要修改其中一个对象的m_name内容,另外两个对象的内容也会跟着被修改,完全破坏了对象的独立性,会导致极难排查的业务逻辑错误。
建议:C++中,只要类里包含指针类型的成员、且手动管理了动态内存,就必须手动实现深拷贝,否则可能会出现重复释放、内存泄漏、数据错乱等问题。
实例5.
#include <iostream>
#include <string>
using namespace std;
class UserInfo {
public:
UserInfo(const string& name, const int& age) : m_name(name), m_age(age) {}
void show() const {
cout << "姓名:" << m_name << ",年龄:" << m_age << endl;
}
private:
const string& m_name;
const int& m_age;
};
UserInfo create_user() {
string user_name = "李四";
int user_age = 25;
return UserInfo(user_name, user_age);
}
int main() {
UserInfo user = create_user();
user.show();
int temp_age = 30;
UserInfo user2("王五", temp_age);
temp_age = 35;
user2.show();
return 0;
}
输出结果:
解释:
1.局部变量的生命周期:user_name和user_age是函数内的栈局部变量,函数执行结束、返回时,函数的栈帧会被销毁,这两个变量的内存会被系统直接回收,引用是变量的“别名”,必须绑定到生命周期有效的对象上。UserInfo的构造函数中,m_name绑定了user_name、m_age绑定了user_age,函数返回后,原变量已经不存在,两个引用直接变成悬垂引用(指向已经被释放的内存),当main函数调用user.show()时,本质是访问已经被释放的栈内存,结果完全不可控。
隐式转换生成临时对象:你传入的第一个参数是字符串字面量"王五"(const char*类型),但构造函数的形参是const string&。编译器会自动执行隐式转换:用"王五"构造一个临时的std::string对象,再把这个临时对象绑定到形参name上。
2. 临时对象的生命周期陷阱:C++中,临时对象的生命周期只有直接绑定到局部引用变量时,才会延长到和引用一致;如果绑定到类的成员引用,这个规则完全不生效。但是这个临时的string对象,在UserInfo的构造函数执行完成后就会被销毁,user2的m_name立刻变成悬垂引用,调用user2.show()时访问m_name,同样是未定义行为。
补充:user2的m_age是合法的
m_age绑定的是main函数里的局部变量temp_age,二者生命周期完全一致(都在main的作用域内),所以m_age始终有效。temp_age修改为35后,user2.show()会输出35,这部分行为符合预期,但无法掩盖m_name的致命问题。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)