北大C++程序设计编程作业答案+解析·继承

北大C++程序设计编程作业答案+解析·继承

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

  1. 全面的MyString
  2. 继承自string的MyString

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

1. 全面的MyString

考点:运算符重载,构造函数,析构函数

解析:本题其实就是要求我们实现我们自己的string类,既然是类,那我们先从需要什么构造函数开始分析:

// 调用类型转换构造函数,无参构造函数,和复制构造函数
MyString s1("abcd-"),s2,s3("efgh-"),s4(s1);
// 调用类型转换构造函数
MyString SArray[4] = {"big","me","about","take"};
// 以下三个都是赋值运算,没有调用构造函数
s4 = s3;
s3 = s1 + s3;
s2 = s1;
// 调用类型转换构造函数
s1 = "ijkl-";
// 以下两个都是赋值运算,没有调用构造函数
// 即:"qrst-" + s2 得到一个临时对象MyString,在赋值给s4
s4 = "qrst-" + s2;
s1 = s2 + s4 + " uvw " + "xyz";

所以我们需要一共三个构造函数,数据结构比较简单,一个char型数组即可,因为每个数组最后会插入’\0’作为终止符,所以不需要长度也可以知道char数组的长度,但是这里为了节省每次都去遍历数组去计算长度,我们增加一个变量存储数组的长度,当然因为为char数组分配了空间,也少不了析构函数进行内存释放:

class MyString {
    char *C;
    size_t n;

public:
    MyString() : C( nullptr ), n( 0 ) {}

    MyString( const char *C_ ) {
        n = strlen(  C_ );
        C = new char[ n + 1 ];
        strcpy( C, C_ );
    }

    MyString( const MyString &s ) {
        n = s.n;
        C = new char[ n + 1 ];
        strcpy( C, s.C );
    }

    ~MyString() {
        if ( C ) delete [] C;
    }
    
    // 运算符重载函数略
};

接下来我们就来分析需要重载哪些运算符:

// 需要重载<<
std::cout << "1. " << s1 << s2 << s3 << s4 << std::endl;
// 需要重载=
// 之前我们提到过,不能调用默认赋值语句,这样会有两个MyString指向同一个char数组
s4 = s3;
// 需要重载+(MyString&)
s3 = s1 + s3;
std::cout << "2. " << s1 << std::endl;
std::cout << "3. " << s2 << std::endl;
std::cout << "4. " << s3 << std::endl;
std::cout << "5. " << s4 << std::endl;
std::cout << "6. " << s1[ 2 ] << std::endl;
s2 = s1;
s1 = "ijkl-";
// 需要重载[]
s1[ 2 ] = 'A';
std::cout << "7. " << s2 << std::endl;
std::cout << "8. " << s1 << std::endl;
// 需要重载+=
s1 += "mnop";
std::cout << "9. " << s1 << std::endl;
// 需要重载+(char*, MyString&)
s4 = "qrst-" + s2;
std::cout << "10. " << s4 << std::endl;
// 需要重载+(MyString&, char*)
s1 = s2 + s4 + " uvw " + "xyz";
std::cout << "11. " << s1 << std::endl;
qsort( SArray, 4, sizeof( MyString ), CompareString );
for ( int i = 0; i < 4; i++ )
    std::cout << SArray[ i ] << std::endl;
// 需要重载()(size_t, size_t)
std::cout << s1( 0, 4 ) << std::endl;
std::cout << s1( 5, 10 ) << std::endl;


int CompareString( const void * e1, const void * e2)
{
	MyString * s1 = (MyString * ) e1;
	MyString * s2 = (MyString * ) e2;
    // 需要重载<
	if( * s1 < *s2 )
	return -1;
    // 需要重载==
	else if( *s1 == *s2)
	return 0;
    // 需要重载>
	else if( *s1 > *s2 )
	return 1;
}

