【C++漂流记】一文搞懂类与对象中的对象特征

这篇具有很好参考价值的文章主要介绍了【C++漂流记】一文搞懂类与对象中的对象特征。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

在C++中,类与对象是面向对象编程的基本概念。类是一种抽象的数据类型,用于描述对象的属性和行为。而对象则是类的实例,具体化了类的属性和行为。本文将介绍C++中类与对象的对象特征,并重点讨论了对象的引用。
【C++漂流记】一文搞懂类与对象中的对象特征,C++漂流记,c++,开发语言



相关链接:
一文搞懂类与对象的封装
一文搞懂C++中的引用
函数的高级应用

一、构造函数和析构函数

当我们创建一个类时,它可能具有一些成员变量和成员函数。构造函数和析构函数是类的特殊成员函数,用于初始化对象清理对象

  1. 构造函数的作用是在创建对象时初始化对象的成员变量。它的名称与类名相同,没有返回类型,并且可以有参数。构造函数可以有多个重载版本,根据传入的参数的类型和数量来确定使用哪个构造函数。构造函数在对象创建时自动调用。

  2. 析构函数的作用是在对象销毁时清理对象的资源。它的名称与类名相同,前面加上一个波浪号(~),没有返回类型,并且不接受任何参数。析构函数只能有一个,不能重载。析构函数在对象销毁时自动调用。

示例代码:

class Person {
public:
    string name;
    int age;

    // 默认构造函数
    Person() {
        name = "Unknown";
        age = 0;
        cout << "Default constructor called" << endl;
    }

    // 带参数的构造函数
    Person(string n, int a) {
        name = n;
        age = a;
        cout << "Parameterized constructor called" << endl;
    }

    // 析构函数
    ~Person() {
        cout << "Destructor called" << endl;
    }
};

int main() {
    // 使用默认构造函数创建对象
    Person p1;
    cout << "Name: " << p1.name << ", Age: " << p1.age << endl;

    // 使用带参数的构造函数创建对象
    Person p2("John", 25);
    cout << "Name: " << p2.name << ", Age: " << p2.age << endl;

    return 0;
}

输出结果:

Default constructor called
Name: Unknown, Age: 0
Parameterized constructor called
Name: John, Age: 25
Destructor called
Destructor called

代码解释:
在上述示例中,我们定义了一个名为Person的类,它具有两个成员变量:nameage。我们使用构造函数和析构函数来初始化和清理这些成员变量。

首先,我们定义了一个默认构造函数,它没有参数。在默认构造函数中,我们将name设置为Unknown,将age设置为0,并打印一条消息来表示构造函数被调用。

接下来,我们定义了一个带参数的构造函数,它接受一个字符串参数和一个整数参数。在带参数的构造函数中,我们将传入的参数值分别赋给nameage成员变量,并打印一条消息来表示构造函数被调用。

在主函数中,我们首先使用默认构造函数创建一个名为p1的对象。由于没有传入任何参数,因此默认构造函数被调用。然后,我们打印出p1对象的nameage成员变量的值,它们分别为Unknown和0。

接下来,我们使用带参数的构造函数创建一个名为p2的对象。我们传入字符串"John"和整数25作为参数,因此带参数的构造函数被调用。然后,我们打印出p2对象的nameage成员变量的值,它们分别为John和25。

在程序结束时,对象p1p2超出了它们的作用域,因此它们被销毁。在对象销毁的过程中,析构函数被自动调用。在示例中,我们在析构函数中打印了一条消息来表示析构函数被调用。


二、函数的分类和调用

1. 分类

  1. 默认构造函数:如果类没有显式定义构造函数,则编译器会自动生成一个默认构造函数。默认构造函数没有参数,也不执行任何初始化操作。它在创建对象时被隐式调用。

  2. 带参数的构造函数:带参数的构造函数接受一个或多个参数,并用这些参数来初始化对象的成员变量。它们在对象创建时被调用,并且根据传入的参数的类型和数量来确定使用哪个构造函数。

  3. 拷贝构造函数:拷贝构造函数是一种特殊的构造函数,它接受一个同类对象的引用作为参数,并使用该对象的值来初始化新创建的对象。拷贝构造函数在以下情况下被调用:

    • 使用一个对象初始化另一个对象时
    • 将对象作为函数参数传递给函数
    • 从函数返回对象

