☰
【C++修炼】类和对象(上):初识类和对象、this指针
2026/10/16 1:43:00 网站建设 项目流程

目录

一、类和对象

1. 类的定义

2. 访问限定符

3. 类域

4. 类的实例化—对象

5. 对象的大小与计算

6. this指针

7. 习题

二、C++和C语⾔实现Stack对⽐


一、类和对象

1. 类的定义

外在格式:

  • class为定义类的关键字。关键字后跟类的名称,例如 Stack。名称后跟花括号{},花括号内是类的主体。注意,类的定义结束时,花括号后要加分号 ;

代码示例 1.1.1

class Stack // class为关键字 Stack为类名 { // 类的主体——成员变量/成员函数 // ... // ... }; // 类定义完成,注意花括号后的分号;不能省略。

类的主体:

  • 类的主体是成员。成员分为:类中的变量,称为类的属性或者成员变量(与C语言中的结构体类似);类中的函数,称为类的⽅法或者成员函数。
  • 类与结构体的区别之一在于成员函数。结构体内仅有变量,而类中还有函数,即方法封装进了类。
  • 类与结构体的区别之二是使用,类名即类型名称。C语言中,如果我们不使用关键字typedef为结构体重命名,则必须在结构体名称前加上struct才是结构体类型的名称。

代码示例 1.1.2

// 数据结构——栈(stack)的实现 // C语言 // Stack不是结构体类型名,struct Stack才是 typedef struct Stack { // 结构体的主体仅有成员变量 int* arr; int top; int capacity; }ST; // typedef重命名结构体类型名struct Stack为ST // C语言只能在结构体外部定义函数 void push(ST* ps, int x) { //... } // 压栈方法 void pop(ST* ps) { //... } // 出栈方法 int top(ST* ps) { //... } // 访问栈顶方法 // C++ // 无需typedef重命名,类名Stack即类类型名称 class Stack // 类的书写格式:class + 类名 + {} + ; { public: // 除了成员变量,类中还可以定义成员函数 void push(int x) { // ... } // 压栈 void pop() { // ... } // 出栈 int top() { // ... } // 获取栈顶元素 // 成员变量 int* arr; int top; int capacity; }; // 分号不能省略! int main() { // C语言 // 定义变量要用类型名称,Stack是结构体名称,struct Stack是结构体类型名称 struct Stack st1; // 使用typedef重命名结构体类型名称为ST ST st2; // 访问结构体中的成员变量 st2->top = 0; // 调用压栈方法 push(&st2, 0); // 方法未封装,需要传结构体变量的地址 // C++ // 类名Stack即类类型名,无需加上struct或class,也无需typedef Stack st3; // Stack类的对象访问成员变量 st3.top = 0; // 调用压栈方法 st3.push(0); // 方法被封装,无需手动传地址,直接调用 return 0; }

2. 访问限定符

访问限定符是C++中帮助实现封装的一种⽅式。类将对象的属性与⽅法(或者说变量与函数)封装到一起,使对象更加完善(如Stack类,压栈方法直接与Stack类绑定),并通过访问权限,选择性地将接口/函数提供给外部使⽤。

访问限定符分为以下三个:

  • 公有public:修饰的成员在类外可以直接被访问
  • 私有private:修饰的成员在类外不能直接被访问
  • 保护protected:修饰的成员在类外不能直接被访问

⼀般,成员变量都会被限制为private/protected,需要给别⼈使⽤的成员函数会放为public。

​

注意,暂时来说,我们可以认为protected和private是一样的,它们修饰的成员在类外都不能被直接访问。等到我们学习了继承后,才能体现出它们的区别。

访问限定符的使用

代码示例 1.2.1

