目录
对象的创建
对象的创建规则
对象的数据成员初始化
对象所占空间大小
总结
指针数据成员
对象的创建
在之前的 Computer 类中,通过自定义的公共成员函数 setBrand 和 setPrice 实现了对数据成员的初始化。实际上,C++ 为类提供了一种特殊的成员函数——构造函数来完成相同的工作。
-
构造函数的作用:就是用来初始化数据成员的
-
构造函数的形式:
没有返回值,即使是void也不能有;
函数名与类名相同,再加上函数参数列表。
构造函数在对象创建时自动调用,用以完成对象成员变量等的初始化及其他操作(如为指针成员动态申请内存等)
对象的创建规则
-
当类中没有显式定义构造函数时 ,编译器会自动生成一个默认 (无参) 构造函数 ,但并不会初始化数据成员;
以Point类为例:
#include <iostream>// 其次是C++的文件,第三方库文件放最下using std::cout; using std::endl; class Point {public:void print(){cout << "(" << _ix << "," << _iy<< ")" << endl;}Point(){cout << "print" << endl;}private:int _ix;int _iy;};void test0(){Point pt;pt.print();}//运行结果显示,pt的_ix,_iy都是不确定的值int main(){test0();return 0;}
这说明了,当类中没有显式定义构造函数时 ,编译器会自动生成一个默认 (无参) 构造函数 ,但并不会初始化数据成员,这里我们将他不做任何处理,看是否会自动调用。
class Point {
public:void print(){cout << "(" << _ix << "," << _iy<< ")" << endl;}
private:int _ix;int _iy;
};void test0()
{Point pt;pt.print();
}
//运行结果显示,pt的_ix,_iy都是不确定的值
Point pt; 这种方式创建的对象,其数据成员没有被初始化,输出的会是不确定的值
2. 一旦当类中显式提供了构造函数时 ,编译器就不会再自动生成默认的构造函数;
class Point {
public:Point(){cout << "Point()" << endl;_ix = 0;_iy = 0;}void print(){cout << "(" << _ix << "," << _iy<< ")" << endl;}
private:int _ix;int _iy;
};void test0()
{Point pt;pt.print();
}
//这次创建pt对象时就调用了自定义的构造函数,而非默认构造函数
3. 编译器自动生成的默认构造函数是无参的,构造函数也可以接收参数,在对象创建时提供更大的自由度;
class Point {
public:Point(int ix, int iy){cout << "Point(int,int)" << endl;_ix = ix;_iy = iy;}void print(){cout << "(" << _ix << "," << _iy<< ")" << endl;}
private:int _ix;int _iy;
};void test0()
{Point pt;//error,没有默认的无参构造函数可供调用Point pt2(10,20);pt2.print();
}
4. 如果还希望通过默认构造函数创建对象, 则必须要手动提供一个默认构造函数;
class Point {
public:Point(){}Point(int ix, int iy){cout << "Point(int,int)" << endl;_ix = ix;_iy = iy;}void print(){cout << "(" << _ix << "," << _iy<< ")" << endl;}
private:int _ix;int _iy;
};void test0()
{Point pt;//okPoint pt2(10,20);pt2.print();
}
5. 构造函数可以重载
如上,一个类中可以有多种形式的构造函数,说明构造函数可以重载。事实上,真实的开发中经常会给一个类定义各种形式的构造函数,以提升代码的灵活性(可以用多种不同的数据来创建出同一类的对象)。
对象的数据成员初始化
上述例子中,在构造函数的函数体中对数据成员进行赋值,其实严格意义上不算初始化(而是算赋值)。
在C++中,对于类中数据成员的初始化,推荐使用初始化列表完成。初始化列表位于构造函数形参列表之后,函数体之前,用冒号开始,如果有多个数据成员,再用逗号分隔,初始值放在一对小括号中。
class Point {
public://...Point(int ix = 0, int iy = 0): _ix(ix), _iy(iy){cout << "Point(int,int)" << endl;}//...
};
如果没有在构造函数的初始化列表中显式地初始化成员,则该成员将在构造函数体之前执行默认初始化。如在“对象的创建规则”示例代码中,有参的构造函数中 _ix 和 _iy 都是先执行默认初始化后,再在函数体中执行赋值操作。
补充:数据成员的初始化并不取决于其在初始化列表中的顺序,而是取决于声明时的顺序(与声明顺序一致)。
-
构造函数的参数也可以按从右向左规则赋默认值,同样的,如果构造函数的声明和定义分开写,只用在声明或定义中的一处设置参数默认值,一般建议在声明中设置默认值。
class Point { public:Point(int ix, int iy = 0);//默认参数设置在声明时//... };Point::Point(int ix, int iy) : _ix(ix) , _iy(iy) {cout << "Point(int,int)" << endl; }void test0(){Point pt(10); }
-
C++11之后,普通的数据成员也可以在声明时就进行初始化。但一些特殊的数据成员初始化只能在初始化列表中进行,故一般情况下统一推荐在初始化列表中进行数据成员初始化。
class Point {
public://...int _ix = 0;//C++11int _iy = 0;
};
-
数据成员初始化的顺序与其声明的顺序保持一致,与它们在初始化列表中的顺序无关(但初始化列表一般保持与数据成员声明的顺序一致)。
对象所占空间大小
之前在讲引用的知识点时,我们提过使用引用作为函数的返回值可以避免多余的复制。内置类型的变量最大也就是long double,占16个字节。但是现在我们学习了类的定义,自定义类型对象的大小可以非常大。
使用sizeof查看一个类的大小和查看该类对象的大小,得到的结果是相同的(类是对象的模板);
void test0(){Point pt(1,2);cout << sizeof(Point) << endl;cout << sizeof(pt) << endl;}
成员函数并不影响对象的大小,对象的大小与数据成员有关(后面学习继承、多态,对象的内存布局会更复杂);
现阶段,在不考虑继承多态的情况下,我们做以下测试。发现有时一个类所占空间大小就是其数据成员类型所占大小之和,有时则不是,这就是因为有内存对齐的机制。
class A{int _num;double _price;
};
//sizeof(A) = 16class B{int _num;int _price;
};
//sizeof(D) = 8
-
为什么要进行内存对齐?
1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因:CPU 对内存的读取不是连续的,而是分成块读取的,块的大小只能是1、2、4、8、16 ... 字节。若不进行内存对齐,可能需要做两次内存访问,性能会大打折扣;而进行过内存对齐仅需要一次访问。
64位系统默认以8个字节的块大小进行读取。
如果没有内存对齐机制,CPU读取_price时,需要两次总线周期来访问内存,第一次读取 _price数据前四个字节的内容,第二次读取后四个字节的内容,还要经过计算,将它们合并成一个数据。
有了内存对齐机制后,以浪费4个字节的空间为代价,读取_price时只需要一次访问,所以编译器会隐式地进行内存对齐。
规则一:
按照类中占空间最大的数据成员大小的倍数对齐;
如果数据成员再多一些,我们发现自定义类型所占的空间大小还与这些数据成员的顺序有关
class C{int _c1;int _c2;double _c3; }; //sizeof(C) = 16class D{int _d1;double _d2;int _d3; }; //sizeof(D) = 24
如果数据成员中有数组类型,会按照除数组以外的其他数据成员中最大的那一个的倍数对齐
class E{double _e;char _eArr[20];double _e1;int _e2; }; //sizeof(E) = 48class F{char _fArr[20]; }; //sizeof(F) = 20
再判断一下,G类所占的空间是多少?
class G{char _gArr[20];int _g1;double _g2; };//32
在C语言的涉及的结构体代码中,我们可能会看到#pragma pack的一些设置,#pragma pack(n)即设置编译器按照n个字节对齐,n可以取值1,2,4,8,16.在C++中也可以使用这个设置,最终的对齐效果将按照 #pragma pack 指定的数值和类中最大的数据成员长度中,比较小的那个的倍数进行对齐。
总结
除数组外,其他类型的数据成员中,以较大的数据成员所占空间的倍数去对齐。
内存对齐还与数据成员的声明顺序有关。
指针数据成员
类的数据成员中有指针时,意味着创建该类的对象时要进行指针成员的初始化,需要申请堆空间。
在初始化列表中申请空间,在函数体中复制内容。
class Computer {
public:Computer(const char * brand, double price): _brand(new char[strlen(brand) + 1]()), _price(price){strcpy(_brand,brand);}private:char * _brand;double _price;
};void test0(){Computer pc("Apple",12000);
}
思考一下,以上代码有没有问题?
代码运行没有报错,但使用memcheck工具检查发现发生了内存泄漏。有new表达式被执行,就要想到通过delete表达式来进行回收。如果没有对应的回收机制,对象被销毁时,它所申请的堆空间不会被回收,就会发生内存泄漏。