当前位置:   article > 正文

北京大学C++程序设计编程作业答案+解析·运算符重载_c++语言程序设计

c++语言程序设计


本章一共包含五个编程习题:

  1. MyString
  2. 看上去好坑的运算符重载
  3. 惊呆!Point竟然能这样输入输出
  4. 二维数组类
  5. 别叫,这个大整数已经很简化了! (本题较为复杂,将会在下篇文章单独进行讲解)

以下习题答案全部通过OJ,使用编译器为:G++(9.3(with c++17))

1. MyString

考点:运算符重载、构造函数、指针及内存的使用

解析:题目中给到的两个构造函数和一个成员变量,那么我们先来分析只有他们是否能满足题意?如果能,那我们何必麻烦自己呢,简单就是最好哒,直接提交!首先一个char*已经满足要求,可以用来保存一个string。但是这里可能有童鞋要问,不提供char数组长度,系统如何知道多少内容需要打印呢?玄机就来自:

// 当cin录入用户的输入,会在内容的最后加入'\0',表示该字符串的结尾
// 比如当你输入123,其实内存里面保存的是:'123\0'
std::cin >> char* 
  • 1
  • 2
  • 3

所以我们不需要添加额外的成员变量,这里也解释了构造函数的初始化char数组的语句,里面为什么要+1:

p = new char[ strlen( s ) + 1 ]; // 多一个位置就是留给'\0'哒
  • 1

接着我们来分析一下题目中所用到的构造函数:

// 类型转换构造函数 和 复制构造函数
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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

所以我们需要单独添加一个复制构造函数,注意复制的对象为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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

然后就是实现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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

最后就是运算符重载了,那么我们需要重载多少个运算符呢?

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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

看似我们只需要重载 << 即可,但是这样真的没问题么?大家可以试试按上面的思路实现,会不会报错呢?这里大家可以停下来想想,如果有报错,什么错误?为什么会有这个错误?不过不出所料,如果我们按上述思路运行程序,会得到下面错误:

// 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

我们可以看到,程序提示我们对同一地址的内存进行多次释放: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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

Line 1 ~ 3都没有什么问题,如果你觉得有问题,或者有看不懂的地方,需要去复习一下本题考点的基础知识了哦!这里我们注意到Line 4有释放内存的操作,也就是调用了析构函数,这里为什么呢?对于对象的赋值语句,有以下两种情况:

// 1) 初始化对象赋值
MyString s1( w1 );
// 2) 非初始化对象赋值
s1 = w2;
  • 1
  • 2
  • 3
  • 4

对于前者,编译器只会调用类型转换构造函数,不会调用析构函数,但是对于后者,会调用析构函数,但不是调用s1的析构函数,而是调用临时对象的析构函数。具体来说,对于第二种情况,编译器会这样执行1

1)调用类型转换构造函数,创建一个临时对象,即MyString( w2 );

2)基于这个临时对象,对原本的对象进行默认赋值操作(即没有重载=运算符),即将临时对象中的所有成员变量,赋值给原本对象中的成员变量;

3)完成赋值操作后,销毁该临时对象,即这个时候调用了它的析构函数;

也就是说,发生如下图所示的过程:

在这里插入图片描述

即编译器自动生成并重载了运算符=:

MyString & MyString::operator=( const MyString &s ) {
    if ( p ) delete[] p;

    p = s.p;
    return *this;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

如果我们使用上述重载函数,执行结果和之前是一样的,由此说明编译器只是简单拷贝了临时对象的成员变量给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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

答案:完整源码地址

// 这里只给到需要补完的代码,完整代码请移步到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 );
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

2. 看上去好坑的运算符重载

考点:运算符重载,友元函数和函数重载

解析:这里题目给到了类的一个成员变量和一个类型转换构造函数,同样我们来分析一下题目代码需要达到的目标有哪些:

// 调用类型转换构造函数
MyInt objInt(n); 
// 需要重载运算符-
objInt-2-1-3; 
// 需要对Inc进行函数重载,原来只有参数为int的形式,与这里的调用形式不符
cout << Inc(objInt);
cout <<","; 
objInt-2-1; 
cout << Inc(objInt) << endl;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

所以首先,我们对运算符-进行重载:

// 这里重载为MyInt的成员函数,所以参数列表只有一个int
// 另外,我们可以连续调用运算符-,比如objInt-2-1-3,所以需要返回对象的引用,进行链式计算
MyInt &operator-( int n ) {
    nVal -= n;
    return *this;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

最后,对Inc函数进行重载:

// 这里需要申明为友元函数,因为我们要在函数内部访问其私有成员变量
friend int Inc( MyInt &i ) {
    return i.nVal + 1;
}
  • 1
  • 2
  • 3
  • 4

答案:完整源码地址

// 这里只给到需要补完的代码,完整代码请移步到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;
    }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

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 );
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

对于构造函数和重载运算符=,之前我们已经遇到过多次,还是不太懂的童鞋可以回顾之前的知识点和习题,这里就不再赘述。这里着重讲解一下运算符[]和()的重载。首先是运算符()的重载,这个函数调用运算符重载并不是重载函数调用的方式,而是可以对类调用时,可以按照你重载的参数进行调用,即可以调用任意个参数,比如本题的重载:

// 这里需要重载(),参数有两个int,返回a[ i ][ j ]的值
std::cout << a( i, j ) << ",";
// 所以实现如下:
int &operator()( size_t r, size_t c ) {
    return A[ r * this->c + c ];
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这里代码不难,但是有个问题,为什么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;
    }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

上一章:编程题解析+答案·魔兽世界之一:备战

下一章:

6. 参考资料

  1. C++程序设计
  2. Does conversion constructor create an object and destroys it if there is no assignment operator in c++?
  3. pixiv illustration: 氷花の舞姫

7. 免责声明

※ 本文之中如有错误和不准确的地方,欢迎大家指正哒~

※ 此项目仅用于学习交流,请不要用于任何形式的商用用途,谢谢呢;


在这里插入图片描述


  1. Does conversion constructor create an object and destroys it if there is no assignment operator in c++? ↩︎

  2. Memory layout of multi-dimensional arrays ↩︎

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/806857
推荐阅读
相关标签
  

闽ICP备14008679号