C++ note (5)
数据抽象和类 Ⅳ
对象成员初始化
非静态数据成员初始化方法:
- 在构造函数的成员 初始化器列表 中。(C++11)
- 通过 默认成员初始化器 ,它是成员声明中包含的 花括号 或 等号 初始化器。(C++11)
- 构造函数体内进行赋值操作。(不要构造成员,除非特别熟悉
new
的用法)
为什么需要初始化器列表和默认成员初始化器?
- 对象初始化分两个阶段:先按声明顺序初始化成员、然后执行构造函数函数体;
- 对于复杂对象成员,如
std::string name
,必须先构造才能在构造函数中赋值。这会导致构造函数必须先构造一个string
中间变量才能赋值给name
; - 对于引用类型成员,const 成员需要预初始化;
- 对象成员初始化需要参数。
为了提升初始化性能,
C++
引入了默认成员初始化器和初始化器列表
语法
与其他函数不同,构造函数除了有名字,参数列表和函数体之外,还可以有初始化器列表,初始化器列表以冒号开头,后跟一系列以逗号分隔的成员初始化器。
1 | Date(int y, int m) : year(y), month(m) {} |
要点
必须使用初始化器列表的时候
除了性能问题之外,有些时候初始化器列表是不可或缺的.以下几种情况时必须使用初始化器列表:
- 常量成员,因为常量只能初始化不能赋值,所以必须放在初始化器列表里面
- 引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要使用初始化器
- 没有默认构造函数的类类型(class type),因为使用初始化器列表可以不必调用无参构造函数来初始化,而是直接用其他构造函数初始化
成员变量声明的顺序
成员是按照他们在类中声明的顺序进行初始化的,而不是按照他们在初始化器列表出现的顺序初始化的
1 | foo(int x) : i(x), j(i) {} |
一个好的习惯是,按照成员声明的顺序进行初始化
示例
1 | /*member initialization*/ |
练习
对象的内存布局
C++内存格局见 {post_link C-note-4}
date area 存放全局变量,静态数据和常量
code area 存放所有类成员函数和非成员函数代码
stack area 存放为运行函数而分配的局部变量、函数参数、返回数据、返回地址等(栈区)
余下的空间都被称为 heap area(堆区)
在类的定义时:
- 类的成员函数被放在 code area
- 类的静态成员变量在 data area
- 非静态成员变量、虚函数指针、虚基类指针在类的实例内,实例在 stack area 或 data area
类的实例
如果是定义的类变量,则在 stack area
如果是new出来的类指针,则在 heap area,同时引用会保存在 heap
注意:
- 对象中包含成员函数指针浪费内存
- 与
C
库struct
不兼容
C++
使用静态联编
对象方法静态联编:
指在编译阶段,就能直接使用代码段函数地址调用动态对象的方法。该方法仅需要向非静态成员函数传送this指针,即可用静态函数调用实现动态调用效果。
优势:
- 对象布局与
C
结构内存布局一致,使得内存中对象便于与其他语言程序库兼容 - 高效率,高性能
拷贝构造函数
概念
在定义语句中用同类型的对象初始化另一个对象
1 | //假定 C 为已定义的类 |
语法
- 用类类型(class type)本身作为参数
- 该参数传递方式为按引用传递,避免在函数调用过程中生成形参副本。
- 该形参声明为
const
,以确保在拷贝构造函数中不修改实参的值例如:1
C::C(const C& obj);
注:1
complex(const complex& other);
- C::C(C& obj) 不用 const 形式已过时。
- C::C(C&& obj) 形式称为移动构造函数(超纲)
要点
- 形参类型为该类类型本身且参数传递方式为按引用传递
- 用一个已存在的该类对象初始化新创建的对象
- 每个类都必须有拷贝构造函数:
- 用户可根据自己的需要显式定义拷贝构造函数
- 若用户未提供,则该类使用由系统提供的缺省拷贝构造函数(可用
= default
),也可用= delete
弃置该函数 - 缺省拷贝构造函数按照初始化顺序,对对象的各基类和非静态成员进行完整的逐成员复制,完成新对象的初始化。即逐一调用成员的拷贝构造,如果成员是基础类型,则复制值(赋值)
隐式调用复制构造
对象作为函数值参
将一个对象作为实参,以按值调用方式传递给被调用的形参对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// 假定C为已定义的类,obj为 C 类对象
void fn(C tmp) {...}
/
/
/
/
/
fn(obj);
// 用obj初始化tmp,如果有 C 类明确定义拷贝构造函数,将调用其;如果没有,将调用缺省拷贝构造函数
// obj传递给fn函数,创建对象tmp时,调用 C 的拷贝构造函数用对象obj初始化对象tmp,tmp生存期结束时调用析构函数
2. 对象作为值从函数返回
生成一个临时对象,作为函数的返回结果:
当函数返回某一对象时,系统将自动创建一个临时对象来保存函数的返回值。当创建此临时对象时,调用拷贝构造函数;当函数调用表达式结束后,撤销该临时对象,调用析构函数
```cpp
C fn() {
C t;
...
return t
}则
1
tmp = fn();
t -> temp_object -> tmp
- 编译优化: 实际的gcc/g++会优化使得tmp和new C的地址是一样的
1
2
3
4
5C fn() {
C tmp;
...
return tmp;
}
示例:
复制策略:拷贝构造函数自定义
浅拷贝只复制成员指针的值,而不复制指向的对象实体,导致新旧对象成员指针指向同一块内存。但深拷贝要求成员指针指向的对象也要复制,新对象跟原对象的成员指针不会指向同一块内存,修改新对象不会改到原对象。
- 对于不含指针成员的类,使用系统提供(编译器合成)的默认拷贝构造函数即可
- 缺省拷贝构造函数使用浅复制策略,不能满足对含指针数据成员的类需要
- 含指针成员的类通常应重写以下内容:
- 构造函数(及拷贝构造函数)中分配内存,深复制策略
- = 操作重写,完成对象深复制策略
- 析构函数中释放内存