类的概念 定义

文章目录

  • 类的概念&定义
    • 继承和多态
  • 构造函数和析构函数
    • 注意点
  • 拷贝构造函数
  • 浅拷贝和深拷贝
  • const 修饰成员函数
  • **this指针**
  • 类的静态成员变量
  • 简单对象模型

类的概念&定义

类可以被认为是一种模板,它描述了如何创建对象,对象是类的实例。

类定义了对象的数据(称为属性)和可以对这些数据进行的操作(称为方法或函数)

class Student {
public:
    string name;  // 学生的名字
    int age;  // 学生的年龄
    double score;  // 学生成绩

    // 学习的方法
    void study() {
        cout << name << " is studying." << endl;
    }

    // 考试的方法
    void takeExam() {
        cout << name << " is taking an exam." << endl;
    }
};

继承和多态

在 C++ 中,一个类可以从另一个类那里继承属性和方法。这个功能非常强大,因为它让我们能够创建更专门化的类,而无需重新编写所有的代码。

例如,我们可以创建一个“大学生”类,它继承了“学生”类的所有属性和方法,并添加了一些新的:

class UniversityStudent : public Student {
public:
    string major;  // 学习的专业

    // 参加实验的方法
    void doExperiment() {
        cout << name << " is doing an experiment." << endl;
    }
};

多态性则是指一个接口可以有多种实现方式,或者说,一个基类的指针可以指向其子类的对象。这是一种强大的特性,因为它让我们能够编写更一般、更灵活的代码。

Student *s = new UniversityStudent();
s->study();  // 调用的是 UniversityStudent 的 study 方法,如果该方法被重写的话

构造函数和析构函数

构造函数是一种特殊的成员函数,当创建类的新对象时,它会自动调用。构造函数的名称与类的名称相同,没有返回类型。你可以使用构造函数来初始化对象的数据成员。

class MyClass {
public:
    int x;
    // 构造函数
    MyClass(int value) : x(value) {}
};

int main() {
    MyClass obj(10);  // 创建对象并初始化x为10
    return 0;
}

析构函数也是一种特殊的成员函数,但是当对象被销毁(即,它离开其作用范围或者被删除)时,它会被自动调用。析构函数的名称是类的名称前加上波浪符(~),没有参数和返回类型。你可以在析构函数中完成任何必要的清理工作。

class MyClass {
public:
    // 析构函数
    ~MyClass() {
        cout << "Object is being destroyed." << endl;
    }
};

int main() {
    MyClass obj;  // 当obj离开其作用范围时,将调用析构函数
    return 0;
}

注意点

  1. 编译器提供的默认构造/析构函数:
  • 如果没有提供构造/析构函数,编译器会提供空实现的构造/析构函数。
  1. 重载构造函数和默认构造函数:
  • 如果你定义了至少一个构造函数,编译器就不会在提供默认构造函数。
class MyClass {
public:
    MyClass(int value) {
        cout << "Constructor called with value: " << value << endl;
    }

    MyClass() {
        cout << "Default constructor called." << endl;
    }
};

int main() {
    MyClass obj1(10);  // 调用带参数的构造函数
    MyClass obj2;  // 调用无参数的构造函数
    return 0;
}
  1. 创建对象和初始化对象

在创建对象时,可以直接为其成员变量赋值。但在创建对象后给其成员变量赋值,和在创建对象时就给其成员函数赋值,二者是有区别的。前者涉及两步操作(先创建,后付制),后者在创建的同时,就为其成员变量赋值,只有一步操作。

class MyClass {
public:
    int x;
};

int main() {
    MyClass obj1;
    obj1.x = 10;  // 先创建对象,然后给x赋值

    MyClass obj2 = {20};  // 创建对象的同时给x赋值
    return 0;
}
  1. 使用new/delete创建/销毁对象:

使用new创建对象时,会调用构造函数。使用delete销毁对象时,会调用析构函数。

class MyClass {
public:
    MyClass() {
        cout << "Constructor called." << endl;
    }

    ~MyClass() {
        cout << "Destructor called." << endl;
    }
};

int main() {
    MyClass* obj = new MyClass;  // 创建对象,调用构造函数
    delete obj;  // 销毁对象,调用析构函数
    return 0;
}
  1. 成员类的构造和析构:

如果一个类的成员是另一个类的对象,那么在创建对象时,会先构造成员类。在销毁对象时,会先析构成员类。

class MemberClass {
public:
    MemberClass() {
        cout << "MemberClass constructor called." << endl;
    }

    ~MemberClass() {
        cout << "MemberClass destructor called." << endl;
    }
};

class MyClass {
public:
    MemberClass member;

    MyClass() {
        cout << "MyClass constructor called." << endl;
    }

    ~MyClass() {
        cout << "MyClass destructor called." << endl;
    }
};

int main() {
    MyClass obj;  // 创建对象
    // 当obj离开其作用范围时,析构函数被调用
    return 0;
}

运行结果:

MemberClass constructor called.
MyClass constructor called.
MyClass destructor called.
MemberClass destructor called.
  1. 在构造函数名后面加括号和参数不是调用构造函数,是创建匿名对象。

在C++中,我们可以不在提供对象名的情况下创建一个对象,这种对象我们称之为匿名对象(也称临时对象)。

当你写出类名并跟上圆括号以及参数时,你实际上时在创建一个匿名对象。这看起来像是在调用构造函数,但实际上是创建了一个匿名对象。

class MyClass {
public:
    MyClass(int value) {
        cout << "Constructor called with value: " << value << endl;
    }
};

int main() {
    MyClass(10);  // 创建一个匿名对象,输出 "Constructor called with value: 10"
    return 0;
}

MyClass(10); 这一行创建了一个匿名对象。这个对象没有名称,而且它在创建后立即被销毁。因此,你只能在创建它的那一行代码中使用它。你应该注意,匿名对象在创建后会立即被销毁,所以你不能在后续的代码中引用它。

匿名对象在许多情况下都很有用。例如,你可以在一个函数返回对象时使用匿名对象,或者在将对象作为函数参数时使用匿名对象。

拷贝构造函数

C++的拷贝构造函数用于通过复制现有的对象来创建新的对象。如果在类中未定义复制构造函数,编译器会自动提供一个,用于复制现有对象的成员变量。

定义拷贝构造函数的语法如下:

ClassName(const ClassName& objectName){...};

创建新的对象的语法如下:

ClassName newObjectName(existingObjectName);
ClassName newObjectName = existingObjectName;

在使用拷贝构造函数时,需要注意以下几点:

  1. 它的访问权限必须是public
  2. 函数名必须和类名相同
  3. 它没有返回值,所以不需要写void
  4. 如果在类中定义了拷贝构造函数,编译器将不会提供默认的拷贝构造函数
  5. 以值传递的方式调用拷贝构造函数时,如果实参为对象,会调用拷贝构造函数
  6. 函数以值得方式返回对象时,可能会调用拷贝构造函数(例如在VS中会调用,linux中不会)
  7. 拷贝构造函数可以重载,可以有默认参数
  8. 如果类中重载了拷贝构造函数但没有定义默认的拷贝构造函数,编译器也会提供默认的拷贝构造函数

例:

#include
using namespace std;

class Point {
private:
    int x, y;
public:
    // 构造函数
    Point(int x1, int y1) {
        x = x1;
        y = y1;
    }

    // 拷贝构造函数
    Point(const Point& p2) {
        x = p2.x;
        y = p2.y;
    }

    int getX() {
        return x;
    }

    int getY() {
        return y;
    }
};

int main() {
    Point p1(10, 15); // Normal constructor is called here
    Point p2 = p1; // Copy constructor is called here

    // Let us access values assigned by constructors
    cout << "p1.x = " << p1.getX() << ", p1.y = " << p1.getY();
    cout << "\np2.x = " << p2.getX() << ", p2.y = " << p2.getY();

    return 0;
}

浅拷贝和深拷贝

浅拷贝和深拷贝是两种不同的对象复制方法,适用于处理类中包含动态分配的内存或指针的情况。

浅拷贝是指当一个类包含指针变量,并使用默认的拷贝构造函数进行复制时,只会复制指针而不会复制指针所指向的内容。这会导致被复制的对象和原始对象中的指针指向相同的内存地址,修改其中一个对象的数据可能会影响另一个对象的数据,这种现象称为别名问题。更糟糕的是,如果其中一个对象被析构,可能会释放掉共享的内存,导致另一个对象的指针变为悬挂指针(野指针),这是浅拷贝的主要问题。

深拷贝是指在拷贝对象时,不仅复制指针变量,而且复制指针所指向的数据。通过深拷贝,我们可以创建一个与原始对象数据相同但完全独立的新对象,修改其中一个对象的数据不会影响另一个对象的数据。实现深拷贝通常需要我们自定义拷贝构造函数。

#include 
using namespace std;

class MyClass {
private:
    int* data;
public:
    MyClass(int value) { 
        data = new int;
        *data = value;
    }

    // 浅拷贝
    MyClass(const MyClass& source) {
        data = source.data; 
    }
    
    // 深拷贝
    MyClass deepCopy(const MyClass& source) {
        MyClass newObj(*source.data); // 使用传入源对象的数据值来创建新对象
        return newObj; // 返回深拷贝对象
    }

    void setValue(int value) {
        *data = value;
    }

    int getValue() {
        return *data;
    }

    ~MyClass() { 
        delete data;
    }
};

int main() {
    MyClass obj1(10);
    MyClass obj2 = obj1; // 浅拷贝
    obj1.setValue(20);
    cout << "After setting obj1 to 20: \n";
    cout << "obj1 value: " << obj1.getValue() << "\n";  // 输出20
    cout << "obj2 value: " << obj2.getValue() << "\n";  // 也输出20,因为obj1和obj2共享内存

    MyClass obj3 = obj1.deepCopy(obj1); // 深拷贝
    obj1.setValue(30);
    cout << "After setting obj1 to 30: \n";
    cout << "obj1 value: " << obj1.getValue() << "\n";  // 输出30
    cout << "obj3 value: " << obj3.getValue() << "\n";  // 输出20,因为obj1和obj3的数据是独立的

    return 0;
}

