C++类对象的创建方式

1. 默认方式创建类对象 (Creating Class Objects in the Default Way)

在这里插入图片描述

1.1 默认构造函数创建类对象 (Creating Class Objects with Default Constructors)

在C++中,当我们定义一个类时,如果没有明确地为类编写构造函数,编译器会为我们提供一个默认的构造函数。这就是我们所说的默认构造函数(Default Constructor)。默认构造函数没有任何参数,但是它会执行一些基本的任务,比如为类中的成员变量分配内存。

让我们以一个简单的类为例:

class Example {
public:
    int a;
    double b;
};

在这个例子中,我们没有明确地提供构造函数。但是,我们仍然可以创建一个Example类的对象:

Example ex;

在这里,ex就是通过默认构造函数创建的Example类的对象。尽管我们没有明确地写出构造函数,但是编译器为我们提供了一个。当我们声明Example ex;时,就调用了这个默认构造函数。此时,ex对象的ab成员并没有被初始化,它们的值是不确定的。

然而,在实际的编程过程中,我们通常希望能够在创建对象的同时,为对象的成员变量赋予一些初始值。因此,我们通常会提供一个自定义的默认构造函数,像这样:

class Example {
public:
    int a;
    double b;

    Example() : a(0), b(0.0) {}
};

在这个版本的Example类中,我们提供了一个默认构造函数,它将a初始化为0,将b初始化为0.0。这样,当我们创建Example对象时,其成员变量将会被自动初始化为我们指定的值:

Example ex;  // ex.a为0,ex.b为0.0

总的来说,利用默认构造函数创建类对象是一种非常基本也非常常见的方式。然而,随着C++的发展,人们开发出了更加高效、灵活的创建类对象的方法。在接下来的小节中,我们将会介绍更多的内容。

1.2 拷贝构造函数创建类对象 (Creating Class Objects with Copy Constructors)

拷贝构造函数(Copy Constructor)在C++中是一个特别重要的概念,它用于创建一个新的对象,作为已存在对象的副本。拷贝构造函数通常接收一个同类对象的引用作为参数。

我们来看一个例子:

class Example {
public:
    int a;
    double b;

    Example() : a(0), b(0.0) {}  // 默认构造函数
    Example(const Example& ex) : a(ex.a), b(ex.b) {}  // 拷贝构造函数
};

在这个例子中,我们除了提供了一个默认构造函数,还提供了一个拷贝构造函数。拷贝构造函数接受一个const Example&类型的参数,然后将新创建的对象的成员变量ab初始化为传入对象的相应成员变量的值。

创建一个新对象并使用拷贝构造函数的方法如下:

Example ex1;  // 使用默认构造函数创建ex1
Example ex2(ex1);  // 使用拷贝构造函数创建ex2,ex2是ex1的一个副本

在这里,ex2就是通过拷贝构造函数创建的,它是ex1的一个完整副本。

拷贝构造函数在很多情况下会被自动调用,比如函数参数的传递、函数返回值的返回等。因此,理解和正确使用拷贝构造函数在C++编程中至关重要。然而,随着C++新特性的引入,如移动构造函数(Move Constructor),有时我们可以更有效率地创建类的对象,这将在后续小节中讲述。

1.3 赋值操作创建类对象 (Creating Class Objects by Assignment Operation)

除了使用构造函数创建类的对象外,我们还可以通过赋值操作来创建新的类对象。这实际上涉及到了拷贝赋值操作符(Copy Assignment Operator)的使用。拷贝赋值操作符用于将一个对象的状态复制到另一个已经存在的对象。

让我们来看一个例子:

class Example {
public:
    int a;
    double b;

    Example() : a(0), b(0.0) {}  // 默认构造函数
    Example(const Example& ex) : a(ex.a), b(ex.b) {}  // 拷贝构造函数
    Example& operator=(const Example& ex) {  // 拷贝赋值操作符
        if (this != &ex) {
            a = ex.a;
            b = ex.b;
        }
        return *this;
    }
};

在这个例子中,我们定义了一个拷贝赋值操作符。它接受一个const Example&类型的参数,然后将*this对象的成员变量ab设置为传入对象的相应成员变量的值,最后返回*this

使用赋值操作创建对象的方法如下:

Example ex1;  // 使用默认构造函数创建ex1
Example ex2 = ex1;  // 使用赋值操作创建ex2,ex2是ex1的一个副本

