C++面向对象程序设计_期中机考试题

前言

由于对C++语法知识的疏忽,导致这次期中机考一团糟,特以此这篇博客记录一下考试内容以及相关考点,加强记忆以及警示自己


考试大纲

读程序写结果题

  • 对象的创建与析构
  • 数组对象与静态变量
  • 函数参数:引用传递、值传递
  • 函数返回:对象、引用
  • 运算符重载:赋值、算数、友元流

编程题

  • 时间类
  • 静态数组
  • 动态数组
  • 链表

原题解析

一、 读程序,写结果 (每小题5分,共60分)

注:所有程序头部,均有:

#include <iostream>
using namespace std;
1、最简单的生死过程
class A
{
int x;
public:
A(int x = 3) :x(x)
{
cout << x << endl;
}
virtual ~A()
{
cout << -x << endl;
}
};
int main()
{
A a1, a2(4);
return 0;
}
输出:
3
4
-4
-3
  • 创建A对象a1时,调用构造函数,输出缺省值3
  • 创建A对象a2时,调用构造函数,输出构造的值4
  • 以栈的形式,先析构对象a2输出-4
  • 再析构对象a1输出-3
2、构造、拷贝构造的生死过程
class A
{
int x;
public:
A(const int x) :x(x)
{
cout << x << endl;
}
A(const A& obj) :x(obj.x + 1)
{
cout << x << endl;
}
virtual ~A()
{
cout << -x << endl;
}
};
int main()
{
A* p1 = new A(3);
A* p2 = new A(*p1);
delete p1;
delete p2;
return 0;
}
输出:
3
4
-3
-4
  • 动态申请一个指向A对象的指针p1,调用构造函数,创建一个值为3的对象A输出3
  • 动态申请一个指向A对象的指针p2,调用复制构造函数,被复制的对象为p1所指的对象,创建一个新的对象A输出4
  • 手动删除p1指针,输出-3
  • 手动删除p2指针,输出-4
3、嵌套类的生过程
class A1
{
int x1;
public:
A1(int x1) :x1(x1) { cout << x1 << endl; }
};
class A2
{
int x2;
public:
A2(int x2) :x2(x2) { cout << x2 << endl; }
};
class A
{
A1 a1;
A2 a2;
int x;
public:
A(int x1, int x2) :a1(x1), a2(x2), x(x1 + x2)
{
cout << x << endl;
}
};
int main()
{
A a(1, 2);
A* p = new A(4, 5);
return 0;
}
输出:
1
2
3
4
5
9
  • 创建A对象a时,由于A类包含了两个对象类数据成员,故继续创建两个对象,分别为A1A2,按照列表顺序依次输出1,2,3
  • 创建了一个指向A对象的指针p,创建了一个A对象,同上,依次输出4,5,9
4、嵌套类的生死过程
class A1
{
int x1;
public:
A1(int x1) :x1(x1) { cout << x1 << endl; }
virtual ~A1() { cout << -x1 << endl; }
};
class A
{
A1* p1;
int x;
public:
A(int x1, int x) :x(x)
{
p1 = new A1(x1);
}
virtual ~A()
{
delete p1; cout << -x << endl;
}
};
int main()
{
A a(1, 2);
A* p = new A(4, 5);
delete p;
return 0;
}
输出:
1
4
-4
-5
-1
-2
  • 创建A对象a,调用A的构造函数,动态申请A1对象,输出1,a中的x存的是2
  • 动态申请指向A对象的指针p,创建A对象,输出4,新创建的A对象中x存的是5
  • 手动删除p指针,析构A对象时,首先删除内部的p1指针,调用A1中的析构函数,输出-4,再输出-5
  • 以栈的形式,自动释放a的空间,同样调用A的析构函数,首先删除内部的p1指针,调用A1中的析构函数,输出-1输出-2
