Fork me on GitHub

Cpp 构造函数初始化列表

Cpp 构造函数初始化列表

本文完全 copy 自 C/C++ : 构造函数的初始化列表_c构造函数初始化列表-CSDN博客

对某些情况做了补充

1. 初始化列表

与一般函数不同,构造函数除了有名字、形参列表和函数体外,还可以有 初始化列表

初始化列表以冒号 : 开头,后跟一系列以逗号分隔的初始化字段。代码示例:

(Point 类表示平面上的一个点,有两个私有成员 x, y,分别表示该点的 (x, y) 坐标的值)。

class Point
{
public:
    Point(int xx, int yy) : x(xx), y(yy) 
    {
        std::cout << "Constructor of Point" << std::endl;
    }
private:
    float x, y;
};

上面代码在功能上等价于下面代码:

class Point
{
public:
    Point(int xx, int yy)
    {
        x = xx;
        y = yy;
        std::cout << "Contructor of Point" << std::endl;
    }
private:
    float x, y;
};

上面两段代码对应于 初始化类成员的两种方式:

1. 使用初始化列表

2. 在构造函数体中进行赋值操作。

但严格来说,上面两段代码只是能实现相同的功能(初始化 Point 类的对象),它们的本质不同:

构造函数的执行分为两个阶段:

1. 执行初始化列表:初始化类中的数据成员;

2. 执行构造函数体:一般是对类中的数据成员进行赋值操作。

初始化与赋值是不同的,后面具体讲解。

2. 使用初始化列表的原因

  1. 某些情况下不得不使用。
  2. 提高效率。

2.1 必须使用初始化列表的情况

2.1.1 类中有常量类型的数据成员

因为常量只能初始化不能赋值,所以 必须在构造函数的初始化列表中对常量类型的数据成员进行初始化

代码示例:

class Point
{
public:
    Point(int xx, int yy) : x(xx), y(yy) 
    {
        std::cout << "Constructor of Point" << std::endl;
    }
    // error
    // Point(float xx, float yy)
    // {
    //     x = xx;
    //     y = yy;
    //     std::cout << "Constructor of Point" << std::endl;
    // }
    // 这样写会报错 `expression must be a modifiable lvalue`
    
private:
    const float x, y;
};

image-20250409153912677

2.1.2 类中有引用类型的数据成员

因为引用类型必须在初始化时绑定引用的对象,所以必须在构造函数的初始化列表中对引用类型的数据成员进行初始化。

代码示例:

class Point
{
public:
    Point(float xx, float yy) : x(xx), y(yy)
    {
        std::cout << "Contructor of Point" << std::endl;
    }
    // error:
    // Point(float xx, float yy)
    // {
    //     x = xx;
    // 	   y = yy;
    // }
}
    
private:
    float &x, &y;
};

image-20250409154300406

这样写会报错 xxx provides no initializer for : reference member "xxxx"

注意此时,构造函数中的参数就必须是 float 类型了,不能是 int 类型,如果还是 int 类型,会报错

a reference of type "float &" (not const-qualified) cannot be initialized with a value of type "int"

image-20250409154541252

2.1.3 类中有无默认构造函数的内嵌对象

内嵌对象 的构造函数只能在 组合类构造函数的初始化列表中调用,不能在 组合类的函数体中调用。

1. 内嵌对象有默认构造函数

当内嵌对象有默认构造函数时,此时在组合类构造函数的初始化列表中可以不显式地为内嵌对象初始化。因为初始化列表会自动调用内嵌对象的默认构造函数,从而初始化内嵌对象。

代码示例:(Line 类为平面上的一条直线,它由平面上的两个点确定)

#include <iostream>

class Point
{
public:
    Point() 
    {
        std::cout << "Defaut constructor of Point" << std::endl;
    }

private:
    float x, y;
};

class Line
{
public:
    Line()
    {
        std::cout << "Default constructor of Line" << std::endl;
    }

private:
    Point p1, p2;
};

int main()
{
    Line l1;
    return 0;
}

输出结果:

Defaut constructor of Point
Defaut constructor of Point
Default constructor of Line

即,内嵌对象有默认构造函数的,组合类的构造函数初始化列表中没有显式为内嵌对象初始化,但是会自动调用内嵌对象的构造函数。

2. 内嵌对象没有默认构造函数

此时,组合类的构造函数的初始化列表中必须显式地为内嵌对象初始化,同时提供必要的参数

因为 C++ 不能自动调用有参的构造函数,而调用内嵌对象的构造函数时又必须提供参数(因为内嵌对象无默认构造函数)。

现在讲讲为什么:

还是 2.3.1.1 中的代码,只是做一个修改,将 Point 类中的无参构造函数注释掉,如下

#include <iostream>

