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

上面我们分析了对象中只存储成员变量,C++规定类实例化的对象也要符合内存对齐的规则。
内存对齐规则:
#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()
{
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字节,纯粹是为了占位标识对象存在。
void Init(Date* const this, int year, int month, int day)this->_year = year;#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)
{
// 编译报错: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;
}下面通过两个选择题测试⼀下前面的知识学得如何?
下面程序编译运行结果是()
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();
(*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),则会触发运行时错误(访问非法内存)。下面程序编译运行结果是()
A、编译报错 B、运行崩溃 C、正常运行
//成员函数的指针不存在对象
//成员变量是存在对象里面的,所以就会有解引用
#include<iostream>
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(不访问成员变量),就不会触发解引用,自然不崩溃。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指针(指向调用函数的对象),使函数能访问对象的成员变量(若函数中使用了的话)。这也解释了 “空指针调用无成员变量访问的成员函数不会崩溃” 的现象 —— 函数调用仅依赖 “类的类型信息”,而非 “对象的有效内存”。 三、总结 成员函数 “属于类” 的本质是:代码共享于代码段,调用时通过类的类型定位,与具体对象的内存无直接绑定。这一设计既保证了函数调用的效率,又避免了因对象数量过多导致的代码冗余。
this指针存在内存哪个区域的()
A、栈 B、堆 C、静态区 D、常量区 E、对象里面
解析: 在 C++ 中,
this指针是成员函数的隐含参数,当调用成员函数时,this指针会被压入栈中。函数执行完毕后,this指针随栈帧的销毁而释放,因此this指针存储在栈区域。
面向对象三大特性:封装、继承、多态,下面的对比我们可以初步了解一下封装。通过下面两份代码对比,我们发现 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;
}#include<iostream>
#include<assert.h>
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;
}