一、类定义格式
类定义的基本格式:
在 C++ 中,使用 class 关键字来定义类,其基本格式如下:
class Stack // class 为定义类的关键字,Stack 为类名 {
// 左大括号表示类体的开始
// 类体内容
}; // 右大括号后的分号不能省略,这是类定义结束的标志
类的成员:
类体中包含的内容统称为类的成员,主要分为两类:
- 成员变量(属性):类中定义的变量,用于描述对象的状态
- 成员函数(方法):类中定义的函数,用于描述对象的行为
成员变量的命名规范:
为了便于区分成员变量与局部变量,业界通常采用以下命名惯例(非强制要求,具体遵循所在团队的编码规范):
- 在成员变量名前加下划线,如:
_top、_size - 在成员变量名后加下划线,如:
top_、size_ - 以"m"开头命名,如:
mTop、mSize
struct 与 class 的关系:
在 C++ 中,struct 也被升级为类,主要体现在:
- 兼容 C 语言中
struct的传统用法 - 扩展功能:
struct中可以定义成员函数 - 主要区别:默认访问权限不同(
struct默认为 public,class默认为 private)
虽然 struct 也能定义类,但通常推荐使用 class 关键字来定义类,这样更能体现面向对象的封装特性。
成员函数的默认特性:
定义在类体内的成员函数默认会被编译器当作内联函数(inline)处理,这有助于提高短小函数的调用效率。
二、访问限定符
访问限定符与封装
C++ 通过访问限定符来实现面向对象的封装特性。封装将对象的属性(成员变量)和方法(成员函数)结合在类内部,并通过访问权限控制,有选择性地向外部使用者暴露接口,隐藏内部实现细节。
三种访问限定符
- public(公有):修饰的成员在类内外都可以直接访问,通常用于对外提供的接口
- protected(保护):修饰的成员在类外不能直接访问,但在派生类(子类)中可以访问
- private(私有):修饰的成员仅在当前类内部可以访问,类外部和派生类都不能直接访问
注:在继承章节之前,protected 和 private 的表现相同,都是类外不可访问;只有在涉及继承时,两者的区别才会体现出来——protected 成员可以被派生类访问,而 private 成员不能。
访问权限的作用域规则
- 访问限定符的作用域从该限定符出现的位置开始,直到下一个访问限定符出现时结束
- 如果某个访问限定符之后没有其他访问限定符,其作用域将持续到类定义结束的右大括号
}
默认访问权限
- 使用
class定义的类:成员默认访问权限为private - 使用
struct定义的类:成员默认访问权限为public
编程实践建议
在实际开发中,通常遵循以下设计原则:
- 成员变量:一般声明为
private或protected,隐藏内部状态,通过公有成员函数提供访问接口 - 成员函数:对外提供的功能接口设为
public,内部辅助函数设为private或protected
三、类域
类域的概念
在 C++ 中,类定义了一个新的作用域,称为类域(Class Scope)。类的所有成员(包括成员变量和成员函数)都位于这个作用域内。这意味着:
- 成员名可以在类内部直接使用
- 在类外部访问成员时,需要通过对象、指针或作用域解析运算符
::来指定所属的类
类域外定义成员
当需要在类体外定义成员函数时,必须使用作用域解析运算符 :: 来指明该函数属于哪个类域,语法格式为:返回类型 类名::函数名 (参数列表)
#include <iostream>
using namespace std;
// 定义 Stack 类
class Stack {
public:
// 成员函数声明(在类体内)
void Init(int n);
void Push(int val);
void Print();
private:
int* array; // 动态数组
int capacity; // 容量
int top; // 栈顶指针
};
// 在类体外定义成员函数,必须使用类域限定符
void Stack::Init(int n) {
capacity = n;
array = new int[capacity];
top = 0;
cout << "栈已初始化,容量:" << capacity << endl;
}
void Stack::Push(int val) {
if (top < capacity) {
array[top++] = val;
cout << "入栈:" << val << endl;
}
}
void Stack::Print() {
cout << "栈内元素:";
for (int i = 0; i < top; i++) {
cout << array[i] << " ";
}
cout << endl;
}
int main() {
Stack st;
st.Init(5); // 通过对象调用成员函数
st.Push(10);
st.Push(20);
st.Push(30);
st.Print();
return 0;
}
四、实例化概念
实例化的概念
在 C++ 中,用类类型在物理内存中创建对象的过程,称为类的实例化。实例化是面向对象编程中的核心概念,它将抽象的类定义转化为具体的、可用的对象。
类与对象的关系
- 类:是对对象的抽象描述,相当于一个蓝图或模型。类中定义的成员变量只是声明,并没有分配实际的物理内存空间。
- 对象:是类的具体实例,通过实例化过程创建。对象占用实际的物理内存,存储类中定义的成员变量。
实例化的特点
- 空间分配:只有实例化对象时,系统才会为成员变量分配内存空间
- 多实例性:一个类可以实例化出多个独立的对象,每个对象都有自己的成员变量存储空间
- 成员函数共享:成员函数在代码区存储,被该类的所有对象共享
#include <iostream>
using namespace std;
// 类的定义(相当于设计图)
class Stack {
public:
int* array; // 成员变量声明(此时未分配空间)
int capacity;
int top;
};
int main() {
// 类就像设计图,本身不占用存储数据的空间
// sizeof 计算的是类型的大小,不是实际存储空间
cout << "Stack 类的大小:" << sizeof(Stack) << " 字节" << endl;
// 实例化对象:用类类型在内存中创建对象
Stack st1; // st1 是 Stack 类的一个实例(对象)
Stack st2; // st2 是另一个独立的对象
// 对象占用实际物理内存,存储成员变量
cout << "对象 st1 的大小:" << sizeof(st1) << " 字节" << endl;
cout << "对象 st2 的大小:" << sizeof(st2) << " 字节" << endl;
// 每个对象都有自己的独立存储空间
st1.capacity = 10;
st2.capacity = 20;
cout << "st1.capacity: " << st1.capacity << endl;
cout << "st2.capacity: " << st2.capacity << endl;
return 0;
}
五、对象大小和内存对齐规则
在 C++ 中,类实例化出的每个对象都有独立的数据空间,但对象中只存储成员变量,不存储成员函数。
为什么成员函数不存储在对象中?
- 函数的本质:函数被编译后是一段指令,存储在代码段(Code Segment)中,而不是存储在对象的内存空间内。
- 没有必要存储函数指针:
- 假设在对象中存储成员函数指针,那么每个对象都要存储相同的函数地址
- 如果实例化 100 个对象,相同的函数指针就会被重复存储 100 次,造成极大的空间浪费
- 编译时确定地址:
- 普通成员函数的调用在编译链接时就已经确定了函数地址
- 编译后的汇编指令为
call 地址,这个地址是在编译链接阶段确定的 - 因此不需要在运行时通过对象查找函数地址
- 特例说明:
- 只有动态多态(虚函数)需要在运行时确定函数地址
- 这时需要通过虚函数表(vtable)来存储函数地址
内存对齐规则
- 第一个成员在与结构体偏移量为 0 的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
- 注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
- VS 中默认的对齐数为 8。
- 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
六、this 指针
为什么需要 this 指针?
当我们定义一个类时,成员函数只有一份代码拷贝,但可能会被多个对象调用。那么问题来了:当对象 d1 调用 Init 和 Print 函数时,函数是如何知道应该访问 d1 的成员变量还是 d2 的成员变量呢?
答案是:C++ 通过隐含的 this 指针来解决这个问题。
this 指针的工作原理
- 编译器的处理
编译器在编译后,会在类的每个非静态成员函数的第一个参数位置,自动添加一个当前类类型的指针参数,这就是 this 指针。 - 原理解析
d1.Init(2024, 1, 1)实际上被编译器转换为Date::Init(&d1, 2024, 1, 1)- 成员函数中对成员变量的访问,如
_year = year,实际上被转换为this->_year = year
- this 指针的特性
- this 指针是 const 指针(
Date* const this),不能修改指向 - 不能在实参和形参位置显式写 this 指针
- 可以在函数体内显式使用 this 指针
- this 指针是 const 指针(
#include <iostream>
using namespace std;
class Date {
private:
int _year;
int _month;
int _day;
public:
// 成员函数 - 编译器实际会转换为:
// void Init(Date* const this, int year, int month, int day)
void Init(int year, int month, int day) {
// 这里的_year 实际是 this->_year
_year = year; // 等价于 this->_year = year
_month = month; // 等价于 this->_month = month
_day = day; // 等价于 this->_day = day
// 可以在函数体内显式使用 this 指针
cout << "this 指针的地址:" << this << endl;
}
// 成员函数 - 编译器实际会转换为:
// void Print(Date* const this)
void Print() {
// 显式使用 this 指针访问成员变量
cout << "对象地址:" << this << endl;
cout << "日期:" << this->_year << "-" << this->_month << "-" << this->_day << endl;
// 也可以隐式访问,效果相同
// cout << "日期:" << _year << "-" << _month << "-" << _day << endl;
}
// 演示 this 指针的用途:比较两个日期是否相等
bool IsSameMonth(const Date& d) {
// this 指向当前对象,&d 是参数对象的地址
return (this->_month == d._month);
}
// 返回当前对象的引用(链式编程常用)
Date& SetYear(int year) {
this->_year = year;
return *this; // 返回对象本身
}
};
int main() {
cout << "=== this 指针演示 ===" << endl;
// 创建两个不同的对象
Date d1;
Date d2;
cout << "d1 的地址:" << &d1 << endl;
d1.Init(2024, 1, 1); // &d1 被隐式传递给 this 指针
d1.Print();
cout << "\nd2 的地址:" << &d2 << endl;
d2.Init(2024, 6, 15); // &d2 被隐式传递给 this 指针
d2.Print();
// 演示 this 指针如何区分对象
cout << "\n=== 比较月份 ===" << endl;
if (d1.IsSameMonth(d2)) {
cout << "d1 和 d2 是同一个月" << endl;
} else {
cout << "d1 和 d2 不是同一个月" << endl;
}
// 演示 this 指针的链式调用
cout << "\n=== 链式调用演示 ===" << endl;
d1.SetYear(2025).Print(); // SetYear 返回*this,可以继续调用 Print
return 0;
}

