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语言中惯用的编程思想