C++详细讲解对象的构造
作者:清风自在 流水潺潺
一、对象的构造(上)
1.1 对象的初始值
问题:对象中成员变量的初始值是多少?
下面的类定义中成员变量 i 和 j 的初始值为多少?
下面看一段成员变量初始值的代码:
#include<stdio.h> class Test { private: int i; int j; public: int getI() {return i;} int getJ() {return j;} }; Test gt; int main() { printf("gt.i = %d\n", gt.getI()); printf("gt.j = %d\n", gt.getJ()); Test t1; printf("t1.i = %d\n", t1.getI()); printf("t1.j = %d\n", t1.getJ()); Test* pt = new Test; printf("pt->i = %d\n", pt->getI()); printf("pt->j = %d\n", pt->getJ()); delete pt; return 0; }
下面为输出结果:
对象t1 所占用的存储空间在栈上面,而且成员变量 i 和 j 也没有明确的初始值,所以初始值就不定。对象 gt 所占用的存储空间在全局数据区,所以初始值统一为 0。
Test* pt = new Test;意味着在堆空间中生成一个 Test 对象,虽然 pt->i 和 pt->j 均为 0,这只是巧合罢了,因为在堆上创建对象时,成员变量初始为随机值。
注:类得到的其实是数据类型,所以说通过这种数据类型在全局数据区、栈和堆上面都能够生成对象。
1.2 对象的初始化
从程序设计的角度,对象只是变量,因此:
- 在栈上创建对象时,成员变量初始为随机值
- 在堆上创建对象时,成员变量初始为随机值
- 在静态存储区创建对象时,成员变量初始为 0 值
生活中的对象都是在初始化后上市的
初始状态(出厂设置)是对象普遍存在的一个状态
—股而言,对象都需要—个确定的初始状态
解决方案
- 在类中提供一个 public 的 initialize 函数
- 对象创建后立即调用 initialize 函数进行初始化
如下:
下面看一段初始化函数的代码:
#include<stdio.h> class Test { private: int i; int j; public: int getI() {return i;} int getJ() {return j;} void initialize() { i = 1; j = 2; } }; Test gt; int main() { gt.initialize(); printf("gt.i = %d\n", gt.getI()); printf("gt.j = %d\n", gt.getJ()); Test t1; t1.initialize(); printf("t1.i = %d\n", t1.getI()); printf("t1.j = %d\n", t1.getJ()); Test* pt = new Test; pt->initialize(); printf("pt->i = %d\n", pt->getI()); printf("pt->j = %d\n", pt->getJ()); delete pt; return 0; }
下面为输出结果:
存在的问题
- initialize 只是一个普通函数,必须显示调用
- 如果未调用 initialize 函数,运行结果是不确定的
下面为解决办法:
C++中可以定义与类名相同的特殊成员函数
这种特殊的成员函数叫做构造函数
- 构造没有任何返回类型的声明
- 构造函数在对象定义时自动被调用
下面来体验一下构造函数:
#include<stdio.h> class Test { private: int i; int j; public: int getI() {return i;} int getJ() {return j;} Test() { printf("Test() Begin\n"); i = 1; j = 2; printf("Test() End\n"); } }; Test gt; int main() { printf("gt.i = %d\n", gt.getI()); printf("gt.j = %d\n", gt.getJ()); Test t1; printf("t1.i = %d\n", t1.getI()); printf("t1.j = %d\n", t1.getJ()); Test* pt = new Test; printf("pt->i = %d\n", pt->getI()); printf("pt->j = %d\n", pt->getJ()); delete pt; return 0; }
下面为输出结果:
可以看到,Test() Begin 和 Test() End 出现了三次,也就是说,Test() 这个构造函数被调用了三次,这是因为创建了三个对象。
1.3 小结
- 每个对象在使用之前都应该初始化
- 类的构造函数用于对象的初始化
- 构造函数与类同名并且没有返回值
- 构造函数在对象定义时自动被调用
二、对象的构造(中)
2.1 构造函数
带有参数的构造函数
- 构造函数可以根据需要定义参数
- 一个类中可以存在多个重载的构造函数
- 构造函数的重载遵循 C++ 重载的规则
如下:
友情提醒
对象定义和对象声明不同
- 对象定义--申请对象的空间并调用构造函数
- 对象声明--告诉编译器存在这样一个对象
如下:
构造函数的自动调用
如下:
下面看一段带参数的构造函数的代码:
#include <stdio.h> class Test { public: Test() { printf("Test()\n"); } Test(int v) { printf("Test(int v), v = %d\n", v); } }; int main() { Test t; // 调用 Test() Test t1(1); // 调用 Test(int v) Test t2 = 2; // 调用 Test(int v) return 0; }
下面为输出结果,和预想中的一致。
这里需要明确一个问题,int i = 1;与 int i; i = 1;的不同。前者是初始化,后者是先定义,再赋值。后者由于定义 i 时没有初始化,所以 i 的值时随机的。C语言中这两者差别很小,但是在 C++ 中两者差异很大。差别在于在 C++ 中初始化会调用构造函数。下面看一个例子,在上述代码的基础上加一行代码 t = t2;
#include <stdio.h> class Test { public: Test() { printf("Test()\n"); } Test(int v) { printf("Test(int v), v = %d\n", v); } }; int main() { Test t; // 调用 Test() Test t1(1); // 调用 Test(int v) Test t2 = 2; // 调用 Test(int v) t = t2; return 0; }
下面为输出结果,可以看到与上面的代码输出结果一模一样。这就因为 C++ 中初始化和赋值不同,初始化会调用构造函数,赋值的时候则不用。
下面再看一个例子:
#include <stdio.h> class Test { public: Test() { printf("Test()\n"); } Test(int v) { printf("Test(int v), v = %d\n", v); } }; int main() { Test t; // 调用 Test() Test t1(1); // 调用 Test(int v) Test t2 = 2; // 调用 Test(int v) int i(100); printf("i = %d\n", i); return 0; }
下面为输出结果:
构造函数的调用
- 一般情况下,构造函数在对象定义时被自动调用
- —些特殊情况下,需要手工调用构造函数
下面看一段构造函数手动调用的代码:
#include <stdio.h> class Test { private: int m_value; public: Test() { printf("Test()\n"); m_value = 0; } Test(int v) { printf("Test(int v), v = %d\n", v); m_value = v; } int getValue() { return m_value; } }; int main() { Test ta[3] = {Test(), Test(1), Test(2)}; for (int i = 0; i < 3; i++) { printf("ta[%d].getValue() = %d\n", i, ta[i].getValue()); } Test t = Test(100); printf("t.getValue() = %d\n", t.getValue()); return 0; }
下面为输出结果,可以看到,Test(1)、Test(2) 和 Test(100) 均为手动调用构造函数。
2.2小实例
需求:开发一个数组类解决原生数组的安全性问题
- 提供函数获取数组长度
- 提供函数获取数组元素
- 提供函数设置数组元素
IntArray.h:
#ifndef _INTARRAY_H_ #define _INTARRAY_H_ class IntArray { private: int m_length; int* m_pointer; public: IntArray(int len); int length(); bool get(int index, int& value); bool set(int index ,int value); void free(); }; #endif
IntArray.cpp:
#include "IntArray.h" IntArray::IntArray(int len) { m_pointer = new int[len]; for (int i = 0; i < len; i++) { m_pointer[i] = 0; } m_length = len; } int IntArray::length() { return m_length; } bool IntArray::get(int index, int& value) { bool ret = (0 <= index) && (index < length()); if( ret ) { value = m_pointer[index]; } return ret; } bool IntArray::set(int index, int value) { bool ret = (0 <= index) && (index < length()); if( ret ) { m_pointer[index] = value; } return ret; } void IntArray::free() { delete[]m_pointer; }
main.cpp:
#include <stdio.h> #include "IntArray.h" int main() { IntArray a(5); for (int i = 0; i < a.length(); i++) { a.set(i, i + 1); } for (int i = 0; i < a .length(); i++) { int value = 0; if( a.get(i, value) ) { printf("a[%d] = %d\n", i, value); } } a.free(); return 0; }
下面为输出结果:
这样写出来的数组很安全,没有数组越界问题。
2.3 小结
- 构造函数可以根据需要定义参数
- 构造函数之间可以存在重载关系
- 构造函数遵循 C++ 中重载函数的规则
- 对象定义时会触发构造函数的调用
- 在一些情况下可以手动调用构造函数
三、对象的构造(下)
3.1 特殊的构造函数
两个特殊的构造函数
无参构造函数
- 没有参数的构造函数
- 当类中没有定义构造函数时,编译器默认提供一个无参构造函数,并且其函数体为空
拷贝构造函数
- 参数为 const class_name& 的构造函数
- 当类中没有定义拷贝构造函数时,编译器默认提供一个拷贝构造函数,简单的进行成员变量的值复制
下面看一段无参数构造函数的代码(代码3-1):
#include <stdio.h> class Test { private: int i; int j; public: int getI() { return i; } int getJ() { return j; } }; int main() { Test t; return 0; }
可以看到,编译通过:
创建一个类的对象必须要调用构造函数,为什么能够编译通过呢?这是因为编译器在发现我们没有定义构造函数时,会默认提供一个无参构造函数,等效如(代码3-2):
#include <stdio.h> class Test { private: int i; int j; public: int getI() { return i; } int getJ() { return j; } Test() { } }; int main() { Test t; return 0; }
小贴士:所以说,class T { }; 里面不是什么都没有,里面至少有一个无参构造函数。
下面再来看一段代码(代码3-3):
#include <stdio.h> class Test { private: int i; int j; public: int getI() { return i; } int getJ() { return j; } }; int main() { Test t1; Test t2 = t1; printf("t1.i = %d, t1.j = %d\n", t1.getI(), t1.getJ()); printf("t2.i = %d, t2.j = %d\n", t2.getI(), t2.getJ()); return 0; }
下面为输出结果:
这里的 i 和 j 打印出来的都是随机值,这是因为类里面没有手工编写的构造函数,所以 t1 和 t2 所采用的就是编译器提供的默认无参构造函数构造的,编译器提供的无参构造函数为空,所以 i 和 j 的值就是随机的。
上述代码就相当于(代码3-4):
#include <stdio.h> class Test { private: int i; int j; public: int getI() { return i; } int getJ() { return j; } Test(const Test& t) { i = t.i; j = t.j; } }; int main() { Test t1; Test t2 = t1; printf("t1.i = %d, t1.j = %d\n", t1.getI(), t1.getJ()); printf("t2.i = %d, t2.j = %d\n", t2.getI(), t2.getJ()); return 0; }
但是编译的时候会报错:
这是因为在类里面没有编写任何构造函数时,编译器才提供默认的无参构造函数。这里手工编写了一个拷贝构造函数,编译器就不会提供默认的无参构造函数,需要自己把无参构造函数加上。
如下,自己加上无参构造函数(代码3-5):
#include <stdio.h> class Test { private: int i; int j; public: int getI() { return i; } int getJ() { return j; } Test(const Test& t) { i = t.i; j = t.j; } Test() { } }; int main() { Test t1; Test t2 = t1; printf("t1.i = %d, t1.j = %d\n", t1.getI(), t1.getJ()); printf("t2.i = %d, t2.j = %d\n", t2.getI(), t2.getJ()); return 0; }
这样就能编译通过了,而且效果跟代码3-3的相同:
3.2 拷贝构造函数
拷贝构造函数的意义
兼容C语言的初始化方式
初始化行为能够符合预期的逻辑
浅拷贝
- 拷贝后对象的物理状态相同(物理状态指的是对象占据的内存当中每个字节是否相等,如代码3-6)
深拷贝
- 拷贝后对象的逻辑状态相同(逻辑状态指的是指针所指向的内存空间的值是否相同,如代码3-9)
注:编译器提供的拷贝构造函数只进行浅拷贝!
下面看一段代码(代码3-6):
#include <stdio.h> class Test { private: int i; int j; int* p; public: int getI() { return i; } int getJ() { return j; } int* getP() { return p; } Test(int v) { i = 1; j = 2; p = new int; *p = v; } }; int main() { Test t1(3); Test t2 = t1; printf("t1.i = %d, t1.j = %d, t1.p = %p\n", t1.getI(), t1.getJ(), t1.getP()); printf("t2.i = %d, t2.j = %d, t2.p = %p\n", t2.getI(), t2.getJ(), t2.getP()); return 0; }
下面为输出结果:
这段程序的第一个问题就是 t1 和 t2 的 p 指针都指向同一个堆空间中的地址,第二个问题就是申请了内存并没有释放,会造成内存泄漏。
下面加上释放内存的代码(代码3-7):
#include <stdio.h> class Test { private: int i; int j; int* p; public: int getI() { return i; } int getJ() { return j; } int* getP() { return p; } Test(int v) { i = 1; j = 2; p = new int; *p = v; } void free() { delete p; } }; int main() { Test t1(3); Test t2 = t1; printf("t1.i = %d, t1.j = %d, t1.p = %p\n", t1.getI(), t1.getJ(), t1.getP()); printf("t2.i = %d, t2.j = %d, t2.p = %p\n", t2.getI(), t2.getJ(), t2.getP()); t1.free(); t2.free(); return 0; }
下面为输出结果,编译能通过,但是运行时发生了错误,释放了两次堆空间的内存:
下面为解决方法(代码3-8):
#include <stdio.h> class Test { private: int i; int j; int* p; public: int getI() { return i; } int getJ() { return j; } int* getP() { return p; } Test(const Test& t) { i = t.i; j = t.j; p = new int; *p = *t.p; } Test(int v) { i = 1; j = 2; p = new int; *p = v; } void free() { delete p; } }; int main() { Test t1(3); Test t2(t1); printf("t1.i = %d, t1.j = %d, t1.p = %p\n", t1.getI(), t1.getJ(), t1.getP()); printf("t2.i = %d, t2.j = %d, t2.p = %p\n", t2.getI(), t2.getJ(), t2.getP()); t1.free(); t2.free(); return 0; }
下面为输出结果,可以到 t1 和 t2 的 p 指针分别指向不同的堆空间地址:
如果我们看一下逻辑状态,也就是 *t1.p 和 *t2.p 的值,代码如下(代码3-9):
#include <stdio.h> class Test { private: int i; int j; int* p; public: int getI() { return i; } int getJ() { return j; } int* getP() { return p; } Test(const Test& t) { i = t.i; j = t.j; p = new int; *p = *t.p; } Test(int v) { i = 1; j = 2; p = new int; *p = v; } void free() { delete p; } }; int main() { Test t1(3); Test t2(t1); printf("t1.i = %d, t1.j = %d, t1.p = %p\n", t1.getI(), t1.getJ(), t1.getP()); printf("t2.i = %d, t2.j = %d, t2.p = %p\n", t2.getI(), t2.getJ(), t2.getP()); t1.free(); t2.free(); return 0; }
下面为输出结果,可以看到 *t1.p 和 *t2.p 的值相同,也就是说逻辑状态相同,这就叫做深拷贝。
什么时候需要进行深拷贝?
对象中有成员指代了系统中的资源
- 成员指向了动态内存空间
- 成员打开了外存中的文件
- 成员使用了系统中的网络端口
- ......
问题分析
下面就是浅拷贝:
一般性原则
自定义拷贝构造函数,必然需要实现深拷贝!!!
下面看一个使用深拷贝,对前面数组的代码进行改造。
IntArray.h:
#ifndef _INTARRAY_H_ #define _INTARRAY_H_ class IntArray { private: int m_length; int* m_pointer; public: IntArray(int len); IntArray(const IntArray& obj); int length(); bool get(int index, int& value); bool set(int index ,int value); void free(); }; #endif
IntArray.cpp:
#include "IntArray.h" IntArray::IntArray(int len) { m_pointer = new int[len]; for (int i = 0; i < len; i++) { m_pointer[i] = 0; } m_length = len; } IntArray::IntArray(const IntArray& obj) { m_length = obj.m_length; m_pointer = new int[obj.m_length]; for (int i = 0; i < obj.m_length; i++) { m_pointer[i] = obj.m_pointer[i]; } } int IntArray::length() { return m_length; } bool IntArray::get(int index, int& value) { bool ret = (0 <= index) && (index < length()); if( ret ) { value = m_pointer[index]; } return ret; } bool IntArray::set(int index, int value) { bool ret = (0 <= index) && (index < length()); if( ret ) { m_pointer[index] = value; } return ret; } void IntArray::free() { delete[]m_pointer; }
main.cpp:
#include <stdio.h> #include "IntArray.h" int main() { IntArray a(5); for (int i = 0; i < a.length(); i++) { a.set(i, i + 1); } for (int i = 0; i < a.length(); i++) { int value = 0; if( a.get(i, value) ) { printf("a[%d] = %d\n", i, value); } } IntArray b = a; for (int i = 0; i < b.length(); i++) { int value = 0; if( b.get(i, value) ) { printf("b[%d] = %d\n", i, value); } } a.free(); b.free(); return 0; }
下面为输出结果:
可以看到 b 数组里面的元素与 a 数组里面的元素相同,这就是深拷贝构造函数的结果。
3.3 小结
C++ 编译器会默认提供构造函数
无参构造函数用于定义对象的默认初始状态
拷贝构造函数在创建对象时拷贝对象的状态
对象的拷贝有浅拷贝和深拷贝两种方式
- 浅拷贝使得对象的物理状态相同
- 深拷贝使得对象的逻辑状态相同
到此这篇关于C++详细讲解对象的构造的文章就介绍到这了,更多相关C++ 对象的构造内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!