2. 调用方式

  1. 直接调用:可以通过类名加括号的方式直接调用构造函数。例如:MyClass obj(10);

  2. 隐式调用:构造函数在创建对象时隐式调用。例如:MyClass obj = MyClass(10);,这里会调用带参数的构造函数来创建对象。

  3. 拷贝初始化:使用一个对象初始化另一个对象时,会调用拷贝构造函数。例如:MyClass obj1(10); MyClass obj2 = obj1;

  4. 函数参数传递:将对象作为函数参数传递给函数时,会调用拷贝构造函数。例如:void func(MyClass obj);,在调用函数func(obj)时会调用拷贝构造函数。

  5. 函数返回对象:从函数返回对象时,会调用拷贝构造函数。例如:MyClass func() { return MyClass(10); }

3. 示例代码

#include <iostream>
using namespace std;

class MyClass {
public:
    int num;

    // 默认构造函数
    MyClass() {
        num = 0;
        cout << "Default constructor called" << endl;
    }

    // 带参数的构造函数
    MyClass(int n) {
        num = n;
        cout << "Parameterized constructor called" << endl;
    }

    // 拷贝构造函数
    MyClass(const MyClass& obj) {
        num = obj.num;
        cout << "Copy constructor called" << endl;
    }

    // 析构函数
    ~MyClass() {
        cout << "Destructor called" << endl;
    }
};

int main() {
    // 直接调用构造函数
    MyClass obj1(10);

    // 隐式调用构造函数
    MyClass obj2 = MyClass(20);

    // 拷贝初始化
    MyClass obj3(obj1);

    // 函数参数传递
    void func(MyClass obj);
    func(obj1);

    // 函数返回对象
    MyClass func();
    MyClass obj4 = func();

    return 0;
}

4. 输出结果:

Parameterized constructor called
Parameterized constructor called
Copy constructor called
Copy constructor called
Destructor called
Destructor called
Destructor called
Destructor called

5. 代码解释

在上述示例中,我们定义了一个名为MyClass的类,它包含一个成员变量num和多个构造函数。我们创建了多个对象,并通过不同的方式调用构造函数。

首先,我们通过直接调用构造函数创建了对象obj1,它会调用带参数的构造函数。然后,我们通过隐式调用构造函数创建了对象obj2,它也会调用带参数的构造函数。

接下来,我们通过拷贝初始化创建了对象obj3,它使用对象obj1的值来初始化。在拷贝初始化过程中,会调用拷贝构造函数。

然后,我们定义了一个函数func,它接受一个MyClass对象作为参数。在调用函数func(obj1)时,会调用拷贝构造函数来将对象obj1传递给函数。

最后,我们定义了一个函数func,它返回一个MyClass对象。在调用函数func()并将返回的对象赋值给obj4时,会调用拷贝构造函数。

在程序结束时,所有对象超出了它们的作用域,因此它们被销毁。在对象销毁的过程中,析构函数被自动调用。在示例中,我们在析构函数中打印了一条消息来表示析构函数被调用。


三、拷贝构造函数的时机

  1. 使用一个对象初始化另一个对象时:当使用一个已经存在的对象来初始化一个新对象时,会调用拷贝构造函数。例如:
class MyClass {
public:
    MyClass(int value) : m_value(value) {}
    MyClass(const MyClass& other) : m_value(other.m_value) {
        std::cout << "Copy constructor called" << std::endl;
    }
private:
    int m_value;
};

MyClass obj1(10);
MyClass obj2 = obj1; // 调用拷贝构造函数
  1. 将对象作为函数参数传递给函数:当将对象作为函数参数传递给函数时,会调用拷贝构造函数。例如:
class MyClass {
public:
    MyClass(int value) : m_value(value) {}
    MyClass(const MyClass& other) : m_value(other.m_value) {
        std::cout << "Copy constructor called" << std::endl;
    }
private:
    int m_value;
};

