字符串类中运算符重载出现的一个问题
最后更新于:2022-04-01 14:38:32
上机辅导。学生的一个程序莫名出问题。她是在做一个String类,主要是要实现字符串的连接。
程序是这样的,请读者将其拷到IDE,边看边调:
~~~
#include <iostream>
#include <cassert>
#include <cstring>
using namespace std;
class String
{
private:
char* p;
int len;
public:
String();
String(const char* s);
String(const String& s);
~String();//问题出现在这儿
void show();
friend String operator+(const String& s1,const String& s2);
friend String operator-(const String& s1,const String& s2);
};
String::String()
{
len=0;
p=NULL;
}
String::String(const char* s)
{
len=strlen(s);
p=new char[len+1];
strcpy(p,s);
}
String::String(const String& s)
{
len=s.len;
if(p!=NULL)delete []p;
p=new char[len+1];
strcpy(p,s.p);
}
String operator+(const String& s1,const String& s2)
{
String total;
total.len=s1.len+s2.len;
total.p=new char[total.len+1];
strcpy(total.p,s1.p);
strcat(total.p,s2.p);
return total;
}
String operator-(const String& s1,const String& s2)
{
char* c1=new char[s1.len+1];
strcpy(c1,s1.p);
int i=s1.len-1;
while(s1.p[i]!=' '&&i>=0)--i;
c1[i+1]='\0';
char* c2=new char[s2.len+1];
strcpy(c2,s2.p);
i=0;
while(i<s2.len&&c2[i]==' ')++i;
int j=0;
while(c2[i]!='\0'&&i<s2.len)
{
c2[j]=c2[i];
++i;
++j;
}
c2[j]='\0';
String s;
s.len=s1.len+s2.len;
s.p=new char[s.len];
strcpy(s.p,c1);
strcat(s.p,c2);
delete c1;
delete c2;
return s;
}
void String::show()
{
cout<<p<<endl;
}
String::~String()
{
delete []p;
}
int main()
{
String str1("123"),str2("456"),str3;
str3=str1+str2;
str3.show();
str3=str1-str2;
str3.show();
return 0;
}
~~~
问题的表现是,`str3.show();`的输出是几个莫名的符号!
我让她单步跟踪一下,却发现无论断点设在哪里,总是要进到析构函数~String()中去!
恰其他同学的问题不少,我让她将这个程序给我发邮件,她不要在此纠缠。我清楚,这里有指针成员。祸水应该与此相关。
学习越来越深入,学生出问题的程序,常需要仔细阅读后才能给指导意见了。
午睡后,打开邮件。将程序拷入IDE,已经注意到了执行`str3=str1+str2;`时,需要有的赋值运算符=的重载。试图找一下单步时进入~String()时的场景,却未遂。
在我的IDE下,`str3=str1+str2;`的结果正常,`str3=str1-str2;`的结果却异常。野指针的表征,真伪混杂最头疼。
重载赋值运算=,在类声明中加:
~~~
String &operator=(const String& s1);
~~~
实现为:
~~~
String &String::operator=(const String &s1)
{
if(!this->p)
delete []p;
p=new char(s1.len+1);
strcpy(p,s1.p);
len=s1.len;
return *this;
}
~~~
先向学生交作业。进入~String()的问题是否还在?暂时放下,待回音。
一个冒号引发的“血案”
最后更新于:2022-04-01 14:38:30
备课中。
从下载的PPT中复制出例题,调试一下,结果:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-07_575640863ea8b.jpg)
何其多的错误,红红的。
不正在声明Base2吗?怎么会“error: ‘Base2’ has not been declared”
斗争!
……
小样,就一个符号的问题,犯得着兴师动众,给出那么多提示?!红红的一片,血案发生?
编译器就是这么好,严格把关,各种提示。
亲爱的读者,看出来了,会心一笑吧。
编程就这样,给人带来愉悦。
C++返回值为对象时复制构造函数不执行怎么破
最后更新于:2022-04-01 14:38:27
先说点背景知识,调用复制构造函数的三种情况:
1.当用类一个对象去初始化另一个对象时。
2.如果函数形参是类对象。
3.如果函数返回值是类对象,函数执行完成返回调用时。
在辅导学生上机时,有同学第3点提出异议。有教材上的例题为证:
~~~
#include <iostream>
using namespace std;
class Point //Point 类的定义
{
public:
Point(int xx=0, int yy=0)
{
x = xx; //构造函数,内联
y = yy;
}
Point(const Point& p); //复制构造函数
void setX(int xx)
{
x=xx;
}
void setY(int yy)
{
y=yy;
}
int getX() const
{
return x; //常函数(第5章)
}
int getY() const
{
return y; //常函数(第5章)
}
private:
int x, y; //私有数据
};
//成员函数的实现
Point::Point (const Point& p)
{
x = p.x;
y = p.y;
cout << "Calling the copy constructor " << endl;
}
//形参为Point类对象的函数
void fun1(Point p)
{
cout << p.getX() << endl;
}
//返回值为Point类对象的函数
Point fun2()
{
Point a(1, 2);
return a;
}
//主程序
int main()
{
Point a(4, 5); //第一个对象A
Point b = a; //情况一,用A初始化B。第一次调用复制构造函数
cout << b.getX() << endl;
fun1(b); //情况二,对象B作为fun1的实参。第二次调用复制构造函数
b = fun2(); //情况三,函数的返回值是类对象,函数返回时调用复制构造函数
cout << b.getX() << endl;
return 0;
}
~~~
证据是,在CodeBlocks中,运行结果是:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-07_57564049cca61.jpg)
而不是期望的:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-07_57564049e2213.jpg)
显然,第3种情况下,复制构造函数没有被执行。
确定问题后,我知道道理是对的,看过的几本书,厚的、薄的,都是这么写的。会不会是编译器的差别?CodeBlocks用的是gcc。gcc开源,跟标准的变化跟得紧,莫不是第3种情况已经成了老黄历,而各种书来不及变?
我让她到VC++6.0中运行。一会儿她的反馈,在VC++6.0中复制构造函数执行了。
真相明白了。
这个问题需要有个交待。
回家后再翻各种书,无果。网络搜索,CSDN上有个贴子[《函数返回值是对象,是调用了拷贝构造函数?》](http://bbs.csdn.net/topics/390803716),其中大家给的结论,是gcc做了优化,返回值为对象时,不再产生临时对象,因而不再调用复制构造函数。
看来不是标准发生变化。
那如果一定想要让这个构造函数执行呢?只需让忽略gcc不要搞这个优化就行了。贴子中给了个线索,在新浪博客《[命名返回值优化](http://blog.sina.com.cn/s/blog_4ab8464c0100kybj.html)》。文章称通过搜索知道“这是一个称为命名返回值优化的问题,而且g++的这个编译优化竟然没有直接的关闭方法给出解决办法”。作者是用命令行工作的,他后来解决的办法,是在编译命令中加上“-fno-elide-constructors”参数,例g++ -fno-elide-constructors testReturn.cpp 。
我的学生还处在用IDE的阶段。本文的价值来了,如何在CodeBlocks下也忽略这个优化项呢?
在CodeBlocks中,通过菜单依次选:settings->Compiler...,在Global compiler settings部分,选择Other options,在文本框中写入“-fno-elide-constructors”,如图,然后就可以ok啦。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-07_57564085f34e2.jpg)
然后,如同苦难的公主终于和王子过上了幸福的生活一样,期望的结果有了。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-07_57564049e2213.jpg)
“-fno-elide-constructors”选项起了作用,有图为证。下面中加上这个参数后,编译完看到的提示信息:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-07_5756408627958.jpg)
数据结构实践——初始化顺序表怎么就内存溢出了?
最后更新于:2022-04-01 14:38:25
有学生调程序,是要建顺序表。
他的程序是这样的:
~~~
#include <stdio.h>
#include <malloc.h>
#define MaxSize 50 //Maxsize将用于后面定义存储空间的大小
typedef int ElemType; //ElemType在不同场合可以根据问题的需要确定,在此取简单的int
typedef struct
{
ElemType data[MaxSize]; //利用了前面MaxSize和ElemType的定义
int length;
} SqList;
//声明自定义函数
SqList InitList(SqList *L); //初始化顺序表
void ListInsert(SqList *L,int i,int b); //插入函数
void DispList(SqList *L); //输出函数
bool ListEmpty(SqList *L);//判定是否为空表ListEmpty(L)
int main()
{
SqList *sq;
InitList(sq);
ListInsert(sq, 1, 5);
ListInsert(sq, 2, 3);
ListInsert(sq, 1, 4);
DispList(sq);
return 0;
}
//输出线性表DispList(L)
void DispList(SqList *L)
{
int i;
if (ListEmpty(L))
return;
for (i=0; i<L->length; i++)
printf("%d ",L->data[i]);
printf("\n");
}
//判定是否为空表ListEmpty(L)
bool ListEmpty(SqList *L)
{
return(L->length==0);
}
//初始化顺序表InitList(*L)
SqList InitList(SqList *L)
{
L=(SqList *)malloc(sizeof(SqList));//这里申请了结点空间
L->length=0;
return *L;
}
void ListInsert(SqList *L,int i,int b) //插入函数
{
int j,k;
if(i<1)
{
printf("插入位置非法/n");
}
for(j=L->length; j>i; j--)
{
L->data[j]=L->data[j-1];
}
L->data[i]=b;
L->length++;
}
~~~
运行结果是这样的:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-07_57564049a297f.jpg "")
他找我帮忙。基本可以断定,内存使用不当,有溢出。
看一下编译提示的信息,有一个警告:
D:\CB\DS\main.cpp|21|warning: ‘sq’ is used uninitialized in this function [-Wuninitialized]|
说在21行,sq未经初始化就使用了。通俗的说法,野指针。
围绕着sq找。在main()函数中有:
~~~
SqList *sq;
InitList(sq);
~~~
这里在调用InitList时,实际参数sq就是野指针。但这还不是出问题的关键,看InitList函数的定义是:
~~~
//初始化顺序表InitList(*L)
SqList InitList(SqList *L)
{
L=(SqList *)malloc(sizeof(SqList));//这里申请了结点空间
L->length=0;
return *L;
}
~~~
调用时,L得到的是野指针,但在函数里为其分配空间了。但调用完,这个地址并未返回到main函数中。调用完InitList,sq仍然还是野指针。这是关键!
沿这个思路,希望能将分配的空间地址能返回给main函数。return *L就不合适了,return L是返回地址。于是,函数定义改为:
~~~
//初始化顺序表InitList(*L)
SqList *InitList(SqList *L)
{
L=(SqList *)malloc(sizeof(SqList));//这里申请了结点空间
L->length=0;
return L;
}
~~~
既然参数SqList *L调用时给的是个野指针,不要也罢。于是改选成无参函数:
~~~
//初始化顺序表InitList(*L)
SqList *InitList()
{
SqList *L=(SqList *)malloc(sizeof(SqList));//这里申请了结点空间
L->length=0;
return L;
}
~~~
在调用时,main函数定义为:
~~~
int main()
{
SqList *sq;
sq = InitList();
ListInsert(sq, 1, 5);
ListInsert(sq, 2, 3);
ListInsert(sq, 1, 4);
DispList(sq);
return 0;
}
~~~
为保证程序能够正确编译,函数声明等处的语法问题不一一列出。解决了这一环节的问题,程序能够运行了,但结果:
![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-07_57564049b75fd.jpg "")
断定问题出在ListInsert函数中。看调用,插入的位置用的是逻辑序(从1开始记数),但函数定义中,直接L->data[i]=b;没有考虑物理存储中,下标是从0开始的。
所以,在ListInsert中加入一个 i–,完成逻辑序号向物理序号的转换,Done。
正确的结果不贴图了,最后改过的程序是:
~~~
#include <stdio.h>
#include <malloc.h>
#define MaxSize 50 //Maxsize将用于后面定义存储空间的大小
typedef int ElemType; //ElemType在不同场合可以根据问题的需要确定,在此取简单的int
typedef struct
{
ElemType data[MaxSize]; //利用了前面MaxSize和ElemType的定义
int length;
} SqList;
//声明自定义函数
SqList *InitList(); //初始化顺序表
void ListInsert(SqList *L,int i,int b); //插入函数
void DispList(SqList *L); //输出函数
bool ListEmpty(SqList *L);//判定是否为空表ListEmpty(L)
int main()
{
SqList *sq;
sq = InitList();
ListInsert(sq, 1, 5);
ListInsert(sq, 2, 3);
ListInsert(sq, 1, 4);
DispList(sq);
return 0;
}
//输出线性表DispList(L)
void DispList(SqList *L)
{
int i;
if (ListEmpty(L))
return;
for (i=0; i<L->length; i++)
printf("%d ",L->data[i]);
printf("\n");
}
//判定是否为空表ListEmpty(L)
bool ListEmpty(SqList *L)
{
return(L->length==0);
}
//初始化顺序表InitList(*L)
SqList *InitList()
{
SqList *L=(SqList *)malloc(sizeof(SqList));//这里申请了结点空间
L->length=0;
return L;
}
void ListInsert(SqList *L,int i,int b) //插入函数
{
int j;
if(i<1)
{
printf("插入位置非法/n");
}
i--;
for(j=L->length; j>i; j--)
{
L->data[j]=L->data[j-1];
}
L->data[i]=b;
L->length++;
}
~~~
找出诡异的Bug:数据怎么存不进去
最后更新于:2022-04-01 14:38:23
带着学生做课程设计。程序一大,课程中做过了小项目,练过了分解动作,一到合起来了,难免还是要乱了分寸。其实,实战的功夫,就是这样出来的。(课程设计指导视频[链接](http://edu.csdn.net/course/detail/474)(第36课时,3.18 银行系统开发),课程主页在[链接](http://blog.csdn.net/sxhelijian/article/details/44117039),指导文档见[链接](http://blog.csdn.net/sxhelijian/article/details/45046901),示例程序见[链接](http://blog.csdn.net/sxhelijian/article/details/45046497))。
话说,已经有两位做银行系统的同学和我说,“文件中写不进去数据。程序一退出,明明写进去了,结果却是空文件。”这不是一个小打击。
做软件,找Bug,有些像打空气,使半天劲,人家就不理你。学计算机的人,练的就是这样的功夫,要学会自己创建线索,找出问题所在。
话说,出问题的两位同学的程序,框架大体如下:
~~~
int main()
{
Bank b; //创建一个银行对象
if (pass()) //用pass校验用户
{
Bank b;
b.work(); //完成各种业务
}
return 0;
}
class Bank
{
……
}
Bank::Bank()
{
ifstream infile("account.dat",ios::in);
if(!infile)
{
cerr<<"open error!"<<endl;
exit(1);
}
//下面的代码,将之前发生过的业务数据从文件读入银行对象
infile.close();
}
Bank::~Bank()
{
ofstream outfile("account.dat",ios::out);
if(!outfile) //测试文件打开操作是否成功,不成功则提示后退出。
{
cerr<<"open error!"<<endl;
exit(1);
}
//下面的代码,将银行对象中的业务数据写入文件
outfile.close();
delete p;
}
~~~
因为数据要在文件里存储,所以,可选的方案是,在构造函数中读文件,在析构函数中写文件。上面的程序就是照这种思路设计的。
然而,程序退出后,文件就是空的。
老贺看了也纳闷,写文件的语句中规中矩,然而就是不对。
仔细审查析构函数中文件的打开方式ios::out,似乎有嫌疑,但排除了。在实际运行的系统中,ios::out的方式不常用,因为这样一打开,也就意味着存在的文件也要重建,用ios::app的更多。
可是,在这个由大一学生实施的设计中,简化的方案是,将所有的数据读入内存,操作针对内存中的数据,而最后,就是要重建文件,将内存中的全部数据重写一遍。
几百行的程序,就不可以用眼睛盯着找问题了。单步跟踪,对这样的程序,如果问题具体在哪儿都不清楚,也不是一个好办法。
析构函数中写文件的部分最可疑。我在析构函数~Bank中加了一句`“cout<<"in destructor."<<endl;”`。结果发现,最后的,析构函数执行了两次。
然后,在main函数的return 0;前加了一句`“cout<<"end of main"<<endl;”`,发现输出的信息是:
> in destructor.
end of main
in destructor.
再看main函数,真相大白了。问题出在main函数中:Bank b出现了两次:一个是属于main函数的局部对象b(前者,第3行),另一个的作用范围,只在if语句的一对花括号内的对象b(后者,第6行)。
程序初次执行,文件为空,前者执行构造函数,b中保存的是空业务。当用户密码验证成功,会创建后者,自然业务信息也空。当执行完b.work();,会执行后者的析构函数,将这次业务后的业务信息保存在了文件中。文件内容不会是空。
然而,当程序的执行离开main函数时,其局部的变量b(前者)也要析构,这时就是问题之所在,这个b中的业务信息是空的,文件打开重建后,没有要写入的信息,最后就是空文件了。
所以,解决的办法,将两个Bank b;,无论前者或后者,去掉一个即可。
问题解决了,再反思。前述的问题自然不该发生,但这里设计的缺陷也存在。在程序中直接将文件名写定,并且写在构造函数和析构函数中,也就意味着该类的所有对象都用同一个文件(如同Person类中的每个对象都用同一个碗吃饭,多家银行将数据存在一个文件中,太可怕了)。合理的做法是,采取某种机制,不同对象,使用不同的文件。
当然,对于本文中的问题,就是不该定义两个 Bank b。
藏身时间类中的妖孽
最后更新于:2022-04-01 14:38:21
下面的代码,是一位同学为《[初识对象](http://blog.csdn.net/sxhelijian/article/details/44116231)》中的[项目3时间类](http://blog.csdn.net/sxhelijian/article/details/44116309)写的。但错误有点诡异,他在QQ群中求助。
~~~
#include <iostream>
using namespace std;
class Time
{
public:
void set_time();
void show_time();
void add_sec(int);
void add_minute(int);
void add_hour(int);
void add_a_sec()
{
sec=sec+1;
if(sec==60)
{
sec=0;
minute=minute+1;
if(minute==60)
{
minute=0;
hour=hour+1;
}
}
void add_a_minute()
{
minute=minute+1;
if(minute==60)
{
minute=0;
hour=hour+1;
}
}
void add_an_hour()
{
hour=hour+1;
}
private:
bool is_time(int, int, int); //这个成员函数设置为私有的,是合适的,请品味
int hour;
int minute;
int sec;
};
void Time::set_time()
{
char c1,c2;
cout<<"请输入时间(格式hh:mm:ss)";
while(1)
{ cin>>hour>>c1>>minute>>c2>>sec;
if(c1!=':'||c2!=':')
cout<<"格式不正确,请重新输入"<<endl;
else if (!is_time(hour,minute,sec))
cout<<"时间非法,请重新输入"<<endl;
else
break;
}
}
void Time::show_time()
{
cout<<hour<<":"<<minute<<":"<<sec<<endl;
}
bool Time::is_time(int h,int m, int s)
{
if (h<0 ||h>24 || m<0 ||m>60 || s<0 ||s>60)
return false;
return true;
}
void Time::add_sec(int adds)
{
sec=sec+adds;
while(sec>=60)
{
sec=sec-60;
minute=minute+1;
if(minute==60)
{
minute=0;
hour=hour+1;
}
}
}
void Time::add_minute(int addm)
{
minute=minute+addm;
while(minute>=60)
{
minute=minute-60;
hour=hour+1;
}
}
void Time::add_hour(int addh)
{
hour=hour+addh;
}
int main( )
{
Time t1;
int adds,addm,addh;
cout<<"请输入要增加的秒,分,小时"<<endl;
cin>>adds>>addm>>addh;
t1.set_time( );
T1.show_time( );
t1.add_a_sec();
t1.add_a_minute();
t1.add_an_hour();
t1.add_sec(adds);
t1.add_minute(adds);
t1.add_hour(addh);
return 0;
}
~~~
读者朋友,你编译一下试试?
我在CodeBlocks中编译,提示一大堆:
~~~
||=== Build: Debug in example (compiler: GNU GCC Compiler) ===|
D:\CPP\codeBlock\example\main.cpp|43|error: extra qualification 'Time::' on member 'set_time' [-fpermissive]|
D:\CPP\codeBlock\example\main.cpp|43|error: 'void Time::set_time()' cannot be overloaded|
D:\CPP\codeBlock\example\main.cpp|6|error: with 'void Time::set_time()'|
D:\CPP\codeBlock\example\main.cpp|57|error: extra qualification 'Time::' on member 'show_time' [-fpermissive]|
D:\CPP\codeBlock\example\main.cpp|57|error: 'void Time::show_time()' cannot be overloaded|
D:\CPP\codeBlock\example\main.cpp|7|error: with 'void Time::show_time()'|
D:\CPP\codeBlock\example\main.cpp|61|error: extra qualification 'Time::' on member 'is_time' [-fpermissive]|
D:\CPP\codeBlock\example\main.cpp|67|error: extra qualification 'Time::' on member 'add_sec' [-fpermissive]|
D:\CPP\codeBlock\example\main.cpp|67|error: 'void Time::add_sec(int)' cannot be overloaded|
D:\CPP\codeBlock\example\main.cpp|8|error: with 'void Time::add_sec(int)'|
D:\CPP\codeBlock\example\main.cpp|81|error: extra qualification 'Time::' on member 'add_minute' [-fpermissive]|
D:\CPP\codeBlock\example\main.cpp|81|error: 'void Time::add_minute(int)' cannot be overloaded|
D:\CPP\codeBlock\example\main.cpp|9|error: with 'void Time::add_minute(int)'|
D:\CPP\codeBlock\example\main.cpp|90|error: extra qualification 'Time::' on member 'add_hour' [-fpermissive]|
D:\CPP\codeBlock\example\main.cpp|90|error: 'void Time::add_hour(int)' cannot be overloaded|
D:\CPP\codeBlock\example\main.cpp|10|error: with 'void Time::add_hour(int)'|
D:\CPP\codeBlock\example\main.cpp|109|error: expected '}' at end of input|
D:\CPP\codeBlock\example\main.cpp||In member function 'void Time::add_a_sec()':|
D:\CPP\codeBlock\example\main.cpp|13|error: 'sec' was not declared in this scope|
D:\CPP\codeBlock\example\main.cpp|17|error: 'minute' was not declared in this scope|
D:\CPP\codeBlock\example\main.cpp|21|error: 'hour' was not declared in this scope|
D:\CPP\codeBlock\example\main.cpp|25|error: a function-definition is not allowed here before '{' token|
D:\CPP\codeBlock\example\main.cpp|42|error: expected '}' at end of input|
D:\CPP\codeBlock\example\main.cpp||In member function 'void Time::set_time()':|
D:\CPP\codeBlock\example\main.cpp|48|error: 'hour' was not declared in this scope|
D:\CPP\codeBlock\example\main.cpp|48|error: 'minute' was not declared in this scope|
D:\CPP\codeBlock\example\main.cpp|48|error: 'sec' was not declared in this scope|
D:\CPP\codeBlock\example\main.cpp||In member function 'void Time::show_time()':|
D:\CPP\codeBlock\example\main.cpp|59|error: 'hour' was not declared in this scope|
D:\CPP\codeBlock\example\main.cpp|59|error: 'minute' was not declared in this scope|
D:\CPP\codeBlock\example\main.cpp|59|error: 'sec' was not declared in this scope|
D:\CPP\codeBlock\example\main.cpp||In member function 'void Time::add_sec(int)':|
D:\CPP\codeBlock\example\main.cpp|69|error: 'sec' was not declared in this scope|
D:\CPP\codeBlock\example\main.cpp|73|error: 'minute' was not declared in this scope|
D:\CPP\codeBlock\example\main.cpp|77|error: 'hour' was not declared in this scope|
D:\CPP\codeBlock\example\main.cpp||In member function 'void Time::add_minute(int)':|
D:\CPP\codeBlock\example\main.cpp|83|error: 'minute' was not declared in this scope|
D:\CPP\codeBlock\example\main.cpp|87|error: 'hour' was not declared in this scope|
D:\CPP\codeBlock\example\main.cpp||In member function 'void Time::add_hour(int)':|
D:\CPP\codeBlock\example\main.cpp|92|error: 'hour' was not declared in this scope|
D:\CPP\codeBlock\example\main.cpp||In member function 'int Time::main()':|
D:\CPP\codeBlock\example\main.cpp|101|error: 'T1' was not declared in this scope|
D:\CPP\codeBlock\example\main.cpp|103|error: 'class Time' has no member named 'add_a_minute'|
D:\CPP\codeBlock\example\main.cpp|104|error: 'class Time' has no member named 'add_an_hour'|
D:\CPP\codeBlock\example\main.cpp|109|error: expected unqualified-id at end of input|
||=== Build failed: 38 error(s), 0 warning(s) (0 minute(s), 1 second(s)) ===|
~~~
第一个提示“extra qualification 'Time::' on member 'set_time'”,说Time::多余,类外定义成员函数,不多余啊!
第二个提示“ 'void Time::set_time()' cannot be overloaded”,Time::set_time()不能被重载。
我大概知道怎么回事了。
用resource code formatter整理程序的格式,缩进发生变化,妖孽立刻现身。
下面是我和学生在QQ群中的对话,读者如果没有找出错误,不妨边看边试,按我的提示,自己找出问题来。
贺老师 2015-3-21 18:54:18
在Code::Blocks中,你整理一下格式,你会知道这个错有多么低级。然后回头再看这些错误提示,可以引出不少有价值的东西来。
贺老师 2015-3-21 18:55:28
其他同学,也可以从XX的低级错误中学到不少,试试吧。
学生 2015-3-21 18:57:37
糗大了
贺老师 2015-3-21 18:59:26
这样的错误,在学习过程中,价值连城!你经过了这样的错误,你就拥有了特别的财富。
学生 2015-3-21 19:00:37
还是有一点不大明白
贺老师 2015-3-21 19:01:13
重排版了吗?
学生 2015-3-21 19:01:25
排了
贺老师 2015-3-21 19:01:34
你能确认void Time::set_time()是在类外了吗?
学生 2015-3-21 19:02:37
嗯 类内定义 类外写函数
贺老师 2015-3-21 19:03:06
如果类外定义,这个函数该顶头才是,
贺老师 2015-3-21 19:03:19
类内定义,才会缩进去。
贺老师 2015-3-21 19:03:46
你类内声明,类内定义。
贺老师 2015-3-21 19:04:22
这就重复了。
贺老师 2015-3-21 19:07:50
发现了吗?要不要再给点线索?
学生 2015-3-21 19:08:34
在给点吧
贺老师 2015-3-21 19:09:07
你看类内定义的void add_a_sec(),在哪儿结束的?
学生 2015-3-21 19:10:09
落了一个括号
贺老师 2015-3-21 19:10:26
手边有砖吗?
学生 2015-3-21 19:10:54
不是 是括号括错了 我去买块豆腐砖
学生 2015-3-21 19:11:45
谢谢贺老
贺老师 2015-3-21 19:11:48
没砖为师就放心了。不必拍,我怕豆腐疼。学习过程中,很正常。
贺老师 2015-3-21 19:12:40
以后错误莫名其妙时,可以排版一下。若缩进不正常,问题一下就出来了。这也是讲究规范的价值。
学生 2015-3-21 19:13:00
恩恩 学到了
记录:50多行程序中找出多写的一个字母e
最后更新于:2022-04-01 14:38:18
[小霍同学](http://blog.csdn.net/u012369069)调程序,做的是第11周的[项目1 - 存储班长信息的学生类](http://blog.csdn.net/sxhelijian/article/details/25141489),但是她写的程序(就在下面),呃,请读者自己运行一下吧。(下午在机房调试时用的是Code::Blocks10.05,输出的是很长的莫名的符号,晚上在家用的是CodeBlocks12.11,典型的内存溢出症状。)
~~~
#include <iostream>
#include <string>
using namespace std;
class Stu //声明基类
{
public:
Stu(int n, string nam ):num(n),name(nam) {} //基类构造函数
void display( ); //成员函数,输出基类数据成员
protected: //(*)访问权限为保护型的数据成员[不能被外界引用但可以被派生类引用]
int num; //学生学号
string name; //学生姓名
};
void Stu::display( ) //成员函数,输出基类数据成员
{
cout<<"学号:"<<num<<endl;
cout<<"姓名:"<<name<<endl;
}
class StuDetail: public Stu //声明派生类StuDetail
{
public:
//学生nam,学号n,a岁,家住ad,他的班长是nam1,学号n1
StuDetail(int n, string nam,int a, string ad,int n1, string nam1); //派生类构造函数
void show( ); //成员函数,输出学生的信息
void show_monitor( ); //成员函数,输出班长信息
private:
Stu monitor; //学生所在班的班长,班长是学生,是Stu类的成员
int age; //学生年龄
string addr; //学生的住址
};
StuDetail::StuDetail(int n, string nam,int a, string ad,int n1, string nam1):Stu(n,name),monitor(n1,nam1)
{
age=a;
addr=ad;
}
void StuDetail::show( ) //成员函数,输出学生的信息
{
cout<<"学生信息:"<<endl;
cout<<"学号:"<<num<<endl;
cout<<"姓名:"<<name<<endl;
cout<<"年龄:"<<age<<endl;
cout<<"住址:"<<addr<<endl;
}
void StuDetail::show_monitor( ) //成员函数,输出班长信息
{
cout<<"班长信息:"<<endl;
monitor.display();
}
int main( )
{
//学生王力,10010号,19岁,家住上海的北京路,他的班长是李孙,学号10001
StuDetail s(10010,"Wang-li",19,"115 Beijing Road,Shanghai",10001,"Li-sun");
s.show( ); //输出学生信息
s.show_monitor(); //输出班长信息
return 0;
}
~~~
老贺及时来帮忙,单步执行是法宝。
在机房调试时用的是Code::Blocks10.05,51行定义并初始化对象s没有问题,顺利通过,而在52行s.show(),输出了莫名的文字。
再次单步,step into到s.show()中,发现混乱来自于39行输出name的地方。很自然,name的值有问题。name值是通过构造函数获取的,需要找源头。问题出在39行,但思维必须得跳出show函数,找到真正的罪犯。
昏花的老眼看30行的构造函数,一眼就看出了冒号后面对基类构造函数的调用Stu(n,name)有蹊跷:形式参数中声明的是(int n, string nam, ...),而实际参数怎么就出来了(n,name),去年一个字母e,构造函数的调用该为Stu(n,nam)。编译再运行,小霍紧锁的眉头放开了。刚才还在怨着计算机的她,应该在反思着自己的大意了。
为何会这样?错误的调用,实际上在用未经初始化的基类成员name作实参,那初始化的结果,name成员仍然是那个“野对象”。string是C++中增加的类,其中的字符符串实际也是用指针实现的。未经初始化的对象,其中就有野指针。
写博文时用的是Code::Blocks12.11,单步执行时,51行的构造函数就下不去,焦点仍然可以锁定在派生类的构造函数上,问题仍能解决。
编程序,练的就是这番功力。
|
~~~
=================== 迂者 贺利坚 CSDN博客专栏=================
|== IT学子成长指导专栏 专栏文章的分类目录(不定期更新) ==|
|== C++ 课堂在线专栏 贺利坚课程教学链接(分课程年级) ==|
|== 我写的书——《逆袭大学——传给IT学子的正能量》 ==|
===== 为IT菜鸟起飞铺跑道,和学生一起享受快乐和激情的大学 =====
~~~
|
|-----|
一个数组越界的C++程序
最后更新于:2022-04-01 14:38:16
学生给我发了私信,一个程序运行了好久,在OJ就是提交不了。
题目是:
**Description**
输入10个整数,将其中最小的数与第一个数对换,把最大的数与最后一个数对换。写三个函数; ①输入10个数;②进行处理;③输出10个数。
**Input**
10个整数
**Output**
整理后的十个数,每个数后跟一个空格(注意最后一个数后也有空格)
**Sample Input**
2 1 3 4 5 6 7 8 10 9
**Sample Output**
1 2 3 4 5 6 7 8 9 10
**HINT**
主函数已给定如下,提交时不需要包含下述主函数
~~~
/* C/C++代码 */
int main()
{
const int n=10;
int a[n];
input(a,n);
handle(a,n);
output(a,n);
return 0;
}
~~~
学生的解答:
~~~
#include<iostream>
#include<cstdio>
using namespace std;
void input(int a[],int);
void handle(int a[],int);
void output(int a[],int);
int main()
{
const int n=10;
int a[n];
//freopen("input.txt","r",stdin);
input(a,n);
handle(a,n);
output(a,n);
return 0;
}
void input(int a[],int n)
{
for(int i=0;i<n;i++)
cin>>a[i];
}
void handle(int a[],int n)
{
int max,i=0,t,min,z,zd,zx;
max=a[i];
min=a[i];
for(i=0;i<n;i++)
{
if(max<a[i])
{
max=a[i];
zd=i; //记录最大值的位置
}
if(min>a[i])
{
min=a[i];
zx=i; //记录最小值的位置
}
}
t=a[9];
a[9]=max;
a[zd]=t; //进行值交换
z=a[0];
a[0]=min;
a[zx]=z;
}
void output(int a[],int n)
{
for(int i=0;i<n;i++)
{
cout<<a[i]<<" ";
}
}
~~~
这个解答在CodeBlocks中编译通过,屡经测试数据,没有异常,然而,提交后提示“Runtime error”,具体是:
Runtime Error:Segmentation fault
辅助解释:Segmentation fault:段错误,检查是否有数组越界,指针异常,访问到不应该访问的内存区域
这不科学!由“一定是我错了”的思维,转向了“可能是机器错了”,利用管理员帐号,看了测试数据,也没有问题。
再逐句看程序,一个地方引起我的注意,会是25、26行引用a[i],但 i 没有赋值吗?不是,i 的初值是0,于算法没有问题。就在第24行,zd、zx两个变量,也应该是同步赋值的,它们表示最大、最小的数的下标,后面循环中,也及时对这两个数做了更新。
问题逐渐明白了,在24行不给zd、zx赋初值的情况下,如果29和34行的if条件有一个始终未能为真,zd、zx就不会赋值,40行开始的交换,就会出现数组越界。
** 在这个程序上小修改的办法有了:第24行,加上给zd、zx赋初值,即:int max,i=0,t,min,z,zd=0,zx=0;**
这个程序,没有用上单步执行。对这种小几率的问题,单步可能也不管用,思维要严密。
这个程序写得稍显罗嗦,下面给出一个改写版。
~~~
#include<iostream>
#include<cstdio>
using namespace std;
void input(int a[],int);
void handle(int a[],int);
void output(int a[],int);
int main()
{
const int n=10;
int a[n];
//freopen("input.txt","r",stdin);
input(a,n);
handle(a,n);
output(a,n);
return 0;
}
void input(int a[],int n)
{
for(int i=0;i<n;i++)
cin>>a[i];
}
void handle(int a[],int n)
{
int i,t,zd=0,zx=0;//默认第0个为最大、最小
for(i=1;i<n;i++) //从第1个开始
{
if(a[zd]<a[i])
zd=i; //记录最大值的位置
if(a[zx]>a[i])
zx=i; //记录最小值的位置
}
t=a[n-1]; //比用含有“神秘数”的a[9]好
a[n-1]=a[zd];
a[zd]=t; //进行值交换
t=a[0];
a[0]=a[zx];
a[zx]=t;
}
void output(int a[],int n)
{
for(int i=0;i<n;i++)
{
cout<<a[i]<<" ";
}
}
~~~
'abc'的值是多少?
最后更新于:2022-04-01 14:38:14
看见一个贴子,发贴人搞不清输出结果中的“为什么”(见[http://bbs.csdn.net/topics/390623518](http://bbs.csdn.net/topics/390623518))。给出的程序是:
~~~
#include <stdio.h>
int main(void)
{
/* 将单个字符赋值给整形变量,结果为字符的ASCII值 */
int a = 'a';
printf("a = %d\n", a);
/* ?这里如何解释 */
a = 'abc';
printf("a = %d\n", a);
/* 将字符串赋值给整形变量,结果为字符串的地址值 */
a = "a";
printf("a = %d\n", a);
a = "abc";
printf("a = %d\n", a);
return 0;
}
~~~
'abc'这样的写法,我也是第一次见到,居然编译器只给一个警告。
程序的运行结果是:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-07_575640493855f.jpg)
第二行的结果,即a = 'abc';后a的值显得诡异,跟贴中,大家纷纷猜测。
我对这种情况也不清楚,这时想到的是,要将整型数a中的4个字节中每个字节的值能分隔出来,应该就一目了然了,最好将每个字节解释成一个之字符直接输出来看。
想到了“联合体”union,将程序改造了一下,得到下面的程序:
~~~
#include <stdio.h>
union un
{
int i;
char c[4];
};
int main(void)
{
/* 将单个字符赋值给整形变量,结果为字符的ASCII值 */
int a = 'a';
union un b;
printf("a = %d\n", a);
/* 这里的b.i就相当于原来的a,而b.c数组与b.i占用同一段4字节的空间 */
b.i = 'abc';
printf("b.i = %d\n", b.i);
printf("b.c = %c %c %c %c\n", b.c[0], b.c[1],b.c[2],b.c[3]);
/* 将字符串赋值给整形变量,结果为字符串的地址值 */
a = "a";
printf("a = %d\n", a);
a = "abc";
printf("a = %d\n", a);
return 0;
}
~~~
运行结果是:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-07_5756404955930.jpg)
结果不解释了,原理应该在“计算机组成原理”或“计算机体系结构”中介绍,学过汇编语言的人也明白。程序员学软件,要对计算机系统有理解,这是专业人员该关注的。
C/C++中的union,似乎在教学中都是一带而过,感兴趣的同学,再找其他资料了解了解即可。
C++字符数组越界问题的一个案例分析
最后更新于:2022-04-01 14:38:12
我的学生[yang1067155909](http://my.csdn.net/yang1067155909http://blog.csdn.net/yang1067155909)给我来信,说的是[C++第11周项目3 - CEmployee类继承自CPerson类](http://blog.csdn.net/sxhelijian/article/details/8910979)中的一个细节:
贺老师:
老师,m_szDepartment=new char[strlen(department)+1];为何需要+1呢?在测试里去掉+1后和这个效果一样啊,不太明白……求指教……
学生,杨腾飞
我回答:
要给'\0'占个座。是用别人的地盘(越界的部分)保存了自己的信息了吧,不定哪次人家要用,运行结果就不一样了。这恰是最危险的问题。
他继续追问:
可是在定义字符数组时,比如a[4]时,可以输入5个字符,那么这个数组的'\0'的位置是不是也占用了别人的?
我为这个机灵的同学的问题感到激动,读程序,切忌只是读,要会提问题。能自己提出问题,就一定能学好。老师给出解答,接着再提出新问题,自己解答,或再问老师,这就是交流。为着学生提出了好问题,作为老师,我骄傲。我的回答是:
你提的问题不是一般的好,见我新发表的博文谈这个问题。
下面就是我对这个问题的解答。不妨针对问题设计一个程序试一试。程序是:
~~~
#include <iostream>
using namespace std;
int main()
{
char a[4];
cin>>a;
cout<<a<<endl;
return 0;
}
~~~
亲爱的读者,读这篇文章时,请不要只“读”,打开你熟悉的编程环境,边读边运行。你会发现什么?
输入abcd然后回车,输出是abcd。cout<<a是将字符数组当字符串输出的,显然abcd已经占满了自己的地盘a[0]到a[3],能够“如愿”输出,实际上已经侵占了不该占的内存a[4]单元。当然,恰好a[4]处给脸,就是'\0'。如果”烫烫烫烫烫烫“不必意外。
再运行,输入abcde。我运行的结果是,在VC++6.0中,输出abcde,并弹出了我们熟悉的内存越界错误提示。在codeBlocks下,输出abcde,什么也没提示。
请读者想想,这是一个多么凶险的Bug。
下面再给出一个程序:
~~~
#include <iostream>
using namespace std;
int main()
{
char a[4],b[4];
cin>>a;
cin>>b;
cout<<a<<endl;
cout<<b<<endl;
return 0;
}
~~~
运行的任务交给读者了,观察输入3个字符、4个字符、5个字符的情形,也可以在多个平台上试试,针对结果想想为什么。用单步执行的手段跟踪一下内存中的数据存储,是个强烈建议的办法。
下面是为a和b数组输入3个字符后(分别是abc和hij),利用单步执行看到的结果:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-07_57564048eed88.jpg)
下面是为a和b数组输入5个字符后(分别是abcef和hijkl),利用单步执行看到的结果:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-07_5756404910e06.jpg)
从中看出,VC++6.0中,先定义的a数组的地址大于后定义的数组b的地址,本来为a中输入了abcde,侵占了别人的地盘,随后为b输入hijkl,侵占的就是a的地盘,b[4]即a[0]为l,b[5]即a[1],存储的是'\0'!
下图是在codeBlocks下,用同样的输入调试截出的结果,结果一样:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-07_575640492441f.jpg)
接下来,再给一个程序,其实就是将输入a和b的顺序换了一下:
~~~
#include <iostream>
using namespace std;
int main()
{
char a[4],b[4];
cin>>b;
cin>>a;
cout<<a<<endl;
cout<<b<<endl;
return 0;
}
~~~
运行结果会是怎样?读者你自己说吧。不要忘了,用调试工具这个法宝解除你的疑惑。
初学者遭遇离奇错误——求两点间的距离(C++)
最后更新于:2022-04-01 14:38:09
有学生向我求助,他编了一个程序,设计一个“点”类,求出两点距离的程序。程序看着没有问题,却出了一大堆的错误。程序如下:
~~~
#include <iostream>
#include <math.h>
using namespace std;
class point
{
public:
double x;
double y;
};
double distance(point p1,point p2);
int main()
{
point p1= {3,5},p2= {6,9};
cout<<distance(p1,p2);
}
double distance(point p1,point p2)
{
double d=sqrt((p1.x-p2.x)*(p1.x-p2.x)+(p1.y-p2.y)*(p1.y-p2.y));
return d;
}
~~~
在codeBlocks下编译,错误直接引到stl_iterator_base_types.h文件中,错误一大堆:
~~~
||=== example, Debug ===|
D:\C++\codeBlock\example\example.cpp|15|instantiated from here|
d:\program files\codeblocks\mingw\bin\..\lib\gcc\mingw32\4.4.1\include\c++\bits\stl_iterator_base_types.h|127|error: no type named 'iterator_category' in 'class point'|
d:\program files\codeblocks\mingw\bin\..\lib\gcc\mingw32\4.4.1\include\c++\bits\stl_iterator_base_types.h|128|error: no type named 'value_type' in 'class point'|
d:\program files\codeblocks\mingw\bin\..\lib\gcc\mingw32\4.4.1\include\c++\bits\stl_iterator_base_types.h|129|error: no type named 'difference_type' in 'class point'|
d:\program files\codeblocks\mingw\bin\..\lib\gcc\mingw32\4.4.1\include\c++\bits\stl_iterator_base_types.h|130|error: no type named 'pointer' in 'class point'|
d:\program files\codeblocks\mingw\bin\..\lib\gcc\mingw32\4.4.1\include\c++\bits\stl_iterator_base_types.h|131|error: no type named 'reference' in 'class point'|
||=== Build finished: 5 errors, 0 warnings ===|
~~~
对于这样的问题,初学C++的同学肯定直接蒙。这里的问题出在命名空间中名字的冲突,再多说一些,与STL也有些关系。不过,解决这样的问题并不一定得知道这么多。我还是试着与大家绕开这个环节,从其他途径找点感觉。
光标置到“D:\C++\codeBlock\example\example.cpp|15|instantiated from here|”一行,双击,发现错误在程序的第15行。鼠标放到15行的distance函数上时,浮现出了一行提示,见图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-07_57564048a2740.jpg)
看出了一点疑惑:distance不是在这个程序中编的自定义函数吗?怎么识别成了std::distance(...,...)?
这就是问题的根源!编译器没有将distance当作自定义函数处理!至于进一步的解释不再深入,抓住这个要点,程序就可以改好了。
**修改方法之一:**既然函数名字上出问题,试试改个名字?将distance改个名字,如dist,一切正常。
**修改方法之二:**凭什么让我改?distance多好的一个函数名(不过提醒,可以自学一下命名空间了,此是好机会,不必等着老师讲。)需要做的工作是,不用std命名空间(删除或注释掉using namespace std;一行)然后在依赖std的cout前加上std::,程序如下:
~~~
#include <iostream>
#include <math.h>
//using namespace std;//不再用命名空间std
class point
{
public:
double x;
double y;
};
double distance(point p1,point p2);
int main()
{
point p1= {3,5},p2= {6,9};
std::cout<<distance(p1,p2); //保证编译系统知道用std命名空间中的cout
}
double distance(point p1,point p2)
{
double d=sqrt((p1.x-p2.x)*(p1.x-p2.x)+(p1.y-p2.y)*(p1.y-p2.y));
return d;
}
~~~
**修改方法之三:**方法二有点自私了。std中有不少常用的东东,就此全都得写std::,这个程序中无妨,如果再大些的程序呢?胳膊不必和大腿拧,换种思路,也是一样。将distance在调用时,写作为::distance,指出distance是当前程序中定义的名字。问题解决就此解决,程序如下:
~~~
#include <iostream>
#include <math.h>
using namespace std;
class point
{
public:
double x;
double y;
};
double distance(point p1,point p2);
int main()
{
point p1= {3,5},p2= {6,9};
cout<<::distance(p1,p2);//指定distance不是别处的,就是本文件中定义的
}
double distance(point p1,point p2)
{
double d=sqrt((p1.x-p2.x)*(p1.x-p2.x)+(p1.y-p2.y)*(p1.y-p2.y));
return d;
}
~~~
**修改方法之四:**前三种方法中,个人倾向于第一种,山不转水转,换个名字也妨。其实这也不是最好的。原始的程序中定义了类,但只有数据成员,没有成员函数,像求距离之类的,设计为成员函数多好。面向对象的机制就是为了信息封装等特性的,为何要如此浪费?这个程序我就不写了,请自行解决。
**补充:**用其他编程环境时,观察和修改的方法也类似,例如在VS2008下编译,错误居然有25个之多:
~~~
1>------ 已启动生成: 项目: example, 配置: Debug Win32 ------
1>正在编译...
1>example.cpp
1>d:\program files\microsoft visual studio 9.0\vc\include\xutility(764) : error C2039: “iterator_category”: 不是“point”的成员
1> d:\c++\vs2008 project\example\example\example.cpp(5) : 参见“point”的声明
1> d:\c++\vs2008 project\example\example\example.cpp(16): 参见对正在编译的类 模板 实例化“std::iterator_traits<_Iter>”的引用
1> with
1> [
1> _Iter=point
1> ]
(此处省略N多的提示)
1>生成日志保存在“file://d:\C++\VS2008 project\example\example\Debug\BuildLog.htm”
1>example - 25 个错误,0 个警告
========== 生成: 成功 0 个,失败 1 个,最新 0 个,跳过 0 个 ==========
~~~
在源程序中,鼠标光临distance函数时,可以看出编译器对distance函数有两种解释,如下图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-07_57564048c83c8.jpg)
编译器对此局面真的很迷茫了。余下的修改思路相同,不再罗嗦。
编程学习,从意外中收获
最后更新于:2022-04-01 14:38:07
提倡同学们积累代码行数,于是有了大量的编程练习。做下来是一个基本的要求,对个别同学而言,已经是轻松面对,他们在基本编程能力方面,已经有了保障,这为今后的学习和自学打下了好的基础。一些同学一路紧随,跟住了都不是易事,有些依然不能按时按量完成。心中想着要多做几个,但调试中遇到问题,难免急躁。
看到一位同学在博文最后写的一段话:“直接抓狂了,经过个人分析,应该是函数change24与changefrom0没有实现其功能,可是定义函数,函数的赋值及调用都与其他同学的相同,问题究竟出在哪里啊?求指点!”
这是C++第7周项目1 - 静态成员应用于时间类(见[http://blog.csdn.net/sxhelijian/article/details/8795951](http://blog.csdn.net/sxhelijian/article/details/8795951)) 的题目。目标是学会静态数据成员以及静态成员函数的用法。
下面是只涉及静态数据成员以及静态成员函数的用法的“缩略版”解答,当然,包含这位同学犯的错误。请读者先读完这段程序。
~~~
#include <iostream>
#include<string>
using namespace std;
class Time
{
public:
Time(int=0,int=0,int=0);
void show_time( ); //根据is_24和from0,输出适合形式-20:23:5/8:23:5 pm/08:23:05 pm
static void change24(); //改变静态成员is_24,在12和24时制之间转换
static void changefrom0(); //改变静态成员from0,切换是否前导0
private:
static bool is_24; //为true时,24小时制,如20:23:5;为flase,12小时制,显示为8:23:5 pm
static bool from0; //为true时,前导0,8:23:5显示为08:23:05
int hour;
int minute;
int sec;
};
Time::Time(int h,int m,int s)
{
hour=h;
minute=m;
sec=s;
}
bool Time::is_24=true;
bool Time::from0=false;
void Time::show_time( )
{
int h=(is_24)?hour:hour%12;
if (h<10&&from0) cout<<'0';
cout<<h<<':';
if(minute<10&&from0) cout<<'0';
cout<<minute<<':';
if(sec<10&&from0) cout<<'0';
cout<<sec;
if(!is_24)
cout<<((hour>12)? " pm":" am");
cout<<endl;
}
void Time::change24()
{
is_24=!is_24;
}
void Time::changefrom0()
{
from0=!from0;
}
int main( )
{
Time t1(23,14,25),t2(8,45,6);
cout<<"24小时制,不导0:"<<endl;
t1.show_time();
t2.show_time();
cout<<"切换是否前导0:"<<endl;
t1.changefrom0();
t2.changefrom0();
t1.show_time();
t2.show_time();
cout<<"换一种制式:"<<endl;
t1.change24();
t2.change24();
t1.show_time();
t2.show_time();
}
~~~
这段程序的实际运行结果是:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-07_575640485279e.png)
可以发现,切换是否前导0,换制式,根本没有起作用。预期中的结果应该是:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-07_5756404874513.png)
面对运行中的问题,最直接的办法就是采用单步执行的办法,跟踪一下程序在执行过程中,相关变量的变化过程。显然,这位同学并没有这样做过。
一再强调的调程序的“利器”,就被藏在“兵器库”中,任由其闲置。这种技能就是要在实践中学会的,这是学会的最佳时刻。有些同学已经用得纯熟,有些还在躲避。同一个教室上课,差别怎么这么大。
不说这些了。其实,不用单步调试,这个事不好做,也真不好说。初学有关知识,都要靠着推理式的排查,这是更难的办法。
这个程序中包括main()函数在内一共5个函数。类的构造函数Time()和用于显示的函数Time::show_time()嫌疑不大,先放过。main()似乎中规中矩,也放下。Time::changefrom0()和Time::change24()与以前编程的程序中有点不一样,声明前加了个static,这时可以看书了,明确这叫做静态成员函数,静态成员函数是用来使用静态数据成员的,再看static bool is_24; 和static bool from0; ,这两个成员不一般,静态的,静态的有何特点?它不属于任何一个对象,属于类。然后看看讲义,老师讲过这个图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-07_575640488ba72.png)
围绕着“静态”,找到main()函数中有这么两句,调用的是静态成员函数:
~~~
t1.changefrom0();
t2.changefrom0();
~~~
这两个静态成员函数操作的将是同一个数据Time::from0,尽管t1.changefrom0()和t2.changefrom0()前面打着t1、t2人民的旗号,干得都是服务大众的工作。from0和hour等不一样,它是静态的。
于是明白了吧。t1.changefrom0()将from0由假为真,t2.changefrom0()又将from0由真为假,最后输出没有变化。
发生在is_24上的事情也是一样。
程序运行结果不在预期中,这是学习的最佳机会。
稀里糊涂走进来,明明白白走出去。这大概是积累代码行的路线中应该收获的,而其中需要的,是冷静,以及实践与看书的结合。
最后,还说,单步调试,闲着太可惜。
找出运行错误的元凶
最后更新于:2022-04-01 14:38:05
收到一位同学的求助信,解决的是[http://blog.csdn.net/sxhelijian/article/details/8737365](http://blog.csdn.net/sxhelijian/article/details/8737365)中的问题。她的程序如下:
~~~
#include <iostream>
using namespace std;
class CFraction
{private:
int nume; // 分子
int deno; // 分母
public:
void input(); //按照"nu/de"的格式,如"5/2"的形式输入
void simplify(); //化简(使分子分母没有公因子)
void amplify(int n); //放大n倍,如2/3放大5倍为10/3
void output(int style); //输出:以8/6为例,style为0时,原样输出8/6;
//style为1时,输出化简后形式4/3;
//style为2时,输出1(1/3)形式,表示一又三分之一;
//style为3时,用小数形式输出,如1.3333;
//不给出参数和非1、2,认为是方式0
};
//主函数
int main()
{
CFraction c;
c.input();
c.simplify();
c.amplify(3);
c.output(1);
c.output(2);
c.output(3);
c.output(0);
return 0;
}
//按照"nu/de"的格式,如"5/2"的形式输入
void CFraction::input()
{
char c;
cout<<"请输入分数的值:"<<endl;
while(1)
{
cin>>nume>>c>>deno;
if(c!='/')
cout<<"格式不正确,请重新输入!"<<endl;
else
break;
}
}
//化简(使分子分母没有公因子)
void CFraction::simplify()
{
int num1,num2,temp;
int x,y;
num1=nume;
num2=deno;
if(num1<num2) //找出较大的值
{
temp=num1;
num1=num2;
num2=temp;
}
x=nume;
y=deno;
while(y>0)
{
temp=x%y;
x=y;
y=temp;
};
num1=num1/x;
num2=num2/y;
cout<<num1<<'/'<<num2<<endl;
}
//放大n倍,如2/3放大5倍为10/3
void CFraction::amplify(int n)
{
int num1,num2;
num1=nume*n;
num2=deno*n;
cout<<num1<<'/'<<num2<<endl;
}
//输出:以8/6为例,style为0时,原样输出8/6;为1时,输出化简后形式4/3;为2时,输出1(1/3)形式,表示一又三分之一;为3时,用小数形式输出,如1.3333;
//不给出参数和非1、2,认为是方式0
void CFraction::output(int style)
{
if(style==1)
{
int num1,num2,temp;
int x,y;
num1=nume;
num2=deno;
if(num1<num2) //找出较大的值
{
temp=num1;
num1=num2;
num2=temp;
}
x=nume;
y=deno;
while(y>0)
{
temp=x%y;
x=y;
y=temp;
};
num1=num1/x;
num2=num2/y;
cout<<num1<<'/'<<num2<<endl;
}
else
{
if(style==2)
{
int a,b,c=0;
a=nume;
b=deno;
while(a>=b)
{
a=a-b;
c++;
}
if(a==0)
cout<<c<<endl;
else
cout<<c<<'('<<a<<'/'<<b<<')'<<endl;
}
if(style==3)
{
double c;
c=nume/deno;
cout<<c<<endl;
}
else
cout<<nume<<'/'<<deno<<endl;
}
}
~~~
她的问题是:编译,链接都没错,但不能运行???
尝试运行,输入后,运行出错。初步判断,调用CFraction::input()没有出问题,CFraction::simplify()的嫌疑很大。
输入分子、分母,单步跟踪进CFraction::simplify(),几步之后,真相出现了。见图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-07_5756404836a58.png)
阶数为0。
这个程序在算法设计上有问题,请自行再去考虑。需要提醒的是,单步调试,可以学会。
BUG现形记(二)——偷工减料的复制构造函数
最后更新于:2022-04-01 14:38:03
【课程支撑】[我的 C++程序设计课程教学材料](http://blog.csdn.net/sxhelijian/article/details/7056008)
【摘要】设计数组类,要实现数组类中两个数组相加的运算,程序却陷入死循环。逐层排查,重载的加法正确,重载的赋值运算也看不出问题。跟踪到赋值运算的实现中发现,传递的参数中有异常,终于找出了嫌疑犯——编制的复制构造函数偷工减料。
【阅读提示】现在打开你熟悉的c++,跟随作者的的思路,重走发现嫌犯的过程。
题目是建立专门的数组类处理有关数组的操作,要完成支持数组操作的类的设计,增强C++内置数组类型功能。——见:[第14周-任务1-数组类的构造](http://blog.csdn.net/sxhelijian/article/details/7587387)
有同学向我求助,他的程序如下:
~~~
#include <iostream>
using namespace std;
class MyArray
{
private:
int *arr; //用于存放动态分配的数组内存首地址
int size; //数组大小
public:
MyArray(int sz=50);
MyArray(int a[],int sz); //由一个内置类型的数组初始化
MyArray(const MyArray &A); //拷贝构造函数
~MyArray(void); //析构函数,注意释放空间
MyArray&operator =(const MyArray &A); //重载“=”使得数组对象可以整体赋值
int& operator[](int i); //重载[],使得Array对象也可以如C++普通数组一样,用a[i]形式取出值【选做】
bool operator == (MyArray& A); //重载==,使得Array对象能整体判断两个数组是否相等(size相等且对应元素相等)
MyArray operator + (MyArray& A); //重载+,使两个Array对象可以整体相加(前提大小相等)【选做】
friend ostream& operator << (ostream& out,MyArray& A); //重载<<,输出数组
int GetSize(void)const; //取数组大小;
void Resize(int sz); //修改数组的大小,如果sz大于数组的原大小,增加的元素初始为;sz小于数组的原大小,舍弃后面的元素【选做】
};
MyArray::MyArray(int sz)
{
size = sz;
arr = new int[size];
for( int i = 0; i < size; i++ )
{
*(arr + i) = 0;
}
}
MyArray::MyArray(int a[],int sz) //由一个内置类型的数组初始化
{
size = sz;
arr = new int[size];
for(int i = 0; i < size; i++)
{
*(arr + i) = *(a + i);
}
}
MyArray::MyArray(const MyArray &A) //拷贝构造函数
{
arr = new int[A.size];
for(int i = 0; i < A.size; i++)
{
*(arr + i) = *(A.arr + i);
}
}
MyArray::~MyArray(void) //析构函数,注意释放空间
{
delete[]arr;
}
MyArray& MyArray::operator =(const MyArray &A) //重载“=”使得数组对象可以整体赋值
{
int n = A.size;
if( size != n )
{
delete[]arr;
arr = new int[n];
size = n;
}
int* destptr=arr;
int* srcptr=A.arr;
while(n--)
{
*destptr=*srcptr;
destptr++;
srcptr++;
}
return *this;
}
int& MyArray::operator[](int i) //重载[],使得Array对象也可以如C++普通数组一样,用a[i]形式取出值【选做】
{
return arr[i];
}
bool MyArray::operator == (MyArray& A) //重载==,使得Array对象能整体判断两个数组是否相等(size相等且对应元素相等)
{
bool m;
m = true;
if( A.size != size )
{
m = false;
}
else
{
for( int i = 0; i < size; i++ )
if( *(A.arr + i) != *(arr + i) )
{
m = false;
break;
}
}
return m;
}
MyArray MyArray::operator + (MyArray& A)
{
int n=A.size; //取A数组的大小
if (size!=n) //大小不一致不能相加
{
cout<<"not same size for add!"<<endl;
exit(1);
}
MyArray a(n); //指定size的数组
for (int i = 0; i < size; i++)
{
a[i]=arr[i]+A[i];
}
return a;//返回当前对象的引用
}
ostream& operator << (ostream& out,MyArray& A) //重载<<,输出数组
{
for( int i = 0; i < A.size; i++)
{
out << A[i] << " ";
}
out << endl;
return out;
}
int MyArray::GetSize(void)const //取数组大小;
{
return size;
}
void MyArray::Resize(int sz) //修改数组的大小,如果sz大于数组的原大小,增加的元素初始为;sz小于数组的原大小,舍弃后面的元素【选做】
{
int *m = new int(sz);
for( int i = 0; i < sz; i++)
{
*m = 0;
}
int n = ( sz <= size )?sz:size;
for(int j = 0; j < n; j++)
{
*(m + j) = *(arr + j);
}
delete[]arr;
arr = m;
}
int main()
{
int a[10]={1,2,3,4,5,6,7,8,9,10};
int b[10]={4,5,6,7,8,9,10,11,12,13};
MyArray arr1(a,10);
MyArray arr2(b,10);
MyArray arr3(10);
cout<<arr3;
arr3 = arr1 +arr2;
cout<<arr3;
// arr3.Resize(20);
// cout<<arr3;
// arr3.Resize(5);
// cout<<arr3;
system("pause");
return 0;
}
~~~
运行程序,结果如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-07_5756400be11e0.JPG)
光标在第二行一闪一闪,就是不见下文。
此症状一般是陷入了死循环。经阅读程序,已经输出的多个0是160行cout<<arr3;的结果。162行同样的语句应该不会出现问题。焦点锁定在第161行:arr3=arr1+arr2。
arr3=arr1+arr2涉及到两个运算符的重载:+ 和 =。
认真地读一下这两个重载函数,比较明显的是第115行的a[i]=arr[i]+A[i];有些别扭。a和A是MyArray类的对象,而arr是当前对象的一个成员(我想起一条胳膊和一个人要加起来,哪能这样!)修改为a.arr[i]=arr[i]+A.arr[i];(相当于a.arr[i]=this->arr[i]+A.arr[i];这就是胳膊和胳膊加了)。这样,对加法结果正确有了把握。
再次运行,问题依旧。也看不出明显的线索。请来法宝,祭起查找Bug的照妖镜——调试工具来帮忙。
在161行上设置断点运行程序,用F11逐语句执行,进入MyArray::operator +() 函数,即对加法的重载直至结束,未见异常。作为返回值的临时对象a,其中的结果正确。a 的两个属性 size 及 arr 和 arr 为起始地址指向的值,也恰好是该求得值的结果。可以在117行也设置一个断点,观察直至此时 相加结果 a 的值。下面是执行到此处时,从局部变量窗口看到的结果(如果要看a.arr[1]及之后的值,可以用监视窗口):
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-07_57564047ef381.JPG)
单步跟踪到MyArray::operator =()函数,即对赋值的重载。很意外地,函数进入到了第59行的 if 语句中。目前程序的运行,所用到的数组,其 size 均为10,应该这个 if 分支是执行不到的。而此时,局部变量的值却让人大吃一惊:MyArray::operator =()中形式参数 A 的 size 成员的值是一个负数!见下图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-07_575640481af11.JPG)
此时该整理一下其中存在的问题了:
(1)问题出在执行arr3=arr1+arr2;上;
(2)计算arr1+arr2,写成函数调用形式是arr1.operator+(arr2),函数调用时,返回的结果是正确的;
(3)接着计算赋值,MyArray::operator =(const MyArray &A) 的形式参数 A 对象,将获得 arr1+arr2 的结果,即实参是 arr1+arr2 的结果,更直观地,执行的是 arr3.operator=(arr1+arr2); ,计算结果本来是正确的,但实参传值给形参后,对象的 size 成员出错了。
在实参给形参传值时,采用的是复制的办法,对类(对象)而言,需要有一个复制构造函数支撑(这个学过)。复制构造函数可以是默认的。但是,如果类中有指针类型的成员时,必须自定义复制构造函数,从而能够处理指针所指向空间的分配和回收问题。
所以,嫌疑犯基本锁定:MyArray类的复制构造函数(也称拷贝构造函数)。看41行开始的拷贝构造函数MyArray::MyArray(const MyArray &A) 的定义,为this->arr成员分配了空间,并将形参对象中指向的数据一一进行复制,惟独没有做的,是为 this->size 赋值!我们需要在这个函数中,增加一行代码:size = A.size;。
至此,案情大白于天下,错误得以纠正,程序得以正确运行。
相关博文:[寻找Bug记实:一个多重继承程序的查错](http://blog.csdn.net/sxhelijian/article/details/7551594)
课程支撑:[我的 C++程序设计课程教学材料](http://blog.csdn.net/sxhelijian/article/details/7056008)
Bug现形记(一):一个多重继承程序的查错
最后更新于:2022-04-01 14:38:00
【课程支撑】[我的 C++程序设计课程教学材料](http://blog.csdn.net/sxhelijian/article/details/7056008)
要完成的任务详见[第12周-任务2-双肩挑干部](http://blog.csdn.net/sxhelijian/article/details/7543916)。题目要求
分别定义Teacher(教师)类和Cadre(干部)类,采用多重继承方式由这两个类派生出新类Teacher_Cadre(教师兼干部)。要求:
(1)在两个基类中都包含姓名、年龄、性别、地址、电话等数据成员。
(2)在Teacher类中还包含数据成员title(职称),在Cadre类中还包含数据成员post(职务),在Teacher_Cadre类中还包含数据成员wages(工资)。
(3)对两个基类中的姓名、年龄、性别、地址、电话等数据成员用相同的名字,在引用这些数据成员时,指定作用域。
(4)在类体中声明成员函数,在类外定义成员函数。
(5)在派生类Teacher_Cadre的成员函数show中调用Teacher类中的display函数,输出姓名、年龄、性别、职称、地址、电话,然后再用cout语句输出职务与工资。
下面是某同学的解答:
~~~
#include <iostream>
#include <string>
using namespace std;
class Teacher
{
public:
Teacher(string nam, int a, char s, string addr, string tel,string t);
void display();
protected:
string name;
int age;
string title;
string address;
char sex;
string telep;
};
class Cadre
{
public:
Cadre(string nam, int a, char s, string addr, string t,string p);
void display1();
protected:
string name;
int age;
string address;
char sex;
string telep;
string post;
};
class Teacher_Cadre:public Teacher,public Cadre
{
public:
Teacher_Cadre(string nam, int a, char s, string addr, string tel,float w,string t,string p);
void show();
protected:
string name;
int age;
string title;
string address;
char sex;
string telep;
string post;
float wages;
};
void Teacher::display()
{
cout<<Teacher::name<<" "<<Teacher::age<<" "<<Teacher::sex<<" "<<Teacher::address<<" "<<Teacher::telep<<" "<<title<<endl;
}
void Cadre::display1()
{
cout<<post<<endl;
}
void Teacher_Cadre::show()
{
display();
cout<<wages<<endl;
}
Teacher::Teacher(string nam, int a, char s, string addr, string tel,string t)
{
name=nam;
age=a;
address=addr;
telep=tel;
title=t;
}
Cadre::Cadre(string nam, int a, char s, string addr, string tel,string p)
{
name=nam;
sex=s;
age=a;
address=addr;
telep=tel;
post=p;
};
Teacher_Cadre::Teacher_Cadre(string nam, int a, char s, string addr, string tel,float w,string t,string p):Teacher(nam,a,s,addr,tel,t),Cadre(nam,a,s,addr,tel,p)
{
wages=w;
}
void main()
{
Teacher_Cadre t1("malin",19,'f',"yantai","18253593419",10000,"student","study");
t1.show();
t1.display1();
system("Pause");
}
~~~
程序的运行结果是:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-07_5756400ba0fb2.JPG)
注意到画红圈的地方。寻找程序执行的流程,应该执行的是第50行,是Teacher::display()输出对象t1信息时显示了这一行。很时显,红圈中的?应该是性别 f 。看第50行没有问题,那究竟在哪儿见着鬼了?
我决定用我们的法宝,调试工具这个照妖镜试试。问题出现在显示时,我在第88行t1.show();处加了断点并运行程序,打开自动窗口(窗口中出现的正是当前代码行涉及的对象的当前值),真相马上出现了。先看截屏:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-07_5756400bb6689.JPG)
可以看到,可疑之处要输出的性别sex出现了3次:①来自基类Teacher;②来自基类Cadre;③来自派生类Teacher_Cadre。根据第50行代码可以判断,运行时出现的异常由于Teacher::sex造成的,而①处取值恰好为 ‘?’(其ASCII码值为52)。症状掌握了。那病因何在呢?
显然,问题不在88行的show()函数,也不是48行Teacher::display()的毛病。问题出现在显示之前,对象根本没有获得正确的sex值。87行在定义对象时做了初始化,这个程序很短,疑点马上集中在对t1的初始化上。按照构造函数的执行过程逆着推上去,罪犯显形了:在63-70行Teacher的构造函数Teacher::Teacher(...)中,唯独缺少了对sex 数据成员的赋值!
罪状昭然于天下,Bug伏法吧!
单就运行结果看,程序没有问题了。但高度的责任感让我想到③处还有个问号。再一看,来自派生类Teacher_Cadre中的数据成员,除了wages的值正确,其他全……。推及Teacher_Cadre的构造函数(80-83行),确实,只给wages做了初始化。
如果简单些处理,在Teacher_Cadre::Teacher_Cadre(...)中再加些赋值不就行了?事情没有这么简单。看Teacher_Cadre的声明(32-46行),其数据成员多达8个!Teacher_Cadre继承了两个类的数据成员,其中有同名的造成了二义性,这还不够麻烦,又将那些数据成员照抄着来了一份,这样,在派生类的对象中,同名的数据成员将被存储3份。如果这样的话,继承还有何用?这反映了对继承的理解还没有到位,或许仅是粗心了。
如何更改程序,读者应该清楚了。
作为结尾,再次提醒同学们用好调试工具。
【课程支撑】[我的 C++程序设计课程教学材料](http://blog.csdn.net/sxhelijian/article/details/7056008)
前言
最后更新于:2022-04-01 14:37:58
> 原文出处:[代码侦探](http://blog.csdn.net/column/details/bugkiller.html)
作者:[贺利坚](http://blog.csdn.net/sxhelijian)
**本系列文章经作者授权在看云整理发布,未经作者允许,请勿转载!**
# 代码侦探
> 通过实例,介绍代码中Bug的寻找问题,帮助程序设计初学者提高程序的查错、纠错能力。专栏原创博文数量以作者平时大量辅导学生,帮助他们调试程序的工作为保证。