C++——类和对象的基础

发布于:2024-07-05 ⋅ 阅读:(22) ⋅ 点赞:(0)

类的引入

什么是类?

类(Class)是面向对象编程(Object-Oriented Programming, OOP)中的一个核心概念。它是一种将抽象思维过程转化为具体软件构造单元的方式,用于定义一组具有相同属性(Attributes)和方法(Methods)的对象的蓝图或模板
简单来说,类是一种数据类型的模板,它指定了如何创建对象的规则,包括对象将拥有哪些属性(数据成员)以及这些对象可以执行哪些操作(成员函数或方法)。

类引入解决了什么问题?

  1. 代码复用:类是创建对象的模板,通过定义类,我们可以封装一组数据(属性)和相关的操作(方法)。当需要创建具有相似属性和方法的多个对象时,只需定义一次类,然后基于该类创建多个对象。这样避免了重复编写相同的代码,提高了代码复用率。
  2. 数据封装:类通过封装将数据(属性)和操作数据的方法(方法)组合在一起,形成了一个独立的单元。这种封装隐藏了类的内部实现细节,只对外提供必要的接口(即公有属性和方法),从而保护了数据的安全性,防止了外部代码直接访问或修改类的内部状态。
  3. 多态性:多态性是面向对象编程的一个核心概念,它允许不同类的对象对同一消息作出响应,但具体执行的操作可能因对象所属类的不同而异。类的引入为实现多态性提供了基础,因为多态性通常是通过继承(Inheritance)和接口(Interface)来实现的,而类和类之间的关系正是通过继承来定义的。
  4. 模块化:通过将程序分解为多个类,我们可以更好地组织代码,提高程序的可读性和可维护性。每个类都负责一组相关的功能,通过类的组合和协作来完成复杂的任务。这种模块化的设计使得代码更加清晰、易于理解和修改。
  5. 扩展性:类的继承机制允许我们创建基于现有类的新类(子类),子类可以继承父类的属性和方法,并可以添加新的属性和方法或覆盖父类的方法。这种扩展性使得我们可以轻松地修改和扩展程序的功能,而不需要修改现有的代码。
  6. 易于理解和交流:面向对象编程中的类与现实世界中的对象具有相似性,这使得代码更加直观、易于理解和交流。通过定义类,我们可以将复杂的业务逻辑抽象为简单的对象模型,使得非技术人员也能更容易地理解程序的结构和功能。

为什么要引入类?

  1. 抽象化:类是现实世界或问题域中实体的抽象表示。通过将复杂的数据结构及其操作封装在类中,我们可以创建一个简化的模型,这个模型更容易理解和操作。类允许我们关注于对象的行为和属性,而不是实现细节。
  2. 封装:封装是面向对象编程的一个基本原则,它通过将数据(属性)和操作这些数据的方法(成员函数)组合在一起,隐藏了内部实现细节。这种封装保护了数据不被外部直接访问或修改,从而提高了数据的安全性和完整性。类是实现封装的主要机制。
  3. 代码复用:通过定义类,我们可以创建可重用的代码模板。当需要创建具有相似属性和方法的新对象时,我们可以简单地实例化这个类,而无需重新编写相同的代码。这大大提高了代码复用率,减少了代码冗余,并使得软件更加模块化。
  4. 多态性:多态性允许不同类的对象对同一消息作出不同的响应。在面向对象编程中,多态性通常是通过类的继承和接口来实现的。类的引入为实现多态性提供了基础,使得我们可以在运行时根据对象的实际类型来调用相应的方法。
  5. 组织和管理复杂性:随着软件系统的增长和复杂化,组织和管理代码变得越来越重要。通过将系统分解为多个类,每个类负责一组相关的功能,我们可以更好地组织代码,降低系统的复杂性。类的引入使得代码更加模块化,易于理解和维护。
  6. 支持面向对象的设计原则:类的引入使得我们能够应用面向对象的设计原则,如单一职责原则、开放封闭原则、里氏替换原则、依赖倒置原则等。这些原则指导我们如何设计类和类之间的关系,以创建高质量、可维护的软件系统。
  7. 提高开发效率:由于类提供了代码复用、封装和多态性等特性,因此使用类进行面向对象编程可以显著提高开发效率。开发人员可以更快地构建软件系统,并在需要时更容易地进行修改和扩展。

类的定义

