
🔥个人主页:艾莉丝努力练剑 ❄专栏传送门:《C语言》、《数据结构与算法》、C语言刷题12天IO强训、LeetCode代码强化刷题、C/C++干货分享&学习过程记录 🍉学习方向:C/C++方向 ⭐️人生格言:为天地立心,为生民立命,为往圣继绝学,为万世开太平
前言:本专栏记录了博主C++从初阶到高阶完整的学习历程,会发布一些博主学习的感悟、碰到的问题、重要的知识点,和大家一起探索C++这门程序语言的奥秘。这个专栏将记录博主C++语法、高阶数据结构、STL的学习过程,正所谓“万丈高楼平地起”,我们话不多说,继续进行C++阶段的学习。上篇文章我们介绍了类的定义等要点,本文我们则来介绍对象。
C++的两个参考文档:
老朋友(非官方文档):cplusplus 官方文档(同步更新):cppreference
用类类型在物理内存空间中创建对象的过程就叫做类实例化出对象;
类是对象进行一种抽象描述,像是个模型似的东西,来限定类有哪些成员变量。
注:这些成员变量仅仅只是声明,并没有实际分配空间,只有在类实例化出对象时才会分配空间。
一个类可以实例化出多个对象,实例化出多个对象占用实际的物理空间,来存储类成员变量。
可以这样理解:
类实例化出对象很像是我们在现实中利用建筑设计图来建造房子,这个类就类似于设计图,它规划了我们要造的这个房子会有多少个房间,这些房间有多大、都具备什么功能等等,但是这个阶段还只是纸上谈兵,还停留在纸面上,并没有把实体的房子建筑建造出来,人又不是纸片人,设计图又不能住人,也就没有实际的物理空间,只有当我们使用设计图把房子造好了才可以住人。 同理,类就像设计图一样,不能存储数据,只有当它实例化出对象分配了物理空间之后才能存储数据(房子实际存在了,才能住人)。 如下图所示——

#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.top = 0;
s1.Init();
Stack s2;
s2.top = 1;
s2.Init(100);
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 地址],其实编译器在编译链接时,就要找到函数的地址,不是在运行时找,只有动态多态是在运行时找,就需要存储函数地址,多态我们之前也说过,之后会详细介绍。

成员函数的指针没存进对象,只考虑成员变量,不考虑成员函数;成员函数不在对象里面。
下面是计算对象大小的程序的演示:
#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()
{
cout << sizeof(A) << endl;
//开1个byte的空间是为了占位,占位不存储实际数据,表达对象存在过(占位标识对象存在)
cout << sizeof(B) << endl;
cout << sizeof(C) << endl;
B b1;
B b2;
cout << &b1 << endl;
cout << &b2 << endl;
return 0;
}
s1和s2成员函数调用的都是同一个(地址都一样),成员函数都一样,每个对象存成员函数是个浪费(编译链接时地址就有了——转换成call 地址),函数指针不会存。
如下图所示——

运行结果如下——

只开1个byte的空间是为了占位,占位不存储实际数据,表达对象存在过(占位标识对象存在) 。
我们之前在C语言的结构体部分介绍过内存对齐的这一规则,链接如下——
【自定义类型:结构体】:类型声明、结构体变量的创建与初始化、内存对齐、传参、位段
这里博主截了一下这篇博客的目录和结构体对齐原则的概念——


上面我们分析了对象中只存储成员变量,C++规定类实例化的对象也要符合内存对齐的规则。
1、第一个成员在与结构体偏移量为0的地址处; 2、其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处; 3、注意:对齐数 = 编译器默认的⼀个对齐数与该成员大小的较小值; 4、VS中默认的对齐数为8; 5、结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍; 6、如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
具体样例参考【对象大小】中的【为什么只开1个byte的空间】。
程序运行后,我们看到没有成员变量的B和C类对象的大小是1,为什么没有成员变量还要给1个字节呢?因为如果一个字节都不给,如何表示对象存在过呢!因此这里给1字节,纯粹是为了占位标识对象存在,就是标识一下。
this指针是隐含的(注意,不能显式地写出来)。

class Date
{
public:
//void Init(int* const year, int month, int day)
void Init(int year, int month, int day)
{
/*cout << this << endl;*/
//const保护this不能修改
//this = nullptr;
//this->_year = year;
this->_year = year;
this->_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.Init(&d1, 2025, 7, 31);
d1.Init(2025, 7, 31);
//d2.Init(&d2, 2025, 7, 31);
d2.Init(2025, 7, 31);
d1.Print();
d2.Print();
return 0;
}
d1和d2成员函数地址都一样,那怎么区分d1和d2?C++看上去是不用传地址的——区分访问的是谁,其实地址还是要传的,只不过这个工作不用我们做,编译器会做的。
不要和前面const相关的权限放大缩小混到一起去。
我们通过三道选择题来测试一下前面的知识掌握得如何——

大家做对了吗?
第二题解析(结合汇编):

面向对象的三大特性:封装、继承、多态。
三大特性之中,我们现在只是了解了一下封装,剩下两个后面再介绍。

现在的C++虽然也比C语言方便了一些,但也没方便多少,还没有什么实质性的变化,等我们继续深入地学习下去,到STL(C++的标准库)学了之后,我们会明显感觉到C++更方便。
这两个代码的实现都比较长。
接下来,我们就通过对比下面两份代码,感受C++实现Stack形态上发生的变化——
#define _CRT_SECURE_NO_WARNINGS 1
#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;
}运行一下,结果如下所示——

#define _CRT_SECURE_NO_WARNINGS 1
#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;
}运行一下,结果如下——

通过上面的C/C++这两份代码之间的对比,我们不难看出:C++实现Stack在形态上还是发生了挺多的变化,底层和逻辑上没有什么变化。
#define _CRT_SECURE_NO_WARNINGS 1
#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.top = 0;
s1.Init();
Stack s2;
s2.top = 1;
s2.Init(100);
cout << sizeof(s1) << endl;
cout << sizeof(Stack) << endl;
return 0;
}
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;
//开1个byte的空间是为了占位,占位不存储实际数据,表达对象存在过(占位标识对象存在)
cout << sizeof(B) << endl;
cout << sizeof(C) << endl;
B b1;
B b2;
cout << &b1 << endl;
cout << &b2 << endl;
return 0;
}
class Date
{
public:
//void Init(int* const year, int month, int day)
void Init(int year, int month, int day)
{
/*cout << this << endl;*/
//const保护this不能修改
//this = nullptr;
//this->_year = year;
this->_year = year;
this->_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.Init(&d1, 2025, 7, 31);
d1.Init(2025, 7, 31);
//d2.Init(&d2, 2025, 7, 31);
d2.Init(2025, 7, 31);
d1.Print();
d2.Print();
return 0;
}
class A
{
public:
void Print()
{
cout << this << endl;
cout << "A::Print()" << endl;
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
//p->Print(); // call 函数地址
(*p).Print();
// 00C3210C mov ecx,dword ptr[p]
// 00C3210F call A::Print(0C31447h)
return 0;
}【C/C++】类和对象(上):(一)类和结构体,命名规范——两大规范,新的作用域——类域
【C/C++】初识C++(三):C++入门内容收尾——const引用,指针和引用关系梳理,inline(内联函数),nullptr替代NULL
【C/C++】初识C++(二):深入详解缺省参数(默认参数)函数重载、引用(重头戏)
【C/C++】初识C++(一):C++历史的简单回顾+命名空间、流插入、命名空间的指定访问、展开问题等概念整理
结语:本文内容到这里就全部结束了, 本文我们在上一篇文章的基础上,继续学习了实例化——类实例化出对象,对象大小,this指针,对比C++/C两种语言实现Stack等知识点,从现在一直到学习到模版初阶学完之后,都是些晦涩的概念,还用不起来,到后面我们就能像之前那样,结合起来介绍。