5、数组对象的生死过程
class A
{
int x;
public:
A(int x = 3) :x(x)
{
cout << x << endl;
}
virtual ~A()
{
cout << -x << endl;
}
};
int main()
{
A a[] = { A(1),A(2) };
A* p[] = { new A(3),new A(4) };
return 0;
}
输出:
1
2
3
4
-2
-1
  • 数组创建对象是按照从左到右的顺序,因此先输出1,2
  • 然后动态申请新指针数组,元素是两个指向新申请出来的A对象的指针,于是输出3,4
  • 最后析构普通数组创建的两个对象,分别输出-2,-1
6、静态变量
class A
{
int x;
static int n;
public:
A(int x) :x(x)
{
n += x; cout << n << endl;
}
virtual ~A()
{
n -= x; cout << n << endl;
}
};
int A::n = 10;
int main()
{
A a[] = { A(1),A(2),A(3) };
return 0;
}
输出:
11
13
16
13
11
10
  • 静态变量无非就是全局变量,感觉唯一考点应该是初始化才对
  • 创建普通数组时,依次+1输出11+2输出13+3输出16
  • 析构数组中的对象时,依次-3输出13-2输出11-1输出10
7、函数参数引用、值传递(拷贝构造)
class A
{
int x;
public:
A(int x) :x(x) { }
A(A& obj) :x(obj.x + 1)
{
cout << x << endl;
}
virtual ~A()
{
cout << -x << endl;
}
};
void f(A& a1, A a2)
{
A a3(a1);
}
int main()
{
A a1(1), a2(2);
f(a1, a2);
return 0;
}
输出:
3
2
-2
-3
-2
-1
  • 首先创建的两个对象没有输出操作
  • 接下来的f()函数才是重头!
  • 引用传递a1,这没什么,但是值传递了a2,就要首先调用复制构造函数复制一个a2对象,于是输出3
  • 接下来执行函数语句,a3复制构造a1输出2
  • 函数执行完毕,按照栈的形式,先析构a3输出-2,再析构复制值传递的临时对象,输出-3,函数彻底结束
  • 最后回到主函数,依次析构a2对象与a1对象,依次输出-2,-1
8、函数返回对象
class A
{
int x;
public:
A(int x = 0) :x(x) { cout << x << endl; }
A(const A& obj) :x(obj.x + 1) { cout << x << endl; }
virtual ~A() { cout << -x << endl; }
};
A f(A a1)
{
return a1;
}
int main()
{
A a1;
A a2 = f(a1);
return 0;
}
输出:
0
1
2
-1
-2
0
  • 首先创建对象a1输出缺省值0
  • 然后到了关键的赋值语句,首先调用f()函数,进入f()函数,由于是值传递,那么首先调用复制构造函数复制a1对象输出1
  • 接着!由于f函数返回类型为对象,故还要重新调用复制构造函数创建一个临时对象复制当前函数中的的a1对象(此时的a1对象其实是已经被复制过一次了),输出2
  • 函数f()执行结束,析构掉由于值传递复制构造出来的函数f()中的a1对象,输出-1
  • 而主函数中的a2对象其实是接收了f()函数return时创建出来的临时对象,即赋值语句(其实是复制)
  • 主函数执行结束,开始依次执行析构函数,首先析构a2输出-2,然后析构a1输出0

这道题与第11题的联系,我先说一下,这两段话可以先看完了11题的解析后再回来看

  • 而第8题返回对象时却会调用复制构造函数,个人认为是因为没有在return之前定义好局部变量,编译器无法识别出返回的对象是什么,于是就需要按照老式的规则,返回时先拷贝一个临时对象进行返回,使用完后再析构掉这个临时对象
  • 第8题中虽然因为值传递复制了一个临时对象用来接收a1对象,但是编译器不会把它作为返回对象,而是仍然需要按照老式规则重新复制一份来返回。也就是说,对于返回对象函数,参数中的值传递复制出来的对象与函数中另外创建出来的对象不同,前者无法触发编译器优化,后者可以触发编译器优化
