实验二参考示例
一.实验目的
1.理解并掌握类的常对象的定义及访问方法;
2.理解并掌握类的常成员函数的含义及使用方法
3.理解并掌握数组对象的含义及使用方法;
4.理解并掌握动态内存分配的原理和使用方法;
5.理解并掌握内存泄漏产生的原因及解决方法;
6.理解并掌握深复制和浅复制的区别和使用方法,深入理解自定义类的拷贝构造函数的必要性和意义;
7.理解并掌握类的继承和派生的方法和原理;
二. 实验内容
1.类的常对象的定义及访问方法
1.请根据要求,完成以下内容:
(1)定义一个class Point类,其中含有2个私有数据成员,分别是点的两个的坐标值:x,y;含有三个成员函数,分别是:int getX()(返回点的x坐标值),int getY()(返回点的y坐标值),double Dist()(计算两点间距离); 成员函数重载,分别为:分别为:double Dist()和double Dist()const, 并要求double Dist()const形式参数为常对象.
(2)主函数定义Point类的普通对象和常对象,以:Point myp1(1,1), myp2(4,5); Point p1(x1, y1), Point p2(x2, y2);进行测试(其中,myp1(1,1)和myp2(4,5)在主函数中定义为常对象,p1(x1,y1)和p2(x2,y2)定义为一般的对象,可调用默认值构造,也可自己给值),分别输出myp1(1,1), myp2(4,5)和p1(x1, y1)和p2(x2, y2)两点间距离。
给出以上程序的完整源代码,并粘贴运行结果截图。
#include <iostream>
#include <cmath>
using namespace std;
class Point {
public:
Point(int xx, int yy) { x = xx, y = yy; }
int getX()const;//必须声明常成员函数,否则常对象无法访问
int getY()const;//同上
double Dis(Point p);
double Dis(const Point p) const;
private:
int x;
int y;
};
inline int Point::getX()const {
return x;
}
inline int Point::getY()const {
return y;
}
double Point::Dis(Point p) {
int xx = x - p.getX();
int yy = y - p.getY();
return sqrt(xx*xx + yy*yy);
}
double Point::Dis(const Point p)const {
int xx = x - p.getX();
int yy = y - p.getY();
return sqrt(xx*xx + yy*yy);
}
int main() {
const Point myp1(1, 1);
const Point myp2(4, 5);
cout << "常对象(1,1) and (4,5) Dis: " << myp1.Dis(myp2) << endl;
cout << "下面开始构造一般对象" << endl;
int x1, y1, x2, y2;
cout << "x1 and y1:";
cin >> x1 >> y1;
cout << "x2 and y2:";
cin >> x2 >> y2;
Point p1(x1, y1),p2(x2, y2);
cout << "一般对象 Dis: " << p1.Dis(p2) << endl;
system("pause");
}
运行结果截图:
2. 数组对象的定义及使用方法
2. 请根据要求,完成以下内容:
(1)请定义一个score类:私有数据成员有:学号,姓名,平时成绩,期末考试成绩,总评成绩;成员函数有:类的构造函数score()(自定义其构造形式),count()(计算总评成绩:按总评成绩=平时*0.4,期末成绩*0.6),ShowScore()(输出各个私有数据成员);
(2)主函数要求以不带初始化的对象数组和带初始化的静态对象数组,分别构造3个对象,并分别输出每个对象的私有数据。
(3)输出格式如下:201701101 Li Hongmei 90 80 84。
(备注:输出顺序为:学号 姓名 平时成绩,期末成绩,总评成绩)
给出程序源代码,并粘贴运行结果截图。
#include <iostream>
#include <cmath>
#include <string>
using namespace std;
class Score {
public:
Score();
Score(string xID, string xname, double sc1, double sc2);
void count();
void showScore();
private:
string ID;
string name;
double daily_grade;//平时
double final_grade;//期末
double total_grade;//总评
};
Score::Score() {
this->Score::Score("null", "null", 0, 0);
}
Score::Score(string xID, string xname, double sc1, double sc2) {
ID = xID;
name = xname;
daily_grade = sc1;
final_grade = sc2;
}
void Score::count() {
total_grade = daily_grade*0.4 + final_grade*0.6;
}
void Score::showScore() {
cout << ID << " " << name
<< " " << daily_grade
<< " " << final_grade
<< " " << total_grade << endl;
}
int main() {
int i;
Score sc1[3];
cout << "不带初始化的对象数组" << endl;
for (i = 0; i<3; i++) {
sc1[i].count();
sc1[i].showScore();
}
cout << "\n带初始化的静态对象数组" << endl;
static Score sc2[3] = {
Score("201700001","Tomt",
91,97),
Score("201700002","Jack",
83,95),
Score("201700003","Mary",
100,97),
};
for (i = 0; i<3; i++) {
sc2[i].count();
sc2[i].showScore();
}
system("pause");
}
运行结果截图:
3.请用动态分配数组实现上面的题目,给出源代码,并粘贴运行结果截图.
静态数组:(用指针数组来new)
静态数组:
#include <iostream>
#include <cmath>
#include <string>
using namespace std;
class Score {
public:
Score();
Score(string xID, string xname, double sc1, double sc2);
void count();
void showScore();
private:
string ID;
string name;
double daily_grade;//平时
double final_grade;//期末
double total_grade;//总评
};
Score::Score() {
this->Score::Score("null", "null", 0, 0);
}
Score::Score(string xID, string xname, double sc1, double sc2) {
ID = xID;
name = xname;
daily_grade = sc1;
final_grade = sc2;
}
void Score::count() {
total_grade = daily_grade*0.4 + final_grade*0.6;
}
void Score::showScore() {
cout << ID << " " << name
<< " " << daily_grade
<< " " << final_grade
<< " " << total_grade << endl;
}
int main() {
int i;
Score *sc1[3];//也可以仿照教材P223页的例题6-17,设一个指针指向在堆中创建的有3个分量的一维数组
cout << "不带初始化的对象数组" << endl;
for (i = 0; i<3; i++) {
sc1[i] = new Score();
sc1[i]->count();
sc1[i]->showScore();
}
cout << "\n带初始化的静态对象数组" << endl;
static Score *sc2[3]= {
new Score("201700001","Tomt",
91,97),
new Score("201700002","Jack",
83,95),
new Score("201700003","Mary",
100,97),
};
for (i = 0; i<3; i++) {
sc2[i]->count();
sc2[i]->showScore();
}
//释放资源
delete [] *sc1;
delete [] *sc2;
system("pause");
}
运行结果截图:
动态数组:(直接new)
#include <iostream>
#include <cmath>
#include <string>
using namespace std;
class Score {
public:
Score();
Score(string xID, string xname, double sc1, double sc2);
void count();
void showScore();
private:
string ID;
string name;
double daily_grade;//平时
double final_grade;//期末
double total_grade;//总评
};
Score::Score() {
this->Score::Score("null", "null", 0, 0);
}
Score::Score(string xID, string xname, double sc1, double sc2) {
ID = xID;
name = xname;
daily_grade = sc1;
final_grade = sc2;
}
void Score::count() {
total_grade = daily_grade * 0.4 + final_grade * 0.6;
}
void Score::showScore()
{
std::cout << ID << " " << name
<< " " << daily_grade
<< " " << final_grade
<< " " << total_grade << endl;
}
int main() {
Score* p = new Score[3]{ Score("201700001","Tomt",
91,97),Score("201700002","Jack",
83,95),Score("201700003","Mary",
100,97), };//也可以仿照教材P223页的例题6-17,设一个指针指向在堆中创建的有3个分量的一维数组
cout << "对象数组: " << endl;
for (int i = 0; i < 3; i++) {
p[i].count();
p[i].showScore();
}
cout << "\n带初始化的静态对象数组" << endl;
//释放资源
delete[] p;
system("pause");
}
运行结果截图:
4.关于深复制和浅复制
1.请分析下列程序的运行结果,回答问题:
#include <iostream>
#include <cstring>
using namespace std;
class CA
{public:
CA(int b,char* cstr)
{a=b;
str=new char[b];
strcpy(str,cstr);
cout << "构造成功"<<endl;}
void Show()
{cout<<"构造的字符串为:"<<str<<endl;
cout<<"构造的字符串地址为:"<<(int)str<<endl;
}
~CA(){delete str;}
private:
int a;
char *str;};
int main()
{CA A(10,"Hello!");
A.Show();
CA B=A;
B.Show();
return 0; }
(1)根据程序的运行结果,推出系统默认的拷贝构造函数是如何编写的?
答:(更正答案)
(2)上述程序实现的是浅复制还是深复制?简述两者的区别,并分析对象A的str和对象B的str内存地址相同的原因。
答:因为:原程序没有构造自己的拷贝构造函数,当原程序执行语句:CA B=A;时,调用的是编译器提供的默认的拷贝构造函数,编译器实现的是浅拷贝。即:编译器执行的浅拷贝的构造函数是: CA(const CA& A){a=A.a;str=A.str; },编译器在构造B对象时,传递给B对象str的是A对象str的地址,所以,二者的地址相同。
(3)浅复制在哪种情况下会带来哪些问题?应该如果解决?
答:同(2)。
(4)上述程序会造成内存泄漏吗?如果存在,请分析内存泄漏会在哪一步发生?
答:
(5)请给出自定义深复制构造函数后的完整程序源代码,并粘贴运行结果截图。
答:源代码:
#include <iostream>
#include <cstring>
using namespace std;
class CA
{public:
CA(int b,char* cstr)
{a=b;
str=new char[b];
strcpy(str,cstr);
cout << "构造成功"<<endl;}
CA(const CA& C)
{a=C.a;str=new char[a]; //深拷贝
if(str!=0)
{strcpy(str,C.str);
cout << "拷贝构造成功"<<endl;}}
void Show()
{cout<<"构造的字符串为:"<<str<<endl;
cout<<"构造的字符串地址为:"<<(int)str<<endl;
}
~CA(){delete str;}
private:
int a;
char *str;};
int main()
{CA A(10,"Hello!");
A.Show();
CA B=A;
B.Show();
return 0;}
5.关于拷贝复制构造函数
5.请阅读下列程序,然后回答问题:
#include<iostream>
using namespace std;
class Rect
{
public:
Rect()
{
count++;
}
~Rect()
{
count--;
}
static int getCount()
{
return count;
}
private:
int width;
int height;
static int count;
};
int Rect::count=0;
int main()
{
Rect rect1;
cout<<"The count of Rect:"<<Rect::getCount()<<endl;
Rect rect2(rect1);
cout<<"The count of Rect:"<<Rect::getCount()<<endl;
return 0;
}
(1) 上述程序的本意是统计矩形的个数,但从实际运行结果看,并没有达到这个目的,请分析原因。
答:1)原程序没有定义自己的拷贝构造函数,主函数在执行语句:Rect rect2(rect1);时,调用的系统默认的拷贝构造函数,而系统默认的拷贝构造函数,只是对非静态成员进行简单的复制,并不会对统计对象个数的静态成员counter做任何处理,所以,counter仍然保持构造拷贝复制对象之前的值,没有随着对象的增加而增加,没有达到预期的目的。
(2)修改上述程序,使其能够正确输出已经创建的矩形数量。请给出源代码完整程序,并粘贴运行结果截图。
答:
#include<iostream>
using namespace std;
class Rect
{
public:
Rect()
{
count++;
}
Rect(const Rect& r)
{
width=r.width;
height=r.height;
count++;
}
~Rect()
{
count--;
}
static int getCount()
{
return count;
}
private:
int width;
int height;
static int count;
};
int Rect::count=0;
int main()
{
Rect rect1;
cout<<"The count of Rect:"<<Rect::getCount()<<endl;
Rect rect2(rect1);
cout<<"The count of Rect:"<<Rect::getCount()<<endl;
return 0;
}
运行结果截图:
6.关于类的继承与派生
6.下面的程序已经定义了一个基类Shape,请按要求完成以下内容:
#include <iostream>
using namespace std;
class Shape
{
public:
Shape(){}
~Shape(){}
virtual float getArea() { return -1; }
};
class Circle : public Shape
//定义Circle类
{
};
class Rectangle : public Shape
//定义Rectangle类
{
}
class Square : public Rectangle
//定义Aquare类
{
};
int main()
{
Shape * sp;
sp = new Circle(5);
cout << "The area of the Circle is " << sp->getArea () << endl;
delete sp;
sp = new Rectangle(4, 6);
cout << "The area of the Rectangle is " << sp->getArea() << endl;
delete sp;
sp = new Square(5);
cout << "The area of the Square is " << sp->getArea() << endl;
delete sp;
return 0;
}
(1)在此基础上派生出Rectangle和Circle,二者都有getArea()函数计算对象的面积;
(2)使用Rectangle类创建一个派生类Square;
请给出上述程序的完整源程序,并粘贴运行结果截图。
答:
#include <iostream>
using namespace std;
class Shape
{
public:
Shape(){}
~Shape(){}
virtual float getArea() { return -1; }
};
class Circle : public Shape
{
public:
Circle(float radius):itsRadius(radius){}
~Circle(){}
float getArea() { return 3.14 * itsRadius * itsRadius; }
private:
float itsRadius;
};
class Rectangle : public Shape
{
public:
Rectangle(float len, float width): itsLength(len), itsWidth(width){};
~Rectangle(){};
virtual float getArea() { return itsLength * itsWidth; }
virtual float getLength() { return itsLength; }
virtual float getWidth() { return itsWidth; }
private:
float itsWidth;
float itsLength;
};
class Square : public Rectangle
{
public:
Square(float len);
~Square(){}
};
Square::Square(float len):
Rectangle(len, len)
{
}
int main()
{
Shape * sp;
sp = new Circle(5);
cout << "The area of the Circle is " << sp->getArea () << endl;
delete sp;
sp = new Rectangle(4, 6);
cout << "The area of the Rectangle is " << sp->getArea() << endl;
delete sp;
sp = new Square(5);
cout << "The area of the Square is " << sp->getArea() << endl;
delete sp;
return 0;
}
运行结果截图:
7.关于继承类成员函数的调用顺序
7.下列程序已经定义了一个哺乳动物类Mammal,再由此派生出狗类Dog,定义一个Dog类的对象,观察基类和派生类的构造函数与析构函数的调用顺序。根据下面程序代码和运行结果截图,请将哺乳动物类Mammal和其派生的狗类Dog的构造函数补充完整,给出程序的完整源代码,并粘贴运行结构截图。
#include <iostream>
using namespace std;
enum myColor {
BLACK, WHITE
};
class Mammal {
public:
// constructors
Mammal();
~Mammal();
//accessors
int getAge() const {
return itsAge;
}
void setAge(int age) {
itsAge = age;
}
int getWeight() const {
return itsWeight;
}
void setWeight(int weight) {
itsWeight = weight;
}
//Other methods
void speak() const {
cout << "Mammal sound!\n";
}
protected:
int itsAge;
int itsWeight;
};
class Dog: public Mammal {
public:
Dog();
~Dog();
myColor getColor() const {
return itsColor;
}
void setColor(myColor color) {
itsColor = color;
}
void wagTail() {
cout << "Tail wagging...\n";
}
private:
myColor itsColor;
};
//下面请将Mammal类的构造函数补充完整
Mammal::Mammal() :
{ }
Mammal::~Mammal() {
cout << "Mammal destructor...\n";
}
//下面请将Dog类的构造函数补充完整
Dog::Dog() : { }
Dog::~Dog() {
cout << "Dog destructor...\n";
}
int main() {
Dog jack;
jack.speak();
jack.wagTail();
cout << " jack is " << jack.getAge() << " years old\n";
return 0;
}
请给出完整源代码和运行结果截图。
答:
#include <iostream>
using namespace std;
enum myColor {
BLACK, WHITE
};
class Mammal {
public:
// constructors
Mammal();
~Mammal();
//accessors
int getAge() const {
return itsAge;
}
void setAge(int age) {
itsAge = age;
}
int getWeight() const {
return itsWeight;
}
void setWeight(int weight) {
itsWeight = weight;
}
//Other methods
void speak() const {
cout << "Mammal sound!\n";
}
protected:
int itsAge;
int itsWeight;
};
class Dog: public Mammal {
public:
Dog();
~Dog();
myColor getColor() const {
return itsColor;
}
void setColor(myColor color) {
itsColor = color;
}
void wagTail() {
cout << "Tail wagging...\n";
}
private:
myColor itsColor;
};
Mammal::Mammal() :
itsAge(1), itsWeight(5) {
cout << "Mammal constructor...\n";
}
Mammal::~Mammal() {
cout << "Mammal destructor...\n";
}
Dog::Dog() :
itsColor(WHITE) {
cout << "Dog constructor...\n";
}
Dog::~Dog() {
cout << "Dog destructor...\n";
}
int main() {
Dog jack;
jack.speak();
jack.wagTail();
cout << " jack is " << jack.getAge() << " years old\n";
return 0;
}
运行结果截图:
作业2-答案
1.下列程序期望的运行结果为:
t1.m_num:1
t2.m_num:1
(1)运行下列程序,给出运行结果截图;
(2)分析程序没有得到预期结果的原因;
(3)尝试给出一种以上解决方案,使程序能够输出期望值;
(4)由上述的分析和尝试,可以得出哪些结论;
#include <iostream>
using namespace std;
class Test
{
public:
int m_num=1;
};
Test* getObj1()
{
Test t1,*p1=&t1;
return p1;
}
Test& getObj2()
{
Test t2, & p2 = t2;
return p2;
}
int main()
{
Test* t1 = getObj1();
Test& t2 = getObj2();
cout << "t1.m_num: " << (*t1).m_num << endl;
cout << "t2.m_num: " << t2.m_num << endl;
return 0;
};
答:
(1)运行结果截图:
(2)原因:
函数getObj1()和getObj2()返回的均是指向各自函数中创建的局部对象的地址(引用的底层实现为指针)。局部对象创建于栈中,随着函数调用的结束,栈空间被释放,即:该空间会被系统标记为可用,可被系统分配给其它进程使用。因此,主函数中的指针变量t1和引用变量t2,就指向了一个已经被标记为可用的内存空间,该空间会被系统赋给一个随机值。所以,程序得不到期望的运行结果,而且每次运行的结果也会不同。
(3)解决方案:
① 将函数中的局部对象,改为静态局部对象,返回静态局部对象的指针和引用;
Test* getObj1()
{
static Test t1; //设置为静态局部变量
Test * p1 = &t1;
return p1;
}
Test& getObj2()
{
static Test t2; //设置为静态局部变量
Test & p2 = t2;
return p2;
}
运行结果截图:
② 将函数中的局部对象,改为全局对象,返回全局对象的指针和引用:
class Test
{
public:
int m_num = 1;
};
Test t1; //设置为全局变量
Test t2; //设置为全局变量
运行结果截图:
③ 在堆中创建对象,返回堆对象的指针和引用(注意在调用函数中,需要销毁指针所指向的堆空间,否则,会造成内存泄漏;调用函数中,引用对象所引用的堆空间,编译器会自动调用对象的析构函数进行析构):
Test* getObj1()
{
Test* p1 = new Test; //在堆中申请空间
return p1;
}
Test& getObj2()
{
Test *p2 = new Test; //在堆中申请空间
Test & p3 = *p2;
return p3;
}
运行结果截图:
④用vector实现
(4)结论:
① 函数返回值为数值时,可以返回函数内部定义的普通局部变量(也称为自动变量);
② 函数返回值为指针或引用时,确定是否允许返回局部指针或引用变量,不在于该指针或引用变量是否为局部指针或局部引用变量,而在于该指针或引用所指向或所引用的对象的生命周期,是否长于其所在的函数;若该指针或引用所指向或所引用的对象的生命周期,长于其所在的函数,即为合法;
2.下列程序期望的运行结果为:1000
(1)程序能否编译通过?若不能通过,说明原因。
(2)修改程序,使其能够输出期望值1000。
(3)在主函数return 0;语句之前,a的值是多少。
#include <iostream>
using namespace std;
int& done()
{
static int a = 10;
return a;
}
int main()
{
int& ret = done();
done () = 1000;
cout << a << endl;
return 0;
}
答:
(1)不能。done()函数中的变量a,虽然是静态变量,其生命周期大于其所在的函数;但是a是局部变量,在其所在的函数外部,不可见,不能被访问。
(2)修改后的程序如下:
#include <iostream>
using namespace std;
int& done()
{
static int a = 10;
return a;
}
int main()
{
int& ret = done();
done () = 1000;
//cout << a << endl;
cout << done() << endl;
return 0;
}
(3)主函数中的“done () = 1000; ”语句,实际是给a赋值,所以a的值是1000。
3.下列关于常引用的程序:
(1)指出下列程序中的错误。
(2)说明常引用的实质作用及其作为函数形参的实质作用。
void test()
{
int a = 100;
const int& aRef = a;
aRef = 200;
a = 100;
cout << "a:" << a << endl;
cout << "aRef:" << aRef << endl;
}
答:(1)
void test()
{
int a = 100;
const int& aRef = a;
//aRef = 200; 常引用不能修改其值。
a = 100;
cout << "a:" << a << endl;
cout << "aRef:" << aRef << endl;
}
(2)常引用作用的实质是:限制了引用变量的权限,即:不能通过常引用变量改变被引用变量的值;采用常引用作为函数形参,实质是:在函数中不能对实参进行改变。
4.下列关于右值引用和移动构造函数的程序:
(1)分别给出允许编译器优化和禁止编译器优化的程序运行结果,并分析结果原因。
(2)说明C++11引入右值引用和移动构造函数的目的。
#include <iostream>
struct Foo {
~Foo() { std::cout << "destruction" << std::endl; }
};
Foo FooFactory() {
return Foo();
}
int main() {
std::cout << "before copy constructor..." << std::endl;
Foo foo1 = FooFactory();
std::cout << "after copy constructor..." << std::endl << std::endl;
Foo&& foo2 = FooFactory();
std::cout << "life time ends!" << std::endl << std::endl;
return 0;
}
答:(1)
①
VS2022编译器优化后的运行结果:
未优化的运行结果(之一:clang编译器)
原因分析(其它运行结果,以本地编译器为准,自行分析):
此程序可以从析构函数的执行次数,来判断复制构造函数和移动构造函数是否被调用执行。设置断点,根据程序单步运行过程,具体分析见程序的注释部分:
#include <iostream>
struct Foo {
~Foo() { std::cout << "destruction" << std::endl; }
};
Foo FooFactory() {
return Foo(); //VS022:优化前,默认复制构造函数被调用,在主函数中被复制构造了一个临时对象,之后Foo()函数生成的匿名对象销毁,析构函数被调用一次;优化后,默认复制构造没有被调用,析构函数也没有被调用,而是主函数直接按照返回值,由主函数调用默认构造函数构造了fool对象。
}
int main() {
std::cout << "before copy constructor..." << std::endl;
Foo foo1 = FooFactory();//VS2022:优化前,默认复制构造函数被调用,将临时对象复制一份赋给fool对象,之后临时对象销毁,析构函数被调用一次;优化后,默认复制构造函数没有被调用,析构函数也没有被调用,因为FooFactory()函数返回值并没有生成临时对象,而是FooFactory()函数直接将返回值放到了主函数构造对象的地址处,由主函数调用默认构造函数构造了fool对象。
std::cout << "after copy constructor..." << std::endl << std::endl;
Foo&& foo2 = FooFactory(); //VS2022:优化前,因为foo2为右值引用对象,主函数会自动调用默认移动构造函数,按照FooFactory()函数返回的临时对象,来构造foo2对象,之后,临时对象销毁,析构函数被调用一次,默认复制构造函数没有被调用,调用的是默认移动构造函数;优化后,默认复制构造函数没有被调用,默认移动构造函数也没有被调用,析构函数也没有被调用,因为FooFactory()函数返回值并没有生成临时对象,而是FooFactory()函数直接将返回值放到了主函数对象foo2的地址处,由主函数调用默认构造函数构造了foo2对象。
std::cout << "life time ends!" << std::endl << std::endl;
return 0;
}
(2)右值引用是对将亡值的引用,目的是获取匿名对象(将亡值)的地址,为能够应用移动复制做准备。
移动构造函数在构造右值引用对象时被调用,构造时不再申请新的空间,而是进行地址赋值,进行浅复制,进而节省了复制的时间,提高了复制效率。