void func(MyClass obj) {
    // Do something with obj
}

MyClass obj1(10);
func(obj1); // 调用拷贝构造函数

  1. 从函数返回对象:当从函数返回对象时,会调用拷贝构造函数。例如:
class MyClass {
public:
    MyClass(int value) : m_value(value) {}
    MyClass(const MyClass& other) : m_value(other.m_value) {
        std::cout << "Copy constructor called" << std::endl;
    }
private:
    int m_value;
};

MyClass func() {
    MyClass obj(10);
    return obj; // 调用拷贝构造函数
}

  1. 在使用类对象进行赋值操作时,也会调用拷贝构造函数。例如:
class MyClass {
public:
    MyClass(int value) : m_value(value) {}
    MyClass(const MyClass& other) : m_value(other.m_value) {
        std::cout << "Copy constructor called" << std::endl;
    }
private:
    int m_value;
};

MyClass obj1(10);
MyClass obj2;
obj2 = obj1; // 调用拷贝构造函数

需要注意的是,编译器有时会进行优化,避免不必要的拷贝构造函数的调用。这种优化称为“拷贝消除”(copy elision)。在某些情况下,编译器可能会直接将对象的值从一个位置移动到另一个位置,而不是进行拷贝构造函数的调用。这可以提高性能,但是不会调用拷贝构造函数。


四、构造函数调用规则

构造函数调用规则如下:

  1. 默认构造函数:如果没有显式定义构造函数,编译器会自动生成一个默认构造函数。默认构造函数没有参数,并且执行默认的初始化操作。当创建对象时,如果没有提供参数,会调用默认构造函数。

  2. 参数化构造函数:参数化构造函数接受一个或多个参数,并用这些参数来初始化对象的成员变量。当创建对象时,如果提供了参数,会调用对应的参数化构造函数。

  3. 拷贝构造函数:拷贝构造函数接受一个同类型的对象作为参数,并使用该对象的值来初始化新对象。拷贝构造函数可以用于对象的拷贝初始化、函数参数传递和函数返回对象等场景。

  4. 移动构造函数:移动构造函数是C++11引入的新特性,它接受一个右值引用作为参数,并使用该参数的值来初始化新对象。移动构造函数通常用于在对象的资源所有权转移时提高性能。

构造函数的调用规则如下:

  • 当创建对象时,会根据提供的参数类型和数量来选择合适的构造函数进行调用。如果没有提供参数,则会调用默认构造函数。
  • 当使用一个对象来初始化另一个对象时,会调用拷贝构造函数。
  • 当将对象作为函数参数传递给函数时,会调用拷贝构造函数。
  • 当从函数返回对象时,会调用拷贝构造函数。
  • 在使用类对象进行赋值操作时,也会调用拷贝构造函数。
  • 在某些情况下,编译器会进行优化,避免不必要的拷贝构造函数的调用。这种优化称为“拷贝消除”(copy elision)。

五、深拷贝和浅拷贝

浅拷贝是指将一个对象的值复制到另一个对象,包括对象的成员变量。这意味着两个对象共享相同的内存地址,对其中一个对象的修改会影响到另一个对象。浅拷贝只复制了对象的表面层次,没有复制对象所拥有的资源。

深拷贝是指将一个对象的值复制到另一个对象,并且为新对象分配独立的内存空间。这样两个对象就拥有了彼此独立的内存空间,对其中一个对象的修改不会影响到另一个对象。深拷贝会递归地复制对象的所有成员变量,包括对象所拥有的资源。

示例代码:

#include <iostream>
#include <cstring>

class Person {
public:
    Person(const char* name, int age) {
        m_name = new char[strlen(name) + 1];
        strcpy(m_name, name);
        m_age = age;
    }
    
    // 拷贝构造函数
    Person(const Person& other) {
        m_name = new char[strlen(other.m_name) + 1];
        strcpy(m_name, other.m_name);
        m_age = other.m_age;
    }
    
    // 析构函数
    ~Person() {
        delete[] m_name;
    }
    
