北京大学C++程序设计编程作业答案+解析·运算符重载
本章一共包含五个编程习题:
- MyString
- 看上去好坑的运算符重载
- 惊呆!Point竟然能这样输入输出
- 二维数组类
- 别叫,这个大整数已经很简化了! (本题较为复杂,将会在下篇文章单独进行讲解)
以下习题答案全部通过OJ,使用编译器为:G++(9.3(with c++17))
1. MyString
考点:运算符重载、构造函数、指针及内存的使用
解析:题目中给到的两个构造函数和一个成员变量,那么我们先来分析只有他们是否能满足题意?如果能,那我们何必麻烦自己呢,简单就是最好哒,直接提交!首先一个char*已经满足要求,可以用来保存一个string。但是这里可能有童鞋要问,不提供char数组长度,系统如何知道多少内容需要打印呢?玄机就来自:
// 当cin录入用户的输入,会在内容的最后加入'\0',表示该字符串的结尾
// 比如当你输入123,其实内存里面保存的是:'123\0'
std::cin >> char*
所以我们不需要添加额外的成员变量,这里也解释了构造函数的初始化char数组的语句,里面为什么要+1:
p = new char[ strlen( s ) + 1 ]; // 多一个位置就是留给'\0'哒
接着我们来分析一下题目中所用到的构造函数:
// 类型转换构造函数 和 复制构造函数
MyString s1( w1 ), s2 = s1;
// 类型转换构造函数
MyString s3( NULL );
s3.Copy( w1 );
std::cout << s1 << "," << s2 << "," << s3 << std::endl;
// 类型转换构造函数
s2 = w2;
// 以下两句都是赋值,没有用到构造函数
s3 = s2;
s1 = s3;
std::cout << s1 << "," << s2 << "," << s3 << std::endl;
所以我们需要单独添加一个复制构造函数,注意复制的对象为null的情况即可:
MyString( const MyString &s ) {
if ( !s.p ) {
p = nullptr;
return;
}
std::cout << "Copy constructor before: " << " | " << "addr: " << &p << std::endl;
p = new char[ strlen( s.p ) + 1 ];
strcpy( p, s.p );
std::cout << "Copy constructor after: " << p << " | " << "addr: " << &p << std::endl;
}
然后就是实现Copy函数,因为s3的p一开始赋值为null,但是调用Copy以后,它可以正常打印w1的值,所以我们自然可以想到Copy函数就是赋值w1的内容到p中,这里需要注意释放p指向的内存,如果其不为null的话:
void Copy( const char *s ) {
if ( p ) delete[] p;
p = new char[ strlen( s ) + 1 ];
strcpy( p, s );
std::cout << "Copy after: " << p << " | " << "addr: " << &p << std::endl;
}
最后就是运算符重载了,那么我们需要重载多少个运算符呢?
MyString s1( w1 ), s2 = s1;
MyString s3( NULL );
s3.Copy( w1 );
// 需要重载 <<
std::cout << s1 << "," << s2 << "," << s3 << std::endl;
s2 = w2;
s3 = s2;
s1 = s3;
std::cout << s1 << "," << s2 << "," << s3 << std::endl;
看似我们只需要重载 << 即可,但是这样真的没问题么?大家可以试试按上面的思路实现,会不会报错呢?这里大家可以停下来想想,如果有报错,什么错误?为什么会有这个错误?不过不出所料,如果我们按上述思路运行程序,会得到下面错误:
// input
abc
abc
def
def
// output
Type convert before: | addr: 0x7ffe4da5caf0
Type convert after: abc | addr: 0x7ffe4da5caf0
Copy constructor before: | addr: 0x7ffe4da5caf8
Copy constructor after: abc | addr: 0x7ffe4da5caf8
Copy after: abc | addr: 0x7ffe4da5cb00
abc,abc,abc
Type convert before: | addr: 0x7ffe4da5cb08
Type convert after: def | addr: 0x7ffe4da5cb08
free: def | addr: 0x7ffe4da5cb08
6��c║,6��c║,6��c║ // Undefined values
free: 6��c║ | addr: 0x7ffe4da5cb00
free(): double free detected in tcache 2 // error
我们可以看到,程序提示我们对同一地址的内存进行多次释放:free(): double free detected in tcache 2,且第二条打印语句输出了一些意义不明的东东,他们从何而来?又为什么会有这个错误呢?为了方便我们分析报错的原因,我在程序函数额外打印了p内存的地址,接下来我们就根据代码和输出,一句句的进行分析。
Line 1: MyString s1( w1 ), s2 = s1;
// Line 1对应p内存地址输出:
// Type convert before: | addr: 0x7ffe4da5caf0
// Type convert after: abc | addr: 0x7ffe4da5caf0
// Copy constructor before: | addr: 0x7ffe4da5caf8
// Copy constructor after: abc | addr: 0x7ffe4da5caf8
// 注意s1( w1 )调用类型转换构造函数,s2 = s1调用复制构造函数
Line 2: MyString s3( NULL );
Line 3: s3.Copy( w1 );
// Line 2 ~ 3对应p内存地址输出:
// Copy after: abc | addr: 0x7ffe4da5cb00
Line 4: s2 = w2;
// Line 4对应p内存地址输出:
// Type convert before: | addr: 0x7ffe4da5cb08
// Type convert after: def | addr: 0x7ffe4da5cb08
// free: def | addr: 0x7ffe4da5cb08
Line 5: s3 = s2;
Line 6: s1 = s3;
// while语句作用域结束时:
// free: 6��c║ | addr: 0x7ffe4da5cb00
// free(): double free detected in tcache 2 // error
Line 1 ~ 3都没有什么问题,如果你觉得有问题,或者有看不懂的地方,需要去复习一下本题考点的基础知识了哦!这里我们注意到Line 4有释放内存的操作,也就是调用了析构函数,这里为什么呢?对于对象的赋值语句,有以下两种情况:
// 1) 初始化对象赋值
MyString s1( w1 );
// 2) 非初始化对象赋值
s1 = w2;
对于前者,编译器只会调用类型转换构造函数,不会调用析构函数,但是对于后者,会调用析构函数,但不是调用s1的析构函数,而是调用临时对象的析构函数。具体来说,对于第二种情况,编译器会这样执行1:
1)调用类型转换构造函数,创建一个临时对象,即MyString( w2 );
2)基于这个临时对象,对原本的对象进行默认赋值操作(即没有重载=运算符),即将临时对象中的所有成员变量,赋值给原本对象中的成员变量;
3)完成赋值操作后,销毁该临时对象,即这个时候调用了它的析构函数;
也就是说,发生如下图所示的过程:
即编译器自动生成并重载了运算符=:
MyString & MyString::operator=( const MyString &s ) {
if ( p ) delete[] p;
p = s.p;
return *this;
}
如果我们使用上述重载函数,执行结果和之前是一样的,由此说明编译器只是简单拷贝了临时对象的成员变量给s2。因此,在没有重载=运算符之前,执行Line 5 ~ 6会把s1,s2和s3中的p指针指向同一个地址,而且这个地址在Line 4之后就会被释放,所以我们需要针对MyString进行=运算重载来避免上述情况的发生:
MyString & MyString::operator=( const MyString &s ) {
if ( p ) delete[] p;
p = new char[ strlen( s.p ) + 1 ];
strcpy( p, s.p );
return *this;
}
答案:完整源码地址
// 这里只给到需要补完的代码,完整代码请移步到github
class MyString {
char *p;
public:
MyString( const char *s ) {
if ( s ) {
std::cout << "Type convert before: " << " | " << "addr: " << &p << std::endl;
p = new char[ strlen( s ) + 1 ];
strcpy( p, s );
std::cout << "Type convert after: " << p << " | " << "addr: " << &p << std::endl;
} else
p = NULL;
}
~MyString() {
if ( p ) {
std::cout << "free: " << p << " | " << "addr: " << &p << std::endl;
delete[] p;
}
}
// 在此处补充你的代码
MyString( const MyString &s ) {
if ( !s.p ) {
p = nullptr;
return;
}
std::cout << "Copy constructor before: " << " | " << "addr: " << &p << std::endl;
p = new char[ strlen( s.p ) + 1 ];
strcpy( p, s.p );
std::cout << "Copy constructor after: " << p << " | " << "addr: " << &p << std::endl;
}
void Copy( const char *s ) {
if ( p ) delete[] p;
p = new char[ strlen( s ) + 1 ];
strcpy( p, s );
std::cout << "Copy after: " << p << " | " << "addr: " << &p << std::endl;
}
// https://learn.microsoft.com/en-us/cpp/standard-library/overloading-the-output-operator-for-your-own-classes?view=msvc-170
friend std::ostream & operator<< ( std::ostream& os, const MyString &s );
// https://stackoverflow.com/questions/61488932/does-conversion-constructor-create-an-object-and-destroys-it-if-there-is-no-assi
MyString &operator= ( const MyString &s );
};
2. 看上去好坑的运算符重载
考点:运算符重载,友元函数和函数重载
解析:这里题目给到了类的一个成员变量和一个类型转换构造函数,同样我们来分析一下题目代码需要达到的目标有哪些:
// 调用类型转换构造函数
MyInt objInt(n);
// 需要重载运算符-
objInt-2-1-3;
// 需要对Inc进行函数重载,原来只有参数为int的形式,与这里的调用形式不符
cout << Inc(objInt);
cout <<",";
objInt-2-1;
cout << Inc(objInt) << endl;
所以首先,我们对运算符-进行重载:
// 这里重载为MyInt的成员函数,所以参数列表只有一个int
// 另外,我们可以连续调用运算符-,比如objInt-2-1-3,所以需要返回对象的引用,进行链式计算
MyInt &operator-( int n ) {
nVal -= n;
return *this;
}
最后,对Inc函数进行重载:
// 这里需要申明为友元函数,因为我们要在函数内部访问其私有成员变量
friend int Inc( MyInt &i ) {
return i.nVal + 1;
}
答案:完整源码地址
// 这里只给到需要补完的代码,完整代码请移步到github
class MyInt {
int nVal;
public:
MyInt( int n ) { nVal = n; }
// 在此处补充你的代码
MyInt &operator-( int n ) {
nVal -= n;
return *this;
}
// https://www.tutorialspoint.com/cplusplus/function_call_operator_overloading.htm
// friend int Inc( int n ); // Unnecessary
friend int Inc( MyInt &i ) {
return i.nVal + 1;
}
};
4. 惊呆!Point竟然能这样输入输出
考点:运算符重载,友元函数
解析:这里主要重载两个流式运算符<< 和 >>,注意这两个运算符重载必须申明为友元函数,因为需要对返回值进行链式计算,也需要返回istream和ostram的引用,传入参数也为其引用。
答案:完整源码地址
// 这里只给到需要补完的代码,完整代码请移步到github
class Point {
private:
int x;
int y;
public:
Point() {};
// 在此处补充你的代码
friend std::istream &operator>>( std::istream& is, Point &p );
friend std::ostream &operator<<( std::ostream& os, const Point &p );
};
5. 二维数组类
考点:运算符重载、构造函数、指针及内存的使用
解析:这里题目的要求是实现Array2类,那么还是同样的方法,先来分析题目需要这个类实现哪些功能,以及需要用到什么成员变量:
// 调用类型转换构造函数,需要存储数组的值,以及数组的行数和列数
Array2 a( 3, 4 );
int i, j;
for ( i = 0; i < 3; ++i )
for ( j = 0; j < 4; j++ )
// 需要对运算符[]进行重载
a[ i ][ j ] = i * 4 + j;
for ( i = 0; i < 3; ++i ) {
for ( j = 0; j < 4; j++ ) {
// 需要对运算符()进行重载
std::cout << a( i, j ) << ",";
}
std::cout << std::endl;
}
std::cout << "next" << std::endl;
// 调用无参构造函数
Array2 b;
// 需要对运算符=进行重载,否则会出现MyString那题的错误
b = a;
for ( i = 0; i < 3; ++i ) {
for ( j = 0; j < 4; j++ ) {
std::cout << b[ i ][ j ] << ",";
}
std::cout << std::endl;
}
对于构造函数和重载运算符=,之前我们已经遇到过多次,还是不太懂的童鞋可以回顾之前的知识点和习题,这里就不再赘述。这里着重讲解一下运算符[]和()的重载。首先是运算符()的重载,这个函数调用运算符重载并不是重载函数调用的方式,而是可以对类调用时,可以按照你重载的参数进行调用,即可以调用任意个参数,比如本题的重载:
// 这里需要重载(),参数有两个int,返回a[ i ][ j ]的值
std::cout << a( i, j ) << ",";
// 所以实现如下:
int &operator()( size_t r, size_t c ) {
return A[ r * this->c + c ];
}
这里代码不难,但是有个问题,为什么a[ i ][ j ]可以写成A[ r * this->c + c ]?不是对二维数组进行取值,怎么写法好像是对一维数组进行取值?为了解释这个问题,我们需要明确数组在内存是如何存储的:
无论是N维数组,在内存中的是存储在一块连续的内存空间中的。
以二维数组为例2:
我们可以看到二维数组,以每行作为单位,存储在连续的一维空间之中。所以如果我们需要对a[ i ][ j ]进行取值,我们需要对二维坐标空间进行降维到一维空间,首先对于i,即行号,每跳过一行,我们就跳过 ( i * 列数 ) 个元素,比如i = 1,对于上面的例子,我们需要跳过 ( 1 * 3 ) 个元素,才能来到第二行。其次是j,即列号,当我们确定了行号,我们只需要跳过列号个元素,即可访问我们需要的元素,即跳过j个元素,所以对于a[ 1 ][ 2 ] = 1.2,我们需要访问a[ 1 * 3 + 2 ] = 1.2。
讲解完运算符()的重载,那么如何重载[]?重载它的思路其实我们在上面例子已经说明了:
比如a[ 1 ][ 2 ],
第一次进行[]运算,即a[ 1 ],即跳过行数,我们需要跳过( 1 * 列数 )个元素,返回该元素的指针
第二次进行[]运算,即( int * )[ 2 ],即跳过列数,之前提到过,我们需要跳过 ( 2 ) 个元素,但是我们之前以前跳过了行数,得到了第二行第一个元素的地址,这里直接进行取值即可,也就是这里并不是Array2类的运算符[]的重载,即
int *t = a[ 1 ]; // 运算符[]的重载
t[ 2 ] = i * 4 + j; //正常数组取值
答案:完整源码地址
// 这里只给到需要补完的代码,完整代码请移步到github
class Array2 {
// 在此处补充你的代码
// https://stackoverflow.com/questions/19732319/difference-between-size-t-and-unsigned-int
size_t r;
size_t c;
int *A;
public:
Array2() {
r = c = 0;
A = nullptr;
}
Array2( size_t r, size_t c ) {
this->r = r;
this->c = c;
A = new int[ r * c ];
}
// Unnecessary
Array2( const Array2 &A ) {
r = c = 0;
this->A = nullptr;
}
~Array2() {
if ( A ) delete [] A;
}
int *operator[] ( size_t r ) {
return r * c + A;
}
int &operator() ( size_t r, size_t c ) {
return A[ r * this->c + c ];
}
Array2 &operator= ( const Array2 &A ) {
if ( this->A ) delete [] this->A;
c = A.c;
r = A.r;
size_t s = A.c * A.r;
this->A = new int[ s ];
std::copy( A.A, A.A + s, this->A );
return *this;
}
};
下一章:
6. 参考资料
- C++程序设计
- Does conversion constructor create an object and destroys it if there is no assignment operator in c++?
- pixiv illustration: 氷花の舞姫
7. 免责声明
※ 本文之中如有错误和不准确的地方,欢迎大家指正哒~
※ 此项目仅用于学习交流,请不要用于任何形式的商用用途,谢谢呢;