C++类与对象(上):基础知识与实例

1. 引言 

C++是一门支持面向对象编程(OOP)的强大语言,而“类”和“对象”正是OOP的基础。在软件开发中,类提供了一种抽象和组织代码的方法,使得程序更加模块化和易于维护;而对象则是类的实例,承载了实际的数据和行为。本篇文章将从基础语法入手,带你了解如何定义类和创建对象,并逐步深入探讨类的核心概念,例如实例化和this指针。

2. 类的定义

2.1 类定义的格式

  • class是用于定义类的关键字,Student为类的名称,{}中包含类的主体部分。注意,类定义后需要以分号结束。类的组成部分称为成员:变量称为属性成员变量,函数称为方法成员函数

  • 为了区分成员变量,通常在变量前添加特殊标识,如加前缀m_或后缀_。虽然C++对此没有强制规定,但遵循团队或公司惯例是良好的实践。

  • 在C++中,struct不仅兼容C中的结构体用法,还升级为类的形式,支持定义函数。尽管如此,建议使用class来定义类以符合常规编程习惯。

  • 类中定义的成员函数默认是inline,有助于提升运行效率。

class Student
{    
    // 类中定义的成员函数默认是inline
    void Init(string name, string id, int age)
    {
        _name = name;
        _id = id;
        _age = age;
    }

    // 为了区分成员变量,通常在变量前添加特殊标识,例如 _name
    string _name;  
    string _id;
    int _age;
};// 类定义后需要以分号结束

// 使用struct定义的 Student 结构体,支持定义成员函数
// C++中的struct不仅可以定义数据成员,还可以定义成员函数
struct Student
{
    // 定义成员函数 Init 来初始化学生的信息
    void Init(string name, string id, int age)
    {
        _name = name;
        _id = id;
        _age = age;
    }
    
    // 成员变量,存储学生的信息
    string _name;
    string _id;
    int _age;
};

2.2 访问限定符

  • 使用public修饰的成员可以在类外直接访问;而protectedprivate修饰的成员不能直接访问,继承的时候,protected成员可以继承到子类,但是private成员是不可见的,子类无法访问

  • 访问权限的作用域从访问限定符开始,直到下一个访问限定符出现为止;如果没有下一个限定符,作用域将一直延续到类的结束}

  • 在类定义中,若成员没有明确的访问限定符修饰,默认为private;而在struct中,默认为public

  • 一般情况下,成员变量会被设为privateprotected,而提供给外部使用的成员函数则会放置为public

所以上边使用class写的学生类是外部不可见的,因为默认是private修饰

class Student
{    
public:
    // 公有成员函数,用于初始化学生对象
    void Init(string name, string id, int age)
    {
        _name = name;
        _id = id;
        _age = age;
    }

private:
    string _name;  
    string _id;
    int _age;
};

通常都会把成员变量设置成私有,  成员函数设置成公有,  允许外部通过这些函数与对象交互。这种做法符合封装的原则,  有助于隐藏对象的复杂性并提高程序的安全性和可维护性。

2.3 类域

类定义了⼀个新的作用域,类的所有成员都在类的作用域中,在类体外定义成员时,需要使用::作 用域操作符指明成员属于哪个类域。 

class Student
{    
public:
    // 公有成员函数,用于初始化学生对象
    void Init(string name, string id, int age);

private:
    string _name;  
    string _id;
    int _age;
};

// 声明和定义分离时,定义函数需要指定类域
void Student::Init(string name, string id, int age)
{
    _name = name;
    _id = id;
    _age = age;
}

在C++中,如果不指定类域,编译器可能会将成员函数误认为是全局函数。例如在Init函数之前没有指定类域,编译器会认为 Init 是全局函数,而找不到像 _name_id 等类成员变量,导致编译错误。通过指定类域(如 Student::Init),编译器知道这是 Student 类的成员函数,会自动到该类的域中查找相应的成员变量,确保代码能够正确编译。

3. 类的实例化 

3.1 实例化的概念

  • 将类在物理内存中创建为具体对象的过程被称为类的实例化。

  • 实例化是从类创建对象的过程。类是一种抽象的模板,而对象是具体的实例化产物。

  • 类就像是一份详细的房屋设计图,定义了房屋的结构、房间的大小、布局和功能。然而,设计图本身是抽象的,无法用来居住。只有当按照设计图建造出具体的房屋后,房屋才会拥有物理形态,可以用来居住。同样,类只是定义了对象的属性和行为,只有通过实例化创建对象后,这些定义才能被具体实现,占用内存并进行操作。

#include <iostream>
#include <string>
using namespace std;

// 设计图类
class HouseBlueprint 
{
public:
    // 初始化房屋设计
    void Init(string type, int rooms, double area) 
    {
        _type = type;
        _rooms = rooms;
        _area = area;
    }

    // 显示房屋信息
    void ShowInfo() 
    {
        cout << "房屋类型: " << _type << endl;
        cout << "房间数量: " << _rooms << endl;
        cout << "总面积: " << _area << " 平方米" << endl;
    }

private:
    string _type;  // 房屋类型
    int _rooms;    // 房间数量
    double _area;  // 总面积
};

int main() 
{
    // 使用设计图创建两个房屋实例
    HouseBlueprint house1, house2;

    // 初始化房屋信息
    house1.Init("别墅", 5, 300.5);
    house2.Init("公寓", 3, 120.8);

    // 显示房屋信息
    cout << "房屋1信息:" << endl;
    house1.ShowInfo();
    
    cout << "房屋2信息:" << endl;
    house2.ShowInfo();

    return 0;
}