    // 打印信息
    void printInfo() {
        std::cout << "Name: " << m_name << ", Age: " << m_age << std::endl;
    }
    
private:
    char* m_name;
    int m_age;
};

int main() {
    Person person1("Alice", 25);
    Person person2 = person1; // 浅拷贝
    person1.printInfo(); // 输出:Name: Alice, Age: 25
    person2.printInfo(); // 输出:Name: Alice, Age: 25
    
    person2.printInfo(); // 输出:Name: Alice, Age: 25
    person2.printInfo(); // 输出:Name: Alice, Age: 25
    
    person1.printInfo(); // 输出:Name: Bob, Age: 30
    person2.printInfo(); // 输出:Name: Alice, Age: 25
    
    return 0;
}

在上面的示例中,我们定义了一个Person类,其中包含一个字符串类型的成员变量m_name和一个整型的成员变量m_age。在构造函数中,我们使用new运算符为m_name分配了独立的内存空间,并将字符串复制到该内存空间中。

然后,我们创建了一个person1对象,并将其值赋给person2对象。由于默认的拷贝构造函数是浅拷贝,所以person2对象的m_name指针指向了与person1对象相同的内存空间。当我们修改person2对象的m_name时,实际上也会修改person1对象的m_name。这就是浅拷贝的特点。

为了实现深拷贝,我们需要自定义拷贝构造函数,并在其中为m_name分配独立的内存空间,并将字符串复制到该空间中。这样,person2对象就拥有了自己独立的m_name内存空间,对其进行修改不会影响到person1对象。

总结起来,浅拷贝只复制对象的表面层次,而深拷贝会递归地复制对象的所有成员变量,包括对象所拥有的资源。深拷贝需要自定义拷贝构造函数来实现。


六、初始化列表

初始化列表是一种在构造函数中初始化成员变量的方法,可以用于实现深拷贝。

在上面的示例中,我们可以使用初始化列表来实现深拷贝,而不需要在拷贝构造函数中手动分配内存和复制字符串。

下面是使用初始化列表实现深拷贝的示例:

#include <iostream>
#include <cstring>

class Person {
public:
    Person(const char* name, int age) : m_age(age) {
        m_name = new char[strlen(name) + 1];
        strcpy(m_name, name);
    }
    
    // 拷贝构造函数
    Person(const Person& other) : m_age(other.m_age) {
        m_name = new char[strlen(other.m_name) + 1];
        strcpy(m_name, other.m_name);
    }
    
    // 析构函数
    ~Person() {
        delete[] m_name;
    }
    
    // 打印信息
    void printInfo() {
        std::cout << "Name: " << m_name << ", Age: " << m_age << std::endl;
    }
    
private:
    char* m_name;
    int m_age;
};

int main() {
    Person person1("Alice", 25);
    Person person2 = person1; // 深拷贝
    person1.printInfo(); // 输出:Name: Alice, Age: 25
    person2.printInfo(); // 输出:Name: Alice, Age: 25
    
    person2.printInfo(); // 输出:Name: Alice, Age: 25
    person2.printInfo(); // 输出:Name: Alice, Age: 25
    
    person1.printInfo(); // 输出:Name: Alice, Age: 25
    person2.printInfo(); // 输出:Name: Alice, Age: 25
    
    return 0;
}

在上面的示例中,我们在构造函数的初始化列表中分配了独立的内存空间,并将字符串复制到该空间中。这样,person2对象就拥有了自己独立的m_name内存空间,对其进行修改不会影响到person1对象。

使用初始化列表可以简化代码,并且可以确保在对象构造时成员变量已经正确初始化。这对于实现深拷贝非常有用。


七、类对象作为类成员

当一个类的成员变量是另一个类的对象时,我们需要在拷贝构造函数中正确地拷贝这些成员变量。

以下是一个示例,其中Person类的一个成员变量是Address类的对象:

#include <iostream>
#include <cstring>

class Address {
public:
    Address(const char* city, const char* street) {
        m_city = new char[strlen(city) + 1];
        strcpy(m_city, city);
        
        m_street = new char[strlen(street) + 1];
        strcpy(m_street, street);
    }
    
