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
修饰的成员可以在类外直接访问;而protected
和private
修饰的成员不能直接访问,继承的时候,protected
成员可以继承到子类,但是private
成员是不可见的,子类无法访问 -
访问权限的作用域从访问限定符开始,直到下一个访问限定符出现为止;如果没有下一个限定符,作用域将一直延续到类的结束
}
。 -
在类定义中,若成员没有明确的访问限定符修饰,默认为
private
;而在struct
中,默认为public
。 -
一般情况下,成员变量会被设为
private
或protected
,而提供给外部使用的成员函数则会放置为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 内存对齐规则:
-
类的第一个成员从地址0开始对齐。
-
其他成员需要按照特定的对齐数(通常是编译器默认值,且与成员大小的较小值相关)对齐。常见的对齐数为8。
-
结构体的总大小应是最大对齐数的整数倍。
-
如果结构体中包含嵌套结构体,每个嵌套结构体也会对齐到其最大对齐数的整数倍,整体结构体大小以所有对齐数的最大值为标准。
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
指针传递给A::Print(p)
。在this
是nullptr
,但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;
}
在这个例子中,
_a
成员变量。由于p
是空指针,this->_a
的操作会导致程序试图对空指针解引用,从而导致程序崩溃。这说明使用this
指针时,如果不注意对象是否为空,可能会带来严重问题。
3. this指针存在内存的哪个区域()
A. 栈 B.堆 C.静态区 D.常量区 E.对象里面
this
指针是隐式传递给成员函数的一个指针,用来指向调用该成员函数的对象。每次调用成员函数时,this
指针会作为隐藏的参数被传入,而这种参数传递通常是通过栈完成的。因此,this
指针实际上是存储在栈上的
5. 总结
在本文中,我们深入探讨了C++中的类、对象及成员函数的概念,重点介绍了类实例化过程、对象大小的计算、内存对齐规则及this
指针的使用。通过代码示例,分析了类实例化、内存管理以及空指针的潜在风险。感谢各位读者的关注。