9、函数返回对象引用
class A
{
int x;
public:
A(const int x) :x(x) { cout << x << endl; }
A(const A& obj) :x(obj.x + 1) { cout << x << endl; }
virtual ~A() { cout << -x << endl; }
};
A& f(A a1)
{
A* p = new A(a1);
return *p;
}
int main()
{
A a1(1);
A& a2 = f(a1);
delete& a2;
return 0;
}
输出:
1
2
3
-2
-3
-1
  • 首先创建了一个a1对象,输出1
  • 接着调用f()函数,由于是值传递,故首先调用复制构造函数复制一份a1f()函数中,于是输出2
  • 在函数f()中,首先动态申请了一个指向A对象的指针p,并且调用复制构造函数,复制一份a1对象(同理,这个a1对象已经是被复制过后的了),于是输出3
  • 接着函数返回这个申请出来的对象的引用,就是给主函数中的a2
  • f()函数执行完之后,需要析构掉值传递复制的临时对象a1,于是输出-2
  • 回到主函数,手动释放a2内存,输出-3
  • 自动析构掉a1对象,输出-1
10、赋值运算+拷贝构造
class A
{
int* p;
public:
A(const int x = 0)
{
p = new int; *p = x;
}
A(const A& obj)
{
p = new int; *p = (*obj.p) + 1;
}
A& operator=(A& obj)
{
if (this == &obj) return *this;
*p = *obj.p + 10;
return *this;
}
virtual ~A()
{
cout << -(*p) << endl; delete p;
}
};
int main()
{
A a1(1);
A a2(a1), a3;
a3 = a2 = a1;
return 0;
}
输出:
-21
-11
-1

说在最前面,在考试之前被问了连加运算的运算顺序,我一直以为是从右往左的,但其实是从左往右的,于是考场上在遇到连等时,脑子也没动就以为也是从左到右执行的,还是学艺不精啊!赋值运算符应该是从右往左的!这与平时只有一个赋值运算符逻辑一致!

  • 首先创建了a1对象,其中的*p为1
  • 接着创建了a2对象,复制了a1,则a2中的*p为2
  • 然后默认构造了a3,其中的*p为缺省值0
  • 在连续赋值运算中,首先执行a2 = a1,则a2中的*p变为了11,然后执行a3 = a2,则a3中的*p变为了21
  • 最后析构时,先释放a3输出-21,再释放a2输出-11,最后释放a1输出-1
11、算术运算+赋值运算

这道题比较有意思,我重点说一下

  • 这道题原题(贴在下面了)

原题代码

class A
{
int* p;
public:
A(int x = 0)
{
p = new int; *p = x;
}
A(A& obj)
{
p = new int; *p = (*obj.p) + 1;
}
A operator+(int x)
{
A ans;
*ans.p = *p + x;
cout << *ans.p << endl;
return ans;
}
A& operator=(const A& obj)
{
if (this == &obj) return *this;
*p = *obj.p + 2;
cout << *p << endl;
return *this;
}
virtual ~A()
{
cout << -(*p) << endl; delete p;
}
};
void main()
{
A a1(1), a2;
a2 = a1 + 3;
}
标答是(老师用的编译器VC 6.0):
4
-4
7
-5
-7
-1
但是在我的本地编译器(VS2022 & Dec C++)输出:
4
6
-4
-6
-1

我先说一下老师的答案的逻辑

  • 首先创建了a1a2两个对象,没有输出
  • 然后进入运算语句,首先看加法运算
  • 首先创建了一个局部变量ans,然后输出4,最后返回一个对象,由于是返回对象,因此需要先拷贝一份临时对象tmp来返回,注意,由于复制构造函数会对内部变量+1,因此此时的tmp对象中的变量值为5
  • 返回后析构加法重载函数中的临时对象ans输出-4,函数结束
  • 接下来进入赋值运算符的重载函数,即a2.operator=(tmp)输出7
  • 接着就要析构掉这个tmp对象了,于是输出-5
  • 最后析构掉主函数中的a2输出-7,析构a1输出-1

而我的答案的逻辑

  • 同样首先创建了a1a2两个对象,没有输出
  • 然后进入加法运算符的重载函数,创建了一个临时对象ans,输出4
  • 函数直接返回了这个临时对象ans,函数结束
  • 接下来同上进入赋值运算符的重载函数,即a2.operator=(tmp)输出6
  • 接着就要析构掉这个临时对象ans了,于是输出-4
  • 最后析构掉主函数中的a2输出-6,析构a1输出-1