    Address(const Address& other) {
        m_city = new char[strlen(other.m_city) + 1];
        strcpy(m_city, other.m_city);
        
        m_street = new char[strlen(other.m_street) + 1];
        strcpy(m_street, other.m_street);
    }
    
    ~Address() {
        delete[] m_city;
        delete[] m_street;
    }
    
    void printInfo() {
        std::cout << "City: " << m_city << ", Street: " << m_street << std::endl;
    }
    
private:
    char* m_city;
    char* m_street;
};

class Person {
public:
    Person(const char* name, int age, const Address& address) : m_age(age), m_address(address) {
        m_name = new char[strlen(name) + 1];
        strcpy(m_name, name);
    }
    
    Person(const Person& other) : m_age(other.m_age), m_address(other.m_address) {
        m_name = new char[strlen(other.m_name) + 1];
        strcpy(m_name, other.m_name);
    }
    
    ~Person() {
        delete[] m_name;
    }
    
    void printInfo() {
        std::cout << "Name: " << m_name << ", Age: " << m_age << std::endl;
        m_address.printInfo();
    }
    
private:
    char* m_name;
    int m_age;
    Address m_address;
};

int main() {
    Address address("New York", "Broadway");
    Person person1("Alice", 25, address);
    Person person2 = person1; // 深拷贝
    person1.printInfo(); // 输出:Name: Alice, Age: 25, City: New York, Street: Broadway
    person2.printInfo(); // 输出:Name: Alice, Age: 25, City: New York, Street: Broadway
    
    return 0;
}

在上面的示例中,Person类的一个成员变量是Address类的对象。在Person类的拷贝构造函数中,我们使用拷贝构造函数来正确地拷贝Address对象。这样,当我们拷贝一个Person对象时,Person对象和其成员变量Address对象都会进行深拷贝。

需要注意的是,在Person类的析构函数中,我们只需要释放m_name成员变量的内存空间,因为m_address成员变量的内存空间会在Address类的析构函数中释放。

总结起来,当一个类的成员变量是另一个类的对象时,我们需要在拷贝构造函数中正确地拷贝这些成员变量,以实现深拷贝。


八、静态成员

静态成员变量是属于类本身而不是类的实例的。因此,在拷贝构造函数中不需要拷贝静态成员变量,因为它们在所有类的实例之间是共享的。

以下是一个示例,其中Person类有一个静态成员变量count

#include <iostream>

class Person {
public:
    Person(const char* name, int age) : m_age(age) {
        m_name = new char[strlen(name) + 1];
        strcpy(m_name, name);
        count++;
    }
    
    Person(const Person& other) : m_age(other.m_age) {
        m_name = new char[strlen(other.m_name) + 1];
        strcpy(m_name, other.m_name);
        count++;
    }
    
    ~Person() {
        delete[] m_name;
        count--;
    }
    
    static int getCount() {
        return count;
    }
    
private:
    char* m_name;
    int m_age;
    static int count;
};

int Person::count = 0;

int main() {
    Person person1("Alice", 25);
    Person person2 = person1; // 深拷贝
    std::cout << "Count: " << Person::getCount() << std::endl; // 输出:Count: 2
    
    return 0;
}

在上面的示例中,Person类有一个静态成员变量count,用于记录创建的Person对象的数量。在构造函数中,我们通过递增count来跟踪对象的数量,在析构函数中通过递减count来更新对象的数量。

在拷贝构造函数中,我们不需要拷贝静态成员变量count,因为它是属于类本身而不是类的实例。因此,在拷贝构造函数中只需要拷贝非静态成员变量即可。

总结起来,静态成员变量不需要在拷贝构造函数中进行拷贝,因为它们是属于类本身而不是类的实例。文章来源地址https://www.toymoban.com/news/detail-728792.html