再知道需要重载哪些运算符,我们就一个个分析如何进行实现,首先是运算符=的重载:

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

    n = s.n;
    C = new char[ s.n + 1 ];
    strcpy( C, s.C );
    return *this;
}

这里需要注意释放原来分配的内存,在进行复制,另外,题目里面提供的strcpy()函数可以复制两个字符串,strlen()函数可以得到一个字符串的长度,里面用到了’\0’作为字符串的终止符来遍历字符串,而不是显性地使用数组长度:

int strlen( const char *s ) {
    int i = 0;
    // s[ i ]就是判断是不是'\0',如果是,则结束遍历
    for ( ; s[ i ]; ++i );
    return i;
}

接下来是运算符<<的重载,这个很简单,我们已经实现多次了:

std::ostream &operator<<( std::ostream &os, const MyString &s ) {
    // 注意空字符串不要打印,否则会意外退出
    if ( s.n > 0 ) std::cout << s.C;
    return os;
}

之后是重载两次运算符+:

// Not good to return a reference.
MyString MyString::operator+( const MyString &s ) {
    char t[ n + s.n + 1 ];
    strcpy( t, C );
    strcat( t, s.C );

    return MyString ( t );
}

MyString operator+( const char *C_, const MyString &s ) {
    char t[ strlen( C_ ) + s.n + 1 ];
    strcpy( t, C_ );
    strcat( t, s.C );

    return MyString ( t );
}

这里就是主要初始化char数组需要+1,因为要预留一个位置给终止符’\0’,还要注意这两个返回的需要是新的对象,而不是参数对象的引用,因为我们想得到一个新的字符串,也不想修改两个参数字符串的内容。另外,题目提供了连接两个字符串的函数strcat(),可以利用起来哒。

下一个就是和运算符+相似的运算符+=:

MyString &MyString::operator+=( const char *C_ ) {
    char *t = C;
    C = new char[ strlen( C_ ) + n + 1 ];

    strcpy( C, t );
    strcat( C, C_ );

    delete [] t;
    return *this;
}

实现思路和运算符+重载类似,这里就不再赘述。下一个是运算符[]重载:

char &MyString::operator[]( size_t i ) {
    return C[ i ];
}

实现很简单,但是注意这里要返回char的引用,因为外面可能会直接修改这个char值,即:

// 如果这里重载运算符[]不返回引用,而是值
// 运行下面语句之后,s1是不会被修改的,特别需要注意
s1[ 2 ] = 'A';

最后是三个比较运算符的重载,<,>,==,可以使用题目提供的函数strcmp(s1, s2)来进行比较,即当s1 < s2,返回-1,s1 > s2,返回1,相等则返回0:

bool MyString::operator<( const MyString &s ) {
    return strcmp( C, s.C ) == -1;
}

bool MyString::operator>( const MyString &s ) {
    return strcmp( C, s.C ) == 1;
}

bool MyString::operator==( const MyString &s ) {
    return strcmp( C, s.C ) == 0;
}

答案:完整源码地址

// 这里只给到需要补完的代码,完整代码请移步到github
class MyString {
    // 在此处补充你的代码
    char *C;
    size_t n;

public:
    MyString() : C( nullptr ), n( 0 ) {}

    MyString( const char *C_ ) {
        n = strlen(  C_ );
        C = new char[ n + 1 ];
        strcpy( C, C_ );
    }

    MyString( const MyString &s ) {
        n = s.n;
        C = new char[ n + 1 ];
        strcpy( C, s.C );
    }

    ~MyString() {
        if ( C ) delete [] C;
    }

    MyString &operator=( const MyString &s );

    friend std::ostream &operator<<( std::ostream &os, const MyString &s );

    MyString operator+( const MyString &s );

    friend MyString operator+( const char* C_, const MyString &s );

    MyString &operator+=( const char* C_ );

    char &operator[]( size_t i );

    MyString operator()( size_t i, size_t n_ );

    bool operator<( const MyString &s );

    bool operator>( const MyString &s );

