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

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


文章目录

  • 一、构造函数和析构函数
  • 二、函数的分类和调用
    • 1. 分类
    • 2. 调用方式
    • 3. 示例代码
    • 4. 输出结果:
    • 5. 代码解释
  • 三、拷贝构造函数的时机
  • 四、构造函数调用规则
  • 五、深拷贝和浅拷贝
  • 六、初始化列表
  • 七、类对象作为类成员
  • 八、静态成员


相关链接:
一文搞懂类与对象的封装
一文搞懂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 
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 
#include 

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 
#include 

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 
#include 

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 

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,因为它是属于类本身而不是类的实例。因此,在拷贝构造函数中只需要拷贝非静态成员变量即可。

总结起来,静态成员变量不需要在拷贝构造函数中进行拷贝,因为它们是属于类本身而不是类的实例。

你可能感兴趣的:(C++漂流记,c++,开发语言)