const 修饰成员函数

在成员函数后面加上const关键字,**这样做的目的是让编译器帮助我们确保这个函数不会修改对象的任何成员变量。**这可以在编译期间预防一类常见的错误:意外地改变对象状态。

  • 一个const成员函数可以被const对象或非const对象调用

  • const对象只能调用const修饰的成员函数,不能调用非cosnt修饰的成员函数。

  • mutable可以突破const的限制,被mutable修饰的成员变量,将永远处于可变的状态,在const修饰的函数中,mutable成员也可以被修改。

注意

  1. 为什么要保护类的成员变量不被修改? 这是为了保证数据的一致性和可预测性。当我们标记一个成员函数为const时,我们告诉编译器和其他程序员,这个函数不会改变对象的状态,这在编程中很有用,因为它可以让我们更好地理解和预测代码的行为。
  2. 为什么用const保护了成员变量,还要再定义一个mutable关键字来突破const的封锁线? 有时候,我们可能有一些类的成员变量,尽管它们可能在const函数中被修改,但不会改变对象的"逻辑状态"。在这种情况下,我们可以使用mutable关键字来允许这种修改。比如,我们可能有一个类的成员变量用来缓存某个计算结果,虽然这个变量在const函数中可能被改变,但是它不改变对象的逻辑状态。
  3. 到底有没有必要使用const和mutable这两个关键字? constmutable的使用主要是提供了额外的编程约束,可以帮助我们更好地设计代码。通过使用这些关键字,我们可以将部分逻辑检查问题交给编译器,这可以减轻我们的编程负担,同时提高代码的可维护性和可读性。

this指针

"this"指针是一个内置的指针,在C++类的非静态成员函数中,它被用来指向调用对象。换句话说,每个对象都可以通过"this"指针来访问自己的地址。

"this"指针的几个重要的应用包括:

  1. 区分同名的数据成员和成员函数的参数。如果成员函数的参数名称与类的数据成员名称相同,可以使用"this"指针来区分它们。
  2. "this"指针可以被用于链接连续调用同一个对象的多个成员函数。例如,对象.func1().func2().func3()。
  3. "this"指针也可以被用于返回调用对象本身的引用,这在链式赋值或方法链中常见。

如果在成员函数的括号后面使用const,意味着该成员函数不会修改对象的状态。因此,你不能通过"this"指针在该函数中修改成员变量。这是一种很好的方式来保证函数不会修改对象的状态,保证数据的一致性。

类的静态成员变量

静态成员是一种特殊的成员,包括静态成员变量和静态成员函数。

静态成员为类的所有对象所共享,而不是为每个对象单独存在一份。它们在程序的生命周期中存在,存在静态存储区。

注意点:

  1. 静态成员变量:使用static关键字声明,它在类的所有对象中是共享的。静态成员变量不会在创建对象时初始化,而需要在全局区明确地初始化。静态成员变量可以通过类名加范围解析运算符(::)访问,无需创建对象。
  2. 静态成员函数:同样使用static关键字声明,只能访问静态成员,不能访问非静态成员。静态成员函数中没有this指针。在非静态成员函数中,可以访问静态成员。
  3. 私有静态成员:私有静态成员在类外无法访问,只能在类内部使用。
  4. const静态成员变量:可以在定义类时初始化。
class MyClass {
public:
    static int static_value;  // 静态成员变量

    static void staticFunction() {  // 静态成员函数
        // this->value; // 错误,静态成员函数中没有this指针
        static_value = 100;  // 在静态成员函数中,可以访问静态成员
    }
};

// 在全局区初始化静态成员变量
int MyClass::static_value = 0;

int main() {
    MyClass::staticFunction();  // 使用类名加范围解析运算符(::)访问静态成员函数
    std::cout << MyClass::static_value << std::endl;  // 输出:100

    return 0;
}

简单对象模型

C++中有两种数据成员:nonstaticstatic,和三种成员函数:nonstaticstaticvirtual

  • 对象的内存大小由以下几部分组成
    • 所有非静态数据成员的大小。
    • 由内存对齐而填补的内存大小。
    • 为了支持virtual成员而产生的额外负担。
  • 静态成员变量属于类,不计算在对象的大小之内。
  • 成员函数时分开存储的,不论对象是否存在都占用存储空间,在内存中只有一个副本,也不计算在对象大小之内。
  • 用空指针可以调用没有用到this指针的非静态成员变量。
  • 对象的地址是第一个非静态成员变量的地址,如果类中没有非静态成员变量,编译器会隐含的增加一个1字节的占位成员。

你可能感兴趣的:(C++学习/笔记,c++,算法,开发语言)