云计算百科
云计算领域专业知识百科平台

C++类与对象完整学习路线(上)

文章导航

前序

一.类的定义

1.类定义格式

1.1 访问限定符默认规则

1.2 类域

二.实例化到底是什么?

1.实例化

2. 对象的大小

三.你了解this指针多少?


        大家好,我是小沐。最近持续深耕C++基础语法,今天也是刚好来到了“类与对象”这一板块,我也刚好整理了一些知识点,对新手友好,适合刚入门的同学查缺补漏。

类的定义、实例化和this指针

一.类的定义

1.类定义格式

        C++中用class和struct两个关键字来定义类,{}中为类的主体,结束时加上分号即可,和C语言中的结构体写法一模一样。那我们不经问,类到底是什么?

类是一种自定义的数据类型,把自变量和函数作为主体(即封装),称里面的自变量为成员变量或属性;称函数为成员函数或方法。

        那小白肯定会有疑惑的,刚从C语言过渡,只见过内置的数据类型如:int 、char、double和struct等,甚至都不知道一直在用的struct也是一种数据类型,那你一个类里面又装函数又装变量的,既然也是一种数据类型,这时候看到这里脑子里面就会懵懵的。

        是的,类就是一种数据类型。

class zhangsan
{
void fuct1()
{
cout << 1 << endl;
}
};

struct lisi
{
void fuct2()
{
cout << 2 << endl;
}
};

        两种类定义和使用方法一模一样,也算沿用C语言结构体的方式了。我们继续看下面这种调用会有什么结果?

#include <iostream>
using namespace std;

class zhangsan
{
void fauct1()
{
cout << 1 << endl;
}
};

struct lisi
{
void fuct2()
{
cout << 2 << endl;
}
};

int main()
{
zhangsan zhang;
lisi li;
zhang.fuct1();
li.fuct2();
return 0;
}

        答案就是编译报错。class和struct定义的类虽然调用方式一样,但语法上存在差异,那就是访问限定符默认规则不一样。

1.1 访问限定符默认规则

class与struct的访问限定符——》public  公开的访问限定符

                                        ——》protect  保护性,有限制的限定符

                                        ——》private  私有性限定符

        总所周知,有这三个限定符修饰的区域,都会有相应的特性在里面,那没有限定符修饰的区域呢?从上面的例子我们知道,并不是两个都可以直接访问默认区域的成员函数的。

        规则:class的默认区域是属于private区域的,类外不以直接访问。

                   struct的默认区域是属于public区域的,类外可以直接访问。

在习一般情况下,我们是不会把成员函数写到private中而是写到public中。

        那么定义在private中的成员变量,外边就没有办法访问了吗?

——是的,类外确实不能直接访问private中的成员变量,但可以通过这种间接的访问获取成员变量。我们以时间类为例:

class Date
{
public:
int GetYear()
{
return _year ;
}
int GetMonth()
{
return _month;
}
int GetDay()
{
return _day;
}
private:
int _year;
int _month;
int _day;
};

       

1.2 类域

        上面我们说类是一种数据类型,那么这种数据类型定义时就会产生一个域,它的所有成员都在里面,我们称这个域为 “ 类域 ”。类域和其它域一样,都会影响编译器的查找规则。

我们一起来看下面的例子:

class Stack
{
public:
//成员函数
void Init(int n = 4);
private:
//成员变量
int* array;
size_t capacity;
size_t top;
};

void Init(int n)
{
array = (int*)malloc(sizeof(int) * n);
if (array == nullptr)
{
perror("Init::malloc");
return;
}
capacity = n;
top = 0;
}
int main()
{
Stack st;
st.Init();
return 0;
}

思考一下,这段代码有没有问题?

答案:有问题!!!

我们说类会定义出一个域,而域要被类外访问是需要通过域访问限定符(::)访问的。也就是说

