C++构造函数详解

本文要点

  • 为什么需要构造函数
  • 默认构造函数什么时候有
  • 构造函数初始值有哪几种方式给出?
  • 如何使用构造函数
  • 什么是委托构造函数?
  • 如何禁止拷贝形式的初始化?

前言

我们在C语言中使用堆栈中的变量时,常常需要给它赋初始值,用于避免使用到了不可预知的值。同样的,在C++中也需要有这样的动作,只是由于C++的对象可能比基本数据要复杂得多,因此使用了一系列的函数来完成这件事。这些函数就是构造函数。那么构造函数到底是怎样,又该如何使用呢?

为什么需要构造函数

有的人可能会奇怪,发现自己写的代码即便没有初始化,也有正常的初始值,而且每次运行都是一样,而不是随机的,这是为什么呢?

1
2
3
4
5
6
7
#include<stdio.h>
int main(void)
{
int i;//没有初始化
printf("%d\n");//一般来说每次运行结果都是随机值
return 0;
}

首先,各个编译器实现可能不一样,具体怎么不一样,有兴趣可以去了解,但是需要注意的,按照标准来做,而不依赖于特定编译器的特性行为,即养成初始化的好习惯因此在C++也常常需要构造函数来控制对象的初始化。

关于初始化也可以参考《被遗忘的初始化》。

构造函数有什么特点

C++中的构造函数有哪些需要注意的呢?在《C++类初识》中已有所介绍,这里再稍微展开一下。

  • 构造函数没有返回值
  • 构造函数名与类名相同
  • 构造函数可以重载
  • 构造函数不能被声明成const

构造函数的返回值我们是拿不到的,因而其返回值对我们来说也是没有意义的。

一个类可以有多个构造函数,其函数名一致,形参不同,因而构造函数可以重载。

我们创建类的一个const对象的时候,需要等到构造函数执行完成,或者说只有初始化完成,才能有真正的const属性。如果构造函数被声明成const,那么它就不能修改成员变量,这样就没法完成初始化了。如下面的代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;
class Test
{
private:
int a;
public:
Test() const
{
/*do something*/
}
};
int main()
{
Test test;
cout<<sizeof(test)<<endl;
return 0;
}

如果将构造函数声明为const,将会出现编译错误:

1
2
error: constructors may not be cv-qualified
Test() const

提示构造函数不能被const修饰。

默认构造函数

如果没有定义任何构造函数,编译器会为我们提供无参的默认构造函数。但是有例外

  • 如果定义了自己的构造函数,编译器也不会提供默认构造函数。
  • 如果类中某个成员它自己没有默认构造函数(无参构造函数),那么编译器也就不能合成默认构造函数

请看下面的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;
class Foo
{
int value = 0;
public:
Foo(int val):value(val){}
};
class Test
{
private:
Foo foo;
};
int main()
{
Test test;
cout<<sizeof(test);
return 0;
}

我们定义了两个类,一个Foo,有一个构造函数,但是没有无参构造函数;一个Test,没有定义构造函数,准备让编译器生成默认的。但是不幸的是编译器报错了:

1
no matching function for call to ‘Foo::Foo()’

也就是说,它试图去调用自己成员的无参构造函数,但是由于成员自己没有,所以报错了。

另外,从这个例子中我们也可以看到,由于Foo中已经定义了自己的构造函数,因此编译器不会为它生成默认的构造函数。

如何解决呢?只需要在Foo类中增加这么一句就可以使用默认构造函数了:

1
Foo()= default;

构造函数初始值

为了在构造函数中给成员赋初始值,可以用下面的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//公众号【编程珠玑】,博客 https://www.yanbinghu.com
#include <iostream>
using namespace std;
class Test
{
private:
int age;
string name;
int month;
public:
/*为成员提供初始值*/
Test(const string &n,int a):name(n),age(a),month(a*12){}
void printTest()
{
cout<<"name:"<<name<<",age:"<<age<<",month:"<<month<<endl;
}
};
int main()
{
Test test("编程珠玑",5);
test.printTest();
return 0;
}

输出结果:

1
name:编程珠玑,age:5,month:60

注意观察Test的构造函数,圆括号内和其他普通函数一样是入参,不过后面跟一个冒号,每个成员变量又用圆括号将初始值括起来。这种赋初始值的方式与下面这种方式的效果是相同的:

1
2
3
4
5
6
Test(const string &n,int a)
{
name = n;
age = a;
month = a *12;
}

但从深层次来说,它们是有区别的,我们将在后面看到。