类本身并不占用内存空间,只有实例化出对象后才会分配物理空间。类就像建造图纸,而对象才是真正的房子。此外,我们可以在栈上定义局部对象,也可以在堆上动态分配内存,但记得及时释放堆内存以避免资源泄漏。

3.2 对象的大小 

内存对齐本质上是通过牺牲一定的内存空间来提升效率,旨在减少CPU在内存访问时的延迟,从而加快数据访问速度。在C++中,类实例化时的对象也需遵循内存对齐规则,类似于C语言中结构体的存储方式。

3.2.1 内存对齐规则: 

  1. 类的第一个成员从地址0开始对齐。

  2. 其他成员需要按照特定的对齐数(通常是编译器默认值,且与成员大小的较小值相关)对齐。常见的对齐数为8。

  3. 结构体的总大小应是最大对齐数的整数倍。

  4. 如果结构体中包含嵌套结构体,每个嵌套结构体也会对齐到其最大对齐数的整数倍,整体结构体大小以所有对齐数的最大值为标准。

class A
{
public:
    void Print()
    {
        cout << _ch << endl;
    }
private:
    char _ch;  // 一个字节
    int _i;    // 四个字节
};

class B
{
public:
    void Print()
    {
        //...
    }
};

class C {};

int main()
{
    A a;
    B b;
    C c;

    cout << "Size of A: " << sizeof(a) << endl; // 输出 8,考虑到内存对齐
    cout << "Size of B: " << sizeof(b) << endl; // 输出 1
    cout << "Size of C: " << sizeof(c) << endl; // 输出 1
    return 0;
}

 3.2.2 内存对齐特殊情况

从上面的例子中,我们可以看到b和c的大小为1,  这是为什么呢

关于上面例子中的成员函数,  因为这些函数是所有对象共享的。如果每个对象都存储一份函数代码,会浪费大量空间。因此,成员函数会被存放在公共代码段中,每个对象只存储它们自己的成员变量。不过,如果涉及到继承和虚函数,虚函数表(vtable)指针将会被存储在对象中,以便实现多态。

在上面的例子中,B 和 C 的对象大小为 1 字节,而不是 0。这是因为 C++ 对象的最小大小至少为 1 字节,在没有成员变量的情况下,编译器仍然需要为每个类分配至少 1 字节的内存来表示对象的存在。 如果一个字节都不给,怎么表示对象存在过呢!

4. this指针

4.1 this指针概念

假设我们有这样一个Date

class Date
{
public:
    void Init(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }

private:
    int _year;
    int _month;
    int _day;
}

int main()
{
    Date d1;
    Date d2;

    d1.Init(2000, 11, 10);
    d2.Init(2001, 10, 12);
    return 0;
}

Date类中有Init成员函数,函数体中没有关于不同对象的区分,当调用Init函数时,该函数是如何知道应该访问的是d1对象还是d2对象呢?

在 C++ 中,当对象调用类的成员函数时,编译器会在函数的隐藏参数中加入一个指向调用对象的指针,这就是 this 指针。它用于区分不同的对象,确保成员函数操作的是当前调用的对象。例如,关于上面的 Date 类,其成员函数 Init 实际原型如下: 

void Init(Date* const this, int year, int month, int day);

函数内的成员变量访问实际上等价于通过 this 指针访问,例如:

this->_year = year;
this->_month = month;
this->_day = day;

虽然 this 指针是隐式传递的,但在函数体内可以显式使用它,例如在需要返回对象自身的场景下。然而,通常情况下,不需要显式使用 this 指针,因为编译器会自动处理对象与成员的关联。需要注意的是,C++ 禁止在形参和实参中直接显式传递 this 指针,它的传递完全由编译器负责。

4.2 this指针经典问题

以下是三道this指针经典问题:

1. 下面程序编译运行结果是()

A、编译报错 B、运行崩溃 C、正常运行

class A {
public:
    void Print() {
        cout << "A::Print()" << endl;
    }
private:
    int _a;
};

int main() {
    A* p = nullptr;
    p->Print(); // 调用成员函数
    return 0;
}

隐式 this 指针的传递:

p->Print() 被调用时,编译器会将 p 的值(即 nullptr)作为隐式的 this 指针传递给 Print 函数。实际调用类似于 A::Print(p)。在 Print 函数内,尽管 thisnullptr,但 Print 并未访问类中的成员变量,因此不会解引用 nullptr,代码能够正常执行。

2. 下面程序编译运行结果是()

A、编译报错 B、运行崩溃 C、正常运行

class A {
public:
    void Print() {
        cout << "A::Print()" << endl;
        cout << _a << endl; // 访问成员变量
    }
private:
    int _a;
};

int main() {
    A* p = nullptr;
    p->Print(); // 调用成员函数
    return 0;
}

在这个例子中,Print 函数尝试访问 _a 成员变量。由于 p 是空指针,this->_a 的操作会导致程序试图对空指针解引用,从而导致程序崩溃。这说明使用 this 指针时,如果不注意对象是否为空,可能会带来严重问题。

3. this指针存在内存的哪个区域()

A. 栈 B.堆 C.静态区 D.常量区 E.对象里面 

this 指针是隐式传递给成员函数的一个指针,用来指向调用该成员函数的对象。每次调用成员函数时,this 指针会作为隐藏的参数被传入,而这种参数传递通常是通过栈完成的。因此,this 指针实际上是存储在栈上的 

5. 总结

在本文中,我们深入探讨了C++中的类、对象及成员函数的概念,重点介绍了类实例化过程、对象大小的计算、内存对齐规则及this指针的使用。通过代码示例,分析了类实例化、内存管理以及空指针的潜在风险。感谢各位读者的关注。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值