void Stack::Init(int n) //这里需要加上Stack::
{
array = (int*)malloc(sizeof(int) * n);
if (array == nullptr)
{
perror("Init::malloc");
return;
}
capacity = n;
top = 0;
}

        如果这里没有加入Stack::,这个初始化函数就只是一个在全局变量中定义的函数,缺少的各种变量编译器都会自动去全局域里面找,没有找到,自然就会报错了。

二.实例化到底是什么?

1.实例化

        定义:类定义出的具体对象叫做实例化

        说得这么高级,其实就是用类定义一个具体对象,如

class Data
{
public:
int month;
int day;
}

int main()
{

Data d1; //d1就是具体对象
return 0;
}

        这里的d1就是具体的对象。定义了具体对象后,开辟了可以运用的空间,这个过程我们就管它就实例化。

形象比喻:类定义时,就像做建筑用的设计图,这时建筑仅仅停留在图纸层面,还不能住人。当定义了一个具体对象后,就像建筑已经从设计图到完成建筑建设,可以住人了。

2. 对象的大小

        思考:既然类是一种数据类型,那么类的大小是多大呢?是所有成员的简单相加吗?

在计算对象大小前,我们先再弄明白一个事情

类的成员函数是存储在类里面吗?

如果是,那就是定义一个具体对象,就会同时在栈帧上开辟一块空间存储函数。

如果不是,那成员函数会存储在哪?多个对象调用时过程是怎样的?

        为回答这个问题,我们将从汇编的层面观察调用过程。以这段代码为例

class Stack
{
public:
//成员函数
void Init(int n = 4)
{
array = (int*)malloc(sizeof(int) * n);
if (array == nullptr)
{
perror("Init::malloc");
return;
}
capacity = n;
top = 0;
}
private:
//成员变量
int* array;
size_t capacity;
size_t top;
};

int main()
{
Stack ST1;
Stack ST2;
ST1.Init();
ST2.Init();
return 0;
}

现在我们分别在ST1和ST2调用出打断点进行调试(为方便观察,我们选择在x86环境进行调试)。

我们看到

第一步:push(入栈)

第二步:把ST1的地址传给寄存器(ecx)

第三步:把Stack里面的Init()函数的地址计算出来,然后跳转到这个函数的地址执行操作。

       我们画的两个红圈中明显标注着ST1和ST2调用时,计算出的地址是一样的呀!这说明它两调用的是同一个函数,也就是说类的成员函数虽然是在类里面定义的,但编译时并没有存储在类的栈区里面。既然成员函数没有存储在类的栈区里面,那么类的对象的大小就不需要计算成员函数的大小。

        好,我们回归对象大小的计算问题,一起看例子,求下面对象的大小:

class ST1
{
public:
int a;
char b;
};
class ST2
{
void fuct()
{
int a;
}
};
class ST3
{ };

int main()
{
cout << sizeof(ST1) << endl;
cout << sizeof(ST2) << endl;
cout << sizeof(ST3) << endl;
return 0;
}

请问:这段代码输出的结果是多少?

A.5,0, 0          B.8 ,0, 0        C.5,1,1        D.8,1,1

        我们来公布答案

        

        由于编译器在运行时,受到内存对齐规则的约束,第一个成员放在结构体偏移量为0的位置,其它成员对齐到对齐数(vs的默认对齐数为8)的整数倍的地址出。

        注:对齐数 = 编译器默认的一个对齐数 与该成员大小的较小值。

                总大小 = 最大对齐数的整数倍

那么ST1在内存中应该是这样的

由于最大对齐数是整型的对齐数4,故总大小应该为4的整数倍,故最终结果应为8个字节。

问:为什么只定义了成员函数和没有成员的对象大小不为0?

因为如果一个字节都不给,就没有办法表示它们确实存在,所以这里给1字节,纯粹是为了占位标识对象存在。

        那么问题又来了,为什么要内存对齐,这不是会造成内存浪费吗?