除此之外,实际上还可以为构造函数提供默认实参。注意的是,为了和入参对应上,默认实参要从后面的实参开始提供,例如,我们可以为a提供默认实参,而n不提供,但是不能为n提供,而a不提供,因为a在其后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//公众号【编程珠玑】,博客 https://www.yanbinghu.com
#include <iostream>
using namespace std;
class Test
{
private:
int age;
string name;
int month;
public:
/*这里为a提供了默认实参*/
Test(const string &n,int a = 3):name(n),age(a),month(a*12){}
void printTest()
{
cout<<"name:"<<name<<",age:"<<age<<",month:"<<month<<endl;
}
};
int main()
{
Test test("编程珠玑");
test.printTest();
return 0;
}

输出结果:

1
name:编程珠玑,age:3,month:36

在这里我们为a提供了默认实参值0,因此只传入一个参数也可以构造Test对象。

初始化const和引用成员

不知道你是否还记得,对于const类型或者是引用类型,我们必须给它初始化,因此对于有const或者引用的成员变量,必须在构造函数中给它初始化,注意是初始化,而不是赋值

1
2
3
4
5
6
7
8
9
#include <iostream>
using namespace std;
class Test
{
/*没有对const和引用类型初始化,会报错*/
private:
const int age;
string &name;
};

这里没有初始化,会报错,下面这里使用了赋值形式的初始化,同样报错:

1
2
3
4
5
6
7
8
9
10
11
12
class Test
{
private:
const int age;
string &name;
public:
Test(int a,string n)
{
age = a;
name = n;
}
};

可行的做法如下:

1
2
3
4
5
6
7
8
class Test
{
private:
const int age;
string &name;
public:
Test(int a,string n):age(a),name(n){}
};

来源:公众号【编程珠玑】
博客:https://www.yanbinghu.com

委托构造函数

C++11中提供的委托构造函数说白了就是构造函数使用其他定义好的构造函数执行自己的初始化过程。例如:

1
2
3
4
5
6
7
8
9
10
11
class Test
{
private:
int age;
string name;
int month;
public:
Test(const string &n,int a):name(n),age(a),month(a*12){}
/*委托构造函数*/
Test(const string &n):Test(n,0){}
}

只有一个入参n的构造函数通过有两个入参的构造函数来完成初始化过程。

使用构造函数

在前面我们已经看到了构造函数的使用方式。对于有参构造函数,使用

1
类名 变量名(实参);

的方式,例如前面看到的:

1
Test test("编程珠玑",5);

而对于默认构造函数,或者说无参构造函数则不能这样:

1
Test test();

由于这种构造函数无参,这会被编译器认为是一个函数的声明,因此要用下面的方式:

1
Test test;

禁止拷贝形式的初始化

在没有其他限制的情况下,对于只有一个实参的构造函数而言,可以使用拷贝形式的初始化,即在初始化test的时候,可以直接将name赋给它而完成初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//公众号【编程珠玑】,博客 https://www.yanbinghu.com
#include <iostream>
using namespace std;
class Test
{
private:
int age;
string name;
int month;
public:
Test(const string &n,int a = 0):name(n),age(a),month(a*12){}
void printTest()
{
cout<<"name:"<<name<<",age:"<<age<<",month:"<<month<<endl;
}
};
int main()
{
string name = "编程珠玑";
/*拷贝形式的初始化*/
Test test = name;
test.printTest();
return 0;
}

而如果要禁止这种形式的初始化,可以使用explicit关键字声明构造函数:

1
explicit Test(const string &n,int a = 0):name(n),age(a),month(a*12){}

但是需要注意的是,explicit关键字只允许在类内的构造函数声明处,如果构造函数在类外声明,则是不允许的。

总结

关于构造函数的内容还有很多,在介绍继承,多态,拷贝,移动等内容后再展开,本文总结如下:

  • 构造函数没有返回值
  • 构造函数名与类名相同
  • 构造函数可以重载
  • 构造函数不能被声明成const
  • 对于只有一个实参的构造函数而言,可以使用拷贝形式的初始化
  • 类中某个成员它自己没有默认构造函数(无参构造函数),那么编译器也就无法合成默认构造函数
  • 如果定义了自己的构造函数,编译器将不会合成默认构造函数
  • 对于有const或者引用的成员变量,必须在构造函数中给它初始化

参考:《C++ primer》

守望 wechat
关注公众号[编程珠玑]获取更多原创技术文章
出入相友,守望相助!