在C++中,类的定义是通过class关键字开始的,后面跟着类名和一对大括号{},大括号内包含了类的成员声明,包括数据成员(也称为属性或字段)和成员函数(也称为方法)。类的定义可以出现在全局作用域、命名空间内或另一个类内(作为嵌套类)。
以下是一个简单的C++类定义示例,该类表示一个点(Point)在二维空间中的位置:

#include <iostream>  
  
class Point {  
private: // 私有成员只能被类的成员函数或友元访问  
    int x, y; // 点的坐标  
  
public: // 公有成员可以在类的外部被访问  
    // 构造函数  
    Point(int x = 0, int y = 0) : x(x), y(y) {}  
  
    // 成员函数:设置点的坐标  
    void setX(int x) {  
        this->x = x; // 使用this指针来区分成员变量和参数  
    }  
  
    void setY(int y) {  
        this->y = y;  
    }  
  
    // 成员函数:获取点的坐标  
    int getX() const {  
        return x;  
    }  
  
    int getY() const {  
        return y;  
    }  
  
    // 成员函数:打印点的坐标  
    void print() const {  
        std::cout << "(" << x << ", " << y << ")" << std::endl;  
    }  
};  
  
int main() {  
    Point p1(10, 20); // 创建一个Point对象p1,并初始化其x和y坐标  
    p1.print(); // 调用成员函数打印p1的坐标  
  
    p1.setX(5); // 修改p1的x坐标  
    p1.setY(15); // 修改p1的y坐标  
    p1.print(); // 再次打印p1的坐标,以验证修改  
  
    return 0;  
}

在这个例子中,Point类有两个私有数据成员x和y,分别表示点的横坐标和纵坐标。类还提供了几个公有成员函数来访问和修改这些私有成员的值,包括构造函数Point(int x = 0, int y = 0)、设置坐标的函数setX和setY、获取坐标的函数getX和getY,以及打印坐标的函数print。

类的访问限定符

在C++中,类的访问限定符是用于指定类成员的访问级别的关键字。这些访问限定符包括public、protected和private,它们控制了类成员(包括变量和函数)的可见性和可访问性。以下是关于这些访问限定符的详细解释:

1. public(公有)

  • 定义:被声明为public的成员可以在类的内部、外部以及派生类(子类)中被访问。
  • 用途:public成员通常用于提供类的接口和公开的数据成员,使外部代码能够直接访问和操作这些成员。
  • 示例
class MyClass {  
public:  
    int publicVar; // 公有成员变量  
    void publicFunc() { // 公有成员函数  
        // 函数体  
    }  
};

在上述示例中,publicVar和publicFunc都可以在类的外部被访问和调用。

2. private(私有)

  • 定义:被声明为private的成员只能在类的内部被访问,外部无法直接访问。
  • 用途:private成员通常用于实现类的内部细节和私有数据成员,隐藏类的实现细节,实现封装。
  • 示例
class MyClass {  
private:  
    int privateVar; // 私有成员变量  
    void privateFunc() { // 私有成员函数  
        // 函数体  
    }  
};

在上述示例中,privateVar和privateFunc只能在MyClass类的内部被访问和调用

3. protected(保护)

  • 定义:被声明为protected的成员可以在类的内部以及派生类中被访问,但无法在类的外部直接访问。
  • 用途:protected成员主要用于在继承中隐藏实现细节但仍允许子类访问这些细节。
  • 示例
class Base {  
protected:  
    int protectedVar; // 保护成员变量  
    void protectedFunc() { // 保护成员函数  
        // 函数体  
    }  
};  
 
class Derived : public Base {  
public:  
    void accessProtected() {  
        protectedVar = 10; // 在派生类中可以直接访问基类的protected成员  
        protectedFunc(); // 在派生类中可以调用基类的protected成员函数  
    }  
};

在上述示例中,protectedVar和protectedFunc在Base类中声明为protected,它们可以在Base类的内部以及Derived类中被访问和调用,但无法在Base类的外部直接访问。

注意事项

  • 访问限定符的作用域是从访问限定符出现的位置开始,到下一个访问限定符出现为止,或者如果没有其他限定符,则作用域到类的结束。
  • class的默认访问权限是private,而struct的默认访问权限是public(为了兼容C语言)。
  • 访问限定符只在编译时有用,它们控制的是成员的访问权限,而不是成员的物理存储位置。

通过合理使用这些访问限定符,可以实现类的封装性、继承性和多态性,使得类的设计更加合理、安全和易于维护。

类的作用域