总的来说就是一句话,没有在加法重载函数结束后,再次复制一个临时对象来拷贝ans,而是直接返回了这个局部变量ans,具体原因我写在这道题的最后了。其实我在单步调试的时候就发现了函数中执行语句的顺序有点奇怪了,因为在创建了ans这个对象之后立刻就跳到了return语句

这其实是编译器优化了,以下是我结合ChatGPT整合出来的结论
问:

A operator+(int x)
{
A ans;
*ans.p = *p + x;
cout << *ans.p << endl;
return ans;
}

这个函数,在创建A对象时,会调用一次空构造函数,但是在返回这个对象时,理论上应该是再拷贝一份临时变量再返回的呀,但是我在本地的编译器上跑出来的结果却是没有再次拷贝了,而是直接返回了ans这个局部变量对象,这是为什么?
答:
这是因为编译器进行了优化,使用了返回值优化(Return Value Optimization,RVO)和命名返回值优化(Named Return Value Optimization,NRVO)
返回值优化是一种编译器优化技术,其目的是消除返回值的拷贝构造函数和析构函数的调用,减少程序的运行时间和空间开销。在该代码中,编译器可以将ans对象直接放在函数返回值的位置,避免了拷贝构造函数的调用。
命名返回值优化与返回值优化类似,不同之处在于编译器会将函数返回值的对象命名为一个局部变量,从而避免了临时对象的创建和拷贝构造函数的调用。因此,在该代码中,编译器可能会将ans对象命名为返回值,并将其直接返回,避免了拷贝构造函数的调用。

问:
这个优化是从什么时候的什么版本开始的?

答:
返回值优化(RVO)C++标准的一部分,自C++98就已经存在了。命名返回值优化(NRVO)C++11引入的新特性,它是对返回值优化的进一步优化。因此,只要使用的是符合C++标准的编译器,就应该支持返回值优化和命名返回值优化。不同的编译器对RVONRVO的支持程度可能会有所不同,但是都应该会尝试进行这种优化。

在和老师的一番沟通后,老师说这道题不应该这么出的,应该修改一下,就没那么多事了,hhh

  • 第一点:重载加号的时候干脆返回一个全局变量(虽然会浪费内存,但是没办法)
  • 第二点:下述程序
    A& operator=(const A& obj)
    {
    if (this == &obj) return *this;
    *p = *obj.p + 2;
    cout << *p << endl;
    return *this;
    }
    其实可以写成
    void operator=(const A& obj)
    {
    if (this == &obj) return;
    *p = *obj.p + 2;
    cout << *p << endl;
    }
    因为本就是对当前对象的修改

改进后题目代码

class A
{
int* p;
public:
A(int x = 0)
{
p = new int; *p = x;
}
A(const A& obj)
{
p = new int; *p = (*obj.p) + 1;
}
A& operator+(int x)
{
A* pAns = new A;
*(pAns->p) = *p + x;
cout << *(pAns->p) << endl;
return *pAns;
}
A& operator=(const A& obj)
{
if (this == &obj) return *this;
*p = *obj.p + 2;
cout << *p << endl;
return *this;
}
virtual ~A()
{
cout << -(*p) << endl; delete p;
}
};
void main()
{
A a1(1), a2;
a2 = a1 + 3;
}
输出:
4
6
-6
-1
  • 这个输出的逻辑就很简单了,首先是加法重载中输出4
  • 然后在赋值重载中输出6
  • 最后析构a2输出-6,析构a1输出-1
  • 只是加法重载中申请的出来的空间,即pAns所指的空间被永久的占用了
