我们在 C 语言中,每个变量都有其初始值。那么问题来了,对象中成员变量的初始值是多少呢?从设计的角度来看,对象只是变量,因此:在栈上创建对象时,成员变量初始为随机值;在堆上创建对象时,成员变量初始为随机值;在静态存储区创建对象时,成员变量初识为 0 值。
下来我们以代码为例进行验证,代码如下
#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 at; printf("at.i = %d\n", at.getI()); printf("at.j = %d\n", at.getJ()); Test* pt = new Test; printf("pt->i = %d\n", pt->getI()); printf("pt->j = %d\n", pt->getJ()); return 0; }
gt 对象是在静态存储区创建的,所以 gt.i 和 gt.j 应该都为 0,;at 对象是在栈上创建的,所以 at.i 和 at.j 应该都为随机值;pt 对象是在堆上创建的,所以 pt->i 和 pt->j 应该也为随机值。我们来编译下,看看是否如我们所分析的那样呢?
我们看到前面两个如我们所分析的那样,最后一个不一样。我们再来看看BCC编译器呢
我们看到BCC编译器是如我们所分析的那样。所以我们不能依赖于某种编译器的特性。
在生活中的对象都是在初始化后上市的,初识状态(出厂设置)是对象普遍存在的一个状态。那么程序中如何对一个对象进行初始化呢?一般而言,对象都需要一个确定的初识状态。解决方案便是在类中提供一个 public 的 initialize 函数,对象创建后立即调用 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 at; at.initialize(); printf("at.i = %d\n", at.getI()); printf("at.j = %d\n", at.getJ()); Test* pt = new Test; pt->initialize(); printf("pt->i = %d\n", pt->getI()); printf("pt->j = %d\n", pt->getJ()); return 0; }
我们编译,看看结果是否初始化好呢
我们看到已经全部初始化为按照我们所想要的状态了。但是这个就存在一个问题了,initialize 只是一个普通的函数,必须显示调用才行。如果为调用 initialize 函数的话,结果是不确定的。如果我们忘记在 at 对象中调用 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 at; printf("at.i = %d\n", at.getI()); printf("at.j = %d\n", at.getJ()); Test* pt = new Test; printf("pt->i = %d\n", pt->getI()); printf("pt->j = %d\n", pt->getJ()); 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 t1; // 调用 Test() Test t2(1); // 调用 Test(int v) Test t3 = 2; // 调用 Test(int v) int i(10); printf("i = %d\n", i); return 0; }
我们看到第 18 行的 t1 对象的构造函数肯定调用了 Test(),第 19 和 20 行则是调用了 Test(int v);在 C 语言中还有 int i(10) 这种写法,我们看看编译是否会通过?
我们看到编译通过,并且如我们所分析的那样。那么构造函数的调用是否有什么规则呢?在一般情况下,构造函数在对象定义时被自动调用,一些特殊情况下,需要手工调用构造函数。我们如何利用构造函数来创建一个数组呢?
#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(10); printf("t.getValue() = %d\n", t.getValue()); return 0; }
我们首先来分析下,数组第一个成员调用的构造函数应该是 Test(),后面两个成员调用的是 Test(int v) 函数,并打印出相应的值。最后定义的对象 t,它会打印出构造函数和得到的值都为 10,我们来看看编译结果
下来我们来开发一个数组类解决原生数组的安全性问题:提供函数获取数组长度;提供函数获取数组元素;提供函数设置数组元素。我们来看看它是怎么实现的
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; }
test.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; }
我们编译后得到如下结果
下来我们来看看特殊的构造函数:无参构造函数和拷贝构造函数。无参构造函数顾名思义就是没有参数的构造函数,而拷贝构造函数则是参数为 const class_name& 的构造函数。那么这两类构造函数有什么区别呢?无参构造函函数是当类中没有定义构造函数时,编译器默认提供一个无参构造函数,并且其函数体为空;拷贝构造函数是当类中没有定义拷贝构造函数时,编译器默认提供一个拷贝构造函数,简单的进行成员变量的值复制。下来我们以代码为例进行分析
#include <stdio.h> class Test { private: int i; int j; public: int getI() { return i; } int getJ() { return j; } /* Test() { printf("Test()\n"); } Test(const Test& t) { printf("Test(const Test& t)\n"); 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; }
我们先将自己提供的无参构造函数和拷贝构造函数注释掉,编译下,看编译器是否提供默认的构造函数,是否可以通过
我们看到编译是通过的,也就是说,编译器通过了默认的构造函数。我们再来自己提供呢,看看是否会发生冲突
我们看到打印出了自己定义的语句,证明它是调用了我们自己写的构造函数。那么这个拷贝构造函数的意义在哪呢?一是兼容 C 语言的初始化方式,二是初始化行为能够符合预期的逻辑。那么这块就牵扯到是浅拷贝还是深拷贝。浅拷贝是拷贝后对象的物理状态相同,深拷贝是拷贝后对象的逻辑状态相同。注意:编译器提供的拷贝构造函数只进行浅拷贝!
下来我们以实例代码看看对象的初始化是怎样进行的
#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; } Test(const Test& t) { i = t.i; j = t.j; p = new int; *p = *t.p; } void free() { delete p; } }; int main() { Test t1(3); Test t2(t1); printf("t1.i = %d, t1.j = %d, *t1.p = %d\n", t1.getI(), t1.getJ(), *t1.getP()); printf("t2.i = %d, t2.j = %d, *t2.p = %d\n", t2.getI(), t2.getJ(), *t2.getP()); t1.free(); t2.free(); return 0; }
我们看看 t1 应该进行的是浅拷贝,t2 应该进行的是深拷贝。我们看看编译结果
我们如果只有浅拷贝,没有深拷贝的话,看看结果会是怎样的,将第 34 - 41 行的代码注释掉,将第 54 和 55 行的打印 *p 的值改为打印 p 的地址。
我们看到它运行的时候报段错误了,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; }
test.cpp 源码
#include <stdio.h> #include "IntArray.h" int main() { IntArray a(5); for(int i=0; i<5; 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); } } printf("\n"); 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; }
我们看看编译结果是否如我们代码所写的那样,创建数组并初始化。用数组 a 初始化数组 b。
通过对对象的构造的学习,总结如下:1、每个对象在使用之前都应该初始化;2、类的构造函数用于对象的初始化,构造函数与类同名并且没有返回值;3、构造函数在对象定义时被自动调用,构造函数可以根据需要定义参数;4、构造函数之间可以存在重载关系,并且构造函数遵循 C++ 中重载函数的规则;5、对象定义时会触发构造函数的调用,在一些情况下可以手动调用构造函数;6、C++ 编译器会默认提供构造函数;7、无参构造函数用于定义对象的默认初识状态,拷贝构造函数在创建对象时拷贝对象的状态;8、对象的拷贝有浅拷贝和深拷贝两种方式:浅拷贝使得对象的物理状态相同,而深拷贝则使得对象的逻辑状态相同。
欢迎大家一起来学习 C++ 语言,可以加我QQ:243343083。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。