目录
C++ struct的升级
类的定义
访问限定符
类域
类的声明和定义分离
C++类的一些约定
封装
类对象模型
隐含的this指针
this指针两道经典面试题
上一篇博客我向大家介绍了面向过程和面向对象的一些区别,并捎带着引入了一点点类和对象的概念,今天我们正式来看一下类和对象。
https://blog.csdn.net/SAKURAjinx/article/details/126990400
C++ struct的升级为了引入类,C++先拿C语言的struct开刀了。
首先C++的struct还是兼容C的用法,但同时也将其升级成了类。也就是说struct里面不仅可以放以前那一套(属性),还可以放入方法(函数)了。
struct stu
{
int name;
int age;
char ID[19];
//...
void StuReturnSch(stu* ps)
{
}
void StuRegisterSch(stu* ps)
{
}
};
struct teacher
{
int name;
int age;
char ID[19];
//...
void TeaReturnTea(teacher* pt)
{
}
void TeaRegisterTea(teacher* pt)
{
}
};
并且因为struct升级成了类,所以stu ,teacher 是类名,结构体实参命名可以不加struct,直接用类名。
struct teacher p1;
teacher p2;
类的定义
一开始C++还只是升级了struct,但后来研发者感觉这种方式不太好,就用class代替了struct。
当然,struct还是可以用,并且功能和class大同小异,但是后来程序员一般还是使用class,这成为一种通用标准了,Java里面类也是用的class。 并且struct还兼容C语言那一套,在其他一些场景也有新的应用。
访问限定符类里面的变量和函数不需要按 “变量在上,函数在下” 的方式书写,可以穿插,可以倒过来写。
但是一般都不会胡乱穿插书写,毕竟代码除了自己,其他程序员也可能会看到的,最好还是变量放一起,函数放一起,方便阅读。
C语言里面变量要放上面,函数要放下面,是因为C语言编译器碰到未知量,是向上查找,不会向下。并且C的变量和函数没有必然的关联性,它们是分离开的。
C++变量和函数都在一个域里面,有必然的联系,因此可以上函数下变量的方式书写。
C++有封装、继承、多态,为了更好地控制封装,C++定义了访问限定符。
类域1、protected 和 private 都是私有,但是有区别,前期我们先不讲它们的区别。
2、public修饰的成员类外面也可以访问,protected 和 private 修饰的成员类外面不能访问。
3、不管是public还是protected 和 private 修饰,类里面都可以访问该成员。
4、class里没用访问限定符修饰的成员,默认私有; struct正相反,默认公有。
class里面的域叫类域。
之前C++命名空间域也是一个域。
stu::age = 20;
问大家一个问题,类里面可以这样像命名空间一样访问吗?
不可以。因为class 里面是声明,没有定义,自然不存在变量空间,因此也没有age的空间,不能访问。
要访问age,先在外部构建对象,再用对象访问成员变量,如下所示:
class stu
{
public:
char name[20];
int age;
char ID[19];
//...
void StuReturnSch(stu* ps)
{
}
void StuRegisterSch(stu* ps)
{
}
};
class teacher
{
public:
char name[20];
int age;
char ID[19];
//...
void TeaReturnTea(teacher* pt)
{
}
void TeaRegisterTea(teacher* pt)
{
}
};
int main()
{
stu p1;
teacher p2;
p1.age = 20;
return 0;
}
如果是静态的,可以用上面那种方式访问,后面再讲。
类的声明和定义分离声明和定义分离的原因:
1、增强可读性。比如公司里需要你加入一个项目,你要迅速了解项目的基本内容,可以查看项目程序的声明及注释。 如果声明和定义在一起,阅读、查找起来就很吃力。
2、增强代码安全性,防止源代码泄漏。
先来看一种声明和定义分离的情况:
project.h
#include
class stu
{
public:
char name[20];
int age;
char ID[19];
void ReturnSch(stu* ps);
void RegisterSch(stu* ps);
};
class teacher
{
public:
char name[20];
int age;
char ID[19];
void ReturnSch(teacher* pt);
void RegisterSch(teacher* pt);
};
project.cpp
#include"project.h"
void ReturnSch(stu* ps)
{
}
void RegisterSch(stu* ps)
{
}
void ReturnSch(teacher* pt)
{
}
void RegisterSch(teacher* pt)
{
}
test.c
#include"project.h"
int main()
{
stu p1;
teacher p2;
return 0;
}
我们发现一个问题,project.cpp里面,函数定义时,分不清哪个是stu的哪个是teacher的。
因此,此时要加上类的限定。
void stu::ReturnSch(stu* ps)
{
}
void stu::RegisterSch(stu* ps)
{
}
void teacher::ReturnSch(teacher* pt)
{
}
void teacher::RegisterSch(teacher* pt)
{
}
另外,如果在类里面定义函数,那么编译器会默认函数是内联的。
C++类的一些约定我们在类里定义变量时,通常会在前面或后面加_ 以示区分。
如:int _year; int year_;
还有:int m_year; int mYear
具体以公司惯例为主。
封装面向对象有三大特性:封装、继承、多态。当然还有一些其他特性(如反射等),但这三个是最主要的。
我们现在所学的类就是封装的一大体现。其本质就是把一系列相关的东西都封到一个地方。
类里面有公有和私有,想给外面使用的就设为公有(如成员函数),不想给外面使用的就设为私有(如成员变量)。
以数据结构的栈为例,C语言实现的时候相对自由一些,它的数据和方法(变量和函数)不是在一起的,是分离的。 而C++则是将数据和方法都封装在了一个类下。
左边C语言因为相对自由,没有封装的约束,也就可以通过访问变量随意修改成员。比如访问栈顶数据,本来需要调用StackTop函数,但是因为这个函数实现就两行太过简单,很多人直接就访问成员变量,这样会出现问题(因为不知道top初始设计是-1还是0)。
C++就不存在上述问题,因为封装在一起,数据是私有的,无法从外部访问,必须调用方法(成员函数),这样就增加了安全性,能更好地管理。
类对象模型如何计算对象的大小
计算类的大小时,和之前讲的结构体内存对齐那一块一样,都涉及到内存对齐规则,不清楚的老铁可以看一下我的博客:自定义类型————结构体、位段、枚举类型、联合体_SAKURAjinx的博客-CSDN博客
先说结论,这里类A的大小不是8,而是1. 很多老铁会把函数也算进大小里面,其实大佬在设计的时候没有将函数算进去,只计算对象的大小。
这里大佬在设计存储方式的时候有三种想法:
第一种:将对象和方法都存进去。
这种显然不好,太浪费空间了。pass
第二种:存对象及函数的地址。
毫无疑问,对象肯定都要存的,将函数放到一起,用一个指针指向它,保存这个地址就可以访问所有函数了。
第三种是大佬选择的方法,只存对象,将函数放到一块公共代码区,每个对象想访问就可以访问到。
方法二比方法三多存了一个地址,其实是没必要的,函数都在公共代码区了,对象都能轻易调用,没有必要特地保存一个地址取找这些函数。
既然函数不算进类的大小,那如果类里面没有对象呢?
#include
using namespace std;
class A1
{};
class A2
{
public:
void func() { }
};
int main()
{
cout << sizeof(A1) << endl;
cout << sizeof(A2) << endl;
return 0;
}
不论是类里面只有函数没有变量,还是什么都没有,编译器默认给的大小是1,不是0
也就是说编译器会给1个字节占位,它不存储有效数据,仅标识类存在。
因为如果需要取类的地址,连空间都没有就不行了。
隐含的this指针class Date
{
public:
void Init(int year = 1,int month = 1,int day = 1)
{
_year = year;
_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(2022,1,1);
d2.Init(2022,1,2);
d1.Print();
d2.Print();
return 0;
}
this指针是关键字,只能叫this,不能用that...代替。
我们用日期类来引出隐含的this指针,看上面代码,结果如下图:
上面已经讲过了,对象是去类里面找,但是函数不是去类里面找的,是去公共代码区找的,不同的对象调用的是同一个,那为什么结果会不同呢?
这就印证了this指针的存在。其实类里面有一个隐含的this指针,它指向调用函数的对象。
void Print(Date* this)
{
cout << this->_year << " " << this->_month << " " << this->_day << endl;
}
d1调用,传给this指针的就是d1 的地址; d2调用,传给this指针的就是d2 的地址。
因此,结果才会不同。
this指针是隐含的、默认的,不需要我们自己加上。在函数参数里加会报错。
虽然定义和传递都不能加this,但是我们可以用,像在函数体里面用 this->就没问题。
正因如此,在类外部不能直接用类访问里面的函数:
因为类里的函数要传对象的地址给this指针,但是这里没有定义对象,只有类名,编译器不知道传什么给this,就报错了。
其实this指针接收实参传过来的地址时,会加个const修饰:
这里const修饰的是this指针本身,而非它指向的值(解引用访问)。
也就是说 this 指针是不能修改的,this = nullptr(X)
this指针两道经典面试题一、this指针存在哪里?
两种经典错误解答:1、在常量取区(代码段)
2、在对象里
答案是存在栈帧中。因为this指针是形参。
二、
乍看之下,好像两道题是一样的,但其实有坑在里面。
很多人以为p是空指针,空指针解引用访问肯定会崩溃,所以都选了B。
这里就要结合我们上面讲的,p是空指针不假,但是注意:print函数的地址是在公共代码区的(代码段),而非在对象里,编译器去找print函数不会去对象里找,而是直接去代码段,所以这里不发生解引用。p是A的指针,直接传给print函数的this指针,指针可以为空,不会报错,因此正常运行,第一题选C。
第二题传指针之前都是一样的,当p指针传给this后,this是nullptr,还原编译器的写法就是:
cout << this->_a << endl;
空指针this解引用访问_a就会崩溃,所以第二题选B。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)