12、关系运算+友员函数
class A
{
int* p;
public:
A(int x = 0)
{
p = new int; *p = x;
}
bool operator<(A& obj)
{
return S(*this) < S(obj);
}
virtual ~A()
{
cout << S(*this) << endl; delete p;
}
friend int S(A& obj)
{
int x = *obj.p;
int s = 0;
while (x > 0)
s += x % 10, x = x / 10;
return s;
}
};
int main()
{
A a1(531), a2(246);
if (a1 < a2)
cout << S(a1) << endl;
else
cout << S(a2) << endl;
return 0;
}
输出:
9
12
9
  • 首先创建了两个对象a1a2,其中的*p分别为531,246
  • 比较运算符计算的是其中数字的数位和,发现a1 < a2,于是输出a1的数位之和9
  • 最后依次析构a2a1依次输出12,9

二、编程(每小题10分,共40分)

注:所有已知代码,不得做任何修改。

1、时间类(成员函数+友员输出)

已知Time类的数据成员定义和部分成员函数定义。要求补充相关函数(Add函数和输出运算重载)的定义,使之能满足main函数的功能需求,输出23:59:20:0:3

class Time
{
int h, m, s;
public:
Time(int h, int m, int s) :h(h), m(m), s(s) { }
};
void main()
{
Time t(23, 59, 2);
cout << t; // 输出23:59:2
t.Add(61);
cout << t; // 输出0:0:3
}

完整程序为:

  • 完善一下友元输出流
  • 添加一下成员函数Add即可
#include <iostream>
using namespace std;
class Time
{
int h, m, s;
public:
Time(int h, int m, int s) :h(h), m(m), s(s) { }
void Add(int delta)
{
s += delta;
m += s / 60; s %= 60;
h += m / 60; m %= 60;
h %= 24;
}
friend ostream& operator<<(ostream& out, Time& t)
{
return out << t.h << ":" << t.m << ":" << t.s << endl;
}
};
void main()
{
Time t(23, 59, 2);
cout << t; // 输出23:59:2
t.Add(61);
cout << t; // 输出0:0:3
}
2、静态数组(赋值、加法重载)

已知FixedArray类的数据成员定义和部分成员函数定义。要求补充相关函数(赋值、加法重载)的定义,使之能满足main函数的功能需求,输出2 4 6 8 10

const int N = 5;
class FixedArray
{
int* a;
public:
FixedArray()
{
a = new int[N];
for (int i = 0; i < N; i++)
a[i] = i + 1;
}
void Output()
{
for (int i = 0; i < N; i++)
cout << a[i] << " ";
cout << endl;
}
};
void main()
{
FixedArray a, b, c;
c = a + b;
c.Output(); // 输出2 4 6 8 10
}

完整程序为:

  • 完善一下加法运算符的重载
    • 值得注意的是,ans.a[i] = a[i] + obj.a[i];是标答,而我在考场上写成了ans.a[i] += obj.a[i]属于是脑子抽了,被构造函数误导了!😢
    • ans[i]ans.a[i]是等价的,其他地方涉及到了下标运算符同理
  • 完善一下赋值运算符的重载
    • 注意点有两个,一个是if (this == &obj) return *this;是为了防止自己给自己赋值,导致删除了自己的指针
    • 还有一个就是赋值运算符的返回值一定是引用!!!考场上漏掉了!😢
#include <iostream>
using namespace std;
const int N = 5;
class FixedArray
{
int* a;
public:
FixedArray()
{
a = new int[N];
for (int i = 0; i < N; i++)
a[i] = i + 1;
}
void Output()
{
for (int i = 0; i < N; i++)
cout << a[i] << " ";
cout << endl;
}
FixedArray& operator=(const FixedArray& obj)
{
if (this == &obj) return *this;
for (int i = 0; i < N; i++)
a[i] = obj.a[i];
return *this;
}
FixedArray operator+(FixedArray& obj)
{
FixedArray ans;
for (int i = 0; i < N; i++)
ans.a[i] = a[i] + obj.a[i];
return ans;
}
};
void main()
{
FixedArray a, b, c;
c = a + b;
c.Output(); // 输出2 4 6 8 10
}
3、动态数组(拷贝构造、( )重载)

