C++编程(四) —— OOP

文章目录

  • 前言
  • 一、this指针
  • 二、构造和析构
  • 三、深拷贝浅拷贝
    • 浅拷贝
    • 深拷贝
  • 编程实践


前言

什么是OOP思想?

OOP语言的四大特征:
抽象,封装/隐藏,继承,多态

一、this指针

this指针=》类=》很多对象
一套成员方法是如何处理多个对象的?具体而言:

成员方法show() => 怎么知道处理哪个对象的信息? 答:具体对象的this指针。
成员方法init (name,price,amount) => 怎么知道把信息初始化给哪一个对象的? 答:具体对象的this指针。
类的成员方法一经编译,所有的方法参数,都会加一个this指针,接收调用该方法的对象的地址。

当有一实例化对象good1,good1调用成员方法show时,编译器会自动加入参数:
show(&good1);
当有一实例化对象good2,good2调用成员方法init时,编译器会自动加入参数:
init(&good2,name,price,amount);

二、构造和析构

OOP实现顺序栈。

要想实现一个类,共有一般放成员方法,私有一般放成员变量。
因为顺序栈要实现内存的扩充,所以需要使用数组指针。

class SeqStack
{
public:
	// 构造函数 
    SeqStack(int size = 10)
    {
        cout << " SeqStack() Ptr" << this << endl;
        _pstack = new int[size];
        _top = -1;
        _size = size;
    }

    // 自定义的拷贝构造函数 《= 对象的浅拷贝现在有问题了
    SeqStack(const SeqStack &src)
    {
        cout << "SeqStack(const SeqStack &src)" << endl;
        _pstack = new int[src._size];
        for (int i = 0; i <= src._top; ++i)
        {
            _pstack[i] = src._pstack[i];
        }
        _top = src._top;
        _size = src._size;
    }

    // 析构函数
    ~SeqStack() 
    {
        cout << this << " ~SeqStack()" << endl;
        delete[]_pstack;
        _pstack = nullptr;
    }
	
	void push(int val)
    {
        if (full()) resize();  // resize不想要用户调他
        _top++;
        _pstack[top] = val;
    }
    void pop()
    {
        if (empty()) return;
        --_top;
    }
    int top() { return _pstack[_top]; }
    bool empty() { return _top == -1; }
    bool full() { return _top == _size - 1; }


private:
	int *_pstack;   // 动态开辟数组,存储顺序栈的元素
	int _top;		// 指向栈顶元素的位置
	int _size;		// 数组扩容的总大小
	void resize()
    {
        int *ptmp = new int[_size * 2];
        for (int i = 0; i < _size; ++i)
        {
            ptmp[i] = _pstack[i];
        } // memcpy(ptmp, _pstack, sizeof(int)*_size); realloc
        delete[]_pstack;
        _pstack = ptmp;
        _size *= 2;
    }
}

析构函数如果需要释放一个指向数组的指针,需要中括号[]

delete[]_pstack;

调用:

SeqStack ps1;  // .data段开辟内存,程序结束才进行析构
int main() 
{
	SeqStack *ps2 = new SeqStack(60);   //heap段开辟内存
	ps2->push(70);
	ps2->pop();
	cout << ps2->top() << endl;
	delete ps2;  
	
	// 1、在stack段开辟内存  2、调用构造
	SeqStack ps3;
}

调用总结:
1、全局对象
全局对象定义时进行构造,程序结束才析构。
2、heap段对象
new时:1、malloc内存开辟;2、构造函数。
delete时:1、析构函数;2、free(ps2)
3、stack段对象
定义时构造,出作用域析构。

三、深拷贝浅拷贝

浅拷贝

内存的拷贝

int main() 
{
	SeqStack s1(10);
	SeqStack s2 = s1;   // #1
	SeqStack s3(s1);    // #2
	// #1和#2都是调用拷贝构造
	// #1相当于 s2.operator=(s1)
	// void operator=(const SeqStack& src)
}

C++编程(四) —— OOP_第1张图片
什么时候不能浅拷贝:
对象中的成员变量指针,指向了对象外的资源。所以在浅拷贝时,两个对象的指针就指向了同一块资源

深拷贝

当对象占用外部资源时,使用深拷贝,使得其各自占有各自的资源。
C++编程(四) —— OOP_第2张图片

class SeqStack
{
public:
	// 构造函数 
    SeqStack(int size = 10) {}

