Last Updated: 2023-08-20 14:15:49 Sunday
-- TOC --
explicit申明对constructor的作用,就是限制implicit转换。比如在用=
给一个对象赋值的时候,或函数调用传参的时候,都可能出现的隐式的对象创建(类型转换)。
请看下面这段测试代码,关注class xyz在main中是如何使用的:
#include <cstdio>
#include <cstring>
#include <stdexcept>
class xyz {
public:
size_t tlen;
char *p;
xyz():
tlen{},
p{} {
}
xyz(size_t size) {
printf("size_t constructor\n");
if (size != 0)
p = new char[size]{};
else
p = NULL;
tlen = size;
}
xyz(const char *cont) {
printf("const char constructor\n");
if (cont != NULL) {
tlen = strlen(cont) + 1;
p = new char[tlen];
strcpy(p, cont);
} else {
p = NULL;
tlen = 0;
}
}
void content(const char *cont) {
if (strlen(cont)+1 > tlen)
throw std::length_error{"content string length is too long"};
memset(p, 0, tlen);
strncpy(p, cont, strlen(cont));
}
void show() {
if (p)
printf("%s\n", p);
else
printf("%s\n", "(NULL)");
}
~xyz() {
delete[] p;
}
};
int main(void) {
char cont[] = "1234567890";
class xyz x = 24; // what?
x.content(cont);
x.show();
class xyz y = "abcdefg"; // what?
y.show();
return 0;
}
在main中,创建了两个xyz对象,他们的初始化,使用了=
符号!
这是合法的,我在C++花式对象初始化这篇文章中,总结过C++支持的各种对象初始化的方法,使用=
是兼容C风格的语法,也比较符合很多程序员的直觉。
C语言中的对象都是所谓的primitive type,这些对象只有一块内存存储数据,没有member functions,初始化就是简单直接地赋值,初始化struct也是赋值。而C++的对象,初始化需要调用constructor,这就让问题复杂了一点点。C++支持函数重载,即相同名称的函数接口,可以有不同的参数,由编译器区分调用时具体指向哪个接口。constructor也可以重载,因此就会出现单参数的constructor,而C++语法,支持用=
的方式,调用单参数constructor。这就是上面测试代码的逻辑。
上面测试代码,打印输出:
$ g++ -Wall -Wextra test_cc.cpp -o test_cc
$ ./test_cc
size_t constructor
1234567890
const char constructor
abcdefg
似乎一切都没问题,但这种语法的存在,容易让程序员写出下面这样的错误代码:
int main(void) {
class xyz z;
z.show();
printf("%zu\n", z.tlen);
z = 32;
z.show();
printf("%zu\n", z.tlen);
return 0;
}
先创建z对象,此时会调用无参数的constructor。然后尝试执行z=32
!错误就出现在这里!先看看运行时会发生什么样的严重错误:
$ g++ -Wall -Wextra test_cc.cpp -o test_cc
$ ./test_cc
(NULL)
0
size_t constructor
|
32
free(): double free detected in tcache 2
Aborted (core dumped)
double free
,为什么会出现double free这样严重的错误?
因为,z=32
这行代码的执行逻辑是这样的:
{
class xyz temp = 32; // implicit type conversion
z = temp;
}
编译器会创建一个临时的对象temp,然后使用copy assignment赋值给z对象(=号两边类型不同,必须这么做,这是C++的lenient rule)。代码应该会先释放temp对象(我猜的,因此上面的代码,我用了大括号括起来),在打印出32后,程序结束,再释放z对象。而编译器提供的默认copy assignment只能完成shallow copy,因此导致了对象内指针p的double free。
解决问题的方法有二:
方法1的代码如下:
class xyz& operator=(const class xyz& a) {
if (this == &a)
return *this;
if (p)
delete[] p;
tlen = a.tlen;
p = new char[tlen];
strcpy(p, a.p);
return *this;
}
方法1可以解决double free的错误。
方法2,就是本文的主题,使用explicit申明单参数constructor,将不允许出现上述的隐式的类型转换。
推荐使用方法2,让一切都变得更简单,减少C++代码的歧义。
按照方法2,上面的测试代码,两个单参数constructor修改为:
explicit xyz(size_t size) {
printf("size_t constructor\n");
if (size != 0)
p = new char[size]{};
else
p = NULL;
tlen = size;
}
explicit xyz(const char *cont) {
printf("const char constructor\n");
if (cont != NULL) {
tlen = strlen(cont) + 1;
p = new char[tlen];
strcpy(p, cont);
} else {
p = NULL;
tlen = 0;
}
}
记得C++推荐用花括号做对象初始化。
还有一种情况,也建议考虑使用explicit申明。
C++支持给函数入参提供默认值,因此,有默认值的constructor接口,也可能形成单参数接口。请看下面的测试代码:
#include <cstdio>
#include <cstring>
#include <stdexcept>
class xyz {
public:
size_t tlen;
char *p;
explicit xyz(size_t size, const char *cont="1234") {
if (size > strlen(cont)) {
p = new char[size]{};
strncpy(p, cont, strlen(cont));
}
else
throw std::length_error{"size is le strlen(cont)"};
tlen = size;
}
void content(const char *cont) {
if (strlen(cont)+1 > tlen)
throw std::length_error{"content string length is too long"};
memset(p, 0, tlen);
strncpy(p, cont, strlen(cont));
}
void show() {
if (p)
printf("%s\n", p);
else
printf("%s\n", "(NULL)");
}
~xyz() {
if (p)
delete[] p;
}
};
int main(void) {
//xyz x = 7; // wrong because of explicit
xyz y{8};
y.show();
xyz z{9,"abcdefg"};
z.show();
return 0;
}
因为等号只能带一个参数,所以这种语法会让编译器去尝试调用只需要一个参数的接口。而使用explicit
,彻底限制了这样的操作。这很好,C++就应该有C++的样儿!
传参时发生的implicit conversion
#include <iostream>
using namespace std;
struct you{
int a;
int b;
you(int a):a{a}{}
};
void func(you y){
cout << y.a << y.b << endl;
}
int main(void) {
func(1);
return 0;
}
func只有一个参数,是个class对象,调用时使用了一个常量,此时常量会隐式地转为对象。
解决方法:
void func(you &y)
,使用reference parameter;(推荐)explicit you(int a): a{a}{}
,使用explicit,本文主题。所有constructor都应该explicit吗?
下面代码是合法的:
#include <iostream>
using namespace std;
struct you{
int a;
int b;
int c;
you(int a, int b, int c): a{a},b{b},c{c}{}
};
void func(you y){
cout << y.a << y.b << y.c << endl;
}
int main(void) {
func({1,2,3});
return 0;
}
如果给you唯一的constructor接口增加explicit申明,编译错误!
本文链接:https://cs.pynote.net/sf/c/cpp/202208311/
-- EOF --
-- MORE --