赞
踩
以下习题答案全部通过OJ,使用编译器为:G++(9.3(with c++17))
考点:运算符重载、构造函数、指针及内存的使用
解析:题目中给到的两个构造函数和一个成员变量,那么我们先来分析只有他们是否能满足题意?如果能,那我们何必麻烦自己呢,简单就是最好哒,直接提交!首先一个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 ); };
考点:运算符重载,友元函数和函数重载
解析:这里题目给到了类的一个成员变量和一个类型转换构造函数,同样我们来分析一下题目代码需要达到的目标有哪些:
// 调用类型转换构造函数
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; } };
考点:运算符重载,友元函数
解析:这里主要重载两个流式运算符<< 和 >>,注意这两个运算符重载必须申明为友元函数,因为需要对返回值进行链式计算,也需要返回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 );
};
考点:运算符重载、构造函数、指针及内存的使用
解析:这里题目的要求是实现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; } };
下一章:
※ 本文之中如有错误和不准确的地方,欢迎大家指正哒~
※ 此项目仅用于学习交流,请不要用于任何形式的商用用途,谢谢呢;
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。