一、类的定义1.1、类定义格式class 为定义类的关键字,Stack 为类的名字,{} 中为类的主体,注意类定义结束时后面分号不能省略。类体中内容称为类的成员:类中的变量称为类的属性或成员变量;类中的函数称为类的方法或者成员函数。为了区分成员变量,一般习惯上成员变量会加一个特殊标识,如成员变量前面或者后面加_或者 m 开头,注意 C++ 中这个并不是强制的,只是一些惯例,具体看公司的要求。C++ 中 struct 也可以定义类,C++ 兼容 C 中 struct 的用法,同时 struct 升级成了类,明显的变化是 struct 中可以定义函数,一般情况下我们还是推荐用 class 定义类。定义在类面的成员函数默认为 inline。1.2、访问限定符C++ 一种实现封装的方式,用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。public 修饰的成员在类外可以直接被访问;protected 和 private 修饰的成员在类外不能直接被访问,protected 和 private 是一样的,以后继承章节才能体现出他们的区别。访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,如果后面没有访问限定符,作用域就到} 即类结束。class 定义成员没有被访问限定符修饰时默认为 private,struct 默认为 public。一般成员变量都会被限制为 private/protected,需要给别人使用的成员函数会放为public。下面我们来看一段代码帮助我们理解上述的俩点:

代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS 1

//C++面向对象的三大特性:封装、继承、多态

//在类和对象这一块我们主要看封装

//在C语言中体现出来的就是:数据和方法是分离的

// 封装的本质体现了更严格的规范管理

//C++封装特点:

//1) 数据和方法封装放到了一起,都在类里面

//

class Stack//class下没给的成员默认私有

{

//从第一个访问限定符到下一个出现为止全都是public

//如果没有下一个访问限定符就到作用域结束为止

public:

//成员函数

void Init()

{

a = nullptr;

top = 0;

capacity = 0;

}

void Push(int x)

{

}

private:

//成员变量

int* a;

int top;

int capacity;

};

//兼容C struct用法

typedef struct A

{

void func();

int a1;

int a2;

}AA;

//升级成了类

//struct默认是公有的

struct B

{

void Init()

{

}

private:

int b1;

int b2;

};

struct ListNode

{

int val;

//C语言中需要加上struct

//struct ListNode* next;

//C++中可以直接定义

ListNode* next;

};

int main()

{

struct A aa1;

AA a2;

B bb1;

bb1.Init();

Stack s1;///类的变量

Stack s2;

//公有的可以直接访问

s1.Init();

s1.Push(1);

//私有的不可以直接访问

//s1.top++

return 0;

}1.3、类域

类定义了一个新的作用域,类的所有成员都在类的作用域中,在类体外定义成员时,需要使用::作用域操作符指明成员属于哪个类域。类域影响的是编译的查找规则,下面程序中 Init 如果不指定类域 Stack,那么编译器就把 Init 当成全局函数,那么编译时,找不到 array 等成员的声明 / 定义在哪里,就会报错。指定类域 Stack,就是知道 Init 是成员函数,当前域找不到的 array 等成员,就会到类域中去查找。代码语言:javascript复制#include

using namespace std;

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;

}二、实例化2.1、实例化概念

用类类型在物理内存中创建对象的过程,称为类实例化出对象。类是对象进行一种抽象描述,是一个模型一样的东西,限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间,用类实例化出对象时,才会分配空间。一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量。打个比方:类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,设计图规划了有多少个房间,房间大小功能等,但是并没有实体的建筑存在,也不能住人,用设计图修建出房子,房子才能住人。同样类就像设计图一样,不能存储数据,实例化出的对象分配物理内存存储数据。代码语言:javascript复制#include

using namespace std;

class Stack

{

public:

//成员函数

void Init(int n = 4)

{

}

private:

//成员变量,声明

//这里并没有开辟空间

int* array;

size_t capacity;

size_t top;

};

int main()

{

//定义,类实例化对象

Stack s1;

s1.Init();

Stack s2;

s2.Init(100);

//sizeof是在编译时运算的可以传对象或者类型

cout << sizeof(s1) << endl;

cout << sizeof(Stack) << endl;

return 0;

}2.2、对象大小 分析一下类对象中哪些成员呢?类实例化出的每个对象,都有独立的数据空间,所以对象中肯定包含成员变量,那么成员函数是否包含呢?首先函数被编译后是一段指令,对象中没办法存储,这些指令存储在一个单独的区域 (代码段),那么对象中非要存储的话,只能是成员函数的指针。再分析一下,对象中是否有存储指针的必要呢,Date 实例化 d1 和 d2 两个对象,d1 和 d2 都有各自独立的成员变量_year/_month/_day 存储各自的数据,但是 d1 和 d2 的成员函数 Init/Print 指针却是一样的,存储在对象中就浪费了。如果用 Date 实例化 100 个对象,那么成员函数指针就重复存储 100 次,太浪费了。这里需要再额外哆嗦一下,其实函数指针是不需要存储的,函数指针是一个地址,调用函数被编译成汇编指令 [call 地址],其实编译器在编译链接时,就要找到函数的地址,不是在运行时找,只有动态多态是在运行时找,就需要存储函数地址,这个我们以后会讲解。

上面我们分析了对象中只存储成员变量,C++规定类实例化的对象也要符合内存对齐的规则。

内存对齐规则:

第一个成员在与结构体偏移量为 0 的地址处。其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。VS 中默认的对齐数为 8结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。代码语言:javascript复制#include

using namespace std;

// 计算⼀下A/B/C实例化的对象是多⼤?

class A

{

public:

void Print()

{

cout << _ch << endl;

}

private:

char _ch;

int _i;

};

class B

{

public:

void Print()

{

//...

}

};

class C

{

};

int main()

{

cout << sizeof(A) << endl;

//开1byte为了占位,不存储实际数据,表示对象存在过

cout << sizeof(B) << endl;

cout << sizeof(C) << endl;

B b1;

B b2;

cout << &b1 << endl;

cout << &b2 << endl;

return 0;

}上面的程序运行后,我们看到没有成员变量的B和C类对象的大小是1,为什么没有成员变量还要给1个字节呢?因为如果一个字节都不给,怎么表示对象存在过呢!所以这里给1字节,纯粹是为了占位标识对象存在。

三、this指针3.1、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 指针。代码语言:javascript复制#include

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)

{

// 编译报错:error C2106: “=”: 左操作数必须为左值

// cout << this << endl

// const保护this不能被修改

// this = nullptr;

// this->_year = year;

///这里this可以写可以不写,例如第一个就没写,后俩个就写了

_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和d2

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.2、习题3.2.1、一下面程序编译运行结果是()

A、编译报错 B、运行崩溃 C、正常运行

代码语言:javascript复制#include

using namespace std;

class A

{

public:

void Print()

{

cout << "A::Print()" << endl;

}

private:

int _a;

};

int main()

{

//空指针、野指针是运行报错不会编译报错(最多给个警告)

A* p = nullptr;

p->Print();

(*p).Print();

//不要被语法表象迷惑

return 0;

} 解析:

这段代码主要展示了 C++ 中空指针调用类成员函数的特殊情况,核心内容总结如下:

代码结构:

定义了类A,包含公有成员函数Print()(功能是输出"A::Print()")和私有成员变量_a。main函数中,定义了类A的空指针p(用nullptr初始化),并通过p->Print()调用成员函数。 关键现象与原理:

编译通过:尽管p是指向A的空指针,但p->Print()的编译不会报错。运行结果:程序能正常输出"A::Print()"(未崩溃)。原因:Print()是类的非虚成员函数,其调用不依赖对象的具体内存(无需访问this指针指向的成员变量_a)。编译器通过p的类型(A*)确定调用A::Print(),执行时无需有效对象即可完成输出。 隐含知识点:

空指针(nullptr)调用成员函数时,若函数未访问任何成员变量(即不使用this指针),程序可正常运行;若访问成员变量(如cout << _a),则会触发运行时错误(访问非法内存)。空指针 / 野指针的错误属于运行时错误,编译阶段通常不会报错(可能仅提示警告)。3.2.2、二下面程序编译运行结果是()

A、编译报错 B、运行崩溃 C、正常运行

代码语言:javascript复制//成员函数的指针不存在对象

//成员变量是存在对象里面的,所以就会有解引用

#include

using namespace std;

class A

{

public:

void Print()

{

cout << "A::Print()" << endl;

cout << _a << endl;

//在成员的前面都会加this

//运行时就要到this指向的对象里面去找_a

//但是_a是空指针,所以就会报错

}

private:

int _a;

};

int main()

{

A* p = nullptr;

p->Print();

return 0;

} 解析:

看懂的核心是:p->Print() 本质不是 “空指针解引用”,而是 “通过指针类型找到成员函数” —— 关键在于「非虚成员函数的调用逻辑」和「this指针是否被实际使用」。

我们用通俗的语言拆解,结合之前的代码(类 A 的Print()无成员变量访问):

第一步:先明确两个关键前提

类的非虚成员函数存在哪里?非虚成员函数(比如Print())是「属于类的」,不是属于某个具体对象的 —— 它在编译时就被编译成独立的代码块,存放在程序的代码段(固定地址),不会因为创建对象而复制多份。简单说:不管你创建 1 个 A 对象,还是 100 个,Print()的代码只有一份,和对象内存无关。

p->Print() 编译器是怎么处理的?当你写 p->Print() 时,编译器不会直接 “解引用 p” 去拿函数(因为函数不在对象里),而是做了两件事:

看 p 的类型:p 是 A* 类型,所以编译器知道要调用「A 类的 Print () 函数」(直接找到代码段里的 Print () 地址);隐式传递 this 指针:把 p 的值(这里是 nullptr)作为 this 指针,传给 Print() 函数(所有非静态成员函数都隐藏一个this参数,指向调用它的对象)。 也就是说,p->Print() 等价于:A::Print(p);(编译器层面的转换)。

第二步:为什么 “空指针” 没崩溃?

崩溃的本质是「访问了非法内存」。而这里的Print()函数,没有做任何需要访问「对象内存」的操作 —— 它没有读取 / 修改成员变量(比如_a)。

若Print()访问成员变量(比如cout << _a;):此时_a等价于this->_a,而this是 nullptr(空指针),访问nullptr->_a就是「解引用空指针访问内存」,必然崩溃。

若Print()不访问成员变量:函数执行时不需要用到this指针指向的对象内存,哪怕this是 nullptr,也只是一个 “没用上的参数”—— 函数只是执行自己的代码(输出字符串),自然不会崩溃。

一句话总结核心区别

「空指针解引用」= 用空指针去访问它指向的内存(比如p->_a,本质是*(p + 偏移量));「空指针调用非虚成员函数」= 通过指针类型找到类的函数代码,再把空指针传给this—— 只要函数不用this(不访问成员变量),就不会触发解引用,自然不崩溃。代码语言:javascript复制class A {

public:

void Print1() {

cout << "A::Print1()" << endl; // 不访问成员变量,this没用上

}

void Print2() {

cout << _a << endl; // 访问成员变量,等价于 this->_a

}

private:

int _a;

};

int main() {

A* p = nullptr;

p->Print1(); // 正常运行!没有解引用,只是调用Print1()代码

p->Print2(); // 崩溃!试图解引用nullptr访问 _a(this->_a)

return 0;

} 在 C++ 中,成员函数(如Print())属于类,而非具体对象,这一特性可从内存存储、调用机制两个维度理解:

一、内存存储:成员函数在 “代码段” 共享

成员函数的代码存储在程序的代码段(文本段)中,是类的 “共享资源”。无论创建多少个类的对象,成员函数的代码只有一份副本,不会因对象数量增加而复制多份。与之对比,对象的成员变量存储在堆或栈中(每个对象有独立的成员变量副本),而成员函数与对象的内存空间完全隔离。 二、调用机制:通过 “类型” 而非 “对象” 定位函数

当通过对象指针(如A* p)调用成员函数(如p->Print())时,编译器并非 “解引用指针访问对象内的函数”,而是:

根据指针的类型(如A*),直接定位到类A的Print()函数在代码段中的地址;隐式传递this指针(指向调用函数的对象),使函数能访问对象的成员变量(若函数中使用了的话)。 这也解释了 “空指针调用无成员变量访问的成员函数不会崩溃” 的现象 —— 函数调用仅依赖 “类的类型信息”,而非 “对象的有效内存”。

三、总结

成员函数 “属于类” 的本质是:代码共享于代码段,调用时通过类的类型定位,与具体对象的内存无直接绑定。这一设计既保证了函数调用的效率,又避免了因对象数量过多导致的代码冗余。

3.2.3、三this指针存在内存哪个区域的()

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

解析:

在 C++ 中,this指针是成员函数的隐含参数,当调用成员函数时,this指针会被压入栈中。函数执行完毕后,this指针随栈帧的销毁而释放,因此this指针存储在栈区域。

四、C++和C语言实现Stack对比 面向对象三大特性:封装、继承、多态,下面的对比我们可以初步了解一下封装。通过下面两份代码对比,我们发现 C++ 实现 Stack 形态上还是发生了挺多的变化,底层和逻辑上没啥变化。

C++ 中数据和函数都放到了类里面,通过访问限定符进行了限制,不能再随意通过对象直接修改数据,这是 C++ 封装的一种体现,这个是最重要的变化。这里的封装的本质是一种更严格规范的管理,避免出现乱访问修改的问题。当然封装不仅仅是这样的,我们后面还需要不断的去学习。C++ 中有一些相对方便的语法,比如 Init 给的缺省参数会方便很多,成员函数每次不需要传对象地址,因为 this 指针隐含的传递了,方便了很多,使用类型不再需要 typedef 用类名就很方便在我们这个 C++ 入门阶段实现的 Stack 看起来变了很多,但是实质上变化不大。等着我们后面看 STL 中的用适配器实现的 Stack,大家再感受 C++ 的魅力。4.1、C语言实现Stack代码代码语言:javascript复制#include

#include

#include

#include

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;

}4.2、C++实现Stack代码代码语言:javascript复制#include

#include

using namespace std;

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;

}