c++中的各类构造函数、类型转换和赋值

本篇博客讨论C++中的各种构造函数,包括列表初始化、隐式/显式构造函数、类的自动转换和强制转换、复制构造函数,还会顺带提一下相关的重载赋值运算符和返回值优化。

构造函数

最简单的构造函数就是使用类名作为构造函数的名称(无返回值),但是构造函数签名可以有多种,以表示对其重载。如果类中没有定义构造函数,编译器就会提供一个空的默认构造函数。

1
2
3
4
5
6
7
8
9
Stock::Stock(const string& co, long n, double pr)
{
// do something
}

int main()
{
Stock foo; // wrong
}

如果不提供无参数的构造函数,就不能像第8行那样直接声明一个对象,除非自行声明一个无参数的构造函数,或者提供一个有默认值的构造函数(但是不能两者都有)。调用默认构造函数时,不要在后面使用圆括号,否则编译器会认为声明了一个返回值为Stock的函数。

1
2
3
4
Stock::Stock(const string& co="hello", long n=0, double pr=0.0)
{
// do something
}

声明和使用构造函数时应该尽量避免二义性。

列表初始化

可以使用花括号提供与某个构造函数的参数列表相匹配的内容,这样就可以调用相应的构造函数:

1
2
3
Stock hot_tip = {"wwww",1000,45.0}; // match constructor with 3 arguments
Stock jock = {"aaa"}; // match constructor with 3 arguments, but the last 2 arguments are default value
Stock temp{}; // match default constructor

自动类型转换与强制类型转换

自动类型转换(隐式类型转换)

当在构造函数中只需要接受一个参数时,我们就可以使用自动类型转换来初始化一个对象。例如:

1
2
3
4
Stock::Stock(double lbs);
// ...
Stock myStock;
myStock = 19.6;

我们来看看这个过程发生了什么,首先编译器会找到19.6这个double值对应的构造函数,将19.6作为参数传递给构造函数,新建出一个临时的对象,然后再按照逐成员赋值的方式将临时对象的值赋给myStock。这个过程是自动执行的,所以成为自动类型转换。

如果一个构造函数有多个参数,但是其他参数被提供了默认值,那么也可以使用自动类型转换:

1
Stock::Stock(double lbs, int stn=0); // can be applied

但是需要注意,不能同时出现上面两种构造函数,否则编译器会报错,提示存在二义性。

总结,在以下情况编译器会使用自动类型转换:

  • 将Stock对象初始化为double值时;
  • 将double值赋给Stock对象时;
  • double值被传递给接受Stock参数的函数时;
  • 返回值被声明为Stock的函数试图返回double时;
  • 上述任意一种情况下,使用可转换为double类型的内置类型时(例如,将int类型的值传递给Stock对象,那么int类型会先转换为double,再经过上述操作)

强制类型转换(显式类型转换)

上面提到的特性可以自动将其他类型转换为我们想要的对象类型,但是有时这样的类型转换是我们不想要的,此时可以在构造函数前加上explicit关键字,从而保证该构造函数只能被我们显示地调用。

1
2
3
4
5
6
explicit Stock(double lbs);
// ...
Stock myCat;
myCat = 19.6; // not valid
myCat = Stock(19.6); // OK
myCat = (Stock)19.6; // OK

转换函数

上面提到将内置类型转换为我们想要的对象,那么转换函数就是将对象转换为对应的内置类型。转换函数的声明方法如下:

1
2
3
4
5
operator double();
// ...
Stock wolfe(285.7);
Stock host = double(wolfe);
Stock thinker = (double)wolfe;

使用转换函数时需要注意以下几点:

  • 转换函数必须是类方法
  • 转换函数不能指定返回值类型
  • 转换函数不能有参数

复制构造函数

复制构造函数用于将一个对象复制到新创建的对象中,与赋值运算符不同的是,它被运用于对象的初始化过程中,而非常规的赋值操作中。当在类中没有声明复制构造函数时,编译器会提供一个默认构造函数,其功能是逐个复制非静态成员(按值复制)。如果想声明一个复制构造函数,其格式如下:

1
Class_Name(const Class_Name &);

何时调用复制构造函数

新建一个对象并将其初始化为一个同类现有对象时,复制构造函数都将被调用。

1
2
3
4
StringBad ditto(motto); // calls copy constructor
StringBad metoo = motto; // calls copy constructor
StringBad also = StringBad(motto); // calss copy constructor
StringBad* pStringBad = new StringBad(motto); //calss copy constructor

其中中间的两种声明有可能使用复制构造函数直接创建两个对象,也有可能会创建一个临时对象,然后再将临时对象赋值给我们新建的对象。当一个对象以值传递的方式作为函数的参数,或者以值传递的方式作为返回值返回时,也会调用复制构造函数。

当作为返回值返回时,有时不会调用复制构造函数,在一些编译器中会使用返回值优化的方法来省去复制构造函数的调用过程。

什么情况下需要自定义复制构造函数

  • 当需要自定义析构函数时,通常也需要自定义复制构造函数;
  • 当类中有成员是指针,被使用new初始化时,需要自定义复制构造函数;
  • 当类中有数组,或者其他“复制构造函数已经被定义”的对象时,可以不定义复制构造函数。

重载赋值运算符

正如上面所提到,当给一个已经存在的对象(而不是新的对象)赋值时,就会调用赋值运算符函数。

1
2
3
4
5
6
7
8
String & String::operator=(const char *s)
{
delete[] this->str;
this->len = std::strlen(s);
this->str = new char[len+1];
std:strcpy(this->str,s);
return *this;
}

赋值运算符函数的调用对象是被赋值的类,所以上面这个代码片段中的this就是被赋值对象的地址,下面两个语句是等价的:

1
2
3
String a,b;
a = b;
a.operator=(b);

返回值优化

考虑下面一段代码:

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
class Foo
{

public:
char *str;
Foo(const char *s)
{
int len = strlen(s);
str = new char[len+1];
strcpy(str,s);
printf("Constructor\n");
}

~Foo()
{
delete[] str;
printf("Destructor\n");
}
};

Foo getFoo(const char *s)
{
return Foo(s);
}

int main()
{
Foo a = getFoo("Hello");
printf("%s\n",a.str);
}

正常来说,不应该在函数体内(栈上)返回一个对象,因为随着函数执行完毕,Foo(s)这个临时对象会调用析构函数删除str指针所指的内容。而且在这段代码中,复制构造函数没有被定义,这就意味着返回Foo时,默认构造函数会被调用,其str指针会直接被赋给对象a,这就导致a的指针指向一个没有意义的区域。

但是,在现代的IDE中执行这段代码,会发现程序居然能够照常运行。实际上,这是c++编译器的一种叫做返回值优化的优化手段,当在函数中试图返回一个对象时,C++会直接在main函数的栈上开辟空间(而不是getFoo函数),这就保证在getFoo函数结束时对象不会调用析构函数释放空间。

这段代码的运行结果为:

1
2
3
Constructor
Hello
Destructor

可以看到,析构函数在main函数结尾才被调用。如果在编译使用-fno-elide-constructors参数将返回值优化关闭,则运行结果会出错。

返回值优化减少了调用析构函数的次数,在一些情况下可以很好地提高程序的性能。但是程序员不应该过分依赖返回值优化,即使这个优化存在,也应该为类定义好复制构造函数,以防出现野指针的情况。而且,在函数的返回分支较多的情况下,编译器可能无法启用返回值优化。


c++中的各类构造函数、类型转换和赋值
http://zhf-xdu.top/2023/11/01/c-中的各类构造函数/
作者
周洪锋
发布于
2023年11月1日
许可协议