class Stack { public: // 顶格写,后面跟冒号 // 冒号之后到下一个限定符前,都是public // 成员函数 void push(int x) {} void pop() {} void top() {} private: // 顶格写,后面跟冒号 // 冒号之后到右花括号},都是private // 成员变量 int* arr; int top; int capacity; };

一般不希望成员变量被类外随意访问,因此成员变量通常为私有。
如栈,一个后进先出的顺序结构,有栈底与栈顶。

arr是数组,用以模拟栈。数组首元素是栈底,末尾元素是栈顶。规定:数据入栈,栈顶入;数据出栈,栈顶出;读取数据从栈顶读,若需要读取下一个,只能先从栈顶出栈,读取新的栈顶元素。

因此,栈是一个严格规定的数据结构,外部若可以随意进行访问、修改或删除操作,是很不规范、不安全的。C语言中,结构体的成员变量只能是公有。C++则为类提供了访问限定符,成员还可以被定义为私有/保护。
同时,public也保证了需要被频繁调用的方法/函数可以在类外正常使用,如Stack类的压栈方法push、出栈方法pop以及获取栈顶方法top。
需要强调,成员变量和成员函数是公有、私有或保护看实际情况。

我们需要成员变量在类外访问修改,也可以定义为公有;我们不需要成员函数在类外调用,也可以定义为私有。

例如代码示例 1.1.2,为了演示,我们需要在类外访问成员变量,也无需保护数据,所以栈的成员变量置为公有。

访问限定符的作用域

  • class 定义类,无访问限定符修饰的部分默认为 private,即左花括号{开始,到右花括号}或第一个访问限定符为止,这段区域都是私有。访问限定符的作⽤域从其出现位置开始,到下一个访问限定符或右花括号为⽌。

代码示例 1.2.2

class Stack { // {到第一个限定符public都是私有 void push(int x) {} public: // 该位置到下一个限定符private都是公有 void pop() {} private: // 该位置到下一个限定符public都是私有 void top() {} public: // 该位置到下一个限定符protected都是公有 int* _arr; int _top; protected: // 该位置,到右花括号}都是保护 int _capacity; }; int main() { Stack st; st.push(); // push方法是私有,不能类外访问 st.pop(); // pop方法是公有,可以类外访问 st.top(); // top方法是私有,不能类外访问 st._arr; // 成员变量_arr是公有,可以类外访问 st._top; // 成员变量_top是公有,可以类外访问 st._capacity;// 成员变量_capacity是保护,不能类外访问 return 0; }

成员变量的命名惯例

  • 为了特别区分成员变量,通常习惯变量名上会加一个特殊标识,例如单词首字母大小写、前下划线、后下划线或加字母m(即英文单词member成员)等等。注意,这并非强制规定或语法要求,只是一些惯例,具体看公司要求

代码示例 1.2.3

class Date { public: // 成员函数 void Init(int year, int month, int day) { // 若不加上特殊标识成员变量就和形参名称一致了,很麻烦,只能修改形参名 // 但修改形参名称,例如void Init(int y, int m, int d),就牺牲了可读性 int _year = year; int _month = month; int _day = day; } private: // 为了区分成员变量,习惯名称上加特殊标识 // 特殊标识不固定,具体看项目要求,以下是一些名称示例 int _year; // year_ mYear m_year year_m int _month; // month_ mMonth m_month month_m int _day; // day_ mDay m_day day_m }; // 额外说明,成员变量和成员函数定义顺序没有限制,只要在类里面就行,例如 class Date { // 顺序不受语法限制 // 但为了美观易读,习惯于成员变量/成员函数定义在一起,成员变量在成员函数下面 private: int _year; int _month; public: // 成员函数 // ... private: int _day; };

有关struct的特殊说明

  • C++中struct也可以定义类,C++兼容C中struct的⽤法,同时struct升级成了类,明显的变化是struct中可以定义函数,⼀般情况下还是推荐⽤class定义类。

C++兼容了很多C的内容,关键字struct只是其中之一。struct升级成了类就是因为这种兼容,某种程度上可以说,类是兼容结构体的,这是一种包含关系,毕竟结构体有的类都有。那么struct与class在C++中有什么区别吗?为什么要有两个作用基本相同的关键字呢?

出现这种情况主要是历史遗留问题,是C++为了兼容C语言做出的综合考量。两者虽然基本相同,但还是有一些细节区分的,例如:

  • class定义类,类中没有被访问限定符修饰的成员,默认为private。而struct 中默认访问权限为 public

代码示例 1.2.4

// 结构体升级成了类 struct Stack { // 默认访问权限为public // struct中可以有成员函数、访问限定符等 void push(int x) {} void pop() {} void top() {} private: // 成员变量 int* arr; int top; int capacity; }; int main() { // 无需typedef了,struct定义的也是类,类名就是类型名称 Stack st; // 兼容C语言的用法,也可以在名称前加struct作为类型名称 struct Stack st; return 0; } struct ListNode { int val; // 在结构体内部定义该结构体类型的变量 // 1.C语言的写法,struct ListNode才是结构体类型名称 // struct ListNode* next; // 必须加struct,typedef也不行 // 2.C++的写法,类名就是类型名称 ListNode* next; // 这种写法C语言中是报错的 };

3. 类域

我们知道,C语言有全局域与局部域的概念。在初识C++中,我们学习了命名空间,又了解了命名空间域。本篇之后,又有了一个新的概念—类域。

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

和命名空间一样,类天然为它的成员提供了一层命名隔离,即使同时定义了两个类,它们都有一个叫Print的函数用来打印成员变量存储的数据,也不会发生命名冲突。并且类域和命名空间域一样,不影响变量的生命周期。

类的成员函数声明与定义分离

谈到类域,还需要结合类的一个特性说明:

  • 定义在类里面的成员函数默认为内联inline。

编译时编译器会将内联函数在其调用地点展开(类似C语言中的宏,一种文本替换,无需建立函数栈帧),但展开申请对于编译器而言只是一个建议,具体实现机制不同,编译器有不同的处理方式,但通常而言规模较小、流程直接或频繁调用之类的非递归函数都可能被展开。

因此,对于符合展开条件的成员函数,通常在类里面声明并定义,否则统一进行声明与定义分离,即在类内声明函数,在类外定义函数。

代码示例 1.3.1

#include<iostream> class Date { // 成员函数Init、Print和成员变量_year,_month,_day都属于Date类域 public: void Init() { _year = 2025; _month = 1; _day = 1; } // 成员函数Print声明与定义分离 void Print(); // 类中声明Print方法 public: // 成员变量 int _year; // 年 int _month; // 月 int _day; // 日 }; void Date::Print() // 类外定义Print方法,必须使用域操作符::指明所属类域 { std::cout << _year << "年" << _month << "月" << _day << "日" << std::endl; } int main() { Date d; d.Init(); // 将Date类对象d年月日置为2025年1月1日 d.Print(); // 打印日期 return 0; }

程序运行结果:​

  • 类域影响编译的查找规则。不指明函数所属类域,编译器无法识别其为成员函数。不知道函数属于哪个类,它就无法在函数内使用类的成员。若指定类域,函数内部没有找到对应的成员变量,编译器就会到类域中查找。

代码示例 1.3.2

#include<iostream> using namespace std; class Date { public: void Init() { _year = 2025; _month = 1; _day = 1; } void Print(); // Print方法的声明 public: int _year; // 年 int _month; // 月 int _day; // 日 }; // 类外定义Print void Print() { cout << _year << "年" << _month << "月" << _day << "日" << endl; }

运行结果,编译报错:

​

不使用域操作符指定类域,那么类外定义Print就是定义全局函数。Date类中声明的Print方法属于类域,天然有一层命名隔离,类外虽然有重名函数也不构成命名冲突。真正的问题出在成员变量的使用上。我们需要理解编译器的查找规则。

类外定义的Print使用了_year,_month,_day等变量,根据就近原则,编译器先在函数内查找,找不到,未指明类域就只能到全局域查找,也没有,则认为是未知变量。因此出现了E0020和C2065报错(VCR001是一个警告,而非报错,因为Date类中的Print只有声明没有定义)。_year,_month,_day都是Date类的成员变量,因此,必须指明类域,编译器才可以到类域查找,才能在类外定义的成员函数中使用类中的成员。就像陌生人无法使用自己家中的物品一样,指明类域就是给别人上了个身份,说明他是亲戚/朋友,经过了允许,这样就能够使用自己家中的物品——其他成员变量/成员函数。

4. 类的实例化—对象

例如现实中使⽤建筑设计图建造房⼦,类像是一张设计图,规划了不同的房间,每个房间有不同的功能。有了设计图不是创造出了实体建筑,不能住⼈。建筑工人们借助设计图修建出房⼦,有了实体才能住⼈。

同样地,类定义后,不能存储数据,只是一个类型,只有定义类类型的变量后,分配了空间,才能向这个类类型的变量中存储数据。

C++中把这个过程叫作实例化,这个变量称为对象。即:

  • ⽤类类型在物理内存中创建对象的过程,称为类实例化出对象。
  • 类是对象的⼀种抽象描述,是个模型一样的东西,规定了该类对象有哪些成员变量,这些成员变量只是声明,没有分配空间,⽤类实例化出对象时,才会分配空间。
  • 一个类可以实例化出多个对象,实例化出的对象占⽤实际的物理空间,存储类的成员变量。

对于同一个类的不同对象,例如一个Date类可以实例化多个对象d1、d2、d3等,这多个对象中的相同成员变量是不同的实例化,存储不同数据。例如d1与d2,对于相同成员变量_year,d1与d2中的_year是不同的变量,存储不同日期。就像同一个设计图建出来的两栋完全一致的楼房,同样是101号房,住户不同。

代码示例 1.4.1

// 只定义了一个Date类,一个Date类的类型,无法存储数据 class Date { public: void Init(int y, int m, int d) { _year = y; _month = m; _day = d; } private: // 此处不是定义变量,只是声明 int _year; // 年 int _month; // 月 int _day; // 日 }; int main() { // 同一个类可以实例化多个对象 // d1、d2、d3都是不同对象,都有成员变量_year、_month、_day // d1->_year与d2->_year与d3->_year是相同成员变量,但属于不同对象,存储不同数据 Date d1; // 日期1——2021年5月1日 d1.Init(2021, 5, 1); Date d2; // 日期2——2029年4月1日 d2.Init(2029, 4, 1); Date d3; // 日期3——2021年5月1日 d1.Init(2021, 5, 1); return 0; }

5. 对象的大小与计算

对象的大小

我们知道,结构体只包含成员变量,其大小与成员变量有关,而类的主体不仅包括成员变量,还有成员函数,那么,类实例化出的对象其大小如何计算?

需要明确:对象中仅存储成员变量,不存储成员函数。

函数被编译后是一段指令,对象中没有办法存储,这些指令存储在一个单独的区域(代码段)。即使对象中非要存储函数,也只能是函数指针(存放第一句函数指令在代码段的地址)。但成员函数本质上是属于类的共享代码。无论创建多少个对象,这些函数的代码都只需要一份。如果每个对象都保存指向成员函数的指针,会造成大量冗余,例如100个Date类对象就要存储100份相同的函数指针。

而且我们需要进一步明确,不管是类的实例化,还是对象中存储数据,都是在程序执行过程中,但获取可执行程序文件,执行程序前,先要对代码文件进行编译链接。编译链接时,代码文件中的函数调用语句会被翻译为call <函数地址>的汇编指令,编译器需要在链接阶段确定<函数地址>;运行程序时,执行函数调用语句,就是执行汇编指令call <函数地址>,编译器已经确定该函数地址,直接跳转到代码段对应地址处执行一连串函数指令即可。因此,从底层去看,在对象中存储函数指针也是完全没有必要的。

代码示例 1.5.1

// 函数调用的本质是call 地址 #include<iostream> using namespace std; 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 d; // 如下面两条函数调用语句,函数调用都会被翻译成汇编指令call<函数地址> // 编译器在链接阶段就已经确定<函数地址>,程序运行阶段,执行该代码时直接调用即可 d.Init(2024, 3, 31); // call Init成员函数的地址 d.Print(); // call Print成员函数的地址 return 0; }

对象的大小计算

对象不存储成员函数,只存储成员变量,因此其计算和结构体一致。C++规定类的实例化也要符合内存对⻬的规则:

  • 第⼀个成员变量(按代码语句的书写顺序)在偏移量为0的地址处
  • 其他成员变量要对⻬到偏移量为自己的对⻬数的整数倍的地址处
    • 对⻬数 = 编译器的默认对⻬数与该成员变量⼤⼩的较⼩值
    • 不同平台不同编译器的默认对齐数未必一致,以Visual Studio为例,默认对⻬数为8
  • 对象的总⼤⼩为:最⼤对⻬数的整数倍(所有成员变量的对齐数互相比较取最大值,再与默认对⻬数比较,取较⼩值)。
  • 如果嵌套定义了对象的情况下,嵌套的对象对⻬到该对象的最⼤对⻬数的整数倍处,对象的整体⼤⼩就是所有最⼤对⻬数(含嵌套对象的最大对⻬数)的整数倍

代码示例 1.5.2

#include<iostream> using namespace std; // 计算⼀下A/B/C/D实例化的对象是多⼤? // A类 class A { public: void Print() {cout << _ch << endl;} private: char _ch; int _i; }; // B类 class B { public: void Print() {} }; // C类 class C {}; // D类 class D { public: void Print() {cout << _ch << endl;} private: char _ch; int _i; A _a; }; int main() { A a; B b; C c; D d; // 计算a、b、c、d的大小 cout << sizeof(a) << endl; cout << sizeof(b) << endl; cout << sizeof(c) << endl; cout << sizeof(d) << endl; return 0; }

运行结果:

​

计算过程分析:

​

​

为什么类B和类C,一个只有成员函数,一个为空,对象的⼤⼩却都是1呢?

这是考虑到如果⼀个字节都不给,怎么表⽰这个类类型定义的对象存在过呢?所以这⾥给1字节,纯粹是为了占位以标识对象存在。

需要注意,虽然类B只有成员函数,但我们是可以通过它定义的对象b调用它的成员函数的,没有成员变量不代表这个类或对象就没有用了。因此标识对象的存在是有必要的,后面我们会接触到一个叫做仿函数的东西,这里的类B就是一种仿函数。

​

6. this指针

之前的代码示例中,其实我们回避了一个问题,成员函数既然是各个对象所共享的,那是怎么区分哪个对象调用的函数呢?怎么传的参数呢?如下:

代码示例 1.6.1

#include<iostream> using namespace std; class Date { public: void Init(int year, int month, int day) { // 函数内部看起来并未特别区分,如何知道_year/_month/_day属于哪个对象呢? // 如果有多个Date类对象d1、d2,如何区分d1->_year与d2->_year? _year = year; _month = month; _day = day; } void Print() { // 如何区分? cout << _year << "/" << _month << "/" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1; Date d2; // d1,d2共用相同成员函数,Init和Print // 成员函数内部似乎没有特别区分,类外调用函数也没有像C语言一样传对象地址 d1.Init(2024, 3, 31); // 没有像C语言一样传地址/传参,如Init(&d1, 2024, 3, 31); d1.Print(); // 没有Print(&d1); d2.Init(2024, 7, 5); // 没有Init(&d2, 2024, 7, 5); d2.Print(); // 没有Print(&d2); return 0; }

其实,虽然C++不需要像C语言一样主动传地址,但我们也被动完成了类似的动作。C++规定:

  • 编译器编译后,类的成员函数默认都会在第一个形参前,隐式增加⼀个当前类类型的指针,特别称呼为this指针。比如Date类的Init方法的真实原型为void Init(Date* const this, int year, int month, int day)
  • 类的成员函数中访问成员变量,本质都是通过this 指针访问的(Init函数中给_year赋值,this->_year = year;)

代码示例 1.6.2

class Date { public: // 默认在第一个形参前隐式增加,编译器完成,不需要自己写,形参列表也看不见 // 自己类类型的指针,例如Date类的this指针就是Date*,A类的this指针是A* // const修饰是为了确保this指针的指向不能改变,this指针始终指向调用该成员函数的对象 void Init(int year, int month, int day) { _year = year; // 实际上是this->_year = year; _month = month; // 实际上是this->_month = month; _day = day; // 实际上是this->_day = day; // 可以显式的用this指针,上下两种使用方式都可以,上面方便,字少 this->_year = year; this->_month = month; this->_day = day; // 同时注意,this指针的指向无法修改 // 例如将this指针设为空指针 // this = nullptr; // 例如指针运算 // this++; } // 真实原型为void Print(Date* const this) void Print() { // 两种都可以,不管是否显式的写this指针,编译器都是通过this指针访问的成员变量 cout << _year << "/" << _month << "/" << _day << endl; cout << this->_year << "/" << this->_month << "/" << this->_day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1; Date d2; // 其实编译器偷偷的把对象的地址传给了this指针 d1.Init(2024, 3, 31); // 本质和Init(&d1, 2024, 3, 31)类似,只是由编译器完成 d1.Print(); // Print(&d1); d2.Init(2024, 7, 5); // Init(&d2, 2024, 7, 5); d2.Print(); // Print(&d2); return 0; }
  • 不能在实参和形参的位置显式的写this 指针,编译器自行处理,但可以在函数体内显式使⽤this 指针

代码示例 1.6.3

#include<iostream> using namespace std; class Date { public: // 不能在形参位置显式的写this指针 // 例如,void Init(Date* const this, int year, int month, int day) void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } // 不能显式写,void Print(Date* const this) void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1; Date d2; // 通过点操作符.调用成员函数时,操作符前的d1、d2对象被编译器传参传给了隐式的this指针 // 不能在实参位置显式传参给this指针,例如 // d1.Init(&d1, 2024, 3, 31)或Init(&d1, 2024, 3, 31) // 前者是形参过多,点操作符前的d1已经传参给this指针,&d1就是多余形参传给了year // 后者其实是在调用全局函数,而Init在类里,因此会出现函数不存在的情况,没有该全局函数 // 类的成员不能直接访问,必须通过对象或类类型指针间接访问 d1.Init(2024, 3, 31); d1.Print(); d2.Init(2024, 7, 5); d2.Print(); return 0; }

7. 习题

下⾯通过几个选择题测试⼀下前⾯的知识学得如何。

(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; }

(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; }

(3)、this指针存在内存的哪个区域中?()

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

正确答案:(1)C;(2)B;(3)B 。

题(1)解析:

main函数中创建了A类类型的指针变量p,并赋值为空指针。这两题的关键都在于p->Print()这句代码。这里容易错误理解p->Print()是解引用,但这里的p指针其实只是起到了一个域操作符::的作用,指明类域调用成员函数。我们知道,函数调用语句会被翻译成汇编代码call <函数地址>,而<函数地址>是成员函数的地址,与p指针无关,p指针是对象的地址,p指针为空只能说明没有创建对象。对象是否存在只与成员变量有关,与成员函数无关,函数地址在程序运行前就确定了,因此可以正常运行程序。第一题选C。

题(2)解析:

Print函数内增加代码cout << _a << endl;。我们知道,类的成员函数内访问成员变量,本质上是通过this指针访问的,该代码本质上是cout << this->_a << endl;。this指针指向的是对象的地址,而p是空指针,也就是对象为空,对象的地址为空,因此this指针为空,this->_a相当于nullptr->_a,这时就不是函数调用了,而是解引用。解引用空指针,程序运行崩溃。第二题选B。

题(3)解析:

this指针虽然比较特殊,但它仍是函数形参。函数被调用,就会在栈上创建栈帧,函数形参随栈帧一起创建,因此this指针应该存在栈上。第三题选B。

二、C++和C语⾔实现Stack对⽐

C++是一门面向对象的语言。⾯向对象有三⼤特性:封装、继承、多态。通过下⾯的对⽐,我们可以浅浅了解⼀下封装。

通过下⾯两份代码对⽐,我们会发现C++实现Stack更多是在形式上发⽣了较多变化,底层和逻辑上没啥变化。⽐如:

  • 如Init,可以通过缺省参数设置默认值,进行初始化等操作,函数的定义更方便。
  • 调用成员函数由编译器通过this指针获取对象地址。
  • 定义类不需要typedef,类名即类型名称。
  • 有了命名空间域与类域,命名更加系统、规范和简洁了,不需要再像C语言中一样,为了避免命名冲突将命名复杂化。
  • C++中数据和函数都放到了类⾥⾯,通过访问限定符进⾏了限制,不能通过对象随意修改数据,这是C++封装的⼀种体现,这个是最重要的变化。这⾥的封装本质是⼀种更严格规范的管理,避免出现乱访问修改的问题。当然封装不仅如此。

在C++⼊⻔阶段实现的Stack看起来比C语言写的变了很多,但其实实质变化不⼤。等到 STL 中⽤适配器实现的Stack,⼤家再感受C++的魅⼒。

代码示例 2.1

// C语言实现Stack #include<stdio.h> #include<stdlib.h> #include<stdbool.h> #include<assert.h> typedef int STDataType; // 重命名,方便随手修改栈中的数据类型 typedef struct Stack // 定义一个结构体类型代表栈类型 { STDataType* data; // 数组,存储栈中数据 int top; // top为栈顶,即数组末尾元素的下一个位置 int capacity; // 栈的空间容量,记录栈最多可存储的元素个数 }ST; // typedef重命名结构体类型struct Stack为ST // 栈的初始化方法 void STInit(ST* ps) // 形参ps接收栈变量的地址 { assert(ps); // 检查ps指针是否为空,为空会报错强制中断程序 ps->data = NULL; ps->top = 0; ps->capacity = 0; } // 栈的销毁方法 void STDestroy(ST* ps) { assert(ps); // 动态申请的内存需要手动释放,free数据空间 free(ps->data); ps->data = NULL; ps->top = ps->capacity = 0; } // 压栈方法 void STPush(ST* ps, STDataType x) { assert(ps); // 插入数据前判断是否满栈 if (ps->top == ps->capacity) { // capacity为0,新空间大小newcapacity置为4,否则2倍扩容 int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2; // 动态申请内存空间 STDataType* tmp = (STDataType*)realloc(ps->data, newcapacity * sizeof(STDataType)); // 判断是否成功扩容 if (tmp == NULL) { // 开辟失败,报错提示并返回 perror("realloc fail"); return; } // 开辟成功,更新数据空间及其大小 ps->data = tmp; ps->capacity = newcapacity; } // 未满栈或扩容成功,数据入栈,同时更新栈顶top ps->data[ps->top++] = x; } // 判断空栈方法 bool STEmpty(ST* ps) { assert(ps); return ps->top == 0; // 栈顶为0说明栈中没有元素,空栈返回true,否则false } // 出栈方法 void STPop(ST* ps) { assert(ps); // 判断是否空栈,空栈无法出栈 assert(!STEmpty(ps)); // 出栈 ps->top--; } // 获取栈顶数据方法 STDataType STTop(ST* ps) { assert(ps); assert(!STEmpty(ps)); // 返回栈顶数据,top为末尾元素的下一位置,top-1即末尾元素 return ps->data[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); // 销毁栈s,释放内存空间 return 0; }

代码示例 2.2

// C++实现Stack #include<iostream> #include<assert.h> using namespace std; typedef int STDataType; // typedef重命名,方便修改栈中数据的类型 // Stack类,方法封装到类中,通过访问限定符限制成员的访问 class Stack { public: // public访问限定符,使成员函数可在类外访问 // 初始化方法 void Init(int n = 4) // 缺省参数,若不传参,n默认为4,即栈的默认大小 { // 由于this指针,无需传地址,无需指针接收地址 // 初始化数据空间 _data = (STDataType*)malloc(sizeof(STDataType) * n); // 判断是否成功开辟空间 if (nullptr == _data) { perror("malloc申请空间失败"); return; } // 成功开辟空间,更新空间大小和栈顶 _capacity = n; _top = 0; } // 压栈方法 void Push(STDataType x) { // 判断是否需要扩容 if (_top == _capacity) { // 直接两倍扩容 int newcapacity = _capacity * 2; STDataType* tmp = (STDataType*)realloc(_data, newcapacity * sizeof(STDataType)); // 判断是否扩容成功 if (tmp == NULL) { perror("realloc fail"); return; } // 扩容成功,更新栈空间与空间大小 _data = tmp; _capacity = newcapacity; } // 压栈,更新栈顶 _data[_top++] = x; } // 判断空栈方法 bool Empty() { return _top == 0; } // 出栈方法 void Pop() { // 隐式this指针调用,this->Empty() assert(!Empty()); // 出栈 --_top; } // 获取栈顶元素方法 int Top() { assert(!Empty()); // 返回栈顶元素 return _data[_top - 1]; } // 销毁栈方法 void Destroy() { free(_data); _data = nullptr; _top = _capacity = 0; } private: // 访问限定符限制成员变量为私有,类外无法访问,保护栈中数据 STDataType* _data; size_t _capacity; size_t _top; }; int main() { Stack s; // 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; }

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

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

立即咨询