在这里,ex2是通过赋值操作创建的,它是ex1的一个副本。注意,这里的赋值操作符实际上调用的是拷贝构造函数,而不是拷贝赋值操作符。这是因为ex2在此语句中被定义,并且直接初始化。而拷贝赋值操作符是用于已经存在的对象的赋值操作。例如:

Example ex3;
ex3 = ex1;  // 这里使用的是拷贝赋值操作符,因为ex3已经存在

总的来说,通过赋值操作创建对象是C++中常见的一种方式,但这涉及到了C++的一些复杂性,例如构造函数和赋值操作符的选择。理解这些可以帮助我们写出更好的C++代码。

2. 使用C++新特性创建类对象

2.1 使用移动构造函数创建类对象 (Creating Class Objects with Move Constructors)

在理解移动构造函数创建类对象之前,我们需要首先了解一些C++11引入的新概念。

右值引用 (Rvalue Reference)

在C++11中,引入了右值引用的概念,标记为"&&"。右值引用主要用于标记临时对象,即那些即将被销毁、不能再赋值的对象。临时对象主要包括函数返回的临时变量、表达式结果等。通过使用右值引用,我们可以避免在创建和删除临时变量时的资源浪费。

移动构造函数 (Move Constructor)

移动构造函数接收一个右值引用作为参数,它将拥有被引用对象的资源。原对象失去资源所有权,成为一个空对象。相较于深拷贝,移动构造函数可以更高效地创建新的对象,因为它避免了资源的复制。

移动语义的使用 (Using Move Semantics)

现在,让我们通过一个例子来了解如何使用移动构造函数创建类对象。

假设我们有一个名为"Buffer"的类,该类包含一个动态分配的字符数组。在C++11之前,如果我们想要创建一个新的Buffer对象并将一个旧的Buffer对的数据复制过来,我们通常会使用拷贝构造函数,这涉及到数据的深拷贝。但在C++11及以后,我们可以使用移动构造函数来实现同样的功能,但效率更高。

以下是"Buffer"类的示例:

class Buffer {
public:
    Buffer(size_t size): size_(size), data_(new char[size]) { }
    ~Buffer() { delete[] data_; }
    // 拷贝构造函数
    Buffer(const Buffer& other): size_(other.size_), data_(new char[other.size_]) { 
        std::copy(other.data_, other.data_ + other.size_, data_);
    }
    // 移动构造函数
    Buffer(Buffer&& other): size_(other.size_), data_(other.data_) { 
        other.size_ = 0;
        other.data_ = nullptr;
    }
private:
    size_t size_;
    char* data_;
};

在上述代码中,我们定义了一个移动构造函数,它接受一个右值引用参数。在这个移动构造函数中,我们直接将"other"对象的资源(即"data_")移动到新创建的对象中,然后将"other"对象的资源指针置为nullptr,防止其析构函数释放资源。

下面是如何使用移动构造函数来创建Buffer类对象的示例:

Buffer buf1(100); // 使用默认构造函数创建对象
Buffer buf2(std::move(buf1)); // 使用移动构造函数创建对象

这里,std::move(buf1)将"buf1"转为右值,触发了移动构造函数,将"buf1"的资源移动到"buf2",而非复制。此时,“buf1"变为空对象,其资源已经转移给了"buf2”。

以上,就是使用移动构造函数创建类对象的方法和优势。实际开发中,这种方法能够提升代码执行效率,特别是在处理大量数据或者复杂对象时。

2.2 使用委托构造函数创建类对象 (Creating Class Objects with Delegating Constructors)

在C++11之前,如果我们有一些类的构造函数需要相同的初始化逻辑,我们可能需要在多个构造函数中重复这个逻辑,这将导致代码冗余。好在,C++11引入了委托构造函数的概念,使我们能够让一个构造函数调用另一个构造函数,这样我们可以将相同的初始化逻辑放在一个地方。

委托构造函数的用法是在初始化列表中调用另一个构造函数,具体来说,如下所示:

class MyClass {
public:
    MyClass() : MyClass(0, 0) { 
        // 不再需要初始化代码 
    }

    MyClass(int a, int b) : a_(a), b_(b) { 
        // 初始化代码只在这里 
    }
private:
    int a_, b_;
};

在这个例子中,无参构造函数委托给了接受两个int参数的构造函数。通过这种方式,我们可以使代码更加简洁,易于维护。

