【C++】初识类和对象

引言

在C语言中,我们用结构体来描述一个复杂的对象,这个对象可能包括许多的成员,如用结构体描述一个学生的成绩,或者描述一个日期等。

cpp 复制代码
struct Date
{
    int _year;
    int _month;
    int _day;
};

如上是一个描述日期的结构体定义,里面可以有年、月、日这些成员,但是不能在里面有函数的声明或定义,这就使得和这个日期对象有关的函数需写在外部,在命名时就需要防止冲突。而且C语言的结构体对成员变量的保护不到位,可以随意访问对象的成员变量,非常不安全。因此,C++在兼容C语言 struct 的用法的同时将它升级为了类,并且C++喜欢用 class 关键字来定义类。


类的定义

cpp 复制代码
class 类名
{
    // 类体:由成员函数和成员变量组成
    
}; // 末尾分号不能落下

类体中的内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者 成员函数。

cpp 复制代码
struct ListNode
{
	//struct ListNode* next; // C语言写法
	ListNode* next;          // C++可以这样用
	int val;
};

类名就是类型,所以不需要再像C语言一样加上 struct 来表示类型。


接下来我们来定义一个简单的日期类:

cpp 复制代码
class Date
{
    void Init(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }

    int _year;
    int _month;
    int _day;
};

我们在类中写了一个 Init 函数用来初始化我们的对象,我们试着用这个类来实例化对象。

cpp 复制代码
int main()
{
    Date d1;
    d1.Init(2024, 1, 22);

    return 0;
}

用类的类型创建对象的过程,称为类的实例化。可以把类理解为设计图纸,用类实例化对象就是用图纸创建出具体的实物。但是目前我们的程序是有问题的,这个函数运行不了,因为目前这个函数是私有的,在类外面无法访问,这涉及到访问限定符的问题。


访问限定符

访问限定符包括:public(公有)、private(私有)、protected(保护),public 修饰的成员在类外可以直接被访问,而 private 和 protected 修饰的成员在类外不能直接被访问,现阶段暂时认为private 和 protected 没有什么区别。由于 struct 需要兼容C语言的用法,因此类中没使用访问限定符时默认是共有的,而 class 默认是私有的,因此我们上面那段代码无法运行。

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

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

加上访问限定符让成员函数的访问权限为公有就能运行了,成员变量一般都定义为私有。值得注意的是,如果成员函数在类中定义,编译器可能会将其当成内联函数处理,因此可以将较短的函数定义在类中,而较长的函数可以将声明与定义分离。


如果要将成员函数的声明和定义分离,在类外定义函数时需要在函数名字前使用域作用限定符指定类名,因为类定义了一个新的作用域。如下所示:

cpp 复制代码
class Date
{
public:
    void Init(int year, int month, int day);

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

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

指定类名相当于告诉编译器这个函数是哪个类的,不然在涉及成员变量时编译器找不到这个变量的定义。


类的实例化

类是对对象进行描述的,就像一张设计图纸,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。在类中定义的成员变量都只是声明,没有内存空间。只有当使用这个类去实例化对象的时候,它们才会作为该对象的专属成员变量而定义,也就是有了空间。

一个类可以实例化出多个对象,那它们是不是都有专属于自己的成员变量呢?是的,来看这个例子

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

    void print() { cout << _year << " " << _month << " " << _day << endl; }

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

int main()
{
    Date d1, d2;
    d1.Init(2024, 1, 22);
    d2.Init(2023, 1, 22);
    d1.print();
    d2.print();

    return 0;
}

这里我们用 Date 类实例化了两个对象比对它们进行初始化,然后借助 print 函数打印了它们各自的成员变量,很明显它们都有各自的成员变量。可是在调用函数时明明没有传参,它们的成员变量是怎么被准确找到的呢?这就是 this 指针的功劳了。


this 指针

其实每个"非静态的成员函数"的第一个参数都是一个隐藏的 this 指针,该指针指向的是当前对象(函数运行时调用该函数的对象)。在函数体中所有涉及"成员"的操作,都是通过该指针去访问。只不过这个过程我们看不到,因为用户不需要传递该指针,编译器会自动帮我们传递与使用该指针。

cpp 复制代码
class Date
{
public:
    // 第一个形参是帮助理解,用户不能自己写
    // 为了篇幅着想,此处 Init 函数只给出声明,定义上面有
    void Init(Date* const this, int year, int month, int day);