到了这里,关于【C++漂流记】一文搞懂类与对象中的对象特征的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • C++初阶学习第三弹——类与对象(上)——初始类与对象

    前言: 在前面,我们已经初步学习了C++的一些基本语法,比如内敛函数、函数重载、缺省参数、引用等等,接下来我们就将正式步入C++的神圣殿堂,首先,先给你找个对象 目录 一、类与对象是什么? 二、类的各部分组成 1、类的定义 2、类的访问限定符及封装 3、类的作用域

    2024年04月26日
    浏览(39)
  • C++ 类与对象(上)

    目录 本节目标  1.面向过程和面向对象初步认识  2.类的引入  3.类的定义 4.类的访问限定符及封装 4.1 访问限定符 4.2 封装 5. 类的作用域 6. 类的实例化  7.类对象模型 7.1 如何计算类对象的大小 7.2 类对象的存储方式猜测 7.3 结构体内存对齐规则  8.this指针 8.1 this指针的引出

    2024年02月19日
    浏览(35)
  • C++ 类与对象(下)

    目录 1. 再谈构造函数 1.1 构造函数体赋值  1.2 初始化列表  1.3 explicit  2. static成员 2.1 概念 2.2 特性  3.友元 3.1友元函数   3.2 友元类 4. 内部类 5.匿名对象  6.拷贝对象时的一些编译器优化  7. 再次理解类和对象 1. 再谈构造函数 2. Static成员 3. 友元 4. 内部类 5.匿名对象

    2024年02月19日
    浏览(31)
  • 【C++】——类与对象(下)

    本文是C++类与对象部分最后一篇文章,类与对象的重点是类与对象(中),本文主要是补充类与对象的剩余知识点及收尾。 在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。 构造函数体中的语句只能将其称为赋初值,而不能称作初始化。

    2024年02月06日
    浏览(27)
  • c++类与对象

    🐶博主主页: @ᰔᩚ. 一怀明月ꦿ  ❤️‍🔥 专栏系列: 线性代数,C初学者入门训练,题解C,C的使用文章 🔥 座右铭: “不要等到什么都没有了,才下定决心去做” 🚀🚀🚀大家觉不错的话,就恳求大家点点关注,点点小爱心,指点指点🚀🚀🚀  目录 🐰类与对象 🌸

    2023年04月08日
    浏览(36)
  • C++:类与对象

    类是对一组具有共同属性特征和行为特征的对象的抽象,它可将相关数据和这些数据的操作(函数)组合在一起。 类定义格式: 类名 : class是声明类的,类名是标识符,且在其作用域内必须是唯一的。 注:C++规定,标识符以字母(大小写均可,但区分大小写)或下划

    2024年02月12日
    浏览(33)
  • c++类与对象详解

    在C++中,对象是类的实例。定义对象的语法为: 其中,class_name 是定义类时指定的类名,object_name 是对象的名称。 例如,在以下示例中,我们定义了一个名为 Rectangle 的类和两个对象 rect1 和 rect2: 这里我们创建了两个 Rectangle 类的对象:rect1 和 rect2。 在C++中,类可以通过以

    2024年02月13日
    浏览(27)
  • C++类与对象(下)

    您好这里是limou3434的博文,感兴趣可以看看我的其他博文系列。 这一部分是对C++类的一些补充:初始化列表、友元、内部类。 在给对象进行初始化的时候,如果选择函数体内进行初始化是一件很麻烦的事情。 但是这样写是没有办法过的,要成功运行的话只能写一个默认构造

    2024年02月11日
    浏览(34)
  • 【C++】类与对象(下)

      若想了解什么是类、封装的意义可以移步  【C++】类与对象(引入)   若对六大成员函数或const成员函数有疑问的这篇文章可能可以帮到你  【C++】类与对象(上) 目录 系列文章 前言 1.初始化列表 1.1概念 1.2特性  1.2.1必须使用初始化列表的情况 1.2.2初始化的顺序 2.expli

    2023年04月14日
    浏览(26)
  • [C++]类与对象下篇

    目录 类与对象下篇::                             1.再谈构造函数                             2.static成员                             3.友元                             4.内部类                             5.匿名对象                             6.拷贝

    2023年04月25日
    浏览(36)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包