第36课 经典问题解析三

1、关于赋值的疑问

    1.1、 疑问
        1.1.1、 编译器为每个类 默认重载了赋值操作符
        1.1.2、 默认的赋值操作符仅完成 浅拷贝
        1.1.3、 当需要进行 深拷贝 时必须 重载赋值操作符
        1.1.4、 赋值操作符与拷贝构造函数有 相同的 存在意义

/**************** 默认赋值操作符重载 ***********/
#include <iostream>
using namespace std;
class Test
{
    int* m_pointer;
public:
    Test()
    {
        m_pointer = NULL; //初始化为NULL
    }


    Test(int i)
    {
        m_pointer = new int(i); //从堆空间申请4字节内存,m_pointer指向这内存,并将这片内存指向的值赋值为i
    }

    //如果在涉及到内存申请等,内存方面的问题,那么拷贝构造函数和=赋值操作符必然要重载实现深拷贝,默认的浅拷贝只是简单的赋值,地址都没有重新申请

    Test(const Test& obj)//这是拷贝构造函数,赋值操作符和其具有相同的存在意义
    {//拷贝构造函数会在声明初始化另一个对象时调用,而赋值=会在直接赋值时调用
        this->m_pointer = new int(*obj.m_pointer);//用已存在的对象的成员变量的值,初始化新成员变量地址的值。
        cout << "调用了拷贝构造函数" << endl;
    }

    //重载赋值操作符,赋值4个注意点:
    //1。返回值为引用(因为可以支持连续赋值)。 2。参数是const引用的对象 3. 避免自赋值(加判断this != &obj)。;
    Test& operator= (const Test& obj)
    {//当需要深拷贝时,需重载赋值操作符。进行内存的深层次拷贝。
        if(this != &obj)
    {
        delete m_pointer; //为什么要先释放。先释放这个对象的成员变量的地址值。
        m_pointer = new int(*obj.m_pointer);//指向新地址,并且赋值
    }
        return *this; //引用和具体的值有啥区别:返回引用是避免生成临时对象。
    }

    void print()
    {
        cout << "m_pointer = " << hex << m_pointer << endl; //打印地址值
    }

    ~Test()//四个对象,调用四次构造函数,类似于栈,后进先出的销毁顺序
    {
        cout << "~Test()" << endl;
        delete m_pointer;
    }

};
    int main()
    {
        Test t1 = 1; //Test t1(1); 带参构造函数
        Test t2;
        //Test t3 = t2;//直接这样使用会段错误,因为t2的*m_pointer没有指向任何值。解引用就会报错。
        t2 = t1; //t2.operator=(t1) , 赋值操作符,这是赋值
        t1.print(); //t1.m_pointer != t2.m_pointer;
        Test t3 = t2;//调用拷贝构造函数。这是用一个对象来初始化另一个对象。
        t2.print();
        return 0;
    }

/*
总结:1、重载赋值操作符=的四要素
2、分清楚什么时候调用拷贝构造函数,什么时候调用赋值=操作符。
3、什么时候使用深拷贝,浅拷贝,深拷贝分配不同空间,默认的浅拷贝只是简单的赋值,并没有重新申请地址。所有属性的拷贝
*/




//数组类的优化

//IntArray.h
#ifndef _TEST_H_
#define _TEST_H_
class IntArray
{
private:
    int m_length;
    int* m_pointer;
    IntArray(int leng);
    bool construct();   //二阶构造
public:
    static IntArray* NewInstance(int length);//提供创建对象的函数,二阶构造核心
    ~IntArray();
    int length();
    bool get(int index, int& value);
    bool set(int index, int value);
    IntArray& operator =(const IntArray& t);
    int& operator[] (int pos);
    IntArray& self();//因为该类只能在堆空间上创建对象,为了操作方便通过这里返回对象的引用,可以避开操作指针的麻烦。
};
#endif

 
 
//IntArray.cpp