在C++中,类的作用域(Class Scope)是指**类成员(包括成员变量、成员函数、嵌套类型等)**在代码中的可见性和可访问性范围。类的作用域主要由类的定义确定,并且可以通过访问修饰符(如public、protected和private)来控制类成员的访问权限。以下是关于类作用域的详细解释:

1. 访问修饰符与类作用域

  • public:成员可以在任何地方被访问,包括类的外部。这意味着,无论是类的实例对象还是类的继承者,都可以直接访问public成员。
  • protected:成员可以在类的内部、派生类内部以及类的友元函数中访问。protected成员对于类的外部是不可见的,但对于派生类是可见的,这有助于实现继承时的封装和隐藏。
  • private:成员只能在类的内部和类的友元函数中访问。private成员是完全隐藏的,外部无法直接访问,只能通过类的公有成员函数进行间接访问。

2. 类的定义与作用域

  • 类的成员在类的定义内部定义,默认情况下,如果没有指定访问修饰符,则默认为private(在class关键字定义的类中)。这意味着这些成员只能在类的内部被访问。
  • 在类的作用域内,可以声明和定义类的成员变量和成员函数。这些成员在类的实例被创建时分配内存,并在实例的生命周期内保持有效。

3. 作用域解析运算符(::)

  • 在访问类的静态成员时,可以使用作用域解析运算符(::)来指定类名和成员名,例如ClassName::staticMemberName。这有助于在多个类或命名空间中避免命名冲突。
  • 在定义成员函数时,如果该函数在类定义之外实现,则需要使用作用域解析运算符来指定函数所属的类,例如void ClassName::functionName() {…}。

4. 类的嵌套与作用域

  • 类可以嵌套在另一个类中,形成嵌套类。嵌套类的作用域被限制在外层类的内部,但它可以独立地定义自己的成员和作用域。
  • 嵌套类可以访问外层类的公有成员和保护成员(如果它们是静态的),但无法直接访问外层类的私有成员。

5. 类的实例与作用域

  • 当创建类的实例(对象)时,该实例的作用域由它的声明位置决定。例如,在函数内部声明的类实例具有局部作用域,而在全局作用域中声明的类实例则具有全局作用域。
  • 类的实例可以访问其成员变量和成员函数,但访问权限受成员访问修饰符的限制。

类的实例化

在C++中,类的实例化是指创建一个类的对象的过程。这个过程涉及为类的实例分配内存,并调用类的构造函数来初始化对象。类的实例化可以通过多种方式实现,以下是几种常见的实例化方法:

1. 使用默认构造函数在栈上实例化

  • 当类定义了默认构造函数(即没有参数的构造函数)时,可以直接使用类名加对象名的方式来创建对象。这种方式是在栈上分配内存的,对象的作用域与声明它的作用域相同,当离开该作用域时,对象会自动被销毁。
class MyClass {  
public:  
    MyClass() { // 默认构造函数  
        // 构造函数的实现  
    }  
};  
 
MyClass obj; // 实例化对象

2. 使用带参数的构造函数在栈上实例化

  • 如果类定义了带参数的构造函数,则需要在创建对象时提供这些参数。
class MyClass {  
public:  
    MyClass(int x) { // 带参数的构造函数  
        // 使用参数x进行初始化  
    }  
};  
 
MyClass obj(10); // 实例化对象,传递参数10

3. 使用new运算符在堆上实例化

  • 使用new运算符可以在堆上动态分配内存来创建对象。这种方式需要手动管理内存,即使用delete运算符来释放分配的内存
class MyClass {  
public:  
    MyClass() {  
        // 构造函数的实现  
    }  
};  
 
MyClass* ptr = new MyClass(); // 动态分配内存,创建对象  
// 使用对象指针  
ptr->someMethod(); // 假设MyClass有someMethod成员函数  
// 释放内存  
delete ptr;

4. 创建对象数组

  • 无论是栈上分配还是堆上分配,都可以创建对象数组。在栈上创建数组时,可以使用默认构造函数或带参数的构造函数(如果数组元素需要初始化)。在堆上创建数组时,通常使用new运算符。

栈上创建对象数组:

MyClass arr[5]; // 创建包含5个MyClass对象的数组,使用默认构造函数

堆上创建对象数组:

MyClass* arrPtr = new MyClass[5]; // 创建包含5个MyClass对象的数组,在堆上分配内存  
// 使用完毕后需要释放内存  
delete[] arrPtr;

