Last Updated: 2023-12-28 03:49:34 Thursday
-- TOC --
在代码中,copy很常见,将一个变量的值赋给另一个变量,函数调用的参数和返回值传递,这些代码都涉及copy动作。在C代码中,这些copy动作的处理是很直接的,因为所有的C变量都代表了某一块内存,编译器直接做内存copy即可。但C++就不同了,如果把一个对象赋值给另一个变量(变量就是有名称的对象),如果不加以控制,容易出现shallow copy的错误。虽然C语言也存在shallow copy的问题,但C语言没有提供直接解决的机制,而是需要程序员自己绕过去。C++提供了直接解决shallow copy的机制,这就是copy semantics,由它实现deep copy
。
overload one constructor function,入参是对象自己的const reference,在这个constructor中,程序员自己实现一个完整的对象数据copy。
#include <cstdio>
#include <cstring>
#include <stdexcept>
struct xyz {
xyz() = default;
xyz(size_t size):
tlen{},
p{} {
reset(size);
}
// copy constructor
xyz(const xyz& a):
tlen{},
p{} {
reset(a.get_tlen());
content(a.get_p());
}
void resize(size_t size) {
reset(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));
}
size_t get_tlen() const { // const is needed
return tlen;
}
char* get_p() const { // const is needed
return p;
}
void show() {
printf("%s\n", p?p:"(nullptr)");
}
~xyz() {
delete[] p;
}
private:
void reset(size_t size) {
delete[] p;
if (size != 0)
p = new char[size]{};
else
p = nullptr;
tlen = size;
}
size_t tlen;
char* p;
};
int main(void) {
char cont[] = "1234567890";
xyz x{};
x.show();
x.resize(8);
try {
x.content(cont);
x.show();
} catch(const std::length_error& e) {
printf("except: %s, %s\n", cont, e.what());
}
x.resize(11);
x.content("1234567890");
x.show();
// call copy constructor
xyz y{x};
y.show();
cont[0] = 'a';
cont[1] = 'b';
y.resize(16);
try {
y.content(cont);
} catch(const std::length_error& e) {
printf("except: %s, %s\n", cont, e.what());
}
x.show();
y.show();
// call copy constructor
xyz z = y;
z.show();
return 0;
}
运行效果如下:
$ g++ -Wall -Wextra tcc.cpp -o tcc
$ ./tcc
(nullptr)
except: 1234567890, content string length is too long
1234567890
1234567890
1234567890
ab34567890
ab34567890
上面的测试代码,y和z都是在调用copy constructor。虽然z的赋值用的是=
,但这是在创建新对象的时候!
测试发现入参的const申明去掉后,编译没问题,在copy constructor中修改入参也可以。这说明本文所介绍的内容,应该只是一种best practice。
在非申明对象的时候,也可以用=
号来实现deep copy,这就是copy assignment。实现copy assignment,需要程序员重载运算符,自己实现其代码逻辑。
增加copy assignment接口,重写main后,代码如下:
#include <cstdio>
#include <cstring>
#include <stdexcept>
struct xyz {
xyz() = default;
xyz(size_t size):
tlen{},
p{nullptr} {
reset(size);
}
// copy constructor
xyz(const xyz& a):
tlen{},
p{nullptr} {
reset(a.get_tlen());
content(a.get_p());
}
// copy assignment
xyz& operator=(const xyz& a) {
if (this == &a)
return *this;
reset(a.get_tlen());
content(a.get_p());
return *this;
}
void resize(size_t size) {
reset(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));
}
size_t get_tlen() const { // const is needed
return tlen;
}
char* get_p() const { // const is needed
return p;
}
void show() {
printf("%s\n", p?p:"(nullptr)");
}
~xyz() {
delete[] p;
}
private:
void reset(size_t size) {
delete[] p;
if (size != 0)
p = new char[size]{};
else
p = nullptr;
tlen = size;
}
size_t tlen;
char* p;
};
int main(void) {
// call copy constructor
xyz y{};
y.show();
y.resize(16);
y.content("123abc");
y.show();
// call copy assignment
xyz z{};
z = y;
z.show();
return 0;
}
运行效果如下:
$ g++ -Wall -Wextra tcc.cpp -o tcc
$ ./tcc
(nullptr)
123abc
123abc
注意:对象reference类型接口的返回值*this
,我觉得可以理解为一种比较特殊的cast,即cast to ref!
上面这段测试代码,使用了default zero-parameter constructor,这意味着,如果创建对象的时候,不使用
{}
将成员变量清零,以上代码就会出现segmentation fault。因为copy assignment会先调用private的reset接口,一开始就delete[] p,此时p如果不是0,程序就挂了。
这篇文章的目的,就是说明C++中解决shallow copy的机制,copy semantics。如果对象拥有非局部的动态内存资源,基本上在需要copy的时候,都要程序员自己实现上面介绍的copy constructor和copy assignment。
而默认情况,default copy,对应的是兼容传统C语言的机制,调用所有member的copy接口!default copy是编译器自动提供的,程序员可以不用写出来。如果要写出来,就是如下面这样的代码:
struct Replicant {
Replicant(const Replicant&) = default;
Replicant& operator=(const Replicant&) = default;
--snip--
};
Often, the compiler will generate default implementations for copy construction and copy assignment. The default implementation is to invoke copy construction or copy assignment on each of a class’s members. Any time a class manages a resource, you must be extremely careful with default copy semantics; they’re likely to be wrong. Best practice dictates that you explicitly declare that default copy assignment and copy construction are acceptable for such classes using the default keyword.
C++的语法支持定义不允许copy的对象,如果代码明确了不允许copy,又出现了copy,会有编译错误!
不允许对象copy的代码如下面这样:
struct Highlander {
Highlander(const Highlander&) = delete;
Highlander& operator=(const Highlander&) = delete;
--snip--
};
Some classes simply cannot or should not be copied, for example, if your class manages a file or if it represents a mutual exclusion lock for concurrent programming. You can suppress the compiler from generating a copy constructor and a copy assignment operator using the delete keyword.
发现C++的一个秘密,似乎任何函数,都可以申明delete
,代码中就不可再调用了。
#include <iostream>
using namespace std;
struct xyz {
xyz(void) { cout << "xyz constructor\n"; }
xyz(const xyz *x) = delete;
xyz(int a) = delete;
void show(int a) = delete;
~xyz(void) { cout << "xyz destructor\n"; }
};
void func(void) = delete;
int main(void) {
//xyz x{1};
//x.show(1);
func();
return 0;
}
这个词是我发明的,表示一个传入相同对象类型的指针的copy constructor或copy assignement。
#include <iostream>
using namespace std;
struct xyz {
xyz(void) { cout << "xyz constructor\n"; }
xyz(const xyz *x) { cout << "xyz pointer copy constructor\n"; }
~xyz(void) { cout << "xyz destructor\n"; }
xyz& operator=(const xyz *x) {
if (this == x)
return *this;
cout << "xyz pointer assign constructor\n";
return *this;
}
};
int main(void) {
xyz x{};
xyz y{&x};
xyz z;
z = &x;
z = &z;
return 0;
}
输出:
xyz constructor
xyz pointer copy constructor
xyz constructor
xyz pointer assign constructor
xyz destructor
xyz destructor
xyz destructor
pointer copy本质上就是constructor的overload。上面的代码看起来稍微有一点怪异,一般不会这样用。但std::allocator容器中的对象,可能需要支持这样的接口。
本文链接:https://cs.pynote.net/sf/c/cpp/202208301/
-- EOF --
-- MORE --