class Point
{
public:
    // Point() 
    // {
    //     std::cout << "Defaut constructor of Point" << std::endl;
    // }

private:
    float x, y;
};

class Line
{
public:
    Line()
    {
        std::cout << "Default constructor of Line" << std::endl;
    }

private:
    Point p1, p2;
};

int main()
{
    Line l1;
    return 0;
}

现在,Point 类无默认构造函数,编译器会自动生成隐含的默认构造函数,上面代码不会报错。也就是在语法和编译层面没有问题,但是在成员变量的初始化逻辑上存在潜在隐患。

  1. Point 类的情况

    • 注释掉了显式地构造函数,但是编译器会生成一个隐式的默认构造函数。

    • 隐式生成的默认构造函数不会初始化 float x, y,这些成员变量的值是未定义的(垃圾值)。

      就上面的代码而言,x 和 y 是私有成员变量,没有初始化,那在 main 函数中和 Line 类中都是无法获取 x 和 y 的

      那如果是 public 的呢?理论上如果 x 和 y 是 public 的,是不是就不需要在构造函数中初始化了呢?

      答案是否定的,也需要,例子如下:

      #include <iostream>
      
      class Point
      {
      public:
          // 无默认构造函数
          float x, y;
          void func()
          {
              std::cout << x << std::endl;
          }
      };
      
      int main()
      {
          Point p1;
          p1.func();
          return 0;
      }
      

      这里不会报错,但是 x 未定义,打印出来的值有问题

      image-20250409180124165

因此,建议在构造函数中显式初始化成员变量(其实不是建议了,而是必须)。

示例代码:

#include <iostream>

class Point
{
public:
    Point(int xx, int yy) : x(xx), y(yy)
    {
        std::cout << "Constructor of Point" << std::endl;
    }
    
    // copy construtor
    Point(const Point& P) : x(P.x), y(P.y)
    {
        std::cout << "Copy constructor of Point" << std::endl;
    }
private:
    int x, y;
};

class Line
{
public:
    Line(Point& pp1, Point& pp2) : p1(pp1), p2(pp2)
    {
        std::cout << "Constructor of Line" << std::endl;
    }
    
private:
    Point p1, p2;
};

int main()
{
    Point p1(1, 0), p2(0, 1);
    Line l1(p1, p2);
    return 0;
}

这段代码会输出什么?

Contructor of Point
Contructor of Point
Copy constructor of Point
Copy constructor of Point
Constructor of Line

问题:如果 Line 类的构造函数中的参数是 Point 类对象的值而不是引用,输出会是什么?

即代码为

class Line
{
public:
    Line(Point pp1, Point pp2) : p1(pp1), p2(pp2)
    {
        std::cout << "Constructor of Line" << std::endl;
    }
    
private:
    Point p1, p2;
}

答:会输出

Contructor of Point
Contructor of Point
Copy constructor of Point
Copy constructor of Point
Copy constructor of Point
Copy constructor of Point
Constructor of Line

解释一下为什么:

第一种写法,参数按引用传递

  • 参数是 Point& (引用),因此 不会创建临时对象,直接绑定到 p1p2 的引用。

当初始化成员变量 p1p2 时:

: p1(pp1), p2(pp2)
  • 由于成员变量 p1p2Point 类型,需要通过拷贝构造函数从引用 pp1pp2 初始化。

  • 调用两次拷贝构造函数

  • 输出:

    Copy constructor of Point
    Copy constructor of Point
    

第二种写法,参数按值传递

  • 参数是 Point (值传递),因此:

    • 需要将 main 中的 p1 拷贝到形参 pp1
    • 需要将 main 中的 p2 拷贝到形参 pp2
  • 调用两次 copy constructor

  • 输出

    Copy constructor of Point
    Copy constructor of Point
    
  • 然后初始化 member variable

    : p1(pp1), p2(pp2)
    
    • member variable p1 需要通过 copy constructor 从形参 pp1 初始化
    • member variable p2 需要通过 copy constructor 从形参 pp2 初始化
    • 再次调用两次拷贝构造函数
  • 输出

    Copy constructor of Point
    Copy constructor of Point
    

再来一个变体,如果 Line 类是这样的

class Line
{
public:
    Line(Point& p1, Point& p2) : p1(pp1), p2(pp2)
    {
        std::cout << "Constructor of Line" << std::endl;
    }
private:
    Point &p1, &p2;
};

这样会输出

Contructor of Point
Contructor of Point
Constructor of Line

也就是说根本就不会调用拷贝构造函数,因为都是按引用传递,不会创建临时对象,直接绑定到 p1, p2。

总结