注意事项

  • 在栈上创建的对象不需要手动释放内存,它们的生命周期由作用域决定。
  • 在堆上创建的对象需要手动释放内存,否则会导致内存泄漏。
  • 选择在栈上还是在堆上创建对象取决于具体的需求,例如对象的生命周期、是否需要动态地分配内存等。

类的实例化是C++编程中非常基础且重要的概念,它允许程序员根据类的定义创建具体的对象,并通过这些对象来访问类的成员变量和成员函数。

类对象模型

如何计算类对象的大小?

在C++中,计算类对象的大小是一个复杂的过程,因为它涉及到多个因素,包括成员变量的大小、数据对齐、虚函数、虚继承等。以下是一个详细的步骤和考虑因素,用于计算类对象的大小:

1. 考虑成员变量的大小

  • 非静态成员变量:类对象的大小主要由其非静态成员变量的大小决定。每个成员变量都会占用一定的内存空间,这些空间在对象实例化时会被分配。
  • 静态成员变量:静态成员变量不属于任何特定的对象实例,而是属于类本身。因此,静态成员变量不占用类对象的内存空间。

2. 考虑数据对齐

  • 编译器为了提高内存访问的效率,会对数据进行对齐。这意味着成员变量可能会占用比其实际大小更多的内存空间,以便它们的起始地址是某个特定值(如2、4、8等)的倍数。
  • 数据对齐的具体规则取决于编译器和目标平台。例如,在64位系统上,编译器可能会默认将数据对齐到8字节的倍数。

3. 考虑虚函数和虚继承

  • 虚函数:如果类包含虚函数,编译器会为该类生成一个虚函数表(vtable),并在每个对象实例中插入一个指向该虚函数表的指针(vptr)。这个指针的大小(通常是4字节或8字节,取决于平台)会被计入对象的大小。
  • 虚继承:在虚继承的情况下,编译器会插入额外的指针(通常是虚基类指针)来管理基类在派生类中的位置。这些指针的大小也会被计入对象的大小。

4. 空类的大小

  • 即使类中没有成员变量,C++标准也规定一个独立的(非附属)对象必须具有非零大小。因此,空类对象的大小通常为1字节,这是为了区分不同的空类对象。

5. 使用sizeof运算符

  • 在C++中,可以使用sizeof运算符来获取类对象的大小。这个运算符会返回对象所占用的字节数,包括所有成员变量、对齐填充、虚函数表指针等。

示例

假设有以下类定义:

class MyClass {  
public:  
    char a;  
    int b;  
    virtual void func() {}  
};
  • a 是一个 char 类型,占用1字节。
  • b 是一个 int 类型,在大多数现代平台上占用4字节。
  • 由于存在虚函数 func(),编译器会为该类生成一个虚函数表,并在每个对象实例中插入一个指向该虚函数表的指针(vptr),这个指针的大小通常是4字节或8字节(取决于平台)。
  • 考虑到数据对齐,编译器可能会在 a 和 b 之间插入填充字节,以确保 b 的起始地址是对齐的。

因此,sizeof(MyClass) 的结果将取决于平台(特别是数据对齐和指针大小)和编译器的具体实现。在64位系统上,它可能是12字节(1字节的 a,3字节的填充,4字节的 b,4字节的vptr)或16字节(如果对齐要求更严格)。

结论

计算类对象的大小需要考虑多个因素,包括成员变量的大小、数据对齐、虚函数、虚继承等。使用 sizeof 运算符是获取类对象大小的最直接方法,但了解背后的原理有助于更好地理解类的内存布局和性能特性。

类对象的存储方式?

类对象的存储方式主要依赖于类中的成员,包括数据成员(成员变量)和成员函数(成员方法)。以下是对类对象存储方式的详细解析:

一、数据成员的存储

  1. 存储位置
    • 数据成员(成员变量)直接存储在对象的内存中。它们按照声明顺序存储,并遵循特定的内存对齐规则。
    • 对于内置类型(如int、float、double等),其占用的字节数和对齐方式由编译器和平台决定。
    • 对于类类型的数据成员,其存储方式取决于该类的定义和存储规则
  2. 特殊情况
    • 如果类中包含虚函数,对象内存布局的最前面会包含一个指向虚函数表的指针(通常是4或8个字节,取决于编译器和平台)。这允许在运行时通过对象的指针或引用来调用虚函数,实现多态性。
    • 空类(没有数据成员和虚函数的类)在大多数编译器中也会被赋予至少1个字节的大小,以便能够区分不同的对象实例。