#include "test.h"
#include <cstdlib>
int m_length;
int* m_pointer;
IntArray::IntArray(int leng)
{
    m_length = leng;
}
bool IntArray::construct()   //二阶构造
{
    bool ret = true;
    m_pointer = new int[m_length];  //只能申请int类型的数组类 。
    if(m_pointer)
    {
        for(int i=0; i<m_length; i++)
        {
            m_pointer[i] = 0;   //将数组初始化为0;
        }
    }
    else
    {
        delete[] m_pointer;
        ret = false;
    }
    return ret;
}
//函数是从属于类的,直接类名调用。
IntArray* IntArray::NewInstance(int length)//提供创建对象的函数,二阶构造核心
{
    IntArray* ret = new IntArray(length);//获得这个类
    if(!((ret) && (ret->construct())))    //调用二阶,构造数组
    {
        delete ret;
        ret = NULL;
    }
    return ret;
}
IntArray::~IntArray()
{
    if(!m_pointer)
    {
        delete[] m_pointer;
    }
}
int IntArray::length()
{
    return m_length;
}
bool IntArray::get(int index, int& value)
{
    bool ret = true;
    if((0<=index)&&(index<m_length))
    {
        value = m_pointer[index];
    }
    else
    {
        ret = false;
    }
    return ret;
}
bool IntArray::set(int index, int value)
{
    bool ret = ((0<=index)&&(index < m_length));
    if(ret)
    {
        m_pointer[index] = value;
    }
    return ret;
}
//重载数组类的=赋值操作符,所有独有的属性都应该赋值进去。
IntArray& IntArray::operator= (const IntArray& t)//等下去掉const试试。
{
    if(this != &t)//避免自赋值
    {
        int* pointer = new int[t.m_length];//申请m_length个int类型大小的空间。
        if(pointer)
        {
            for(int i=0; i<t.m_length; i++)    //赋值,将t对象里面的数组元素,赋给this
            {
                pointer[i] = t.m_pointer[i];
            }
            this->m_length = t.m_length;
            delete[] this->m_pointer;//释放掉这个对象当前的指针。
            this->m_pointer = pointer;//this,编译期间就决定的。
        }
    }
    return *this;
}
int& IntArray::operator[] (int pos)
{
    return m_pointer[pos];//返回引用可以做左值,重载操作符。
}
IntArray& IntArray::self()
{
    return *this;   //奇淫技巧,谁调用这个函数,将返回谁的引用。避免直接操作指针的麻烦。
}

//main.cpp

#include <iostream>
#include "test.h"
using namespace std;
int main()
{
    IntArray* a = IntArray::NewInstance(5);
    IntArray* b = IntArray::NewInstance(10);
    IntArray& array = a->self();//利用对象的this关键字,获得引用
    IntArray& brray = b->self();
    cout << "array.length() = " << array.length() << endl;
    cout << "brray.length() = " << brray.length() << endl;
    array = brray;
    cout << endl;
    cout << "array.length() = " << array.length() << endl;
    cout << "brray.length() = " << brray.length() << endl;
    delete b;
    delete a;
    return 0;
}




1.2、 编译器默认提供的函数(就是说一个什么都没有的类里面有这些东西。)
    1.2.1、 不带参的构造函数
    1.2.2、 拷贝构造函数
    1.2.3、 默认的赋值操作符
    1.2.4、 析构函数

1.3、 一般原则:重载赋值操作符,必然需要实现深拷贝,因为默认操作符已经是浅拷贝了。

2、关于string的疑问

    2.1、string类内部维护了一个指向数据的char*指针(m_cstr),这里用于存放字符串数据的堆空间地址。因字符串操作(如复制、合并、追加等),所
这个指针可能在程序运行的过程中发生改变

#include <iostream>
#include <cstring>
using namespace std;

