PoEduo - C++阶段班【Po学校】-06自定义String类讲解- 课堂笔记
#pragma once //不能跨平台,就改为下面代码.
#ifndef _STRING_H_ #define _STRING_H_ #endif //! _STRING_H_
1.2 两种方式命名空间包含:
第1种:
using namespace PoEdu;
第2种:
namespace PoEdu { String::String() { } String::~String() { } }
提倡第2种写法,不会有命名空间污染的产生。
1.3 默认生成类有哪几种函数: 1 构造 2 析构 3 拷贝赋值 4 拷贝构造 5 重载& 6 重载 *
1.4 成员数据类成员最好设为私有,还有注意命名的规范
private: char *data_ //现在统一为后下划线规范
2 --> String类 重写构造函数
String::String(const char *str) { int len = strlen(str) + sizeof(char); data_ = new char[len]; strcpy(data_,str); }
2.7 这里析构函数还需要判断吗?
#include "String.h" #include <cstring> namespace PoEdu { String::String(const char *str) { int len = strlen(str) + sizeof(char); data_ = new char[len]; strcpy(data_,str); } String::~String() { if(data_) //此时还需要判断吗? delete[]data_; } }
不需要判断,因为构造函数默认给一个空字符串。
2.8 看下面代码,会delete2次:
int main() { using namespace PoEdu; String demo1; String demo2 = demo1; return 0; }
实践运行代码:
String demo2 = demo1; 这句代码会调用默认拷贝构造,因为我们没有重载它,自动生成的默认拷贝构造函数里面,是浅拷贝,如果涉及到指针,浅拷贝是一个危险的行为。
String::~String() { std::cout << "~String::String()析构函数" << std::endl; if(data_) delete[]data_; }
第2次析构进来,判断if(data_)为不为空?它不是空的,那么再次delete[]data_;就此触发一个断点。因为你在同一个地方进行第2次的delete。
delete之后,data_在内存里面也不是空的,这时判断语句就变得没用了。以前提到过一种掩耳盗铃的方法,就是在delete[]data_;之后,再给(data_=nullptr;)空指针,这种方法是有问题的。
然而说回来,以上代码就让它触发一个断点,对调试程序来说,还是一件好事。
所以,代码中的判断还是不写了。规避这种风险的正确方法:实现拷贝构造,让其深拷贝。
3 --> String类 重写拷贝构造函数
3.0 两段代码相似度很高,可以做个抽象,放在一个函数里面。
String::String(const char *str) { int len = strlen(str) + sizeof(char); data_ = new char[len]; strcpy(data_,str); } String::String(const String& other) { int len = strlen(other.data_) + sizeof(char); data_ = new char[len]; strcpy(data_, other.data_); }
3.1 加入一个返回char*的分配函数 char* Alloc(const char *data);
char* String::Alloc(const char* data) { int len = strlen(data) + sizeof(char); data_ = new char[len]; strcpy(data_, data); return data_; }
3.2 这里返回data_,相对来说,关联性太强,可以改成下面代码:
String::String(const char *str) { data_ = Alloc(str); } String::String(const String& other) { data_ = Alloc(other.data_); } char* String::Alloc(const char* data) { int len = strlen(data) + sizeof(char); char * newData = new char[len]; strcpy(newData, data); return newData; }
3.3 用一个关联度不是很强的char* newData,方便后面函数中再来调用Alloc。
4--> String类 重载赋值函数operator=()
4.0 思考:赋值函数返回一个什么是恰当的? 答:引用,因为后面要用到连续赋值,用来当左值时,返回引用是恰当的。看示例
int num = 1, num1 = 2; num = num1 = 3; //要的是改变之后的值 返回 自身对象
num1在第2行要来当左值,那么 此时替代num1位置的函数,其返回就应当设置为一个引用。
4.0.1 扩展:什么时候函数返回一个对象?
int num = 1, num1 = 2; num = num1 + 3; //要的是改变之前的值
此时代替num1的函数,返回一个临时对象才是正确的。
4.0.2 小结:函数返回怎么设计,主要看代码需要,如果当时要的是改变之后的值,就返回自身;如果当时要的是改变之前的值,就返回临时对象。
int num = 1, num1 = 2; num = ++num1; //要的是改变之后的值 返回 自身对象 num = num1++; //要的是改变之前的值 返回 临时对象
4.1 重写赋值 operator=()
String & String::operator=(const String & other) { delete[]data_; data_ = Alloc(other.data_); return *this; }
4.2 注意点:加入检测,当自我拷贝时,直接返回本身。
String & String::operator=(const String & other) { if (this == &other) //比较地址 { return *this; } delete[]data_; data_ = Alloc(other.data_); return *this; }
4.3 写入其它重载operator=()函数
头文件String.h
//重载赋值 String& operator=(const String &other); String& operator=(const char* str); String& operator=(const int num); String& operator=(const double num);
String.cpp
String & String::operator=(const int num) { char *str = nullptr; itoa(num,str,10); //以十进制显示 delete[]data_; data_= Alloc(str); return *this; }
4.3.0 上面str没有分配空间~!要加一个常量:const int MAXSIZE = 255;
const int MAXSIZE = 255; String & String::operator=(const int num) { char str[MAXSIZE] = {0}; itoa(num,str,10); delete[]data_; data_= Alloc(str); return *this; }
4.3.1 const int MAXSIZE = 255; 这句代码写在头文件,又写在cpp里面,会发生重定义错误。(头文件里面的代码将会被展开来的。)所以,全局变量不要写在头文件里。
4.3.2 在需要外部使用的时候,用extern来导出。
4.4 注意点:1 itoa()这个函数,所在的头文件在<cstdlib>里面。 2 itoa()函数的用法 :里面有3个参数,第1个是value,第2个是Buffer, 第3个是进制。
4.5 注意点:网上有言int类型只需10个就足够,那是32位系统中int的位数,如果平台一换,系统一换,10个就远远不够用。
4.6 注意点:寻找函数:itof()。
5--> String类 重载操作符函数operator+()
5.0 写+之前,先写+= 。
String String::operator+(const String& other) //利用重载的operator+=() { String temp = *this; temp += other; return temp; } String & String::operator+=(const String & other) //operator+=() { int len = strlen(data_) + sizeof(other.data_); char *newData = new char[len]; strcpy(newData, data_); strcat(newData, other.data_); data_ = newData; return *this; }
6--> String类 操作符operator[]()
6.0 思考:返回应该是引用,参数应该是unsigned int。
char& String::operator[](unsigned int index) { return data_[index]; }
6.1 课堂问答,关于双目运算中,参数前后的问答。有些类型的参数在前时,要用友元重写操作符。
6.2 课堂问答小结:所有操作符,在C++中的本质是:用当前对象,调用operator重载函数。
6.3 小结续:String demo = "Hello"; demo = "Love " + "Mark" + demo; 编译不能通过。参数过多,无法实现。3个参数是不存在的。
6.4 友元要注意:1 运算的数量 + - 2元 1元 2 连加的操作时,要注意对象本身要支持此操作符。