☰
【C++】类和对象(上):从类的定义到this指针
2026/10/10 10:14:27 网站建设 项目流程

本文是 C++ 类和对象入门的第一篇,主要讲解类的定义、访问限定符、类域、实例化、对象大小计算以及 this 指针等核心概念,最后通过 C 与 C++ 实现 Stack 的对比,初步感受封装的思想。

一、类的定义

1.1 类定义格式

在 C++ 中,class是定义类的关键字,语法格式如下:

cpp

class ClassName { // 类体:成员变量 + 成员函数 }; // 注意分号不能省略!

几个关键点:

  • 类体内容称为类的成员:变量称为属性/成员变量,函数称为方法/成员函数。

  • 成员变量命名惯例:为了区分成员变量,通常加特殊标识,如_year、m_year、year_等。这不是强制要求,具体看公司规范。

  • struct 也能定义类:C++ 兼容 C 的 struct 用法,同时将其升级为类,struct 内可以定义函数。但一般推荐用class。

  • 类内定义的成员函数默认为 inline。

示例:用 class 实现 Stack

cpp

#include<iostream> 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; st.Init(); st.Push(1); st.Push(2); cout << st.Top() << endl; st.Destroy(); return 0; }
示例:Date 类

cpp

class Date { public: void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } private: // 为了区分成员变量,一般习惯上成员变量 // 会加一个特殊标识,如 _ 或者 m 开头 int _year; // year_ m_year int _month; int _day; }; int main() { Date d; d.Init(2024, 3, 31); return 0; }
struct 升级为类

cpp

// C++兼容C中struct的用法 typedef struct ListNodeC { struct ListNodeC* next; int val; }LTNode; // 不再需要typedef,ListNodeCPP就可以代表类型 struct ListNodeCPP { void Init(int x) { next = nullptr; val = x; } ListNodeCPP* next; int val; };

1.2 访问限定符

C++ 实现封装的方式:用类将对象的属性与方法结合在一块,通过访问权限选择性地将接口提供给外部用户使用。

限定符说明
public类外可以直接访问
protected类外不能直接访问(与 private 目前表现一致,继承章节才体现区别)
private类外不能直接访问

作用域规则:从该访问限定符出现的位置开始,直到下一个访问限定符出现为止;如果后面没有访问限定符,作用域就到}即类结束。

默认权限:

  • class定义的成员未加限定符时默认为private

  • struct定义的成员未加限定符时默认为public

一般规范:成员变量限制为 private/protected,需要给外部使用的成员函数放为 public。

1.3 类域

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

cpp

class Stack { public: // 成员函数声明 void Init(int n = 4); private: // 成员变量 int* array; size_t capacity; size_t top; }; // 声明和定义分离,需要指定类域 void Stack::Init(int n) { array = (int*)malloc(sizeof(int) * n); if (nullptr == array) { perror("malloc申请空间失败"); return; } capacity = n; top = 0; } int main() { Stack st; st.Init(); return 0; }

为什么必须指定类域?

类域影响的是编译的查找规则。如果Init不指定类域Stack,编译器就把它当成全局函数,编译时找不到array等成员的声明/定义,就会报错。指定类域Stack后,编译器知道Init是成员函数,当前域找不到的array等成员,就会到类域中去查找。


二、实例化

2.1 实例化概念

  • 用类类型在物理内存中创建对象的过程,称为类实例化出对象。

  • 类是对对象的一种抽象描述,是一个"模型",限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间。用类实例化出对象时,才会分配空间。

  • 一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量。

比喻:类实例化出对象就像使用建筑设计图建造出房子。类就像设计图,规划了有多少个房间、房间大小功能等,但并没有实体的建筑存在,也不能住人;用设计图修建出房子,房子才能住人。同样,类不能存储数据,实例化出的对象分配物理内存存储数据。

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 Date d1; Date d2; d1.Init(2024, 3, 31); d1.Print(); d2.Init(2024, 7, 5); d2.Print(); return 0; }

2.2 对象大小

类对象中只存储成员变量,不存储成员函数。

原因分析:

  • 函数被编译后是一段指令,存储在代码段,对象中没办法存储。

  • 如果非要存储,只能存成员函数的指针。但d1和d2的成员函数指针是一样的,存储就浪费了。如果用Date实例化 100 个对象,函数指针就重复存储 100 次。

  • 函数指针不需要存储,因为调用函数被编译成汇编指令[call 地址],编译器在编译链接时就要找到函数的地址,不是在运行时找(只有动态多态是运行时找)。

C++ 规定类实例化的对象也要符合内存对齐的规则。

内存对齐规则
  1. 第一个成员在与结构体偏移量为 0 的地址处。

  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

    • 对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值

    • VS 中默认的对齐数为 8

  3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。

  4. 如果嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

示例分析

cpp

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 << sizeof(a) << endl; // 8 cout << sizeof(b) << endl; // 1 cout << sizeof(c) << endl; // 1 return 0; }

结果分析:

  • A:char _ch占 1 字节,int _i要对齐到 4 的倍数,所以偏移 4 开始占 4 字节,总大小 8 字节。

  • B、C:没有成员变量,大小为 1。为什么没有成员变量还要给 1 个字节?因为如果一个字节都不给,怎么表示对象存在过呢!所以这里给 1 字节,纯粹是为了占位标识对象存在。


三、this 指针

3.1 为什么需要 this 指针?

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

C++ 给了一个隐含的 this 指针解决这里的问题。