二、成员函数的存储

  1. 不直接存储于对象内
    • 成员函数(成员方法)的代码并不直接存储在对象内存中。这是因为多个对象实例共享相同的成员函数代码,因此没有必要在每个对象中复制一份。
    • 成员函数通常存储在代码段(Code Segment)中,这是一个只读区域,用于存储可执行指令。
  2. 调用方式
    • 当对象调用成员函数时,编译器会隐式地将对象的地址(通过this指针)传递给函数。这使得函数能够访问和操作对象的数据成员。
    • this指针是一个指向当前对象的指针,它作为非静态成员函数的隐含参数存在。用户不需要显式传递this指针,编译器会自动处理。

三、示例说明

假设有以下类定义:

class MyClass {  
public:  
    int a;  
    double b;  
    void func() {  
        // 成员函数体  
    }  
};
  • 当创建MyClass的一个对象时,该对象在内存中的布局将包括一个int类型的a(占用4个字节,假设在32位系统上),一个double类型的b(占用8个字节),以及(如果类中有虚函数的话)一个指向虚函数表的指针(可能是4或8个字节)。
  • 成员函数func的代码不直接存储在对象中,而是存储在代码段中。当对象调用func时,编译器会生成代码来传递对象的地址给func,并通过this指针访问对象的数据成员。

四、总结

类对象的存储方式主要包括数据成员的存储和成员函数的调用方式。数据成员直接存储在对象中,而成员函数则存储在代码段中,并通过this指针与对象关联。这种存储方式既节省了空间(因为多个对象共享相同的成员函数代码),又提供了灵活性和多态性(通过虚函数和虚函数表)。

结构体的对齐规则?

结构体的对齐规则是编程中确保数据在内存中高效存取的重要机制。这些规则通常依赖于编译器、目标平台(如32位或64位系统)以及处理器架构。以下是结构体对齐的一般规则:

1. 第一个成员的对齐

  • 第一个成员变量总是放在结构体偏移量为0的地址处。这意味着结构体的起始地址就是第一个成员变量的地址。

2. 其他成员的对齐

  • 每个成员变量之后的地址必须是该成员大小的整数倍,或者是对齐数的整数倍,其中对齐数是编译器默认的对齐数与成员大小的较小值。对齐数可以通过编译器指令(如#pragma pack(n))进行调整。
  • 例如,在32位系统中,如果编译器默认的对齐数为4,那么对于int类型的成员(大小为4字节),它会被放在偏移量为4的倍数的地址上。

3. 结构体的总大小对齐

  • 结构体的总大小必须是其最大对齐数的整数倍。最大对齐数是结构体中所有成员变量对齐数的最大值。
  • 这意味着结构体末尾可能会有一些未使用的字节(称为填充字节),以确保结构体总大小满足对齐要求。

4. 嵌套结构体的对齐

  • 如果结构体中嵌套了其他结构体,那么嵌套的结构体也会按照其自己的对齐规则进行对齐。
  • 结构体的整体大小将是所有最大对齐数(包括嵌套结构体的对齐数)的整数倍。

5. 编译器和平台的影响

  • 不同的编译器和平台可能有不同的默认对齐数。例如,在Visual Studio中,默认的对齐数可能是8(对于64位系统),而在GCC中则可能是4(取决于具体的编译器选项和平台)。
  • 因此,在编写跨平台代码时,需要注意对齐规则可能导致的差异,并可能需要使用编译器指令来显式指定对齐数。

6. 对齐的作用

  • 对齐的主要作用是优化内存访问速度。由于现代处理器通常能够更高效地访问对齐的数据,因此通过确保数据对齐,可以提高程序的运行效率。
  • 另外,一些硬件平台对特定类型的数据访问有对齐要求,如果不满足这些要求,可能会导致硬件异常或性能下降。

示例

假设有以下结构体定义(在32位系统上,编译器默认对齐数为4):

struct S {  
    char a; // 1字节,对齐到偏移量0  
    int b;  // 4字节,对齐到偏移量4(因为前面有1字节的a和3字节的填充)  
    char c; // 1字节,但后面会有3字节的填充,以确保结构体总大小为8字节的整数倍  
};

在这个例子中,结构体S的总大小为8字节,尽管成员变量总共只占用了6字节的存储空间。这是因为成员b需要对其到4字节的倍数,而结构体本身也需要对其到其最大对齐数(在这里是4)的整数倍。

this指针


网站公告

今日签到

点亮在社区的每一天
去签到