成员变量初始化

构造函数内部初始化和初始化列表

初始化类的成员有两种方式,一是使用初始化列表,二是在构造函数体内进行赋值操作。

  • 对于内置类型,如int, float等,使用初始化列表和在构造函数体内初始化差别不大;
  • 对于类类型来说,使用初始化列表会减少调用默认构造函数的次数,更加高效。
 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
class A {
public:
    A() { cout << "A()" << endl; }
    A(int _a) {
        a = _a;
        cout << "A(_a)" << endl;
    }

private:
    int a;
};

class B {
public:
    B() { cout << "B()" << endl; }
    B(int _b) : b(_b), m_a(_b) { // 初始化列表初始化
        // b = _b, m_a = A(_b);  // 构造函数内部赋值操作
        cout << "B(_b)" << endl;
    }

private:
    int b;
    A m_a;
};

int main(int argc, char** argv) {
    B b(100);
}

这段代码输出:

1
2
A(_a)
B(_b)

如果不使用初始化列表,使用17行构造函数内部赋值的方式,会输出:

1
2
3
A()
A(_a)
B(_b)

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

  1. 类的const成员

  2. 引用类型

    const对象或引用只能初始化但是不能赋值,构造函数的函数体内只能做赋值而不是初始化。因此初始化const对象或引用的唯一机会是构造函数函数体之前的初始化列表中。

  3. 没有默认构造函数的类类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class A {
public:
    A(int a): i(a) {}
    int i;
};

class B {
public:
    A a ;
    B(const A& a1) {  
        a = a1;  // error: 类A不存在默认构造函数
    }
};

class C: public A {
public:
    C(int _x): x(_x) {}  // error: 类A不存在默认构造函数
private:
    int x;
}

以上代码无法通过编译,因为B的构造函数中a=a1这一行会先调用A的默认构造函数来初始化a1,由于A没有默认的构造函数,所以无法执行,故而编译错误。

如果基类没有默认构造函数,派生类必须在其初始化列表中显示调用基类的构造函数。

构造函数调用顺序

  1. 调用基类构造函数,调用顺序按照他们的继承时声明的顺序。
  2. 调用内嵌成员对象的构造函数,调用顺序按照他们在类中声明的顺序。
  3. 调用派生类自己的构造函数体中的内容

析构函数的调用顺序相反。

更详细的:构造函数的执行可以分成两个阶段,初始化阶段和计算阶段,初始化阶段先于计算阶段。

  1. 初始化阶段:所有类类型(class type)的成员都会在初始化阶段初始化,即使该成员没有出现在构造函数的初始化列表中。
  2. 计算阶段:一般用于执行构造函数体内的赋值操作。

所以,初始化列表总是先于构造函数体执行:基类初始化列表 → 基类的构造函数 → 派生类初始化列表 → 派生类的构造函数。

成员变量初始化顺序

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class A {
private:
    int n1;
    int n2;
	
public:
    A(): n2(0), n1(n2+1) {}
    void Print(){
        cout << "n1: " << n1 << ", n2: " << n2 <<endl;  
    }
};
 
int main() {
    A a;
    a.Print();  // 输出是:`n1: 18749281, n2: 0`,而不是`n1: 1, n2: 0`
    return 0;
}

const和constexpr

constexpr关键字是C++11新增的,其作用包括:

  1. 定义编译期常量[编译期常量]
  2. 定义常量表达式函数[常量表达式函数]
  3. 定义编译期常量对象[编译期常量对象]

常量分为编译期常量运行期常量。 编译期常量在编译阶段就可以确定其值,并将其结果展开到使用的地方,一般存放在rodata段。 运行期常量本质上是只读的变量(存放在栈区),编译时无法确定其值;运行时,无法修改其值。

  • const 可以定义编译期常量,也可以定义运行期常量;
  • constexpr 只能定义编译期常量。
1
2
3
4
5
6
7
const int a = 10;  // 编译期常量,存放在静态存储区的rodata段

int main() {
    int b = 20;      // 栈
    const int c = b; // 栈(运行期常量)
    // constexpr int c = b; Error:  因为b是一个普通变量,必须到运行期才能确定,constexpr无法修饰运行期常量。
}

普通函数必须在运行时才能执行,进而计算出结果。而常量表达式函数要求函数在编译期就计算出结果,运行时直接使用结果。也就是说将函数的执行从运行阶段转移到编译阶段,提升程序运行效率。 只需要在函数返回值类型前面加上constexpr关键字即可定义常量表达式函数,我们必须使用constexpr修饰的编译期常量来保存常量表达式函数的结果,否则常量表达式函数仍然会在运行期执行。

1
2
3
4
5
6
7
8
constexpr int my_sum(int n) {
    if (n == 1) return 1;
    return n + my_sum(n - 1);
}
 
void test() {
    constexpr int a = my_sum(3);
}

编译期常量对象的任何计算都在编译期完成。定义编译期常量对象,有以下几点要求:

  1. 构造函数使用constexpr修饰,必须使用初始化列表对成员进行初始化;
  2. 对象调用的成员函数必须使用constexpr修饰。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Box {
public:
    // 构造函数使用constexptr修饰,并且使用初始化列表对成员进行初始化。这就保证了对象成员m_l、m_w、m_h在编译期确定其值。
    constexpr Box(int l, int w, int h) : m_l(l), m_w(w), m_h(h) {}
    
    // get_volume函数被定义为了常量表达式成员函数,调用该函数则会在编译阶段计算出结果。
    constexpr int get_volume() const {
        return m_l * m_w * m_h;
    }
 	
    // get_sum函数则是普通函数,该函数必须声明为const函数,否则无法被常量对象调用
    int get_sum() const {
        return (m_l + m_w + m_h);
    }
 
public:
    int m_l, m_w, m_h;
};
 
void test() {
    constexpr Box box(10, 20, 30);  // 程序编译期间调用常量表达式构造函数
    constexpr int volume = box.get_volume();  // 在编译期计算出函数的执行结果,并赋值给编译期常量volume
    int my_sum = box.get_sum();  // 在运行期进行执行
}

单例模式

单例模式是一种设计模式,它确保一个类只有一个实例,并且提供一个全局访问点来访问它。 单例模式的实现一般需要将构造函数、析构函数私有化,禁用拷贝构造函数和赋值运算符,将成员变量和成员函数都设置成静态类型

懒汉单例模式实现

  • 最简单的懒汉模式(在全局访问入口中声明静态变量。局部静态变量在C++11后也是线程安全的)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Singleton {
private:
    Singleton();
    ~Singleton();
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    
public:
    static Singleton* getInstance() {
        static Singleton instance;
        return &instance;
    }
};
  • 加锁版本的懒汉单例模式
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Singleton {
private:
    Singleton();
    ~Singleton();
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static Singleton* instance;
    static pthread_mutex_t mutex;
    
public:
    static Singleton* getInstance() {
        if (instance == nullptr) {
            pthread_mutex_lock(&mutex);
            instance = new Singleton();
            pthread_mutex_unlock(&mutex);
        }
        return instance;
    }
};
Singleton* Singleton::instance = nullptr;
pthread_mutex_t Singleton::mutex = PTHREAD_MUTEX_INITIALIZER;

饿汉单例模式实现

饿汉单例模式天生线程安全,因为在main函数前已经初始化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Singleton {
private:
    Singleton();
    ~Singleton();
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    
    static Singleton* instance;
    
public:
    static Singleton* getInstance() { return instance; }
};
Singleton* Singleton::instance = new Singleton();