C++ note (5)
Eiaton

数据抽象和类 Ⅳ

对象成员初始化

非静态数据成员初始化方法:

  • 在构造函数的成员 初始化器列表 中。(C++11)
  • 通过 默认成员初始化器 ,它是成员声明中包含的 花括号 或 等号 初始化器。(C++11)
  • 构造函数体内进行赋值操作。(不要构造成员,除非特别熟悉 new 的用法)

为什么需要初始化器列表和默认成员初始化器?

  • 对象初始化分两个阶段:先按声明顺序初始化成员、然后执行构造函数函数体;
  • 对于复杂对象成员,如 std::string name,必须先构造才能在构造函数中赋值。这会导致构造函数必须先构造一个 string 中间变量才能赋值给 name
  • 对于引用类型成员,const 成员需要预初始化;
  • 对象成员初始化需要参数

为了提升初始化性能C++ 引入了默认成员初始化器和初始化器列表

语法

与其他函数不同,构造函数除了有名字,参数列表和函数体之外,还可以有初始化器列表,初始化器列表以冒号开头,后跟一系列以逗号分隔的成员初始化器。

1
Date(int y, int m) : year(y), month(m) {}

要点

必须使用初始化器列表的时候

除了性能问题之外,有些时候初始化器列表是不可或缺的.以下几种情况时必须使用初始化器列表:

  1. 常量成员,因为常量只能初始化不能赋值,所以必须放在初始化器列表里面
  2. 引用类型,引用必须在定义的时候初始化并且不能重新赋值,所以也要使用初始化器
  3. 没有默认构造函数的类类型(class type),因为使用初始化器列表可以不必调用无参构造函数来初始化,而是直接用其他构造函数初始化

成员变量声明的顺序

成员是按照他们在类中声明的顺序进行初始化的,而不是按照他们在初始化器列表出现的顺序初始化的

1
2
foo(int x) : i(x), j(i) {}
// 先初始化i,后初始化j

一个好的习惯是,按照成员声明的顺序进行初始化

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*member initialization*/
struct S {

int n;
// 非静态数据成员
int& r = n;
// 引用类型的非静态数据成员; =默认构造
int a[2] = {1, 2};
// 带默认成员初始化器的非静态数据成员(C++11)
string s{'H','C'};
// 带默认成员初始化器

struct nestedS {
string s;
nestedS(std::string s = "hello") : s(s) {};
} d5;
// 具有嵌套类型的非静态数据成员

const char bit : 2;
// 2 位的位域, const初始化

S() : n(7), bit(3) {}
// " : n(7), bit(3)" 是初始化器列表; "{}" 是函数体
S(int x) : n{x}, bit(3) {}
};

int main() {
S s; // 调用 S::S()
S s2(10); // 调用 S::S(int)
}

练习

对象的内存布局

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

注意:

  1. 对象中包含成员函数指针浪费内存
  2. Cstruct 不兼容

C++ 使用静态联编

对象方法静态联编:

指在编译阶段,就能直接使用代码段函数地址调用动态对象的方法。该方法仅需要向非静态成员函数传送this指针,即可用静态函数调用实现动态调用效果。

优势:

  1. 对象布局与 C 结构内存布局一致,使得内存中对象便于与其他语言程序库兼容
  2. 高效率,高性能

拷贝构造函数

概念

在定义语句中用同类型的对象初始化另一个对象

1
2
3
4
5
6
7
8
9
10
11
//假定 C 为已定义的类
C obj1; //调用 C 的(1)无参构造函数
C obj1(1,2) //调用 C 的有参(2)普通构造函数
//如无(1)(2)则调用默认构造函数

/*调用 C 的(3)拷贝构造函数用对象obj1初始化对象obj2。
如果有为 C 类明确定义拷贝构造函数,将调用这个拷贝构造函数;
如果没有为 C 类定义拷贝构造函数,将调用默认拷贝构造函数。*/

C obj2(obja); //或
C obj2 = obja ; //两者等价(注意:不是赋值运算)

语法

  • 用类类型(class type)本身作为参数
  • 该参数传递方式为按引用传递避免在函数调用过程中生成形参副本
  • 该形参声明为 const以确保在拷贝构造函数中不修改实参的值
    1
    C::C(const C& obj);
    例如:
    1
    complex(const complex& other);
    注:
  1. C::C(C& obj) 不用 const 形式已过时。
  2. C::C(C&& obj) 形式称为移动构造函数(超纲)

要点

  • 形参类型为该类类型本身且参数传递方式为按引用传递
  • 用一个已存在的该类对象初始化新创建的对象
  • 每个类都必须有拷贝构造函数:
    • 用户可根据自己的需要显式定义拷贝构造函数
    • 若用户未提供,则该类使用由系统提供的缺省拷贝构造函数(可用= default),也可用 = delete 弃置该函数
    • 缺省拷贝构造函数按照初始化顺序,对对象的各基类和非静态成员进行完整的逐成员复制,完成新对象的初始化。即逐一调用成员的拷贝构造,如果成员是基础类型,则复制值(赋值)

隐式调用复制构造

  1. 对象作为函数值参

    将一个对象作为实参,以按值调用方式传递给被调用的形参对象

    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

  • 编译优化:
    1
    2
    3
    4
    5
    C fn() {
    C tmp;
    ...
    return tmp;
    }
    实际的gcc/g++会优化使得tmp和new C的地址是一样的

示例:

Example 1

复制策略:拷贝构造函数自定义

浅拷贝只复制成员指针的值,而不复制指向的对象实体,导致新旧对象成员指针指向同一块内存。但深拷贝要求成员指针指向的对象也要复制,新对象跟原对象的成员指针不会指向同一块内存,修改新对象不会改到原对象。

  1. 对于不含指针成员的类,使用系统提供(编译器合成)的默认拷贝构造函数即可
  2. 缺省拷贝构造函数使用浅复制策略,不能满足对含指针数据成员的类需要
  3. 含指针成员的类通常应重写以下内容:
  • 构造函数(及拷贝构造函数)分配内存,深复制策略
  • = 操作重写,完成对象深复制策略
  • 析构函数释放内存

示例:

Example 2