您的位置:首页 > 编程语言 > C语言/C++

C++技术点积累(2)——拷贝构造函数、深拷贝、浅拷贝

2015-09-07 20:55 483 查看
C++技术点积累:

1、因为在A类外的B类是无法使用A类的private成的,但是有的时候又有这种需求(A类和B类有一些业务数据联系),一般我们可以A类中编写一些get()函数,getAx(){ return x;//把A类的成员变量甩出去},这样,我们在B类就可以调用A类的getAx()函数来使用A类的成员变量x。

2、拷贝构造函数:

       (1)拷贝初始化 和 直接初始化

               string dots(10,'s');              //直接初始化

               string nines = string (100,'9');            //拷贝初始化

               string s2 = dots;                                //拷贝初始化

       

      (2)拷贝初始化通常使用拷贝构造函数来完成,在下列情况将发生拷贝初始化:

               A. 用 = 定义变量时发生;

               B. 将 一个对象 作为实参 传递给一个 非引用类型的 形参;

               C. 从一个返回类型为非引用类型的函数 返回一个对象;

               D. 用花括号列表初始化一个数组中的元素 或 一个聚合类中的成员;

        

      (3)拷贝构造函数 的第一个参数 必须是 一个引用类型:
class Foo
{
public:
Foo();             //默认构造函数
Foo(const Foo&);   //拷贝构造函数
}

        拷贝构造函数被用来 初始化非引用类类型参数。 如果拷贝构造函数自己的参数不是引用类型,则调用永远也无法成功——为了调用拷贝构造函数,我们必须拷贝它的实参,但为了拷贝实参,我们又需要调用拷贝构造函数,如此无限循环。——这也就解释了为什么拷贝构造函数自己的参数必须是引用类型。————引用是别名,避免了循环调用拷贝动作!

     (4)拷贝构造函数的四种调用情景(拷贝构造函数调用时机)
class AA
{
public:
AA() //无参构造函数 默认构造函数
{
cout<<"我是构造函数,自动被调用了"<<endl;
}
AA(int _a) //无参构造函数 默认构造函数
{
a = _a;
}
AA(const AA & obj2)  //拷贝构造函数
{
cout<<"我也是构造函数,我是通过另外一个对象obj2,来初始化我自己"<<endl;
a = obj2.a + 10;
}
private:
int a;
}
a.第1种和第2种调用场景:——对对象进行赋值、初始化
#include <iostream>
using namespace std;

class Test4
{
public:
Test4()  //无参数构造函数
{
m_a = 0;
m_b = 0;
cout << "无参数构造函数" << endl;
}

Test4(int a)
{
m_a = a;
m_b = 0;
}

Test4(int a, int b) //有参数构造函数   //3种方法
{
m_a = a;
m_b = b;
cout << "有参数构造函数" << endl;
}

//赋值构造函数 (拷贝构造函数)
Test4(const Test4 & obj)
{
cout << "我也是构造函数 " << endl;
m_b = obj.m_b + 100;
m_a = obj.m_a + 100;
//cout << "m_a" << m_a << " m_a" << m_b << endl;//没有上面的赋值,将输出乱码
}

public:
void printT()
{
cout << "普通成员函数" << endl;
cout << "m_a:" << m_a << "  m_b:" << m_b << endl;
}
private:
int m_a;
int m_b;
};

//赋值构造函数——用1个对象去初始化另外一个对象,对象之间的赋值
void main()
{
Test4 t1(1, 2);
Test4 t0(1, 2);

//赋值=操作 不会调用构造函数,编译器给我们提供的浅copy(可以单步调试检验)
//调用的是:operator=()
t0 = t1; //用t1 给 t0赋值操作 和 初始化是两个不同的概念

//第1种调用方法
Test4 t2 = t1; //用t1 来初始化 t2 ,这个时候会调用t2的构造函数,而且是t2的拷贝构造函数
//虽然看上去是“=”,但是通过构造函数我们实现了初始时个性化的t2
t2.printT();

return;
}

//第二种调用时机
void main()
{
Test4 t1(1, 2);
Test4 t0(1, 2);

//第2种调用方法
Test4 t2(t1);  //用t1对象 初始化 t2对象
t2.printT();
}
b.第3种调用场景:——对象做形参,一个对象以值传递的方式传入函数体 
#include <iostream>
using namespace std;

class Location
{
public:
Location(int xx = 0, int yy = 0)
{
X = xx;  Y = yy;  cout << "Constructor Object.\n";
}

//copy构造函数,完成对象的初始化
Location(const Location & obj)
{
X = obj.X;
Y = obj.Y;
}

~Location()
{
cout << X << "," << Y << ":" << " Object destroyed." << endl;
}
int GetX() { return X; }
int GetY() { return Y; }
private:
int  X, Y;
};

//业务函数  形参是一个元素
void f(Location p)
{
cout << p.GetX() << endl;
}

void playobj()
{
Location  a(1, 2);

//调用构造函数
Location  b = a;//C++编译器会去自动调用copy构造函数
cout << "b对象已经初始化完毕" << endl;

//第三种调用情景
f(b); //b实参去初始化形参p,会调用copy构造函数
}

void main()
{
playobj();
return;
}
c.第4种调用场景:——一个对象以值传递的方式从函数返回,返回一个匿名对象
#include <iostream>
using namespace std;

class Location
{
public:
Location(int xx = 0, int yy = 0)
{
X = xx;
Y = yy;
cout << "Constructor Object.\n";
}

//copy构造函数  完成对象的初始化
Location(const Location & obj)
{
X = obj.X;
Y = obj.Y;
}
~Location()
{
cout << X << "," << Y << "   Object destroyed." << endl;
}
int GetX() { return X; }
int GetY() { return Y; }
private:
int  X, Y;
};

//g函数 返回一个元素(objplay2()函数)
//结论1 : 函数的返回值是一个元素(复杂类型的), 返回的是一个新的匿名对象(所以会调用匿名对象类的copy构造函数)

//结论2: 有关 匿名对象的去和留
//如果用匿名对象  初始化 另外一个同类型的对象(objplay3()), 匿名对象 转成有名对象
//如果用匿名对象  赋值给 另外一个同类型的对象(objplay4()), 匿名对象 被析构

//第四种调用场景
//返回一个新对象——匿名对象(没有名字),所以会去调用匿名对象类的构造函数
Location g()
{
Location A(1, 2);
return A;//返回一个对象,这个时候会去调用拷贝构造函数
//完了以后直接先去调用一次析构函数析构A,但是匿名对象还在
}

void objplay2()
{
g();
}

void objplay3()
{
Location m = g();//用匿名对象初始化m 此时c++编译器 直接把匿名对转成m,从匿名转成有名字了m(扶正)
//先去牺牲了空间,但是提高了执行效率
printf("用匿名对象直接初始化,被扶正,不会析构掉\n");
cout << m.GetX() << endl;
}

void objplay4()
{
Location m2(1, 2);
m2 = g();//用匿名对象 赋值给 m2后, 匿名对象被析构
printf("因为用匿名对象 =给m2,匿名对象,被析构\n");
cout << m2.GetX() << endl;
}

void main()
{
//objplay2();
//objplay3();
objplay4();
cout << "hello..." << endl;
system("pause");
return;
}

3、深拷贝和浅拷贝

(1)什么时候会出现浅拷贝?

       在某些状况下,类内成员变量需要动态开辟堆内存,如果实行浅拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。

  深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,而是指向了同一块内存,就是浅拷贝。

#define  _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;

class  Name
{
public:
Name(const char *myp)
{
m_len = strlen(myp);
m_p =(char *) malloc(m_len + 1); //在堆上分配内存
strcpy(m_p, myp);
}

~Name()
{
if (m_p != NULL)
{
free(m_p);
m_p = NULL;
m_len = 0;
}
}
private:
char *m_p;   //类中有指针
int m_len;
};

//对象析构的时候,程序当掉
void objplaymain()
{
Name obj1("abcdefg");
Name obj2 = obj1; //出错!对象析构的时候程序当掉!
                 //调用C++编译器提供的默认拷贝构造函数———浅拷贝
               //浅拷贝——仅仅是指针变量进行了赋值,而指针所指的内存空间没有拷贝过去
                 //先析构obj2,再析构obj1,同一块内存空间析构了两次!
}

void main()
{
objplaymain();
}



        这种情况下,使用C++编译器提供的默认构造函数时,只是将指针变量进行了赋值,并没有对obj2的*p重新开辟内存空间将数据拷贝过去!——浅拷贝

       解决方案: 手工的编写拷贝构造函数, 使用深copy(蓝色线条
Name(const Name& obj1)
{
m_len = obj1.m_len;
m_p = (char *)malloc(m_len + 1);
strcpy(m_p, obj1.m_p);
}

补充:C++编译器提供的“等号操作”,也属 浅拷贝
void objplaymain()
{
Name obj1("abcdefg");
Name obj3("obj3");
obj3 = obj1;  // C++编译器提供的“等号操作”, 也属  浅拷贝,——这个时候需要重载“等号操作”
}



       简单分析:执行“等号操作”时,也是把指针变量赋值过去了,在析构时,还是析构了两次!并且还出现了内存泄露——原来obj3的内存空间无法释放!具体代码见:http://blog.csdn.net/songshimvp1/article/details/48273653 ——2、 3)重载=赋值运算符

附录:

        尽管编译器能为我们合成拷贝、赋值、和销毁的操作,但是对于某些类来说合成的版本无法正常工作。特别是,如上面例子当类需要分配类对象之外的资源时,合成的版本往往会失效。

        不过值得注意的是,如果类包含vector 或者 string成员,则其拷贝、赋值、和销毁的合成版本能够正常工作。当我们对含有vector成员的对象执行拷贝 或者 赋值 操作时,vector会拷贝或者赋值成员中的元素,当对象被销毁时,将销毁vector对象,也就是依次销毁vector中的每一个元素。

       所以,很多需要动态内存的类能(而且应该)使用vector对象或者string对象管理必要的存储空间,使用vector或者string的类能够避免分配和释放内存带来的复杂性。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息