值得注意的是,在使用委托构造函数时,被委托的构造函数必须在同一个类中,并且委托关系不能形成环。也就是说,我们不能让构造函数A委托给构造函数B,然后又让构造函数B委托给构造函数A。如果这样做,编译器会报错。

在大型项目中,有效使用委托构造函数可以极大地提高代码的可读性和可维护性。

2.3 使用统一初始化语法创建类对象 (Creating Class Objects with Uniform Initialization Syntax)

在C++11中,为了解决C++中各种初始化语法的混乱,引入了一种新的初始化方式,称为统一初始化或列表初始化。它可以用于所有数据类型的初始化,包括基础类型、数组、结构体、容器等,也包括我们正在讨论的类对象。

让我们看一个例子,假设有一个名为"Person"的类,定义如下:

class Person {
public:
    Person(std::string name, int age) : name_(name), age_(age) {}
private:
    std::string name_;
    int age_;
};

使用统一初始化语法,我们可以这样创建一个Person对象:

Person person{"张三", 25};

在这个例子中,我们使用一对花括号包含的参数列表来创建Person对象。这种语法简洁明了,可读性较高。这是C++11中引入的新特性,也是现代C++编程的推荐做法。

使用统一初始化语法创建类对象还有一个好处,那就是能够防止“窄化转换”。所谓窄化转换,就是可能丢失信息的类型转换,比如将double类型转换为int类型,或将大的整数类型转换为小的整数类型。如果我们在列表初始化中进行窄化转换,编译器将给出错误信息。

以上就是使用统一初始化语法创建类对象的介绍,这种方法在很多情况下都很实用,推荐在实际编程中使用。

3. 使用智能指针创建类对象

3.1 使用shared_ptr创建类对象

在C++中,我们经常会遇到动态内存管理的问题,而shared_ptr(共享指针)提供了一种有效的解决方案。在介绍如何使用shared_ptr创建类对象之前,我们需要先理解shared_ptr的一些基本概念。

shared_ptr是C++11引入的一种智能指针,它可以确保在任何时候都能正确地释放所占用的内存。shared_ptr使用了引用计数的方式来记录有多少个shared_ptr共享同一块内存,只有当最后一个shared_ptr不再使用时,内存才会被释放。

创建shared_ptr类对象的基本方法如下:

std::shared_ptr<Class> obj = std::make_shared<Class>(constructor arguments);

这里Class代表你的类名,constructor arguments代表类构造函数的参数。std::make_shared函数会返回一个新建的Class对象的shared_ptr。

这种方式创建对象有如下优势:

  1. 安全:shared_ptr能自动管理内存,当所有的shared_ptr都不再引用对象时,对象会被自动删除,无需程序员手动释放内存。

  2. 灵活:可以通过复制或者赋值来传递shared_ptr,使得多个shared_ptr可以共享同一个对象。

  3. 效率:使用make_shared创建shared_ptr,只需要一次内存分配,而直接使用new创建shared_ptr则需要两次内存分配,一次是为对象本身分配内存,一次是为控制块分配内存。

然而,shared_ptr并非万能的,它也有其局限性,比如循环引用问题。如果两个对象互相包含对方的shared_ptr,那么他们的引用计数永远不会降为0,导致内存无法被正确释放。

在使用shared_ptr创建类对象时,我们需要对这些特性有深入的理解,这样才能有效地使用shared_ptr,避免出现内存泄漏等问题。

好的,下面是第三章的3.2部分“使用unique_ptr创建类对象”的内容:

3.2 使用unique_ptr创建类对象

在C++11之后,unique_ptr(独占指针)成为了智能指针的另一种选择,它代表了对象所有权的概念。与shared_ptr相比,每个对象只能被一个unique_ptr所拥有。因此,使用unique_ptr创建对象可以使得代码更清晰,更易于理解。

创建unique_ptr类对象的基本方式如下:

std::unique_ptr<Class> obj(new Class(constructor arguments));

这里Class代表你的类名,constructor arguments代表类构造函数的参数。

使用unique_ptr创建类对象有以下优点:

  1. 独占性:unique_ptr拥有其指向的对象,保证同一时间只有一个unique_ptr指向该对象,有助于避免多线程环境下的数据竞争。

  2. 自动管理:当unique_ptr超出作用域或被删除时,它所拥有的对象会被自动删除,无需手动管理内存。

  3. 转移所有权:通过std::move,unique_ptr可以将所有权转移给其他unique_ptr,而原unique_ptr将变为null。