    void print(Date* const this) 
    { 
        // 在函数中显示使用 this 是可以的,大部分情况不需要,交给编译器
        cout << this->_year << " " << this->_month << " " << this->_day << endl; 
    }

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

int main()
{
    Date d1, d2;
    // 同样第一个实参用于帮助理解,用户不能自己传
    d1.Init(&d1, 2024, 1, 22);
    d1.print(&d1);

    return 0;
}

通过这段代码你应该知道对象的成员变量如何被访问了,实际上是编译器在负重前行,默默地帮我们传递对象的地址,默默地使用 this 指针去访问成员变量。

注意我给出的 this 指针的类型,它被 const 所修饰,因此不能改变指向,也非常合理,要是哪位大牛在函数内把 this 指针改了可就不好玩了。

总结一下: this 指针的类型为:类类型* const,它是"成员函数"第一个隐含的指针形参,当对象调用成员函数时,自动将对象地址作为实参传递给this形参,并且只能在"成员函数"的内部使用,一般情况由编译器通过 ecx 寄存器自动传递,不需要用户传递。


类(对象)的大小

cpp 复制代码
int main()
{
    Date d1;
    d1.Init(2024, 1, 22);
    cout << sizeof(d1) << endl;

    return 0;
}

依旧是上面的日期类,你认为对象 d1 的大小是多少呢?通过前面的学习,成员变量存储在对象中应该已经是大家的共识了,那么成员函数会不会存在对象里呢?我们运行下程序看看。

由结果可见,成员函数是不存在对象中的。这也很符合实际,一个类可以实例化出那么多对象,要是每个对象都存一次函数,那岂不是太浪费空间了。事实上,成员函数存放在公共的代码段,大家都能调用。


好了现在你已经知道一个类的大小就是该类中"成员变量"之和,并且要注意内存对齐。那么请问 A类的大小又是多少?

cpp 复制代码
class A
{};

int main()
{
    cout << sizeof(A) << endl;

    return 0;
}

简单,没有成员变量,所以大小是0,是这样吗?那么如果我用这个类实例化一个对象,你怎么表示这个对象存在过呢?简单来说就是你怎么表示这个对象。因此,对于没有成员变量的类(就算它有成员函数也一样,对象中不存储成员函数),其实例化对象大小为 1 个字节,这个字节不存储有效数据,只是来唯一标识这个类的对象。


现在问题又来了,this 指针为什么不存在对象中呢?首先 this 指针是形参,形参一般都是存在栈上的,也就是函数栈帧中,函数结束它就销毁了。但在 vs 下,this 指针通常存储在 ecx 寄存器中,因为它需要频繁使用,而且本身大小较小,将其存放在寄存器中可以提高访问速度。


现在补充一下内存对齐的知识:

  1. 第一个成员变量存储在结构体偏移量为0的地址,即从结构体的起始位置开始。
  2. 其他成员变量要对齐到对齐数的整数倍的地址处。 注意:对齐数 = 编译器默认的一个对齐数与 该成员大小的较小值。 (VS中默认的对齐数为8)
  3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

有趣的程序

cpp 复制代码
class A
{
public:
	void Print()
	{
		cout << "Print()" << endl;
	}

private:
	int _a;
};

int main()
{
	A* p = nullptr;
	p->Print();

	return 0;
}

该程序可以正常运行,虽然 p 是空指针,但是并没有涉及到访问空指针的问题,因为成员函数不存放在对象中,通过 p 能调用函数只是因为 p 指向的对象是 A 类的,而 Print 函数也是 A 类的。随后在函数内也没有访问成员变量,因此程序没问题。如图所示:

这里 p 的作用主要有两个:第一,让编译器知道调用的是 A 类里的 Print 函数。第二,调用函数时顺便把对象的地址,也就是 p 本身传给了形参 this 指针。

相关推荐
娅娅梨12 分钟前
C++ 错题本--not found for architecture x86_64 问题
开发语言·c++
兵哥工控16 分钟前
MFC工控项目实例二十九主对话框调用子对话框设定参数值
c++·mfc
汤米粥18 分钟前
小皮PHP连接数据库提示could not find driver
开发语言·php
冰淇淋烤布蕾21 分钟前
EasyExcel使用
java·开发语言·excel
我爱工作&工作love我24 分钟前
1435:【例题3】曲线 一本通 代替三分
c++·算法
拾荒的小海螺27 分钟前
JAVA:探索 EasyExcel 的技术指南
java·开发语言
马剑威(威哥爱编程)1 小时前
哇喔!20种单例模式的实现与变异总结
java·开发语言·单例模式
娃娃丢没有坏心思1 小时前
C++20 概念与约束(2)—— 初识概念与约束
c语言·c++·现代c++
lexusv8ls600h1 小时前
探索 C++20:C++ 的新纪元
c++·c++20
lexusv8ls600h1 小时前
C++20 中最优雅的那个小特性 - Ranges
c++·c++20