这和机器硬件层面有关,硬件里面有一个叫做地址总线,有32条或64条(与机器有关),每一条通过充放电表示1和0,也就是2进制的表现形式。以32条地址总线的机器为例,无论你数据在内存中如何存放,我cpu每次读取都是按照32bit的大小的整数倍范围进行读取,如果数据存放没有内存对齐,有可能出现数据被截断的情况,就需要在读取结束后进行拼接,更加繁琐,还导致性能下降。这个时候我们的选择是牺牲空间换取时间

三.你了解this指针多少?

        上面我们有说过,类的成员函数并存储在类的栈帧区域里面,但一个类同时定义出多个具体对象时,它们可以调用统一函数,那么这个函数是如何识别是哪个具体对象在调用它呢?

答案就是这部分的主角this指针。

class Date
{
public:
void DATE(int year, int month, int day) //void DATE(DATE* const this, int year, int month, int day)
{
cout << year << '/' << month << '/' << day << endl;
}
};

int main()
{
Date d1;
Date d2;
d1.DATE(2026, 8, 3); //d1.DATE(&d1,2026, 8, 3);
d2.DATE(2023, 9, 1); //d2.DATE(&d2,2023, 9, 1);
return 0;
}

        在类域里面定义的函数都隐藏了一个this指针,在调用函数时,编译器会自动帮你把对象的地址也传过去,在输出部分实际上是这样的

void DATE(int year, int month, int day) //void DATE(DATE* const this, int year, int month, int day)
{
cout << this->year << '/' << this->month << '/' << this->day << endl;
}

        只是编译器把this指针给隐藏了。

C++规定:不能在实参和形参的位置显式的写this指针,但是可以在函数体内部显式写this指针。

        既然是指针,就有可能出现空指针解引用的情况,现在我们结合前面的知识,来判断下面两段代码运行结果是什么?

代码1:

class A
{
public:
void Print()
{
cout << "Print(int a = 4)" << endl;
}
private:
int _a;
};
class B
{
public:
void Print()
{
cout << "Print(int a = 4)" << endl;
cout << _a << endl;
}
private:
int _a;
};

int main()
{
A* a = nullptr;
//B* b = nullptr;
a->Print();
//b->Print();
return 0;
}

代码2:

class A
{
public:
void Print()
{
cout << "Print(int a = 4)" << endl;
}
private:
int _a;
};
class B
{
public:
void Print()
{
cout << "Print(int a = 4)" << endl;
cout << _a << endl;
}
private:
int _a;
};

int main()
{
//A* a = nullptr;
B* b = nullptr;
//a->Print();
b->Print();
return 0;
}

代码1:编译报错          运行崩溃        正常运行

代码2:编译报错          运行崩溃        正常运行

        那么答案是代码1正常运行,代码2运行崩溃。

如果这个问题没有做对,说明你对this指针还是不了解。

知识点:this指针传的是对象的地址,不是对象指针的地址

这两个定义,因为a和b已经存放的是对象的地址了,所以这里传值的时候默认是这样

a->Print(a);//不是a->Print(&a)
b->Print(b);//不是b->Print(&b)

        那我们又知道,地址传过去后,如果没有解引用操作,编译器是不会报错的。而代码1仅仅是打印了一个字符串啊,并没有解引用操作。   对于代码2就不一样了

     

class B
{
public:
void Print()
{
cout << "Print(int a = 4)" << endl;
cout << _a << endl; //这里本质就是this->_a
}
private:
int _a;
};

        这里面_a的打印实际就是this->_a,但是我们的this指针传过去的是空指针啊,对空指针解引用就导致程序崩溃。


        好了本篇到这里就结束了,你在学习这部分的时候有没有踩过什么坑?评论区聊聊,我整理到下一篇里~

赞(0)
未经允许不得转载:网硕互联帮助中心 » C++类与对象完整学习路线(上)
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!