首页 / 资讯中心 / 文章详情

C++ 类和对象(上):封装、实例化与 this 指针

C++ 类和对象(上):封装、实例化与 this 指针 ★ FEATURED ARTICLE
一、类的定义面向过程写程序时数据和处理数据的函数是分开的。以栈为例C 语言里需要一个Stack结构体再配一组STPush、STPop函数。任何拿到结构体指针的代码都能直接改top数据没有保护。C 的做法是把数据和操作数据的函数放进同一个类再用访问限定符控制外部能碰哪些成员。这一篇围绕类怎么定义、对象怎么创建、对象占多大空间、this 指针做了什么展开。1.1 定义格式与成员命名class是定义类的关键字后面的名字是类名{}内是类的主体。类定义结束处的分号不能省略漏掉会直接编译报错。类体内的内容统称类的成员。变量称为成员变量也叫类的属性函数称为成员函数也叫类的方法。成员变量在命名上习惯加一个标识常见做法是在变量前或后加_或者用m开头。C 不强制这条规则各公司的编码规范可能不同。struct在 C 里也能定义类。它兼容 C 中struct的用法同时升级成了类最明显的变化是可以在struct里定义函数。定义类时更常用class。定义在类内部的成员函数默认是 inline 的。写不写inline关键字结果都一样。1.2 访问限定符与封装访问限定符是 C 实现封装的手段。类把对象的属性和方法绑在一起再通过访问权限决定哪些接口暴露给外部。public修饰的成员在类外可以直接访问。protected和private修饰的成员在类外不能直接访问这两个限定符当前行为一致区别在继承中才体现。一个访问限定符的作用范围从它出现的位置开始到下一个访问限定符或}结束。class的成员若没有限定符修饰默认是privatestruct默认是public。成员变量一般设为private或protected需要对外使用的成员函数设为public。下面用class定义一个栈。#include iostream using namespace std; class Stack { public: // 成员函数 void Init(int capacity 4) { _arr nullptr; // 实际使用时应 malloc 申请空间 _top 0; _capacity 0; } void Push(int x) { } private: // 成员变量 int* _arr; int _top; int _capacity; }; // 分号不能省略 int main() { Stack s1; s1.Init(); // s1.top; // 私有成员类外无法直接访问 return 0; }public的成员函数写在前面private的成员变量写在后面。C 语言里标识符只能向上查找类中向上向下都能查找所以成员函数可以先使用后面才声明的成员变量。同样的结构用struct定义。#include iostream using namespace std; // 兼容 C 中 struct 的用法升级成了类 // 不 typedef 也可以省略前面的 struct typedef struct A { void func() { } int a1; int a2; } AA; // 升级成了类 struct B // 等价于 class B但默认访问权限不同 { void Init() { } private: int b1; int b2; }; int main() { // struct A aa1; // 不需要这样写了 AA aa2; B bb1; bb1.Init(); }定义链表节点这类纯数据结构时用struct更省事成员默认公有。struct ListNode { // struct 中未加限定符的成员默认公有 public: // 这里给不给 public 都一样 int data; // struct ListNode* next; // 不需要这样写了 ListNode* next; };1.3 类域类定义了一个新的作用域所有成员都在这个作用域内。在类体外定义成员时要用::作用域操作符指明成员属于哪个类。类域影响的是编译期的名字查找规则。不写Stack::编译器会把Init当成全局函数函数体里的_a、_top在当前作用域找不到声明编译报错。写上Stack::编译器知道Init是成员函数当前作用域找不到的名字会到类域里继续查找。把声明和定义拆到不同文件时这条规则直接决定代码能否编译。Stack.h#pragma once #include iostream class Stack { public: void Init(int capacity 4); void Push(int x); private: int* _a; int _top; int _capacity; };Stack.cpp#define _CRT_SECURE_NO_WARNINGS 1 #include Stack.h // 必须带上 Stack:: 指明类域 void Stack::Init(int capacity) { _a nullptr; // malloc _top 0; _capacity capacity; } void Stack::Push(int x) { // ... }test.cpp#include Stack.h int main() { Stack st; st.Init(); return 0; }二、实例化2.1 类与对象的关系用类类型在物理内存中创建对象的过程称为类的实例化。类是对对象的抽象描述像一张设计图。它限定了有哪些成员变量但这些成员变量只是声明不分配空间。用类实例化出对象时才会分配空间。一个类可以实例化出多个对象每个对象占用独立的物理空间用来存成员变量。设计图规划了房间数量和大小但没有实体建筑也不能住人按图造出房子才能住人。类不能直接存数据实例化出的对象才存数据。#include iostream using namespace std; class Stack { public: void Init(int capacity 4) { _arr nullptr; _top 0; _capacity 0; } void Push(int x) { } private: // 成员变量这里是声明不分配空间 int* _arr; int _top; int _capacity; }; int main() { // 实例化出对象 s1、s2此时才分配空间 Stack s1; Stack s2; // 成员函数不在对象里存放在代码段通过地址调用 s1.Init(); // 编译为 call 地址 s1.Init(100); // 成员变量存在对象里 // s1._top 和 s2._top 是两个不同的变量 // s1.Init 和 s2.Init 调用的是同一段代码 cout sizeof(s1) \n; cout sizeof(Stack) \n; return 0; }sizeof既可以作用于对象也可以作用于类类型。两者结果相同都遵守内存对齐规则。2.2 对象里存了什么每个对象有独立的数据空间成员变量一定在对象里。成员函数不在对象里。函数编译后是一段指令指令要存在代码段对象里放不下。若对象里非要存和函数有关的东西只能存函数指针。再看有没有存函数指针的必要Date实例化出d1、d2两者各有自己的_year、_month、_day但d1.Init和d2.Init指向同一段代码。把函数指针存进每个对象就是重复存储。实例化 100 个对象同一份地址就存了 100 遍。函数地址在编译链接阶段就确定了。调用成员函数被编译成call 地址这样的指令地址直接写在指令里运行时不需要去对象里取。只有虚函数动态多态才需要运行时确定地址那种情况下对象里会存一个指向虚函数表的指针。2.3 空类为什么是 1 字节先看下面三个类实例化后的对象大小。#include iostream using namespace std; 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; // 函数不在对象里所以 b、c 都是空类 // 空类的大小为 1 字节这 1 字节用于占位 // 不含实际数据作用是给对象一个可区分的地址 cout sizeof(b) endl; cout sizeof(c) endl; return 0; }A的大小按内存对齐规则计算。B和C没有成员变量成员函数不占空间两者都是空类大小为 1 字节。空类为什么不是 0对象需要在内存中有一个地址。B b1, b2;两个对象必须占用不同的地址b1 ! b2。若大小为 0编译器无法为每个对象分配互不重叠的地址数组下标运算、对象指针比较都会失去意义。分配 1 字节是满足这一要求的最小代价。2.4 内存对齐规则sizeof的结果由内存对齐决定规则如下。第一个成员放在偏移为 0 的位置。其余每个成员放在「对齐数」的整数倍偏移处。对齐数取成员自身大小与编译器默认对齐数中的较小值。Visual Studio 的默认对齐数是 8g 是 4。结构体或类的总大小对齐到所有成员对齐数中的最大值含默认对齐数的整数倍。结构体嵌套时内层结构体按其内部的最大对齐数对齐。回到A_ch是char占 1 字节放在偏移 0。_i是int对齐数是 4必须放在偏移 4 的整数倍处偏移 1 到 3 空出来。_i占偏移 4 到 7。成员最大对齐数是 4总大小 8 是 4 的整数倍所以sizeof(A)是 8。调整成员顺序会改变结果。把_i放到前面、_ch放到后面总大小仍是 8若两个char与一个int按不同顺序排列可能得到 8 或 12。理解规则的用途在于判断结构体大小以及在跨平台二进制接口中避免对齐差异。三、this 指针3.1 成员函数怎么知道操作哪个对象Date类有Init和Print两个成员函数函数体里没有区分对象的代码。d1.Init()和d2.Init()调用同一段代码它怎么知道该改哪个对象的数据C 用一个隐含的 this 指针解决这个问题。编译器编译成员函数时默认在形参列表第一个位置加一个当前类类型的指针名字叫 this。Date::Init的真实原型相当于void Init(Date* const this, int year, int month, int day);成员函数里访问成员变量都是通过 this 访问。Init里给_year赋值实际执行的是this-_year year;。调用d1.Init(2025, 7, 31)时编译器把d1的地址作为 this 传进去等价于Init(d1, 2025, 7, 31)。C 规定不能在实参和形参位置显式写 this编译器会自己处理。函数体内可以显式使用 this。#include iostream using namespace std; class Date { public: // 真实原型是 void Init(Date* const this, int year, int month, int day) // 但不能这样写出来 void Init(int year, int month, int day) { // 函数体内可以显式写 this this-_year year; _month month; // 等价于 this-_month month; _day day; } void Print() { cout _year / _month / _day \n; } private: // 成员变量这里只是声明不分配空间 int _year; int _month; int _day; }; int main() { Date d1; Date d2; // d1.Init(d1, 2025, 7, 31); 不能这样写 d1.Init(2025, 7, 31); // d2.Init(d2, 2025, 9, 1); 不能这样写 d2.Init(2025, 7, 9); d1.Print(); d2.Print(); return 0; }this的类型是Date* const指针本身不可改但它指向的对象可以被修改。3.2 自测一空指针调用成员函数题目下面程序编译运行的结果是——A 编译报错B 运行崩溃C 正常运行。#include iostream using namespace std; class A { public: void Print() { cout this endl; cout A::Print() endl; } private: int _a; }; int main() { A* p nullptr; p-Print(); // 编译为 call 地址 (*p).Print(); return 0; }结果是 C正常运行退出码为 0。分析过程分两步。p为nullptrp-Print()编译后不会去访问p指向的内存。成员函数不存放在对象里调用它只需要函数地址编译成call A::Print这类指令。对象指针在这里的唯一作用是作为 this 传入this 的值就是nullptr
阅读完成 · 觉得有帮助?
咨询建站