• C++学习:对象的构造


    一、C++的类对象中成员变量的初始值

    程序设计的角度,对象只是变量,因此类的对象也应该有下面的特性:

    1、在栈上创建的对象,成员变量初始值是随机值;

    2、在堆上创建的对象,成员变量初始值为随机值;

    3、在静态存储区创建的对象,成员变量初始值为0。

    程序实例1:局部类对象,成员变量默认初始值

    1. #include
    2. class Test
    3. {
    4. private:
    5. int i;
    6. int j;
    7. public:
    8. int getI() { return i; };
    9. int getJ() { return j; };
    10. };
    11. int main()
    12. {
    13. Test t1;
    14. printf("i = %d\n", t1.getI());
    15. printf("j = %d\n", t1.getJ());
    16. return 0;
    17. }

    输出结果:

    1. i = -858993460
    2. j = -858993460

    结果分析:一看结果就知道是个随机值,因为类是一种特殊的自定义类型,类的对象相当于变量,t1是一个test类对象,定义在main函数的内部,是一个局部变量,局部变量保存在栈中,ti的成员数据的初始值就是一个随机值。

    程序实例2:全局类对象,成员变量默认初始值

    1. #include
    2. class Test
    3. {
    4. private:
    5. int i;
    6. int j;
    7. public:
    8. int getI() { return i; };
    9. int getJ() { return j; };
    10. };
    11. Test t2;
    12. int main()
    13. {
    14. printf("i = %d\n", t2.getI());
    15. printf("j = %d\n", t2.getJ());
    16. return 0;
    17. }

    输出结果:

    1. i = 0
    2. j = 0

    结果分析:因为类是一种特殊的自定义类型,类的对象相当于变量,t2定义在main函数的外面,是一个全局变量,全局变量保存在全局数据区,未初始化的变量,默认初始值为0。

    程序实例3:堆空间申请一个对象

    1. #include
    2. class Test
    3. {
    4. private:
    5. int i;
    6. int j;
    7. public:
    8. int getI() { return i; };
    9. int getJ() { return j; };
    10. };
    11. int main()
    12. {
    13. Test* t3 = new Test;//在堆空间申请一个对象
    14. printf("i = %d\n", t3->getI());
    15. printf("j = %d\n", t3->getJ());
    16. delete t3;
    17. return 0;
    18. }

    输出结果:

    1. i = -842150451
    2. j = -842150451

    结果分析: t3是在堆空间申请的对象,成员变量的默认值是随机值;

    二、对象的初始化(构造函数)

    通过前面的3段代码,我们可以看出,类对象定义在不同的区域,默认值是不一样的,这个随机性不是一个好事,写代码还是严谨一些比较好。因此需要想办法把每个对象都初始化一个特定的值。

    方案1:在类中定义一个成员函数,这个函数专门用来初始化类中的成员变量initialize,对象创建后立即调用initialize函数进行初始化。

    程序实例4:创建初始化函数,在定义了对象之后马上调用初始化函数。

    1. #include
    2. class Test
    3. {
    4. private:
    5. int i;
    6. int j;
    7. public:
    8. int getI() { return i; };
    9. int getJ() { return j; };
    10. void initialize()
    11. {
    12. i = 1;
    13. j = 2;
    14. }
    15. };
    16. int main()
    17. {
    18. Test t1; //定义一个类对象
    19. t1.initialize(); //调用类对象的初始化函数
    20. printf("i = %d\n", t1.getI());
    21. printf("j = %d\n", t1.getJ());
    22. return 0;
    23. }

    输出结果:

    1. i = 1
    2. j = 2

    结果分析:显然,调用initialize函数可以将对象的成员变量初始化。但是总是手动调用初始化函数,万一忘记了怎么办,太不智能了,应该有更好的办法。

    解决方案2:构造函数

    C++中可以定义与类名相同的特殊成员函数,这个特殊的成员函数就是构造函数;

    构造函数可以像解决方案1中的初始化函数initialize一样,将对象初始化;构造函数在对象定义的时候自动被调用,不用自己手动的调用,显然这样更方便。

    程序实例5:构造函数的自动调用

    1. #include
    2. class Test
    3. {
    4. private:
    5. int i;
    6. int j;
    7. public:
    8. int getI() { return i; };
    9. int getJ() { return j; };
    10. Test() //定义构造函数,与类同名,没有返回值
    11. {
    12. i = 3;
    13. j = 4;
    14. printf("调用了构造函数Test()\n");
    15. }
    16. };
    17. int main()
    18. {
    19. Test t1; //定义一个类对象,自动调用了构造函数
    20. printf("i = %d\n", t1.getI());
    21. printf("j = %d\n", t1.getJ());
    22. return 0;
    23. }

    输出结果:

    1. 调用了构造函数Test()
    2. i = 3
    3. j = 4

    结果分析:我们只是定义了一个构造函数,构造函数将t1对象的成员变量i和j初始化,但是我们在t1被定义之后,并没有显式的调用构造函数,为什么输出结果与定义构造函数里面的值是一样的?因为类对象在定义的时候,编译器自动的调用了构造函数,只是你看不到而已。 

    三、构造函数的特征

    1、构造函数用于对象的初始化;

    2、构造函数与类同名

    3、构造函数与类同名且没有返回值

    4、构造函数在对象被定义时自动被调用

    带参数的构造函数

    构造函数可以根据需要定义参数;

    一个类中可以存在多个重载的构造函数;

    构造函数的重载遵循C++的重载规则;

    带参构造函数的意义在于,可以给类对象不同的初始化值;就像买汽车一样,有不同的配置,你选什么样的配置,交付到你手上就是什么样的配置,这个配置就是初始化的过程,不同的配置就好比不同的构造函数。

    需要注意的是,对象定义与声明是不一样的概念

    对象的定义:申请对象的存储空间,并调用构造函数;

    对象的声明:告诉编译器存在这样一个对象;

    构造函数的自动调用

    一般情况下,构造函数在定义的时候被自动调用,但是在一些特殊情况下,需要手动调用构造函数

    程序实例6:构造函数的自动调用

    1. #include
    2. class Test
    3. {
    4. public:
    5. //无参的构造函数
    6. Test()
    7. {
    8. printf("using Test()\n");
    9. }
    10. //带参数的构造函数
    11. Test(int v)
    12. {
    13. printf("using Test(int v), v = %d\n",v);
    14. }
    15. };
    16. int main()
    17. {
    18. Test t1; //调用Test()
    19. Test t2(2); //调用Test(int v)
    20. Test t3 = 3;//调用Test(int v)
    21. return 0;
    22. }

    输出结果:

    1. using Test()
    2. using Test(int v), v = 2
    3. using Test(int v), v = 3

    结果分析:

    对象t1 调用的是无参构造函数;

    定义对象t2的时候,t2后面还有括号,括号里面还有一个参数2;这其实是在告诉编译器,定义了一个对象,要调用构造函数Test(int v)来初始化它,初始化的值是2;

    定义对象t3的时候更奇怪,Test t3 = 3;有点像C语言的写法,定义一个变量之后马上初始化一个值,但是我们知道t3的类型是Test,而3的类型是int,这里面涉及到类型的默认转换,C++编译器总觉得自己很牛逼,你写的代码不管怎么垃圾,它总是把你往好的方面去想,还自己想尽办法来证明你写的语句没问题。最终的结果就是t3也调用构造函数Test(int v)来初始化它,初始化值是3。

    构造函数的手动调用

    在生活中,我们买车的时候,还是要自己先确定车的配置之后,再来讨价还价,顺便加装一些东西吧。很少有这种土豪直接把钱往销售身上一甩,说你看看这么多钱够买什么配置的车,随便给我来一辆,销售就喜欢这样的土豪。土豪的做法就相当于自动调用构造函数,正常人还是希望自己手动的来调用构造函数,这样做事明明白白,减少纠纷。

    程序实例7:构造函数的手动调用,创建对象数组

    1. #include
    2. using namespace std;
    3. class Test
    4. {
    5. private:
    6. int m_value;
    7. public:
    8. //定义一个无参构造函数
    9. Test()
    10. {
    11. cout << "using Test()\n";
    12. m_value = 0;
    13. }
    14. //定义一个带参数的构造函数
    15. Test(int v)
    16. {
    17. cout << "using Test(int v), v = "<< v << endl;;
    18. m_value = v; //类成员变量初始化为v
    19. }
    20. //定义一个成员函数来返回成员变量m_value的值
    21. int getValue()
    22. {
    23. return m_value;
    24. }
    25. };
    26. int main()
    27. {
    28. //手动调用构造函数
    29. //定义一个数组ta,ta有3个元素,元素的类型是Test类
    30. Test ta[3] = {Test(), Test(1), Test(2)}; //分别调用Test()、Test(int v)、Test(int v)
    31. //遍历数组ta,打印每个数组成员的 成员变量的值
    32. for (int i = 0; i < 3; i++)
    33. {
    34. cout << "ta[" << i << "] = " << ta[i].getValue() << endl;
    35. }
    36. //定义一个对象t,用Test(100)来初始化它
    37. Test t = Test(100); //这也是一种初始化的方式
    38. //打印对象t的成员变量的值
    39. cout << "t.getValue() = " << t.getValue() << endl;
    40. return 0;
    41. }

    输出结果:

    1. using Test()
    2. using Test(int v), v = 1
    3. using Test(int v), v = 2
    4. ta[0] = 0
    5. ta[1] = 1
    6. ta[2] = 2
    7. using Test(int v), v = 100
    8. t.getValue() = 100

     结果分析:

    语句 Test ta[3] = {Test(), Test(1), Test(2)};就是安排的明明白白的,第一个元素就用Test()构造函数来初始化,第二个元素和第三个元素用Test(int v)构造函数来初始化。

    四、开发一个数组类解决原生数组的安全问题

    我们知道C语言里面的数组稍微不小心就会越界,我们可以编写一个类

    1、可以获取数组长度

    2、可以获取数组元素值

    3、可以设置数组元素值

    IntArray.h头文件

    1. #ifndef INTARRAY_H
    2. #define INTARRAY_H
    3. class IntArray
    4. {
    5. private:
    6. //定义两个成员变量来封装数组的长度和数据
    7. int m_length; //封装长度
    8. int* m_pointer; //封装数据
    9. public:
    10. IntArray(int len); //定义一个构造函数来初始化数组的长度
    11. int length(); //获得数组的长度
    12. bool get(int index, int& value); //获得对应位置的元素值
    13. bool set(int index, int value); //设置对应位置的元素值
    14. void free();
    15. };
    16. #endif // INTARRAY_H

    IntArray.cpp源文件

    1. #include "intarray.h"
    2. //构造函数
    3. //构造函数在堆中申请一段数组空间,数组的长度为len
    4. IntArray::IntArray(int len)
    5. {
    6. m_pointer = new int[len]; //在堆中申请一段数组内存
    7. for (int i = 0; i < len; i++)
    8. {
    9. m_pointer[i] = 0; //数组里面元素值设置为0
    10. }
    11. m_length = len; //确定数组的长度
    12. }
    13. //返回数组的长度
    14. int IntArray::length()
    15. {
    16. return m_length;
    17. }
    18. //获取数组中特定位置的值
    19. bool IntArray::get(int index, int& value)
    20. {
    21. bool ret = (0 <= index) && (index <= length());//确保数组不越界
    22. if (ret)
    23. {
    24. value = m_pointer[index]; //通过引用获得特定位置的值
    25. }
    26. return ret;
    27. }
    28. //设置数组特定位置的值
    29. bool IntArray::set(int index, int value)
    30. {
    31. bool ret = (0 <= index) && (index <= length());//确保数组不越界
    32. if (ret)
    33. {
    34. m_pointer[index] = value ;
    35. }
    36. return ret;
    37. }
    38. //有new就要有delete
    39. void IntArray::free()
    40. {
    41. delete[] m_pointer;
    42. }

    main文件

    1. #include
    2. #include "intarray.h"
    3. using namespace std;
    4. int main()
    5. {
    6. IntArray a(5); //定义一个对象a,并调用构造函数在堆中申请一段数组空间5个int元素
    7. for (int i = 0; i < a.length(); i++)
    8. {
    9. a.set(i, i+1); //遍历数组,将数组的值分别设置为1/2/3/4/5
    10. }
    11. //遍历数组,获得每个元素的值,并将值传给value,并打印
    12. for (int i = 0; i < a.length(); i++)
    13. {
    14. int value = 0;
    15. if (a.get(i, value))
    16. {
    17. printf("a[%d] = %d\n", i, value);
    18. }
    19. }
    20. a.free(); //释放堆空间
    21. return 0;
    22. }

    输出结果:

    1. a[0] = 1
    2. a[1] = 2
    3. a[2] = 3
    4. a[3] = 4
    5. a[4] = 5

    结果分析:注释已经写的很清晰了,既然想要的是安全,可以试下 打印a[10],肯定是打印不出来的。

    五、对象的构造与内存操作的关系

    特殊的构造函数

    构造函数已经很特殊了,还有更特殊的无参构造函数、拷贝构造函数。

    无参构造函数:没有参数的构造函数

    无参构造函数前面一直在用,特殊在哪里?

    前面使用的无参构造函数时我们自己编写的。

    但是当类中没有定义构造函数的时候,编译器会默认提供一个构造函数,也是一个构造函数,并且函数体是空的,什么都不做。

    如果我们已经定义了构造函数,那编译器就不会提供默认的无参构造函数。

    拷贝构造函数:参数为const class_name&的构造函数

    当类中没有定义拷贝构造函数的时候编译器会默认提供一个拷贝构造函数,简单的进行成员变量值的复制。

    编译器提供的无参构造函数

    C++有一个准则:创建一个类对象的时候必须调用构造函数将类对象进行初始化。

    如果已经定义了构造函数,编译器就不会提供无参构造参数。

    那么下面的代码,使用一个没有定义构造函数的类,在编译的时候竟然没报错

    1. #include
    2. using namespace std;
    3. //定义一个类,没有定义构造函数
    4. class Test
    5. {
    6. private:
    7. int i;
    8. int j;
    9. public:
    10. int getI() { return i; }
    11. int getJ() { return j; }
    12. };
    13. int main()
    14. {
    15. Test t; //定义个一个类对象t
    16. return 0;
    17. }

    原因分析:你没有编写构造函数那是你的事,不代表类没有构造函数,当类中没有编写构造函数时,编译器会给类提供一个默认的无参构造函数。前面有说过C++的编译器认为自己很牛逼,它总会想办法让代码正确。编译器没看到构造函数,自己就动手添一个,如下面代码所示

    1. #include
    2. using namespace std;
    3. //定义一个类,没有定义构造函数
    4. class Test
    5. {
    6. private:
    7. int i;
    8. int j;
    9. public:
    10. int getI() { return i; }
    11. int getJ() { return j; }
    12. //空的构造函数
    13. Test()
    14. {
    15. }
    16. };
    17. int main()
    18. {
    19. Test t; //定义个一个类对象t
    20. return 0;
    21. }

     编译器提供的拷贝构造函数

    首先拷贝构造函数也是构造函数;

    拷贝函数时用来兼容C语言的初始化方式,C语言可以用一个变量来初始化另一个变量,如:

    1. int i = 2;
    2. int j = i; //用变量初始化变量

    使用拷贝函数可以用一个已经存在的对象去初始化另一个对象;如

    1. #include
    2. using namespace std;
    3. //定义一个类,没有定义构造函数
    4. class Test
    5. {
    6. private:
    7. int i;
    8. int j;
    9. public:
    10. int getI() { return i; };
    11. int getJ() { return j; };
    12. };
    13. int main()
    14. {
    15. Test t1; //定义个一个类对象
    16. Test t2 = t1; //用对象t1去初始化t2,需要调用拷贝构造函数
    17. cout << "t1.i = " << t1.getI() << " t1.j = " << t1.getJ() << endl;
    18. cout << "t2.i = " << t2.getI() << " t2.j = " << t2.getJ() << endl;
    19. return 0;
    20. }
    Test t2 = t1; 语句就是用对象t1去初始化t2,需要调用拷贝构造函数,但是代码里面根本就没有拷贝构造函数,也没有构造函数,那编译器就自作主张给你搞一个拷贝构造函数和一个无参构造函数,效果如下面的代码所示:
    1. #include
    2. using namespace std;
    3. //定义一个类,没有定义构造函数
    4. class Test
    5. {
    6. private:
    7. int i;
    8. int j;
    9. public:
    10. int getI() { return i; };
    11. int getJ() { return j; };
    12. Test(const Test& t)
    13. {
    14. i = t.i;
    15. j = t.j;
    16. }
    17. Test()
    18. {
    19. }
    20. };
    21. int main()
    22. {
    23. Test t1; //定义个一个类对象
    24. Test t2 = t1; //用对象t1去初始化t2,需要调用拷贝构造函数
    25. cout << "t1.i = " << t1.getI() << " t1.j = " << t1.getJ() << endl;
    26. cout << "t2.i = " << t2.getI() << " t2.j = " << t2.getJ() << endl;
    27. return 0;
    28. }

    深拷贝与浅拷贝

    拷贝函数还要分深拷贝与浅拷贝;

    浅拷贝,就是拷贝后对象的物理状态相同,编译器提供的拷贝构造函数只进行浅拷贝;

    深拷贝,就是拷贝后对象的逻辑状态相同;

    看下面的代码,没有编写拷贝构造函数,使用编译器提供的拷贝函数

    1. #include
    2. using namespace std;
    3. //定义一个类,没有定义构造函数
    4. class Test
    5. {
    6. private:
    7. int i;
    8. int j;
    9. int* p;
    10. public:
    11. int getI() { return i; }
    12. int getJ() { return j; }
    13. int* getP() {return p;}
    14. Test(int v)
    15. {
    16. i = 1;
    17. j = 2;
    18. p = new int;
    19. *p = v;
    20. }
    21. };
    22. int main()
    23. {
    24. Test t1(3); //定义个一个类对象
    25. Test t2 = t1; //用对象t1去初始化t2,需要调用拷贝构造函数
    26. cout << "t1.i = " << t1.getI() << " t1.j = " << t1.getJ() << " t1.p = "<< t1.getP() << endl;
    27. cout << "t2.i = " << t2.getI() << " t2.j = " << t2.getJ() << " t2.p = "<< t2.getP() << endl;
    28. return 0;
    29. }

    输出结果

    1. t1.i = 1 t1.j = 2 t1.p = 0x1121ba0
    2. t2.i = 1 t2.j = 2 t2.p = 0x1121ba0

    结果分析:

    t1.p与t2.p的结果是一样的,它们指向相同的堆空间的内容,这就会存在问题,因为按照规矩,使用new在堆空间申请一片空间之后,要使用delete来释放,我们又知道同一片堆空间释放两次是要出问题的,如下面的代码所示,同一块空间释放两次,出错。

    1. #include
    2. using namespace std;
    3. //定义一个类,没有定义构造函数
    4. class Test
    5. {
    6. private:
    7. int i;
    8. int j;
    9. int* p;
    10. public:
    11. int getI() { return i; }
    12. int getJ() { return j; }
    13. int* getP() {return p;}
    14. Test(int v)
    15. {
    16. i = 1;
    17. j = 2;
    18. p = new int;
    19. *p = v;
    20. }
    21. //释放堆空间
    22. void free()
    23. {
    24. delete p;
    25. }
    26. };
    27. int main()
    28. {
    29. Test t1(3); //定义个一个类对象
    30. Test t2 = t1; //用对象t1去初始化t2,需要调用拷贝构造函数
    31. cout << "t1.i = " << t1.getI() << " t1.j = " << t1.getJ() << " t1.p = "<< t1.getP() << endl;
    32. cout << "t2.i = " << t2.getI() << " t2.j = " << t2.getJ() << " t2.p = "<< t2.getP() << endl;
    33. t1.free();
    34. t2.free();
    35. return 0;
    36. }

    所以要想办法,让类对象给对象初始化时,使用的构造函数让两个对象使用不同的地址空间,就像用变量对变量进行初始化的时候,变量的地址空间是不一样的,这就不能使用编译器提供的拷贝构造函数了,需要自己编写拷贝构造函数。如下所示

    1. #include
    2. using namespace std;
    3. //定义一个类,没有定义构造函数
    4. class Test
    5. {
    6. private:
    7. int i;
    8. int j;
    9. int* p;
    10. public:
    11. int getI() { return i; }
    12. int getJ() { return j; }
    13. int* getP() {return p;}
    14. Test(int v)
    15. {
    16. i = 1;
    17. j = 2;
    18. p = new int;
    19. *p = v;
    20. }
    21. //定义一个拷贝构造函数
    22. Test(const Test& t)
    23. {
    24. i = t.i;
    25. j = t.j;
    26. p = new int; //重新申请堆空间
    27. *p = *t.p; //再赋值
    28. }
    29. //释放堆空间
    30. void free()
    31. {
    32. delete p;
    33. }
    34. };
    35. int main()
    36. {
    37. Test t1(3); //定义个一个类对象
    38. Test t2 = t1; //等效于 Test t2(t1),用对象t1去初始化t2,需要调用拷贝构造函数
    39. cout << "t1.i = " << t1.getI() << " t1.j = " << t1.getJ() << " t1.p = "<< t1.getP() << " *t1.p = "<< *t1.getP() << endl;
    40. cout << "t2.i = " << t2.getI() << " t2.j = " << t2.getJ() << " t2.p = "<< t2.getP() << " *t1.p = "<< *t1.getP()<< endl;
    41. t1.free();
    42. t2.free();
    43. return 0;
    44. }

    输出结果

    1. t1.i = 1 t1.j = 2 t1.p = 0x9c1ba0 *t1.p = 3
    2. t2.i = 1 t2.j = 2 t2.p = 0x9c1980 *t1.p = 3

    这样就地址不一样,值一样了。

    如何判断使用深拷贝还是浅拷贝

    对象中有成员指代了系统中的资源,如:成员指向了动态内存空间;成员打开了外存的文件;成员使用了系统中的网络端口等就要使用深拷贝,要自己定义拷贝构造函数。

    一般自定义拷贝构造函数,就使用深拷贝。

  • 相关阅读:
    【pen200-lab】10.11.1.222
    银河麒麟V10安装MySQL8.0.28并实现远程访问
    java毕业生设计学术会议论文稿件管理系统计算机源码+系统+mysql+调试部署+lw
    RSCMVR
    CentOS7安装Redis集群
    docker 基本用法-操作镜像
    RK3588平台开发系列讲解(视频篇)ffmpeg 的移植
    慕思股份深交所上市:靠床垫和“洋老头”走红 市值224亿
    MCE 产品发表高分文章锦集
    Apache Doris (五十二): Doris Join类型 - Broadcast Join
  • 原文地址:https://blog.csdn.net/m0_49968063/article/details/133578569