    bool operator==( const MyString &s );
};

2. 继承自string的MyString

考点:运算符重载,继承

解析:这里题目其实就是要求我们实现一个自己的MyString类,但是这个类已经继承标准库里面的std::string,也就是说,这个MyString已经有了所有的字符串基本功能,所以需要实现的功能其实就一个,也就是题目提示一里面说的:

提示 1:如果将程序中所有 “MyString” 用 “string” 替换,那么除了最后两条红色的语句编译无法通过外,其他语句都没有问题,而且输出和前面给的结果吻合。也就是说,MyString 类对 string 类的功能扩充只体现在最后两条语句上面。

也就是说从功能拓展性来说,我们只需要重载运算符()(size_t,size_t)即可:

MyString operator()( size_t i, size_t n );

// 注意std::string里面是有截取substring的函数方法的,直接调用,再转换成MyString返回即可
// 同样不能返回引用,因为我们需要重新生成一个substring,也不想修改原来的字符串
MyString MyString::operator()( size_t i, size_t n ) {
    // https://www.geeksforgeeks.org/substring-in-cpp/
    std::string t = substr( i, n );
    // 这里需要定义一个类型转换函数,即std::string => MyString
    return MyString( t );
}

但是添加这个重载定义以后,编译器还是会报错,那么我们就来分析一下,为什么还会报错,不是功能都实现完了么?

// 首先是这里会报错:No viable overloaded '='
s3 = s1 + s3;

这里报错说没有可以重载的运算符=,这是为什么呢?我们可以看到 s1 + s3,其实调用了std::string里面的运算符+的重载,那么返回类型也是std::string,而且我们学过类的继承,基类不能赋值给派生类,因为基类不是派生类,就像可以说人是动物,但是你不能说动物都是人,这里动物是基类,人是动物的派生类。那么解决方法可以再次重载一下运算符=:

std::string &operator=( const std::string &s );

这样就不会报错了,但是会在后面一条语句报错:

// 这里会报错:Use of overloaded operator '=' is ambiguous (with operand types 'MyString' and 'const char[6]')
s1 = "ijkl-";

这个报错是什么意思呢?这里说明有多个相似的函数定义,编译器不知道用哪个。那么是哪两个函数产生歧义了呢?

MyString( const MyString &s ) : std::string( s ) {} // 这里用于MyString s4( s1 )的初始化
std::string &operator=( const std::string &s );

也就是说上面的语句,编译器可以调用类型转换构造函数,也可以使用运算符=的重载函数,从而产生了歧义,所以这条思路不行。那么我们换一条思路想想,既然重载运算符=的类型成std::string不合适,那么我们换成MyString类型,总可以了吧?所以我们重新重载一下运算符=:

MyString operator+( const MyString &s );

但是后面还是会有新的报错:

// No viable overloaded '='
s4 = "qrst-" + s2;

需要重载一下运算符+(char*, MyString):

friend MyString operator+( const char *C, const MyString &s );

好像解决问题了,但是!后面还有新的报错:

// Use of overloaded operator '+' is ambiguous (with operand types 'MyString' and 'const char[6]')
s1 = s2 + s4 + " uvw " + "xyz";

报错原因还是和之前相似,有多个相似的函数定义,编译器无法知道这里具体使用哪个,但这里相似定义的函数有一些不一样,有三个相似定义的函数:

// MyString
MyString operator+( const MyString &s );
// std::string
template<typename _CharT, typename _Traits, typename _Alloc>
inline basic_string<_CharT, _Traits, _Alloc>
operator+(const basic_string<_CharT, _Traits, _Alloc>& __lhs,
      const _CharT* __rhs);
template<typename _CharT, typename _Traits, typename _Alloc>
inline basic_string<_CharT, _Traits, _Alloc>
operator+(basic_string<_CharT, _Traits, _Alloc>&& __lhs,
      const _CharT* __rhs);

