1.类的定义
1.1类定义格式
• class为定义类的关键字,Stack为类的名字(跟结构体名类似),{}中为类的主体,注意类定义结束时后面分号不能省略。类体中内容称为类的成员:类中的变量称为类的属性或成员变量, 类中的函数称为类的方法或者成员函数。
我们来用类封装一个简单的基于动态数组的栈(Stack)数据结构:
#include<iostream>
#include<assert.h>
using namespace std;class Stack
{
public:// 成员函数void Init(int n = 4)//缺省参数{array = (int*)malloc(sizeof(int) * n);if (nullptr == array){perror("malloc申请空间失败");return;}capacity = n;top = 0;}void Push(int x){// ...扩容array[top++] = x;}int Top(){assert(top > 0);return array[top - 1];}void Destroy(){free(array);array = nullptr;top = capacity = 0;}private:// 成员变量int* array;size_t capacity;size_t top;
}; // 分号不能省略int main()
{Stack st;//创建了一个 Stack 类的对象 stst.Init();st.Push(1);st.Push(2);cout << st.Top() << endl;st.Destroy();return 0;
}
这段代码是用 C++ 的 类(class) 实现的栈(Stack),相比 C 语言的结构体(struct)+ 函数的方式,它更加面向对象,封装性更好,代码更清晰。
• 为了区分成员变量,⼀般习惯上成员变量会加⼀个特殊标识,如成员变量前面或者后面加_ 或者 m开头,注意C++中这个并不是强制的,只是⼀些惯例。
例如:
class Data
{
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()
{Data da;da.Init(2023, 1, 1);da.print();return 0;
}
• C++中struct也可以定义类,C++兼容C中struct的用法,同时struct升级成了类,明显的变化是struct中可以定义函数,但是⼀般情况下我们还是推荐用class定义类。
// C语言写法
typedef struct ListNodeC
{struct ListNodeC* next;int val;
}LTNode;// C++写法
// C++升级struct升级成了类
// 1、类⾥⾯可以定义函数
// 2、struct名称就可以代表类型
struct ListNodeCPP
{
public:void Init(int x){next = nullptr;val = x;}private:ListNodeCPP* next;int val;
};
•在 C++ 中,定义在类内部的成员函数(即在类声明中直接实现的函数)默认会被编译器视为 inline 的候选,但最终是否真正内联展开由编译器决定。inline只是建议,并不代表绝对。
1.2访问限定符
在上面代码中,我们可以看到public
和private
出现,它们代表着什么意思呢?
在
C++
中,封装是一种核心的面向对象编程特性,它通过 类(class) 将数据(属性)和操作数据的方法(成员函数)捆绑在一起,并通过 访问控制权限 选择性暴露接口,隐藏实现细节。
public
,private
和protected
就是用于实现封装的关键访问控制修饰符,它们决定了类成员的可见性和可访问性。
public
修饰的成员在类外可以直接被访问;protected
和private
修饰的成员在类外不能直接被访问,protected和private是⼀样的,要在学习继承知识时才能看出它们的区别,这里不过多描述。
• 访问权限作用域从该访问限定符出现的位置开始直到下⼀个访问限定符出现时为止,如果后面没有访问限定符,作用域就到 }
即类结束。
• class定义成员没有被访问限定符修饰时默认为private,struct默认为public。
• ⼀般成员变量都会被限制为private/protected,需要给别⼈使用的成员函数会放为public。
1.3类域
在 C++ 中,类定义了一个独立的作用域(class scope),所有成员(变量和函数)都属于这个作用域。当在类外定义成员函数时,必须通过 ::
作用域解析运算符 显式指明该函数属于哪个类,否则编译器会将其视为全局函数,导致编译错误。
核心概念
类作用域
类的成员(如 array、Push())的作用域仅限于类内部。
在类外直接访问这些成员时,编译器无法识别(除非通过对象或 :: 指定类域)。
::
的作用 显式声明成员函数的归属类,指导编译器在正确的类作用域中查找成员变量和其他依赖。
class Stack
{
public:void Init(int n = 4);private:int* arr;int top;int capacity;
};// 声明和定义分离,需要指定类域
// 如果没有指定类域,编译器会认为 Init 是全局函数,找不到 arr(因为 arr 属于 Stack 类作用域)
void Stack::Init(int n)
{int* arr = (int*)malloc(sizeof(int) * n);if (arr == nullptr){perror("malloc fail");return;}top = 0;capacity = n;
}int main()
{Stack st;st.Init();return 0;
}
类作用域的实际影响
-
名称查找规则
在类外定义成员函数时,编译器按以下顺序查找符号:
当前函数局部作用域 → 2. 类作用域(通过 :: 指定)→ 3. 全局作用域。
若未指定类域,编译器直接跳到全局作用域查找,导致成员变量无法找到。 -
与全局函数的区分
void Init(); // 全局函数
void Stack::Init(); // 类的成员函数
即使同名,因作用域不同,二者不会冲突。
2.类的实例化
2.1 什么是实例化
在C++中,类的实例化是将抽象的类定义转化为具体对象的过程。类本身只是对对象的一种抽象描述,它声明了成员变量和方法,但并不会实际分配内存空间,就像一个建筑设计图规定了房间的数量和功能,但图纸本身并不能住人。只有当通过类创建对象(如Person p;)时,系统才会为对象的成员变量分配物理内存空间,此时对象才能存储和操作真实数据。一个类可以实例化出多个独立的对象,每个对象都拥有自己的内存空间来存储数据,就像根据同一张设计图可以建造出多栋实际可居住的房子。类的成员函数(行为)则被所有对象共享,存储在代码段中,不会因实例化而重复占用内存。
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和d2Date d1;Date d2;d1.Init(2020, 1, 1);d1.print();d2.Init(2020, 1, 2);d2.print();return 0;
}
2.2 计算类的实例化对象大小
在C++中,类实例化的对象仅包含成员变量,不存储成员函数
。成员函数编译后位于代码段,所有对象共享同一份函数代码。调用成员函数时,编译器通过隐式传递this
指针(如d1.Print()编译为Print(&d1))确定操作对象,无需在每个对象中存储函数指针。静态绑定(普通成员函数)的地址在编译期确定,直接硬编码到调用指令中;只有虚函数(动态多态)才需要运行时查表(通过虚函数表指针vptr),此时对象需额外存储vptr指向虚函数表,虚函数在以后我们会提及到。因此,非虚成员函数既不需要存储代码也不需存储指针,仅虚函数机制会引入额外指针开销(每个对象一个vptr),避免了重复存储造成的空间浪费。
this指针
• Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调⽤Init和Print函数时,该函数是如何知道应该访问的是d1对象还是d2对象呢?那么这⾥就要看到C++给了⼀个隐含的this指针解决这⾥的问题
• 编译器编译后,类的成员函数默认都会在形参第⼀个位置,增加⼀个当前类类型的指针,叫做this指针。⽐如Date类的Init的真实原型为, void Init(Date* const this, int year,int month, int day)
• 类的成员函数中访问成员变量,本质都是通过this指针访问的,如Init函数中给_year赋值, this->_year = year;
• C++规定不能在实参和形参的位置显示的写this指针(编译时编译器会处理),但是可以在函数体内显示使用this指针。
public:// void Init(Date* const this, int year, int month, int day)void Init(int year, int month, int day)//二者等价,但C++规定不能在形参显示this{//this = nullptr;编译报错:error C2106: “=”: 左操作数必须为左值// this->_year = year;_year = year;//二者等价,并且任意一种写法都符合语法this->_month = month;this->_day = day;}
言归正传,上⾯我们分析了对象中只存储成员变量,C++规定类实例化的对象也要符合内存对⻬的规则。
内存对齐规则其实我们以前学习C语言结构体大小时学习过,这里我们再复习一下。
内存对⻬规则
• 第⼀个成员在与结构体偏移量为0的地址处。
• 其他成员变量要对⻬到对⻬数的整数倍的地址处。
• 注意:对⻬数 = 编译器默认的⼀个对⻬数 与 该成员⼤⼩的较⼩值。
• VS中默认的对⻬数为8
• 结构体总⼤⼩为:最⼤对⻬数(所有变量类型最⼤者与默认对⻬参数取最⼩)的整数倍。
• 如果嵌套了结构体的情况,嵌套的结构体对⻬到⾃⼰的最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体的对⻬数)的整数倍。
看两个例子:
class A 的成员依次是 char _ch、int _i,内存对齐会按照以下步骤排布:
放置第一个成员 char _ch:
从地址 0 开始,char 占 1 字节,此时占用地址 0。
对齐填充:
下一个成员是 int _i,它的对齐值是 4 字节。当前偏移是 1,不是 4 的倍数,所以需要填充 3 个字节(填充到地址 3 之后,让下一个成员的起始地址满足自身对齐要求 ),填充的字节没有实际意义,是为了对齐。
放置第二个成员 int _i:
从地址 4 开始,int 占 4 字节,占用地址 4 到 7。
整体对齐:
类的整体大小需要是其最大对齐值(这里 int 的对齐值 4 是最大的)的整数倍。当前已用空间是 8 字节(1 + 3 + 4 = 8 ),8 是 4 的倍数,无需额外填充。
大家第一眼看会不会觉得结果是0,其实并不是,为什么呢?
因为如果⼀个字节都不给,怎么表示该对象存在过呢!所以这里给1字节,纯粹是为了占位标识对象存在。
更多的内存对齐计算例子可以看结构体博客。
以下是几道围绕 C++ 类 / 结构体布局设计的题目:
1.下⾯程序编译运⾏结果是()
A、编译报错 B、运⾏崩溃 C、正常运⾏
#include<iostream>
using namespace std;
class A
{
public:void Print(){cout << "A::Print()" << endl;}
private:int _a;
};int main()
{A* p = nullptr;p->Print();return 0;
}
这段代码的编译运行结果是 C、正常运行。
-
成员函数的调用机制
p->Print()
会被编译器转换为A::Print()
,隐式传递this
指针(此处 p 是nullptr
)。但 Print() 函数内部并未访问任何成员变量(如 _a),因此不会解引用 this 指针。
-
未触发解引用
只有通过 this 访问成员变量(如
this->_a
)时才会引发解引用空指针崩溃。本例中 Print() 仅输出字符串,不依赖对象内存,故无崩溃风险。
2.下⾯程序编译运⾏结果是()
A、编译报错 B、运⾏崩溃 C、正常运⾏
#include<iostream>
using namespace std;
class A
{
public:void Print(){cout << "A::Print()" << endl;cout << _a << endl;}
private:int _a;
};
int main()
{A* p = nullptr;p->Print();return 0;
}
这段代码的编译运行结果是 B、运行崩溃。
-
空指针调用成员函数
虽然
p->Print()
的语法可以编译通过(因为 Print() 是普通成员函数,非虚函数),但实际执行时会传递this = nullptr
给函数。 -
访问成员变量导致解引用空指针
在 Print() 函数中,第 10 行 cout << _a << endl; 实际上等价于 cout << this->_a << endl;。
由于 this 是 nullptr,尝试访问 _a 会触发 解引用空指针,导致运行时崩溃(如段错误)。
-
与安全调用的区别
如果 Print() 函数 不访问任何成员变量(如原题第 1 问),则不会解引用 this,可以正常运行。
但本题中 _a 的访问直接依赖 this 指针,因此必然崩溃。
3.this指针存在内存哪个区域的 ()
A. 栈 B.堆 C.静态区 D.常量区 E.对象里面
在 C++ 中,this 指针的存储位置取决于调用成员函数的上下文环境,但最准确的答案是:A. 栈。当通过对象调用成员函数(如 obj.func())时,this 指针作为函数的隐式参数,会被编译器放入栈中。
我们来回忆一下这几个区域的存储内容
内存区域 | 存储内容 | 管理方式 | 生命周期 |
---|---|---|---|
栈 | 局部变量、函数参数 | 自动 | 函数执行期间 |
堆 | 动态分配的对象 | 手动 | 直到显式释放 |
静态区 | 全局/静态变量 | 自动 | 整个程序运行期间 |
常量区 | 字符串字面量、constexpr | 自动 | 整个程序运行期间 |
代码区 | 函数体、指令 | 自动 | 整个程序运行期间 |
this指针:作为函数调用的隐式参数,通常存储在栈或寄存器中。
对象成员:非静态成员变量存储在对象所属的内存区域(栈或堆),静态成员变量存储在静态区。