/*
对于c_str的解释:
c_str返回一个指向C字符串的指针,(注意const)以const char*的形式返回string内含字符串,内容与本string相同
注意:一定要使用strcpy()函数等来操作c_str()返回的指针,
不要这样使用:
const char* c;
string s = "1234";
c = s.c_str(); //c最后指向的是垃圾,因为s是对象,被析构,内存释放了,c就成了野指针(指向的内容不确定性)
应该这样用:
char c[20];
string s = "1234";
strcpy(c, s.c_str());//直接将指向的值copy过去。
//这样才不会出错,c_str()返回的是一个临时指针,不能对其直接进行赋值操作
const char* 若想直接赋值给char *就需要做一些转换的操作了。可以利用strcpy和strcat
*/

int main()
{
    string s = "12345";

    //程序试图在C的方式访问字符串(不建议这样用!)
    //返回当前字符串的首字符地址。c_str函数的返回值是const char*的。
    const char* p = s.c_str();  //c_str表示C方式的字符串。

    cout << p << endl;//C语言中p就是一个字符串数组名, p代表首元素的首地址。*p代表对数组第一个元素解引用

    for(int i =0; i<5; i++)
    {
        cout << p[i] << endl;//以C的方式访问字符串。
    }

    s.append("abcde");//p成为野指针,因为追加字符串,可能导致堆内存的重新分配,从而m_cstr指的堆内存地址改变了,但p并不知道。

    cout << p << endl;      //仍然指向旧的地址(野指针),p所指向的字符串并没有改变。
    cout<<endl;
    cout<<endl;
    cout << s << endl;   //这样是附加正确的

    cout << "-----另一个例子-----" << endl;

    //因为s_str()返回的是const char* 要是想赋值,增加字符串的话,必须做一些转换,
    string str1("Hello,");
    string str2("World");//const对象的作用,对象里面的成员不可变
    const char* cfirst = str1.c_str();
    const char* csecond = str2.c_str();
    //+1因为最后一个空格是\0
    char *copy = new char[strlen(cfirst) + strlen(csecond) + 1];//申请一片内存,内存大小在括号里
    strcpy(copy, cfirst);
    strcat(copy, csecond);          //作用是连接两个字符串。将csecond指向的字符添加到copy指向的空间中,并且覆盖copy后面的’\0‘
    //str1.operator=(各种类型的参数)。操作符重载赋值操作符=。
    str1 = copy;//这里不理解:指针赋值给对象,和操作符重载有关?编译器优化?
    string t5(copy);
    cout << "t5 = " << t5 << endl;//将copy指向的字符串作为拷贝构造函数的参数,本质上为函数的重载。
    cout << "str1 = " << str1 << endl;//操作符重载的自动识别,因为重载了多个类型。
    delete copy;


    return 0;
}



    2.2、string类内部维护了一个m_length的变量,用于指示字符串中字符的个数。当使用C的方式使用string对象时,这个m_length可能不会自动更新

#include <iostream>
using namespace std;

int main()
{
    const char* p = "12345";    //C语言方式的字符串
    string s = "";  //s.length() == 0;
    
    //保留一定量内存以容纳一定数量的字符。
    s.reserve(10);  //s.length() == 0;
    
    //不要使用C语言的方式操作C++中的字符串!!!!!!!否则string 类的对象内部一些成员不会更新。
    for(int i=0; i<5; i++)
    {
        s[i] = p[i];//注意,虽然此时s对象中的字符串内存确实被赋新的值了。但用这种方式赋值相当于只是通过指针赋值, s.length()不会自动更新,即仍为0;
    }
    
    cout << s.length() << endl; //0
    cout << s.empty() << endl;  //1,表示为空的
    cout<< s[4] << endl;
    cout << s << endl;  //这里将不会有任何输出,因为s.length=0;,需要用指针方式访问
    cout << p << endl;  //
    
    return 0;
//不要乱用C语言字符串指针和C++的字符串string的问题。会导致string中某些成员方法无法更新。
}

3、小结

    3.1、在需要进行深拷贝的时候必须重载赋值操作符
    3.2、赋值操作符和拷贝构造函数有同等重要的意义
    3.3、string类通过一个数据空间保存字符数据
    3.4、string类通过一个成员变量保存当前字符串的长度(通过字符串操作时,会更新这个变量。)
    3.5、C++开发时尽量避开C语言中惯用的编程思想







评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值