    // 自定义的拷贝构造函数 《= 对象的浅拷贝现在有问题了
    SeqStack(const SeqStack &src)
    {
        cout << "SeqStack(const SeqStack &src)" << endl;
        _pstack = new int[src._size];
        for (int i = 0; i <= src._top; ++i)
        {
            _pstack[i] = src._pstack[i];
        }
        _top = src._top;
        _size = src._size;
    }
    void operator= (const SeqStack &src)
    {
    	if(this == &src) return;  // 防止自赋值
    	delele[] _pstack;  // 重要,需要先释放当前对象的内存
    	
    	_pstack = new int[src.size];
    	for (int i = 0; i <= src._top; ++i)
        {
            _pstack[i] = src._pstack[i];
        }
        _top = src._top;
        _size = src._size;
	}

    // 析构函数
    ~SeqStack() {}
	
	void push(int val){}
    void pop(){}
    int top() {}
    bool empty() {}
    bool full() {}
private:
	int *_pstack;   // 动态开辟数组,存储顺序栈的元素
	int _top;		// 指向栈顶元素的位置
	int _size;		// 数组扩容的总大小
	void resize(){}
}

注意:
1、为什么拷贝时要使用for循环,而不是直接

memcpy(ptmp, _pstack, sizeof(int)*_size);

使用memcpy仍然是浅拷贝。
2、赋值构造函数时,有一个释放当前对象的内存的操作。对应shared_ptr引l用计数在赋值的时候也会减1(随即会加1)
3、赋值构造函数应该防止s1 = s1这种自赋值的情况。

编程实践

Mystring:
注意:
1、在普通构造函数中,无论如何也要开辟一块内存。保证对象是有一个有效的对象。
2、普通构造函数的输入为const char *str,因为在新版编译器中不让普通的指针指向常量字符串

char *p = "hello world";  // 不能修改*p
现在编译器都不让普通的指针指向常量字符串,应该这么写:
const char *p = "hello world";
#include 

using namespace std;

class String
{
public:
    String(const char *str = nullptr) 
    {
        if (str != nullptr)
        {
            m_data = new char[strlen(str) + 1];  // 加上'/0'
            strcpy(this->m_data, str);
        }
        else
        {
            m_data = new char[1]; // new char;
            *m_data = '\0'; // 0
        }
    }
    String(const String &other)
    {
        m_data = new char[strlen(other.m_data) + 1]; // 深拷贝
        strcpy(m_data, other.m_data);
    }
    ~String(void)  // 析构函数
    {
        delete[]m_data;
        m_data = nullptr;
    }
    // String& 是为了支持连续的operator=赋值操作
    String& operator=(const String &other) // 赋值重载函数
    {
        if (this == &other)
        {
            return *this;  // str1
        }

        delete[]m_data;    // 析构

        m_data = new char[strlen(other.m_data) + 1];
        strcpy(m_data, other.m_data);
        return *this; // str1
    }
private:
    char *m_data; // 用于保存字符串
};
int main()
{
    // 调用带const char*参数的构造函数
    String str1;
    String str2("hello");
    String str3 = "world";

    // 调用拷贝构造函数
    String str4 = str3;
    String str5(str3);

    // 调用赋值重载函数
    /*
    str1 = str2
    str1.operator=(str2) => str1
    str3 = str1
    */
    str3 = str1 = str2;

    return 0;
}

关于赋值重载函数中返回值为引用的情况:
如果一个方法的返回值是一个引用,那么它返回的是某个对象的引用,而不是对象本身的副本。这意味着通过引用返回的值与原始对象共享同一块内存,对返回值的修改将直接影响原始对象。
返回引用的方法有以下几个作用:

  • 避免对象的复制:通过返回引用而不是副本,可以避免在函数调用中进行大量的复制操作,提高性能。特别是对于大型对象或复杂的数据结构,避免复制可以节省时间和内存。
  • 允许链式操作:返回引用可以使多个方法调用可以连接在一起形成链式操作。这种方法通常用于实现流畅的、易读的代码,比如在输入/输出流中使用<<和>>运算符。
  • 允许对返回值进行修改:返回引用允许在函数外部对返回值进行修改。这可以用于实现函数返回某个对象的引用,以便可以直接修改该对象的状态。

需要注意的是,返回引用时需要确保返回的引用在函数调用结束后仍然有效。一般来说,应该返回指向静态、全局、或动态分配的对象的引用,而不是指向局部对象的引用,以避免出现悬垂引用(dangling references)的问题。

完整的Mystring重写见:参考链接

你可能感兴趣的:(c++,开发语言)