关键字:类的引入,类的定义,访问限定符,类的作用域,类的实例化,类的大小计算,this指针
目录
什么是类与对象?
对象,指的是现实世界中客观存在的实体,但是,计算机是没有主观意识的,它无法主动地去理解并认识各个实体。但是当我们使用计算机去解决具体的问题时,需要让计算机认识现实生活中的对象,此时就需要用户给计算机描述对象。
对对象的描述过程本质上就是对实体的抽象过程。
一般来说,向计算机描述必须借助一种面向对象的程序语言,如C++、Java等。
在C++语言中,我们通常借助类来向计算机描述对象。
类与对象的定义
类是现实世界或思维世界中的实体在计算机中的反应,它将数据以及数据的相关操作封装在一起。
对象是具有类类型的变量,是类的实例化。
类是对象的抽象,它用于描述一组对象的共同特征与行为。
类的引入
在C语言中,结构体中只能定义变量,但是在C++中,结构体中不仅可以定义变量,也可以定义函数。这个拓展可能更多是源于对C语言包容的考量。
在C++中,我们更多的会使用class来同时定义变量与函数。
struct s_date {
private:
int _day;
int _month;
int _year;
public:
void print() {
cout << _year << ' ' <<
_month << ' ' <<
_day << endl;
}
};
class c_date {
private:
int _day;
int _month;
int _year;
public:
void print() {
cout << _year << ' ' <<
_month << ' ' <<
_day << endl;
}
};
类的定义
如上边所说,类是数据及数据的相关操作的封装。
C++中,类的定义格式为:
class className {
//类体,由成员函数及成员变量组成
};
//与结构体类似,类的定义结束需要加分号。
class是定义类的关键字,className是类的名字, { } 中为类的主体。
类中的元素被称为类的成员:
类中的数据称为类的属性,或者成员变量
类中的函数称为类的方法,或者成员函数
通常情况下,类有两种定义格式:
1.成员函数的声明与定义都放在类体中。
class date {
private:
int _year;
int _month;
int _day;
public:
void print() {
cout << _year << "年" << endl;
cout << _month << "月" << endl;
cout << _day << "日" << endl;
}
};
2.成员函数的声明在类体中,实现在类体外。
class date {
private:
int _year;
int _month;
int _day;
public:
void print();
};
void date::print() {
cout << _year << "年" << endl;
cout << _month << "月" << endl;
cout << _day << "日" << endl;
}
需要注意的是,第一种定义方式中,由于类的成员函数会默认被inline修饰,如果成员函数的声明与定义都在类中,编译器可能会将成员函数作为内联函数处理。
所以,一般情况下,第二种他方式是更被推荐的。
类的访问限定符与封装
封装
面向对象的语言拥有三大特性,分别是封装,继承,多态。
继承与多态属于较复杂的特性,我在后续的博客中会有介绍,在此不表。
那么,
什么是封装?
封装,即把数据及其操作方法进行有机结合,隐藏对象的属性与实现操作,仅对外公开接口来和对象进行交互。
封装本质上是一种管理:就像我们的计算机操作系统,如果我们将操作系统的所有细节都开放给所有的用户,很难保证会不会有用户进行一些奇奇怪怪的操作最终导致操作系统的崩溃。所以这时候我们就需要给设置一些访问权限,设置了root用户以及用户组等权限。开放一些安全的,并不会导致太大影响的内容给所有的用户。
类也是一样的,我们将数据以及数据的操作方法封装到了一起,将一些重要的成员封装起来,开放一些共有的成员函数及成员变量给他人访问。
需要注意的是,C++并不是纯粹的面向对象的语言,它是基于面向对象的。
C++既包含了面向过程,也包含了面向对象。
访问限定符
封装的核心在于数据及方法的有机结合,以及隐藏一些,开放一些。
很显然,C++中的类提供了前者。但后者:我们该如何给予成员或开放或隐藏的可访问性?这就要使用到访问限定符了。
访问限定符:
是用于指定成员或者类型的可访问性的关键字。
C++中提供了三种访问限定符,分别是:
访问限定符 | 访问权限 |
public | 公有 |
protected | 保护 |
private | 私有 |
三者具体的访问权限我们可以用表格表示为:
访问权限 | public | protected | private |
本类成员 | 可以 | 可以 | 可以 |
子类成员 | 可以 | 可以 | 不可以 |
调用方(外部) | 可以 | 不可以 | 不可以 |
访问权限的作用域是从该限定符出现的位置到下一个访问限定符出现为止。
我们前边提到过,C++中的结构体struct也可以同时包含变量与函数,我们也将struct所定义的数据与方法的有机结合称为类,而struct与class所定义的类的根本区别在于:
struct定义的类:成员默认的访问权限是public
class定义的类:成员默认的访问权限是private
PS.毕竟struct要兼容C语言。
访问限定符只在编译时有用,当数据映射到内存后,并没有访问权限上的区别。
类的作用域
每一个类都定义了一个新的作用域,类中所有的成员都在类的作用域之中。在类外定义成员,实现类中声明的成员函数,需要使用 :: 作用域解析符指明成员属于哪一个类作用域。
class date {
private:
int _year;
int _month;
int _day;
public:
void print();
};
void date::print() {
cout << _year << "年" << endl;
cout << _month << "月" << endl;
cout << _day << "日" << endl;
}
另外,在类中声明,在类外通过域解析符 :: 知名作用域后定义的成员函数本质上还属于这个类,希望不要引起什么不必要的牛角尖。
类的实例化
用类类型创建对象的过程称为类的实例化。
1.类是一种数据类型,是一个模型一样的东西,它限定了类具有什么属性,拥有哪些行为、方法等。定义一个类并没有给它分配任何的内存空间。
2.一个类可以实例化出多个对象,就像int可以定义出多个整型变量一样。实例化出来的对象占用实际的物理空间,存储成员变量。
类对象模型
结构体内存对齐
对于类对象模型的分析之前,我们需要先回忆一下结构体内存对齐。
1. 第一个成员在与结构体偏移量为 0 的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS 中默认的对齐数为 8
3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。————————————————
版权声明:本文为CSDN博主「云雷屯176」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_56713382/article/details/122796603
这是从我之前的博客中摘取的,好像也来自于对书籍资料他人博客的整理总结。
类的大小计算
感谢C++对C语言的向下兼容。
通过结构体内存对齐的知识,再结合一些简单示例,我们很容易就可以得到计算类的大小的方法。
class date {
private:
int _year;
char insert;
int _month;
int _day;
public:
void print();
};
void date::print() {
cout << _year << "年" << endl;
cout << _month << "月" << endl;
cout << _day << "日" << endl;
}
class date2 {
public:
int _year;
char insert;
int _month;
int _day;
};
int main() {
date a1;
date2 a2;
cout << sizeof(a1) << endl;
cout << sizeof(a2) << endl;
return 0;
}
运行结果:
可一看到,当我们类中以如上格式定义三个int类型以及一个char类型的成员变量时,根据结构体内存对齐的相关知识,我们很容易得出只有成员变量的类的大小应为16个字节。
然而,再加入了一个成员函数之后,再次通过sizeof()函数进行计算,发现最后对象的大小仍然是16个字节。
由此我们很容易得出结论:
成员函数的大小不在类的对象里,计算类的大小只需要按照结构体内存对齐规则计算相应的成员变量大小即可。
成员函数大小不计算再类的对象中,这实际上是很好理解的。
一个类定义出的不同对象,对象之间的差距只在于其内所定义的成员变量的值,同一个类定义出的对象,它们所拥有的成员函数的代码必然是完全相同的。
如果每个对象创建时,都将成员函数及其代码拷贝进去,势必会造成极大的资源浪费。
所以,同一个类的所有对象的函数代码是共享的。
除此之外,还有一点需要注意的是:
class date {
};
int main() {
date a1;
cout << sizeof(a1) << endl;
return 0;
}
运行结果:
空类的大小为1
为什么空类的大小不是0,而是1?
因为空类可以被实例化,如果对于空类的大小取值为0的话,那么编译器将无法在内存地址上区分出同一个类定义出的不同的对象。所以,为了每个实例在内存中都有一个独一无二的地址,主流编译器往往会给空类隐含地加上一个字节,这样空类地实例化对象就有了一个独一无二的地址。所以空类所占的内存大小为1个字节。
this指针
class date {
private:
int _year;
char insert;
int _month;
int _day;
public:
void print();
void init(int year = 1900, int month = 1, int day = 1);
};
void date::init(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
void date::print() {
cout << _year << "年" << endl;
cout << _month << "月" << endl;
cout << _day << "日" << endl;
}
int main() {
date a1;
date a2;
a1.init();
a2.init(2022, 4, 19);
a1.print();
return 0;
}
运行结果:
对于C语言中结构体比较熟悉的朋友都知道,虽然C语言的结构体中没有访问权限的说法,但是当我们需要访问结构体中的变量时,会需要如 . 或 -> 一类的操作。
但是上方的代码中的 init(int _year, int _month, int _day) 与 print() 成员函数,并没有涉及到任何关于对象的说明,但是在我们使用对象 a1 对这两个函数进行访问时,却得到了预期的结果。
那么,这些成员方法在执行的时候,是如何知道要对哪个对象进行操作的?如何知道我们要访问的是 a1 对象的成员变量,而不是 a2 的?
这个问题是通过C++中引入的 this 指针解决的。
C++给每一个非静态成员函数都添加了一个隐藏的指针参数this,令该指针指向当前对象,即函数运行时调用该函数的对象。在该对象所调用的函数体中,所有对于该对象的成员变量的操作,都是通过this指针去访问的。只不过所有的操作对于用户来说都是透明的,即不需要用户来传递该参数,由编译器自动完成。
class date {
private:
int _year;
char insert;
int _month;
int _day;
public:
void print();
void init(int year = 1900, int month = 1, int day = 1);
};
void date::init(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
void date::print() {
cout << "this:" << this << endl;
cout << _year << "年" << endl;
cout << _month << "月" << endl;
cout << _day << "日" << endl;
}
int main() {
date a1;
cout << "&a1:" << &a1 << endl;
a1.init();
a1.print();
return 0;
}
可见 this 中的值与 &a1 的值相等,以此可以佐证非静态成员函数中隐含一个指向调用函数的对象的指针。
this指针的特性
1.this指针的类型为:类类型* const
2.只能在非静态成员函数中使用。
3.this指针本质上是成员函数的一个形参。当对象调用成员函数时,编译器会将对象的地址作为实参传递给this形参。所以对象中不存储this指针。
4.this指针是成员函数第一个隐含的指针形参,一般情况下由编译器通过ecx寄存器自动传递,不需要用户传递。
通过对上方代码的反汇编,我们可以看到如下指令:
在此,我们可以看到传入参数时,编译器通过lea指令,取a1的地址传入ecx寄存器中。
在进入函数之后,编译器会将ecx寄存器中的值传入this指针之中。
然而根据网络上多方资料的查询及验证:
class date {
private:
int _year;
char insert;
int _month;
int _day;
public:
void test1(int a, int b);
void test2(int a, ...);
};
void date::test1(int a, int b) {
cout <<"test1" << this <<endl;
}
void date::test2(int a, ...) {
cout << "test2" << this << endl;
}
int main() {
date a1;
a1.test1(1, 2);
a1.test2(1, 2, 3);
return 0;
}
在只声明成员函数而不定义的时候,我们可以看到,当函数参数可变时,函数的调用约定与函数参数固定的调用约定不同。
当完善了函数体之后
通过对以上两种调用约定的情况下汇编指令的分析,我们可以看出:
使用VC++编译器,在固定参数的非静态成员函数中,编译器使用的是 __thiscall 调用约定,此时this指针存储在ecx寄存器上,通过ecx传递给调用者。在可变参数的非静态成员函数中,编译器使用的是 __cdecl 调用约定,编译器在将所有的参数压入栈中后,将对象的地址拷贝到eax寄存器中,而后将寄存器的值入栈,即this指针存储在栈上。
可见由于调用约定的不同,函数参数的传入顺序不同,this指针的传参方式也不同。
从根本上说无论何种this指针的存储都是在栈上,无论ecx还是eax寄存器都只是起到了一个传递的作用。
this指针可以为空吗?
可以为空,我们可以定义一个类类型指针,令这个指针为空,由于this指针本质上是对象的地址,所以当类类型指针为空时,this指针也为空。
但是当this指针为空时,如果我们所调用的非静态成员函数中不涉及对成员变量的访问,那么程序可以正常运行。
而如果我们所调用的非静态成员函数函数体中涉及对成员变量的访问,由于成员变量是通过this指针来访问的,this指针为空,程序就会崩溃。