3.2 this 指针的本质

  • 编译器编译后,类的成员函数默认都会在形参第一个位置,增加一个当前类类型的指针,叫做this 指针。

  • 比如Date类的Init的真实原型为:

    cpp

    void Init(Date* const this, int year, int month, int day)
  • 类的成员函数中访问成员变量,本质都是通过 this 指针访问的,如Init函数中给_year赋值,实际是this->_year = year;

  • C++ 规定不能在实参和形参的位置显式地写 this 指针(编译时编译器会处理),但可以在函数体内显式使用 this 指针。

cpp

class Date { public: // void Init(Date* const this, int year, int month, int day) void Init(int year, int month, int day) { // 编译报错:error C2106: "=": 左操作数必须为左值 // this = nullptr; // this->_year = year; _year = year; this->_month = month; this->_day = day; } void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1; Date d2; // d1.Init(&d1, 2024, 3, 31); d1.Init(2024, 3, 31); d1.Print(); d2.Init(2024, 7, 5); d2.Print(); return 0; }

3.3 经典面试题

题目 1:下面程序编译运行结果是?

cpp

class A { public: void Print() { cout << "A::Print()" << endl; } private: int _a; }; int main() { A* p = nullptr; p->Print(); return 0; }

答案:C、正常运行

解析:Print()函数体内没有访问任何成员变量,不需要通过 this 指针解引用,因此空指针调用不会崩溃。

题目 2:下面程序编译运行结果是?

cpp

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、运行崩溃

解析:cout << _a本质是this->_a,而this是nullptr,对空指针解引用会导致运行时崩溃。

题目 3:this 指针存在内存哪个区域?

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

答案:A、栈

解析:this 指针是成员函数的隐含形参,函数调用时在栈上开辟空间存储,因此存在于栈区。


四、C++ 和 C 语言实现 Stack 对比

面向对象三大特性:封装、继承、多态。通过对比可以初步了解封装。

C 实现 Stack

c

typedef int STDataType; typedef struct Stack { STDataType* a; int top; int capacity; }ST; void STInit(ST* ps) { assert(ps); ps->a = NULL; ps->top = 0; ps->capacity = 0; } void STDestroy(ST* ps) { assert(ps); free(ps->a); ps->a = NULL; ps->top = ps->capacity = 0; } void STPush(ST* ps, STDataType x) { assert(ps); if (ps->top == ps->capacity) { int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2; STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType)); if (tmp == NULL) { perror("realloc fail"); return; } ps->a = tmp; ps->capacity = newcapacity; } ps->a[ps->top] = x; ps->top++; } bool STEmpty(ST* ps) { assert(ps); return ps->top == 0; } void STPop(ST* ps) { assert(ps); assert(!STEmpty(ps)); ps->top--; } STDataType STTop(ST* ps) { assert(ps); assert(!STEmpty(ps)); return ps->a[ps->top - 1]; } int STSize(ST* ps) { assert(ps); return ps->top; } int main() { ST s; STInit(&s); STPush(&s, 1); STPush(&s, 2); STPush(&s, 3); STPush(&s, 4); while (!STEmpty(&s)) { printf("%d\n", STTop(&s)); STPop(&s); } STDestroy(&s); return 0; }

C++ 实现 Stack

cpp

typedef int STDataType; class Stack { public: // 成员函数 void Init(int n = 4) { _a = (STDataType*)malloc(sizeof(STDataType) * n); if (nullptr == _a) { perror("malloc申请空间失败"); return; } _capacity = n; _top = 0; } void Push(STDataType x) { if (_top == _capacity) { int newcapacity = _capacity * 2; STDataType* tmp = (STDataType*)realloc(_a, newcapacity * sizeof(STDataType)); if (tmp == NULL) { perror("realloc fail"); return; } _a = tmp; _capacity = newcapacity; } _a[_top++] = x; } void Pop() { assert(_top > 0); --_top; } bool Empty() { return _top == 0; } int Top() { assert(_top > 0); return _a[_top - 1]; } void Destroy() { free(_a); _a = nullptr; _top = _capacity = 0; } private: // 成员变量 STDataType* _a; size_t _capacity; size_t _top; }; int main() { Stack s; s.Init(); s.Push(1); s.Push(2); s.Push(3); s.Push(4); while (!s.Empty()) { printf("%d\n", s.Top()); s.Pop(); } s.Destroy(); return 0; }

对比总结

  1. 封装性:C++ 中数据和函数都放到类里面,通过访问限定符进行限制,不能再随意通过对象直接修改数据。这是 C++ 封装的一种体现,本质是一种更严格规范的管理,避免出现乱访问修改的问题。

  2. 语法便利:

    • Init给的缺省参数方便很多;

    • 成员函数每次不需要传对象地址,因为 this 指针隐含地传递了;

    • 使用类型不再需要typedef,用类名就很方便。

  3. 本质未变:入门阶段实现的 Stack 看起来变了很多,但实质上变化不大。等学到 STL 中用适配器实现的 Stack,再感受 C++ 的魅力。


五、总结

知识点核心要点
类的定义class关键字,分号不能省略;成员变量+成员函数;类内函数默认 inline
访问限定符public / protected / private;class 默认 private,struct 默认 public
类域类外定义成员需用::指定类域
实例化类只是"设计图",实例化对象才分配空间
对象大小只包含成员变量,符合内存对齐规则;空类大小为 1
this 指针隐含形参,指向当前对象;存储在栈上;不能在形参/实参位置显式写

以上就是 C++ 类和对象(上)的全部内容。下一篇文章将继续讲解类和对象(中),包括构造函数、析构函数、拷贝构造等默认成员函数,敬请期待!

如果本文对你有帮助,欢迎点赞、收藏、评论三连支持一下!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询