组合类中的内嵌对象一定会在组合类构造函数的初始化列表中被初始化。如果内嵌对象无默认构造函数,那么一定要在组合类构造函数的初始化列表中显式地初始化;如果内嵌对象有默认构造函数,那么在组合类构造函数的初始化列表中可以不显式地初始化,但组合类构造函数的初始化列表会自动调用内嵌对象的默认构造函数,从而为其初始化。

2.1.4 基类无默认构造函数

1. 基类有默认构造函数

代码:

#include <iostream>

class Base
{
public:
    Base()
    {
        std::cout << "Default constructor of Base" << std::endl;
    }
    
private:
    float x, y;
};

class Derived : public Base
{
    public:
    Derived()
    {
        std::cout << "Default constructor of Derived" << std::endl;
    }
};

int main()
{
    Derived d;
    return 0;
}

输出结果:

Default constructor of Base
Default constructor of Derived
2. 基类无默认构造函数

代码:

#include <iostream>

class Base
{
public:
    Base(float xx, float yy) : x(xx), y(yy)
    {
        std::cout << "Default constructor of Base" << std::endl;
    }
    
private:
    float x, y;
};

class Derived : public Base
{
    public:
    Derived(float xx, float yy) : Base(xx, yy)
    {
        std::cout << "Default constructor of Derived" << std::endl;
    }
};

int main()
{
    Derived d(0, 0);
    return 0;
}

输出结果:

Default constructor of Base   
Default constructor of Derived

也就是说,基类没有构造函数的时候,需要在构造函数的初始化列表中显式初始化成员变量

3. 初始化列表如何提高效率

效率提高的关键点在于:如果没有使用初始化列表,会隐式自动调用默认构造函数进行初始化

用代码示例说明:

3.1 不使用初始化列表的情况

代码:

Family 类中内嵌有一个 Member 类对象成员,其中 Member 类中有默认构造函数、拷贝构造函数和重载的赋值运算符以及私有成员 age

#include <iostream>

class Member
{
public:
    Member()
    {
        std::cout << "Default constructor of Member" << std::endl;
    }
    
    // copy constructor
    Member(const Member& m) : age(m.age)
    {
        std::cout << "Copy constructor of Member" << std::endl;
    }
    
    Member& operator = (const Member& m)
    {
        age = m.age;
        std::cout << "Assignment of Member" << std::endl;
        return *this;		// 返回对 this 的解引用
    }
    
private:
	int age;
};

class Family
{
public:
    Family(Member& m)
    {
    	m1 = m;
        std::cout << "Constructor of Family" << std::endl;
    }
    
private:
    Member m1;
};

int main()
{
	Member mm1; 
    Family f1(mm1);
    
    return 0;
}

输出结果:

Default constructor of Member
Default constructor of Member
Assignment of Member
Constructor of Family

Member mm1; 打印第一行

Family f1(mm1);

关键点:成员变量 m1 的初始化发生在构造函数体之前

  • 由于 Family 的构造函数 没有使用成员初始化列表 显式初始化 m1:

    // 实际发生的隐式初始化步骤:
    Family(Member& m) : m1()		// 隐式调用默认构造函数
    {
        m1 = m;
        // ...
    }
    
  • 因此,在进入构造函数体之前,成员 m1 会被 自动调用默认构造函数初始化,产生第二行输出。

后面就不解释了

3.2 使用初始化列表的情况

#include <iostream>

class Member
{
public:
    Member()
    {
        std::cout << "Constructor of Member" << std::endl;
    }
    
    Member(const Member& M) : age(M.age)
    {
        std::cout << "Copy Contructor of Member" << std::endl;
    }
    
    Member& operator = (const Member& M)
    {
        age = M.age;
        std::cout << "Assignment of Member" << std::endl;
        return *this;
    }
    
private:
    int age;
};

class Family
{
public:
    Family(Member& mm1) : m1(mm1)
    {
        std::cout << "Constructor of Family" << std::endl;
    }
private:
    Member m1;
};

int main()
{
    Member mm1;
    Family f1(mm1);
    return 0;
}

输出结果:

Constructor of Member
Copy Contructor of Member
Constructor of Family

第一行不说了

第二行,在进入 Family f1(mm1) 的时候,Family(Member& mm1) 这里参数是引用传递,没有 copy,后面 : m1(mm1) 的时候是值传递,需要调用 Member 的 copy constructor 来初始化 f1 的私有成员 m1

然后第三行。

对比可以发现,使用初始化列表少了一次调用默认构造函数的过程。对于数据密集型的类来说,能提高工作效率。

本文完全 copy 自 C/C++ : 构造函数的初始化列表_c构造函数初始化列表-CSDN博客,仅仅对部分情况做了补充。

posted @ 2025-04-09 21:54  icewalnut  阅读(30)  评论(0)    收藏  举报