--上述要点会在接下来的例子中给大家详细介绍

// C++中数据和方法封装放到了一起,都在类里面
// 封装的本质体现了更严格的规范管理
//我们直接大概定义一个看一看
#include<iostream>
using namespace std;
class Stack
{
//访问限定符,class中没给的话里面默认是私有
public://公有,一般是把成员函数公有,在类外也可以直接访问
//成员函数
void Init(int capacity = 4)//可以不用像之前一样前面加个类似ST的区分
{
_arr = nullptr;//这里实际上是要malloc的
_top = 0;
_capacity = 0;
}
void Push(int x)
{
}
//public这个访问限定符到这里结束
private://私有,一般是把成员变量私有,在类外不能直接访问,更规范。
//成员变量
int* _arr;
int _top;
int _capacity;
};//分号不能掉
//private后面没有访问限定符了,到}这里结束
//以前C语言中只能向上找,类中向上向下都可以。
//所以我们前面先实现公有的成员函数,再定义的私有里面的成员变量也是可以的
int main()
{
Stack s1;
s1.Init();
//像之前C语言中这样不规范的写发,我们通过C++的访问限定符规范了
//s1.top++;//私有成员,类外无法直接访问
return 0;
}#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();
}一般情况下我们更喜欢用class,但是像链表定义节点这种还是比较喜欢用struct的(默认公有):
//一般情况下我们更喜欢用class,但是像链表定义节点这种还是比较喜欢用struct的(默认公有)
struct ListNode
{
//访问限定符,struct中没给的话里面默认是公有
public://所以这里其实给不给这个公有都行
int data;
//struct ListNode* next;//不需要这样写了
ListNode* next;
};举例说明(注释):
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;
}
#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;
}分析: 类实例化出的每个对象,都有独立的数据空间,所以对象中肯定包含成员变量,那么成员函数是否包含呢?首先函数被编译后是⼀段指令,对象中没办法存储,这些指令存储在⼀个单独的区域(代码段),那么对象中非要存储的话,只能是成员函数的指针。再分析⼀下,对象中是否有存储指针的必要呢,Date实例化d1和d2两个对象,d1和d2都有各自独立的成员变量_year/_month/_day存储各自的数据,但是d1和d2的成员函数Init/Print指针却是⼀样的,存储在对象中就浪费了。如果用Date实例化100个对象,那么成员函数指针就重复存储100次,太浪费了。这里需要再额外哆嗦⼀下,其实函数指针是不需要存储的,函数指针是⼀个地址,调用函数被编译成汇编指令[call 地址], 其实编译器在编译链接时,就要找到函数的地址,不是在运行时找,只有动态多态是在运行时找,就需要存储函数地址,这个我们以后会讲解

#include<iostream>
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()
{
A a;
B b;
C c;
cout << sizeof(a) << endl;
//a的这个不用多说,根据内存对齐规则就可以算出来
//因为函数不存在对象里,所以b,c都是空类
//空类的大小为1,开1byte是为例占位,不存在实际数据,只是为了表示对象存在过
cout << sizeof(b) << endl;
cout << sizeof(c) << endl;
//大家可以想一下不开空间怎么表示对象存在过呢,不开地址怎么给呢?
//所以肯定是会给的
return 0;
}#include<iostream>
using namespace std;
class Date
{
public:
//成员函数
//所以这里应该是这样的
//void Init(Data* const this,int year,int month,int day)--也不能这样写出来
void Init(int year,int month,int day)
{
//但是这里的this是可以写的
this->_year = year;
_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和Print函数时,
//该函数是如何知道应该访问的是d1对象还是d2对象呢?
//其实这里就涉及到了this指针,它在函数实参和形参的位置是不可以显式出现的
//但是在函数体内可以出现
//d1.Init(&d1,2025,7,31);--但是这里不能这样写出来
d1.Init(2025, 7, 31);
//d2.Init(&d2,2025,9,1);--也不能写出来
d2.Init(2025, 7, 9);
//和Init同理
d1.Print();
d2.Print();
return 0;
}题目1:下面程序编译运行结果是(C),修改后的结果是(C)
A、编译报错 B、运行崩溃 C、正常运行
//题目1--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 地址
//因为这里的函数地址并不存在对象之中(而是直接通过地址调用的),this传过去也没影响
//所以这里不存在空指针的解引用
//如果改成这样呢
(*p).Print();
//也是一样的可以正常运行,这里转成反汇编看比较好,实际上是直接调用函数的
//注意:不要只看语法的表象。
return 0;
}我们在代码的注释中对题目进行了解释和分析,这题大部分人看到空指针可能都会直接选择B也就是认为这里是空指针的解引用,但大家结合我们上面学习的知识就会发现,这里调用函数(成员函数是不存于对象之中的),函数体内也没有成员变量,所以没有问题。

都可以正常运行,退出码为0
题目2:下面程序编译运行结果是(B)
A、编译报错 B、运行崩溃 C、正常运行
//题目2--B.运行崩溃
#include<iostream>
using namespace std;
class A
{
public:
void Print()
{
cout << this << endl;
cout << "A::Print()" << endl;
cout << _a << endl;//调用函数执行到这里时就有问题了
//_a是成员变量,是存在对象里的,所以会出现问题。
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}这题会出现运行崩溃的问题,是因为在函数体内存中成员变量,这个就会受到空指针的影响了

运行错误,退出码不为0
题目3:this指针存在内存哪个区域的 (A)
A. 栈 B.堆 C.静态区 D.常量区 E.对象里面
this是存在栈里面的,就像形参一样,类似于局部变量。但是this也是很有可能存在寄存器里的 ,所以这里这题其实是不太严谨的
面向对象三大特性:封装、继承、多态,下面的对比我们可以初步了解⼀下封装。
通过下面两份代码对比,我们发现C++实现Stack形态上还是发生了挺多的变化,但是在底层和逻辑上没啥变化。
//C实现Stack
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
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
#include<iostream>
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;
}完整源代码:
往期回顾:
《C++进阶:引用补充、内联函数与nullptr 核心用法》
总结:这篇博客到这里就结束了,接下来会进入类和对象(中)的学习,类和对象是很重要的,我们在LeetCode等刷题软件都可以看到类和对象的存在,所以大家一定要琢磨透,不然其它内容的学习都会变得比较吃力,如果文章对你有帮助的话,欢迎评论,点赞,收藏加关注,感谢大家的支持。