没有输出的输入是不完整的

0%

Class-with-pointer-mString-Houjie-Bilibili

在Bilibili跟着侯捷老师学C++系列第二篇,带指针的类mString类。
人生得遇良师,实属大幸,侯捷老师就属于这种老师,再夸一遍!!

因为C++可以编写高效的程序,所以对于C++自己一定要好好掌握,个人看法,你如果能学会C++,其他语言都将不在话下。然后刚好碰到一个非常好的教程,有一位非常棒的老师-侯捷老师,有一个好的平台-Bilibili,还有非常好的训练场地——leetcode,所以此时不学什么时候学呢?

教程

侯捷C++手把手教学(上),适合新手

代码文件

mstring.hpp

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
49
50
51
52
53
54
55
56
57
58
59
#ifndef mstring_hpp
#define mstring_hpp

#include <stdio.h>
#include <cstring>
#include <iostream>
using namespace std;
class mstring{
public:
mstring(const char* a = 0);
mstring(const mstring& str);
mstring& operator =(const mstring& str);
~mstring();
char* get_c_str() const {return mdata;}

private:
char* mdata;

};

inline
mstring::mstring(const char* a){
if(a){
mdata = new char[strlen(a)+1];
strcpy(mdata, a);
}else{
mdata = new char[1];
*mdata = '\0';
}
};

inline
mstring::mstring(const mstring& str){
mdata = new char[std::strlen(str.mdata)+1];
strcpy(mdata, str.mdata);
};

inline
mstring& mstring::operator=(const mstring& str){
if(this == &str){
return *this;
}
char* newdata = new char[strlen(str.mdata)+1];
strcpy(newdata, str.mdata);
delete[] mdata;
mdata = newdata;
return *this;
}

inline
mstring::~mstring(){
delete[] mdata;
};

inline ostream& operator << (ostream& os,const mstring& str){
return os<<str.get_c_str();
};

#endif /* mstring_hpp */

mstring.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "mstring.hpp"
#include <iostream>
using namespace std;
int main(){
mstring s1;
cout<<"s1 = "<<s1<<endl;
mstring s2("hello");
cout<<"s2 = "<<s2<<endl;
s1 = s2;
cout<<"s1 = "<<s1<<endl;
mstring s3(s2);
cout<<"s3 = "<<s3<<endl;

mstring *p = new mstring("world");
cout<<"*p = "<<*p<<endl;
delete p;
}

运行结果

1
2
3
4
5
6
s1 = 
s2 = hello
s1 = hello
s3 = hello
*p = world
Program ended with exit code: 0

细节剖析

  1. 构造函数
1
mstring(const char* a = 0);

构造函数的名称和类名一致,没有返回值,参数可以设置默认参数(实际实现的时候就不需要再说明默认参数了)
2. 拷贝构造

1
mstring(const mstring& str);

接收一个同种类型的对象作为自己的参数,然后产生一个新的对象。

这里要注意深复制和浅复制的区别,深复制就是会新创建一个对象,这个对象有属于自己的内存空间,只是值和原来的是一致的。浅复制是说两个对象指向同一个内存空间,所以一旦有一个更改,另一个肯定会被更改,很危险。
3. 拷贝赋值

1
mstring& operator =(const mstring& str);

这里首先要注意的是我们重载了运算符=,然后是当前类的方法。其次是我们传入的参数,我们不会修改,所以设置为const,是通过引用传进来的,返回值同样是一个mstring对象,我们通过引用返回。

在非静态成员函数中,this指向当前对象,所以可以用this == &str判断是否是自我赋值,是的话就直接返回。其他情况下,先申请新空间并复制str的内容,再删除旧数据,最后更新mdata。注意这个顺序:既要释放旧空间来避免内存泄漏,也要在new抛出异常时保留原来的数据。
4. 析构函数

1
~mstring();

析构函数是指当对象生命周期结束之后会自动调用当前对象的析构函数,主要负责释放通过new关键字请求得到的内存空间。如果不自行解决,那么new出来的这块空间就会变成无主空间,会造成内存泄露。
5. 对于像本例这样直接拥有动态内存的类,要一起考虑拷贝构造、拷贝赋值和析构函数;只是保存非拥有型指针时,不一定需要自行实现这三个函数。
6. 其他成员函数的编写

1
char* get_c_str() const {return mdata;}

首先这个函数可以认为是一个辅助函数,方便之后重载<< 符号的,因为ostream已经被重载过所以可以输出char类型的指针所指向的数据,所以我们这里单独写一个用于输出。
其次要注意这个函数有个const,对于类对象本身没有更改的方法都要写上这个const。
然后要注意这里的返回值类型,是一个char类型的指针。
7. array new 一定要搭配一个array delete

1
2
3
4
5
6
7
8
9
10
inline
mstring::mstring(const char* a){
if(a){
mdata = new char[strlen(a)+1];
strcpy(mdata, a);
}else{
mdata = new char[1];
*mdata = '\0';
}
};
1
2
3
4
inline
mstring::~mstring(){
delete[] mdata;
};

在构造函数中是创建了一个char类型的数组,那么在析构函数中也要以同样的方式进行释放。
8. 关于栈存储
Stack通常用来保存函数调用的参数、返回地址和普通非static局部对象,优化后有些局部对象也可能放进寄存器。这里要注意,自动存储期的局部对象会在离开作用域时销毁,所以我们上一节才说,不能返回这类local对象的引用或者指针,而应该按值返回。static局部对象和new创建的对象有不同的存储期,下面分别举例。
9. 关于堆存储
Heap,或者叫做system heap,是指由操作系统提供的一块global的内存空间,程序可以通过new来动态分配获得若干区块。这样就需要自己来动手释放掉。
10. stack object的生命周期

1
2
3
{
Complex c1(1,2);
}

在一个大括号(作用域)中创建的对象也就是所谓的local对象,比如c1,在作用域结束的时候就会被自动清理。
11. static local object的生命周期

1
2
3
{
static Complex c2(1,2);
}

当一个局部变量被static关键字修饰的时候,比如c2,它就是一个静态对象,其生命在作用域结束之后仍然存在,直到整个程序结束。
12. global objects的生命周期

1
2
3
4
Complex c3(1,2);
int main(){

}

全局对象,写在{}之外的对象,比如c3,其生命周期是整个程序。这个经常在算法题目中出现。
13. heap objects的生命周期

1
2
3
4
{
Complex* p = new Complex(1,2);
delete p;
}

这里p本身是局部指针,离开作用域时它的生命周期就结束;p指向的Complex对象才是动态分配的对象,需要通过delete p销毁。如果只离开作用域而不delete,指针消失了,动态对象却不会自动释放,就会造成内存泄漏。
14. new关键字:先分配内存,然后调用构造函数
15. delete关键字:先调用析构函数,然后再释放内存。