北大C++程序设计编程作业答案+解析·继承
本章一共包含两个编程习题:
以下习题答案全部通过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. 参考资料
- C++程序设计
- pixiv illustration: WutheringWaves
4. 免责声明
※ 本文之中如有错误和不准确的地方,欢迎大家指正哒~
※ 此项目仅用于学习交流,请不要用于任何形式的商用用途,谢谢呢;