先来看一下面试经常遇到的问题:实现strcpy函数
char *strcpy(char *strDest, const char *strSrc)
{
if (strDest == NULL || strSrc == NULL) {
return NULL;
}
if (strDest == strSrc) {
return strDest;
}
char *tempptr = strDest ;
while((*strDest++ = *strSrc++) != '\0');
return tempptr ;
}
string类的实现
曾在面试的时候遇到过要求实现string类的构造函数,析构函数,拷贝构造函数,个人觉得string的实现涉及很多c++的基础知识、内存控制及异常处理等问题,仔细研究起来非常复杂,在这里总结一下。
整体的类的框架如下:
class String
{
public:
String(const char *str = NULL); //通用构造函数
String(const String &str); //拷贝构造函数
~String(); //析构函数
String operator+(const String &str) const; //重载+
String& operator=(const String &str); //重载=
String& operator+=(const String &str); //重载+=
bool operator==(const String &str) const; //重载==
char& operator[](int n) const; //重载[]
size_t size() const; //获取长度
const char* c_str() const; //获取C字符串
friend istream& operator>>(istream &is, String &str); //输入
friend ostream& operator<<(ostream &os, String &str); //输出
private:
char *data; //字符串
size_t length; //长度
};
类的成员函数中,有一些是加了const修饰的,表示这个函数不会对类的成员进行任何修改。一些函数的输入参数也加了const修饰,表示该函数不会对改变这个参数的值。const具有保护成员或参数不被修改的功能。
构造函数是用一个字符串数组进行string的初始化,默认的字符串数组为空,这里的函数定义中不需要再定义参数的默认值,因为在类中已经声明过了。使用C函数strlen的时候需要注意字符串参数是否为空,对空指针调用strlen会引发内存错误。
String::String(const char *str)//通用构造函数
{
if (!str) {
length = 0;
data = new char[1];
*data = '\0';
}
else {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
}
}
拷贝构造函数需要进行深复制,默认的拷贝构造函数在运行时只会进行浅复制,即只复制内存区域的指针,会造成两个对象指向同一块内存区域的现象。如果一个对象销毁或改变了该内存区域,会造成另一个对象运行或者逻辑上出错。这时就要求程序员自己实现这些函数进行深复制,即不止复制指针,需要连同内存的内容一起复制。
String::String(const String &str)//拷贝构造函数
{
length = str.size();
data = new char[length + 1];
strcpy(data, str.c_str());
}
析构函数需要进行内存的释放及长度的归零。
String::~String()//析构函数
{
delete []data;
length = 0;
}
重载字符串连接运算,这个运算会返回一个新的字符串。
String String::operator+(const String &str) const//重载+
{
String newString;
newString.length = length + str.size();
newString.data = new char[newString.length + 1];
strcpy(newString.data, data);
strcat(newString.data, str.data);
return newString;
}
重载字符串赋值运算,这个运算会改变原有字符串的值,为了避免内存泄露,这里释放了原先申请的内存再重新申请一块适当大小的内存存放新的字符串。
String& String::operator=(const String &str)//重载=
{
if (this == &str) {
return *this;//相等的情况易被忽视
}
delete []data;//释放自身内存
length = str.length;
data = new char[length + 1];
strcpy(data, str.c_str());
return *this;
}
这是c++教材上提供的参考代码,如果接受面试的是应届毕业生或者c++初级程序员能全面的考虑前面几点并完整的写出代码,那么面试官可能会让你通过这轮面试,但如果是c++高级程序员,则面试可能会失败。
考虑异常安全性的解法,高级程序员必备:
在上面的函数中,我们在分配内存之前先用delete释放了实例data的内存,如果此时内存不足导致new data 抛出异常,则data则是一个空指针,这样很容易导致程序崩溃,也就是说,一旦在赋值运算符内部抛出一个异常,String的实例不再保持有效的状态,这就违背了异常安全性(Exception Safety)原则。
要想在赋值运算符函数中实现异常安全性,我们有两种办法,一种简单的办法是我们现用new分配内容,再用delete释放已有的内容,这样只在分配内容成功之后再释放原来的内容,也就是当分配内存失败时我们能确保string的实例不会被修改,还有一个更好的办法,即先创建一个临时对象实例,再交换临时实例和原来的实例,下面是这种思路的参考代码:
String& String::operator=(const String &str)//重载=
{
if (this != &str) {
String strTemp(str);
char* pTemp = strTemp.data;
strTemp.data = data;
data = pTemp;
}
return *this;
}
在这个函数中,我们先创建一个临时实例strTemp,接着把 strTemp.data和自身的data进行交换,由于strTemp是一个局部变量,但程序运行到if外面时也就出了该变量的作用域,就会自动调用strTemp的析构函数,把strTemp.data所指向的内存释放,由于strTemp.data指向的内存就是实例之前data的内存,这就相当于自动调用析构函数释放实例的内存。
在新的代码中,我们在String的构造函数里用new分配内存,如果由于内存不足抛出诸如bad_alloc等异常,但我们还没有修改原来实例的状态,因此实例的状态还是有效的,这就保障了异常安全性。
如果面试者可以考虑到这个层面,面试官可能会觉得他对代码的异常安全性有很深的理解,那么他自然也就能通过此轮面试了。
重载字符串+=操作,总体上是以上两个操作的结合。
String& String::operator+=(const String &str)//重载+=
{
length += str.length;
char *newData = new char[length + 1];
strcpy(newData, data);
strcat(newData, str.data);
delete []data;
data = newData;
return *this;
}
重载相等关系运算,这里定义为内联函数加快运行速度。
inline bool String::operator==(const String &str) const//重载==
{
if (length != str.length) {
return false;
}
return strcmp(data, str.data) ? false : true;
}