然而,需要注意的是,unique_ptr的独占性也限制了其使用。我们不能复制unique_ptr,也不能将unique_ptr用于STL容器。但是,这种特性使得我们在编写代码时更加关注对象所有权的问题,有助于编写出更好的代码。

综上,unique_ptr是一种轻量且安全的创建类对象的方式,其独占性和自动内存管理特性使其在某些场合下比shared_ptr更为合适。在实际编程中,我们可以根据需求选择合适的智能指针类型,达到更好的编程效果。

当然,下面是第三章的3.3部分“使用weak_ptr创建类对象”的内容:

3.3 使用weak_ptr创建类对象

在C++中,weak_ptr(弱指针)也是一种重要的智能指针,它与shared_ptr一起协作,解决了智能指针中的循环引用问题。

创建weak_ptr类对象的基本方式如下:

std::shared_ptr<Class> shared_obj = std::make_shared<Class>(constructor arguments);
std::weak_ptr<Class> weak_obj(shared_obj);

这里Class代表你的类名,constructor arguments代表类构造函数的参数。需要注意的是,我们不能直接使用weak_ptr创建对象,因为weak_ptr没有提供直接管理对象的能力。

weak_ptr的主要特点和优势如下:

  1. 不影响引用计数:weak_ptr指向shared_ptr管理的对象,但是不会增加该对象的引用计数。即使有多个weak_ptr指向同一个对象,只要没有shared_ptr指向该对象,该对象就会被正确删除。

  2. 避免循环引用:weak_ptr可以打破shared_ptr的循环引用,从而避免内存泄漏。

  3. 可检查对象是否存在:在使用weak_ptr指向的对象之前,我们可以调用其lock()方法,如果返回的shared_ptr不为空,说明对象仍然存在,否则对象已经被删除。

然而,weak_ptr也有其局限性。由于weak_ptr不直接管理对象,我们不能通过weak_ptr直接访问对象,必须先将其转换为shared_ptr。而且,weak_ptr主要用于配合shared_ptr,用于处理复杂的内存管理问题,如果没有这样的需求,使用weak_ptr可能会使代码变得更复杂。

总的来说,weak_ptr提供了一种安全且有效的方式来解决智能指针的循环引用问题。在使用shared_ptr管理内存时,如果出现了复杂的引用关系,我们可以适当地使用weak_ptr来简化内存管理。

智能指针完全可以与new一起使用。在早期的C++标准(C++03)中,我们通常是这样使用的。例如,对于std::unique_ptr

3.4 std::make_shared或std::make_unique 为什么代替了new?

std::unique_ptr<Class> ptr(new Class(constructor arguments));

对于std::shared_ptr

std::shared_ptr<Class> ptr(new Class(constructor arguments));

然而,自C++11起,引入了std::make_sharedstd::make_unique(自C++14起)这两个函数,它们都可以替代new来创建智能指针。使用这些函数创建智能指针不仅更简洁,还有可能更高效,因为它们可以将对象的创建和相关的控制块(用于跟踪资源)合并到一个内存分配中,而使用new则需要两次分配。

因此,尽管你可以使用new创建智能指针,但在C++11及更高版本的标准中,通常推荐使用std::make_sharedstd::make_unique

std::make_sharedstd::make_unique是两个用于创建智能指针的函数,它们分别用于创建std::shared_ptrstd::unique_ptr。使用这两个函数创建智能指针的优势如下:

  1. 一步创建std::make_sharedstd::make_unique可以在一个语句中同时创建对象和其相关的智能指针。这样做可以简化代码,增强代码的可读性。

  2. 异常安全:如果你先使用new创建对象,然后再将其传递给智能指针,那么在两步之间如果发生异常,就可能导致内存泄漏。而使用std::make_sharedstd::make_unique可以避免这种问题,因为它们会保证在对象创建和智能指针绑定的过程中不会发生内存泄漏。

  3. 提高效率:对于std::make_shared,它会在一个内存分配中同时创建对象和相关的控制块(用于记录有多少个智能指针指向同一对象),而直接使用newstd::shared_ptr需要两次内存分配。这样,std::make_shared在某些情况下可以提供更好的性能。

  4. 简化代码std::make_sharedstd::make_unique不需要显示地写出new,使得代码看起来更简洁。

因此,尽管你可以直接使用new和智能指针一起创建对象,但在C++11及其后的版本中,更推荐使用std::make_sharedstd::make_unique来创建智能指针。

  • 7
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

泡沫o0

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值