这里可以看到,有两个重载std::string的重载函数,一个MyString的重载函数:

MyString operator+( const MyString &s );
// 为了更好的理解,我简化了std::string里面的重载参数
std::string operator+(const std::string& __lhs, const char* __rhs);
std::string operator+(const std::string&& __lhs, const char* __rhs);
// 可以看到 s2 + s4 + " uvw ",可以有两种解读方式:
// 1): MyString + MyString => 重载第一个
// 2): std::string + char* => 重载第二个或第三个

从上面的分析,可以得到这样重载运算符会有很多的歧义,那我们怎么办呢?有什么线索呢?我们可以注意到,其实MyString继承与std::string,而且std::string里面其实已经重载了运算符+了,我们是否能看看基类是如何实现的呢?从来进行函数重载,用来避免歧义呢?我们可以通过下面的语句来找到这个运算符=重载:

std::string s5, s6;
s5 + s6;

// 这里 s5 + s6 的重载定义为:
template<typename _CharT, typename _Traits, typename _Alloc>
basic_string<_CharT, _Traits, _Alloc>
operator+(const basic_string<_CharT, _Traits, _Alloc>& __lhs,
 	 const basic_string<_CharT, _Traits, _Alloc>& __rhs);
// 进行参数类型简化:
std::string operator+( const std::string& __lhs, const std::string& __rhs );

std::string里面定义了一个外部的运算符重载,那么根据这个线索,我们定义了一个相似的MyString运算符重载:

// 添加重载
friend MyString operator+( const MyString &s1, const MyString &s2 ); 
s4 = "qrst-" + s2; // 报错,没有可用运算符重载
// 添加重载
friend MyString operator+( const char *C, const MyString &s );
s1 = s2 + s4 + " uvw " + "xyz"; // 报错,多个相似函数定义可调用

怎么饶了半天,还是同一个错误?难道我们的思路又错了?先不要放弃哒,根据上面的思路,我们分析一下哪些函数定义有歧义:

friend MyString operator+( const MyString &s1, const MyString &s2 );
std::string operator+(const std::string& __lhs, const char* __rhs);
std::string operator+(const std::string&& __lhs, const char* __rhs);

那么,我们可以在添加一个重载定义来解决这些歧义:

friend MyString operator+( const MyString &s, const char *C );

从而告诉编译器,s2 + s4 + " uvw " 只能解读为:MyString + char*,从而解决问题。这里还需要注意,再之前得歧义里面添加这个定义还是不能解决问题,因为下面两个定义会产生歧义:

MyString operator+( const MyString &s );
friend MyString operator+( const MyString &s, const char *C );

从这题可以看出,其实解题过程就是一步步探究的过程,尝试所有可能的结果,往往最后那个就是真相哒。当然如果一开始就看答案就不会有这样的问题,但是非常不利于大家举一反三,解决未知的问题,所以大家以后遇到新问题,就按照已知的线索一步步尝试,遇到问题具体分析。这也是我一步步分析如何得到最终答案得目的,否则直接答案糊脸不就完事了么?因此这个得到答案的过程,往往比结果更重要哦~

答案:完整源码地址

// 这里只给到需要补完的代码,完整代码请移步到github
class MyString : public std::string {
    // 在此处补充你的代码
    MyString( std::string &s ) : std::string( s ) {}

public:
    MyString(): std::string() {}

    MyString( const char *C_ ): std::string( C_ ) {}

    MyString( const MyString &s ) : std::string( s ) {}

    friend MyString operator+( const MyString &s1, const MyString &s2 );

    friend MyString operator+( const char *C, const MyString &s );

    friend MyString operator+( const MyString &s, const char *C );

    MyString operator()( size_t i, size_t n );
};

上一章:

下一章:编程作业答案+解析·多态

3. 参考资料

  1. C++程序设计
  2. pixiv illustration: WutheringWaves

4. 免责声明

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

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


在这里插入图片描述

  • 25
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值