您的位置:首页 > 编程语言 > C语言/C++

c++教程(十九: Special members)

2017-02-07 20:39 381 查看
特殊成员函数是在某些情况下被隐式定义为类的成员函数,下面6种:



下面对每一个进行介绍

默认构造函数

默认构造函数就是一个对象被声明以后而没有初始化元素的时候会被调用。

如果一个类没有默认的构造函数,编译器就假设这个类有一个默认的构造函数。因此声明一个类可以像下面这样:

class Example {
public:
int total;
void accumulate (int x) { total += x; }
};


这个例子中编译器默认有一个构造函数。因此这个类可以简单的表示为不带参数的形式:

Example ex;


但是假如一个类一旦拥有了构造函数,不管这个构造函数带不带参数,那么编译器将不会在产生默认的构造函数,新的类声明中也不允许这个类不带参数,例如:

class Example2 {
public:
int total;
Example2 (int initial_value) : total(initial_value) { };
void accumulate (int x) { total += x; };
};


这里,可以声明一个带参数int类型的类构造函数,因此下面的一个声明是对的:

Example2 ex (100);   // ok: calls constructor


但是下面这样不对:

Example2 ex;         // not valid: no default constructor


上面这一个将无效,因为在类的声明中是需要一个参数的构造函数来代替了没有参数的默认构造函数。

因此如果这个类的实体没有被带参数的构造函数所构造,那么默认的构造函数就会在类中被声明。例如:

// classes and default constructors
#include <iostream>
#include <string>
using namespace std;

class Example3 {
string data;
public:
Example3 (const string& str) : data(str) {}
Example3() {}
const string& content() const {return data;}
};

int main () {
Example3 foo;
Example3 bar ("Example");

cout << "bar's content: " << bar.content() << '\n';
return 0;
}


在这里,Example3 有一个默认的构造函数(没有参数)用空模块定义:

Example3() {}


这就可以让类Example3以不带参数的构造函数出现(如同前面的foo函数例子)。通常情况下,默认的构造函数是隐式定义的,所有的类都没有其他构造函数,因此不需要显式定义。但在这种情况下,Example3 有另一个构造函数:

Example3 (const string& str);

当不管有什么其他的构造函数被声明时,那么编译器就不会自动提供默认的构造函数了。

析构函数

析构函数完成构造函数相反的功能:他们负责必要的清理需要结束的一些类。我们在前面的章节中定义的类没有分配任何资源,因此并没有真正需要任何清理。

但是现在,想象一下最后一个例子中的类如果动态分配内存来存储它作为数据成员的字符串,在这种情况下,如果有一个函数在这个类生命结束时自动调用来清除这个内存将是非常有用的。要做到这一点,我们使用一个析构函数。析构函数是一个默认构造函数非常相似的成员函数:它不需要参数,没有返回,不是void。它还使用类的名字作为自己的名字,但前面有一个符号标志(~):

// destructors
#include <iostream>
#include <string>
using namespace std;

class Example4 {
string* ptr;
public:
// constructors:
Example4() : ptr(new string) {}
Example4 (const string& str) : ptr(new string(str)) {}
// destructor:
~Example4 () {delete ptr;}
// access content:
const string& content() const {return *ptr;}
};

int main () {
Example4 foo;
Example4 bar ("Example");

cout << "bar's content: " << bar.content() << '\n';
return 0;
}


这个例子中,Example4 分配一个内存个string,这块内存稍后会被析构函数释放掉。

析构函数会在这个类使用周期结束的时候执行。

这个例子中foo和bar就是在main函数结束的时候执行。

构造函数复制

当一个对象通过一个它自己类型的被命名对象作为参数时,它的构造函数就会被以构造函数的一个副本被复制。

copy 构造函数的第一个参数是它的类本身的类型引用(可能是const),它可以用一个参数调用该类型。例如,一个类MyClass,复制构造函数可以像下面的形式:

MyClass::MyClass (const MyClass&);


如果一个类没有定义copy 或者move构造函数,那么则提供了一个隐式copy 构造函数。此copy 构造函数只执行其自己的成员的副本。例如,对于一个类:

class MyClass {
public:
int a, b; string c;
};


这里隐式copy 构造函数会自动生成,这个假设这个函数 执行一个shallow copy,大致相当于:

MyClass::MyClass(const MyClass& x) : a(x.a), b(x.b), c(x.c) {}


这个默认的copy构造函数可以满足许多类的需要。但shallow copy复制该类本身的成员,这或许不是我们所期望的像我们上面定义 Example4的类那样,因为它包含指针,它操作了内存。对于那个类,进行shallow copy意味着指针的值被复制,而不是内容本身;这里意味着物体(复制的和原件的)将共享一个字符串对象(他们都会指向同一个对象),并在一些点(结构上)对象会尝试删除相同的内存块,可能导致程序崩溃的运行。这可以通过定义执行以下deep copy来代替shallow copy解决:

// copy constructor: deep copy
#include <iostream>
#include <string>
using namespace std;

class Example5 {
string* ptr;
public:
Example5 (const string& str) : ptr(new string(str)) {}
~Example5 () {delete ptr;}
// copy constructor:
Example5 (const Example5& x) : ptr(new string(x.content())) {}
// access content:
const string& content() const {return *ptr;}
};

int main () {
Example5 foo ("Example");
Example5 bar = foo;

cout << "bar's content: " << bar.content() << '\n';
return 0;
}


该 deep copy函数为一个新的字符串分配存储执行的深拷贝,该存储被初始化为原始对象的一个副本。在这种方式中,两个对象(复制和原始)都有不同的副本存储在不同的位置。

复制赋值

对象不仅在结构上可以被复制,当它们被初始化时,它们也可以被复制到任何赋值操作上。看下面的不同例子:

MyClass foo;
MyClass bar (foo);       // object initialization: copy constructor called
MyClass baz = foo;       // object initialization: copy constructor called
foo = bar;               // object already initialized: copy assignment called


注意,baz 使用等号初始化,但这不是一个赋值操作!(尽管它可能看起来像一个):对象的声明不是一个赋值操作,它是另一个的语法调用单个参数的构造函数。

foo的分配是一个赋值操作。在这里没有对象被声明,但这个操作是在现有对象上执行。

复制赋值操作符是运算符的重载,它以参数的值或引用本身作为参数。返回值通常是*this指针(虽然这不是必需的)。例如,一个类myclass,副copy操作可能具有以下特点:

MyClass& operator= (const MyClass&);


复制赋值操作符也是一个特殊的函数,也被定义为隐式,如果一个类没有自定义的copy ,也没有move 赋值(也没有move构造函数),那么将会自动隐式定义。

但是,隐版的浅拷贝适用于很多类,而不只是类的对象为指针的处理存储,比如example5例子。在这种情况下,不仅类会导致删除指向对象的双倍风险,而且任务在赋值之前通过不删除对象所指向的对象而创建内存泄漏。可以使用删除前一个对象的副本分配来解决这些问题,并执行一个深度拷贝:

Example5& operator= (const Example5& x) {
delete ptr;                      // delete currently pointed string
ptr = new string (x.content());  // allocate space for new string, and copy
return *this;
}


更好的操作是,因为它的string成员不是常量,可以重新利用相同的string对象:

Example5& operator= (const Example5& x) {
*ptr = x.content();
return *this;
}


构造函数的移动与赋值

和复制一样,移动也使用一个对象的值来设置到另一个对象上。但是,不像复制,移动的内容实际上是从一个对象(源)转移到另一个对象上(目的地):源对象会丢失所有的内容,到达目标对象上。只有当源对象的值是一个未命名的对象时才会发生这种情况。

未命名的对象是临时性的对象,因此没有被命名。未命名对象的典型例子是函数或类型转换的返回值。

使用一个临时对象的值,如对象初始化为另一个对象或赋值,不需要拷贝:对象永远不会被用于其他任何东西,因此,它的值可以被移动到目标对象。这些情况视为移动构造函数和移动任务分配:

当使用一个未命名的临时对象来初始化一个对象时,调用这个移动构造函数。同样,当一个对象被赋值给一个未命名的临时的值时,调用这个移动赋值:

MyClass fn();            // function returning a MyClass object
MyClass foo;             // default constructor
MyClass bar = foo;       // copy constructor
MyClass baz = fn();      // move constructor
foo = bar;               // copy assignment
baz = MyClass();         // move assignment


无论是fn的返回值还是MyClass类返回值都是无名的临时量。在这些情况下,没有必要复制,因为未命名的对象是非常短暂的,可以通过其他对象获取,当这是一个更有效的操作。

移动构造函数和移动赋值是需要类本身作为一个参数的成员:

MyClass (MyClass&&);             // move-constructor
MyClass& operator= (MyClass&&);  // move-assignment


右值引用指定由以下两个符号类型(&&)。作为一个参数,右值引用符合这种类型的临时参数。

移动的概念是管理他们使用的内存、如分配新的和删除存储的对象的最有用的对象。在这样的对象中,复制和移动是真的不同的操作:

从A到B的复制意味着新的内存被分配给B,然后一个被复制到这个新的内存分配给B的整个内容。

从A到B的移动意味着已经分配给A的内存被转移到B没有分配任何新的存储。它涉及简单的指针复制。

例如:

// move constructor/assignment
#include <iostream>
#include <string>
using namespace std;

class Example6 {
string* ptr;
public:
Example6 (const string& str) : ptr(new string(str)) {}
~Example6 () {delete ptr;}
// move constructor
Example6 (Example6&& x) : ptr(x.ptr) {x.ptr=nullptr;}
// move assignment
Example6& operator= (Example6&& x) {
delete ptr;
ptr = x.ptr;
x.ptr=nullptr;
return *this;
}
// access content:
const string& content() const {return *ptr;}
// addition:
Example6 operator+(const Example6& rhs) {
return Example6(content()+rhs.content());
}
};

int main () {
Example6 foo ("Exam");
Example6 bar = Example6("ple");   // move-construction

foo = foo + bar;                  // move-assignment

cout << "foo's content: " << foo.content() << '\n';
return 0;
}


编译器已经优化了许多情况,一般情况下需要一个移动构造的被称为Return Value Optimization。最值得注意的是,当函数返回的值用于初始化一个对象时,在这种情况下,移动构造函数实际上可能永远不会被调用。

注意即使用于任何函数参数的类型右值引用,它也比移动构造函数使用的少。右值引用是棘手的,不必要的情况下使用错误很难跟踪。

隐式成员

上面描述的六个特别成员函数是在某些情况下在类上隐式声明的成员:



注意,在同一情况下,不是所有的特殊成员函数都是隐式定义的。这主要是由于C的结构和早期的C++版本的向后兼容性,事实上,包括一些过时的例子。幸运的是,每个类都可以显式地选择这些成员中存在的默认定义或通过使用关键字默认和删除删除,语法之一是:

function_declaration = default;
function_declaration = delete;


例如:

// default and delete implicit members
#include <iostream>
using namespace std;

class Rectangle {
int width, height;
public:
Rectangle (int x, int y) : width(x), height(y) {}
Rectangle() = default;
Rectangle (const Rectangle& other) = delete;
int area() {return width*height;}
};

int main () {
Rectangle foo;
Rectangle bar (10,20);

cout << "bar's area: " << bar.area() << '\n';
return 0;
}


在这里,Rectangle 可以有两个int参数或默认构造构造(不带参数)。它不能从另一个Rectangle 对象复制构建,因为这个函数已经被删除了。因此,假设示例是的最后一个对象,下面的语句将无效:

Rectangle baz (foo);


然而,它可以通过将其复制构造函数定义为显式有效的:

Rectangle::Rectangle (const Rectangle& other) = default;


这基本上相当于:

Rectangle::Rectangle (const Rectangle& other) : width(other.width), height(other.height) {}


请注意,默认关键字不定义一个成员函数等于默认构造函数(即,默认构造函数意味着没有参数的构造函数),但等于将隐式定义的构造函数,如果没有删除。

在一般情况下,为和未来的兼容性,需要明确定义一个复制/移动构造函数,或一个复制/移动分配,但不是两个都鼓励指定删除或默认的其他他们没有明确定义的特殊成员的功能。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: