前引: 在C++的面向对象编程中,对象模型是理解语言行为的核心。无论是类的成员函数如何访问数据,还是资源管理如何自动化,其底层机制均围绕两个关键概念展开:this指针与六大默认成员函数。它们如同对象的“隐形守护者”,默默支撑着代码的健壮性与效率。本文将从技术底层出发,结合内存布局、编译器行为与实际案例,深入探讨:
(1)this指针如何解决成员函数与数据成员的绑定难题,并支持链式调用与多态
(2)六大默认成员函数(构造、析构、拷贝/移动构造与赋值)如何实现资源的自动化管理
(3)它们如何协同工作,避免内存泄漏、悬空指针等常见陷阱。
无论你是刚接触C++的新手,还是希望优化代码性能的资深开发者,本文将为你揭开对象模型的神秘面纱,助你写出更安全、高效的C++代码,正文开始~
目录
this指针
在C++中,this指针是类的非静态成员函数中隐藏的指针,用于指向调用该函数的对象实例
类型
this 指针由编译器自动生成,无需显示声明,类型为 ClassName * const (指向当前类的常量指针),即成员函数中不能给 this 指针赋值
作用域
只可以在成员函数内使用,例如:
指针的传递
this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
指针的本质
this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给 this 形参。所以对象中不存储this指针
指针的储存位置
我们知道 this 指针只能在成员函数中使用,那么应该属于成员函数的额一部分,因此储存在 栈区
面试题
(1)this指针可以为空吗
我们知道将对象地址作为实参传递给 this 形参 ,那么如果要让 this 指针为空,得保证传的地址为空(下面有实例)
(2)this指针存在哪里
this 指针作用在成员函数里面,因此存在 栈区
(3)下面程序的运行结果
这个指针是 类A 型的,所以可以调用这个类里面的成员,但是它为nullptr 。此时 this 指针拿到的就是 nullptr ,同时 p 调用 Print函数。
此时这个函数没有调用里面的类成员,所以不会发生解引用,自然也就不会报错,程序会正常运行
问:为什么可以通过 p(NULL)指针访问成员函数?
因为指针 p 是类A型的,p->Print 等价于 Print(p)
(4)下面程序的运行结果
前面的情况还是跟刚才一样,二者存储空间不同, this 指针拿到的也就是NULL,但是调用Print函数时访问了成员变量,也就是对 this 指针发生了解引用,实质是对NULL指针解引用,会崩溃
类的六大成员函数
六大成员函数引入
在开始之前我们来看一个头疼的实例:
如果我们用C语言实现栈,那么面临的是初始化......释放空间,那么在写栈类的题目时,可能忘记初始化、忘记及时释放空间。写栈时相关函数操作有以下共性:
(1)每个函数的第一个参数都是Stack*
(2)函数中必须要对第一个参数检测,因为该参数可能会为NULL
(3)函数中都是通过Stack*参数操作栈的 调用时必须传递Stack结构体变量的地址
结构体中只能定义存放数据的结构,操作数据的方法不能放在结构体中,即数据和操作数据的方式是分离开的,而且实现上相当复杂一点,涉及到大量指针操作,稍不注意可能就会出错!
C++解决了C语言在这方面的不足,进阶到帮你自动初始化、释放空间
如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?
并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数
构造函数
构造函数属于特殊的一个成员函数
作用:由编译器调用对数进行初始化
特点:(多元化)
一:函数名 与 类名相同,不用写返回值
二:创建类型对象实例化时由编译器自动调用,保证对数据进行一个初始化操作
三:在对象整个生命周期仅仅调用一次
四:支持重载
特点详解(1)
首先我们来简单写一个类:
class Myspace
{
public:
void Func()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
下面我们来写一个构造函数,注意函数名相同、不写返回值(类只有实例化之后才会开辟空间)
上面是调用无参的构造函数(缺省),下面我们换一种初始化:调用有参的构造函数
总结:
(1)只有类对象在实例化时才会开辟空间
(2)构造函数是可以设置参数进行缺省的
特点详解(2)
变量/函数类型:
(1)内置类型/基本类型(int、char、double.....)
(2)自定义类型(struct、class.....)
编译器调用构造函数规则:
(1)当只有内置类型时,编译器不做处理(有些编译器会有个性化行为进行处理)
(2)当存在自定义类型时,编译器会自己调用自己的构造函数
例如:当只有内置类型时,编译器不会处理
例如:当存在自定义类型且我们没有自定义构造函数,编译器就会自己调用自己(都会通过编译)
总结:
(1)构造函数不会对内置类型进行初始化
(2)没有自定义构造函数,编译器会自己生成自己的,但是不会做什么事情
有自定义构造函数函数,编译器就不会生成自己的,功能由自己设置
特点详解(3、4)
C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,解决了构造函数不会对内置类型作用
即:内置类型成员变量在类中声明时可以给默认值(让编译器默认的构造函数对内置对象进行初始化)
例如:
问:构造函数只能存在一个吗?
构造函数是支持重载的,但是规定:编译器自己的构造函数、无参的构造函数、全缺省的构造函数都称为默认构造函数,默认构造函数只能有一个
例如编译器自己的构造函数:
例如无参的构造函数:
例如全缺省的构造函数:
不同的对象可以有多种构造,例如:
总结
(1)当有自定义构造函数时,编译器不会调用自己的;当没有自定义构造函数,编译器调用自己 的构造函数,但是哈也不会做
(2)构造函数不会对内置类型处理,需要使用者自己对内置函数进行声明
(3)一个类只能有一个默认构造函数,多种实例化对象可以有多种构造方式
问:怎么理解一个生命周期仅仅调用一次?
比如一个类 class Stack,现在创造了两个变量(每个变量都是不同的):
Stack A Stack B
这两个变量可以分别选择一种构造方式,比如:
Stack A选择 默认构造(仅仅出现一次);
Stack B选择 缺省构造;
一个类只能出现一种默认构造方式,但是一个类可以有多种非默认构造方式
析构函数
作用:自动帮助我们完成结尾操作,功能自己定义,比如:释放空间......
特点:(独一无二)
(1)析构函数名是在类名前加上字符 ~
(2) 无参数无返回值类型
(3) 一个类只能有一个析构函数。
若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
(5)对象生命周期结束时,C++编译系统系统自动调用析构函数
例如:现在小编写了一个类用来演示构造:开辟空间 ,析构:释放空间
class Myspace
{
public:
//构造函数(帮助完成初始化操作)
Myspace(int _top = 0)
{
top = _top;
newnode = (int*)malloc(sizeof(int) * MAX);
if (newnode == nullptr)
{
perror("空间开辟失败\n");
return;
}
cout << "空间开辟成功" << endl;
}
//析构函数(释放空间)
~Myspace()
{
top = 0;
free(newnode);
newnode = nullptr;
cout << "空间释放成功" << endl;
}
private:
//这里不是初始化,而是声明,给编译器默认构造函数用
int* newnode = nullptr;
int top = 0;
};
效果展示:
问:编译器自己的析构函数会干什么?
可以和编译器默认构析函数一样理解:调了也不干什么 ,功能全靠自定义
构造与析构的调用顺序
析构是按照构造的相反顺序进行析构(销毁顺序优先:局部<-静态-<全局)
例如: A对象(局部)构造 B对象(局部)构造
销毁: B->A
注意:
如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数
比如 Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类
【雾非雾】期待与你的下次相遇!