已知Matrix类的数据成员定义和部分成员函数定义。要求补充相关函数(拷贝构造、( )重载)的定义,使之能满足main函数的功能需求,输出0 1 2 3 4 5

class Matrix
{
int* p; int row, col;
public:
Matrix(int row = 1, int col = 1)
{
this->row = row; this->col = col;
p = new int[row * col];
}
virtual ~Matrix() { delete[]p; }
friend ostream& operator<<(ostream& o, Matrix& m)
{
for (int i = 0; i < m.row; i++)
for (int j = 0; j < m.col; j++)
o << m.p[i * m.col + j] << " ";
return o;
}
};
void main()
{
Matrix m1(2, 3); int v = 0;
for (int i = 0; i < 2; i++)
for (int j = 0; j < 3; j++)
m1(i, j) = v++;
Matrix m2(m1);
cout << m2;
}

完整程序为:

  • 完善一下下标运算符重载
  • 完善一下有动态内存分配的复制构造函数
#include <iostream>
using namespace std;
class Matrix
{
int* p; int row, col;
public:
Matrix(int row = 1, int col = 1)
{
this->row = row; this->col = col;
p = new int[row * col];
}
Matrix(Matrix& m)
{
this->row = m.row;
this->col = m.col;
p = new int[row * col];
for (int i = 0; i < row; i++)
for (int j = 0; j < col; j++)
(*this)(i, j) = m(i, j);
}
int& operator()(int i, int j)
{
return *(p + i * col + j);
}
virtual ~Matrix() { delete[]p; }
friend ostream& operator<<(ostream& o, Matrix& m)
{
for (int i = 0; i < m.row; i++)
for (int j = 0; j < m.col; j++)
o << m.p[i * m.col + j] << " ";
return o;
}
};
void main()
{
Matrix m1(2, 3); int v = 0;
for (int i = 0; i < 2; i++)
for (int j = 0; j < 3; j++)
m1(i, j) = v++;
Matrix m2(m1);
cout << m2;
}
4、链表(头插、[]重载)

已知LinkList类的数据成员定义和部分成员函数定义。要求补充相关函数(头插函数Push[]重载)的定义,使之能满足main函数的功能需求,输出7 6 5 4 3 2

struct Node
{
int data; Node* next;
Node(int data)
{
this->data = data; this->next = NULL;
}
};
class LinkList
{
Node* head;
public:
LinkList(int a[], int n) :head(NULL)
{
for (int i = 0; i < n; i++)
Push(a[i]); // 头插入
}
virtual ~LinkList()
{
while (head != NULL)
{
Node* p = head; head = head->next; delete p;
}
}
};
void main()
{
int a[] = { 1,2,3,4,5,6 };
int i, n = 6;
LinkList L(a, n);
for (i = 0; i < n; i++) L[i]++;
for (i = 0; i < n; i++) cout << L[i] << " ";
}

完整程序为:

  • 完善一下头插入
    • 新建结点
    • 新结点的下一个连向旧的头
    • 头指针指向新建的结点
  • 完善一下下标运算符的重载
    • 从头结点开始依次往后指即可
    • 最后返回当前指针所指的结点的data即可
#include <iostream>
using namespace std;
struct Node
{
int data; Node* next;
Node(int data)
{
this->data = data; this->next = NULL;
}
};
class LinkList
{
Node* head;
public:
LinkList(int a[], int n) :head(NULL)
{
for (int i = 0; i < n; i++)
Push(a[i]); // 头插入
}
void Push(int x)
{
Node* newp = new Node(x);
newp->next = head;
head = newp;
}
int& operator[](int index)
{
Node* p = head;
for (int i = 0; i < index; i++)
p = p->next;
return p->data;
}
virtual ~LinkList()
{
while (head != NULL)
{
Node* p = head; head = head->next; delete p;
}
}
};
void main()
{
int a[] = { 1,2,3,4,5,6 };
int i, n = 6;
LinkList L(a, n);
for (i = 0; i < n; i++) L[i]++;
for (i = 0; i < n; i++) cout << L[i] << " ";
}
posted @   Mr_Dwj  阅读(147)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示